test(05-03): add CableTestPage unit tests with Vitest setup
- Install vitest, @testing-library/react, jsdom (Rule 3: missing test infra) - Add vitest.config.ts with jsdom environment and @ alias - Add src/test/setup.ts with jest-dom matchers and matchMedia stub - Add CableTestPage.test.tsx: 4 tests (no-tester, recent list, print success, print_skipped) - Mock AppShell to avoid TanStack Router context in tests - All 4 tests pass; npm run build exits 0
This commit is contained in:
parent
499dbb9929
commit
4e5a3547f9
5 changed files with 2629 additions and 3 deletions
2430
web/package-lock.json
generated
2430
web/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -7,7 +7,8 @@
|
|||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"test": "vitest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-slot": "^1.1.0",
|
||||
|
|
@ -26,17 +27,23 @@
|
|||
"zustand": "^4.5.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@testing-library/user-event": "^14.6.1",
|
||||
"@types/node": "^25.6.0",
|
||||
"@types/react": "^18.3.3",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@typescript-eslint/eslint-plugin": "^8.0.0",
|
||||
"@typescript-eslint/parser": "^8.0.0",
|
||||
"@vitejs/plugin-react": "^4.3.1",
|
||||
"@vitest/coverage-v8": "^4.1.4",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"eslint": "^9.7.0",
|
||||
"jsdom": "^29.0.2",
|
||||
"postcss": "^8.4.40",
|
||||
"tailwindcss": "^3.4.7",
|
||||
"typescript": "^5.5.3",
|
||||
"vite": "^5.3.5"
|
||||
"vite": "^5.3.5",
|
||||
"vitest": "^4.1.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
158
web/src/pages/CableTestPage.test.tsx
Normal file
158
web/src/pages/CableTestPage.test.tsx
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import * as React from 'react'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import { render, screen, waitFor } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||
import { Toaster } from 'react-hot-toast'
|
||||
import { CableTestPage } from './CableTestPage'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock AppShell — strips out TopBar which requires TanStack Router context
|
||||
// ---------------------------------------------------------------------------
|
||||
vi.mock('@/components/layout/AppShell', () => ({
|
||||
AppShell: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
}))
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock EventSource — prevents real SSE connections in tests
|
||||
// ---------------------------------------------------------------------------
|
||||
class MockEventSource {
|
||||
addEventListener = vi.fn()
|
||||
removeEventListener = vi.fn()
|
||||
close = vi.fn()
|
||||
}
|
||||
vi.stubGlobal('EventSource', MockEventSource)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Mock the API module
|
||||
// ---------------------------------------------------------------------------
|
||||
vi.mock('@/api/test', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('@/api/test')>()
|
||||
return {
|
||||
...actual,
|
||||
streamTestEvents: (_onReading: unknown) => new MockEventSource(),
|
||||
getRecentTests: vi.fn().mockResolvedValue([]),
|
||||
submitCableTest: vi.fn(),
|
||||
}
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function makeClient() {
|
||||
return new QueryClient({
|
||||
defaultOptions: { queries: { retry: false } },
|
||||
})
|
||||
}
|
||||
|
||||
function renderPage() {
|
||||
const client = makeClient()
|
||||
return render(
|
||||
<QueryClientProvider client={client}>
|
||||
<CableTestPage />
|
||||
<Toaster />
|
||||
</QueryClientProvider>,
|
||||
)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('CableTestPage', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it('renders "No tester connected" when no SSE data', () => {
|
||||
renderPage()
|
||||
expect(screen.getByText('No tester connected')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders recent tests list', async () => {
|
||||
const { getRecentTests } = await import('@/api/test')
|
||||
vi.mocked(getRecentTests).mockResolvedValue([
|
||||
{
|
||||
cable_type: 0,
|
||||
usb_version: 'USB 3.2 Gen 2',
|
||||
dp_version: '',
|
||||
hdmi_version: '',
|
||||
speed_gbps: 10,
|
||||
max_watts: 100,
|
||||
pin_continuity: true,
|
||||
has_emarker: true,
|
||||
resistance_ohm: 0.12,
|
||||
hw_id: 'HW-00001',
|
||||
},
|
||||
{
|
||||
cable_type: 0,
|
||||
usb_version: 'USB 2.0',
|
||||
dp_version: '',
|
||||
hdmi_version: '',
|
||||
speed_gbps: 0.48,
|
||||
max_watts: 10,
|
||||
pin_continuity: false,
|
||||
has_emarker: false,
|
||||
resistance_ohm: 0.5,
|
||||
hw_id: 'HW-00002',
|
||||
},
|
||||
])
|
||||
|
||||
renderPage()
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('HW-00001')).toBeInTheDocument()
|
||||
expect(screen.getByText('HW-00002')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('Print & Next calls submitCableTest and shows success toast', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { submitCableTest } = await import('@/api/test')
|
||||
vi.mocked(submitCableTest).mockResolvedValue({
|
||||
hw_id: 'HW-00001',
|
||||
netbox_id: 1,
|
||||
print_skipped: false,
|
||||
})
|
||||
|
||||
renderPage()
|
||||
|
||||
// Fill in the HW ID
|
||||
const hwIdInput = screen.getByLabelText('HW ID')
|
||||
await user.clear(hwIdInput)
|
||||
await user.type(hwIdInput, 'HW-00001')
|
||||
|
||||
// Click Print & Next
|
||||
const button = screen.getByRole('button', { name: /print label and advance/i })
|
||||
await user.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(submitCableTest).toHaveBeenCalled()
|
||||
expect(screen.getByText('Label printed for HW-00001')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
it('Print & Next shows print_skipped toast', async () => {
|
||||
const user = userEvent.setup()
|
||||
const { submitCableTest } = await import('@/api/test')
|
||||
vi.mocked(submitCableTest).mockResolvedValue({
|
||||
hw_id: 'HW-00001',
|
||||
netbox_id: 1,
|
||||
print_skipped: true,
|
||||
})
|
||||
|
||||
renderPage()
|
||||
|
||||
const hwIdInput = screen.getByLabelText('HW ID')
|
||||
await user.clear(hwIdInput)
|
||||
await user.type(hwIdInput, 'HW-00001')
|
||||
|
||||
const button = screen.getByRole('button', { name: /print label and advance/i })
|
||||
await user.click(button)
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText('Saved — printer not available')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
})
|
||||
16
web/src/test/setup.ts
Normal file
16
web/src/test/setup.ts
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
import '@testing-library/jest-dom'
|
||||
|
||||
// jsdom does not implement matchMedia — stub it so react-hot-toast renders
|
||||
Object.defineProperty(window, 'matchMedia', {
|
||||
writable: true,
|
||||
value: (query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addListener: () => {},
|
||||
removeListener: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}),
|
||||
})
|
||||
17
web/vitest.config.ts
Normal file
17
web/vitest.config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test/setup.ts'],
|
||||
},
|
||||
})
|
||||
Loading…
Add table
Reference in a new issue