4d64576f22
- 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
114 lines
3.0 KiB
TypeScript
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()
|
|
})
|
|
})
|