renders++; } public function render(): string { return <<<'BLADE'

confirming: {{ var_export($confirming, true) }}

deleting: {{ var_export($deletingId, true) }}

renders: {{ $renders }}

BLADE; } } class CollapseProbe extends Component { public bool $fineTuning = false; public int $renders = 0; public function touch(): void { $this->renders++; } public function expand(): void { $this->fineTuning = true; } public function collapse(): void { $this->fineTuning = false; } public function render(): string { return <<<'BLADE'

fine tuning: {{ var_export($fineTuning, true) }}

renders: {{ $renders }}

Zones and paces.

advanced:

Everything else.
BLADE; } } function collapseProbe() { Livewire::component('collapse-probe', CollapseProbe::class); Route::middleware('web')->get('/collapse-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/collapse-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } function overlayProbe() { Livewire::component('overlay-probe', OverlayProbe::class); Route::middleware('web')->get('/overlay-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/overlay-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } function containment() { return visit('/material/containment')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } it('writes back what closed means: false for a flag, null for an id', function () { $page = overlayProbe(); $page->click('button:has-text("Confirm")') ->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Are you sure?'))"); $page->click('dialog[open] button:has-text("Re-render")') ->assertSeeIn('#renders', '1') ->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Are you sure?'))"); $page->keys('dialog[open] button:has-text("Re-render")', 'Escape') ->assertScript("! [...document.querySelectorAll('dialog')].some((d) => d.open)") ->assertSeeIn('#confirming', 'false'); $page->click('button:has-text("Delete 7")') ->assertSeeIn('#deleting', '7') ->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Delete share?'))"); $page->script("document.querySelector('dialog[open]').dispatchEvent(new Event('cancel', { cancelable: true }))"); $page->assertSeeIn('#deleting', 'NULL'); }); it('closes a showcase dialog on Escape and gives focus back to its trigger', function () { // Opened from the keyboard: Safari does not focus a button on click, so after a click there // is nothing for the dialog to hand focus back to. $page = containment(); $page->script("[...document.querySelectorAll('#containment button')].find((b) => b.textContent.trim() === 'Basic dialog').focus()"); $page->keys(':focus', 'Enter') ->assertScript("document.querySelector('#containment dialog').open"); $page->keys('#containment dialog[open] button:has-text("Cancel")', 'Escape') ->assertScript("! document.querySelector('#containment dialog').open") ->assertScript("document.activeElement.textContent.trim() === 'Basic dialog'"); }); it('slides a side sheet in, traps the page, and closes on the scrim', function () { $sheet = "document.querySelector('#containment aside[role=\"dialog\"]')"; $page = containment() ->click('#containment button:has-text("Side sheet")') ->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') !== null"); // This sheet's own scrim: the standard side sheet above it on the page is a `[data-md-drawer]` too. $page->script("{$sheet}.parentElement.querySelector(':scope > [aria-hidden=\"true\"]').click()"); $page->assertScript("getComputedStyle({$sheet}).display === 'none'") ->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') === null"); }); it('dismisses a bottom sheet dragged down past a quarter of its height', function () { $sheet = "document.querySelector('#containment section[role=\"dialog\"]')"; $page = containment() ->click('#containment button:has(> span:text-is("Bottom sheet"))') ->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->wait(0.5); $page->script(<< { const handle = {$sheet}.querySelector('[data-md-bottom-sheet-handle]'); const box = handle.getBoundingClientRect(); const x = box.x + box.width / 2, y = box.y + box.height / 2; handle.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: x, clientY: y, button: 0 })); window.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y + 400 })); window.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y + 400 })); })() JS); $page->assertScript("getComputedStyle({$sheet}).display === 'none'"); }); class NestedDialogProbe extends Component { public bool $outer = false; public bool $inner = false; public function render(): string { return <<<'BLADE'
{{-- Short, and the trigger for the inner dialog right after it: both stay in view without scrolling, so opening the inner dialog is not itself a scroll of the outer one's body — the very thing this test rules out. --}}

Outer body, short enough to fit without scrolling.

Inner body, line one of several, independent of the outer dialog it opened from and its own paragraphs so the stack genuinely overflows the box regardless of the window's height.

Inner body, line two, long enough on its own to take real vertical room in the scrolling body.

Inner body, line three, long enough on its own to take real vertical room in the scrolling body.

Inner body, line four, long enough on its own to take real vertical room in the scrolling body.

Inner body, line five, long enough on its own to take real vertical room in the scrolling body.

Inner body, line six, long enough on its own to take real vertical room in the scrolling body.

BLADE; } } function nestedDialogProbe() { Livewire::component('nested-dialog-probe', NestedDialogProbe::class); Route::middleware('web')->get('/nested-dialog-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/nested-dialog-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } it('marks a scrolling dialog\'s divider at the edge with more content, and a nested dialog keeps its own marks', function () { // Short and narrow so the probes' paragraphs genuinely overflow their body. $page = nestedDialogProbe()->resize(320, 300); $page->click('button:has-text("Open outer")'); // Matched by each dialog's own title element, not its full (recursive) textContent: the // outer dialog's textContent also contains the nested inner dialog's text, "Inner" included, // so a substring match on the whole dialog would find the outer one for both. $outer = "[...document.querySelectorAll('dialog[open]')].find((d) => d.querySelector('[data-md-modal-title]')?.textContent === 'Outer')"; $outerHead = "{$outer}.querySelector('[data-md-modal-head]')"; $outerBody = "{$outer}.querySelector('[data-md-modal-body]')"; // Scrolled to the top: nothing hidden above, so the head's rule stays off. $page->assertScript("getComputedStyle({$outerHead}, '::after').opacity === '0'"); $page->click('button:has-text("Open inner")'); $inner = "[...document.querySelectorAll('dialog[open]')].find((d) => d.querySelector('[data-md-modal-title]')?.textContent === 'Inner')"; $innerHead = "{$inner}.querySelector('[data-md-modal-head]')"; $innerBody = "{$inner}.querySelector('[data-md-modal-body]')"; $page->assertScript("getComputedStyle({$innerHead}, '::after').opacity === '0'"); $page->script("{$innerBody}.scrollTop = {$innerBody}.scrollHeight"); // The scroll listener that marks the dialog runs off the native `scroll` event, which fires // asynchronously after the assignment above. $page->wait(0.2); // The inner dialog's own body scrolled to its end marks its own head; the outer dialog, whose // body was never touched, keeps its own mark off — each dialog watches only its own scroll. $page->assertScript("getComputedStyle({$innerHead}, '::after').opacity === '1'") ->assertScript("getComputedStyle({$outerHead}, '::after').opacity === '0'"); }); it('shows a full-screen dialog\'s 56px phone bar below 600px, and the basic dialog above it', function () { // Opened wide, before narrowing: the standard side sheet example further down the page opens // by default too, and below 840px its scrim (correctly) covers the whole viewport and traps // the page — narrowing first would leave nothing else clickable. The bar's visibility is pure // CSS on the fullscreen flag, so which comes first makes no difference to what is asserted. $page = containment(); $page->click('#containment button:has-text("Full screen on a phone")'); $page->resize(400, 800); $dialog = "document.querySelector('#containment dialog[open]')"; $bar = "{$dialog}.querySelector('[data-md-modal-bar]')"; $page->assertScript("getComputedStyle({$bar}).display !== 'none'") ->assertScript("Math.round({$bar}.getBoundingClientRect().height) === 56"); $page->resize(900, 800) ->assertScript("getComputedStyle({$bar}).display === 'none'"); }); it('draws separator dividers whatever the scroll, on a dialog that fits without scrolling', function () { $page = containment(); $page->click('#containment button:has-text("Always divided (separator)")'); $dialog = "document.querySelector('#containment dialog[open]')"; $head = "{$dialog}.querySelector('[data-md-modal-head]')"; $actions = "{$dialog}.querySelector('[data-md-modal-actions]')"; $body = "{$dialog}.querySelector('[data-md-modal-body]')"; // The body fits without scrolling, so an unmarked dialog would show neither rule. $page->assertScript("{$body}.scrollHeight <= {$body}.clientHeight + 1") ->assertScript("getComputedStyle({$head}, '::after').opacity === '1'") ->assertScript("getComputedStyle({$actions}, '::before').opacity === '1'"); }); it('opens a dialog with no fade under reduced motion, but a real one without it', function () { $dialog = "document.querySelector('#containment dialog[open]')"; // Without reduced motion: the spring is a real, non-zero transition. $normal = containment() ->assertScript("! window.matchMedia('(prefers-reduced-motion: reduce)').matches"); $normal->click('#containment button:has-text("Basic dialog")'); $normal->assertScript("parseFloat(getComputedStyle({$dialog}).transitionDuration) > 0"); // With reduced motion (the plugin's own emulation, `visit(..., ['reducedMotion' => 'reduce'])`): // tokens/motion.css takes the same spring's duration to zero, so there is no transition to run, // and the dialog is at its final opacity at once rather than mid-fade. $reduced = visit('/material/containment', ['reducedMotion' => 'reduce'])->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'") ->assertScript("window.matchMedia('(prefers-reduced-motion: reduce)').matches"); $reduced->click('#containment button:has-text("Basic dialog")'); $reduced->assertScript("parseFloat(getComputedStyle({$dialog}).transitionDuration) === 0") ->assertScript("getComputedStyle({$dialog}).opacity === '1'"); }); class TextOnlyDialogProbe extends Component { public bool $open = false; public function render(): string { return <<<'BLADE'

Line one of a body with nothing else focusable inside it, so the dialog hands its first focus to the scrolling body itself: line two, line three, line four, line five, line six, line seven, line eight, line nine, line ten, line eleven, line twelve, line thirteen, line fourteen, line fifteen.

BLADE; } } function textOnlyDialogProbe() { Livewire::component('text-only-dialog-probe', TextOnlyDialogProbe::class); Route::middleware('web')->get('/text-only-dialog-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); return visit('/text-only-dialog-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); } it('gives the scrolling body an inset focus ring when a text-only dialog opens from the keyboard', function () { // A short, narrow window so the paragraph genuinely overflows the body before the dialog // opens: showModal() picks its initial focus once, at open, from what is focusable then. $page = textOnlyDialogProbe()->resize(320, 300); $page->script("document.querySelector('button').focus()"); $page->keys(':focus', 'Enter'); $body = "document.querySelector('dialog[open] [data-md-modal-body]')"; $page->assertScript("document.activeElement.matches('[data-md-modal-body]')") ->assertScript("getComputedStyle({$body}).outlineStyle === 'solid'") ->assertScript("getComputedStyle({$body}).outlineWidth === '3px'") ->assertScript("getComputedStyle({$body}).outlineOffset === '-3px'"); }); it('cycles a bottom sheet\'s preset heights from its handle, announcing each, and closes from the last', function () { $page = containment(); $page->click('#containment button:has-text("Bottom sheet with preset heights")'); $sheet = "document.querySelector('#containment [data-md-bottom-sheet-panel][data-md-preset]')"; $handle = "{$sheet}.querySelector('[data-md-bottom-sheet-grip]')"; $announce = "{$sheet}.querySelector('[data-md-bottom-sheet-announce]')"; $page->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '50dvh'"); $gripSelector = '#containment [data-md-bottom-sheet-panel][data-md-preset] [data-md-bottom-sheet-grip]'; $page->click($gripSelector); $page->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '90dvh'") ->assertScript("{$announce}.textContent.trim() === 'Height 3 of 3'"); // From the last stop, activating the handle closes the sheet, as a handle with no stops does. $page->click($gripSelector); $page->assertScript("getComputedStyle({$sheet}).display === 'none'"); }); it('settles a dragged bottom sheet on the nearest preset height', function () { $page = containment(); $page->click('#containment button:has-text("Bottom sheet with preset heights")'); $sheet = "document.querySelector('#containment [data-md-bottom-sheet-panel][data-md-preset]')"; $page->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->wait(0.5); // Drag up from the 50dvh stop a little way — not as far as 90dvh — and release: it settles // back on 50dvh, the nearest stop, rather than creep on to the next one. $page->script(<< { const handle = {$sheet}.querySelector('[data-md-bottom-sheet-handle]'); const box = handle.getBoundingClientRect(); const x = box.x + box.width / 2, y = box.y + box.height / 2; handle.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true, clientX: x, clientY: y, button: 0 })); window.dispatchEvent(new PointerEvent('pointermove', { clientX: x, clientY: y - 40 })); window.dispatchEvent(new PointerEvent('pointerup', { clientX: x, clientY: y - 40 })); })() JS); $page->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '50dvh'"); }); it('is a standard side sheet from 840px, without a scrim or a focus trap, and the modal one below', function () { $page = containment()->resize(1000, 800); $sheet = "document.querySelector('#containment aside[data-md-side]')"; $scrim = "{$sheet}.parentElement.querySelector(':scope > [data-md-drawer-scrim]')"; $toggle = '#containment button:has-text("Show or hide the filters")'; // From 840px it opens co-planar (the example starts `open: true`): no scrim shown, and the // rest of the page stays interactive. $page->assertScript("getComputedStyle({$sheet}).position === 'relative'") ->assertScript("getComputedStyle({$scrim}).display === 'none'") ->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') === null") ->assertScript("! {$sheet}.hasAttribute('inert')"); $page->click($toggle); $page->assertScript("getComputedStyle({$sheet}).opacity === '0'"); // Below 840px the same sheet becomes the modal one: fixed, with a scrim, and the page inert. $page->resize(700, 800) ->click($toggle); $page->assertScript("getComputedStyle({$sheet}).position === 'fixed'") ->assertScript("getComputedStyle({$scrim}).display !== 'none'") ->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') !== null"); }); it('opens a row\'s opener from a press anywhere on the row, but not from its own buttons', function () { $page = containment(); $row = '#containment [data-md-card][data-md-list-row]:not([data-md-list-actionable])'; $page->script("window.__opened = 0; document.querySelector('{$row} [data-md-list-open]').addEventListener('click', (event) => { event.preventDefault(); window.__opened++ })"); $page->click("{$row} p") ->assertScript('window.__opened === 1'); $page->click("{$row} button:has-text(\"Copy link\")") ->assertScript('window.__opened === 1'); }); it('makes a directly actionable card the one tab stop, answering Enter', function () { $card = '#containment [data-md-card][data-md-list-actionable]'; $page = containment(); $page->script("window.__opened = 0; document.querySelector('{$card} [data-md-list-open]').addEventListener('click', (event) => { event.preventDefault(); window.__opened++ })"); $page->script("document.querySelector('{$card}').focus()"); $page->assertScript("document.activeElement.matches('{$card}')"); $page->keys($card, 'Enter') ->assertScript('window.__opened === 1'); }); it('binds a collapse to a Livewire property both ways', function () { $collapse = "document.querySelector('#fine-tuning-collapse')"; $page = collapseProbe() ->assertScript("{$collapse}.open === false"); $page->click('#fine-tuning-collapse summary') ->assertScript("{$collapse}.open === true") ->click('button:has-text("Re-render")') ->assertSeeIn('#renders', '1') ->assertSeeIn('#fine-tuning', 'true') ->assertScript("{$collapse}.open === true"); $page->click('button:has-text("Close from the server")') ->assertSeeIn('#fine-tuning', 'false') ->assertScript("{$collapse}.open === false"); $page->click('button:has-text("Open from the server")') ->assertSeeIn('#fine-tuning', 'true') ->assertScript("{$collapse}.open === true"); }); it('binds a collapse to an Alpine property both ways', function () { $collapse = "document.querySelector('#advanced-collapse')"; $page = collapseProbe() ->assertScript("{$collapse}.open === false"); $page->click('#advanced-collapse summary') ->assertScript("{$collapse}.open === true") ->assertSeeIn('#advanced', 'true'); $page->click('button:has-text("Close from Alpine")') ->assertSeeIn('#advanced', 'false') ->assertScript("{$collapse}.open === false"); }); it('fades the side sheet\'s scrim out on close, rather than making it vanish', function () { $sheet = "document.querySelector('#containment aside[role=\"dialog\"]')"; $scrim = "{$sheet}.parentElement.querySelector(':scope > [data-md-drawer-scrim]')"; $page = containment() ->click('#containment button:has-text("Side sheet")') ->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->assertScript("getComputedStyle({$scrim}).opacity === '1'"); // Triggering the close and sampling for a mid-fade opacity in the same round trip: a round // trip apiece for a separate `script` and `assertScript` already costs real time, easily as // much as the 200ms fade itself, so a click, then a later separate read, can just as easily // land before the fade starts or after it ends. This samples every few ms, in the page itself, // for whether it was ever caught strictly between fully shown and fully hidden, rather than // simply gone at once. $midFade = $page->script(<< { {$scrim}.click() for (let i = 0; i < 60; i++) { const opacity = parseFloat(getComputedStyle({$scrim}).opacity) if (opacity > 0.02 && opacity < 0.98) return true await new Promise((resolve) => setTimeout(resolve, 5)) } return false })() JS); expect($midFade)->toBeTrue(); $page->assertScript("getComputedStyle({$scrim}).display === 'none'"); }); it('fades the bottom sheet\'s scrim out on close, rather than making it vanish', function () { $sheet = "document.querySelector('#containment section[role=\"dialog\"]')"; $scrim = "{$sheet}.parentElement.querySelector(':scope > [data-md-bottom-sheet-scrim]')"; $page = containment() ->click('#containment button:has(> span:text-is("Bottom sheet"))') ->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->assertScript("getComputedStyle({$scrim}).opacity === '1'"); $midFade = $page->script(<< { {$scrim}.click() for (let i = 0; i < 60; i++) { const opacity = parseFloat(getComputedStyle({$scrim}).opacity) if (opacity > 0.02 && opacity < 0.98) return true await new Promise((resolve) => setTimeout(resolve, 5)) } return false })() JS); expect($midFade)->toBeTrue(); $page->assertScript("getComputedStyle({$scrim}).display === 'none'"); }); it('slides the side sheet in from its own edge in a right-to-left page', function () { Route::middleware('web')->get('/drawer-rtl-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles
Narrow the list down.
@livewireScripts BLADE)); $page = visit('/drawer-rtl-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'"); $sheet = "document.querySelector('aside[data-md-side]')"; $page->click('button:has-text("Open")'); // `side="end"` (the default) is the trailing edge — the *left* of a right-to-left page — so it // slides in from off the left, not the right (the CSS `:dir(rtl)` mirror the Tailwind build // rewrites to a `:lang()` list, hence `lang="ar"` here as well as `dir="rtl"`): caught well // before the 500ms spatial transition ends, it is still off-screen on the left. $page->wait(0.05); $page->assertScript("getComputedStyle({$sheet}).display !== 'none'") ->assertScript("{$sheet}.getBoundingClientRect().left < 0"); // And settles flush against that same left edge, not the right. $page->wait(0.6) ->assertScript("Math.round({$sheet}.getBoundingClientRect().left) === 0"); }); class NestedFullscreenDialogProbe extends Component { public bool $settings = false; public bool $confirm = false; public function render(): string { return <<<'BLADE'
A form that needs the room. Recipients lose access at once.
BLADE; } } it('keeps a basic dialog\'s own title and padding when it is nested inside a full-screen dialog at 400px', function () { Livewire::component('nested-fullscreen-dialog-probe', NestedFullscreenDialogProbe::class); Route::middleware('web')->get('/nested-fullscreen-dialog-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles @livewireScripts BLADE)); $page = visit('/nested-fullscreen-dialog-probe')->resize(400, 800)->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'"); $page->click('button:has-text("Open settings")'); $outerBar = "document.querySelector('dialog[data-md-fullscreen][open] [data-md-modal-bar]')"; // The rule hiding a full-screen dialog's own head below 600px, so the phone bar alone names // it, is scoped to that dialog's own box — confirmed here, since the nested basic dialog below // must keep its own head regardless. $page->assertScript("getComputedStyle({$outerBar}).display !== 'none'"); $page->click('button:has-text("Delete the share")'); $inner = "[...document.querySelectorAll('dialog[open]')].find((d) => d.querySelector('[data-md-modal-title]')?.textContent === 'Delete this share?')"; $innerHead = "{$inner}.querySelector('[data-md-modal-head]')"; $innerTitle = "{$inner}.querySelector('[data-md-modal-title]')"; $page->assertScript("getComputedStyle({$innerHead}).display !== 'none'") ->assertScript("getComputedStyle({$innerTitle}).display !== 'none'") ->assertScript("parseFloat(getComputedStyle({$innerHead}).paddingTop) > 0") ->assertScript("parseFloat(getComputedStyle({$innerHead}).paddingInlineStart) > 0"); }); it('shows the 16% dragged state layer on the showcase card being dragged', function () { $page = containment(); $card = "[...document.querySelectorAll('#containment [data-md-card]')].find((c) => c.textContent.includes('Elevation 4 and the 16% state layer'))"; $page->assertScript("getComputedStyle({$card}, '::before').opacity === '0'"); $page->click('#containment button:has-text("Pick it up or put it down")'); $page->assertScript("{$card}.hasAttribute('data-md-dragged')") ->assertScript("getComputedStyle({$card}, '::before').opacity === '0.16'"); $page->click('#containment button:has-text("Pick it up or put it down")'); $page->assertScript("! {$card}.hasAttribute('data-md-dragged')") ->assertScript("getComputedStyle({$card}, '::before').opacity === '0'"); }); it('keeps a selected segmented row\'s fill under the hover and focus tint', function () { Route::middleware('web')->get('/segmented-selected-row-probe', fn () => Blade::render(<<<'BLADE' @vite(config('livewire-material.showcase.vite')) @livewireStyles
@livewireScripts BLADE)); $page = visit('/segmented-selected-row-probe')->waitForEvent('networkidle') ->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'"); $selected = "document.querySelector('[data-md-selected]')"; $plain = "document.querySelector('[data-md-list-item]:not([data-md-selected])')"; $restBackground = $page->script("getComputedStyle({$selected}).backgroundColor"); $page->hover('[data-md-selected]'); $selectedHoverBackground = $page->script("getComputedStyle({$selected}).backgroundColor"); $page->hover('[data-md-list-item]:not([data-md-selected])'); $plainHoverBackground = $page->script("getComputedStyle({$plain}).backgroundColor"); // The hover tint is mixed *over* the row's own fill (`--md-list-row-fill`, list-item.css): // the selected row's secondary-container stays under it, so its hover colour differs from // both its own resting colour and a plain row's hover, which has no fill to keep. expect($selectedHoverBackground)->not->toBe($restBackground) ->and($selectedHoverBackground)->not->toBe($plainHoverBackground); $page->script("document.querySelector('[data-md-selected] [data-md-list-open]').focus()"); $selectedFocusBackground = $page->script("getComputedStyle({$selected}).backgroundColor"); expect($selectedFocusBackground)->not->toBe($restBackground) ->and($selectedFocusBackground)->not->toBe($selectedHoverBackground); });