8bd8363ffa
将原生壳公开主站统一为 www.genarrative.world 调整 Tauri release 直接加载线上主站并移除 frontendDist 打包 补齐创作入口配置开发代理与回归测试 同步原生壳方案文档和共享记忆
204 lines
5.7 KiB
TypeScript
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();
|
|
});
|
|
});
|