Three operator-UX gaps closed in one pass — the diagnostic
strip stayed plain Text (no copy), the wavy underline gave no
hover tooltip (had to read the strip), and there was no way to
act on an issue without leaving the editor.
- **Selectable + copyable strip**. Both the header summary
and the per-row issue message are now SelectableText. Per-
row Copy button (compact icon) lives next to the message;
header carries a 'Copy all' that copies every issue as
'L7: <message>' lines. Trackpad-only operators no longer
have to use the system selection gesture.
- **Hover tooltip on the wavy underline**. The controller
attaches TextSpan.onEnter / onExit handlers to every
issue-overlapping leaf and publishes IssueHoverRequest via
a ValueNotifier. FlowEditorPage listens and inserts an
OverlayEntry tooltip card near the cursor. Card carries
its own MouseRegion that cancels the dismiss timer so the
operator can slide INTO it to click the action buttons.
- **Quick fixes for the two most common issues**:
· 'Unknown capability X' → InstallCapabilityFix (delegated
to host via the new onInstallCapability callback). On
success, controller.setAvailableCapabilities + reanalyze
clear the issue automatically.
· 'Unknown type Y' with a Levenshtein-distance-≤2 match
→ ReplaceLineValueFix. Applied by the editor itself:
line is mutated, key + indent preserved, comment
preserved, then reanalyze.
Distance > 2 stays unfixed — pushing 'zonglefax' to 'bytes'
would be worse than no suggestion.
New public surface (exported from the package):
- QuickFix sealed base + InstallCapabilityFix + ReplaceLineValueFix
- InstallCapabilityCallback typedef
- IssueHoverRequest + IssueHoverSeverity
- FlowEditorPage.onInstallCapability prop
Tests:
- FlowAnalyzer attaches InstallCapabilityFix to capability
issues
- FlowAnalyzer suggests the closest valid type for typos
- FlowAnalyzer emits no fix when the typo is too far
All 33 editor tests green. Bumped to 0.17.0.
Signed-off-by: flemming-it <sf@flemming.it>
158 lines
4.6 KiB
Dart
158 lines
4.6 KiB
Dart
// Behavioural tests for FlowAnalyzer — the YAML-aware
|
|
// AbstractAnalyzer that feeds the flow editor's gutter + the
|
|
// custom CodeController's wavy-underline overrides.
|
|
|
|
import 'package:fai_studio_flow_editor/src/flow_analyzer.dart';
|
|
import 'package:fai_studio_flow_editor/src/quick_fix.dart';
|
|
import 'package:flutter_code_editor/flutter_code_editor.dart';
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:highlight/languages/yaml.dart';
|
|
|
|
Code _wrap(String text) => Code(text: text, language: yaml);
|
|
|
|
void main() {
|
|
group('FlowAnalyzer', () {
|
|
test('empty buffer reports no issues', () async {
|
|
final a = FlowAnalyzer(availableCapabilities: () => const []);
|
|
final r = await a.analyze(_wrap(''));
|
|
expect(r.issues, isEmpty);
|
|
});
|
|
|
|
test('valid hello flow reports no issues', () async {
|
|
final a = FlowAnalyzer(
|
|
availableCapabilities: () => const ['debug.echo'],
|
|
);
|
|
final r = await a.analyze(_wrap('''
|
|
name: hello
|
|
inputs:
|
|
name: text
|
|
steps:
|
|
- id: greet
|
|
use: debug.echo@^0
|
|
with:
|
|
message: \$inputs.name
|
|
outputs:
|
|
greeting: \$greet.echoed
|
|
'''));
|
|
expect(r.issues, isEmpty);
|
|
});
|
|
|
|
test('flags unknown capability on the use line', () async {
|
|
final a = FlowAnalyzer(
|
|
availableCapabilities: () => const ['debug.echo'],
|
|
);
|
|
final r = await a.analyze(_wrap('''
|
|
name: hello
|
|
inputs:
|
|
msg: text
|
|
steps:
|
|
- id: t
|
|
use: text.translate@^1
|
|
outputs: {}
|
|
'''));
|
|
expect(r.issues, hasLength(1));
|
|
expect(r.issues.first.message, contains('text.translate'));
|
|
expect(r.issues.first.type, IssueType.error);
|
|
});
|
|
|
|
test('flags unknown input type', () async {
|
|
final a = FlowAnalyzer(availableCapabilities: () => const []);
|
|
final r = await a.analyze(_wrap('''
|
|
name: hello
|
|
inputs:
|
|
pdf: byes
|
|
steps: []
|
|
outputs: {}
|
|
'''));
|
|
expect(r.issues, hasLength(1));
|
|
expect(r.issues.first.message, contains('byes'));
|
|
expect(r.issues.first.type, IssueType.warning);
|
|
});
|
|
|
|
test('YAML parse error pins to the offending line', () async {
|
|
final a = FlowAnalyzer(availableCapabilities: () => const []);
|
|
// Unterminated quoted string — a real parse error rather
|
|
// than a structural oddity YAML happens to tolerate.
|
|
final r = await a.analyze(_wrap('name: "unterminated\n'));
|
|
expect(r.issues, hasLength(1));
|
|
expect(r.issues.first.type, IssueType.error);
|
|
expect(r.issues.first.message, startsWith('YAML:'));
|
|
});
|
|
|
|
test('matches the bare capability id ignoring @version', () async {
|
|
final a = FlowAnalyzer(
|
|
availableCapabilities: () => const ['text.echo'],
|
|
);
|
|
final r = await a.analyze(_wrap('''
|
|
name: x
|
|
steps:
|
|
- id: e
|
|
use: text.echo@^2
|
|
'''));
|
|
expect(r.issues, isEmpty);
|
|
});
|
|
|
|
test('empty installed list silences capability checks', () async {
|
|
// When the host hasn't supplied a list (Studio still
|
|
// booting, or running in CLI-only mode), we deliberately
|
|
// don't false-positive every `use:` row.
|
|
final a = FlowAnalyzer(availableCapabilities: () => const []);
|
|
final r = await a.analyze(_wrap('''
|
|
steps:
|
|
- id: e
|
|
use: text.echo@^2
|
|
'''));
|
|
expect(r.issues, isEmpty);
|
|
});
|
|
|
|
test('attaches InstallCapabilityFix to unknown-capability issue',
|
|
() async {
|
|
final a = FlowAnalyzer(
|
|
availableCapabilities: () => const ['debug.echo'],
|
|
);
|
|
final r = await a.analyze(_wrap('''
|
|
name: x
|
|
steps:
|
|
- id: c
|
|
use: text.classify@^1
|
|
'''));
|
|
expect(r.issues, hasLength(1));
|
|
final fixes = a.fixesFor(r.issues.first);
|
|
expect(fixes, hasLength(1));
|
|
final fix = fixes.first;
|
|
expect(fix, isA<InstallCapabilityFix>());
|
|
expect((fix as InstallCapabilityFix).capability, 'text.classify@^1');
|
|
});
|
|
|
|
test('suggests closest valid type for a misspelled value', () async {
|
|
final a = FlowAnalyzer(availableCapabilities: () => const []);
|
|
final r = await a.analyze(_wrap('''
|
|
name: x
|
|
inputs:
|
|
pdf: byes
|
|
steps: []
|
|
'''));
|
|
expect(r.issues, hasLength(1));
|
|
final fixes = a.fixesFor(r.issues.first);
|
|
expect(fixes, hasLength(1));
|
|
final fix = fixes.first;
|
|
expect(fix, isA<ReplaceLineValueFix>());
|
|
expect((fix as ReplaceLineValueFix).replacement, 'bytes');
|
|
expect(fix.line, r.issues.first.line);
|
|
});
|
|
|
|
test('emits no fix when the typo is far from every known type',
|
|
() async {
|
|
final a = FlowAnalyzer(availableCapabilities: () => const []);
|
|
final r = await a.analyze(_wrap('''
|
|
name: x
|
|
inputs:
|
|
pdf: zonglefax
|
|
steps: []
|
|
'''));
|
|
expect(r.issues, hasLength(1));
|
|
final fixes = a.fixesFor(r.issues.first);
|
|
expect(fixes, isEmpty);
|
|
});
|
|
});
|
|
}
|