How to properly test Filament forms with Repeater fields in Actions
The Problem
I recently spent way too much time debugging a failing test in a FilamentPHP application. I had a form with a repeater field for resource links, and no matter what I tried, my test assertions kept failing.
The test was pretty straightforward:
#[Test]
public function editor_can_create_post_with_resource_links()
{
// Set up test
// ...
// Test the form submission
Livewire::test(CreatePost::class)
->callAction('create', data: [
'title' => 'My Test Post',
'content' => 'Lorem ipsum dolor sit amet',
'resource_links' => [
[
'label' => 'Official Documentation',
'url' => 'https://docs.example.com',
'featured' => true,
],
],
]);
// Check what was saved
$post = Post::latest()->first();
// This kept failing ↓
$this->assertEquals([
[
'label' => 'Official Documentation',
'url' => 'https://docs.example.com',
'featured' => true,
],
], $post->resource_links);
}
For context, here's a simplified version of how the action with a repeater is defined in the Filament component:
protected function createAction()
{
return Action::make('create')
->label('Create Post')
->form([
TextInput::make('title')->required(),
TextInput::make('content')->required(),
// Here's the repeater we're testing
Repeater::make('resource_links')
->schema([
TextInput::make('label')->required(),
TextInput::make('url')->url()->required(),
Toggle::make('featured')->default(false),
])
->default(function () {
// Add a default empty item
return [
[
'label' => 'Default link here',
'url' => 'https://filamentphp.com/docs',
'featured' => false,
],
];
})
->minItems(1),
])
->action(function (array $data) {
// Create the post with the data
$post = Post::create([
'title' => $data['title'],
'content' => $data['content'],
'resource_links' => $data['resource_links'],
// ...
]);
// ...
});
}
But my test kept failing with errors like:
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
Array (
0 => Array (
'label' => 'Official Documentation'
'url' => 'https://docs.example.com'
- 'featured' => true
+ 'featured' => false
)
+ 1 => [...] // <-- This is the default item that I wasn't expecting
)
The problem was that my form had a repeater with default values that were merging with my test data. I spent ages trying different approaches until I found a solution.
The Solution: Repeater::fake()
After much frustration, I finally found
the Filament documentation page about testing repeaters.
Turns out, Filament provides a simple method to make repeater testing work properly: Repeater::fake()
.
Here's how to use it:
use Filament\Forms\Components\Repeater;
#[Test]
public function editor_can_create_post_with_resource_links()
{
// Set up test...
// Add this line before testing
$undoRepeaterFake = Repeater::fake();
// Test the form submission
Livewire::test(CreatePost::class)
->callAction('create', data: [
'title' => 'My Test Post',
'content' => 'Lorem ipsum dolor sit amet',
'resource_links' => [
[
'label' => 'Official Documentation',
'url' => 'https://docs.example.com',
'featured' => true,
],
],
]);
// Don't forget this line after testing
$undoRepeaterFake();
// Now this works!
$post = Post::latest()->first();
$this->assertEquals([
[
'label' => 'Official Documentation',
'url' => 'https://docs.example.com',
'featured' => true,
],
], $post->resource_links);
}
That's it. Problem solved.
I honestly don't know exactly why this is necessary (something about UUIDs internally in Filament's repeater component), but I was in a hurry to get my tests passing and this fixed it. Good enough for me!
Why Does This Work?
From what I understand, the Repeater::fake()
method disables whatever UUID generation Filament is doing internally for
repeaters during tests, making the structure match what we'd expect.
The docs mention:
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.
But the key is that you don't have to worry about all that - just use Repeater::fake()
at the start of your test and
call the returned function when you're done.
Real Example That Actually Works
Here's what fixed my failing test for a blog post editor with resource links:
#[Test]
public function editor_can_create_post_with_resource_links()
{
$this->actingAs($this->createUser(['role' => 'editor']));
// This line fixed everything
$undoRepeaterFake = Repeater::fake();
Livewire::test(CreatePost::class)
->callAction('create', data: [
'title' => 'Testing Filament Forms',
'content' => 'This is a test post with resource links',
'category_id' => 1,
'published' => true,
'resource_links' => [
[
'label' => 'Filament Docs',
'url' => 'https://filamentphp.com/docs',
'featured' => true,
],
],
]);
// Don't forget this
$undoRepeaterFake();
// Now this assertion passes!
$post = Post::latest()->first();
$this->assertEquals([
[
'label' => 'Filament Docs',
'url' => 'https://filamentphp.com/docs',
'featured' => true,
],
], $post->resource_links);
}
Tips to Remember
-
Always use
Repeater::fake()
when testing forms with repeaters. Save yourself the frustration. -
Don't forget to call the cleanup function. It's returned from
Repeater::fake()
and needs to be called after your test. -
This applies to all Filament Actions with repeaters. Works with forms, tables, or other components.
-
Bookmark the Filament docs on testing repeaters. You'll need it again.
Bonus Tip: Testing Just the Count
If you just want to check that the right number of items got saved:
$undoRepeaterFake = Repeater::fake();
Livewire::test(CreatePost::class)
->assertFormSet(function (array $state) {
expect($state['resource_links'])
->toHaveCount(3);
});
$undoRepeaterFake();
I've run into this issue with Filament several times. The solution is in the docs, but it's hard to find when you're
frantically Googling error messages. Next time you're stuck with Filament repeater testing, just reach for
Repeater::fake()
.