Files
clide/test/builtin/files/file_tree_view_md_test.dart
T
jpmschweitzerandClaude Opus 4.8 6d0ebab721 chore: adopt Dart 3.9 toolchain — honest floor + tall-style reformat (T-353)
Raise the declared minimums in pubspec.yaml to what our deps already
require: Flutter >=3.35.0 / Dart >=3.9.0 (was 3.19.0 / 3.5.0). alchemist
0.12 needs Flutter 3.32; Dart 3.9 first ships in Flutter 3.35, so 3.35 is
the binding floor. Pin the exact build toolchain in .fvmrc (Flutter
3.44.1).

Moving to the Dart 3.9 language level switches `dart format` to the new
"tall" style and enables two new lints. This commit is the resulting
mechanical churn, isolated from any behaviour change:
  - whole-tree `dart format` reformat (tall style)
  - `dart fix` for unnecessary_underscores + use_null_aware_elements

No runtime behaviour change; `make test` green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-11 12:11:53 +02:00

155 lines
6.0 KiB
Dart

/// Widget tests for the .md routing fix in FileTreeView (T-187).
///
/// Clicking a .md file must publish ('builtin.markdown', 'selection', {path})
/// onto the kernel MessageBus rather than calling editor.open. Non-.md files
/// must still route to editor.open. Both the tree-row path (_FileRow) and the
/// filtered-row path (_FilteredFileRow) are covered.
library;
import 'package:clide/builtin/files/src/file_tree_view.dart';
import 'package:clide/clide.dart';
import 'package:clide/kernel/kernel.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../../helpers/kernel_fixture.dart';
import '../../helpers/widget_harness.dart';
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
IpcResponse _ok(Map<String, Object?> data) => IpcResponse.ok(id: '', data: data);
/// Stub the minimum IPC calls that FileTreeController.load() needs.
///
/// The [root] entry list populates the workspace root directory so the tree
/// renders at least one file row.
void _stubTree(KernelFixture f, {required String rootPath, required List<Map<String, Object?>> entries}) {
f.ipc.stub('files.root', (_) async => _ok({'path': rootPath}));
f.ipc.stub('files.watch', (_) async => _ok(const {}));
f.ipc.stub('files.ls', (args) async => _ok({'entries': entries}));
}
Map<String, Object?> _file(String name, String path) => {'name': name, 'path': path, 'isDirectory': false, 'isSymlink': false, 'sizeBytes': 0, 'modifiedMs': 0};
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
void main() {
late KernelFixture f;
setUp(() async => f = await KernelFixture.create());
tearDown(() => f.dispose());
group('FileTreeView .md routing (T-187)', () {
testWidgets('clicking a .md file publishes to builtin.markdown selection (tree row)', (tester) async {
const mdPath = 'docs/README.md';
_stubTree(f, rootPath: '/repo', entries: [_file('README.md', mdPath)]);
final published = <Message>[];
final sub = f.services.messages.subscribe(publisher: 'builtin.markdown', channel: 'selection').listen(published.add);
addTearDown(sub.cancel);
await tester.pumpWidget(harness(f, const FileTreeView()));
await pumpAsync(tester);
// The root node is auto-expanded; the file row should be visible.
final rowFinder = find.text('README.md');
expect(rowFinder, findsOneWidget);
await tester.tap(rowFinder);
await tester.pump();
// Broadcast-stream events deliver asynchronously.
await pumpAsync(tester);
expect(published, hasLength(1));
expect(published.first.data['path'], mdPath);
});
testWidgets('clicking a non-.md file calls editor.open, not the markdown bus (tree row)', (tester) async {
const dartPath = 'lib/main.dart';
_stubTree(f, rootPath: '/repo', entries: [_file('main.dart', dartPath)]);
final published = <Message>[];
final sub = f.services.messages.subscribe(publisher: 'builtin.markdown', channel: 'selection').listen(published.add);
addTearDown(sub.cancel);
final opened = <String>[];
f.ipc.stub('editor.open', (args) async {
opened.add(args['path'] as String? ?? '');
return _ok(const {});
});
await tester.pumpWidget(harness(f, const FileTreeView()));
await pumpAsync(tester);
await tester.tap(find.text('main.dart'));
await tester.pump();
await pumpAsync(tester);
// markdown bus must NOT have been published
expect(published, isEmpty);
// editor.open must have been called
expect(opened, hasLength(1));
expect(opened.first, dartPath);
});
testWidgets('clicking a .md in the filtered view publishes to builtin.markdown selection', (tester) async {
const mdPath = 'governance/decisions/architecture.md';
_stubTree(f, rootPath: '/repo', entries: [_file('architecture.md', mdPath), _file('tooling.md', 'governance/decisions/tooling.md')]);
final published = <Message>[];
final sub = f.services.messages.subscribe(publisher: 'builtin.markdown', channel: 'selection').listen(published.add);
addTearDown(sub.cancel);
await tester.pumpWidget(harness(f, const FileTreeView()));
await pumpAsync(tester);
// Type in the filter box to switch to the filtered view.
final filterBox = find.byWidgetPredicate((w) => w is EditableText);
await tester.enterText(filterBox.first, 'architecture');
await tester.pump(const Duration(milliseconds: 250)); // past ClideFilterBox's 200ms debounce
await tester.pump();
await tester.tap(find.text(mdPath));
await tester.pump();
await pumpAsync(tester);
expect(published, hasLength(1));
expect(published.first.data['path'], mdPath);
});
testWidgets('clicking a non-.md in the filtered view calls editor.open', (tester) async {
const dartPath = 'lib/app.dart';
_stubTree(f, rootPath: '/repo', entries: [_file('app.dart', dartPath)]);
final published = <Message>[];
final sub = f.services.messages.subscribe(publisher: 'builtin.markdown', channel: 'selection').listen(published.add);
addTearDown(sub.cancel);
final opened = <String>[];
f.ipc.stub('editor.open', (args) async {
opened.add(args['path'] as String? ?? '');
return _ok(const {});
});
await tester.pumpWidget(harness(f, const FileTreeView()));
await pumpAsync(tester);
final filterBox = find.byWidgetPredicate((w) => w is EditableText);
await tester.enterText(filterBox.first, 'app');
await tester.pump(const Duration(milliseconds: 250)); // past ClideFilterBox's 200ms debounce
await tester.pump();
await tester.tap(find.text(dartPath));
await tester.pump();
await pumpAsync(tester);
expect(published, isEmpty);
expect(opened, hasLength(1));
expect(opened.first, dartPath);
});
});
}