diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index 30d9f922be..f42337be37 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/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([]) diff --git a/apps/web/tests/settings-chrome.e2e.ts b/apps/web/tests/settings-chrome.e2e.ts index e617c9aead..8ef2af7048 100644 --- a/apps/web/tests/settings-chrome.e2e.ts +++ b/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 => 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 => { + 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)