Onboarding::fake() swaps the manager for an OnboardingFake — behind the facade and in the container, so injected managers and the GetsOnboarded trait use it too — and returns it. The fake:
- keeps every flow and the resolver registered before it, so flows resolve normally;
- captures the package’s StepCompleted and FlowCompleted events, like Bus::fake();
- records every dismissal — Onboarding::for($user)->dismiss(), a flow’s dismiss() and $user->dismissOnboardingStep();
- replaces the store with an in-memory one you can seed; useStore() is ignored under the fake.
use RoundlyConsulting\Onboarding\Facades\Onboarding;
$fake = Onboarding::fake();
$fake->seedCompleted($user, 'verify-email'); // completedAt() reports it; record() skips it
$fake->seedDismissed($user, 'add-bio'); // the step drops out of $user's flow
$user->onboarding()->record('photo');
$user->dismissOnboardingStep('tour');
$fake->assertStepCompleted('photo')
->assertFlowCompleted() // optionally pass a flow title to filter
->assertStepNotCompleted('bio')
->assertDismissed('tour', $user) // subject optional
->assertNotDismissed('add-bio'); // seeding is not a dismissal
Onboarding::fake()->assertNothingRecorded()->assertNothingDismissed();Assertions
| Method | Purpose |
|---|---|
assertStepCompleted(string $key, ?callable $callback = null) | A StepCompleted was dispatched for the key; the optional callback receives the event and returns bool. |
assertStepNotCompleted(string $key) | No StepCompleted was dispatched for the key. |
assertFlowCompleted(?string $flowKey = null) | A FlowCompleted was dispatched; the optional argument filters by the flow’s title. |
assertNothingRecorded() | Neither event was dispatched. |
assertDismissed(string $step, $subject = null) | The step was dismissed — for that subject (saved models compare by key), or for any. |
assertNotDismissed(string $step, $subject = null) | The step was not dismissed. Seeding with seedDismissed() doesn’t count. |
assertNothingDismissed() | No dismissal was recorded. |
Every assertion returns the fake for chaining, and each is also callable statically — Onboarding::assertDismissed(…). assertFlowCompleted() filters by the flow’s title, not its registry key — the event carries the Flow itself.
Seeding the store
| Method | Purpose |
|---|---|
seedCompleted($subject, string ...$steps) | completedAt() reports the steps as completed now, so record() won’t announce them. |
seedDismissed($subject, string ...$steps) | The steps count as dismissed — hidden when they are optional and dismissible. |
A Pest example
use App\Models\User;
use RoundlyConsulting\Onboarding\Facades\Onboarding;
it('announces the photo step for the uploading user', function () {
$fake = Onboarding::fake();
$user = User::factory()->create(['avatar_path' => 'avatars/jane-doe.jpg']);
$user->onboarding()?->record('photo');
$fake->assertStepCompleted('photo', fn ($event) => $event->for === $user)
->assertStepNotCompleted('bio');
});
it('hides the bio step once the user says not now', function () {
Onboarding::fake();
$user = User::factory()->create();
Onboarding::for($user)?->dismiss('add-bio'); // 'add-bio' is optional()->dismissible()
Onboarding::assertDismissed('add-bio', $user); // static calls work too
expect(Onboarding::for($user)?->hasStep('add-bio'))->toBeFalse();
});Testing progress directly
Because progress is derived from data, most tests need no fake at all — set up the model and read the flow:
it('sends a fresh account to the photo step', function () {
$user = User::factory()->create(['avatar_path' => null]);
expect($user->hasCompletedOnboarding())->toBeFalse()
->and($user->onboardingProgress())->toBe(0.0)
->and($user->nextOnboardingStep()?->stepKey())->toBe('photo');
});The manager is a container singleton, so each test’s fresh application starts from the flows your service providers register. Call Onboarding::flush() or forget() to start clean when a test registers its own flows.
Show your open-source love
This package is free and MIT-licensed. If it saves you time, a one-off donation or a Patreon membership keeps it maintained, tested and documented.
More ways to support, including cryptoBy donating, you agree to our donation terms.
Want this built into your product?
We integrate our packages into custom Laravel and AI builds. Tell us what you're working on and we'll reply within 48 hours.