import { describe, it, expect, vi, beforeEach } from 'vitest' import { render, screen, waitFor } from '@testing-library/react' import { useCanvasStore } from '@/stores/canvasStore' // ── Mock heavy dependencies ──────────────────────────────────────────────── vi.mock('@xyflow/react', () => ({ ReactFlowProvider: ({ children }: { children: React.ReactNode }) => <>{children}, ReactFlow: () =>
, Background: () => null, Controls: () => null, BackgroundVariant: { Dots: 'dots' }, ConnectionMode: { Loose: 'loose' }, useReactFlow: () => ({ fitView: vi.fn() }), })) vi.mock('@xyflow/react/dist/style.css', () => ({})) vi.mock('@/api/client', () => ({ liveviewApi: { load: vi.fn() }, })) import { liveviewApi } from '@/api/client' import LiveView from '../LiveView' // ── Helpers ──────────────────────────────────────────────────────────────── function setSearch(params: string) { Object.defineProperty(window, 'location', { writable: true, value: { ...window.location, search: params, pathname: '/view' }, }) } const canvasPayload = { data: { nodes: [{ id: 'n1', type: 'server', label: 'CI Node', status: 'online', services: [], pos_x: 0, pos_y: 0, created_at: '2024-01-01T00:00:00Z', updated_at: '2024-01-01T00:00:00Z', }], edges: [], viewport: { x: 0, y: 0, zoom: 1 }, }, } // ── Tests ────────────────────────────────────────────────────────────────── describe('LiveView (non-standalone)', () => { beforeEach(() => { vi.mocked(liveviewApi.load).mockReset() useCanvasStore.setState({ nodes: [], edges: [] }) }) afterEach(() => { setSearch('') }) // ── No key ──────────────────────────────────────────────────────────────── it('shows no-key error when ?key= is missing', async () => { setSearch('') render() await waitFor(() => { expect(screen.getByText('Access Denied')).toBeDefined() expect(screen.getByText(/Missing key/)).toBeDefined() }) expect(liveviewApi.load).not.toHaveBeenCalled() }) // ── Disabled ────────────────────────────────────────────────────────────── it('shows disabled error when backend returns "Live view is disabled"', async () => { setSearch('?key=anything') vi.mocked(liveviewApi.load).mockRejectedValue({ response: { data: { detail: 'Live view is disabled' } }, }) render() await waitFor(() => { expect(screen.getByText(/disabled on this instance/)).toBeDefined() }) }) // ── Invalid key ─────────────────────────────────────────────────────────── it('shows invalid-key error when backend returns "Invalid live view key"', async () => { setSearch('?key=wrong') vi.mocked(liveviewApi.load).mockRejectedValue({ response: { data: { detail: 'Invalid live view key' } }, }) render() await waitFor(() => { expect(screen.getByText(/Invalid or expired/)).toBeDefined() }) }) it('shows network-error for non-response errors (offline, CORS, 500)', async () => { setSearch('?key=anything') vi.mocked(liveviewApi.load).mockRejectedValue(new Error('network')) render() await waitFor(() => { expect(screen.getByText(/Could not reach the server/)).toBeDefined() }) }) // ── Valid key → canvas rendered ─────────────────────────────────────────── it('renders the canvas on valid key', async () => { setSearch('?key=correct-key') vi.mocked(liveviewApi.load).mockResolvedValue(canvasPayload as never) render() await waitFor(() => { expect(screen.getByTestId('react-flow')).toBeDefined() }) expect(liveviewApi.load).toHaveBeenCalledWith('correct-key') }) it('loads nodes into the canvas store on success', async () => { setSearch('?key=secret') vi.mocked(liveviewApi.load).mockResolvedValue(canvasPayload as never) render() await waitFor(() => { expect(screen.getByTestId('react-flow')).toBeDefined() }) const { nodes } = useCanvasStore.getState() expect(nodes.find((n) => n.id === 'n1')).toBeDefined() }) // ── No editing props passed ─────────────────────────────────────────────── it('does not show any Access Denied when key is valid', async () => { setSearch('?key=valid') vi.mocked(liveviewApi.load).mockResolvedValue(canvasPayload as never) render() await waitFor(() => expect(screen.getByTestId('react-flow')).toBeDefined()) expect(screen.queryByText('Access Denied')).toBeNull() }) }) // ── Standalone mode ──────────────────────────────────────────────────────── const XYFLOW_MOCK = { ReactFlowProvider: ({ children }: { children: React.ReactNode }) => <>{children}, ReactFlow: () =>
, Background: () => null, Controls: () => null, BackgroundVariant: { Dots: 'dots' }, ConnectionMode: { Loose: 'loose' }, useReactFlow: () => ({ fitView: vi.fn() }), } describe('LiveView (standalone — localStorage)', () => { beforeEach(() => { localStorage.clear() useCanvasStore.setState({ nodes: [], edges: [] }) }) afterEach(() => { setSearch('') vi.unstubAllEnvs() }) it('loads canvas from localStorage without calling the API', async () => { const stored = { nodes: [{ id: 'ls-node', type: 'router', position: { x: 10, y: 20 }, data: { label: 'Router', type: 'router', status: 'unknown', services: [] }, }], edges: [], } localStorage.setItem('homelable_canvas', JSON.stringify(stored)) vi.stubEnv('VITE_STANDALONE', 'true') vi.resetModules() const mockLoad = vi.fn() vi.doMock('@xyflow/react', () => XYFLOW_MOCK) vi.doMock('@xyflow/react/dist/style.css', () => ({})) vi.doMock('@/api/client', () => ({ liveviewApi: { load: mockLoad } })) const { default: LiveViewStandalone } = await import('../LiveView') setSearch('') render() await waitFor(() => { expect(screen.getByTestId('react-flow')).toBeDefined() }) expect(mockLoad).not.toHaveBeenCalled() }) it('shows canvas (empty) when localStorage has no saved data', async () => { vi.stubEnv('VITE_STANDALONE', 'true') vi.resetModules() const mockLoad = vi.fn() vi.doMock('@xyflow/react', () => XYFLOW_MOCK) vi.doMock('@xyflow/react/dist/style.css', () => ({})) vi.doMock('@/api/client', () => ({ liveviewApi: { load: mockLoad } })) const { default: LiveViewStandalone } = await import('../LiveView') setSearch('') render() await waitFor(() => { expect(screen.getByTestId('react-flow')).toBeDefined() }) expect(mockLoad).not.toHaveBeenCalled() }) })