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