fix(client): rebind the scrollbar indirection on three missed elevated surfaces
Review found three scroll containers sitting on surfaces the rebinding contract covers, none of which rebound: ui-primitives' shared Menu card on --dsw-specific-menu (the surface PopupSelectView already rebinds for), and the composer input and question composer cards, both on --dsw-specific-input-major. Each rendered the l1 thumb, which differs from l2 only in the dark palette and only on that surface, so a light-palette screenshot and a code read both look correct. Adds the mechanical check that would have caught them instead of leaving it to inspection: a sheet that scrolls somewhere and paints a known elevated surface somewhere must rebind. The elevated set is derived from the sheets that already rebind, since a rebinding rule paints the surface whose elevation it declares, so a new elevated surface joins the set by rebinding rather than by anyone updating a list. Surface-level rather than element-level because the card and the descendant that scrolls are separate rules and CSS text does not say which contains which. Verified by reverting each of the three fixes in turn: the check names the sheet and the surface every time. Also commits snapshots/sidebar-scrollbar/geometry.expected.md, the resolved scrollbar style and geometry in both palettes. The aria goldens the other web scenarios commit cannot carry a CSS-only change, since it alters no DOM and no accessible name and leaves their trees byte-identical. Absolute coordinates stay out: they track font metrics and the laid-out sidebar width, so committing them would document the platform and force a per-platform re-record.
This commit is contained in:
@@ -59,13 +59,27 @@
|
||||
// the same query flipped from the hover colour to the resting one).
|
||||
import { readFile } from 'node:fs/promises'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
|
||||
import { launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold } from './scaffold.ts'
|
||||
import {
|
||||
assertFixtureInventory, compareOrRefreshGolden, launchWebScaffold, seedSession, watchConsole,
|
||||
webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { saveFailureShot } from './support.ts'
|
||||
|
||||
const SEED = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/sidebar-scrollbar', import.meta.url))
|
||||
/**
|
||||
* Committed golden of the resolved scrollbar style and geometry, in both
|
||||
* palettes. The aria goldens the other scenarios commit cannot carry this
|
||||
* change: it alters no DOM and no accessible name, so their normalized trees are
|
||||
* byte-identical with and without it. This one records the values instead, which
|
||||
* makes an unintended shift in thumb colour, band width, or rendering path a
|
||||
* reviewable diff rather than an assertion someone has to think about.
|
||||
*/
|
||||
const GEOMETRY_EXPECTED = join(SNAPSHOT_DIR, 'geometry.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
/** Enough rows that the list overflows the 800px-tall viewport's sidebar; the scenario asserts the overflow rather than trusting it. */
|
||||
const SEED_COUNT = 24
|
||||
@@ -179,6 +193,48 @@ function measureList(page: Page): Promise<ListMetrics> {
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the golden body: the resolved scrollbar style of the list in each
|
||||
* palette, plus the geometric relations the fix establishes.
|
||||
*
|
||||
* Absolute coordinates are deliberately absent. `timeRight`, `clientRight`, and
|
||||
* `borderRight` depend on the sidebar's laid-out width and on font metrics, so
|
||||
* committing them would make the golden fail on a machine whose fonts measure
|
||||
* differently — a fixture that has to be re-recorded per platform documents the
|
||||
* platform, not the change. What is recorded instead is the band, the overlap,
|
||||
* and the two orderings, each of which is a difference or a comparison and so
|
||||
* survives any layout that keeps the reservation.
|
||||
* @param light - metrics measured under the light palette.
|
||||
* @param dark - metrics measured under the dark palette.
|
||||
* @returns the golden body, without a trailing newline.
|
||||
*/
|
||||
function renderGeometry(light: ListMetrics, dark: ListMetrics): string {
|
||||
const palette = (name: string, metrics: ListMetrics): string[] => [
|
||||
`## ${name}`,
|
||||
'',
|
||||
`- scrollbar-gutter: ${metrics.gutter}`,
|
||||
`- ::-webkit-scrollbar width: ${metrics.width}`,
|
||||
`- ::-webkit-scrollbar-track background: ${metrics.track}`,
|
||||
`- scrollbar-width: ${metrics.standardWidth}`,
|
||||
`- scrollbar-color: ${metrics.standardColor}`,
|
||||
`- ::-webkit-scrollbar-thumb:hover declarations: ${metrics.hoverRules.join(' | ')}`,
|
||||
`- --dsh-scrollbar-thumb: ${metrics.token}`,
|
||||
`- --dsh-scrollbar-thumb-hover: ${metrics.hoverToken}`,
|
||||
`- list overflows: ${String(metrics.overflows)}`,
|
||||
`- reserved band: ${String(metrics.band)}px`,
|
||||
`- relative time covered by the bar: ${String(metrics.timeCoveredBy)}px`,
|
||||
`- relative time ends inside the content area: ${String(metrics.timeRight <= metrics.clientRight)}`,
|
||||
`- content area ends before the border box: ${String(metrics.clientRight < metrics.borderRight)}`,
|
||||
'',
|
||||
]
|
||||
return [
|
||||
'# Sidebar session list scrollbar',
|
||||
'',
|
||||
...palette('Light palette', light),
|
||||
...palette('Dark palette', dark),
|
||||
].join('\n').trimEnd()
|
||||
}
|
||||
|
||||
/**
|
||||
* Reveal the seeded rows: every seeded session is unattached, so they all sit
|
||||
* in the collapsed Ungrouped bucket. Converges on expanded rather than
|
||||
@@ -296,6 +352,22 @@ describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thum
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('matches the committed scrollbar geometry golden in both palettes', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-golden'))
|
||||
const light = await measureList(page)
|
||||
await page.evaluate(() => { document.body.setAttribute('data-ds-dark-theme', '') })
|
||||
const dark = await measureList(page)
|
||||
await page.evaluate(() => { document.body.removeAttribute('data-ds-dark-theme') })
|
||||
await compareOrRefreshGolden(GEOMETRY_EXPECTED, renderGeometry(light, dark), MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('commits exactly the fixtures it reads', async () => {
|
||||
// The scenario borrows seeded-history's seed.jsonl rather than committing a
|
||||
// second copy, so this directory holds the golden alone.
|
||||
await assertFixtureInventory(SNAPSHOT_DIR, ['geometry.expected.md'])
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('issued zero model calls and stayed clean', () => {
|
||||
expect(tripwire.warnings).toEqual([])
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
# Sidebar session list scrollbar
|
||||
|
||||
## Light palette
|
||||
|
||||
- scrollbar-gutter: stable
|
||||
- ::-webkit-scrollbar width: 8px
|
||||
- ::-webkit-scrollbar-track background: rgba(0, 0, 0, 0)
|
||||
- scrollbar-width: auto
|
||||
- scrollbar-color: auto
|
||||
- ::-webkit-scrollbar-thumb:hover declarations: var(--dsh-scrollbar-thumb-hover)
|
||||
- --dsh-scrollbar-thumb: rgb(229, 229, 229)
|
||||
- --dsh-scrollbar-thumb-hover: rgb(212, 212, 212)
|
||||
- list overflows: true
|
||||
- reserved band: 8px
|
||||
- relative time covered by the bar: 0px
|
||||
- relative time ends inside the content area: true
|
||||
- content area ends before the border box: true
|
||||
|
||||
## Dark palette
|
||||
|
||||
- scrollbar-gutter: stable
|
||||
- ::-webkit-scrollbar width: 8px
|
||||
- ::-webkit-scrollbar-track background: rgba(0, 0, 0, 0)
|
||||
- scrollbar-width: auto
|
||||
- scrollbar-color: auto
|
||||
- ::-webkit-scrollbar-thumb:hover declarations: var(--dsh-scrollbar-thumb-hover)
|
||||
- --dsh-scrollbar-thumb: rgb(60, 60, 61)
|
||||
- --dsh-scrollbar-thumb-hover: rgb(84, 85, 87)
|
||||
- list overflows: true
|
||||
- reserved band: 8px
|
||||
- relative time covered by the bar: 0px
|
||||
- relative time ends inside the content area: true
|
||||
- content area ends before the border box: true
|
||||
Reference in New Issue
Block a user