test: add shared frontend test infra (factories, mocks, render)
Foundation for the frontend test consolidation (mirrors backend #267): - src/test/factories.ts: canonical makeNode/makeEdge/makeNodeData/makeDesign - src/test/mocks/: reusable mockSonner/mockReactFlow/makeUseCanvasStore builders consumed via async vi.mock dynamic-import to survive hoisting - src/test/render.tsx: renderWithProviders wrapping Tooltip + ReactFlow providers Migrate ScanConfigModal sonner mock as canary. No behavior change. ha-relevant: yes
This commit is contained in:
@@ -9,7 +9,7 @@ vi.mock('@/api/client', () => ({
|
|||||||
trigger: vi.fn(),
|
trigger: vi.fn(),
|
||||||
},
|
},
|
||||||
}))
|
}))
|
||||||
vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn(), info: vi.fn() } }))
|
vi.mock('sonner', async () => (await import('@/test/mocks')).mockSonner())
|
||||||
|
|
||||||
import { scanApi } from '@/api/client'
|
import { scanApi } from '@/api/client'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
/**
|
||||||
|
* Canonical test fixture builders. Prefer these over redefining `makeNode` /
|
||||||
|
* `makeEdge` inline per test file — one source of truth keeps fixtures in sync
|
||||||
|
* with the domain types.
|
||||||
|
*
|
||||||
|
* See CLAUDE.md → Testing Protocol.
|
||||||
|
*/
|
||||||
|
import type { Node, Edge } from '@xyflow/react'
|
||||||
|
import type { NodeData, EdgeData, Design } from '@/types'
|
||||||
|
|
||||||
|
/** Build a `NodeData` payload with sane defaults; override any field. */
|
||||||
|
export function makeNodeData(overrides: Partial<NodeData> = {}): NodeData {
|
||||||
|
return {
|
||||||
|
label: overrides.label ?? 'Test Node',
|
||||||
|
type: 'server',
|
||||||
|
status: 'unknown',
|
||||||
|
services: [],
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a React Flow node. Accepts either an id string or a partial-node
|
||||||
|
* object as the first arg, so it covers both inline patterns that existed
|
||||||
|
* across the suite:
|
||||||
|
* makeNode('n1', { type: 'router' })
|
||||||
|
* makeNode({ data: makeNodeData({ ip: '10.0.0.1' }) })
|
||||||
|
*/
|
||||||
|
export function makeNode(
|
||||||
|
idOrOverrides: string | Partial<Node<NodeData>> = 'n1',
|
||||||
|
dataOverrides: Partial<NodeData> = {},
|
||||||
|
): Node<NodeData> {
|
||||||
|
const base: Partial<Node<NodeData>> =
|
||||||
|
typeof idOrOverrides === 'string' ? { id: idOrOverrides } : idOrOverrides
|
||||||
|
const id = base.id ?? 'n1'
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
type: base.type ?? base.data?.type ?? dataOverrides.type ?? 'server',
|
||||||
|
position: base.position ?? { x: 0, y: 0 },
|
||||||
|
...base,
|
||||||
|
data: makeNodeData({ label: id, ...dataOverrides, ...base.data }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a React Flow edge between two node ids. */
|
||||||
|
export function makeEdge(
|
||||||
|
id: string,
|
||||||
|
source: string,
|
||||||
|
target: string,
|
||||||
|
overrides: Partial<Edge<EdgeData>> = {},
|
||||||
|
): Edge<EdgeData> {
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
type: 'ethernet',
|
||||||
|
...overrides,
|
||||||
|
data: { type: 'ethernet', ...overrides.data },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Build a Design row. */
|
||||||
|
export function makeDesign(overrides: Partial<Design> = {}): Design {
|
||||||
|
return {
|
||||||
|
id: 'd1',
|
||||||
|
name: 'Test Design',
|
||||||
|
design_type: 'network',
|
||||||
|
icon: null,
|
||||||
|
created_at: '2026-01-01T00:00:00Z',
|
||||||
|
updated_at: '2026-01-01T00:00:00Z',
|
||||||
|
...overrides,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
/**
|
||||||
|
* Reusable mock builders. Consume them from an *async* `vi.mock` factory via
|
||||||
|
* dynamic import so the boilerplate lives in one place:
|
||||||
|
*
|
||||||
|
* vi.mock('sonner', async () => (await import('@/test/mocks')).mockSonner())
|
||||||
|
*
|
||||||
|
* `vi.mock` factories are hoisted above the file's imports, so a plain
|
||||||
|
* top-level `import { mockSonner }` referenced inside the factory throws
|
||||||
|
* "cannot access before initialization". The dynamic `import()` sidesteps
|
||||||
|
* that — it resolves lazily when the mocked module is first loaded.
|
||||||
|
*
|
||||||
|
* See CLAUDE.md → Testing Protocol.
|
||||||
|
*/
|
||||||
|
import { vi } from 'vitest'
|
||||||
|
|
||||||
|
/** `sonner` toast stub. Assert via `import { toast } from 'sonner'`. */
|
||||||
|
export function mockSonner() {
|
||||||
|
return {
|
||||||
|
toast: {
|
||||||
|
success: vi.fn(),
|
||||||
|
error: vi.fn(),
|
||||||
|
info: vi.fn(),
|
||||||
|
warning: vi.fn(),
|
||||||
|
message: vi.fn(),
|
||||||
|
loading: vi.fn(),
|
||||||
|
dismiss: vi.fn(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* `@xyflow/react` stub covering the exports most node/edge component tests
|
||||||
|
* need. Pass `extra` to add or override exports for a specific test.
|
||||||
|
*/
|
||||||
|
export function mockReactFlow(extra: Record<string, unknown> = {}) {
|
||||||
|
return {
|
||||||
|
Handle: () => null,
|
||||||
|
Position: { Top: 'top', Bottom: 'bottom', Left: 'left', Right: 'right' },
|
||||||
|
NodeResizer: () => null,
|
||||||
|
NodeToolbar: () => null,
|
||||||
|
BaseEdge: () => null,
|
||||||
|
EdgeLabelRenderer: ({ children }: { children?: unknown }) => children ?? null,
|
||||||
|
useUpdateNodeInternals: () => vi.fn(),
|
||||||
|
useViewport: () => ({ x: 0, y: 0, zoom: 1 }),
|
||||||
|
useReactFlow: () => ({
|
||||||
|
getNode: vi.fn(),
|
||||||
|
getNodes: vi.fn(() => []),
|
||||||
|
getEdges: vi.fn(() => []),
|
||||||
|
screenToFlowPosition: vi.fn((p: unknown) => p),
|
||||||
|
}),
|
||||||
|
getBezierPath: () => ['M0,0', 0, 0] as const,
|
||||||
|
getSmoothStepPath: () => ['M0,0', 0, 0] as const,
|
||||||
|
...extra,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a `useCanvasStore` replacement for component tests that only read a
|
||||||
|
* selected slice of store state. Returns a function usable as the hook: it
|
||||||
|
* invokes the passed selector against `state` (falling back to returning the
|
||||||
|
* whole state when called with no selector).
|
||||||
|
*/
|
||||||
|
export function makeUseCanvasStore<S extends Record<string, unknown>>(state: S) {
|
||||||
|
return <R>(selector?: (s: S) => R): R | S => (selector ? selector(state) : state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Common `serviceStatusKey` helper mirrored from the real store. */
|
||||||
|
export function serviceStatusKey(nodeId: string, port?: number, protocol?: string) {
|
||||||
|
return `${nodeId}:${port ?? ''}/${protocol ?? ''}`
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/* eslint-disable react-refresh/only-export-components -- test helper module, not a component module */
|
||||||
|
/**
|
||||||
|
* Custom render that wraps the UI in the same providers the app mounts
|
||||||
|
* (`TooltipProvider`, `ReactFlowProvider`), so component tests that touch
|
||||||
|
* React Flow context or tooltips don't each re-declare the wrapper.
|
||||||
|
*
|
||||||
|
* Re-exports everything from `@testing-library/react`, so a test can:
|
||||||
|
* import { renderWithProviders, screen } from '@/test/render'
|
||||||
|
*
|
||||||
|
* Use the plain `render` from `@testing-library/react` for pure presentational
|
||||||
|
* components that need no context. See CLAUDE.md → Testing Protocol.
|
||||||
|
*/
|
||||||
|
import type { ReactElement, ReactNode } from 'react'
|
||||||
|
import { render, type RenderOptions } from '@testing-library/react'
|
||||||
|
import { ReactFlowProvider } from '@xyflow/react'
|
||||||
|
import { TooltipProvider } from '@/components/ui/tooltip'
|
||||||
|
|
||||||
|
function AllProviders({ children }: { children: ReactNode }) {
|
||||||
|
return (
|
||||||
|
<TooltipProvider>
|
||||||
|
<ReactFlowProvider>{children}</ReactFlowProvider>
|
||||||
|
</TooltipProvider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderWithProviders(ui: ReactElement, options?: Omit<RenderOptions, 'wrapper'>) {
|
||||||
|
return render(ui, { wrapper: AllProviders, ...options })
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from '@testing-library/react'
|
||||||
Reference in New Issue
Block a user