收口个人中心状态弹层与扫码组件

新增 PlatformStatusDialog 统一支付结果与确认中状态弹层
新增 PlatformProfileQrScannerModal 统一个人中心扫码面板
RpgEntryHomeView 改用共享组件并删除内联支付与扫码实现
更新 PlatformUiKit 收口文档与团队决策记录
This commit is contained in:
2026-06-10 20:24:09 +08:00
parent 914b74ce8e
commit f54c3ee936
7 changed files with 583 additions and 260 deletions

View File

@@ -0,0 +1,144 @@
/* @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();
}