Adds SignInPrompt (sign in / create account), showSignInPromptProvider, HomeScreen as ConsumerWidget, l10n de/en, widget/unit tests and a Playwright web test (#49 criterion 1, QUE-09 criterion 13, QUE-06). Refs #49 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
243 lines
8.6 KiB
Dart
243 lines
8.6 KiB
Dart
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<void> open(
|
|
WidgetTester tester, {
|
|
AuthStatus? status = const AuthStatus.signedOut(),
|
|
Stream<AuthStatus>? 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<AuthStatus>.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: the buttons stay reachable and tappable.
|
|
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);
|
|
|
|
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
|
await tester.pumpAndSettle();
|
|
expect(find.byKey(const Key('register_screen')), findsOneWidget);
|
|
},
|
|
);
|
|
});
|
|
}
|