trypost/tests/Feature/OnboardingControllerTest.php

352 lines
11 KiB
PHP
Raw Normal View History

2026-01-18 23:33:45 +00:00
<?php
declare(strict_types=1);
2026-01-18 23:33:45 +00:00
use App\Enums\SocialAccount\Platform;
use App\Enums\User\Persona;
use App\Enums\User\Setup;
use App\Models\SocialAccount;
use App\Models\User;
use App\Models\Workspace;
feat: add brand autofill from website URL in onboarding Users on the Brand step can type their website URL and click 'Preencher' (Portuguese) / 'Autofill' (English). The backend fetches their homepage, parses standard meta tags, and returns: - name ← og:site_name | title (suffix stripped at ' | ', ' - ') - description ← meta[name=description] | og:description - language ← <html lang> mapped to en / pt-BR / es - logo ← apple-touch-icon | largest link[rel*=icon] | og:image The logo is downloaded, validated (mime whitelist, 2MB max), and attached to the workspace's 'logo' media collection so the avatar updates immediately. Zero LLM calls, zero external APIs. Uses symfony/dom-crawler + symfony/css-selector (newly required) for meta extraction. Everything else (Http client, workspace media, Intervention) was already in the project. Security: - SSRF guardrail: resolves the host, rejects private / loopback / link-local ranges, enforces http(s) scheme on both the initial page fetch and the logo download. - Rate limited at 10 req/min per user via the throttle middleware alias on the route. - Logo content-type must be one of the allowed image mimes; wrong types are silently dropped so users never see broken images. UX: - 'Autofill' button next to the website input, disabled until there is a URL; shows a spinner while running. - If a logo was captured, a small preview appears below the input so users can see what was pulled before saving. - Success and error paths both surface as vue-sonner toasts, with translations in en / pt-BR / es. - Failures leave the form untouched — nothing is destructively overwritten if parsing gave us nothing. Tests (16 new): action-level coverage for happy path, title-suffix fallback, language code normalization across 6 locales, scheme rejection, private-range SSRF rejection, implicit https prefixing, empty sites, upstream errors, and wrong-mime logo rejection. Plus two controller-level tests for the autofill endpoint.
2026-04-16 14:13:54 +00:00
use Illuminate\Support\Facades\Http;
2026-01-18 23:33:45 +00:00
beforeEach(function () {
$this->user = User::factory()->create(['setup' => Setup::Role]);
});
// Step 1 tests
test('step1 requires authentication', function () {
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response = $this->get(route('app.onboarding.role'));
2026-01-18 23:33:45 +00:00
$response->assertRedirect(route('login'));
});
test('step1 shows persona selection', function () {
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response = $this->actingAs($this->user)->get(route('app.onboarding.role'));
2026-01-18 23:33:45 +00:00
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('onboarding/Role', false)
2026-01-18 23:33:45 +00:00
->has('personas')
);
});
// Store Step 1 tests
test('store step1 requires authentication', function () {
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response = $this->post(route('app.onboarding.role.store'), [
2026-01-18 23:33:45 +00:00
'persona' => Persona::Founder->value,
]);
$response->assertRedirect(route('login'));
});
feat: add brand configuration step to onboarding After users pick their persona (role), they now land on a new Brand step that collects the same fields available in Settings → Workspace → Brand: website, description, tone, voice notes, and content language. When they continue, every AI-generated post for this workspace already has sensible defaults — before the user's first post is ever drafted. Flow: Role (persona) → Brand (new) → Connections → Subscription → Completed. A 'Skip for now' button on the brand step advances to Connections without touching the workspace (defaults stay at their seed values). Backend: - Setup enum gets a new Brand case slotted between Role and Connections with matching stepNumber updates. - OnboardingController::brand() renders the form pre-filled from the current workspace. storeBrand() validates via a new StoreBrandRequest form request and writes the fields onto the workspace, then advances setup. skipBrand() just advances. - storeRole() redirects to brand instead of account. enforceStep() knows how to redirect users whose setup is Brand. - Three new routes: GET /onboarding/brand, POST /onboarding/brand, POST /onboarding/brand/skip. Frontend: - New Brand.vue page mirrors the Settings brand form but inside the onboarding AuthLayout. Tone + language sit side by side, both selects take full width. Translations added to en, pt-BR, and es. - Wayfinder regenerated so the page can import storeBrand / skipBrand. Tests: - Renamed 'redirects to step2' → 'redirects to brand step' and assert new setup. - Added six new tests covering brand step auth, redirects, render, successful store, validation of tone and content_language, and skip. - Updated UserSetupTest for the new enum case + reshuffled step numbers.
2026-04-16 14:00:16 +00:00
test('store step1 saves persona and redirects to brand step', function () {
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response = $this->actingAs($this->user)->post(route('app.onboarding.role.store'), [
2026-01-18 23:33:45 +00:00
'persona' => Persona::Founder->value,
]);
feat: add brand configuration step to onboarding After users pick their persona (role), they now land on a new Brand step that collects the same fields available in Settings → Workspace → Brand: website, description, tone, voice notes, and content language. When they continue, every AI-generated post for this workspace already has sensible defaults — before the user's first post is ever drafted. Flow: Role (persona) → Brand (new) → Connections → Subscription → Completed. A 'Skip for now' button on the brand step advances to Connections without touching the workspace (defaults stay at their seed values). Backend: - Setup enum gets a new Brand case slotted between Role and Connections with matching stepNumber updates. - OnboardingController::brand() renders the form pre-filled from the current workspace. storeBrand() validates via a new StoreBrandRequest form request and writes the fields onto the workspace, then advances setup. skipBrand() just advances. - storeRole() redirects to brand instead of account. enforceStep() knows how to redirect users whose setup is Brand. - Three new routes: GET /onboarding/brand, POST /onboarding/brand, POST /onboarding/brand/skip. Frontend: - New Brand.vue page mirrors the Settings brand form but inside the onboarding AuthLayout. Tone + language sit side by side, both selects take full width. Translations added to en, pt-BR, and es. - Wayfinder regenerated so the page can import storeBrand / skipBrand. Tests: - Renamed 'redirects to step2' → 'redirects to brand step' and assert new setup. - Added six new tests covering brand step auth, redirects, render, successful store, validation of tone and content_language, and skip. - Updated UserSetupTest for the new enum case + reshuffled step numbers.
2026-04-16 14:00:16 +00:00
$response->assertRedirect(route('app.onboarding.brand'));
2026-01-18 23:33:45 +00:00
$this->user->refresh();
expect($this->user->persona)->toBe(Persona::Founder);
feat: add brand configuration step to onboarding After users pick their persona (role), they now land on a new Brand step that collects the same fields available in Settings → Workspace → Brand: website, description, tone, voice notes, and content language. When they continue, every AI-generated post for this workspace already has sensible defaults — before the user's first post is ever drafted. Flow: Role (persona) → Brand (new) → Connections → Subscription → Completed. A 'Skip for now' button on the brand step advances to Connections without touching the workspace (defaults stay at their seed values). Backend: - Setup enum gets a new Brand case slotted between Role and Connections with matching stepNumber updates. - OnboardingController::brand() renders the form pre-filled from the current workspace. storeBrand() validates via a new StoreBrandRequest form request and writes the fields onto the workspace, then advances setup. skipBrand() just advances. - storeRole() redirects to brand instead of account. enforceStep() knows how to redirect users whose setup is Brand. - Three new routes: GET /onboarding/brand, POST /onboarding/brand, POST /onboarding/brand/skip. Frontend: - New Brand.vue page mirrors the Settings brand form but inside the onboarding AuthLayout. Tone + language sit side by side, both selects take full width. Translations added to en, pt-BR, and es. - Wayfinder regenerated so the page can import storeBrand / skipBrand. Tests: - Renamed 'redirects to step2' → 'redirects to brand step' and assert new setup. - Added six new tests covering brand step auth, redirects, render, successful store, validation of tone and content_language, and skip. - Updated UserSetupTest for the new enum case + reshuffled step numbers.
2026-04-16 14:00:16 +00:00
expect($this->user->setup)->toBe(Setup::Brand);
2026-01-18 23:33:45 +00:00
});
test('store step1 validates persona is required', function () {
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response = $this->actingAs($this->user)->post(route('app.onboarding.role.store'), [
2026-01-18 23:33:45 +00:00
'persona' => '',
]);
$response->assertSessionHasErrors('persona');
});
test('store step1 validates persona is valid enum', function () {
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response = $this->actingAs($this->user)->post(route('app.onboarding.role.store'), [
2026-01-18 23:33:45 +00:00
'persona' => 'invalid',
]);
$response->assertSessionHasErrors('persona');
});
feat: add brand configuration step to onboarding After users pick their persona (role), they now land on a new Brand step that collects the same fields available in Settings → Workspace → Brand: website, description, tone, voice notes, and content language. When they continue, every AI-generated post for this workspace already has sensible defaults — before the user's first post is ever drafted. Flow: Role (persona) → Brand (new) → Connections → Subscription → Completed. A 'Skip for now' button on the brand step advances to Connections without touching the workspace (defaults stay at their seed values). Backend: - Setup enum gets a new Brand case slotted between Role and Connections with matching stepNumber updates. - OnboardingController::brand() renders the form pre-filled from the current workspace. storeBrand() validates via a new StoreBrandRequest form request and writes the fields onto the workspace, then advances setup. skipBrand() just advances. - storeRole() redirects to brand instead of account. enforceStep() knows how to redirect users whose setup is Brand. - Three new routes: GET /onboarding/brand, POST /onboarding/brand, POST /onboarding/brand/skip. Frontend: - New Brand.vue page mirrors the Settings brand form but inside the onboarding AuthLayout. Tone + language sit side by side, both selects take full width. Translations added to en, pt-BR, and es. - Wayfinder regenerated so the page can import storeBrand / skipBrand. Tests: - Renamed 'redirects to step2' → 'redirects to brand step' and assert new setup. - Added six new tests covering brand step auth, redirects, render, successful store, validation of tone and content_language, and skip. - Updated UserSetupTest for the new enum case + reshuffled step numbers.
2026-04-16 14:00:16 +00:00
// Brand step tests
test('brand step requires authentication', function () {
$response = $this->get(route('app.onboarding.brand'));
$response->assertRedirect(route('login'));
});
test('brand step redirects users not at the brand step', function () {
$this->user->update(['setup' => Setup::Role]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.brand'));
$response->assertRedirect(route('app.onboarding.role'));
});
test('brand step shows the form with workspace defaults', function () {
$workspace = Workspace::factory()->create([
'user_id' => $this->user->id,
'brand_tone' => 'casual',
'content_language' => 'pt-BR',
]);
$this->user->update([
'current_workspace_id' => $workspace->id,
'setup' => Setup::Brand,
]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.brand'));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('onboarding/Brand', false)
->where('workspace.brand_tone', 'casual')
->where('workspace.content_language', 'pt-BR')
);
});
test('store brand persists workspace settings and advances to connections', function () {
$workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->user->update([
'current_workspace_id' => $workspace->id,
'setup' => Setup::Brand,
]);
$response = $this->actingAs($this->user)->post(route('app.onboarding.brand.store'), [
'brand_website' => 'https://example.com',
'brand_description' => 'We build social tools.',
'brand_tone' => 'friendly',
'brand_voice_notes' => 'Short and punchy.',
'content_language' => 'pt-BR',
]);
$response->assertRedirect(route('app.onboarding.account'));
$workspace->refresh();
expect($workspace->brand_website)->toBe('https://example.com');
expect($workspace->brand_tone)->toBe('friendly');
expect($workspace->content_language)->toBe('pt-BR');
$this->user->refresh();
expect($this->user->setup)->toBe(Setup::Connections);
});
test('store brand validates tone is in allowed list', function () {
$workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->user->update([
'current_workspace_id' => $workspace->id,
'setup' => Setup::Brand,
]);
$response = $this->actingAs($this->user)->post(route('app.onboarding.brand.store'), [
'brand_tone' => 'invalid-tone',
'content_language' => 'en',
]);
$response->assertSessionHasErrors('brand_tone');
});
test('store brand validates content_language is supported', function () {
$workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->user->update([
'current_workspace_id' => $workspace->id,
'setup' => Setup::Brand,
]);
$response = $this->actingAs($this->user)->post(route('app.onboarding.brand.store'), [
'brand_tone' => 'professional',
'content_language' => 'fr',
]);
$response->assertSessionHasErrors('content_language');
});
feat: add brand autofill from website URL in onboarding Users on the Brand step can type their website URL and click 'Preencher' (Portuguese) / 'Autofill' (English). The backend fetches their homepage, parses standard meta tags, and returns: - name ← og:site_name | title (suffix stripped at ' | ', ' - ') - description ← meta[name=description] | og:description - language ← <html lang> mapped to en / pt-BR / es - logo ← apple-touch-icon | largest link[rel*=icon] | og:image The logo is downloaded, validated (mime whitelist, 2MB max), and attached to the workspace's 'logo' media collection so the avatar updates immediately. Zero LLM calls, zero external APIs. Uses symfony/dom-crawler + symfony/css-selector (newly required) for meta extraction. Everything else (Http client, workspace media, Intervention) was already in the project. Security: - SSRF guardrail: resolves the host, rejects private / loopback / link-local ranges, enforces http(s) scheme on both the initial page fetch and the logo download. - Rate limited at 10 req/min per user via the throttle middleware alias on the route. - Logo content-type must be one of the allowed image mimes; wrong types are silently dropped so users never see broken images. UX: - 'Autofill' button next to the website input, disabled until there is a URL; shows a spinner while running. - If a logo was captured, a small preview appears below the input so users can see what was pulled before saving. - Success and error paths both surface as vue-sonner toasts, with translations in en / pt-BR / es. - Failures leave the form untouched — nothing is destructively overwritten if parsing gave us nothing. Tests (16 new): action-level coverage for happy path, title-suffix fallback, language code normalization across 6 locales, scheme rejection, private-range SSRF rejection, implicit https prefixing, empty sites, upstream errors, and wrong-mime logo rejection. Plus two controller-level tests for the autofill endpoint.
2026-04-16 14:13:54 +00:00
test('autofill brand returns extracted fields from the site', function () {
$workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->user->update([
'current_workspace_id' => $workspace->id,
'setup' => Setup::Brand,
]);
Http::fake([
'acme.com' => Http::response(<<<'HTML'
<html lang="pt-BR">
<head>
<title>Acme | Best in class</title>
<meta name="description" content="Tools for creators.">
<meta property="og:site_name" content="Acme">
</head>
</html>
HTML, 200),
]);
$response = $this->actingAs($this->user)->postJson(
route('app.onboarding.brand.autofill'),
['url' => 'https://acme.com'],
);
$response->assertOk();
$response->assertJson([
'name' => 'Acme',
'brand_description' => 'Tools for creators.',
'content_language' => 'pt-BR',
]);
});
test('autofill brand returns 422 when site cannot be reached', function () {
$workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->user->update([
'current_workspace_id' => $workspace->id,
'setup' => Setup::Brand,
]);
$response = $this->actingAs($this->user)->postJson(
route('app.onboarding.brand.autofill'),
['url' => 'http://127.0.0.1'],
);
$response->assertUnprocessable();
$response->assertJsonStructure(['message']);
});
feat: add brand configuration step to onboarding After users pick their persona (role), they now land on a new Brand step that collects the same fields available in Settings → Workspace → Brand: website, description, tone, voice notes, and content language. When they continue, every AI-generated post for this workspace already has sensible defaults — before the user's first post is ever drafted. Flow: Role (persona) → Brand (new) → Connections → Subscription → Completed. A 'Skip for now' button on the brand step advances to Connections without touching the workspace (defaults stay at their seed values). Backend: - Setup enum gets a new Brand case slotted between Role and Connections with matching stepNumber updates. - OnboardingController::brand() renders the form pre-filled from the current workspace. storeBrand() validates via a new StoreBrandRequest form request and writes the fields onto the workspace, then advances setup. skipBrand() just advances. - storeRole() redirects to brand instead of account. enforceStep() knows how to redirect users whose setup is Brand. - Three new routes: GET /onboarding/brand, POST /onboarding/brand, POST /onboarding/brand/skip. Frontend: - New Brand.vue page mirrors the Settings brand form but inside the onboarding AuthLayout. Tone + language sit side by side, both selects take full width. Translations added to en, pt-BR, and es. - Wayfinder regenerated so the page can import storeBrand / skipBrand. Tests: - Renamed 'redirects to step2' → 'redirects to brand step' and assert new setup. - Added six new tests covering brand step auth, redirects, render, successful store, validation of tone and content_language, and skip. - Updated UserSetupTest for the new enum case + reshuffled step numbers.
2026-04-16 14:00:16 +00:00
test('skip brand advances setup without saving workspace changes', function () {
$workspace = Workspace::factory()->create([
'user_id' => $this->user->id,
'brand_tone' => 'casual',
]);
$this->user->update([
'current_workspace_id' => $workspace->id,
'setup' => Setup::Brand,
]);
$response = $this->actingAs($this->user)->post(route('app.onboarding.brand.skip'));
$response->assertRedirect(route('app.onboarding.account'));
$workspace->refresh();
expect($workspace->brand_tone)->toBe('casual');
$this->user->refresh();
expect($this->user->setup)->toBe(Setup::Connections);
});
2026-01-18 23:33:45 +00:00
// Step 2 tests
test('step2 requires authentication', function () {
$response = $this->get(route('app.onboarding.account'));
2026-01-18 23:33:45 +00:00
$response->assertRedirect(route('login'));
});
test('step2 shows social accounts connection', function () {
$this->user->update(['setup' => Setup::Connections]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.account'));
2026-01-18 23:33:45 +00:00
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('onboarding/Account', false)
2026-01-18 23:33:45 +00:00
->has('platforms')
->has('hasWorkspace')
);
});
test('step2 shows connected accounts for workspace', function () {
$this->user->update(['setup' => Setup::Connections]);
$workspace = Workspace::factory()->create(['user_id' => $this->user->id]);
$this->user->update(['current_workspace_id' => $workspace->id]);
SocialAccount::factory()->create([
'workspace_id' => $workspace->id,
'platform' => Platform::LinkedIn,
]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.account'));
2026-01-18 23:33:45 +00:00
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->where('hasWorkspace', true)
);
});
// Store Step 2 tests
test('store step2 requires authentication', function () {
$response = $this->post(route('app.onboarding.account.store'));
2026-01-18 23:33:45 +00:00
$response->assertRedirect(route('login'));
});
test('store step2 completes setup in self-hosted mode', function () {
config(['trypost.self_hosted' => true]);
$this->user->update(['setup' => Setup::Connections]);
$response = $this->actingAs($this->user)->post(route('app.onboarding.account.store'));
2026-01-18 23:33:45 +00:00
$response->assertRedirect(route('app.calendar'));
2026-01-18 23:33:45 +00:00
$this->user->refresh();
expect($this->user->setup)->toBe(Setup::Completed);
});
// Complete tests
test('complete requires authentication', function () {
$response = $this->get(route('app.onboarding.account'));
2026-01-18 23:33:45 +00:00
$response->assertRedirect(route('login'));
});
test('completed user is redirected to calendar', function () {
$this->user->update(['setup' => Setup::Completed]);
2026-01-18 23:33:45 +00:00
$response = $this->actingAs($this->user)->get(route('app.onboarding.account'));
2026-01-18 23:33:45 +00:00
$response->assertRedirect(route('app.calendar'));
2026-01-18 23:33:45 +00:00
});
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
// Step enforcement tests
test('step1 redirects to connect when user already completed role step', function () {
$this->user->update(['setup' => Setup::Connections]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.role'));
$response->assertRedirect(route('app.onboarding.account'));
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
});
test('step1 redirects to calendar when setup is completed', function () {
$this->user->update(['setup' => Setup::Completed]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.role'));
$response->assertRedirect(route('app.calendar'));
});
test('step2 redirects to role when user has not completed role step', function () {
$this->user->update(['setup' => Setup::Role]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.account'));
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response->assertRedirect(route('app.onboarding.role'));
});
test('step2 redirects to calendar when setup is completed', function () {
$this->user->update(['setup' => Setup::Completed]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.account'));
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response->assertRedirect(route('app.calendar'));
});
test('user on role step can access role page', function () {
$this->user->update(['setup' => Setup::Role]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.role'));
$response->assertOk();
});
test('user on connections step can access connect page', function () {
$this->user->update(['setup' => Setup::Connections]);
$response = $this->actingAs($this->user)->get(route('app.onboarding.account'));
refactor: auth split layout, subscribe redesign, onboarding, i18n, cookie locale Auth pages: - Create AuthSplitLayout with animated feature slides (6 slides, 3 languages) - All auth pages use split layout (form left, visual right) - Add show/hide password toggle with tooltip on Register - Legal footer only shown on Register via showLegal prop Subscribe page: - Redesign to match auth card pattern (centered, clean) - Platform icons, feature checklist, dynamic trial days (trialDays - 1) - Add "Switch workspace" link - Full i18n (en, es, pt-BR) Onboarding: - Rename URLs: step1 -> role, step2 -> connect - Add enforceStep() to prevent skipping/going back steps - Redirect /onboarding to /onboarding/role - Redesign Step2 with AuthSplitLayout and compact platform list - 21 tests covering all step enforcement scenarios Workspaces page: - Redesign with AuthSplitLayout (list with avatars, current badge) Language system: - Move locale from DB to cookie (forever, unencrypted, session.domain) - Create SetLocale middleware (sets cookie if missing, validates against config) - Rename lang/pt-br to lang/pt-BR - Add dayjs es locale Other: - Copy utils.ts from sendkit (formatNumber, formatMoney, copyToClipboard) - ConfirmDeleteModal with text confirmation (sendkit pattern) - i18n for ConfirmDeleteModal internal strings (common.php) - EmptyState component for posts index - Exact match for "All" posts in sidebar - Posts breadcrumbs show current status filter - DialogFooter buttons aligned left - API Keys page redesign with Table, DropdownMenu, EmptyState - Extract CreateApiKeyDialog and InviteMemberDialog to components - Remove API Keys from sidebar - DropdownMenuItem destructive variant for Remove action
2026-03-30 14:53:42 +00:00
$response->assertOk();
});