Measure carousel items against the padded focal size in browser tests

Plan step 32. Since containment.md C-12 the carousel defaults to M3's 16dp
of content padding, and Compose's createShiftedKeylineListForContentPadding
shrinks every keyline near the ends by a share of it, so a large item at
rest at the start or the end is masked by 1.33px a side in the showcase.
The tests asked for a zero inset there and failed; `open(i)` asks whether
an item is at the largest size any item has at that position. The helper's
list of showcase carousels gains the multi-aspect one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qwx5USif3wFFmxtHg5U1g9
This commit is contained in:
Andreas Reinhold / reini
2026-09-14 13:35:58 +02:00
co-authored by Claude Opus 5
parent 9942e98fdd
commit 3434426e23
+17 -11
View File
@@ -34,11 +34,16 @@ class CarouselMorphProbe extends Component
/** /**
* A script run against one carousel of the showcase (0 multi-browse, 1 hero, 2 uncontained, * A script run against one carousel of the showcase (0 multi-browse, 1 hero, 2 uncontained,
* 3 full-screen), or of another page under `scope`, scrolled into view. `root`, `scroller` and * 3 multi-aspect, 4 full-screen), or of another page under `scope`, scrolled into view. `root`,
* `items` are in scope, with `surface(i)`, `inset(i)` (the mask on each side of item i), * `scroller` and `items` are in scope, with `surface(i)`, `inset(i)` (the mask on each side of
* `snap(i)` (the scroll offset that brings item i into focus) and `at(i)`; the script's value is * item i), `open(i)` (item i is at the largest size any item has here), `snap(i)` (the scroll
* the result. Pest retries a failing assertion and gives each attempt a second: keep scripts well * offset that brings item i into focus) and `at(i)`; the script's value is the result. Pest
* inside it. * retries a failing assertion and gives each attempt a second: keep scripts well inside it.
*
* `open(i)` rather than a zero inset: M3's 16dp of content padding, the carousel's default since
* containment.md C-12, shrinks every keyline near the ends (Compose's
* createShiftedKeylineListForContentPadding), so a large item resting at the start or the end is
* masked by that share of the padding — 1.33px on each side in the showcase's first carousel.
*/ */
function onCarousel(int $index, string $body, string $scope = '#carousel'): string function onCarousel(int $index, string $body, string $scope = '#carousel'): string
{ {
@@ -51,6 +56,7 @@ function onCarousel(int $index, string $body, string $scope = '#carousel'): stri
const size = parseFloat(root.style.getPropertyValue('--material-carousel-slot')) const size = parseFloat(root.style.getPropertyValue('--material-carousel-slot'))
const surface = (i) => items[i].querySelector('[data-material-carousel-surface]') const surface = (i) => items[i].querySelector('[data-material-carousel-surface]')
const inset = (i) => parseFloat(surface(i).style.getPropertyValue('--material-carousel-inset')) const inset = (i) => parseFloat(surface(i).style.getPropertyValue('--material-carousel-inset'))
const open = (i) => inset(i) - Math.min(...items.map((_, j) => inset(j))) < 0.5
const snap = (i) => Math.min(Math.max(i * (size + gap) - parseFloat(items[i].style.scrollMarginInlineStart), 0), scroller.scrollWidth - scroller.clientWidth) const snap = (i) => Math.min(Math.max(i * (size + gap) - parseFloat(items[i].style.scrollMarginInlineStart), 0), scroller.scrollWidth - scroller.clientWidth)
const at = (i) => Math.abs(Math.abs(scroller.scrollLeft) - snap(i)) < 1.5 const at = (i) => Math.abs(Math.abs(scroller.scrollLeft) - snap(i)) < 1.5
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms)) const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms))
@@ -73,7 +79,7 @@ it('masks items by their place between the keylines, and changes the large one a
$page = carouselShowcase() $page = carouselShowcase()
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertScript(onCarousel(0, <<<'JS' ->assertScript(onCarousel(0, <<<'JS'
return size > 0 && inset(0) < 0.5 && inset(items.length - 1) > 0.5 return size > 0 && open(0) && ! open(items.length - 1)
&& getComputedStyle(surface(0)).clipPath.startsWith('inset(') && getComputedStyle(surface(0)).clipPath.startsWith('inset(')
JS)); JS));
@@ -122,7 +128,7 @@ it('moves one item with the arrow keys, and to the ends with Home and End', func
->assertScript(onCarousel(0, 'return at(1)')); ->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 && inset(items.length - 1) < 0.5 && 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'));
$page->keys(':focus', 'Home') $page->keys(':focus', 'Home')
->assertScript(onCarousel(0, 'return at(0)')); ->assertScript(onCarousel(0, 'return at(0)'));
@@ -141,11 +147,11 @@ it('snaps a scroll that stops between items onto an item', function () {
it('brings a partly hidden item into focus when it is pressed', function () { it('brings a partly hidden item into focus when it is pressed', function () {
$page = carouselShowcase() $page = carouselShowcase()
->assertScript(onCarousel(0, 'return inset(4) > 0.5')); ->assertScript(onCarousel(0, 'return ! open(4)'));
$page->script(onCarousel(0, 'items[4].querySelector(\'[data-material-carousel-content]\').click()')); $page->script(onCarousel(0, 'items[4].querySelector(\'[data-material-carousel-content]\').click()'));
$page->assertScript(onCarousel(0, 'return inset(4) < 0.5 && scroller.scrollLeft > 0')); $page->assertScript(onCarousel(0, 'return open(4) && scroller.scrollLeft > 0'));
}); });
it('scrolls instantly and leaves every item unmasked under reduced motion', function () { it('scrolls instantly and leaves every item unmasked under reduced motion', function () {
@@ -183,11 +189,11 @@ it('mirrors in a right-to-left page', function () {
$page = visit('/carousel-rtl-probe')->waitForEvent('networkidle') $page = visit('/carousel-rtl-probe')->waitForEvent('networkidle')
->assertNoJavaScriptErrors() ->assertNoJavaScriptErrors()
->assertScript(onCarousel(0, 'return size > 0 && at(0) && inset(0) < 0.5 && inset(items.length - 1) > 0.5', 'body')); ->assertScript(onCarousel(0, 'return size > 0 && at(0) && open(0) && ! open(items.length - 1)', 'body'));
$page->keys('[data-material-carousel-item] >> nth=0', 'ArrowLeft') $page->keys('[data-material-carousel-item] >> nth=0', 'ArrowLeft')
->assertScript(onCarousel(0, <<<'JS' ->assertScript(onCarousel(0, <<<'JS'
return scroller.scrollLeft < -1 && at(1) && inset(0) > 0.5 && inset(1) < 0.5 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'));