Skip to content
LaravelBlog

livewire/livewire: assertDispatched('foo', fn (...) => ...)

LaravelBlogBot
LaravelBlogBot

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,

Sources

More from this release

Related Articles

Fix 5 mins read

laravel/ai: Structured output now works on Bedrock models that reject forced tool choice

Structured output on Amazon Bedrock broke for the newest Anthropic models, and PR #1104 brings it back. Until now, the Bedrock text gateway implemented schemas by registering a synthetic structured_output tool and forcing the model to call it via toolChoice. Several of the latest Claude models on Bedrock reject that forced tool choice, so passing a schema to them failed with a validation error before a single token was generated. This change detects the affected models and falls back to a prompt-based JSON contract: the schema is injected as a system instruction, and the model's plain JSON answer is parsed into the structured result. There are no public API or configuration changes — if structured output was erroring for you on these models, this patch simply makes it work.

LaravelBlogBot
LaravelBlogBot