补齐移动分发未声明能力测试
移动 dispatch 直接覆盖未声明 HostBridge method 返回 unsupported 原生壳结构门禁登记移动 dispatch 测试文件 共享决策日志补充移动 dispatch 单测边界
This commit is contained in:
@@ -44,6 +44,11 @@ const clipboardPath = new URL('../src/host-bridge/clipboard.ts', import.meta.url
|
||||
const clipboardSource = fs.readFileSync(clipboardPath, 'utf8');
|
||||
const dispatchPath = new URL('../src/host-bridge/dispatch.ts', import.meta.url);
|
||||
const dispatchSource = fs.readFileSync(dispatchPath, 'utf8');
|
||||
const dispatchTestPath = new URL(
|
||||
'../src/host-bridge/dispatch.test.ts',
|
||||
import.meta.url,
|
||||
);
|
||||
const dispatchTestSource = fs.readFileSync(dispatchTestPath, 'utf8');
|
||||
const filesPath = new URL('../src/host-bridge/files.ts', import.meta.url);
|
||||
const filesSource = fs.readFileSync(filesPath, 'utf8');
|
||||
const filesTestPath = new URL('../src/host-bridge/files.test.ts', import.meta.url);
|
||||
@@ -955,11 +960,12 @@ for (const snippet of [
|
||||
'const IOS_UNDECLARED_HOST_BRIDGE_METHODS = HOST_BRIDGE_METHODS.filter(',
|
||||
'(method) => !HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES.includes(method)',
|
||||
'test.each(IOS_UNDECLARED_HOST_BRIDGE_METHODS)',
|
||||
"expect(failedResponse.error.code).toBe('unsupported_method')",
|
||||
'dispatchMobileHostBridgeRequest(request(method))',
|
||||
"expect(response.error.code).toBe('unsupported_method')",
|
||||
]) {
|
||||
if (!bridgeTestSource.includes(snippet)) {
|
||||
if (!dispatchTestSource.includes(snippet)) {
|
||||
throw new Error(
|
||||
`mobile shell bridge tests must derive unsupported method coverage from shared profiles: ${snippet}`,
|
||||
`mobile shell dispatch tests must derive unsupported method coverage from shared profiles: ${snippet}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES,
|
||||
HOST_BRIDGE_METHODS,
|
||||
HOST_BRIDGE_PROTOCOL,
|
||||
HOST_BRIDGE_VERSION,
|
||||
type HostBridgeMethod,
|
||||
type HostBridgeRequest,
|
||||
} from '../../../../packages/shared/src/contracts/hostBridge';
|
||||
import {
|
||||
dispatchMobileHostBridgeRequest,
|
||||
resetMobileHostBridgeDispatchForTest,
|
||||
} from './dispatch';
|
||||
|
||||
vi.mock('expo-clipboard', () => ({
|
||||
getStringAsync: vi.fn(),
|
||||
setStringAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('expo-document-picker', () => ({
|
||||
getDocumentAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('expo-file-system', () => ({
|
||||
File: class TestFile {},
|
||||
Paths: {
|
||||
cache: 'file:///cache/',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('expo-haptics', () => ({
|
||||
ImpactFeedbackStyle: {
|
||||
Heavy: 'heavy',
|
||||
Light: 'light',
|
||||
Medium: 'medium',
|
||||
},
|
||||
impactAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('expo-image-picker', () => ({
|
||||
PermissionStatus: {
|
||||
DENIED: 'denied',
|
||||
GRANTED: 'granted',
|
||||
},
|
||||
launchCameraAsync: vi.fn(),
|
||||
launchImageLibraryAsync: vi.fn(),
|
||||
requestCameraPermissionsAsync: vi.fn(),
|
||||
requestMediaLibraryPermissionsAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('expo-linking', () => ({
|
||||
canOpenURL: vi.fn(),
|
||||
openURL: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('expo-network', () => ({
|
||||
getNetworkStateAsync: vi.fn(),
|
||||
NetworkStateType: {
|
||||
CELLULAR: 'CELLULAR',
|
||||
ETHERNET: 'ETHERNET',
|
||||
NONE: 'NONE',
|
||||
WIFI: 'WIFI',
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('expo-notifications', () => ({
|
||||
AndroidImportance: {
|
||||
DEFAULT: 'default',
|
||||
},
|
||||
getPermissionsAsync: vi.fn(),
|
||||
IosAuthorizationStatus: {
|
||||
PROVISIONAL: 3,
|
||||
},
|
||||
requestPermissionsAsync: vi.fn(),
|
||||
scheduleNotificationAsync: vi.fn(),
|
||||
setNotificationChannelAsync: vi.fn(),
|
||||
setNotificationHandler: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('expo-sharing', () => ({
|
||||
isAvailableAsync: vi.fn(),
|
||||
shareAsync: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('react-native', () => ({
|
||||
Appearance: {
|
||||
getColorScheme: vi.fn(),
|
||||
},
|
||||
Platform: {
|
||||
OS: 'android',
|
||||
},
|
||||
PushNotificationIOS: {
|
||||
setApplicationIconBadgeNumber: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
function request(method: HostBridgeMethod): HostBridgeRequest {
|
||||
return {
|
||||
bridge: HOST_BRIDGE_PROTOCOL,
|
||||
version: HOST_BRIDGE_VERSION,
|
||||
id: `dispatch-${method}`,
|
||||
method,
|
||||
};
|
||||
}
|
||||
|
||||
const IOS_UNDECLARED_HOST_BRIDGE_METHODS = HOST_BRIDGE_METHODS.filter(
|
||||
(method) => !HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES.includes(method),
|
||||
);
|
||||
|
||||
describe('mobile HostBridge dispatch', () => {
|
||||
test.each(IOS_UNDECLARED_HOST_BRIDGE_METHODS)(
|
||||
'%s 未被移动壳声明时明确返回 unsupported',
|
||||
async (method) => {
|
||||
resetMobileHostBridgeDispatchForTest();
|
||||
|
||||
const response = await dispatchMobileHostBridgeRequest(request(method));
|
||||
|
||||
expect(response.ok).toBe(false);
|
||||
if (response.ok) {
|
||||
throw new Error(`${method} unexpectedly succeeded`);
|
||||
}
|
||||
expect(response.error.code).toBe('unsupported_method');
|
||||
expect(response.error.message).toContain(method);
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -51,6 +51,7 @@
|
||||
- 2026-06-18 能力声明收紧:`packages/shared/src/contracts/hostBridge.ts` 提供 HostBridge method / capability 白名单,H5 的 `getHostRuntime()` 会解析并过滤 `hostCapabilities`;`openHostShare`、`writeHostClipboardText`、`requestHostHapticsImpact`、`setHostAppTitle`、`exportHostTextFile` 等 native 能力只在宿主声明对应 capability 后调用。发布分享弹窗只有声明 `share.open` 时才显示“系统分享”,避免旧壳或裁剪壳露出不可用入口。
|
||||
- 2026-06-20 H5 原生能力门控收口:除 `host.getRuntime` 为了支持旧入口 URL 缺少 capability 时回读真实 runtime 可保留特殊判断外,H5 facade 中所有 native_app request 能力都必须通过 `canUseNativeHostCapability(...)` 统一门控,不得在业务能力函数内直接读取 `runtime.hostCapabilities.includes(...)`,避免各能力复制门控规则;根级 `npm run check:native-shells` 会从共享 `HOST_BRIDGE_METHODS` 自动派生需门控的 request capability 清单,新增 method 时必须同步补齐 H5 facade 门控。
|
||||
- 2026-06-20 移动壳未声明 method 覆盖:Expo 移动壳对未进入 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 的共享 request method 必须由测试从 `HOST_BRIDGE_METHODS` 自动派生覆盖,并在请求到达时返回明确 `unsupported_method`;平台差异能力如 Android 不声明的 `app.setBadgeCount` 保持独立 `unsupported_capability` 语义,不混入未声明 method 清单,移动壳配置检查必须反查 Android 角标请求失败测试仍存在。
|
||||
- 2026-06-20 移动 dispatch 单测边界:`apps/mobile-shell/src/host-bridge/dispatch.test.ts` 直接从 `HOST_BRIDGE_METHODS` 与 `HOST_BRIDGE_EXPO_MOBILE_IOS_CAPABILITIES` 派生未声明 method 清单,覆盖 `dispatchMobileHostBridgeRequest(...)` 返回 `unsupported_method`,避免只靠完整 `bridge.test.ts` 间接证明 `auth.requestLogin`、`payment.request` 等等待真实 SDK 的能力不会伪成功。
|
||||
- 2026-06-20 移动壳能力清单单测边界:`apps/mobile-shell/src/host-bridge/capabilities.test.ts` 直接覆盖 Expo 移动壳 `MOBILE_HOST_CAPABILITIES` / `IOS_MOBILE_HOST_CAPABILITIES` 必须引用共享 `HOST_BRIDGE_EXPO_MOBILE_*` profile,Android 只使用 base profile 且不声明 `app.setBadgeCount`,iOS 只额外声明真实角标能力,并且两端在真实 SDK / 渠道流程落地前不得声明 `auth.requestLogin` 或 `payment.request`。根级 `npm run check:native-shells` 会把该测试文件列入移动桥接层结构清单,避免移动壳 capability profile 本地复制或伪声明。
|
||||
- 2026-06-20 壳文档能力清单反查:`npm run check:native-shells` 会同时反查移动壳 / 桌面壳主状态段落能力清单和后续完整能力清单;主状态段落按集合检查,允许按叙述需要调整顺序,但不得漏写或多写 capability,完整能力清单继续按共享 profile 顺序检查。
|
||||
- 2026-06-18 宿主 runtime 回读:主 App 启动时会通过真实 `host.getRuntime` 回读 Expo / Tauri runtime 并缓存过滤后的能力清单,能力来源为 URL `hostCapabilities` 与宿主真实回包的并集;裁剪壳或旧入口 URL 缺少 `hostCapabilities` 时也能启用真实声明能力,但仍不会仅凭 `native_app` 或 transport 存在推断能力可用。该回读请求的短超时由共享契约 `HOST_BRIDGE_RUNTIME_REFRESH_TIMEOUT_MS` 声明,H5 facade 不得本地重声明。
|
||||
|
||||
@@ -259,6 +259,7 @@ const expectedMobileHostBridgeFiles = [
|
||||
'capabilities.ts',
|
||||
'clipboard.test.ts',
|
||||
'clipboard.ts',
|
||||
'dispatch.test.ts',
|
||||
'dispatch.ts',
|
||||
'filePayloads.test.ts',
|
||||
'filePayloads.ts',
|
||||
|
||||
Reference in New Issue
Block a user