diff --git a/apps/mobile-shell/scripts/check-config.mjs b/apps/mobile-shell/scripts/check-config.mjs index 7787b1905..dbfc094f2 100644 --- a/apps/mobile-shell/scripts/check-config.mjs +++ b/apps/mobile-shell/scripts/check-config.mjs @@ -2207,7 +2207,7 @@ if ( 'const clipboardText = normalizeHostBridgeClipboardText(', ) || !clipboardSource.includes('Clipboard.setStringAsync(clipboardText.text)') || - !clipboardSource.includes('await Clipboard.getStringAsync()') + !clipboardSource.includes('rawText = await Clipboard.getStringAsync()') ) { throw new Error( 'mobile shell clipboard bridge must normalize text with the shared HostBridge clipboard boundary', @@ -2236,6 +2236,8 @@ for (const snippet of [ 'writeMobileClipboardText(text)', 'readMobileHostBridgeClipboardText', 'ok(request, await readMobileClipboardText())', + "message: 'clipboard write unavailable'", + "message: 'clipboard read unavailable'", ]) { if (!clipboardSource.includes(snippet)) { throw new Error(`mobile shell clipboard module is missing ${snippet}`); @@ -2245,6 +2247,10 @@ if (!clipboardTestSource.includes("'猫'.repeat(100000)")) { throw new Error('mobile shell clipboard tests must cover Unicode truncation'); } for (const snippet of [ + 'maps native clipboard write failures to stable host errors', + "message: 'clipboard write unavailable'", + 'maps native clipboard read failures to stable host errors', + "message: 'clipboard read unavailable'", 'rejects unavailable clipboard text without reporting a successful empty value', 'Clipboard.getStringAsync).mockResolvedValue(undefined as never)', "code: 'host_error'", diff --git a/apps/mobile-shell/src/host-bridge/bridge.test.ts b/apps/mobile-shell/src/host-bridge/bridge.test.ts index 506357b0b..07d728313 100644 --- a/apps/mobile-shell/src/host-bridge/bridge.test.ts +++ b/apps/mobile-shell/src/host-bridge/bridge.test.ts @@ -587,13 +587,17 @@ describe('handleMobileHostBridgeMessage', () => { }); test('原生异常对象不会透传非协议错误码', async () => { - vi.mocked(Clipboard.getStringAsync).mockRejectedValue({ - code: 'native_clipboard_failure', - message: 'native clipboard failed', + vi.mocked(Linking.canOpenURL).mockRejectedValue({ + code: 'native_linking_failure', + message: 'native linking failed', nativeStackIOS: ['private native frame'], }); - const response = await send(request('clipboard.readText')); + const response = await send( + request('app.openExternalUrl', { + url: 'https://example.com/path', + }), + ); const failedResponse = expectFailed(response); diff --git a/apps/mobile-shell/src/host-bridge/clipboard.test.ts b/apps/mobile-shell/src/host-bridge/clipboard.test.ts index 1f1d49da4..05a5fd747 100644 --- a/apps/mobile-shell/src/host-bridge/clipboard.test.ts +++ b/apps/mobile-shell/src/host-bridge/clipboard.test.ts @@ -63,6 +63,17 @@ describe('mobile clipboard helpers', () => { expect(Clipboard.setStringAsync).not.toHaveBeenCalled(); }); + test('maps native clipboard write failures to stable host errors', async () => { + vi.mocked(Clipboard.setStringAsync).mockRejectedValueOnce( + new Error('native clipboard write failed'), + ); + + await expect(writeMobileClipboardText('作品号 PZ-1')).rejects.toMatchObject({ + code: 'host_error', + message: 'clipboard write unavailable', + }); + }); + test('wraps HostBridge clipboard write responses', async () => { const response = await writeMobileHostBridgeClipboardText( request('clipboard.writeText', { @@ -128,6 +139,17 @@ describe('mobile clipboard helpers', () => { }); }); + test('maps native clipboard read failures to stable host errors', async () => { + vi.mocked(Clipboard.getStringAsync).mockRejectedValueOnce( + new Error('native clipboard read failed'), + ); + + await expect(readMobileClipboardText()).rejects.toMatchObject({ + code: 'host_error', + message: 'clipboard read unavailable', + }); + }); + test('wraps HostBridge clipboard read responses', async () => { vi.mocked(Clipboard.getStringAsync).mockResolvedValue('作品号 PZ-1'); diff --git a/apps/mobile-shell/src/host-bridge/clipboard.ts b/apps/mobile-shell/src/host-bridge/clipboard.ts index 8b74e47ef..6aa3b1e28 100644 --- a/apps/mobile-shell/src/host-bridge/clipboard.ts +++ b/apps/mobile-shell/src/host-bridge/clipboard.ts @@ -15,7 +15,14 @@ export async function writeMobileClipboardText(rawText: unknown) { return false; } - await Clipboard.setStringAsync(clipboardText.text); + try { + await Clipboard.setStringAsync(clipboardText.text); + } catch { + throw { + code: 'host_error', + message: 'clipboard write unavailable', + } satisfies HostBridgeError; + } return true; } @@ -37,9 +44,17 @@ export async function readMobileHostBridgeClipboardText( } export async function readMobileClipboardText(): Promise { - const result = normalizeHostBridgeClipboardText( - await Clipboard.getStringAsync(), - ); + let rawText: unknown; + try { + rawText = await Clipboard.getStringAsync(); + } catch { + throw { + code: 'host_error', + message: 'clipboard read unavailable', + } satisfies HostBridgeError; + } + + const result = normalizeHostBridgeClipboardText(rawText); if (!result) { throw { code: 'host_error',