import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/misc.dart' show Override; import 'package:flutter_riverpod/misc.dart' show ProviderListenable; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:mocktail/mocktail.dart'; import 'package:shuffle/core/l10n/app_locale.dart'; import 'package:shuffle/core/platform/platform_info.dart'; import 'package:shuffle/core/platform/url_opener.dart'; import 'package:shuffle/core/result/result.dart'; import 'package:shuffle/features/auth/domain/auth_failure.dart'; import 'package:shuffle/features/auth/domain/auth_repository.dart'; import 'package:shuffle/features/auth/domain/email_link_type.dart'; import 'package:shuffle/features/auth/domain/registration_input.dart'; import 'package:shuffle/features/auth/domain/validation/common_passwords.dart'; import 'package:shuffle/features/auth/presentation/auth_routes.dart'; import 'package:shuffle/features/auth/presentation/providers/auth_providers.dart'; import '../../../helpers/fakes.dart'; import '../../../helpers/pump_app.dart'; class MockAuthRepository extends Mock implements AuthRepository {} class FakeRegistrationInput extends Fake implements RegistrationInput {} /// [AppLocale] with a fixed language (tests do not depend on the device). class FixedAppLocale extends AppLocale { FixedAppLocale(this.locale); final Locale locale; @override Locale build() => locale; } const commonTestPasswords = SetCommonPasswordList({'passwort1234'}); void registerAuthFallbacks() { registerFallbackValue(FakeRegistrationInput()); registerFallbackValue(EmailLinkType.confirmation); } Result ok() => const Result.success(null); Result failed(AuthFailure value) => Result.failure(value); /// Overrides for auth UI tests: repository mock, fixed language, common /// password list, URL opener and platform. List authUiOverrides( MockAuthRepository repository, { Locale locale = const Locale('de'), UrlOpener? urlOpener, bool mobileWeb = false, Future Function()? commonPasswords, List extra = const [], }) => testOverrides( overrides: [ authRepositoryProvider.overrideWithValue(repository), appLocaleProvider.overrideWith(() => FixedAppLocale(locale)), commonPasswordListProvider.overrideWith( (ref) => commonPasswords?.call() ?? Future.value(commonTestPasswords), ), urlOpenerProvider.overrideWithValue(urlOpener ?? (_) async => true), isMobileWebProvider.overrideWithValue(mobileWeb), ...extra, ], ); /// The text field inside an `AppTextField`/`AppPasswordField` with [key]. Finder fieldOf(String key) => find.descendant(of: find.byKey(Key(key)), matching: find.byType(TextField)); /// Keeps an auto-dispose provider alive during a controller test. void keepAlive(ProviderContainer container, ProviderListenable p) { container.listen(p, (_, _) {}); } typedef AuthUiHandle = ({ProviderContainer container, GoRouter router}); /// Pumps the real auth routes (`authRoutes`, without router redirects) at /// [location] on a phone-sized screen. Future pumpAuthRoute( WidgetTester tester, MockAuthRepository repository, { String location = '/register', Object? extra, Locale locale = const Locale('de'), ThemeMode themeMode = ThemeMode.light, double? textScale, Size size = const Size(400, 800), UrlOpener? urlOpener, bool mobileWeb = false, bool settle = true, }) async { tester.view.physicalSize = size; tester.view.devicePixelRatio = 1; addTearDown(tester.view.reset); final router = GoRouter( initialLocation: location, initialExtra: extra, routes: authRoutes, ); addTearDown(router.dispose); final container = await pumpApp( tester, router: router, locale: locale, themeMode: themeMode, textScale: textScale, settle: settle, overrides: authUiOverrides( repository, locale: locale, urlOpener: urlOpener, mobileWeb: mobileWeb, ), ); return (container: container, router: router); } /// All texts currently visible (for comparing two screens exactly). List visibleTexts(WidgetTester tester) => [ for (final text in tester.widgetList(find.byType(Text))) if (text.data != null) text.data!, ]; /// Wide layout (>= 600 dp): the screen sits in a card (issue #41). const wideScreenSize = Size(1280, 720); /// Asserts that title, body text and button are separate semantics nodes /// (heading / text / button with label), not folded into one node (issue #41). void expectSeparateSemantics( WidgetTester tester, { required String title, required Key buttonKey, required String buttonLabel, Key? bodyKey, String? bodyLabel, }) { expect( tester.getSemantics(find.text(title).first), matchesSemantics(label: title, isHeader: true), reason: 'title must be a heading of its own', ); if (bodyKey != null) { expect( tester.getSemantics(find.byKey(bodyKey)), matchesSemantics(label: bodyLabel), reason: 'body text must be a text node of its own', ); } expect( tester.getSemantics(find.byKey(buttonKey)), matchesSemantics( label: buttonLabel, isButton: true, isEnabled: true, hasEnabledState: true, hasTapAction: true, ), reason: 'button must be a button node of its own', ); }