1
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
/* @vitest-environment jsdom */
|
||||
|
||||
import { render, screen, waitFor, within } from '@testing-library/react';
|
||||
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';
|
||||
@@ -113,6 +114,19 @@ function ProtectedActionButton({ onAuthenticated }: { onAuthenticated: () => voi
|
||||
);
|
||||
}
|
||||
|
||||
function PlatformTabStateProbe() {
|
||||
const [tab, setTab] = useState<'home' | 'create'>('home');
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div>当前Tab:{tab === 'home' ? '首页' : '创作'}</div>
|
||||
<button type="button" onClick={() => setTab('create')}>
|
||||
创作
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
test('auth gate keeps platform content visible when phone login is available', async () => {
|
||||
authMocks.getAuthLoginOptions.mockResolvedValue({
|
||||
availableLoginMethods: ['phone'],
|
||||
@@ -208,6 +222,48 @@ test('auth gate opens a login modal for protected actions and resumes after logi
|
||||
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(
|
||||
<AuthGate>
|
||||
<PlatformTabStateProbe />
|
||||
</AuthGate>,
|
||||
);
|
||||
|
||||
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<string>((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();
|
||||
|
||||
@@ -239,7 +295,7 @@ test('auth gate shows sms send feedback in the login modal', async () => {
|
||||
});
|
||||
|
||||
expect(
|
||||
within(dialog).getByText('验证码已发送,有效期约 5 分钟。'),
|
||||
within(dialog).getByText('短信请求已提交,请留意手机短信。验证码有效期约 5 分钟。'),
|
||||
).toBeTruthy();
|
||||
expect(within(dialog).getByRole('button', { name: '60s' })).toBeTruthy();
|
||||
});
|
||||
|
||||
@@ -90,10 +90,19 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
const [changePhoneCaptchaChallenge, setChangePhoneCaptchaChallenge] =
|
||||
useState<AuthCaptchaChallenge | null>(null);
|
||||
const pendingProtectedActionRef = useRef<(() => void) | null>(null);
|
||||
const readyUser = status === 'ready' ? user : null;
|
||||
const hasRenderedPlatformContentRef = useRef(false);
|
||||
const canKeepPlatformContentMounted =
|
||||
hasRenderedPlatformContentRef.current &&
|
||||
(status === 'checking' || status === 'recovering');
|
||||
const readyUser =
|
||||
status === 'ready' || canKeepPlatformContentMounted ? user : null;
|
||||
const settings = useGameSettings(readyUser?.id ?? null);
|
||||
const platformThemeClass = `platform-theme--${settings.platformTheme}`;
|
||||
|
||||
if (status === 'ready' || status === 'unauthenticated') {
|
||||
hasRenderedPlatformContentRef.current = true;
|
||||
}
|
||||
|
||||
const activateReadyUser = useCallback((nextUser: AuthUser) => {
|
||||
// 受保护业务 hook 只在 readyUser 暴露后启动,必须先保证请求层能带 Bearer token。
|
||||
setUser(nextUser);
|
||||
@@ -380,6 +389,9 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
const authUiValue = useMemo(
|
||||
() => ({
|
||||
user: readyUser,
|
||||
// 平台内容在 checking/recovering 阶段可以继续挂载,避免闪烁;
|
||||
// 但受保护请求只能在真实 ready 且存在用户时再启动。
|
||||
canAccessProtectedData: status === 'ready' && Boolean(readyUser),
|
||||
openLoginModal,
|
||||
requireAuth,
|
||||
openSettingsModal,
|
||||
@@ -402,6 +414,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
openSettingsModal,
|
||||
readyUser,
|
||||
requireAuth,
|
||||
status,
|
||||
settings.isHydratingSettings,
|
||||
settings.isPersistingSettings,
|
||||
settings.musicVolume,
|
||||
@@ -412,7 +425,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
],
|
||||
);
|
||||
|
||||
if (status === 'checking') {
|
||||
if (status === 'checking' && !canKeepPlatformContentMounted) {
|
||||
return (
|
||||
<div className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] text-sm text-[var(--platform-text-base)]`}>
|
||||
正在校验登录状态...
|
||||
@@ -420,7 +433,7 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (status === 'recovering') {
|
||||
if (status === 'recovering' && !canKeepPlatformContentMounted) {
|
||||
return (
|
||||
<div className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] text-sm text-[var(--platform-text-base)]`}>
|
||||
正在自动创建或恢复账号...
|
||||
@@ -485,7 +498,11 @@ export function AuthGate({ children }: AuthGateProps) {
|
||||
);
|
||||
}
|
||||
|
||||
if (status !== 'ready' && status !== 'unauthenticated') {
|
||||
if (
|
||||
status !== 'ready' &&
|
||||
status !== 'unauthenticated' &&
|
||||
!canKeepPlatformContentMounted
|
||||
) {
|
||||
return (
|
||||
<div className={`platform-theme ${platformThemeClass} flex min-h-screen items-center justify-center bg-[var(--platform-body-fill)] px-6 text-[var(--platform-text-base)]`}>
|
||||
<div className="platform-auth-card max-w-md rounded-3xl px-6 py-7 text-center">
|
||||
|
||||
@@ -12,6 +12,7 @@ export type PlatformSettingsSection =
|
||||
|
||||
type AuthUiContextValue = {
|
||||
user: AuthUser | null;
|
||||
canAccessProtectedData: boolean;
|
||||
openLoginModal: (postLoginAction?: (() => void) | null) => void;
|
||||
requireAuth: (action: () => void) => void;
|
||||
openSettingsModal: (section?: PlatformSettingsSection) => void;
|
||||
|
||||
Reference in New Issue
Block a user