区分原生文件选择取消语义

H5 HostBridge 将原生取消从不支持能力错误中拆出

文件导入与拍摄取消统一返回 null,保留不支持和畸形结果为 false

补充取消导入与拍摄回归测试并加入原生壳门禁反查
This commit is contained in:
2026-06-20 05:07:43 +08:00
parent 17c1dac76d
commit 3c853da830
3 changed files with 86 additions and 15 deletions
+24
View File
@@ -1252,26 +1252,32 @@ function assertH5HostBridgePayloadBoundaries() {
{
functionName: 'importHostTextFile',
normalizer: 'normalizeHostBridgeImportTextResult',
preserveCancellation: true,
},
{
functionName: 'importHostDocumentFile',
normalizer: 'normalizeHostBridgeImportDocumentResult',
preserveCancellation: true,
},
{
functionName: 'importHostImageFile',
normalizer: 'normalizeHostBridgeImportImageResult',
preserveCancellation: true,
},
{
functionName: 'captureHostImageFile',
normalizer: 'normalizeHostBridgeImportImageResult',
preserveCancellation: true,
},
{
functionName: 'importHostAudioFile',
normalizer: 'normalizeHostBridgeImportAudioResult',
preserveCancellation: true,
},
{
functionName: 'subscribeHostImageDrop',
normalizer: 'normalizeHostBridgeImportImageResult',
preserveCancellation: false,
},
]) {
const functionSource = extractFunctionSource(
@@ -1283,6 +1289,24 @@ function assertH5HostBridgePayloadBoundaries() {
`${importBoundary.functionName} must normalize imported file results with shared ${importBoundary.normalizer}`,
);
}
if (
importBoundary.preserveCancellation &&
(!functionSource.includes('isCancelledHostBridgeError(error)') ||
!functionSource.includes('return null;'))
) {
throw new Error(
`${importBoundary.functionName} must preserve native user cancellation as null`,
);
}
}
const unsupportedHelperSource = extractFunctionSource(
h5HostBridgeSource,
'isUnsupportedHostBridgeError',
);
if (unsupportedHelperSource.includes("'cancelled'")) {
throw new Error(
'H5 HostBridge facade must not treat native user cancellation as unsupported capability',
);
}
for (const localMimeSet of [
'HOST_BRIDGE_TEXT_MIME_TYPE_SET',
+41 -12
View File
@@ -1488,8 +1488,10 @@ describe('hostBridge', () => {
test('原生 App HostBridge 读取剪贴板结果按共享清单归一化', async () => {
const oversizedText = 'a'.repeat(HOST_BRIDGE_CLIPBOARD_TEXT_MAX_LENGTH + 5);
let requestCount = 0;
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
requestCount += 1;
const request = (args as { request: { id: string } }).request;
return {
bridge: 'GenarrativeHostBridge',
@@ -1497,10 +1499,7 @@ describe('hostBridge', () => {
id: request.id,
ok: true,
result: {
text:
invoke.mock.calls.length === 1
? 42
: oversizedText,
text: requestCount === 1 ? 42 : oversizedText,
},
};
},
@@ -2147,7 +2146,7 @@ describe('hostBridge', () => {
expect(invoke).not.toHaveBeenCalled();
});
test('原生 App 宿主取消导入音频时回退为 false', async () => {
test('原生 App 宿主取消导入音频时返回 null', async () => {
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: { id: string } }).request;
@@ -2174,10 +2173,10 @@ describe('hostBridge', () => {
},
};
await expect(importHostAudioFile()).resolves.toBe(false);
await expect(importHostAudioFile()).resolves.toBeNull();
});
test('原生 App 宿主取消导入文本时回退为 false', async () => {
test('原生 App 宿主取消导入文本时返回 null', async () => {
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: { id: string } }).request;
@@ -2204,10 +2203,10 @@ describe('hostBridge', () => {
},
};
await expect(importHostTextFile()).resolves.toBe(false);
await expect(importHostTextFile()).resolves.toBeNull();
});
test('原生 App 宿主取消导入文档时回退为 false', async () => {
test('原生 App 宿主取消导入文档时返回 null', async () => {
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: { id: string } }).request;
@@ -2234,7 +2233,7 @@ describe('hostBridge', () => {
},
};
await expect(importHostDocumentFile()).resolves.toBe(false);
await expect(importHostDocumentFile()).resolves.toBeNull();
});
test('原生 App 宿主导入文档返回畸形 payload 时回退为 false', async () => {
@@ -2303,7 +2302,7 @@ describe('hostBridge', () => {
await expect(importHostAudioFile()).resolves.toBe(false);
});
test('原生 App 宿主取消导入图片时回退为 false', async () => {
test('原生 App 宿主取消导入图片时返回 null', async () => {
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: { id: string } }).request;
@@ -2330,7 +2329,37 @@ describe('hostBridge', () => {
},
};
await expect(importHostImageFile()).resolves.toBe(false);
await expect(importHostImageFile()).resolves.toBeNull();
});
test('原生 App 宿主取消拍摄图片时返回 null', async () => {
const invoke = vi.fn(
async (_command: string, args?: Record<string, unknown>) => {
const request = (args as { request: { id: string } }).request;
return {
bridge: 'GenarrativeHostBridge',
version: 1,
id: request.id,
ok: false,
error: {
code: 'cancelled',
message: 'camera capture cancelled',
},
};
},
);
window.history.replaceState(
null,
'',
nativeAppPath(['file.captureImage']),
);
window.__TAURI__ = {
core: {
invoke: asTauriInvoke(invoke),
},
};
await expect(captureHostImageFile()).resolves.toBeNull();
});
test('原生 App 宿主取消扫码时不触发 H5 摄像头回退', async () => {
+21 -3
View File
@@ -173,11 +173,14 @@ function isUnsupportedHostBridgeError(error: unknown) {
return (
error instanceof Error &&
(error.name === 'unsupported_method' ||
error.name === 'unsupported_capability' ||
error.name === 'cancelled')
error.name === 'unsupported_capability')
);
}
function isCancelledHostBridgeError(error: unknown) {
return error instanceof Error && error.name === 'cancelled';
}
function normalizeHostCapabilities(values: unknown): HostBridgeCapability[] {
if (!Array.isArray(values)) {
return [];
@@ -994,6 +997,9 @@ export async function importHostImageFile() {
),
) ?? false;
} catch (error) {
if (isCancelledHostBridgeError(error)) {
return null;
}
if (isUnsupportedHostBridgeError(error)) {
return false;
}
@@ -1015,6 +1021,9 @@ export async function captureHostImageFile() {
),
) ?? false;
} catch (error) {
if (isCancelledHostBridgeError(error)) {
return null;
}
if (isUnsupportedHostBridgeError(error)) {
return false;
}
@@ -1036,7 +1045,7 @@ export async function scanHostQrCode() {
);
return normalizeHostBridgeQrCodeValue(result?.value) ?? false;
} catch (error) {
if (error instanceof Error && error.name === 'cancelled') {
if (isCancelledHostBridgeError(error)) {
return null;
}
if (isUnsupportedHostBridgeError(error)) {
@@ -1060,6 +1069,9 @@ export async function importHostTextFile() {
),
) ?? false;
} catch (error) {
if (isCancelledHostBridgeError(error)) {
return null;
}
if (isUnsupportedHostBridgeError(error)) {
return false;
}
@@ -1081,6 +1093,9 @@ export async function importHostDocumentFile() {
),
) ?? false;
} catch (error) {
if (isCancelledHostBridgeError(error)) {
return null;
}
if (isUnsupportedHostBridgeError(error)) {
return false;
}
@@ -1102,6 +1117,9 @@ export async function importHostAudioFile() {
),
) ?? false;
} catch (error) {
if (isCancelledHostBridgeError(error)) {
return null;
}
if (isUnsupportedHostBridgeError(error)) {
return false;
}