Stage 2: Library search, filters and Exercise detail with Flip Animation

- LibraryFilter: word search on names; muscle (primary), equipment and
  level choices that widen within a filter and narrow across filters
- ExerciseBrowser (reusable for the Plan picker) with search field,
  filter sheets showing live result counts, a clear button and an
  empty state
- Exercise detail: start/end Flip Animation at each Mode's pace (still
  with reduced motion), facts, main/secondary muscles, numbered steps

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
CedricandClaude Opus 5.5 committed 2026-10-28 23:33:07 +01:00
1 parent 1289fadb98
commit 786dd52e30
16 files changed
+1272 -32

No files matched your search

+83
View File
@@ -0,0 +1,83 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:zenxercise/src/exercises/views/exercise_detail_view.dart';
import 'package:zenxercise/src/exercises/views/flip_animation.dart';
import '../support/test_app.dart';
void main() {
Future<void> openLibrary(WidgetTester tester) async {
usePhoneScreen(tester);
await pumpApp(tester);
await tester.tap(find.byTooltip('Library'));
await tester.pumpAndSettle();
}
testWidgets('search narrows the list and the count follows', (tester) async {
await openLibrary(tester);
await tester.enterText(find.byType(TextField), 'calf');
await tester.pumpAndSettle();
expect(find.text('Calf Stretch'), findsOneWidget);
expect(find.text('90/90 Hamstring'), findsNothing);
expect(find.text('1 of 2 stretches'), findsOneWidget);
});
testWidgets('a muscle filter narrows the list and clearing brings everything back', (tester) async {
await openLibrary(tester);
await tester.tap(find.text('Muscle'));
await tester.pumpAndSettle();
await tester.tap(find.widgetWithText(FilterChip, 'Calves'));
await tester.pumpAndSettle();
await tester.tap(find.text('Show 1 result'));
await tester.pumpAndSettle();
expect(find.text('Calf Stretch'), findsOneWidget);
expect(find.text('90/90 Hamstring'), findsNothing);
await tester.tap(find.byTooltip('Clear filters'));
await tester.pumpAndSettle();
expect(find.text('90/90 Hamstring'), findsOneWidget);
});
testWidgets('no matches says so and offers to clear', (tester) async {
await openLibrary(tester);
await tester.enterText(find.byType(TextField), 'zzz');
await tester.pumpAndSettle();
expect(find.text('No matches'), findsOneWidget);
await tester.tap(find.text('Clear filters'));
await tester.pumpAndSettle();
expect(find.text('90/90 Hamstring'), findsOneWidget);
expect(find.text('zzz'), findsNothing);
});
testWidgets('an Exercise opens its detail page, whose images flip', (tester) async {
await openLibrary(tester);
await tester.tap(find.text('90/90 Hamstring'));
await tester.pumpAndSettle();
expect(find.byType(ExerciseDetailView), findsOneWidget);
int frame() => tester.state<FlipAnimationState>(find.byType(FlipAnimation)).frame;
expect(frame(), 0);
await tester.pump(const Duration(milliseconds: 1500));
expect(frame(), 1);
final page = find.descendant(of: find.byType(ExerciseDetailView), matching: find.byType(Scrollable)).first;
for (final text in ['Hamstrings', 'Calves', 'Lie on your back.']) {
await tester.scrollUntilVisible(find.text(text), 100, scrollable: page);
expect(find.text(text), findsOneWidget);
}
// Leave the page so its flip timer stops.
await tester.pageBack();
await tester.pumpAndSettle();
});
}