Skip to content

Commit

Permalink
feature: Easier Repeater / Builder testing
Browse files Browse the repository at this point in the history
  • Loading branch information
danharrin committed May 13, 2024
1 parent 9fda21f commit 9e5be32
Show file tree
Hide file tree
Showing 15 changed files with 498 additions and 47 deletions.
4 changes: 2 additions & 2 deletions packages/forms/.stubs.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
use Closure;

class Testable {
public function fillForm(array $state = [], string $formName = 'form'): static {}
public function fillForm(array | Closure $state = [], string $formName = 'form'): static {}

public function assertFormSet(array $state, string $formName = 'form'): static {}
public function assertFormSet(array | Closure $state, string $formName = 'form'): static {}

public function assertHasFormErrors(array $keys = [], string $formName = 'form'): static {}

Expand Down
45 changes: 45 additions & 0 deletions packages/forms/docs/03-fields/12-repeater.md
Original file line number Diff line number Diff line change
Expand Up @@ -663,3 +663,48 @@ $state[Str::uuid()] = [
// Set the new data for the repeater
$component->state($state);
```

## Testing repeaters

Internally, repeaters generate UUIDs for items to keep track of them in the Livewire HTML easier. This means that when you are testing a form with a repeater, you need to ensure that the UUIDs are consistent between the form and the test. This can be tricky, and if you don't do it correctly, your tests can fail as the tests are expecting a UUID, not a numeric key.

However, since Livewire doesn't need to keep track of the UUIDs in a test, you can disable the UUID generation and replace them with numeric keys, using the `Repeater::fake()` method at the start of your test:

```php
use Filament\Forms\Components\Repeater;
use function Pest\Livewire\livewire;

$undoRepeaterFake = Repeater::fake();

livewire(EditPost::class, ['record' => $post])
->assertFormSet([
'quotes' => [
[
'content' => 'First quote',
],
[
'content' => 'Second quote',
],
],
// ...
]);

$undoRepeaterFake();
```

You may also find it useful to access test the number of items in a repeater by passing a function to the `assertFormSet()` method:

```php
use Filament\Forms\Components\Repeater;
use function Pest\Livewire\livewire;

$undoRepeaterFake = Repeater::fake();

livewire(EditPost::class, ['record' => $post])
->assertFormSet(function (array $state) {
expect($state['quotes'])
->toHaveCount(2);
});

$undoRepeaterFake();
```
52 changes: 52 additions & 0 deletions packages/forms/docs/03-fields/13-builder.md
Original file line number Diff line number Diff line change
Expand Up @@ -463,3 +463,55 @@ $state[Str::uuid()] = [
// Set the new data for the builder
$component->state($state);
```

## Testing builders

Internally, builders generate UUIDs for items to keep track of them in the Livewire HTML easier. This means that when you are testing a form with a builder, you need to ensure that the UUIDs are consistent between the form and the test. This can be tricky, and if you don't do it correctly, your tests can fail as the tests are expecting a UUID, not a numeric key.

However, since Livewire doesn't need to keep track of the UUIDs in a test, you can disable the UUID generation and replace them with numeric keys, using the `Builder::fake()` method at the start of your test:

```php
use Filament\Forms\Components\Builder;
use function Pest\Livewire\livewire;

$undoBuilderFake = Builder::fake();

livewire(EditPost::class, ['record' => $post])
->assertFormSet([
'content' => [
[
'type' => 'heading',
'data' => [
'content' => 'Hello, world!',
'level' => 'h1',
],
],
[
'type' => 'paragraph',
'data' => [
'content' => 'This is a test post.',
],
],
],
// ...
]);

$undoBuilderFake();
```

You may also find it useful to access test the number of items in a repeater by passing a function to the `assertFormSet()` method:

```php
use Filament\Forms\Components\Builder;
use function Pest\Livewire\livewire;

$undoBuilderFake = Builder::fake();

livewire(EditPost::class, ['record' => $post])
->assertFormSet(function (array $state) {
expect($state['content'])
->toHaveCount(2);
});

$undoBuilderFake();
```
26 changes: 26 additions & 0 deletions packages/forms/docs/09-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,32 @@ it('can automatically generate a slug from the title', function () {

> If you have multiple forms on a Livewire component, you can specify which form you want to check using `assertFormSet([...], 'createPostForm')`.
You may also find it useful to pass a function to the `assertFormSet()` method, which allows you to access the form `$state` and perform additional assertions:

```php
use Illuminate\Support\Str;
use function Pest\Livewire\livewire;

it('can automatically generate a slug from the title without any spaces', function () {
$title = fake()->sentence();

livewire(CreatePost::class)
->fillForm([
'title' => $title,
])
->assertFormSet(function (array $state): array {
expect($state['slug'])
->not->toContain(' ');

return [
'slug' => Str::slug($title),
];
});
});
```

You can return an array from the function if you want Filament to continue to assert the form state after the function has been run.

## Validation

Use `assertHasFormErrors()` to ensure that data is properly validated in a form:
Expand Down
60 changes: 43 additions & 17 deletions packages/forms/src/Components/Builder.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,11 @@ protected function setUp(): void
$items = [];

foreach ($state ?? [] as $itemData) {
$items[$component->generateUuid()] = $itemData;
if ($uuid = $component->generateUuid()) {
$items[$uuid] = $itemData;
} else {
$items[] = $itemData;
}
}

$component->state($items);
Expand Down Expand Up @@ -135,14 +139,22 @@ public function getAddAction(): Action
$newUuid = $component->generateUuid();

$items = $component->getState();
$items[$newUuid] = [
'type' => $arguments['block'],
'data' => [],
];

if ($newUuid) {
$items[$newUuid] = [
'type' => $arguments['block'],
'data' => [],
];
} else {
$items[] = [
'type' => $arguments['block'],
'data' => [],
];
}

$component->state($items);

$component->getChildComponentContainer($newUuid)->fill();
$component->getChildComponentContainer($newUuid ?? array_key_last($items))->fill();

$component->collapsed(false, shouldMakeComponentCollapsible: false);

Expand Down Expand Up @@ -180,24 +192,33 @@ public function getAddBetweenAction(): Action
->label(fn (Builder $component) => $component->getAddBetweenActionLabel())
->color('gray')
->action(function (array $arguments, Builder $component): void {
$newUuid = $component->generateUuid();
$newKey = $component->generateUuid();

$items = [];

foreach ($component->getState() ?? [] as $uuid => $item) {
$items[$uuid] = $item;

if ($uuid === $arguments['afterItem']) {
$items[$newUuid] = [
'type' => $arguments['block'],
'data' => [],
];
foreach ($component->getState() ?? [] as $key => $item) {
$items[$key] = $item;

if ($key === $arguments['afterItem']) {
if ($newKey) {
$items[$newKey] = [
'type' => $arguments['block'],
'data' => [],
];
} else {
$items[] = [
'type' => $arguments['block'],
'data' => [],
];

$newKey = array_key_last($items);
}
}
}

$component->state($items);

$component->getChildComponentContainer($newUuid)->fill();
$component->getChildComponentContainer($newKey)->fill();

$component->collapsed(false, shouldMakeComponentCollapsible: false);

Expand Down Expand Up @@ -239,7 +260,12 @@ public function getCloneAction(): Action
$newUuid = $component->generateUuid();

$items = $component->getState();
$items[$newUuid] = $items[$arguments['item']];

if ($newUuid) {
$items[$newUuid] = $items[$arguments['item']];
} else {
$items[] = $items[$arguments['item']];
}

$component->state($items);

Expand Down
17 changes: 14 additions & 3 deletions packages/forms/src/Components/Concerns/CanGenerateUuids.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,32 @@

trait CanGenerateUuids
{
protected ?Closure $generateUuidUsing = null;
protected Closure | bool | null $generateUuidUsing = null;

public function generateUuidUsing(?Closure $callback): static
public function generateUuidUsing(Closure | bool | null $callback): static
{
$this->generateUuidUsing = $callback;

return $this;
}

public function generateUuid(): string
public function generateUuid(): ?string
{
if ($this->generateUuidUsing) {
return $this->evaluate($this->generateUuidUsing);
}

if ($this->generateUuidUsing === false) {
return null;
}

return (string) Str::uuid();
}

public static function fake(): Closure
{
return static::configureUsing(
fn ($component) => $component->generateUuidUsing(false),
);
}
}
58 changes: 43 additions & 15 deletions packages/forms/src/Components/Repeater.php
Original file line number Diff line number Diff line change
Expand Up @@ -119,9 +119,15 @@ protected function setUp(): void
$simpleField = $component->getSimpleField();

foreach ($state ?? [] as $itemData) {
$items[$component->generateUuid()] = $simpleField ?
[$simpleField->getName() => $itemData] :
$itemData;
if ($simpleField) {
$itemData = [$simpleField->getName() => $itemData];
}

if ($uuid = $component->generateUuid()) {
$items[$uuid] = $itemData;
} else {
$items[] = $itemData;
}
}

$component->state($items);
Expand Down Expand Up @@ -162,11 +168,16 @@ public function getAddAction(): Action
$newUuid = $component->generateUuid();

$items = $component->getState();
$items[$newUuid] = [];

if ($newUuid) {
$items[$newUuid] = [];
} else {
$items[] = [];
}

$component->state($items);

$component->getChildComponentContainer($newUuid)->fill();
$component->getChildComponentContainer($newUuid ?? array_key_last($items))->fill();

$component->collapsed(false, shouldMakeComponentCollapsible: false);

Expand Down Expand Up @@ -203,21 +214,27 @@ public function getAddBetweenAction(): Action
->label(fn (Repeater $component) => $component->getAddBetweenActionLabel())
->color('gray')
->action(function (array $arguments, Repeater $component): void {
$newUuid = $component->generateUuid();
$newKey = $component->generateUuid();

$items = [];

foreach ($component->getState() ?? [] as $uuid => $item) {
$items[$uuid] = $item;
foreach ($component->getState() ?? [] as $key => $item) {
$items[$key] = $item;

if ($uuid === $arguments['afterItem']) {
$items[$newUuid] = [];
if ($key === $arguments['afterItem']) {
if ($newKey) {
$items[$newKey] = [];
} else {
$items[] = [];

$newKey = array_key_last($items);
}
}
}

$component->state($items);

$component->getChildComponentContainer($newUuid)->fill();
$component->getChildComponentContainer($newKey)->fill();

$component->collapsed(false, shouldMakeComponentCollapsible: false);

Expand Down Expand Up @@ -270,7 +287,12 @@ public function getCloneAction(): Action
$newUuid = $component->generateUuid();

$items = $component->getState();
$items[$newUuid] = $items[$arguments['item']];

if ($newUuid) {
$items[$newUuid] = $items[$arguments['item']];
} else {
$items[] = $items[$arguments['item']];
}

$component->state($items);

Expand Down Expand Up @@ -640,9 +662,15 @@ public function default(mixed $state): static
$items = [];

foreach ($state ?? [] as $itemData) {
$items[$component->generateUuid()] = $simpleField ?
[$simpleField->getName() => $itemData] :
$itemData;
if ($simpleField) {
$itemData = [$simpleField->getName() => $itemData];
}

if ($uuid = $component->generateUuid()) {
$items[$uuid] = $itemData;
} else {
$items[] = $itemData;
}
}

$component->hydratedDefaultState = $items;
Expand Down
Loading

0 comments on commit 9e5be32

Please sign in to comment.