All checks were successful
Correct the focus comment (refs #61), test Space key and focus state, prove scrollability at 200 percent text, drop needless minWidth, English doc comment, allow OutlinedButton as secondary action in standalone cards, English prompt text says plays instead of games. Refs #49 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
271 lines
9.8 KiB
Dart
271 lines
9.8 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: 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);
|
|
});
|
|
});
|
|
}
|