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
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user