From 577e2b4c82d0b5a612eb8d9844a9d3be5a5738a1 Mon Sep 17 00:00:00 2001 From: Xiao Liu Date: Sun, 30 Aug 2026 05:37:33 +0800 Subject: [PATCH] test(desktop): story-cover browser panel edge and navigation-failure states MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ninth surface under #3944 (one surface per PR): extend the 浏览器 panel (via Product/Session Workbar) with the reachable states the existing Browser* stories never show. BrowserState carries no error/crashed field — a failed load re-emits the same seven fields — so the panel has no error render branch; its only real failure affordances are the two navigation toasts. - BrowserCanGoForward — the one nav-control combination never shown: forward enabled, not just back. - BrowserLongUrl — a very long URL + title at the 320px column floor, so the address field keeps the URL legible. - BrowserInvalidAddress — a non-HTTP(S) address is rejected before navigating and raises the "无法打开地址" toast. - BrowserNavigationFailed — a valid address whose navigate rejects raises the "浏览器导航失败" toast. `bridge()` gains a `browserNavigateFails` option (navigate rejects) in the same idiom as the existing task/review overrides. No error/crashed/blocked/offline render state exists on this surface, so none is faked. Refs #3944, #3893 Generated-by: Claude Code --- .../stories/session-workbar.stories.tsx | 72 ++++++++++++++++++- 1 file changed, 70 insertions(+), 2 deletions(-) diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx index a46d591ace..9db999f4f7 100644 --- a/apps/desktop/stories/session-workbar.stories.tsx +++ b/apps/desktop/stories/session-workbar.stories.tsx @@ -19,7 +19,7 @@ import type { CSSProperties } from 'react'; import type { Decorator, Meta, StoryObj } from '@storybook/react-vite'; -import { expect, waitFor } from 'storybook/test'; +import { expect, userEvent, waitFor, within } from 'storybook/test'; import type { ArtifactRecord } from '@maka/core/artifacts'; import type { BrowserState } from '@maka/core/browser'; import type { GitReviewSnapshot } from '@maka/core/git-review'; @@ -90,6 +90,13 @@ const LOADED_BROWSER_STATE: BrowserState = { secure: true, hasPage: true, }; +// A pathologically long address + title — the one layout edge a browser value +// can break, since the address field has to keep the URL legible at the +// column's 320px floor. +const LONG_BROWSER_URL = + 'https://maka.apache.org/docs/getting-started/configuration/advanced/runtime-host/agent-graph/scheduling/readiness-and-activation/edge-cases?highlight=very-long-query-string-that-keeps-going#a-deep-anchor-that-also-runs-long'; +const LONG_BROWSER_TITLE = + 'Getting started · Configuration · Advanced · Runtime Host · Agent Graph scheduling, readiness, and activation edge cases — Apache Maka documentation'; const TOOL_PICKER_SOURCE_SESSION: SessionSummary = { id: SESSION_ID, name: '工作栏组件审查', @@ -608,6 +615,8 @@ function bridge(options: { /** The context snapshot the composition block reads (#2323). */ context?: ContextDiagnosticsResult; browserState?: BrowserState; + /** Make `browser.navigate` reject, so a valid address surfaces the navigation-failed toast. */ + browserNavigateFails?: boolean; } = {}): Decorator { const browserState = options.browserState ?? EMPTY_BROWSER_STATE; const services = createFakeWorkbarServices({ @@ -684,7 +693,9 @@ function bridge(options: { browser: { setActiveSession: noop, setViewport: noop, - navigate: async () => undefined, + navigate: async () => { + if (options.browserNavigateFails) throw new Error('navigation failed'); + }, back: async () => undefined, forward: async () => undefined, reload: async () => undefined, @@ -911,6 +922,63 @@ export const BrowserStacked: Story = { decorators: [bridge({ browserState: LOADED_BROWSER_STATE })], render: () => , }; +// Real path: 任务工作栏 → 浏览器 mid-history — the one nav-control combination +// the other browser stories never show: forward enabled, not just back. +export const BrowserCanGoForward: Story = { + decorators: [bridge({ browserState: { ...LOADED_BROWSER_STATE, canGoForward: true } })], + render: () => , + play: async ({ canvasElement }) => { + await waitFor(() => + expect(within(canvasElement).getByRole('button', { name: '浏览器前进' })).toBeEnabled(), + ); + }, +}; + +// Real path: 任务工作栏 → 浏览器 on a page with a very long URL and title, at the +// column's 320px floor — the address field must keep the URL legible instead of +// pushing the toolbar controls out. +export const BrowserLongUrl: Story = { + decorators: [ + bridge({ + browserState: { ...LOADED_BROWSER_STATE, url: LONG_BROWSER_URL, title: LONG_BROWSER_TITLE }, + }), + ], + render: () => , + play: async ({ canvasElement }) => { + await waitFor(() => + expect(within(canvasElement).getByRole('textbox', { name: '浏览器地址' })).toHaveValue( + LONG_BROWSER_URL, + ), + ); + }, +}; + +// Real path: 任务工作栏 → 浏览器 when the typed address is not an HTTP(S) URL. The +// embedded browser rejects it before navigating and raises the "无法打开地址" +// toast — a failure a bare loaded page never shows. +export const BrowserInvalidAddress: Story = { + decorators: [bridge({ browserState: EMPTY_BROWSER_STATE })], + render: () => , + play: async ({ canvasElement }) => { + const address = await within(canvasElement).findByRole('textbox', { name: '浏览器地址' }); + await userEvent.type(address, 'mailto:team@maka.apache.org{Enter}'); + await within(document.body).findByText('无法打开地址'); + }, +}; + +// Real path: 任务工作栏 → 浏览器 when a valid address fails to load (navigate +// rejects); the panel raises the "浏览器导航失败" toast. +export const BrowserNavigationFailed: Story = { + decorators: [bridge({ browserState: LOADED_BROWSER_STATE, browserNavigateFails: true })], + render: () => , + play: async ({ canvasElement }) => { + const address = await within(canvasElement).findByRole('textbox', { name: '浏览器地址' }); + await userEvent.clear(address); + await userEvent.type(address, 'https://example.com/status{Enter}'); + await within(document.body).findByText('浏览器导航失败'); + }, +}; + // Real path: 任务工作栏 → 文件, on a session whose agent wrote artifacts. The // count in the tab is the pane's own filtered total, reported upward. // The pane's empty state renders the same EmptyState as TraceEmpty below, so it