194 lines
6.8 KiB
Dart
194 lines
6.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:sxchnge_analytics/sxchnge_analytics.dart';
|
||
|
||
import 'package:sxchnge_auth/sxchnge_auth.dart';
|
||
|
||
import 'package:sxchnge_app/main.dart';
|
||
import 'package:sxchnge_app/routing/routes.dart';
|
||
|
||
Future<void> open(
|
||
WidgetTester tester,
|
||
String path,
|
||
DummyAnalyticsService sink,
|
||
) async {
|
||
tester.platformDispatcher.defaultRouteNameTestValue = path;
|
||
addTearDown(tester.platformDispatcher.clearDefaultRouteNameTestValue);
|
||
await tester.pumpWidget(
|
||
SxchngeApp(
|
||
analyticsService: sink,
|
||
signupService: const DummySignupService(delay: Duration.zero),
|
||
verificationService: const DummyVerificationService(delay: Duration.zero),
|
||
),
|
||
);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
|
||
Future<void> tap(WidgetTester tester, Finder finder) async {
|
||
await tester.ensureVisible(finder);
|
||
await tester.tap(finder);
|
||
await tester.pumpAndSettle();
|
||
}
|
||
|
||
void main() {
|
||
testWidgets(
|
||
'recipient change removes sent claim, focuses editor and requires a new send',
|
||
(tester) async {
|
||
final sink = DummyAnalyticsService();
|
||
await open(tester, '/verification-sent', sink);
|
||
final email = find.byKey(const ValueKey('verification-email'));
|
||
await tester.enterText(email, 'alex@example.com');
|
||
await tap(tester, find.byKey(const ValueKey('verification-resend')));
|
||
expect(email, findsNothing);
|
||
expect(
|
||
find.byKey(const ValueKey('verification-recipient')),
|
||
findsOneWidget,
|
||
);
|
||
await tap(
|
||
tester,
|
||
find.byKey(const ValueKey('verification-change-email')),
|
||
);
|
||
expect(find.byKey(const ValueKey('verification-sent')), findsNothing);
|
||
final editable = tester.widget<EditableText>(
|
||
find.descendant(of: email, matching: find.byType(EditableText)),
|
||
);
|
||
expect(editable.focusNode.hasFocus, isTrue);
|
||
await tester.enterText(email, 'different@example.com');
|
||
expect(
|
||
find.byKey(const ValueKey('verification-recipient')),
|
||
findsNothing,
|
||
);
|
||
await tester.testTextInput.receiveAction(TextInputAction.send);
|
||
await tester.pumpAndSettle();
|
||
expect(find.text('different@example.com'), findsOneWidget);
|
||
expect(
|
||
sink.events
|
||
.where(
|
||
(e) =>
|
||
e.step == JourneyStep.emailDelivery &&
|
||
e.signal == JourneySignal.requested,
|
||
)
|
||
.length,
|
||
2,
|
||
);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'confirmation error has primary retry and keyboard-accessible new-email recovery',
|
||
(tester) async {
|
||
final semantics = tester.ensureSemantics();
|
||
final sink = DummyAnalyticsService();
|
||
await open(tester, '/verify-email?token=demo-internal', sink);
|
||
expect(
|
||
tester.widget(find.byKey(const ValueKey('verification-retry'))),
|
||
isA<FilledButton>(),
|
||
);
|
||
expect(find.byKey(const ValueKey('verification-email')), findsNothing);
|
||
final node = tester.getSemantics(
|
||
find.byKey(const ValueKey('verification-error')),
|
||
);
|
||
expect(node.getSemanticsData().flagsCollection.isHeader, isTrue);
|
||
expect(node.getSemanticsData().flagsCollection.isLiveRegion, isTrue);
|
||
await tap(tester, find.text('Send a new verification email'));
|
||
await tester.enterText(
|
||
find.byKey(const ValueKey('verification-email')),
|
||
'alex@example.com',
|
||
);
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.tab);
|
||
await tester.pump();
|
||
await tester.sendKeyEvent(LogicalKeyboardKey.enter);
|
||
await tester.pumpAndSettle();
|
||
expect(find.byKey(const ValueKey('verification-sent')), findsOneWidget);
|
||
expect(sink.events.map((e) => e.journeyId).toSet().length, 1);
|
||
semantics.dispose();
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'signup verification keeps simulated analytics with ordinary product copy',
|
||
(tester) async {
|
||
final sink = DummyAnalyticsService();
|
||
await open(tester, '/', sink);
|
||
await tap(tester, find.text('Sign up'));
|
||
await tester.enterText(find.byKey(const ValueKey('name')), 'Alex');
|
||
await tester.enterText(
|
||
find.byKey(const ValueKey('email')),
|
||
'alex@example.com',
|
||
);
|
||
await tester.enterText(
|
||
find.byKey(const ValueKey('password')),
|
||
'example-password',
|
||
);
|
||
await tester.enterText(
|
||
find.byKey(const ValueKey('confirmation')),
|
||
'example-password',
|
||
);
|
||
await tap(tester, find.byKey(const ValueKey('submit')));
|
||
expect(find.text('Check your inbox.'), findsOneWidget);
|
||
expect(find.textContaining('We’ve sent you'), findsOneWidget);
|
||
await tap(tester, find.byKey(const ValueKey('demo-confirmation')));
|
||
expect(find.text('Your email\nis verified.'), findsOneWidget);
|
||
expect(sink.events.map((e) => e.journeyId).toSet().length, 1);
|
||
expect(sink.events.where((e) => e.terminal).length, 1);
|
||
expect(
|
||
sink.events.where((e) => e.signal == JourneySignal.succeeded),
|
||
isEmpty,
|
||
);
|
||
expect(sink.events.every((e) => e.simulated), isTrue);
|
||
final payload = sink.events.map((e) => e.properties).toString();
|
||
for (final secret in [
|
||
'alex@example.com',
|
||
'example-password',
|
||
'Alex',
|
||
'demo-valid',
|
||
]) {
|
||
expect(payload, isNot(contains(secret)));
|
||
}
|
||
final count = sink.events.length;
|
||
await tester.pumpAndSettle();
|
||
expect(sink.events.length, count);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'exit from a deep-linked failed confirmation records abandonment once',
|
||
(tester) async {
|
||
final sink = DummyAnalyticsService();
|
||
await open(tester, '/verify-email?token=demo-expired', sink);
|
||
await tap(tester, find.text('Back to login'));
|
||
expect(sink.events.last.signal, JourneySignal.abandoned);
|
||
expect(sink.events.last.exit, JourneyExit.navigation);
|
||
expect(sink.events.where((e) => e.terminal).length, 1);
|
||
final router = GoRouter.of(tester.element(find.byType(Scaffold)));
|
||
router.goNamed(AppRoutes.login);
|
||
await tester.pumpAndSettle();
|
||
expect(sink.events.where((e) => e.terminal).length, 1);
|
||
},
|
||
);
|
||
|
||
testWidgets(
|
||
'verification controls have accessible labels and mobile tap targets',
|
||
(tester) async {
|
||
final semantics = tester.ensureSemantics();
|
||
tester.view.physicalSize = const Size(390, 844);
|
||
tester.view.devicePixelRatio = 1;
|
||
addTearDown(tester.view.resetPhysicalSize);
|
||
addTearDown(tester.view.resetDevicePixelRatio);
|
||
await open(
|
||
tester,
|
||
'/verify-email?token=demo-internal',
|
||
DummyAnalyticsService(),
|
||
);
|
||
await expectLater(tester, meetsGuideline(labeledTapTargetGuideline));
|
||
await expectLater(tester, meetsGuideline(androidTapTargetGuideline));
|
||
semantics.dispose();
|
||
},
|
||
);
|
||
}
|