Three changes: 1. Type-checked port connections. Drag from an output port only highlights compatible-type input ports as drop targets; incompatible drops are silently refused. Source of truth: ModuleSpec for declared types. When either side is unknown (no manifest, implicit YAML field) the check degrades to 'compatible with anything' so a missing manifest never blocks composition. 2. NodeGeometry.heightFor now adds a 2 px allowance for the 1 px AnimatedContainer border on top + bottom of the card. Without this the last port-row's Column would overflow by 2 px under tight layout constraints (caught by the new widget tests; production canvas clipped it silently inside InteractiveViewer). 3. Width parameter on FlowNode now actually drives the outer SizedBox. The dynamic _stepWidth value from the canvas finally reaches the card border rather than being shadowed by NodeGeometry.width. Long labels (model_endpoint, source_language) no longer ellipsis-clip. Tests: 8 new widget + geometry tests covering width growth, two-column body layout, port-tooltip attachment, row-alignment between input and output sides, and the existing kindForStep classifier. All 20 editor tests pass. Bumps fai_studio_flow_editor to 0.11.0. Signed-off-by: flemming-it <sf@flemming.it>
112 lines
3.6 KiB
Dart
112 lines
3.6 KiB
Dart
// 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);
|
|
});
|
|
});
|
|
}
|