goal-command-presentation.e2e.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. // Web e2e: /goal opts its command input into the human transcript while the
  2. // command remains log-only. The shipped composition runs with no model adapter,
  3. // so an accidental turn fails loud in addition to the event-level assertions.
  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 type { SessionEvent } from '@deepseek-ai/dsh-session/types'
  9. import type {} from '@deepseek-ai/dsh-commands/types'
  10. import {
  11. acknowledgeReloadConnectionLoss, assertFixtureInventory, captureStableAria,
  12. compareOrRefreshGolden, launchWebScaffold, watchConsole, webSnapshotMode,
  13. type WebScaffold,
  14. } from './scaffold.ts'
  15. import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './support.ts'
  16. const SNAPSHOT_DIR = fileURLToPath(new URL('./expected/goal-command-presentation', import.meta.url))
  17. const UI_EXPECTED = fileURLToPath(new URL(
  18. './expected/goal-command-presentation/ui.expected.md', import.meta.url,
  19. ))
  20. const MODE = webSnapshotMode()
  21. describe('web e2e: /goal human transcript presentation', () => {
  22. let scaffold: WebScaffold
  23. let browser: Browser
  24. let page: Page
  25. let tripwire: ReturnType<typeof watchConsole>
  26. const events: SessionEvent[] = []
  27. beforeAll(async () => {
  28. scaffold = await launchWebScaffold()
  29. scaffold.ctx.on('session/event', (_session, event: SessionEvent) => { events.push(event) })
  30. browser = await chromium.launch()
  31. page = await newEnglishPage(browser)
  32. tripwire = watchConsole(page)
  33. await page.goto(scaffold.authenticatedUrl, { waitUntil: 'load' })
  34. await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
  35. await connectFreshWorkspace(page, scaffold.workspaceCwd)
  36. }, 120_000)
  37. afterAll(async () => {
  38. await browser?.close()
  39. await scaffold?.close()
  40. })
  41. it('completes with Tab and shows the bare input and result without a model turn', async () => {
  42. onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-command-presentation'))
  43. await expect.poll(() => page.getByText('Into the Unknown', { exact: false }).count(), {
  44. timeout: 15_000,
  45. }).toBe(1)
  46. const input = page.locator('[data-composer-input]').first()
  47. await input.fill('/go')
  48. const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
  49. await menu.getByRole('option', { name: 'goal set or view the goal for a long-running task' })
  50. .waitFor({ timeout: 10_000 })
  51. await input.press('Tab')
  52. await expect.poll(() => input.textContent()).toBe('/goal ')
  53. await expect.poll(() => menu.count()).toBe(0)
  54. await input.press('Enter')
  55. const commandInput = page.locator('[data-command-input]')
  56. await commandInput.waitFor({ timeout: 10_000 })
  57. await expect.poll(() => commandInput.textContent()).toBe('/goal')
  58. expect(await commandInput.getAttribute('role')).toBe('group')
  59. expect(await commandInput.getAttribute('aria-label')).toBe('Command input')
  60. expect(await commandInput.getByRole('button').count()).toBe(0)
  61. const typography = await commandInput.evaluate((element) => {
  62. const bubble = element.firstElementChild?.firstElementChild
  63. if (!(bubble instanceof HTMLElement)) throw new Error('command input bubble is missing')
  64. const rootStyle = getComputedStyle(element)
  65. const bubbleStyle = getComputedStyle(bubble)
  66. return {
  67. fontFamily: bubbleStyle.fontFamily,
  68. parentFontFamily: rootStyle.fontFamily,
  69. fontSize: bubbleStyle.fontSize,
  70. lineHeight: bubbleStyle.lineHeight,
  71. }
  72. })
  73. expect(typography).toMatchObject({ fontSize: '14px', lineHeight: '22px' })
  74. expect(typography.fontFamily).not.toBe(typography.parentFontFamily)
  75. const resultRow = page.locator('[data-variant="others"]').filter({ hasText: 'No goal is currently set.' })
  76. await expect.poll(() => resultRow.count(), { timeout: 10_000 }).toBe(1)
  77. expect(await resultRow.getByText('goal', { exact: true }).count()).toBe(1)
  78. await expect.poll(() => page.locator('[data-phase="active"]').count()).toBe(1)
  79. expect(await page.getByText('Into the Unknown', { exact: false }).count()).toBe(0)
  80. const run = events.find(event => event.type === 'command/run')
  81. expect(run).toMatchObject({
  82. type: 'command/run',
  83. data: { name: 'goal', args: ' ', source: { kind: 'user' } },
  84. })
  85. expect(events.some(event => event.type === 'command/done')).toBe(true)
  86. expect(events.some(event => event.type === 'user/message')).toBe(false)
  87. expect(events.some(event => event.type === 'turn/start')).toBe(false)
  88. expect(events.some(event => event.type === 'step/start')).toBe(false)
  89. expect(events.some(event => event.type === 'request/header')).toBe(false)
  90. const snapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
  91. await compareOrRefreshGolden(UI_EXPECTED, snapshot, MODE)
  92. }, 60_000)
  93. it('reloads the same bubble and result from the persisted command lifecycle', async () => {
  94. onTestFailed(() => saveFailureShot(page, 'web-e2e-goal-command-presentation-reload'))
  95. const warningStart = tripwire.warnings.length
  96. await page.reload({ waitUntil: 'load' })
  97. await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
  98. acknowledgeReloadConnectionLoss(tripwire, warningStart)
  99. await expect.poll(() => page.locator('[data-command-input]').textContent(), { timeout: 15_000 }).toBe('/goal')
  100. const resultRow = page.locator('[data-variant="others"]').filter({ hasText: 'No goal is currently set.' })
  101. await expect.poll(() => resultRow.count(), { timeout: 10_000 }).toBe(1)
  102. await expect.poll(() => page.locator('[data-phase="active"]').count()).toBe(1)
  103. const sessions = scaffold.ctx.sessions.list()
  104. expect(sessions).toHaveLength(1)
  105. const persisted = sessions[0]?.events ?? []
  106. expect(persisted.filter(event => event.type === 'command/run' || event.type === 'command/done')
  107. .map(event => event.type)).toEqual(['command/run', 'command/done'])
  108. expect(persisted.some(event => event.type === 'user/message')).toBe(false)
  109. expect(persisted.some(event => event.type === 'turn/start')).toBe(false)
  110. expect(persisted.some(event => event.type === 'step/start')).toBe(false)
  111. expect(persisted.some(event => event.type === 'request/header')).toBe(false)
  112. expect(tripwire.pageErrors).toEqual([])
  113. expect(tripwire.warnings).toEqual([])
  114. await assertFixtureInventory(SNAPSHOT_DIR, ['ui.expected.md'])
  115. }, 90_000)
  116. })