071faa482c
纳入 AGC Cargo workspace 的统一 rustfmt 检查与格式化入口 完成项目 TypeScript/Prettier 与 Rust 全量格式化 修复 Pingora expected executable 门禁的空白敏感误报 同步开发运维文档与 AGC skill pack 格式化忽略规则
191 lines
5.6 KiB
TypeScript
191 lines
5.6 KiB
TypeScript
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('maps native clipboard write failures to stable host errors', async () => {
|
|
const warnSpy = vi
|
|
.spyOn(console, 'warn')
|
|
.mockImplementation(() => undefined);
|
|
const nativeError = new Error('native clipboard write failed');
|
|
vi.mocked(Clipboard.setStringAsync).mockRejectedValueOnce(nativeError);
|
|
|
|
try {
|
|
await expect(
|
|
writeMobileClipboardText('作品号 PZ-1'),
|
|
).rejects.toMatchObject({
|
|
code: 'host_error',
|
|
message: 'clipboard write unavailable',
|
|
});
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
'mobile clipboard failed for write.set_string',
|
|
);
|
|
} finally {
|
|
warnSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
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('rejects unavailable clipboard text without reporting a successful empty value', async () => {
|
|
vi.mocked(Clipboard.getStringAsync).mockResolvedValue(undefined as never);
|
|
|
|
await expect(readMobileClipboardText()).rejects.toMatchObject({
|
|
code: 'host_error',
|
|
message: 'clipboard text unavailable',
|
|
});
|
|
});
|
|
|
|
test('maps native clipboard read failures to stable host errors', async () => {
|
|
const warnSpy = vi
|
|
.spyOn(console, 'warn')
|
|
.mockImplementation(() => undefined);
|
|
const nativeError = new Error('native clipboard read failed');
|
|
vi.mocked(Clipboard.getStringAsync).mockRejectedValueOnce(nativeError);
|
|
|
|
try {
|
|
await expect(readMobileClipboardText()).rejects.toMatchObject({
|
|
code: 'host_error',
|
|
message: 'clipboard read unavailable',
|
|
});
|
|
expect(warnSpy).toHaveBeenCalledWith(
|
|
'mobile clipboard failed for read.get_string',
|
|
);
|
|
} finally {
|
|
warnSpy.mockRestore();
|
|
}
|
|
});
|
|
|
|
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',
|
|
},
|
|
});
|
|
});
|
|
});
|