Cut duplicated and speculative code across the package

An over-engineering audit of the whole tree, applied in five reviewed
batches. Behaviour stays the same except where UPGRADE.md says otherwise.

PHP: the showcase and error-page stylesheets are prebuilt into
resources/dist by bin/stylesheets.mjs, through Vite's own postcss-import
(first occurrence kept, the order an application's build gives), instead
of Stylesheets::bundle() inlining imports on every request; only the
import walk DesignGuard needs stays. SchemeStylesheet::withProfiles()
replaces three copies of the scheme-plus-profiles loop, material:scheme
leaves spec and contrast checks to the node script that already made
them, and the error page's scheme cache, the hashed view namespace, the
translations path with no lang/ folder and DesignGuard's 1.x-name hints
are gone.

JS: the androidx shape port progress.js and both bin scripts each carried
lives once in resources/js/shapes.js (the generated SVGs are unchanged);
util.js holds ringIndex(), ms(), reopenGuard() and remember(), which
were written out several times; listeners are released through
AbortController; tooltip.js's hoverPopover() serves the rich tooltip too.

CSS: every rule for an element inside the navigation rail queries
`--md-navigation-rail-value` instead of repeating the seven collapsed
conditions under five media branches; badge, alert, progress, slider and
button read one non-inheriting colour-role table (components/color.css);
the dialog chrome, the submenu's popover chrome, the chip's state layer
and touch target, and the visually-hidden inputs use the shared rules
they copied; foundation/tokens.css is folded into foundation.css.

Views: Support\Field and Support\Link replace the error-key, bound-value
and link-attribute blocks copied into the fields and link components;
the timepicker period group, the menu filter and the showcase head are
partials; the datepicker's steppers and entry fields are loops; component
docblocks no longer restate SKILL.md.

Tests and tooling: one dataset-driven ComponentStylesheetsTest replaces
four per-group files, DesignGuardTest and the layout-component tests use
datasets, browser tests share one ready() helper, CSS parsing lives in
ComponentStylesheet alone. docs/audits and the finding IDs citing it are
removed, as are pestphp/pest-plugin-laravel, the unused composer scripts
and check:font; the lint job runs in the feature job, which now installs
node packages so the prebuilt-stylesheet staleness test runs in CI.

Feature suite 1177 passed, Chrome browser suite 299 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Andreas Reinhold / reini
2026-09-17 19:29:21 +02:00
co-authored by Claude Opus 5
parent 471d927e64
commit 247c596c3a
233 changed files with 16635 additions and 10579 deletions
+8 -16
View File
@@ -122,14 +122,12 @@ function menuMorphProbe()
</html>
BLADE));
return visit('/menu-morph-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/menu-morph-probe'));
}
function showcase(string $section = 'buttons')
{
return visit("/material/{$section}")->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit("/material/{$section}"));
}
function focused(string $expression): string
@@ -308,8 +306,7 @@ it('hangs a menu on its menu button, even when the button is fixed to a corner o
$placed = menuAgainst('fab', 'Create');
$above = "(({ control, menu }) => getComputedStyle(document.querySelector('[data-test=\"fab\"]')).position === 'fixed' && menu.bottom <= control.top && control.top - menu.bottom <= 16 && Math.abs(menu.right - control.right) <= 16 && menu.left >= 0 && menu.top >= 0)({$placed})";
$page = visit('/menu-anchor-probe')->resize(400, 800)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/menu-anchor-probe')->resize(400, 800));
$page->click('@fab')
->assertAttribute('@fab', 'aria-expanded', 'true')
@@ -559,8 +556,7 @@ it('hides a disabled fab button below 600px, as M3 removes a FAB whose action is
return [getComputedStyle(button).display !== 'none' && button.checkVisibility(), getComputedStyle(button).position === 'fixed'];
})()";
$page = visit('/disabled-fab-probe')->resize(393, 800)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/disabled-fab-probe')->resize(393, 800), alpine: false);
expect($page->script($state('enabled-fab')))->toBe([true, true])
->and($page->script($state('disabled-fab')))->toBe([false, true])
@@ -627,8 +623,7 @@ function groupSizeProbe()
</html>
BLADE));
return visit('/group-size-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
return ready(visit('/group-size-probe'), livewire: false);
}
it('takes a press at an xs or sm group segment\'s 48px edge, past what it draws, from the shared classes', function () {
@@ -767,8 +762,7 @@ function longMenuProbe()
</html>
BLADE));
return visit('/long-menu-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
return ready(visit('/long-menu-probe'), livewire: false);
}
function sheetMenuProbe()
@@ -790,8 +784,7 @@ function sheetMenuProbe()
</html>
BLADE));
return visit('/sheet-menu-probe')->resize(400, 800)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/sheet-menu-probe')->resize(400, 800));
}
it('opens a submenu with Right, Enter or Space, and closes it with Left or Escape, back on the item', function () {
@@ -984,8 +977,7 @@ function longFabMenuProbe()
</html>
BLADE));
return visit('/long-fab-menu-probe')->resize(320, 420)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
return ready(visit('/long-fab-menu-probe')->resize(320, 420), livewire: false);
}
it('scrolls the FAB menu\'s items on a short window, behind the close button, which stays put', function () {
+6 -11
View File
@@ -82,8 +82,7 @@ function barsProbe()
</html>
BLADE));
return visit('/bars-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/bars-probe'));
}
it('moves between tabs with the arrow keys, skipping disabled ones, and tells Livewire', function () {
@@ -285,8 +284,7 @@ function appBarOverflowProbe()
</html>
BLADE));
return visit('/app-bar-overflow-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/app-bar-overflow-probe'));
}
it('shows the app bar overflow at exactly 599 and 600px', function () {
@@ -370,8 +368,7 @@ function toolbarScaffoldProbe()
</html>
BLADE));
return visit('/toolbar-scaffold-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/toolbar-scaffold-probe'));
}
it('keeps a docked toolbar clear of the navigation bar below medium', function () {
@@ -414,8 +411,7 @@ it('lifts a snackbar above a floating toolbar placed at the bottom, without a sc
</html>
BLADE));
$page = visit('/floating-toolbar-probe')->resize(393, 800)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/floating-toolbar-probe')->resize(393, 800));
$page->script("materialToast('Link copied', { timeout: 10000 })");
$page->wait(1);
@@ -490,11 +486,10 @@ it('never flashes a tab panel before Alpine boots', function () {
// Only the chosen ("people") panel is on screen at the moment the inline script right after
// the panels runs — the server's own `style="display: none"`, not Alpine's x-show, is what
// kept the other two off screen up to here (N-05).
// kept the other two off screen up to here.
$page = visit('/tabs-first-paint-probe')
->assertScript("window.eval('window.firstPaint.join(\",\")') === 'none,block,none'");
$page->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'")
ready($page, livewire: false)
->assertNoJavaScriptErrors();
});
+3 -3
View File
@@ -40,8 +40,8 @@ class CarouselMorphProbe extends Component
* offset that brings item i into focus) and `at(i)`; the script's value is the result. Pest
* retries a failing assertion and gives each attempt a second: keep scripts well inside it.
*
* `open(i)` rather than a zero inset: M3's 16dp of content padding, the carousel's default since
* containment.md C-12, shrinks every keyline near the ends (Compose's
* `open(i)` rather than a zero inset: M3's 16dp of content padding, the carousel's default,
* shrinks every keyline near the ends (Compose's
* createShiftedKeylineListForContentPadding), so a large item resting at the start or the end is
* masked by that share of the padding — 1.33px on each side in the showcase's first carousel.
*/
@@ -188,7 +188,7 @@ it('scrolls instantly and leaves every item unmasked under reduced motion', func
it('brings an item cut off at the row\'s edge into view when it is pressed under reduced motion', function () {
// Owed by the Chromium baseline (step 32, docs/plans/material-3-browser-tests.md): reduced
// motion writes a zero inset for every item (C-05), so a mask cannot say an item is only partly
// motion writes a zero inset for every item, so a mask cannot say an item is only partly
// shown; carousel.js reads the item's box against the row's instead, as for multi-aspect.
$page = carouselShowcase(['reducedMotion' => 'reduce'])
->assertScript(onCarousel(0, <<<'JS'
+9 -11
View File
@@ -1,10 +1,10 @@
<?php
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Route;
use Livewire\Component;
use Livewire\Livewire;
use NoNameWeb\LivewireMaterial\Support\Stylesheets;
/**
* The central claims of the `@layer material` design (plan step 42, docs/plans/material-3-alignment.md
@@ -17,8 +17,7 @@ use NoNameWeb\LivewireMaterial\Support\Stylesheets;
*/
function cascadeReady(mixed $page): mixed
{
return $page->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready($page);
}
it('lets an application\'s unlayered rule beat a component default', function () {
@@ -73,14 +72,13 @@ it('lets [hidden] beat a component\'s display', function () {
it('keeps the layer order even when a component file is imported before the foundation', function () {
// Every package stylesheet opens with the same full `@layer` statement (plan step 33), so the
// order it establishes holds whichever file a page reaches first — button.css's own copy of it,
// reached here before foundation.css's. Inlined through Stylesheets::bundle(), the same PHP
// bundler the showcase and the error page's fallback use, in that deliberate order: button.css's
// rules, in `material.components`, physically precede foundation.css's reset, in
// `material.reset` — and still lose the layer priority fight, so the button's own padding
// survives the reset's `padding: 0`.
$css = Stylesheets::bundle([__DIR__.'/../../resources/css/components/button.css'])
.Stylesheets::bundle([__DIR__.'/../../resources/css/foundation.css']);
// order it establishes holds whichever file a page reaches first — the error layout's copy of
// it, reached here before the foundation's. The two prebuilt error-page stylesheets, in that
// deliberate order: the layout's bundle, whose button.css rules sit in `material.components`,
// physically precedes the fallback's foundation reset, in `material.reset` — and still loses
// the layer priority fight, so the button's own padding survives the reset's `padding: 0`.
$css = File::get(__DIR__.'/../../resources/dist/error-page.css')
.File::get(__DIR__.'/../../resources/dist/error-page-fallback.css');
// Served as its own response, not inlined through Blade::render(): CSS's own `@media`,
// `@layer` and `@import` would otherwise reach Blade's directive compiler along with the page.
+3 -6
View File
@@ -68,14 +68,12 @@ function chipProbe()
</html>
BLADE));
return visit('/chip-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/chip-probe'));
}
function chipShowcase()
{
return visit('/material/chips')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/material/chips'));
}
/**
@@ -234,8 +232,7 @@ function chipScrollProbe(string $dir)
</html>
BLADE));
return visit("/chip-scroll-probe?dir={$dir}")->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
return ready(visit("/chip-scroll-probe?dir={$dir}"), livewire: false);
}
it('shows a scroll button only on the edge the row can still scroll towards, in either direction', function (string $dir) {
+1 -2
View File
@@ -39,8 +39,7 @@ function pagePrimary(): string
function profilesReady(mixed $page): mixed
{
return $page->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready($page);
}
it('draws each profile in light and dark, and the default without the attribute', function () {
+17 -32
View File
@@ -39,14 +39,13 @@ class RichTooltipMorphProbe extends Component
}
}
// The live region is the host itself, in the page before any message is (actions.md ACT-02).
// The live region is the host itself, in the page before any message is.
const SNACKBAR = "document.querySelector('[x-data=\"materialSnackbar\"][aria-live]')";
const TOAST = "document.querySelector('[data-md-toast-snackbar]')";
it('shows a toast as a snackbar, then the next in turn', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
$page->script("materialToast('First', { type: 'success', timeout: 600 }); materialToast('Second', { type: 'error' })");
@@ -60,8 +59,7 @@ it('shows a toast as a snackbar, then the next in turn', function () {
});
it('shows a toast dispatched as a browser event, as the Toasts concern does', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
// Through Livewire, in the page's own realm: an event built inside Playwright's evaluate
// carries a detail object Firefox's sandbox will not let the page read.
@@ -71,8 +69,7 @@ it('shows a toast dispatched as a browser event, as the Toasts concern does', fu
});
it('runs a toast\'s action and dismisses it', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
$page->script("window.__undone = false; materialToast('Share deleted', { action: { label: 'Undo', handler: () => window.__undone = true } })");
@@ -83,8 +80,7 @@ it('runs a toast\'s action and dismisses it', function () {
});
it('does not let a toast dismissed early cut the next one short', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
$page->script("window.eval(\"materialToast('Share deleted', { timeout: 800, action: { label: 'Undo' } }); materialToast('Link copied', { timeout: 2500 })\")");
@@ -99,8 +95,7 @@ it('does not let a toast dismissed early cut the next one short', function () {
});
it('keeps a sticky toast aside while a passing one shows, brings back the newest, and lets it go once dismissed', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
// As an application dispatches it; built in the page's realm (see above).
$page->script("window.eval(\"window.dispatchEvent(new CustomEvent('toast', { detail: { type: 'info', title: 'A new version is ready', sticky: true, timeout: 300, action: { label: 'Reload', event: 'material:test-reload' } } }))\")");
@@ -130,8 +125,7 @@ it('keeps a sticky toast aside while a passing one shows, brings back the newest
});
it('dispatches a toast action\'s window event alongside its handler, and closes', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
$page->script("window.eval(\"window.reloads = 0; window.handled = 0; window.addEventListener('material:test-reload', () => window.reloads++); materialToast('A new version is ready', { sticky: true, action: { label: 'Reload', event: 'material:test-reload', handler: () => window.handled++ } })\")");
@@ -142,10 +136,9 @@ it('dispatches a toast action\'s window event alongside its handler, and closes'
});
it('keeps an actioned snackbar on screen past the default timeout, until it is acted on', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
// No timeout written out: M3 forbids an actioned snackbar from auto-dismissing (ACT-03).
// No timeout written out: M3 forbids an actioned snackbar from auto-dismissing.
$page->script("materialToast('Share deleted', { action: { label: 'Undo', handler: () => {} } })");
$page->assertScript(TOAST."?.textContent.includes('Share deleted')")
@@ -157,8 +150,7 @@ it('keeps an actioned snackbar on screen past the default timeout, until it is a
});
it('dismisses a focused snackbar with Escape', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
$page->script("materialToast('Share deleted', { action: { label: 'Undo', handler: () => {} } })");
@@ -171,8 +163,7 @@ it('dismisses a focused snackbar with Escape', function () {
});
it('keeps the snackbar\'s live region in the page, polite, before any message arrives', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
$page->assertScript(SNACKBAR.' !== null')
->assertScript(SNACKBAR."?.getAttribute('aria-live') === 'polite'")
@@ -181,8 +172,7 @@ it('keeps the snackbar\'s live region in the page, polite, before any message ar
});
it('grows to M3\'s two-line height with a description, and Alt+G reaches its action from anywhere on the page', function () {
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
$page->script("materialToast('Upload paused', { description: 'The connection dropped at 64%.', action: { label: 'Retry now', handler: () => {} } })");
@@ -198,8 +188,7 @@ it('grows to M3\'s two-line height with a description, and Alt+G reaches its act
});
it('keeps a two-line snackbar\'s action on the title\'s row from 600px, wrapping under it only below that', function () {
$page = visit('/material/communication')->resize(400, 800)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication')->resize(400, 800));
$page->script("materialToast('Upload paused', { description: 'The connection dropped at 64%.', action: { label: 'Retry now', handler: () => {} } })");
@@ -220,8 +209,7 @@ it('keeps a two-line snackbar\'s action on the title\'s row from 600px, wrapping
it('opens a persistent rich tooltip on press', function () {
$bubble = "document.querySelector('#communication [role=\"dialog\"][popover]')";
visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'")
ready(visit('/material/communication'))
->click('#communication button:has-text("Press for details")')
->assertScript("{$bubble}.matches(':popover-open')");
});
@@ -230,8 +218,7 @@ it('closes a transient rich tooltip 1.5s after focus leaves, not at once', funct
$trigger = '#communication button[aria-label="About expiry"]';
$bubble = "document.querySelector('#communication [role=\"tooltip\"][popover]')";
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
// A key press first, so the browser is in keyboard modality, as a keyboard user would be.
$page->keys('#communication', 'Tab');
@@ -272,8 +259,7 @@ it('keeps a rich tooltip open while its action renders the component, and opens
$persistent = "document.querySelector('[role=\"dialog\"][popover]')";
$transient = "document.querySelector('[role=\"tooltip\"][popover]')";
$page = visit('/rich-tooltip-morph-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'")
$page = ready(visit('/rich-tooltip-morph-probe'))
->assertNoJavaScriptErrors();
$page->click('@details')
@@ -300,8 +286,7 @@ it('keeps a rich tooltip open while its action renders the component, and opens
it('fades a persistent rich tooltip out when a second press closes it', function () {
$bubble = "document.querySelector('#communication [role=\"dialog\"][popover]')";
$page = visit('/material/communication')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/communication'));
// Slow tokens, so the exit copy (resources/js/popover-exit.js) is still on screen after the round trip.
$page->script("document.head.insertAdjacentHTML('beforeend', '<style>:root { --md-sys-motion-effects-fast-duration: 1500ms; }</style>')");
+60 -103
View File
@@ -104,8 +104,7 @@ function collapseProbe()
</html>
BLADE));
return visit('/collapse-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/collapse-probe'));
}
function overlayProbe()
@@ -127,14 +126,12 @@ function overlayProbe()
</html>
BLADE));
return visit('/overlay-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/overlay-probe'));
}
function containment()
{
return visit('/material/containment')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/material/containment'));
}
it('writes back what closed means: false for a flag, null for an id', function () {
@@ -265,8 +262,7 @@ function nestedDialogProbe()
</html>
BLADE));
return visit('/nested-dialog-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/nested-dialog-probe'));
}
it('marks a scrolling dialog\'s divider at the edge with more content, and a nested dialog keeps its own marks', function () {
@@ -356,8 +352,7 @@ it('opens a dialog with no fade under reduced motion, but a real one without it'
// 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'")
$reduced = ready(visit('/material/containment', ['reducedMotion' => 'reduce']))
->assertScript("window.matchMedia('(prefers-reduced-motion: reduce)').matches");
$reduced->click('#containment button:has-text("Basic dialog")');
@@ -403,8 +398,7 @@ function textOnlyDialogProbe()
</html>
BLADE));
return visit('/text-only-dialog-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/text-only-dialog-probe'));
}
it('gives the scrolling body an inset focus ring when a text-only dialog opens from the keyboard', function () {
@@ -634,8 +628,7 @@ function layersProbe(): mixed
</html>
BLADE));
return visit('/layers-probe')->resize(1000, 800)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
return ready(visit('/layers-probe')->resize(1000, 800), livewire: false);
}
function sheetOpen(string $id): string
@@ -963,12 +956,39 @@ it('binds a collapse to an Alpine property both ways', function () {
->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]')";
/**
* The side sheet and the bottom sheet close the same way — a fade on the scrim, a slide on the
* sheet itself — differing only in the selector each renders under, the click that opens it, the
* scrim's own hook attribute, the dimension it slides across, the `translate` component that
* carries the offset (a single value for the side sheet's horizontal slide, the second of a
* `translate: x y` pair for the bottom sheet's vertical one) and whether that offset is read
* signed: the side sheet's can slide either way (a right-to-left page), so its mid-slide sample
* takes `Math.abs()`; the bottom sheet only ever slides down, so its own stays signed.
*/
dataset('containment sheets', [
'side sheet' => [
"document.querySelector('#containment aside[role=\"dialog\"]')",
'#containment button:has-text("Side sheet")',
'data-md-drawer-scrim',
'width',
'',
true,
],
'bottom sheet' => [
"document.querySelector('#containment section[role=\"dialog\"]')",
'#containment button:has(> span:text-is("Bottom sheet"))',
'data-md-bottom-sheet-scrim',
'height',
".split(' ')[1]",
false,
],
]);
it('fades the sheet\'s scrim out on close, rather than making it vanish', function (string $sheet, string $trigger, string $scrimAttr) {
$scrim = "{$sheet}.parentElement.querySelector(':scope > [{$scrimAttr}]')";
$page = containment()
->click('#containment button:has-text("Side sheet")')
->click($trigger)
->assertScript("getComputedStyle({$sheet}).display !== 'none'")
->assertScript("getComputedStyle({$scrim}).opacity === '1'");
@@ -994,57 +1014,31 @@ it('fades the side sheet\'s scrim out on close, rather than making it vanish', f
expect($midFade)->toBeTrue();
$page->assertScript("getComputedStyle({$scrim}).display === 'none'");
});
})->with('containment sheets');
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]')";
it('slides the sheet out on close, rather than making it vanish', function (string $sheet, string $trigger, string $scrimAttr, string $dimension, string $axis, bool $absolute) {
$scrim = "{$sheet}.parentElement.querySelector(':scope > [{$scrimAttr}]')";
$offset = "(parseFloat(getComputedStyle({$sheet}).translate{$axis}) || 0)";
$sample = $absolute ? "Math.abs(parseFloat(style.translate{$axis}) || 0)" : "(parseFloat(style.translate{$axis}) || 0)";
$page = containment()
->click('#containment button:has(> span:text-is("Bottom sheet"))')
->click($trigger)
->assertScript("getComputedStyle({$sheet}).display !== 'none'")
->assertScript("getComputedStyle({$scrim}).opacity === '1'");
$midFade = $page->script(<<<JS
(async () => {
{$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 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("Math.abs(parseFloat(getComputedStyle({$sheet}).translate)) < 0.5");
->assertScript("Math.abs({$offset}) < 0.5");
// Sampled in the page, in the same round trip as the close, as the scrim's fade is above: the
// sheet must be caught still displayed and part of the way to its closed offset (one sheet
// width past its edge), not gone at once — which is what Firefox showed while the exit leaned
// on `allow-discrete` holding `display`.
// sheet must be caught still displayed and part of the way to its closed offset (one sheet's
// own size past its edge), not gone at once — which is what Firefox showed while the exit
// leaned on `allow-discrete` holding `display`.
$midSlide = $page->script(<<<JS
(async () => {
const width = {$sheet}.getBoundingClientRect().width
const size = {$sheet}.getBoundingClientRect().{$dimension}
{$scrim}.click()
for (let i = 0; i < 60; i++) {
const style = getComputedStyle({$sheet})
const offset = Math.abs(parseFloat(style.translate) || 0)
if (style.display !== 'none' && offset > 1 && offset < width - 1) return true
const offset = {$sample}
if (style.display !== 'none' && offset > 1 && offset < size - 1) return true
await new Promise((resolve) => setTimeout(resolve, 5))
}
return false
@@ -1054,36 +1048,7 @@ it('slides the side sheet out on close, rather than making it vanish', function
expect($midSlide)->toBeTrue();
$page->assertScript("getComputedStyle({$sheet}).display === 'none'");
});
it('slides the bottom sheet down 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]')";
$offset = "(parseFloat(getComputedStyle({$sheet}).translate.split(' ')[1]) || 0)";
$page = containment()
->click('#containment button:has(> span:text-is("Bottom sheet"))')
->assertScript("getComputedStyle({$sheet}).display !== 'none'")
->assertScript("Math.abs({$offset}) < 0.5");
$midSlide = $page->script(<<<JS
(async () => {
const height = {$sheet}.getBoundingClientRect().height
{$scrim}.click()
for (let i = 0; i < 60; i++) {
const offset = {$offset}
if (getComputedStyle({$sheet}).display !== 'none' && offset > 1 && offset < height - 1) return true
await new Promise((resolve) => setTimeout(resolve, 5))
}
return false
})()
JS);
expect($midSlide)->toBeTrue();
$page->assertScript("getComputedStyle({$sheet}).display === 'none'");
});
})->with('containment sheets');
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'
@@ -1104,8 +1069,7 @@ it('slides the side sheet in from its own edge in a right-to-left page', functio
</html>
BLADE));
$page = visit('/drawer-rtl-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
$page = ready(visit('/drawer-rtl-probe'), livewire: false);
$sheet = "document.querySelector('aside[data-md-side]')";
@@ -1171,8 +1135,7 @@ it('keeps a basic dialog\'s own title and padding when it is nested inside a ful
</html>
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 = ready(visit('/nested-fullscreen-dialog-probe')->resize(400, 800));
$page->click('button:has-text("Open settings")');
@@ -1234,8 +1197,7 @@ it('keeps a selected segmented row\'s fill under the hover and focus tint', func
</html>
BLADE));
$page = visit('/segmented-selected-row-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
$page = ready(visit('/segmented-selected-row-probe'), livewire: false);
$selected = "document.querySelector('[data-md-selected]')";
$plain = "document.querySelector('[data-md-list-item]:not([data-md-selected])')";
@@ -1301,8 +1263,7 @@ it('fills a selected row written by hand in secondary-container in both themes,
</html>
BLADE));
$page = visit('/hand-made-selected-row-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
$page = ready(visit('/hand-made-selected-row-probe'), livewire: false);
$swatch = settledPaint("document.getElementById('swatch')");
$row = settledPaint("document.getElementById('selected-row')");
@@ -1376,8 +1337,7 @@ function collapseMotionProbe(string $durations = ''): mixed
</html>
BLADE));
$page = visit('/collapse-motion-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
$page = ready(visit('/collapse-motion-probe'), livewire: false);
if ($durations !== '') {
$page->script("document.head.insertAdjacentHTML('beforeend', '<style>:root { --md-sys-motion-spatial-fast-duration: {$durations}; }</style>')");
@@ -1658,8 +1618,7 @@ it('shows no tooltip on the control a sheet or a dialog focuses as it opens, but
// A phone, where the sheet is modal and traps the focus, which it moves to its ✕ on open: here
// on a page loaded with the sheet open, as a link to a session opens it.
$page = visit('/opened-layers-probe?workout=7', ['viewport' => ['width' => 393, 'height' => 800]])->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'")
$page = ready(visit('/opened-layers-probe?workout=7', ['viewport' => ['width' => 393, 'height' => 800]]), livewire: false)
->assertScript("document.activeElement.matches('{$sheet} button[aria-label=\"Close\"]')")
->wait(0.3)
->assertScript('! '.closeTooltipOpen($sheet));
@@ -1670,8 +1629,7 @@ it('shows no tooltip on the control a sheet or a dialog focuses as it opens, but
->assertScript(closeTooltipOpen($sheet));
// A full-screen dialog opened from the keyboard moves the focus to its bar's ✕ in showModal().
$page = visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]])->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]]), alpine: false);
$page->keys('#open-setback', 'Enter')
->assertScript("document.querySelector('{$dialog}').open && document.activeElement.matches('{$dialog} button[aria-label=\"Close\"]')")
@@ -1683,8 +1641,7 @@ it('shows no tooltip on the control a sheet or a dialog focuses as it opens, but
->assertScript(closeTooltipOpen($dialog));
// A rich tooltip's trigger as the first control of a dialog opened from the keyboard.
$page = visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]])->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]]), alpine: false);
$page->keys('#open-about', 'Enter')
->assertScript("document.activeElement.id === 'about-help'")
+2 -4
View File
@@ -76,8 +76,7 @@ function dataProbe()
</html>
BLADE));
return visit('/data-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/data-probe'));
}
it('sorts by a column and flips the direction on a second press', function () {
@@ -149,8 +148,7 @@ it('shows a page step\'s keyboard focus ring, from md-focus-ring', function () {
});
it('keeps a selected table row\'s fill under the hover and focus tint', function () {
$page = visit('/material/data')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/data'));
$selected = "document.querySelector('#data tr[data-md-list-row][aria-selected=\"true\"]')";
$plain = "document.querySelector('#data tr[data-md-list-row]:not([aria-selected=\"true\"])')";
+3 -6
View File
@@ -65,8 +65,7 @@ function dateProbe(string $locale = 'en')
BLADE);
});
return visit("/date-probe/{$locale}")->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit("/date-probe/{$locale}"));
}
class DateFormatProbe extends Component
@@ -121,8 +120,7 @@ function dateFormatProbe(string $locale = 'en')
BLADE);
});
return visit("/date-format-probe/{$locale}")->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit("/date-format-probe/{$locale}"));
}
class ScopedDateProbe extends Component
@@ -161,8 +159,7 @@ function scopedDateProbe()
</html>
BLADE));
return visit('/scoped-date-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/scoped-date-probe'));
}
/** A picker's day cell, by its ISO date. */
+7 -5
View File
@@ -111,11 +111,13 @@ it('draws the layout from its own inlined rules under a build that never imports
File::ensureDirectoryExists($public.'/build/assets');
// The foundation and the scheme, as an application's bundle would carry them; without the font
// face, whose relative url would 404 from here and is no part of what this test proves.
$css = Stylesheets::bundle([
realpath(__DIR__.'/../../resources/css/foundation.css'),
workbench_path('resources/css/material-scheme.css'),
]);
// face, whose relative url would 404 from here and is no part of what this test proves. The
// foundation's files hold either imports or rules (base.css's one import is the font face), so
// each file's rules in the order the imports reach them are the bundle.
$css = collect(Stylesheets::resolvedFiles([__DIR__.'/../../resources/css/foundation.css']))
->map(fn (string $file): string => (string) preg_replace('/^@import [^;]+;/m', '', File::get($file)))
->push(File::get(workbench_path('resources/css/material-scheme.css')))
->implode("\n");
File::put($public.'/build/assets/app-probe.css', (string) preg_replace('/@font-face\s*\{[^}]*\}/', '', $css));
File::put($public.'/build/manifest.json', json_encode([
'resources/css/app.css' => ['file' => 'assets/app-probe.css', 'src' => 'resources/css/app.css', 'isEntry' => true],
+4 -8
View File
@@ -85,8 +85,7 @@ function fieldProbe()
</html>
BLADE));
return visit('/field-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/field-probe'));
}
it('floats the label while the field has focus or holds a value', function () {
@@ -231,8 +230,7 @@ it('ends a select\'s value before its arrow with an ellipsis, at every size and
</html>
BLADE));
$page = visit('/select-value-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/select-value-probe'));
foreach (['format-md', 'format-sm', 'format-xs', 'format-filled', 'format-icon'] as $id) {
expect($page->script(selectValue($id)))->toBe([true, 'ellipsis', 'nowrap', 'hidden', 'Day first, dots — 17.08.2026']);
@@ -359,8 +357,7 @@ function fieldStateProbe(): mixed
BLADE);
});
return visit('/field-state-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
return ready(visit('/field-state-probe'), livewire: false);
}
/** A field's edge, "width colour", once its colour transition has run: `Top` outlined, `Bottom` filled. */
@@ -558,8 +555,7 @@ it('keeps a switch\'s track and a checkbox\'s box whole beside long text in a ro
})()";
// A phone's window: the row is 329px across, less than the text beside each control would take.
$page = visit('/selection-row-probe')->resize(393, 800)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete'");
$page = ready(visit('/selection-row-probe')->resize(393, 800), alpine: false, livewire: false);
expect($page->script($control('row-bare-switch', '[data-md-switch]', '[data-md-toggle]')))->toBe([52, true, true])
->and($page->script($control('row-labelled-switch', '[data-md-switch]', '[data-md-toggle]')))->toBe([52, true, true])
+1 -2
View File
@@ -36,8 +36,7 @@ function layoutPage(string $body, int $width, int $height = 900, string $dir = '
function layoutReady(mixed $page): mixed
{
return $page->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready($page);
}
/** A computed style of the element matching the selector. */
+1 -2
View File
@@ -9,8 +9,7 @@ const BOTTOM_BAR = "document.querySelector('[data-md-scaffold-bar]')";
function navigationReady(mixed $page): mixed
{
return $page->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready($page);
}
function shellPage(int $width = 1280, int $height = 900, string $path = '/material/shell'): mixed
+4 -18
View File
@@ -75,8 +75,7 @@ function pickProbe()
</html>
BLADE));
return visit('/pick-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/pick-probe'));
}
it('keeps the values of filter chips as the property\'s own type', function () {
@@ -161,8 +160,7 @@ it('hangs a searchable choice\'s list under its field and as wide, bounded at 40
return [Math.round(field.width), Math.round(list.width), Math.round(list.left - field.left)];
})()";
$page = visit('/wide-choices-probe')->resize(1280, 800)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined'");
$page = ready(visit('/wide-choices-probe')->resize(1280, 800), livewire: false);
$page->click('#bounded-zone')->assertScript("document.getElementById('bounded-zone-list').matches(':popover-open')");
@@ -177,7 +175,7 @@ it('hangs a searchable choice\'s list under its field and as wide, bounded at 40
it('opens the search view with the results Livewire renders for the query', function () {
$view = "document.querySelector('#find-view')";
// The combobox is the wrapper around the input, which carries `aria-expanded` (inputs.md IN-01).
// The combobox is the wrapper around the input, which carries `aria-expanded`.
$page = pickProbe()
->click('#find')
->assertScript("getComputedStyle({$view}).display !== 'none'")
@@ -204,7 +202,7 @@ it('says so when nothing matches, and closes on a press outside or a chosen resu
->type('#find', 'zzz')
->assertSeeIn('#find-view', 'No files match.');
// The docked view opens over a scrim that covers the rest of the page (inputs.md IN-09), so a
// The docked view opens over a scrim that covers the rest of the page, so a
// press outside the view lands on the scrim.
$page->click('[data-md-search]:has(#find) [data-md-search-scrim]')
->assertScript("getComputedStyle({$view}).display === 'none'");
@@ -266,18 +264,6 @@ it('keeps the docked search above the page while its view closes', function () {
$page->assertScript("getComputedStyle({$view}).display === 'none' && getComputedStyle(document.querySelector('[data-md-search]:has(#find)')).zIndex !== '50'");
});
it('closes the docked search view back into its bar, rather than making it vanish', function () {
$view = "document.querySelector('#find-view')";
$page = pickProbe()
->click('#find')
->assertScript("getComputedStyle({$view}).display !== 'none' && getComputedStyle({$view}).opacity === '1'");
expect($page->script(searchCloseSample('[data-md-search-view]', 'parseFloat(getComputedStyle(element).opacity)')))->toBeTrue();
$page->assertScript("getComputedStyle({$view}).display === 'none'");
});
it('takes the whole screen on a compact window, with a back arrow', function () {
$page = pickProbe()->resize(400, 800);
+1 -2
View File
@@ -55,8 +55,7 @@ function progressShowcase(array $options = [])
{
// networkidle alone can return before a repeated visit has even loaded in Firefox; the
// assertion retries until the page is complete and Alpine has started.
return visit('/material/progress', $options)->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/material/progress', $options));
}
it('draws a linear indicator to its value', function () {
+4 -8
View File
@@ -7,8 +7,7 @@ it('opens the showcase in a real browser', function () {
});
it('moves between sections through the rail and the next-section link, keeping the rail', function () {
$page = visit('/material')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material'));
$page->click('[data-md-navigation-rail-panel] a[href$="/material/buttons"]')
->assertSee('Variants')
@@ -21,8 +20,7 @@ it('moves between sections through the rail and the next-section link, keeping t
});
it('finds a component from anywhere and opens its section', function () {
$page = visit('/material/buttons')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material/buttons'));
$page->keys('#content', '/')
->assertScript("document.activeElement.id === 'showcase-search'");
@@ -36,8 +34,7 @@ it('finds a component from anywhere and opens its section', function () {
});
it('opens an example on another page at the example', function () {
$page = visit('/material')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
$page = ready(visit('/material'));
$page->click('#showcase-search')
->type('#showcase-search', 'split button')
@@ -47,8 +44,7 @@ it('opens an example on another page at the example', function () {
});
it('says so when nothing matches', function () {
visit('/material')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'")
ready(visit('/material'))
->click('#showcase-search')
->type('#showcase-search', 'zzzz')
->assertSeeIn('[data-md-search-view]', 'Nothing matches');
+2 -4
View File
@@ -53,14 +53,12 @@ function sliderProbe()
</html>
BLADE));
return visit('/slider-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/slider-probe'));
}
function sliderShowcase()
{
return visit('/material/sliders')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/material/sliders'));
}
/**
+2 -5
View File
@@ -26,9 +26,7 @@ it('follows the operating system while the visitor has not chosen', function ()
it('keeps the visitor\'s choice over the operating system', function () {
// The theme switch waits for Alpine (x-cloak); a press before then would find nothing to press.
$page = visit('/material')->inDarkMode()
->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'")
$page = ready(visit('/material')->inDarkMode())
->assertScript(theme('data-theme', 'dark'));
$page->click('label:has(input[name="material-theme"][value="light"])')
@@ -81,8 +79,7 @@ function pageSurfaceIs(string $hex): string
function themeReady(mixed $page): mixed
{
return $page->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready($page);
}
it('adds a theme-color meta in the painted surface, and follows the theme and the colour profile', function () {
+1 -2
View File
@@ -56,8 +56,7 @@ function timeProbe()
</html>
BLADE));
return visit('/time-probe')->waitForEvent('networkidle')
->assertScript("document.readyState === 'complete' && typeof window.Alpine !== 'undefined' && typeof window.Livewire !== 'undefined'");
return ready(visit('/time-probe'));
}
/** A JavaScript expression for an element inside the named picker's dialog. */