details-session-lifecycle.e2e.ts 4.4 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. // Keyless browser regression for the details column's Session ownership.
  2. // The shipped composition retains geometry through unselected states and closes it only when a different Session takes ownership.
  3. import { readFile } from 'node:fs/promises'
  4. import { fileURLToPath } from 'node:url'
  5. import type { Browser, Page } from 'playwright'
  6. import { chromium } from 'playwright'
  7. import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
  8. import {
  9. fixtureUserPrompts, launchWebScaffold, seedSession, watchConsole, webSnapshotMode, type WebScaffold,
  10. } from './scaffold.ts'
  11. import { connectFreshWorkspace, saveFailureShot } from './support.ts'
  12. const FIXTURE = fileURLToPath(new URL('./snapshots/lifecycle-chrome/session.jsonl', import.meta.url))
  13. const SEED_FIXTURE = fileURLToPath(new URL('./snapshots/seeded-history/seed.jsonl', import.meta.url))
  14. const PROMPT = 'Reply with the single word LIGHTHOUSE and stop.'
  15. const MODE = webSnapshotMode()
  16. /** Last AppFrame grid track in CSS pixels. */
  17. async function detailsTrack(page: Page): Promise<number> {
  18. return await appFrame(page).evaluate((element) => {
  19. const tracks = getComputedStyle(element).gridTemplateColumns.split(' ')
  20. return Number.parseFloat(tracks.at(-1) ?? 'NaN')
  21. })
  22. }
  23. /** AppFrame is the only product element with an inline grid track template. */
  24. function appFrame(page: Page) {
  25. return page.locator('[style*="grid-template-columns"]').first()
  26. }
  27. describe.skipIf(MODE === 'record')('web e2e: details panel follows the current Session lifecycle', () => {
  28. let scaffold: WebScaffold
  29. let browser: Browser
  30. let page: Page
  31. let tripwire: ReturnType<typeof watchConsole>
  32. beforeAll(async () => {
  33. const fixture = await readFile(FIXTURE, 'utf8')
  34. expect(fixtureUserPrompts(fixture)).toEqual([PROMPT])
  35. scaffold = await launchWebScaffold({ replayFixture: FIXTURE, paceMs: 5 })
  36. await seedSession(scaffold, await readFile(SEED_FIXTURE, 'utf8'), 'details-session-lifecycle-seed')
  37. browser = await chromium.launch()
  38. page = await browser.newPage({ viewport: { width: 1680, height: 1000 } })
  39. tripwire = watchConsole(page)
  40. await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
  41. await appFrame(page).waitFor({ timeout: 30_000 })
  42. await connectFreshWorkspace(page)
  43. }, 120_000)
  44. afterAll(async () => {
  45. await browser?.close()
  46. await scaffold?.close()
  47. })
  48. it('retains geometry through hero and closes it for a different Session', async () => {
  49. onTestFailed(() => saveFailureShot(page, 'web-e2e-details-session-lifecycle'))
  50. const settled = scaffold.whenTurnSettled()
  51. const input = page.locator('textarea').first()
  52. await input.fill(PROMPT)
  53. await input.press('Enter')
  54. await settled
  55. await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
  56. await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
  57. expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
  58. await page.getByRole('button', { name: 'New session', exact: true }).last().click()
  59. await page.getByText("Let's start building", { exact: false }).waitFor({ timeout: 15_000 })
  60. await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
  61. expect(await page.getByText('详情', { exact: true }).isVisible()).toBe(false)
  62. const original = page.locator('[role=treeitem]').filter({ hasText: 'Reply with the single word' }).first()
  63. await original.click()
  64. await page.getByText('LIGHTHOUSE', { exact: true }).waitFor({ timeout: 15_000 })
  65. await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(360)
  66. expect(await page.getByText('详情', { exact: true }).count()).toBe(1)
  67. const ungrouped = page.getByText('Ungrouped', { exact: true })
  68. const ungroupedRow = ungrouped.locator('..').locator('..')
  69. const ungroupedSection = ungroupedRow.locator('..')
  70. await expect.poll(async () => {
  71. if (await ungroupedRow.getAttribute('aria-expanded') !== 'true') {
  72. await ungrouped.click()
  73. await page.waitForTimeout(50)
  74. }
  75. return await ungroupedRow.getAttribute('aria-expanded')
  76. }, { timeout: 5_000 }).toBe('true')
  77. const seeded = ungroupedSection.locator('[role="treeitem"]').nth(1)
  78. await seeded.click()
  79. await page.getByText('DONE', { exact: true }).waitFor({ timeout: 15_000 })
  80. await expect.poll(() => detailsTrack(page), { timeout: 5_000 }).toBe(0)
  81. expect(tripwire.pageErrors).toEqual([])
  82. expect(tripwire.warnings).toEqual([])
  83. }, 90_000)
  84. })