'2026-09-13', 'end' => '2026-09-15'];
public int $renders = 0;
public function touch(): void
{
$this->renders++;
}
public function render(): string
{
return <<<'BLADE'
expires: {{ $expires }}
birthday: {{ $birthday }}
delivery: {{ $delivery }}
trip: {{ json_encode($trip) }}
renders: {{ $renders }}
BLADE;
}
}
function dateProbe(string $locale = 'en')
{
Livewire::component('date-probe', DateProbe::class);
Route::middleware('web')->get('/date-probe/{locale}', function (string $locale) {
app()->setLocale($locale);
return Blade::render(<<<'BLADE'
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
@livewireScripts
BLADE);
});
return visit("/date-probe/{$locale}")->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
}
class DateFormatProbe extends Component
{
public ?string $sunday = '2026-09-13';
public ?string $iso = '2026-09-13';
public ?string $dotted = '2026-09-13';
/** @var array{start: ?string, end: ?string} */
public array $span = ['start' => '2026-09-13', 'end' => '2026-09-15'];
public function render(): string
{
return <<<'BLADE'
sunday: {{ $sunday }}
iso: {{ $iso }}
dotted: {{ $dotted }}
span: {{ json_encode($span) }}
BLADE;
}
}
function dateFormatProbe(string $locale = 'en')
{
Livewire::component('date-format-probe', DateFormatProbe::class);
Route::middleware('web')->get('/date-format-probe/{locale}', function (string $locale) {
app()->setLocale($locale);
return Blade::render(<<<'BLADE'
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
@livewireScripts
BLADE);
});
return visit("/date-format-probe/{$locale}")->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
}
class ScopedDateProbe extends Component
{
public ?string $early = '2026-09-13';
public ?string $late = '2026-09-13';
public function render(): string
{
return <<<'BLADE'
BLADE;
}
}
function scopedDateProbe()
{
Livewire::component('scoped-date-probe', ScopedDateProbe::class);
Route::middleware('web')->get('/scoped-date-probe', fn () => Blade::render(<<<'BLADE'
@vite(config('livewire-material.showcase.vite'))
@livewireStyles
@livewireScripts
BLADE));
return visit('/scoped-date-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
}
/** A picker's day cell, by its ISO date. */
function day(string $picker, string $date): string
{
return "#{$picker}-picker [data-md-value=\"{$date}\"]";
}
function focusedDay(string $date): string
{
return "document.activeElement.dataset.mdValue === '{$date}'";
}
it('opens the docked picker under its field and walks the grid from the keyboard', function () {
$picker = "document.querySelector('#expires-field-picker')";
$page = dateProbe()
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
->assertScript("{$picker}.matches(':popover-open') && {$picker}.dataset.mdPresentation === 'docked'")
->assertAttribute('#expires-field', 'aria-expanded', 'true')
->assertScript("(() => { const field = document.querySelector('#expires-field').closest('[data-md-field-box]').getBoundingClientRect(); const box = {$picker}.getBoundingClientRect(); return Math.abs(box.top - field.bottom - 4) < 2 && Math.abs(box.left - field.left) < 2; })()")
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'ArrowRight')->assertScript(focusedDay('2026-09-14'));
$page->keys(':focus', 'ArrowDown')->assertScript(focusedDay('2026-09-21'));
$page->keys(':focus', 'End')->assertScript(focusedDay('2026-09-26'));
$page->keys(':focus', 'Home')->assertScript(focusedDay('2026-09-20'));
$page->keys(':focus', 'ArrowUp')->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'PageDown')
->assertScript(focusedDay('2026-10-13'))
->assertScript("document.querySelector('#expires-field-month').textContent === 'October 2026'");
$page->keys(':focus', 'Enter')
->assertSeeIn('#expires', '2026-10-13')
->assertScript("! {$picker}.matches(':popover-open')")
->assertScript("document.activeElement.id === 'expires-field'")
->assertValue('#expires-field', '10/13/2026');
});
it('keeps focus and choice inside min and max', function () {
$page = dateProbe()
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
->assertScript(focusedDay('2026-09-13'));
// Playwright will not press an aria-disabled cell, so the press is dispatched.
$page->assertAttribute(day('expires-field', '2026-09-09'), 'aria-disabled', 'true')
->script("document.querySelector('".day('expires-field', '2026-09-09')."').click()");
$page->assertScript("! document.querySelector('".day('expires-field', '2026-09-09')."').hasAttribute('data-md-selected')")
->assertScript("document.querySelector('".day('expires-field', '2026-09-13')."').hasAttribute('data-md-selected')");
$page->keys(':focus', 'PageUp')->assertScript(focusedDay('2026-09-10'));
$page->keys(':focus', 'ArrowLeft')->assertScript(focusedDay('2026-09-10'));
$page->keys(':focus', 'Shift+PageDown')->assertScript(focusedDay('2026-10-20'));
$page->keys(':focus', 'Enter')->assertSeeIn('#expires', '2026-10-20');
});
it('takes a date typed into the docked field once it is whole, and says when it cannot', function () {
$page = dateProbe()
->type('#expires-field', '09/15/2026')
->assertSeeIn('#expires', '2026-09-15');
$page->type('#expires-field', '13/45/2026')
->keys('#expires-field', 'Enter')
->assertSee('Date does not match expected pattern: MM/DD/YYYY')
->assertAttribute('#expires-field', 'aria-invalid', 'true')
->assertSeeIn('#expires', '2026-09-15');
$page->type('#expires-field', '09/01/2026')
->keys('#expires-field', 'Enter')
->assertSee('Date not allowed: Sep 1, 2026')
->assertSeeIn('#expires', '2026-09-15');
});
it('opens the modal picker, jumps through the year grid and confirms with OK', function () {
$dialog = "document.querySelector('#birthday-field-picker')";
$page = dateProbe()
->click('#birthday-field')
->assertScript("{$dialog}.matches(':modal')")
->assertSeeIn('#birthday-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'May 17, 2000')
->click('#birthday-field-picker [data-md-datepicker-nav]:not([data-md-docked]) [data-md-datepicker-menu-button]')
->assertScript("getComputedStyle(document.querySelector('#birthday-field-picker [data-md-datepicker-years]')).display !== 'none'")
->assertScript("document.activeElement.textContent.trim() === '2000'");
$page->click('#birthday-field-picker [data-md-datepicker-year]:has-text("1990")')
->assertSeeIn('#birthday-field-picker [data-md-datepicker-nav]', 'May 1990')
->click(day('birthday-field', '1990-05-03'))
->assertSeeIn('#birthday-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'May 3, 1990')
->assertSeeIn('#birthday', '2000-05-17');
$page->click('#birthday-field-picker [data-md-datepicker-confirm]')
->assertSeeIn('#birthday', '1990-05-03')
->assertScript("! {$dialog}.open");
});
it('closes on Escape without keeping the draft and hands focus back to the field', function () {
$dialog = "document.querySelector('#birthday-field-picker')";
$page = dateProbe();
$page->script("document.querySelector('#birthday-field').focus()");
$page->keys('#birthday-field', 'Enter')
->assertScript("{$dialog}.open")
->assertScript(focusedDay('2000-05-17'));
$page->keys(':focus', 'ArrowRight')
->keys(':focus', 'Space')
->assertSeeIn('#birthday-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'May 18, 2000');
$page->keys(':focus', 'Escape')
->assertScript("! {$dialog}.open")
->assertScript("document.activeElement.id === 'birthday-field'")
->assertSeeIn('#birthday', '2000-05-17');
// The docked picker too.
$page->script("document.querySelector('[aria-controls=\"expires-field-picker\"][data-md-datepicker-toggle]').focus()");
$page->keys(':focus', 'Enter')
->assertScript("document.querySelector('#expires-field-picker').matches(':popover-open')")
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'Escape')
->assertScript("! document.querySelector('#expires-field-picker').matches(':popover-open')")
->assertScript("document.activeElement.id === 'expires-field'");
});
it('reads the modal input in the locale\'s format and explains a date it cannot take', function () {
$dialog = "document.querySelector('#delivery-field-picker')";
$page = dateProbe()
->click('#delivery-field')
->assertScript("{$dialog}.matches(':modal')")
->assertScript("document.activeElement.id === 'delivery-field-entry'")
->assertSeeIn('#delivery-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'Entered date');
$page->type('#delivery-field-entry', '2026-03-07')
->click('#delivery-field-picker [data-md-datepicker-confirm]')
->assertSeeIn('#delivery-field-entry-support', 'Date does not match expected pattern: MM/DD/YYYY')
->assertAttribute('#delivery-field-entry', 'aria-invalid', 'true')
->assertScript("{$dialog}.open");
$page->type('#delivery-field-entry', '12/24/2025')
->keys('#delivery-field-entry', 'Enter')
->assertSeeIn('#delivery-field-entry-support', 'Date not allowed: Dec 24, 2025')
->assertScript("{$dialog}.open");
$page->type('#delivery-field-entry', '3/7/2026')
->assertSeeIn('#delivery-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'Mar 7, 2026')
->keys('#delivery-field-entry', 'Enter')
->assertSeeIn('#delivery', '2026-03-07')
->assertScript("! {$dialog}.open")
->assertValue('#delivery-field', '03/07/2026');
});
it('picks a range: a start, an end, and the days between', function () {
$page = dateProbe()
->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
->assertAttribute(day('trip-field', '2026-09-14'), 'aria-selected', 'true');
$page->click(day('trip-field', '2026-09-20'))
->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-md-selected')")
->assertScript("! document.querySelector('".day('trip-field', '2026-09-14')."').hasAttribute('data-md-between')");
$page->click(day('trip-field', '2026-09-24'))
->assertScript("[...document.querySelectorAll('#trip-field-picker [data-md-between]')].map((cell) => cell.dataset.mdValue).join() === '2026-09-21,2026-09-22,2026-09-23'")
->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-md-start') && document.querySelector('".day('trip-field', '2026-09-24')."').hasAttribute('data-md-end')")
->assertSeeIn('#trip', '2026-09-15');
$page->click('#trip-field-picker [data-md-datepicker-confirm]')
->assertSeeIn('#trip', '{"start":"2026-09-20","end":"2026-09-24"}')
->assertValue('#trip-field', '09/20/2026 – 09/24/2026');
});
it('follows the locale: German weeks start on Monday, American ones on Sunday', function () {
$firstWeekday = "document.querySelector('#expires-field-picker thead th').getAttribute('abbr')";
$page = dateProbe('de')
->assertValue('#expires-field', '13.09.2026')
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
->assertScript("{$firstWeekday} === 'Montag'")
->assertScript("document.querySelector('#expires-field-month').textContent === 'September 2026'")
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'Home')->assertScript(focusedDay('2026-09-10'));
$page->keys(':focus', 'Escape');
$page->type('#expires-field', '20.9.2026')
->assertSeeIn('#expires', '2026-09-20')
->assertAttribute('#expires-field', 'placeholder', 'DD.MM.YYYY');
dateProbe('en_US')
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
->assertScript("{$firstWeekday} === 'Sunday'");
});
it('stays open, where it was, through a Livewire render', function () {
$page = dateProbe()
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'ArrowRight')->assertScript(focusedDay('2026-09-14'));
$page->script('window.eval("Livewire.first().touch()")');
$page->assertSeeIn('#renders', '1')
->assertScript("document.querySelector('#expires-field-picker').matches(':popover-open')")
->assertAttribute('#expires-field', 'aria-expanded', 'true')
->assertScript("document.querySelector('".day('expires-field', '2026-09-14')."').tabIndex === 0");
$page->keys(day('expires-field', '2026-09-14'), 'Enter')->assertSeeIn('#expires', '2026-09-14');
$page->click('#birthday-field')
->script('window.eval("Livewire.first().touch()")');
$page->assertSeeIn('#renders', '2')
->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')");
});
it('opens a docked picker as a modal one on a compact window', function () {
dateProbe()
->resize(400, 800)
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
->assertScript("document.querySelector('#expires-field-picker').matches(':modal')")
->assertScript("getComputedStyle(document.querySelector('#expires-field-picker [data-md-datepicker-header]')).display !== 'none'");
});
it('empties a date, or both ends of a range, with its clear button', function () {
$page = dateProbe()
->assertScript("getComputedStyle(document.querySelector('#expires-field').closest('[data-md-field]').querySelector('[data-md-field-clear]')).display !== 'none'");
$page->click('[data-md-datepicker]:has(#expires-field) [data-md-field-clear]')
->assertScript("document.querySelector('#expires').textContent === ''")
->assertValue('#expires-field', '')
->assertScript("document.activeElement.id === 'expires-field'")
->assertScript("getComputedStyle(document.querySelector('[data-md-datepicker]:has(#expires-field) [data-md-field-clear]')).display === 'none'");
$page->click('[data-md-datepicker]:has(#trip-field) [data-md-field-clear]')
->assertSeeIn('#trip', '{"start":null,"end":null}')
->assertValue('#trip-field', '');
});
it('starts the week on the day week-start names, whatever the locale says', function () {
$page = dateFormatProbe('de')
->assertValue('#sunday-field', '13.09.2026')
->click('[aria-controls="sunday-field-picker"][data-md-datepicker-toggle]')
->assertScript("document.querySelector('#sunday-field-picker thead th').getAttribute('abbr') === 'Sonntag'")
->assertScript("document.querySelector('#sunday-field-picker tbody td').dataset.mdValue === '2026-08-30'")
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'End')->assertScript(focusedDay('2026-09-19'));
$page->keys(':focus', 'Home')->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'Enter')
->assertSeeIn('#sunday', '2026-09-13')
->assertValue('#sunday-field', '13.09.2026');
});
it('shows and reads a year-first format and still binds Y-m-d', function () {
$page = dateFormatProbe()
->assertValue('#iso-field', '2026-09-13')
->assertAttribute('#iso-field', 'placeholder', 'YYYY-MM-DD')
->type('#iso-field', '2026-10-01')
->assertSeeIn('#iso', '2026-10-01');
$page->type('#iso-field', '10/02/2026')
->keys('#iso-field', 'Enter')
->assertSee('Date does not match expected pattern: YYYY-MM-DD')
->assertSeeIn('#iso', '2026-10-01');
$page->click('[aria-controls="iso-field-picker"][data-md-datepicker-toggle]')
->assertScript(focusedDay('2026-10-01'));
$page->keys(':focus', 'ArrowRight')->assertScript(focusedDay('2026-10-02'));
$page->keys(':focus', 'Enter')
->assertSeeIn('#iso', '2026-10-02')
->assertValue('#iso-field', '2026-10-02');
});
it('reads the dialog\'s text field in the given format, not the locale\'s', function () {
$dialog = "document.querySelector('#dotted-field-picker')";
$page = dateFormatProbe()
->assertValue('#dotted-field', '13.09.2026')
->click('#dotted-field')
->assertScript("{$dialog}.matches(':modal')")
->assertScript("document.activeElement.id === 'dotted-field-entry'")
->assertValue('#dotted-field-entry', '13.09.2026')
->assertAttribute('#dotted-field-entry', 'placeholder', 'DD.MM.YYYY');
$page->type('#dotted-field-entry', '2026-10-01')
->keys('#dotted-field-entry', 'Enter')
->assertSeeIn('#dotted-field-entry-support', 'Date does not match expected pattern: DD.MM.YYYY')
->assertScript("{$dialog}.open");
$page->type('#dotted-field-entry', '1.10.2026')
->assertSeeIn('#dotted-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'Oct 1, 2026')
->keys('#dotted-field-entry', 'Enter')
->assertSeeIn('#dotted', '2026-10-01')
->assertScript("! {$dialog}.open")
->assertValue('#dotted-field', '01.10.2026');
});
it('lays out and shows a range in the given first day and format', function () {
$dialog = "document.querySelector('#span-field-picker')";
$page = dateFormatProbe()->assertValue('#span-field', '13/09/2026 – 15/09/2026');
$page->script("document.querySelector('#span-field').focus()");
$page->keys('#span-field', 'Enter')
->assertScript("{$dialog}.matches(':modal')")
->assertScript("document.querySelector('#span-field-picker thead th').getAttribute('abbr') === 'Saturday'")
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'Home')->assertScript(focusedDay('2026-09-12'));
$page->keys(':focus', 'End')->assertScript(focusedDay('2026-09-18'));
$page->click(day('span-field', '2026-09-20'))
->click(day('span-field', '2026-09-24'))
->click('#span-field-picker [data-md-datepicker-confirm]')
->assertSeeIn('#span', '{"start":"2026-09-20","end":"2026-09-24"}')
->assertValue('#span-field', '20/09/2026 – 24/09/2026');
});
it('keeps each picker\'s own settings inside a page\'s outer x-data scope', function () {
// Settings assigned in init() without being declared would land on the outermost scope,
// where the last picker's null min and Monday start would overwrite the first's.
$page = scopedDateProbe()
->click('[aria-controls="early-field-picker"][data-md-datepicker-toggle]')
->assertScript(focusedDay('2026-09-13'));
$page->assertAttribute(day('early-field', '2026-09-09'), 'aria-disabled', 'true')
->assertScript('! (\'min\' in Alpine.$data(document.querySelector(\'[x-data="{ step: 1 }"]\')))');
});
it('opens the range picker full screen below 600px and grows its month list both ways by keyboard paging', function () {
$picker = "document.querySelector('#trip-field-picker')";
$scope = "Alpine.\$data(document.querySelector('[data-md-datepicker]:has(#trip-field)'))";
$page = dateProbe()
->resize(390, 800)
->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
->assertScript("{$picker}.dataset.mdPresentation === 'full'")
->assertScript("{$picker}.matches(':modal')")
->assertScript("document.querySelector('#trip-field-picker [data-md-datepicker-app-bar]').offsetHeight > 0")
// M3's 14-element anatomy: a close (×) icon button and Save in the app bar, not Cancel/OK.
->assertScript("document.querySelector('#trip-field-picker [data-md-datepicker-close]').getClientRects().length > 0")
->assertScript("document.querySelector('#trip-field-picker [data-md-datepicker-save]').getClientRects().length > 0")
->assertScript("getComputedStyle(document.querySelector('#trip-field-picker [data-md-datepicker-actions]')).display === 'none'")
// The window opens six months either side of the trip's start month.
->assertScript("{$scope}.monthsFrom === '2026-03-01' && {$scope}.monthsTo === '2027-03-01'")
->assertScript(focusedDay('2026-09-13'));
// Paging seven months forward crosses the window's forward edge, growing it to cover the month walked to.
foreach (range(1, 7) as $ignored) {
$page->keys(':focus', 'PageDown');
}
$page->assertScript("{$scope}.monthsTo > '2027-03-01'")
->assertScript(focusedDay('2027-04-13'))
->assertScript("[...document.querySelectorAll('#trip-field-picker [data-md-datepicker-month-label]')].some((label) => label.textContent.trim() === 'April 2027')");
// Paging fourteen months back does the same the other way, past the original start.
foreach (range(1, 14) as $ignored) {
$page->keys(':focus', 'PageUp');
}
$page->assertScript("{$scope}.monthsFrom < '2026-03-01'")
->assertScript(focusedDay('2026-02-13'))
->assertScript("[...document.querySelectorAll('#trip-field-picker [data-md-datepicker-month-label]')].some((label) => label.textContent.trim() === 'February 2026')");
// Picking a range and pressing Save commits it and closes the dialog.
// script() returns the script's result, not the page, so the chain starts again after it.
$page->script("{$scope}.moveTo('2026-09-13', false)");
$page->click(day('trip-field', '2026-09-20'))
->click(day('trip-field', '2026-09-24'))
->click('#trip-field-picker [data-md-datepicker-save]')
->assertSeeIn('#trip', '{"start":"2026-09-20","end":"2026-09-24"}')
->assertScript("! {$picker}.open");
});
it('closes the full-screen range picker from its app bar close button without keeping the draft', function () {
$picker = "document.querySelector('#trip-field-picker')";
dateProbe()
->resize(390, 800)
->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
->click(day('trip-field', '2026-09-20'))
->click('#trip-field-picker [data-md-datepicker-close]')
->assertScript("! {$picker}.open")
->assertSeeIn('#trip', '{"start":"2026-09-13","end":"2026-09-15"}');
});
it('reaches the month and year dropdowns with Shift+M and Shift+Y', function () {
$toggle = '[aria-controls="expires-field-picker"][data-md-datepicker-toggle]';
$page = dateProbe()
->click($toggle)
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'Shift+M')
->assertScript("document.activeElement.dataset.mdDatepickerMenuButton === 'months'");
$page->keys(':focus', 'Escape')
->assertScript("! document.querySelector('#expires-field-picker').matches(':popover-open')");
$page->click($toggle)
->assertScript(focusedDay('2026-09-13'));
$page->keys(':focus', 'Shift+Y')
->assertScript("document.activeElement.dataset.mdDatepickerMenuButton === 'years'");
});