chain-studio-flow-editor/test/flow_yaml_controller_test.dart
flemming-it ae443c6f81 test(editor): three widget cases pin type-token recolouring
Locks the FlowYamlCodeController.buildTextSpan post-processing
against silent regression. The original 0.15.0 ship used the
wrong regex — `type: <name>` instead of `<field>: <name>` —
and no test caught it; the only signal was the user saying 'I
see no colours'. These cases would have flipped red.

Covers:

  - indented `field: bytes` value gets the wire-bytes hue +
    the 600 weight that signals 'recoloured by F∆I' (and not
    the highlighter's stock string colour)
  - top-level `name: text` is NOT flagged as a type decl
    even though the right-hand side reads as a known type
    token — the leading-whitespace guard must hold
  - explicit `type: bytes` row in a module-shape inputs list
    still gets recoloured (the regex now handles both shapes
    via the same key:value pattern)

Tests render the controller into a tiny MaterialApp tree, walk
the produced span tree, and assert colour+weight on the
matching leaf. Pumping a full second after layout drains the
analyzer's 500ms debounce timer so the harness doesn't leak it.

Signed-off-by: flemming-it <sf@flemming.it>
2026-06-04 02:48:54 +02:00

169 lines
5.6 KiB
Dart

// Tests for FlowYamlCodeController's type-token recolouring.
//
// The controller post-processes the YAML highlighter's span
// tree to recolour any `key: <type>` value where the value is
// one of `text|json|bytes|file|number|integer` and the key sits
// at an indented depth (so top-level `name: text` doesn't
// false-match). We can't easily inspect TextSpan colours without
// rendering, so the tests below render the controller into a
// CodeField inside a TestWidget tree and walk the produced
// span tree.
import 'package:fai_studio_flow_editor/src/flow_yaml_controller.dart';
import 'package:fai_studio_flow_editor/src/wire_colors.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
group('FlowYamlCodeController type-token recoloring', () {
testWidgets('colours indented `field: bytes` value with bytes wire hue', (
tester,
) async {
final c = FlowYamlCodeController();
addTearDown(c.dispose);
c.fullText = '''
inputs:
document: bytes
''';
late TextSpan span;
late ThemeData theme;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
theme = Theme.of(context);
span = c.buildTextSpan(context: context, withComposing: false);
return const SizedBox();
},
),
),
);
// Let CodeController's 500ms analyzer debounce fire so
// the test teardown isn't blocked by a pending timer.
await tester.pump(const Duration(seconds: 1));
final tokens = _flattenLeaves(span);
// Find the leaf whose text is the word "bytes" and assert
// it carries the wire-bytes colour from the shared palette.
final bytesLeaf = tokens.firstWhere(
(t) => t.text == 'bytes',
orElse: () => const _Leaf('', null),
);
expect(bytesLeaf.text, 'bytes', reason: 'expected a "bytes" leaf');
final expected = wireColor('bytes', theme);
expect(bytesLeaf.style?.color, expected);
expect(bytesLeaf.style?.fontWeight, FontWeight.w600);
});
testWidgets('does NOT recolour top-level `name: text` (not indented)', (
tester,
) async {
final c = FlowYamlCodeController();
addTearDown(c.dispose);
// The flow's top-level `name: hello` is also not a type
// declaration; we use `text` here to make the trap
// explicit. The leading `name:` is at column 0 — not
// indented — so the recolour must skip it.
c.fullText = 'name: text\n';
late TextSpan span;
late ThemeData theme;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
theme = Theme.of(context);
span = c.buildTextSpan(context: context, withComposing: false);
return const SizedBox();
},
),
),
);
// Let CodeController's 500ms analyzer debounce fire so
// the test teardown isn't blocked by a pending timer.
await tester.pump(const Duration(seconds: 1));
final tokens = _flattenLeaves(span);
final textLeaf = tokens.firstWhere(
(t) => t.text == 'text',
orElse: () => const _Leaf('', null),
);
// At top level the highlighter does paint it as a string
// (green-ish, palette-derived), but it must NOT match the
// explicit wire-text hue + 600 weight combo our recolour
// applies — that's the signal we'd be incorrectly tagging
// it as a type declaration.
final wireText = wireColor('text', theme);
final isRecoloured =
textLeaf.style?.color == wireText &&
textLeaf.style?.fontWeight == FontWeight.w600;
expect(
isRecoloured,
isFalse,
reason: 'top-level `name: text` must not be flagged as a type decl',
);
});
testWidgets('colours `type: text` value (explicit-type module shape)', (
tester,
) async {
final c = FlowYamlCodeController();
addTearDown(c.dispose);
c.fullText = '''
inputs:
- name: pdf
type: bytes
''';
late TextSpan span;
late ThemeData theme;
await tester.pumpWidget(
MaterialApp(
home: Builder(
builder: (context) {
theme = Theme.of(context);
span = c.buildTextSpan(context: context, withComposing: false);
return const SizedBox();
},
),
),
);
// Let CodeController's 500ms analyzer debounce fire so
// the test teardown isn't blocked by a pending timer.
await tester.pump(const Duration(seconds: 1));
final tokens = _flattenLeaves(span);
final bytesLeaves = tokens.where((t) => t.text == 'bytes').toList();
expect(bytesLeaves, isNotEmpty);
final expected = wireColor('bytes', theme);
// Every `bytes` leaf in this snippet is the type value of
// the `type: bytes` line.
for (final leaf in bytesLeaves) {
expect(leaf.style?.color, expected);
}
});
});
}
class _Leaf {
final String text;
final TextStyle? style;
const _Leaf(this.text, this.style);
}
List<_Leaf> _flattenLeaves(InlineSpan span) {
final out = <_Leaf>[];
void visit(InlineSpan s, TextStyle? inherited) {
if (s is! TextSpan) return;
final merged = inherited == null
? s.style
: (s.style == null ? inherited : inherited.merge(s.style));
final t = s.text;
if (t != null && t.isNotEmpty) out.add(_Leaf(t, merged));
final children = s.children;
if (children != null) {
for (final c in children) visit(c, merged);
}
}
visit(span, null);
return out;
}