e4b9cbf09d
新增 scanner.scanQrCode HostBridge 契约与二维码结果归一。 接入 Expo Camera 扫码 overlay 并保留 H5 浏览器扫码回退。 让 Tauri 白名单识别扫码 method 但不声明桌面扫码能力。 同步原生壳门禁、Expo 权限检查、方案文档和共享决策记录。
277 lines
11 KiB
TypeScript
277 lines
11 KiB
TypeScript
import { describe, expect, test } from 'vitest';
|
|
|
|
import {
|
|
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
|
|
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
|
|
HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES,
|
|
HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES,
|
|
HOST_BRIDGE_PUBLIC_WEB_ORIGIN,
|
|
HOST_BRIDGE_PUBLIC_WEB_URL,
|
|
HOST_BRIDGE_TAURI_COMMAND,
|
|
HOST_BRIDGE_CAPABILITIES,
|
|
HOST_BRIDGE_EVENTS,
|
|
isHostBridgeMethod,
|
|
isHostBridgeCapability,
|
|
isHostBridgeEventName,
|
|
normalizeHostBridgeBadgeCount,
|
|
normalizeHostBridgeClipboardText,
|
|
normalizeHostBridgeColorScheme,
|
|
normalizeHostBridgeConnectionType,
|
|
normalizeHostBridgeExportFileName,
|
|
normalizeHostBridgeExternalUrl,
|
|
normalizeHostBridgeHapticsImpactStyle,
|
|
normalizeHostBridgeLifecycleState,
|
|
normalizeHostBridgeLocalNotification,
|
|
normalizeHostBridgeQrCodeValue,
|
|
normalizeHostBridgeRequestId,
|
|
} from './hostBridge';
|
|
|
|
describe('HostBridge shared contract helpers', () => {
|
|
test('固定公开 H5 主站入口', () => {
|
|
expect(HOST_BRIDGE_PUBLIC_WEB_ORIGIN).toBe('https://app.genarrative.world');
|
|
expect(HOST_BRIDGE_PUBLIC_WEB_URL).toBe('https://app.genarrative.world/');
|
|
});
|
|
|
|
test('固定 Tauri 只暴露唯一 HostBridge command', () => {
|
|
expect(HOST_BRIDGE_TAURI_COMMAND).toBe('host_bridge_request');
|
|
});
|
|
|
|
test('只允许明确的外链协议交给宿主打开', () => {
|
|
expect(normalizeHostBridgeExternalUrl(' https://example.com/a ')).toBe(
|
|
'https://example.com/a',
|
|
);
|
|
expect(normalizeHostBridgeExternalUrl('mailto:hi@example.com')).toBe(
|
|
'mailto:hi@example.com',
|
|
);
|
|
expect(normalizeHostBridgeExternalUrl('tel:+12345678')).toBe(
|
|
'tel:+12345678',
|
|
);
|
|
});
|
|
|
|
test('拒绝空值、控制字符和危险协议', () => {
|
|
expect(normalizeHostBridgeExternalUrl('')).toBeNull();
|
|
expect(normalizeHostBridgeExternalUrl('javascript:alert(1)')).toBeNull();
|
|
expect(normalizeHostBridgeExternalUrl('file:///etc/passwd')).toBeNull();
|
|
expect(
|
|
normalizeHostBridgeExternalUrl('https://example.com/\nnext'),
|
|
).toBeNull();
|
|
expect(normalizeHostBridgeExternalUrl('/relative/path')).toBeNull();
|
|
});
|
|
|
|
test('归一化宿主导出文件名', () => {
|
|
expect(normalizeHostBridgeExportFileName(' 作品:记录?.txt ')).toBe(
|
|
'作品-记录-.txt',
|
|
);
|
|
expect(normalizeHostBridgeExportFileName('../secret.txt')).toBe(
|
|
'secret.txt',
|
|
);
|
|
expect(normalizeHostBridgeExportFileName('')).toBe(
|
|
'genarrative-export.txt',
|
|
);
|
|
expect(normalizeHostBridgeExportFileName('a'.repeat(140))).toHaveLength(
|
|
120,
|
|
);
|
|
});
|
|
|
|
test('识别 HostBridge 能力白名单', () => {
|
|
expect(isHostBridgeMethod('host.getRuntime')).toBe(true);
|
|
expect(isHostBridgeMethod('share.open')).toBe(true);
|
|
expect(isHostBridgeMethod('app.lifecycle')).toBe(false);
|
|
expect(isHostBridgeMethod('unknown.method')).toBe(false);
|
|
expect(isHostBridgeCapability('appearance.getColorScheme')).toBe(true);
|
|
expect(isHostBridgeCapability('share.open')).toBe(true);
|
|
expect(isHostBridgeCapability('app.reloadWebView')).toBe(true);
|
|
expect(isHostBridgeCapability('app.lifecycle')).toBe(true);
|
|
expect(isHostBridgeCapability('network.status')).toBe(true);
|
|
expect(isHostBridgeCapability('network.statusChanged')).toBe(true);
|
|
expect(isHostBridgeCapability('clipboard.readText')).toBe(true);
|
|
expect(isHostBridgeCapability('file.importText')).toBe(true);
|
|
expect(isHostBridgeCapability('file.importImage')).toBe(true);
|
|
expect(isHostBridgeCapability('file.captureImage')).toBe(true);
|
|
expect(isHostBridgeCapability('scanner.scanQrCode')).toBe(true);
|
|
expect(isHostBridgeCapability('file.importAudio')).toBe(true);
|
|
expect(isHostBridgeCapability('file.exportAudio')).toBe(true);
|
|
expect(isHostBridgeCapability('file.imageDropped')).toBe(true);
|
|
expect(isHostBridgeCapability('app.setBadgeCount')).toBe(true);
|
|
expect(isHostBridgeCapability('notification.showLocal')).toBe(true);
|
|
expect(isHostBridgeCapability('navigation.canGoBack')).toBe(true);
|
|
expect(isHostBridgeCapability('unknown.capability')).toBe(false);
|
|
expect(isHostBridgeCapability(null)).toBe(false);
|
|
});
|
|
|
|
test('识别 HostBridge 事件白名单', () => {
|
|
expect(HOST_BRIDGE_EVENTS).toEqual([
|
|
'app.lifecycle',
|
|
'network.statusChanged',
|
|
'navigation.canGoBack',
|
|
'file.imageDropped',
|
|
]);
|
|
for (const eventName of HOST_BRIDGE_EVENTS) {
|
|
expect(isHostBridgeEventName(eventName)).toBe(true);
|
|
expect(isHostBridgeCapability(eventName)).toBe(true);
|
|
expect(isHostBridgeMethod(eventName)).toBe(false);
|
|
}
|
|
expect(isHostBridgeEventName('host.getRuntime')).toBe(false);
|
|
expect(isHostBridgeEventName('unknown.event')).toBe(false);
|
|
expect(isHostBridgeEventName(null)).toBe(false);
|
|
});
|
|
|
|
test('原生壳 capability profile 来自共享白名单', () => {
|
|
const profiles = [
|
|
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
|
|
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
|
|
HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES,
|
|
];
|
|
for (const profile of profiles) {
|
|
expect(profile.every((capability) =>
|
|
HOST_BRIDGE_CAPABILITIES.includes(capability),
|
|
)).toBe(true);
|
|
expect(profile).not.toContain('auth.requestLogin');
|
|
expect(profile).not.toContain('payment.request');
|
|
}
|
|
});
|
|
|
|
test('移动和桌面 profile 保留真实平台差异', () => {
|
|
expect(HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES).toEqual([
|
|
'app.setBadgeCount',
|
|
]);
|
|
expect(HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES).toEqual([
|
|
...HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
|
|
...HOST_BRIDGE_EXPO_MOBILE_IOS_EXTRA_CAPABILITIES,
|
|
]);
|
|
expect(HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES).toContain(
|
|
'file.captureImage',
|
|
);
|
|
expect(HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES).toContain(
|
|
'scanner.scanQrCode',
|
|
);
|
|
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).toContain('app.setTitle');
|
|
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).toContain(
|
|
'file.imageDropped',
|
|
);
|
|
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).not.toContain(
|
|
'file.captureImage',
|
|
);
|
|
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).not.toContain(
|
|
'scanner.scanQrCode',
|
|
);
|
|
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).not.toContain(
|
|
'haptics.impact',
|
|
);
|
|
});
|
|
|
|
test('归一化 HostBridge request id', () => {
|
|
expect(normalizeHostBridgeRequestId(' request-1 ')).toBe('request-1');
|
|
expect(normalizeHostBridgeRequestId('')).toBeNull();
|
|
expect(normalizeHostBridgeRequestId('request\n1')).toBeNull();
|
|
expect(normalizeHostBridgeRequestId('a'.repeat(121))).toBeNull();
|
|
expect(normalizeHostBridgeRequestId(null)).toBeNull();
|
|
});
|
|
|
|
test('归一化宿主剪贴板读取文本', () => {
|
|
expect(normalizeHostBridgeClipboardText('作品号 PZ-1')).toEqual({
|
|
text: '作品号 PZ-1',
|
|
});
|
|
expect(normalizeHostBridgeClipboardText('a'.repeat(100010))).toEqual({
|
|
text: 'a'.repeat(100000),
|
|
});
|
|
expect(normalizeHostBridgeClipboardText(null)).toBeNull();
|
|
});
|
|
|
|
test('归一化宿主二维码扫码结果', () => {
|
|
expect(normalizeHostBridgeQrCodeValue(' https://example.com/a ')).toEqual({
|
|
value: 'https://example.com/a',
|
|
format: 'qr_code',
|
|
});
|
|
expect(normalizeHostBridgeQrCodeValue('')).toBeNull();
|
|
expect(normalizeHostBridgeQrCodeValue('bad\nvalue')).toBeNull();
|
|
expect(normalizeHostBridgeQrCodeValue(null)).toBeNull();
|
|
expect(normalizeHostBridgeQrCodeValue('a'.repeat(4100))).toEqual({
|
|
value: 'a'.repeat(4096),
|
|
format: 'qr_code',
|
|
});
|
|
});
|
|
|
|
test('归一化宿主本地通知内容', () => {
|
|
expect(
|
|
normalizeHostBridgeLocalNotification({
|
|
title: ' 生成完成 ',
|
|
body: ' 作品已准备好 可以试玩 ',
|
|
}),
|
|
).toEqual({
|
|
title: '生成完成',
|
|
body: '作品已准备好 可以试玩',
|
|
});
|
|
expect(
|
|
normalizeHostBridgeLocalNotification({
|
|
title: '生成完成',
|
|
body: '',
|
|
}),
|
|
).toEqual({
|
|
title: '生成完成',
|
|
});
|
|
expect(
|
|
normalizeHostBridgeLocalNotification({
|
|
title: 'a'.repeat(90),
|
|
body: 'b'.repeat(250),
|
|
}),
|
|
).toEqual({
|
|
title: 'a'.repeat(80),
|
|
body: 'b'.repeat(240),
|
|
});
|
|
expect(normalizeHostBridgeLocalNotification({ title: '' })).toBeNull();
|
|
expect(
|
|
normalizeHostBridgeLocalNotification({ title: '生成\n完成' }),
|
|
).toBeNull();
|
|
expect(
|
|
normalizeHostBridgeLocalNotification({
|
|
title: '生成完成',
|
|
body: '坏\u0001内容',
|
|
}),
|
|
).toBeNull();
|
|
expect(normalizeHostBridgeLocalNotification(null)).toBeNull();
|
|
});
|
|
|
|
test('归一化宿主触觉反馈强度', () => {
|
|
expect(normalizeHostBridgeHapticsImpactStyle(undefined)).toBe('light');
|
|
expect(normalizeHostBridgeHapticsImpactStyle('light')).toBe('light');
|
|
expect(normalizeHostBridgeHapticsImpactStyle('medium')).toBe('medium');
|
|
expect(normalizeHostBridgeHapticsImpactStyle('heavy')).toBe('heavy');
|
|
expect(normalizeHostBridgeHapticsImpactStyle('rigid')).toBeNull();
|
|
expect(normalizeHostBridgeHapticsImpactStyle(null)).toBeNull();
|
|
});
|
|
|
|
test('归一化宿主角标数量', () => {
|
|
expect(normalizeHostBridgeBadgeCount(0)).toBe(0);
|
|
expect(normalizeHostBridgeBadgeCount(12)).toBe(12);
|
|
expect(normalizeHostBridgeBadgeCount(99999)).toBe(99999);
|
|
expect(normalizeHostBridgeBadgeCount(-1)).toBeNull();
|
|
expect(normalizeHostBridgeBadgeCount(1.5)).toBeNull();
|
|
expect(normalizeHostBridgeBadgeCount(100000)).toBeNull();
|
|
expect(normalizeHostBridgeBadgeCount('1')).toBeNull();
|
|
});
|
|
|
|
test('归一化宿主配色模式', () => {
|
|
expect(normalizeHostBridgeColorScheme('light')).toBe('light');
|
|
expect(normalizeHostBridgeColorScheme('dark')).toBe('dark');
|
|
expect(normalizeHostBridgeColorScheme('unspecified')).toBe('unknown');
|
|
expect(normalizeHostBridgeColorScheme(null)).toBe('unknown');
|
|
});
|
|
|
|
test('归一化宿主生命周期状态', () => {
|
|
expect(normalizeHostBridgeLifecycleState('active')).toBe('active');
|
|
expect(normalizeHostBridgeLifecycleState('background')).toBe('background');
|
|
expect(normalizeHostBridgeLifecycleState('extension')).toBe('inactive');
|
|
expect(normalizeHostBridgeLifecycleState(null)).toBe('inactive');
|
|
});
|
|
|
|
test('归一化宿主网络连接类型', () => {
|
|
expect(normalizeHostBridgeConnectionType('WIFI')).toBe('wifi');
|
|
expect(normalizeHostBridgeConnectionType('CELLULAR')).toBe('cellular');
|
|
expect(normalizeHostBridgeConnectionType('ETHERNET')).toBe('ethernet');
|
|
expect(normalizeHostBridgeConnectionType('NONE')).toBe('none');
|
|
expect(normalizeHostBridgeConnectionType('satellite')).toBe('unknown');
|
|
expect(normalizeHostBridgeConnectionType(null)).toBe('unknown');
|
|
});
|
|
});
|