Files
sxchnge/frontend/app/test/ux_analytics_test.dart
T

194 lines
6.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();
},
);
}