区分原生文件选择取消语义
H5 HostBridge 将原生取消从不支持能力错误中拆出 文件导入与拍摄取消统一返回 null,保留不支持和畸形结果为 false 补充取消导入与拍摄回归测试并加入原生壳门禁反查
This commit is contained in:
@@ -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',
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user