Deliver localized auth app with verification and go_router

Integrate themed login/signup, EN/RU localization, shared dummy providers, verification recovery screens and named URL routes. Store frontend guidelines, workflow skills, validation and delivery documentation. Related to #2, #3, #5 and #6.
This commit is contained in:
Stephan D committed 2026-09-11 15:37:07 +02:00
1 parent 27424ed886
commit 10a278c65a
44 files changed
+4128 -28

No files matched your search

+136
View File
@@ -0,0 +1,136 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:sxchnge_auth/sxchnge_auth.dart';
import 'package:sxchnge_app/main.dart';
class RecoveringService implements VerificationService {
int sends = 0;
int confirmations = 0;
@override
Future<VerificationResult> send({
required String email,
required String locale,
}) async {
if (++sends == 1) throw StateError('private error');
return const VerificationResult(isSimulated: true);
}
@override
Future<VerificationResult> confirm({required String token}) async {
if (++confirmations == 1) throw StateError('private error');
return const VerificationResult(isSimulated: true);
}
}
Future<void> openVerification(
WidgetTester tester,
String route, {
VerificationService service = const DummyVerificationService(
delay: Duration.zero,
),
}) async {
tester.platformDispatcher.defaultRouteNameTestValue = route;
addTearDown(tester.platformDispatcher.clearDefaultRouteNameTestValue);
await tester.pumpWidget(SxchngeApp(verificationService: service));
await tester.pumpAndSettle();
}
void main() {
for (final scenario in {
'demo-valid': 'verified',
'demo-expired': 'expired',
'wrong': 'invalid',
'': 'invalid',
'demo-internal': 'error',
}.entries) {
testWidgets(
'confirmation link resolves ${scenario.key} as ${scenario.value}',
(tester) async {
await openVerification(tester, '/verify-email?token=${scenario.key}');
expect(
find.byKey(ValueKey('verification-${scenario.value}')),
findsOneWidget,
);
expect(find.textContaining('DEMO'), findsOneWidget);
expect(tester.takeException(), isNull);
},
);
}
testWidgets('internal confirmation error can retry without entering email', (
tester,
) async {
final service = RecoveringService();
await openVerification(
tester,
'/verify-email?token=opaque',
service: service,
);
expect(find.text('private error'), findsNothing);
final retry = find.byKey(const ValueKey('verification-retry'));
await tester.ensureVisible(retry);
await tester.tap(retry);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('verification-verified')), findsOneWidget);
expect(service.confirmations, 2);
});
testWidgets(
'request validates email, reports send error and resends successfully',
(tester) async {
final service = RecoveringService();
await openVerification(tester, '/verification-sent', service: service);
final resend = find.byKey(const ValueKey('verification-resend'));
await tester.ensureVisible(resend);
await tester.tap(resend);
await tester.pumpAndSettle();
expect(find.text('Enter your email address.'), findsOneWidget);
expect(service.sends, 0);
await tester.enterText(
find.byKey(const ValueKey('verification-email')),
'alex@example.com',
);
await tester.ensureVisible(resend);
await tester.tap(resend);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('verification-error')), findsOneWidget);
await tester.ensureVisible(resend);
await tester.tap(resend);
await tester.pumpAndSettle();
expect(find.byKey(const ValueKey('verification-sent')), findsOneWidget);
expect(service.sends, 2);
},
);
for (final route in [
'/verification-sent',
'/verify-email?token=demo-valid',
'/verify-email?token=demo-expired',
'/verify-email?token=bad',
'/verify-email?token=demo-internal',
]) {
testWidgets('$route fits narrow viewport at 200% in EN and RU', (
tester,
) async {
tester.view.physicalSize = const Size(320, 740);
tester.view.devicePixelRatio = 1;
tester.platformDispatcher.textScaleFactorTestValue = 2;
addTearDown(tester.view.resetPhysicalSize);
addTearDown(tester.view.resetDevicePixelRatio);
addTearDown(tester.platformDispatcher.clearTextScaleFactorTestValue);
await openVerification(tester, route);
expect(tester.takeException(), isNull);
await tester.tap(find.byKey(const ValueKey('locale-selector')));
await tester.pumpAndSettle();
await tester.tap(
find.ancestor(
of: find.text('Русский'),
matching: find.byType(CheckedPopupMenuItem<Locale>),
),
);
await tester.pumpAndSettle();
expect(find.text('RU'), findsOneWidget);
expect(tester.takeException(), isNull);
});
}
}