You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@maka.apache.org by as...@apache.org on 2026/10/12 04:22:50 UTC

(maka) branch main updated: fix(desktop): report browser toolbar action failures (#5955)

This is an automated email from the ASF dual-hosted git repository.

Astro-Han pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/maka.git


The following commit(s) were added to refs/heads/main by this push:
     new 27a9d0512b fix(desktop): report browser toolbar action failures (#5955)
27a9d0512b is described below

commit 27a9d0512bdc0a822a24481b64102c83c7b3eb2e
Author: Dan Martin <93...@users.noreply.github.com>
AuthorDate: Mon Oct 12 12:22:44 2026 +0800

    fix(desktop): report browser toolbar action failures (#5955)
    
    Handle rejected back, forward, reload, stop, and close requests with localized feedback scoped to the mounted session. Cover failures and late responses in component tests and show the reload failure in Storybook.
    
    Generated-by: OpenAI Codex
---
 .../src/main/__tests__/browser-panel.test.ts       | 74 +++++++++++++++++++++-
 .../workbar/tools/browser/browser-panel.tsx        | 19 ++++--
 apps/desktop/src/renderer/locales/browser-copy.ts  |  8 +++
 apps/desktop/stories/session-workbar.stories.tsx   | 18 +++++-
 4 files changed, 112 insertions(+), 7 deletions(-)

diff --git a/apps/desktop/src/main/__tests__/browser-panel.test.ts b/apps/desktop/src/main/__tests__/browser-panel.test.ts
index cc77b29cf4..e48203eb68 100644
--- a/apps/desktop/src/main/__tests__/browser-panel.test.ts
+++ b/apps/desktop/src/main/__tests__/browser-panel.test.ts
@@ -35,6 +35,13 @@ const empty: BrowserState = {
   hasPage: false, secure: false, loadError: null,
 };
 const failed: BrowserState = { ...empty, loadError: { url: 'https://example.test/failed', code: -105 } };
+const toolbarActions = [
+  { method: 'back', label: 'backAria' },
+  { method: 'forward', label: 'forwardAria' },
+  { method: 'reload', label: 'refreshAria' },
+  { method: 'stop', label: 'stopAria' },
+  { method: 'close', label: 'closeAria' },
+] as const;
 let root: Root | undefined;
 let restore: (() => void) | undefined;
 
@@ -74,11 +81,11 @@ function setup() {
   return container;
 }
 
-function render(services: WorkbarServices, sessionId = 'a', hidden = false, locale: 'en' | 'zh-CN' | 'zh-TW' = 'en') {
+function render(services: WorkbarServices, sessionId = 'a', hidden = false, locale: 'en' | 'zh-CN' | 'zh-TW' = 'en', showPanel = true) {
   root!.render(createElement(LocaleProvider, {
     locale,
     children: createElement(ToastProvider, {
-      children: createElement(WorkbarServicesProvider, { services }, createElement(BrowserPanel, { sessionId, hidden })),
+      children: createElement(WorkbarServicesProvider, { services }, showPanel ? createElement(BrowserPanel, { sessionId, hidden }) : null),
     }),
   }));
 }
@@ -158,3 +165,66 @@ test('hiding and switching sessions cannot leak an old failure; showing reseeds
   await act(async () => render(services, 'a'));
   assert.ok(container.querySelector('[role="alert"]'));
 });
+
+for (const action of toolbarActions) {
+  function actionServices(perform: (sessionId: string) => Promise<void>) {
+    const defaults = createFakeWorkbarServices();
+    return createFakeWorkbarServices({ browser: {
+      ...defaults.browser,
+      getState: async () => ({
+        ...empty, url: 'https://example.test/', hasPage: true, secure: true,
+        canGoBack: true, canGoForward: true, loading: action.method === 'stop',
+      }),
+      [action.method]: perform,
+    } });
+  }
+
+  for (const locale of ['en', 'zh-CN', 'zh-TW'] as const) {
+    test(`${action.method} reports one localized failure for its session in ${locale}`, async () => {
+      const container = setup();
+      const calls: string[] = [];
+      const services = actionServices(async (sessionId) => {
+        calls.push(sessionId);
+        throw new Error('private transport details');
+      });
+      await act(async () => render(services, 'a', false, locale));
+      const copy = getBrowserCopy(locale);
+      const button = container.querySelector<HTMLButtonElement>(`button[aria-label="${copy[action.label]}"]`);
+      assert.ok(button);
+      await act(async () => button.click());
+      assert.deepEqual(calls, ['a']);
+      const text = container.ownerDocument.body.textContent ?? '';
+      assert.equal(text.split(copy.actionFailed).length - 1, 1);
+      assert.ok(text.includes(copy.actionFailedDetail));
+      assert.ok(!text.includes('private transport details'));
+    });
+  }
+
+  for (const transition of ['success', 'switch-session', 'unmount'] as const) {
+    test(`${action.method} stays quiet after ${transition}`, async () => {
+      const container = setup();
+      const completion = deferred<void>();
+      const calls: string[] = [];
+      const services = actionServices((sessionId) => {
+        calls.push(sessionId);
+        return completion.promise;
+      });
+      await act(async () => render(services));
+      const copy = getBrowserCopy('en');
+      const button = container.querySelector<HTMLButtonElement>(`button[aria-label="${copy[action.label]}"]`);
+      assert.ok(button);
+      await act(async () => button.click());
+      assert.deepEqual(calls, ['a']);
+      if (transition === 'switch-session') await act(async () => render(services, 'b'));
+      if (transition === 'unmount') {
+        // Keep the toast provider alive so a late notification remains observable.
+        await act(async () => render(services, 'a', false, 'en', false));
+      }
+      await act(async () => {
+        if (transition === 'success') completion.resolve();
+        else completion.reject(new Error('late transport failure'));
+      });
+      assert.ok(!container.ownerDocument.body.textContent?.includes(copy.actionFailed));
+    });
+  }
+}
diff --git a/apps/desktop/src/renderer/features/workbar/tools/browser/browser-panel.tsx b/apps/desktop/src/renderer/features/workbar/tools/browser/browser-panel.tsx
index e8913b5869..2dd9617471 100644
--- a/apps/desktop/src/renderer/features/workbar/tools/browser/browser-panel.tsx
+++ b/apps/desktop/src/renderer/features/workbar/tools/browser/browser-panel.tsx
@@ -225,6 +225,17 @@ export function BrowserPanel(props: { sessionId: string; hidden: boolean; focuse
       }
     });
   };
+
+  const runToolbarAction = async (action: 'back' | 'forward' | 'reload' | 'stop' | 'close') => {
+    const ownerSessionId = sessionId;
+    try {
+      await browser[action](ownerSessionId);
+    } catch {
+      if (isBrowserPanelSessionCurrent(ownerSessionId)) {
+        toast.error(copy.actionFailed, copy.actionFailedDetail, undefined, { sessionId: ownerSessionId });
+      }
+    }
+  };
   const liveAddress = state.loadError?.url ?? state.url;
 
   return (
@@ -254,7 +265,7 @@ export function BrowserPanel(props: { sessionId: string; hidden: boolean; focuse
                 variant="ghost"
                 size="sm"
                 isDisabled={!state.canGoBack}
-                onClick={() => void browser.back(sessionId)}
+                onClick={() => void runToolbarAction('back')}
               />
             </Tooltip>
             <Tooltip content={copy.forward}>
@@ -264,7 +275,7 @@ export function BrowserPanel(props: { sessionId: string; hidden: boolean; focuse
                 variant="ghost"
                 size="sm"
                 isDisabled={!state.canGoForward}
-                onClick={() => void browser.forward(sessionId)}
+                onClick={() => void runToolbarAction('forward')}
               />
             </Tooltip>
             <Tooltip content={state.loading ? copy.stop : copy.refresh}>
@@ -275,7 +286,7 @@ export function BrowserPanel(props: { sessionId: string; hidden: boolean; focuse
                 size="sm"
                 isDisabled={!state.hasPage && !state.loading && !state.loadError}
                 onClick={() =>
-                  state.loading ? void browser.stop(sessionId) : void browser.reload(sessionId)
+                  void runToolbarAction(state.loading ? 'stop' : 'reload')
                 }
               />
             </Tooltip>
@@ -333,7 +344,7 @@ export function BrowserPanel(props: { sessionId: string; hidden: boolean; focuse
               icon={<X size={ICON_SIZE.chrome} aria-hidden />}
               variant="ghost"
               size="sm"
-              onClick={() => { props.onPreviewExit?.(); void browser.close(sessionId); }}
+              onClick={() => { props.onPreviewExit?.(); void runToolbarAction('close'); }}
             />
           </Tooltip>
           </div>
diff --git a/apps/desktop/src/renderer/locales/browser-copy.ts b/apps/desktop/src/renderer/locales/browser-copy.ts
index e7594a2995..c2ac816479 100644
--- a/apps/desktop/src/renderer/locales/browser-copy.ts
+++ b/apps/desktop/src/renderer/locales/browser-copy.ts
@@ -25,6 +25,8 @@ export type BrowserCopy = {
   openFailed: string;
   navigationFailed: string;
   navigationFailedDetail: string;
+  actionFailed: string;
+  actionFailedDetail: string;
   loadFailed: string;
   retry: string;
   retryAria: string;
@@ -65,6 +67,8 @@ const BROWSER_COPY = {
     openFailed: '无法打开地址',
     navigationFailed: '浏览器导航失败',
     navigationFailedDetail: '页面暂时无法打开,请稍后重试。',
+    actionFailed: '浏览器操作失败',
+    actionFailedDetail: '无法完成此操作,请重试。',
     loadFailed: '页面加载失败',
     retry: '重试',
     retryAria: '重试加载页面',
@@ -103,6 +107,8 @@ const BROWSER_COPY = {
     openFailed: '無法開啟地址',
     navigationFailed: '瀏覽器導航失敗',
     navigationFailedDetail: '頁面暫時無法開啟,請稍後重試。',
+    actionFailed: '瀏覽器操作失敗',
+    actionFailedDetail: '無法完成此操作,請重試。',
     loadFailed: '頁面載入失敗',
     retry: '重試',
     retryAria: '重試載入頁面',
@@ -141,6 +147,8 @@ const BROWSER_COPY = {
     openFailed: 'Could not open address',
     navigationFailed: 'Browser navigation failed',
     navigationFailedDetail: 'The page could not be opened. Try again later.',
+    actionFailed: 'Browser action failed',
+    actionFailedDetail: 'The action could not be completed. Try again.',
     loadFailed: 'Page failed to load',
     retry: 'Retry',
     retryAria: 'Retry loading page',
diff --git a/apps/desktop/stories/session-workbar.stories.tsx b/apps/desktop/stories/session-workbar.stories.tsx
index 3f8648b24a..80f0977114 100644
--- a/apps/desktop/stories/session-workbar.stories.tsx
+++ b/apps/desktop/stories/session-workbar.stories.tsx
@@ -888,6 +888,8 @@ function bridge(options: {
   popupMenu?: WorkbarServices['popupMenu'];
   /** Make `browser.navigate` reject, so a valid address surfaces the navigation-failed toast. */
   browserNavigateFails?: boolean;
+  /** Make `browser.reload` reject, so the toolbar shows the action-failed toast. */
+  browserReloadFails?: boolean;
   /** The git-review read result the 变更 panel receives (empty / source error / truncated / edge diffs). */
   review?: GitReviewReadResult;
   /** Make `review.read` reject, so the panel shows its load-error banner. */
@@ -984,7 +986,9 @@ function bridge(options: {
       },
       back: async () => undefined,
       forward: async () => undefined,
-      reload: async () => undefined,
+      reload: async () => {
+        if (options.browserReloadFails) throw new Error('reload failed');
+      },
       stop: async () => undefined,
       close: async () => undefined,
       getState: async () => browserState,
@@ -1843,6 +1847,18 @@ export const BrowserNavigationFailed: Story = {
   },
 };
 
+// Real path: session → Browser → reload; a rejected Desktop action leaves the
+// page visible and reports a localized failure through the existing toast.
+export const BrowserToolbarActionFailed: Story = {
+  decorators: [bridge({ browserState: LOADED_BROWSER_STATE, browserReloadFails: true })],
+  render: () => <Workbar tab="browser" />,
+  play: async ({ canvasElement }) => {
+    const reload = await within(canvasElement).findByRole('button', { name: '刷新页面' });
+    await userEvent.click(reload);
+    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


---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@maka.apache.org
For additional commands, e-mail: commits-help@maka.apache.org