From 0b7dd7d46a8b064cc2bf155ca67a4b2b04ebd5c0 Mon Sep 17 00:00:00 2001 From: Andreas Reinhold / reini Date: Mon, 14 Sep 2026 20:04:40 +0200 Subject: [PATCH] Cover the datepicker's shared classes, hover layer and 20px root font Browser-test coverage the reviewer asked for on the step 36 batch: the year, menu-button and list-option state layer/focus ring (with the option's ring drawn inset in its row), a selected day's hover layer over its primary fill versus none on a disabled or blank day, and the 40px day indicator holding its size at a 20px root font size while only its (rem-sized) text grows. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9 --- tests/Browser/DatepickerTest.php | 96 ++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/tests/Browser/DatepickerTest.php b/tests/Browser/DatepickerTest.php index bfc6fbb8..262a94b2 100644 --- a/tests/Browser/DatepickerTest.php +++ b/tests/Browser/DatepickerTest.php @@ -581,3 +581,99 @@ it('reaches the month and year dropdowns with Shift+M and Shift+Y', function () $page->keys(':focus', 'Shift+Y') ->assertScript("document.activeElement.dataset.mdDatepickerMenuButton === 'years'"); }); + +it('draws the shared state layer and focus ring on the menu buttons, the year and the list options, the option\'s ring drawn inset', function () { + $monthButton = '#expires-field-picker [data-md-datepicker-menu-button="months"]'; + $yearButton = '#expires-field-picker [data-md-datepicker-menu-button="years"]'; + $modalYearButton = '#birthday-field-picker [data-md-datepicker-nav]:not([data-md-docked]) [data-md-datepicker-menu-button]'; + $selectedOption = "document.querySelector('#expires-field-picker [data-md-datepicker-option][aria-selected=\"true\"]')"; + $selectedYear = "document.querySelector('#birthday-field-picker [data-md-datepicker-year][aria-selected=\"true\"]')"; + + $hasBoth = fn (string $expr): string => "({$expr}).classList.contains('md-state-layer') && ({$expr}).classList.contains('md-focus-ring')"; + + $page = dateProbe() + ->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]') + ->assertScript($hasBoth("document.querySelector('{$monthButton}')")) + ->assertScript($hasBoth("document.querySelector('{$yearButton}')")) + ->click($monthButton) + // toggleView() scrolls the selected option into view and focuses it once settled. + ->assertScript("document.activeElement === {$selectedOption}") + ->assertScript($hasBoth($selectedOption)) + // A real keydown (rather than the script focus above) puts the browser in keyboard + // modality, so :focus-visible — and the ring the foundation draws inside the row rather + // than round the control — actually applies. + ->keys(':focus', 'ArrowDown') + ->assertScript("document.activeElement.matches('[data-md-datepicker-option]')") + ->assertScript("getComputedStyle(document.activeElement).outlineOffset === '-3px'"); + + // Close the docked popover, or it sits over the next field and swallows the click below. + $page->keys(':focus', 'Escape') + ->assertScript("! document.querySelector('#expires-field-picker').matches(':popover-open')"); + + $page->click('#birthday-field') + ->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')") + ->click($modalYearButton) + ->assertScript($hasBoth("document.querySelector('{$modalYearButton}')")) + ->assertScript("document.activeElement === {$selectedYear}") + ->assertScript($hasBoth($selectedYear)); +}); + +it('shows a state layer over a selected day\'s primary fill on hover, and none on a disabled or blank day', function () { + $selectedDay = day('expires-field', '2026-09-13'); + $selectedSpan = "document.querySelector('{$selectedDay} > span')"; + $disabledDay = day('expires-field', '2026-09-09'); + $disabledSpan = "document.querySelector('{$disabledDay} > span')"; + + $page = dateProbe() + ->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]') + ->assertScript("document.querySelector('{$selectedDay}').hasAttribute('data-md-selected')") + ->assertAttribute($disabledDay, 'aria-disabled', 'true') + // The day's own fill transitions in when the popover opens: let it settle before reading + // a resting value, or the "unchanged by hover" comparison below race against that instead. + ->wait(0.3); + + $fill = $page->script("getComputedStyle({$selectedSpan}).backgroundColor"); + + $page->hover($selectedDay) + // The layer draws over the same primary fill: hovering never changes the day's own background. + ->assertScript("getComputedStyle({$selectedSpan}).backgroundColor === '{$fill}'") + ->assertScript("getComputedStyle({$selectedSpan}, '::before').opacity !== '0'"); + + $page->hover($disabledDay) + ->assertScript("getComputedStyle({$disabledSpan}, '::before').display === 'none'"); + + $page->script("document.querySelector('{$disabledDay}').focus()"); + + $page->assertScript("document.activeElement === document.querySelector('{$disabledDay}')") + ->assertScript("getComputedStyle({$disabledSpan}, '::before').display === 'none'"); + + // Close the docked popover, or it sits over the next field and swallows the click below. + $page->keys(':focus', 'Escape') + ->assertScript("! document.querySelector('#expires-field-picker').matches(':popover-open')"); + + // A blank cell (no date, outside the shown month in modal presentation) can't be focused at + // all — the same rule hides its layer regardless. + $page->click('#birthday-field') + ->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')") + ->hover('#birthday-field-picker [data-md-blank] > span >> nth=0') + ->assertScript("getComputedStyle([...document.querySelectorAll('#birthday-field-picker [data-md-blank] > span')][0], '::before').display === 'none'"); +}); + +it('keeps the day indicator its fixed 40px size at a 20px root font size, growing only its text', function () { + $daySpan = "document.querySelector('".day('expires-field', '2026-09-13')." > span')"; + $width = "Math.round({$daySpan}.getBoundingClientRect().width)"; + $font = "getComputedStyle({$daySpan}).fontSize"; + $picker = "document.querySelector('#expires-field-picker')"; + + $page = dateProbe()->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]'); + + $restingWidth = (int) $page->script($width); + $restingFont = $page->script($font); + + $page->script("document.documentElement.style.fontSize = '20px'"); + + $page->assertScript("({$width}) === {$restingWidth}") + ->assertScript("({$font}) !== '{$restingFont}'") + // The px-sized grid sets the popover's size, not the (rem-sized) text now growing inside it. + ->assertScript("{$picker}.scrollWidth <= {$picker}.clientWidth + 1 && {$picker}.scrollHeight <= {$picker}.clientHeight + 1"); +});