import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:sxchnge_analytics/sxchnge_analytics.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'; import 'destination_helpers.dart'; Future click(WidgetTester t, Finder finder) async { await t.ensureVisible(finder); await t.pumpAndSettle(); await t.tap(finder); await t.pumpAndSettle(); } Future open( WidgetTester t, { String path = '/settings/payout-methods', String locale = 'en', DummyDestinationService? destinations, DummyAnalyticsService? analytics, }) async { t.platformDispatcher.defaultRouteNameTestValue = path; addTearDown(t.platformDispatcher.clearDefaultRouteNameTestValue); final wallet = DummyWalletService( DummyIdentityService(delay: Duration.zero), delay: Duration.zero, destinations: destinations, initialLocale: locale, settlement: Duration.zero, ); await t.pumpWidget( SxchngeApp( walletService: wallet, analyticsService: analytics, localeProvider: LocaleProvider(initialLocale: Locale(locale)), ), ); await t.pumpAndSettle(); addTearDown(() => t.pumpWidget(const SizedBox.shrink())); return wallet; } Future cryptoForm(WidgetTester t, {bool payout = false}) async { await click( t, find.text(payout ? 'Use a new destination' : 'Add payout method').first, ); await click(t, find.byType(DropdownButtonFormField)); await click(t, find.text('Crypto address').last); await click(t, find.byKey(const ValueKey('asset-null'))); await click(t, find.text('ETH').last); await click(t, find.byType(DropdownButtonFormField).last); await click(t, find.text('Ethereum').last); await t.enterText( find.byKey(const ValueKey('destination-address')), testDestination.address, ); await t.pump(); } void main() { testWidgets('Settings saves without nickname and exposes renamed method', ( t, ) async { final wallet = await open(t); await cryptoForm(t); expect(find.text('Save for future payouts'), findsNothing); await click(t, find.byKey(const ValueKey('destination-continue'))); expect((await wallet.destinations.list()).length, 1); expect(find.textContaining('ETH · Ethereum'), findsOneWidget); await click(t, find.textContaining('ETH · Ethereum')); await t.enterText( find.byKey(const ValueKey('destination-nickname')), 'My own wallet', ); await click(t, find.text('Rename')); expect((await wallet.destinations.list()).single.nickname, 'My own wallet'); await click(t, find.text('Remove')); expect(find.textContaining('Submitted payouts'), findsOneWidget); await click(t, find.widgetWithText(FilledButton, 'Remove')); expect(await wallet.destinations.list(), isEmpty); expect(t.takeException(), isNull); }); testWidgets('new one-time payout keeps saving unchecked and never persists', ( t, ) async { final wallet = await open(t, path: '/payout'); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '5'); await click(t, find.byKey(const ValueKey('choose-payout-destination'))); await cryptoForm(t, payout: true); expect( t.widget(find.byType(CheckboxListTile)).value, isFalse, ); expect(find.byKey(const ValueKey('destination-nickname')), findsNothing); await click(t, find.byKey(const ValueKey('destination-continue'))); await click(t, find.byKey(const ValueKey('transfer-review'))); expect(find.textContaining(testDestination.address), findsOneWidget); await click(t, find.byKey(const ValueKey('transfer-confirm'))); expect(find.byKey(const ValueKey('operation-completed')), findsOneWidget); expect(await wallet.destinations.list(), isEmpty); }); testWidgets( 'optional saving failure permits explicit one-time continuation', (t) async { final service = DummyDestinationService(); final wallet = await open(t, path: '/payout', destinations: service); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '5'); await click(t, find.byKey(const ValueKey('choose-payout-destination'))); await cryptoForm(t, payout: true); await click(t, find.text('Save for future payouts')); service.nextSaveError = DestinationIssue.conflict; await click(t, find.byKey(const ValueKey('destination-continue'))); expect(find.textContaining('Couldn’t save'), findsOneWidget); await click(t, find.text('Continue without saving')); await click(t, find.byKey(const ValueKey('transfer-review'))); await click(t, find.byKey(const ValueKey('transfer-confirm'))); expect(find.byKey(const ValueKey('operation-completed')), findsOneWidget); expect(await wallet.destinations.list(), isEmpty); }, ); testWidgets( 'saved destination is never selected automatically and removal preserves amount', (t) async { final service = DummyDestinationService(); await seedDestination(service); final wallet = await open(t, path: '/payout', destinations: service); await t.enterText(find.byKey(const ValueKey('transfer-amount')), '5'); expect( t .widget(find.byKey(const ValueKey('transfer-review'))) .onPressed, isNull, ); await selectPayout(t); await click(t, find.byKey(const ValueKey('transfer-review'))); final d = (await service.list()).single; await service.remove(d.id, d.metadataRevision, d.executionRevision); await click(t, find.byKey(const ValueKey('transfer-confirm'))); expect(find.textContaining('Payout terms changed'), findsOneWidget); expect(find.text('5'), findsOneWidget); expect((await t.runAsync(wallet.fetch))!.operations, isEmpty); }, ); for (final locale in ['en', 'ru']) { testWidgets('all method forms fit $locale at 320px with large 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, locale: locale); await click( t, find .text( locale == 'en' ? 'Add payout method' : 'Добавить способ вывода', ) .first, ); for (final name in locale == 'en' ? ['Crypto address', 'Bank card', 'Russian bank account'] : [ 'Криптоадрес', 'Банковская карта', 'Российский банковский счёт', ]) { await click(t, find.byType(DropdownButtonFormField)); await click(t, find.text(name).last); expect(t.takeException(), isNull); } }); } testWidgets('semantic save outcomes contain no destination fields', ( t, ) async { final analytics = DummyAnalyticsService(); await open(t, analytics: analytics); await cryptoForm(t); await click(t, find.byKey(const ValueKey('destination-continue'))); await t.pumpAndSettle(); final events = analytics.events .where((e) => e.kind == JourneyKind.destination) .toList(); expect( events.any( (e) => e.step == JourneyStep.destinationSave && e.signal == JourneySignal.simulated, ), isTrue, ); expect( events.map((e) => e.properties.toString()).join(), isNot(contains(testDestination.address)), ); }); testWidgets( 'binding recovery uses secure capture and preserves the saved card', (t) async { final service = DummyDestinationService(); final created = await service.create( const CardDestination( capture: 'demo-card-recover', brand: '', last4: '', expiryMonth: 0, expiryYear: 0, ), 'Own card', 'creation-key-000001', ); await open(t, destinations: service); await click(t, find.text('Own card')); await click(t, find.text('Update authorization')); expect(find.byKey(const ValueKey('destination-nickname')), findsNothing); await click(t, find.text('Demo card •••• 4242')); await click(t, find.byKey(const ValueKey('destination-continue'))); final saved = await service.get(created.destinationId); expect(saved.executionRevision, 2); expect(saved.nickname, 'Own card'); expect((await service.list()).length, 1); expect( (await service.assess( saved.input, const PayoutContext('eur'), )).eligible, isTrue, ); }, ); }