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/features/auth/domain/auth_status.dart'; import 'package:shuffle/features/auth/domain/auth_user.dart'; import 'package:shuffle/features/auth/presentation/auth_routes.dart'; import 'package:shuffle/features/auth/presentation/providers/auth_providers.dart'; import 'package:shuffle/features/home/presentation/home_screen.dart'; import 'package:shuffle/l10n/app_localizations_de.dart'; import 'package:shuffle/l10n/app_localizations_en.dart'; import '../../../helpers/pump_app.dart'; import '../../auth/presentation/auth_ui_support.dart'; final _de = AppLocalizationsDe(); final _en = AppLocalizationsEn(); const _user = AuthUser(id: 'u1', email: 'a@example.test', emailConfirmed: true); void main() { late MockAuthRepository repository; setUp(() => repository = MockAuthRepository()); /// Pumps the home route plus the real auth routes. [status] `null` means /// the status stream has no value yet (loading); [statusStream] overrides it. Future open( WidgetTester tester, { AuthStatus? status = const AuthStatus.signedOut(), Stream? statusStream, Locale locale = const Locale('de'), ThemeMode themeMode = ThemeMode.light, double? textScale, Size size = const Size(400, 800), }) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final router = GoRouter( routes: [ GoRoute(path: '/', builder: (_, _) => const HomeScreen()), ...authRoutes, ], ); addTearDown(router.dispose); final stream = statusStream ?? (status == null ? const Stream.empty() : Stream.value(status)); await pumpApp( tester, router: router, locale: locale, themeMode: themeMode, textScale: textScale, settle: status != null || statusStream != null, overrides: authUiOverrides( repository, locale: locale, extra: [authStatusProvider.overrideWith((ref) => stream)], ), ); if (status == null && statusStream == null) await tester.pump(); } final promptCard = find.byKey(const Key('sign_in_prompt')); final signInButton = find.byKey(const Key('sign_in_prompt_sign_in')); final registerButton = find.byKey(const Key('sign_in_prompt_register')); group('#49 Krit. 1: Anmelde-Hinweis auf der Startseite', () { testWidgets('abgemeldet: Hinweis mit zwei Knöpfen und Platzhaltertext', ( tester, ) async { await open(tester); expect(promptCard, findsOneWidget); expect(find.text(_de.signInPromptTitle), findsOneWidget); expect(find.text(_de.signInPromptBody), findsOneWidget); expect(find.text(_de.signInPromptSignIn), findsOneWidget); expect(find.text(_de.signInPromptRegister), findsOneWidget); expect(find.text(_de.homePlaceholderMessage), findsOneWidget); }); testWidgets('QUE-05 Krit. 2: abgemeldet, en: englische Texte', ( tester, ) async { await open(tester, locale: const Locale('en')); expect(find.text(_en.signInPromptTitle), findsOneWidget); expect(find.text(_en.signInPromptBody), findsOneWidget); expect(find.text(_en.signInPromptSignIn), findsOneWidget); expect(find.text(_en.signInPromptRegister), findsOneWidget); }); testWidgets('angemeldet: kein Hinweis, Platzhaltertext bleibt', ( tester, ) async { await open(tester, status: const AuthStatus.signedIn(_user)); expect(promptCard, findsNothing); expect(find.text(_de.homePlaceholderMessage), findsOneWidget); }); testWidgets('Laden (kein erster Wert): kein Hinweis', (tester) async { await open(tester, status: null); expect(promptCard, findsNothing); expect(find.text(_de.homePlaceholderMessage), findsOneWidget); }); testWidgets('Fehler im Statusstrom: Hinweis wird angezeigt', ( tester, ) async { await open(tester, statusStream: Stream.error(StateError('boom'))); expect(promptCard, findsOneWidget); }); testWidgets('KTO-01 Krit. 4: Tipp auf „Anmelden“ öffnet den LoginScreen', ( tester, ) async { await open(tester); await tester.tap(signInButton); await tester.pumpAndSettle(); expect(find.byKey(const Key('login_screen')), findsOneWidget); }); testWidgets( 'KTO-01 Krit. 1: Tipp auf „Konto erstellen“ öffnet den RegisterScreen', (tester) async { await open(tester); await tester.tap(registerButton); await tester.pumpAndSettle(); expect(find.byKey(const Key('register_screen')), findsOneWidget); }, ); testWidgets('breit (1280 dp): Karte höchstens 480 dp breit, zentriert', ( tester, ) async { await open(tester, size: wideScreenSize); final rect = tester.getRect(promptCard); expect(rect.width, lessThanOrEqualTo(480)); expect(rect.center.dx, closeTo(wideScreenSize.width / 2, 1)); }); }); group('QUE-06: Barrierefreiheit des Anmelde-Hinweises', () { for (final (name, size, theme) in [ ('Handy hell', const Size(400, 800), ThemeMode.light), ('Handy dunkel', const Size(400, 800), ThemeMode.dark), ('breit hell', wideScreenSize, ThemeMode.light), ]) { testWidgets('QUE-06 Krit. 1-3: Guidelines ($name)', (tester) async { final handle = tester.ensureSemantics(); await open(tester, size: size, themeMode: theme); await expectLater(tester, meetsGuideline(androidTapTargetGuideline)); await expectLater(tester, meetsGuideline(iOSTapTargetGuideline)); await expectLater(tester, meetsGuideline(labeledTapTargetGuideline)); await expectLater(tester, meetsGuideline(textContrastGuideline)); handle.dispose(); }); } testWidgets( 'QUE-06 Krit. 1: Überschrift, Text und Knöpfe sind eigene Semantics-Knoten', (tester) async { final handle = tester.ensureSemantics(); await open(tester, size: wideScreenSize); expectSeparateSemantics( tester, title: _de.signInPromptTitle, bodyKey: const Key('sign_in_prompt_body'), bodyLabel: _de.signInPromptBody, buttonKey: const Key('sign_in_prompt_sign_in'), buttonLabel: _de.signInPromptSignIn, ); expectButtonSemantics(tester, registerButton, _de.signInPromptRegister); handle.dispose(); }, ); for (final locale in const [Locale('de'), Locale('en')]) { testWidgets( 'QUE-06 Krit. 3: Schriftgröße 200 % ohne Überlauf (${locale.languageCode})', (tester) async { await open( tester, textScale: 2, size: const Size(320, 640), locale: locale, ); expect(tester.takeException(), isNull); // The page scrolls: dragging moves the content, the buttons stay // reachable and tappable. final before = tester.getTopLeft(registerButton).dy; await tester.drag( find.byType(SingleChildScrollView), const Offset(0, -200), ); await tester.pump(); expect(tester.getTopLeft(registerButton).dy, lessThan(before)); await tester.ensureVisible(registerButton); await tester.pump(); expect( tester.getSize(registerButton).height, greaterThanOrEqualTo(48), ); await tester.tap(registerButton); await tester.pumpAndSettle(); expect(find.byKey(const Key('register_screen')), findsOneWidget); }, ); } testWidgets( 'QUE-06 Krit. 4: Tab-Reihenfolge Anmelden -> Konto erstellen, Enter löst aus', (tester) async { await open(tester); bool focusIn(Finder button) { final focus = FocusManager.instance.primaryFocus; if (focus?.context == null) return false; return find .descendant( of: button, matching: find.byElementPredicate( (e) => e == focus!.context, ), ) .evaluate() .isNotEmpty || find .ancestor( of: find.byElementPredicate((e) => e == focus!.context), matching: button, ) .evaluate() .isNotEmpty; } await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pump(); expect(focusIn(signInButton), isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pump(); expect(focusIn(registerButton), isTrue); // Visible focus: keyboard navigation switches to traditional highlight // mode and the button holds the primary focus (the focus ring comes // from the focused state of the theme). expect( FocusManager.instance.highlightMode, FocusHighlightMode.traditional, ); expect(FocusManager.instance.primaryFocus?.hasPrimaryFocus, isTrue); await tester.sendKeyEvent(LogicalKeyboardKey.enter); await tester.pumpAndSettle(); expect(find.byKey(const Key('register_screen')), findsOneWidget); }, ); testWidgets('QUE-06 Krit. 4: Leertaste löst „Konto erstellen“ aus', ( tester, ) async { await open(tester); await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.sendKeyEvent(LogicalKeyboardKey.tab); await tester.pump(); await tester.sendKeyEvent(LogicalKeyboardKey.space); await tester.pumpAndSettle(); expect(find.byKey(const Key('register_screen')), findsOneWidget); }); }); }