diff --git a/app/error.empty-fallback.test.tsx b/app/error.empty-fallback.test.tsx new file mode 100644 index 000000000..1bed148da --- /dev/null +++ b/app/error.empty-fallback.test.tsx @@ -0,0 +1,124 @@ +// app/error.empty-fallback.test.tsx + +import { describe, expect, it, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; + +vi.mock('next/link', () => ({ + default: ({ + children, + ...props + }: React.AnchorHTMLAttributes & { children: React.ReactNode }) => ( + {children} + ), +})); + +vi.mock('sonner', () => ({ + toast: { + success: vi.fn(), + error: vi.fn(), + }, +})); + +import ErrorBoundary from './error'; + +describe('Root Error Boundary - Empty & Missing Input Fallbacks', () => { + beforeEach(() => { + vi.clearAllMocks(); + // Mock clipboard API + Object.defineProperty(navigator, 'clipboard', { + value: { + writeText: vi.fn().mockResolvedValue(undefined), + }, + configurable: true, + writable: true, + }); + }); + + it('renders successfully when error is null', () => { + expect(() => + render() + ).not.toThrow(); + + expect( + screen.getByRole('heading', { + name: /Looks like an exception was thrown in the application/i, + }) + ).toBeInTheDocument(); + expect(screen.getByText('Unknown runtime error occurred.')).toBeInTheDocument(); + }); + + it('renders successfully when error is undefined', () => { + expect(() => + render() + ).not.toThrow(); + + expect( + screen.getByRole('heading', { + name: /Looks like an exception was thrown in the application/i, + }) + ).toBeInTheDocument(); + expect(screen.getByText('Unknown runtime error occurred.')).toBeInTheDocument(); + }); + + it('renders successfully when error is an empty object', () => { + expect(() => + render() + ).not.toThrow(); + + expect( + screen.getByRole('heading', { + name: /Looks like an exception was thrown in the application/i, + }) + ).toBeInTheDocument(); + expect(screen.getByText('Unknown runtime error occurred.')).toBeInTheDocument(); + }); + + it('renders successfully when error has no message property', () => { + expect(() => + render() + ).not.toThrow(); + + expect( + screen.getByRole('heading', { + name: /Looks like an exception was thrown in the application/i, + }) + ).toBeInTheDocument(); + expect(screen.getByText('Unknown runtime error occurred.')).toBeInTheDocument(); + }); + + it('renders successfully when error message is an empty string', () => { + expect(() => render()).not.toThrow(); + + expect( + screen.getByRole('heading', { + name: /Looks like an exception was thrown in the application/i, + }) + ).toBeInTheDocument(); + expect(screen.getByText('Unknown runtime error occurred.')).toBeInTheDocument(); + }); + + it('renders interactive elements and triggers actions correctly in fallback state', async () => { + const resetMock = vi.fn(); + render(); + + // Check actions + const retryButton = screen.getByRole('button', { name: /git fetch/i }); + expect(retryButton).toBeInTheDocument(); + fireEvent.click(retryButton); + expect(resetMock).toHaveBeenCalledOnce(); + + const homeLink = screen.getByRole('link', { name: /Return to main/i }); + expect(homeLink).toBeInTheDocument(); + + // Check clipboard copy fallback behavior + const terminalContainer = screen.getByText('commitpulse — error').closest('div'); + expect(terminalContainer).toBeInTheDocument(); + if (terminalContainer) { + fireEvent.click(terminalContainer); + expect(navigator.clipboard.writeText).toHaveBeenCalledWith( + expect.stringContaining('Unknown exception in the render tree.') + ); + } + }); +}); diff --git a/app/error.tsx b/app/error.tsx index 4667ff618..3910cd0e3 100644 --- a/app/error.tsx +++ b/app/error.tsx @@ -11,16 +11,20 @@ export default function ErrorBoundary({ reset: () => void; }) { useEffect(() => { - console.error(error); + if (error) { + console.error(error); + } }, [error]); + const errorMessage = error?.message || ''; + const terminalContent = `git status fatal: Your branch and 'origin/main' have diverged, and have 1 and 1 different commits each, respectively. Error details: - ${error.message || 'Unknown exception in the render tree.'}`; + ${errorMessage || 'Unknown exception in the render tree.'}`; const handleCopy = async () => { try { @@ -88,7 +92,7 @@ export default function ErrorBoundary({ fatal: Your branch and 'origin/main' have diverged.
- {error.message || 'Unknown runtime error occurred.'} + {errorMessage || 'Unknown runtime error occurred.'}

diff --git a/app/global-error.empty-fallback.test.tsx b/app/global-error.empty-fallback.test.tsx new file mode 100644 index 000000000..54c8b3373 --- /dev/null +++ b/app/global-error.empty-fallback.test.tsx @@ -0,0 +1,72 @@ +// app/global-error.empty-fallback.test.tsx + +import { describe, expect, it, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import '@testing-library/jest-dom/vitest'; + +vi.mock('next/link', () => ({ + default: ({ + children, + ...props + }: React.AnchorHTMLAttributes & { children: React.ReactNode }) => ( + {children} + ), +})); + +import GlobalError from './global-error'; + +describe('Global Error Page - Empty & Missing Input Fallbacks', () => { + it('renders successfully when error is null', () => { + expect(() => + render() + ).not.toThrow(); + + expect(screen.getByText('500')).toBeInTheDocument(); + expect(screen.getByText('A critical error occurred at the root level.')).toBeInTheDocument(); + expect(screen.getByText('Unknown global error')).toBeInTheDocument(); + }); + + it('renders successfully when error is undefined', () => { + expect(() => + render() + ).not.toThrow(); + + expect(screen.getByText('500')).toBeInTheDocument(); + expect(screen.getByText('Unknown global error')).toBeInTheDocument(); + }); + + it('renders successfully when error is an empty object', () => { + expect(() => + render() + ).not.toThrow(); + + expect(screen.getByText('500')).toBeInTheDocument(); + expect(screen.getByText('Unknown global error')).toBeInTheDocument(); + }); + + it('renders successfully when error has no message property', () => { + expect(() => + render() + ).not.toThrow(); + + expect(screen.getByText('500')).toBeInTheDocument(); + expect(screen.getByText('Unknown global error')).toBeInTheDocument(); + }); + + it('renders successfully when error message is an empty string', () => { + expect(() => render()).not.toThrow(); + + expect(screen.getByText('500')).toBeInTheDocument(); + expect(screen.getByText('Unknown global error')).toBeInTheDocument(); + }); + + it('renders interactive elements and triggers reset correctly in fallback state', () => { + const resetMock = vi.fn(); + render(); + + const retryButton = screen.getByRole('button', { name: /Try again/i }); + expect(retryButton).toBeInTheDocument(); + fireEvent.click(retryButton); + expect(resetMock).toHaveBeenCalledOnce(); + }); +}); diff --git a/app/global-error.tsx b/app/global-error.tsx index f8d4c1a8d..acdb867b2 100644 --- a/app/global-error.tsx +++ b/app/global-error.tsx @@ -1,5 +1,4 @@ 'use client'; -import Link from 'next/link'; export default function GlobalError({ error, @@ -8,6 +7,8 @@ export default function GlobalError({ error: Error & { digest?: string }; reset: () => void; }) { + const errorMessage = error?.message || ''; + return ( @@ -17,7 +18,7 @@ export default function GlobalError({

A critical error occurred at the root level.

- {error.message || 'Unknown global error'} + {errorMessage || 'Unknown global error'}