import { render, screen, fireEvent, waitFor, act } from '@testing-library/react'; import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; const mockSigninRedirect = vi.fn(); const mockAuth = { user: { access_token: 'valid-token', }, isAuthenticated: true, signinRedirect: mockSigninRedirect, }; vi.mock('react-oidc-context', () => ({ useAuth: () => mockAuth, })); vi.mock('framer-motion', () => ({ motion: { div: ({ children, ...props }: any) =>
{children}
, header: ({ children, ...props }: any) =>
{children}
, button: ({ children, ...props }: any) => , }, AnimatePresence: ({ children }: any) => children, })); const originalFetch = global.fetch; describe('UpgradePage', () => { let UpgradePage: typeof import('../../components/UpgradePage').UpgradePage; beforeEach(async () => { vi.clearAllMocks(); vi.resetModules(); mockAuth.user = { access_token: 'valid-token' }; mockAuth.isAuthenticated = true; global.fetch = vi.fn(); const module = await import('../../components/UpgradePage'); UpgradePage = module.UpgradePage; }); afterEach(() => { global.fetch = originalFetch; vi.resetAllMocks(); }); describe('loading state', () => { it('shows loader while checking status', async () => { let resolveStatus: (value: any) => void; const statusPromise = new Promise((resolve) => { resolveStatus = resolve; }); (global.fetch as ReturnType).mockReturnValue(statusPromise); render(); // The loader has animate-spin class expect(document.querySelector('.animate-spin')).toBeInTheDocument(); await act(async () => { resolveStatus!({ ok: true, json: () => Promise.resolve({ hasAccess: false }), }); }); }); }); describe('user without access', () => { beforeEach(() => { (global.fetch as ReturnType).mockResolvedValue({ ok: true, json: () => Promise.resolve({ hasAccess: false }), }); }); it('renders upgrade page with pricing', async () => { render(); await waitFor(() => { expect(screen.getByText(/Unlock/i)).toBeInTheDocument(); }); expect(screen.getByText('Monthly')).toBeInTheDocument(); expect(screen.getByText('Yearly')).toBeInTheDocument(); expect(screen.getByText('Starter')).toBeInTheDocument(); expect(screen.getByText('Pro Gamer')).toBeInTheDocument(); }); it('shows $0 for Starter plan', async () => { render(); await waitFor(() => { expect(screen.getByText('$0')).toBeInTheDocument(); }); }); it('defaults to yearly billing cycle', async () => { render(); await waitFor(() => { expect(screen.getByText('$4.17')).toBeInTheDocument(); }); expect(screen.getByText(/Billed \$50 yearly/i)).toBeInTheDocument(); }); it('switches to monthly pricing when monthly is clicked', async () => { render(); await waitFor(() => { expect(screen.getByText('$4.17')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Monthly')); await waitFor(() => { expect(screen.getByText('$5')).toBeInTheDocument(); }); expect(screen.queryByText(/Billed \$50 yearly/i)).not.toBeInTheDocument(); }); it('shows feature comparison', async () => { render(); await waitFor(() => { expect(screen.getByText('5 AI generations per month')).toBeInTheDocument(); }); expect(screen.getByText('250 AI generations per month')).toBeInTheDocument(); expect(screen.getByText('Basic quiz topics')).toBeInTheDocument(); expect(screen.getByText('Host up to 10 players')).toBeInTheDocument(); expect(screen.getByText('Host up to 100 players')).toBeInTheDocument(); }); it('shows back button when onBack is provided', async () => { const mockOnBack = vi.fn(); render(); await waitFor(() => { expect(screen.getByText(/Unlock/i)).toBeInTheDocument(); }); const backButton = document.querySelector('button[class*="absolute"]'); expect(backButton).toBeInTheDocument(); }); it('calls onBack when back button is clicked', async () => { const mockOnBack = vi.fn(); render(); await waitFor(() => { expect(screen.getByText(/Unlock/i)).toBeInTheDocument(); }); const backButton = document.querySelector('button[class*="absolute"]'); if (backButton) { fireEvent.click(backButton); expect(mockOnBack).toHaveBeenCalled(); } }); }); describe('user with access', () => { beforeEach(() => { (global.fetch as ReturnType).mockResolvedValue({ ok: true, json: () => Promise.resolve({ hasAccess: true }), }); }); it('shows "You\'re a Pro!" message', async () => { render(); await waitFor(() => { expect(screen.getByText("You're a Pro!")).toBeInTheDocument(); }); }); it('shows "Back to Game" button', async () => { const mockOnBack = vi.fn(); render(); await waitFor(() => { expect(screen.getByText('Back to Game')).toBeInTheDocument(); }); }); it('calls onBack when "Back to Game" is clicked', async () => { const mockOnBack = vi.fn(); render(); await waitFor(() => { expect(screen.getByText('Back to Game')).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Back to Game')); expect(mockOnBack).toHaveBeenCalled(); }); }); describe('checkout flow', () => { beforeEach(() => { (global.fetch as ReturnType).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ hasAccess: false }), }); }); it('calls checkout endpoint when "Upgrade Now" is clicked', async () => { (global.fetch as ReturnType).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ url: 'https://checkout.stripe.com/test' }), }); const originalHref = window.location.href; Object.defineProperty(window, 'location', { value: { href: '', origin: 'http://localhost' }, writable: true, }); render(); await waitFor(() => { expect(screen.getByText(/Upgrade Now/i)).toBeInTheDocument(); }); fireEvent.click(screen.getByText(/Upgrade Now/i)); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining('/api/payments/checkout'), expect.objectContaining({ method: 'POST', headers: expect.objectContaining({ 'Content-Type': 'application/json', Authorization: 'Bearer valid-token', }), }) ); }); Object.defineProperty(window, 'location', { value: { href: originalHref }, writable: true, }); }); it('redirects to sign in if no token', async () => { mockAuth.user = null; render(); await waitFor(() => { expect(screen.getByText(/Upgrade Now/i)).toBeInTheDocument(); }); fireEvent.click(screen.getByText(/Upgrade Now/i)); await waitFor(() => { expect(mockSigninRedirect).toHaveBeenCalled(); }); }); it('shows error message on checkout failure', async () => { (global.fetch as ReturnType).mockResolvedValueOnce({ ok: false, status: 500, }); render(); await waitFor(() => { expect(screen.getByText(/Upgrade Now/i)).toBeInTheDocument(); }); fireEvent.click(screen.getByText(/Upgrade Now/i)); await waitFor(() => { expect(screen.getByText(/Something went wrong/i)).toBeInTheDocument(); }); }); it('passes correct planType for yearly billing', async () => { (global.fetch as ReturnType).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ url: 'https://checkout.stripe.com/test' }), }); const originalHref = window.location.href; Object.defineProperty(window, 'location', { value: { href: '', origin: 'http://localhost' }, writable: true, }); render(); await waitFor(() => { expect(screen.getByText(/Upgrade Now/i)).toBeInTheDocument(); }); fireEvent.click(screen.getByText(/Upgrade Now/i)); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: expect.stringContaining('"planType":"yearly"'), }) ); }); Object.defineProperty(window, 'location', { value: { href: originalHref }, writable: true, }); }); it('passes correct planType for monthly billing', async () => { (global.fetch as ReturnType).mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({ url: 'https://checkout.stripe.com/test' }), }); const originalHref = window.location.href; Object.defineProperty(window, 'location', { value: { href: '', origin: 'http://localhost' }, writable: true, }); render(); await waitFor(() => { expect(screen.getByText(/Upgrade Now/i)).toBeInTheDocument(); }); fireEvent.click(screen.getByText('Monthly')); await waitFor(() => { expect(screen.getByText('$5')).toBeInTheDocument(); }); fireEvent.click(screen.getByText(/Upgrade Now/i)); await waitFor(() => { expect(global.fetch).toHaveBeenCalledWith( expect.any(String), expect.objectContaining({ body: expect.stringContaining('"planType":"monthly"'), }) ); }); Object.defineProperty(window, 'location', { value: { href: originalHref }, writable: true, }); }); }); describe('status check error handling', () => { it('handles status check failure gracefully', async () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); (global.fetch as ReturnType).mockRejectedValueOnce(new Error('Network error')); render(); await waitFor(() => { expect(screen.getByText(/Unlock/i)).toBeInTheDocument(); }); expect(consoleSpy).toHaveBeenCalledWith('Failed to check status:', expect.any(Error)); consoleSpy.mockRestore(); }); it('handles missing token during status check', async () => { mockAuth.user = null; render(); await waitFor(() => { expect(screen.getByText(/Unlock/i)).toBeInTheDocument(); }); }); }); describe('UI elements', () => { beforeEach(() => { (global.fetch as ReturnType).mockResolvedValue({ ok: true, json: () => Promise.resolve({ hasAccess: false }), }); }); it('shows discount badge for yearly billing', async () => { render(); await waitFor(() => { expect(screen.getByText('-17%')).toBeInTheDocument(); }); }); it('shows "Most Popular" badge on Pro plan', async () => { render(); await waitFor(() => { expect(screen.getByText('Most Popular')).toBeInTheDocument(); }); }); it('shows "Current Plan" button disabled for Starter', async () => { render(); await waitFor(() => { expect(screen.getByText('Current Plan')).toBeInTheDocument(); }); const currentPlanButton = screen.getByText('Current Plan'); expect(currentPlanButton).toBeDisabled(); }); it('shows security and guarantee info', async () => { render(); await waitFor(() => { expect(screen.getByText(/Secure payment via Stripe/i)).toBeInTheDocument(); }); expect(screen.getByText(/Cancel anytime/i)).toBeInTheDocument(); expect(screen.getByText(/7-day money-back guarantee/i)).toBeInTheDocument(); }); }); });