Files
sxchnge/frontend/app/test/ux_analytics_test.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();
},
);
}