diff --git a/apps/web/tests/built-boot.snapshot.ts b/apps/web/tests/built-boot.snapshot.ts index be719bda15..35a918c228 100644 --- a/apps/web/tests/built-boot.snapshot.ts +++ b/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('[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 diff --git a/apps/web/tests/chat-long-interactions.e2e.ts b/apps/web/tests/chat-long-interactions.e2e.ts index aca85146b5..242f7cb66d 100644 --- a/apps/web/tests/chat-long-interactions.e2e.ts +++ b/apps/web/tests/chat-long-interactions.e2e.ts @@ -78,8 +78,13 @@ async function nextPaint(page: Page): Promise { } async function openSeed(page: Page): Promise { - 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 }) diff --git a/apps/web/tests/chat-scroll-contract.e2e.ts b/apps/web/tests/chat-scroll-contract.e2e.ts index 0c75afb4e7..5274759f47 100644 --- a/apps/web/tests/chat-scroll-contract.e2e.ts +++ b/apps/web/tests/chat-scroll-contract.e2e.ts @@ -168,8 +168,10 @@ async function launchScrollWorld(options: ScrollWorldOptions): Promise { } async function openSeed(page: Page, fixture: ChatScrollFixture, tailMarker?: string): Promise { - 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 diff --git a/apps/web/tests/composer-tab-geometry.e2e.ts b/apps/web/tests/composer-tab-geometry.e2e.ts index 26077f6eab..bb43e20406 100644 --- a/apps/web/tests/composer-tab-geometry.e2e.ts +++ b/apps/web/tests/composer-tab-geometry.e2e.ts @@ -249,7 +249,10 @@ async function compareTabsWithoutReservation(page: Page): Promise * @param page - the page under test. */ async function openSeededSession(page: Page): Promise { - 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 diff --git a/apps/web/tests/lifecycle-chrome.e2e.ts b/apps/web/tests/lifecycle-chrome.e2e.ts index 0c7dd36fe3..15b3533070 100644 --- a/apps/web/tests/lifecycle-chrome.e2e.ts +++ b/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 diff --git a/apps/web/tests/navigation-panes.e2e.ts b/apps/web/tests/navigation-panes.e2e.ts index c8b6455296..dda4ece74f 100644 --- a/apps/web/tests/navigation-panes.e2e.ts +++ b/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 { 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') diff --git a/apps/web/tests/pwsh-terminal.e2e.ts b/apps/web/tests/pwsh-terminal.e2e.ts index 85b52a734f..44912d9ca5 100644 --- a/apps/web/tests/pwsh-terminal.e2e.ts +++ b/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) diff --git a/apps/web/tests/trajectory-virtualization.e2e.ts b/apps/web/tests/trajectory-virtualization.e2e.ts index 287d38c29d..64f149646b 100644 --- a/apps/web/tests/trajectory-virtualization.e2e.ts +++ b/apps/web/tests/trajectory-virtualization.e2e.ts @@ -59,7 +59,10 @@ interface RowAnchor { } async function openSeed(page: Page): Promise { - 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 () => { diff --git a/apps/web/tests/workspace-management.e2e.ts b/apps/web/tests/workspace-management.e2e.ts index a435592da5..e5a25e1c6c 100644 --- a/apps/web/tests/workspace-management.e2e.ts +++ b/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([])