diff --git a/web/app/signup/components/input-mail.spec.tsx b/web/app/signup/components/input-mail.spec.tsx new file mode 100644 index 0000000000..d5acc92153 --- /dev/null +++ b/web/app/signup/components/input-mail.spec.tsx @@ -0,0 +1,158 @@ +import type { MockedFunction } from 'vitest' +import type { SystemFeatures } from '@/types/feature' +import { fireEvent, render, screen, waitFor } from '@testing-library/react' +import * as React from 'react' +import { useGlobalPublicStore } from '@/context/global-public-context' +import { useLocale } from '@/context/i18n' +import { useSendMail } from '@/service/use-common' +import { defaultSystemFeatures } from '@/types/feature' +import Form from './input-mail' + +const mockSubmitMail = vi.fn() +const mockOnSuccess = vi.fn() + +type SystemFeaturesOverrides = Partial> & { + branding?: Partial +} + +const buildSystemFeatures = (overrides: SystemFeaturesOverrides = {}): SystemFeatures => ({ + ...defaultSystemFeatures, + ...overrides, + branding: { + ...defaultSystemFeatures.branding, + ...overrides.branding, + }, +}) + +vi.mock('next/link', () => ({ + default: ({ children, href, className, target, rel }: { children: React.ReactNode, href: string, className?: string, target?: string, rel?: string }) => ( + + {children} + + ), +})) + +vi.mock('@/context/global-public-context', () => ({ + useGlobalPublicStore: vi.fn(), +})) + +vi.mock('@/context/i18n', () => ({ + useLocale: vi.fn(), +})) + +vi.mock('@/service/use-common', () => ({ + useSendMail: vi.fn(), +})) + +type UseSendMailResult = ReturnType + +const mockUseGlobalPublicStore = useGlobalPublicStore as unknown as MockedFunction +const mockUseLocale = useLocale as unknown as MockedFunction +const mockUseSendMail = useSendMail as unknown as MockedFunction + +const renderForm = ({ + brandingEnabled = false, + isPending = false, +}: { + brandingEnabled?: boolean + isPending?: boolean +} = {}) => { + mockUseGlobalPublicStore.mockReturnValue({ + systemFeatures: buildSystemFeatures({ + branding: { enabled: brandingEnabled }, + }), + }) + mockUseLocale.mockReturnValue('en-US') + mockUseSendMail.mockReturnValue({ + mutateAsync: mockSubmitMail, + isPending, + } as unknown as UseSendMailResult) + return render(
) +} + +describe('InputMail Form', () => { + beforeEach(() => { + vi.clearAllMocks() + mockSubmitMail.mockResolvedValue({ result: 'success', data: 'token' }) + }) + + // Rendering baseline UI elements. + describe('Rendering', () => { + it('should render email input and submit button', () => { + renderForm() + + expect(screen.getByLabelText('login.email')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'login.signup.verifyMail' })).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'login.signup.signIn' })).toBeInTheDocument() + }) + }) + + // Prop-driven branding content visibility. + describe('Props', () => { + it('should show terms links when branding is disabled', () => { + renderForm({ brandingEnabled: false }) + + expect(screen.getByRole('link', { name: 'login.tos' })).toBeInTheDocument() + expect(screen.getByRole('link', { name: 'login.pp' })).toBeInTheDocument() + }) + + it('should hide terms links when branding is enabled', () => { + renderForm({ brandingEnabled: true }) + + expect(screen.queryByRole('link', { name: 'login.tos' })).not.toBeInTheDocument() + expect(screen.queryByRole('link', { name: 'login.pp' })).not.toBeInTheDocument() + }) + }) + + // Submission flow and mutation integration. + describe('User Interactions', () => { + it('should submit email and call onSuccess when mutation succeeds', async () => { + renderForm() + const input = screen.getByLabelText('login.email') + const button = screen.getByRole('button', { name: 'login.signup.verifyMail' }) + + fireEvent.change(input, { target: { value: 'test@example.com' } }) + fireEvent.click(button) + + expect(mockSubmitMail).toHaveBeenCalledWith({ + email: 'test@example.com', + language: 'en-US', + }) + + await waitFor(() => { + expect(mockOnSuccess).toHaveBeenCalledWith('test@example.com', 'token') + }) + }) + }) + + // Validation and failure paths. + describe('Edge Cases', () => { + it('should block submission when email is invalid', () => { + const { container } = renderForm() + const form = container.querySelector('form') + const input = screen.getByLabelText('login.email') + + fireEvent.change(input, { target: { value: 'invalid-email' } }) + expect(form).not.toBeNull() + fireEvent.submit(form as HTMLFormElement) + + expect(mockSubmitMail).not.toHaveBeenCalled() + expect(mockOnSuccess).not.toHaveBeenCalled() + }) + + it('should not call onSuccess when mutation does not succeed', async () => { + mockSubmitMail.mockResolvedValue({ result: 'failed', data: 'token' }) + renderForm() + const input = screen.getByLabelText('login.email') + const button = screen.getByRole('button', { name: 'login.signup.verifyMail' }) + + fireEvent.change(input, { target: { value: 'test@example.com' } }) + fireEvent.click(button) + + await waitFor(() => { + expect(mockSubmitMail).toHaveBeenCalled() + }) + expect(mockOnSuccess).not.toHaveBeenCalled() + }) + }) +}) diff --git a/web/app/signup/components/input-mail.tsx b/web/app/signup/components/input-mail.tsx index 6342e7909c..1b88007ce4 100644 --- a/web/app/signup/components/input-mail.tsx +++ b/web/app/signup/components/input-mail.tsx @@ -1,6 +1,5 @@ 'use client' import type { MailSendResponse } from '@/service/use-common' -import { noop } from 'es-toolkit/function' import Link from 'next/link' import { useCallback, useState } from 'react' import { useTranslation } from 'react-i18next' @@ -27,6 +26,9 @@ export default function Form({ const { mutateAsync: submitMail, isPending } = useSendMail() const handleSubmit = useCallback(async () => { + if (isPending) + return + if (!email) { Toast.notify({ type: 'error', message: t('error.emailEmpty', { ns: 'login' }) }) return @@ -41,10 +43,14 @@ export default function Form({ const res = await submitMail({ email, language: locale }) if ((res as MailSendResponse).result === 'success') onSuccess(email, (res as MailSendResponse).data) - }, [email, locale, submitMail, t]) + }, [email, locale, submitMail, t, isPending, onSuccess]) return ( - + { + e.preventDefault() + handleSubmit() + }} + >