Build exchange workspace and shared transaction services (#28)
This commit is contained in:
1 parent
52b7135eda
commit
c496da8a55
48 files changed
+4757
-120
No files matched your search
@@ -0,0 +1,220 @@
|
||||
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<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 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<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);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user