feat(editor): type-checked connections + card-height fix + widget tests
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>
This commit is contained in:
parent
a1d25fd48a
commit
7b256b5e35
5 changed files with 264 additions and 21 deletions
112
test/flow_node_test.dart
Normal file
112
test/flow_node_test.dart
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue