Parcourir la source

test(web): await durable UI and responsive layout publication

Tianyi Cui il y a 1 mois
Parent
commit
e7feedab5e

+ 2 - 2
.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write .agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md
-2026-09-08-ci-completion-observations.md: c03f8ee40ad433c964d2c4f166dfb18e53b79bf3
-2026-09-08-ci-completion-observations.zh.md: 92ba2130e21459cbc6b5fbc5600db30fbd896d2f
+2026-09-08-ci-completion-observations.md: 52deed9cf3e7bdbb7a6a6ad509834bc66b533165
+2026-09-08-ci-completion-observations.zh.md: b909efdd505a59a6344668cd7fc6811601338099

+ 4 - 0
.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.md

@@ -22,6 +22,10 @@ The [publint runner tests](../../../../scripts/publint-all.spec.ts) await asynch
 
 The [Details Session-lifecycle test](../../../../apps/web/tests/details-session-lifecycle.e2e.ts) awaits the frame’s captured animation promises after closed state appears, then retains the zero-width assertion. Completed and cancelled transitions both reach that assertion; cancellation cannot make a persistent nonzero track pass. A paused real grid transition reproduces the close assertion failure and completes successfully only after release, while a persistent one-pixel track remains rejected.
 
+The [feedback-release test](../../../../apps/web/tests/feedback-release.e2e.ts) waits for the model menu to close after selection: its projected label can change while the selection response remains in flight. The [queued-image test](../../../../apps/web/tests/queue-image.e2e.ts) distinguishes the optimistic preview from the durable row’s attachment; thumbnail capture follows admission and the durable attachment response. Held-response controls pin both orderings, and independent test-finished cleanup releases intercepted requests even after timeout.
+
+The [wide-table test](../../../../apps/web/tests/markdown-wide-table.e2e.ts) waits for requested and rendered frame tracks to agree and for the conversation ResizeObserver to publish the current rendered width. Two identical intermediate table widths do not prove those inputs settled. Paused native transitions and delayed observer callbacks reproduce the stale breakout reading; an intentionally broken breakout still fails the unchanged geometry assertions.
+
 ### Built-client import classification
 
 The [master Windows run](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34204779455/job/101996934534) also rejects the intentional CSS import exposed by `ui-dockkit`. The [Node import sweep](../../../../packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts) admits that exact bundle only when Node reports `ERR_UNKNOWN_FILE_EXTENSION` for its `dockkit.module.css`. Other errors at the same entry still fail, and an entry that imports successfully reports a stale exemption. This preserves the import sweep without requiring a browser-only component library to load its stylesheet under bare Node.

+ 4 - 0
.agents/notes/implemented/testing/2026-09-08-ci-completion-observations.zh.md

@@ -22,6 +22,10 @@ Status: implemented
 
 [详情 Session 生命周期测试](../../../../apps/web/tests/details-session-lifecycle.e2e.ts)在关闭状态出现后等待框架已捕获的动画 Promise,再保留宽度为零的断言。完成和取消的过渡都会进入该断言;取消不能让持续非零的轨道通过。暂停真实网格过渡可以复现关闭断言失败,只有释放后才成功完成,而持续一像素的轨道仍被拒绝。
 
+[反馈释放测试](../../../../apps/web/tests/feedback-release.e2e.ts)在选择模型后等待菜单关闭:选择响应仍在传输时,投影标签就可能变化。[排队图片测试](../../../../apps/web/tests/queue-image.e2e.ts)区分乐观预览和持久化行的附件;缩略图捕获发生在接纳和持久化附件响应之后。受控响应屏障固定这两种顺序,独立的测试完成清理即使在超时后也会释放拦截的请求。
+
+[宽表格测试](../../../../apps/web/tests/markdown-wide-table.e2e.ts)等待请求的框架轨道与渲染轨道一致,并等待会话 ResizeObserver 发布当前渲染宽度。两次相同的中间表格宽度不能证明这些输入已稳定。暂停原生过渡和延迟观察回调能够复现过期的溢出读数;故意破坏溢出布局时,未修改的几何断言仍然失败。
+
 ### 已构建客户端的导入分类
 
 [master Windows 运行](https://github.com/deepseek-harness/deepseek-harness/actions/runs/34204779455/job/101996934534)还拒绝了 `ui-dockkit` 有意暴露的 CSS 导入。[Node 导入检查](../../../../packages/experimental/webworker-runtime/tests/compile/transform-corpus-check.ts)仅在 Node 针对其 `dockkit.module.css` 报告 `ERR_UNKNOWN_FILE_EXTENSION` 时,才允许这个精确的 bundle。相同入口的其他错误仍然失败,成功导入则报告豁免已过期。这保留了导入检查,同时不要求仅面向浏览器的组件库在裸 Node 中加载样式表。

+ 2 - 2
apps/web/tests/README.i18n.yaml

@@ -2,5 +2,5 @@
 # side as of the last confirmed-consistent state. Both languages carry equal authority;
 # after editing either side, bring the other along and re-record with:
 #   pnpm run verify-translation-pairing --write apps/web/tests/README.md
-README.md: 692de9bbbd4071ca077fd9521e3f2f326886f120
-README.zh.md: 8467e7a64b0f7e875ae3f95d6ba69ea2f9193427
+README.md: 252ed70f81a5fb2d41e94f79a00ea989255ce0ab
+README.zh.md: f59bb623c8eae2fa0a80dc4212fc44a6addd3e72

+ 2 - 0
apps/web/tests/README.md

@@ -14,6 +14,8 @@ The GitHub webhook scenario awaits the deterministic model adapter’s first req
 
 Queue layout assertions wait for the responsive frame to settle and sample both related rectangles in one browser evaluation; separate round trips can observe different layout states. Details-panel close assertions await the frame’s running transitions before checking zero width; published closed state alone does not imply settled geometry.
 
+Feedback selection waits for menu closure, queued-image capture waits for the durable attachment after admission, and wide-table measurements wait for frame tracks and observer-published conversation width to agree with rendered geometry.
+
 ## These are Host-face tests
 
 They type-check in the root `tsconfig.host.json`, not in the Client aggregate,

+ 2 - 0
apps/web/tests/README.zh.md

@@ -13,6 +13,8 @@ GitHub webhook 场景先等待确定性模型适配器收到首个请求,再
 
 Queue 布局断言等待响应式框架稳定,并在一次浏览器执行中采样相关的两个矩形;分开的往返可能观察到不同布局状态。详情面板关闭断言先等待框架正在运行的过渡结束,再检查宽度为零;已发布的关闭状态本身不代表几何布局已稳定。
 
+反馈选择等待菜单关闭,排队图片捕获等待接纳后的持久化附件,宽表格测量等待框架轨道和观察器发布的会话宽度与渲染几何一致。
+
 ## 这些是 Host 面的测试
 
 它们在根 `tsconfig.host.json` 中做类型检查,而不在 Client aggregate 中,因为它们直接读取

+ 27 - 2
apps/web/tests/feedback-release.e2e.ts

@@ -9,7 +9,7 @@ import { gunzipSync } from 'node:zlib'
 import type { Browser, Page } from 'playwright'
 import { chromium } from 'playwright'
 import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session'
-import { afterAll, beforeAll, describe, expect, it, onTestFailed } from 'vitest'
+import { afterAll, beforeAll, describe, expect, it, onTestFailed, onTestFinished } from 'vitest'
 import {
   acknowledgeReloadConnectionLoss, assertFixtureInventory, captureExpandedTurnProcessAria, captureStableAria,
   compareOrRefreshGolden, fixtureUserPrompts,
@@ -199,7 +199,32 @@ describe.each(MODE === 'record' ? ['deepseek-official'] : ['deepseek-official',
     const events = await readPersistedEvents(scaffold, sessionId)
     expect(events.at(-1)?.type).toBe('command/done')
     expect(events.at(-1)!.seq).toBeGreaterThan(authorized.at(-1)!.seq)
-    await selectModel('Feedback mock')
+    const release = Promise.withResolvers<undefined>()
+    onTestFinished(async () => {
+      release.resolve(undefined)
+      await page.unrouteAll({ behavior: 'wait' })
+    }, 15_000)
+    let responseReceived = false
+    await page.route('**/api/session/selectModel', async (route) => {
+      const response = await route.fetch({ timeout: 10_000 })
+      responseReceived = true
+      await release.promise
+      await route.fulfill({ response })
+    }, { times: 1 })
+    const selection = selectModel('Feedback mock')
+    // Observe rejection immediately; the original promise is awaited after releasing the response.
+    const observedSelection = Promise.allSettled([selection])
+    try {
+      await expect.poll(() => responseReceived, { timeout: 10_000 }).toBe(true)
+      const trigger = page.getByRole('button', { name: /^Select model, current/ })
+      await expect.poll(() => trigger.getAttribute('aria-label'), { timeout: 10_000 }).toContain('Feedback mock')
+      expect(await trigger.getAttribute('aria-expanded')).toBe('true')
+    } finally {
+      release.resolve(undefined)
+      await page.unrouteAll({ behavior: 'wait' })
+      await observedSelection
+    }
+    await selection
     await selectModel('DeepSeek-V4-Flash')
     const warningStart = tripwire.warnings.length
     await page.reload({ waitUntil: 'load' })

+ 45 - 13
apps/web/tests/queue-image.e2e.ts

@@ -11,7 +11,7 @@ import { fileURLToPath } from 'node:url'
 import { join } from 'node:path'
 import type { Browser, Page } from 'playwright'
 import { chromium } from 'playwright'
-import { afterEach, describe, expect, it, onTestFailed } from 'vitest'
+import { afterEach, describe, expect, it, onTestFailed, onTestFinished } from 'vitest'
 import { deriveReplayScript, parseSessionLog, type ReplayEntry } from '@deepseek-ai/dsh-llm-replay'
 import type { SessionEvent } from '@deepseek-ai/dsh-session'
 import {
@@ -97,18 +97,50 @@ describe('web e2e: queued image submission', () => {
     await page.locator('[data-composer-input][contenteditable="true"]').first().waitFor({ timeout: 10_000 })
     await pasteImage(page, await readFile(PNG))
     await page.getByRole('img', { name: 'queued.png' }).waitFor({ timeout: 10_000 })
-    await input.fill(QUEUED_TEXT)
-    await input.press('Enter')
-
-    // Admission replaces the local preview; the durable row loads its own thumbnail.
-    await page.getByRole('button', { name: 'Remove queued message', disabled: false }).waitFor({ timeout: 15_000 })
-    const dockThumb = page.locator('[data-queue-dock] li:not([data-submission-echo]) img[alt="Queued message image"]')
-    await dockThumb.waitFor({ timeout: 15_000 })
-    await expect.poll(() => dockThumb.getAttribute('src')).toMatch(/^blob:/)
-    await expect.poll(() => dockThumb.evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true)
-    await page.getByText(QUEUED_TEXT, { exact: true }).waitFor()
-    const queuedSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
-    await compareOrRefreshGolden(QUEUED_EXPECTED, queuedSnapshot, MODE)
+    const releasePrompt = Promise.withResolvers<undefined>()
+    const releaseImage = Promise.withResolvers<undefined>()
+    onTestFinished(async () => {
+      releasePrompt.resolve(undefined)
+      releaseImage.resolve(undefined)
+      // afterEach closes this test's browser before onTestFinished runs.
+      if (!page.isClosed()) await page.unrouteAll({ behavior: 'wait' })
+    }, 15_000)
+    let imageRequested = false
+    await page.route('**/api/session/prompt', async (route) => {
+      await releasePrompt.promise
+      await route.continue()
+    }, { times: 1 })
+    await page.route('**/api/session/attachment', async (route) => {
+      imageRequested = true
+      await releaseImage.promise
+      await route.continue()
+    }, { times: 1 })
+    const dockThumb = page.locator('[data-queue-dock] img[alt="Queued message image"]')
+    try {
+      await input.fill(QUEUED_TEXT)
+      await input.press('Enter')
+      await dockThumb.waitFor({ timeout: 15_000 })
+      await expect.poll(() => dockThumb.getAttribute('src'), { timeout: 15_000 }).toMatch(/^blob:/)
+      expect(await page.locator('[data-queue-dock] [data-submission-echo]').count()).toBe(1)
+      releasePrompt.resolve(undefined)
+      await expect.poll(() => imageRequested, { timeout: 15_000 }).toBe(true)
+      await page.getByText(QUEUED_TEXT, { exact: true }).waitFor()
+      await page.getByRole('button', { name: 'Remove queued message', disabled: false }).waitFor({ timeout: 15_000 })
+      expect(await page.locator('[data-queue-dock] [data-submission-echo]').count()).toBe(0)
+      expect(await dockThumb.count()).toBe(0)
+      releaseImage.resolve(undefined)
+      // Admission replaces the optimistic image; wait for the durable row's own thumbnail.
+      const durableThumb = page.locator('[data-queue-dock] li:not([data-submission-echo]) img[alt="Queued message image"]')
+      await durableThumb.waitFor({ timeout: 15_000 })
+      await expect.poll(() => durableThumb.getAttribute('src'), { timeout: 15_000 }).toMatch(/^blob:/)
+      await expect.poll(() => durableThumb.evaluate((image: HTMLImageElement) => image.complete && image.naturalWidth > 0)).toBe(true)
+      const queuedSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
+      await compareOrRefreshGolden(QUEUED_EXPECTED, queuedSnapshot, MODE)
+    } finally {
+      releasePrompt.resolve(undefined)
+      releaseImage.resolve(undefined)
+      await page.unrouteAll({ behavior: 'wait' })
+    }
 
     // Stop parks the accepted queue; the next waking send delivers the image
     // message first (FIFO), then its own text as the following turn.