锁定移动壳外链导航失败响应

移动壳外链原生探测失败返回稳定不可打开结果

移动壳外链原生打开失败返回稳定不可打开结果

移动壳 HostBridge 外链异常返回稳定错误

移动壳配置检查补充外链原生失败边界反查
This commit is contained in:
2026-06-20 14:47:22 +08:00
parent bdee19b510
commit 6b2a18416a
6 changed files with 121 additions and 13 deletions
+10 -6
View File
@@ -450,17 +450,18 @@ describe('ShellApp HostBridge event injection', () => {
warnSpy.mockRestore();
});
test('external WebView navigation open failure is logged instead of hidden', async () => {
test('external WebView navigation native failures stay outside the WebView', async () => {
const ShellApp = await importShellApp();
render(<ShellApp />);
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);
vi.mocked(Linking.openURL).mockRejectedValueOnce(
new Error('system browser unavailable'),
);
expect(
webViewProps.onShouldStartLoadWithRequest?.({
@@ -469,11 +470,14 @@ describe('ShellApp HostBridge event injection', () => {
).toBe(false);
await waitFor(() => {
expect(warnSpy).toHaveBeenCalledWith(
'mobile shell navigation failed for external_navigation.open',
navigationError,
expect(Linking.openURL).toHaveBeenCalledWith(
'https://outside.example/work/1',
);
});
expect(warnSpy).not.toHaveBeenCalledWith(
'mobile shell navigation failed for external_navigation.open',
expect.any(Error),
);
warnSpy.mockRestore();
});
@@ -2,6 +2,7 @@ import { describe, expect, test, vi } from 'vitest';
import { HOST_BRIDGE_EXTERNAL_URL_PROTOCOLS } from '../../../../packages/shared/src/contracts/hostBridge';
import {
type MobileShellExternalNavigator,
openMobileShellExternalNavigation,
resolveMobileShellExternalUrl,
resolveMobileShellWebViewUrl,
@@ -100,6 +101,30 @@ describe('shouldOpenInMobileShellWebView', () => {
expect(navigator.openURL).not.toHaveBeenCalled();
});
test('WebView 外链原生探测或打开失败时留在壳内', async () => {
const navigator: MobileShellExternalNavigator = {
canOpenURL: vi.fn(async () => {
throw new Error('native canOpenURL failed');
}),
openURL: vi.fn(async () => undefined),
};
await expect(
openMobileShellExternalNavigation(navigator, 'https://example.com/path'),
).resolves.toBe(false);
expect(navigator.openURL).not.toHaveBeenCalled();
vi.mocked(navigator.canOpenURL).mockReset();
vi.mocked(navigator.canOpenURL).mockResolvedValue(true);
vi.mocked(navigator.openURL).mockRejectedValueOnce(
new Error('native openURL failed'),
);
await expect(
openMobileShellExternalNavigation(navigator, 'https://example.com/path'),
).resolves.toBe(false);
});
test('HostBridge 主动导航只解析同源网页目标', () => {
const allowedOrigin = 'https://app.genarrative.world';
+11 -3
View File
@@ -42,12 +42,20 @@ export async function openMobileShellExternalNavigation(
rawUrl: string,
) {
const externalUrl = resolveMobileShellExternalUrl(rawUrl);
if (!externalUrl || !(await navigator.canOpenURL(externalUrl))) {
if (!externalUrl) {
return false;
}
await navigator.openURL(externalUrl);
return true;
try {
if (!(await navigator.canOpenURL(externalUrl))) {
return false;
}
await navigator.openURL(externalUrl);
return true;
} catch {
return false;
}
}
export function shouldAcceptMobileShellHostBridgeMessage(