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..c438cd854d --- /dev/null +++ b/web/app/signin/invite-settings/__tests__/page.spec.tsx @@ -0,0 +1,113 @@ +import type { MockedFunction } from 'vitest' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { useLocale } from '@/context/i18n' +import { useRouter, useSearchParams } from '@/next/navigation' +import { activateMember } from '@/service/common' +import { useInvitationCheck } from '@/service/use-common' +import { getBrowserTimezone } from '@/utils/timezone' +import InviteSettingsPage from '../page' + +vi.mock('@tanstack/react-query', async () => { + const actual = await vi.importActual('@tanstack/react-query') + return { + ...actual, + useSuspenseQuery: vi.fn(() => ({ + data: { + branding: { + enabled: true, + }, + }, + })), + } +}) + +vi.mock('@/context/i18n', () => ({ + useLocale: vi.fn(), +})) + +vi.mock('@/i18n-config', () => ({ + setLocaleOnClient: vi.fn(() => Promise.resolve()), +})) + +vi.mock('@/next/navigation', () => ({ + useRouter: vi.fn(), + useSearchParams: vi.fn(), +})) + +vi.mock('@/service/common', () => ({ + activateMember: vi.fn(), +})) + +vi.mock('@/service/use-common', () => ({ + useInvitationCheck: vi.fn(), +})) + +vi.mock('@/utils/timezone', () => ({ + getBrowserTimezone: vi.fn(), + timezones: [ + { value: 'Asia/Shanghai', name: 'Asia/Shanghai' }, + { value: 'America/Los_Angeles', name: 'America/Los_Angeles' }, + ], +})) + +vi.mock('../utils/post-login-redirect', () => ({ + resolvePostLoginRedirect: vi.fn(() => null), +})) + +const mockReplace = vi.fn() +const mockRefetch = vi.fn() + +const mockUseLocale = useLocale as unknown as MockedFunction +const mockUseRouter = useRouter as unknown as MockedFunction +const mockUseSearchParams = useSearchParams as unknown as MockedFunction +const mockActivateMember = activateMember as unknown as MockedFunction +const mockUseInvitationCheck = useInvitationCheck as unknown as MockedFunction +const mockGetBrowserTimezone = getBrowserTimezone as unknown as MockedFunction + +describe('InviteSettingsPage', () => { + beforeEach(() => { + vi.clearAllMocks() + mockUseLocale.mockReturnValue('zh-Hans') + mockUseRouter.mockReturnValue({ replace: mockReplace } as unknown as ReturnType) + mockUseSearchParams.mockReturnValue( + new URLSearchParams('invite_token=invite-token') as unknown as ReturnType, + ) + mockUseInvitationCheck.mockReturnValue({ + data: { + is_valid: true, + data: { + workspace_name: 'Acme', + workspace_id: 'workspace-id', + email: 'invitee@example.com', + }, + }, + refetch: mockRefetch, + } as unknown as ReturnType) + mockGetBrowserTimezone.mockReturnValue('Asia/Shanghai') + mockActivateMember.mockResolvedValue({ result: 'success' }) + }) + + describe('Activation payload', () => { + it('should default language to the current UI locale', async () => { + render() + + fireEvent.change(screen.getByLabelText('login.name'), { + target: { value: 'Invitee' }, + }) + fireEvent.click(screen.getByRole('button', { name: 'login.join Acme' })) + + await waitFor(() => { + expect(mockActivateMember).toHaveBeenCalledWith({ + url: '/activate', + body: { + token: 'invite-token', + name: 'Invitee', + interface_language: 'zh-Hans', + timezone: 'Asia/Shanghai', + }, + }) + }) + }) + }) +}) diff --git a/web/app/signin/invite-settings/page.tsx b/web/app/signin/invite-settings/page.tsx index 6ec39d0e83..9acc09cb1c 100644 --- a/web/app/signin/invite-settings/page.tsx +++ b/web/app/signin/invite-settings/page.tsx @@ -11,6 +11,7 @@ import { useTranslation } from 'react-i18next' import Input from '@/app/components/base/input' import Loading from '@/app/components/base/loading' import { LICENSE_LINK } from '@/constants/link' +import { useLocale } from '@/context/i18n' import { setLocaleOnClient } from '@/i18n-config' import { languages, LanguagesSupported } from '@/i18n-config/language' import Link from '@/next/link' @@ -43,14 +44,22 @@ const TIMEZONE_OPTIONS: TimezoneSelectOption[] = timezones.map(item => ({ name: item.name, })) +const getInitialLanguage = (locale: Locale): Locale => { + if (LANGUAGE_OPTIONS.some(item => item.value === locale)) + return locale + + return LanguagesSupported[0]! +} + export default function InviteSettingsPage() { const { t } = useTranslation() const { data: systemFeatures } = useSuspenseQuery(systemFeaturesQueryOptions()) const router = useRouter() const searchParams = useSearchParams() const token = decodeURIComponent(searchParams.get('invite_token') as string) + const locale = useLocale() const [name, setName] = useState('') - const [language, setLanguage] = useState(LanguagesSupported[0]) + const [language, setLanguage] = useState(() => getInitialLanguage(locale)) const [timezone, setTimezone] = useState(() => getBrowserTimezone() || 'America/Los_Angeles') const selectedLanguage = LANGUAGE_OPTIONS.find(item => item.value === language) const selectedTimezone = TIMEZONE_OPTIONS.find(item => item.value === timezone)