add ClideCollapserCard primitive + shared card metrics (T-305)
First slice of the conversation-panel card unification. Adds the category-3 collapser primitive (lib/widgets/): a list of 1..N inner item cards, collapsed ticker <-> expanded framed inner canvas, with the agreed chrome — color drives the border + chevron/label tint, a fixed-width counter slot, the status icon hard against the right edge, chevron hard against the left edge, background + caret toggle (D-78 tail-follow). The aggregate status/count/echoed-title are caller-computed, so the widget stays free of conversation semantics; inner items keep their own per-item status. Also adds clide_card_metrics.dart — shared spacing constants (gap, radius, header padding, counter slot width) for all three card categories. Not yet wired into the stream (no user-visible change); migration of the group/tool cards follows. Widget test + golden included. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,139 @@
|
||||
/// Widget tests for [ClideCollapserCard] (T-305) — the conversation stream's
|
||||
/// category-3 collapsible card. Covers the collapsed ticker chrome (label +
|
||||
/// echoed summary + fixed counter + aggregate status), expand/collapse via the
|
||||
/// row, the background-as-toggle, item taps not hijacked, the keyboard/AT path,
|
||||
/// and the `color` driving border + label tint.
|
||||
library;
|
||||
|
||||
import 'package:clide/widgets/widgets.dart';
|
||||
import 'package:flutter/widgets.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import '../../helpers/kernel_fixture.dart';
|
||||
import '../../helpers/widget_harness.dart';
|
||||
|
||||
void main() {
|
||||
late KernelFixture f;
|
||||
setUp(() async => f = await KernelFixture.create());
|
||||
tearDown(() => f.dispose());
|
||||
|
||||
Future<void> pump(
|
||||
WidgetTester tester, {
|
||||
List<Widget>? children,
|
||||
String label = 'Edits',
|
||||
String? summary = 'clide_markdown.dart',
|
||||
String? counter = '3 edits',
|
||||
ClideRunStatus? status = ClideRunStatus.success,
|
||||
Color? color,
|
||||
bool expanded = false,
|
||||
}) async {
|
||||
await tester.pumpWidget(harness(
|
||||
f,
|
||||
Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SizedBox(
|
||||
width: 400,
|
||||
child: ClideCollapserCard(
|
||||
label: label,
|
||||
collapsedSummary: summary,
|
||||
counter: counter,
|
||||
status: status,
|
||||
color: color,
|
||||
initiallyExpanded: expanded,
|
||||
children: children ?? const [Text('item body', textDirection: TextDirection.ltr)],
|
||||
),
|
||||
),
|
||||
),
|
||||
));
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('collapsed ticker shows label, echoed summary, counter + status; items hidden', (tester) async {
|
||||
await pump(tester);
|
||||
expect(find.text('Edits'), findsOneWidget);
|
||||
expect(find.text('clide_markdown.dart'), findsOneWidget);
|
||||
expect(find.text('3 edits'), findsOneWidget);
|
||||
expect(find.byType(ClideStatusIndicator), findsOneWidget);
|
||||
expect(find.bySemanticsLabel('Edits, 3 edits, collapsed'), findsOneWidget);
|
||||
expect(find.text('item body'), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets('tapping the ticker expands to the framed item canvas', (tester) async {
|
||||
await pump(tester);
|
||||
await tester.tap(find.bySemanticsLabel('Edits, 3 edits, collapsed'));
|
||||
await tester.pump();
|
||||
expect(find.bySemanticsLabel('Edits, 3 edits, expanded'), findsOneWidget);
|
||||
expect(find.text('item body'), findsOneWidget);
|
||||
// The label + chrome persist in the expanded header.
|
||||
expect(find.text('Edits'), findsOneWidget);
|
||||
expect(find.text('3 edits'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a single item still sits in the list (1-item collapser)', (tester) async {
|
||||
await pump(tester, counter: '1 edit', children: const [Text('only item', textDirection: TextDirection.ltr)], expanded: true);
|
||||
expect(find.text('only item'), findsOneWidget);
|
||||
expect(find.text('1 edit'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a tap on the frame BACKGROUND (gutter) collapses it', (tester) async {
|
||||
await pump(tester, expanded: true);
|
||||
expect(find.text('item body'), findsOneWidget);
|
||||
|
||||
// The left gutter (x = left+4) is collapser background — items start at
|
||||
// left+10 — and below the header, so this hits the background toggle.
|
||||
final r = tester.getRect(find.byType(ClideCollapserCard));
|
||||
await tester.tapAt(Offset(r.left + 4, r.center.dy));
|
||||
await tester.pump();
|
||||
expect(find.text('item body'), findsNothing);
|
||||
expect(find.bySemanticsLabel('Edits, 3 edits, collapsed'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('a tap on an item does NOT toggle the collapser', (tester) async {
|
||||
await pump(tester, expanded: true);
|
||||
await tester.tap(find.text('item body'));
|
||||
await tester.pump();
|
||||
expect(find.text('item body'), findsOneWidget); // still expanded
|
||||
expect(find.bySemanticsLabel('Edits, 3 edits, expanded'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('the ticker is keyboard-focusable and toggles on Activate (a11y)', (tester) async {
|
||||
await pump(tester);
|
||||
final focusWidget = tester.widget<Focus>(
|
||||
find.ancestor(of: find.text('clide_markdown.dart'), matching: find.byType(Focus)).first,
|
||||
);
|
||||
focusWidget.focusNode!.requestFocus();
|
||||
await tester.pump();
|
||||
expect(focusWidget.focusNode!.hasFocus, isTrue);
|
||||
|
||||
Actions.invoke(focusWidget.focusNode!.context!, const ActivateIntent());
|
||||
await tester.pump();
|
||||
expect(find.bySemanticsLabel('Edits, 3 edits, expanded'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('color drives the border + the label tint', (tester) async {
|
||||
const teal = Color(0xFF00AB9A);
|
||||
await pump(tester, color: teal);
|
||||
|
||||
// Label text carries the color.
|
||||
final label = tester.widget<ClideText>(find.byWidgetPredicate((w) => w is ClideText && w.data == 'Edits'));
|
||||
expect(label.color, teal);
|
||||
|
||||
// Some frame Container carries a border in the color.
|
||||
expect(
|
||||
find.byWidgetPredicate((w) {
|
||||
if (w is! Container) return false;
|
||||
final d = w.decoration;
|
||||
return d is BoxDecoration && d.border?.bottom.color == teal || (d is BoxDecoration && d.border is Border && (d.border as Border).top.color == teal);
|
||||
}),
|
||||
findsWidgets,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets('no counter + no status renders a bare ticker without error', (tester) async {
|
||||
await pump(tester, counter: null, status: null, summary: null);
|
||||
expect(find.text('Edits'), findsOneWidget);
|
||||
expect(find.byType(ClideStatusIndicator), findsNothing);
|
||||
expect(tester.takeException(), isNull);
|
||||
expect(find.bySemanticsLabel('Edits, collapsed'), findsOneWidget);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user