Files
cannamanage/cannamanage-frontend/src/__tests__/components/api-error-boundary.test.tsx
T
Patrick Plate 4d64576f22 test: Vitest setup + unit tests for API client, hooks, services + staff E2E
- Vitest + React Testing Library + MSW setup
- API client: 11 unit tests (fetch, errors, auth header, download, network failure)
- Service hooks: 26 tests across members, distributions, stock, dashboard, staff
- Custom hooks: 5 debounce tests (timer behavior, reset, custom delay)
- Components: 5 tests (offline banner, error boundary with retry)
- E2E: staff management page interactions
- npm scripts: test, test:run, test:coverage
2026-06-12 20:50:45 +02:00

114 lines
3.0 KiB
TypeScript

import { fireEvent, render, screen } from "@testing-library/react"
import { describe, expect, it, vi } from "vitest"
import React from "react"
// Error Boundary component for testing
class ApiErrorBoundary extends React.Component<
{ children: React.ReactNode; fallback?: React.ReactNode },
{ hasError: boolean; error: Error | null }
> {
constructor(props: { children: React.ReactNode; fallback?: React.ReactNode }) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
handleRetry = () => {
this.setState({ hasError: false, error: null })
}
render() {
if (this.state.hasError) {
return (
<div role="alert" data-testid="error-boundary">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={this.handleRetry}>Retry</button>
</div>
)
}
return this.props.children
}
}
// Test component that throws
function ThrowingComponent({ shouldThrow }: { shouldThrow: boolean }) {
if (shouldThrow) {
throw new Error("Test error message")
}
return <div data-testid="child-content">Child content</div>
}
describe("ApiErrorBoundary", () => {
// Suppress React error boundary console.error in tests
const originalConsoleError = console.error
beforeAll(() => {
console.error = (...args: unknown[]) => {
if (
typeof args[0] === "string" &&
args[0].includes("React will try to recreate")
) {
return
}
originalConsoleError(...args)
}
})
afterAll(() => {
console.error = originalConsoleError
})
it("renders children normally when no error", () => {
render(
<ApiErrorBoundary>
<ThrowingComponent shouldThrow={false} />
</ApiErrorBoundary>
)
expect(screen.getByTestId("child-content")).toBeInTheDocument()
expect(screen.getByText("Child content")).toBeInTheDocument()
})
it("shows error UI when child throws", () => {
render(
<ApiErrorBoundary>
<ThrowingComponent shouldThrow={true} />
</ApiErrorBoundary>
)
expect(screen.getByTestId("error-boundary")).toBeInTheDocument()
expect(screen.getByText("Something went wrong")).toBeInTheDocument()
expect(screen.getByText("Test error message")).toBeInTheDocument()
})
it("retry button resets error state", () => {
// Use a mutable ref to control throwing
let shouldThrow = true
function ConditionalThrower() {
if (shouldThrow) throw new Error("Temp error")
return <div data-testid="recovered">Recovered!</div>
}
render(
<ApiErrorBoundary>
<ConditionalThrower />
</ApiErrorBoundary>
)
// Error state shown
expect(screen.getByTestId("error-boundary")).toBeInTheDocument()
// Fix the condition
shouldThrow = false
// Click retry
fireEvent.click(screen.getByRole("button", { name: /retry/i }))
// Should now render children again
expect(screen.getByTestId("recovered")).toBeInTheDocument()
})
})