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 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 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( 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(), ); 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(); }, ); }