diff --git a/test/flow_yaml_controller_test.dart b/test/flow_yaml_controller_test.dart new file mode 100644 index 0000000..4dfe2bc --- /dev/null +++ b/test/flow_yaml_controller_test.dart @@ -0,0 +1,169 @@ +// Tests for FlowYamlCodeController's type-token recolouring. +// +// The controller post-processes the YAML highlighter's span +// tree to recolour any `key: ` 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; +}