收紧移动壳能力声明与迟到响应边界

移动壳卸载后丢弃迟到 HostBridge 响应

移动壳 ShellApp 测试覆盖卸载后异步响应不注入

移动壳配置检查拒绝只返回 unsupported 的声明能力

共享决策记录补充移动壳消息注入和能力声明约定
This commit is contained in:
2026-06-20 16:02:47 +08:00
parent 4f95e29895
commit e892113060
4 changed files with 103 additions and 1 deletions
@@ -368,6 +368,60 @@ describe('ShellApp QR scanner HostBridge flow', () => {
warnSpy.mockRestore();
});
test('drops delayed HostBridge responses after shell unmount', async () => {
const ShellApp = await importShellApp();
let resolveNetworkStatus:
(state: Awaited<ReturnType<typeof Network.getNetworkStateAsync>>) => void =
() => {
throw new Error('network status resolver missing');
};
vi.mocked(Network.getNetworkStateAsync).mockReturnValueOnce(
new Promise((resolve) => {
resolveNetworkStatus = resolve;
}),
);
const { unmount } = render(<ShellApp />);
const webViewProps = shellHarness.webViewProps.current as {
onMessage?: (event: {
nativeEvent: {
data: string;
url: string;
};
}) => void;
source?: { uri?: string };
};
const webViewUrl = webViewProps.source?.uri ?? 'https://app.genarrative.world/';
webViewProps.onMessage?.({
nativeEvent: {
data: JSON.stringify({
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
id: 'network-request-1',
method: 'network.status',
}),
url: webViewUrl,
},
});
unmount();
resolveNetworkStatus({
isConnected: true,
isInternetReachable: true,
type: Network.NetworkStateType.WIFI,
});
await waitFor(() => {
expect(Network.getNetworkStateAsync).toHaveBeenCalled();
});
expect(
shellHarness.injectedScripts.some((script) =>
script.includes('network-request-1'),
),
).toBe(false);
});
});
describe('ShellApp HostBridge event injection', () => {
+12
View File
@@ -89,6 +89,7 @@ type MobileWebViewHttpErrorEvent = {
export default function ShellApp() {
const webViewRef = useRef<WebView>(null);
const isShellMountedRef = useRef(true);
const nativeCanGoBackRef = useRef(false);
const h5CanGoBackRef = useRef(false);
const [canGoBack, setCanGoBack] = useState(false);
@@ -112,11 +113,22 @@ export default function ShellApp() {
const reloadCurrentWebView = useCallback(() => {
webViewRef.current?.reload();
}, []);
useEffect(() => {
isShellMountedRef.current = true;
return () => {
isShellMountedRef.current = false;
};
}, []);
const injectHostBridgeMessage = useCallback(
(
message: unknown,
onError: (error: unknown) => void,
) => {
if (!isShellMountedRef.current) {
return;
}
try {
webViewRef.current?.injectJavaScript(
buildHostBridgeMessageScript(message),