// Copyright (c) 2025 Patrick Motsch // All rights reserved. // // Plan #2 — Track A1.1: Component-level tests for RequiredAttributePicker. // Validates the 0/1/N rendering logic that orchestrates DataPicker selection // + the iterierens-suggestion (T5, T6). // // We mock the two consumed contexts (LanguageContext + Automation2DataFlow) // and the DataPicker child so we can assert on the picker UI in isolation. import React from 'react'; import { describe, expect, it, vi } from 'vitest'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import type { CanvasConnection, CanvasNode } from '../../editor/FlowCanvas'; import type { NodeType, PortField, PortSchema } from '../../../../api/workflowApi'; import type { DataRef, SystemVarRef } from './dataRef'; // --------------------------------------------------------------------------- // Module mocks — must be registered before importing the SUT // --------------------------------------------------------------------------- vi.mock('../../../../providers/language/LanguageContext', () => ({ useLanguage: () => ({ t: (s: string) => s }), })); let _ctxValue: unknown = null; vi.mock('../../context/Automation2DataFlowContext', () => ({ useAutomation2DataFlow: () => _ctxValue, })); vi.mock('./DataPicker', () => ({ DataPicker: (props: { open: boolean; onClose: () => void; onPick: (ref: DataRef | SystemVarRef) => void; }) => { if (!props.open) return null; return (
); }, })); // SUT imported AFTER mocks (so mocks are applied) import { RequiredAttributePicker } from './RequiredAttributePicker'; // --------------------------------------------------------------------------- // Fixtures // --------------------------------------------------------------------------- function _field(name: string, type: string): PortField { return { name, type, description: '', required: false }; } const _docListSchema: PortSchema = { name: 'DocumentList', fields: [_field('documents', 'List[UdmDocument]'), _field('count', 'int')], }; const _udmDocumentSchema: PortSchema = { name: 'UdmDocument', fields: [_field('name', 'str'), _field('mimeType', 'str')], }; const _portCatalog: Record = { DocumentList: _docListSchema, UdmDocument: _udmDocumentSchema, }; function _setContext(opts: { consumerNodeId: string; nodes: CanvasNode[]; connections: CanvasConnection[]; nodeTypes: NodeType[]; }) { _ctxValue = { currentNodeId: opts.consumerNodeId, nodes: opts.nodes, connections: opts.connections, nodeTypes: opts.nodeTypes, portTypeCatalog: _portCatalog, nodeOutputsPreview: {}, systemVariables: {}, language: 'de', getNodeLabel: (n: { id: string; title?: string }) => n.title ?? n.id, getAvailableSourceIds: () => opts.nodes.map((n) => n.id).filter((id) => id !== opts.consumerNodeId), parseGraphDefinedSchema: () => null, }; } function _node(id: string, type: string): CanvasNode { return { id, type, title: id, x: 0, y: 0, inputs: 1, outputs: 1, parameters: {} }; } function _conn(id: string, src: string, tgt: string): CanvasConnection { return { id, sourceId: src, sourceHandle: 0, targetId: tgt, targetHandle: 0 }; } function _nodeType(id: string, outputSchema: string): NodeType { return { id, label: id, description: id, category: 'test', parameters: [], inputs: 1, outputs: 1, outputPorts: [{ schema: outputSchema }], } as unknown as NodeType; } // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe('RequiredAttributePicker — 0/1/N rendering (T5/T6)', () => { it('shows red "no source" pill when no upstream candidate matches (0-case)', () => { _setContext({ consumerNodeId: 'cons', nodes: [_node('cons', 'ai.summarizeDocument')], connections: [], nodeTypes: [_nodeType('ai.summarizeDocument', 'AiResult')], }); render( {}} />, ); expect( screen.getByText(/Keine typkompatible Quelle vorhanden/i), ).toBeInTheDocument(); }); it('shows auto-bind suggestion when exactly one candidate matches (1-case)', () => { _setContext({ consumerNodeId: 'cons', nodes: [_node('up', 'sharepoint.readDocs'), _node('cons', 'ai.summarizeDocument')], connections: [_conn('c1', 'up', 'cons')], nodeTypes: [ _nodeType('sharepoint.readDocs', 'DocumentList'), _nodeType('ai.summarizeDocument', 'AiResult'), ], }); render( {}} />, ); expect(screen.getByText(/Vorschlag übernehmen/i)).toBeInTheDocument(); }); it('shows iterieren-suggestion when upstream is List[X] and required is X (T6)', () => { _setContext({ consumerNodeId: 'cons', nodes: [_node('up', 'sharepoint.readDocs'), _node('cons', 'ai.summarizeDocument')], connections: [_conn('c1', 'up', 'cons')], nodeTypes: [ _nodeType('sharepoint.readDocs', 'DocumentList'), _nodeType('ai.summarizeDocument', 'AiResult'), ], }); render( {}} />, ); expect(screen.getByText(/iterieren/i)).toBeInTheDocument(); }); it('renders bound chip + "Andere wählen" when value is already a DataRef', async () => { _setContext({ consumerNodeId: 'cons', nodes: [_node('up', 'sharepoint.readDocs'), _node('cons', 'ai.summarizeDocument')], connections: [_conn('c1', 'up', 'cons')], nodeTypes: [ _nodeType('sharepoint.readDocs', 'DocumentList'), _nodeType('ai.summarizeDocument', 'AiResult'), ], }); const onChange = vi.fn(); render( , ); expect(screen.getByText('up')).toBeInTheDocument(); const clearButton = screen.getByTitle(/Bindung entfernen/i); await userEvent.click(clearButton); expect(onChange).toHaveBeenCalledWith(null); }); it('opens DataPicker via "Andere wählen" and forwards the picked ref to onChange', async () => { _setContext({ consumerNodeId: 'cons', nodes: [_node('up', 'sharepoint.readDocs'), _node('cons', 'ai.summarizeDocument')], connections: [_conn('c1', 'up', 'cons')], nodeTypes: [ _nodeType('sharepoint.readDocs', 'DocumentList'), _nodeType('ai.summarizeDocument', 'AiResult'), ], }); const onChange = vi.fn(); render( , ); const otherButton = screen.getByText(/Andere wählen…/i); await userEvent.click(otherButton); expect(screen.getByTestId('mock-data-picker')).toBeInTheDocument(); await userEvent.click(screen.getByText('mock-pick')); expect(onChange).toHaveBeenCalledWith( expect.objectContaining({ type: 'ref', nodeId: 'picked', expectedType: 'DocumentList' }), ); }); });