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>
This commit is contained in:
parent
911f368362
commit
ae443c6f81
1 changed files with 169 additions and 0 deletions
169
test/flow_yaml_controller_test.dart
Normal file
169
test/flow_yaml_controller_test.dart
Normal file
|
|
@ -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: <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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue