8bd8363ffa
将原生壳公开主站统一为 www.genarrative.world 调整 Tauri release 直接加载线上主站并移除 frontendDist 打包 补齐创作入口配置开发代理与回归测试 同步原生壳方案文档和共享记忆
201 lines
5.1 KiB
TypeScript
201 lines
5.1 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { act, render, screen } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
import * as hostBridgeServices from '../../services/host-bridge/hostBridge';
|
|
import { PlatformProfileQrScannerModal } from './PlatformProfileQrScannerModal';
|
|
|
|
type MockTrack = {
|
|
stop: ReturnType<typeof vi.fn>;
|
|
};
|
|
|
|
type MockStream = {
|
|
getTracks: () => MockTrack[];
|
|
};
|
|
|
|
const originalBarcodeDetector = (
|
|
globalThis as typeof globalThis & {
|
|
BarcodeDetector?: unknown;
|
|
}
|
|
).BarcodeDetector;
|
|
|
|
describe('PlatformProfileQrScannerModal', () => {
|
|
beforeEach(() => {
|
|
vi.useFakeTimers();
|
|
vi.spyOn(HTMLMediaElement.prototype, 'play').mockResolvedValue(undefined);
|
|
Object.defineProperty(HTMLMediaElement.prototype, 'readyState', {
|
|
configurable: true,
|
|
get: () => 4,
|
|
});
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.runOnlyPendingTimers();
|
|
vi.useRealTimers();
|
|
vi.restoreAllMocks();
|
|
if (originalBarcodeDetector === undefined) {
|
|
delete (
|
|
globalThis as typeof globalThis & {
|
|
BarcodeDetector?: unknown;
|
|
}
|
|
).BarcodeDetector;
|
|
} else {
|
|
(
|
|
globalThis as typeof globalThis & {
|
|
BarcodeDetector?: unknown;
|
|
}
|
|
).BarcodeDetector = originalBarcodeDetector;
|
|
}
|
|
});
|
|
|
|
test('detects qr result and stops camera tracks', async () => {
|
|
vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(false);
|
|
const stop = vi.fn();
|
|
const stream = buildStream([{ stop }]);
|
|
const getUserMedia = vi.fn().mockResolvedValue(stream);
|
|
const detect = vi.fn().mockResolvedValue([{ rawValue: ' hello-world ' }]);
|
|
const onResult = vi.fn();
|
|
|
|
installMediaDevices(getUserMedia);
|
|
installBarcodeDetector(detect);
|
|
|
|
render(
|
|
<PlatformProfileQrScannerModal
|
|
error={null}
|
|
result={null}
|
|
onClose={vi.fn()}
|
|
onError={vi.fn()}
|
|
onResult={onResult}
|
|
/>,
|
|
);
|
|
|
|
await act(async () => {
|
|
await flushPromises();
|
|
});
|
|
expect(getUserMedia).toHaveBeenCalledTimes(1);
|
|
|
|
await act(async () => {
|
|
vi.advanceTimersByTime(360);
|
|
await flushPromises();
|
|
});
|
|
|
|
expect(onResult).toHaveBeenCalledWith('hello-world');
|
|
expect(detect).toHaveBeenCalledTimes(1);
|
|
expect(stop).toHaveBeenCalledTimes(1);
|
|
});
|
|
|
|
test('releases camera resource when modal unmounts before recognition', async () => {
|
|
vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(false);
|
|
const stop = vi.fn();
|
|
const stream = buildStream([{ stop }]);
|
|
const getUserMedia = vi.fn().mockResolvedValue(stream);
|
|
const detect = vi.fn().mockResolvedValue([]);
|
|
|
|
installMediaDevices(getUserMedia);
|
|
installBarcodeDetector(detect);
|
|
|
|
const { unmount } = render(
|
|
<PlatformProfileQrScannerModal
|
|
error={null}
|
|
result={null}
|
|
onClose={vi.fn()}
|
|
onError={vi.fn()}
|
|
onResult={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await act(async () => {
|
|
await flushPromises();
|
|
});
|
|
expect(getUserMedia).toHaveBeenCalledTimes(1);
|
|
|
|
unmount();
|
|
|
|
expect(stop).toHaveBeenCalledTimes(1);
|
|
expect(screen.queryByRole('dialog', { name: '扫码' })).toBeNull();
|
|
});
|
|
|
|
test('uses native QR scanner before browser camera', async () => {
|
|
const getUserMedia = vi.fn();
|
|
const onResult = vi.fn();
|
|
vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue({
|
|
value: 'https://www.genarrative.world/works/detail?work=PZ-1',
|
|
format: 'qr_code',
|
|
});
|
|
installMediaDevices(getUserMedia);
|
|
|
|
render(
|
|
<PlatformProfileQrScannerModal
|
|
error={null}
|
|
result={null}
|
|
onClose={vi.fn()}
|
|
onError={vi.fn()}
|
|
onResult={onResult}
|
|
/>,
|
|
);
|
|
|
|
await act(async () => {
|
|
await flushPromises();
|
|
});
|
|
|
|
expect(onResult).toHaveBeenCalledWith(
|
|
'https://www.genarrative.world/works/detail?work=PZ-1',
|
|
);
|
|
expect(getUserMedia).not.toHaveBeenCalled();
|
|
});
|
|
|
|
test('closes scanner without browser fallback when native scan is cancelled', async () => {
|
|
const getUserMedia = vi.fn();
|
|
const onClose = vi.fn();
|
|
vi.spyOn(hostBridgeServices, 'scanHostQrCode').mockResolvedValue(null);
|
|
installMediaDevices(getUserMedia);
|
|
|
|
render(
|
|
<PlatformProfileQrScannerModal
|
|
error={null}
|
|
result={null}
|
|
onClose={onClose}
|
|
onError={vi.fn()}
|
|
onResult={vi.fn()}
|
|
/>,
|
|
);
|
|
|
|
await act(async () => {
|
|
await flushPromises();
|
|
});
|
|
|
|
expect(onClose).toHaveBeenCalledTimes(1);
|
|
expect(getUserMedia).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
function buildStream(tracks: MockTrack[]): MockStream {
|
|
return {
|
|
getTracks: () => tracks,
|
|
};
|
|
}
|
|
|
|
function installMediaDevices(getUserMedia: ReturnType<typeof vi.fn>) {
|
|
Object.defineProperty(globalThis.navigator, 'mediaDevices', {
|
|
configurable: true,
|
|
value: { getUserMedia },
|
|
});
|
|
}
|
|
|
|
function installBarcodeDetector(detect: ReturnType<typeof vi.fn>) {
|
|
class MockBarcodeDetector {
|
|
detect = detect;
|
|
}
|
|
|
|
(
|
|
globalThis as typeof globalThis & {
|
|
BarcodeDetector?: unknown;
|
|
}
|
|
).BarcodeDetector = MockBarcodeDetector;
|
|
}
|
|
|
|
async function flushPromises() {
|
|
await Promise.resolve();
|
|
}
|