Files
clide/test/builtin/tickets/tickets_view_test.dart
T
jpmschweitzerandClaude Opus 4.8 a62003a833 tickets panel: parent/child as a tree, bold subject ticket (T-281)
Replace the ambiguous inline "T-1 ← T-9" id row on ticket cards with a
small tree: the parent renders as a muted, clickable breadcrumb above, and
the card's own ticket sits below it under a "└" connector in bold — so it's
clear which id is the subject and which is its parent. Tapping the parent
selects it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 13:49:36 +02:00

140 lines
5.0 KiB
Dart

/// Tests for the tickets sidebar list (TicketsView): load, sectioned
/// rendering, filtering, card selection, and the empty/error states.
library;
import 'package:clide/builtin/tickets/src/tickets_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';
Map<String, Object?> _t(String id, String title, String status, {String? type, String? parentId}) => {
'id': id,
'title': title,
'status': status,
'type': type ?? 'task',
'priority': 'medium',
if (parentId != null) 'parent_id': parentId,
};
IpcResponse _list(List<Map<String, Object?>> tickets) => IpcResponse.ok(id: '', data: {'tickets': tickets});
void main() {
late KernelFixture f;
setUp(() async {
f = await KernelFixture.create();
});
tearDown(() => f.dispose());
Future<void> pumpView(WidgetTester tester) async {
tester.view.physicalSize = const Size(600, 900);
tester.view.devicePixelRatio = 1.0;
addTearDown(() {
tester.view.resetPhysicalSize();
tester.view.resetDevicePixelRatio();
});
await tester.pumpWidget(harness(f, const TicketsView()));
await pumpAsync(tester);
}
testWidgets('shows a loading placeholder until the list resolves', (tester) async {
f.ipc.stub('pql.tickets.list', (_) async => _list([_t('T-1', 'First', 'backlog')]));
await tester.pumpWidget(harness(f, const TicketsView()));
expect(find.text('Loading tickets...'), findsOneWidget);
await pumpAsync(tester);
expect(find.text('Loading tickets...'), findsNothing);
});
testWidgets('renders sectioned cards from the loaded list', (tester) async {
f.ipc.stub(
'pql.tickets.list',
(_) async => _list([
_t('T-1', 'Active thing', 'in_progress', parentId: 'T-9'),
_t('T-2', 'Queued thing', 'backlog'),
]));
await pumpView(tester);
expect(find.textContaining('IN PROGRESS'), findsOneWidget);
expect(find.textContaining('BACKLOG'), findsOneWidget);
expect(find.text('T-1'), findsOneWidget);
expect(find.text('Active thing'), findsOneWidget);
expect(find.text('Queued thing'), findsOneWidget);
// parent breadcrumb on T-1
expect(find.text('T-9'), findsOneWidget);
});
testWidgets('filter narrows the visible cards', (tester) async {
f.ipc.stub(
'pql.tickets.list',
(_) async => _list([
_t('T-1', 'Alpha', 'backlog'),
_t('T-2', 'Beta', 'backlog'),
]));
await pumpView(tester);
expect(find.text('Alpha'), findsOneWidget);
await tester.enterText(find.byType(EditableText).first, 'beta');
await tester.pump(const Duration(milliseconds: 250));
expect(find.text('Beta'), findsOneWidget);
expect(find.text('Alpha'), findsNothing);
});
testWidgets('tapping a card publishes a selection', (tester) async {
f.ipc.stub('pql.tickets.list', (_) async => _list([_t('T-1', 'Tap me', 'backlog')]));
final selections = <Message>[];
final sub = f.services.messages.subscribe(publisher: 'builtin.tickets', channel: 'selection').listen(selections.add);
addTearDown(sub.cancel);
await pumpView(tester);
await tester.tap(find.text('Tap me'));
await pumpAsync(tester);
expect(selections.single.data['id'], 'T-1');
});
testWidgets('tapping the parent breadcrumb selects the parent (T-281)', (tester) async {
f.ipc.stub('pql.tickets.list', (_) async => _list([_t('T-1', 'Child', 'backlog', parentId: 'T-9')]));
final selections = <Message>[];
final sub = f.services.messages.subscribe(publisher: 'builtin.tickets', channel: 'selection').listen(selections.add);
addTearDown(sub.cancel);
await pumpView(tester);
await tester.tap(find.text('T-9')); // the muted parent breadcrumb
await pumpAsync(tester);
expect(selections.single.data['id'], 'T-9');
});
testWidgets('empty list shows the placeholder', (tester) async {
f.ipc.stub('pql.tickets.list', (_) async => _list(const []));
await pumpView(tester);
expect(find.textContaining('No tickets'), findsOneWidget);
});
testWidgets('a load error is surfaced', (tester) async {
f.ipc.stub(
'pql.tickets.list',
(_) async => IpcResponse.err(
id: '',
error: IpcError(code: IpcExitCode.toolError, kind: IpcErrorKind.toolError, message: 'boom'),
));
await pumpView(tester);
expect(find.text('boom'), findsOneWidget);
});
testWidgets('a changed event triggers a refresh', (tester) async {
var calls = 0;
f.ipc.stub('pql.tickets.list', (_) async {
calls++;
return _list([_t('T-1', 'Thing', 'backlog')]);
});
await pumpView(tester);
final before = calls;
f.services.messages.publish('builtin.tickets', 'changed', {'id': 'T-1'});
await pumpAsync(tester);
expect(calls, greaterThan(before));
});
}