收紧移动壳事件注入失败边界
移动壳 HostBridge 事件注入捕获 WebView 同步异常并记录稳定日志 移动壳 ShellApp 测试覆盖事件注入失败不崩溃 移动壳配置检查反查事件注入失败边界 共享决策记录补充移动事件注入失败约定
This commit is contained in:
@@ -1379,6 +1379,8 @@ for (const snippet of [
|
||||
'injectLifecycleEvent',
|
||||
'injectNetworkStatusEvent',
|
||||
'logMobileHostEventFailure',
|
||||
'try {',
|
||||
'logMobileHostEventFailure(event, error)',
|
||||
"logMobileHostEventFailure('network.statusChanged', error)",
|
||||
'handleWebViewLoad',
|
||||
'onLoad={handleWebViewLoad}',
|
||||
@@ -2435,6 +2437,9 @@ for (const snippet of [
|
||||
"lastHostBridgeEvent('navigation.canGoBack')",
|
||||
"shellHarness.appStateListeners[0]?.('background')",
|
||||
'shellHarness.networkListeners[0]?.({',
|
||||
'injectJavaScriptError',
|
||||
"test('host event injection failures are logged without crashing the shell'",
|
||||
"new Error('webview injection failed')",
|
||||
"type: 'genarrative.mobile.historyState'",
|
||||
'mobile host event failed for network.statusChanged',
|
||||
'external WebView navigation native failures stay outside the WebView',
|
||||
|
||||
@@ -15,18 +15,21 @@ const shellHarness = vi.hoisted(() => {
|
||||
const appStateListeners = [] as Array<(state: string) => void>;
|
||||
const webViewProps = { current: null as Record<string, unknown> | null };
|
||||
const cameraViewProps = { current: null as Record<string, unknown> | null };
|
||||
const injectJavaScriptError = { current: null as Error | null };
|
||||
const injectedScripts = [] as string[];
|
||||
const networkListeners = [] as Array<(state: Record<string, unknown>) => void>;
|
||||
|
||||
return {
|
||||
appStateListeners,
|
||||
cameraViewProps,
|
||||
injectJavaScriptError,
|
||||
injectedScripts,
|
||||
networkListeners,
|
||||
reset() {
|
||||
appStateListeners.length = 0;
|
||||
webViewProps.current = null;
|
||||
cameraViewProps.current = null;
|
||||
injectJavaScriptError.current = null;
|
||||
injectedScripts.length = 0;
|
||||
networkListeners.length = 0;
|
||||
},
|
||||
@@ -158,6 +161,9 @@ vi.mock('react-native-webview', () => ({
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
goBack: vi.fn(),
|
||||
injectJavaScript: (script: string) => {
|
||||
if (shellHarness.injectJavaScriptError.current) {
|
||||
throw shellHarness.injectJavaScriptError.current;
|
||||
}
|
||||
shellHarness.injectedScripts.push(script);
|
||||
},
|
||||
reload: vi.fn(),
|
||||
@@ -372,6 +378,34 @@ describe('ShellApp HostBridge event injection', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('host event injection failures are logged without crashing the shell', async () => {
|
||||
const ShellApp = await importShellApp();
|
||||
render(<ShellApp />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(shellHarness.networkListeners.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
const injectionError = new Error('webview injection failed');
|
||||
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
|
||||
shellHarness.injectJavaScriptError.current = injectionError;
|
||||
|
||||
expect(() => {
|
||||
shellHarness.networkListeners[0]?.({
|
||||
isConnected: false,
|
||||
isInternetReachable: false,
|
||||
type: 'NONE',
|
||||
});
|
||||
}).not.toThrow();
|
||||
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
'mobile host event failed for network.statusChanged',
|
||||
injectionError,
|
||||
);
|
||||
|
||||
warnSpy.mockRestore();
|
||||
});
|
||||
|
||||
test('native and H5 navigation state inject combined navigation.canGoBack events', async () => {
|
||||
const ShellApp = await importShellApp();
|
||||
render(<ShellApp />);
|
||||
|
||||
@@ -124,9 +124,13 @@ export default function ShellApp() {
|
||||
}, []);
|
||||
const injectHostBridgeEvent = useCallback(
|
||||
(event: HostBridgeEventName, payload: unknown) => {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
buildHostBridgeEventScript(event, payload),
|
||||
);
|
||||
try {
|
||||
webViewRef.current?.injectJavaScript(
|
||||
buildHostBridgeEventScript(event, payload),
|
||||
);
|
||||
} catch (error) {
|
||||
logMobileHostEventFailure(event, error);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
@@ -16,6 +16,14 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-06-20 移动 HostBridge 事件注入失败边界
|
||||
|
||||
- 背景:Expo 移动壳通过 WebView `injectJavaScript` 把 `app.lifecycle`、`network.statusChanged`、`navigation.canGoBack` 等宿主事件回放给 H5;如果 WebView 进程切换、页面卸载或注入同步失败,壳层不能因为宿主事件回放异常而崩溃。
|
||||
- 决策:移动壳所有 HostBridge 事件注入必须统一经过 `injectHostBridgeEvent`,该函数捕获同步注入异常并用 `logMobileHostEventFailure(event, error)` 记录;配置检查反查运行时 try/catch 和 ShellApp 注入失败测试。
|
||||
- 影响范围:`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`。
|
||||
- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md`、`docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`。
|
||||
|
||||
## 2026-06-20 桌面图片拖拽坐标非负边界
|
||||
|
||||
- 背景:Tauri 桌面壳通过系统拖拽事件向 H5 发送 `file.imageDropped`,拖拽坐标来自窗口事件;窗口边缘或平台差异可能产生负数或小数坐标,H5 只负责校验有限 number,不负责裁剪桌面系统坐标。
|
||||
|
||||
Reference in New Issue
Block a user