Compare commits
8
Commits
fb7007c976
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a7aeb3c4ec | ||
|
|
ebdc2ef2e1 | ||
|
|
ad230e0f48 | ||
|
|
e7885ce1eb | ||
|
|
8da8b1fa20 | ||
|
|
1b38b39a47 | ||
|
|
94d6e31432 | ||
|
|
145f60aa11 |
@@ -14,11 +14,6 @@ jobs:
|
|||||||
feature:
|
feature:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
php: ['8.4', '8.5']
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||||
@@ -28,7 +23,7 @@ jobs:
|
|||||||
- name: Setup PHP
|
- name: Setup PHP
|
||||||
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
uses: shivammathur/setup-php@f3e473d116dcccaddc5834248c87452386958240 # v2
|
||||||
with:
|
with:
|
||||||
php-version: ${{ matrix.php }}
|
php-version: '8.5'
|
||||||
tools: composer:v2
|
tools: composer:v2
|
||||||
coverage: none
|
coverage: none
|
||||||
|
|
||||||
@@ -45,7 +40,6 @@ jobs:
|
|||||||
npm ci
|
npm ci
|
||||||
|
|
||||||
- name: Check formatting
|
- name: Check formatting
|
||||||
if: matrix.php == '8.5'
|
|
||||||
run: vendor/bin/pint --test
|
run: vendor/bin/pint --test
|
||||||
|
|
||||||
- name: Run feature tests
|
- name: Run feature tests
|
||||||
@@ -101,4 +95,5 @@ jobs:
|
|||||||
- name: Run browser tests
|
- name: Run browser tests
|
||||||
run: vendor/bin/pest --testsuite=Browser --browser ${{ matrix.browser }}
|
run: vendor/bin/pest --testsuite=Browser --browser ${{ matrix.browser }}
|
||||||
env:
|
env:
|
||||||
BROWSER_TIMEOUT: 15000
|
BROWSER_TIMEOUT: 45000
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ No JavaScript libraries beyond the Alpine that ships with Livewire. Browsers: Ch
|
|||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
PHP 8.4+, Laravel 13, Livewire 4, Vite, and Node (for `material:scheme`).
|
PHP 8.5+, Laravel 13, Livewire 4, Vite, and Node (for `material:scheme`).
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
# Upgrading
|
# Upgrading
|
||||||
|
|
||||||
|
## From 2.2.0 to 2.3.0
|
||||||
|
|
||||||
|
- **PHP 8.5 is required.** Composer refuses to install the package on 8.4. Nothing in the package's
|
||||||
|
code needs 8.5 — 2.2.0 runs on 8.4 and stays available for an application still on it — but the
|
||||||
|
package is now built and tested against one runtime rather than two.
|
||||||
|
|
||||||
## From 2.1.0 to 2.2.0
|
## From 2.1.0 to 2.2.0
|
||||||
|
|
||||||
- **Run `php artisan view:clear` after upgrading.** The components are no longer registered under a
|
- **Run `php artisan view:clear` after upgrading.** The components are no longer registered under a
|
||||||
|
|||||||
+1
-1
@@ -20,7 +20,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.4",
|
"php": "^8.5",
|
||||||
"laravel/framework": "^13.0",
|
"laravel/framework": "^13.0",
|
||||||
"livewire/livewire": "^4.0"
|
"livewire/livewire": "^4.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -189,11 +189,16 @@
|
|||||||
padding: 0 var(--md-sys-measurement-space300) var(--md-sys-measurement-space200);
|
padding: 0 var(--md-sys-measurement-space300) var(--md-sys-measurement-space200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The calendar is the only part that scrolls, with the weekday header held at its top. */
|
/* The calendar is the only part that scrolls, with the weekday header held at its top. It
|
||||||
|
opts out of the browser's scroll anchoring: when months are added above, extendMonths()
|
||||||
|
(resources/js/datepicker.js) puts the scroll back itself, in every engine alike, and an
|
||||||
|
engine that anchors as well moved it back a second time — six months on from where the
|
||||||
|
reader was. */
|
||||||
[data-md-datepicker-picker][data-md-presentation='full'] [data-md-datepicker-calendar] {
|
[data-md-datepicker-picker][data-md-presentation='full'] [data-md-datepicker-calendar] {
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-anchor: none;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Vendored
+1
@@ -8060,6 +8060,7 @@
|
|||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
overflow-anchor: none;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -625,7 +625,10 @@ document.addEventListener('alpine:init', () => {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Grows the window when the scroll comes near either end. Adding months above moves
|
* Grows the window when the scroll comes near either end. Adding months above moves
|
||||||
* everything down, so the scroll is put back by however much the list grew.
|
* everything down, so the scroll is put back by however much the list grew — here, and
|
||||||
|
* only here: the list opts out of the browser's scroll anchoring (datepicker.css), which
|
||||||
|
* Chrome, Firefox and WebKit would otherwise apply on top of this, and a Safari without
|
||||||
|
* `overflow-anchor` would not apply at all.
|
||||||
*/
|
*/
|
||||||
extendMonths(event) {
|
extendMonths(event) {
|
||||||
if (this.presentation !== 'full' || this.monthsFrom === null) {
|
if (this.presentation !== 'full' || this.monthsFrom === null) {
|
||||||
|
|||||||
+36
-26
@@ -24,19 +24,15 @@
|
|||||||
* transition has run, while the header bar fades out with it (search.css). Without it the bar went
|
* transition has run, while the header bar fades out with it (search.css). Without it the bar went
|
||||||
* back to its resting form, or to nothing behind the icon, on the first frame of the exit, and the
|
* back to its resting form, or to nothing behind the icon, on the first frame of the exit, and the
|
||||||
* fading view dropped to the docked layout. The focus trap lets go at the close itself, not at the
|
* fading view dropped to the docked layout. The focus trap lets go at the close itself, not at the
|
||||||
* end of the exit, so its return of focus still lands inside RETURN_GUARD_MS.
|
* end of the exit, so `returning` (close()) covers its return of focus like the field's own.
|
||||||
*/
|
*/
|
||||||
import { upTo } from './breakpoints.js'
|
import { upTo } from './breakpoints.js'
|
||||||
|
import { ms } from './util.js'
|
||||||
|
|
||||||
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
const FOCUSABLE = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
|
||||||
|
|
||||||
const CHOOSES = 'a[href], button:not([disabled]), [data-md-list-open]'
|
const CHOOSES = 'a[href], button:not([disabled]), [data-md-list-open]'
|
||||||
|
|
||||||
// A close hands focus back to the input: Escape does, and so does the full-screen view's focus trap
|
|
||||||
// when it lets go, a moment later. Focus arriving this soon after a close is that, not someone
|
|
||||||
// coming to search.
|
|
||||||
const RETURN_GUARD_MS = 250
|
|
||||||
|
|
||||||
// A Livewire morph replaces the results in several mutations; wait for the batch to end before
|
// A Livewire morph replaces the results in several mutations; wait for the batch to end before
|
||||||
// counting, so the live region speaks once.
|
// counting, so the live region speaks once.
|
||||||
const SETTLE_MS = 120
|
const SETTLE_MS = 120
|
||||||
@@ -48,7 +44,8 @@ document.addEventListener('alpine:init', () => {
|
|||||||
leaving: false,
|
leaving: false,
|
||||||
leavings: 0,
|
leavings: 0,
|
||||||
compact: false,
|
compact: false,
|
||||||
closedAt: -Infinity,
|
// A close is handing focus back; see close().
|
||||||
|
returning: false,
|
||||||
announcement: '',
|
announcement: '',
|
||||||
// What is in the field, which is what tells suggestions from results.
|
// What is in the field, which is what tells suggestions from results.
|
||||||
query: '',
|
query: '',
|
||||||
@@ -126,51 +123,64 @@ document.addEventListener('alpine:init', () => {
|
|||||||
},
|
},
|
||||||
|
|
||||||
focused() {
|
focused() {
|
||||||
if (performance.now() - this.closedAt > RETURN_GUARD_MS) {
|
if (!this.returning) {
|
||||||
this.show()
|
this.show()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `refocus` puts focus back where the view came from: the icon button, or the field itself.
|
||||||
|
*
|
||||||
|
* Either way a close hands focus back — the full-screen view's focus trap returns it as it
|
||||||
|
* lets go, whether this asked for it or not — and that focus must not read as someone
|
||||||
|
* coming to search, or the view would open again on its way out. `returning` covers it
|
||||||
|
* until the hand-back has run, rather than for a fixed stretch of wall-clock time after
|
||||||
|
* the close: both returns are scheduled on frames, and a runner painting a handful of
|
||||||
|
* frames a second takes longer over one than any such guard would allow, which left the
|
||||||
|
* view open for good.
|
||||||
|
*/
|
||||||
close(refocus = false) {
|
close(refocus = false) {
|
||||||
const fromFullScreen = this.open && this.fullScreen
|
const fromFullScreen = this.open && this.fullScreen
|
||||||
|
const back = refocus ? (this.$refs.trigger ?? this.$refs.input) : null
|
||||||
|
|
||||||
this.open = false
|
this.open = false
|
||||||
this.closedAt = performance.now()
|
this.returning = true
|
||||||
|
|
||||||
if (fromFullScreen) {
|
if (fromFullScreen) {
|
||||||
this.hold()
|
this.hold()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (refocus) {
|
// A frame after the tick, as `expand()` waits: the icon button is `x-show`n, and Alpine
|
||||||
// Back to whatever opened the view: the icon button, or the field itself. A frame
|
// only shows it on that frame, so a focus in the tick reaches a hidden button, which
|
||||||
// after the tick, as `expand()` waits: the icon button is `x-show`n, and Alpine only
|
|
||||||
// shows it on that frame, so a focus in the tick reaches a hidden button, which
|
|
||||||
// Firefox and WebKit refuse (in Chrome the trap's own return had covered for it).
|
// Firefox and WebKit refuse (in Chrome the trap's own return had covered for it).
|
||||||
const back = this.$refs.trigger ?? this.$refs.input
|
this.$nextTick(() => requestAnimationFrame(() => {
|
||||||
|
back?.focus()
|
||||||
this.$nextTick(() => requestAnimationFrame(() => back.focus()))
|
this.returning = false
|
||||||
}
|
}))
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Keeps the full-screen layout for the length of the view's exit: until the animations the
|
* Keeps the full-screen layout for the length of the view's exit — the duration search.css
|
||||||
* closed state starts, a frame on, have finished (at once when none run); reopening, which
|
* gives it, which is also the one Alpine's `x-transition` holds its `display` for, so the
|
||||||
* counts `leavings` up, lets a pending end go by.
|
* two end together. Reopening, which counts `leavings` up, lets a pending end go by, and
|
||||||
|
* reduced motion zeroes the token, which ends the hold on the next task, as it should.
|
||||||
|
*
|
||||||
|
* The duration rather than the exit's own animations: `getAnimations()` is empty both
|
||||||
|
* before an engine has created the transitions and after they have finished, and nothing
|
||||||
|
* on the element tells the two apart. Waiting a frame or two for them to appear only moves
|
||||||
|
* the guess — a loaded engine can leave a second between two frames, and holding for what
|
||||||
|
* it had not started yet ended the full-screen layout at once, mid-exit.
|
||||||
*/
|
*/
|
||||||
hold() {
|
hold() {
|
||||||
const leaving = ++this.leavings
|
const leaving = ++this.leavings
|
||||||
|
|
||||||
this.leaving = true
|
this.leaving = true
|
||||||
|
|
||||||
requestAnimationFrame(() => {
|
setTimeout(() => {
|
||||||
const animations = this.$refs.view?.getAnimations() ?? []
|
|
||||||
|
|
||||||
Promise.allSettled(animations.map((animation) => animation.finished)).then(() => {
|
|
||||||
if (leaving === this.leavings) {
|
if (leaving === this.leavings) {
|
||||||
this.leaving = false
|
this.leaving = false
|
||||||
}
|
}
|
||||||
})
|
}, ms(this.$refs.view, '--md-sys-motion-spatial-fast-duration') ?? 0)
|
||||||
})
|
|
||||||
},
|
},
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
|
|||||||
@@ -7,6 +7,10 @@
|
|||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||||||
<meta name="robots" content="noindex" />
|
<meta name="robots" content="noindex" />
|
||||||
|
|
||||||
|
{{-- The showcase is the package's own page and ships no icon: an empty data URI keeps the
|
||||||
|
browser from asking the application for /favicon.ico, which its routes need not answer. --}}
|
||||||
|
<link rel="icon" href="data:," />
|
||||||
|
|
||||||
<title>{{ $pageTitle }}</title>
|
<title>{{ $pageTitle }}</title>
|
||||||
|
|
||||||
<x-livewire-material::theme-script />
|
<x-livewire-material::theme-script />
|
||||||
|
|||||||
+217
-126
@@ -111,6 +111,7 @@ function menuMorphProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -136,10 +137,12 @@ function focused(string $expression): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('opens a menu on the first item and walks it with the keyboard', function () {
|
it('opens a menu on the first item and walks it with the keyboard', function () {
|
||||||
$page = showcase('menus')
|
$page = showcase('menus')->assertNoJavaScriptErrors();
|
||||||
->assertNoJavaScriptErrors()
|
|
||||||
->click(MORE)
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'true')
|
pressOnce($page)->click(MORE);
|
||||||
|
|
||||||
|
$page->assertAttribute(MORE, 'aria-expanded', 'true')
|
||||||
->assertScript(focused("textContent.trim().startsWith('Copy link')"));
|
->assertScript(focused("textContent.trim().startsWith('Copy link')"));
|
||||||
|
|
||||||
$page->keys(':focus', ['ArrowDown', 'ArrowDown'])
|
$page->keys(':focus', ['ArrowDown', 'ArrowDown'])
|
||||||
@@ -164,8 +167,10 @@ it('opens a menu from the keyboard, on the last item with ArrowUp', function ()
|
|||||||
|
|
||||||
$page->script("document.querySelector('".MORE."').focus()");
|
$page->script("document.querySelector('".MORE."').focus()");
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowUp')
|
// ArrowUp on the trigger opens the menu, same as clicking it: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'true')
|
pressOnce($page)->keys(':focus', 'ArrowUp');
|
||||||
|
|
||||||
|
$page->assertAttribute(MORE, 'aria-expanded', 'true')
|
||||||
->assertScript(focused("textContent.trim().startsWith('Delete')"));
|
->assertScript(focused("textContent.trim().startsWith('Delete')"));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -173,7 +178,10 @@ it('shows no state layer on a disabled menu item even when keyboard focus reache
|
|||||||
$trigger = '#menus button:has-text("Sort")';
|
$trigger = '#menus button:has-text("Sort")';
|
||||||
$reset = '#menus [role="menuitem"]:has-text("Reset")';
|
$reset = '#menus [role="menuitem"]:has-text("Reset")';
|
||||||
|
|
||||||
$page = showcase('menus')->click($trigger);
|
$page = showcase('menus');
|
||||||
|
|
||||||
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
// M3 keeps a disabled item reachable so a person can find out it is there, but
|
// M3 keeps a disabled item reachable so a person can find out it is there, but
|
||||||
// .md-state-layer withholds the layer itself from [aria-disabled="true"] (interaction.css).
|
// .md-state-layer withholds the layer itself from [aria-disabled="true"] (interaction.css).
|
||||||
@@ -186,21 +194,28 @@ it('shows no state layer on a disabled menu item even when keyboard focus reache
|
|||||||
it('opens a menu the moment the page can be used', function () {
|
it('opens a menu the moment the page can be used', function () {
|
||||||
// The guard against a light-dismiss press reopening the menu once measured from the page's
|
// The guard against a light-dismiss press reopening the menu once measured from the page's
|
||||||
// time origin, and swallowed every click in the first quarter second.
|
// time origin, and swallowed every click in the first quarter second.
|
||||||
visit('/material/menus')
|
$page = visit('/material/menus');
|
||||||
->click(MORE)
|
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'true');
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click(MORE);
|
||||||
|
|
||||||
|
$page->assertAttribute(MORE, 'aria-expanded', 'true');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes a menu when an item is chosen, but not one that keeps it open', function () {
|
it('closes a menu when an item is chosen, but not one that keeps it open', function () {
|
||||||
$page = showcase('menus');
|
$page = showcase('menus');
|
||||||
|
|
||||||
$page->click(MORE)
|
// Opens the menu, then an item closes it: pressOnce(), tests/Pest.php.
|
||||||
->click('#menus [role="menuitem"]:has-text("Download")')
|
pressOnce($page)->click(MORE)
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'false');
|
->click('#menus [role="menuitem"]:has-text("Download")');
|
||||||
|
|
||||||
$page->click('#menus button:has-text("Sort")')
|
$page->assertAttribute(MORE, 'aria-expanded', 'false');
|
||||||
->click('#menus [role="menuitemcheckbox"]:has-text("Largest")')
|
|
||||||
->assertScript("document.querySelector('#menus [role=\"menu\"][aria-label=\"Sort\"]').matches(':popover-open')");
|
// Opens the menu, then a checkbox item toggles: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#menus button:has-text("Sort")')
|
||||||
|
->click('#menus [role="menuitemcheckbox"]:has-text("Largest")');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#menus [role=\"menu\"][aria-label=\"Sort\"]').matches(':popover-open')");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a tooltip on keyboard focus and hides it on Escape', function () {
|
it('shows a tooltip on keyboard focus and hides it on Escape', function () {
|
||||||
@@ -236,9 +251,13 @@ it('moves a connected group\'s choice with the arrow keys', function () {
|
|||||||
it('rounds a split button\'s trailing half while its menu is open', function () {
|
it('rounds a split button\'s trailing half while its menu is open', function () {
|
||||||
$trailing = "document.querySelector('[data-md-split=\"trailing\"]')";
|
$trailing = "document.querySelector('[data-md-split=\"trailing\"]')";
|
||||||
|
|
||||||
showcase()
|
$page = showcase();
|
||||||
->click('[data-md-split="trailing"] >> nth=0')
|
|
||||||
->assertScript("{$trailing}.getAttribute('aria-expanded') === 'true'")
|
// Opens the split button's menu over the trailing half that triggers it: pressOnce(),
|
||||||
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-split="trailing"] >> nth=0');
|
||||||
|
|
||||||
|
$page->assertScript("{$trailing}.getAttribute('aria-expanded') === 'true'")
|
||||||
->assertScript("getComputedStyle({$trailing}).borderTopLeftRadius === ({$trailing}.offsetHeight / 2) + 'px'");
|
->assertScript("getComputedStyle({$trailing}).borderTopLeftRadius === ({$trailing}.offsetHeight / 2) + 'px'");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -254,9 +273,12 @@ it('keeps a connected segment\'s small inner corners, which a 9999px outer corne
|
|||||||
it('turns the FAB into a close button while its menu is open', function () {
|
it('turns the FAB into a close button while its menu is open', function () {
|
||||||
$fab = "document.querySelector('button[aria-label=\"New\"]')";
|
$fab = "document.querySelector('button[aria-label=\"New\"]')";
|
||||||
|
|
||||||
$page = showcase()
|
$page = showcase();
|
||||||
->click('button[aria-label="New"]')
|
|
||||||
->assertScript("{$fab}.getAttribute('aria-expanded') === 'true'")
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button[aria-label="New"]');
|
||||||
|
|
||||||
|
$page->assertScript("{$fab}.getAttribute('aria-expanded') === 'true'")
|
||||||
->assertScript(focused("textContent.trim() === 'Upload files'"));
|
->assertScript(focused("textContent.trim() === 'Upload files'"));
|
||||||
|
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
@@ -292,6 +314,7 @@ it('hangs a menu on its menu button, even when the button is fixed to a corner o
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -308,8 +331,10 @@ it('hangs a menu on its menu button, even when the button is fixed to a corner o
|
|||||||
|
|
||||||
$page = ready(visit('/menu-anchor-probe')->resize(400, 800));
|
$page = ready(visit('/menu-anchor-probe')->resize(400, 800));
|
||||||
|
|
||||||
$page->click('@fab')
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
->assertAttribute('@fab', 'aria-expanded', 'true')
|
pressOnce($page)->click('@fab');
|
||||||
|
|
||||||
|
$page->assertAttribute('@fab', 'aria-expanded', 'true')
|
||||||
->assertScript($above);
|
->assertScript($above);
|
||||||
|
|
||||||
$page->keys(':focus', 'Escape')->assertAttribute('@fab', 'aria-expanded', 'false');
|
$page->keys(':focus', 'Escape')->assertAttribute('@fab', 'aria-expanded', 'false');
|
||||||
@@ -321,14 +346,19 @@ it('hangs a menu on its menu button, even when the button is fixed to a corner o
|
|||||||
$page->assertSeeIn('#renders', '1')
|
$page->assertSeeIn('#renders', '1')
|
||||||
->script("document.querySelector('[data-test=\"fab\"]').focus()");
|
->script("document.querySelector('[data-test=\"fab\"]').focus()");
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowDown')
|
// ArrowDown on the trigger opens the menu, same as clicking it: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute('@fab', 'aria-expanded', 'true')
|
pressOnce($page)->keys(':focus', 'ArrowDown');
|
||||||
|
|
||||||
|
$page->assertAttribute('@fab', 'aria-expanded', 'true')
|
||||||
->assertScript($above);
|
->assertScript($above);
|
||||||
|
|
||||||
// A button that also anchors its own tooltip keeps it, and the menu hangs under the button.
|
// A button that also anchors its own tooltip keeps it, and the menu hangs under the button.
|
||||||
$page->resize(1024, 800)
|
$page->resize(1024, 800);
|
||||||
->click('@more')
|
|
||||||
->assertAttribute('@more', 'aria-expanded', 'true')
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click('@more');
|
||||||
|
|
||||||
|
$page->assertAttribute('@more', 'aria-expanded', 'true')
|
||||||
->assertScript("(() => { const names = getComputedStyle(document.querySelector('[data-test=\"more\"]')).getPropertyValue('anchor-name'); return names.includes('--material-button-') && names.includes('--material-menu-'); })()")
|
->assertScript("(() => { const names = getComputedStyle(document.querySelector('[data-test=\"more\"]')).getPropertyValue('anchor-name'); return names.includes('--material-button-') && names.includes('--material-menu-'); })()")
|
||||||
->assertScript('(({ control, menu }) => menu.top >= control.bottom && menu.top - control.bottom <= 16 && Math.abs(menu.left - control.left) <= 16)('.menuAgainst('more', 'Share actions').')');
|
->assertScript('(({ control, menu }) => menu.top >= control.bottom && menu.top - control.bottom <= 16 && Math.abs(menu.left - control.left) <= 16)('.menuAgainst('more', 'Share actions').')');
|
||||||
});
|
});
|
||||||
@@ -338,6 +368,7 @@ it('paints a group\'s hint and a menu item\'s icon in the colour their classes n
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
</head>
|
</head>
|
||||||
@@ -366,8 +397,10 @@ it('paints a group\'s hint and a menu item\'s icon in the colour their classes n
|
|||||||
it('keeps a menu open while the component around it renders, and closes it cleanly after', function () {
|
it('keeps a menu open while the component around it renders, and closes it cleanly after', function () {
|
||||||
$page = menuMorphProbe()->assertNoJavaScriptErrors();
|
$page = menuMorphProbe()->assertNoJavaScriptErrors();
|
||||||
|
|
||||||
$page->click('@sort')
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
->assertAttribute('@sort', 'aria-expanded', 'true');
|
pressOnce($page)->click('@sort');
|
||||||
|
|
||||||
|
$page->assertAttribute('@sort', 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->script('window.eval("Livewire.first().touch()")');
|
$page->script('window.eval("Livewire.first().touch()")');
|
||||||
|
|
||||||
@@ -383,9 +416,11 @@ it('keeps a menu open while the component around it renders, and closes it clean
|
|||||||
->assertScript(focused("dataset.test === 'sort'"));
|
->assertScript(focused("dataset.test === 'sort'"));
|
||||||
|
|
||||||
// Opened from the keyboard: a press this soon after the menu closed is taken for the
|
// Opened from the keyboard: a press this soon after the menu closed is taken for the
|
||||||
// light-dismiss press and ignored.
|
// light-dismiss press and ignored. ArrowDown on the trigger opens the menu, same as clicking
|
||||||
$page->keys('@sort', 'ArrowDown')
|
// it: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute('@sort', 'aria-expanded', 'true');
|
pressOnce($page)->keys('@sort', 'ArrowDown');
|
||||||
|
|
||||||
|
$page->assertAttribute('@sort', 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->script('window.eval("Livewire.first().touch()")');
|
$page->script('window.eval("Livewire.first().touch()")');
|
||||||
|
|
||||||
@@ -399,15 +434,20 @@ it('closes a menu on a second press of its menu button, and after the component
|
|||||||
$page = menuMorphProbe();
|
$page = menuMorphProbe();
|
||||||
|
|
||||||
// The press closes the menu before its click reaches the button: the guard against that click
|
// The press closes the menu before its click reaches the button: the guard against that click
|
||||||
// opening it again once waited for the queued toggle event, which comes after the click.
|
// opening it again once waited for the queued toggle event, which comes after the click. Each
|
||||||
$page->click('@sort')
|
// press toggles it, so each is pressed once: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute('@sort', 'aria-expanded', 'true')
|
pressOnce($page)->click('@sort');
|
||||||
->click('@sort')
|
|
||||||
->assertAttribute('@sort', 'aria-expanded', 'false')
|
$page->assertAttribute('@sort', 'aria-expanded', 'true');
|
||||||
|
|
||||||
|
pressOnce($page)->click('@sort');
|
||||||
|
|
||||||
|
$page->assertAttribute('@sort', 'aria-expanded', 'false')
|
||||||
->assertScript('! '.SORT_MENU.".matches(':popover-open')");
|
->assertScript('! '.SORT_MENU.".matches(':popover-open')");
|
||||||
|
|
||||||
$page->keys('@sort', 'ArrowDown')
|
pressOnce($page)->keys('@sort', 'ArrowDown');
|
||||||
->assertAttribute('@sort', 'aria-expanded', 'true');
|
|
||||||
|
$page->assertAttribute('@sort', 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->script('window.eval("Livewire.first().touch()")');
|
$page->script('window.eval("Livewire.first().touch()")');
|
||||||
|
|
||||||
@@ -415,30 +455,36 @@ it('closes a menu on a second press of its menu button, and after the component
|
|||||||
$page->assertSeeIn('#renders', '1')
|
$page->assertSeeIn('#renders', '1')
|
||||||
->wait(0.3);
|
->wait(0.3);
|
||||||
|
|
||||||
$page->click('@sort')
|
pressOnce($page)->click('@sort');
|
||||||
->assertAttribute('@sort', 'aria-expanded', 'false')
|
|
||||||
|
$page->assertAttribute('@sort', 'aria-expanded', 'false')
|
||||||
->assertScript('! '.SORT_MENU.".matches(':popover-open')");
|
->assertScript('! '.SORT_MENU.".matches(':popover-open')");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps a menu open while a keep-open item\'s action runs', function () {
|
it('keeps a menu open while a keep-open item\'s action runs', function () {
|
||||||
$page = menuMorphProbe();
|
$page = menuMorphProbe();
|
||||||
|
|
||||||
$page->click('@sort')
|
// Opens the menu, then a checkbox item toggles: pressOnce(), tests/Pest.php.
|
||||||
->click('[role="menuitemcheckbox"]:has-text("Largest")')
|
pressOnce($page)->click('@sort')
|
||||||
->assertAttribute('[role="menuitemcheckbox"]:has-text("Largest")', 'aria-checked', 'true')
|
->click('[role="menuitemcheckbox"]:has-text("Largest")');
|
||||||
|
|
||||||
|
$page->assertAttribute('[role="menuitemcheckbox"]:has-text("Largest")', 'aria-checked', 'true')
|
||||||
->assertScript(SORT_MENU.".matches(':popover-open')")
|
->assertScript(SORT_MENU.".matches(':popover-open')")
|
||||||
->assertAttribute('@sort', 'aria-expanded', 'true');
|
->assertAttribute('@sort', 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->click('[role="menuitemcheckbox"]:has-text("Newest")')
|
pressOnce($page)->click('[role="menuitemcheckbox"]:has-text("Newest")');
|
||||||
->assertAttribute('[role="menuitemcheckbox"]:has-text("Newest")', 'aria-checked', 'true')
|
|
||||||
|
$page->assertAttribute('[role="menuitemcheckbox"]:has-text("Newest")', 'aria-checked', 'true')
|
||||||
->assertScript(SORT_MENU.".matches(':popover-open')");
|
->assertScript(SORT_MENU.".matches(':popover-open')");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps a FAB menu open while the component around it renders, and closes it cleanly after', function () {
|
it('keeps a FAB menu open while the component around it renders, and closes it cleanly after', function () {
|
||||||
$page = menuMorphProbe();
|
$page = menuMorphProbe();
|
||||||
|
|
||||||
$page->click(NEW_FAB)
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
->assertAttribute(NEW_FAB, 'aria-expanded', 'true');
|
pressOnce($page)->click(NEW_FAB);
|
||||||
|
|
||||||
|
$page->assertAttribute(NEW_FAB, 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->script('window.eval("Livewire.first().touch()")');
|
$page->script('window.eval("Livewire.first().touch()")');
|
||||||
|
|
||||||
@@ -451,8 +497,10 @@ it('keeps a FAB menu open while the component around it renders, and closes it c
|
|||||||
->assertAttribute(NEW_FAB, 'aria-expanded', 'false')
|
->assertAttribute(NEW_FAB, 'aria-expanded', 'false')
|
||||||
->assertScript(focused("getAttribute('aria-label') === 'New'"));
|
->assertScript(focused("getAttribute('aria-label') === 'New'"));
|
||||||
|
|
||||||
$page->keys(NEW_FAB, 'ArrowDown')
|
// ArrowDown on the trigger opens the menu, same as clicking it: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute(NEW_FAB, 'aria-expanded', 'true');
|
pressOnce($page)->keys(NEW_FAB, 'ArrowDown');
|
||||||
|
|
||||||
|
$page->assertAttribute(NEW_FAB, 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->script('window.eval("Livewire.first().touch()")');
|
$page->script('window.eval("Livewire.first().touch()")');
|
||||||
|
|
||||||
@@ -460,23 +508,28 @@ it('keeps a FAB menu open while the component around it renders, and closes it c
|
|||||||
$page->assertSeeIn('#renders', '2')
|
$page->assertSeeIn('#renders', '2')
|
||||||
->wait(0.3);
|
->wait(0.3);
|
||||||
|
|
||||||
$page->click(NEW_FAB)
|
pressOnce($page)->click(NEW_FAB);
|
||||||
->assertAttribute(NEW_FAB, 'aria-expanded', 'false')
|
|
||||||
|
$page->assertAttribute(NEW_FAB, 'aria-expanded', 'false')
|
||||||
->assertScript('! '.FAB_MENU.".matches(':popover-open')");
|
->assertScript('! '.FAB_MENU.".matches(':popover-open')");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes a FAB menu when an item\'s action runs, and opens and closes it cleanly after', function () {
|
it('closes a FAB menu when an item\'s action runs, and opens and closes it cleanly after', function () {
|
||||||
$page = menuMorphProbe();
|
$page = menuMorphProbe();
|
||||||
|
|
||||||
$page->click(NEW_FAB)
|
// Opens the FAB menu, then an item closes it: pressOnce(), tests/Pest.php.
|
||||||
->click('[role="menuitem"]:has-text("Upload files")')
|
pressOnce($page)->click(NEW_FAB)
|
||||||
->assertSeeIn('#created', 'files')
|
->click('[role="menuitem"]:has-text("Upload files")');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#created', 'files')
|
||||||
->assertAttribute(NEW_FAB, 'aria-expanded', 'false');
|
->assertAttribute(NEW_FAB, 'aria-expanded', 'false');
|
||||||
|
|
||||||
$page->script("document.querySelector('".NEW_FAB."').focus()");
|
$page->script("document.querySelector('".NEW_FAB."').focus()");
|
||||||
|
|
||||||
$page->keys(NEW_FAB, 'ArrowDown')
|
// ArrowDown on the trigger opens the menu, same as clicking it: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute(NEW_FAB, 'aria-expanded', 'true')
|
pressOnce($page)->keys(NEW_FAB, 'ArrowDown');
|
||||||
|
|
||||||
|
$page->assertAttribute(NEW_FAB, 'aria-expanded', 'true')
|
||||||
->assertScript(focused("textContent.trim() === 'Upload files'"));
|
->assertScript(focused("textContent.trim() === 'Upload files'"));
|
||||||
|
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
@@ -537,6 +590,7 @@ it('hides a disabled fab button below 600px, as M3 removes a FAB whose action is
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -563,9 +617,11 @@ it('hides a disabled fab button below 600px, as M3 removes a FAB whose action is
|
|||||||
->and($page->script($state('disabled-link-fab')))->toBe([false, true]);
|
->and($page->script($state('disabled-link-fab')))->toBe([false, true]);
|
||||||
|
|
||||||
// A spinner disables its button while the action runs; that FAB is busy, not unavailable, and
|
// A spinner disables its button while the action runs; that FAB is busy, not unavailable, and
|
||||||
// stays on screen with its loading indicator.
|
// stays on screen with its loading indicator. A counter a retry would double: pressOnce(),
|
||||||
$page->click('#enabled-fab')
|
// tests/Pest.php.
|
||||||
->assertScript("(() => { const button = document.getElementById('enabled-fab'); return button.hasAttribute('data-loading') && button.disabled && button.checkVisibility() && button.querySelector('[data-md-button-spinner]').checkVisibility(); })()")
|
pressOnce($page)->click('#enabled-fab');
|
||||||
|
|
||||||
|
$page->assertScript("(() => { const button = document.getElementById('enabled-fab'); return button.hasAttribute('data-loading') && button.disabled && button.checkVisibility() && button.querySelector('[data-md-button-spinner]').checkVisibility(); })()")
|
||||||
->assertSeeIn('#created', '1');
|
->assertSeeIn('#created', '1');
|
||||||
|
|
||||||
$page->resize(600, 800)->assertScript("getComputedStyle(document.getElementById('enabled-fab')).position !== 'fixed'");
|
$page->resize(600, 800)->assertScript("getComputedStyle(document.getElementById('enabled-fab')).position !== 'fixed'");
|
||||||
@@ -612,6 +668,7 @@ function groupSizeProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -674,14 +731,19 @@ it('toggles each choice of a multi selection group, down to none', function () {
|
|||||||
$group = '#buttons [data-md-selection="multi"]';
|
$group = '#buttons [data-md-selection="multi"]';
|
||||||
$marks = "document.querySelector('{$group}').parentElement.querySelectorAll('code')[1].textContent";
|
$marks = "document.querySelector('{$group}').parentElement.querySelectorAll('code')[1].textContent";
|
||||||
|
|
||||||
showcase()
|
$page = showcase()
|
||||||
->assertAttribute("{$group} [aria-label=\"Bold\"]", 'aria-pressed', 'true')
|
->assertAttribute("{$group} [aria-label=\"Bold\"]", 'aria-pressed', 'true');
|
||||||
->click("{$group} [aria-label=\"Italic\"]")
|
|
||||||
->assertAttribute("{$group} [aria-label=\"Italic\"]", 'aria-pressed', 'true')
|
// Each press toggles the choice: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$marks} === 'bold, italic'")
|
pressOnce($page)->click("{$group} [aria-label=\"Italic\"]");
|
||||||
->click("{$group} [aria-label=\"Bold\"]")
|
|
||||||
->click("{$group} [aria-label=\"Italic\"]")
|
$page->assertAttribute("{$group} [aria-label=\"Italic\"]", 'aria-pressed', 'true')
|
||||||
->assertAttribute("{$group} [aria-label=\"Bold\"]", 'aria-pressed', 'false')
|
->assertScript("{$marks} === 'bold, italic'");
|
||||||
|
|
||||||
|
pressOnce($page)->click("{$group} [aria-label=\"Bold\"]")
|
||||||
|
->click("{$group} [aria-label=\"Italic\"]");
|
||||||
|
|
||||||
|
$page->assertAttribute("{$group} [aria-label=\"Bold\"]", 'aria-pressed', 'false')
|
||||||
->assertAttribute("{$group} [aria-label=\"Italic\"]", 'aria-pressed', 'false')
|
->assertAttribute("{$group} [aria-label=\"Italic\"]", 'aria-pressed', 'false')
|
||||||
->assertScript("{$marks} === 'none'");
|
->assertScript("{$marks} === 'none'");
|
||||||
});
|
});
|
||||||
@@ -749,6 +811,7 @@ function longMenuProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -771,6 +834,7 @@ function sheetMenuProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -789,9 +853,12 @@ it('opens a submenu with Right, Enter or Space, and closes it with Left or Escap
|
|||||||
$trigger = '#menus button:has-text("Share")';
|
$trigger = '#menus button:has-text("Share")';
|
||||||
$sendTo = '#menus [role="menuitem"]:has-text("Send to")';
|
$sendTo = '#menus [role="menuitem"]:has-text("Send to")';
|
||||||
|
|
||||||
$page = showcase('menus')
|
$page = showcase('menus');
|
||||||
->click($trigger)
|
|
||||||
->assertAttribute($trigger, 'aria-expanded', 'true')
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
|
$page->assertAttribute($trigger, 'aria-expanded', 'true')
|
||||||
->assertScript(focused("textContent.trim().startsWith('Copy link')"));
|
->assertScript(focused("textContent.trim().startsWith('Copy link')"));
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowDown')
|
$page->keys(':focus', 'ArrowDown')
|
||||||
@@ -824,7 +891,10 @@ it('opens a submenu on hover for a fine pointer, and closes it once the pointer
|
|||||||
// "Delete" is an item in five menus on the page; only the open one's is visible.
|
// "Delete" is an item in five menus on the page; only the open one's is visible.
|
||||||
$delete = '#menus [role="menuitem"]:has-text("Delete"):visible';
|
$delete = '#menus [role="menuitem"]:has-text("Delete"):visible';
|
||||||
|
|
||||||
$page = showcase('menus')->click($trigger);
|
$page = showcase('menus');
|
||||||
|
|
||||||
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
$page->hover($sendTo)
|
$page->hover($sendTo)
|
||||||
->wait(0.3)
|
->wait(0.3)
|
||||||
@@ -847,9 +917,12 @@ it('filters a menu\'s items as its field is typed into, moves the highlight with
|
|||||||
$ada = "{$list}.find((item) => item.textContent.trim().startsWith('Ada Lovelace')).id";
|
$ada = "{$list}.find((item) => item.textContent.trim().startsWith('Ada Lovelace')).id";
|
||||||
$grace = "{$list}.find((item) => item.textContent.trim().startsWith('Grace Hopper')).id";
|
$grace = "{$list}.find((item) => item.textContent.trim().startsWith('Grace Hopper')).id";
|
||||||
|
|
||||||
$page = showcase('menus')
|
$page = showcase('menus');
|
||||||
->click($trigger)
|
|
||||||
->assertScript(focused("getAttribute('aria-label') === 'Find a person'"));
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
|
$page->assertScript(focused("getAttribute('aria-label') === 'Find a person'"));
|
||||||
|
|
||||||
// Ada Lovelace and Grace Hopper are both "Engineering"; the rest are Research or Networks.
|
// Ada Lovelace and Grace Hopper are both "Engineering"; the rest are Research or Networks.
|
||||||
$page->type($field, 'engineering')
|
$page->type($field, 'engineering')
|
||||||
@@ -863,8 +936,9 @@ it('filters a menu\'s items as its field is typed into, moves the highlight with
|
|||||||
->assertAttribute($trigger, 'aria-expanded', 'false');
|
->assertAttribute($trigger, 'aria-expanded', 'false');
|
||||||
|
|
||||||
// Closing clears the query, so a reopen shows the whole list again.
|
// Closing clears the query, so a reopen shows the whole list again.
|
||||||
$page->click($trigger)
|
pressOnce($page)->click($trigger);
|
||||||
->assertScript("document.querySelector('{$field}').value === ''")
|
|
||||||
|
$page->assertScript("document.querySelector('{$field}').value === ''")
|
||||||
->assertScript("{$list}.every((item) => !item.hidden)");
|
->assertScript("{$list}.every((item) => !item.hidden)");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -879,8 +953,10 @@ it('opens a sheet-at-compact menu below 600px, focused on its first item, and th
|
|||||||
|
|
||||||
$page = showcase('menus')->resize(400, 800);
|
$page = showcase('menus')->resize(400, 800);
|
||||||
|
|
||||||
$page->click($trigger)
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
->assertAttribute($trigger, 'aria-haspopup', 'dialog')
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
|
$page->assertAttribute($trigger, 'aria-haspopup', 'dialog')
|
||||||
->assertScript(focused("textContent.trim().startsWith('Set as wallpaper')"))
|
->assertScript(focused("textContent.trim().startsWith('Set as wallpaper')"))
|
||||||
->assertVisible($dialog);
|
->assertVisible($dialog);
|
||||||
|
|
||||||
@@ -899,11 +975,14 @@ it('opens a sheet-at-compact menu below 600px, focused on its first item, and th
|
|||||||
->assertScript(focused("textContent.trim() === 'Photo'"));
|
->assertScript(focused("textContent.trim() === 'Photo'"));
|
||||||
|
|
||||||
// From 600px the trigger opens the popover instead, and a resize across it while one is open
|
// From 600px the trigger opens the popover instead, and a resize across it while one is open
|
||||||
// closes whichever was shown.
|
// closes whichever was shown. The trigger says which it opens now before it is pressed — and
|
||||||
|
// pressed once, as it toggles: pressOnce() in tests/Pest.php.
|
||||||
$page->resize(900, 800)
|
$page->resize(900, 800)
|
||||||
->click($trigger)
|
->assertAttribute($trigger, 'aria-haspopup', 'menu');
|
||||||
->assertAttribute($trigger, 'aria-haspopup', 'menu')
|
|
||||||
->assertAttribute($trigger, 'aria-expanded', 'true');
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
|
$page->assertAttribute($trigger, 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->resize(400, 800)
|
$page->resize(400, 800)
|
||||||
->assertAttribute($trigger, 'aria-expanded', 'false');
|
->assertAttribute($trigger, 'aria-expanded', 'false');
|
||||||
@@ -912,8 +991,10 @@ it('opens a sheet-at-compact menu below 600px, focused on its first item, and th
|
|||||||
it('filters in the sheet too, and a Livewire render keeps it open with the field\'s focus', function () {
|
it('filters in the sheet too, and a Livewire render keeps it open with the field\'s focus', function () {
|
||||||
$page = sheetMenuProbe();
|
$page = sheetMenuProbe();
|
||||||
|
|
||||||
$page->click('button:has-text("Assign")')
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
->assertScript(focused("getAttribute('aria-label') === 'Find a person'"));
|
pressOnce($page)->click('button:has-text("Assign")');
|
||||||
|
|
||||||
|
$page->assertScript(focused("getAttribute('aria-label') === 'Find a person'"));
|
||||||
|
|
||||||
$page->type(':focus', 'grace')
|
$page->type(':focus', 'grace')
|
||||||
->assertScript("[...document.querySelectorAll('[role=\"dialog\"] [role=\"menuitem\"]')].filter((item) => !item.hidden).length === 1");
|
->assertScript("[...document.querySelectorAll('[role=\"dialog\"] [role=\"menuitem\"]')].filter((item) => !item.hidden).length === 1");
|
||||||
@@ -931,9 +1012,12 @@ it('filters in the sheet too, and a Livewire render keeps it open with the field
|
|||||||
it('scrolls a long menu and keeps the item the keyboard reaches inside its visible box', function () {
|
it('scrolls a long menu and keeps the item the keyboard reaches inside its visible box', function () {
|
||||||
$popover = "document.querySelector('[role=\"menu\"][aria-label=\"Long list\"]')";
|
$popover = "document.querySelector('[role=\"menu\"][aria-label=\"Long list\"]')";
|
||||||
|
|
||||||
$page = longMenuProbe()
|
$page = longMenuProbe();
|
||||||
->click('button:has-text("Open")')
|
|
||||||
->assertScript("{$popover}.scrollHeight > {$popover}.clientHeight");
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button:has-text("Open")');
|
||||||
|
|
||||||
|
$page->assertScript("{$popover}.scrollHeight > {$popover}.clientHeight");
|
||||||
|
|
||||||
$page->keys(':focus', 'End')
|
$page->keys(':focus', 'End')
|
||||||
->assertScript(focused("textContent.trim() === 'Item 20'"))
|
->assertScript(focused("textContent.trim() === 'Item 20'"))
|
||||||
@@ -964,6 +1048,7 @@ function longFabMenuProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -982,9 +1067,12 @@ it('scrolls the FAB menu\'s items on a short window, behind the close button, wh
|
|||||||
$trigger = "document.querySelector('button[aria-label=\"New\"]')";
|
$trigger = "document.querySelector('button[aria-label=\"New\"]')";
|
||||||
$list = "document.querySelector('[role=\"menu\"][aria-label=\"New\"]')";
|
$list = "document.querySelector('[role=\"menu\"][aria-label=\"New\"]')";
|
||||||
|
|
||||||
$page = longFabMenuProbe()
|
$page = longFabMenuProbe();
|
||||||
->click('button[aria-label="New"]')
|
|
||||||
->assertAttribute('button[aria-label="New"]', 'aria-expanded', 'true')
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button[aria-label="New"]');
|
||||||
|
|
||||||
|
$page->assertAttribute('button[aria-label="New"]', 'aria-expanded', 'true')
|
||||||
->assertScript("{$list}.scrollHeight > {$list}.clientHeight");
|
->assertScript("{$list}.scrollHeight > {$list}.clientHeight");
|
||||||
|
|
||||||
$page->script("window.__fabTop = {$trigger}.getBoundingClientRect().top; {$list}.scrollTop = 40");
|
$page->script("window.__fabTop = {$trigger}.getBoundingClientRect().top; {$list}.scrollTop = 40");
|
||||||
@@ -993,20 +1081,6 @@ it('scrolls the FAB menu\'s items on a short window, behind the close button, wh
|
|||||||
$page->assertScript("{$trigger}.getBoundingClientRect().top === window.__fabTop");
|
$page->assertScript("{$trigger}.getBoundingClientRect().top === window.__fabTop");
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Slows the page's motion tokens, so a popover's exit copy (resources/js/popover-exit.js) is
|
|
||||||
* still on screen after a separate round trip to the browser: a key press or a click already
|
|
||||||
* costs about as long as a 150–350ms exit, and the copy moves on the tokens as the popover would.
|
|
||||||
*/
|
|
||||||
function slowMotion(mixed $page, string $duration = '1500ms'): mixed
|
|
||||||
{
|
|
||||||
$page->script(<<<JS
|
|
||||||
document.head.insertAdjacentHTML('beforeend', '<style>:root { --md-sys-motion-effects-fast-duration: {$duration}; --md-sys-motion-effects-default-duration: {$duration}; --md-sys-motion-spatial-fast-duration: {$duration}; --md-sys-motion-spatial-default-duration: {$duration}; }</style>')
|
|
||||||
JS);
|
|
||||||
|
|
||||||
return $page;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** The exit copy on screen: shown, inert, hidden from assistive technology, with no ids of its own. */
|
/** The exit copy on screen: shown, inert, hidden from assistive technology, with no ids of its own. */
|
||||||
const EXIT_COPY = "(() => { const copy = document.querySelector('[data-md-popover-ghost]'); return copy !== null && copy.matches(':popover-open') && copy.inert && copy.getAttribute('aria-hidden') === 'true' && ! copy.hasAttribute('id') && copy.querySelector('[id], [popover]:not([data-md-popover-ghost])') === null; })()";
|
const EXIT_COPY = "(() => { const copy = document.querySelector('[data-md-popover-ghost]'); return copy !== null && copy.matches(':popover-open') && copy.inert && copy.getAttribute('aria-hidden') === 'true' && ! copy.hasAttribute('id') && copy.querySelector('[id], [popover]:not([data-md-popover-ghost])') === null; })()";
|
||||||
|
|
||||||
@@ -1016,9 +1090,12 @@ const EXIT_COPY_FADING = "(() => { const copy = document.querySelector('[data-md
|
|||||||
it('fades a menu out after the browser has closed it, on Escape or a press outside, in every engine', function () {
|
it('fades a menu out after the browser has closed it, on Escape or a press outside, in every engine', function () {
|
||||||
$menu = 'document.getElementById(document.querySelector(\''.MORE.'\').getAttribute(\'aria-controls\'))';
|
$menu = 'document.getElementById(document.querySelector(\''.MORE.'\').getAttribute(\'aria-controls\'))';
|
||||||
|
|
||||||
$page = slowMotion(showcase('menus'))
|
$page = slowMotion(showcase('menus'));
|
||||||
->click(MORE)
|
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'true');
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click(MORE);
|
||||||
|
|
||||||
|
$page->assertAttribute(MORE, 'aria-expanded', 'true');
|
||||||
|
|
||||||
// Escape: the browser's own light dismiss, which no script can hold open.
|
// Escape: the browser's own light dismiss, which no script can hold open.
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
@@ -1029,13 +1106,17 @@ it('fades a menu out after the browser has closed it, on Escape or a press outsi
|
|||||||
->assertScript(EXIT_COPY_FADING)
|
->assertScript(EXIT_COPY_FADING)
|
||||||
// The copy is decoration: Alpine starts nothing inside it.
|
// The copy is decoration: Alpine starts nothing inside it.
|
||||||
->assertScript("window.eval(\"[...document.querySelectorAll('[data-md-popover-ghost], [data-md-popover-ghost] *')].every((element) => element._x_dataStack === undefined)\")")
|
->assertScript("window.eval(\"[...document.querySelectorAll('[data-md-popover-ghost], [data-md-popover-ghost] *')].every((element) => element._x_dataStack === undefined)\")")
|
||||||
// Gone once the slowest of its transitions has run.
|
// Gone once the slowest of its transitions has run: assertScript's own retry waits for it,
|
||||||
->assertScript('new Promise((resolve) => setTimeout(() => resolve(document.querySelector("[data-md-popover-ghost]") === null), 1800))');
|
// where sleeping for the exit's length inside the page outlived the plugin's 1000ms budget
|
||||||
|
// for one script attempt and had the whole expectation run again until it timed out.
|
||||||
|
->assertScript('document.querySelector("[data-md-popover-ghost]") === null');
|
||||||
|
|
||||||
// A press outside it.
|
// A press outside it.
|
||||||
$page->click(MORE)
|
pressOnce($page)->click(MORE);
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'true')
|
|
||||||
->click('#menus')
|
$page->assertAttribute(MORE, 'aria-expanded', 'true');
|
||||||
|
|
||||||
|
$page->click('#menus')
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'false')
|
->assertAttribute(MORE, 'aria-expanded', 'false')
|
||||||
->assertScript(EXIT_COPY)
|
->assertScript(EXIT_COPY)
|
||||||
->assertScript(EXIT_COPY_FADING)
|
->assertScript(EXIT_COPY_FADING)
|
||||||
@@ -1043,8 +1124,10 @@ it('fades a menu out after the browser has closed it, on Escape or a press outsi
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('takes a menu\'s exit copy away when the menu opens again part-way through it', function () {
|
it('takes a menu\'s exit copy away when the menu opens again part-way through it', function () {
|
||||||
$page = slowMotion(showcase('menus'))
|
$page = slowMotion(showcase('menus'));
|
||||||
->click(MORE);
|
|
||||||
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click(MORE);
|
||||||
|
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertScript(EXIT_COPY)
|
->assertScript(EXIT_COPY)
|
||||||
@@ -1053,15 +1136,18 @@ it('takes a menu\'s exit copy away when the menu opens again part-way through it
|
|||||||
->wait(0.3)
|
->wait(0.3)
|
||||||
->assertScript(EXIT_COPY);
|
->assertScript(EXIT_COPY);
|
||||||
|
|
||||||
$page->click(MORE)
|
pressOnce($page)->click(MORE);
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'true')
|
|
||||||
|
$page->assertAttribute(MORE, 'aria-expanded', 'true')
|
||||||
->assertScript("document.querySelector('[data-md-popover-ghost]') === null")
|
->assertScript("document.querySelector('[data-md-popover-ghost]') === null")
|
||||||
->assertScript('document.getElementById(document.querySelector(\''.MORE.'\').getAttribute(\'aria-controls\')).matches(\':popover-open\')');
|
->assertScript('document.getElementById(document.querySelector(\''.MORE.'\').getAttribute(\'aria-controls\')).matches(\':popover-open\')');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('leaves no exit copy under reduced motion, where every duration token is zero', function () {
|
it('leaves no exit copy under reduced motion, where every duration token is zero', function () {
|
||||||
$page = slowMotion(showcase('menus'), '0ms')
|
$page = slowMotion(showcase('menus'), '0ms');
|
||||||
->click(MORE);
|
|
||||||
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click(MORE);
|
||||||
|
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertAttribute(MORE, 'aria-expanded', 'false')
|
->assertAttribute(MORE, 'aria-expanded', 'false')
|
||||||
@@ -1072,8 +1158,10 @@ it('fades a submenu out on its own, while its menu stays open', function () {
|
|||||||
$trigger = '#menus button:has-text("Share")';
|
$trigger = '#menus button:has-text("Share")';
|
||||||
$sendTo = '#menus [role="menuitem"]:has-text("Send to")';
|
$sendTo = '#menus [role="menuitem"]:has-text("Send to")';
|
||||||
|
|
||||||
$page = slowMotion(showcase('menus'))
|
$page = slowMotion(showcase('menus'));
|
||||||
->click($trigger);
|
|
||||||
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowDown');
|
$page->keys(':focus', 'ArrowDown');
|
||||||
$page->keys(':focus', 'ArrowRight')
|
$page->keys(':focus', 'ArrowRight')
|
||||||
@@ -1102,9 +1190,12 @@ it('fades a tooltip out after Escape hides it', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('sinks a FAB menu\'s items back after the menu has closed', function () {
|
it('sinks a FAB menu\'s items back after the menu has closed', function () {
|
||||||
$page = slowMotion(showcase())
|
$page = slowMotion(showcase());
|
||||||
->click('button[aria-label="New"]')
|
|
||||||
->assertScript("document.querySelector('button[aria-label=\"New\"]').getAttribute('aria-expanded') === 'true'");
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button[aria-label="New"]');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('button[aria-label=\"New\"]').getAttribute('aria-expanded') === 'true'");
|
||||||
|
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertScript("document.querySelector('button[aria-label=\"New\"]').getAttribute('aria-expanded') === 'false'")
|
->assertScript("document.querySelector('button[aria-label=\"New\"]').getAttribute('aria-expanded') === 'false'")
|
||||||
|
|||||||
+51
-24
@@ -71,6 +71,7 @@ function barsProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -93,8 +94,11 @@ it('moves between tabs with the arrow keys, skipping disabled ones, and tells Li
|
|||||||
->assertAttribute('#share-people', 'tabindex', '0')
|
->assertAttribute('#share-people', 'tabindex', '0')
|
||||||
->assertAttribute('#share-files', 'tabindex', '-1');
|
->assertAttribute('#share-files', 'tabindex', '-1');
|
||||||
|
|
||||||
$page->keys('#share-people', 'ArrowRight')
|
// Each arrow key sends its own wire:model.live request and moves one tab further: pressOnce(),
|
||||||
->assertSeeIn('#tab', 'activity')
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->keys('#share-people', 'ArrowRight');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#tab', 'activity')
|
||||||
->assertScript("document.activeElement.id === 'share-activity'")
|
->assertScript("document.activeElement.id === 'share-activity'")
|
||||||
->assertScript("getComputedStyle(document.querySelector('#share-activity [data-md-tab-indicator]')).opacity === '1'");
|
->assertScript("getComputedStyle(document.querySelector('#share-activity [data-md-tab-indicator]')).opacity === '1'");
|
||||||
|
|
||||||
@@ -102,8 +106,9 @@ it('moves between tabs with the arrow keys, skipping disabled ones, and tells Li
|
|||||||
->assertSeeIn('#tab', 'files')
|
->assertSeeIn('#tab', 'files')
|
||||||
->assertScript("document.activeElement.id === 'share-files'");
|
->assertScript("document.activeElement.id === 'share-files'");
|
||||||
|
|
||||||
$page->keys('#share-files', 'ArrowLeft')
|
pressOnce($page)->keys('#share-files', 'ArrowLeft');
|
||||||
->assertSeeIn('#tab', 'activity');
|
|
||||||
|
$page->assertSeeIn('#tab', 'activity');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends a request per click with wire:model.live, but nothing until then with plain wire:model', function () {
|
it('sends a request per click with wire:model.live, but nothing until then with plain wire:model', function () {
|
||||||
@@ -165,23 +170,32 @@ it('switches the theme from a toggle, a cycle and a picker', function () {
|
|||||||
->assertScript("document.documentElement.dataset.theme === 'light'")
|
->assertScript("document.documentElement.dataset.theme === 'light'")
|
||||||
->assertAttribute('#toggle', 'aria-pressed', 'false');
|
->assertAttribute('#toggle', 'aria-pressed', 'false');
|
||||||
|
|
||||||
$page->click('#toggle')
|
// A theme toggle flips on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.documentElement.dataset.theme === 'dark'")
|
pressOnce($page)->click('#toggle');
|
||||||
|
|
||||||
|
$page->assertScript("document.documentElement.dataset.theme === 'dark'")
|
||||||
->assertAttribute('#toggle', 'aria-pressed', 'true')
|
->assertAttribute('#toggle', 'aria-pressed', 'true')
|
||||||
->assertScript('document.querySelector(\'input[name="material-theme"][value="dark"]\').checked');
|
->assertScript('document.querySelector(\'input[name="material-theme"][value="dark"]\').checked');
|
||||||
|
|
||||||
$page->click('#cycle')
|
// A cycling toggle a retry would cycle twice: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.documentElement.dataset.themeChoice === 'system'")
|
pressOnce($page)->click('#cycle');
|
||||||
->click('#cycle')
|
|
||||||
->assertScript("document.documentElement.dataset.themeChoice === 'light'");
|
$page->assertScript("document.documentElement.dataset.themeChoice === 'system'");
|
||||||
|
|
||||||
|
pressOnce($page)->click('#cycle');
|
||||||
|
|
||||||
|
$page->assertScript("document.documentElement.dataset.themeChoice === 'light'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('turns section tabs into a picker on a phone', function () {
|
it('turns section tabs into a picker on a phone', function () {
|
||||||
barsProbe()
|
$page = barsProbe()
|
||||||
->resize(400, 800)
|
->resize(400, 800)
|
||||||
->assertScript("getComputedStyle(document.querySelector('[data-md-section-nav] nav')).display === 'none'")
|
->assertScript("getComputedStyle(document.querySelector('[data-md-section-nav] nav')).display === 'none'");
|
||||||
->click('[data-md-section-nav-picker] button')
|
|
||||||
->assertScript("document.querySelector('[data-md-section-nav-picker] [popover]').matches(':popover-open')")
|
// Opens the picker menu over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-section-nav-picker] button');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('[data-md-section-nav-picker] [popover]').matches(':popover-open')")
|
||||||
// A menu of places: the current section is the page, not a checked choice.
|
// A menu of places: the current section is the page, not a checked choice.
|
||||||
->assertAttribute('[data-md-section-nav-picker] [role="menuitem"][href="#profile"]', 'aria-current', 'page')
|
->assertAttribute('[data-md-section-nav-picker] [role="menuitem"][href="#profile"]', 'aria-current', 'page')
|
||||||
->assertScript("! document.querySelector('[data-md-section-nav-picker] [role=\"menuitem\"][href=\"#security\"]').hasAttribute('aria-current')");
|
->assertScript("! document.querySelector('[data-md-section-nav-picker] [role=\"menuitem\"][href=\"#security\"]').hasAttribute('aria-current')");
|
||||||
@@ -273,6 +287,7 @@ function appBarOverflowProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -306,8 +321,10 @@ it('shows the app bar overflow at exactly 599 and 600px', function () {
|
|||||||
it('moves the keyboard through an app bar\'s overflow menu, and closes it once the width no longer shows it', function () {
|
it('moves the keyboard through an app bar\'s overflow menu, and closes it once the width no longer shows it', function () {
|
||||||
$page = appBarOverflowProbe()->resize(599, 800);
|
$page = appBarOverflowProbe()->resize(599, 800);
|
||||||
|
|
||||||
$page->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button')
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
->assertScript("document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
|
pressOnce($page)->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
|
||||||
->keys(':focus', 'ArrowDown')
|
->keys(':focus', 'ArrowDown')
|
||||||
->assertScript("document.activeElement.textContent.includes('Star')")
|
->assertScript("document.activeElement.textContent.includes('Star')")
|
||||||
->keys(':focus', 'ArrowDown')
|
->keys(':focus', 'ArrowDown')
|
||||||
@@ -320,8 +337,10 @@ it('moves the keyboard through an app bar\'s overflow menu, and closes it once t
|
|||||||
|
|
||||||
// Reopen it, then resize across 600px: the compact menu's trigger is no longer shown, so the
|
// Reopen it, then resize across 600px: the compact menu's trigger is no longer shown, so the
|
||||||
// menu it opened closes rather than staying open anchored to nothing.
|
// menu it opened closes rather than staying open anchored to nothing.
|
||||||
$page->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button')
|
// A menu's trigger toggles, so it is pressed once: pressOnce() in tests/Pest.php.
|
||||||
->assertScript("document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
|
pressOnce($page)->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
|
||||||
->resize(900, 800)
|
->resize(900, 800)
|
||||||
->assertScript("! document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
|
->assertScript("! document.querySelector('#bar [data-md-app-bar-overflow=\"compact\"] [data-md-menu-popover]').matches(':popover-open')")
|
||||||
->assertNoJavaScriptErrors();
|
->assertNoJavaScriptErrors();
|
||||||
@@ -330,13 +349,18 @@ it('moves the keyboard through an app bar\'s overflow menu, and closes it once t
|
|||||||
it('runs a wire:click action from either the icon button or its overflow menu item', function () {
|
it('runs a wire:click action from either the icon button or its overflow menu item', function () {
|
||||||
$page = appBarOverflowProbe()->resize(900, 800);
|
$page = appBarOverflowProbe()->resize(900, 800);
|
||||||
|
|
||||||
$page->click('#bar [data-md-app-bar-action] [aria-label="Star"]')
|
// A counter a retry would double: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#stars', '1');
|
pressOnce($page)->click('#bar [data-md-app-bar-action] [aria-label="Star"]');
|
||||||
|
|
||||||
$page->resize(599, 800)
|
$page->assertSeeIn('#stars', '1');
|
||||||
->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button')
|
|
||||||
->click('[role="menuitem"]:has-text("Star")')
|
$page->resize(599, 800);
|
||||||
->assertSeeIn('#stars', '2');
|
|
||||||
|
// Opens the overflow menu, then a counter a retry would double: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#bar [data-md-app-bar-overflow="compact"] [data-md-menu-trigger] button')
|
||||||
|
->click('[role="menuitem"]:has-text("Star")');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#stars', '2');
|
||||||
});
|
});
|
||||||
|
|
||||||
function toolbarScaffoldProbe()
|
function toolbarScaffoldProbe()
|
||||||
@@ -345,6 +369,7 @@ function toolbarScaffoldProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -397,6 +422,7 @@ it('lifts a snackbar above a floating toolbar placed at the bottom, without a sc
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -458,6 +484,7 @@ function tabsFirstPaintProbe(): void
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
|
|||||||
@@ -107,29 +107,37 @@ it('moves one item with the next and previous buttons', function () {
|
|||||||
$page = carouselShowcase()
|
$page = carouselShowcase()
|
||||||
->assertScript(onCarousel(0, 'return at(0) && root.querySelector(\'[aria-label="Previous"]\').disabled'));
|
->assertScript(onCarousel(0, 'return at(0) && root.querySelector(\'[aria-label="Previous"]\').disabled'));
|
||||||
|
|
||||||
$page->click('#carousel button[aria-label="Next"] >> nth=0')
|
// A retried press drifts the carousel one further item each time: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(onCarousel(0, 'return at(1) && inset(1) < 0.5 && !root.querySelector(\'[aria-label="Previous"]\').disabled'));
|
pressOnce($page)->click('#carousel button[aria-label="Next"] >> nth=0');
|
||||||
|
|
||||||
$page->click('#carousel button[aria-label="Next"] >> nth=0')
|
$page->assertScript(onCarousel(0, 'return at(1) && inset(1) < 0.5 && !root.querySelector(\'[aria-label="Previous"]\').disabled'));
|
||||||
->assertScript(onCarousel(0, 'return at(2)'));
|
|
||||||
|
|
||||||
$page->click('#carousel button[aria-label="Previous"] >> nth=0')
|
pressOnce($page)->click('#carousel button[aria-label="Next"] >> nth=0');
|
||||||
->assertScript(onCarousel(0, 'return at(1)'));
|
|
||||||
|
$page->assertScript(onCarousel(0, 'return at(2)'));
|
||||||
|
|
||||||
|
pressOnce($page)->click('#carousel button[aria-label="Previous"] >> nth=0');
|
||||||
|
|
||||||
|
$page->assertScript(onCarousel(0, 'return at(1)'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves one item with the arrow keys, and to the ends with Home and End', function () {
|
it('moves one item with the arrow keys, and to the ends with Home and End', function () {
|
||||||
$page = carouselShowcase();
|
$page = carouselShowcase();
|
||||||
|
|
||||||
// M3 puts the tab stop on the item, so the keys are the focused item's and each one moves
|
// M3 puts the tab stop on the item, so the keys are the focused item's and each one moves
|
||||||
// focus to the item it scrolls to.
|
// focus to the item it scrolls to. A retried press drifts the carousel one further item each
|
||||||
$page->keys(FIRST_ITEM, 'ArrowRight')
|
// time: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(onCarousel(0, 'return at(1) && document.activeElement === items[1]'));
|
pressOnce($page)->keys(FIRST_ITEM, 'ArrowRight');
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowRight')
|
$page->assertScript(onCarousel(0, 'return at(1) && document.activeElement === items[1]'));
|
||||||
->assertScript(onCarousel(0, 'return at(2)'));
|
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowLeft')
|
pressOnce($page)->keys(':focus', 'ArrowRight');
|
||||||
->assertScript(onCarousel(0, 'return at(1)'));
|
|
||||||
|
$page->assertScript(onCarousel(0, 'return at(2)'));
|
||||||
|
|
||||||
|
pressOnce($page)->keys(':focus', 'ArrowLeft');
|
||||||
|
|
||||||
|
$page->assertScript(onCarousel(0, 'return at(1)'));
|
||||||
|
|
||||||
$page->keys(':focus', 'End')
|
$page->keys(':focus', 'End')
|
||||||
->assertScript(onCarousel(0, 'return Math.abs(scroller.scrollLeft - (scroller.scrollWidth - scroller.clientWidth)) < 1.5 && open(items.length - 1) && root.querySelector(\'[aria-label="Next"]\').disabled'));
|
->assertScript(onCarousel(0, 'return Math.abs(scroller.scrollLeft - (scroller.scrollWidth - scroller.clientWidth)) < 1.5 && open(items.length - 1) && root.querySelector(\'[aria-label="Next"]\').disabled'));
|
||||||
@@ -212,6 +220,7 @@ it('mirrors in a right-to-left page', function () {
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html dir="rtl">
|
<html dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -233,17 +242,21 @@ it('mirrors in a right-to-left page', function () {
|
|||||||
->assertNoJavaScriptErrors()
|
->assertNoJavaScriptErrors()
|
||||||
->assertScript(onCarousel(0, 'return size > 0 && at(0) && open(0) && ! open(items.length - 1)', 'body'));
|
->assertScript(onCarousel(0, 'return size > 0 && at(0) && open(0) && ! open(items.length - 1)', 'body'));
|
||||||
|
|
||||||
$page->keys('[data-md-carousel-item] >> nth=0', 'ArrowLeft')
|
// A retried press drifts the carousel one further item each time: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(onCarousel(0, <<<'JS'
|
pressOnce($page)->keys('[data-md-carousel-item] >> nth=0', 'ArrowLeft');
|
||||||
|
|
||||||
|
$page->assertScript(onCarousel(0, <<<'JS'
|
||||||
return scroller.scrollLeft < -1 && at(1) && ! open(0) && open(1)
|
return scroller.scrollLeft < -1 && at(1) && ! open(0) && open(1)
|
||||||
&& parseFloat(surface(0).style.getPropertyValue('--material-carousel-shift')) < 0
|
&& parseFloat(surface(0).style.getPropertyValue('--material-carousel-shift')) < 0
|
||||||
JS, 'body'));
|
JS, 'body'));
|
||||||
|
|
||||||
$page->click('button[aria-label="Next"]')
|
pressOnce($page)->click('button[aria-label="Next"]');
|
||||||
->assertScript(onCarousel(0, 'return at(2)', 'body'));
|
|
||||||
|
|
||||||
$page->keys('[data-md-carousel-item] >> nth=2', 'ArrowRight')
|
$page->assertScript(onCarousel(0, 'return at(2)', 'body'));
|
||||||
->assertScript(onCarousel(0, 'return at(1)', 'body'));
|
|
||||||
|
pressOnce($page)->keys('[data-md-carousel-item] >> nth=2', 'ArrowRight');
|
||||||
|
|
||||||
|
$page->assertScript(onCarousel(0, 'return at(1)', 'body'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('measures itself again after a Livewire morph adds an item', function () {
|
it('measures itself again after a Livewire morph adds an item', function () {
|
||||||
@@ -253,6 +266,7 @@ it('measures itself again after a Livewire morph adds an item', function () {
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -271,8 +285,10 @@ it('measures itself again after a Livewire morph adds an item', function () {
|
|||||||
->assertScript($masked, 6)
|
->assertScript($masked, 6)
|
||||||
->assertAttribute('[data-md-carousel-item] >> nth=5', 'aria-label', '6 of 6');
|
->assertAttribute('[data-md-carousel-item] >> nth=5', 'aria-label', '6 of 6');
|
||||||
|
|
||||||
$page->click('Add')
|
// A counter a retry would double: pressOnce(), tests/Pest.php.
|
||||||
->assertScript($masked, 7)
|
pressOnce($page)->click('Add');
|
||||||
|
|
||||||
|
$page->assertScript($masked, 7)
|
||||||
->assertAttribute('[data-md-carousel-item] >> nth=6', 'aria-label', '7 of 7');
|
->assertAttribute('[data-md-carousel-item] >> nth=6', 'aria-label', '7 of 7');
|
||||||
|
|
||||||
// Scrolled once the morph has settled, so only the row's own scroll listener can re-mask the
|
// Scrolled once the morph has settled, so only the row's own scroll listener can re-mask the
|
||||||
@@ -314,26 +330,31 @@ it('moves the multi-aspect carousel one item at a time with the buttons, arrows,
|
|||||||
->assertNoJavaScriptErrors()
|
->assertNoJavaScriptErrors()
|
||||||
->assertScript(onMultiAspectCarousel('return scroller.scrollLeft === 0 && items.length > 2'));
|
->assertScript(onMultiAspectCarousel('return scroller.scrollLeft === 0 && items.length > 2'));
|
||||||
|
|
||||||
$page->click(MULTI_ASPECT_SCOPE.' button[aria-label="Next"]')
|
// A retried press drifts the carousel one further item each time: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(onMultiAspectCarousel(<<<'JS'
|
pressOnce($page)->click(MULTI_ASPECT_SCOPE.' button[aria-label="Next"]');
|
||||||
|
|
||||||
|
$page->assertScript(onMultiAspectCarousel(<<<'JS'
|
||||||
await pause(400)
|
await pause(400)
|
||||||
return scroller.scrollLeft > 0
|
return scroller.scrollLeft > 0
|
||||||
JS));
|
JS));
|
||||||
|
|
||||||
$page->click(MULTI_ASPECT_SCOPE.' button[aria-label="Previous"]')
|
pressOnce($page)->click(MULTI_ASPECT_SCOPE.' button[aria-label="Previous"]');
|
||||||
->assertScript(onMultiAspectCarousel(<<<'JS'
|
|
||||||
|
$page->assertScript(onMultiAspectCarousel(<<<'JS'
|
||||||
await pause(400)
|
await pause(400)
|
||||||
return scroller.scrollLeft === 0
|
return scroller.scrollLeft === 0
|
||||||
JS));
|
JS));
|
||||||
|
|
||||||
$page->keys(MULTI_ASPECT_SCOPE.' [data-md-carousel-item] >> nth=0', 'ArrowRight')
|
pressOnce($page)->keys(MULTI_ASPECT_SCOPE.' [data-md-carousel-item] >> nth=0', 'ArrowRight');
|
||||||
->assertScript(onMultiAspectCarousel(<<<'JS'
|
|
||||||
|
$page->assertScript(onMultiAspectCarousel(<<<'JS'
|
||||||
await pause(400)
|
await pause(400)
|
||||||
return scroller.scrollLeft > 0 && document.activeElement === items[1]
|
return scroller.scrollLeft > 0 && document.activeElement === items[1]
|
||||||
JS));
|
JS));
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowLeft')
|
pressOnce($page)->keys(':focus', 'ArrowLeft');
|
||||||
->assertScript(onMultiAspectCarousel(<<<'JS'
|
|
||||||
|
$page->assertScript(onMultiAspectCarousel(<<<'JS'
|
||||||
await pause(400)
|
await pause(400)
|
||||||
return scroller.scrollLeft === 0 && document.activeElement === items[0]
|
return scroller.scrollLeft === 0 && document.activeElement === items[0]
|
||||||
JS));
|
JS));
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ it('lets an application\'s unlayered rule beat a component default', function ()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -52,6 +53,7 @@ it('lets [hidden] beat a component\'s display', function () {
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -88,6 +90,7 @@ it('keeps the layer order even when a component file is imported before the foun
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<link rel="stylesheet" href="/cascade-layer-order-probe.css">
|
<link rel="stylesheet" href="/cascade-layer-order-probe.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -131,6 +134,7 @@ it('keeps a data-md-* attribute through a Livewire $refresh', function () {
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -146,12 +150,15 @@ it('keeps a data-md-* attribute through a Livewire $refresh', function () {
|
|||||||
$page = cascadeReady(visit('/cascade-variant-probe'))
|
$page = cascadeReady(visit('/cascade-variant-probe'))
|
||||||
->assertScript("{$button}.getAttribute('data-md-variant') === 'filled'");
|
->assertScript("{$button}.getAttribute('data-md-variant') === 'filled'");
|
||||||
|
|
||||||
$page->click('[data-test="button"]')
|
// A second press flips the variant back: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$button}.getAttribute('data-md-variant') === 'outlined'");
|
pressOnce($page)->click('[data-test="button"]');
|
||||||
|
|
||||||
|
$page->assertScript("{$button}.getAttribute('data-md-variant') === 'outlined'");
|
||||||
|
|
||||||
// Twice, so the morph is proven both ways round, not just once off the initial paint.
|
// Twice, so the morph is proven both ways round, not just once off the initial paint.
|
||||||
$page->click('[data-test="button"]')
|
pressOnce($page)->click('[data-test="button"]');
|
||||||
->assertScript("{$button}.getAttribute('data-md-variant') === 'filled'");
|
|
||||||
|
$page->assertScript("{$button}.getAttribute('data-md-variant') === 'filled'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('mirrors an icon in a right-to-left document, not in left-to-right', function (string $dir, string $transform) {
|
it('mirrors an icon in a right-to-left document, not in left-to-right', function (string $dir, string $transform) {
|
||||||
@@ -159,6 +166,7 @@ it('mirrors an icon in a right-to-left document, not in left-to-right', function
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html dir="{$dir}">
|
<html dir="{$dir}">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
|
|||||||
+34
-16
@@ -57,6 +57,7 @@ function chipProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -89,8 +90,10 @@ it('toggles a filter chip with a click and with Space, and grows its check in',
|
|||||||
|
|
||||||
$page = chipShowcase()->assertNoJavaScriptErrors();
|
$page = chipShowcase()->assertNoJavaScriptErrors();
|
||||||
|
|
||||||
$page->click('#chips label:has-text("Documents")')
|
// A filter chip toggles on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(filterInput('documents').'.checked')
|
pressOnce($page)->click('#chips label:has-text("Documents")');
|
||||||
|
|
||||||
|
$page->assertScript(filterInput('documents').'.checked')
|
||||||
->assertScript("Math.round({$check('documents')}) === 18")
|
->assertScript("Math.round({$check('documents')}) === 18")
|
||||||
->assertScript('getComputedStyle('.filterInput('documents').".parentElement).backgroundColor !== 'rgba(0, 0, 0, 0)'")
|
->assertScript('getComputedStyle('.filterInput('documents').".parentElement).backgroundColor !== 'rgba(0, 0, 0, 0)'")
|
||||||
->assertScript("window.eval(\"Alpine.\$data(document.querySelector('#chips input[value=documents]')).kinds.join(',')\") === 'photos,documents'");
|
->assertScript("window.eval(\"Alpine.\$data(document.querySelector('#chips input[value=documents]')).kinds.join(',')\") === 'photos,documents'");
|
||||||
@@ -99,12 +102,16 @@ it('toggles a filter chip with a click and with Space, and grows its check in',
|
|||||||
// because WebKit leaves form controls out of the Tab order unless full keyboard access is on.
|
// because WebKit leaves form controls out of the Tab order unless full keyboard access is on.
|
||||||
$page->keys('#content', 'Tab');
|
$page->keys('#content', 'Tab');
|
||||||
$page->script(filterInput('archives').'.focus()');
|
$page->script(filterInput('archives').'.focus()');
|
||||||
$page->keys(':focus', 'Space')
|
|
||||||
->assertScript(filterInput('archives').'.checked')
|
// Space toggles a filter chip on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->keys(':focus', 'Space');
|
||||||
|
|
||||||
|
$page->assertScript(filterInput('archives').'.checked')
|
||||||
->assertScript("Math.round({$check('archives')}) === 18");
|
->assertScript("Math.round({$check('archives')}) === 18");
|
||||||
|
|
||||||
$page->keys(':focus', 'Space')
|
pressOnce($page)->keys(':focus', 'Space');
|
||||||
->assertScript('! '.filterInput('archives').'.checked')
|
|
||||||
|
$page->assertScript('! '.filterInput('archives').'.checked')
|
||||||
->assertScript("Math.round({$check('archives')}) === 0");
|
->assertScript("Math.round({$check('archives')}) === 0");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -115,8 +122,10 @@ it('binds filter chips to a Livewire array, and a server change reaches them aft
|
|||||||
|
|
||||||
$page->assertScript("{$input('photos')}.checked && ! {$input('documents')}.checked");
|
$page->assertScript("{$input('photos')}.checked && ! {$input('documents')}.checked");
|
||||||
|
|
||||||
$page->click('label:has-text("Documents")')
|
// A filter chip toggles on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#kinds', 'photos,documents');
|
pressOnce($page)->click('label:has-text("Documents")');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#kinds', 'photos,documents');
|
||||||
|
|
||||||
$page->click('button:has-text("Only video")')
|
$page->click('button:has-text("Only video")')
|
||||||
->assertSeeIn('#kinds', 'video')
|
->assertSeeIn('#kinds', 'video')
|
||||||
@@ -128,14 +137,18 @@ it('binds filter chips to a Livewire array, and a server change reaches them aft
|
|||||||
it('removes a Livewire input chip with its button and with Delete, focusing the chip after it', function () {
|
it('removes a Livewire input chip with its button and with Delete, focusing the chip after it', function () {
|
||||||
$page = chipProbe();
|
$page = chipProbe();
|
||||||
|
|
||||||
$page->click('button[aria-label="Remove js"]')
|
// The remove button vanishes once its chip is gone: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#tags', 'php,css')
|
pressOnce($page)->click('button[aria-label="Remove js"]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#tags', 'php,css')
|
||||||
->assertScript("document.querySelector('button[aria-label=\"Remove js\"]') === null");
|
->assertScript("document.querySelector('button[aria-label=\"Remove js\"]') === null");
|
||||||
|
|
||||||
$page->script("document.querySelector('button[aria-label=\"Remove php\"]').focus()");
|
$page->script("document.querySelector('button[aria-label=\"Remove php\"]').focus()");
|
||||||
|
|
||||||
$page->keys(':focus', 'Delete')
|
// Removes the focused chip, whose button then vanishes: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.querySelector('#tags').textContent === 'css'")
|
pressOnce($page)->keys(':focus', 'Delete');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#tags').textContent === 'css'")
|
||||||
->assertScript("document.querySelector('button[aria-label=\"Remove php\"]') === null")
|
->assertScript("document.querySelector('button[aria-label=\"Remove php\"]') === null")
|
||||||
->assertScript("document.activeElement.getAttribute('aria-label') === 'Remove css'");
|
->assertScript("document.activeElement.getAttribute('aria-label') === 'Remove css'");
|
||||||
});
|
});
|
||||||
@@ -149,8 +162,10 @@ it('removes an Alpine input chip with Backspace, focusing the chip before it', f
|
|||||||
$page->assertScript("{$remove('ben')} !== null");
|
$page->assertScript("{$remove('ben')} !== null");
|
||||||
$page->script("{$remove('ben')}.focus()");
|
$page->script("{$remove('ben')}.focus()");
|
||||||
|
|
||||||
$page->keys(':focus', 'Backspace')
|
// Removes the focused chip, whose button then vanishes: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$remove('ben')} === null")
|
pressOnce($page)->keys(':focus', 'Backspace');
|
||||||
|
|
||||||
|
$page->assertScript("{$remove('ben')} === null")
|
||||||
->assertScript("{$remove('anna')} !== null && {$remove('chiara')} !== null")
|
->assertScript("{$remove('anna')} !== null && {$remove('chiara')} !== null")
|
||||||
->assertScript("document.activeElement.getAttribute('aria-label') === 'Remove anna@example.com'");
|
->assertScript("document.activeElement.getAttribute('aria-label') === 'Remove anna@example.com'");
|
||||||
});
|
});
|
||||||
@@ -215,6 +230,7 @@ function chipScrollProbe(string $dir)
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html dir="{{ request('dir') }}">
|
<html dir="{{ request('dir') }}">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -243,8 +259,10 @@ it('shows a scroll button only on the edge the row can still scroll towards, in
|
|||||||
->assertScript("{$row}.scrollWidth > {$row}.clientWidth")
|
->assertScript("{$row}.scrollWidth > {$row}.clientWidth")
|
||||||
->assertScript("! ({$shown('start')}) && ({$shown('end')})");
|
->assertScript("! ({$shown('start')}) && ({$shown('end')})");
|
||||||
|
|
||||||
$page->click('[data-md-chip-scroll="end"]')
|
// A paging key a retry would page twice: pressOnce(), tests/Pest.php.
|
||||||
->wait(0.8)
|
pressOnce($page)->click('[data-md-chip-scroll="end"]');
|
||||||
|
|
||||||
|
$page->wait(0.8)
|
||||||
->assertScript("Math.abs({$row}.scrollLeft) > 0")
|
->assertScript("Math.abs({$row}.scrollLeft) > 0")
|
||||||
->assertScript("({$shown('start')})");
|
->assertScript("({$shown('start')})");
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,11 @@ it('previews a profile from the picker and the showcase menu, and keeps it throu
|
|||||||
->assertScript(pagePrimary()." === '{$this->profiles['rose']['light']['primary']}'")
|
->assertScript(pagePrimary()." === '{$this->profiles['rose']['light']['primary']}'")
|
||||||
->assertScript("window.eval(\"Alpine.store('theme').scheme\") === 'rose'");
|
->assertScript("window.eval(\"Alpine.store('theme').scheme\") === 'rose'");
|
||||||
|
|
||||||
$page->click('[data-test="showcase-profiles"] [aria-haspopup="menu"]')
|
// Opens the menu, then an item closes it: pressOnce(), tests/Pest.php.
|
||||||
->click('[data-scheme-preview="teal"]')
|
pressOnce($page)->click('[data-test="showcase-profiles"] [aria-haspopup="menu"]')
|
||||||
->assertScript("document.documentElement.getAttribute('data-scheme') === 'teal'");
|
->click('[data-scheme-preview="teal"]');
|
||||||
|
|
||||||
|
$page->assertScript("document.documentElement.getAttribute('data-scheme') === 'teal'");
|
||||||
|
|
||||||
$page->click('[data-md-navigation-rail-panel] a[href$="/material/buttons"]')
|
$page->click('[data-md-navigation-rail-panel] a[href$="/material/buttons"]')
|
||||||
->assertScript("location.pathname.endsWith('/material/buttons')")
|
->assertScript("location.pathname.endsWith('/material/buttons')")
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ it('runs a toast\'s action and dismisses it', function () {
|
|||||||
|
|
||||||
$page->script("window.__undone = false; materialToast('Share deleted', { action: { label: 'Undo', handler: () => window.__undone = true } })");
|
$page->script("window.__undone = false; materialToast('Share deleted', { action: { label: 'Undo', handler: () => window.__undone = true } })");
|
||||||
|
|
||||||
$page->click('[x-data="materialSnackbar"] button:has-text("Undo")')
|
// The Undo button vanishes once the toast is dismissed: pressOnce(), tests/Pest.php.
|
||||||
->assertScript('window.__undone === true')
|
pressOnce($page)->click('[x-data="materialSnackbar"] button:has-text("Undo")');
|
||||||
|
|
||||||
|
$page->assertScript('window.__undone === true')
|
||||||
->assertScript(TOAST.' === null')
|
->assertScript(TOAST.' === null')
|
||||||
->assertScript(SNACKBAR.' !== null');
|
->assertScript(SNACKBAR.' !== null');
|
||||||
});
|
});
|
||||||
@@ -112,16 +114,20 @@ it('keeps a sticky toast aside while a passing one shows, brings back the newest
|
|||||||
|
|
||||||
$page->assertScript(TOAST."?.textContent.includes('Version 2 is ready')");
|
$page->assertScript(TOAST."?.textContent.includes('Version 2 is ready')");
|
||||||
|
|
||||||
$page->click('[data-md-toast-dismiss]')
|
// The dismiss button vanishes with its toast: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(TOAST.' === null');
|
pressOnce($page)->click('[data-md-toast-dismiss]');
|
||||||
|
|
||||||
|
$page->assertScript(TOAST.' === null');
|
||||||
|
|
||||||
// Dismissed rather than left to time out: the pointer still rests where the snackbar appears,
|
// Dismissed rather than left to time out: the pointer still rests where the snackbar appears,
|
||||||
// and hovering pauses it.
|
// and hovering pauses it.
|
||||||
$page->script("window.eval(\"materialToast('Link copied', { timeout: 0 })\")");
|
$page->script("window.eval(\"materialToast('Link copied', { timeout: 0 })\")");
|
||||||
|
|
||||||
$page->assertScript(TOAST."?.textContent.includes('Link copied')")
|
$page->assertScript(TOAST."?.textContent.includes('Link copied')");
|
||||||
->click('[data-md-toast-dismiss]')
|
|
||||||
->assertScript(TOAST.' === null');
|
pressOnce($page)->click('[data-md-toast-dismiss]');
|
||||||
|
|
||||||
|
$page->assertScript(TOAST.' === null');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dispatches a toast action\'s window event alongside its handler, and closes', function () {
|
it('dispatches a toast action\'s window event alongside its handler, and closes', function () {
|
||||||
@@ -129,8 +135,10 @@ it('dispatches a toast action\'s window event alongside its handler, and closes'
|
|||||||
|
|
||||||
$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++ } })\")");
|
$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++ } })\")");
|
||||||
|
|
||||||
$page->click('[data-md-toast-action]')
|
// The action closes the toast: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(TOAST.' === null')
|
pressOnce($page)->click('[data-md-toast-action]');
|
||||||
|
|
||||||
|
$page->assertScript(TOAST.' === null')
|
||||||
->assertScript("window.eval('window.reloads') === 1")
|
->assertScript("window.eval('window.reloads') === 1")
|
||||||
->assertScript("window.eval('window.handled') === 1");
|
->assertScript("window.eval('window.handled') === 1");
|
||||||
});
|
});
|
||||||
@@ -144,9 +152,12 @@ it('keeps an actioned snackbar on screen past the default timeout, until it is a
|
|||||||
$page->assertScript(TOAST."?.textContent.includes('Share deleted')")
|
$page->assertScript(TOAST."?.textContent.includes('Share deleted')")
|
||||||
->wait(4.5);
|
->wait(4.5);
|
||||||
|
|
||||||
$page->assertScript(TOAST."?.textContent.includes('Share deleted')")
|
$page->assertScript(TOAST."?.textContent.includes('Share deleted')");
|
||||||
->click('[data-md-toast-dismiss]')
|
|
||||||
->assertScript(TOAST.' === null');
|
// The dismiss button vanishes with its toast: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-toast-dismiss]');
|
||||||
|
|
||||||
|
$page->assertScript(TOAST.' === null');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('dismisses a focused snackbar with Escape', function () {
|
it('dismisses a focused snackbar with Escape', function () {
|
||||||
@@ -209,9 +220,12 @@ 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 () {
|
it('opens a persistent rich tooltip on press', function () {
|
||||||
$bubble = "document.querySelector('#communication [role=\"dialog\"][popover]')";
|
$bubble = "document.querySelector('#communication [role=\"dialog\"][popover]')";
|
||||||
|
|
||||||
ready(visit('/material/communication'))
|
$page = ready(visit('/material/communication'));
|
||||||
->click('#communication button:has-text("Press for details")')
|
|
||||||
->assertScript("{$bubble}.matches(':popover-open')");
|
// Opens the persistent tooltip over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#communication button:has-text("Press for details")');
|
||||||
|
|
||||||
|
$page->assertScript("{$bubble}.matches(':popover-open')");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('closes a transient rich tooltip 1.5s after focus leaves, not at once', function () {
|
it('closes a transient rich tooltip 1.5s after focus leaves, not at once', function () {
|
||||||
@@ -245,6 +259,7 @@ it('keeps a rich tooltip open while its action renders the component, and opens
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -262,10 +277,14 @@ it('keeps a rich tooltip open while its action renders the component, and opens
|
|||||||
$page = ready(visit('/rich-tooltip-morph-probe'))
|
$page = ready(visit('/rich-tooltip-morph-probe'))
|
||||||
->assertNoJavaScriptErrors();
|
->assertNoJavaScriptErrors();
|
||||||
|
|
||||||
$page->click('@details')
|
// Opens the tooltip, then a counter a retry would double: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$persistent}.matches(':popover-open')")
|
pressOnce($page)->click('@details');
|
||||||
->click('@refresh')
|
|
||||||
->assertSeeIn('#renders', '1')
|
$page->assertScript("{$persistent}.matches(':popover-open')");
|
||||||
|
|
||||||
|
pressOnce($page)->click('@refresh');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#renders', '1')
|
||||||
->assertScript("{$persistent}.matches(':popover-open')");
|
->assertScript("{$persistent}.matches(':popover-open')");
|
||||||
|
|
||||||
// Past the reopen guard (rich-tooltip.js's REOPEN_GUARD_MS), which takes a press this soon after
|
// Past the reopen guard (rich-tooltip.js's REOPEN_GUARD_MS), which takes a press this soon after
|
||||||
@@ -274,8 +293,9 @@ it('keeps a rich tooltip open while its action renders the component, and opens
|
|||||||
->assertScript("! {$persistent}.matches(':popover-open')")
|
->assertScript("! {$persistent}.matches(':popover-open')")
|
||||||
->wait(0.3);
|
->wait(0.3);
|
||||||
|
|
||||||
$page->click('@details')
|
pressOnce($page)->click('@details');
|
||||||
->assertScript("{$persistent}.matches(':popover-open')");
|
|
||||||
|
$page->assertScript("{$persistent}.matches(':popover-open')");
|
||||||
|
|
||||||
$page->click('#outside')
|
$page->click('#outside')
|
||||||
->hover('@hint')
|
->hover('@hint')
|
||||||
@@ -291,9 +311,13 @@ it('fades a persistent rich tooltip out when a second press closes it', function
|
|||||||
// Slow tokens, so the exit copy (resources/js/popover-exit.js) is still on screen after the round trip.
|
// 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>')");
|
$page->script("document.head.insertAdjacentHTML('beforeend', '<style>:root { --md-sys-motion-effects-fast-duration: 1500ms; }</style>')");
|
||||||
|
|
||||||
$page->click('#communication button:has-text("Press for details")')
|
// The trigger toggles the tooltip, so each press is pressed once: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$bubble}.matches(':popover-open')")
|
pressOnce($page)->click('#communication button:has-text("Press for details")');
|
||||||
->click('#communication button:has-text("Press for details")')
|
|
||||||
->assertScript("! {$bubble}.matches(':popover-open')")
|
$page->assertScript("{$bubble}.matches(':popover-open')");
|
||||||
|
|
||||||
|
pressOnce($page)->click('#communication button:has-text("Press for details")');
|
||||||
|
|
||||||
|
$page->assertScript("! {$bubble}.matches(':popover-open')")
|
||||||
->assertScript("(() => { const copy = document.querySelector('[data-md-popover-ghost]'); const opacity = copy && parseFloat(getComputedStyle(copy).opacity); return copy !== null && copy.hasAttribute('data-md-rich-tooltip-bubble') && copy.inert && opacity > 0.02 && opacity < 0.98; })()");
|
->assertScript("(() => { const copy = document.querySelector('[data-md-popover-ghost]'); const opacity = copy && parseFloat(getComputedStyle(copy).opacity); return copy !== null && copy.hasAttribute('data-md-rich-tooltip-bubble') && copy.inert && opacity > 0.02 && opacity < 0.98; })()");
|
||||||
});
|
});
|
||||||
|
|||||||
+234
-124
@@ -93,6 +93,7 @@ function collapseProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -115,6 +116,7 @@ function overlayProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -137,19 +139,25 @@ function containment()
|
|||||||
it('writes back what closed means: false for a flag, null for an id', function () {
|
it('writes back what closed means: false for a flag, null for an id', function () {
|
||||||
$page = overlayProbe();
|
$page = overlayProbe();
|
||||||
|
|
||||||
$page->click('button:has-text("Confirm")')
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Are you sure?'))");
|
pressOnce($page)->click('button:has-text("Confirm")');
|
||||||
|
|
||||||
$page->click('dialog[open] button:has-text("Re-render")')
|
$page->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Are you sure?'))");
|
||||||
->assertSeeIn('#renders', '1')
|
|
||||||
|
// A counter a retry would double: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('dialog[open] button:has-text("Re-render")');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#renders', '1')
|
||||||
->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Are you sure?'))");
|
->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Are you sure?'))");
|
||||||
|
|
||||||
$page->keys('dialog[open] button:has-text("Re-render")', 'Escape')
|
$page->keys('dialog[open] button:has-text("Re-render")', 'Escape')
|
||||||
->assertScript("! [...document.querySelectorAll('dialog')].some((d) => d.open)")
|
->assertScript("! [...document.querySelectorAll('dialog')].some((d) => d.open)")
|
||||||
->assertSeeIn('#confirming', 'false');
|
->assertSeeIn('#confirming', 'false');
|
||||||
|
|
||||||
$page->click('button:has-text("Delete 7")')
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#deleting', '7')
|
pressOnce($page)->click('button:has-text("Delete 7")');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#deleting', '7')
|
||||||
->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Delete share?'))");
|
->assertScript("[...document.querySelectorAll('dialog')].some((d) => d.open && d.textContent.includes('Delete share?'))");
|
||||||
|
|
||||||
$page->script("document.querySelector('dialog[open]').dispatchEvent(new Event('cancel', { cancelable: true }))");
|
$page->script("document.querySelector('dialog[open]').dispatchEvent(new Event('cancel', { cancelable: true }))");
|
||||||
@@ -164,8 +172,11 @@ it('closes a showcase dialog on Escape and gives focus back to its trigger', fun
|
|||||||
|
|
||||||
$page->script("[...document.querySelectorAll('#containment button')].find((b) => b.textContent.trim() === 'Basic dialog').focus()");
|
$page->script("[...document.querySelectorAll('#containment button')].find((b) => b.textContent.trim() === 'Basic dialog').focus()");
|
||||||
|
|
||||||
$page->keys(':focus', 'Enter')
|
// Enter on the focused button is a native click on it, opening the dialog over it: pressOnce(),
|
||||||
->assertScript("document.querySelector('#containment dialog').open");
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->keys(':focus', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#containment dialog').open");
|
||||||
|
|
||||||
$page->keys('#containment dialog[open] button:has-text("Cancel")', 'Escape')
|
$page->keys('#containment dialog[open] button:has-text("Cancel")', 'Escape')
|
||||||
->assertScript("! document.querySelector('#containment dialog').open")
|
->assertScript("! document.querySelector('#containment dialog').open")
|
||||||
@@ -175,9 +186,12 @@ it('closes a showcase dialog on Escape and gives focus back to its trigger', fun
|
|||||||
it('slides a side sheet in, traps the page, and closes on the scrim', function () {
|
it('slides a side sheet in, traps the page, and closes on the scrim', function () {
|
||||||
$sheet = "document.querySelector('#containment aside[role=\"dialog\"]')";
|
$sheet = "document.querySelector('#containment aside[role=\"dialog\"]')";
|
||||||
|
|
||||||
$page = containment()
|
$page = containment();
|
||||||
->click('#containment button:has-text("Side sheet")')
|
|
||||||
->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#containment button:has-text("Side sheet")');
|
||||||
|
|
||||||
|
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
||||||
->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') !== null");
|
->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') !== null");
|
||||||
|
|
||||||
// This sheet's own scrim: the standard side sheet above it on the page is a `[data-md-drawer]` too.
|
// This sheet's own scrim: the standard side sheet above it on the page is a `[data-md-drawer]` too.
|
||||||
@@ -190,9 +204,12 @@ it('slides a side sheet in, traps the page, and closes on the scrim', function (
|
|||||||
it('dismisses a bottom sheet dragged down past a quarter of its height', function () {
|
it('dismisses a bottom sheet dragged down past a quarter of its height', function () {
|
||||||
$sheet = "document.querySelector('#containment section[role=\"dialog\"]')";
|
$sheet = "document.querySelector('#containment section[role=\"dialog\"]')";
|
||||||
|
|
||||||
$page = containment()
|
$page = containment();
|
||||||
->click('#containment button:has(> span:text-is("Bottom sheet"))')
|
|
||||||
->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#containment button:has(> span:text-is("Bottom sheet"))');
|
||||||
|
|
||||||
|
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
||||||
->wait(0.5);
|
->wait(0.5);
|
||||||
|
|
||||||
$page->script(<<<JS
|
$page->script(<<<JS
|
||||||
@@ -251,6 +268,7 @@ function nestedDialogProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -269,7 +287,8 @@ it('marks a scrolling dialog\'s divider at the edge with more content, and a nes
|
|||||||
// Short and narrow so the probes' paragraphs genuinely overflow their body.
|
// Short and narrow so the probes' paragraphs genuinely overflow their body.
|
||||||
$page = nestedDialogProbe()->resize(320, 300);
|
$page = nestedDialogProbe()->resize(320, 300);
|
||||||
|
|
||||||
$page->click('button:has-text("Open outer")');
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button:has-text("Open outer")');
|
||||||
|
|
||||||
// Matched by each dialog's own title element, not its full (recursive) textContent: the
|
// Matched by each dialog's own title element, not its full (recursive) textContent: the
|
||||||
// outer dialog's textContent also contains the nested inner dialog's text, "Inner" included,
|
// outer dialog's textContent also contains the nested inner dialog's text, "Inner" included,
|
||||||
@@ -281,7 +300,8 @@ it('marks a scrolling dialog\'s divider at the edge with more content, and a nes
|
|||||||
// Scrolled to the top: nothing hidden above, so the head's rule stays off.
|
// Scrolled to the top: nothing hidden above, so the head's rule stays off.
|
||||||
$page->assertScript("getComputedStyle({$outerHead}, '::after').opacity === '0'");
|
$page->assertScript("getComputedStyle({$outerHead}, '::after').opacity === '0'");
|
||||||
|
|
||||||
$page->click('button:has-text("Open inner")');
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button:has-text("Open inner")');
|
||||||
|
|
||||||
$inner = "[...document.querySelectorAll('dialog[open]')].find((d) => d.querySelector('[data-md-modal-title]')?.textContent === 'Inner')";
|
$inner = "[...document.querySelectorAll('dialog[open]')].find((d) => d.querySelector('[data-md-modal-title]')?.textContent === 'Inner')";
|
||||||
$innerHead = "{$inner}.querySelector('[data-md-modal-head]')";
|
$innerHead = "{$inner}.querySelector('[data-md-modal-head]')";
|
||||||
@@ -308,7 +328,8 @@ it('shows a full-screen dialog\'s 56px phone bar below 600px, and the basic dial
|
|||||||
// CSS on the fullscreen flag, so which comes first makes no difference to what is asserted.
|
// CSS on the fullscreen flag, so which comes first makes no difference to what is asserted.
|
||||||
$page = containment();
|
$page = containment();
|
||||||
|
|
||||||
$page->click('#containment button:has-text("Full screen on a phone")');
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#containment button:has-text("Full screen on a phone")');
|
||||||
|
|
||||||
$page->resize(400, 800);
|
$page->resize(400, 800);
|
||||||
|
|
||||||
@@ -325,7 +346,8 @@ it('shows a full-screen dialog\'s 56px phone bar below 600px, and the basic dial
|
|||||||
it('draws separator dividers whatever the scroll, on a dialog that fits without scrolling', function () {
|
it('draws separator dividers whatever the scroll, on a dialog that fits without scrolling', function () {
|
||||||
$page = containment();
|
$page = containment();
|
||||||
|
|
||||||
$page->click('#containment button:has-text("Always divided (separator)")');
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#containment button:has-text("Always divided (separator)")');
|
||||||
|
|
||||||
$dialog = "document.querySelector('#containment dialog[open]')";
|
$dialog = "document.querySelector('#containment dialog[open]')";
|
||||||
$head = "{$dialog}.querySelector('[data-md-modal-head]')";
|
$head = "{$dialog}.querySelector('[data-md-modal-head]')";
|
||||||
@@ -345,7 +367,8 @@ it('opens a dialog with no fade under reduced motion, but a real one without it'
|
|||||||
$normal = containment()
|
$normal = containment()
|
||||||
->assertScript("! window.matchMedia('(prefers-reduced-motion: reduce)').matches");
|
->assertScript("! window.matchMedia('(prefers-reduced-motion: reduce)').matches");
|
||||||
|
|
||||||
$normal->click('#containment button:has-text("Basic dialog")');
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($normal)->click('#containment button:has-text("Basic dialog")');
|
||||||
|
|
||||||
$normal->assertScript("parseFloat(getComputedStyle({$dialog}).transitionDuration) > 0");
|
$normal->assertScript("parseFloat(getComputedStyle({$dialog}).transitionDuration) > 0");
|
||||||
|
|
||||||
@@ -355,7 +378,8 @@ it('opens a dialog with no fade under reduced motion, but a real one without it'
|
|||||||
$reduced = ready(visit('/material/containment', ['reducedMotion' => 'reduce']))
|
$reduced = ready(visit('/material/containment', ['reducedMotion' => 'reduce']))
|
||||||
->assertScript("window.matchMedia('(prefers-reduced-motion: reduce)').matches");
|
->assertScript("window.matchMedia('(prefers-reduced-motion: reduce)').matches");
|
||||||
|
|
||||||
$reduced->click('#containment button:has-text("Basic dialog")');
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($reduced)->click('#containment button:has-text("Basic dialog")');
|
||||||
|
|
||||||
$reduced->assertScript("parseFloat(getComputedStyle({$dialog}).transitionDuration) === 0")
|
$reduced->assertScript("parseFloat(getComputedStyle({$dialog}).transitionDuration) === 0")
|
||||||
->assertScript("getComputedStyle({$dialog}).opacity === '1'");
|
->assertScript("getComputedStyle({$dialog}).opacity === '1'");
|
||||||
@@ -387,6 +411,7 @@ function textOnlyDialogProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -408,7 +433,9 @@ it('gives the scrolling body an inset focus ring when a text-only dialog opens f
|
|||||||
|
|
||||||
$page->script("document.querySelector('button').focus()");
|
$page->script("document.querySelector('button').focus()");
|
||||||
|
|
||||||
$page->keys(':focus', 'Enter');
|
// Enter on the focused button is a native click on it, opening the dialog over it: pressOnce(),
|
||||||
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->keys(':focus', 'Enter');
|
||||||
|
|
||||||
$body = "document.querySelector('dialog[open] [data-md-modal-body]')";
|
$body = "document.querySelector('dialog[open] [data-md-modal-body]')";
|
||||||
|
|
||||||
@@ -428,24 +455,34 @@ it('gives the scrolling body an inset focus ring when a text-only dialog opens f
|
|||||||
it('cycles a bottom sheet\'s preset heights from its handle, announcing each, and closes from the last', function () {
|
it('cycles a bottom sheet\'s preset heights from its handle, announcing each, and closes from the last', function () {
|
||||||
$page = containment();
|
$page = containment();
|
||||||
|
|
||||||
$page->click('#containment button:has-text("Bottom sheet with preset heights")');
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#containment button:has-text("Bottom sheet with preset heights")');
|
||||||
|
|
||||||
$sheet = "document.querySelector('#containment [data-md-bottom-sheet-panel][data-md-preset]')";
|
$sheet = "document.querySelector('#containment [data-md-bottom-sheet-panel][data-md-preset]')";
|
||||||
$handle = "{$sheet}.querySelector('[data-md-bottom-sheet-grip]')";
|
$handle = "{$sheet}.querySelector('[data-md-bottom-sheet-grip]')";
|
||||||
$announce = "{$sheet}.querySelector('[data-md-bottom-sheet-announce]')";
|
$announce = "{$sheet}.querySelector('[data-md-bottom-sheet-announce]')";
|
||||||
|
|
||||||
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
||||||
->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '50dvh'");
|
->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '50dvh'")
|
||||||
|
// Settled, not still sliding in: a grip pressed while the entry is still under way would
|
||||||
|
// read as unstable on a loaded runner, the way opening it does elsewhere in this file.
|
||||||
|
->assertScript(settled($sheet, subtree: true));
|
||||||
|
|
||||||
$gripSelector = '#containment [data-md-bottom-sheet-panel][data-md-preset] [data-md-bottom-sheet-grip]';
|
$gripSelector = '#containment [data-md-bottom-sheet-panel][data-md-preset] [data-md-bottom-sheet-grip]';
|
||||||
|
|
||||||
$page->click($gripSelector);
|
// Each press moves the sheet to its next stop, and from the last one closes it, so a press
|
||||||
|
// repeated by the plugin's retry would skip a stop: pressed once, pressOnce() in tests/Pest.php.
|
||||||
|
pressOnce($page)->click($gripSelector);
|
||||||
|
|
||||||
$page->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '90dvh'")
|
$page->assertScript("{$sheet}.style.getPropertyValue('--sheet-stop').trim() === '90dvh'")
|
||||||
->assertScript("{$announce}.textContent.trim() === 'Height 3 of 3'");
|
->assertScript("{$announce}.textContent.trim() === 'Height 3 of 3'")
|
||||||
|
// The stop is written before the sheet has grown to it, so wait for the growth as well as
|
||||||
|
// for the entry above: the grip travels with the sheet's edge, and a press that lands
|
||||||
|
// while it is moving never reads as stable.
|
||||||
|
->assertScript(settled($sheet, subtree: true));
|
||||||
|
|
||||||
// From the last stop, activating the handle closes the sheet, as a handle with no stops does.
|
// From the last stop, activating the handle closes the sheet, as a handle with no stops does.
|
||||||
$page->click($gripSelector);
|
pressOnce($page)->click($gripSelector);
|
||||||
|
|
||||||
$page->assertScript("getComputedStyle({$sheet}).display === 'none'");
|
$page->assertScript("getComputedStyle({$sheet}).display === 'none'");
|
||||||
});
|
});
|
||||||
@@ -453,7 +490,8 @@ it('cycles a bottom sheet\'s preset heights from its handle, announcing each, an
|
|||||||
it('settles a dragged bottom sheet on the nearest preset height', function () {
|
it('settles a dragged bottom sheet on the nearest preset height', function () {
|
||||||
$page = containment();
|
$page = containment();
|
||||||
|
|
||||||
$page->click('#containment button:has-text("Bottom sheet with preset heights")');
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#containment button:has-text("Bottom sheet with preset heights")');
|
||||||
|
|
||||||
$sheet = "document.querySelector('#containment [data-md-bottom-sheet-panel][data-md-preset]')";
|
$sheet = "document.querySelector('#containment [data-md-bottom-sheet-panel][data-md-preset]')";
|
||||||
|
|
||||||
@@ -490,13 +528,15 @@ it('is a standard side sheet from 840px, without a scrim or a focus trap, and th
|
|||||||
->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') === null")
|
->assertScript("document.querySelector('header').closest('[aria-hidden=\"true\"]') === null")
|
||||||
->assertScript("! {$sheet}.hasAttribute('inert')");
|
->assertScript("! {$sheet}.hasAttribute('inert')");
|
||||||
|
|
||||||
$page->click($toggle);
|
// The toggle button opens or closes the sheet on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click($toggle);
|
||||||
|
|
||||||
$page->assertScript("getComputedStyle({$sheet}).opacity === '0'");
|
$page->assertScript("getComputedStyle({$sheet}).opacity === '0'");
|
||||||
|
|
||||||
// Below 840px the same sheet becomes the modal one: fixed, with a scrim, and the page inert.
|
// Below 840px the same sheet becomes the modal one: fixed, with a scrim, and the page inert.
|
||||||
$page->resize(700, 800)
|
$page->resize(700, 800);
|
||||||
->click($toggle);
|
|
||||||
|
pressOnce($page)->click($toggle);
|
||||||
|
|
||||||
$page->assertScript("getComputedStyle({$sheet}).position === 'fixed'")
|
$page->assertScript("getComputedStyle({$sheet}).position === 'fixed'")
|
||||||
->assertScript("getComputedStyle({$scrim}).display !== 'none'")
|
->assertScript("getComputedStyle({$scrim}).display !== 'none'")
|
||||||
@@ -512,35 +552,35 @@ it('gives a closing standard side sheet\'s room back to the content beside it as
|
|||||||
// The example starts open, and stands open from the first frame Alpine draws: full 400px, fully
|
// The example starts open, and stands open from the first frame Alpine draws: full 400px, fully
|
||||||
// shown, nothing on it animating.
|
// shown, nothing on it animating.
|
||||||
$page = containment()->resize(1000, 800)
|
$page = containment()->resize(1000, 800)
|
||||||
->assertScript("{$root}.hasAttribute('data-md-open') && Math.round({$root}.getBoundingClientRect().width) === 400 && getComputedStyle({$sheet}).opacity === '1' && {$root}.getAnimations({ subtree: true }).length === 0");
|
->assertScript("{$root}.hasAttribute('data-md-open') && Math.round({$root}.getBoundingClientRect().width) === 400 && getComputedStyle({$sheet}).opacity === '1'")
|
||||||
|
->assertScript(settled($root, subtree: true))
|
||||||
|
// Only once the view has settled, two frames after Alpine starts, which WebKit can still
|
||||||
|
// be waiting for here: until then the sheet has no transitions, on purpose, and closes at
|
||||||
|
// once.
|
||||||
|
->assertScript("{$root}.hasAttribute('data-md-drawer-settled')");
|
||||||
|
|
||||||
// Closed and sampled in one round trip: the sheet's root caught part-way between its width
|
// Closed and sampled in one round trip: the sheet's root caught part-way between its width
|
||||||
// and none, still in the layout, while the column beside it has grown part of the way — not
|
// and none, still in the layout, while the column beside it has grown part of the way — not
|
||||||
// the whole sheet and its gap handed back in one jump at the end. Only once the view has
|
// the whole sheet and its gap handed back in one jump at the end. Stretched, as every other
|
||||||
// settled, two frames after Alpine starts, which WebKit can still be waiting for here: until
|
// mid-exit sample is: a loaded engine can paint too few frames in the real exit to see it.
|
||||||
// then the sheet has no transitions, on purpose, and closes at once.
|
slowMotion($page);
|
||||||
$midExit = $page->script(<<<JS
|
|
||||||
(async () => {
|
$midExit = $page->script(caughtMidExit(
|
||||||
|
<<<JS
|
||||||
const root = {$root}
|
const root = {$root}
|
||||||
const column = {$column}
|
const column = {$column}
|
||||||
|
|
||||||
for (let i = 0; i < 100 && ! root.hasAttribute('data-md-drawer-settled'); i++) {
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 10))
|
|
||||||
}
|
|
||||||
|
|
||||||
const open = { root: root.getBoundingClientRect().width, column: column.getBoundingClientRect().width, row: root.parentElement.getBoundingClientRect().width }
|
const open = { root: root.getBoundingClientRect().width, column: column.getBoundingClientRect().width, row: root.parentElement.getBoundingClientRect().width }
|
||||||
{$toggle}.click()
|
{$toggle}.click()
|
||||||
|
JS,
|
||||||
for (let i = 0; i < 80; i++) {
|
<<<'JS'
|
||||||
|
(() => {
|
||||||
const width = root.getBoundingClientRect().width
|
const width = root.getBoundingClientRect().width
|
||||||
const columnWidth = column.getBoundingClientRect().width
|
const columnWidth = column.getBoundingClientRect().width
|
||||||
|
|
||||||
if (! root.hasAttribute('data-md-drawer-collapsed') && getComputedStyle(root).display !== 'none' && width > 1 && width < open.root - 1 && columnWidth > open.column + 1 && columnWidth < open.row - 1) return true
|
return ! root.hasAttribute('data-md-drawer-collapsed') && getComputedStyle(root).display !== 'none' && width > 1 && width < open.root - 1 && columnWidth > open.column + 1 && columnWidth < open.row - 1
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})()
|
})()
|
||||||
JS);
|
JS,
|
||||||
|
));
|
||||||
|
|
||||||
expect($midExit)->toBeTrue();
|
expect($midExit)->toBeTrue();
|
||||||
|
|
||||||
@@ -548,6 +588,9 @@ it('gives a closing standard side sheet\'s room back to the content beside it as
|
|||||||
$page->assertScript("{$root}.hasAttribute('data-md-drawer-collapsed') && getComputedStyle({$root}).display === 'none'")
|
$page->assertScript("{$root}.hasAttribute('data-md-drawer-collapsed') && getComputedStyle({$root}).display === 'none'")
|
||||||
->assertScript("Math.abs({$column}.getBoundingClientRect().width - {$root}.parentElement.getBoundingClientRect().width) < 1");
|
->assertScript("Math.abs({$column}.getBoundingClientRect().width - {$root}.parentElement.getBoundingClientRect().width) < 1");
|
||||||
|
|
||||||
|
// What follows times a reopen against the real exit.
|
||||||
|
fullSpeed($page);
|
||||||
|
|
||||||
// Reopened part-way through its exit, it ends open, not collapsed. Each script stays well under
|
// Reopened part-way through its exit, it ends open, not collapsed. Each script stays well under
|
||||||
// the browser plugin's one-second call timeout, past which it runs the script a second time.
|
// the browser plugin's one-second call timeout, past which it runs the script a second time.
|
||||||
$page->script("{$toggle}.click()");
|
$page->script("{$toggle}.click()");
|
||||||
@@ -577,6 +620,7 @@ function layersProbe(): mixed
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -644,8 +688,11 @@ function hiddenFromAssistiveTech(string $id): string
|
|||||||
it('closes only a dialog opened over a modal sheet on Escape, readable and tabbable while it is open', function () {
|
it('closes only a dialog opened over a modal sheet on Escape, readable and tabbable while it is open', function () {
|
||||||
$page = layersProbe();
|
$page = layersProbe();
|
||||||
|
|
||||||
$page->click('#open-sheet')->assertScript(sheetOpen('sheet'));
|
// Opens the sheet, then the dialog over it: pressOnce(), tests/Pest.php.
|
||||||
$page->click('#open-dialog')->assertScript("document.getElementById('dialog').open");
|
pressOnce($page)->click('#open-sheet');
|
||||||
|
$page->assertScript(sheetOpen('sheet'));
|
||||||
|
pressOnce($page)->click('#open-dialog');
|
||||||
|
$page->assertScript("document.getElementById('dialog').open");
|
||||||
|
|
||||||
// Rendered outside the sheet, the dialog is not inside what the sheet hides, and the sheet's
|
// Rendered outside the sheet, the dialog is not inside what the sheet hides, and the sheet's
|
||||||
// focus trap does not take the Tab back to the sheet.
|
// focus trap does not take the Tab back to the sheet.
|
||||||
@@ -670,14 +717,20 @@ it('closes only a dialog opened over a modal sheet on Escape, readable and tabba
|
|||||||
it('closes only the menu, select list or searchable choice open inside a modal sheet on Escape', function () {
|
it('closes only the menu, select list or searchable choice open inside a modal sheet on Escape', function () {
|
||||||
$page = layersProbe();
|
$page = layersProbe();
|
||||||
|
|
||||||
$page->click('#open-sheet')->assertScript(sheetOpen('sheet'));
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#open-sheet');
|
||||||
|
$page->assertScript(sheetOpen('sheet'));
|
||||||
|
|
||||||
$page->click('#open-menu')->assertScript("document.querySelector('#sheet [data-md-menu-popover]').matches(':popover-open')");
|
// Opens the menu over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#open-menu');
|
||||||
|
$page->assertScript("document.querySelector('#sheet [data-md-menu-popover]').matches(':popover-open')");
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertScript("! document.querySelector('#sheet [data-md-menu-popover]').matches(':popover-open')")
|
->assertScript("! document.querySelector('#sheet [data-md-menu-popover]').matches(':popover-open')")
|
||||||
->assertScript(sheetOpen('sheet'));
|
->assertScript(sheetOpen('sheet'));
|
||||||
|
|
||||||
$page->click('#sheet-choices')->assertScript("document.querySelector('#sheet [data-md-field-menu]').matches(':popover-open')");
|
// Opens the searchable choice's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#sheet-choices');
|
||||||
|
$page->assertScript("document.querySelector('#sheet [data-md-field-menu]').matches(':popover-open')");
|
||||||
$page->keys('#sheet-choices', 'Escape')
|
$page->keys('#sheet-choices', 'Escape')
|
||||||
->assertScript("! document.querySelector('#sheet [data-md-field-menu]').matches(':popover-open')")
|
->assertScript("! document.querySelector('#sheet [data-md-field-menu]').matches(':popover-open')")
|
||||||
->assertScript(sheetOpen('sheet'));
|
->assertScript(sheetOpen('sheet'));
|
||||||
@@ -685,7 +738,9 @@ it('closes only the menu, select list or searchable choice open inside a modal s
|
|||||||
// A customizable select's list, where the browser has one: elsewhere there is no list of the
|
// A customizable select's list, where the browser has one: elsewhere there is no list of the
|
||||||
// page's own open over the sheet.
|
// page's own open over the sheet.
|
||||||
if ($page->script("CSS.supports('appearance', 'base-select')") === true) {
|
if ($page->script("CSS.supports('appearance', 'base-select')") === true) {
|
||||||
$page->click('#sheet-select')->assertScript("document.getElementById('sheet-select').matches(':open')");
|
// Opens the native select's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#sheet-select');
|
||||||
|
$page->assertScript("document.getElementById('sheet-select').matches(':open')");
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertScript("! document.getElementById('sheet-select').matches(':open')")
|
->assertScript("! document.getElementById('sheet-select').matches(':open')")
|
||||||
->assertScript(sheetOpen('sheet'));
|
->assertScript(sheetOpen('sheet'));
|
||||||
@@ -697,15 +752,19 @@ it('closes only the menu, select list or searchable choice open inside a modal s
|
|||||||
it('closes only the sheet on top on Escape, inside the first or beside it, and keeps the one beside it readable', function () {
|
it('closes only the sheet on top on Escape, inside the first or beside it, and keeps the one beside it readable', function () {
|
||||||
$page = layersProbe();
|
$page = layersProbe();
|
||||||
|
|
||||||
$page->click('#open-sheet')->assertScript(sheetOpen('sheet'));
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#open-sheet');
|
||||||
|
$page->assertScript(sheetOpen('sheet'));
|
||||||
|
|
||||||
$page->click('#open-inner')->assertScript(sheetOpen('inner-sheet'));
|
pressOnce($page)->click('#open-inner');
|
||||||
|
$page->assertScript(sheetOpen('inner-sheet'));
|
||||||
$page->click('#inner-field');
|
$page->click('#inner-field');
|
||||||
$page->keys('#inner-field', 'Escape')
|
$page->keys('#inner-field', 'Escape')
|
||||||
->assertScript('! '.sheetOpen('inner-sheet'))
|
->assertScript('! '.sheetOpen('inner-sheet'))
|
||||||
->assertScript(sheetOpen('sheet'));
|
->assertScript(sheetOpen('sheet'));
|
||||||
|
|
||||||
$page->click('#open-beside')->assertScript(sheetOpen('beside-sheet'))
|
pressOnce($page)->click('#open-beside');
|
||||||
|
$page->assertScript(sheetOpen('beside-sheet'))
|
||||||
->assertScript('! '.hiddenFromAssistiveTech('beside-sheet'));
|
->assertScript('! '.hiddenFromAssistiveTech('beside-sheet'));
|
||||||
$page->click('#beside-field');
|
$page->click('#beside-field');
|
||||||
$page->keys('#beside-field', 'Escape')
|
$page->keys('#beside-field', 'Escape')
|
||||||
@@ -721,24 +780,32 @@ it('closes only the sheet, searchable choice, menu or select list open inside a
|
|||||||
$page->script("window.dispatchEvent(new CustomEvent('open-probe-dialog'))");
|
$page->script("window.dispatchEvent(new CustomEvent('open-probe-dialog'))");
|
||||||
$page->assertScript("document.getElementById('dialog').open");
|
$page->assertScript("document.getElementById('dialog').open");
|
||||||
|
|
||||||
$page->click('#open-dialog-sheet')->assertScript(sheetOpen('dialog-sheet'));
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#open-dialog-sheet');
|
||||||
|
$page->assertScript(sheetOpen('dialog-sheet'));
|
||||||
$page->click('#dialog-sheet-field');
|
$page->click('#dialog-sheet-field');
|
||||||
$page->keys('#dialog-sheet-field', 'Escape')
|
$page->keys('#dialog-sheet-field', 'Escape')
|
||||||
->assertScript('! '.sheetOpen('dialog-sheet'))
|
->assertScript('! '.sheetOpen('dialog-sheet'))
|
||||||
->assertScript("document.getElementById('dialog').open");
|
->assertScript("document.getElementById('dialog').open");
|
||||||
|
|
||||||
$page->click('#dialog-choices')->assertScript("document.querySelector('#dialog [data-md-field-menu]').matches(':popover-open')");
|
// Opens the searchable choice's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#dialog-choices');
|
||||||
|
$page->assertScript("document.querySelector('#dialog [data-md-field-menu]').matches(':popover-open')");
|
||||||
$page->keys('#dialog-choices', 'Escape')
|
$page->keys('#dialog-choices', 'Escape')
|
||||||
->assertScript("! document.querySelector('#dialog [data-md-field-menu]').matches(':popover-open')")
|
->assertScript("! document.querySelector('#dialog [data-md-field-menu]').matches(':popover-open')")
|
||||||
->assertScript("document.getElementById('dialog').open");
|
->assertScript("document.getElementById('dialog').open");
|
||||||
|
|
||||||
$page->click('#open-dialog-menu')->assertScript("document.querySelector('#dialog [data-md-menu-popover]').matches(':popover-open')");
|
// Opens the menu over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#open-dialog-menu');
|
||||||
|
$page->assertScript("document.querySelector('#dialog [data-md-menu-popover]').matches(':popover-open')");
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertScript("! document.querySelector('#dialog [data-md-menu-popover]').matches(':popover-open')")
|
->assertScript("! document.querySelector('#dialog [data-md-menu-popover]').matches(':popover-open')")
|
||||||
->assertScript("document.getElementById('dialog').open");
|
->assertScript("document.getElementById('dialog').open");
|
||||||
|
|
||||||
if ($page->script("CSS.supports('appearance', 'base-select')") === true) {
|
if ($page->script("CSS.supports('appearance', 'base-select')") === true) {
|
||||||
$page->click('#dialog-select')->assertScript("document.getElementById('dialog-select').matches(':open')");
|
// Opens the native select's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#dialog-select');
|
||||||
|
$page->assertScript("document.getElementById('dialog-select').matches(':open')");
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertScript("! document.getElementById('dialog-select').matches(':open')")
|
->assertScript("! document.getElementById('dialog-select').matches(':open')")
|
||||||
->assertScript("document.getElementById('dialog').open");
|
->assertScript("document.getElementById('dialog').open");
|
||||||
@@ -753,6 +820,7 @@ it('draws a standard side sheet that starts open standing open, without growing
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -788,6 +856,7 @@ it('sticks a standard side sheet under the top safe area and a scaffold\'s stick
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -866,6 +935,7 @@ it('draws standard side sheets as the server renders them from the first paint,
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -897,8 +967,10 @@ it('opens a row\'s opener from a press anywhere on the row, but not from its own
|
|||||||
|
|
||||||
$page->script("window.__opened = 0; document.querySelector('{$row} [data-md-list-open]').addEventListener('click', (event) => { event.preventDefault(); window.__opened++ })");
|
$page->script("window.__opened = 0; document.querySelector('{$row} [data-md-list-open]').addEventListener('click', (event) => { event.preventDefault(); window.__opened++ })");
|
||||||
|
|
||||||
$page->click("{$row} p")
|
// A counter a retry would double: pressOnce(), tests/Pest.php.
|
||||||
->assertScript('window.__opened === 1');
|
pressOnce($page)->click("{$row} p");
|
||||||
|
|
||||||
|
$page->assertScript('window.__opened === 1');
|
||||||
|
|
||||||
$page->click("{$row} button:has-text(\"Copy link\")")
|
$page->click("{$row} button:has-text(\"Copy link\")")
|
||||||
->assertScript('window.__opened === 1');
|
->assertScript('window.__opened === 1');
|
||||||
@@ -915,8 +987,10 @@ it('makes a directly actionable card the one tab stop, answering Enter', functio
|
|||||||
|
|
||||||
$page->assertScript("document.activeElement.matches('{$card}')");
|
$page->assertScript("document.activeElement.matches('{$card}')");
|
||||||
|
|
||||||
$page->keys($card, 'Enter')
|
// A counter a retry would double: pressOnce(), tests/Pest.php.
|
||||||
->assertScript('window.__opened === 1');
|
pressOnce($page)->keys($card, 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript('window.__opened === 1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('binds a collapse to a Livewire property both ways', function () {
|
it('binds a collapse to a Livewire property both ways', function () {
|
||||||
@@ -925,10 +999,15 @@ it('binds a collapse to a Livewire property both ways', function () {
|
|||||||
$page = collapseProbe()
|
$page = collapseProbe()
|
||||||
->assertScript("{$collapse}.open === false");
|
->assertScript("{$collapse}.open === false");
|
||||||
|
|
||||||
$page->click('#fine-tuning-collapse summary')
|
// A native <summary> click toggles the collapse: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$collapse}.open === true")
|
pressOnce($page)->click('#fine-tuning-collapse summary');
|
||||||
->click('button:has-text("Re-render")')
|
|
||||||
->assertSeeIn('#renders', '1')
|
$page->assertScript("{$collapse}.open === true");
|
||||||
|
|
||||||
|
// A counter a retry would double: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button:has-text("Re-render")');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#renders', '1')
|
||||||
->assertSeeIn('#fine-tuning', 'true')
|
->assertSeeIn('#fine-tuning', 'true')
|
||||||
->assertScript("{$collapse}.open === true");
|
->assertScript("{$collapse}.open === true");
|
||||||
|
|
||||||
@@ -947,8 +1026,10 @@ it('binds a collapse to an Alpine property both ways', function () {
|
|||||||
$page = collapseProbe()
|
$page = collapseProbe()
|
||||||
->assertScript("{$collapse}.open === false");
|
->assertScript("{$collapse}.open === false");
|
||||||
|
|
||||||
$page->click('#advanced-collapse summary')
|
// A native <summary> click toggles the collapse: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$collapse}.open === true")
|
pressOnce($page)->click('#advanced-collapse summary');
|
||||||
|
|
||||||
|
$page->assertScript("{$collapse}.open === true")
|
||||||
->assertSeeIn('#advanced', 'true');
|
->assertSeeIn('#advanced', 'true');
|
||||||
|
|
||||||
$page->click('button:has-text("Close from Alpine")')
|
$page->click('button:has-text("Close from Alpine")')
|
||||||
@@ -987,29 +1068,30 @@ dataset('containment sheets', [
|
|||||||
it('fades the sheet\'s scrim out on close, rather than making it vanish', function (string $sheet, string $trigger, string $scrimAttr) {
|
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}]')";
|
$scrim = "{$sheet}.parentElement.querySelector(':scope > [{$scrimAttr}]')";
|
||||||
|
|
||||||
$page = containment()
|
$page = containment();
|
||||||
->click($trigger)
|
|
||||||
->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("getComputedStyle({$scrim}).opacity === '1'");
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
|
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
||||||
|
->assertScript("getComputedStyle({$scrim}).opacity === '1'")
|
||||||
|
// Settled, not still fading in: an engine reads a transition's end value until its next
|
||||||
|
// refresh tick, so an opacity of 1 alone does not say the entry has run — and a close
|
||||||
|
// sampled from there has nothing left to fade.
|
||||||
|
->assertScript(settled($scrim));
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
// Triggering the close and sampling for a mid-fade opacity in the same round trip: a round
|
// Triggering the close and sampling for a mid-fade opacity in the same round trip: a round
|
||||||
// trip apiece for a separate `script` and `assertScript` already costs real time, easily as
|
// trip apiece for a separate `script` and `assertScript` already costs real time, easily as
|
||||||
// much as the 200ms fade itself, so a click, then a later separate read, can just as easily
|
// much as the fade itself, so a click, then a later separate read, can just as easily land
|
||||||
// land before the fade starts or after it ends. This samples every few ms, in the page itself,
|
// before the fade starts or after it ends. This samples once a frame, in the page itself, for
|
||||||
// for whether it was ever caught strictly between fully shown and fully hidden, rather than
|
// whether it was ever caught strictly between fully shown and fully hidden, rather than simply
|
||||||
// simply gone at once.
|
// gone at once (caughtMidExit() in tests/Pest.php).
|
||||||
$midFade = $page->script(<<<JS
|
$midFade = $page->script(caughtMidExit(
|
||||||
(async () => {
|
"{$scrim}.click()",
|
||||||
{$scrim}.click()
|
"(() => { const opacity = parseFloat(getComputedStyle({$scrim}).opacity); return opacity > 0.02 && opacity < 0.98 })()",
|
||||||
|
));
|
||||||
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();
|
expect($midFade)->toBeTrue();
|
||||||
|
|
||||||
@@ -1021,29 +1103,38 @@ it('slides the sheet out on close, rather than making it vanish', function (stri
|
|||||||
$offset = "(parseFloat(getComputedStyle({$sheet}).translate{$axis}) || 0)";
|
$offset = "(parseFloat(getComputedStyle({$sheet}).translate{$axis}) || 0)";
|
||||||
$sample = $absolute ? "Math.abs(parseFloat(style.translate{$axis}) || 0)" : "(parseFloat(style.translate{$axis}) || 0)";
|
$sample = $absolute ? "Math.abs(parseFloat(style.translate{$axis}) || 0)" : "(parseFloat(style.translate{$axis}) || 0)";
|
||||||
|
|
||||||
$page = containment()
|
$page = containment();
|
||||||
->click($trigger)
|
|
||||||
->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("Math.abs({$offset}) < 0.5");
|
pressOnce($page)->click($trigger);
|
||||||
|
|
||||||
|
$page->assertScript("getComputedStyle({$sheet}).display !== 'none'")
|
||||||
|
->assertScript("Math.abs({$offset}) < 0.5")
|
||||||
|
// Settled, not still sliding in: see the scrim's fade above.
|
||||||
|
->assertScript(settled($sheet));
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
// Sampled in the page, in the same round trip as the close, as the scrim's fade is above: the
|
// 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's
|
// 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
|
// own size past its edge), not gone at once — which is what Firefox showed while the exit
|
||||||
// leaned on `allow-discrete` holding `display`.
|
// leaned on `allow-discrete` holding `display`. The sheet closes on the emphasized-accelerate
|
||||||
$midSlide = $page->script(<<<JS
|
// easing, which stays close to its start for a good part of its run, so it takes a few of the
|
||||||
(async () => {
|
// stretched exit's frames before the offset has moved past a pixel.
|
||||||
|
$midSlide = $page->script(caughtMidExit(
|
||||||
|
<<<JS
|
||||||
const size = {$sheet}.getBoundingClientRect().{$dimension}
|
const size = {$sheet}.getBoundingClientRect().{$dimension}
|
||||||
{$scrim}.click()
|
{$scrim}.click()
|
||||||
|
JS,
|
||||||
for (let i = 0; i < 60; i++) {
|
<<<JS
|
||||||
|
(() => {
|
||||||
const style = getComputedStyle({$sheet})
|
const style = getComputedStyle({$sheet})
|
||||||
const offset = {$sample}
|
const offset = {$sample}
|
||||||
if (style.display !== 'none' && offset > 1 && offset < size - 1) return true
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
return style.display !== 'none' && offset > 1 && offset < size - 1
|
||||||
}
|
|
||||||
return false
|
|
||||||
})()
|
})()
|
||||||
JS);
|
JS,
|
||||||
|
));
|
||||||
|
|
||||||
expect($midSlide)->toBeTrue();
|
expect($midSlide)->toBeTrue();
|
||||||
|
|
||||||
@@ -1055,6 +1146,7 @@ it('slides the side sheet in from its own edge in a right-to-left page', functio
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html dir="rtl">
|
<html dir="rtl">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -1073,7 +1165,8 @@ it('slides the side sheet in from its own edge in a right-to-left page', functio
|
|||||||
|
|
||||||
$sheet = "document.querySelector('aside[data-md-side]')";
|
$sheet = "document.querySelector('aside[data-md-side]')";
|
||||||
|
|
||||||
$page->click('button:has-text("Open")');
|
// Opens the sheet over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button:has-text("Open")');
|
||||||
|
|
||||||
// `side="end"` (the default) is the trailing edge — the *left* of a right-to-left page — so it
|
// `side="end"` (the default) is the trailing edge — the *left* of a right-to-left page — so it
|
||||||
// slides in from off the left, not the right (the CSS mirror reads `[dir='rtl']` rather than
|
// slides in from off the left, not the right (the CSS mirror reads `[dir='rtl']` rather than
|
||||||
@@ -1123,6 +1216,7 @@ it('keeps a basic dialog\'s own title and padding when it is nested inside a ful
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -1136,7 +1230,8 @@ it('keeps a basic dialog\'s own title and padding when it is nested inside a ful
|
|||||||
|
|
||||||
$page = ready(visit('/nested-fullscreen-dialog-probe')->resize(400, 800));
|
$page = ready(visit('/nested-fullscreen-dialog-probe')->resize(400, 800));
|
||||||
|
|
||||||
$page->click('button:has-text("Open settings")');
|
// Opens the dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button:has-text("Open settings")');
|
||||||
|
|
||||||
$outerBar = "document.querySelector('dialog[data-md-fullscreen][open] [data-md-modal-bar]')";
|
$outerBar = "document.querySelector('dialog[data-md-fullscreen][open] [data-md-modal-bar]')";
|
||||||
|
|
||||||
@@ -1145,7 +1240,8 @@ it('keeps a basic dialog\'s own title and padding when it is nested inside a ful
|
|||||||
// must keep its own head regardless.
|
// must keep its own head regardless.
|
||||||
$page->assertScript("getComputedStyle({$outerBar}).display !== 'none'");
|
$page->assertScript("getComputedStyle({$outerBar}).display !== 'none'");
|
||||||
|
|
||||||
$page->click('button:has-text("Delete the share")');
|
// Opens the nested dialog over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('button:has-text("Delete the share")');
|
||||||
|
|
||||||
$inner = "[...document.querySelectorAll('dialog[open]')].find((d) => d.querySelector('[data-md-modal-title]')?.textContent === 'Delete this share?')";
|
$inner = "[...document.querySelectorAll('dialog[open]')].find((d) => d.querySelector('[data-md-modal-title]')?.textContent === 'Delete this share?')";
|
||||||
$innerHead = "{$inner}.querySelector('[data-md-modal-head]')";
|
$innerHead = "{$inner}.querySelector('[data-md-modal-head]')";
|
||||||
@@ -1164,12 +1260,13 @@ it('shows the 16% dragged state layer on the showcase card being dragged', funct
|
|||||||
|
|
||||||
$page->assertScript("getComputedStyle({$card}, '::before').opacity === '0'");
|
$page->assertScript("getComputedStyle({$card}, '::before').opacity === '0'");
|
||||||
|
|
||||||
$page->click('#containment button:has-text("Pick it up or put it down")');
|
// Toggles the drag state on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#containment button:has-text("Pick it up or put it down")');
|
||||||
|
|
||||||
$page->assertScript("{$card}.hasAttribute('data-md-dragged')")
|
$page->assertScript("{$card}.hasAttribute('data-md-dragged')")
|
||||||
->assertScript("getComputedStyle({$card}, '::before').opacity === '0.16'");
|
->assertScript("getComputedStyle({$card}, '::before').opacity === '0.16'");
|
||||||
|
|
||||||
$page->click('#containment button:has-text("Pick it up or put it down")');
|
pressOnce($page)->click('#containment button:has-text("Pick it up or put it down")');
|
||||||
|
|
||||||
$page->assertScript("! {$card}.hasAttribute('data-md-dragged')")
|
$page->assertScript("! {$card}.hasAttribute('data-md-dragged')")
|
||||||
->assertScript("getComputedStyle({$card}, '::before').opacity === '0'");
|
->assertScript("getComputedStyle({$card}, '::before').opacity === '0'");
|
||||||
@@ -1180,6 +1277,7 @@ it('keeps a selected segmented row\'s fill under the hover and focus tint', func
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -1237,6 +1335,7 @@ it('fills a selected row written by hand in secondary-container in both themes,
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -1306,6 +1405,7 @@ function collapseMotionProbe(string $durations = ''): mixed
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -1441,10 +1541,12 @@ it('eases a collapse open and shut in every engine, moving what is under it', fu
|
|||||||
|
|
||||||
expect($clipped)->toBe('clip');
|
expect($clipped)->toBe('clip');
|
||||||
|
|
||||||
// From the keyboard: Enter on the summary is the same press, taken over the same way.
|
// From the keyboard: Enter on the summary is the same press, taken over the same way, and it
|
||||||
|
// toggles: pressOnce(), tests/Pest.php.
|
||||||
$page->script("document.querySelector('#plain summary').focus()");
|
$page->script("document.querySelector('#plain summary').focus()");
|
||||||
$page->keys('#plain summary', 'Enter')
|
pressOnce($page)->keys('#plain summary', 'Enter');
|
||||||
->assertScript("document.querySelector('#plain').open === false && document.querySelector('#plain').getAnimations().length === 0");
|
|
||||||
|
$page->assertScript("document.querySelector('#plain').open === false && document.querySelector('#plain').getAnimations().length === 0");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('eases a collapse bound to Alpine open and shut, keeping the binding in step', function () {
|
it('eases a collapse bound to Alpine open and shut, keeping the binding in step', function () {
|
||||||
@@ -1466,16 +1568,19 @@ it('eases a collapse bound to Alpine open and shut, keeping the binding in step'
|
|||||||
|
|
||||||
$page->assertSeeIn('#advanced', 'false');
|
$page->assertSeeIn('#advanced', 'false');
|
||||||
|
|
||||||
// A press on the summary tells the binding when the section has closed, and never loops.
|
// A press on the summary tells the binding when the section has closed, and never loops; a
|
||||||
$page->click('#bound summary')
|
// native <summary> click toggles the collapse: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#advanced', 'true')
|
pressOnce($page)->click('#bound summary');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#advanced', 'true')
|
||||||
->assertScript("document.querySelector('#bound').open === true");
|
->assertScript("document.querySelector('#bound').open === true");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('turns a collapse round from the height it has reached, ending as the last press asked', function () {
|
it('turns a collapse round from the height it has reached, ending as the last press asked', function () {
|
||||||
$page = collapseMotionProbe('1500ms');
|
$page = collapseMotionProbe('1500ms');
|
||||||
|
|
||||||
$result = $page->script(<<<'JS'
|
// Two seconds in the page, so it is run once: see onceInPage() in tests/Pest.php.
|
||||||
|
$result = $page->script(onceInPage(<<<'JS'
|
||||||
(async () => {
|
(async () => {
|
||||||
const details = document.querySelector('#plain')
|
const details = document.querySelector('#plain')
|
||||||
const summary = details.querySelector('summary')
|
const summary = details.querySelector('summary')
|
||||||
@@ -1493,7 +1598,7 @@ it('turns a collapse round from the height it has reached, ending as the last pr
|
|||||||
|
|
||||||
return { closed, midway, turned, end: details.getBoundingClientRect().height, open: details.open, style: details.getAttribute('style') }
|
return { closed, midway, turned, end: details.getBoundingClientRect().height, open: details.open, style: details.getAttribute('style') }
|
||||||
})()
|
})()
|
||||||
JS);
|
JS));
|
||||||
|
|
||||||
expect($result['midway'])->toBeGreaterThan($result['closed'] + 2)
|
expect($result['midway'])->toBeGreaterThan($result['closed'] + 2)
|
||||||
// Turned round from where it was, not from either end.
|
// Turned round from where it was, not from either end.
|
||||||
@@ -1508,7 +1613,7 @@ it('closes the open member of a name group on the same spring as the one opening
|
|||||||
|
|
||||||
$first = "document.querySelector('#first')";
|
$first = "document.querySelector('#first')";
|
||||||
|
|
||||||
$result = $page->script(<<<'JS'
|
$result = $page->script(onceInPage(<<<'JS'
|
||||||
(async () => {
|
(async () => {
|
||||||
const first = document.querySelector('#first')
|
const first = document.querySelector('#first')
|
||||||
const second = document.querySelector('#second')
|
const second = document.querySelector('#second')
|
||||||
@@ -1527,7 +1632,7 @@ it('closes the open member of a name group on the same spring as the one opening
|
|||||||
|
|
||||||
return { stillOpen, caught, first: first.open, second: second.open, name: first.getAttribute('name') }
|
return { stillOpen, caught, first: first.open, second: second.open, name: first.getAttribute('name') }
|
||||||
})()
|
})()
|
||||||
JS);
|
JS));
|
||||||
|
|
||||||
expect($result)->toBe(['stillOpen' => true, 'caught' => true, 'first' => false, 'second' => true, 'name' => 'faq']);
|
expect($result)->toBe(['stillOpen' => true, 'caught' => true, 'first' => false, 'second' => true, 'name' => 'faq']);
|
||||||
|
|
||||||
@@ -1601,6 +1706,7 @@ it('shows no tooltip on the control a sheet or a dialog focuses as it opens, but
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -1630,8 +1736,10 @@ it('shows no tooltip on the control a sheet or a dialog focuses as it opens, but
|
|||||||
// A full-screen dialog opened from the keyboard moves the focus to its bar's ✕ in showModal().
|
// A full-screen dialog opened from the keyboard moves the focus to its bar's ✕ in showModal().
|
||||||
$page = ready(visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]]), alpine: false);
|
$page = ready(visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]]), alpine: false);
|
||||||
|
|
||||||
$page->keys('#open-setback', 'Enter')
|
// Enter on the focused button opens the dialog over it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.querySelector('{$dialog}').open && document.activeElement.matches('{$dialog} button[aria-label=\"Close\"]')")
|
pressOnce($page)->keys('#open-setback', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('{$dialog}').open && document.activeElement.matches('{$dialog} button[aria-label=\"Close\"]')")
|
||||||
->wait(0.3)
|
->wait(0.3)
|
||||||
->assertScript('! '.closeTooltipOpen($dialog));
|
->assertScript('! '.closeTooltipOpen($dialog));
|
||||||
|
|
||||||
@@ -1642,8 +1750,10 @@ it('shows no tooltip on the control a sheet or a dialog focuses as it opens, but
|
|||||||
// A rich tooltip's trigger as the first control of a dialog opened from the keyboard.
|
// A rich tooltip's trigger as the first control of a dialog opened from the keyboard.
|
||||||
$page = ready(visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]]), alpine: false);
|
$page = ready(visit('/opened-layers-probe', ['viewport' => ['width' => 393, 'height' => 800]]), alpine: false);
|
||||||
|
|
||||||
$page->keys('#open-about', 'Enter')
|
// Enter on the focused button opens the dialog over it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.activeElement.id === 'about-help'")
|
pressOnce($page)->keys('#open-about', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript("document.activeElement.id === 'about-help'")
|
||||||
->wait(0.3)
|
->wait(0.3)
|
||||||
->assertScript("! document.querySelector('[data-md-rich-tooltip-bubble]').matches(':popover-open')");
|
->assertScript("! document.querySelector('[data-md-rich-tooltip-bubble]').matches(':popover-open')");
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ function dataProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -86,12 +87,15 @@ it('sorts by a column and flips the direction on a second press', function () {
|
|||||||
->assertAttribute('#by-name', 'aria-sort', 'ascending')
|
->assertAttribute('#by-name', 'aria-sort', 'ascending')
|
||||||
->assertScript("{$first} === 'file-01.zip'");
|
->assertScript("{$first} === 'file-01.zip'");
|
||||||
|
|
||||||
$page->click('#by-name button')
|
// A second press on the same header flips its direction again: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute('#by-name', 'aria-sort', 'descending')
|
pressOnce($page)->click('#by-name button');
|
||||||
|
|
||||||
|
$page->assertAttribute('#by-name', 'aria-sort', 'descending')
|
||||||
->assertScript("{$first} === 'file-25.zip'");
|
->assertScript("{$first} === 'file-25.zip'");
|
||||||
|
|
||||||
$page->click('#by-size button')
|
pressOnce($page)->click('#by-size button');
|
||||||
->assertAttribute('#by-size', 'aria-sort', 'ascending')
|
|
||||||
|
$page->assertAttribute('#by-size', 'aria-sort', 'ascending')
|
||||||
->assertScript("! document.querySelector('#by-name').hasAttribute('aria-sort')");
|
->assertScript("! document.querySelector('#by-name').hasAttribute('aria-sort')");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -103,8 +107,10 @@ it('pages through Livewire results and marks the current page', function () {
|
|||||||
->assertSee('21–25 of 25')
|
->assertSee('21–25 of 25')
|
||||||
->assertSeeIn('[data-md-pagination] [aria-current="page"]', '3');
|
->assertSeeIn('[data-md-pagination] [aria-current="page"]', '3');
|
||||||
|
|
||||||
$page->click('[data-md-pagination] button[aria-label="Previous"]')
|
// A relative paging key a retry would page twice: pressOnce(), tests/Pest.php.
|
||||||
->assertSee('11–20 of 25');
|
pressOnce($page)->click('[data-md-pagination] button[aria-label="Previous"]');
|
||||||
|
|
||||||
|
$page->assertSee('11–20 of 25');
|
||||||
|
|
||||||
$page->resize(400, 800)
|
$page->resize(400, 800)
|
||||||
->assertSee('Page 2 of 3');
|
->assertSee('Page 2 of 3');
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ function dateProbe(string $locale = 'en')
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -108,6 +109,7 @@ function dateFormatProbe(string $locale = 'en')
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -148,6 +150,7 @@ function scopedDateProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -176,9 +179,12 @@ function focusedDay(string $date): string
|
|||||||
it('opens the docked picker under its field and walks the grid from the keyboard', function () {
|
it('opens the docked picker under its field and walks the grid from the keyboard', function () {
|
||||||
$picker = "document.querySelector('#expires-field-picker')";
|
$picker = "document.querySelector('#expires-field-picker')";
|
||||||
|
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript("{$picker}.matches(':popover-open') && {$picker}.dataset.mdPresentation === 'docked'")
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript("{$picker}.matches(':popover-open') && {$picker}.dataset.mdPresentation === 'docked'")
|
||||||
->assertAttribute('#expires-field', 'aria-expanded', 'true')
|
->assertAttribute('#expires-field', 'aria-expanded', 'true')
|
||||||
->assertScript("(() => { const field = document.querySelector('#expires-field').closest('[data-md-field-box]').getBoundingClientRect(); const box = {$picker}.getBoundingClientRect(); return Math.abs(box.top - field.bottom - 4) < 2 && Math.abs(box.left - field.left) < 2; })()")
|
->assertScript("(() => { const field = document.querySelector('#expires-field').closest('[data-md-field-box]').getBoundingClientRect(); const box = {$picker}.getBoundingClientRect(); return Math.abs(box.top - field.bottom - 4) < 2 && Math.abs(box.left - field.left) < 2; })()")
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
->assertScript(focusedDay('2026-09-13'));
|
||||||
@@ -201,9 +207,12 @@ it('opens the docked picker under its field and walks the grid from the keyboard
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('keeps focus and choice inside min and max', function () {
|
it('keeps focus and choice inside min and max', function () {
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
// Playwright will not press an aria-disabled cell, so the press is dispatched.
|
// Playwright will not press an aria-disabled cell, so the press is dispatched.
|
||||||
$page->assertAttribute(day('expires-field', '2026-09-09'), 'aria-disabled', 'true')
|
$page->assertAttribute(day('expires-field', '2026-09-09'), 'aria-disabled', 'true')
|
||||||
@@ -239,9 +248,12 @@ it('takes a date typed into the docked field once it is whole, and says when it
|
|||||||
it('opens the modal picker, jumps through the year grid and confirms with OK', function () {
|
it('opens the modal picker, jumps through the year grid and confirms with OK', function () {
|
||||||
$dialog = "document.querySelector('#birthday-field-picker')";
|
$dialog = "document.querySelector('#birthday-field-picker')";
|
||||||
|
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click('#birthday-field')
|
|
||||||
->assertScript("{$dialog}.matches(':modal')")
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#birthday-field');
|
||||||
|
|
||||||
|
$page->assertScript("{$dialog}.matches(':modal')")
|
||||||
->assertSeeIn('#birthday-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'May 17, 2000')
|
->assertSeeIn('#birthday-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'May 17, 2000')
|
||||||
->click('#birthday-field-picker [data-md-datepicker-nav]:not([data-md-docked]) [data-md-datepicker-menu-button]')
|
->click('#birthday-field-picker [data-md-datepicker-nav]:not([data-md-docked]) [data-md-datepicker-menu-button]')
|
||||||
->assertScript("getComputedStyle(document.querySelector('#birthday-field-picker [data-md-datepicker-years]')).display !== 'none'")
|
->assertScript("getComputedStyle(document.querySelector('#birthday-field-picker [data-md-datepicker-years]')).display !== 'none'")
|
||||||
@@ -253,8 +265,10 @@ it('opens the modal picker, jumps through the year grid and confirms with OK', f
|
|||||||
->assertSeeIn('#birthday-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'May 3, 1990')
|
->assertSeeIn('#birthday-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'May 3, 1990')
|
||||||
->assertSeeIn('#birthday', '2000-05-17');
|
->assertSeeIn('#birthday', '2000-05-17');
|
||||||
|
|
||||||
$page->click('#birthday-field-picker [data-md-datepicker-confirm]')
|
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#birthday', '1990-05-03')
|
pressOnce($page)->click('#birthday-field-picker [data-md-datepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#birthday', '1990-05-03')
|
||||||
->assertScript("! {$dialog}.open");
|
->assertScript("! {$dialog}.open");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,8 +279,10 @@ it('closes on Escape without keeping the draft and hands focus back to the field
|
|||||||
|
|
||||||
$page->script("document.querySelector('#birthday-field').focus()");
|
$page->script("document.querySelector('#birthday-field').focus()");
|
||||||
|
|
||||||
$page->keys('#birthday-field', 'Enter')
|
// Enter on the field opens the dialog, same as clicking it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$dialog}.open")
|
pressOnce($page)->keys('#birthday-field', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript("{$dialog}.open")
|
||||||
->assertScript(focusedDay('2000-05-17'));
|
->assertScript(focusedDay('2000-05-17'));
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowRight')
|
$page->keys(':focus', 'ArrowRight')
|
||||||
@@ -281,8 +297,11 @@ it('closes on Escape without keeping the draft and hands focus back to the field
|
|||||||
// The docked picker too.
|
// The docked picker too.
|
||||||
$page->script("document.querySelector('[aria-controls=\"expires-field-picker\"][data-md-datepicker-toggle]').focus()");
|
$page->script("document.querySelector('[aria-controls=\"expires-field-picker\"][data-md-datepicker-toggle]').focus()");
|
||||||
|
|
||||||
$page->keys(':focus', 'Enter')
|
// Enter on the focused toggle button is a native click on it, and it toggles: pressOnce(),
|
||||||
->assertScript("document.querySelector('#expires-field-picker').matches(':popover-open')")
|
// tests/Pest.php. A retried Enter would land on the day it opened onto instead, closing it.
|
||||||
|
pressOnce($page)->keys(':focus', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#expires-field-picker').matches(':popover-open')")
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
@@ -293,15 +312,22 @@ it('closes on Escape without keeping the draft and hands focus back to the field
|
|||||||
it('reads the modal input in the locale\'s format and explains a date it cannot take', function () {
|
it('reads the modal input in the locale\'s format and explains a date it cannot take', function () {
|
||||||
$dialog = "document.querySelector('#delivery-field-picker')";
|
$dialog = "document.querySelector('#delivery-field-picker')";
|
||||||
|
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click('#delivery-field')
|
|
||||||
->assertScript("{$dialog}.matches(':modal')")
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#delivery-field');
|
||||||
|
|
||||||
|
$page->assertScript("{$dialog}.matches(':modal')")
|
||||||
->assertScript("document.activeElement.id === 'delivery-field-entry'")
|
->assertScript("document.activeElement.id === 'delivery-field-entry'")
|
||||||
->assertSeeIn('#delivery-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'Entered date');
|
->assertSeeIn('#delivery-field-picker [data-md-datepicker-header] [data-md-datepicker-headline]', 'Entered date');
|
||||||
|
|
||||||
$page->type('#delivery-field-entry', '2026-03-07')
|
$page->type('#delivery-field-entry', '2026-03-07');
|
||||||
->click('#delivery-field-picker [data-md-datepicker-confirm]')
|
|
||||||
->assertSeeIn('#delivery-field-entry-support', 'Date does not match expected pattern: MM/DD/YYYY')
|
// The confirm button closes the dialog when the date is valid; here it stays open (invalid),
|
||||||
|
// but pressed once anyway: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#delivery-field-picker [data-md-datepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#delivery-field-entry-support', 'Date does not match expected pattern: MM/DD/YYYY')
|
||||||
->assertAttribute('#delivery-field-entry', 'aria-invalid', 'true')
|
->assertAttribute('#delivery-field-entry', 'aria-invalid', 'true')
|
||||||
->assertScript("{$dialog}.open");
|
->assertScript("{$dialog}.open");
|
||||||
|
|
||||||
@@ -319,21 +345,29 @@ it('reads the modal input in the locale\'s format and explains a date it cannot
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('picks a range: a start, an end, and the days between', function () {
|
it('picks a range: a start, an end, and the days between', function () {
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertAttribute(day('trip-field', '2026-09-14'), 'aria-selected', 'true');
|
|
||||||
|
|
||||||
$page->click(day('trip-field', '2026-09-20'))
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-md-selected')")
|
pressOnce($page)->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertAttribute(day('trip-field', '2026-09-14'), 'aria-selected', 'true');
|
||||||
|
|
||||||
|
// A day pressed twice ends the range it began: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click(day('trip-field', '2026-09-20'));
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-md-selected')")
|
||||||
->assertScript("! document.querySelector('".day('trip-field', '2026-09-14')."').hasAttribute('data-md-between')");
|
->assertScript("! document.querySelector('".day('trip-field', '2026-09-14')."').hasAttribute('data-md-between')");
|
||||||
|
|
||||||
$page->click(day('trip-field', '2026-09-24'))
|
pressOnce($page)->click(day('trip-field', '2026-09-24'));
|
||||||
->assertScript("[...document.querySelectorAll('#trip-field-picker [data-md-between]')].map((cell) => cell.dataset.mdValue).join() === '2026-09-21,2026-09-22,2026-09-23'")
|
|
||||||
|
$page->assertScript("[...document.querySelectorAll('#trip-field-picker [data-md-between]')].map((cell) => cell.dataset.mdValue).join() === '2026-09-21,2026-09-22,2026-09-23'")
|
||||||
->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-md-start') && document.querySelector('".day('trip-field', '2026-09-24')."').hasAttribute('data-md-end')")
|
->assertScript("document.querySelector('".day('trip-field', '2026-09-20')."').hasAttribute('data-md-start') && document.querySelector('".day('trip-field', '2026-09-24')."').hasAttribute('data-md-end')")
|
||||||
->assertSeeIn('#trip', '2026-09-15');
|
->assertSeeIn('#trip', '2026-09-15');
|
||||||
|
|
||||||
$page->click('#trip-field-picker [data-md-datepicker-confirm]')
|
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#trip', '{"start":"2026-09-20","end":"2026-09-24"}')
|
pressOnce($page)->click('#trip-field-picker [data-md-datepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#trip', '{"start":"2026-09-20","end":"2026-09-24"}')
|
||||||
->assertValue('#trip-field', '09/20/2026 – 09/24/2026');
|
->assertValue('#trip-field', '09/20/2026 – 09/24/2026');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -341,9 +375,12 @@ it('follows the locale: German weeks start on Monday, American ones on Sunday',
|
|||||||
$firstWeekday = "document.querySelector('#expires-field-picker thead th').getAttribute('abbr')";
|
$firstWeekday = "document.querySelector('#expires-field-picker thead th').getAttribute('abbr')";
|
||||||
|
|
||||||
$page = dateProbe('de')
|
$page = dateProbe('de')
|
||||||
->assertValue('#expires-field', '13.09.2026')
|
->assertValue('#expires-field', '13.09.2026');
|
||||||
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript("{$firstWeekday} === 'Montag'")
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript("{$firstWeekday} === 'Montag'")
|
||||||
->assertScript("document.querySelector('#expires-field-month').textContent === 'September 2026'")
|
->assertScript("document.querySelector('#expires-field-month').textContent === 'September 2026'")
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
@@ -354,15 +391,20 @@ it('follows the locale: German weeks start on Monday, American ones on Sunday',
|
|||||||
->assertSeeIn('#expires', '2026-09-20')
|
->assertSeeIn('#expires', '2026-09-20')
|
||||||
->assertAttribute('#expires-field', 'placeholder', 'DD.MM.YYYY');
|
->assertAttribute('#expires-field', 'placeholder', 'DD.MM.YYYY');
|
||||||
|
|
||||||
dateProbe('en_US')
|
$page = dateProbe('en_US');
|
||||||
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript("{$firstWeekday} === 'Sunday'");
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript("{$firstWeekday} === 'Sunday'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('stays open, where it was, through a Livewire render', function () {
|
it('stays open, where it was, through a Livewire render', function () {
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowRight')->assertScript(focusedDay('2026-09-14'));
|
$page->keys(':focus', 'ArrowRight')->assertScript(focusedDay('2026-09-14'));
|
||||||
|
|
||||||
@@ -375,18 +417,23 @@ it('stays open, where it was, through a Livewire render', function () {
|
|||||||
|
|
||||||
$page->keys(day('expires-field', '2026-09-14'), 'Enter')->assertSeeIn('#expires', '2026-09-14');
|
$page->keys(day('expires-field', '2026-09-14'), 'Enter')->assertSeeIn('#expires', '2026-09-14');
|
||||||
|
|
||||||
$page->click('#birthday-field')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->script('window.eval("Livewire.first().touch()")');
|
pressOnce($page)->click('#birthday-field');
|
||||||
|
|
||||||
|
$page->script('window.eval("Livewire.first().touch()")');
|
||||||
|
|
||||||
$page->assertSeeIn('#renders', '2')
|
$page->assertSeeIn('#renders', '2')
|
||||||
->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')");
|
->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens a docked picker as a modal one on a compact window', function () {
|
it('opens a docked picker as a modal one on a compact window', function () {
|
||||||
dateProbe()
|
$page = dateProbe()
|
||||||
->resize(400, 800)
|
->resize(400, 800);
|
||||||
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript("document.querySelector('#expires-field-picker').matches(':modal')")
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#expires-field-picker').matches(':modal')")
|
||||||
->assertScript("getComputedStyle(document.querySelector('#expires-field-picker [data-md-datepicker-header]')).display !== 'none'");
|
->assertScript("getComputedStyle(document.querySelector('#expires-field-picker [data-md-datepicker-header]')).display !== 'none'");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -394,22 +441,28 @@ it('empties a date, or both ends of a range, with its clear button', function ()
|
|||||||
$page = dateProbe()
|
$page = dateProbe()
|
||||||
->assertScript("getComputedStyle(document.querySelector('#expires-field').closest('[data-md-field]').querySelector('[data-md-field-clear]')).display !== 'none'");
|
->assertScript("getComputedStyle(document.querySelector('#expires-field').closest('[data-md-field]').querySelector('[data-md-field-clear]')).display !== 'none'");
|
||||||
|
|
||||||
$page->click('[data-md-datepicker]:has(#expires-field) [data-md-field-clear]')
|
// The clear button vanishes once its field is empty: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.querySelector('#expires').textContent === ''")
|
pressOnce($page)->click('[data-md-datepicker]:has(#expires-field) [data-md-field-clear]');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#expires').textContent === ''")
|
||||||
->assertValue('#expires-field', '')
|
->assertValue('#expires-field', '')
|
||||||
->assertScript("document.activeElement.id === 'expires-field'")
|
->assertScript("document.activeElement.id === 'expires-field'")
|
||||||
->assertScript("getComputedStyle(document.querySelector('[data-md-datepicker]:has(#expires-field) [data-md-field-clear]')).display === 'none'");
|
->assertScript("getComputedStyle(document.querySelector('[data-md-datepicker]:has(#expires-field) [data-md-field-clear]')).display === 'none'");
|
||||||
|
|
||||||
$page->click('[data-md-datepicker]:has(#trip-field) [data-md-field-clear]')
|
pressOnce($page)->click('[data-md-datepicker]:has(#trip-field) [data-md-field-clear]');
|
||||||
->assertSeeIn('#trip', '{"start":null,"end":null}')
|
|
||||||
|
$page->assertSeeIn('#trip', '{"start":null,"end":null}')
|
||||||
->assertValue('#trip-field', '');
|
->assertValue('#trip-field', '');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('starts the week on the day week-start names, whatever the locale says', function () {
|
it('starts the week on the day week-start names, whatever the locale says', function () {
|
||||||
$page = dateFormatProbe('de')
|
$page = dateFormatProbe('de')
|
||||||
->assertValue('#sunday-field', '13.09.2026')
|
->assertValue('#sunday-field', '13.09.2026');
|
||||||
->click('[aria-controls="sunday-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript("document.querySelector('#sunday-field-picker thead th').getAttribute('abbr') === 'Sonntag'")
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="sunday-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#sunday-field-picker thead th').getAttribute('abbr') === 'Sonntag'")
|
||||||
->assertScript("document.querySelector('#sunday-field-picker tbody td').dataset.mdValue === '2026-08-30'")
|
->assertScript("document.querySelector('#sunday-field-picker tbody td').dataset.mdValue === '2026-08-30'")
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
@@ -433,8 +486,10 @@ it('shows and reads a year-first format and still binds Y-m-d', function () {
|
|||||||
->assertSee('Date does not match expected pattern: YYYY-MM-DD')
|
->assertSee('Date does not match expected pattern: YYYY-MM-DD')
|
||||||
->assertSeeIn('#iso', '2026-10-01');
|
->assertSeeIn('#iso', '2026-10-01');
|
||||||
|
|
||||||
$page->click('[aria-controls="iso-field-picker"][data-md-datepicker-toggle]')
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(focusedDay('2026-10-01'));
|
pressOnce($page)->click('[aria-controls="iso-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript(focusedDay('2026-10-01'));
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowRight')->assertScript(focusedDay('2026-10-02'));
|
$page->keys(':focus', 'ArrowRight')->assertScript(focusedDay('2026-10-02'));
|
||||||
|
|
||||||
@@ -447,9 +502,12 @@ it('reads the dialog\'s text field in the given format, not the locale\'s', func
|
|||||||
$dialog = "document.querySelector('#dotted-field-picker')";
|
$dialog = "document.querySelector('#dotted-field-picker')";
|
||||||
|
|
||||||
$page = dateFormatProbe()
|
$page = dateFormatProbe()
|
||||||
->assertValue('#dotted-field', '13.09.2026')
|
->assertValue('#dotted-field', '13.09.2026');
|
||||||
->click('#dotted-field')
|
|
||||||
->assertScript("{$dialog}.matches(':modal')")
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#dotted-field');
|
||||||
|
|
||||||
|
$page->assertScript("{$dialog}.matches(':modal')")
|
||||||
->assertScript("document.activeElement.id === 'dotted-field-entry'")
|
->assertScript("document.activeElement.id === 'dotted-field-entry'")
|
||||||
->assertValue('#dotted-field-entry', '13.09.2026')
|
->assertValue('#dotted-field-entry', '13.09.2026')
|
||||||
->assertAttribute('#dotted-field-entry', 'placeholder', 'DD.MM.YYYY');
|
->assertAttribute('#dotted-field-entry', 'placeholder', 'DD.MM.YYYY');
|
||||||
@@ -474,40 +532,89 @@ it('lays out and shows a range in the given first day and format', function () {
|
|||||||
|
|
||||||
$page->script("document.querySelector('#span-field').focus()");
|
$page->script("document.querySelector('#span-field').focus()");
|
||||||
|
|
||||||
$page->keys('#span-field', 'Enter')
|
// Enter on the readonly field opens the dialog, same as clicking it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$dialog}.matches(':modal')")
|
pressOnce($page)->keys('#span-field', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript("{$dialog}.matches(':modal')")
|
||||||
->assertScript("document.querySelector('#span-field-picker thead th').getAttribute('abbr') === 'Saturday'")
|
->assertScript("document.querySelector('#span-field-picker thead th').getAttribute('abbr') === 'Saturday'")
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
$page->keys(':focus', 'Home')->assertScript(focusedDay('2026-09-12'));
|
$page->keys(':focus', 'Home')->assertScript(focusedDay('2026-09-12'));
|
||||||
$page->keys(':focus', 'End')->assertScript(focusedDay('2026-09-18'));
|
$page->keys(':focus', 'End')->assertScript(focusedDay('2026-09-18'));
|
||||||
|
|
||||||
$page->click(day('span-field', '2026-09-20'))
|
// A day pressed twice ends the range it began: pressOnce(), tests/Pest.php.
|
||||||
->click(day('span-field', '2026-09-24'))
|
pressOnce($page)->click(day('span-field', '2026-09-20'))
|
||||||
->click('#span-field-picker [data-md-datepicker-confirm]')
|
->click(day('span-field', '2026-09-24'));
|
||||||
->assertSeeIn('#span', '{"start":"2026-09-20","end":"2026-09-24"}')
|
|
||||||
|
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#span-field-picker [data-md-datepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#span', '{"start":"2026-09-20","end":"2026-09-24"}')
|
||||||
->assertValue('#span-field', '20/09/2026 – 24/09/2026');
|
->assertValue('#span-field', '20/09/2026 – 24/09/2026');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps each picker\'s own settings inside a page\'s outer x-data scope', function () {
|
it('keeps each picker\'s own settings inside a page\'s outer x-data scope', function () {
|
||||||
// Settings assigned in init() without being declared would land on the outermost scope,
|
// Settings assigned in init() without being declared would land on the outermost scope,
|
||||||
// where the last picker's null min and Monday start would overwrite the first's.
|
// where the last picker's null min and Monday start would overwrite the first's.
|
||||||
$page = scopedDateProbe()
|
$page = scopedDateProbe();
|
||||||
->click('[aria-controls="early-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="early-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
$page->assertAttribute(day('early-field', '2026-09-09'), 'aria-disabled', 'true')
|
$page->assertAttribute(day('early-field', '2026-09-09'), 'aria-disabled', 'true')
|
||||||
->assertScript('! (\'min\' in Alpine.$data(document.querySelector(\'[x-data="{ step: 1 }"]\')))');
|
->assertScript('! (\'min\' in Alpine.$data(document.querySelector(\'[x-data="{ step: 1 }"]\')))');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* What the browser would hit at the centre of `$selector`, as `tag#id[attributes]`, and whether the
|
||||||
|
* element itself is the answer. Playwright waits for a click target to be visible, still and on top,
|
||||||
|
* and gives up with nothing but a timeout when it never is; this says what stood in the way, on a
|
||||||
|
* runner where a layout that works elsewhere does not.
|
||||||
|
*/
|
||||||
|
function hitTarget(string $selector): string
|
||||||
|
{
|
||||||
|
return <<<JS
|
||||||
|
(() => {
|
||||||
|
const target = document.querySelector("{$selector}")
|
||||||
|
|
||||||
|
if (! target) return 'nothing matches {$selector}'
|
||||||
|
|
||||||
|
const box = target.getBoundingClientRect()
|
||||||
|
const top = document.elementFromPoint(box.left + box.width / 2, box.top + box.height / 2)
|
||||||
|
|
||||||
|
if (top === null) return `outside the viewport: \${JSON.stringify(box)}`
|
||||||
|
if (target === top || target.contains(top)) return 'the target'
|
||||||
|
|
||||||
|
const name = (element) => element.tagName.toLowerCase() + (element.id ? '#' + element.id : '')
|
||||||
|
+ [...element.attributes].filter((a) => a.name.startsWith('data-md')).map((a) => `[\${a.name}]`).join('')
|
||||||
|
|
||||||
|
return `\${name(top)} covers \${name(target)}`
|
||||||
|
})()
|
||||||
|
JS;
|
||||||
|
}
|
||||||
|
|
||||||
it('opens the range picker full screen below 600px and grows its month list both ways by keyboard paging', function () {
|
it('opens the range picker full screen below 600px and grows its month list both ways by keyboard paging', function () {
|
||||||
$picker = "document.querySelector('#trip-field-picker')";
|
$picker = "document.querySelector('#trip-field-picker')";
|
||||||
$scope = "Alpine.\$data(document.querySelector('[data-md-datepicker]:has(#trip-field)'))";
|
$scope = "Alpine.\$data(document.querySelector('[data-md-datepicker]:has(#trip-field)'))";
|
||||||
|
|
||||||
$page = dateProbe()
|
$page = dateProbe()
|
||||||
->resize(390, 800)
|
->resize(390, 800)
|
||||||
->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
|
// Explicit, not implicit: the resize's own reflow can still be under way when the press
|
||||||
->assertScript("{$picker}.dataset.mdPresentation === 'full'")
|
// that follows lands.
|
||||||
|
->assertScript('window.innerWidth === 390')
|
||||||
|
->assertScript(settled('document.documentElement', subtree: true));
|
||||||
|
|
||||||
|
expect($page->script(hitTarget('[aria-controls=\"trip-field-picker\"][data-md-datepicker-toggle]')))->toBe('the target');
|
||||||
|
|
||||||
|
// Every press into the full-screen picker goes through pressOnce(): it re-renders every day of
|
||||||
|
// every month it holds on each one, which on a loaded runner takes longer than the plugin's
|
||||||
|
// second for an attempt — and a retried press is a second press. The first one here opens the
|
||||||
|
// picker over its own toggle, so a second could never land.
|
||||||
|
pressOnce($page)->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript("{$picker}.dataset.mdPresentation === 'full'")
|
||||||
->assertScript("{$picker}.matches(':modal')")
|
->assertScript("{$picker}.matches(':modal')")
|
||||||
->assertScript("document.querySelector('#trip-field-picker [data-md-datepicker-app-bar]').offsetHeight > 0")
|
->assertScript("document.querySelector('#trip-field-picker [data-md-datepicker-app-bar]').offsetHeight > 0")
|
||||||
// M3's 14-element anatomy: a close (×) icon button and Save in the app bar, not Cancel/OK.
|
// M3's 14-element anatomy: a close (×) icon button and Save in the app bar, not Cancel/OK.
|
||||||
@@ -520,7 +627,7 @@ it('opens the range picker full screen below 600px and grows its month list both
|
|||||||
|
|
||||||
// Paging seven months forward crosses the window's forward edge, growing it to cover the month walked to.
|
// Paging seven months forward crosses the window's forward edge, growing it to cover the month walked to.
|
||||||
foreach (range(1, 7) as $ignored) {
|
foreach (range(1, 7) as $ignored) {
|
||||||
$page->keys(':focus', 'PageDown');
|
pressOnce($page)->keys(':focus', 'PageDown');
|
||||||
}
|
}
|
||||||
|
|
||||||
$page->assertScript("{$scope}.monthsTo > '2027-03-01'")
|
$page->assertScript("{$scope}.monthsTo > '2027-03-01'")
|
||||||
@@ -529,7 +636,7 @@ it('opens the range picker full screen below 600px and grows its month list both
|
|||||||
|
|
||||||
// Paging fourteen months back does the same the other way, past the original start.
|
// Paging fourteen months back does the same the other way, past the original start.
|
||||||
foreach (range(1, 14) as $ignored) {
|
foreach (range(1, 14) as $ignored) {
|
||||||
$page->keys(':focus', 'PageUp');
|
pressOnce($page)->keys(':focus', 'PageUp');
|
||||||
}
|
}
|
||||||
|
|
||||||
$page->assertScript("{$scope}.monthsFrom < '2026-03-01'")
|
$page->assertScript("{$scope}.monthsFrom < '2026-03-01'")
|
||||||
@@ -540,31 +647,95 @@ it('opens the range picker full screen below 600px and grows its month list both
|
|||||||
// script() returns the script's result, not the page, so the chain starts again after it.
|
// script() returns the script's result, not the page, so the chain starts again after it.
|
||||||
$page->script("{$scope}.moveTo('2026-09-13', false)");
|
$page->script("{$scope}.moveTo('2026-09-13', false)");
|
||||||
|
|
||||||
$page->click(day('trip-field', '2026-09-20'))
|
pressOnce($page)->click(day('trip-field', '2026-09-20'))
|
||||||
->click(day('trip-field', '2026-09-24'))
|
->click(day('trip-field', '2026-09-24'));
|
||||||
->click('#trip-field-picker [data-md-datepicker-save]')
|
|
||||||
->assertSeeIn('#trip', '{"start":"2026-09-20","end":"2026-09-24"}')
|
// What Save is about to commit, read from the picker before it is pressed: a day pressed twice
|
||||||
|
// makes it the range's end as well as its start, and the next press then begins a new range —
|
||||||
|
// which otherwise shows up only as the value never reaching the server.
|
||||||
|
$page->assertScript("JSON.stringify({$scope}.draft) === '{\"start\":\"2026-09-20\",\"end\":\"2026-09-24\"}'");
|
||||||
|
|
||||||
|
pressOnce($page)->click('#trip-field-picker [data-md-datepicker-save]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#trip', '{"start":"2026-09-20","end":"2026-09-24"}')
|
||||||
->assertScript("! {$picker}.open");
|
->assertScript("! {$picker}.open");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('keeps the months in view where they were when the full-screen range picker adds months above them', function () {
|
||||||
|
$picker = "document.querySelector('#trip-field-picker')";
|
||||||
|
|
||||||
|
$page = dateProbe()
|
||||||
|
->resize(390, 800)
|
||||||
|
// Settled before the press, as the test above says at length.
|
||||||
|
->assertScript('window.innerWidth === 390')
|
||||||
|
->assertScript(settled('document.documentElement', subtree: true));
|
||||||
|
|
||||||
|
pressOnce($page)->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript("{$picker}.dataset.mdPresentation === 'full'")
|
||||||
|
->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
|
// Scrolled close to the top, the list grows by six months above what is on screen. What sits
|
||||||
|
// under a fixed point of the list stays what sat there, rather than the list being put back
|
||||||
|
// twice — once by the picker and once more by the browser's own scroll anchoring — and
|
||||||
|
// landing half a year on.
|
||||||
|
$result = $page->script(onceInPage(<<<'JS'
|
||||||
|
(async () => {
|
||||||
|
const list = document.querySelector('#trip-field-picker [data-md-datepicker-calendar]')
|
||||||
|
const scope = Alpine.$data(document.querySelector('[data-md-datepicker]:has(#trip-field)'))
|
||||||
|
const frame = () => new Promise((resolve) => requestAnimationFrame(resolve))
|
||||||
|
const under = () => {
|
||||||
|
const box = list.getBoundingClientRect()
|
||||||
|
const element = document.elementFromPoint(box.left + box.width / 2, box.top + 150)
|
||||||
|
|
||||||
|
return element?.closest('[data-md-value]')?.dataset.mdValue ?? element?.textContent.trim() ?? null
|
||||||
|
}
|
||||||
|
|
||||||
|
const from = scope.monthsFrom
|
||||||
|
list.scrollTop = 300
|
||||||
|
const before = under()
|
||||||
|
|
||||||
|
for (let i = 0; i < 4; i++) {
|
||||||
|
await frame()
|
||||||
|
}
|
||||||
|
|
||||||
|
return { grew: scope.monthsFrom < from, before, after: under() }
|
||||||
|
})()
|
||||||
|
JS));
|
||||||
|
|
||||||
|
expect($result['grew'])->toBeTrue()
|
||||||
|
->and($result['before'])->not->toBeNull()
|
||||||
|
->and($result['after'])->toBe($result['before']);
|
||||||
|
});
|
||||||
|
|
||||||
it('closes the full-screen range picker from its app bar close button without keeping the draft', function () {
|
it('closes the full-screen range picker from its app bar close button without keeping the draft', function () {
|
||||||
$picker = "document.querySelector('#trip-field-picker')";
|
$picker = "document.querySelector('#trip-field-picker')";
|
||||||
|
|
||||||
dateProbe()
|
$page = dateProbe()
|
||||||
->resize(390, 800)
|
->resize(390, 800)
|
||||||
->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
|
// Settled before the press, as the test above says at length.
|
||||||
|
->assertScript('window.innerWidth === 390')
|
||||||
|
->assertScript(settled('document.documentElement', subtree: true));
|
||||||
|
|
||||||
|
expect($page->script(hitTarget('[aria-controls=\"trip-field-picker\"][data-md-datepicker-toggle]')))->toBe('the target');
|
||||||
|
|
||||||
|
pressOnce($page)->click('[aria-controls="trip-field-picker"][data-md-datepicker-toggle]')
|
||||||
->click(day('trip-field', '2026-09-20'))
|
->click(day('trip-field', '2026-09-20'))
|
||||||
->click('#trip-field-picker [data-md-datepicker-close]')
|
->click('#trip-field-picker [data-md-datepicker-close]');
|
||||||
->assertScript("! {$picker}.open")
|
|
||||||
|
$page->assertScript("! {$picker}.open")
|
||||||
->assertSeeIn('#trip', '{"start":"2026-09-13","end":"2026-09-15"}');
|
->assertSeeIn('#trip', '{"start":"2026-09-13","end":"2026-09-15"}');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('reaches the month and year dropdowns with Shift+M and Shift+Y', function () {
|
it('reaches the month and year dropdowns with Shift+M and Shift+Y', function () {
|
||||||
$toggle = '[aria-controls="expires-field-picker"][data-md-datepicker-toggle]';
|
$toggle = '[aria-controls="expires-field-picker"][data-md-datepicker-toggle]';
|
||||||
|
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click($toggle)
|
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click($toggle);
|
||||||
|
|
||||||
|
$page->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
$page->keys(':focus', 'Shift+M')
|
$page->keys(':focus', 'Shift+M')
|
||||||
->assertScript("document.activeElement.dataset.mdDatepickerMenuButton === 'months'");
|
->assertScript("document.activeElement.dataset.mdDatepickerMenuButton === 'months'");
|
||||||
@@ -572,8 +743,9 @@ it('reaches the month and year dropdowns with Shift+M and Shift+Y', function ()
|
|||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertScript("! document.querySelector('#expires-field-picker').matches(':popover-open')");
|
->assertScript("! document.querySelector('#expires-field-picker').matches(':popover-open')");
|
||||||
|
|
||||||
$page->click($toggle)
|
pressOnce($page)->click($toggle);
|
||||||
->assertScript(focusedDay('2026-09-13'));
|
|
||||||
|
$page->assertScript(focusedDay('2026-09-13'));
|
||||||
|
|
||||||
$page->keys(':focus', 'Shift+Y')
|
$page->keys(':focus', 'Shift+Y')
|
||||||
->assertScript("document.activeElement.dataset.mdDatepickerMenuButton === 'years'");
|
->assertScript("document.activeElement.dataset.mdDatepickerMenuButton === 'years'");
|
||||||
@@ -588,9 +760,12 @@ it('draws the shared state layer and focus ring on the menu buttons, the year an
|
|||||||
|
|
||||||
$hasBoth = fn (string $expr): string => "({$expr}).classList.contains('md-state-layer') && ({$expr}).classList.contains('md-focus-ring')";
|
$hasBoth = fn (string $expr): string => "({$expr}).classList.contains('md-state-layer') && ({$expr}).classList.contains('md-focus-ring')";
|
||||||
|
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript($hasBoth("document.querySelector('{$monthButton}')"))
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript($hasBoth("document.querySelector('{$monthButton}')"))
|
||||||
->assertScript($hasBoth("document.querySelector('{$yearButton}')"))
|
->assertScript($hasBoth("document.querySelector('{$yearButton}')"))
|
||||||
->click($monthButton)
|
->click($monthButton)
|
||||||
// toggleView() scrolls the selected option into view and focuses it once settled.
|
// toggleView() scrolls the selected option into view and focuses it once settled.
|
||||||
@@ -607,8 +782,10 @@ it('draws the shared state layer and focus ring on the menu buttons, the year an
|
|||||||
$page->keys(':focus', 'Escape')
|
$page->keys(':focus', 'Escape')
|
||||||
->assertScript("! document.querySelector('#expires-field-picker').matches(':popover-open')");
|
->assertScript("! document.querySelector('#expires-field-picker').matches(':popover-open')");
|
||||||
|
|
||||||
$page->click('#birthday-field')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')")
|
pressOnce($page)->click('#birthday-field');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')")
|
||||||
->click($modalYearButton)
|
->click($modalYearButton)
|
||||||
->assertScript($hasBoth("document.querySelector('{$modalYearButton}')"))
|
->assertScript($hasBoth("document.querySelector('{$modalYearButton}')"))
|
||||||
->assertScript("document.activeElement === {$selectedYear}")
|
->assertScript("document.activeElement === {$selectedYear}")
|
||||||
@@ -621,9 +798,12 @@ it('shows a state layer over a selected day\'s primary fill on hover, and none o
|
|||||||
$disabledDay = day('expires-field', '2026-09-09');
|
$disabledDay = day('expires-field', '2026-09-09');
|
||||||
$disabledSpan = "document.querySelector('{$disabledDay} > span')";
|
$disabledSpan = "document.querySelector('{$disabledDay} > span')";
|
||||||
|
|
||||||
$page = dateProbe()
|
$page = dateProbe();
|
||||||
->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]')
|
|
||||||
->assertScript("document.querySelector('{$selectedDay}').hasAttribute('data-md-selected')")
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('{$selectedDay}').hasAttribute('data-md-selected')")
|
||||||
->assertAttribute($disabledDay, 'aria-disabled', 'true')
|
->assertAttribute($disabledDay, 'aria-disabled', 'true')
|
||||||
// The day's own fill transitions in when the popover opens: let it settle before reading
|
// The day's own fill transitions in when the popover opens: let it settle before reading
|
||||||
// a resting value, or the "unchanged by hover" comparison below race against that instead.
|
// a resting value, or the "unchanged by hover" comparison below race against that instead.
|
||||||
@@ -650,8 +830,10 @@ it('shows a state layer over a selected day\'s primary fill on hover, and none o
|
|||||||
|
|
||||||
// A blank cell (no date, outside the shown month in modal presentation) can't be focused at
|
// A blank cell (no date, outside the shown month in modal presentation) can't be focused at
|
||||||
// all — the same rule hides its layer regardless.
|
// all — the same rule hides its layer regardless.
|
||||||
$page->click('#birthday-field')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')")
|
pressOnce($page)->click('#birthday-field');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#birthday-field-picker').matches(':modal')")
|
||||||
->hover('#birthday-field-picker [data-md-blank] > span >> nth=0')
|
->hover('#birthday-field-picker [data-md-blank] > span >> nth=0')
|
||||||
->assertScript("getComputedStyle([...document.querySelectorAll('#birthday-field-picker [data-md-blank] > span')][0], '::before').display === 'none'");
|
->assertScript("getComputedStyle([...document.querySelectorAll('#birthday-field-picker [data-md-blank] > span')][0], '::before').display === 'none'");
|
||||||
});
|
});
|
||||||
@@ -662,7 +844,10 @@ it('keeps the day indicator its fixed 40px size at a 20px root font size, growin
|
|||||||
$font = "getComputedStyle({$daySpan}).fontSize";
|
$font = "getComputedStyle({$daySpan}).fontSize";
|
||||||
$picker = "document.querySelector('#expires-field-picker')";
|
$picker = "document.querySelector('#expires-field-picker')";
|
||||||
|
|
||||||
$page = dateProbe()->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
$page = dateProbe();
|
||||||
|
|
||||||
|
// The toggle button opens or closes the picker on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[aria-controls="expires-field-picker"][data-md-datepicker-toggle]');
|
||||||
|
|
||||||
$restingWidth = (int) $page->script($width);
|
$restingWidth = (int) $page->script($width);
|
||||||
$restingFont = $page->script($font);
|
$restingFont = $page->script($font);
|
||||||
|
|||||||
@@ -73,6 +73,7 @@ function fieldProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -102,11 +103,14 @@ it('floats the label while the field has focus or holds a value', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('clears a field and tells Livewire', function () {
|
it('clears a field and tells Livewire', function () {
|
||||||
fieldProbe()
|
$page = fieldProbe()
|
||||||
->type('#name-field', 'Holiday')
|
->type('#name-field', 'Holiday')
|
||||||
->assertSeeIn('#name', 'Holiday')
|
->assertSeeIn('#name', 'Holiday');
|
||||||
->click('[data-md-field-clear]')
|
|
||||||
->assertScript("document.querySelector('#name-field').value === ''")
|
// The clear button vanishes once its field is empty: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-field-clear]');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#name-field').value === ''")
|
||||||
->assertScript("document.querySelector('#name').textContent === ''")
|
->assertScript("document.querySelector('#name').textContent === ''")
|
||||||
->assertScript("document.activeElement.id === 'name-field'");
|
->assertScript("document.activeElement.id === 'name-field'");
|
||||||
});
|
});
|
||||||
@@ -122,13 +126,18 @@ it('copies a field\'s value and says so', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('shows and hides a password', function () {
|
it('shows and hides a password', function () {
|
||||||
fieldProbe()
|
$page = fieldProbe()
|
||||||
->type('#password-field', 'secret')
|
->type('#password-field', 'secret');
|
||||||
->click('[data-md-field-reveal]')
|
|
||||||
->assertScript("document.querySelector('#password-field').type === 'text'")
|
// The reveal button toggles on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute('[data-md-field-reveal]', 'aria-label', 'Hide password')
|
pressOnce($page)->click('[data-md-field-reveal]');
|
||||||
->click('[data-md-field-reveal]')
|
|
||||||
->assertScript("document.querySelector('#password-field').type === 'password'");
|
$page->assertScript("document.querySelector('#password-field').type === 'text'")
|
||||||
|
->assertAttribute('[data-md-field-reveal]', 'aria-label', 'Hide password');
|
||||||
|
|
||||||
|
pressOnce($page)->click('[data-md-field-reveal]');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#password-field').type === 'password'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows the server\'s error in place of the hint', function () {
|
it('shows the server\'s error in place of the hint', function () {
|
||||||
@@ -161,8 +170,10 @@ it('opens the customizable select picker as M3\'s menu, where the browser suppor
|
|||||||
|
|
||||||
$page->assertScript("getComputedStyle({$select}).appearance === 'base-select'");
|
$page->assertScript("getComputedStyle({$select}).appearance === 'base-select'");
|
||||||
|
|
||||||
$page->click('#hours-field')
|
// Opens the customizable select's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$select}.matches(':open')")
|
pressOnce($page)->click('#hours-field');
|
||||||
|
|
||||||
|
$page->assertScript("{$select}.matches(':open')")
|
||||||
// M3's menu row height (menu-item.css's 48px), not the browser's own tiny option row.
|
// M3's menu row height (menu-item.css's 48px), not the browser's own tiny option row.
|
||||||
->assertScript("parseFloat(getComputedStyle({$option}).minBlockSize) === 48");
|
->assertScript("parseFloat(getComputedStyle({$option}).minBlockSize) === 48");
|
||||||
});
|
});
|
||||||
@@ -219,6 +230,7 @@ it('ends a select\'s value before its arrow with an ellipsis, at every size and
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -248,30 +260,42 @@ it('keeps a partly ticked checkbox in step with the server', function () {
|
|||||||
|
|
||||||
$page = fieldProbe()->assertScript("! {$all}.indeterminate");
|
$page = fieldProbe()->assertScript("! {$all}.indeterminate");
|
||||||
|
|
||||||
$page->click('label[for="file-a"]')
|
// A checkbox toggles on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#files', 'a')
|
pressOnce($page)->click('label[for="file-a"]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#files', 'a')
|
||||||
->assertScript("{$all}.hasAttribute('data-md-indeterminate') && {$all}.indeterminate");
|
->assertScript("{$all}.hasAttribute('data-md-indeterminate') && {$all}.indeterminate");
|
||||||
|
|
||||||
$page->click('label[for="file-b"]')
|
pressOnce($page)->click('label[for="file-b"]')
|
||||||
->click('label[for="file-c"]')
|
->click('label[for="file-c"]');
|
||||||
->assertSeeIn('#files', 'a,b,c')
|
|
||||||
|
$page->assertSeeIn('#files', 'a,b,c')
|
||||||
->assertScript("! {$all}.hasAttribute('data-md-indeterminate') && ! {$all}.indeterminate");
|
->assertScript("! {$all}.hasAttribute('data-md-indeterminate') && ! {$all}.indeterminate");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves between radio buttons with the arrow keys', function () {
|
it('moves between radio buttons with the arrow keys', function () {
|
||||||
fieldProbe()
|
$page = fieldProbe();
|
||||||
->keys('input[type="radio"][value="anyone"]', 'ArrowDown')
|
|
||||||
->assertSeeIn('#audience', 'team')
|
// A two-option radio group: a second press of the same arrow cycles back: pressOnce(),
|
||||||
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->keys('input[type="radio"][value="anyone"]', 'ArrowDown');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#audience', 'team')
|
||||||
->assertScript("document.querySelector('input[type=\"radio\"][value=\"team\"]').checked");
|
->assertScript("document.querySelector('input[type=\"radio\"][value=\"team\"]').checked");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('turns a switch on from the keyboard and from its label', function () {
|
it('turns a switch on from the keyboard and from its label', function () {
|
||||||
$page = fieldProbe()
|
$page = fieldProbe();
|
||||||
->keys('#public', 'Space')
|
|
||||||
->assertSeeIn('#public-state', 'true');
|
|
||||||
|
|
||||||
$page->click('label[for="public"]')
|
// Space toggles a switch on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#public-state', 'false')
|
pressOnce($page)->keys('#public', 'Space');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#public-state', 'true');
|
||||||
|
|
||||||
|
// A switch's label toggles it, same as the switch itself: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('label[for="public"]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#public-state', 'false')
|
||||||
->assertAttribute('#public', 'role', 'switch');
|
->assertAttribute('#public', 'role', 'switch');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,6 +360,7 @@ function fieldStateProbe(): mixed
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -391,13 +416,19 @@ it('keeps a focused field\'s focus edge under the pointer, as M3 layers focus ov
|
|||||||
// A customizable select reads as focused while its menu is open, though focus is then on an
|
// A customizable select reads as focused while its menu is open, though focus is then on an
|
||||||
// option in the top layer: in primary under the pointer, and in error when the field is.
|
// option in the top layer: in primary under the pointer, and in error when the field is.
|
||||||
if ($page->script("CSS.supports('appearance', 'base-select')") === true) {
|
if ($page->script("CSS.supports('appearance', 'base-select')") === true) {
|
||||||
$page->click('#select-field')->assertScript("document.getElementById('select-field').matches(':open')");
|
// Opens the customizable select's list over the field that triggers it: pressOnce(),
|
||||||
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#select-field');
|
||||||
|
|
||||||
|
$page->assertScript("document.getElementById('select-field').matches(':open')");
|
||||||
|
|
||||||
expect($page->script(settledEdge('select-field')))->toBe('2px '.$role('primary'));
|
expect($page->script(settledEdge('select-field')))->toBe('2px '.$role('primary'));
|
||||||
|
|
||||||
$page->keys('#select-field', 'Escape')->assertScript("! document.getElementById('select-field').matches(':open')");
|
$page->keys('#select-field', 'Escape')->assertScript("! document.getElementById('select-field').matches(':open')");
|
||||||
|
|
||||||
$page->click('#invalid-select-field')->assertScript("document.getElementById('invalid-select-field').matches(':open')");
|
pressOnce($page)->click('#invalid-select-field');
|
||||||
|
|
||||||
|
$page->assertScript("document.getElementById('invalid-select-field').matches(':open')");
|
||||||
|
|
||||||
expect($page->script(settledEdge('invalid-select-field')))->toBe('2px '.$role('error'));
|
expect($page->script(settledEdge('invalid-select-field')))->toBe('2px '.$role('error'));
|
||||||
}
|
}
|
||||||
@@ -431,6 +462,7 @@ function fieldWidthProbe(int $width)
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
<style>.probe-narrow { max-width: 200px; }</style>
|
<style>.probe-narrow { max-width: 200px; }</style>
|
||||||
@@ -509,8 +541,10 @@ it('draws the switch\'s handle at SwitchTokens\' sizes and centres, off and on',
|
|||||||
|
|
||||||
$page = fieldProbe()->assertScript("{$handle} === '52,32,16,16,16'");
|
$page = fieldProbe()->assertScript("{$handle} === '52,32,16,16,16'");
|
||||||
|
|
||||||
$page->keys('#public', 'Space')
|
// Space toggles a switch on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#public-state', 'true')
|
pressOnce($page)->keys('#public', 'Space');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#public-state', 'true')
|
||||||
->wait(0.7)
|
->wait(0.7)
|
||||||
->assertScript("{$handle} === '52,32,24,36,16'");
|
->assertScript("{$handle} === '52,32,24,36,16'");
|
||||||
});
|
});
|
||||||
@@ -520,6 +554,7 @@ it('keeps a switch\'s track and a checkbox\'s box whole beside long text in a ro
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
</head>
|
</head>
|
||||||
@@ -555,7 +590,11 @@ 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.
|
// A phone's window: the row is 329px across, less than the text beside each control would take.
|
||||||
$page = ready(visit('/selection-row-probe')->resize(393, 800), alpine: false, livewire: false);
|
$page = ready(visit('/selection-row-probe')->resize(393, 800), alpine: false, livewire: false)
|
||||||
|
// Measured once, so measured only when the layout is final: the resize has landed and the
|
||||||
|
// brand face has replaced its fallback, either of which moves every width below.
|
||||||
|
->assertScript("window.innerWidth === 393 && document.fonts.status === 'loaded'")
|
||||||
|
->assertScript(settled('document.documentElement', subtree: true));
|
||||||
|
|
||||||
expect($page->script($control('row-bare-switch', '[data-md-switch]', '[data-md-toggle]')))->toBe([52, true, true])
|
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])
|
->and($page->script($control('row-labelled-switch', '[data-md-switch]', '[data-md-toggle]')))->toBe([52, true, true])
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ function layoutPage(string $body, int $width, int $height = 900, string $dir = '
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html dir="{$dir}" lang="en">
|
<html dir="{$dir}" lang="en">
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@@ -249,8 +250,11 @@ it('shows the list alone below expanded, and the detail with a back button once
|
|||||||
$page = listDetailPage(599);
|
$page = listDetailPage(599);
|
||||||
|
|
||||||
$page->script("document.querySelector('a[href=\"#message-2\"]').focus()");
|
$page->script("document.querySelector('a[href=\"#message-2\"]').focus()");
|
||||||
$page->keys('a[href="#message-2"]', 'Enter')
|
|
||||||
->assertSeeIn('#selected', '2')
|
// The selected item's link is hidden once the detail pane covers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->keys('a[href="#message-2"]', 'Enter');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#selected', '2')
|
||||||
->assertSeeIn('#detail-text', 'The message')
|
->assertSeeIn('#detail-text', 'The message')
|
||||||
->assertScript('! '.listDetailVisible(LIST_PANE))
|
->assertScript('! '.listDetailVisible(LIST_PANE))
|
||||||
->assertScript(listDetailVisible(DETAIL_PANE))
|
->assertScript(listDetailVisible(DETAIL_PANE))
|
||||||
@@ -258,8 +262,10 @@ it('shows the list alone below expanded, and the detail with a back button once
|
|||||||
->assertScript("document.activeElement === document.querySelector('".DETAIL_PANE."')")
|
->assertScript("document.activeElement === document.querySelector('".DETAIL_PANE."')")
|
||||||
->assertScript("document.querySelector('".DETAIL_PANE." [data-md-list-detail-back] button').checkVisibility()");
|
->assertScript("document.querySelector('".DETAIL_PANE." [data-md-list-detail-back] button').checkVisibility()");
|
||||||
|
|
||||||
$page->click(DETAIL_PANE.' [data-md-list-detail-back] button')
|
// The back button closes the detail pane: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#selected', 'NULL')
|
pressOnce($page)->click(DETAIL_PANE.' [data-md-list-detail-back] button');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#selected', 'NULL')
|
||||||
->assertScript(listDetailVisible(LIST_PANE))
|
->assertScript(listDetailVisible(LIST_PANE))
|
||||||
->assertScript('! '.listDetailVisible(DETAIL_PANE))
|
->assertScript('! '.listDetailVisible(DETAIL_PANE))
|
||||||
// And back to the item it came from.
|
// And back to the item it came from.
|
||||||
@@ -302,8 +308,10 @@ it('mirrors the list-detail in a right-to-left document', function () {
|
|||||||
|
|
||||||
$page = listDetailPage(599, 'rtl');
|
$page = listDetailPage(599, 'rtl');
|
||||||
|
|
||||||
$page->click('a[href="#message-1"]')
|
// The selected item's link is hidden once the detail pane covers it: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#selected', '1')
|
pressOnce($page)->click('a[href="#message-1"]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#selected', '1')
|
||||||
->assertScript("getComputedStyle(document.querySelector('".DETAIL_PANE." [data-md-list-detail-back] svg')).transform !== 'none'");
|
->assertScript("getComputedStyle(document.querySelector('".DETAIL_PANE." [data-md-list-detail-back] svg')).transform !== 'none'");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -320,14 +328,18 @@ it('selects from Alpine through x-model, with the layout\'s own back row', funct
|
|||||||
|
|
||||||
$page = layoutPage($body, 599);
|
$page = layoutPage($body, 599);
|
||||||
|
|
||||||
$page->click('#pick')
|
// The list pane is hidden once the button that picks it is covered: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#chosen', 'b')
|
pressOnce($page)->click('#pick');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#chosen', 'b')
|
||||||
->assertSeeIn('#shown', 'b')
|
->assertSeeIn('#shown', 'b')
|
||||||
->assertScript('! '.listDetailVisible(LIST_PANE))
|
->assertScript('! '.listDetailVisible(LIST_PANE))
|
||||||
->assertScript("document.querySelector('[data-md-list-detail-back-row]').checkVisibility()");
|
->assertScript("document.querySelector('[data-md-list-detail-back-row]').checkVisibility()");
|
||||||
|
|
||||||
$page->click('[data-md-list-detail-back-row] button')
|
// The back button closes the detail pane: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#chosen', 'null')
|
pressOnce($page)->click('[data-md-list-detail-back-row] button');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#chosen', 'null')
|
||||||
->assertScript(listDetailVisible(LIST_PANE))
|
->assertScript(listDetailVisible(LIST_PANE))
|
||||||
->assertScript("document.activeElement === document.querySelector('#pick')");
|
->assertScript("document.activeElement === document.querySelector('#pick')");
|
||||||
});
|
});
|
||||||
@@ -385,8 +397,10 @@ it('docks the supporting pane as a bottom sheet below expanded, opened and close
|
|||||||
->assertScript(layoutRect(SUPPORTING_SIDE, 'bottom').' === window.innerHeight')
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'bottom').' === window.innerHeight')
|
||||||
->assertScript(layoutStyle(SUPPORTING_SIDE, 'borderTopLeftRadius')." === '28px'");
|
->assertScript(layoutStyle(SUPPORTING_SIDE, 'borderTopLeftRadius')." === '28px'");
|
||||||
|
|
||||||
$page->click($handle)
|
// The handle toggles the sheet on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertAttribute($handle, 'aria-expanded', 'true')
|
pressOnce($page)->click($handle);
|
||||||
|
|
||||||
|
$page->assertAttribute($handle, 'aria-expanded', 'true')
|
||||||
->assertScript("document.querySelector('#comment').checkVisibility()")
|
->assertScript("document.querySelector('#comment').checkVisibility()")
|
||||||
->assertScript(layoutRect(SUPPORTING_SIDE, 'bottom').' === window.innerHeight');
|
->assertScript(layoutRect(SUPPORTING_SIDE, 'bottom').' === window.innerHeight');
|
||||||
|
|
||||||
@@ -559,8 +573,10 @@ it('shows each canonical layout working on its own Layout page', function () {
|
|||||||
|
|
||||||
$compactListDetail = layoutReady(visit('/material/layout/list-detail')->resize(599, 900));
|
$compactListDetail = layoutReady(visit('/material/layout/list-detail')->resize(599, 900));
|
||||||
|
|
||||||
$compactListDetail->click('#example-list-detail a:has-text("Chiara Rossi")')
|
// The selected item's link is hidden once the detail pane covers it: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#example-list-detail [data-md-list-detail-pane="detail"]', 'Train times for Friday')
|
pressOnce($compactListDetail)->click('#example-list-detail a:has-text("Chiara Rossi")');
|
||||||
|
|
||||||
|
$compactListDetail->assertSeeIn('#example-list-detail [data-md-list-detail-pane="detail"]', 'Train times for Friday')
|
||||||
->assertScript("! document.querySelector('#example-list-detail [data-md-list-detail-pane=\"list\"]').checkVisibility()")
|
->assertScript("! document.querySelector('#example-list-detail [data-md-list-detail-pane=\"list\"]').checkVisibility()")
|
||||||
->assertNoJavaScriptErrors();
|
->assertNoJavaScriptErrors();
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ function firstPaintProbe(): void
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -101,8 +102,10 @@ it('starts the rail collapsed across the expanded class and expands it in the la
|
|||||||
->assertScript('getComputedStyle('.BOTTOM_BAR.").display === 'none'")
|
->assertScript('getComputedStyle('.BOTTOM_BAR.").display === 'none'")
|
||||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'false');
|
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'false');
|
||||||
|
|
||||||
$page->click('[data-md-navigation-rail-menu]')
|
// The menu button toggles the rail on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(railWidth(256))
|
pressOnce($page)->click('[data-md-navigation-rail-menu]');
|
||||||
|
|
||||||
|
$page->assertScript(railWidth(256))
|
||||||
->assertScript('! '.RAIL.".hasAttribute('data-md-open')")
|
->assertScript('! '.RAIL.".hasAttribute('data-md-open')")
|
||||||
->assertScript("getComputedStyle(document.querySelector('[data-md-navigation-rail-scrim]')).display === 'none'")
|
->assertScript("getComputedStyle(document.querySelector('[data-md-navigation-rail-scrim]')).display === 'none'")
|
||||||
->assertScript("document.getElementById('content').closest('[aria-hidden=\"true\"]') === null")
|
->assertScript("document.getElementById('content').closest('[aria-hidden=\"true\"]') === null")
|
||||||
@@ -193,7 +196,10 @@ it('morphs the rail header FAB between collapsed (icon only) and extended (with
|
|||||||
->assertScript("getComputedStyle({$label}).maxWidth === '0px'")
|
->assertScript("getComputedStyle({$label}).maxWidth === '0px'")
|
||||||
->assertScript("Math.round({$fab}.getBoundingClientRect().width) === Math.round({$fab}.getBoundingClientRect().height)");
|
->assertScript("Math.round({$fab}.getBoundingClientRect().width) === Math.round({$fab}.getBoundingClientRect().height)");
|
||||||
|
|
||||||
$page->click('[data-md-navigation-rail-menu]')
|
// The menu button toggles the rail on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-navigation-rail-menu]');
|
||||||
|
|
||||||
|
$page
|
||||||
// The label's width and the FAB's own shape both spring on the rail's spatial-default
|
// The label's width and the FAB's own shape both spring on the rail's spatial-default
|
||||||
// transition; wait past it for the settled, extended end state.
|
// transition; wait past it for the settled, extended end state.
|
||||||
->wait(0.6)
|
->wait(0.6)
|
||||||
@@ -215,8 +221,10 @@ it('expands the rail from large, and keeps it collapsed across a reload from the
|
|||||||
->assertScript("window.eval('window.firstPaint.width') === '256px'")
|
->assertScript("window.eval('window.firstPaint.width') === '256px'")
|
||||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'true');
|
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->click('[data-md-navigation-rail-menu]')
|
// The menu button toggles the rail on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.documentElement.getAttribute('data-rail') === 'collapsed'")
|
pressOnce($page)->click('[data-md-navigation-rail-menu]');
|
||||||
|
|
||||||
|
$page->assertScript("document.documentElement.getAttribute('data-rail') === 'collapsed'")
|
||||||
->assertScript("localStorage.getItem('material-rail') === 'collapsed'")
|
->assertScript("localStorage.getItem('material-rail') === 'collapsed'")
|
||||||
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'false')
|
->assertAttribute('[data-md-navigation-rail-menu]', 'aria-expanded', 'false')
|
||||||
->assertScript(railWidth(96));
|
->assertScript(railWidth(96));
|
||||||
@@ -248,8 +256,12 @@ it('opens the modal rail from its menu button below expanded, holding focus unti
|
|||||||
|
|
||||||
// From the keyboard: WebKit neither focuses a button on click nor returns focus to one that never had it.
|
// From the keyboard: WebKit neither focuses a button on click nor returns focus to one that never had it.
|
||||||
$page->script("document.querySelector('[data-md-navigation-rail-menu]').focus()");
|
$page->script("document.querySelector('[data-md-navigation-rail-menu]').focus()");
|
||||||
$page->keys('[data-md-navigation-rail-menu]', 'Enter')
|
|
||||||
->assertScript(RAIL.".hasAttribute('data-md-open')")
|
// Enter on the focused button is a native click on it, and it toggles the rail: pressOnce(),
|
||||||
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->keys('[data-md-navigation-rail-menu]', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript(RAIL.".hasAttribute('data-md-open')")
|
||||||
->assertScript('Math.round('.RAIL_PANEL.'.getBoundingClientRect().width) === 256')
|
->assertScript('Math.round('.RAIL_PANEL.'.getBoundingClientRect().width) === 256')
|
||||||
->assertScript(railWidth(96))
|
->assertScript(railWidth(96))
|
||||||
->assertScript("getComputedStyle(document.querySelector('[data-md-navigation-rail-heading]')).display !== 'none'")
|
->assertScript("getComputedStyle(document.querySelector('[data-md-navigation-rail-heading]')).display !== 'none'")
|
||||||
@@ -262,19 +274,26 @@ it('opens the modal rail from its menu button below expanded, holding focus unti
|
|||||||
->assertScript("document.getElementById('content').closest('[aria-hidden=\"true\"]') === null")
|
->assertScript("document.getElementById('content').closest('[aria-hidden=\"true\"]') === null")
|
||||||
->assertScript("document.activeElement === document.querySelector('[data-md-navigation-rail-menu]')");
|
->assertScript("document.activeElement === document.querySelector('[data-md-navigation-rail-menu]')");
|
||||||
|
|
||||||
$page->keys('[data-md-navigation-rail-menu]', 'Enter')
|
// Enter on the focused button is a native click on it, and it toggles the rail: pressOnce(),
|
||||||
->assertScript(RAIL.".hasAttribute('data-md-open')");
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->keys('[data-md-navigation-rail-menu]', 'Enter');
|
||||||
|
|
||||||
$page->click('[data-md-navigation-rail-scrim]')
|
$page->assertScript(RAIL.".hasAttribute('data-md-open')");
|
||||||
->assertScript('! '.RAIL.".hasAttribute('data-md-open')")
|
|
||||||
|
// The scrim closes the modal rail: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-navigation-rail-scrim]');
|
||||||
|
|
||||||
|
$page->assertScript('! '.RAIL.".hasAttribute('data-md-open')")
|
||||||
->assertScript("localStorage.getItem('material-rail') === null");
|
->assertScript("localStorage.getItem('material-rail') === null");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('slides the modal rail in on a compact window from the app bar\'s menu button', function () {
|
it('slides the modal rail in on a compact window from the app bar\'s menu button', function () {
|
||||||
$page = shellPage(599, 860);
|
$page = shellPage(599, 860);
|
||||||
|
|
||||||
$page->click('@shell-menu')
|
// Opens the modal rail over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(RAIL.".hasAttribute('data-md-open')")
|
pressOnce($page)->click('@shell-menu');
|
||||||
|
|
||||||
|
$page->assertScript(RAIL.".hasAttribute('data-md-open')")
|
||||||
->assertScript('getComputedStyle('.RAIL_PANEL.").display === 'flex'")
|
->assertScript('getComputedStyle('.RAIL_PANEL.").display === 'flex'")
|
||||||
->assertScript('Math.round('.RAIL_PANEL.'.getBoundingClientRect().left) === 0')
|
->assertScript('Math.round('.RAIL_PANEL.'.getBoundingClientRect().left) === 0')
|
||||||
->assertScript(RAIL_PANEL.'.contains(document.activeElement)');
|
->assertScript(RAIL_PANEL.'.contains(document.activeElement)');
|
||||||
@@ -287,8 +306,10 @@ it('slides the modal rail in on a compact window from the app bar\'s menu button
|
|||||||
it('keeps the rail and the theme through wire:navigate and moves aria-current', function () {
|
it('keeps the rail and the theme through wire:navigate and moves aria-current', function () {
|
||||||
$page = shellPage(1200);
|
$page = shellPage(1200);
|
||||||
|
|
||||||
$page->click('[data-md-navigation-rail-menu]')
|
// The menu button toggles the rail on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.documentElement.getAttribute('data-rail') === 'collapsed'");
|
pressOnce($page)->click('[data-md-navigation-rail-menu]');
|
||||||
|
|
||||||
|
$page->assertScript("document.documentElement.getAttribute('data-rail') === 'collapsed'");
|
||||||
|
|
||||||
$page->script("window.eval(\"Alpine.store('theme').set('dark'); window.samePage = true\")");
|
$page->script("window.eval(\"Alpine.store('theme').set('dark'); window.samePage = true\")");
|
||||||
|
|
||||||
@@ -358,6 +379,7 @@ function navigationExtrasProbe(): mixed
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -452,14 +474,19 @@ it('takes a rail out of the layout entirely when it hides collapsed, and back ov
|
|||||||
$rail = "document.querySelector('#hiding-rail [data-md-navigation-rail]')";
|
$rail = "document.querySelector('#hiding-rail [data-md-navigation-rail]')";
|
||||||
$railWidth = "Math.round({$rail}.getBoundingClientRect().width)";
|
$railWidth = "Math.round({$rail}.getBoundingClientRect().width)";
|
||||||
|
|
||||||
$page->assertScript("{$railWidth} > 200")
|
$page->assertScript("{$railWidth} > 200");
|
||||||
->click('#hiding-rail [data-md-navigation-rail-menu]')
|
|
||||||
->assertScript("{$railWidth} === 0")
|
// The menu button toggles the rail on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#hiding-rail [data-md-navigation-rail-menu]');
|
||||||
|
|
||||||
|
$page->assertScript("{$railWidth} === 0")
|
||||||
->assertScript("! {$rail}.hasAttribute('data-md-open')");
|
->assertScript("! {$rail}.hasAttribute('data-md-open')");
|
||||||
|
|
||||||
// The only way back: an application's own menu button calling $store.rail.show().
|
// The only way back: an application's own menu button calling $store.rail.show(), which opens
|
||||||
$page->click('#open-hiding-rail')
|
// the rail back over the page: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$rail}.hasAttribute('data-md-open')")
|
pressOnce($page)->click('#open-hiding-rail');
|
||||||
|
|
||||||
|
$page->assertScript("{$rail}.hasAttribute('data-md-open')")
|
||||||
->assertScript("getComputedStyle(document.querySelector('#hiding-rail [data-md-navigation-rail-panel]')).display === 'flex'")
|
->assertScript("getComputedStyle(document.querySelector('#hiding-rail [data-md-navigation-rail-panel]')).display === 'flex'")
|
||||||
->assertNoJavaScriptErrors();
|
->assertNoJavaScriptErrors();
|
||||||
});
|
});
|
||||||
@@ -469,8 +496,10 @@ it('docks a rail that hid itself back into the layout from $store.rail.toggle(),
|
|||||||
$rail = "document.querySelector('#hiding-rail [data-md-navigation-rail]')";
|
$rail = "document.querySelector('#hiding-rail [data-md-navigation-rail]')";
|
||||||
$railWidth = "Math.round({$rail}.getBoundingClientRect().width)";
|
$railWidth = "Math.round({$rail}.getBoundingClientRect().width)";
|
||||||
|
|
||||||
$page->click('#hiding-rail [data-md-navigation-rail-menu]')
|
// The menu button toggles the rail on every press: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$railWidth} === 0");
|
pressOnce($page)->click('#hiding-rail [data-md-navigation-rail-menu]');
|
||||||
|
|
||||||
|
$page->assertScript("{$railWidth} === 0");
|
||||||
|
|
||||||
// The first interactive rail on the page is this one: the narrow rail above it is fixed.
|
// The first interactive rail on the page is this one: the narrow rail above it is fixed.
|
||||||
$page->script(TOGGLE_RAIL);
|
$page->script(TOGGLE_RAIL);
|
||||||
@@ -531,6 +560,7 @@ function railValueProbe(string $mode, int $width = 1280): mixed
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -617,9 +647,12 @@ it('moves an adaptive rail\'s value with the window and the menu button, in step
|
|||||||
|
|
||||||
// `expanded`: collapsed until the menu button widens it in place.
|
// `expanded`: collapsed until the menu button widens it in place.
|
||||||
$page->resize(840, 900)
|
$page->resize(840, 900)
|
||||||
->assertScript(RAIL_VALUE." === 'collapsed'")
|
->assertScript(RAIL_VALUE." === 'collapsed'");
|
||||||
->click('[data-md-navigation-rail-menu]')
|
|
||||||
->assertScript(railWidth(256))
|
// The menu button toggles the rail on every press: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-navigation-rail-menu]');
|
||||||
|
|
||||||
|
$page->assertScript(railWidth(256))
|
||||||
->assertScript(RAIL_VALUE." === 'expanded'")
|
->assertScript(RAIL_VALUE." === 'expanded'")
|
||||||
->assertScript(railShows('expanded-only'))
|
->assertScript(railShows('expanded-only'))
|
||||||
->assertNoJavaScriptErrors();
|
->assertNoJavaScriptErrors();
|
||||||
@@ -630,8 +663,10 @@ it('reads expanded while a modal rail is open over the page, and collapsed once
|
|||||||
->assertScript(RAIL_VALUE." === 'collapsed'")
|
->assertScript(RAIL_VALUE." === 'collapsed'")
|
||||||
->assertScript('! '.railShows('expanded-only'));
|
->assertScript('! '.railShows('expanded-only'));
|
||||||
|
|
||||||
$page->click('#open-rail')
|
// Opens the modal rail over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(RAIL.".hasAttribute('data-md-open')")
|
pressOnce($page)->click('#open-rail');
|
||||||
|
|
||||||
|
$page->assertScript(RAIL.".hasAttribute('data-md-open')")
|
||||||
->assertScript(RAIL_VALUE." === 'expanded'")
|
->assertScript(RAIL_VALUE." === 'expanded'")
|
||||||
->assertScript(railShows('expanded-only'))
|
->assertScript(railShows('expanded-only'))
|
||||||
->assertScript('! '.railShows('collapsed-only'));
|
->assertScript('! '.railShows('collapsed-only'));
|
||||||
@@ -644,34 +679,15 @@ it('reads expanded while a modal rail is open over the page, and collapsed once
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes a rail with the given script and reports whether, sampling every few ms in the page
|
* Closes a rail with the given script and reports whether, sampling once a frame in the page
|
||||||
* itself, it was ever caught part-way: the whole close in one round trip, since a separate
|
* itself, it was ever caught part-way (caughtMidExit() in tests/Pest.php). Called after
|
||||||
* `script` and `assertScript` apiece already take as long as the 200ms exit (see ContainmentTest's
|
* slowMotion(), so the panel's own close — on the emphasized-accelerate easing, which stays close
|
||||||
* scrim fades).
|
* to its start for a good part of its run — has moved the couple of pixels `$partWay` looks for
|
||||||
|
* within a handful of the stretched exit's frames.
|
||||||
*/
|
*/
|
||||||
function caughtMidClose(mixed $page, string $close, string $partWay): bool
|
function caughtMidClose(mixed $page, string $close, string $partWay): bool
|
||||||
{
|
{
|
||||||
return $page->script(<<<JS
|
return $page->script(caughtMidExit($close, $partWay)) === true;
|
||||||
(async () => {
|
|
||||||
{$close}
|
|
||||||
|
|
||||||
for (let i = 0; i < 60; i++) {
|
|
||||||
if ({$partWay}) return true
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})()
|
|
||||||
JS) === true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Opened and done opening: no transition left on the element. Firefox creates a transition on its
|
|
||||||
* next refresh tick, so straight after the change its computed style already reads the end value
|
|
||||||
* while the entry has not begun, and a close then would start from where the entry did.
|
|
||||||
*/
|
|
||||||
function settled(string $element): string
|
|
||||||
{
|
|
||||||
return "{$element}.getAnimations().length === 0";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Still drawn, and strictly between resting at the window's edge and gone past it. */
|
/** Still drawn, and strictly between resting at the window's edge and gone past it. */
|
||||||
@@ -681,10 +697,16 @@ function slidingOut(string $panel): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('slides the compact rail out on close, rather than making it vanish', function () {
|
it('slides the compact rail out on close, rather than making it vanish', function () {
|
||||||
$page = shellPage(599, 860)
|
$page = shellPage(599, 860);
|
||||||
->click('@shell-menu')
|
|
||||||
->assertScript(RAIL.".hasAttribute('data-md-open')")
|
// Opens the modal rail over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(settled(RAIL_PANEL).' && Math.round('.RAIL_PANEL.'.getBoundingClientRect().left) === 0');
|
pressOnce($page)->click('@shell-menu');
|
||||||
|
|
||||||
|
$page->assertScript(RAIL.".hasAttribute('data-md-open')")
|
||||||
|
->assertScript(settled(RAIL_PANEL))
|
||||||
|
->assertScript('Math.round('.RAIL_PANEL.'.getBoundingClientRect().left) === 0');
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
expect(caughtMidClose($page, "document.querySelector('[data-md-navigation-rail-scrim]').click()", slidingOut(RAIL_PANEL)))->toBeTrue();
|
expect(caughtMidClose($page, "document.querySelector('[data-md-navigation-rail-scrim]').click()", slidingOut(RAIL_PANEL)))->toBeTrue();
|
||||||
|
|
||||||
@@ -698,11 +720,19 @@ it('slides a rail that hides when collapsed out on close, rather than making it
|
|||||||
$rail = "document.querySelector('#hiding-rail [data-md-navigation-rail]')";
|
$rail = "document.querySelector('#hiding-rail [data-md-navigation-rail]')";
|
||||||
$panel = "{$rail}.querySelector(':scope > [data-md-navigation-rail-panel]')";
|
$panel = "{$rail}.querySelector(':scope > [data-md-navigation-rail-panel]')";
|
||||||
|
|
||||||
$page->click('#hiding-rail [data-md-navigation-rail-menu]')
|
// The menu button toggles the rail, then the open button brings it back over the page:
|
||||||
->assertScript("Math.round({$rail}.getBoundingClientRect().width) === 0")
|
// pressOnce(), tests/Pest.php.
|
||||||
->click('#open-hiding-rail')
|
pressOnce($page)->click('#hiding-rail [data-md-navigation-rail-menu]');
|
||||||
->assertScript("{$rail}.hasAttribute('data-md-open')")
|
|
||||||
->assertScript(settled($panel)." && Math.round({$panel}.getBoundingClientRect().left) === 0 && Math.round({$panel}.getBoundingClientRect().width) === 256");
|
$page->assertScript("Math.round({$rail}.getBoundingClientRect().width) === 0");
|
||||||
|
|
||||||
|
pressOnce($page)->click('#open-hiding-rail');
|
||||||
|
|
||||||
|
$page->assertScript("{$rail}.hasAttribute('data-md-open')")
|
||||||
|
->assertScript(settled($panel))
|
||||||
|
->assertScript("Math.round({$panel}.getBoundingClientRect().left) === 0 && Math.round({$panel}.getBoundingClientRect().width) === 256");
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
expect(caughtMidClose($page, "{$rail}.querySelector(':scope > [data-md-navigation-rail-scrim]').click()", slidingOut($panel)))->toBeTrue();
|
expect(caughtMidClose($page, "{$rail}.querySelector(':scope > [data-md-navigation-rail-scrim]').click()", slidingOut($panel)))->toBeTrue();
|
||||||
|
|
||||||
@@ -714,13 +744,19 @@ it('slides a rail that hides when collapsed out on close, rather than making it
|
|||||||
it('fades a modal rail\'s scrim out on close while the rail stands collapsed in the layout', function () {
|
it('fades a modal rail\'s scrim out on close while the rail stands collapsed in the layout', function () {
|
||||||
$scrim = RAIL.".querySelector(':scope > [data-md-navigation-rail-scrim]')";
|
$scrim = RAIL.".querySelector(':scope > [data-md-navigation-rail-scrim]')";
|
||||||
|
|
||||||
$page = railValueProbe('modal', 1000)
|
$page = railValueProbe('modal', 1000);
|
||||||
->click('#open-rail')
|
|
||||||
->assertScript(RAIL.".hasAttribute('data-md-open')")
|
// Opens the modal rail over the button that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript(settled($scrim)." && getComputedStyle({$scrim}).opacity === '1'");
|
pressOnce($page)->click('#open-rail');
|
||||||
|
|
||||||
|
$page->assertScript(RAIL.".hasAttribute('data-md-open')")
|
||||||
|
->assertScript(settled($scrim))
|
||||||
|
->assertScript("getComputedStyle({$scrim}).opacity === '1'");
|
||||||
|
|
||||||
$fading = "(getComputedStyle({$scrim}).display !== 'none' && parseFloat(getComputedStyle({$scrim}).opacity) > 0.02 && parseFloat(getComputedStyle({$scrim}).opacity) < 0.98)";
|
$fading = "(getComputedStyle({$scrim}).display !== 'none' && parseFloat(getComputedStyle({$scrim}).opacity) > 0.02 && parseFloat(getComputedStyle({$scrim}).opacity) < 0.98)";
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
expect(caughtMidClose($page, "{$scrim}.click()", $fading))->toBeTrue();
|
expect(caughtMidClose($page, "{$scrim}.click()", $fading))->toBeTrue();
|
||||||
|
|
||||||
// The panel goes straight back into the layout: only the scrim was over the page.
|
// The panel goes straight back into the layout: only the scrim was over the page.
|
||||||
|
|||||||
+154
-77
@@ -64,6 +64,7 @@ function pickProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -79,12 +80,17 @@ function pickProbe()
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('keeps the values of filter chips as the property\'s own type', function () {
|
it('keeps the values of filter chips as the property\'s own type', function () {
|
||||||
pickProbe()
|
$page = pickProbe()
|
||||||
->assertAttribute('button[aria-pressed="true"]', 'aria-pressed', 'true')
|
->assertAttribute('button[aria-pressed="true"]', 'aria-pressed', 'true');
|
||||||
->click('button:has-text("Mon")')
|
|
||||||
->assertSeeIn('#days', '[2,1]')
|
// A filter chip toggles on every press: pressOnce(), tests/Pest.php.
|
||||||
->click('button:has-text("Tue")')
|
pressOnce($page)->click('button:has-text("Mon")');
|
||||||
->assertSeeIn('#days', '[1]')
|
|
||||||
|
$page->assertSeeIn('#days', '[2,1]');
|
||||||
|
|
||||||
|
pressOnce($page)->click('button:has-text("Tue")');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#days', '[1]')
|
||||||
->assertScript("[...document.querySelectorAll('[aria-pressed=\"true\"]')].map((chip) => chip.textContent.trim()).join() === 'Mon'");
|
->assertScript("[...document.querySelectorAll('[aria-pressed=\"true\"]')].map((chip) => chip.textContent.trim()).join() === 'Mon'");
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -92,9 +98,12 @@ it('filters a searchable choice as it is typed and chooses from the keyboard', f
|
|||||||
$list = "document.querySelector('#zone-field-list')";
|
$list = "document.querySelector('#zone-field-list')";
|
||||||
|
|
||||||
$page = pickProbe()
|
$page = pickProbe()
|
||||||
->assertValue('#zone-field', 'Zurich')
|
->assertValue('#zone-field', 'Zurich');
|
||||||
->click('#zone-field')
|
|
||||||
->assertScript("{$list}.matches(':popover-open')")
|
// Opens the searchable choice's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#zone-field');
|
||||||
|
|
||||||
|
$page->assertScript("{$list}.matches(':popover-open')")
|
||||||
->assertAttribute('#zone-field', 'aria-expanded', 'true');
|
->assertAttribute('#zone-field', 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->type('#zone-field', 'ber')
|
$page->type('#zone-field', 'ber')
|
||||||
@@ -107,20 +116,30 @@ it('filters a searchable choice as it is typed and chooses from the keyboard', f
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('puts a searchable choice back on Escape and never chooses a disabled option', function () {
|
it('puts a searchable choice back on Escape and never chooses a disabled option', function () {
|
||||||
$page = pickProbe()->click('#zone-field')->type('#zone-field', 'ber');
|
$page = pickProbe();
|
||||||
|
|
||||||
|
// Opens the searchable choice's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#zone-field');
|
||||||
|
|
||||||
|
$page->type('#zone-field', 'ber');
|
||||||
|
|
||||||
$page->keys('#zone-field', 'Escape')
|
$page->keys('#zone-field', 'Escape')
|
||||||
->assertValue('#zone-field', 'Zurich')
|
->assertValue('#zone-field', 'Zurich')
|
||||||
->assertSeeIn('#zone', 'Europe/Zurich');
|
->assertSeeIn('#zone', 'Europe/Zurich');
|
||||||
|
|
||||||
$page->click('#zone-field')->type('#zone-field', 'UTC')->keys('#zone-field', 'Enter')
|
pressOnce($page)->click('#zone-field');
|
||||||
|
|
||||||
|
$page->type('#zone-field', 'UTC')->keys('#zone-field', 'Enter')
|
||||||
->assertSeeIn('#zone', 'Europe/Zurich');
|
->assertSeeIn('#zone', 'Europe/Zurich');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('opens a searchable choice\'s list above a container that clips', function () {
|
it('opens a searchable choice\'s list above a container that clips', function () {
|
||||||
pickProbe()
|
$page = pickProbe();
|
||||||
->click('#zone-field')
|
|
||||||
->assertScript(<<<'JS'
|
// Opens the searchable choice's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#zone-field');
|
||||||
|
|
||||||
|
$page->assertScript(<<<'JS'
|
||||||
(() => {
|
(() => {
|
||||||
const list = document.querySelector('#zone-field-list');
|
const list = document.querySelector('#zone-field-list');
|
||||||
const clip = document.querySelector('#clip').getBoundingClientRect();
|
const clip = document.querySelector('#clip').getBoundingClientRect();
|
||||||
@@ -137,6 +156,7 @@ it('hangs a searchable choice\'s list under its field and as wide, bounded at 40
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -162,12 +182,18 @@ it('hangs a searchable choice\'s list under its field and as wide, bounded at 40
|
|||||||
|
|
||||||
$page = ready(visit('/wide-choices-probe')->resize(1280, 800), livewire: false);
|
$page = ready(visit('/wide-choices-probe')->resize(1280, 800), livewire: false);
|
||||||
|
|
||||||
$page->click('#bounded-zone')->assertScript("document.getElementById('bounded-zone-list').matches(':popover-open')");
|
// Opens the searchable choice's list over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#bounded-zone');
|
||||||
|
|
||||||
|
$page->assertScript("document.getElementById('bounded-zone-list').matches(':popover-open')");
|
||||||
|
|
||||||
expect($page->script($placed('bounded-zone')))->toBe([640, 640, 0]);
|
expect($page->script($placed('bounded-zone')))->toBe([640, 640, 0]);
|
||||||
|
|
||||||
$page->keys('#bounded-zone', 'Escape')->assertScript("! document.getElementById('bounded-zone-list').matches(':popover-open')");
|
$page->keys('#bounded-zone', 'Escape')->assertScript("! document.getElementById('bounded-zone-list').matches(':popover-open')");
|
||||||
$page->click('#full-zone')->assertScript("document.getElementById('full-zone-list').matches(':popover-open')");
|
|
||||||
|
pressOnce($page)->click('#full-zone');
|
||||||
|
|
||||||
|
$page->assertScript("document.getElementById('full-zone-list').matches(':popover-open')");
|
||||||
|
|
||||||
expect($page->script($placed('full-zone')))->toBe([1168, 1168, 0]);
|
expect($page->script($placed('full-zone')))->toBe([1168, 1168, 0]);
|
||||||
});
|
});
|
||||||
@@ -176,9 +202,12 @@ it('opens the search view with the results Livewire renders for the query', func
|
|||||||
$view = "document.querySelector('#find-view')";
|
$view = "document.querySelector('#find-view')";
|
||||||
|
|
||||||
// The combobox is the wrapper around the input, which carries `aria-expanded`.
|
// The combobox is the wrapper around the input, which carries `aria-expanded`.
|
||||||
$page = pickProbe()
|
$page = pickProbe();
|
||||||
->click('#find')
|
|
||||||
->assertScript("getComputedStyle({$view}).display !== 'none'")
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#find');
|
||||||
|
|
||||||
|
$page->assertScript("getComputedStyle({$view}).display !== 'none'")
|
||||||
->assertAttribute('[role="combobox"]:has(> #find)', 'aria-expanded', 'true');
|
->assertAttribute('[role="combobox"]:has(> #find)', 'aria-expanded', 'true');
|
||||||
|
|
||||||
$page->type('#find', 'con')
|
$page->type('#find', 'con')
|
||||||
@@ -197,21 +226,29 @@ it('opens the search view with the results Livewire renders for the query', func
|
|||||||
it('says so when nothing matches, and closes on a press outside or a chosen result', function () {
|
it('says so when nothing matches, and closes on a press outside or a chosen result', function () {
|
||||||
$view = "document.querySelector('#find-view')";
|
$view = "document.querySelector('#find-view')";
|
||||||
|
|
||||||
$page = pickProbe()
|
$page = pickProbe();
|
||||||
->click('#find')
|
|
||||||
->type('#find', 'zzz')
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#find');
|
||||||
|
|
||||||
|
$page->type('#find', 'zzz')
|
||||||
->assertSeeIn('#find-view', 'No files match.');
|
->assertSeeIn('#find-view', 'No files match.');
|
||||||
|
|
||||||
// The docked view opens over a scrim that covers the rest of the page, so a
|
// The docked view opens over a scrim that covers the rest of the page, so a press outside the
|
||||||
// press outside the view lands on the scrim.
|
// view lands on the scrim, which closes the view: pressOnce(), tests/Pest.php.
|
||||||
$page->click('[data-md-search]:has(#find) [data-md-search-scrim]')
|
pressOnce($page)->click('[data-md-search]:has(#find) [data-md-search-scrim]');
|
||||||
->assertScript("getComputedStyle({$view}).display === 'none'");
|
|
||||||
|
|
||||||
$page->click('#find')
|
$page->assertScript("getComputedStyle({$view}).display === 'none'");
|
||||||
->clear('#find')
|
|
||||||
->assertScript("{$view}.querySelectorAll('button').length === 3")
|
pressOnce($page)->click('#find');
|
||||||
->click('#find-view button:has-text("review.mp4")')
|
|
||||||
->assertScript("getComputedStyle({$view}).display === 'none'");
|
$page->clear('#find')
|
||||||
|
->assertScript("{$view}.querySelectorAll('button').length === 3");
|
||||||
|
|
||||||
|
// Selecting a result closes the view: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#find-view button:has-text("review.mp4")');
|
||||||
|
|
||||||
|
$page->assertScript("getComputedStyle({$view}).display === 'none'");
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,30 +258,40 @@ it('says so when nothing matches, and closes on a press outside or a chosen resu
|
|||||||
*/
|
*/
|
||||||
function searchCloseSample(string $element, string $expression): string
|
function searchCloseSample(string $element, string $expression): string
|
||||||
{
|
{
|
||||||
return <<<JS
|
return caughtMidExit(
|
||||||
(async () => {
|
<<<'JS'
|
||||||
const root = document.querySelector('[data-md-search]:has(#find)')
|
const root = document.querySelector('[data-md-search]:has(#find)')
|
||||||
root.querySelector('[data-md-search-scrim]').dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
root.querySelector('[data-md-search-scrim]').dispatchEvent(new PointerEvent('pointerdown', { bubbles: true }))
|
||||||
|
JS,
|
||||||
for (let i = 0; i < 80; i++) {
|
<<<JS
|
||||||
|
(() => {
|
||||||
const element = root.querySelector('{$element}')
|
const element = root.querySelector('{$element}')
|
||||||
const value = {$expression}
|
const value = {$expression}
|
||||||
if (getComputedStyle(element).display !== 'none' && value > 0.02 && value < 0.98) return true
|
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
return getComputedStyle(element).display !== 'none' && value > 0.02 && value < 0.98
|
||||||
}
|
|
||||||
return false
|
|
||||||
})()
|
})()
|
||||||
JS;
|
JS,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
it('fades the docked search\'s scrim out on close, rather than making it vanish', function () {
|
it('fades the docked search\'s scrim out on close, rather than making it vanish', function () {
|
||||||
$root = "document.querySelector('[data-md-search]:has(#find)')";
|
$root = "document.querySelector('[data-md-search]:has(#find)')";
|
||||||
|
|
||||||
$page = pickProbe()
|
$page = pickProbe();
|
||||||
->click('#find')
|
|
||||||
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#find');
|
||||||
|
|
||||||
|
$page
|
||||||
// Displayed as well as opaque: until the first frame of its entry the scrim is still
|
// Displayed as well as opaque: until the first frame of its entry the scrim is still
|
||||||
// `display: none` at the open state's full opacity.
|
// `display: none` at the open state's full opacity. And settled, not still fading in: an
|
||||||
->assertScript("(({ display, opacity }) => display !== 'none' && opacity === '1')(getComputedStyle({$root}.querySelector('[data-md-search-scrim]')))");
|
// engine reads a transition's end value until its next refresh tick, so an opacity of 1
|
||||||
|
// alone does not say the entry has run — and a close sampled from there has nothing left
|
||||||
|
// to fade.
|
||||||
|
->assertScript("(({ display, opacity }) => display !== 'none' && opacity === '1')(getComputedStyle({$root}.querySelector('[data-md-search-scrim]')))")
|
||||||
|
->assertScript(settled("{$root}.querySelector('[data-md-search-scrim]')"));
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
expect($page->script(searchCloseSample('[data-md-search-scrim]', 'parseFloat(getComputedStyle(element).opacity)')))->toBeTrue();
|
expect($page->script(searchCloseSample('[data-md-search-scrim]', 'parseFloat(getComputedStyle(element).opacity)')))->toBeTrue();
|
||||||
|
|
||||||
@@ -254,9 +301,16 @@ it('fades the docked search\'s scrim out on close, rather than making it vanish'
|
|||||||
it('keeps the docked search above the page while its view closes', function () {
|
it('keeps the docked search above the page while its view closes', function () {
|
||||||
$view = "document.querySelector('#find-view')";
|
$view = "document.querySelector('#find-view')";
|
||||||
|
|
||||||
$page = pickProbe()
|
$page = pickProbe();
|
||||||
->click('#find')
|
|
||||||
->assertScript("getComputedStyle({$view}).display !== 'none' && getComputedStyle({$view}).opacity === '1'");
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#find');
|
||||||
|
|
||||||
|
$page->assertScript("getComputedStyle({$view}).display !== 'none' && getComputedStyle({$view}).opacity === '1'")
|
||||||
|
// Settled, not still fading in: see the scrim's fade above.
|
||||||
|
->assertScript(settled($view));
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
// The view caught part-way through its fade only counts while the root is still at z-index 50.
|
// The view caught part-way through its fade only counts while the root is still at z-index 50.
|
||||||
expect($page->script(searchCloseSample('[data-md-search-view]', "getComputedStyle(root).zIndex === '50' ? parseFloat(getComputedStyle(element).opacity) : 0")))->toBeTrue();
|
expect($page->script(searchCloseSample('[data-md-search-view]', "getComputedStyle(root).zIndex === '50' ? parseFloat(getComputedStyle(element).opacity) : 0")))->toBeTrue();
|
||||||
@@ -267,20 +321,28 @@ it('keeps the docked search above the page while its view closes', function () {
|
|||||||
it('takes the whole screen on a compact window, with a back arrow', function () {
|
it('takes the whole screen on a compact window, with a back arrow', function () {
|
||||||
$page = pickProbe()->resize(400, 800);
|
$page = pickProbe()->resize(400, 800);
|
||||||
|
|
||||||
$page->click('#find')
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.querySelector('[data-md-search]').hasAttribute('data-md-full-screen')")
|
pressOnce($page)->click('#find');
|
||||||
->assertScript("(() => { const box = document.querySelector('#find-view').getBoundingClientRect(); return box.top === 0 && box.width === 400; })()")
|
|
||||||
->click('[data-md-search]:has(#find) [data-md-search-back]')
|
$page->assertScript("document.querySelector('[data-md-search]').hasAttribute('data-md-full-screen')")
|
||||||
->assertScript("! document.querySelector('[data-md-search]').hasAttribute('data-md-open')");
|
->assertScript("(() => { const box = document.querySelector('#find-view').getBoundingClientRect(); return box.top === 0 && box.width === 400; })()");
|
||||||
|
|
||||||
|
// The back arrow closes the search: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-search]:has(#find) [data-md-search-back]');
|
||||||
|
|
||||||
|
$page->assertScript("! document.querySelector('[data-md-search]').hasAttribute('data-md-open')");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('says how many results there are, politely, as they change', function () {
|
it('says how many results there are, politely, as they change', function () {
|
||||||
$status = "document.querySelector('[data-md-search]:has(#find) [data-md-search-status]')";
|
$status = "document.querySelector('[data-md-search]:has(#find) [data-md-search-status]')";
|
||||||
|
|
||||||
$page = pickProbe()
|
$page = pickProbe()
|
||||||
->assertScript("{$status}.getAttribute('aria-live') === 'polite' && {$status}.getAttribute('aria-atomic') === 'true'")
|
->assertScript("{$status}.getAttribute('aria-live') === 'polite' && {$status}.getAttribute('aria-atomic') === 'true'");
|
||||||
->click('#find')
|
|
||||||
->assertScript("{$status}.textContent === '3 results'");
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#find');
|
||||||
|
|
||||||
|
$page->assertScript("{$status}.textContent === '3 results'");
|
||||||
|
|
||||||
$page->type('#find', 'con')
|
$page->type('#find', 'con')
|
||||||
->assertSeeIn('#query', 'con')
|
->assertSeeIn('#query', 'con')
|
||||||
@@ -311,8 +373,10 @@ it('expands the search icon into the full-screen view, and hands focus back to t
|
|||||||
->assertScript("getComputedStyle({$root}.querySelector('[data-md-search-bar]')).display === 'none'")
|
->assertScript("getComputedStyle({$root}.querySelector('[data-md-search-bar]')).display === 'none'")
|
||||||
->assertScript("{$root}.getBoundingClientRect().width === 48 && {$trigger}.getAttribute('aria-expanded') === 'false'");
|
->assertScript("{$root}.getBoundingClientRect().width === 48 && {$trigger}.getAttribute('aria-expanded') === 'false'");
|
||||||
|
|
||||||
$page->click('[data-md-search]:has(#find-icon) [data-md-search-trigger]')
|
// Opens the search view over the trigger that expands it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$root}.hasAttribute('data-md-open') && {$root}.hasAttribute('data-md-full-screen')")
|
pressOnce($page)->click('[data-md-search]:has(#find-icon) [data-md-search-trigger]');
|
||||||
|
|
||||||
|
$page->assertScript("{$root}.hasAttribute('data-md-open') && {$root}.hasAttribute('data-md-full-screen')")
|
||||||
->assertScript("document.activeElement.id === 'find-icon'")
|
->assertScript("document.activeElement.id === 'find-icon'")
|
||||||
->assertScript("(() => { const view = {$root}.querySelector('[data-md-search-view]').getBoundingClientRect(); return view.top === 0 && view.left === 0 && view.width === innerWidth; })()")
|
->assertScript("(() => { const view = {$root}.querySelector('[data-md-search-view]').getBoundingClientRect(); return view.top === 0 && view.left === 0 && view.width === innerWidth; })()")
|
||||||
// The toolbar does not shift under the expanded search.
|
// The toolbar does not shift under the expanded search.
|
||||||
@@ -328,38 +392,44 @@ it('expands the search icon into the full-screen view, and hands focus back to t
|
|||||||
* Closes a full-screen search from its back arrow and samples, in the page, for a moment in its
|
* Closes a full-screen search from its back arrow and samples, in the page, for a moment in its
|
||||||
* exit where the layout is still full screen — the root marked, the header bar fixed at the top —
|
* exit where the layout is still full screen — the root marked, the header bar fixed at the top —
|
||||||
* with both the bar and the view part-way through their fade, rather than the bar gone back to
|
* with both the bar and the view part-way through their fade, rather than the bar gone back to
|
||||||
* its resting form (or to nothing, behind the icon) on the first frame.
|
* its resting form (or to nothing, behind the icon) on the first frame. Call it after
|
||||||
|
* slowMotion(): a loaded runner paints only a handful of frames a second, and a real 350ms exit
|
||||||
|
* can pass between two of them with nothing in between to catch.
|
||||||
*/
|
*/
|
||||||
function fullScreenCloseSample(string $input): string
|
function fullScreenCloseSample(string $input): string
|
||||||
{
|
{
|
||||||
return <<<JS
|
return caughtMidExit(
|
||||||
(async () => {
|
<<<JS
|
||||||
const root = document.querySelector('[data-md-search]:has(#{$input})')
|
const root = document.querySelector('[data-md-search]:has(#{$input})')
|
||||||
root.querySelector('[data-md-search-back]').click()
|
root.querySelector('[data-md-search-back]').click()
|
||||||
|
JS,
|
||||||
for (let i = 0; i < 80; i++) {
|
<<<'JS'
|
||||||
|
(() => {
|
||||||
const bar = getComputedStyle(root.querySelector('[data-md-search-bar]'))
|
const bar = getComputedStyle(root.querySelector('[data-md-search-bar]'))
|
||||||
const view = getComputedStyle(root.querySelector('[data-md-search-view]'))
|
const view = getComputedStyle(root.querySelector('[data-md-search-view]'))
|
||||||
const fading = (style) => style.display !== 'none' && parseFloat(style.opacity) > 0.02 && parseFloat(style.opacity) < 0.98
|
const fading = (style) => style.display !== 'none' && parseFloat(style.opacity) > 0.02 && parseFloat(style.opacity) < 0.98
|
||||||
|
|
||||||
if (! root.hasAttribute('data-md-open') && root.hasAttribute('data-md-full-screen') && bar.position === 'fixed' && fading(bar) && fading(view)) return true
|
return ! root.hasAttribute('data-md-open') && root.hasAttribute('data-md-full-screen') && bar.position === 'fixed' && fading(bar) && fading(view)
|
||||||
await new Promise((resolve) => setTimeout(resolve, 5))
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})()
|
})()
|
||||||
JS;
|
JS,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
it('closes the full-screen view and its bar together, back into the search icon', function () {
|
it('closes the full-screen view and its bar together, back into the search icon', function () {
|
||||||
$root = "document.querySelector('[data-md-search]:has(#find-icon)')";
|
$root = "document.querySelector('[data-md-search]:has(#find-icon)')";
|
||||||
$trigger = "{$root}.querySelector('[data-md-search-trigger]')";
|
$trigger = "{$root}.querySelector('[data-md-search-trigger]')";
|
||||||
|
|
||||||
$page = pickProbe()
|
$page = pickProbe();
|
||||||
->click('[data-md-search]:has(#find-icon) [data-md-search-trigger]')
|
|
||||||
->assertScript("{$root}.hasAttribute('data-md-full-screen')")
|
// Opens the search view over the trigger that expands it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-search]:has(#find-icon) [data-md-search-trigger]');
|
||||||
|
|
||||||
|
$page->assertScript("{$root}.hasAttribute('data-md-full-screen')")
|
||||||
// Its entry run: Firefox reads a transition's end value until its next refresh tick, so an
|
// Its entry run: Firefox reads a transition's end value until its next refresh tick, so an
|
||||||
// opacity of 1 alone does not say the view has finished fading in.
|
// opacity of 1 alone does not say the view has finished fading in.
|
||||||
->assertScript("{$root}.getAnimations({ subtree: true }).length === 0");
|
->assertScript(settled($root, subtree: true));
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
expect($page->script(fullScreenCloseSample('find-icon')))->toBeTrue();
|
expect($page->script(fullScreenCloseSample('find-icon')))->toBeTrue();
|
||||||
|
|
||||||
@@ -376,9 +446,13 @@ it('closes a compact window\'s full-screen view and its bar together, back into
|
|||||||
|
|
||||||
$page = pickProbe()->resize(400, 800);
|
$page = pickProbe()->resize(400, 800);
|
||||||
|
|
||||||
$page->click('#find')
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("{$root}.hasAttribute('data-md-full-screen')")
|
pressOnce($page)->click('#find');
|
||||||
->assertScript("{$root}.getAnimations({ subtree: true }).length === 0");
|
|
||||||
|
$page->assertScript("{$root}.hasAttribute('data-md-full-screen')")
|
||||||
|
->assertScript(settled($root, subtree: true));
|
||||||
|
|
||||||
|
slowMotion($page);
|
||||||
|
|
||||||
expect($page->script(fullScreenCloseSample('find')))->toBeTrue();
|
expect($page->script(fullScreenCloseSample('find')))->toBeTrue();
|
||||||
|
|
||||||
@@ -393,9 +467,12 @@ it('shows the suggestions until the first key, then the results, and counts whic
|
|||||||
$root = "document.querySelector('[data-md-search]:has(#find-icon)')";
|
$root = "document.querySelector('[data-md-search]:has(#find-icon)')";
|
||||||
$shown = fn (string $list): string => "{$root}.querySelector('[data-md-search-{$list}]').checkVisibility()";
|
$shown = fn (string $list): string => "{$root}.querySelector('[data-md-search-{$list}]').checkVisibility()";
|
||||||
|
|
||||||
$page = pickProbe()
|
$page = pickProbe();
|
||||||
->click('[data-md-search]:has(#find-icon) [data-md-search-trigger]')
|
|
||||||
->assertScript("({$shown('suggestions')}) && ! ({$shown('results')})")
|
// Opens the search view over the trigger that expands it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('[data-md-search]:has(#find-icon) [data-md-search-trigger]');
|
||||||
|
|
||||||
|
$page->assertScript("({$shown('suggestions')}) && ! ({$shown('results')})")
|
||||||
->assertScript("{$root}.querySelector('[data-md-search-status]').textContent === '3 suggestions'");
|
->assertScript("{$root}.querySelector('[data-md-search-status]').textContent === '3 suggestions'");
|
||||||
|
|
||||||
$page->type('#find-icon', 'h')
|
$page->type('#find-icon', 'h')
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ it('follows a value the server changes through a Livewire morph', function () {
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ 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 () {
|
it('finds a component from anywhere and opens its section', function () {
|
||||||
$page = ready(visit('/material/buttons'));
|
$page = ready(visit('/material/buttons'));
|
||||||
|
|
||||||
$page->keys('#content', '/')
|
// "/" opens the search view: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.activeElement.id === 'showcase-search'");
|
pressOnce($page)->keys('#content', '/');
|
||||||
|
|
||||||
|
$page->assertScript("document.activeElement.id === 'showcase-search'");
|
||||||
|
|
||||||
$page->type('#showcase-search', 'datepicker')
|
$page->type('#showcase-search', 'datepicker')
|
||||||
->assertSeeIn('[data-md-search-view]', '<x-datepicker>');
|
->assertSeeIn('[data-md-search-view]', '<x-datepicker>');
|
||||||
@@ -36,16 +38,21 @@ it('finds a component from anywhere and opens its section', function () {
|
|||||||
it('opens an example on another page at the example', function () {
|
it('opens an example on another page at the example', function () {
|
||||||
$page = ready(visit('/material'));
|
$page = ready(visit('/material'));
|
||||||
|
|
||||||
$page->click('#showcase-search')
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->type('#showcase-search', 'split button')
|
pressOnce($page)->click('#showcase-search');
|
||||||
|
|
||||||
|
$page->type('#showcase-search', 'split button')
|
||||||
->click('[data-showcase-result]:has-text("Example")')
|
->click('[data-showcase-result]:has-text("Example")')
|
||||||
->assertScript("location.pathname.endsWith('/material/buttons') && location.hash.startsWith('#example-')")
|
->assertScript("location.pathname.endsWith('/material/buttons') && location.hash.startsWith('#example-')")
|
||||||
->assertScript('(() => { const box = document.getElementById(decodeURIComponent(location.hash.slice(1))).getBoundingClientRect(); return box.top >= 0 && box.top < innerHeight; })()');
|
->assertScript('(() => { const box = document.getElementById(decodeURIComponent(location.hash.slice(1))).getBoundingClientRect(); return box.top >= 0 && box.top < innerHeight; })()');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('says so when nothing matches', function () {
|
it('says so when nothing matches', function () {
|
||||||
ready(visit('/material'))
|
$page = ready(visit('/material'));
|
||||||
->click('#showcase-search')
|
|
||||||
->type('#showcase-search', 'zzzz')
|
// Opens the search view over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#showcase-search');
|
||||||
|
|
||||||
|
$page->type('#showcase-search', 'zzzz')
|
||||||
->assertSeeIn('[data-md-search-view]', 'Nothing matches');
|
->assertSeeIn('[data-md-search-view]', 'Nothing matches');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ function sliderProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
|
|||||||
@@ -127,6 +127,7 @@ it('repaints the contrast level at once, and it survives a wire:navigate', funct
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -165,6 +166,7 @@ it('repaints the page\'s own theme-color meta, and the next page\'s after wire:n
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<meta name="theme-color" content="#000000" />
|
<meta name="theme-color" content="#000000" />
|
||||||
<meta name="theme-color" media="print" content="#ffffff" />
|
<meta name="theme-color" media="print" content="#ffffff" />
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ function timeProbe()
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
|
<link rel="icon" href="data:,">
|
||||||
<x-theme-script />
|
<x-theme-script />
|
||||||
@vite(config('livewire-material.showcase.vite'))
|
@vite(config('livewire-material.showcase.vite'))
|
||||||
@livewireStyles
|
@livewireStyles
|
||||||
@@ -72,10 +73,13 @@ function supportText(string $id): string
|
|||||||
}
|
}
|
||||||
|
|
||||||
it('opens the dial on the hour, as the bound time says', function () {
|
it('opens the dial on the hour, as the bound time says', function () {
|
||||||
timeProbe()
|
$page = timeProbe()
|
||||||
->assertValue('#meeting', '9:30 AM')
|
->assertValue('#meeting', '9:30 AM');
|
||||||
->click('#meeting')
|
|
||||||
->assertScript("document.querySelector('#meeting-dialog').open")
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#meeting-dialog').open")
|
||||||
->assertAttribute('#meeting', 'aria-expanded', 'true')
|
->assertAttribute('#meeting', 'aria-expanded', 'true')
|
||||||
->assertSeeIn('#meeting-title', 'Select time')
|
->assertSeeIn('#meeting-title', 'Select time')
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-box="hour"]', 'aria-pressed', 'true')
|
->assertAttribute('#meeting-dialog [data-md-timepicker-box="hour"]', 'aria-pressed', 'true')
|
||||||
@@ -89,9 +93,12 @@ it('opens the dial on the hour, as the bound time says', function () {
|
|||||||
it('writes the hour and the minute pressed on the dial to Livewire on OK', function () {
|
it('writes the hour and the minute pressed on the dial to Livewire on OK', function () {
|
||||||
$dial = inPicker('meeting', '[data-md-timepicker-dial]');
|
$dial = inPicker('meeting', '[data-md-timepicker-dial]');
|
||||||
|
|
||||||
$page = timeProbe()
|
$page = timeProbe();
|
||||||
->click('#meeting')
|
|
||||||
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
|
$page->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]')
|
||||||
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '03')
|
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '03')
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
|
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-box="minute"]', 'aria-pressed', 'true')
|
->assertAttribute('#meeting-dialog [data-md-timepicker-box="minute"]', 'aria-pressed', 'true')
|
||||||
@@ -107,34 +114,49 @@ it('writes the hour and the minute pressed on the dial to Livewire on OK', funct
|
|||||||
|
|
||||||
$page->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="45"]')
|
$page->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="45"]')
|
||||||
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="minute"]', '45')
|
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="minute"]', '45')
|
||||||
->assertSeeIn('#meeting-value', '09:30')
|
->assertSeeIn('#meeting-value', '09:30');
|
||||||
->click('#meeting-dialog [data-md-timepicker-confirm]')
|
|
||||||
->assertSeeIn('#meeting-value', '03:45')
|
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#meeting-value', '03:45')
|
||||||
->assertValue('#meeting', '3:45 AM')
|
->assertValue('#meeting', '3:45 AM')
|
||||||
->assertScript("! document.querySelector('#meeting-dialog').open")
|
->assertScript("! document.querySelector('#meeting-dialog').open")
|
||||||
->assertAttribute('#meeting', 'aria-expanded', 'false');
|
->assertAttribute('#meeting', 'aria-expanded', 'false');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('leaves the time alone when the picker is cancelled', function () {
|
it('leaves the time alone when the picker is cancelled', function () {
|
||||||
timeProbe()
|
$page = timeProbe();
|
||||||
->click('#meeting')
|
|
||||||
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="7"]')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '07')
|
pressOnce($page)->click('#meeting');
|
||||||
->click('#meeting-dialog [data-md-timepicker-cancel]')
|
|
||||||
->assertScript("! document.querySelector('#meeting-dialog').open")
|
$page->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="7"]')
|
||||||
|
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '07');
|
||||||
|
|
||||||
|
// The cancel button closes the dialog: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-cancel]');
|
||||||
|
|
||||||
|
$page->assertScript("! document.querySelector('#meeting-dialog').open")
|
||||||
->assertSeeIn('#meeting-value', '09:30')
|
->assertSeeIn('#meeting-value', '09:30')
|
||||||
->assertValue('#meeting', '9:30 AM');
|
->assertValue('#meeting', '9:30 AM');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('turns three o\'clock into 15 with PM', function () {
|
it('turns three o\'clock into 15 with PM', function () {
|
||||||
timeProbe()
|
$page = timeProbe();
|
||||||
->click('#meeting')
|
|
||||||
->click('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
|
$page->click('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]')
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]', 'aria-checked', 'true')
|
->assertAttribute('#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]', 'aria-checked', 'true')
|
||||||
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]')
|
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="3"]')
|
||||||
->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="15"]')
|
->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="15"]');
|
||||||
->click('#meeting-dialog [data-md-timepicker-confirm]')
|
|
||||||
->assertSeeIn('#meeting-value', '15:15')
|
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#meeting-value', '15:15')
|
||||||
->assertValue('#meeting', '3:15 PM');
|
->assertValue('#meeting', '3:15 PM');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -149,9 +171,12 @@ it('draws a German dial with 24 hours, 12 to 23 on the inner ring', function ()
|
|||||||
})()
|
})()
|
||||||
JS;
|
JS;
|
||||||
|
|
||||||
$page = timeProbe()
|
$page = timeProbe();
|
||||||
->click('#alarm')
|
|
||||||
->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-cycle', '24')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#alarm');
|
||||||
|
|
||||||
|
$page->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-cycle', '24')
|
||||||
->assertScript("getComputedStyle(document.querySelector('#alarm-dialog [data-md-timepicker-period]')).display === 'none'")
|
->assertScript("getComputedStyle(document.querySelector('#alarm-dialog [data-md-timepicker-period]')).display === 'none'")
|
||||||
->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"0\"]').textContent.trim() === '00'")
|
->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"0\"]').textContent.trim() === '00'")
|
||||||
->assertScript($radius('0').' === 101')
|
->assertScript($radius('0').' === 101')
|
||||||
@@ -163,24 +188,33 @@ it('draws a German dial with 24 hours, 12 to 23 on the inner ring', function ()
|
|||||||
$page->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="15"]')
|
$page->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="15"]')
|
||||||
->assertSeeIn('#alarm-dialog [data-md-timepicker-box="hour"]', '15')
|
->assertSeeIn('#alarm-dialog [data-md-timepicker-box="hour"]', '15')
|
||||||
->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
|
->assertAttribute('#alarm-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
|
||||||
->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]')
|
->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]');
|
||||||
->click('#alarm-dialog [data-md-timepicker-confirm]')
|
|
||||||
->assertSeeIn('#alarm-value', '15:00')
|
// The confirm button closes the dialog: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#alarm-dialog [data-md-timepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#alarm-value', '15:00')
|
||||||
->assertValue('#alarm', '15:00');
|
->assertValue('#alarm', '15:00');
|
||||||
|
|
||||||
// The outer ring is the morning.
|
// The outer ring is the morning.
|
||||||
$page->click('#alarm')
|
pressOnce($page)->click('#alarm');
|
||||||
->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="3"]')
|
|
||||||
|
$page->click('#alarm-dialog [data-md-timepicker-label="hour24"][data-md-value="3"]')
|
||||||
->assertSeeIn('#alarm-dialog [data-md-timepicker-box="hour"]', '03')
|
->assertSeeIn('#alarm-dialog [data-md-timepicker-box="hour"]', '03')
|
||||||
->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]')
|
->click('#alarm-dialog [data-md-timepicker-label="minute"][data-md-value="0"]');
|
||||||
->click('#alarm-dialog [data-md-timepicker-confirm]')
|
|
||||||
->assertSeeIn('#alarm-value', '03:00');
|
pressOnce($page)->click('#alarm-dialog [data-md-timepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#alarm-value', '03:00');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('follows a drag round the dial and settles on the nearest hour', function () {
|
it('follows a drag round the dial and settles on the nearest hour', function () {
|
||||||
$dial = inPicker('meeting', '[data-md-timepicker-dial]');
|
$dial = inPicker('meeting', '[data-md-timepicker-dial]');
|
||||||
|
|
||||||
$page = timeProbe()->click('#meeting');
|
$page = timeProbe();
|
||||||
|
|
||||||
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
// Pointer events built in the page's own realm, from twelve o'clock round to just past four.
|
// Pointer events built in the page's own realm, from twelve o'clock round to just past four.
|
||||||
$send = fn (string $type, int $degrees): string => <<<JS
|
$send = fn (string $type, int $degrees): string => <<<JS
|
||||||
@@ -215,8 +249,10 @@ it('changes the focused hour and minute with the arrow keys and confirms with En
|
|||||||
|
|
||||||
$page->script("document.querySelector('#meeting').focus()");
|
$page->script("document.querySelector('#meeting').focus()");
|
||||||
|
|
||||||
$page->keys('#meeting', 'Enter')
|
// Enter on the field opens the dialog, same as clicking it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript('document.activeElement === '.inPicker('meeting', '[data-md-timepicker-dial]'))
|
pressOnce($page)->keys('#meeting', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript('document.activeElement === '.inPicker('meeting', '[data-md-timepicker-dial]'))
|
||||||
->keys(':focus', 'ArrowUp')
|
->keys(':focus', 'ArrowUp')
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '10')
|
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '10')
|
||||||
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '10')
|
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '10')
|
||||||
@@ -232,16 +268,22 @@ it('changes the focused hour and minute with the arrow keys and confirms with En
|
|||||||
|
|
||||||
$page->keys(':focus', ['ArrowDown', 'ArrowLeft'])
|
$page->keys(':focus', ['ArrowDown', 'ArrowLeft'])
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '28')
|
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuenow', '28')
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuetext', '28 minutes')
|
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'aria-valuetext', '28 minutes');
|
||||||
->keys(':focus', 'Enter')
|
|
||||||
->assertScript("! document.querySelector('#meeting-dialog').open")
|
// Enter on the dial confirms and closes the dialog: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->keys(':focus', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript("! document.querySelector('#meeting-dialog').open")
|
||||||
->assertSeeIn('#meeting-value', '10:28');
|
->assertSeeIn('#meeting-value', '10:28');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('takes a typed time in the input variant and says what is wrong with it', function () {
|
it('takes a typed time in the input variant and says what is wrong with it', function () {
|
||||||
$page = timeProbe()
|
$page = timeProbe();
|
||||||
->click('#meeting')
|
|
||||||
->click('#meeting-dialog [data-md-timepicker-mode="input"]')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
|
$page->click('#meeting-dialog [data-md-timepicker-mode="input"]')
|
||||||
->assertSeeIn('#meeting-title', 'Enter time')
|
->assertSeeIn('#meeting-title', 'Enter time')
|
||||||
->assertScript("document.activeElement.id === 'meeting-hour'")
|
->assertScript("document.activeElement.id === 'meeting-hour'")
|
||||||
->assertValue('#meeting-hour', '09')
|
->assertValue('#meeting-hour', '09')
|
||||||
@@ -250,9 +292,13 @@ it('takes a typed time in the input variant and says what is wrong with it', fun
|
|||||||
$page->type('#meeting-hour', '13')
|
$page->type('#meeting-hour', '13')
|
||||||
->assertAttribute('#meeting-hour', 'aria-invalid', 'true')
|
->assertAttribute('#meeting-hour', 'aria-invalid', 'true')
|
||||||
// Word joiners keep the range on one line in the narrow column.
|
// Word joiners keep the range on one line in the narrow column.
|
||||||
->assertScript(supportText('meeting-hour')." === 'Hour must be 1–12'")
|
->assertScript(supportText('meeting-hour')." === 'Hour must be 1–12'");
|
||||||
->click('#meeting-dialog [data-md-timepicker-confirm]')
|
|
||||||
->assertScript("document.querySelector('#meeting-dialog').open")
|
// The confirm button closes the dialog when the time is valid; here it stays open (invalid),
|
||||||
|
// but pressed once anyway: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#meeting-dialog').open")
|
||||||
->assertSeeIn('#meeting-value', '09:30');
|
->assertSeeIn('#meeting-value', '09:30');
|
||||||
|
|
||||||
$page->clear('#meeting-hour')
|
$page->clear('#meeting-hour')
|
||||||
@@ -274,9 +320,12 @@ it('takes a typed time in the input variant and says what is wrong with it', fun
|
|||||||
it('keeps to the step and the limits', function () {
|
it('keeps to the step and the limits', function () {
|
||||||
$dial = inPicker('slot', '[data-md-timepicker-dial]');
|
$dial = inPicker('slot', '[data-md-timepicker-dial]');
|
||||||
|
|
||||||
$page = timeProbe()
|
$page = timeProbe();
|
||||||
->click('#slot')
|
|
||||||
->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"8\"]').hasAttribute('data-md-disabled')")
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#slot');
|
||||||
|
|
||||||
|
$page->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"8\"]').hasAttribute('data-md-disabled')")
|
||||||
->assertScript("! {$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"9\"]').hasAttribute('data-md-disabled')")
|
->assertScript("! {$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"9\"]').hasAttribute('data-md-disabled')")
|
||||||
->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"18\"]').hasAttribute('data-md-disabled')")
|
->assertScript("{$dial}.querySelector('[data-md-timepicker-label=\"hour24\"][data-md-value=\"18\"]').hasAttribute('data-md-disabled')")
|
||||||
// An hour outside the limits is not taken.
|
// An hour outside the limits is not taken.
|
||||||
@@ -298,12 +347,18 @@ it('keeps to the step and the limits', function () {
|
|||||||
->assertSeeIn('#slot-dialog [data-md-timepicker-range-error]', 'Choose a time from 09:00 to 17:00')
|
->assertSeeIn('#slot-dialog [data-md-timepicker-range-error]', 'Choose a time from 09:00 to 17:00')
|
||||||
->type('#slot-hour', '16')
|
->type('#slot-hour', '16')
|
||||||
->type('#slot-minute', '20')
|
->type('#slot-minute', '20')
|
||||||
->assertSeeIn('#slot-minute-support', 'Minute must be a multiple of 15')
|
->assertSeeIn('#slot-minute-support', 'Minute must be a multiple of 15');
|
||||||
->click('#slot-dialog [data-md-timepicker-confirm]')
|
|
||||||
->assertScript("document.activeElement.id === 'slot-minute'")
|
// The confirm button closes the dialog when the time is valid; here it stays open (invalid),
|
||||||
->type('#slot-minute', '45')
|
// but pressed once anyway: pressOnce(), tests/Pest.php.
|
||||||
->click('#slot-dialog [data-md-timepicker-confirm]')
|
pressOnce($page)->click('#slot-dialog [data-md-timepicker-confirm]');
|
||||||
->assertSeeIn('#slot-value', '16:45');
|
|
||||||
|
$page->assertScript("document.activeElement.id === 'slot-minute'")
|
||||||
|
->type('#slot-minute', '45');
|
||||||
|
|
||||||
|
pressOnce($page)->click('#slot-dialog [data-md-timepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#slot-value', '16:45');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('gives focus back to the field on Escape', function () {
|
it('gives focus back to the field on Escape', function () {
|
||||||
@@ -311,8 +366,10 @@ it('gives focus back to the field on Escape', function () {
|
|||||||
|
|
||||||
$page->script("document.querySelector('#meeting').focus()");
|
$page->script("document.querySelector('#meeting').focus()");
|
||||||
|
|
||||||
$page->keys('#meeting', 'Enter')
|
// Enter on the field opens the dialog, same as clicking it: pressOnce(), tests/Pest.php.
|
||||||
->assertScript("document.querySelector('#meeting-dialog').open")
|
pressOnce($page)->keys('#meeting', 'Enter');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#meeting-dialog').open")
|
||||||
->keys(':focus', 'ArrowUp')
|
->keys(':focus', 'ArrowUp')
|
||||||
->keys(':focus', 'Escape')
|
->keys(':focus', 'Escape')
|
||||||
->assertScript("! document.querySelector('#meeting-dialog').open")
|
->assertScript("! document.querySelector('#meeting-dialog').open")
|
||||||
@@ -321,9 +378,12 @@ it('gives focus back to the field on Escape', function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('stays open with its draft through a Livewire render', function () {
|
it('stays open with its draft through a Livewire render', function () {
|
||||||
$page = timeProbe()
|
$page = timeProbe();
|
||||||
->click('#meeting')
|
|
||||||
->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="5"]')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
|
$page->click('#meeting-dialog [data-md-timepicker-label="hour12"][data-md-value="5"]')
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute');
|
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute');
|
||||||
|
|
||||||
$page->script('window.eval("Livewire.first().touch()")');
|
$page->script('window.eval("Livewire.first().touch()")');
|
||||||
@@ -332,18 +392,23 @@ it('stays open with its draft through a Livewire render', function () {
|
|||||||
->assertScript("document.querySelector('#meeting-dialog').open")
|
->assertScript("document.querySelector('#meeting-dialog').open")
|
||||||
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '05')
|
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '05')
|
||||||
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
|
->assertAttribute('#meeting-dialog [data-md-timepicker-dial]', 'data-md-view', 'minute')
|
||||||
->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="10"]')
|
->click('#meeting-dialog [data-md-timepicker-label="minute"][data-md-value="10"]');
|
||||||
->click('#meeting-dialog [data-md-timepicker-confirm]')
|
|
||||||
->assertSeeIn('#meeting-value', '05:10');
|
pressOnce($page)->click('#meeting-dialog [data-md-timepicker-confirm]');
|
||||||
|
|
||||||
|
$page->assertSeeIn('#meeting-value', '05:10');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('moves the period selector between AM and PM with arrow keys, as a radio group', function () {
|
it('moves the period selector between AM and PM with arrow keys, as a radio group', function () {
|
||||||
$am = '#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="am"]';
|
$am = '#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="am"]';
|
||||||
$pm = '#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]';
|
$pm = '#meeting-dialog [data-md-timepicker-display] [data-md-timepicker-period-option="pm"]';
|
||||||
|
|
||||||
$page = timeProbe()
|
$page = timeProbe();
|
||||||
->click('#meeting')
|
|
||||||
->assertAttribute($am, 'role', 'radio')
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
|
$page->assertAttribute($am, 'role', 'radio')
|
||||||
->assertAttribute($am, 'aria-checked', 'true')
|
->assertAttribute($am, 'aria-checked', 'true')
|
||||||
->assertAttribute($pm, 'aria-checked', 'false')
|
->assertAttribute($pm, 'aria-checked', 'false')
|
||||||
// show() moves focus to the dial from inside Alpine's $nextTick, which Livewire's bundled
|
// show() moves focus to the dial from inside Alpine's $nextTick, which Livewire's bundled
|
||||||
@@ -355,25 +420,32 @@ it('moves the period selector between AM and PM with arrow keys, as a radio grou
|
|||||||
|
|
||||||
$page->script("document.querySelector('{$am}').focus()");
|
$page->script("document.querySelector('{$am}').focus()");
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowRight')
|
// A two-option radio group: a second press of the same arrow cycles back: pressOnce(),
|
||||||
->assertAttribute($pm, 'aria-checked', 'true')
|
// tests/Pest.php.
|
||||||
|
pressOnce($page)->keys(':focus', 'ArrowRight');
|
||||||
|
|
||||||
|
$page->assertAttribute($pm, 'aria-checked', 'true')
|
||||||
->assertAttribute($am, 'aria-checked', 'false')
|
->assertAttribute($am, 'aria-checked', 'false')
|
||||||
->assertScript("document.activeElement.dataset.mdTimepickerPeriodOption === 'pm'")
|
->assertScript("document.activeElement.dataset.mdTimepickerPeriodOption === 'pm'")
|
||||||
// M3's radio-in-a-list: an arrow key moves to the other option and selects it, so PM
|
// M3's radio-in-a-list: an arrow key moves to the other option and selects it, so PM
|
||||||
// reaches the dial straight away — the hour turns 9 into 21 without a confirm.
|
// reaches the dial straight away — the hour turns 9 into 21 without a confirm.
|
||||||
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '09');
|
->assertSeeIn('#meeting-dialog [data-md-timepicker-box="hour"]', '09');
|
||||||
|
|
||||||
$page->keys(':focus', 'ArrowLeft')
|
pressOnce($page)->keys(':focus', 'ArrowLeft');
|
||||||
->assertAttribute($am, 'aria-checked', 'true')
|
|
||||||
|
$page->assertAttribute($am, 'aria-checked', 'true')
|
||||||
->assertAttribute($pm, 'aria-checked', 'false')
|
->assertAttribute($pm, 'aria-checked', 'false')
|
||||||
->assertScript("document.activeElement.dataset.mdTimepickerPeriodOption === 'am'");
|
->assertScript("document.activeElement.dataset.mdTimepickerPeriodOption === 'am'");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('lies the dial on its side in a short landscape window', function () {
|
it('lies the dial on its side in a short landscape window', function () {
|
||||||
$page = timeProbe()
|
$page = timeProbe()
|
||||||
->resize(700, 400)
|
->resize(700, 400);
|
||||||
->click('#meeting')
|
|
||||||
->assertScript("document.querySelector('#meeting-dialog').open");
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
|
$page->assertScript("document.querySelector('#meeting-dialog').open");
|
||||||
|
|
||||||
// Side by side rather than stacked: the display (216px wide) sits centred in the dial's row,
|
// Side by side rather than stacked: the display (216px wide) sits centred in the dial's row,
|
||||||
// inside its height, with the dial 36px after it, and the picker's own layout (flex column)
|
// inside its height, with the dial 36px after it, and the picker's own layout (flex column)
|
||||||
@@ -406,7 +478,14 @@ it('keeps the dial and the boxes their fixed pixel size at a 20px root font size
|
|||||||
$dialWidth = "Math.round({$dial}.getBoundingClientRect().width)";
|
$dialWidth = "Math.round({$dial}.getBoundingClientRect().width)";
|
||||||
$font = "getComputedStyle({$hourBox}).fontSize";
|
$font = "getComputedStyle({$hourBox}).fontSize";
|
||||||
|
|
||||||
$page = timeProbe()->click('#meeting');
|
$page = timeProbe();
|
||||||
|
|
||||||
|
// Opens the modal dialog over the field that triggers it: pressOnce(), tests/Pest.php.
|
||||||
|
pressOnce($page)->click('#meeting');
|
||||||
|
|
||||||
|
// After the dialog's entry, not during it: mid-scale the dial measures 0.95 of its own size,
|
||||||
|
// which would make the resting width this test compares against a number the CSS never sets.
|
||||||
|
$page->assertScript(settled("document.querySelector('#meeting-dialog')", subtree: true));
|
||||||
|
|
||||||
$restingWidth = (int) $page->script($dialWidth);
|
$restingWidth = (int) $page->script($dialWidth);
|
||||||
$restingFont = $page->script($font);
|
$restingFont = $page->script($font);
|
||||||
|
|||||||
+159
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use NoNameWeb\LivewireMaterial\Tests\TestCase;
|
use NoNameWeb\LivewireMaterial\Tests\TestCase;
|
||||||
|
use Pest\Browser\Api\Webpage;
|
||||||
|
|
||||||
pest()->extend(TestCase::class)->in('Feature', 'Browser');
|
pest()->extend(TestCase::class)->in('Feature', 'Browser');
|
||||||
|
|
||||||
@@ -33,6 +34,164 @@ function layoutRoot(string $html, ?string $tag = null): array
|
|||||||
return $root + collect($pairs)->mapWithKeys(fn (array $pair): array => [$pair[1] => $pair[2] ?? ''])->all();
|
return $root + collect($pairs)->mapWithKeys(fn (array $pair): array => [$pair[1] => $pair[2] ?? ''])->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stretches every motion duration token (resources/css/tokens/motion.css) on :root to $duration,
|
||||||
|
* so a test that has to catch an exit half-way is not racing the machine: a round trip for a
|
||||||
|
* separate script() and assertScript() already costs real time, easily as much as a real exit
|
||||||
|
* (150–650ms), so a single sample taken after triggering one can just as easily land before it
|
||||||
|
* starts or after it has already finished on a loaded runner. Chain it, after the open has
|
||||||
|
* settled, immediately before the action that triggers the exit being sampled.
|
||||||
|
*
|
||||||
|
* Three seconds, not one: a loaded runner paints only a handful of frames a second, and a one-second
|
||||||
|
* exit can begin and end between two of them with no frame in between showing it part-way. It costs
|
||||||
|
* nothing in runtime — what made those tests take 45 seconds apiece was a script sleeping through
|
||||||
|
* the exit in the page, past the plugin's budget for one attempt (onceInPage()), not the stretch.
|
||||||
|
*
|
||||||
|
* Use this only in a test asserting *that* something animates — a part-way opacity, offset or
|
||||||
|
* transform. Never in a test asserting a duration value, which this would make wrong.
|
||||||
|
*/
|
||||||
|
function slowMotion(mixed $page, string $duration = '3s'): mixed
|
||||||
|
{
|
||||||
|
$tokens = [
|
||||||
|
'--md-sys-motion-spatial-fast-duration',
|
||||||
|
'--md-sys-motion-spatial-default-duration',
|
||||||
|
'--md-sys-motion-spatial-slow-duration',
|
||||||
|
'--md-sys-motion-effects-fast-duration',
|
||||||
|
'--md-sys-motion-effects-default-duration',
|
||||||
|
'--md-sys-motion-effects-slow-duration',
|
||||||
|
'--md-sys-motion-duration-short',
|
||||||
|
'--md-sys-motion-duration-medium',
|
||||||
|
'--md-sys-motion-duration-long',
|
||||||
|
];
|
||||||
|
|
||||||
|
$css = collect($tokens)->map(fn (string $token): string => "{$token}: {$duration} !important;")->implode(' ');
|
||||||
|
|
||||||
|
$page->script(<<<JS
|
||||||
|
document.head.insertAdjacentHTML('beforeend', '<style data-slow-motion>:root { {$css} }</style>')
|
||||||
|
JS);
|
||||||
|
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Takes slowMotion() back off, for a test that samples one exit and then goes on to time the next
|
||||||
|
* against the real durations.
|
||||||
|
*/
|
||||||
|
function fullSpeed(mixed $page): mixed
|
||||||
|
{
|
||||||
|
$page->script("document.querySelectorAll('style[data-slow-motion]').forEach((style) => style.remove())");
|
||||||
|
|
||||||
|
return $page;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JS expression answering whether `$element` has finished whatever it animates — chained as an
|
||||||
|
* assertScript() after an open, before a test stretches motion and samples the exit, or before it
|
||||||
|
* presses something whose box is still moving. `$subtree` reads its descendants' animations too.
|
||||||
|
*
|
||||||
|
* Two animation frames first, because an empty `getAnimations()` means two different things: an
|
||||||
|
* entry that has run, and one the engine has not created yet. Until the first style update after
|
||||||
|
* `x-show` reveals an element its `@starting-style` entry does not exist, and the computed style
|
||||||
|
* already reads the open value, so a bare "no animations, opacity 1" passes on a page that has
|
||||||
|
* drawn nothing since the open — and the exit sampled from there turns out to be an entry barely
|
||||||
|
* begun, with nothing left to catch. A runner painting a handful of frames a second sits in that
|
||||||
|
* window for hundreds of milliseconds at a time.
|
||||||
|
*/
|
||||||
|
function settled(string $element, bool $subtree = false): string
|
||||||
|
{
|
||||||
|
$options = $subtree ? '{ subtree: true }' : '';
|
||||||
|
|
||||||
|
return <<<JS
|
||||||
|
(async () => {
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(resolve))
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(resolve))
|
||||||
|
|
||||||
|
return {$element}.getAnimations({$options}).length === 0
|
||||||
|
})()
|
||||||
|
JS;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs `$expression` — an async IIFE — in the page once, however often the plugin asks for it, and
|
||||||
|
* answers with what that one run returned.
|
||||||
|
*
|
||||||
|
* The plugin gives each script() and assertScript() attempt 1000ms and then runs the whole
|
||||||
|
* expression again, for 45 seconds, before one last attempt with the full timeout (AwaitableWebpage,
|
||||||
|
* Execution::waitForExpectation). Anything that takes longer than a second in the page therefore
|
||||||
|
* runs some fifty times and only succeeds on that last attempt, which costs 45 seconds a test —
|
||||||
|
* and where the expression presses something, every run after the first presses it again, on a page
|
||||||
|
* the first run has already left somewhere else, so what comes back describes none of them.
|
||||||
|
*/
|
||||||
|
function onceInPage(string $expression): string
|
||||||
|
{
|
||||||
|
$memo = '__onceInPage'.crc32($expression);
|
||||||
|
|
||||||
|
return "(() => (window.{$memo} ??= {$expression}))()";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `$page` for an action that must happen exactly once: the plugin's own page object, without the
|
||||||
|
* retry every call on `$page` goes through (see onceInPage()). That retry does the whole call
|
||||||
|
* again after 1000ms, which costs an assertion nothing but repeats a press: a key or a click the
|
||||||
|
* page took longer than that to handle arrived twice — one PageDown paging two months, a day
|
||||||
|
* pressed twice making the range's start its end as well — or the second press found what the
|
||||||
|
* first one opened lying over its button, and never landed. Here the action runs once, with
|
||||||
|
* Playwright's own wait for the element and the whole timeout.
|
||||||
|
*
|
||||||
|
* For a press a second one would undo or overturn — a menu's trigger toggles it, a day pressed
|
||||||
|
* twice ends the range it began — into something that can take a loaded runner that long: opening
|
||||||
|
* a menu, or the full-screen range picker, which re-renders every day of every month it holds on
|
||||||
|
* each press. Assert on `$page` again. The plugin retrying every action is the cause, and every
|
||||||
|
* press in the suite is exposed to it; these are the ones slow and undoable enough to have shown it.
|
||||||
|
*
|
||||||
|
* The rule is every press that opens a covering layer (a dialog, a sheet, a full-screen view, a
|
||||||
|
* modal picker, a menu) or changes state a second press would alter (a toggle, a day cell, a
|
||||||
|
* paging key, a chip or switch, a grip, a Save or close), not only the ones a failure has caught.
|
||||||
|
*/
|
||||||
|
function pressOnce(mixed $page): Webpage
|
||||||
|
{
|
||||||
|
return new Webpage($page->page(), $page->url());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A JS expression that runs `$trigger` in the page and then samples `$condition` there once per
|
||||||
|
* animation frame, answering whether it ever held: the whole exit and its sample in one round trip,
|
||||||
|
* since a separate script() and assertScript() apiece already cost as much as a real exit.
|
||||||
|
*
|
||||||
|
* On animation frames rather than a timer, which is what the sampling loops used to do: a loaded
|
||||||
|
* engine — WebKitGTK on the runner — stops painting while a tight `setTimeout` loop holds the main
|
||||||
|
* thread, so the CSS transition never advances and every sample reads the opening value, then
|
||||||
|
* `display: none`. The exit was reported as a vanishing when nothing had gone wrong but the frames.
|
||||||
|
* Asking for a frame both paces the loop to the renderer and makes it run, so each sample is a
|
||||||
|
* frame someone would have seen.
|
||||||
|
*
|
||||||
|
* The budget covers most of a slowMotion() exit, because a loaded runner can leave a second between
|
||||||
|
* two frames of a heavy page and the one frame that shows the exit half-way may be the third.
|
||||||
|
*
|
||||||
|
* Sampled once through onceInPage(), because triggering the exit a second time on a page whose
|
||||||
|
* first one is long finished would report a miss whatever the page did the first time.
|
||||||
|
*/
|
||||||
|
function caughtMidExit(string $trigger, string $condition, int $budget = 2500): string
|
||||||
|
{
|
||||||
|
return onceInPage(<<<JS
|
||||||
|
(async () => {
|
||||||
|
{$trigger}
|
||||||
|
|
||||||
|
const deadline = performance.now() + {$budget}
|
||||||
|
|
||||||
|
while (performance.now() < deadline) {
|
||||||
|
if ({$condition}) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => requestAnimationFrame(resolve))
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})()
|
||||||
|
JS);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The page is done loading, and Alpine and/or Livewire (whichever the page renders) have booted —
|
* The page is done loading, and Alpine and/or Livewire (whichever the page renders) have booted —
|
||||||
* chained after visit(), before a Browser test probe reads anything either one wires up. Most
|
* chained after visit(), before a Browser test probe reads anything either one wires up. Most
|
||||||
|
|||||||
Reference in New Issue
Block a user