test(onboarding): cover the full flow; drop goal exclusivity rule
- add an end-to-end walk (account gate -> persona -> goals -> connect -> Stripe) and a self-hosted bypass test, plus the connect no-workspace and just-exploring-saved cases - drop the just_exploring exclusivity: the backend now saves any valid goal combination (the front-end still clears siblings as a UX nicety), removing the withValidator rule, the unused Goal::isExclusive(), and the now-unused goals_exclusive copy
This commit is contained in:
parent
afc4c7a80b
commit
2525d834b0
6 changed files with 79 additions and 26 deletions
|
|
@ -18,13 +18,4 @@ enum Goal: string
|
|||
case TrackPerformance = 'track_performance';
|
||||
case JustExploring = 'just_exploring';
|
||||
case Other = 'other';
|
||||
|
||||
/**
|
||||
* Selecting this goal means the user has no specific intent yet, so it is
|
||||
* mutually exclusive with every other goal.
|
||||
*/
|
||||
public function isExclusive(): bool
|
||||
{
|
||||
return $this === self::JustExploring;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@
|
|||
namespace App\Http\Requests\App\Onboarding;
|
||||
|
||||
use App\Enums\User\Goal;
|
||||
use Illuminate\Contracts\Validation\Validator;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
use Illuminate\Validation\Rule;
|
||||
|
||||
|
|
@ -26,15 +25,4 @@ public function rules(): array
|
|||
'goals.*' => [Rule::enum(Goal::class)],
|
||||
];
|
||||
}
|
||||
|
||||
public function withValidator(Validator $validator): void
|
||||
{
|
||||
$validator->after(function (Validator $validator): void {
|
||||
$goals = (array) $this->input('goals', []);
|
||||
|
||||
if (in_array(Goal::JustExploring->value, $goals, true) && count($goals) > 1) {
|
||||
$validator->errors()->add('goals', __('onboarding.goals_exclusive'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
],
|
||||
'goals_title' => 'What\'s your goal with TryPost?',
|
||||
'goals_description' => 'Pick everything that fits and we\'ll set TryPost up for you.',
|
||||
'goals_exclusive' => 'If you\'re just exploring, leave the other goals unchecked.',
|
||||
'goals' => [
|
||||
'save_time' => 'Save time by posting everywhere at once',
|
||||
'ai_content' => 'Create posts faster with AI',
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
],
|
||||
'goals_title' => '¿Cuál es tu objetivo con TryPost?',
|
||||
'goals_description' => 'Marca todo lo que encaje y adaptamos TryPost a ti.',
|
||||
'goals_exclusive' => 'Si solo estás explorando, deja los demás objetivos sin marcar.',
|
||||
'goals' => [
|
||||
'save_time' => 'Ahorrar tiempo publicando en todas mis redes a la vez',
|
||||
'ai_content' => 'Crear publicaciones más rápido con IA',
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
],
|
||||
'goals_title' => 'Qual o seu objetivo com o TryPost?',
|
||||
'goals_description' => 'Marque tudo que faz sentido e a gente ajusta o TryPost pra você.',
|
||||
'goals_exclusive' => 'Se você só está dando uma olhada, deixe os outros desmarcados.',
|
||||
'goals' => [
|
||||
'save_time' => 'Economizar tempo postando em todas as redes de uma vez',
|
||||
'ai_content' => 'Criar posts mais rápido com IA',
|
||||
|
|
|
|||
|
|
@ -175,14 +175,15 @@ function subscribeOnboardingAccount(Account $account): void
|
|||
$response->assertSessionHasErrors('goals.0');
|
||||
});
|
||||
|
||||
test('goals store rejects just exploring combined with other goals', function () {
|
||||
test('goals store accepts any combination of valid goals', function () {
|
||||
$this->user->update(['persona' => Persona::Agency->value]);
|
||||
|
||||
$response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.goals.store'), [
|
||||
'goals' => [Goal::JustExploring->value, Goal::SaveTime->value],
|
||||
]);
|
||||
|
||||
$response->assertSessionHasErrors('goals');
|
||||
$response->assertRedirect(route('app.onboarding.connect'));
|
||||
expect($this->user->fresh()->goals)->toBe([Goal::JustExploring->value, Goal::SaveTime->value]);
|
||||
});
|
||||
|
||||
test('goals store saves the goals, mirrors to PostHog and advances to the connect step', function () {
|
||||
|
|
@ -201,6 +202,22 @@ function subscribeOnboardingAccount(Account $account): void
|
|||
Bus::assertDispatched(SendEvent::class);
|
||||
});
|
||||
|
||||
test('goals store saves just exploring on its own as a real signal', function () {
|
||||
config(['services.posthog.enabled' => true, 'services.posthog.api_key' => 'phc_test']);
|
||||
Bus::fake();
|
||||
|
||||
$this->user->update(['persona' => Persona::Agency->value]);
|
||||
|
||||
$response = $this->actingAs($this->user->fresh())->post(route('app.onboarding.goals.store'), [
|
||||
'goals' => [Goal::JustExploring->value],
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('app.onboarding.connect'));
|
||||
expect($this->user->fresh()->goals)->toBe([Goal::JustExploring->value]);
|
||||
|
||||
Bus::assertDispatched(SendEvent::class);
|
||||
});
|
||||
|
||||
test('goals store redirects to the persona step when no persona was chosen', function () {
|
||||
$response = $this->actingAs($this->user)->post(route('app.onboarding.goals.store'), [
|
||||
'goals' => [Goal::SaveTime->value],
|
||||
|
|
@ -356,3 +373,63 @@ function subscribeOnboardingAccount(Account $account): void
|
|||
|
||||
$response->assertRedirect(route('app.calendar'));
|
||||
});
|
||||
|
||||
test('connect redirects to workspace creation when no workspace exists', function () {
|
||||
$this->user->update(['persona' => Persona::Agency->value, 'goals' => [Goal::SaveTime->value]]);
|
||||
|
||||
$response = $this->actingAs($this->user->fresh())->get(route('app.onboarding.connect'));
|
||||
|
||||
$response->assertRedirect(route('app.workspaces.create'));
|
||||
});
|
||||
|
||||
test('a user walks the full onboarding flow from the account gate to stripe checkout', function () {
|
||||
Plan::where('slug', Slug::Workspace)->firstOrFail()->update(['stripe_monthly_price_id' => 'price_monthly_test']);
|
||||
$workspace = onboardingWorkspace($this->user);
|
||||
|
||||
// The account gate sends an unsubscribed user into onboarding.
|
||||
$this->actingAs($this->user->fresh())->get(route('app.calendar'))
|
||||
->assertRedirect(route('app.onboarding'));
|
||||
|
||||
// Persona advances to goals.
|
||||
$this->actingAs($this->user->fresh())->post(route('app.onboarding.store'), ['persona' => Persona::Creator->value])
|
||||
->assertRedirect(route('app.onboarding.goals'));
|
||||
|
||||
// Goals advances to connect.
|
||||
$this->actingAs($this->user->fresh())->post(route('app.onboarding.goals.store'), [
|
||||
'goals' => [Goal::AiContent->value, Goal::SaveTime->value],
|
||||
])->assertRedirect(route('app.onboarding.connect'));
|
||||
|
||||
// Connect renders now that persona, goals and a workspace are present.
|
||||
$this->actingAs($this->user->fresh())->get(route('app.onboarding.connect'))->assertOk();
|
||||
|
||||
// Checkout blocks until a network is connected.
|
||||
$this->actingAs($this->user->fresh())->post(route('app.onboarding.checkout'))
|
||||
->assertRedirect(route('app.onboarding.connect'));
|
||||
|
||||
// Once a network is connected, checkout hands off to Stripe.
|
||||
SocialAccount::factory()->create(['workspace_id' => $workspace->id]);
|
||||
|
||||
$this->mock(StartSubscriptionCheckout::class)
|
||||
->shouldReceive('redirect')
|
||||
->once()
|
||||
->andReturn(redirect('https://checkout.stripe.test/session'));
|
||||
|
||||
$this->actingAs($this->user->fresh())->post(route('app.onboarding.checkout'))
|
||||
->assertRedirect('https://checkout.stripe.test/session');
|
||||
|
||||
expect($this->user->fresh()->persona)->toBe(Persona::Creator);
|
||||
expect($this->user->fresh()->goals)->toBe([Goal::AiContent->value, Goal::SaveTime->value]);
|
||||
});
|
||||
|
||||
test('a self-hosted user never enters the onboarding flow', function () {
|
||||
config(['trypost.self_hosted' => true]);
|
||||
onboardingWorkspace($this->user);
|
||||
|
||||
// The app is reachable directly, with no subscription.
|
||||
$this->actingAs($this->user->fresh())->get(route('app.calendar'))->assertOk();
|
||||
|
||||
// Every onboarding step just bounces to the calendar.
|
||||
foreach (['app.onboarding', 'app.onboarding.goals', 'app.onboarding.connect'] as $routeName) {
|
||||
$this->actingAs($this->user->fresh())->get(route($routeName))->assertRedirect(route('app.calendar'));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue