Files
Genarrative/apps/mobile-shell/src/shell/QrScannerOverlay.test.tsx
T
kdletters 8bd8363ffa 修正原生壳公开主站配置
将原生壳公开主站统一为 www.genarrative.world

调整 Tauri release 直接加载线上主站并移除 frontendDist 打包

补齐创作入口配置开发代理与回归测试

同步原生壳方案文档和共享记忆
2026-06-22 15:07:02 +08:00

204 lines
5.7 KiB
TypeScript

/* @vitest-environment jsdom */
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';
import {
resetQrScannerForTest,
scanQrCode,
} from '../host-bridge/scanner';
import { QrScannerOverlay } from './QrScannerOverlay';
const overlayHarness = vi.hoisted(() => ({
cameraViewProps: {
current: null as Record<string, unknown> | null,
},
reset() {
this.cameraViewProps.current = null;
},
}));
vi.mock('expo-camera', () => ({
Camera: {
requestCameraPermissionsAsync: vi.fn(),
},
CameraView: (props: Record<string, unknown>) => {
overlayHarness.cameraViewProps.current = props;
return React.createElement('mobile-camera-view');
},
}));
vi.mock('react-native', () => ({
Pressable: ({
children,
onPress,
accessibilityLabel: _accessibilityLabel,
accessibilityRole: _accessibilityRole,
...props
}: {
accessibilityLabel?: string;
accessibilityRole?: string;
children?: React.ReactNode;
onPress?: () => void;
}) => React.createElement('button', { ...props, onClick: onPress }, children),
StyleSheet: {
create: <T,>(styles: T) => styles,
},
Text: ({ children, ...props }: { children?: React.ReactNode }) =>
React.createElement('span', props, children),
View: ({
children,
pointerEvents: _pointerEvents,
...props
}: {
children?: React.ReactNode;
pointerEvents?: string;
}) => React.createElement('div', props, children),
}));
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();
overlayHarness.reset();
vi.clearAllMocks();
});
test('requests camera permission and completes a QR scan result', async () => {
cameraPermissionMock.mockResolvedValue({
granted: true,
} as Awaited<ReturnType<typeof CameraModule.Camera.requestCameraPermissionsAsync>>);
render(<QrScannerOverlay />);
const scanPromise = scanQrCode();
await waitFor(() => {
expect(overlayHarness.cameraViewProps.current).toBeTruthy();
});
expect(cameraPermissionMock).toHaveBeenCalledTimes(1);
expect(overlayHarness.cameraViewProps.current).toMatchObject({
facing: 'back',
barcodeScannerSettings: {
barcodeTypes: ['qr'],
},
});
const cameraProps = overlayHarness.cameraViewProps.current as {
onBarcodeScanned?: (result: { type: string; data: string }) => void;
};
cameraProps.onBarcodeScanned?.({
type: 'qr',
data: ' https://www.genarrative.world/works/detail?work=PZ-1 ',
});
await expect(scanPromise).resolves.toEqual({
format: 'qr_code',
value: 'https://www.genarrative.world/works/detail?work=PZ-1',
});
});
test('rejects the active scan when camera permission is denied', async () => {
cameraPermissionMock.mockResolvedValue({
granted: false,
} as Awaited<ReturnType<typeof CameraModule.Camera.requestCameraPermissionsAsync>>);
render(<QrScannerOverlay />);
const scanPromise = scanQrCode();
await expect(scanPromise).rejects.toMatchObject({
code: 'host_error',
message: 'qr scanner unavailable',
});
expect(overlayHarness.cameraViewProps.current).toBeNull();
});
test('logs and rejects the active scan when camera permission request fails', async () => {
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => undefined);
const error = new Error('camera unavailable');
cameraPermissionMock.mockRejectedValue(error);
render(<QrScannerOverlay />);
const scanPromise = scanQrCode();
await expect(scanPromise).rejects.toMatchObject({
code: 'host_error',
message: 'qr scanner unavailable',
});
expect(warnSpy).toHaveBeenCalledWith(
'mobile QR scanner permission request failed',
);
expect(overlayHarness.cameraViewProps.current).toBeNull();
});
test('close button cancels the pending scan', async () => {
cameraPermissionMock.mockResolvedValue({
granted: true,
} as Awaited<ReturnType<typeof CameraModule.Camera.requestCameraPermissionsAsync>>);
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',
});
});
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();
});
});