锁定移动扫码取消边界

移动扫码覆盖权限请求迟到时不重新激活相机视图

移动壳配置检查反查扫码取消异步边界测试

共享决策记录补充扫码权限异步取消约定
This commit is contained in:
2026-06-20 15:35:07 +08:00
parent a330ce5bbb
commit e2433ec02f
3 changed files with 55 additions and 1 deletions
@@ -2402,6 +2402,7 @@ for (const snippet of [
"message: 'qr scanner unavailable'",
"message: 'qr scan cancelled'",
"test('logs and rejects the active scan when camera permission request fails'",
"test('ignores late camera permission after the scan is cancelled'",
"'mobile QR scanner permission request failed'",
"barcodeTypes: ['qr']",
"type: 'qr'",
@@ -1,6 +1,6 @@
/* @vitest-environment jsdom */
import { render, waitFor } from '@testing-library/react';
import { act, render, waitFor } from '@testing-library/react';
import * as CameraModule from 'expo-camera';
import React from 'react';
import { afterEach, describe, expect, test, vi } from 'vitest';
@@ -62,6 +62,23 @@ const cameraPermissionMock = vi.mocked(
CameraModule.Camera.requestCameraPermissionsAsync,
);
function createDeferredPermission() {
let resolvePermission!: (
permission: Awaited<
ReturnType<typeof CameraModule.Camera.requestCameraPermissionsAsync>
>,
) => void;
const promise = new Promise<
Awaited<ReturnType<typeof CameraModule.Camera.requestCameraPermissionsAsync>>
>((resolve) => {
resolvePermission = resolve;
});
return {
promise,
resolvePermission,
};
}
describe('QrScannerOverlay', () => {
afterEach(() => {
resetQrScannerForTest();
@@ -156,4 +173,32 @@ describe('QrScannerOverlay', () => {
message: 'qr scan cancelled',
});
});
test('ignores late camera permission after the scan is cancelled', async () => {
const permission = createDeferredPermission();
cameraPermissionMock.mockReturnValue(permission.promise);
const screen = render(<QrScannerOverlay />);
const scanPromise = scanQrCode();
await waitFor(() => {
expect(screen.getByText('关闭')).toBeTruthy();
});
screen.getByText('关闭').click();
await expect(scanPromise).rejects.toMatchObject({
code: 'cancelled',
message: 'qr scan cancelled',
});
await act(async () => {
permission.resolvePermission({
granted: true,
} as Awaited<ReturnType<typeof CameraModule.Camera.requestCameraPermissionsAsync>>);
await permission.promise;
});
expect(overlayHarness.cameraViewProps.current).toBeNull();
});
});
@@ -16,6 +16,14 @@
---
## 2026-06-20 移动扫码权限异步取消边界
- 背景:Expo 移动壳 `scanner.scanQrCode` 会打开真实相机权限请求和扫码 overlay;如果用户在系统权限 Promise 返回前关闭扫码,旧权限结果不能重新激活 CameraView,也不能完成已经取消的 HostBridge 请求。
- 决策:`QrScannerOverlay` 必须在 active/requestKey 变化和组件清理时忽略迟到的权限结果;移动壳配置检查必须反查“取消后迟到权限不重新打开 CameraView”的测试用例。
- 影响范围:`apps/mobile-shell/src/shell/QrScannerOverlay.tsx``apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx``apps/mobile-shell/scripts/check-config.mjs`
- 验证方式:`npm run mobile-shell:test -- src/shell/QrScannerOverlay.test.tsx``npm run mobile-shell:typecheck``npm run check:native-shells``npm run check:encoding``git diff --check`
- 关联文档:`docs/【前端架构】ExpoReactNative与Tauri宿主壳方案-2026-06-17.md``docs/【前端架构】宿主壳能力统一协议-2026-06-17.md`
## 2026-06-20 桌面壳系统能力调用顺序门禁
- 背景:Tauri 桌面壳文件导出和本地通知已经在运行时代码中先校验 HostBridge payload,再打开系统保存对话框、读取通知权限或请求通知权限;如果后续重构把系统能力调用提前,非法请求会触达原生系统边界。