group consecutive same-file edits into one collapsed card (T-296)

A run of 2+ consecutive edits to the same file now folds into one ClideHolderCard
labelled '# edits' (coalesceEditRuns, run after groupConversation) instead of a
stack of cards; a different file or an interleaving step splits the run. Every
edit stays reachable on expand.

The holder gained an optional aggregate status. New owned primitives: ClideSpinner
(the logo mark, monochrome, 3D Y-axis rotation, reduced-motion-aware) and
ClideStatusIndicator (running→spinner / success→check / error→cross, with an
AnimatedSwitcher seam for a richer transition later — kept self-contained, not
built on ConversationCard's mark). The activity card shares the same indicator.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 18:34:27 +02:00
co-authored by Claude Opus 4.8
parent 6efb3b5d2b
commit 407ed25ab0
10 changed files with 447 additions and 6 deletions
@@ -0,0 +1,61 @@
/// Tests for ClideSpinner + ClideStatusIndicator (T-296): the run-status glyph
/// (spinner / check / cross) and its reduced-motion behaviour.
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());
// Disable animations so the spinner is static (no perpetual rotation) and
// pumpAndSettle can settle — mirroring how every animated widget behaves
// under reduced motion.
Future<void> pump(WidgetTester tester, Widget child) => tester.pumpWidget(harness(
f,
Builder(
builder: (ctx) => MediaQuery(
data: MediaQuery.of(ctx).copyWith(disableAnimations: true),
child: Center(child: child),
),
),
));
Finder iconWith(Object painterType) => find.byWidgetPredicate((w) => w is ClideIcon && w.painter.runtimeType == painterType);
group('ClideSpinner', () {
testWidgets('renders the logo mark and settles under reduced motion', (tester) async {
await pump(tester, const ClideSpinner(size: 16));
await tester.pumpAndSettle(); // would hang if it kept rotating
expect(find.byType(ClideSvgView), findsOneWidget);
expect(tester.takeException(), isNull);
});
});
group('ClideStatusIndicator', () {
testWidgets('running shows the spinner', (tester) async {
await pump(tester, const ClideStatusIndicator(status: ClideRunStatus.running));
await tester.pumpAndSettle();
expect(find.byType(ClideSpinner), findsOneWidget);
});
testWidgets('success shows a check', (tester) async {
await pump(tester, const ClideStatusIndicator(status: ClideRunStatus.success));
await tester.pumpAndSettle();
expect(iconWith(CheckIcon), findsOneWidget);
expect(find.byType(ClideSpinner), findsNothing);
});
testWidgets('error shows a cross', (tester) async {
await pump(tester, const ClideStatusIndicator(status: ClideRunStatus.error));
await tester.pumpAndSettle();
expect(iconWith(CloseIcon), findsOneWidget);
});
});
}