补齐移动壳剪贴板单测

移动壳剪贴板 helper 增加独立单测覆盖

原生壳结构门禁登记移动剪贴板测试

宿主壳文档和共享决策同步剪贴板测试边界
This commit is contained in:
2026-06-20 07:47:23 +08:00
parent 97c3971111
commit 55f844a67e
5 changed files with 145 additions and 4 deletions
@@ -0,0 +1,139 @@
import * as Clipboard from 'expo-clipboard';
import { beforeEach, describe, expect, test, vi } from 'vitest';
import {
HOST_BRIDGE_PROTOCOL,
HOST_BRIDGE_VERSION,
type HostBridgeRequest,
} from '../../../../packages/shared/src/contracts/hostBridge';
import {
readMobileClipboardText,
readMobileHostBridgeClipboardText,
writeMobileClipboardText,
writeMobileHostBridgeClipboardText,
} from './clipboard';
vi.mock('expo-clipboard', () => ({
getStringAsync: vi.fn(),
setStringAsync: vi.fn(),
}));
function request(
method: 'clipboard.writeText' | 'clipboard.readText',
payload?: unknown,
): HostBridgeRequest {
return {
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
id: `${method}-request`,
method,
payload,
};
}
beforeEach(() => {
vi.mocked(Clipboard.getStringAsync).mockReset();
vi.mocked(Clipboard.setStringAsync).mockReset();
});
describe('mobile clipboard helpers', () => {
test('writes normalized text to the Expo clipboard', async () => {
await expect(writeMobileClipboardText('作品号 PZ-1')).resolves.toBe(true);
expect(Clipboard.setStringAsync).toHaveBeenCalledWith('作品号 PZ-1');
});
test('truncates long text before writing to the Expo clipboard', async () => {
await expect(writeMobileClipboardText('猫'.repeat(100010))).resolves.toBe(
true,
);
expect(Clipboard.setStringAsync).toHaveBeenCalledWith('猫'.repeat(100000));
});
test('writes empty text as a valid pure-text clipboard payload', async () => {
await expect(writeMobileClipboardText('')).resolves.toBe(true);
expect(Clipboard.setStringAsync).toHaveBeenCalledWith('');
});
test('rejects non-string write text without touching the Expo clipboard', async () => {
await expect(writeMobileClipboardText(undefined)).resolves.toBe(false);
expect(Clipboard.setStringAsync).not.toHaveBeenCalled();
});
test('wraps HostBridge clipboard write responses', async () => {
const response = await writeMobileHostBridgeClipboardText(
request('clipboard.writeText', {
text: '作品号 PZ-1',
}),
);
expect(response).toEqual({
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
id: 'clipboard.writeText-request',
ok: true,
result: true,
});
expect(Clipboard.setStringAsync).toHaveBeenCalledWith('作品号 PZ-1');
});
test('wraps invalid HostBridge clipboard write payloads', async () => {
await expect(
writeMobileHostBridgeClipboardText(
request('clipboard.writeText', {
text: undefined,
}),
),
).rejects.toMatchObject({
code: 'invalid_request',
message: 'text is required',
});
expect(Clipboard.setStringAsync).not.toHaveBeenCalled();
});
test('reads and normalizes pure text from the Expo clipboard', async () => {
vi.mocked(Clipboard.getStringAsync).mockResolvedValue('作品号 PZ-1');
await expect(readMobileClipboardText()).resolves.toEqual({
text: '作品号 PZ-1',
});
});
test('truncates long clipboard reads at the shared text boundary', async () => {
vi.mocked(Clipboard.getStringAsync).mockResolvedValue('猫'.repeat(100010));
await expect(readMobileClipboardText()).resolves.toEqual({
text: '猫'.repeat(100000),
});
});
test('reads empty clipboard text as valid pure text', async () => {
vi.mocked(Clipboard.getStringAsync).mockResolvedValue('');
await expect(readMobileClipboardText()).resolves.toEqual({
text: '',
});
});
test('wraps HostBridge clipboard read responses', async () => {
vi.mocked(Clipboard.getStringAsync).mockResolvedValue('作品号 PZ-1');
const response = await readMobileHostBridgeClipboardText(
request('clipboard.readText'),
);
expect(response).toEqual({
bridge: HOST_BRIDGE_PROTOCOL,
version: HOST_BRIDGE_VERSION,
id: 'clipboard.readText-request',
ok: true,
result: {
text: '作品号 PZ-1',
},
});
});
});
@@ -106,6 +106,7 @@
- 2026-06-19 草稿生成 HostBridge 消费门禁:`PlatformEntryFlowShellImpl` 只负责派生草稿通知和未读数量,实际宿主同步经 `platformHostBridgeSync.ts` 调用 `showHostLocalNotification` / `setHostAppBadgeCount``npm run check:native-shells` 必须运行该同步层的真实 Tauri transport 测试,并继续覆盖通知模型、未读计数模型、音频导入和文档导入等 H5 HostBridge 消费测试。
- 2026-06-18 剪贴板读取能力:新增 `clipboard.readText` HostBridge capabilityH5 只能读取纯文本结果,契约限制返回文本最多 100000 字符;Expo 壳通过 `expo-clipboard` 读取系统剪贴板文本,Tauri 壳通过 Rust 侧 `tauri-plugin-clipboard-manager` 读取文本且不开放插件 JS guest API。该能力不读取图片、HTML、文件列表或剪贴板监听事件,宿主未声明或读取失败时由 H5 视作失败并保留原流程。
- 2026-06-19 移动壳剪贴板边界:Expo `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写、共享 100000 字符归一、payload 校验和 HostBridge 成功 / 失败响应边界统一收口在 `apps/mobile-shell/src/host-bridge/clipboard.ts``dispatch.ts` 只负责把 HostBridge request 委托给 `writeMobileHostBridgeClipboardText(request)` / `readMobileHostBridgeClipboardText(request)`,不得直接导入 `expo-clipboard`、调用 `Clipboard.setStringAsync` / `Clipboard.getStringAsync` 或包装剪贴板成功响应。移动壳配置检查会覆盖该模块结构、共享文本边界和 dispatch 委托关系,避免剪贴板能力散落到分发层。
- 2026-06-20 移动剪贴板单测边界:`apps/mobile-shell/src/host-bridge/clipboard.test.ts` 直接覆盖 Expo `clipboard.writeText` / `clipboard.readText` 的共享文本归一、100000 字符截断、非字符串写入拒绝、空字符串纯文本读写、系统剪贴板读写调用和 HostBridge 成功 / 失败响应包装;根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,避免移动剪贴板边界只靠完整 HostBridge bridge 流程间接覆盖。
- 2026-06-19 桌面壳剪贴板边界:Tauri `clipboard.writeText` / `clipboard.readText` 的系统剪贴板读写、共享 100000 字符归一、payload 校验和响应边界统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/clipboard.rs``dispatch.rs` 只负责把 HostBridge request 委托给 `write_desktop_host_bridge_clipboard_text(...)` / `read_desktop_host_bridge_clipboard_text(...)`,不得直接承接剪贴板文本截断、payload 解析或插件读写细节;桌面 share fallback 仍可调用底层写剪贴板函数复制分享文本。桌面壳配置检查会覆盖该模块结构、共享文本边界和 dispatch 委托关系,避免剪贴板能力散落到分发层。
- 2026-06-19 桌面壳本地通知边界:Tauri `notification.showLocal` 的 payload 清洗、权限状态检查、prompt 权限请求和系统通知发送统一收口在 `apps/desktop-shell/src-tauri/src/host_bridge/notifications.rs``dispatch.rs` 只负责把 HostBridge request 委托给 `show_desktop_local_notification(...)` 并映射响应,不得直接调用 `app.notification()``NotificationExt``PermissionState`。桌面壳配置检查会覆盖该模块结构、权限语义和 dispatch 委托关系,避免本地通知能力散落到分发层。
- 2026-06-18 文本文件导入能力:新增 `file.importText` HostBridge capabilityH5 统一通过 `importHostTextFile()` 读取宿主返回的纯文本内容;Expo 壳通过 `expo-document-picker` 打开系统文档选择器,Tauri 壳通过系统文件选择框读取真实文本文件。两端只接受 `text/plain``text/markdown``text/csv``application/json` 或对应扩展名,单次不超过 5 MiB,成功只返回清洗后的文件名、MIME、UTF-8 文本内容和字节数,不暴露设备 URI / 本机绝对路径,也不开放通用文件系统。Expo 文本导入的 DocumentPicker 调用、大小校验、文本读取和 HostBridge 成功响应包装统一收口在 `apps/mobile-shell/src/host-bridge/files.ts``dispatch.ts` 只委托文件模块。
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -254,6 +254,7 @@ const expectedMobileHostBridgeFiles = [
'bridge.test.ts',
'bridge.ts',
'capabilities.ts',
'clipboard.test.ts',
'clipboard.ts',
'dispatch.ts',
'filePayloads.test.ts',