新增 PlatformStatusDialog 统一支付结果与确认中状态弹层 新增 PlatformProfileQrScannerModal 统一个人中心扫码面板 RpgEntryHomeView 改用共享组件并删除内联支付与扫码实现 更新 PlatformUiKit 收口文档与团队决策记录
145 lines
3.5 KiB
TypeScript
145 lines
3.5 KiB
TypeScript
/* @vitest-environment jsdom */
|
|
|
|
import { act, render, screen } from '@testing-library/react';
|
|
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
|
|
|
|
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 () => {
|
|
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 () => {
|
|
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();
|
|
});
|
|
});
|
|
|
|
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();
|
|
}
|