Просмотр исходного кода

test(web): adapt workspace browser e2e interactions to the compact sidebar

The sidebar redesign removed group session counts, collapsed search into a
header action, and moved grouping into the View options menu. Update the
browser scenarios that waited on the old surfaces:

- Wait on the Ungrouped bucket row instead of the removed session-count text.
- Expand the collapsed search control before filling the renamed input.
- Open the View options menu instead of the removed Group by button.
- Assert the persisted grouping under the new dsh.workspace.view.v4 key.
- built-boot: assert the fixture workspace group row instead of its count.
_Kerman 1 месяц назад
Родитель
Сommit
e76a03a20c

+ 6 - 1
apps/web/tests/built-boot.snapshot.ts

@@ -21,7 +21,12 @@ it('boots the built plugin graph and renders a fixture session end to end', asyn
 
   // The sidebar renders from the boot graph: every inject layer activated.
   const tree = await screen.findByRole('tree', { name: 'Sessions' }, { timeout: 10_000 })
-  await within(tree).findByText('4 sessions')
+  // The compact layout dropped group session counts; the fixture workspace
+  // group row renders immediately with its sessions beneath it.
+  const fixtureGroup = (await within(tree).findAllByText('fixture'))
+    .map(el => el.closest<HTMLElement>('[role="treeitem"]'))
+    .find(el => el?.getAttribute('aria-expanded') !== null)
+  if (fixtureGroup === undefined) throw new Error('fixture Workspace group missing')
 
   // The resident fixture has both a question and an approval; composer routing
   // exposes the question first, and the assembled workspace plugin mirrors that

+ 7 - 2
apps/web/tests/chat-long-interactions.e2e.ts

@@ -78,8 +78,13 @@ async function nextPaint(page: Page): Promise<void> {
 }
 
 async function openSeed(page: Page): Promise<void> {
-  await page.getByText(/^\d+ sessions?$/, { exact: true }).waitFor({ timeout: 30_000 })
-  const search = page.getByRole('textbox', { name: 'Search name, keywords...', exact: true })
+  // The compact layout dropped group session counts; the seeded baseline is
+  // the Ungrouped bucket once cold summaries load.
+  await page.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
+  // Search collapsed into a header action; expand it before filling.
+  const searchButton = page.getByRole('button', { name: 'Search sessions' })
+  if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
+  const search = page.getByRole('textbox', { name: 'Search sessions...', exact: true })
   await search.fill(FIXTURE.markers.user(1))
   const results = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem')
   await results.first().waitFor({ timeout: 60_000 })

+ 8 - 3
apps/web/tests/chat-scroll-contract.e2e.ts

@@ -168,8 +168,10 @@ async function launchScrollWorld(options: ScrollWorldOptions): Promise<ScrollWor
     await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
     await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
     // Session-list bootstrap can replace the controlled search state. Wait
-    // for the seeded baseline before openSeed starts the lazy content query.
-    await page.getByText(/^\d+ sessions?$/, { exact: true }).waitFor({ timeout: 30_000 })
+    // for the seeded baseline before openSeed starts the lazy content query
+    // (the compact layout dropped group session counts; the Ungrouped bucket
+    // row is the barrier).
+    await page.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
     return {
       events,
       page,
@@ -258,7 +260,10 @@ async function conversationTurns(page: Page): Promise<number> {
 }
 
 async function openSeed(page: Page, fixture: ChatScrollFixture, tailMarker?: string): Promise<void> {
-  const search = page.getByRole('textbox', { name: 'Search name, keywords...', exact: true })
+  // Search collapsed into a header action; expand it before filling.
+  const searchButton = page.getByRole('button', { name: 'Search sessions' })
+  if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
+  const search = page.getByRole('textbox', { name: 'Search sessions...', exact: true })
   // Cold summaries initially show the temporary workspace basename, so the
   // persisted first-message marker is the stable user-facing identity. The
   // query itself triggers lazy content-index reconciliation; no transient

+ 4 - 1
apps/web/tests/composer-tab-geometry.e2e.ts

@@ -249,7 +249,10 @@ async function compareTabsWithoutReservation(page: Page): Promise<TabComparison>
  * @param page - the page under test.
  */
 async function openSeededSession(page: Page): Promise<void> {
-  const search = page.getByRole('textbox', { name: 'Search name, keywords...', exact: true })
+  // Search collapsed into a header action; expand it before filling.
+  const searchButton = page.getByRole('button', { name: 'Search sessions' })
+  if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
+  const search = page.getByRole('textbox', { name: 'Search sessions...', exact: true })
   await search.fill(FIXTURE.markers.user(1))
   const results = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem')
   const deadline = Date.now() + 60_000

+ 7 - 2
apps/web/tests/lifecycle-chrome.e2e.ts

@@ -197,8 +197,13 @@ describe('web e2e: lifecycle & chrome (workspace flow / reload / dark mode)', ()
   it.skipIf(MODE === 'record')('materialized a real Workspace and Session over the wire', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-lifecycle-materialize'))
     // Browser: the sidebar tree now carries the auto-created workspace group
-    // with its one session, and the opened session is the selected row.
-    await expect.poll(() => page.getByText('1 session', { exact: true }).count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(1)
+    // with its one session, and the opened session is the selected row. The
+    // compact layout dropped group session counts, so the group row itself is
+    // the barrier.
+    await expect.poll(
+      () => page.locator('[role="treeitem"][aria-expanded]').filter({ hasText: 'workspace' }).count(),
+      { timeout: 15_000 },
+    ).toBeGreaterThanOrEqual(1)
     await expect.poll(() => page.locator('[role="treeitem"][aria-selected="true"]').count(), { timeout: 10_000 }).toBe(1)
     await expect.poll(() => page.getByText('LIGHTHOUSE', { exact: true }).count(), { timeout: 15_000 }).toBeGreaterThanOrEqual(1)
     // Host: the session's durable header cwd is the folder the workspace

+ 14 - 6
apps/web/tests/navigation-panes.e2e.ts

@@ -53,7 +53,10 @@ async function assertBaselineSucceeded(response: Response, method: string): Prom
 
 async function ensureSeedOpen(page: Page): Promise<void> {
   const chat = page.getByRole('tab', { name: 'Chat', exact: true })
-  const search = page.getByPlaceholder('Search name, keywords', { exact: false })
+  // Search is a collapsed header action; expand it so the input is actionable.
+  const searchButton = page.getByRole('button', { name: 'Search sessions' })
+  if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
+  const search = page.getByPlaceholder('Search sessions', { exact: false })
   if (await chat.count() === 0) {
     await search.fill('WATERFALL')
     const result = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem')
@@ -119,8 +122,9 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
     await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
     // The frame mounts before the asynchronous session-list baseline lands.
     // Search must target the settled seeded row, not the startup input that
-    // the ready projection replaces.
-    await page.getByText('1 session', { exact: true }).waitFor({ timeout: 30_000 })
+    // the ready projection replaces (the compact layout dropped group session
+    // counts; the Ungrouped bucket row is the barrier).
+    await page.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
   }, 120_000)
 
   afterEach(async () => {
@@ -176,9 +180,13 @@ describe('web e2e: navigation & panes over a rich seeded session', () => {
   it.skipIf(MODE === 'record')('finds an unopened seeded session by message content and opens it', async () => {
     onTestFailed(() => saveFailureShot(page, 'web-e2e-navigation-search'))
     // The API baselines can settle before React commits their projection. The
-    // seeded count is the final user-visible barrier before editing search.
-    await page.getByText('1 session', { exact: true }).waitFor({ timeout: 30_000 })
-    const search = page.getByPlaceholder('Search name, keywords', { exact: false })
+    // seeded Ungrouped bucket row is the final user-visible barrier before
+    // editing search (the compact layout dropped group session counts).
+    await page.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
+    // Search is a collapsed header action; expand it so the input is actionable.
+    const searchButton = page.getByRole('button', { name: 'Search sessions' })
+    if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
+    const search = page.getByPlaceholder('Search sessions', { exact: false })
     // The cold row has not been opened, so only the persisted log can satisfy
     // this query. First search lazily reconciles the SQLite content index.
     await search.fill('zzzqx-no-such-session')

+ 5 - 2
apps/web/tests/pwsh-terminal.e2e.ts

@@ -68,8 +68,11 @@ describe.skipIf(MODE === 'record' || !HAS_PWSH)('web e2e: pwsh calls use the bas
     onTestFailed(() => saveFailureShot(page, 'web-e2e-pwsh-terminal'))
     // Open the seeded session through content search: the sidebar groups
     // sessions by workspace and its row order is world-dependent, while the
-    // search index covers the seeded log deterministically.
-    const search = page.getByPlaceholder('Search name, keywords', { exact: false })
+    // search index covers the seeded log deterministically. Search is a
+    // collapsed header action; expand it so the input is actionable.
+    const searchButton = page.getByRole('button', { name: 'Search sessions' })
+    if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
+    const search = page.getByPlaceholder('Search sessions', { exact: false })
     await search.fill('Run a PowerShell command')
     const result = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem')
     await expect.poll(() => result.count(), { timeout: 15_000 }).toBe(1)

+ 7 - 2
apps/web/tests/trajectory-virtualization.e2e.ts

@@ -59,7 +59,10 @@ interface RowAnchor {
 }
 
 async function openSeed(page: Page): Promise<void> {
-  const search = page.getByRole('textbox', { name: 'Search name, keywords...', exact: true })
+  // Search collapsed into a header action; expand it before filling.
+  const searchButton = page.getByRole('button', { name: 'Search sessions' })
+  if (await searchButton.getAttribute('aria-expanded') !== 'true') await searchButton.click()
+  const search = page.getByRole('textbox', { name: 'Search sessions...', exact: true })
   await search.fill(FIXTURE.markers.user(1))
   const result = page.getByRole('tree', { name: 'Search results' }).getByRole('treeitem')
   await expect.poll(() => result.count(), { timeout: 60_000 }).toBe(1)
@@ -182,7 +185,9 @@ describe('web e2e: Trajectory virtualization over tail-paged history', () => {
     tripwire = watchConsole(page)
     await page.goto(scaffold.baseUrl, { waitUntil: 'load' })
     await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
-    await page.getByText('1 session', { exact: true }).waitFor({ timeout: 30_000 })
+    // The compact layout dropped group session counts; the seeded baseline is
+    // the Ungrouped bucket once cold summaries load.
+    await page.getByText('Ungrouped', { exact: true }).waitFor({ timeout: 30_000 })
   }, 120_000)
 
   afterAll(async () => {

+ 4 - 3
apps/web/tests/workspace-management.e2e.ts

@@ -372,21 +372,22 @@ describe('web e2e: workspace management (create / rename / flat view / hover aff
     // Grouped default: workspace group rows render (the seeded session sits
     // under Ungrouped; the created workspaces are empty groups).
     await expect.poll(() => page.getByText('Workspaces', { exact: true }).count(), { timeout: 10_000 }).toBe(1)
-    await page.getByRole('button', { name: 'Group by' }).click()
+    // Grouping and ordering moved into the View options menu.
+    await page.getByRole('button', { name: 'View options' }).click()
     await page.getByRole('menuitem', { name: 'In one list' }).click()
     // Flat mode: the section label flips and the seeded session is a
     // top-level row with no group headers above it.
     await expect.poll(() => page.getByText('Sessions', { exact: true }).count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1)
     await expect.poll(() => page.getByText('Ungrouped', { exact: true }).count(), { timeout: 5_000 }).toBe(0)
     await expect.poll(() => page.locator('[role="treeitem"]').count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1)
-    expect(await page.evaluate(() => localStorage.getItem('dsh.workspace.view'))).toContain('flat')
+    expect(await page.evaluate(() => localStorage.getItem('dsh.workspace.view.v4'))).toContain('flat')
     // Persisted across reload; then restore grouped for inter-spec hygiene.
     const warningStart = tripwire.warnings.length
     await page.reload({ waitUntil: 'load' })
     await page.waitForSelector('[class*="frame"]', { timeout: 30_000 })
     acknowledgeReloadConnectionLoss(tripwire, warningStart)
     await expect.poll(() => page.getByText('Ungrouped', { exact: true }).count(), { timeout: 15_000 }).toBe(0)
-    await page.getByRole('button', { name: 'Group by' }).click()
+    await page.getByRole('button', { name: 'View options' }).click()
     await page.getByRole('menuitem', { name: 'WorkSpace' }).click()
     await expect.poll(() => page.getByText('Ungrouped', { exact: true }).count(), { timeout: 10_000 }).toBeGreaterThanOrEqual(1)
     expect(tripwire.pageErrors).toEqual([])