// Keyless browser regression for the details column's default visibility and Session ownership. // The shipped composition starts closed after selection and reload, retains an explicitly opened width through // unselected states, and closes it only when a different Session takes ownership. import { readFile } from 'node:fs/promises' import { fileURLToPath } from 'node:url' import type { Browser, Page } from 'playwright' import { chromium } from 'playwright' import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest' import { acknowledgeReloadConnectionLoss, fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold, } from './scaffold.ts' import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts' const FIXTURE = fileURLToPath(new URL('./snapshots/lifecycle-chrome/session.jsonl', import.meta.url)) const SEED_FIXTURE = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url)) const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.' const MODE = webSnapshotMode() /** Last AppFrame grid track in CSS pixels. */ async function detailsTrack(page: Page): Promise { return await appFrame(page).evaluate((element) => { const tracks = getComputedStyle(element).gridTemplateColumns.split(' ') return Number.parseFloat(tracks.at(-1) ?? 'NaN') }) } /** AppFrame is the only product element with an inline grid track template. */ function appFrame(page: Page) { return page.locator('[style*="grid-template-columns"]').first() } describe.skipIf(MODE === 'record')('web e2e: details panel follows the current Session lifecycle', () => { let scaffold: WebScaffold let browser: Browser let page: Page let tripwire: ReturnType beforeAll(async () => { const fixture = await readFile(FIXTURE, 'utf8') expect(fixtureUserPrompts(fixture)).toEqual([PROMPT]) scaffold = await launchWebScaffold({ replayFixture: FIXTURE, paceMs: 5 }) await seedSession(scaffold, await readFile(SEED_FIXTURE, 'utf8'), 'details-session-lifecycle-seed') browser = await chromium.launch() page = await newEnglishPage(browser) tripwire = watchConsole(page) await page.goto(scaffold.baseUrl, { waitUntil: 'load' }) await appFrame(page).waitFor({ timeout: 30_000 }) await connectFreshWorkspace(page) }, 120_000) afterAll(async () => { await browser?.close() await scaffold?.close() }) it('starts and reloads closed, then stays closed across Session ownership changes', async () => { onTestFailed(() => saveFailureShot(page, 'web-e2e-details-session-lifecycle')) const settled = scaffold.whenTurnSettled() const input = page.locator('textarea').first() await input.fill(PROMPT) await input.press('Enter') await settled await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0) expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false) const warningStart = tripwire.warnings.length await page.reload({ waitUntil: 'load' }) acknowledgeReloadConnectionLoss(tripwire, warningStart) await appFrame(page).waitFor({ timeout: 30_000 }) await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0) expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false) await page.getByRole('button', { name: /^(?:New session|新.*会话)$/ }).last().click() await page.getByText("Let's start building", { exact: false }).waitFor({ timeout: 15_000 }) await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0) expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false) const original = page.locator('[role=treeitem]').filter({ hasText: 'Reply with the single word' }).first() await original.click() await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 }) await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0) expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false) const ungrouped = page.getByText('Ungrouped', { exact: true }) const ungroupedRow = ungrouped.locator('..').locator('..') const ungroupedSection = ungroupedRow.locator('..') await expect.poll(async () => { if (await ungroupedRow.getAttribute('aria-expanded') !== 'true') { await ungrouped.click() await page.waitForTimeout(50) } return await ungroupedRow.getAttribute('aria-expanded') }, { timeout: 5_000 }).toBe('true') const seeded = ungroupedSection.locator('[role="treeitem"]').nth(1) await seeded.click() await page.getByText('DONE', { exact: true }).waitFor({ timeout: 15_000 }) await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0) expect(tripwire.pageErrors).toEqual([]) expect(tripwire.warnings).toEqual([]) }, 90_000) })