All checks were successful
Refs #41 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
172 lines
5.7 KiB
Dart
172 lines
5.7 KiB
Dart
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<void, AuthFailure> ok() => const Result.success(null);
|
|
|
|
Result<void, AuthFailure> failed(AuthFailure value) => Result.failure(value);
|
|
|
|
/// Overrides for auth UI tests: repository mock, fixed language, common
|
|
/// password list, URL opener and platform.
|
|
List<Override> authUiOverrides(
|
|
MockAuthRepository repository, {
|
|
Locale locale = const Locale('de'),
|
|
UrlOpener? urlOpener,
|
|
bool mobileWeb = false,
|
|
Future<CommonPasswordList> Function()? commonPasswords,
|
|
List<Override> 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<Object?> 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<AuthUiHandle> 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<String> visibleTexts(WidgetTester tester) => [
|
|
for (final text in tester.widgetList<Text>(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 [target] is an enabled button node of its own with [label].
|
|
///
|
|
/// `isSemantics` only checks the given properties, so extra flags
|
|
/// (focusable, ...) added by future Flutter versions do not break it.
|
|
void expectButtonSemantics(WidgetTester tester, Finder target, String label) {
|
|
expect(
|
|
tester.getSemantics(target),
|
|
isSemantics(
|
|
label: label,
|
|
isButton: true,
|
|
isEnabled: true,
|
|
hasEnabledState: true,
|
|
hasTapAction: true,
|
|
),
|
|
reason: 'button must be a button node of its own',
|
|
);
|
|
}
|
|
|
|
/// 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),
|
|
isSemantics(label: title, isHeader: true, isButton: false),
|
|
reason: 'title must be a heading of its own',
|
|
);
|
|
if (bodyKey != null) {
|
|
expect(
|
|
tester.getSemantics(find.byKey(bodyKey)),
|
|
isSemantics(label: bodyLabel, isHeader: false, isButton: false),
|
|
reason: 'body text must be a text node of its own',
|
|
);
|
|
}
|
|
expectButtonSemantics(tester, find.byKey(buttonKey), buttonLabel);
|
|
}
|