From bdee19b51064b88ba4df137eed5bde5a1c37021d Mon Sep 17 00:00:00 2001 From: kdletters Date: Sat, 20 Jun 2026 14:29:35 +0800 Subject: [PATCH] =?UTF-8?q?=E9=94=81=E5=AE=9A=E7=A7=BB=E5=8A=A8=E5=A3=B3?= =?UTF-8?q?=E5=89=AA=E8=B4=B4=E6=9D=BF=E5=A4=B1=E8=B4=A5=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 移动壳剪贴板写入失败返回稳定错误 移动壳剪贴板读取失败返回稳定错误 移动壳协议兜底测试改用外链原生异常触发 移动壳配置检查补充剪贴板失败边界反查 --- apps/mobile-shell/scripts/check-config.mjs | 8 ++++++- .../src/host-bridge/bridge.test.ts | 12 ++++++---- .../src/host-bridge/clipboard.test.ts | 22 ++++++++++++++++++ .../mobile-shell/src/host-bridge/clipboard.ts | 23 +++++++++++++++---- 4 files changed, 56 insertions(+), 9 deletions(-) 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',