/* @vitest-environment jsdom */ import { act, render, screen, waitFor, within } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { useState } from 'react'; import { beforeEach, expect, test, vi } from 'vitest'; import type { AuthUser } from '../../services/authService'; import { AuthGate } from './AuthGate'; import { useAuthUi } from './AuthUiContext'; const authMocks = vi.hoisted(() => ({ ensureStoredAccessToken: vi.fn(), ensureAutoAuthUser: vi.fn(), getAuthLoginOptions: vi.fn(), getCurrentAuthUser: vi.fn(), loginWithPhoneCode: vi.fn(), sendPhoneLoginCode: vi.fn(), startWechatLogin: vi.fn(), consumeAuthCallbackResult: vi.fn(), })); vi.mock('../../services/apiClient', () => ({ AUTH_STATE_EVENT: 'genarrative-auth-state-changed', ensureStoredAccessToken: authMocks.ensureStoredAccessToken, })); vi.mock('../../services/authService', () => ({ bindWechatPhone: vi.fn(), changePhoneNumber: vi.fn(), consumeAuthCallbackResult: authMocks.consumeAuthCallbackResult, ensureAutoAuthUser: authMocks.ensureAutoAuthUser, getAuthAuditLogs: vi.fn(), getAuthLoginOptions: authMocks.getAuthLoginOptions, getAuthRiskBlocks: vi.fn(), getCurrentAuthUser: authMocks.getCurrentAuthUser, getAuthSessions: vi.fn(), getCaptchaChallengeFromError: vi.fn(() => null), liftAuthRiskBlock: vi.fn(), loginWithPhoneCode: authMocks.loginWithPhoneCode, logoutAllAuthSessions: vi.fn(), logoutAuthUser: vi.fn(), revokeAuthSession: vi.fn(), sendPhoneLoginCode: authMocks.sendPhoneLoginCode, startWechatLogin: authMocks.startWechatLogin, })); vi.mock('../../hooks/useGameSettings', () => ({ useGameSettings: () => ({ musicVolume: 0.42, setMusicVolume: () => {}, platformTheme: 'light', setPlatformTheme: () => {}, hasHydratedSettings: true, isHydratingSettings: false, isPersistingSettings: false, settingsError: null, }), })); vi.mock('./AccountModal', () => ({ AccountModal: () => null, })); vi.mock('./BindPhoneScreen', () => ({ BindPhoneScreen: () =>
绑定手机号
, })); const mockUser: AuthUser = { id: 'user-1', username: 'tester', displayName: '测试玩家', publicUserCode: 'user-tester', phoneNumberMasked: '138****8000', loginMethod: 'phone', bindingStatus: 'active', wechatBound: false, }; beforeEach(() => { vi.clearAllMocks(); authMocks.consumeAuthCallbackResult.mockReturnValue(null); authMocks.ensureStoredAccessToken.mockResolvedValue('jwt-existing-token'); authMocks.getCurrentAuthUser.mockResolvedValue({ user: null, availableLoginMethods: ['phone'], }); authMocks.loginWithPhoneCode.mockResolvedValue(mockUser); authMocks.sendPhoneLoginCode.mockResolvedValue({ cooldownSeconds: 60, expiresInSeconds: 300, }); authMocks.startWechatLogin.mockResolvedValue(undefined); authMocks.ensureAutoAuthUser.mockResolvedValue({ user: mockUser, credentials: { username: 'guest_tester', password: 'auto_password', }, }); }); function ProtectedActionButton({ onAuthenticated }: { onAuthenticated: () => void }) { const authUi = useAuthUi(); return ( ); } function PlatformTabStateProbe() { const [tab, setTab] = useState<'home' | 'create'>('home'); return (
当前Tab:{tab === 'home' ? '首页' : '创作'}
); } test('auth gate keeps platform content visible when phone login is available', async () => { authMocks.getAuthLoginOptions.mockResolvedValue({ availableLoginMethods: ['phone'], }); render(
应用内容
, ); expect(await screen.findByText('应用内容')).toBeTruthy(); expect(screen.queryByRole('button', { name: '登录' })).toBeNull(); expect(screen.queryByText('先登录账号,再同步你的冒险进度。')).toBeNull(); expect(authMocks.ensureAutoAuthUser).not.toHaveBeenCalled(); }); test('auth gate waits for access token refresh before exposing restored user content', async () => { let resolveToken!: (token: string) => void; const tokenPromise = new Promise((resolve) => { resolveToken = resolve; }); authMocks.ensureStoredAccessToken.mockReturnValue(tokenPromise); authMocks.getCurrentAuthUser.mockResolvedValue({ user: mockUser, availableLoginMethods: ['phone'], }); render(
应用内容
, ); expect(screen.getByText('正在校验登录状态...')).toBeTruthy(); expect(authMocks.getCurrentAuthUser).not.toHaveBeenCalled(); resolveToken('jwt-restored-token'); expect(await screen.findByText('应用内容')).toBeTruthy(); expect(authMocks.ensureStoredAccessToken).toHaveBeenCalledTimes(1); expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(1); }); test('auth gate does not auto-create a guest account when dev guest switch is not explicitly enabled', async () => { authMocks.getAuthLoginOptions.mockResolvedValue({ availableLoginMethods: [], }); render(
应用内容
, ); expect(await screen.findByText('应用内容')).toBeTruthy(); expect(authMocks.ensureAutoAuthUser).not.toHaveBeenCalled(); }); test('auth gate opens a login modal for protected actions and resumes after login', async () => { const user = userEvent.setup(); const onAuthenticated = vi.fn(); authMocks.getAuthLoginOptions.mockResolvedValue({ availableLoginMethods: ['phone'], }); render( , ); await user.click(await screen.findByRole('button', { name: '进入作品' })); const dialog = screen.getByRole('dialog', { name: '登录账号' }); expect(dialog).toBeTruthy(); expect(screen.queryByText('先登录账号,再同步你的冒险进度。')).toBeNull(); await user.type(within(dialog).getByLabelText('手机号'), '13800000000'); await user.type(within(dialog).getByLabelText('验证码'), '123456'); await user.click(within(dialog).getByRole('button', { name: '登录' })); await waitFor(() => { expect(authMocks.loginWithPhoneCode).toHaveBeenCalledWith( '13800000000', '123456', ); expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(1); expect(onAuthenticated).toHaveBeenCalledTimes(1); }); expect(screen.queryByRole('dialog', { name: '登录账号' })).toBeNull(); }); test('auth state refresh keeps mounted platform content and local tab state', async () => { const user = userEvent.setup(); authMocks.getCurrentAuthUser.mockResolvedValue({ user: mockUser, availableLoginMethods: ['phone'], }); render( , ); expect(await screen.findByText('当前Tab:首页')).toBeTruthy(); await user.click(screen.getByRole('button', { name: '创作' })); expect(screen.getByText('当前Tab:创作')).toBeTruthy(); let resolveToken!: (token: string) => void; const tokenPromise = new Promise((resolve) => { resolveToken = resolve; }); authMocks.ensureStoredAccessToken.mockReturnValueOnce(tokenPromise); act(() => { window.dispatchEvent(new Event('genarrative-auth-state-changed')); }); expect(screen.queryByText('正在校验登录状态...')).toBeNull(); expect(screen.getByText('当前Tab:创作')).toBeTruthy(); await act(async () => { resolveToken('jwt-refreshed-token'); await tokenPromise; }); await waitFor(() => { expect(authMocks.getCurrentAuthUser).toHaveBeenCalledTimes(2); }); expect(screen.getByText('当前Tab:创作')).toBeTruthy(); }); test('auth gate shows sms send feedback in the login modal', async () => { const user = userEvent.setup(); authMocks.getAuthLoginOptions.mockResolvedValue({ availableLoginMethods: ['phone'], }); render( , ); await user.click(await screen.findByRole('button', { name: '进入作品' })); const dialog = screen.getByRole('dialog', { name: '登录账号' }); await user.type(within(dialog).getByLabelText('手机号'), '13800000000'); await user.click(within(dialog).getByRole('button', { name: '获取验证码' })); await waitFor(() => { expect(authMocks.sendPhoneLoginCode).toHaveBeenCalledWith( '13800000000', 'login', { challengeId: undefined, answer: '', }, ); }); expect( within(dialog).getByText('短信请求已提交,请留意手机短信。验证码有效期约 5 分钟。'), ).toBeTruthy(); expect(within(dialog).getByRole('button', { name: '60s' })).toBeTruthy(); });