From 7fc0014d66e3d920b77dff00e440bc2471762eb1 Mon Sep 17 00:00:00 2001 From: Stephen Zhou <38493346+hyoban@users.noreply.github.com> Date: Thu, 19 Mar 2026 11:18:28 +0800 Subject: [PATCH] tweaks --- .../invite-settings/__tests__/page.spec.tsx | 111 ++++++++++++++++++ web/app/signin/invite-settings/page.tsx | 10 +- 2 files changed, 118 insertions(+), 3 deletions(-) create mode 100644 web/app/signin/invite-settings/__tests__/page.spec.tsx diff --git a/web/app/signin/invite-settings/__tests__/page.spec.tsx b/web/app/signin/invite-settings/__tests__/page.spec.tsx new file mode 100644 index 0000000000..a13fba55a3 --- /dev/null +++ b/web/app/signin/invite-settings/__tests__/page.spec.tsx @@ -0,0 +1,111 @@ +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import InviteSettingsPage from '../page' + +const mockReplace = vi.fn() +const mockRefetch = vi.fn() +const mockActivateMember = vi.fn() +const mockSetLocaleOnClient = vi.fn() +const mockResolvePostLoginRedirect = vi.fn() + +let mockInviteToken = 'invite-token' +let mockCheckRes: { + is_valid: boolean + data: { + workspace_name: string + email: string + workspace_id: string + } +} | undefined + +vi.mock('@/next/navigation', () => ({ + useRouter: () => ({ + replace: mockReplace, + }), + useSearchParams: () => ({ + get: (key: string) => key === 'invite_token' ? mockInviteToken : null, + }), +})) + +vi.mock('@/context/global-public-context', () => ({ + useGlobalPublicStore: (selector: (state: { systemFeatures: { branding: { enabled: boolean } } }) => unknown) => + selector({ + systemFeatures: { + branding: { + enabled: true, + }, + }, + }), +})) + +vi.mock('@/service/use-common', () => ({ + useInvitationCheck: () => ({ + data: mockCheckRes, + refetch: mockRefetch, + }), +})) + +vi.mock('@/service/common', () => ({ + activateMember: (...args: unknown[]) => mockActivateMember(...args), +})) + +vi.mock('@/i18n-config', () => ({ + setLocaleOnClient: (...args: unknown[]) => mockSetLocaleOnClient(...args), +})) + +vi.mock('../../utils/post-login-redirect', () => ({ + resolvePostLoginRedirect: () => mockResolvePostLoginRedirect(), +})) + +describe('InviteSettingsPage', () => { + beforeEach(() => { + vi.clearAllMocks() + mockInviteToken = 'invite-token' + mockCheckRes = undefined + mockActivateMember.mockResolvedValue({ result: 'success' }) + mockSetLocaleOnClient.mockResolvedValue(undefined) + mockResolvePostLoginRedirect.mockReturnValue('/apps') + }) + + describe('Activation Gating', () => { + it('should not activate when invitation validation is still pending and Enter is pressed', () => { + render() + + const nameInput = screen.getByLabelText('login.name') + fireEvent.change(nameInput, { target: { value: 'Alice' } }) + fireEvent.keyDown(nameInput, { key: 'Enter', code: 'Enter', charCode: 13 }) + + expect(mockActivateMember).not.toHaveBeenCalled() + }) + + it('should activate when invitation validation has succeeded', async () => { + mockCheckRes = { + is_valid: true, + data: { + workspace_name: 'Demo Workspace', + email: 'alice@example.com', + workspace_id: 'workspace-1', + }, + } + + render() + + const nameInput = screen.getByLabelText('login.name') + fireEvent.change(nameInput, { target: { value: 'Alice' } }) + fireEvent.keyDown(nameInput, { key: 'Enter', code: 'Enter', charCode: 13 }) + + await waitFor(() => { + expect(mockActivateMember).toHaveBeenCalledWith({ + url: '/activate', + body: { + token: 'invite-token', + name: 'Alice', + interface_language: 'en-US', + timezone: expect.any(String), + }, + }) + }) + expect(mockSetLocaleOnClient).toHaveBeenCalledWith('en-US', false) + expect(mockReplace).toHaveBeenCalledWith('/apps') + }) + }) +}) diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index 16c398ad24..fd4c805f19 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -36,9 +36,12 @@ export default function InviteSettingsPage() { }, } const { data: checkRes, refetch: recheck } = useInvitationCheck(checkParams.params, !!token) + const canActivate = checkRes?.is_valid === true const handleActivate = useCallback(async () => { try { + if (!canActivate) + return if (!name) { Toast.notify({ type: 'error', message: t('enterYourName', { ns: 'login' }) }) return @@ -62,7 +65,7 @@ export default function InviteSettingsPage() { catch { recheck() } - }, [language, name, recheck, timezone, token, router, t]) + }, [canActivate, language, name, recheck, timezone, token, router, t]) if (checkRes?.is_valid === false) { return ( @@ -104,7 +107,8 @@ export default function InviteSettingsPage() { if (e.key === 'Enter') { e.preventDefault() e.stopPropagation() - handleActivate() + if (canActivate) + handleActivate() } }} /> @@ -144,7 +148,7 @@ export default function InviteSettingsPage() { variant="primary" className="w-full" onClick={handleActivate} - disabled={!checkRes?.is_valid} + disabled={!canActivate} > {`${t('join', { ns: 'login' })} ${checkRes?.data?.workspace_name ?? ''}`}