197 lines
6.8 KiB
Dart
197 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('a verification email is on its way'),
|
|
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();
|
|
},
|
|
);
|
|
}
|