Skip to content

Commit

Permalink
Merge pull request #123 from musa11971/livewire-testing
Browse files Browse the repository at this point in the history
Add `Component::testStep` to allow easy testing of wizard StepComponents
  • Loading branch information
freekmurze authored Dec 16, 2024
2 parents ff55250 + 543ca02 commit 9f938c5
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 102 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.idea
.php_cs
.php_cs.cache
.phpunit.cache
.phpunit.result.cache
build
composer.lock
Expand Down
135 changes: 35 additions & 100 deletions docs/usage/testing-wizards.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,115 +3,50 @@ title: Testing wizards
weight: 6
---

On this page we'll show you a few tips on how to tests wizards created with this package.
On this page we'll show you a few tips on how to test wizard steps created with this package. For explanation purposes, consider an example wizard called `CheckoutWizardComponent`. It has the following steps:
- `CartStepComponent`: Displays the products in the user's cart.
- `DeliveryAddressStepComponent`: Allows the user to enter their delivery address.
- `ConfirmOrderStepComponent`: Displays the order details and allows the user to confirm the order.

## A peek under the hood

To understand how you should test a wizard, you should know how it works under the hood. Let's take a look!

A wizard keeps track of its steps and state. An event is emitted from a `StepComponent` to a `WizardComponent` when you call `nextStep` (or related functions). This event will contain the state of the step it is called from.

The wizard will receive the event and shows you the step you requested, along
with the state for that step. If you're familiar with Livewire, the wizard
renders a component like this:

```blade
@livewire('first-step', $state, 'unique-key')
```

As a wizard and its steps are tightly coupled, it's not always useful to test
individual components. They don't paint the full picture.

## Testing navigation of the wizard

When testing a wizard- or step-component, you may make use of the `emitEvents()`
method provided by our package.

`emitEvents` allows you to take all events from a `StepComponent` and fire them in the `WizardComponent`.

Here's an example:
## Testing a wizard step without state
The first step of our checkout wizard is a simple step that doesn't require any state. It only displays the products in the user's cart. To test this step, we can use the following test:

```php
$wizard = Livewire::test(CartWizard::class);

$wizard->assertSee('cart');

Livewire::test(ShowCartStep::class)
->call('nextStep')
->emitEvents()->in($wizard);
session()->put('cart', [
['name' => 'Candle', 'price' => 400],
['name' => 'Chocolate', 'price' => 150],
]);

$wizard->assertSee('fill in your address');
CheckoutWizardComponent::testStep(CartStepComponent::class)
->assertSuccessful()
->assertSee('Items in your cart')
->assertSee('Candle')
->assertSee('Chocolate');
```

`ShowCartStep` shows the contents of a customers cart and is the first step of
our wizard. The next step, although the class is not shown here, is to fill in
your address details.

The wizard renders each step, so we first assert if the first step has been
loaded correctly. A simple test is to assert that a string is visible. In this
case, we assert that it shows 'cart'.

We then move to the next step by calling `nextStep`. This emits an event to be
picked up by the wizard. `emitEvents` takes the event thats emitted by `nextStep` and passes it to the
wizard. The wizard processes it and takes you to the next step. We then assert if the second step is loaded.
The above test will assert that the step is shown successfully, and that it displays the products in the cart.

## Testing state in a StepComponent

Now that you know how to navigate your wizard in your tests, let's talk state.

State is stored in the wizard. Each step by its own has no idea or access to
it. In a browser, state is passed to steps automatically, but in your tests you
need `getStepState` to fetch the state from the wizard and pass it to a step.

We go back to our cart wizard where we want to test state. Our example is a
simple one, but this can be used to test your final step where you're
processing your cart.

We're going to emulate ordering [Laravel Comments](https://laravel-comments.com). `initialState` is
used to populate the cart. We're not implementing the address step here and
go straight to checkout.
## Testing a wizard step with state
You may need to test a wizard step that requires some state from a previous step. As an example, consider the last step of our checkout wizard.

```php
$initialState = 'show-cart-step' => [
'items' => [
0 => [
'detail' => 'Laravel Comments'
'quantity' => 1,
CheckoutWizardComponent::testStep(ConfirmOrderStepComponent::class, [
'wizard.delivery-address-step-component' => [
'street' => '1818 Sherman Street',
'city' => 'Hope',
'state' => 'Kansas',
'zip' => '67451',
'deliveryDate' => '2022-01-12', // Wednesday
],
],
],

$wizard = Livewire::test(CartWizard::class, [
'initialState' => $initialState,
]);

$showCartState = $wizard->getStepState('show-cart-step');

Livewire::test(ShowCartStep::class, $showCartState)
->assertSet('items.0.detail', 'Laravel Comments')
->call('items.0.quantity', 5)
->call('nextStep')
->emitEvents()->in($wizard);

$checkoutState = $wizard->getStepState('checkout-step');

Livewire::test(CheckoutStep::class, $checkoutState)
->call('placeOrder');

$this->assertDatabaseHas(Order::class, [...]);
])
->assertSuccessful()
->assertSee('Please confirm your order')
->assertSee('Delivery Address: 1818 Sherman Street, Hope, Kansas, 67451')
->assertSee('Delivery on Wednesday, 12th January 2022')
->call('confirmOrder');

// Assert that the order is created and other expectations...
expect(Order::count())->toBe(1);
```

We start by creating some dummy data for our cart. We've added the Laravel
Comments package to it. The customer decides they more licenses for their team,
and increases the quantity to 5.

In the `CheckoutStep` the user clicks to place the order and we want to assert
that this logic works as expected. This means we need access to all state.

`getStepState` gets all state relevant for that step. It will make sure the
step component behaves exactly the same as in a browser. It also includes the
global state, which you happen to need in the checkout.

Once the user clicks `placeOrder`, it fetches state from the first step and
creates the order. We then assert that the database has an order and things
are working as expected.
In the above test, we pass the state required by the `ConfirmOrderStepComponent` as an array. In this case, it is the delivery address that the user entered in the previous step. We are then able to assert whether the text is displayed properly and call the `confirmOrder` method on the Step to submit the order.
10 changes: 10 additions & 0 deletions src/WizardServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

namespace Spatie\LivewireWizard;

use Livewire\Component;
use Livewire\Features\SupportTesting\Testable;
use Livewire\Livewire;
use Spatie\LaravelPackageTools\Package;
Expand All @@ -26,6 +27,15 @@ public function bootingPackage()

public function registerLivewireTestMacros()
{
Component::macro('testStep', function (string $stepClass, array $state = []) {
$wizardComponent = Livewire::test(static::class, ['initialState' => $state]);
$wizard = $wizardComponent->invade();
$wizard->mountMountsWizard($stepClass, $state);

return Livewire::test($stepClass, $wizard->getCurrentStepState($stepClass))
->emitEvents()->in($wizardComponent);
});

Testable::macro('emitEvents', function () {
return new EventEmitter($this);
});
Expand Down
27 changes: 27 additions & 0 deletions tests/TestStepMacroTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

use Spatie\LivewireWizard\Tests\TestSupport\Components\MyWizardComponent;
use Spatie\LivewireWizard\Tests\TestSupport\Components\Steps\FirstStepComponent;
use Spatie\LivewireWizard\Tests\TestSupport\Components\Steps\SecondStepComponent;

it('can test a step without state', function () {
MyWizardComponent::testStep(FirstStepComponent::class)
->assertSee('first step');
});

it('can test a step with initial state', function () {
MyWizardComponent::testStep(SecondStepComponent::class, [
'first-step' => [
'order' => 220,
]
])
->assertSee('Order value is: 220');
});

it('cannot test a step with invalid initial state', function () {
MyWizardComponent::testStep(SecondStepComponent::class, [
'random-fake-step' => [
'order' => 220,
]
]);
})->throws(Exception::class);
1 change: 1 addition & 0 deletions tests/TestSupport/Components/Steps/SecondStepComponent.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ public function render()
'allStepState' => $this->state()->all(),
'firstStepState' => $this->state()->forStep('second-step'),
'currentStepState' => $this->state()->currentStep(),
'orderValueFromPreviousStep' => $this->state()->forStepClass(FirstStepComponent::class)['order'] ?? 'none',
]);
}
}
10 changes: 8 additions & 2 deletions tests/TestSupport/resources/views/second-step.blade.php
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
<div>
This is the second step
counter: {{ $this->counter }}
<p>
This is the second step
counter: {{ $this->counter }}
</p>

<p>
This is the "order" value, from step one. Order value is: {{ $orderValueFromPreviousStep }}
</p>

{{-- used in ste test --}}
<div id="currentStepState">@json($currentStepState)</div>
Expand Down

0 comments on commit 9f938c5

Please sign in to comment.