/** * Tests de ConfirmInvitePage — flujo de invitación resiliente a email scanners. * El token NO se consume al montar: solo cuando el usuario hace click en el botón. */ import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, fireEvent, waitFor } from '@testing-library/react' import React from 'react' // ─── Referencias mutables para controlar los mocks entre tests ──────────────── const { mockVerifyOtp, mockNavigate, searchRef } = vi.hoisted(() => ({ mockVerifyOtp: vi.fn(), mockNavigate: vi.fn(), searchRef: { current: { token_hash: 'valid-hash-abc123', type: 'invite' } as Record }, })) vi.mock('@/lib/supabase', () => ({ supabase: { auth: { verifyOtp: mockVerifyOtp, }, }, })) vi.mock('@tanstack/react-router', () => ({ useNavigate: () => mockNavigate, useSearch: () => searchRef.current, Link: ({ children, to, className }: { children: React.ReactNode; to: string; className?: string }) => ( {children} ), })) // Importar después de los mocks import { ConfirmInvitePage } from '@/features/auth/ConfirmInvitePage' // ─── Tests ──────────────────────────────────────────────────────────────────── describe('ConfirmInvitePage', () => { beforeEach(() => { vi.clearAllMocks() searchRef.current = { token_hash: 'valid-hash-abc123', type: 'invite' } mockVerifyOtp.mockResolvedValue({ data: {}, error: null }) }) it('1. renderiza el botón "Confirmar acceso" sin llamar a verifyOtp al montar', () => { render() expect(screen.getByRole('button', { name: /confirmar acceso/i })).toBeInTheDocument() expect(mockVerifyOtp).not.toHaveBeenCalled() }) it('2. token_hash vacío muestra estado de error directamente (sin botón de confirmación)', () => { searchRef.current = { token_hash: '', type: 'invite' } render() expect(screen.queryByRole('button', { name: /confirmar acceso/i })).not.toBeInTheDocument() expect(screen.getByText(/link de invitación no es válido o ya fue utilizado/i)).toBeInTheDocument() expect(screen.getByRole('link', { name: /ir al login/i })).toBeInTheDocument() expect(mockVerifyOtp).not.toHaveBeenCalled() }) it('3. type distinto de "invite" muestra error directamente', () => { searchRef.current = { token_hash: 'some-hash', type: 'recovery' } render() expect(screen.queryByRole('button', { name: /confirmar acceso/i })).not.toBeInTheDocument() expect(screen.getByText(/link de invitación no es válido o ya fue utilizado/i)).toBeInTheDocument() expect(mockVerifyOtp).not.toHaveBeenCalled() }) it('4. click en el botón llama verifyOtp con el token_hash y type correctos', async () => { render() fireEvent.click(screen.getByRole('button', { name: /confirmar acceso/i })) await waitFor(() => { expect(mockVerifyOtp).toHaveBeenCalledOnce() expect(mockVerifyOtp).toHaveBeenCalledWith({ type: 'invite', token_hash: 'valid-hash-abc123', }) }) }) it('5. verifyOtp exitoso navega a /reset-password', async () => { mockVerifyOtp.mockResolvedValue({ data: {}, error: null }) render() fireEvent.click(screen.getByRole('button', { name: /confirmar acceso/i })) await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith({ to: '/reset-password' }) }) expect(screen.queryByText(/link de invitación no es válido/i)).not.toBeInTheDocument() }) it('6. verifyOtp con error muestra mensaje de error y no navega', async () => { mockVerifyOtp.mockResolvedValue({ data: null, error: { message: 'Token has expired or already used' } }) render() fireEvent.click(screen.getByRole('button', { name: /confirmar acceso/i })) await waitFor(() => { expect(screen.getByText(/link de invitación no es válido o ya fue utilizado/i)).toBeInTheDocument() }) expect(screen.getByText(/Token has expired or already used/i)).toBeInTheDocument() expect(mockNavigate).not.toHaveBeenCalled() expect(screen.getByRole('link', { name: /ir al login/i })).toBeInTheDocument() }) })