Files

218 lines
8.4 KiB
Dart

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';
import 'destination_helpers.dart';
Future<void> tap(WidgetTester t, Finder f) async {
await t.ensureVisible(f);
await t.tap(f);
await t.pumpAndSettle();
}
Future<DummyWalletService> 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 seedDestination(service.destinations);
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 selectPayout(t);
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 selectPayout(t);
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<void>.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);
});
}