Files
Genarrative/packages/shared/src/contracts/hostBridge.test.ts
T
kdletters e2fc7d78f3 收口移动壳导入文件名归一
新增共享 HostBridge 导入文件名清洗函数

移动文件导入改用导入专用文件名归一边界

补充契约测试、移动配置门禁和协议文档
2026-06-20 03:04:21 +08:00

643 lines
22 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_NATIVE_APP_QUERY,
HOST_BRIDGE_NATIVE_APP_QUERY_KEY,
HOST_BRIDGE_NATIVE_APP_QUERY_KEYS,
HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS,
HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY,
HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES,
HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES,
HOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY,
HOST_BRIDGE_PUBLIC_WEB_ORIGIN,
HOST_BRIDGE_PUBLIC_WEB_URL,
HOST_BRIDGE_TAURI_COMMAND,
HOST_BRIDGE_CAPABILITIES,
HOST_BRIDGE_DOCUMENT_MIME_TYPES,
HOST_BRIDGE_DEFAULT_REQUEST_TIMEOUT_MS,
HOST_BRIDGE_DESKTOP_NETWORK_CHECK_TIMEOUT_MS,
HOST_BRIDGE_EVENTS,
HOST_BRIDGE_IMPORT_DOCUMENT_MAX_BYTES,
HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS,
HOST_BRIDGE_MOBILE_LOCAL_NOTIFICATION_CHANNEL_ID,
HOST_BRIDGE_MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS,
HOST_BRIDGE_RESPONSE_CACHE_MAX,
HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS,
HOST_BRIDGE_SCANNER_TIMEOUT_MS,
HOST_BRIDGE_TEXT_MIME_TYPES,
HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS,
isHostBridgeMethod,
isHostBridgeCapability,
isHostBridgeEventName,
normalizeHostBridgeAppTitle,
normalizeHostBridgeBadgeCount,
normalizeHostBridgeClipboardText,
normalizeHostBridgeColorScheme,
normalizeHostBridgeConnectionType,
normalizeHostBridgeExportAudioPayload,
normalizeHostBridgeExportFileName,
normalizeHostBridgeExportImagePayload,
normalizeHostBridgeExportTextPayload,
normalizeHostBridgeExternalUrl,
normalizeHostBridgeExternalUrlPayload,
normalizeHostBridgeHapticsImpactStyle,
normalizeHostBridgeImportAudioResult,
normalizeHostBridgeImportDocumentResult,
normalizeHostBridgeImportFileName,
normalizeHostBridgeImportImageResult,
normalizeHostBridgeImportTextResult,
normalizeHostBridgeLifecycleState,
normalizeHostBridgeLocalNotification,
normalizeHostBridgeQrCodeValue,
normalizeHostBridgeRequestId,
normalizeHostBridgeShareOpenPayload,
} 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('固定移动壳 WebView 下载协议阻断清单', () => {
expect(HOST_BRIDGE_MOBILE_WEBVIEW_BLOCKED_DOWNLOAD_PROTOCOLS).toEqual([
'blob:',
'data:',
'file:',
'filesystem:',
]);
});
test('固定移动壳本地通知频道', () => {
expect(HOST_BRIDGE_MOBILE_LOCAL_NOTIFICATION_CHANNEL_ID).toBe(
'genarrative-local',
);
});
test('固定原生壳请求超时边界', () => {
expect(HOST_BRIDGE_DEFAULT_REQUEST_TIMEOUT_MS).toBe(8000);
expect(HOST_BRIDGE_MAX_REQUEST_TIMEOUT_MS).toBe(60000);
expect(HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS).toBe(3000);
expect(HOST_BRIDGE_USER_INTERACTION_TIMEOUT_MS).toBe(30000);
expect(HOST_BRIDGE_SCANNER_TIMEOUT_MS).toBe(60000);
expect(HOST_BRIDGE_DESKTOP_NETWORK_CHECK_TIMEOUT_MS).toBe(1200);
});
test('固定宿主侧响应回放缓存边界', () => {
expect(HOST_BRIDGE_RESPONSE_CACHE_MAX).toBe(128);
});
test('固定宿主上下文 query 契约', () => {
expect(HOST_BRIDGE_RUNTIME_CONTEXT_QUERY_KEY).toEqual({
clientRuntime: 'clientRuntime',
clientType: 'clientType',
miniProgramEnv: 'miniProgramEnv',
hostShell: 'hostShell',
hostPlatform: 'hostPlatform',
hostVersion: 'hostVersion',
bridgeVersion: 'bridgeVersion',
hostCapabilities: 'hostCapabilities',
});
expect(HOST_BRIDGE_PRESERVED_RUNTIME_CONTEXT_QUERY_KEYS).toEqual([
'clientType',
'clientRuntime',
'miniProgramEnv',
'hostShell',
'hostPlatform',
'hostVersion',
'bridgeVersion',
'hostCapabilities',
]);
expect(HOST_BRIDGE_NATIVE_APP_QUERY_KEY).toEqual({
clientRuntime: 'clientRuntime',
clientType: 'clientType',
hostShell: 'hostShell',
hostPlatform: 'hostPlatform',
hostVersion: 'hostVersion',
bridgeVersion: 'bridgeVersion',
hostCapabilities: 'hostCapabilities',
});
expect(HOST_BRIDGE_NATIVE_APP_QUERY_KEYS).toEqual([
'clientRuntime',
'clientType',
'hostShell',
'hostPlatform',
'hostVersion',
'bridgeVersion',
'hostCapabilities',
]);
expect(HOST_BRIDGE_NATIVE_APP_QUERY).toEqual({
clientRuntime: 'native_app',
clientType: 'native_app',
hostShellExpoMobile: 'expo_mobile',
hostShellTauriDesktop: 'tauri_desktop',
});
expect(HOST_BRIDGE_WECHAT_MINI_PROGRAM_SOURCE_QUERY).toEqual({
clientType: 'mini_program',
clientRuntime: 'wechat_mini_program',
});
});
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.importDocument')).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_WECHAT_MINI_PROGRAM_CAPABILITIES,
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(HOST_BRIDGE_WECHAT_MINI_PROGRAM_CAPABILITIES).toEqual([
'auth.requestLogin',
'payment.request',
'share.setTarget',
'share.open',
]);
for (const nativeAppProfile of [
HOST_BRIDGE_EXPO_MOBILE_BASE_CAPABILITIES,
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES,
]) {
expect(nativeAppProfile).not.toContain('auth.requestLogin');
expect(nativeAppProfile).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(
'file.importDocument',
);
expect(HOST_BRIDGE_TAURI_DESKTOP_CAPABILITIES).toContain(
'file.importDocument',
);
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(
normalizeHostBridgeExternalUrlPayload(' https://example.com/path '),
).toEqual({
url: 'https://example.com/path',
});
expect(normalizeHostBridgeExternalUrlPayload('mailto:hi@example.com')).toEqual({
url: 'mailto:hi@example.com',
});
expect(normalizeHostBridgeExternalUrlPayload('')).toBeNull();
expect(normalizeHostBridgeExternalUrlPayload('bad\nurl')).toBeNull();
expect(
normalizeHostBridgeExternalUrlPayload('javascript:alert(1)'),
).toBeNull();
expect(normalizeHostBridgeExternalUrlPayload(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(normalizeHostBridgeAppTitle(' 拼图 - 陶泥儿 ')).toEqual({
title: '拼图 - 陶泥儿',
});
expect(normalizeHostBridgeAppTitle('')).toBeNull();
expect(normalizeHostBridgeAppTitle('拼图\n陶泥儿')).toBeNull();
expect(normalizeHostBridgeAppTitle('a'.repeat(90))).toEqual({
title: 'a'.repeat(80),
});
});
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('文档导入契约包含文本和 DOCX 边界', () => {
expect(HOST_BRIDGE_DOCUMENT_MIME_TYPES).toEqual([
...HOST_BRIDGE_TEXT_MIME_TYPES,
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
]);
expect(HOST_BRIDGE_IMPORT_DOCUMENT_MAX_BYTES).toBe(5 * 1024 * 1024);
});
test('归一化宿主文本、图片和音频导出载荷', () => {
expect(
normalizeHostBridgeExportTextPayload({
fileName: ' ../作品:记录?.md ',
content: 'content',
mimeType: 'text/markdown',
}),
).toEqual({
fileName: '作品-记录-.md',
content: 'content',
mimeType: 'text/markdown',
});
expect(
normalizeHostBridgeExportTextPayload({
fileName: '作品记录.txt',
content: 'content',
}),
).toEqual({
fileName: '作品记录.txt',
content: 'content',
});
expect(
normalizeHostBridgeExportImagePayload({
fileName: ' ../分享:卡?.png ',
base64Data: ' c2hhcmU= ',
mimeType: 'image/png',
}),
).toEqual({
fileName: '分享-卡-.png',
base64Data: 'c2hhcmU=',
mimeType: 'image/png',
});
expect(
normalizeHostBridgeExportAudioPayload({
fileName: '敲击音效.wav',
base64Data: ' YXVkaW8= ',
mimeType: 'audio/wav',
}),
).toEqual({
fileName: '敲击音效.wav',
base64Data: 'YXVkaW8=',
mimeType: 'audio/wav',
});
expect(
normalizeHostBridgeExportTextPayload({
fileName: 'bad.bin',
content: 'content',
mimeType: 'application/octet-stream',
}),
).toBeNull();
expect(
normalizeHostBridgeExportImagePayload({
fileName: 'bad.gif',
base64Data: 'c2hhcmU=',
mimeType: 'image/gif',
}),
).toBeNull();
expect(
normalizeHostBridgeExportAudioPayload({
fileName: 'empty.wav',
base64Data: ' ',
mimeType: 'audio/wav',
}),
).toBeNull();
});
test('归一化宿主文件导入结果', () => {
expect(normalizeHostBridgeImportFileName(' ../剧情:草稿?.md ')).toBe(
'剧情-草稿-.md',
);
expect(
normalizeHostBridgeImportTextResult({
action: 'selected',
fileName: ' ../剧情:草稿?.md ',
content: '暖灯猫街',
mimeType: 'text/markdown',
bytes: 12,
}),
).toEqual({
action: 'selected',
fileName: '剧情-草稿-.md',
content: '暖灯猫街',
mimeType: 'text/markdown',
bytes: 12,
});
expect(
normalizeHostBridgeImportDocumentResult({
action: 'selected',
fileName: ' 世界设定.docx ',
base64Data: ' UEsDBGRvY3g= ',
mimeType:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
bytes: 8,
}),
).toEqual({
action: 'selected',
fileName: '世界设定.docx',
base64Data: 'UEsDBGRvY3g=',
mimeType:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
bytes: 8,
});
expect(
normalizeHostBridgeImportImageResult({
action: 'dropped',
fileName: ' 参考图.png ',
base64Data: ' aW1hZ2U= ',
mimeType: 'image/png',
bytes: 5,
position: { x: 12, y: 24 },
}),
).toEqual({
action: 'dropped',
fileName: '参考图.png',
base64Data: 'aW1hZ2U=',
mimeType: 'image/png',
bytes: 5,
position: { x: 12, y: 24 },
});
expect(
normalizeHostBridgeImportAudioResult({
action: 'selected',
fileName: ' 敲击音效.webm ',
base64Data: ' YXVkaW8= ',
mimeType: 'audio/webm',
bytes: 5,
}),
).toEqual({
action: 'selected',
fileName: '敲击音效.webm',
base64Data: 'YXVkaW8=',
mimeType: 'audio/webm',
bytes: 5,
});
expect(
normalizeHostBridgeImportTextResult({
action: 'selected',
fileName: 'bad.bin',
content: 'content',
mimeType: 'application/octet-stream',
bytes: 7,
}),
).toBeNull();
expect(
normalizeHostBridgeImportAudioResult({
action: 'selected',
fileName: 'huge.webm',
base64Data: 'YXVkaW8=',
mimeType: 'audio/webm',
bytes: 20 * 1024 * 1024 + 1,
}),
).toBeNull();
});
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(
normalizeHostBridgeShareOpenPayload({
title: ' 暖灯猫街 ',
message: ' 来玩 ',
url: '/works/detail?work=PZ-1',
}),
).toEqual({
status: 'valid',
payload: {
title: '暖灯猫街',
message: '来玩',
url: 'https://app.genarrative.world/works/detail?work=PZ-1',
},
});
expect(
normalizeHostBridgeShareOpenPayload({
title: '作品',
work: 'PZ 1/二',
}),
).toEqual({
status: 'valid',
payload: {
title: '作品',
url: 'https://app.genarrative.world/works/detail?work=PZ+1%2F%E4%BA%8C',
},
});
expect(
normalizeHostBridgeShareOpenPayload({
title: '作品',
targetPath: '//app.genarrative.world/works/detail?work=PZ-1',
}),
).toEqual({ status: 'invalid' });
expect(
normalizeHostBridgeShareOpenPayload({
title: '作品',
url: 'https://example.com/works/detail?work=PZ-1',
}),
).toEqual({ status: 'invalid' });
expect(normalizeHostBridgeShareOpenPayload({})).toEqual({
status: 'empty',
});
expect(normalizeHostBridgeShareOpenPayload(null)).toEqual({
status: 'empty',
});
});
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');
});
});