From 836e1745b70b56a0fe77ff53afa4b4cb18b523ed Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 15 Jul 2026 19:35:27 +0800 Subject: [PATCH 1/6] test(web-shell): add a mermaid diagram visual scenario MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `mermaid diagram` scenario to the visuals suite so the preview covers the Mermaid rendering surface — an assistant message with a mermaid fenced flowchart. It renders the real MermaidBlock (async mermaid import, injected ) in light and dark, waiting on the rendered SVG so the capture is never the "rendering…" placeholder. This is the surface the diagram zoom/pan work (#6881) enriches, so once the before/after preview lands it gives that PR a real before/after target instead of an unrelated canned screenshot. --- .../client/e2e/visuals/screenshots.spec.ts | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts index 50a5fa6c265..c10e0f73e55 100644 --- a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts +++ b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts @@ -60,6 +60,45 @@ for (const theme of THEMES) { await captureScreenshot(page, `session-transcript-${theme}`); }); + test(`mermaid diagram`, async ({ page }, testInfo) => { + const scenario = createWebShellDaemonScenario({ + events: [ + userTextEvent('Diagram the tool-approval flow so I can review it.', { + id: 1, + }), + assistantTextEvent( + 'Here is the tool-approval flow:\n\n' + + '```mermaid\n' + + 'flowchart LR\n' + + ' A[Tool call] --> B{Folder trusted?}\n' + + ' B -->|Yes| C[Run immediately]\n' + + ' B -->|No| D{Ask approval}\n' + + ' D -->|Approve| C\n' + + ' D -->|Reject| E[Cancel turn]\n' + + ' C --> F[Return result]\n' + + '```', + { id: 2 }, + ), + turnCompleteEvent('prompt-mermaid', { id: 3 }), + ], + }); + const daemon = await installScenario( + page, + scenario, + resolveBaseURL(testInfo), + ); + await gotoSession(page, scenario, daemon, theme); + + // MermaidBlock lazy-imports `mermaid` and renders asynchronously (behind a + // ~150ms timer), swapping a "rendering…" placeholder for the injected + // ``. Wait for that SVG so the diagram is captured + // rendered — not the placeholder — on every run. + await expect( + page.locator('[data-web-shell-message-list] svg[id^="mermaid-"]'), + ).toBeVisible(); + await captureScreenshot(page, `mermaid-diagram-${theme}`); + }); + test(`slash menu`, async ({ page }, testInfo) => { const scenario = createWebShellDaemonScenario(); const daemon = await installScenario( From 90414c130c36b7aea6d9dd96c90091e5dbc86c53 Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 15 Jul 2026 21:17:52 +0800 Subject: [PATCH 2/6] test(web-shell): add a split-view (+ maximize) visual scenario Add a `split view` scenario: enter the two-pane split via the `?split=a,b` deep link, then maximize one pane (#6951). Captures the tiled state (both panes, with the maximize controls) and the maximized state (one pane filling, restore control) in light and dark, driving the real SplitView against the mock daemon serving two sessions. --- .../client/e2e/visuals/screenshots.spec.ts | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts index c10e0f73e55..c039ce4d69a 100644 --- a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts +++ b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts @@ -14,6 +14,7 @@ import { } from '../utils/mockDaemon'; import { captureScreenshot, + completeReplay, fillComposer, gotoSession, installScenario, @@ -99,6 +100,53 @@ for (const theme of THEMES) { await captureScreenshot(page, `mermaid-diagram-${theme}`); }); + test(`split view`, async ({ page }, testInfo) => { + const scenario = createWebShellDaemonScenario({ + events: [ + userTextEvent('Review two sessions side by side.', { id: 1 }), + assistantTextEvent('Here is the first pane of the split.', { id: 2 }), + turnCompleteEvent('prompt-split', { id: 3 }), + ], + }); + const daemon = await installScenario( + page, + scenario, + resolveBaseURL(testInfo), + ); + // Load the primary session (this also primes the theme), then enter the + // split via the `?split=a,b` deep link so two panes render side by side. + await gotoSession(page, scenario, daemon, theme); + await page.goto( + `/session/${encodeURIComponent(scenario.sessionId)}` + + `?split=${encodeURIComponent(scenario.sessionId)},previous-session` + + `&theme=${theme}`, + ); + await expect(page.locator('[data-testid="split-view"]')).toBeVisible(); + // Both panes reconnect on the split navigation; settle each replay so + // neither pane is stuck on the loading state. + await completeReplay( + page, + daemon, + scenario.sessionId, + scenario.events.length, + ); + await completeReplay(page, daemon, 'previous-session', 0); + // The maximize control only appears with 2+ panes (#6951); waiting on it + // confirms the split actually rendered both panes. + await expect( + page.getByRole('button', { name: 'Maximize pane' }).first(), + ).toBeVisible(); + await captureScreenshot(page, `split-view-${theme}`); + + // Maximize the first pane (#6951): it fills the split and the other pane + // hides; the button flips to "Restore pane". + await page.getByRole('button', { name: 'Maximize pane' }).first().click(); + await expect( + page.getByRole('button', { name: 'Restore pane' }), + ).toBeVisible(); + await captureScreenshot(page, `split-view-maximized-${theme}`); + }); + test(`slash menu`, async ({ page }, testInfo) => { const scenario = createWebShellDaemonScenario(); const daemon = await installScenario( From 34bb2915ca5d0e530d49b1514e0a686b76eb2977 Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 15 Jul 2026 21:51:32 +0800 Subject: [PATCH 3/6] test(web-shell): add a sidebar attention-badge visual scenario MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a `sidebar attention` scenario: four sessions in distinct states — waiting-on-permission, waiting-on-user-question, running, idle — so the sidebar renders #6956's "Waiting for approval" / "User input needed" attention pills. Renders in light and dark; asserts on session names (present with or without the pills) so the frame is the same shape on main and the PR, letting the before/after preview surface the pills. --- .../client/e2e/visuals/screenshots.spec.ts | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts index c039ce4d69a..13be63c3cd2 100644 --- a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts +++ b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts @@ -147,6 +147,62 @@ for (const theme of THEMES) { await captureScreenshot(page, `split-view-maximized-${theme}`); }); + test(`sidebar attention`, async ({ page }, testInfo) => { + const stamp = '2026-07-03T00:00:00.000Z'; + const base = { + workspaceCwd: '/tmp/qwen-web-shell-e2e', + createdAt: stamp, + updatedAt: stamp, + clientCount: 1, + }; + const scenario = createWebShellDaemonScenario({ + sessionId: 'sess-running', + displayName: 'Run test suite', + sessions: [ + { + ...base, + sessionId: 'sess-approval', + displayName: 'Deploy to staging', + hasActivePrompt: true, + isWaitingForPermission: true, + }, + { + ...base, + sessionId: 'sess-question', + displayName: 'Refactor auth module', + hasActivePrompt: true, + isWaitingForUserQuestion: true, + }, + { + ...base, + sessionId: 'sess-running', + displayName: 'Run test suite', + hasActivePrompt: true, + }, + { + ...base, + sessionId: 'sess-idle', + displayName: 'Draft release notes', + clientCount: 0, + hasActivePrompt: false, + }, + ], + }); + const daemon = await installScenario( + page, + scenario, + resolveBaseURL(testInfo), + ); + await gotoSession(page, scenario, daemon, theme); + // The sidebar lists every session; #6956 adds an attention pill to the + // ones waiting on the user. Assert on session names (present on both + // `main` and the PR) so the frame is the same shape either way — the pill + // itself is the PR's diff that the before/after preview surfaces. + await expect(page.getByText('Deploy to staging')).toBeVisible(); + await expect(page.getByText('Refactor auth module')).toBeVisible(); + await captureScreenshot(page, `sidebar-attention-${theme}`); + }); + test(`slash menu`, async ({ page }, testInfo) => { const scenario = createWebShellDaemonScenario(); const daemon = await installScenario( From 9900089d6864134bbecca509245ab05ee7c601c5 Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 15 Jul 2026 23:13:07 +0800 Subject: [PATCH 4/6] test(web-shell): derive the split view's second session from the scenario list Addresses a review suggestion: the split view test hardcoded the 'previous-session' id, which only worked because it is in createWebShellDaemonScenario's default sessions list. Derive the second pane's session from the scenario's own list instead (and throw a clear error if absent), so a future rename/removal of that default surfaces as a self-explaining failure rather than a confusing SSE connection timeout. --- .../client/e2e/visuals/screenshots.spec.ts | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts index 13be63c3cd2..7c605ff0491 100644 --- a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts +++ b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts @@ -108,6 +108,18 @@ for (const theme of THEMES) { turnCompleteEvent('prompt-split', { id: 3 }), ], }); + // Derive the second pane's session from the scenario's OWN sessions list + // rather than hardcoding an id: a rename/removal of the default entry + // would otherwise surface here as a confusing SSE connection timeout + // instead of a clear, self-explaining error. + const secondSessionId = scenario.sessions.find( + (s) => s.sessionId !== scenario.sessionId, + )?.sessionId; + if (!secondSessionId) { + throw new Error( + 'split view scenario expects a second session in the list', + ); + } const daemon = await installScenario( page, scenario, @@ -118,7 +130,7 @@ for (const theme of THEMES) { await gotoSession(page, scenario, daemon, theme); await page.goto( `/session/${encodeURIComponent(scenario.sessionId)}` + - `?split=${encodeURIComponent(scenario.sessionId)},previous-session` + + `?split=${encodeURIComponent(scenario.sessionId)},${encodeURIComponent(secondSessionId)}` + `&theme=${theme}`, ); await expect(page.locator('[data-testid="split-view"]')).toBeVisible(); @@ -130,7 +142,7 @@ for (const theme of THEMES) { scenario.sessionId, scenario.events.length, ); - await completeReplay(page, daemon, 'previous-session', 0); + await completeReplay(page, daemon, secondSessionId, 0); // The maximize control only appears with 2+ panes (#6951); waiting on it // confirms the split actually rendered both panes. await expect( From b80800b297cd433fb50b9a8e9685a289af5a9723 Mon Sep 17 00:00:00 2001 From: wenshao Date: Wed, 15 Jul 2026 23:13:19 +0800 Subject: [PATCH 5/6] test(web-shell): tidy split copy and mermaid width in visual scenarios Address review nits on the visual scenarios: - Split scenario: the mock replays the same events into both panes, so "Here is the first pane of the split." read wrong in the second pane. Use pane-neutral copy ("Here are the two sessions, side by side."). - Mermaid scenario: the flowchart's rightmost node clipped at the code-block edge at the 1280px capture viewport. Shorten the node labels (same nodes and flow) so the whole diagram fits with margin. Re-ran both scenarios (light + dark) locally: 4/4 pass, and confirmed in the captures that the diagram no longer clips and the neutral copy reads correctly in both panes. --- .../client/e2e/visuals/screenshots.spec.ts | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts index 7c605ff0491..f69216cb380 100644 --- a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts +++ b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts @@ -71,12 +71,12 @@ for (const theme of THEMES) { 'Here is the tool-approval flow:\n\n' + '```mermaid\n' + 'flowchart LR\n' + - ' A[Tool call] --> B{Folder trusted?}\n' + - ' B -->|Yes| C[Run immediately]\n' + - ' B -->|No| D{Ask approval}\n' + - ' D -->|Approve| C\n' + - ' D -->|Reject| E[Cancel turn]\n' + - ' C --> F[Return result]\n' + + ' A[Tool call] --> B{Trusted?}\n' + + ' B -->|Yes| C[Run]\n' + + ' B -->|No| D{Approve?}\n' + + ' D -->|Yes| C\n' + + ' D -->|No| E[Cancel]\n' + + ' C --> F[Result]\n' + '```', { id: 2 }, ), @@ -104,7 +104,12 @@ for (const theme of THEMES) { const scenario = createWebShellDaemonScenario({ events: [ userTextEvent('Review two sessions side by side.', { id: 1 }), - assistantTextEvent('Here is the first pane of the split.', { id: 2 }), + // Pane-neutral copy: the mock replays these same events into *both* + // panes, so wording that names "the first pane" would read wrong in + // the second one. + assistantTextEvent('Here are the two sessions, side by side.', { + id: 2, + }), turnCompleteEvent('prompt-split', { id: 3 }), ], }); From fe3c44ee26f8c5c8720cae75a7df84d608055525 Mon Sep 17 00:00:00 2001 From: wenshao Date: Thu, 16 Jul 2026 00:39:30 +0800 Subject: [PATCH 6/6] test(web-shell): capture split-view restore and assert all sidebar sessions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Address review nits on the visual scenarios: - Split view: after maximize, click "Restore pane" and capture the restored tiled layout, asserting the maximize control returns on both panes — so a regression in the restore path is caught, not just the tiled and maximized states. - Sidebar attention: assert all four session names render (not just the two waiting ones). The running session is also the loaded one, so its name also shows in the main view — scope the running/idle checks to the sidebar landmark so the match stays unambiguous. --- .../client/e2e/visuals/screenshots.spec.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts index f69216cb380..906d3b4a828 100644 --- a/packages/web-shell/client/e2e/visuals/screenshots.spec.ts +++ b/packages/web-shell/client/e2e/visuals/screenshots.spec.ts @@ -162,6 +162,15 @@ for (const theme of THEMES) { page.getByRole('button', { name: 'Restore pane' }), ).toBeVisible(); await captureScreenshot(page, `split-view-maximized-${theme}`); + + // Restore the tiled layout (#6951): the solo pane returns to the split and + // the hidden pane reappears — so the maximize control is back on both + // panes. Captures the restore path so a regression there is caught too. + await page.getByRole('button', { name: 'Restore pane' }).click(); + await expect( + page.getByRole('button', { name: 'Maximize pane' }).first(), + ).toBeVisible(); + await captureScreenshot(page, `split-view-restored-${theme}`); }); test(`sidebar attention`, async ({ page }, testInfo) => { @@ -217,6 +226,13 @@ for (const theme of THEMES) { // itself is the PR's diff that the before/after preview surfaces. await expect(page.getByText('Deploy to staging')).toBeVisible(); await expect(page.getByText('Refactor auth module')).toBeVisible(); + // Assert all four sessions render (not just the two waiting ones), so a + // regression that truncates the running or idle session is caught. The + // running session is also the loaded one, so its name shows in the main + // view too — scope to the sidebar landmark to keep the match unambiguous. + const sidebar = page.getByRole('complementary'); + await expect(sidebar.getByText('Run test suite')).toBeVisible(); + await expect(sidebar.getByText('Draft release notes')).toBeVisible(); await captureScreenshot(page, `sidebar-attention-${theme}`); });