243 lines
7.9 KiB
TypeScript
243 lines
7.9 KiB
TypeScript
// 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 (
|
|
<div data-testid="mock-data-picker">
|
|
<button
|
|
type="button"
|
|
onClick={() => {
|
|
props.onPick({ type: 'ref', nodeId: 'picked', path: [], expectedType: 'DocumentList' });
|
|
props.onClose();
|
|
}}
|
|
>
|
|
mock-pick
|
|
</button>
|
|
<button type="button" onClick={props.onClose}>
|
|
mock-close
|
|
</button>
|
|
</div>
|
|
);
|
|
},
|
|
}));
|
|
|
|
// 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<string, PortSchema> = {
|
|
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(
|
|
<RequiredAttributePicker
|
|
label="Document List"
|
|
expectedType="DocumentList"
|
|
value={undefined}
|
|
onChange={() => {}}
|
|
/>,
|
|
);
|
|
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(
|
|
<RequiredAttributePicker
|
|
label="Document List"
|
|
expectedType="DocumentList"
|
|
value={undefined}
|
|
onChange={() => {}}
|
|
/>,
|
|
);
|
|
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(
|
|
<RequiredAttributePicker
|
|
label="Single document"
|
|
expectedType="UdmDocument"
|
|
value={undefined}
|
|
onChange={() => {}}
|
|
/>,
|
|
);
|
|
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(
|
|
<RequiredAttributePicker
|
|
label="Document List"
|
|
expectedType="DocumentList"
|
|
value={{ type: 'ref', nodeId: 'up', path: [], expectedType: 'DocumentList' }}
|
|
onChange={onChange}
|
|
/>,
|
|
);
|
|
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(
|
|
<RequiredAttributePicker
|
|
label="Document List"
|
|
expectedType="DocumentList"
|
|
value={{ type: 'ref', nodeId: 'up', path: [], expectedType: 'DocumentList' }}
|
|
onChange={onChange}
|
|
/>,
|
|
);
|
|
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' }),
|
|
);
|
|
});
|
|
});
|