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:
co-authored by
Claude Opus 5
parent
471d927e64
commit
247c596c3a
@@ -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 () {
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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>')");
|
||||
|
||||
@@ -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'")
|
||||
|
||||
@@ -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\"])')";
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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. */
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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 () {
|
||||
|
||||
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user