Răsfoiți Sursa

test(web): await command and settings readiness before assertions

imccyu 5 zile în urmă
părinte
comite
58144ccf78
2 a modificat fișierele cu 40 adăugiri și 18 ștergeri
  1. 13 5
      apps/web/tests/lifecycle-chrome.e2e.ts
  2. 27 13
      apps/web/tests/settings-chrome.e2e.ts

+ 13 - 5
apps/web/tests/lifecycle-chrome.e2e.ts

@@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url'
 import { join } from 'node:path'
 import type { Browser, Page, WebSocketRoute } from 'playwright'
 import { chromium } from 'playwright'
-import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished } from 'vitest'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
 import {
   acknowledgeReloadConnectionLoss, assertFixtureInventory, captureExpandedTurnProcessAria,
@@ -72,10 +72,17 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
 
   it.skipIf(MODE === 'record')('opens the shared slash menu from plus with only Command candidates', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-command-menu-launcher'))
+    const input = page.locator('[data-composer-input]').first()
+    onTestFinished(async () => {
+      await input.press('Escape')
+      await writeComposerDraft(page, input, '')
+      await page.getByRole('listbox', { name: 'Trigger suggestions' }).waitFor({ state: 'hidden' })
+    })
     const launcher = page.getByRole('button', { name: 'Commands' })
     await launcher.click()
     const menu = page.getByRole('listbox', { name: 'Trigger suggestions' })
-    await menu.waitFor({ timeout: 10_000 })
+    await menu.getByRole('option').first().waitFor({ timeout: 10_000 })
+    await menu.getByRole('status').waitFor({ state: 'hidden', timeout: 10_000 })
     const snapshot = await captureStableAria(page, '[role="listbox"]', scaffold.workspaceCwd)
     await compareOrRefreshGolden(COMMAND_MENU_EXPECTED, snapshot, MODE)
     expect(snapshot).toContain('text: Commands')
@@ -84,9 +91,9 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
     const launchedBox = await menu.boundingBox()
     await page.locator('[data-composer-input]').first().press('Escape')
     await expect.poll(() => menu.count()).toBe(0)
-    const input = page.locator('[data-composer-input]').first()
     await writeComposerDraft(page, input, '/')
-    await menu.waitFor({ timeout: 10_000 })
+    await menu.getByRole('option').first().waitFor({ timeout: 10_000 })
+    await menu.getByRole('status').waitFor({ state: 'hidden', timeout: 10_000 })
     const typedBox = await menu.boundingBox()
     expect(launchedBox).not.toBeNull()
     expect(typedBox).not.toBeNull()
@@ -114,7 +121,8 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
       const launcher = zhPage.getByRole('button', { name: '指令' })
       await launcher.click()
       const menu = zhPage.getByRole('listbox', { name: '触发候选建议' })
-      await menu.waitFor({ timeout: 10_000 })
+      await menu.getByRole('option').first().waitFor({ timeout: 10_000 })
+      await menu.getByRole('status').waitFor({ state: 'hidden', timeout: 10_000 })
       const snapshot = await captureStableAria(zhPage, '[role="listbox"]', scaffold.workspaceCwd)
       await compareOrRefreshGolden(COMMAND_MENU_ZH_EXPECTED, snapshot, MODE)
       expect(zhTripwire.pageErrors).toEqual([])

+ 27 - 13
apps/web/tests/settings-chrome.e2e.ts

@@ -12,7 +12,7 @@ import { readFile } from 'node:fs/promises'
 import { fileURLToPath } from 'node:url'
 import type { Browser, Locator, Page } from 'playwright'
 import { chromium } from 'playwright'
-import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished } from 'vitest'
 import { join } from 'node:path'
 import { SessionId } from '@deepseek-ai/dsh-session'
 import {
@@ -378,6 +378,10 @@ describe('web e2e: settings modal and General preferences', () => {
 
   it('steps the content font size, applies it to body, and persists across reload', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-settings-font-size'))
+    onTestFinished(async () => {
+      await page.keyboard.press('Escape')
+      await page.getByRole('dialog', { name: '设置', exact: true }).waitFor({ state: 'hidden' })
+    })
     const readFontSize = async (target: Page = page): Promise<string> => await target.evaluate(
       () => document.body.style.getPropertyValue('--dsh-content-font-size'),
     )
@@ -392,6 +396,24 @@ describe('web e2e: settings modal and General preferences', () => {
       probe.remove()
       return size
     })
+    // The displayed value is optimistic; wait for the write before the next step.
+    const stepFontSize = async (button: Locator, px: number): Promise<void> => {
+      const [response] = await Promise.all([
+        page.waitForResponse((reply) => {
+          if (new URL(reply.url()).pathname !== '/api/settings/mutate' || reply.request().method() !== 'POST') return false
+          const request = reply.request().postDataJSON() as { payload: { args: { ns: string } } }
+          return request.payload.args.ns === 'ui-theme'
+        }),
+        button.click(),
+      ])
+      expect(await response.finished()).toBeNull()
+      const envelope = await response.json() as { result: { ok: boolean } }
+      expect(envelope.result.ok).toBe(true)
+      await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
+        .toMatch(new RegExp(`ui-theme:\n(?:\\s+\\w+: .*\n)*?\\s+fontSize: ${px}`))
+      await page.getByRole('dialog', { name: '设置' }).getByText(String(px), { exact: true }).waitFor({ timeout: 5_000 })
+      await expect.poll(readFontSize, { timeout: 5_000 }).toBe(`${px}px`)
+    }
     expect(await readFontSize()).toBe('14px')
     expect(await readSecondaryFontSize()).toBe('13px')
     await page.getByRole('button', { name: '设置', exact: true }).click()
@@ -400,17 +422,12 @@ describe('web e2e: settings modal and General preferences', () => {
     // The stepper reveals its arrows on hover; the up arrow steps 14 → 15 → 16.
     await dialog.getByText('14', { exact: true }).hover()
     const increase = dialog.getByRole('button', { name: '增大字号' })
-    await increase.click()
-    await dialog.getByText('15', { exact: true }).waitFor({ timeout: 5_000 })
+    await stepFontSize(increase, 15)
     // 15 is the piecewise boundary: the secondary tier holds at 13px (−2)
     // where the ≤14 branch would have given 14px (−1).
     await expect.poll(readSecondaryFontSize, { timeout: 5_000 }).toBe('13px')
-    await increase.click()
-    await dialog.getByText('16', { exact: true }).waitFor({ timeout: 5_000 })
-    await expect.poll(readFontSize, { timeout: 5_000 }).toBe('16px')
+    await stepFontSize(increase, 16)
     await expect.poll(readSecondaryFontSize, { timeout: 5_000 }).toBe('14px')
-    await expect.poll(async () => readFile(join(scaffold.harnessHome, 'settings.yaml'), 'utf8'), { timeout: 5_000 })
-      .toMatch(/ui-theme:\n(?:\s+\w+: .*\n)*?\s+fontSize: 16/)
     await page.keyboard.press('Escape')
 
     // Reload: the boot script embeds the durable size and ThemeRuntime seeds
@@ -429,11 +446,8 @@ describe('web e2e: settings modal and General preferences', () => {
     await restored.waitFor({ timeout: 10_000 })
     await restored.getByText('16', { exact: true }).hover()
     const decrease = restored.getByRole('button', { name: '减小字号' })
-    await decrease.click()
-    await restored.getByText('15', { exact: true }).waitFor({ timeout: 5_000 })
-    await decrease.click()
-    await restored.getByText('14', { exact: true }).waitFor({ timeout: 5_000 })
-    await expect.poll(readFontSize, { timeout: 5_000 }).toBe('14px')
+    await stepFontSize(decrease, 15)
+    await stepFontSize(decrease, 14)
     await page.keyboard.press('Escape')
     expect(tripwire.pageErrors).toEqual([])
   }, 90_000)