import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:go_router/go_router.dart'; import 'package:sxchnge_kyc/sxchnge_kyc.dart'; import 'package:sxchnge_wallet/sxchnge_wallet.dart'; import 'package:sxchnge_app/main.dart'; import 'package:sxchnge_app/providers/locale.dart'; Future tap(WidgetTester t, Finder f) async { await t.ensureVisible(f); await t.tap(f); await t.pumpAndSettle(); } Future open( WidgetTester t, { String path = '/accounts', String locale = 'en', Duration settlement = Duration.zero, }) async { t.platformDispatcher.defaultRouteNameTestValue = path; addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue); final identity = DummyIdentityService(delay: Duration.zero); final service = DummyWalletService( identity, delay: Duration.zero, settlement: settlement, initialLocale: locale, ); await t.pumpWidget( SxchngeApp( identityService: identity, walletService: service, localeProvider: LocaleProvider(initialLocale: Locale(locale)), ), ); await t.pumpAndSettle(); addTearDown(() => t.pumpWidget(const SizedBox.shrink())); return service; } void main() { testWidgets('available-only accounts expose one set of actions', (t) async { await open(t); expect(find.text('Reserved'), findsNothing); expect(find.text('Held'), findsNothing); expect(find.byKey(const ValueKey('exchange-eur')), findsOneWidget); expect(find.byKey(const ValueKey('exchange-usd')), findsNothing); await tap(t, find.byKey(const ValueKey('account-usd'))); expect(find.byKey(const ValueKey('topUp-usd')), findsOneWidget); expect(find.byKey(const ValueKey('exchange-eur')), findsNothing); }); testWidgets('top-up exchange payout end-to-end and history', (t) async { final s = await open(t); await tap(t, find.byKey(const ValueKey('topUp-eur'))); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '10'); await tap(t, find.byKey(const ValueKey('transfer-review'))); expect(find.text('You pay'), findsOneWidget); await tap(t, find.byKey(const ValueKey('transfer-confirm'))); expect(find.byKey(const ValueKey('operation-completed')), findsOneWidget); expect( find.text( 'Your account has been credited. The money is available to use.', ), findsOneWidget, ); await tap(t, find.text('Exchange').last); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '20'); await tap(t, find.byKey(const ValueKey('transfer-review'))); expect(find.text('Leaves your account'), findsOneWidget); expect(find.text('Fee'), findsOneWidget); await tap(t, find.byKey(const ValueKey('transfer-confirm'))); expect(find.byKey(const ValueKey('operation-completed')), findsOneWidget); await tap(t, find.text('Pay out').last); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '5'); await t.enterText( find.byKey(const ValueKey('transfer-recipient')), 'Sample recipient', ); await tap(t, find.byKey(const ValueKey('transfer-review'))); expect(find.text('Recipient receives'), findsOneWidget); await tap(t, find.byKey(const ValueKey('transfer-confirm'))); expect(find.byKey(const ValueKey('operation-completed')), findsOneWidget); final data = (await t.runAsync(() => s.fetch()))!; expect(data.operations.length, 3); expect(data.accounts.first.available, 6490); expect(data.accounts[1].available, 1600); }); testWidgets( 'processing payout explains reduced available balance then failed refund', (t) async { final s = await open( t, path: '/payout', settlement: const Duration(milliseconds: 100), ); s.failNextOperation = true; await t.enterText(find.byKey(const ValueKey('transfer-amount')), '10'); await t.enterText( find.byKey(const ValueKey('transfer-recipient')), 'Sample recipient', ); await tap(t, find.byKey(const ValueKey('transfer-review'))); await tap(t, find.byKey(const ValueKey('transfer-confirm'))); // Settlement is service-owned; refresh after the simulated processing period. await t.runAsync( () => Future.delayed(const Duration(milliseconds: 150)), ); await t.pump(const Duration(seconds: 3)); await t.pumpAndSettle(); expect(find.byKey(const ValueKey('operation-failed')), findsOneWidget); expect( find.text( 'The transaction did not complete. The amount and fee are available in your account again.', ), findsOneWidget, ); expect( ((await t.runAsync(() => s.fetch()))!).accounts.first.available, 7500, ); }, ); testWidgets( 'settings saves account language and hides page language selectors', (t) async { final s = await open(t, path: '/settings'); expect(find.byKey(const ValueKey('locale-selector')), findsNothing); await tap(t, find.byKey(const ValueKey('account-language-en'))); await tap(t, find.text('Русский').last); expect(((await t.runAsync(() => s.profile()))!).locale, 'ru'); expect(find.text('Язык аккаунта'), findsWidgets); }, ); testWidgets( 'language save failure retains session choice and provides retry', (t) async { final s = await open(t, path: '/settings'); s.nextError = WalletError.unavailable; await tap(t, find.byKey(const ValueKey('account-language-en'))); await tap(t, find.text('Русский').last); expect( find.text( 'Изменено для текущей сессии, но не сохранено в аккаунте. Повторите попытку ниже.', ), findsOneWidget, ); expect(((await t.runAsync(() => s.profile()))!).locale, 'en'); await tap(t, find.text('Обновить').last); expect(((await t.runAsync(() => s.profile()))!).locale, 'ru'); }, ); testWidgets( 'insufficient funds preserves entered amount and can be corrected', (t) async { await open(t, path: '/exchange'); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '900'); await tap(t, find.byKey(const ValueKey('transfer-review'))); expect(find.byKey(const ValueKey('transfer-error')), findsOneWidget); expect(find.text('900'), findsOneWidget); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '5'); await tap(t, find.byKey(const ValueKey('transfer-review'))); expect(find.byKey(const ValueKey('transfer-confirm')), findsOneWidget); }, ); testWidgets('uncertain submission checks outcome instead of sending twice', ( t, ) async { final s = await open(t, path: '/exchange'); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '5'); await tap(t, find.byKey(const ValueKey('transfer-review'))); s.loseNextResponse = true; await tap(t, find.byKey(const ValueKey('transfer-confirm'))); expect(find.text('Check outcome'), findsOneWidget); await tap(t, find.text('Check outcome')); expect(((await t.runAsync(() => s.fetch()))!).operations.length, 1); expect(find.byKey(const ValueKey('operation-completed')), findsOneWidget); }); for (final path in [ '/accounts', '/exchange', '/payout', '/activity', '/limits', '/settings', ]) { testWidgets('$path fits Russian at 320px with larger text', (t) async { t.view.physicalSize = const Size(320, 1100); t.view.devicePixelRatio = 1; t.platformDispatcher.textScaleFactorTestValue = 1.6; addTearDown(t.view.resetPhysicalSize); addTearDown(t.view.resetDevicePixelRatio); addTearDown(t.platformDispatcher.clearTextScaleFactorTestValue); await open(t, path: path, locale: 'ru'); expect(t.takeException(), null); if (path == '/exchange') { await t.enterText(find.byKey(const ValueKey('transfer-amount')), '5'); await tap(t, find.byKey(const ValueKey('transfer-review'))); expect(t.takeException(), null); } }); } testWidgets('leaving a draft requires a clear discard choice', (t) async { await open(t, path: '/exchange'); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '5'); final router = GoRouter.of( t.element(find.byKey(const ValueKey('transfer-amount'))), ); router.go('/accounts'); await t.pumpAndSettle(); expect(find.text('Leave this transaction?'), findsOneWidget); await tap(t, find.text('Keep editing')); expect(find.text('5'), findsOneWidget); }); }