// Widget tests for FlowNode — the canvas card. // // Cover: dynamic width grows with long labels, two-column // body renders both input + output labels, port tooltips are // attached when supplied, height accounts for max(inputs, // outputs). import 'package:fai_studio_flow_editor/src/model/flow_graph.dart'; import 'package:fai_studio_flow_editor/src/widgets/flow_node.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; void main() { Widget pump(Widget child) => MaterialApp(home: Scaffold(body: Center(child: child))); group('NodeGeometry', () { test('widthFor grows beyond minimum when labels are long', () { // Bare minimum case. expect( NodeGeometry.widthFor(maxInputChars: 4, maxOutputChars: 4), NodeGeometry.width, ); // Long labels push past the minimum. final wide = NodeGeometry.widthFor( maxInputChars: 13, // "system_prompt" maxOutputChars: 14, // "model_endpoint" ); expect(wide, greaterThan(NodeGeometry.width)); }); test('heightFor uses the taller of inputs / outputs', () { // 3 inputs + 5 outputs → 5-row body. final h35 = NodeGeometry.heightFor(3, 5); // 5 inputs + 3 outputs → also 5-row body, same height. final h53 = NodeGeometry.heightFor(5, 3); expect(h35, h53); }); test('inputPortY/outputPortY align row-for-row', () { // Both sides walk the same row geometry from the body's // top; that's how the labels stay visually aligned with // their dots. expect(NodeGeometry.inputPortY(0), NodeGeometry.outputPortY(0)); expect(NodeGeometry.inputPortY(2), NodeGeometry.outputPortY(2)); }); }); group('FlowNode rendering', () { testWidgets('renders both input and output labels', (tester) async { await tester.pumpWidget( pump( const FlowNode( id: 'sum', title: 'sum', subtitle: 'llm.chat@^0', inputPortLabels: ['prompt', 'endpoint'], outputPortLabels: ['response', 'model_endpoint'], ), ), ); expect(find.text('prompt'), findsOneWidget); expect(find.text('endpoint'), findsOneWidget); expect(find.text('response'), findsOneWidget); expect(find.text('model_endpoint'), findsOneWidget); }); testWidgets('honors explicit width parameter', (tester) async { await tester.pumpWidget( pump( const FlowNode( id: 'x', title: 'x', inputPortLabels: ['a_very_long_input_label'], outputPortLabels: ['another_long_output_label'], width: 320, ), ), ); final size = tester.getSize(find.byType(FlowNode)); expect(size.width, 320); }); testWidgets('attaches Tooltip when portTooltips is non-empty', ( tester, ) async { await tester.pumpWidget( pump( const FlowNode( id: 'sum', title: 'sum', inputPortLabels: ['prompt'], portTooltips: {'prompt': 'The user-facing prompt text.'}, ), ), ); expect(find.byType(Tooltip), findsAtLeastNWidgets(1)); }); }); group('kindForStep', () { test('classifies system.approval steps as approval kind', () { final approval = FlowStep(id: 'r', use: 'system.approval@^0'); expect(kindForStep(approval), NodeVisualKind.approval); }); test('classifies generic capabilities as module kind', () { final mod = FlowStep(id: 's', use: 'llm.chat@^0'); expect(kindForStep(mod), NodeVisualKind.module); }); }); }