From cd479da0eb9f0a210f19195952ff9fc162252744 Mon Sep 17 00:00:00 2001 From: liuyuanchuang Date: Fri, 6 Mar 2026 15:01:34 +0800 Subject: [PATCH] optimize register error tip --- src/App.tsx | 16 ---- src/components/AuthModal.tsx | 82 +++++++++++++++++---- src/components/__tests__/App.test.tsx | 50 ++++++++++++- src/components/__tests__/AuthModal.test.tsx | 57 +++++++------- src/lib/translations.ts | 6 ++ 5 files changed, 149 insertions(+), 62 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 9b0dbd5..ead6205 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -239,10 +239,6 @@ function App() { } }); - // Auto-select first file if none selected - if (!selectedFileId) { - setSelectedFileId(fileRecords[0].id); - } } else { setFiles([]); } @@ -569,18 +565,6 @@ function App() { )} - - {/* ICP Footer */} -
- - 京ICP备2025152973号 - -
); } diff --git a/src/components/AuthModal.tsx b/src/components/AuthModal.tsx index 590f5d7..04e04d6 100644 --- a/src/components/AuthModal.tsx +++ b/src/components/AuthModal.tsx @@ -17,6 +17,7 @@ export default function AuthModal({ onClose, mandatory = false }: AuthModalProps const [password, setPassword] = useState(''); const [confirmPassword, setConfirmPassword] = useState(''); const [localError, setLocalError] = useState(''); + const [fieldErrors, setFieldErrors] = useState<{ email?: string; password?: string; confirmPassword?: string }>({}); const isBusy = useMemo( () => ['email_signing_in', 'email_signing_up', 'oauth_redirecting', 'oauth_exchanging'].includes(authPhase), @@ -28,20 +29,37 @@ export default function AuthModal({ onClose, mandatory = false }: AuthModalProps const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setLocalError(''); + const nextFieldErrors: { email?: string; password?: string; confirmPassword?: string } = {}; + const normalizedEmail = email.trim(); + + if (!normalizedEmail) { + nextFieldErrors.email = t.auth.emailRequired; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(normalizedEmail)) { + nextFieldErrors.email = t.auth.emailInvalid; + } + + if (!password) { + nextFieldErrors.password = t.auth.passwordRequired; + } if (mode === 'signup') { - if (password.length < 6) { - setLocalError(t.auth.passwordHint); - return; + if (password && password.length < 6) { + nextFieldErrors.password = t.auth.passwordHint; } - if (password !== confirmPassword) { - setLocalError(t.auth.passwordMismatch); - return; + if (!confirmPassword) { + nextFieldErrors.confirmPassword = t.auth.passwordRequired; + } else if (password !== confirmPassword) { + nextFieldErrors.confirmPassword = t.auth.passwordMismatch; } } - const result = mode === 'signup' ? await signUp(email, password) : await signIn(email, password); + setFieldErrors(nextFieldErrors); + if (Object.keys(nextFieldErrors).length > 0) { + return; + } + + const result = mode === 'signup' ? await signUp(normalizedEmail, password) : await signIn(normalizedEmail, password); if (!result.error) { onClose(); @@ -75,7 +93,11 @@ export default function AuthModal({ onClose, mandatory = false }: AuthModalProps
-
+
)} diff --git a/src/components/__tests__/App.test.tsx b/src/components/__tests__/App.test.tsx index 39fb1cc..0c1c7ee 100644 --- a/src/components/__tests__/App.test.tsx +++ b/src/components/__tests__/App.test.tsx @@ -1,7 +1,8 @@ -import { fireEvent, render, screen } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import App from '../../App'; +import { uploadService } from '../../lib/uploadService'; const { useAuthMock } = vi.hoisted(() => ({ useAuthMock: vi.fn(), @@ -57,7 +58,7 @@ vi.mock('../../components/LeftSidebar', () => ({ })); vi.mock('../../components/FilePreview', () => ({ - default: () =>
preview
, + default: ({ file }: { file: { id: string } | null }) =>
{file ? `preview:${file.id}` : 'preview-empty'}
, })); vi.mock('../../components/ResultPanel', () => ({ @@ -110,3 +111,48 @@ describe('App anonymous usage limit', () => { expect(screen.queryByText('upload-modal')).not.toBeInTheDocument(); }); }); + +describe('App initial selection', () => { + beforeEach(() => { + vi.clearAllMocks(); + localStorage.clear(); + localStorage.setItem('hasSeenGuide', 'true'); + }); + + it('does not auto-select the first history record on initial load', async () => { + useAuthMock.mockReturnValue({ + user: { id: 'u1' }, + initializing: false, + }); + + vi.mocked(uploadService.getTaskList).mockResolvedValue({ + total: 1, + task_list: [ + { + task_id: 'task-1', + file_name: 'sample.png', + status: 2, + origin_url: 'https://example.com/sample.png', + task_type: 'FORMULA', + created_at: '2026-03-06T00:00:00Z', + latex: '', + markdown: 'content', + mathml: '', + mml: '', + image_blob: '', + docx_url: '', + pdf_url: '', + }, + ], + }); + + render(); + + await waitFor(() => { + expect(uploadService.getTaskList).toHaveBeenCalled(); + }); + + expect(screen.getByText('preview-empty')).toBeInTheDocument(); + expect(screen.queryByText('preview:task-1')).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/__tests__/AuthModal.test.tsx b/src/components/__tests__/AuthModal.test.tsx index c973246..944db69 100644 --- a/src/components/__tests__/AuthModal.test.tsx +++ b/src/components/__tests__/AuthModal.test.tsx @@ -4,6 +4,9 @@ import { describe, expect, it, vi } from 'vitest'; import AuthModal from '../AuthModal'; const useAuthMock = vi.fn(); +const signInMock = vi.fn().mockResolvedValue({ error: null }); +const signUpMock = vi.fn().mockResolvedValue({ error: null }); +const beginGoogleOAuthMock = vi.fn().mockResolvedValue({ error: null }); vi.mock('../../contexts/AuthContext', () => ({ useAuth: () => useAuthMock(), @@ -28,21 +31,37 @@ vi.mock('../../contexts/LanguageContext', () => ({ passwordHint: '密码至少 6 位,建议使用字母和数字组合。', confirmPassword: '确认密码', passwordMismatch: '两次输入的密码不一致。', + emailRequired: '请输入邮箱地址。', + emailInvalid: '请输入有效的邮箱地址。', + passwordRequired: '请输入密码。', oauthRedirecting: '正在跳转 Google...', }, }, }), })); +const createAuthState = (overrides?: Record) => ({ + signIn: signInMock, + signUp: signUpMock, + beginGoogleOAuth: beginGoogleOAuthMock, + authPhase: 'idle', + authError: null, + ...overrides, +}); + describe('AuthModal', () => { + it('shows email required message for empty signin submit', async () => { + useAuthMock.mockReturnValue(createAuthState()); + render(); + + fireEvent.click(document.querySelector('button[type="submit"]') as HTMLButtonElement); + + expect(await screen.findByText('请输入邮箱地址。')).toBeInTheDocument(); + expect(signInMock).not.toHaveBeenCalled(); + }); + it('renders google oauth button', () => { - useAuthMock.mockReturnValue({ - signIn: vi.fn().mockResolvedValue({ error: null }), - signUp: vi.fn().mockResolvedValue({ error: null }), - beginGoogleOAuth: vi.fn().mockResolvedValue({ error: null }), - authPhase: 'idle', - authError: null, - }); + useAuthMock.mockReturnValue(createAuthState()); render(); @@ -50,13 +69,7 @@ describe('AuthModal', () => { }); it('disables inputs and submit while oauth redirecting', () => { - useAuthMock.mockReturnValue({ - signIn: vi.fn().mockResolvedValue({ error: null }), - signUp: vi.fn().mockResolvedValue({ error: null }), - beginGoogleOAuth: vi.fn().mockResolvedValue({ error: null }), - authPhase: 'oauth_redirecting', - authError: null, - }); + useAuthMock.mockReturnValue(createAuthState({ authPhase: 'oauth_redirecting' })); render(); @@ -68,13 +81,7 @@ describe('AuthModal', () => { }); it('switches between signin and signup with segmented tabs', () => { - useAuthMock.mockReturnValue({ - signIn: vi.fn().mockResolvedValue({ error: null }), - signUp: vi.fn().mockResolvedValue({ error: null }), - beginGoogleOAuth: vi.fn().mockResolvedValue({ error: null }), - authPhase: 'idle', - authError: null, - }); + useAuthMock.mockReturnValue(createAuthState()); render(); @@ -85,13 +92,7 @@ describe('AuthModal', () => { }); it('shows friendlier signup guidance', () => { - useAuthMock.mockReturnValue({ - signIn: vi.fn().mockResolvedValue({ error: null }), - signUp: vi.fn().mockResolvedValue({ error: null }), - beginGoogleOAuth: vi.fn().mockResolvedValue({ error: null }), - authPhase: 'idle', - authError: null, - }); + useAuthMock.mockReturnValue(createAuthState()); render(); diff --git a/src/lib/translations.ts b/src/lib/translations.ts index c413c1d..a737362 100644 --- a/src/lib/translations.ts +++ b/src/lib/translations.ts @@ -62,6 +62,9 @@ export const translations = { noAccount: 'No account? Register', continueWithGoogle: 'Google', emailHint: 'Used only for sign-in and history sync.', + emailRequired: 'Please enter your email address.', + emailInvalid: 'Please enter a valid email address.', + passwordRequired: 'Please enter your password.', passwordHint: 'Use at least 6 characters. Letters and numbers are recommended.', confirmPassword: 'Confirm Password', passwordMismatch: 'The two passwords do not match.', @@ -167,6 +170,9 @@ export const translations = { noAccount: '没有账号?去注册', continueWithGoogle: 'Google', emailHint: '仅用于登录和同步记录。', + emailRequired: '请输入邮箱地址。', + emailInvalid: '请输入有效的邮箱地址。', + passwordRequired: '请输入密码。', passwordHint: '密码至少 6 位,建议使用字母和数字组合。', confirmPassword: '确认密码', passwordMismatch: '两次输入的密码不一致。',