Files
Genarrative/apps/ai-game-creator-shell/tests/authFailureReporting.test.tsx
T
k88936 25c5ecdf6e
Project CI / AI game creator shell Rust crates (pull_request) Successful in 2m58s
Project CI / AI game creator shell Rust lane 1/2 (pull_request) Successful in 6m34s
Project CI / AI game creator shell Rust lane 2/2 (pull_request) Successful in 5m29s
Project CI / Backend tests (pull_request) Failing after 4m38s
Project CI / Frontend tests (pull_request) Successful in 3m33s
Project CI / Repository checks (pull_request) Failing after 4m4s
Project CI / AI game creator shell web tests (pull_request) Successful in 3m13s
Project CI / Native shell tests (pull_request) Successful in 6m35s
测试:全仓测试统一 Tauri 运行时替身安装点
- 78 处内联 window.__TAURI__ 赋值改为 installTauriRuntime,覆盖 appSurface 之外的 37 个测试文件
- 31 处 delete window.__TAURI__ / window.__TAURI__ = undefined 复位改为 resetTauriRuntime
- 删除 recentProjectsHook、projectCreationDirectory、projectResourceLiveIntegration、useProjectResourceCardPreviews、resourceVersionReplacementModel、unityProjectOpen 里的本地 asTauriInvoke / asTauriListen 适配器与相关类型别名
- designProjectRestore 的 setup 参数由 ReturnType<typeof vi.fn> 收窄为具体 invoke 签名
- 迁移后仅 tests/tauriRuntimeFake.ts 直接读写 window.__TAURI__
2026-10-03 20:19:45 +08:00

188 lines
6.1 KiB
TypeScript

/** @vitest-environment jsdom */
import {
cleanup,
fireEvent,
render,
screen,
waitFor,
} from '@testing-library/react';
import { createElement } from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';
/**
* 认证失败分流的上报判据(端到端,判据是"这条失败到底有没有进错误池")。
*
* 认证命令与错误上报走的不是同一个 invoke 入口:认证命令来自 `window.__TAURI__.core.invoke`
* (本文件注入替身),错误上报来自 `@tauri-apps/api/core` 的模块级 `invoke`。所以只有 mock 掉
* 模块级 invoke,才能观察到 `report_client_error` 是否真的被调用。
*/
vi.mock('@tauri-apps/api/core', () => ({
invoke: vi.fn(async (command: string, _args?: Record<string, unknown>) => {
if (command === 'get_pending_error_reports') return [];
return undefined;
}),
}));
import { invoke } from '@tauri-apps/api/core';
import { AuthenticatedClient } from '../src/app/AuthenticatedClient';
import { resetPlatformSessionForTests } from '../src/services/platformSession';
import { installTauriRuntime, resetTauriRuntime } from './tauriRuntimeFake';
import { installUnhandledRejectionBridge } from './unhandledRejectionBridge';
type AuthInvoke = (
command: string,
args?: Record<string, unknown>,
) => Promise<unknown>;
function reportCalls() {
return vi
.mocked(invoke)
.mock.calls.filter(([command]) => command === 'report_client_error');
}
function renderLogin(invokeAuth: AuthInvoke) {
installTauriRuntime({ core: { invoke: invokeAuth } });
render(
// React 19 的 createElement 的 children 形参只接受 ReactNode,渲染函数子节点改编进 props。
createElement(AuthenticatedClient, {
children: () => createElement('main', { 'aria-label': '已登录' }),
}),
);
}
async function submitPasswordLogin(invokeAuth: AuthInvoke) {
renderLogin(invokeAuth);
await screen.findByRole('main', { name: '登录' });
fireEvent.click(screen.getByRole('button', { name: '密码登录' }));
fireEvent.change(screen.getByLabelText('手机号'), {
target: { value: '13800000000' },
});
fireEvent.change(screen.getByLabelText('密码'), {
target: { value: 'wrong-password' },
});
fireEvent.click(screen.getByRole('button', { name: '登录' }));
}
describe('认证失败的上报判据', () => {
afterEach(() => {
cleanup();
resetPlatformSessionForTests();
resetTauriRuntime();
vi.mocked(invoke).mockClear();
});
it('业务拒绝(手机号或密码错误)只显示文案,不写错误池', async () => {
await submitPasswordLogin(async (command) => {
if (command === 'read_client_auth_state') {
return { status: 'unauthenticated' };
}
if (command === 'login_client_with_password') {
throw { type: 'phoneOrPasswordMismatch' };
}
return null;
});
expect(await screen.findByText('手机号或密码错误')).not.toBeNull();
expect(reportCalls()).toHaveLength(0);
});
it('服务端 400 的原文经 passwordLoginRejected 透出,仍不写错误池', async () => {
await submitPasswordLogin(async (command) => {
if (command === 'read_client_auth_state') {
return { status: 'unauthenticated' };
}
if (command === 'login_client_with_password') {
throw {
type: 'passwordLoginRejected',
serverMessage: '密码长度需要在 6 到 128 位之间',
};
}
return null;
});
expect(
await screen.findByText('登录失败: 密码长度需要在 6 到 128 位之间'),
).not.toBeNull();
expect(reportCalls()).toHaveLength(0);
});
it('发码限流同样只显示文案,不写错误池', async () => {
renderLogin(async (command) => {
if (command === 'read_client_auth_state') {
return { status: 'unauthenticated' };
}
if (command === 'send_client_phone_login_code') {
throw { type: 'smsCodeThrottled' };
}
return null;
});
await screen.findByRole('main', { name: '登录' });
fireEvent.change(screen.getByLabelText('手机号'), {
target: { value: '13800000000' },
});
fireEvent.click(screen.getByRole('button', { name: '获取验证码' }));
expect(
await screen.findByText('发送验证码过于频繁,请稍后再试'),
).not.toBeNull();
expect(reportCalls()).toHaveLength(0);
});
it('系统变体(网络不可达)原样抛出,经 unhandledrejection 上报一次', async () => {
const uninstall = installUnhandledRejectionBridge();
try {
await submitPasswordLogin(async (command) => {
if (command === 'read_client_auth_state') {
return { status: 'unauthenticated' };
}
if (command === 'login_client_with_password') {
throw { type: 'authNetworkFailure', reason: 'unreachable' };
}
return null;
});
await waitFor(() => expect(reportCalls()).toHaveLength(1));
expect(reportCalls()[0]?.[1]).toMatchObject({
source: 'unhandledrejection',
message: JSON.stringify({
type: 'authNetworkFailure',
reason: 'unreachable',
}),
});
} finally {
uninstall();
}
});
it('Tauri 的意外拒绝形状也被包装后原样抛出上报', async () => {
const uninstall = installUnhandledRejectionBridge();
try {
await submitPasswordLogin(async (command) => {
if (command === 'read_client_auth_state') {
return { status: 'unauthenticated' };
}
if (command === 'login_client_with_password') {
throw new Error('IPC 桥接不可用');
}
return null;
});
await waitFor(() => expect(reportCalls()).toHaveLength(1));
expect(reportCalls()[0]?.[1]).toMatchObject({
source: 'unhandledrejection',
message: 'IPC 桥接不可用',
});
// 映射外的拒绝仍然上报,但界面要给一行泛化提示,不能停在无解释的登录页。
expect(
await screen.findByText(
'登录服务返回了未识别的错误,请重试或提交错误报告',
),
).not.toBeNull();
} finally {
uninstall();
}
});
});