import 'dart:ui' as ui; 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_kyc/sxchnge_kyc.dart'; import 'package:sxchnge_app/kyc/country.dart'; import 'package:sxchnge_app/main.dart'; import 'package:sxchnge_app/providers/locale.dart'; class Photos implements PhotoService { Photos(this.bytes); final Uint8List bytes; KycError? error; @override Future pick() async { if (error != null) throw KycException(error!); return Photo(bytes); } } Future tap(WidgetTester t, Finder finder) async { await t.ensureVisible(finder); await t.tap(finder); await t.pumpAndSettle(); } void main() { late Uint8List bytes; setUpAll(() async { final r = ui.PictureRecorder(); final c = ui.Canvas(r); c.drawRect(const Rect.fromLTWH(0, 0, 600, 600), Paint()); final pic = r.endRecording(); final image = await pic.toImage(600, 600); bytes = (await image.toByteData(format: ui.ImageByteFormat.png))!.buffer .asUint8List(); image.dispose(); pic.dispose(); }); Future open( WidgetTester t, { Photos? photos, Locale? locale, }) async { t.platformDispatcher.defaultRouteNameTestValue = '/identity'; addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue); final service = DummyIdentityService(delay: Duration.zero); final provider = LocaleProvider(); if (locale != null) await provider.setLocale(locale); await t.pumpWidget( SxchngeApp( identityService: service, photoService: photos ?? Photos(bytes), localeProvider: provider, ), ); await t.pumpAndSettle(); return service; } Future submit(WidgetTester t) async { await tap(t, find.byKey(const ValueKey('kyc-start'))); await t.enterText(find.byKey(const ValueKey('kyc-name')), 'Example Person'); await t.enterText(find.byKey(const ValueKey('kyc-birth')), '1990-04-23'); await tap( t, find.descendant( of: find.byKey(const ValueKey('kyc-country')), matching: find.byType(OutlinedButton), ), ); await t.enterText(find.byKey(const ValueKey('country-search')), 'PL'); await t.pumpAndSettle(); await tap(t, find.byType(ListTile)); expect( t.widget(find.byKey(const ValueKey('kyc-country'))).value, 'PL', ); await t.pumpAndSettle(); await tap(t, find.byKey(const ValueKey('pick-passport'))); await tap(t, find.byKey(const ValueKey('kyc-next'))); await tap(t, find.byKey(const ValueKey('pick-selfie'))); await tap(t, find.byKey(const ValueKey('kyc-next'))); await tap(t, find.byType(CheckboxListTile)); await tap(t, find.byKey(const ValueKey('kyc-submit'))); } testWidgets('two photos, review, pending and approval with service limits', ( t, ) async { await open(t); expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget); await submit(t); expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget); await tap(t, find.byType(ExpansionTile)); await tap(t, find.text('Approve')); expect(find.byKey(const ValueKey('kyc-approved')), findsOneWidget); expect(find.text('Verified'), findsOneWidget); expect(t.takeException(), null); }); testWidgets('denial provides explanation and support, no resubmit', ( t, ) async { await open(t); await submit(t); await tap(t, find.byType(ExpansionTile)); await tap(t, find.text('Decline')); expect(find.byKey(const ValueKey('kyc-denied')), findsOneWidget); expect(find.byKey(const ValueKey('kyc-start')), findsNothing); await tap(t, find.text('Contact support')); expect(find.byType(AlertDialog), findsOneWidget); }); testWidgets('optional onboarding can be skipped before collecting proofs', ( t, ) async { t.platformDispatcher.defaultRouteNameTestValue = '/identity?onboarding=1'; addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue); await t.pumpWidget( SxchngeApp( identityService: DummyIdentityService(delay: Duration.zero), photoService: Photos(bytes), ), ); await t.pumpAndSettle(); final router = GoRouter.of( t.element(find.byKey(const ValueKey('kyc-start'))), ); await tap(t, find.text('Keep basic limits')); expect(router.routeInformationProvider.value.uri.path, '/accounts'); expect(find.byType(AlertDialog), findsNothing); }); testWidgets('correctable rejection can be resubmitted', (t) async { await open(t); await submit(t); await tap(t, find.byType(ExpansionTile)); await tap(t, find.text('Request clearer passport')); expect(find.byKey(const ValueKey('kyc-changesRequested')), findsOneWidget); await submit(t); expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget); }); testWidgets('picker error recovery and discard confirmation', (t) async { final photos = Photos(bytes)..error = KycError.permission; await open(t, photos: photos); await tap(t, find.byKey(const ValueKey('kyc-start'))); await tap(t, find.byKey(const ValueKey('pick-passport'))); expect(find.byKey(const ValueKey('kyc-error')), findsOneWidget); photos.error = null; await tap(t, find.byKey(const ValueKey('pick-passport'))); await tap(t, find.text('Keep basic limits')); expect(find.byType(AlertDialog), findsOneWidget); await tap(t, find.text('Keep editing')); expect(find.byKey(const ValueKey('pick-passport')), findsOneWidget); await tap(t, find.text('Keep basic limits')); await tap(t, find.text('Discard & leave')); expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget); }); testWidgets( '320px Russian at large text works through submission and keyboard', (t) async { t.view.physicalSize = const Size(320, 900); t.view.devicePixelRatio = 1; t.platformDispatcher.textScaleFactorTestValue = 1.6; addTearDown(t.view.resetPhysicalSize); addTearDown(t.view.resetDevicePixelRatio); addTearDown(t.platformDispatcher.clearTextScaleFactorTestValue); await open(t, locale: const Locale('ru')); expect(find.text('Верификация и лимиты'), findsOneWidget); await t.sendKeyEvent(LogicalKeyboardKey.tab); await t.pump(); await submit(t); expect(find.byKey(const ValueKey('kyc-pending')), findsOneWidget); expect(t.takeException(), null); }, ); testWidgets('status failure exposes retry without asserting basic state', ( t, ) async { t.platformDispatcher.defaultRouteNameTestValue = '/identity'; addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue); final service = DummyIdentityService(delay: Duration.zero) ..nextError = KycError.unavailable; await t.pumpWidget( SxchngeApp( identityService: service, photoService: Photos(bytes), analyticsService: DummyAnalyticsService(), ), ); await t.pumpAndSettle(); expect(find.byKey(const ValueKey('kyc-unverified')), findsNothing); await tap(t, find.text('Refresh status & limits')); expect(find.byKey(const ValueKey('kyc-unverified')), findsOneWidget); }); testWidgets('route guard preserves draft when navigation is cancelled', ( t, ) async { await open(t); await tap(t, find.byKey(const ValueKey('kyc-start'))); await tap(t, find.byKey(const ValueKey('pick-passport'))); final router = GoRouter.of(t.element(find.text('Verification & limits'))); router.go('/'); await t.pumpAndSettle(); expect(find.byType(AlertDialog), findsOneWidget); await tap(t, find.text('Keep editing')); expect(find.byKey(const ValueKey('pick-passport')), findsOneWidget); router.go('/'); await t.pumpAndSettle(); await tap(t, find.text('Discard & leave')); expect(find.byKey(const ValueKey('kyc-start')), findsNothing); expect(find.text('Explore your accounts'), findsOneWidget); }); testWidgets('overview controls have accessible labels and touch targets', ( t, ) async { final semantics = t.ensureSemantics(); await open(t); expect(t, meetsGuideline(labeledTapTargetGuideline)); expect(t, meetsGuideline(androidTapTargetGuideline)); final start = find.byKey(const ValueKey('kyc-start')); await t.ensureVisible(start); final buttonContext = t.element(start); FocusScope.of(buttonContext).requestFocus(Focus.of(buttonContext)); await t.sendKeyEvent(LogicalKeyboardKey.tab); await t.pump(); expect(t.takeException(), null); semantics.dispose(); }); }