test(catalog): widget, a11y, URL sync and controller tests for catalog search
KAT-01 Kriterium 1-6 and 8, QUE-05, QUE-06. Refs #3 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
28404edd10
commit
3b66f4afb0
7 changed files with 942 additions and 23 deletions
190
app/test/core/widgets/app_state_message_test.dart
Normal file
190
app/test/core/widgets/app_state_message_test.dart
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shuffle/core/widgets/app_list_footer.dart';
|
||||||
|
import 'package:shuffle/core/widgets/app_state_message.dart';
|
||||||
|
|
||||||
|
import '../../helpers/pump_app.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('AppStateMessage', () {
|
||||||
|
Future<void> pumpMessage(
|
||||||
|
WidgetTester tester, {
|
||||||
|
String? message,
|
||||||
|
List<AppStateAction> actions = const [],
|
||||||
|
ThemeMode themeMode = ThemeMode.light,
|
||||||
|
double? textScale,
|
||||||
|
}) => pumpApp(
|
||||||
|
tester,
|
||||||
|
themeMode: themeMode,
|
||||||
|
textScale: textScale,
|
||||||
|
home: Scaffold(
|
||||||
|
body: AppStateMessage(
|
||||||
|
icon: Icons.search_off,
|
||||||
|
title: 'Nichts gefunden',
|
||||||
|
message: message,
|
||||||
|
actions: actions,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('shows title, hint and actions', (tester) async {
|
||||||
|
var pressed = 0;
|
||||||
|
await pumpMessage(
|
||||||
|
tester,
|
||||||
|
message: 'Prüfe die Schreibweise.',
|
||||||
|
actions: [
|
||||||
|
AppStateAction(
|
||||||
|
key: const Key('first'),
|
||||||
|
label: 'Erneut versuchen',
|
||||||
|
onPressed: () => pressed++,
|
||||||
|
),
|
||||||
|
AppStateAction(label: 'Zurücksetzen', onPressed: () {}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Nichts gefunden'), findsOneWidget);
|
||||||
|
expect(find.text('Prüfe die Schreibweise.'), findsOneWidget);
|
||||||
|
await tester.tap(find.byKey(const Key('first')));
|
||||||
|
expect(pressed, 1);
|
||||||
|
expect(find.text('Zurücksetzen'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('without hint and actions it is just icon and title', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
await pumpMessage(tester);
|
||||||
|
|
||||||
|
expect(find.byType(FilledButton), findsNothing);
|
||||||
|
expect(find.byType(OutlinedButton), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final mode in ThemeMode.values.where((m) => m != ThemeMode.system)) {
|
||||||
|
testWidgets('meets accessibility guidelines ($mode)', (tester) async {
|
||||||
|
final handle = tester.ensureSemantics();
|
||||||
|
await pumpMessage(
|
||||||
|
tester,
|
||||||
|
message: 'Hinweis',
|
||||||
|
themeMode: mode,
|
||||||
|
actions: [
|
||||||
|
AppStateAction(label: 'Erneut versuchen', onPressed: () {}),
|
||||||
|
AppStateAction(label: 'Zurücksetzen', onPressed: () {}),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||||
|
handle.dispose();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('text scale 2.0 does not overflow', (tester) async {
|
||||||
|
await pumpMessage(
|
||||||
|
tester,
|
||||||
|
message: 'Ein längerer Hinweis, der umbrechen muss.',
|
||||||
|
textScale: 2,
|
||||||
|
actions: [AppStateAction(label: 'Erneut versuchen', onPressed: () {})],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('AppListFooter', () {
|
||||||
|
Future<void> pumpFooter(
|
||||||
|
WidgetTester tester, {
|
||||||
|
bool isLoading = false,
|
||||||
|
bool hasMore = false,
|
||||||
|
String? errorMessage,
|
||||||
|
VoidCallback? onLoadMore,
|
||||||
|
VoidCallback? onRetry,
|
||||||
|
}) => pumpApp(
|
||||||
|
tester,
|
||||||
|
home: Scaffold(
|
||||||
|
body: AppListFooter(
|
||||||
|
isLoading: isLoading,
|
||||||
|
hasMore: hasMore,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
loadingLabel: 'Lädt',
|
||||||
|
loadMoreLabel: 'Weitere laden',
|
||||||
|
retryLabel: 'Erneut versuchen',
|
||||||
|
onLoadMore: onLoadMore ?? () {},
|
||||||
|
onRetry: onRetry ?? () {},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
settle: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('shows the load-more button while more is available', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
var loads = 0;
|
||||||
|
await pumpFooter(tester, hasMore: true, onLoadMore: () => loads++);
|
||||||
|
|
||||||
|
await tester.tap(find.text('Weitere laden'));
|
||||||
|
|
||||||
|
expect(loads, 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows a labeled indicator while loading', (tester) async {
|
||||||
|
final handle = tester.ensureSemantics();
|
||||||
|
await pumpFooter(tester, hasMore: true, isLoading: true);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('list_footer_loading')), findsOneWidget);
|
||||||
|
expect(find.text('Weitere laden'), findsNothing);
|
||||||
|
expect(find.bySemanticsLabel('Lädt'), findsOneWidget);
|
||||||
|
handle.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows the error with retry, ahead of everything else', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
var retries = 0;
|
||||||
|
await pumpFooter(
|
||||||
|
tester,
|
||||||
|
hasMore: true,
|
||||||
|
isLoading: true,
|
||||||
|
errorMessage: 'Fehler beim Laden',
|
||||||
|
onRetry: () => retries++,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Fehler beim Laden'), findsOneWidget);
|
||||||
|
await tester.tap(find.byKey(const Key('list_footer_retry')));
|
||||||
|
expect(retries, 1);
|
||||||
|
expect(find.byKey(const Key('list_footer_loading')), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('shows nothing at the end of the list', (tester) async {
|
||||||
|
await pumpFooter(tester);
|
||||||
|
|
||||||
|
expect(find.byType(OutlinedButton), findsNothing);
|
||||||
|
expect(find.byKey(const Key('list_footer_loading')), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the load-more button takes a focus node', (tester) async {
|
||||||
|
final node = FocusNode();
|
||||||
|
addTearDown(node.dispose);
|
||||||
|
await pumpApp(
|
||||||
|
tester,
|
||||||
|
home: Scaffold(
|
||||||
|
body: AppListFooter(
|
||||||
|
isLoading: false,
|
||||||
|
hasMore: true,
|
||||||
|
loadingLabel: 'Lädt',
|
||||||
|
loadMoreLabel: 'Weitere laden',
|
||||||
|
retryLabel: 'Erneut versuchen',
|
||||||
|
onLoadMore: () {},
|
||||||
|
onRetry: () {},
|
||||||
|
loadMoreFocusNode: node,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
settle: false,
|
||||||
|
);
|
||||||
|
|
||||||
|
node.requestFocus();
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
expect(node.hasPrimaryFocus, isTrue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -6,10 +6,10 @@ import 'package:http/http.dart' as http;
|
||||||
import 'package:http/testing.dart';
|
import 'package:http/testing.dart';
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
import 'package:shuffle/core/logging/app_logger.dart';
|
import 'package:shuffle/core/logging/app_logger.dart';
|
||||||
|
import 'package:shuffle/features/catalog/data/supabase_catalog_repository.dart';
|
||||||
import 'package:shuffle/features/catalog/domain/catalog_failure.dart';
|
import 'package:shuffle/features/catalog/domain/catalog_failure.dart';
|
||||||
import 'package:shuffle/features/catalog/domain/catalog_search_query.dart';
|
import 'package:shuffle/features/catalog/domain/catalog_search_query.dart';
|
||||||
import 'package:supabase_flutter/supabase_flutter.dart';
|
import 'package:supabase_flutter/supabase_flutter.dart';
|
||||||
import 'package:shuffle/features/catalog/data/supabase_catalog_repository.dart';
|
|
||||||
|
|
||||||
import '../catalog_test_support.dart';
|
import '../catalog_test_support.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,162 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shuffle/features/catalog/domain/catalog_search_query.dart';
|
||||||
|
|
||||||
|
import '../../catalog_test_support.dart';
|
||||||
|
import 'catalog_search_view_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('announcements', () {
|
||||||
|
testWidgets('QUE-06 Kriterium 2: the hit count is announced once', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: List.generate(75, game));
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
tester.takeAnnouncements();
|
||||||
|
|
||||||
|
await tester.enterText(searchInput, 'spiel');
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||||
|
await tester.pump();
|
||||||
|
// Not at once: the announcement waits for a quiet moment.
|
||||||
|
expect(tester.takeAnnouncements(), isEmpty);
|
||||||
|
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final messages = tester.takeAnnouncements().map((a) => a.message);
|
||||||
|
expect(messages, ['75 Spiele gefunden']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('QUE-06 Kriterium 2: nothing is announced while typing', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: List.generate(75, game));
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
tester.takeAnnouncements();
|
||||||
|
|
||||||
|
await tester.enterText(searchInput, 'sp');
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.enterText(searchInput, 'spi');
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
|
||||||
|
expect(tester.takeAnnouncements(), isEmpty);
|
||||||
|
// Let the pending search run out before the test ends.
|
||||||
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('quick filter changes give one announcement (debounced)', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
tester.takeAnnouncements();
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_filter_button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreStrategy')));
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreFamily')));
|
||||||
|
await tester.pump(const Duration(milliseconds: 100));
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$mechanicDice')));
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final messages = tester.takeAnnouncements().map((a) => a.message);
|
||||||
|
expect(messages, ['1 Spiel gefunden']);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('an empty result is announced as well', (tester) async {
|
||||||
|
final repository = FakeCatalogRepository();
|
||||||
|
await pumpView(
|
||||||
|
tester,
|
||||||
|
repository,
|
||||||
|
initialQuery: const CatalogSearchQuery(text: 'zzz'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final messages = tester.takeAnnouncements().map((a) => a.message);
|
||||||
|
expect(messages, contains('Keine Spiele gefunden'));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('English announcement', (tester) async {
|
||||||
|
final repository = FakeCatalogRepository(all: List.generate(2, game));
|
||||||
|
await pumpView(tester, repository, locale: const Locale('en'));
|
||||||
|
|
||||||
|
await tester.pump(const Duration(seconds: 1));
|
||||||
|
|
||||||
|
final messages = tester.takeAnnouncements().map((a) => a.message);
|
||||||
|
expect(messages, contains('2 games found'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('accessibility guidelines (QUE-06 Kriterium 1, 3)', () {
|
||||||
|
Future<void> check(WidgetTester tester, {double width = 420}) async {
|
||||||
|
final handle = tester.ensureSemantics();
|
||||||
|
final repository = FakeCatalogRepository(
|
||||||
|
all: [
|
||||||
|
game(1, matchedAltTitle: 'Alternativtitel'),
|
||||||
|
game(2, publishers: ['A', 'B', 'C']),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
await pumpView(
|
||||||
|
tester,
|
||||||
|
repository,
|
||||||
|
width: width,
|
||||||
|
initialQuery: const CatalogSearchQuery(text: 'spiel'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(iOSTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||||
|
handle.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('compact layout', check);
|
||||||
|
testWidgets('expanded layout with side bar', (tester) async {
|
||||||
|
await check(tester, width: 1200);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('filter sheet', (tester) async {
|
||||||
|
final handle = tester.ensureSemantics();
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_filter_button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||||
|
handle.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('empty and error states', (tester) async {
|
||||||
|
final handle = tester.ensureSemantics();
|
||||||
|
final repository = FakeCatalogRepository();
|
||||||
|
await pumpView(
|
||||||
|
tester,
|
||||||
|
repository,
|
||||||
|
initialQuery: const CatalogSearchQuery(
|
||||||
|
text: 'zzz',
|
||||||
|
filters: CatalogFilters(players: 2),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||||||
|
await expectLater(tester, meetsGuideline(textContrastGuideline));
|
||||||
|
handle.dispose();
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('search field is labeled for screen readers', (tester) async {
|
||||||
|
final handle = tester.ensureSemantics();
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
|
||||||
|
expect(find.bySemanticsLabel('Spiele suchen'), findsWidgets);
|
||||||
|
expect(find.bySemanticsLabel('Suchbegriff löschen'), findsNothing);
|
||||||
|
handle.dispose();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,362 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:shuffle/features/catalog/domain/catalog_failure.dart';
|
||||||
|
import 'package:shuffle/features/catalog/domain/catalog_search_query.dart';
|
||||||
|
|
||||||
|
import '../../catalog_test_support.dart';
|
||||||
|
import 'catalog_search_view_test.dart';
|
||||||
|
|
||||||
|
Future<void> openFilterSheet(WidgetTester tester) async {
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_filter_button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('filters in the bottom sheet (compact)', () {
|
||||||
|
testWidgets('KAT-01 Kriterium 4: the button shows the number of filters', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
|
||||||
|
await pumpView(
|
||||||
|
tester,
|
||||||
|
repository,
|
||||||
|
initialQuery: const CatalogSearchQuery(
|
||||||
|
filters: CatalogFilters(players: 2, genreIds: {genreFamily}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(find.text('Filter (2)'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the button shows plain "Filter" without filters', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
|
||||||
|
expect(find.text('Filter'), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('KAT-01 Kriterium 4: each filter kind sets its value', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
await openFilterSheet(tester);
|
||||||
|
|
||||||
|
Future<void> pick(String fieldKey, String label) async {
|
||||||
|
await tester.tap(find.byKey(Key(fieldKey)));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text(label).last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
}
|
||||||
|
|
||||||
|
await pick('catalog_filter_players', '4 Spieler');
|
||||||
|
await pick('catalog_filter_duration', 'Bis 60 Min.');
|
||||||
|
await pick('catalog_filter_age', 'Ab 10 Jahren geeignet');
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreStrategy')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$mechanicDice')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final filters = repository.calls.last.query.filters;
|
||||||
|
expect(filters.players, 4);
|
||||||
|
expect(filters.maxMinutes, 60);
|
||||||
|
expect(filters.minAgeSuitable, 10);
|
||||||
|
expect(filters.genreIds, {genreStrategy});
|
||||||
|
expect(filters.mechanicIds, {mechanicDice});
|
||||||
|
// The list behind the sheet updates immediately.
|
||||||
|
expect(repository.calls.length, greaterThan(5));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('KAT-01 Kriterium 4: two genres are both sent (AND)', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
await openFilterSheet(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreStrategy')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreFamily')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.calls.last.query.filters.genreIds, {
|
||||||
|
genreStrategy,
|
||||||
|
genreFamily,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('KAT-01 Kriterium 4: a chip can be deselected again', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
await openFilterSheet(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreStrategy')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreStrategy')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.calls.last.query.filters.genreIds, isEmpty);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('reset removes all filters and Done closes the sheet', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(
|
||||||
|
tester,
|
||||||
|
repository,
|
||||||
|
initialQuery: const CatalogSearchQuery(
|
||||||
|
filters: CatalogFilters(players: 2, genreIds: {genreFamily}),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
await openFilterSheet(tester);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_filter_reset')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(repository.calls.last.query.hasFilters, isFalse);
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_filter_done')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('catalog_filter_sheet')), findsNothing);
|
||||||
|
expect(find.text('Filter'), findsWidgets);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('QUE-05 Kriterium 4: terms are named and sorted by language', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
await openFilterSheet(tester);
|
||||||
|
|
||||||
|
// German: Familie before Strategie.
|
||||||
|
expect(find.text('Familie'), findsOneWidget);
|
||||||
|
final de = tester.getTopLeft(find.text('Familie')).dx;
|
||||||
|
final de2 = tester.getTopLeft(find.text('Strategie')).dx;
|
||||||
|
expect(de, lessThan(de2));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('QUE-05 Kriterium 4: English names and order', (tester) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository, locale: const Locale('en'));
|
||||||
|
await openFilterSheet(tester);
|
||||||
|
|
||||||
|
expect(find.text('Dice rolling'), findsOneWidget);
|
||||||
|
expect(find.text('Strategy'), findsOneWidget);
|
||||||
|
final family = tester.getTopLeft(find.text('Family')).dx;
|
||||||
|
final strategy = tester.getTopLeft(find.text('Strategy')).dx;
|
||||||
|
expect(family, lessThan(strategy));
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('a vocabulary error shows a hint with retry, rest works', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)])
|
||||||
|
..termsFailWith = const CatalogFailure.network();
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
await openFilterSheet(tester);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.text('Die Auswahl konnte nicht geladen werden.'),
|
||||||
|
findsNWidgets(2),
|
||||||
|
);
|
||||||
|
// The other filters still work.
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_filter_players')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.tap(find.text('2 Spieler').last);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(repository.calls.last.query.filters.players, 2);
|
||||||
|
|
||||||
|
repository.termsFailWith = null;
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_terms_retry')).first);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(
|
||||||
|
find.byKey(const Key('catalog_term_$genreStrategy')),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('keyboard', () {
|
||||||
|
testWidgets('QUE-06 Kriterium 4: Tab leads from the field to the filters', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
|
||||||
|
await tester.tap(searchInput);
|
||||||
|
await tester.pump();
|
||||||
|
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||||||
|
await tester.pump();
|
||||||
|
final focused = FocusManager.instance.primaryFocus!.context;
|
||||||
|
expect(
|
||||||
|
find.descendant(
|
||||||
|
of: find.byKey(const Key('catalog_filter_button')),
|
||||||
|
matching: find.byElementPredicate((e) => e == focused),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('layout', () {
|
||||||
|
testWidgets('Plattformen Kriterium 2: side bar from 1024 dp', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
|
||||||
|
await pumpView(tester, repository, width: 1200);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('catalog_filter_sidebar')), findsOneWidget);
|
||||||
|
expect(find.byKey(const Key('catalog_filter_button')), findsNothing);
|
||||||
|
// The filters work without a sheet.
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreFamily')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(repository.calls.last.query.filters.genreIds, {genreFamily});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('below 1024 dp there is no side bar', (tester) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
|
||||||
|
await pumpView(tester, repository, width: 800);
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('catalog_filter_sidebar')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('catalog_filter_button')), findsOneWidget);
|
||||||
|
});
|
||||||
|
|
||||||
|
for (final width in [420.0, 800.0, 1200.0]) {
|
||||||
|
testWidgets('QUE-06 Kriterium 3: text scale 2.0 at $width dp', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(
|
||||||
|
all: [
|
||||||
|
game(
|
||||||
|
1,
|
||||||
|
title: 'Ein sehr langer Spieltitel für den Test',
|
||||||
|
matchedAltTitle: 'Noch ein langer alternativer Titel',
|
||||||
|
),
|
||||||
|
game(2, publishers: ['Verlag A', 'Verlag B', 'Verlag C']),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
await pumpView(tester, repository, width: width, textScale: 2.0);
|
||||||
|
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
if (width < 1024) {
|
||||||
|
await openFilterSheet(tester);
|
||||||
|
expect(tester.takeException(), isNull);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
group('paging', () {
|
||||||
|
testWidgets(
|
||||||
|
'KAT-01 Kriterium 5: scrolling to the end loads the next page',
|
||||||
|
(tester) async {
|
||||||
|
final repository = FakeCatalogRepository(all: List.generate(75, game));
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
expect(repository.calls, hasLength(1));
|
||||||
|
|
||||||
|
await tester.drag(
|
||||||
|
find.byKey(const Key('catalog_result_list')),
|
||||||
|
const Offset(0, -3000),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.calls.length, greaterThanOrEqualTo(2));
|
||||||
|
expect(repository.calls[1].offset, 30);
|
||||||
|
expect(repository.calls[1].query.isEmpty, isTrue);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
testWidgets('a first page that does not fill the screen loads on', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: List.generate(75, game));
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
repository.calls.clear();
|
||||||
|
|
||||||
|
// No scroll event can come when everything is visible at once.
|
||||||
|
tester.view.physicalSize = const Size(420, 9000);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(repository.calls, isNotEmpty);
|
||||||
|
expect(repository.calls.first.offset, 30);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('the end of the list shows no more button', (tester) async {
|
||||||
|
final repository = FakeCatalogRepository(all: List.generate(5, game));
|
||||||
|
|
||||||
|
await pumpView(tester, repository);
|
||||||
|
|
||||||
|
expect(repository.calls, hasLength(1));
|
||||||
|
expect(find.byKey(const Key('list_footer_load_more')), findsNothing);
|
||||||
|
expect(find.byKey(const Key('list_footer_loading')), findsNothing);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets(
|
||||||
|
'KAT-01 Kriterium 5: a failed load keeps the hits; retry moves focus '
|
||||||
|
'to the first new tile (QUE-06 Kriterium 4)',
|
||||||
|
(tester) async {
|
||||||
|
final repository = FakeCatalogRepository(manual: true);
|
||||||
|
await pumpView(tester, repository, settle: false);
|
||||||
|
await tester.pump();
|
||||||
|
repository.complete(0, items: List.generate(30, game), totalCount: 90);
|
||||||
|
await tester.pump();
|
||||||
|
|
||||||
|
await tester.drag(
|
||||||
|
find.byKey(const Key('catalog_result_list')),
|
||||||
|
const Offset(0, -3000),
|
||||||
|
);
|
||||||
|
await tester.pump();
|
||||||
|
expect(repository.calls, hasLength(2));
|
||||||
|
expect(find.byKey(const Key('list_footer_loading')), findsOneWidget);
|
||||||
|
|
||||||
|
repository.complete(1, failure: const CatalogFailure.network());
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
await tester.drag(
|
||||||
|
find.byKey(const Key('catalog_result_list')),
|
||||||
|
const Offset(0, -3000),
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
find.text('Weitere Treffer konnten nicht geladen werden.'),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
expect(find.byKey(const Key('catalog_tile_game-29')), findsOneWidget);
|
||||||
|
// No automatic retry loop.
|
||||||
|
expect(repository.calls, hasLength(2));
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('list_footer_retry')));
|
||||||
|
await tester.pump();
|
||||||
|
expect(repository.calls, hasLength(3));
|
||||||
|
expect(repository.calls[2].offset, 30);
|
||||||
|
repository.complete(
|
||||||
|
2,
|
||||||
|
items: List.generate(30, (i) => game(30 + i)),
|
||||||
|
totalCount: 90,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final tile = find.byKey(const Key('catalog_tile_game-30'));
|
||||||
|
expect(tile, findsOneWidget);
|
||||||
|
expect(
|
||||||
|
find.ancestor(
|
||||||
|
of: tile,
|
||||||
|
matching: find.byWidgetPredicate(
|
||||||
|
(w) => w is Focus && (w.focusNode?.hasPrimaryFocus ?? false),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
findsOneWidget,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shuffle/features/catalog/domain/catalog_failure.dart';
|
import 'package:shuffle/features/catalog/domain/catalog_failure.dart';
|
||||||
|
|
@ -125,7 +127,6 @@ void main() {
|
||||||
run('KAT-01 Kriterium 1: only the latest request counts', (tester) async {
|
run('KAT-01 Kriterium 1: only the latest request counts', (tester) async {
|
||||||
final repo = FakeCatalogRepository(manual: true);
|
final repo = FakeCatalogRepository(manual: true);
|
||||||
useRepository(repo);
|
useRepository(repo);
|
||||||
|
|
||||||
|
|
||||||
controller().init(const CatalogSearchQuery());
|
controller().init(const CatalogSearchQuery());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
@ -151,7 +152,7 @@ void main() {
|
||||||
) async {
|
) async {
|
||||||
controller().init(const CatalogSearchQuery());
|
controller().init(const CatalogSearchQuery());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
repository.calls.clear();
|
repository.calls.clear();
|
||||||
|
|
||||||
|
|
@ -200,20 +201,20 @@ void main() {
|
||||||
controller().init(const CatalogSearchQuery());
|
controller().init(const CatalogSearchQuery());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(state().items, hasLength(60));
|
expect(state().items, hasLength(60));
|
||||||
expect(repository.calls.last.offset, 30);
|
expect(repository.calls.last.offset, 30);
|
||||||
expect(state().hasMore, isTrue);
|
expect(state().hasMore, isTrue);
|
||||||
|
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(state().items, hasLength(75));
|
expect(state().items, hasLength(75));
|
||||||
expect(state().hasMore, isFalse);
|
expect(state().hasMore, isFalse);
|
||||||
|
|
||||||
// Nothing more to load.
|
// Nothing more to load.
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(repository.calls, hasLength(3));
|
expect(repository.calls, hasLength(3));
|
||||||
});
|
});
|
||||||
|
|
@ -226,15 +227,14 @@ void main() {
|
||||||
manual: true,
|
manual: true,
|
||||||
);
|
);
|
||||||
useRepository(repo);
|
useRepository(repo);
|
||||||
|
|
||||||
controller().init(const CatalogSearchQuery());
|
controller().init(const CatalogSearchQuery());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
repo.complete(0, items: repo.all.sublist(0, 30), totalCount: 75);
|
repo.complete(0, items: repo.all.sublist(0, 30), totalCount: 75);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
controller()
|
unawaited(controller().loadMore());
|
||||||
..loadMore()
|
unawaited(controller().loadMore());
|
||||||
..loadMore();
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(repo.calls, hasLength(2));
|
expect(repo.calls, hasLength(2));
|
||||||
|
|
@ -246,13 +246,13 @@ void main() {
|
||||||
) async {
|
) async {
|
||||||
final repo = FakeCatalogRepository(manual: true);
|
final repo = FakeCatalogRepository(manual: true);
|
||||||
useRepository(repo);
|
useRepository(repo);
|
||||||
|
|
||||||
controller().init(const CatalogSearchQuery());
|
controller().init(const CatalogSearchQuery());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
repo.complete(0, items: [game(1), game(2)], totalCount: 4);
|
repo.complete(0, items: [game(1), game(2)], totalCount: 4);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
repo.complete(1, items: [game(2), game(3)], totalCount: 4);
|
repo.complete(1, items: [game(2), game(3)], totalCount: 4);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
@ -265,13 +265,13 @@ void main() {
|
||||||
) async {
|
) async {
|
||||||
final repo = FakeCatalogRepository(manual: true);
|
final repo = FakeCatalogRepository(manual: true);
|
||||||
useRepository(repo);
|
useRepository(repo);
|
||||||
|
|
||||||
controller().init(const CatalogSearchQuery());
|
controller().init(const CatalogSearchQuery());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
repo.complete(0, items: [game(1), game(2)], totalCount: 5);
|
repo.complete(0, items: [game(1), game(2)], totalCount: 5);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
repo.complete(1, items: const [], totalCount: 0);
|
repo.complete(1, items: const [], totalCount: 0);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
@ -284,12 +284,12 @@ void main() {
|
||||||
run('a new search discards a running loadMore', (tester) async {
|
run('a new search discards a running loadMore', (tester) async {
|
||||||
final repo = FakeCatalogRepository(manual: true);
|
final repo = FakeCatalogRepository(manual: true);
|
||||||
useRepository(repo);
|
useRepository(repo);
|
||||||
|
|
||||||
controller().init(const CatalogSearchQuery());
|
controller().init(const CatalogSearchQuery());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
repo.complete(0, items: [game(1)], totalCount: 3);
|
repo.complete(0, items: [game(1)], totalCount: 3);
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
controller().submitText('neu');
|
controller().submitText('neu');
|
||||||
|
|
@ -309,7 +309,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
repository.failWith = const CatalogFailure.network();
|
repository.failWith = const CatalogFailure.network();
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(state().items, hasLength(30));
|
expect(state().items, hasLength(30));
|
||||||
|
|
@ -318,7 +318,7 @@ void main() {
|
||||||
expect(state().loadingMore, isFalse);
|
expect(state().loadingMore, isFalse);
|
||||||
|
|
||||||
repository.failWith = null;
|
repository.failWith = null;
|
||||||
controller().retry();
|
unawaited(controller().retry());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(state().items, hasLength(60));
|
expect(state().items, hasLength(60));
|
||||||
|
|
@ -343,7 +343,7 @@ void main() {
|
||||||
expect(state().query.filters.players, 3);
|
expect(state().query.filters.players, 3);
|
||||||
|
|
||||||
repository.failWith = null;
|
repository.failWith = null;
|
||||||
controller().retry();
|
unawaited(controller().retry());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(state().status, CatalogSearchStatus.data);
|
expect(state().status, CatalogSearchStatus.data);
|
||||||
|
|
@ -354,7 +354,6 @@ void main() {
|
||||||
run('Zustände Kriterium 1: no hits give the empty status', (tester) async {
|
run('Zustände Kriterium 1: no hits give the empty status', (tester) async {
|
||||||
final repo = FakeCatalogRepository();
|
final repo = FakeCatalogRepository();
|
||||||
useRepository(repo);
|
useRepository(repo);
|
||||||
|
|
||||||
|
|
||||||
controller().init(const CatalogSearchQuery(text: 'zzz'));
|
controller().init(const CatalogSearchQuery(text: 'zzz'));
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
@ -366,7 +365,7 @@ void main() {
|
||||||
run('a new search keeps the old hits visible while it runs', (tester) async {
|
run('a new search keeps the old hits visible while it runs', (tester) async {
|
||||||
final repo = FakeCatalogRepository(manual: true);
|
final repo = FakeCatalogRepository(manual: true);
|
||||||
useRepository(repo);
|
useRepository(repo);
|
||||||
|
|
||||||
controller().init(const CatalogSearchQuery());
|
controller().init(const CatalogSearchQuery());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
repo.complete(0, items: [game(1)], totalCount: 1);
|
repo.complete(0, items: [game(1)], totalCount: 1);
|
||||||
|
|
@ -390,7 +389,7 @@ void main() {
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(state().searchGeneration, 1);
|
expect(state().searchGeneration, 1);
|
||||||
|
|
||||||
controller().loadMore();
|
unawaited(controller().loadMore());
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
expect(state().searchGeneration, 1);
|
expect(state().searchGeneration, 1);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,207 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:shuffle/core/routing/app_router.dart';
|
||||||
|
import 'package:shuffle/core/routing/url_replacer.dart';
|
||||||
|
import 'package:shuffle/features/auth/domain/auth_status.dart';
|
||||||
|
import 'package:shuffle/features/auth/domain/auth_user.dart';
|
||||||
|
import 'package:shuffle/features/auth/presentation/providers/auth_redirect.dart';
|
||||||
|
import 'package:shuffle/features/catalog/presentation/catalog_routes.dart';
|
||||||
|
import 'package:shuffle/features/catalog/presentation/search/catalog_search_view.dart';
|
||||||
|
|
||||||
|
import '../../../../helpers/pump_app.dart';
|
||||||
|
import '../../../../helpers/router_helpers.dart';
|
||||||
|
import '../../catalog_test_support.dart';
|
||||||
|
import 'catalog_search_view_test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('route /catalog', () {
|
||||||
|
testWidgets('Plattformen: /catalog is public (no sign-in redirect)', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
expect(
|
||||||
|
authRedirectFor(
|
||||||
|
const SignedOut(),
|
||||||
|
Uri.parse('/catalog?q=catan'),
|
||||||
|
privatePaths: const [],
|
||||||
|
),
|
||||||
|
isNull,
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
authRedirectFor(
|
||||||
|
const SignedIn(
|
||||||
|
AuthUser(id: 'u1', email: 'a@example.com', emailConfirmed: true),
|
||||||
|
),
|
||||||
|
Uri.parse('/catalog'),
|
||||||
|
),
|
||||||
|
isNull,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('KAT-01 Kriterium 1: the URL parameters are the start values', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
useWindow(tester, 420);
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
final container = await pumpShuffleApp(
|
||||||
|
tester,
|
||||||
|
overrides: catalogOverrides(repository),
|
||||||
|
);
|
||||||
|
|
||||||
|
container
|
||||||
|
.read(appRouterProvider)
|
||||||
|
.go('/catalog?q=catan&players=3&maxMinutes=60&genre=$genreStrategy');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('catalog_search_screen')), findsOneWidget);
|
||||||
|
final query = repository.calls.first.query;
|
||||||
|
expect(query.text, 'catan');
|
||||||
|
expect(query.filters.players, 3);
|
||||||
|
expect(query.filters.maxMinutes, 60);
|
||||||
|
expect(query.filters.genreIds, {genreStrategy});
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('invalid URL parameters are ignored, the page still opens', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
useWindow(tester, 420);
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
final container = await pumpShuffleApp(
|
||||||
|
tester,
|
||||||
|
overrides: catalogOverrides(repository),
|
||||||
|
);
|
||||||
|
|
||||||
|
container
|
||||||
|
.read(appRouterProvider)
|
||||||
|
.go('/catalog?players=99&genre=xyz&maxMinutes=abc');
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(find.byKey(const Key('catalog_search_screen')), findsOneWidget);
|
||||||
|
expect(repository.calls.first.query.isEmpty, isTrue);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('start page entry opens the search and back returns', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
useWindow(tester, 420);
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
await pumpShuffleApp(tester, overrides: catalogOverrides(repository));
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('home_catalog_search_button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('catalog_search_screen')), findsOneWidget);
|
||||||
|
expect(find.text('Spiel 1'), findsOneWidget);
|
||||||
|
|
||||||
|
await tester.tap(find.byType(BackButton));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
expect(find.byKey(const Key('home_screen')), findsOneWidget);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group('URL sync (web)', () {
|
||||||
|
late List<MethodCall> platformCalls;
|
||||||
|
|
||||||
|
setUp(() => platformCalls = []);
|
||||||
|
|
||||||
|
Future<GoRouter> pumpRouted(
|
||||||
|
WidgetTester tester,
|
||||||
|
FakeCatalogRepository repository, {
|
||||||
|
required bool isWeb,
|
||||||
|
}) async {
|
||||||
|
useWindow(tester, 420);
|
||||||
|
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
SystemChannels.navigation,
|
||||||
|
(call) async {
|
||||||
|
platformCalls.add(call);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
addTearDown(
|
||||||
|
() => tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||||
|
SystemChannels.navigation,
|
||||||
|
null,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
final router = buildTestRouter(
|
||||||
|
routes: catalogRoutes,
|
||||||
|
initialLocation: '/catalog?q=alt',
|
||||||
|
);
|
||||||
|
addTearDown(router.dispose);
|
||||||
|
await pumpApp(
|
||||||
|
tester,
|
||||||
|
router: router,
|
||||||
|
overrides: [
|
||||||
|
...catalogOverrides(repository),
|
||||||
|
urlReplacerProvider.overrideWithValue(UrlReplacer(isWeb: isWeb)),
|
||||||
|
],
|
||||||
|
settle: false,
|
||||||
|
);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
platformCalls.clear();
|
||||||
|
return router;
|
||||||
|
}
|
||||||
|
|
||||||
|
testWidgets('URL is replaced after a search without rebuilding the page', (
|
||||||
|
tester,
|
||||||
|
) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
final router = await pumpRouted(tester, repository, isWeb: true);
|
||||||
|
final stateBefore = tester.state(find.byType(CatalogSearchView));
|
||||||
|
final callsBefore = repository.calls.length;
|
||||||
|
|
||||||
|
await tester.enterText(searchInput, 'catan');
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
// Router and browser address agree on the new search.
|
||||||
|
expect(currentUri(router).queryParameters['q'], 'catan');
|
||||||
|
final updates = platformCalls.where(
|
||||||
|
(c) => c.method == 'routeInformationUpdated',
|
||||||
|
);
|
||||||
|
expect(updates, isNotEmpty);
|
||||||
|
final last = updates.last.arguments as Map<Object?, Object?>;
|
||||||
|
expect(last['uri'].toString(), '/catalog?q=catan');
|
||||||
|
expect(last['replace'], isTrue);
|
||||||
|
// No history entry from the router for this change.
|
||||||
|
expect(
|
||||||
|
updates.where((c) => (c.arguments as Map)['replace'] != true),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
// Same page state: nothing rebuilt, field and focus intact.
|
||||||
|
expect(tester.state(find.byType(CatalogSearchView)), same(stateBefore));
|
||||||
|
expect(tester.widget<TextField>(searchInput).controller!.text, 'catan');
|
||||||
|
// Exactly one more search (no re-init after the URL change).
|
||||||
|
expect(repository.calls.length, callsBefore + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('filters are part of the written URL', (tester) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
final router = await pumpRouted(tester, repository, isWeb: true);
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_filter_button')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
await tester.tap(find.byKey(const Key('catalog_term_$genreFamily')));
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
final params = currentUri(router).queryParameters;
|
||||||
|
expect(params['q'], 'alt');
|
||||||
|
expect(params['genre'], genreFamily);
|
||||||
|
});
|
||||||
|
|
||||||
|
testWidgets('outside the web nothing is written', (tester) async {
|
||||||
|
final repository = FakeCatalogRepository(all: [game(1)]);
|
||||||
|
final router = await pumpRouted(tester, repository, isWeb: false);
|
||||||
|
|
||||||
|
await tester.enterText(searchInput, 'catan');
|
||||||
|
await tester.testTextInput.receiveAction(TextInputAction.search);
|
||||||
|
await tester.pumpAndSettle();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
platformCalls.where((c) => c.method == 'routeInformationUpdated'),
|
||||||
|
isEmpty,
|
||||||
|
);
|
||||||
|
expect(currentUri(router).queryParameters['q'], 'alt');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/semantics.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:shuffle/core/widgets/game_cover.dart';
|
import 'package:shuffle/core/widgets/game_cover.dart';
|
||||||
import 'package:shuffle/features/catalog/domain/catalog_failure.dart';
|
import 'package:shuffle/features/catalog/domain/catalog_failure.dart';
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue