Merge remote-tracking branch 'origin/master' into codex/status-bar-token-metrics
# Conflicts: # docs/cordis-catalog/events.md # docs/event-producer-consumer.md # packages/client/runtime/src/client/sessions/conversation.ts # packages/client/runtime/src/client/sessions/session.ts # packages/client/runtime/tests/fake-api.ts # packages/client/runtime/tests/session.spec.ts # packages/client/ui-conversation/tests/chat-stats-bash-sample.spec.tsx # packages/client/ui-conversation/tests/chat-toolview-slot.spec.tsx # packages/client/ui-conversation/tests/chat-view.spec.tsx # packages/client/ui-conversation/tests/gate-branch-tails.spec.tsx # packages/client/ui-conversation/tests/queue-dock.spec.tsx # packages/core/agent-loop/README.i18n.yaml # packages/core/agent/README.i18n.yaml # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/src/api-proxy.ts # packages/host/apiproxy/src/api/events.schema.ts # packages/host/apiproxy/src/api/events.ts # packages/host/apiproxy/src/api/index.ts # packages/host/apiproxy/src/api/sessions.schema.ts # packages/host/apiproxy/src/api/sessions.ts # packages/host/apiproxy/tests/rpc-schemas.spec.ts
This commit is contained in:
@@ -71,6 +71,35 @@ describe('web e2e: seeded history renders through cold resume', () => {
|
||||
await recordFixture(scaffold, sessionId, SEED)
|
||||
}, 200_000)
|
||||
|
||||
it.skipIf(MODE === 'record')('serves the projections baseline on the real composition tail page', async () => {
|
||||
// Composition regression tripwire: the projection registry must be a row
|
||||
// in the SHIPPED cordis.yml — with it absent every domain unit's optional
|
||||
// injection stays silent and this block disappears (no titles/todos on
|
||||
// the web), while fixture-level suites stay green. Assert through the
|
||||
// real HTTP wire against the booted real host.
|
||||
const response = await fetch(`${scaffold.baseUrl}/api/session.history`, {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
type: 'client-request', rpcId: 'seeded-projections', method: 'session.history',
|
||||
payload: { sessionId: SEED_ID },
|
||||
}),
|
||||
})
|
||||
expect(response.ok).toBe(true)
|
||||
const body = await response.json() as {
|
||||
result: { ok: boolean; value?: { projections?: { asOfSeq: number; values: Record<string, unknown> } } }
|
||||
}
|
||||
expect(body.result.ok).toBe(true)
|
||||
const projections = body.result.value?.projections
|
||||
expect(projections).toBeDefined()
|
||||
expect(projections?.asOfSeq).toBeGreaterThanOrEqual(0)
|
||||
// The seed carries a session/title event: the title unit must serve it.
|
||||
expect(typeof projections?.values.title).toBe('string')
|
||||
// tool-todo is composed but the seed has no todo/write: whole-value null,
|
||||
// key PRESENT (absence would mean the unit never registered).
|
||||
expect(projections?.values).toHaveProperty('todos', null)
|
||||
})
|
||||
|
||||
it.skipIf(MODE === 'record')('lists the seeded session cold and renders its history from the log', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-seeded-history'))
|
||||
// The sidebar tree collapses workspace groups by default: click the group
|
||||
|
||||
375
apps/web/tests/sidebar-scrollbar.e2e.ts
Normal file
375
apps/web/tests/sidebar-scrollbar.e2e.ts
Normal file
@@ -0,0 +1,375 @@
|
||||
// Web e2e scenario: the sidebar session list's scrollbar as the browser
|
||||
// actually lays it out — the observable half of the themed-scrollbar change
|
||||
// (packages/client/ui-theme/src/styles/scrollbar.css plus the
|
||||
// `scrollbar-gutter: stable` reservation on WorkspaceBrowser's `.list`). The
|
||||
// ui-theme/ui-workspace unit specs read the CSS text; only a real engine
|
||||
// reports the reserved gutter width and the substituted `scrollbar-color`, so
|
||||
// those two facts live here.
|
||||
//
|
||||
// Zero model calls: the list only has to overflow, so the scenario seeds many
|
||||
// cold sessions from another spec's committed fixture (seeded-history's
|
||||
// seed.jsonl, reused read-only — this spec needs row count, not new recorded
|
||||
// content) and never launches a replay row. A stray stream would fail loud
|
||||
// with NO_ADAPTER.
|
||||
//
|
||||
// Headless-chromium caveats, load-bearing for what is asserted below.
|
||||
//
|
||||
// Headless chromium defaults to an OVERLAY scrollbar: one drawn on top of the
|
||||
// content, consuming no layout width unless something reserves space. That is
|
||||
// the mode in which the reported symptom exists at all, so this environment
|
||||
// reproduces it rather than merely approximating it — measured against clean
|
||||
// master, where the list's band is 0 and the bar covers 7px of the relative
|
||||
// time. (Under a classic space-consuming bar, `clientWidth` already excludes
|
||||
// the bar and nothing can be covered; a headed run under xvfb behaves that way
|
||||
// and cannot show the symptom.)
|
||||
//
|
||||
// The consequence for assertions: comparing the time element's right edge
|
||||
// against the list's CLIENT-area right edge holds in both states and proves
|
||||
// nothing, because with an overlay bar the client edge is the border edge. The
|
||||
// two signals that do separate the states are the reserved band width and
|
||||
// `timeCoveredBy`, which measures the overlap against the bar's own width.
|
||||
//
|
||||
// Both the `scrollbar-gutter: stable` reservation and the sheet's
|
||||
// `::-webkit-scrollbar` width are needed for that band, and neither suffices:
|
||||
// measured on the running app, deleting either one takes the band from 8 to 0
|
||||
// while the other stays in force. The gutter states that space be reserved; the
|
||||
// pseudo-element width is what makes chromium treat the bar as occupying layout
|
||||
// space in the first place.
|
||||
//
|
||||
// That conjunction is why `band` and `timeCoveredBy` are both asserted and
|
||||
// neither replaces the other. Removing only the gutter leaves `timeCoveredBy` at
|
||||
// 0, because the bar is then 8px wide and the row's right padding is also 8px,
|
||||
// so it abuts the timestamp without covering it; `band` catches that case.
|
||||
// Removing both — the actual master state — is what produces the reported
|
||||
// overlap, and `timeCoveredBy` measures it at 7. Each was mutation-checked with
|
||||
// the other assertions in its test silenced.
|
||||
//
|
||||
// Chromium also takes the `::-webkit-scrollbar*` path, not the standard
|
||||
// properties: scrollbar.css gates `scrollbar-width`/`scrollbar-color` behind
|
||||
// `@supports not selector(::-webkit-scrollbar)`, which is false here. The
|
||||
// resolved standard properties therefore read `auto`, and that reading is
|
||||
// asserted — a concrete value would mean the gate leaked and silenced the
|
||||
// pseudo-element rules. What the theme test measures instead is the pair the
|
||||
// pseudo-element rules read: the indirection variables as they resolve ON the
|
||||
// list, plus the `::-webkit-scrollbar-thumb:hover` declaration as it stands in
|
||||
// the cascade. The hover thumb colour is not observable any other way —
|
||||
// chromium folds the `:hover` rule into `getComputedStyle(el,
|
||||
// '::-webkit-scrollbar-thumb')`, so that query reports the hover colour at
|
||||
// rest and cannot pin either state (measured by deleting the hover rule live:
|
||||
// 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 {
|
||||
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
|
||||
|
||||
/** Geometry and resolved scrollbar style of one scroll container, measured in the page. */
|
||||
interface ListMetrics {
|
||||
/** Resolved `scrollbar-gutter`. */
|
||||
gutter: string
|
||||
/** Resolved `::-webkit-scrollbar` width: the pseudo-element path's own sizing. */
|
||||
width: string
|
||||
/** Resolved `::-webkit-scrollbar-track` background. */
|
||||
track: string
|
||||
/** Resolved `scrollbar-width`, expected `auto` because the gate excludes chromium. */
|
||||
standardWidth: string
|
||||
/** Resolved `scrollbar-color`, expected `auto` for the same reason. */
|
||||
standardColor: string
|
||||
/** `::-webkit-scrollbar-thumb:hover` background declarations found in the cascade, in sheet order. */
|
||||
hoverRules: string[]
|
||||
/** `--dsh-scrollbar-thumb` resolved on the list, serialized as a colour. */
|
||||
token: string
|
||||
/** `--dsh-scrollbar-thumb-hover` resolved on the list, serialized the same way. */
|
||||
hoverToken: string
|
||||
/** True when the list actually scrolls. */
|
||||
overflows: boolean
|
||||
/** Border-box width minus client width: the space the scrollbar takes out of the content area. */
|
||||
band: number
|
||||
/** Client-area right edge in viewport coordinates (`clientWidth` excludes the scrollbar band). */
|
||||
clientRight: number
|
||||
/** Border-box right edge in viewport coordinates. */
|
||||
borderRight: number
|
||||
/** Right edge of the first row's relative-time element, the content the unreserved bar covered. */
|
||||
timeRight: number
|
||||
/**
|
||||
* Pixels of the relative time the scrollbar paints over: how far its right
|
||||
* edge reaches into the band the bar occupies, `[borderRight - barWidth,
|
||||
* borderRight]`. This is the reported symptom as a number, and it is the one
|
||||
* geometric signal that separates the two states in this environment — see
|
||||
* the file header on why `clientWidth` comparisons cannot.
|
||||
*/
|
||||
timeCoveredBy: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the sidebar list in the page.
|
||||
* @param page - the page under test.
|
||||
* @returns the list's resolved scrollbar style and the geometry the fix changes.
|
||||
*/
|
||||
function measureList(page: Page): Promise<ListMetrics> {
|
||||
return page.evaluate(() => {
|
||||
const list = document.querySelector<HTMLElement>('[role="tree"][aria-label="Sessions"]')
|
||||
if (list === null) throw new Error('sidebar session list not in the DOM')
|
||||
const time = list.querySelector<HTMLElement>('[class*="time"]')
|
||||
if (time === null) throw new Error('no row relative-time element in the sidebar list')
|
||||
// Each indirection variable is resolved through its own throwaway probe
|
||||
// appended to the list: `var()` substitution then happens where the list
|
||||
// sits in the cascade, which is the claim, and `color` normalizes whatever
|
||||
// notation the palette sheet chose into one comparable serialization. A
|
||||
// REUSED probe would report only the last value read — `getComputedStyle`
|
||||
// returns a live declaration, so reassigning `style.color` retroactively
|
||||
// changes every earlier read.
|
||||
const resolve = (name: string): string => {
|
||||
const probe = document.createElement('span')
|
||||
probe.style.color = `var(${name})`
|
||||
list.append(probe)
|
||||
const value = getComputedStyle(probe).color
|
||||
probe.remove()
|
||||
return value
|
||||
}
|
||||
// The hover colour is read out of the cascade rather than computed:
|
||||
// chromium reports the `:hover` background for the resting pseudo-element
|
||||
// too (see the file header), so no computed query separates the states.
|
||||
// Cross-origin sheets throw on `cssRules`; none is expected, and skipping
|
||||
// them cannot mask the rule under test, which ships in the app's own CSS.
|
||||
const hoverRules = [...document.styleSheets]
|
||||
.flatMap((sheet) => {
|
||||
try {
|
||||
return [...sheet.cssRules]
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
})
|
||||
.filter((rule): rule is CSSStyleRule => rule instanceof CSSStyleRule)
|
||||
.filter(rule => rule.selectorText === '::-webkit-scrollbar-thumb:hover')
|
||||
.map(rule => rule.style.getPropertyValue('background'))
|
||||
const style = getComputedStyle(list)
|
||||
const pseudoWidth = getComputedStyle(list, '::-webkit-scrollbar').width
|
||||
const barWidth = pseudoWidth === 'auto' ? 15 : Number.parseFloat(pseudoWidth)
|
||||
return {
|
||||
gutter: style.scrollbarGutter,
|
||||
width: pseudoWidth,
|
||||
track: getComputedStyle(list, '::-webkit-scrollbar-track').backgroundColor,
|
||||
standardWidth: style.scrollbarWidth,
|
||||
standardColor: style.scrollbarColor,
|
||||
hoverRules,
|
||||
token: resolve('--dsh-scrollbar-thumb'),
|
||||
hoverToken: resolve('--dsh-scrollbar-thumb-hover'),
|
||||
overflows: list.scrollHeight > list.clientHeight,
|
||||
band: list.getBoundingClientRect().width - list.clientWidth,
|
||||
clientRight: list.getBoundingClientRect().left + list.clientWidth,
|
||||
borderRight: list.getBoundingClientRect().right,
|
||||
timeRight: time.getBoundingClientRect().right,
|
||||
// The bar is drawn in the rightmost `barWidth` of the border box, whether
|
||||
// or not that space was reserved. Its width comes from the sheet where the
|
||||
// sheet applies, and from the UA's own overlay bar otherwise — 15px is
|
||||
// what this chromium paints, measured against master where the rule is
|
||||
// absent. Taking the UA width as the fallback is what keeps the assertion
|
||||
// honest: assuming 0 there would report no occlusion precisely in the
|
||||
// state that has it.
|
||||
timeCoveredBy: Math.max(0, time.getBoundingClientRect().right - (list.getBoundingClientRect().right - barWidth)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
* clicking once — startup auto-selection can expand the bucket first, and a
|
||||
* second click would collapse it again. Hand-rolled polling because
|
||||
* `expect.poll` is test-scoped and this runs in `beforeAll`.
|
||||
* @param page - the page under test.
|
||||
*/
|
||||
async function expandSeededSessions(page: Page): Promise<void> {
|
||||
const bucket = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..')
|
||||
await bucket.waitFor({ timeout: 15_000 })
|
||||
const rows = page.locator('[role="tree"][aria-label="Sessions"] [role="treeitem"]')
|
||||
const deadline = Date.now() + 30_000
|
||||
for (;;) {
|
||||
if (await bucket.getAttribute('aria-expanded') !== 'true') {
|
||||
await page.getByText('Ungrouped', { exact: true }).click()
|
||||
}
|
||||
if (await bucket.getAttribute('aria-expanded') === 'true' && await rows.count() > SEED_COUNT / 2) return
|
||||
if (Date.now() > deadline) {
|
||||
throw new Error(`Ungrouped bucket never revealed more than ${SEED_COUNT / 2} rows`)
|
||||
}
|
||||
await page.waitForTimeout(200)
|
||||
}
|
||||
}
|
||||
|
||||
describe('web e2e: sidebar session list scrollbar (reserved gutter / themed thumb)', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
const fixture = await readFile(SEED, 'utf8')
|
||||
for (let index = 0; index < SEED_COUNT; index += 1) {
|
||||
await seedSession(scaffold, fixture, `sidebar-scrollbar-web-e2e-${String(index).padStart(2, '0')}`)
|
||||
}
|
||||
browser = await chromium.launch()
|
||||
// Shorter than the other scenarios' 1000px so SEED_COUNT rows overflow
|
||||
// the list with room to spare.
|
||||
page = await browser.newPage({ viewport: { width: 1680, height: 800 } })
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
await expandSeededSessions(page)
|
||||
}, 180_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('reserves a scrollbar gutter on the overflowing session list', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-gutter'))
|
||||
// Vacuity guard: with a non-overflowing list `stable` still reserves, but
|
||||
// the scenario would no longer be reproducing the reported situation.
|
||||
await expect.poll(async () => (await measureList(page)).overflows, { timeout: 10_000 }).toBe(true)
|
||||
const metrics = await measureList(page)
|
||||
expect(metrics.gutter).toBe('stable')
|
||||
// The control. `band > 0` is the whole observable effect of the
|
||||
// reservation: the scrollbar is taken out of the content area instead of
|
||||
// drawn over it. Removing the declaration makes it exactly 0. The value
|
||||
// itself is not pinned — it tracks `scrollbar-width` and the platform.
|
||||
expect(metrics.band).toBeGreaterThan(0)
|
||||
// The reported symptom, stated directly: no part of the row's relative time
|
||||
// lies under the bar. Measures 7 on clean master — the `h` of `1h` is the
|
||||
// covered part. Unlike the client-edge comparison below it does not go
|
||||
// vacuous under an overlay scrollbar, because it measures against the bar's
|
||||
// own width rather than against a content edge the overlay bar does not
|
||||
// move. It is not a replacement for the band assertion above; see the file
|
||||
// header for which regression each one catches.
|
||||
expect(metrics.timeCoveredBy).toBe(0)
|
||||
// Corollaries of the reservation, kept because they pin where the band sits
|
||||
// rather than only that it exists: the time ends inside the content area,
|
||||
// and the content area ends before the border box. Each holds in both
|
||||
// states on its own (see the file header) and is meaningful only alongside
|
||||
// the two assertions above.
|
||||
expect(metrics.timeRight).toBeLessThanOrEqual(metrics.clientRight)
|
||||
expect(metrics.clientRight).toBeLessThan(metrics.borderRight)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('renders the themed thumb through the WebKit path in both palettes', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-scrollbar-theme'))
|
||||
const light = await measureList(page)
|
||||
// The gate's signature on this engine, and the reason it exists: chromium
|
||||
// implements `::-webkit-scrollbar`, so the standard properties stay at
|
||||
// their initial `auto`. A concrete value here would mean the gate leaked,
|
||||
// which is exactly what makes chromium discard the pseudo-element rules —
|
||||
// the hover token included.
|
||||
expect(light.standardWidth).toBe('auto')
|
||||
expect(light.standardColor).toBe('auto')
|
||||
// The pseudo-element path is the one in force: the sheet's own 8px sizing
|
||||
// and transparent track reached a container it never names.
|
||||
expect(light.width).toBe('8px')
|
||||
expect(light.track).toBe('rgba(0, 0, 0, 0)')
|
||||
// The resting and the hover rule each read the rebindable indirection, and
|
||||
// the two resolve to DIFFERENT colours on this list: the l1 pair arrived
|
||||
// here intact rather than collapsing to one value or falling back.
|
||||
expect(light.hoverRules).toEqual(['var(--dsh-scrollbar-thumb-hover)'])
|
||||
expect(light.token).toMatch(/^rgba?\(/)
|
||||
expect(light.hoverToken).not.toBe(light.token)
|
||||
// The dark palette declares different scrollbar tokens; driving the body
|
||||
// attribute pins the cascade the way lifecycle-chrome does (the Settings
|
||||
// gesture that sets it is owned there).
|
||||
await page.evaluate(() => { document.body.setAttribute('data-ds-dark-theme', '') })
|
||||
const dark = await measureList(page)
|
||||
expect(dark.token).not.toBe(light.token)
|
||||
expect(dark.hoverToken).not.toBe(dark.token)
|
||||
expect(dark.hoverToken).not.toBe(light.hoverToken)
|
||||
await page.evaluate(() => { document.body.removeAttribute('data-ds-dark-theme') })
|
||||
const restored = await measureList(page)
|
||||
expect(restored.token).toBe(light.token)
|
||||
expect(restored.hoverToken).toBe(light.hoverToken)
|
||||
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
|
||||
@@ -32,6 +32,7 @@
|
||||
"tests/workspace-management.e2e.ts",
|
||||
"tests/replay-round-trip.e2e.ts",
|
||||
"tests/seeded-history.e2e.ts",
|
||||
"tests/sidebar-scrollbar.e2e.ts",
|
||||
"tests/code-mode-round.e2e.ts",
|
||||
"tests/cordis-tool-round.e2e.ts"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user