import { render, screen, fireEvent } from '@testing-library/react'; import { WorkflowRunViewer } from '../WorkflowRunViewer'; import { ExecNodeDetails } from '../ExecNodeDetails'; import type { WfExecutionView, NodeExecView } from '../types'; // jsdom lacks matchMedia, which DualPaneWorkspace uses for its `auto` layout. beforeAll(() => { Object.defineProperty(window, 'matchMedia', { writable: true, value: (query: string) => ({ matches: false, media: query, onchange: null, addEventListener: () => {}, removeEventListener: () => {}, addListener: () => {}, removeListener: () => {}, dispatchEvent: () => false, }), }); }); function makeExecution(overrides: Partial = {}): WfExecutionView { const nodeA: NodeExecView = { nodeId: 'a', name: 'Fetch data', type: 'trigger.event', status: 'success', executionOrder: 0, executionTimeMs: 1000, inputData: { foo: 'in' }, outputData: { foo: 'out' }, }; const nodeB: NodeExecView = { nodeId: 'b', name: 'Transform', type: 'transform.set', status: 'running', executionOrder: 1, }; return { executionId: 'exec-1', status: 'running', nodes: { a: nodeA, b: nodeB }, ...overrides, }; } describe('WorkflowRunViewer', () => { it('renders the run status header badge', () => { render(); expect(screen.getByText('Completed')).toBeInTheDocument(); }); it('renders the timeline with a row per node, ordered by executionOrder', () => { render(); const rows = screen.getAllByRole('listitem'); expect(rows).toHaveLength(2); expect(rows[0]).toHaveTextContent('Fetch data'); expect(rows[1]).toHaveTextContent('Transform'); }); it('shows selected-node details when a timeline row is selected', () => { render(); fireEvent.click(screen.getByText('Fetch data')); // detail panel shows the selected node name as a heading + pretty JSON expect( screen.getByRole('heading', { name: 'Fetch data' }) ).toBeInTheDocument(); expect(screen.getByText(/"foo": "out"/)).toBeInTheDocument(); }); it('updates the selected-node details when a new snapshot is passed', () => { const { rerender } = render(); fireEvent.click(screen.getByText('Transform')); // node b selected; detail panel headed by its name expect( screen.getByRole('heading', { name: 'Transform' }) ).toBeInTheDocument(); const next = makeExecution({ status: 'completed' }); next.nodes.b = { ...next.nodes.b, status: 'success', executionTimeMs: 250, outputData: { done: true }, }; rerender(); // detail panel reflects the new snapshot for the still-selected node b expect(screen.getByText(/"done": true/)).toBeInTheDocument(); }); it('renders an empty state when there are no node executions', () => { render( ); expect(screen.queryAllByRole('listitem')).toHaveLength(0); expect(screen.getByText('Pending')).toBeInTheDocument(); }); }); describe('ExecNodeDetails', () => { const node: NodeExecView = { nodeId: 'a', name: 'Fetch data', type: 'trigger.event', status: 'error', executionOrder: 0, error: 'boom', inputData: { a: 1 }, outputData: { b: 2 }, }; it('renders node name, type and status', () => { render(); expect(screen.getByText('Fetch data')).toBeInTheDocument(); expect(screen.getByText('trigger.event')).toBeInTheDocument(); expect(screen.getByText('Error')).toBeInTheDocument(); }); it('pretty-prints input and output JSON', () => { render(); expect(screen.getByText(/"a": 1/)).toBeInTheDocument(); expect(screen.getByText(/"b": 2/)).toBeInTheDocument(); }); it('renders error message when present', () => { render(); expect(screen.getByText('boom')).toBeInTheDocument(); }); it('renders an empty state when no node is selected', () => { render(); expect(screen.getByText(/select a node/i)).toBeInTheDocument(); }); });