Be part of JetBrains PHPverse 2026 on June 9 – a free online event bringing PHP devs worldwide together.

ljbirch's avatar
Level 10

How to test for blade component passing data into Livewire component

Hi all, I've just been following Pest Driven Laravel by Christoph Rumpel and have started a project driven by TDD.

I just wanted to get some opinions on test coverage, etc. and how you would approach the problem I have.

Currently I have a Create Enquiry component that requires a vehicle:

class CreateEnquiry extends Component
{
    #[Locked]
    public Vehicle $vehicle;

    public EnquiryForm $form;

    public bool $submitted = false;

    public function mount(Vehicle $vehicle): void
    {
        $this->vehicle = $vehicle;
    }

    public function render(): View
    {
        return view('livewire.create-enquiry');
    }

    public function save(): void
    {
        $this->form->store($this->vehicle);

        $this->submitted = true;
    }
}

with Livewire form component:

class EnquiryForm extends Form
{
    #[Validate('required')]
    public string $name;

    #[Validate('required')]
    public string $phone_number;

    #[Validate('required')]
    public string $message;

    public function store(Vehicle $vehicle): void
    {
        $this->validate();

        Enquiry::create(array_merge(
            $this->only(['name', 'phone_number', 'message']),
            ['vehicle_id' => $vehicle->id]
        ));
    }
}

that gets used in a blade template:

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport"
          content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
{{ $vehicle->title }}
{{ $vehicle->getFormattedPrice() }}

@livewire('create-enquiry')

</body>
</html>

which get passed a vehicle from:

class PageDealershipListingController extends Controller
{
    public function __invoke(Dealership $dealership, Vehicle $vehicle)
    {
        return view('pages.dealership.listing', compact('vehicle'));
    }
}

Through TDD it all works fine!

HTTP level blade tests:

it('has an enquiry submission form', function () {
    // Arrange
    $dealership = Dealership::factory()->create();
    $vehicle = Vehicle::factory()
        ->for($dealership)
        ->create();

    // Act & Assert
    get(route('page.dealership.listing', ['dealership' => $dealership, 'vehicle' => $vehicle]))
        ->assertOk()
        ->assertViewHas('vehicle', fn($v) => $v->is($vehicle))
        ->assertSeeLivewire(CreateEnquiry::class);
});

and then Livewire specific tests:

However when then using the app I discovered that when submitting the form it would error due to not having a vehicle_id, which was because I was missing passing it from the blade view into the Laravel component:

// Currently:
@livewire('create-enquiry')

// Should be:
@livewire('create-enquiry', ['vehicle_id' => $vehicle])

I've not come up with a good way to test this without an E2E dusk style browser test yet. Is there an easier way to assure that a given Livewire component has/hasn't been passed the data it needs?

As it feels from outside Livewire on http style tests you can only assert that the Livewire component is present, not anything about it. Then when you test the Livewire component you're doing so in isolation, without consideration for how data is currently being fed into it.

I might be going a bit over the top here but with TDD its annoying to find all passing tests when in reality there is a gap in coverage that completely breaks a feature and would only be picked up with an E2E browser test!

Please tell me if I'm off my rocking horse here...

0 likes
3 replies
LaryAI's avatar
Level 58

You're absolutely right—this is a subtle but important gap in test coverage when using Livewire with TDD. Your HTTP-level (feature) tests only check that the Livewire component is present, not that it's being passed the correct data from the Blade view. Your Livewire component tests, meanwhile, test the component in isolation and don't catch issues with how it's invoked in the Blade template.

The core issue:
If you forget to pass :vehicle="$vehicle" to the Livewire component in your Blade view, your tests still pass, but the app breaks at runtime.

Solution: Test the Blade-to-Livewire Data Passing

1. Feature Test: Assert the Livewire Component Receives the Data

You can write a feature test that renders the page and inspects the rendered HTML to ensure the wire:initial-data attribute (or the hidden input for the property) contains the expected vehicle data. However, Livewire serializes the data, so this can be brittle and is not recommended for complex objects.

Instead, the most robust approach is to test the Blade view directly and assert that the Livewire component is being invoked with the correct parameters.

Example: Blade View Test

If you're using Pest, you can do something like this:

it('passes the vehicle to the Livewire component in the blade view', function () {
    $dealership = Dealership::factory()->create();
    $vehicle = Vehicle::factory()->for($dealership)->create();

    $view = $this->view('pages.dealership.listing', [
        'dealership' => $dealership,
        'vehicle' => $vehicle,
    ]);

    // Assert that the Livewire component is rendered with the vehicle
    $view->assertSee('@livewire(\'create-enquiry\', [\'vehicle\' => $vehicle])');
});

However, this only works if you use the Blade directive with the array syntax. If you use the @livewire('create-enquiry', ['vehicle' => $vehicle]) syntax, you can assert that the rendered view contains the correct Livewire markup.

Alternatively: Rendered HTML Assertion

You can check that the rendered HTML contains the vehicle's data (e.g., the vehicle's title or ID in a hidden input):

it('renders the vehicle data in the Livewire component markup', function () {
    $dealership = Dealership::factory()->create();
    $vehicle = Vehicle::factory()->for($dealership)->create();

    $response = $this->get(route('page.dealership.listing', [
        'dealership' => $dealership,
        'vehicle' => $vehicle,
    ]));

    $response->assertSee((string) $vehicle->id); // or another unique property
});

2. Recommendation: Always Pass Required Props Explicitly

To avoid this class of bug, always pass required data explicitly:

@livewire('create-enquiry', ['vehicle' => $vehicle])

You can even write a test that fails if you forget to do this, as above.

3. Consider a Custom Blade Component

If you want to enforce this pattern, you could wrap your Livewire component in a custom Blade component that requires the vehicle as a prop, and then always pass it through.

4. E2E Tests

Ultimately, only an E2E test (with Laravel Dusk, Pest Browser, or Cypress) can guarantee the full integration works. But the above strategies can catch most issues earlier.


Summary

  • Feature tests can assert the presence of the Livewire component, but not the data passed to it.
  • Blade view tests can assert that the Livewire component is invoked with the correct data.
  • Rendered HTML tests can check for the presence of the vehicle's data in the markup.
  • E2E/browser tests are the only way to guarantee the full integration, but you can catch most issues with careful Blade and feature tests.

TL;DR:
Add a Blade view test to assert that @livewire('create-enquiry', ['vehicle' => $vehicle]) is present in your view. This will fail if you forget to pass the required data, closing the gap in your TDD workflow.

Let me know if you'd like more concrete examples or have follow-up questions!

ljbirch's avatar
Level 10

For the time being I've gone for:

it('renders the vehicle data in the Livewire component markup', function () {
    $dealership = Dealership::factory()->create();
    $vehicle = Vehicle::factory()->for($dealership)->create();

    $response = $this->get(route('page.dealership.listing', [
        'dealership' => $dealership,
        'vehicle' => $vehicle,
    ]));

    $response->assertSeeHtml(
        '<input type="hidden" name="vehicle_slug" value="'
        .$vehicle->slug.'"'
    );
});

combined with:

<input type="hidden" name="vehicle_slug" value="{{ $vehicle->slug }}" />

But i'm not a huge fan of having to add hidden fields to validate this sort of thing!

Please or to participate in this conversation.