Browse Source

test(web): settle queue tooltip and select exact seeded session

Tianyi Cui 4 days ago
parent
commit
f3c98e4539

+ 2 - 2
.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.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-06-pr-ci-runner-temporary-storage.md
-2026-09-06-pr-ci-runner-temporary-storage.md: 0e6acd23ebf449b6b768d0c5f96ea4ba90dac2f0
-2026-09-06-pr-ci-runner-temporary-storage.zh.md: 4b1b5a76d94fdd907c5e194a2353ed5829d429ab
+2026-09-06-pr-ci-runner-temporary-storage.md: 53e5ac8498200b82bde1c31393df6a38327db8ef
+2026-09-06-pr-ci-runner-temporary-storage.zh.md: 931ab5fd4da363ad350159cd3ebfe9ab1d093158

+ 2 - 0
.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.md

@@ -32,6 +32,8 @@ The reference-composer fixture maps the known home-abbreviated workspace display
 
 The advanced Python snapshot pauses only its matching workflow child’s first pre-step until the parent’s durable workflow membership event is observed. The fixture supports either event-arrival order and cancels pending waits on abort or disposal. This pins the scenario’s cross-session ordering without sorting notifications or changing production scheduling.
 
+The queue snapshot moves the pointer away from the Stop/Send control and waits for its Send tooltip to close before capture. Workspace-management tests select the sole non-blank Session by its actions affordance, not row position, and select that Session before asserting that archiving it removes the empty Ungrouped bucket. Hover behavior, queue contents, durable archive identity, and reload assertions remain unchanged.
+
 ## Alternatives considered
 
 **Delete shared temporary files from a PR job.** Another runner may still own those files. Repository jobs must not reclaim a shared directory by pathname or age.

+ 2 - 0
.agents/notes/implemented/testing/2026-09-06-pr-ci-runner-temporary-storage.zh.md

@@ -32,6 +32,8 @@ Reference-composer 夹具将已知的 home 缩写 workspace 显示映射到既
 
 高级 Python 快照仅暂停其匹配的 workflow 子进程首次 pre-step,直到观察到父 Session 的持久化 workflow 成员事件。夹具支持事件先到或等待先建立两种顺序,并在取消或销毁时结束未完成等待。这固定了场景的跨 Session 顺序,而不排序通知或改变生产调度。
 
+Queue 快照在捕获前将指针移离 Stop/Send 控件,并等待其 Send tooltip 关闭。Workspace-management 测试通过操作按钮定位唯一非空 Session,而不依赖行位置;在断言归档会移除空的 Ungrouped 分组前,先选中该 Session。Hover 行为、队列内容、持久化归档身份及重载断言保持不变。
+
 ## 考虑过的替代方案
 
 **由 PR 作业删除共享临时文件。** 其他 runner 可能仍在使用这些文件。仓库作业不得按路径或文件年龄回收共享目录。

+ 3 - 0
apps/web/tests/queue-actions.e2e.ts

@@ -222,6 +222,9 @@ describe('web e2e: queue row actions', () => {
     await expect.poll(() => page.getByRole('button', { name: 'Remove queued message' }).count())
       .toBe(2)
 
+    // Stop becomes Send under the pointer; dismiss its hover tooltip before capture.
+    await page.mouse.move(0, 0)
+    await expect.poll(() => page.getByRole('tooltip').filter({ hasText: 'Send message' }).count()).toBe(0)
     const preservedSnapshot = await captureStableAria(page, '[class*="centerCol"]', scaffold.workspaceCwd)
     await compareOrRefreshGolden(PRESERVED_EXPECTED, preservedSnapshot, MODE)
     const expanded = await captureExpandedTurnProcessAria(

+ 14 - 26
apps/web/tests/workspace-management.e2e.ts

@@ -463,9 +463,8 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
   }, 60_000)
 
   /**
-   * Expand Ungrouped and return its seeded session row. The only visible child
-   * is the non-blank persisted Session; the blank Session created while
-   * adopting the Workspace stays hidden.
+   * Expand Ungrouped and return its only non-blank session row. A selected
+   * blank Session from a deleted Workspace may also be visible, without actions.
    * @returns the session row locator, already present.
    */
   async function seededSessionRow() {
@@ -480,9 +479,11 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
       }
       return await ungroupedRow.getAttribute('aria-expanded')
     }, { timeout: 5_000 }).toBe('true')
-    const row = ungroupedSection.locator('[role="treeitem"]').nth(1)
-    await row.waitFor({ timeout: 10_000 })
-    return row
+    // CSS includes the actions button while it is hidden until row hover.
+    const rows = ungroupedSection.locator('[role="treeitem"]')
+      .filter({ has: page.locator('button[aria-label^="Session actions for "]') })
+    await expect.poll(() => rows.count(), { timeout: 10_000 }).toBe(1)
+    return rows.first()
   }
 
   it('shows the session hover card after a dwell on the row', async () => {
@@ -552,26 +553,13 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
 
   it('archives the seeded session from its row menu, hiding it durably across reload', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-ws-archive'))
-    // The seeded session lives under Ungrouped (expanded by the hover-card
-    // test's gesture; converge again for order independence).
-    const ungroupedRow = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..')
-    const ungroupedSection = ungroupedRow.locator('..')
-    await expect.poll(async () => {
-      if (await ungroupedRow.getAttribute('aria-expanded') !== 'true') {
-        await page.getByText('Ungrouped', { exact: true }).click()
-        await page.waitForTimeout(50)
-      }
-      return await ungroupedRow.getAttribute('aria-expanded')
-    }, { timeout: 5_000 }).toBe('true')
-    // Anchor on session rows (the rows carrying a session actions button),
-    // not a positional index, and assert the single-stray assumption loudly
-    // so a fixture gaining a second stray fails here instead of archiving
-    // the wrong row. CSS attribute match, not getByRole: the button is
-    // display:none until its row hovers, and role queries skip hidden nodes.
-    const sessionRows = ungroupedSection.locator('[role="treeitem"]')
-      .filter({ has: page.locator('button[aria-label^="Session actions for "]') })
-    await expect.poll(() => sessionRows.count(), { timeout: 10_000 }).toBe(1)
-    const sessionRow = sessionRows.first()
+    const sessionRow = await seededSessionRow()
+    // Selecting the seed hides any blank stray left by Workspace deletion,
+    // so archiving this last visible Ungrouped Session must remove the bucket.
+    await sessionRow.click()
+    await expect.poll(() => sessionRow.getAttribute('aria-selected'), { timeout: 10_000 }).toBe('true')
+    const ungroupedSection = page.getByText('Ungrouped', { exact: true }).locator('..').locator('..').locator('..')
+    await expect.poll(() => ungroupedSection.locator('[role="treeitem"]').count(), { timeout: 10_000 }).toBe(2)
     const rowTitle = await sessionRow.locator('[class*="title"]').innerText()
     // Row menu: hover reveals the actions button; Archive session commits
     // without a confirmation dialog (non-destructive: log + accounting stay).