In a Laravel Dusk sign-up test, how do you fill a birth-date field driven by a JavaScript date picker with a read-only input?
answer
- type() sends real keystrokes
- read-only inputs reject typing
- value() sets the DOM, fires no events
- click the widget, wait for its popup
- assert what the server received
basics
~20 sOperate the widget: click the input, waitFor() the calendar, click the day via dusk selectors, then assert. type() cannot fill a read-only input, and value() writes the DOM without input or change events, so bound JavaScript state misses it.
solid answer
~40 sA date picker usually makes its input `readonly` and fills it from a popup calendar, so `type('birth_date', ...)` cannot enter text. `$browser->value('@birth-date', '1990-04-12')` looks tempting but only runs `element.value = ...` in JavaScript: no `input` or `change` event fires, so a framework that binds the field (Alpine's `x-model`, a Livewire `wire:model`, a Vue component) keeps its old state and the submitted form may omit the date. The robust approach is to operate the widget: `click('@birth-date')`, `waitFor('@date-picker')`, navigate to the month, `click('@day-12')`, then `assertInputValue('birth_date', '1990-04-12')`, and after `press('@signup-button')` and a wait, assert the saved result. If driving the widget is impractical, use `script()` to set the value **and** dispatch the events the widget expects, knowing that this bypasses the widget.
code
php · 19 lines<?php
use Laravel\Dusk\Browser;
$this->browse(function (Browser $browser) {
$browser->visit('/register')
->type('name', 'Ada Lovelace')
->type('email', '[email protected]')
->click('@birth-date')
->whenAvailable('@date-picker', function (Browser $picker) {
$picker->select('@year-select', '1990')
->select('@month-select', '4')
->click('@day-12');
})
->assertInputValue('birth_date', '1990-04-12')
->press('@signup-button')
->waitForLocation('/dashboard')
->assertSee('Ada Lovelace');
});go deeper
Recall that type() sends keystrokes and that a read-only date input has to be filled by clicking the picker.
Explain why value() sets the DOM without events and how that breaks framework bindings, and use waitFor or whenAvailable around the popup.
Decide when to drive a third-party widget and when a scripted value with dispatched events is an acceptable shortcut, and assert the stored outcome either way.
Agree with frontend owners on dusk attributes for interactive widgets so browser tests exercise real user paths without brittle markup coupling.
## Why date pickers are hard to automate A JavaScript date picker replaces free typing with a popup calendar. To stop users typing invalid dates, many pickers mark the underlying `<input>` as `readonly` and write to it only when a day is clicked. Some keep a hidden input for the form and show a formatted value elsewhere. Frontend frameworks add a layer: Alpine's `x-model`, Livewire's `wire:model` or a Vue component reads the value from **events**, not by polling the DOM. A Dusk test has three ways to set the value, and they behave very differently. ## Option 1: `type()` — real keystrokes `type('birth_date', '1990-04-12')` clears the field and sends keystrokes through WebDriver. On a normal input this is the most user-like action and fires the same events typing would. On a `readonly` input the browser refuses the keystrokes, so the step errors or leaves the field empty. It is only an option when the picker allows typing. ## Option 2: `value()` — a DOM property write `$browser->value('@birth-date', '1990-04-12')` resolves the selector and executes: ```js document.querySelector('[dusk="birth-date"]').value = "1990-04-12"; ``` That bypasses `readonly`, which is why it seems to work: `assertInputValue()` even passes. But **no `input` or `change` event is dispatched**. Consequences: - A bound framework model keeps its old value, so the submitted payload may not include the date. - The picker's own state (selected day, validation) is unchanged. - The test proves nothing about whether a user can pick a date. ## Option 3: drive the widget The reliable approach treats the picker as a user would, with dusk attributes on the parts the test touches: 1. `click('@birth-date')` to open the calendar. 2. `waitFor('@date-picker')`, because the popup renders asynchronously. 3. Navigate: `click('@year-select')` or `select('@year-select', '1990')`, then the month. 4. `click('@day-12')` to choose the day. 5. `waitUntilMissing('@date-picker')` if the popup closes itself. 6. `assertInputValue('birth_date', '1990-04-12')` to check the field. 7. Submit with `press('@signup-button')`, then `waitForLocation('/dashboard')` and assert on the outcome. `whenAvailable('@date-picker', fn (Browser $picker) => $picker->click('@day-12'))` combines the wait with scoping, so `@day-12` is looked up only inside the calendar. ## When a shortcut is acceptable If the picker is a third-party widget covered by its own tests and your concern is the sign-up flow, you can set the value with `script()` and dispatch the events yourself: ```php $browser->script("const el = document.querySelector('[dusk=\"birth-date\"]'); el.value = '1990-04-12'; el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true }));"); ``` This is a deliberate trade: the test is faster and less brittle, but it no longer proves the widget works. Record that choice in the test name or a comment. ## Comparison | Approach | Works on `readonly` | Fires input/change events | Proves a user can pick a date | |---|---|---|---| | `type()` | no | yes | yes, where typing is allowed | | `value()` | yes | **no** | no | | `script()` with dispatched events | yes | yes, the ones you send | no | | clicking the widget | yes | yes, from the widget | **yes** | ## What to verify at the end The field's value is an intermediate signal. The end-to-end proof is what the server stored: after the submit and a wait, assert what the page shows, such as the profile displaying the birth date.
- The test uses value() on the date field, assertInputValue passes, yet the account is saved without a birth date. Why?`value()` writes the DOM property through JavaScript but dispatches no `input` or `change` event. A bound model, such as Alpine's `x-model` or Livewire's `wire:model`, never learns about the new value, so the submitted data still lacks it. Drive the widget, or dispatch the events yourself with `script()`.
- How do you stop the day click from hitting a same-numbered link elsewhere on the page?Scope it: `whenAvailable('@date-picker', fn (Browser $picker) => $picker->click('@day-12'))` waits for the calendar and resolves every selector inside it. Unique dusk attributes on the calendar's parts make the lookup unambiguous as well.
saying these in an interview costs you the question
- value() behaves exactly like a user typing into the field
- type() works on read-only inputs because it uses WebDriver
- A passing assertInputValue() proves the server received the date
- Clicking the picker needs no wait because the popup is already in the DOM