Merge commit '4ebc88329d1da067edad6032d40688e52e401bf5' into codex/product-subagent-one-shot-background
This commit is contained in:
60
apps/web/tests/cold-blank-session.e2e.ts
Normal file
60
apps/web/tests/cold-blank-session.e2e.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
/** Cold Session list visibility through the shipped compressed JSONL backend. */
|
||||
|
||||
import { mkdir, stat } 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 {
|
||||
captureStableAria, compareOrRefreshGolden, launchWebScaffold, seedBlankSession,
|
||||
watchConsole, webSnapshotMode, type WebScaffold,
|
||||
} from './scaffold.ts'
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/cold-blank-session', import.meta.url))
|
||||
const SIDEBAR_EXPECTED = join(SNAPSHOT_DIR, 'sidebar.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
const SESSION_ID = 'cold-blank-session-web-e2e'
|
||||
const WORKSPACE_NAME = 'cold-blank-workspace'
|
||||
|
||||
describe('web e2e: cold blank Session visibility', () => {
|
||||
let scaffold: WebScaffold
|
||||
let browser: Browser
|
||||
let page: Page
|
||||
let tripwire: ReturnType<typeof watchConsole>
|
||||
|
||||
beforeAll(async () => {
|
||||
scaffold = await launchWebScaffold({})
|
||||
const cwd = join(scaffold.workspaceCwd, WORKSPACE_NAME)
|
||||
await mkdir(cwd, { recursive: true })
|
||||
await seedBlankSession(scaffold, SESSION_ID, cwd)
|
||||
const header = (await scaffold.ctx.sessionPersistence.list())
|
||||
.find(candidate => candidate.id === SESSION_ID)
|
||||
if (header === undefined) throw new Error('blank Session fixture did not materialize')
|
||||
const location = scaffold.ctx.sessionPersistence.locate(header)
|
||||
if (location === undefined) throw new Error('JSONL fixture has no physical artifact')
|
||||
expect((await stat(location.path)).size).toBeLessThanOrEqual(1024)
|
||||
|
||||
browser = await chromium.launch()
|
||||
page = await newEnglishPage(browser)
|
||||
tripwire = watchConsole(page)
|
||||
await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
|
||||
await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
await browser?.close()
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('keeps the verified cold blank Session out of the sidebar', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-cold-blank-session'))
|
||||
const tree = page.getByRole('tree', { name: 'Sessions' })
|
||||
await tree.waitFor({ timeout: 30_000 })
|
||||
expect(await tree.getByText(WORKSPACE_NAME, { exact: true }).count()).toBe(0)
|
||||
const sidebar = await captureStableAria(page, '[role="tree"][aria-label="Sessions"]', scaffold.workspaceCwd)
|
||||
await compareOrRefreshGolden(SIDEBAR_EXPECTED, sidebar, MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
})
|
||||
})
|
||||
@@ -11,12 +11,13 @@
|
||||
// gets an absolutely positioned seat instead, laid out against the padding box,
|
||||
// which the scrollbar never reduces.
|
||||
//
|
||||
// Without a shared reservation the two tabs disagree by exactly the bar's
|
||||
// width for as long as the transcript overflows: the card jumps sideways on
|
||||
// every tab switch, and inside Chat alone at the moment a growing transcript
|
||||
// starts to scroll. The column reserves the gutter unconditionally
|
||||
// (`scrollbar-gutter: stable`) and states the overlay branch as a scroll
|
||||
// container on the same axes, so both edges are the same edge.
|
||||
// The column handles the two edges without reserving the gutter on both: Chat
|
||||
// keeps `scrollbar-gutter: stable` so its seat's content box never jumps as the
|
||||
// transcript starts to scroll; the overlay branch does NOT reserve (the view
|
||||
// owns its own scrollers, so a reserved gutter would only narrow the view's
|
||||
// content by the bar's width), and the overlay seat instead gives back the
|
||||
// bar's width (`right: var(--dsh-scrollbar-width)`) so both seats measure the
|
||||
// same width and the card does not move.
|
||||
//
|
||||
// Only a real engine can show this. The seat's geometry is layout: jsdom gives
|
||||
// every element a zero-sized box and reports no scrollbar at all, so a unit spec
|
||||
@@ -27,18 +28,17 @@
|
||||
// The browser is launched WITHOUT Playwright's default `--hide-scrollbars`,
|
||||
// which is load-bearing rather than incidental. Under that argument a scroll
|
||||
// container's bar consumes no layout width at all, so the two tabs agree with
|
||||
// and without the reservation and every comparison below holds vacuously —
|
||||
// measured: the unreserved cascade leaves both tabs' bands at 0 there, against
|
||||
// 8 and 0 with the argument dropped. Dropping it is also the faithful
|
||||
// and without the compensation and every comparison below holds vacuously —
|
||||
// measured: the uncompensated cascade leaves both tabs' bands at 0 there,
|
||||
// against 8 and 0 with the argument dropped. Dropping it is also the faithful
|
||||
// configuration: ui-theme's scrollbar.css gives `::-webkit-scrollbar` a width,
|
||||
// and a bar that occupies layout space is what the product actually draws.
|
||||
//
|
||||
// The scenario runs that unreserved cascade in the page — `scrollbar-gutter: auto`
|
||||
// on the scroller, `overflow: hidden` on the overlay branch — and measures the
|
||||
// same two tabs through it, which is what keeps the equal rectangles above from
|
||||
// being explained by a tab switch that never reached the layout. It is the
|
||||
// reported symptom as a number: the card moves 4px, half the 8px band, on each
|
||||
// edge.
|
||||
// The scenario runs that uncompensated cascade in the page — the overlay seat's
|
||||
// `right` compensation dropped to 0 — and measures the same two tabs through
|
||||
// it, which is what keeps the equal rectangles above from being explained by a
|
||||
// tab switch that never reached the layout. It is the reported symptom as a
|
||||
// number: the card moves 4px, half the 8px band, on each edge.
|
||||
//
|
||||
// Zero model calls: a seeded cold session renders from its log, and switching
|
||||
// tabs asks the host for nothing. A stray stream would fail loud with NO_ADAPTER.
|
||||
@@ -62,9 +62,9 @@ const SNAPSHOT_DIR = fileURLToPath(new URL('./snapshots/composer-tab-geometry',
|
||||
* Absolute coordinates are deliberately absent: they depend on the sidebar's
|
||||
* laid-out width and on font metrics, so committing them would produce a fixture
|
||||
* that has to be re-recorded per platform. What is recorded is the distance
|
||||
* between the two tabs' rectangles, which is zero when the reservation holds and
|
||||
* between the two tabs' rectangles, which is zero when the compensation holds and
|
||||
* the bar's width when it does not — including under the control, so the golden
|
||||
* carries the shift the unreserved cascade produces rather than only its absence.
|
||||
* carries the shift the uncompensated cascade produces rather than only its absence.
|
||||
*/
|
||||
const GEOMETRY_EXPECTED = join(SNAPSHOT_DIR, 'geometry.expected.md')
|
||||
const MODE = webSnapshotMode()
|
||||
@@ -114,15 +114,15 @@ async function setMeasuredViewport(
|
||||
}
|
||||
|
||||
/**
|
||||
* The unreserved cascade, injected into the page: the reservation dropped and
|
||||
* the overlay branch forced to a hidden box. `!important` beats the module
|
||||
* The uncompensated cascade, injected into the page: the overlay seat's `right`
|
||||
* compensation dropped to 0, so it measures the full padding box while Chat's
|
||||
* seat still rides the reserved content box. `!important` beats the module
|
||||
* rules without a rebuild, and the id lets the control be lifted again in the
|
||||
* same session.
|
||||
*/
|
||||
const CONTROL_STYLE_ID = 'composer-tab-geometry-control'
|
||||
const CONTROL_CSS = `
|
||||
[data-conversation-scroll] { scrollbar-gutter: auto !important; }
|
||||
[data-conversation-scroll]:has([data-conversation-composer-overlay]) { overflow: hidden !important; }
|
||||
[data-conversation-scroll]:has([data-conversation-composer-overlay]) > [data-composer-seat] { right: 0 !important; }
|
||||
`
|
||||
|
||||
/** The column scroller and the input card as the browser lays them out, in one tab. */
|
||||
@@ -221,11 +221,13 @@ async function compareTabs(page: Page): Promise<TabComparison> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the unreserved cascade in the page for one measurement, then lift it.
|
||||
* Run the uncompensated cascade in the page for one measurement, then lift it:
|
||||
* the overlay seat's `right` compensation dropped to 0, so it measures the
|
||||
* full padding box while Chat's seat still rides the reserved content box.
|
||||
* @param page - the page under test.
|
||||
* @returns the comparison as the column lays out without the reservation.
|
||||
* @returns the comparison as the column lays out without the compensation.
|
||||
*/
|
||||
async function compareTabsWithoutReservation(page: Page): Promise<TabComparison> {
|
||||
async function compareTabsWithoutCompensation(page: Page): Promise<TabComparison> {
|
||||
await page.evaluate(({ id, css }) => {
|
||||
const style = document.createElement('style')
|
||||
style.id = id
|
||||
@@ -268,7 +270,7 @@ async function openSeededSession(page: Page): Promise<void> {
|
||||
* Render the golden body.
|
||||
* @param wide - comparison at the viewport where the card sits at its width cap.
|
||||
* @param narrow - comparison at the viewport where the card shrinks with the column.
|
||||
* @param control - comparison at the wide viewport with the reservation removed.
|
||||
* @param control - comparison at the wide viewport with the compensation removed.
|
||||
* @returns the golden body, without a trailing newline.
|
||||
*/
|
||||
function renderGeometry(wide: TabComparison, narrow: TabComparison, control: TabComparison): string {
|
||||
@@ -291,7 +293,7 @@ function renderGeometry(wide: TabComparison, narrow: TabComparison, control: Tab
|
||||
'',
|
||||
...section(`Wide viewport (${String(WIDE_VIEWPORT.width)}px, card at its cap)`, wide),
|
||||
...section(`Narrow viewport (${String(NARROW_VIEWPORT.width)}px, card shrinking with the column)`, narrow),
|
||||
...section('Wide viewport, reservation removed in the page (control)', control),
|
||||
...section('Wide viewport, seat compensation removed in the page (control)', control),
|
||||
].join('\n').trimEnd()
|
||||
}
|
||||
|
||||
@@ -322,22 +324,29 @@ describe('web e2e: input card position across view tabs', () => {
|
||||
await scaffold?.close()
|
||||
})
|
||||
|
||||
it('reserves the same gutter in both tabs while the transcript scrolls', async () => {
|
||||
it('reserves the gutter in Chat and lets Trajectory own its width', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-tab-geometry-band'))
|
||||
await setMeasuredViewport(page, WIDE_VIEWPORT, false)
|
||||
// Vacuity guard, in two parts. A transcript that does not overflow gives
|
||||
// Chat no scrollbar, and a hidden or overlaid bar gives it no width; either
|
||||
// would make the tabs agree without the reservation doing anything.
|
||||
// Vacuity guard. The scenario must be able to fail: on an engine that
|
||||
// does not implement `scrollbar-gutter`, Chat reserves nothing and the
|
||||
// overlay seat's fixed compensation stands alone, manufacturing an 8px
|
||||
// deviation the equal-rectangle assertions would catch. `stable` reserves
|
||||
// even without overflow, so a short transcript is not a vacuous case; the
|
||||
// poll still pins the measurement to the overflowing state the product
|
||||
// ships.
|
||||
await expect.poll(async () => (await measureTab(page)).scrolls, { timeout: 10_000 }).toBe(true)
|
||||
const comparison = await compareTabs(page)
|
||||
expect(comparison.chat.band).toBeGreaterThan(0)
|
||||
// The reservation reaches both states, which is the whole point: the same
|
||||
// band, on a box that scrolls and on one that only holds a view.
|
||||
// Chat keeps the unconditional reservation so its seat's content box never
|
||||
// jumps as the transcript starts to scroll.
|
||||
expect(comparison.chat.gutter).toBe('stable')
|
||||
expect(comparison.trajectory.gutter).toBe('stable')
|
||||
expect(comparison.trajectory.band).toBe(comparison.chat.band)
|
||||
// The overlay branch does NOT reserve: the view owns its own scrollers, so
|
||||
// a reserved gutter would only narrow the view's content by the bar's
|
||||
// width. The seat compensates instead, which the next test asserts.
|
||||
expect(comparison.trajectory.gutter).toBe('auto')
|
||||
expect(comparison.trajectory.band).toBe(0)
|
||||
// Declared as a scroll container on both axes rather than left to compute:
|
||||
// `overflow: hidden` would drop the reservation in WebKit, and a `visible`
|
||||
// `overflow: hidden` would drop any reservation in WebKit, and a `visible`
|
||||
// horizontal axis computes to `auto` beside a scrolling one.
|
||||
expect(comparison.trajectory.overflowY).toBe('auto')
|
||||
expect(comparison.trajectory.overflowX).toBe('hidden')
|
||||
@@ -351,7 +360,7 @@ describe('web e2e: input card position across view tabs', () => {
|
||||
await setMeasuredViewport(page, WIDE_VIEWPORT, false)
|
||||
const comparison = await compareTabs(page)
|
||||
// The reported symptom as a number. At this viewport the card sits at its
|
||||
// width cap, so the unreserved cascade's shift shows up as a centring
|
||||
// width cap, so the uncompensated cascade's shift shows up as a centring
|
||||
// difference — half the band on each edge — rather than as a width change.
|
||||
expect(comparison.leftShift).toBe(0)
|
||||
expect(comparison.rightShift).toBe(0)
|
||||
@@ -378,20 +387,21 @@ describe('web e2e: input card position across view tabs', () => {
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
it('moves the card again once the reservation is removed in the page', async () => {
|
||||
it('moves the card again once the seat compensation is removed in the page', async () => {
|
||||
onTestFailed(() => saveFailureShot(page, 'web-e2e-composer-tab-geometry-control'))
|
||||
await setMeasuredViewport(page, WIDE_VIEWPORT, false)
|
||||
// The control: without it, equal rectangles could also mean the tab switch
|
||||
// never reached the layout. Under the unreserved cascade the Chat scroller
|
||||
// keeps its bar and the Trajectory branch becomes a hidden box with none,
|
||||
// and the card moves by half the band on each edge.
|
||||
const comparison = await compareTabsWithoutReservation(page)
|
||||
expect(comparison.chat.gutter).toBe('auto')
|
||||
// never reached the layout. Under the uncompensated cascade the overlay seat
|
||||
// loses its `right` compensation and measures the full padding box, so the
|
||||
// card moves by half the band on each edge. Chat's own reservation is
|
||||
// untouched — that is the side that must not change.
|
||||
const comparison = await compareTabsWithoutCompensation(page)
|
||||
expect(comparison.chat.gutter).toBe('stable')
|
||||
expect(comparison.chat.band).toBeGreaterThan(0)
|
||||
expect(comparison.trajectory.band).toBe(0)
|
||||
expect(comparison.leftShift).toBe(comparison.chat.band / 2)
|
||||
expect(comparison.rightShift).toBe(comparison.chat.band / 2)
|
||||
// Restoring the sheet restores the reservation, so the control cannot leak
|
||||
// Restoring the sheet restores the compensation, so the control cannot leak
|
||||
// into the remaining measurements.
|
||||
const restored = await compareTabs(page)
|
||||
expect(restored.leftShift).toBe(0)
|
||||
@@ -405,7 +415,7 @@ describe('web e2e: input card position across view tabs', () => {
|
||||
await setMeasuredViewport(page, NARROW_VIEWPORT, true)
|
||||
const narrow = await compareTabs(page)
|
||||
await setMeasuredViewport(page, WIDE_VIEWPORT, false)
|
||||
const control = await compareTabsWithoutReservation(page)
|
||||
const control = await compareTabsWithoutCompensation(page)
|
||||
await compareOrRefreshGolden(GEOMETRY_EXPECTED, renderGeometry(wide, narrow, control), MODE)
|
||||
expect(tripwire.pageErrors).toEqual([])
|
||||
}, 60_000)
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
// (the plugin-row path discards the ReplayHandle; the direct install keeps
|
||||
// assertConsumed for the teardown fixture-consumption check).
|
||||
import { existsSync } from 'node:fs'
|
||||
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, utimes, writeFile } from 'node:fs/promises'
|
||||
import { mkdir, mkdtemp, readFile, readdir, realpath, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { pathToFileURL } from 'node:url'
|
||||
@@ -703,6 +703,38 @@ export async function seedSession(
|
||||
delegationDepth: 0,
|
||||
...agentPreset === undefined ? {} : { agentPreset },
|
||||
}
|
||||
await persistSeedSession(scaffold, meta, events)
|
||||
return meta.id
|
||||
}
|
||||
|
||||
/** Seed one materialized cold Session whose log has no turn/start event. */
|
||||
export async function seedBlankSession(
|
||||
scaffold: WebScaffold,
|
||||
id: string,
|
||||
cwd: string,
|
||||
): Promise<SessionId> {
|
||||
const meta: SessionHeader = {
|
||||
version: SESSION_FORMAT_VERSION,
|
||||
id: SessionId(id),
|
||||
createdAt: Date.now() - 60_000,
|
||||
cwd,
|
||||
delegationDepth: 0,
|
||||
}
|
||||
await persistSeedSession(scaffold, meta, [{
|
||||
type: 'session/end-seed',
|
||||
seq: 0,
|
||||
time: meta.createdAt,
|
||||
data: {},
|
||||
}])
|
||||
return meta.id
|
||||
}
|
||||
|
||||
/** Materialize one detached Session fixture through the shipped JSONL provider. */
|
||||
async function persistSeedSession(
|
||||
scaffold: WebScaffold,
|
||||
meta: SessionHeader,
|
||||
events: readonly SessionEvent[],
|
||||
): Promise<void> {
|
||||
const seeder = new Context()
|
||||
try {
|
||||
await seeder.plugin(SessionStore)
|
||||
@@ -711,16 +743,9 @@ export async function seedSession(
|
||||
await seeder.plugin(JsonlSessionPersistence, { root: scaffold.persistenceRoot })
|
||||
await seeder.sessionPersistence.create(meta)
|
||||
await seeder.sessionPersistence.append(meta.id, events)
|
||||
// Deterministic sidebar order: cold summaries take updatedAt from mtime.
|
||||
const located = seeder.sessionPersistence.locate(meta)
|
||||
if (located !== undefined) {
|
||||
const backdated = new Date(meta.createdAt)
|
||||
await utimes(located.path, backdated, backdated)
|
||||
}
|
||||
} finally {
|
||||
await seeder.fiber.dispose()
|
||||
}
|
||||
return meta.id
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
- tree "Sessions": No sessions yet
|
||||
@@ -5,9 +5,9 @@
|
||||
- Chat: scrollbar-gutter stable, overflow hidden/auto
|
||||
- Chat scroller scrolls: true
|
||||
- Chat reserved band: 8px
|
||||
- Trajectory: scrollbar-gutter stable, overflow hidden/auto
|
||||
- Trajectory: scrollbar-gutter auto, overflow hidden/auto
|
||||
- Trajectory scroller scrolls: false
|
||||
- Trajectory reserved band: 8px
|
||||
- Trajectory reserved band: 0px
|
||||
- input card left edge moves between tabs: 0px
|
||||
- input card right edge moves between tabs: 0px
|
||||
- input card width changes between tabs: 0px
|
||||
@@ -17,19 +17,19 @@
|
||||
- Chat: scrollbar-gutter stable, overflow hidden/auto
|
||||
- Chat scroller scrolls: true
|
||||
- Chat reserved band: 8px
|
||||
- Trajectory: scrollbar-gutter stable, overflow hidden/auto
|
||||
- Trajectory: scrollbar-gutter auto, overflow hidden/auto
|
||||
- Trajectory scroller scrolls: false
|
||||
- Trajectory reserved band: 8px
|
||||
- Trajectory reserved band: 0px
|
||||
- input card left edge moves between tabs: 0px
|
||||
- input card right edge moves between tabs: 0px
|
||||
- input card width changes between tabs: 0px
|
||||
|
||||
## Wide viewport, reservation removed in the page (control)
|
||||
## Wide viewport, seat compensation removed in the page (control)
|
||||
|
||||
- Chat: scrollbar-gutter auto, overflow hidden/auto
|
||||
- Chat: scrollbar-gutter stable, overflow hidden/auto
|
||||
- Chat scroller scrolls: true
|
||||
- Chat reserved band: 8px
|
||||
- Trajectory: scrollbar-gutter auto, overflow hidden/hidden
|
||||
- Trajectory: scrollbar-gutter auto, overflow hidden/auto
|
||||
- Trajectory scroller scrolls: false
|
||||
- Trajectory reserved band: 0px
|
||||
- input card left edge moves between tabs: 4px
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
- row "Load earlier history":
|
||||
- cell "Load earlier history":
|
||||
- button "Load earlier history":
|
||||
- status
|
||||
@@ -3,6 +3,7 @@
|
||||
// DOM mounting stays bounded, and every scroll range remains reachable.
|
||||
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { join } from 'node:path'
|
||||
import type { Browser, Page } from 'playwright'
|
||||
import { chromium } from 'playwright'
|
||||
@@ -11,6 +12,8 @@ import type { StreamChunk } from '@deepseek-ai/dsh-llm'
|
||||
import type { ReplayEntry } from '@deepseek-ai/dsh-llm-replay'
|
||||
import { createChatScrollFixture } from './chat-scroll-fixture.ts'
|
||||
import {
|
||||
captureStableAria,
|
||||
compareOrRefreshGolden,
|
||||
launchWebScaffold,
|
||||
seedSession,
|
||||
watchConsole,
|
||||
@@ -20,6 +23,10 @@ import {
|
||||
import { newEnglishPage, saveFailureShot } from './support.ts'
|
||||
|
||||
const MODE = webSnapshotMode()
|
||||
const LOAD_MORE_EXPECTED = fileURLToPath(new URL(
|
||||
'./snapshots/trajectory-virtualization/load-more.expected.md',
|
||||
import.meta.url,
|
||||
))
|
||||
const SESSION_ID = 'trajectory-virtualization-e2e'
|
||||
const FIXTURE = createChatScrollFixture({
|
||||
markerPrefix: 'TRAJECTORY_VIRTUAL',
|
||||
@@ -150,10 +157,16 @@ async function loadToFirstTurn(page: Page): Promise<void> {
|
||||
await scrollToRatio(page, 0)
|
||||
if (await page.getByText(marker, { exact: false }).count() > 0) return
|
||||
const before = await logicalRows(page)
|
||||
const anchor = await firstVisibleRow(page)
|
||||
await expect.poll(async () => ({
|
||||
marker: await page.getByText(marker, { exact: false }).count() > 0,
|
||||
rows: await logicalRows(page),
|
||||
}), { timeout: 30_000 }).not.toEqual({ marker: false, rows: before })
|
||||
await nextPaint(page)
|
||||
await expect.poll(async () => {
|
||||
const top = await rowTop(page, anchor.key)
|
||||
return top === null ? Number.POSITIVE_INFINITY : Math.abs(top - anchor.top)
|
||||
}, { timeout: 15_000 }).toBeLessThanOrEqual(GEOMETRY_TOLERANCE)
|
||||
}
|
||||
throw new Error('trajectory did not reach the first turn after twelve older-page requests')
|
||||
}
|
||||
@@ -230,8 +243,27 @@ describe('web e2e: Trajectory virtualization over tail-paged history', () => {
|
||||
expect(await page.getByText('Initial System Prompt', { exact: true }).count()).toBe(0)
|
||||
expect(await mountedRows(page)).toBeLessThanOrEqual(MAX_MOUNTED_ROWS)
|
||||
|
||||
await scrollToRatio(page, 0)
|
||||
const loadMore = page.locator('[data-history-load] button')
|
||||
await loadMore.waitFor({ timeout: 15_000 })
|
||||
expect(await loadMore.textContent()).toBe('Load earlier history')
|
||||
const loadMoreSnapshot = await captureStableAria(
|
||||
page,
|
||||
'[data-history-load]',
|
||||
scaffold.workspaceCwd,
|
||||
)
|
||||
await compareOrRefreshGolden(LOAD_MORE_EXPECTED, loadMoreSnapshot, MODE)
|
||||
// Avoid Playwright scrolling the offscreen first row into the automatic-load threshold.
|
||||
await loadMore.evaluate((button: HTMLButtonElement) => { button.click() })
|
||||
await expect.poll(() => held, { timeout: 15_000 }).toBe(true)
|
||||
await expect.poll(async () => ({
|
||||
disabled: await loadMore.isDisabled(),
|
||||
label: await loadMore.getAttribute('aria-label'),
|
||||
}), { timeout: 15_000 }).toEqual({
|
||||
disabled: true,
|
||||
label: 'Loading earlier history…',
|
||||
})
|
||||
|
||||
await scrollToRatio(page, 0)
|
||||
const anchor = await firstVisibleRow(page)
|
||||
const selectedRow = page.locator(
|
||||
`[data-trajectory-scroll] tr[data-trajectory-row-key=${JSON.stringify(anchor.key)}]`,
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"tests/replay-round-trip.e2e.ts",
|
||||
"tests/hmr-live.e2e.ts",
|
||||
"tests/seeded-history.e2e.ts",
|
||||
"tests/cold-blank-session.e2e.ts",
|
||||
"tests/stats-paged-history.e2e.ts",
|
||||
"tests/sidebar-scrollbar.e2e.ts",
|
||||
"tests/conversation-column-overflow.e2e.ts",
|
||||
|
||||
Reference in New Issue
Block a user