sidebar-title-hover-scroll.e2e.ts 4.3 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586
  1. // Web e2e scenario: hovering a sidebar session row reveals a title wider than
  2. // its cell. The jsdom lane pins only the scroll position the handler writes;
  3. // the assembled browser is where the title really clips, really reaches its own
  4. // hovered extent (the trailing relative-time cell yields to the row menu while
  5. // the pointer rests on the row, so the hovered cell is wider than the resting
  6. // one), and really returns to its start when the pointer leaves.
  7. //
  8. // Zero model calls: seeding one renamed session and hovering its row touches no
  9. // provider.
  10. import { readFile } from 'node:fs/promises'
  11. import { fileURLToPath } from 'node:url'
  12. import { chromium } from 'playwright'
  13. import type { Browser, Page } from 'playwright'
  14. import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
  15. import { launchWebScaffold, seedSession, watchConsole, type WebScaffold } from './scaffold.ts'
  16. import { newEnglishPage, saveFailureShot } from './support.ts'
  17. const SEED = fileURLToPath(new URL('../../../snapshots/web/seeded-history/session.v3.jsonl', import.meta.url))
  18. /** Wider than the sidebar cell, under the 80-byte title limit, with a far-edge suffix the reveal must reach. */
  19. const TITLE = 'Forked session clipped before its suffix (1)'
  20. describe('web e2e: hovering a clipped session title scrolls it to its far edge', () => {
  21. let scaffold: WebScaffold
  22. let browser: Browser
  23. let page: Page
  24. let tripwire: ReturnType<typeof watchConsole>
  25. beforeAll(async () => {
  26. scaffold = await launchWebScaffold({})
  27. const workspace = await scaffold.ctx.workspaceRegistry.create(scaffold.workspaceCwd)
  28. const id = await seedSession(scaffold, await readFile(SEED, 'utf8'), 'sidebar-title-hover-scroll')
  29. await workspace.attachSession(id)
  30. await scaffold.ctx.sessionController.rename({ sessionId: id, title: TITLE })
  31. browser = await chromium.launch()
  32. page = await newEnglishPage(browser)
  33. tripwire = watchConsole(page)
  34. await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
  35. await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
  36. }, 120_000)
  37. afterAll(async () => {
  38. await browser?.close()
  39. await scaffold?.close()
  40. })
  41. it('reveals the clipped title under the pointer and restores its start after', async () => {
  42. onTestFailed(() => saveFailureShot(page, 'web-e2e-sidebar-title-hover-scroll'))
  43. const row = page.getByRole('treeitem').filter({ has: page.getByText(TITLE, { exact: true }) })
  44. await row.waitFor({ timeout: 20_000 })
  45. const title = row.getByText(TITLE, { exact: true })
  46. // At rest the row clips its title with an ellipsis and holds its start.
  47. expect(await title.evaluate(el => el.scrollWidth - el.clientWidth)).toBeGreaterThan(0)
  48. expect(await title.evaluate(el => getComputedStyle(el).textOverflow)).toBe('ellipsis')
  49. expect(await title.evaluate(el => el.scrollLeft)).toBe(0)
  50. expect(await title.evaluate(el => getComputedStyle(el).scrollBehavior)).toBe('smooth')
  51. await row.hover()
  52. // Poll against the live extent: the reveal must reach the far edge of the
  53. // hovered cell (the wider one — the relative-time label yields to the row
  54. // menu), whatever width the handler itself read.
  55. await expect.poll(
  56. async () => title.evaluate(el => el.scrollLeft >= el.scrollWidth - el.clientWidth - 1),
  57. { timeout: 10_000 },
  58. ).toBe(true)
  59. // Scrolled to its end, the title must not paint the ellipsis over the
  60. // characters the reveal reached.
  61. expect(await title.evaluate(el => getComputedStyle(el).textOverflow)).toBe('clip')
  62. // Leaving returns in one step: a smooth return would still be travelling
  63. // inside this window, and it would carry the resting ellipsis back with it.
  64. await page.mouse.move(0, 0)
  65. await expect.poll(
  66. async () => title.evaluate(el => el.scrollLeft),
  67. { timeout: 150, interval: 20 },
  68. ).toBe(0)
  69. expect(await title.evaluate(el => getComputedStyle(el).textOverflow)).toBe('ellipsis')
  70. // The reduce override reaches the real stylesheet, so the reveal jumps
  71. // instead of gliding.
  72. await page.emulateMedia({ reducedMotion: 'reduce' })
  73. expect(await title.evaluate(el => getComputedStyle(el).scrollBehavior)).toBe('auto')
  74. await page.emulateMedia({ reducedMotion: null })
  75. expect(tripwire.pageErrors).toEqual([])
  76. }, 60_000)
  77. })