livewire/livewire: assertDispatched('foo', fn (...) => ...)
Contributed by calebporzio
Part of the livewire v4.4.7 release
Pull request #10715 fixes a subtle inconsistency in Livewire's testing API: when a component dispatched the same event more than once, assertDispatched() with a callback only ever inspected the first occurrence. The named-parameter style of the same assertion behaved differently, so two equivalent-looking checks could disagree. This change on the 4.x branch makes callback assertions scan every dispatched event with the requested name and stop at the first match — bringing them in line with how named-parameter assertions already worked. If you are upgrading from v3.8.10, this is a test-only behavioral fix: nothing changes at runtime, but a few of your test expectations may deserve a second look.
The bug: only the first event was inspected
Consider a component that dispatches the same event twice, which is common when iterating over a collection:
public function markAllRead()
{
foreach ($this->notifications as $notification) {
$this->dispatch('notification-read', id: $notification->id);
}
}
In v3.8.10, these two assertions — which read as equivalent — disagreed:
$component->assertDispatched('notification-read', id: 2); // Passes
$component->assertDispatched(
'notification-read',
fn ($event, $params) => $params['id'] === 2
); // Fails
The named-parameter form checked every notification-read event, while the callback form grabbed only the first one and evaluated the callback against it. If the first notification had id: 1, a callback looking for id: 2 never stood a chance. The mirrored assertion had the opposite false-negative: assertNotDispatched() with a callback incorrectly passed when a later event with the same name matched, as long as the first one didn't.
What changed under the hood
In TestsEvents.php, the callback branch previously located the first event with the matching name and then ran the callback against that single event:
// Before: only the first matching event was tested
$event = collect($dispatches)->first(fn ($item) => $item['name'] === $value);
$test = $event && $params[0]($event['name'], $event['params']);
The fix collapses this into a single contains() check that evaluates the name and the callback together across all recorded dispatches:
// After: every event with the name is tested until one matches
$test = collect($dispatches)->contains(
fn ($item) => $item['name'] === $value
&& $params[0]($item['name'], $item['params'])
);
The callback signature is unchanged: the first argument is the event name, the second is the params array.
Why it matters
Callback assertions are the more expressive of the two styles — they can reach into nested values, combine conditions, and reference closures. Until now, that expressiveness came with a hidden trap: they silently ignored everything after the first dispatch of an event name. After the fix,