From c2379e4d473d58040ea96e1ec5a392f67e91d1b0 Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 10:03:20 +0800 Subject: [PATCH] =?UTF-8?q?=E8=AE=B0=E5=BD=95=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E5=A4=96=E9=93=BE=E6=89=93=E5=BC=80=E5=A4=B1=E8=B4=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动 WebView 外链交给系统打开失败时记录错误 移动壳测试和配置检查拒绝静默吞掉外链失败 共享决策日志补充外链打开失败边界 --- apps/mobile-shell/scripts/check-config.mjs | 9 ++++++ apps/mobile-shell/src/shell/ShellApp.test.tsx | 28 +++++++++++++++++++ apps/mobile-shell/src/shell/ShellApp.tsx | 8 +++++- .../shared-memory/decision-log.md | 7 +++++ 4 files changed, 51 insertions(+), 1 deletion(-) diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index accbfa141..ff7e89476 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -1380,12 +1380,19 @@ for (const snippet of [ 'handleBlockedFileDownload', 'onFileDownload={handleBlockedFileDownload}', 'setSupportMultipleWindows={false}', + 'logMobileShellNavigationFailure', + 'mobile shell navigation failed for', + 'external_navigation.open', ]) { if (!shellAppSource.includes(snippet)) { throw new Error(`mobile shell ShellApp missing ${snippet}`); } } +if (shellAppSource.includes('catch(() => undefined)')) { + throw new Error('mobile shell ShellApp must not hide async navigation failures'); +} + for (const snippet of [ "describe('mobile shell safe area'", "test('protects the WebView from every device edge'", @@ -2230,6 +2237,8 @@ for (const snippet of [ 'shellHarness.networkListeners[0]?.({', "type: 'genarrative.mobile.historyState'", 'mobile host event failed for network.statusChanged', + 'external WebView navigation open failure is logged instead of hidden', + 'mobile shell navigation failed for external_navigation.open', ]) { if (!shellAppTestSource.includes(snippet)) { throw new Error(`mobile shell ShellApp HostBridge event test missing ${snippet}`); diff --git a/apps/mobile-shell/src/shell/ShellApp.test.tsx b/apps/mobile-shell/src/shell/ShellApp.test.tsx index 5b0f40abc..336db22c4 100644 --- a/apps/mobile-shell/src/shell/ShellApp.test.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.test.tsx @@ -449,4 +449,32 @@ describe('ShellApp HostBridge event injection', () => { warnSpy.mockRestore(); }); + + test('external WebView navigation open failure is logged instead of hidden', async () => { + const ShellApp = await importShellApp(); + render(); + + const webViewProps = shellHarness.webViewProps.current as { + onShouldStartLoadWithRequest?: (request: { url: string }) => boolean; + }; + const navigationError = new Error('system browser unavailable'); + const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined); + const { Linking } = await import('react-native'); + vi.mocked(Linking.openURL).mockRejectedValueOnce(navigationError); + + expect( + webViewProps.onShouldStartLoadWithRequest?.({ + url: 'https://outside.example/work/1', + }), + ).toBe(false); + + await waitFor(() => { + expect(warnSpy).toHaveBeenCalledWith( + 'mobile shell navigation failed for external_navigation.open', + navigationError, + ); + }); + + warnSpy.mockRestore(); + }); }); diff --git a/apps/mobile-shell/src/shell/ShellApp.tsx b/apps/mobile-shell/src/shell/ShellApp.tsx index 0b25ce63a..950d17095 100644 --- a/apps/mobile-shell/src/shell/ShellApp.tsx +++ b/apps/mobile-shell/src/shell/ShellApp.tsx @@ -77,6 +77,10 @@ function logMobileHostEventFailure(label: HostBridgeEventName, error: unknown) { console.warn(`mobile host event failed for ${label}`, error); } +function logMobileShellNavigationFailure(label: string, error: unknown) { + console.warn(`mobile shell navigation failed for ${label}`, error); +} + type MobileWebViewLoadErrorEvent = { nativeEvent: { url: string; @@ -275,7 +279,9 @@ export default function ShellApp() { } void openMobileShellExternalNavigation(Linking, request.url).catch( - () => undefined, + (error: unknown) => { + logMobileShellNavigationFailure('external_navigation.open', error); + }, ); return false; }; diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 993b25d7e..459772a6a 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -2944,6 +2944,13 @@ - 影响范围:`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/ShellApp.test.tsx`、`apps/mobile-shell/scripts/check-config.mjs`、宿主壳能力统一协议文档。 - 验证方式:`npm run mobile-shell:test -- src/shell/ShellApp.test.tsx`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 +## 2026-06-20 移动壳 WebView 外链打开失败不可静默 + +- 背景:Expo WebView 外域导航会离开带 HostBridge 的主站容器并交给系统浏览器或系统应用;如果 `Linking.openURL(...)` reject 后静默吞掉,用户会看到点击外链无反应且开发侧无法区分协议、系统能力或原生模块失败。 +- 决策:`ShellApp` 的 WebView 外链分流必须继续复用 `openMobileShellExternalNavigation(Linking, request.url)`,但 Promise reject 路径必须调用 `logMobileShellNavigationFailure('external_navigation.open', error)` 记录错误;配置检查拒绝 `ShellApp` 重新出现 `catch(() => undefined)`,并反查外链失败日志测试。 +- 影响范围:`apps/mobile-shell/src/shell/ShellApp.tsx`、`apps/mobile-shell/src/shell/ShellApp.test.tsx`、`apps/mobile-shell/scripts/check-config.mjs`。 +- 验证方式:`npm run mobile-shell:test -- src/shell/ShellApp.test.tsx`、`npm run mobile-shell:typecheck`、`npm run check:native-shells`、`npm run check:encoding`、`git diff --check`。 + ## 2026-06-20 移动壳生命周期映射门禁 - 背景:Expo `AppState` 的 `active`、`background`、`inactive` 以及未知状态都会进入 `app.lifecycle` 事件;如果归一化映射漂移,H5 游戏循环、背景音乐和固定玩法音频会错误恢复或暂停。