feat: Testing the Exchange flow section, selecting sending and receiving currencies

This commit is contained in:
Blazebrain 2024-06-27 07:17:06 +01:00
parent 8ec38e10ff
commit 66af74f0c0
16 changed files with 315 additions and 64 deletions

View file

@ -1,5 +1,5 @@
import 'package:cake_wallet/main.dart' as app; import 'package:cake_wallet/main.dart' as app;
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
@ -7,6 +7,7 @@ import 'package:integration_test/integration_test.dart';
import 'robots/dashboard_page_robot.dart'; import 'robots/dashboard_page_robot.dart';
import 'robots/disclaimer_page_robot.dart'; import 'robots/disclaimer_page_robot.dart';
import 'robots/exchange_page_robot.dart';
import 'robots/new_wallet_type_page_robot.dart'; import 'robots/new_wallet_type_page_robot.dart';
import 'robots/restore_from_seed_or_key_robot.dart'; import 'robots/restore_from_seed_or_key_robot.dart';
import 'robots/restore_options_page_robot.dart'; import 'robots/restore_options_page_robot.dart';
@ -32,6 +33,7 @@ void main() {
NewWalletTypePageRobot newWalletTypePageRobot; NewWalletTypePageRobot newWalletTypePageRobot;
RestoreFromSeedOrKeysPageRobot restoreFromSeedOrKeysPageRobot; RestoreFromSeedOrKeysPageRobot restoreFromSeedOrKeysPageRobot;
DashboardPageRobot dashboardPageRobot; DashboardPageRobot dashboardPageRobot;
ExchangePageRobot exchangePageRobot;
group('Startup Test', () { group('Startup Test', () {
testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL', testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL',
@ -43,6 +45,7 @@ void main() {
newWalletTypePageRobot = NewWalletTypePageRobot(tester); newWalletTypePageRobot = NewWalletTypePageRobot(tester);
restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(tester); restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(tester);
dashboardPageRobot = DashboardPageRobot(tester); dashboardPageRobot = DashboardPageRobot(tester);
exchangePageRobot = ExchangePageRobot(tester);
await app.main(); await app.main();
await tester.pumpAndSettle(); await tester.pumpAndSettle();
@ -139,14 +142,28 @@ void main() {
await restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonTapped(); await restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonTapped();
// ----------- RestoreFromSeedOrKeys Page ------------- // ----------- RestoreFromSeedOrKeys Page -------------
await dashboardPageRobot.isDashboardPage(); // await dashboardPageRobot.isDashboardPage();
dashboardPageRobot.confirmServiceUpdateButtonDisplays(); // dashboardPageRobot.confirmServiceUpdateButtonDisplays();
dashboardPageRobot.confirmMenuButtonDisplays(); // dashboardPageRobot.confirmMenuButtonDisplays();
dashboardPageRobot.confirmSyncIndicatorButtonDisplays(); // dashboardPageRobot.confirmSyncIndicatorButtonDisplays();
await dashboardPageRobot.confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType.solana); // await dashboardPageRobot.confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType.solana);
await dashboardPageRobot.navigateToExchangePage(); await dashboardPageRobot.navigateToExchangePage();
await Future.delayed(Duration(seconds: 5)); await Future.delayed(Duration(seconds: 2));
// ----------- Exchange Page -------------
await exchangePageRobot.isExchangePage();
exchangePageRobot.hasTitle();
exchangePageRobot.hasResetButton();
await exchangePageRobot.displayBothExchangeCards();
exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards();
exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards();
await exchangePageRobot.selectDepositCurrency(CryptoCurrency.btc);
await exchangePageRobot.selectReceiveCurrency(CryptoCurrency.usdtSol);
await Future.delayed(Duration(seconds: 10));
}); });
}); });
} }

View file

@ -29,7 +29,7 @@ class CommonTestCases {
expect(typeWidget, findsOneWidget); expect(typeWidget, findsOneWidget);
} }
void hasKey(String key) { void hasValueKey(String key) {
final typeWidget = find.byKey(ValueKey(key)); final typeWidget = find.byKey(ValueKey(key));
expect(typeWidget, findsOneWidget); expect(typeWidget, findsOneWidget);
} }
@ -51,5 +51,19 @@ class CommonTestCases {
await tester.pumpAndSettle(); await tester.pumpAndSettle();
} }
Future<void> defaultSleepTime({int seconds = 2})async => await Future.delayed(Duration(seconds: seconds)); Future<void> scrollUntilVisible(String childKey, String parentScrollableKey, {double delta = 300}) async {
final scrollableWidget = find.descendant(
of: find.byKey(Key(parentScrollableKey)),
matching: find.byType(Scrollable),
);
await tester.scrollUntilVisible(
find.byKey(ValueKey(childKey)),
delta,
scrollable: scrollableWidget,
);
}
Future<void> defaultSleepTime({int seconds = 2}) async =>
await Future.delayed(Duration(seconds: seconds));
} }

View file

@ -16,15 +16,15 @@ class DashboardPageRobot {
} }
void confirmServiceUpdateButtonDisplays() { void confirmServiceUpdateButtonDisplays() {
commonTestCases.hasKey('dashboard_page_services_update_button_key'); commonTestCases.hasValueKey('dashboard_page_services_update_button_key');
} }
void confirmSyncIndicatorButtonDisplays() { void confirmSyncIndicatorButtonDisplays() {
commonTestCases.hasKey('dashboard_page_sync_indicator_button_key'); commonTestCases.hasValueKey('dashboard_page_sync_indicator_button_key');
} }
void confirmMenuButtonDisplays() { void confirmMenuButtonDisplays() {
commonTestCases.hasKey('dashboard_page_wallet_menu_button_key'); commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key');
} }
Future<void> confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type, Future<void> confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type,

View file

@ -0,0 +1,152 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/exchange_card.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_checks.dart';
class ExchangePageRobot {
ExchangePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isExchangePage() async {
await commonTestCases.isSpecificPage<ExchangePage>();
}
void hasTitle() {
// commonTestCases.hasText(S.current.exchange);
}
void hasResetButton() {
commonTestCases.hasText(S.current.reset);
}
void displaysPresentProviderPicker() {
commonTestCases.hasType<PresentProviderPicker>();
}
Future<void> displayBothExchangeCards() async {
final ExchangePage exchangeCard = tester.widget<ExchangePage>(
find.byType(ExchangePage),
);
final depositKey = exchangeCard.depositKey;
final receiveKey = exchangeCard.receiveKey;
final depositExchangeCard = find.byKey(depositKey);
expect(depositExchangeCard, findsOneWidget);
final receiveExchangeCard = find.byKey(receiveKey);
expect(receiveExchangeCard, findsOneWidget);
}
void confirmRightComponentsDisplayOnDepositExchangeCards() {
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
final depositCardPrefix = 'deposit_exchange_card';
commonTestCases.hasValueKey('${depositCardPrefix}_title_key');
commonTestCases.hasValueKey('${depositCardPrefix}_currency_picker_button_key');
commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_text_key');
commonTestCases.hasValueKey('${depositCardPrefix}_amount_textfield_key');
commonTestCases.hasValueKey('${depositCardPrefix}_min_limit_text_key');
final initialCurrency = exchangeViewModel.depositCurrency;
if (initialCurrency.tag != null) {
commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_tag_text_key');
}
if (exchangeViewModel.hasAllAmount) {
commonTestCases.hasValueKey('${depositCardPrefix}_send_all_button_key');
}
if (exchangeViewModel.isMoneroWallet) {
commonTestCases.hasValueKey('${depositCardPrefix}_address_book_button_key');
}
if (exchangeViewModel.isDepositAddressEnabled) {
commonTestCases.hasValueKey('${depositCardPrefix}_editable_address_textfield_key');
} else {
commonTestCases.hasValueKey('${depositCardPrefix}_non_editable_address_textfield_key');
commonTestCases.hasValueKey('${depositCardPrefix}_copy_refund_address_button_key');
}
// commonTestCases.hasValueKey('${depositCardPrefix}_max_limit_text_key');
}
void confirmRightComponentsDisplayOnReceiveExchangeCards() {
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
final receiveCardPrefix = 'receive_exchange_card';
commonTestCases.hasValueKey('${receiveCardPrefix}_title_key');
commonTestCases.hasValueKey('${receiveCardPrefix}_currency_picker_button_key');
commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_text_key');
commonTestCases.hasValueKey('${receiveCardPrefix}_amount_textfield_key');
commonTestCases.hasValueKey('${receiveCardPrefix}_min_limit_text_key');
final initialCurrency = exchangeViewModel.receiveCurrency;
if (initialCurrency.tag != null) {
commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_tag_text_key');
}
if (exchangeViewModel.hasAllAmount) {
commonTestCases.hasValueKey('${receiveCardPrefix}_send_all_button_key');
}
if (exchangeViewModel.isMoneroWallet) {
commonTestCases.hasValueKey('${receiveCardPrefix}_address_book_button_key');
}
commonTestCases.hasValueKey('${receiveCardPrefix}_editable_address_textfield_key');
}
Future<void> selectDepositCurrency(CryptoCurrency depositCurrency) async {
final depositPrefix = 'deposit_exchange_card';
final currencyPickerKey = '${depositPrefix}_currency_picker_button_key';
final currencyPickerDialogKey = '${depositPrefix}_currency_picker_dialog_button_key';
await commonTestCases.tapItemByKey(currencyPickerKey);
commonTestCases.hasValueKey(currencyPickerDialogKey);
await commonTestCases.scrollUntilVisible(
'picker_items_index_${depositCurrency.name}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${depositCurrency.name}_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
final receivePrefix = 'receive_exchange_card';
final currencyPickerKey = '${receivePrefix}_currency_picker_button_key';
final currencyPickerDialogKey = '${receivePrefix}_currency_picker_dialog_button_key';
await commonTestCases.tapItemByKey(currencyPickerKey);
commonTestCases.hasValueKey(currencyPickerDialogKey);
await commonTestCases.scrollUntilVisible(
'picker_items_index_${receiveCurrency.name}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> enterDepositAddress(String depositAddress) async {
final amountTextField = find.byKey(ValueKey('deposit_exchange_card_amount_textfield_key'));
}
Future<void> enterReceiveAddress(String receiveAddress) async {}
}

View file

@ -21,10 +21,10 @@ class RestoreFromSeedOrKeysPageRobot {
commonTestCases.hasText(S.current.enter_seed_phrase); commonTestCases.hasText(S.current.enter_seed_phrase);
commonTestCases.hasText(S.current.restore_title_from_seed); commonTestCases.hasText(S.current.restore_title_from_seed);
commonTestCases.hasKey('wallet_restore_from_seed_wallet_name_textfield_key'); commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_textfield_key');
commonTestCases.hasKey('wallet_restore_from_seed_wallet_name_refresh_button_key'); commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key');
commonTestCases.hasKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
commonTestCases.hasKey('wallet_restore_from_seed_wallet_seeds_textfield_key'); commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key');
commonTestCases.hasText(S.current.private_key, hasWidget: false); commonTestCases.hasText(S.current.private_key, hasWidget: false);
commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false); commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false);
@ -43,11 +43,11 @@ class RestoreFromSeedOrKeysPageRobot {
} }
void confirmRestoreButtonDisplays() { void confirmRestoreButtonDisplays() {
commonTestCases.hasKey('wallet_restore_seed_or_key_restore_button_key'); commonTestCases.hasValueKey('wallet_restore_seed_or_key_restore_button_key');
} }
void confirmAdvancedSettingButtonDisplays() { void confirmAdvancedSettingButtonDisplays() {
commonTestCases.hasKey('wallet_restore_advanced_settings_button_key'); commonTestCases.hasValueKey('wallet_restore_advanced_settings_button_key');
} }
Future<void> enterWalletNameText(String walletName) async { Future<void> enterWalletNameText(String walletName) async {
@ -76,6 +76,6 @@ class RestoreFromSeedOrKeysPageRobot {
Future<void> onRestoreWalletButtonTapped() async { Future<void> onRestoreWalletButtonTapped() async {
await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key'); await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key');
await commonTestCases.defaultSleepTime(seconds: 15); // await commonTestCases.defaultSleepTime(seconds: 15);
} }
} }

View file

@ -14,10 +14,10 @@ class RestoreOptionsPageRobot {
} }
void hasRestoreOptionsButton() { void hasRestoreOptionsButton() {
commonTestCases.hasKey('restore_options_from_seeds_button_key'); commonTestCases.hasValueKey('restore_options_from_seeds_button_key');
commonTestCases.hasKey('restore_options_from_backup_button_key'); commonTestCases.hasValueKey('restore_options_from_backup_button_key');
commonTestCases.hasKey('restore_options_from_hardware_wallet_button_key'); commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key');
commonTestCases.hasKey('restore_options_from_qr_button_key'); commonTestCases.hasValueKey('restore_options_from_qr_button_key');
} }
Future<void> navigateToRestoreFromSeedsPage() async { Future<void> navigateToRestoreFromSeedsPage() async {

View file

@ -28,11 +28,11 @@ class SetupPinCodeRobot {
void hasNumberButtonsVisible() { void hasNumberButtonsVisible() {
// Confirmation for buttons 1-9 // Confirmation for buttons 1-9
for (var i = 1; i < 10; i++) { for (var i = 1; i < 10; i++) {
commonTestCases.hasKey('pin_code_button_${i}_key'); commonTestCases.hasValueKey('pin_code_button_${i}_key');
} }
// Confirmation for 0 button // Confirmation for 0 button
commonTestCases.hasKey('pin_code_button_0_key'); commonTestCases.hasValueKey('pin_code_button_0_key');
} }
Future<void> pushPinButton(int index) async { Future<void> pushPinButton(int index) async {

View file

@ -101,11 +101,11 @@ class ExchangePage extends BasePage {
@override @override
Function(BuildContext)? get pushToNextWidget => (context) { Function(BuildContext)? get pushToNextWidget => (context) {
FocusScopeNode currentFocus = FocusScope.of(context); FocusScopeNode currentFocus = FocusScope.of(context);
if (!currentFocus.hasPrimaryFocus) { if (!currentFocus.hasPrimaryFocus) {
currentFocus.focusedChild?.unfocus(); currentFocus.focusedChild?.unfocus();
} }
}; };
@override @override
Widget middle(BuildContext context) => Row( Widget middle(BuildContext context) => Row(
@ -239,6 +239,7 @@ class ExchangePage extends BasePage {
), ),
Observer( Observer(
builder: (_) => LoadingPrimaryButton( builder: (_) => LoadingPrimaryButton(
key: ValueKey('exchange_page_exchange_button_key'),
text: S.of(context).exchange, text: S.of(context).exchange,
onPressed: () { onPressed: () {
if (_formKey.currentState != null && if (_formKey.currentState != null &&
@ -340,7 +341,6 @@ class ExchangePage extends BasePage {
void applyTemplate( void applyTemplate(
BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async { BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
final depositCryptoCurrency = CryptoCurrency.fromString(template.depositCurrency); final depositCryptoCurrency = CryptoCurrency.fromString(template.depositCurrency);
final receiveCryptoCurrency = CryptoCurrency.fromString(template.receiveCurrency); final receiveCryptoCurrency = CryptoCurrency.fromString(template.receiveCurrency);
@ -354,10 +354,12 @@ class ExchangePage extends BasePage {
exchangeViewModel.isFixedRateMode = false; exchangeViewModel.isFixedRateMode = false;
var domain = template.depositAddress; var domain = template.depositAddress;
exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, depositCryptoCurrency); exchangeViewModel.depositAddress =
await fetchParsedAddress(context, domain, depositCryptoCurrency);
domain = template.receiveAddress; domain = template.receiveAddress;
exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, receiveCryptoCurrency); exchangeViewModel.receiveAddress =
await fetchParsedAddress(context, domain, receiveCryptoCurrency);
} }
void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) { void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) {
@ -529,14 +531,16 @@ class ExchangePage extends BasePage {
_depositAddressFocus.addListener(() async { _depositAddressFocus.addListener(() async {
if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) { if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) {
final domain = depositAddressController.text; final domain = depositAddressController.text;
exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency); exchangeViewModel.depositAddress =
await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency);
} }
}); });
_receiveAddressFocus.addListener(() async { _receiveAddressFocus.addListener(() async {
if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) { if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) {
final domain = receiveAddressController.text; final domain = receiveAddressController.text;
exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency); exchangeViewModel.receiveAddress =
await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency);
} }
}); });
@ -589,7 +593,8 @@ class ExchangePage extends BasePage {
} }
} }
Future<String> fetchParsedAddress(BuildContext context, String domain, CryptoCurrency currency) async { Future<String> fetchParsedAddress(
BuildContext context, String domain, CryptoCurrency currency) async {
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency); final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
final address = await extractAddressFromParsed(context, parsedAddress); final address = await extractAddressFromParsed(context, parsedAddress);
return address; return address;
@ -619,6 +624,7 @@ class ExchangePage extends BasePage {
Widget _exchangeCardsSection(BuildContext context) { Widget _exchangeCardsSection(BuildContext context) {
final firstExchangeCard = Observer( final firstExchangeCard = Observer(
builder: (_) => ExchangeCard( builder: (_) => ExchangeCard(
cardInstanceName: 'deposit_exchange_card',
onDispose: disposeBestRateSync, onDispose: disposeBestRateSync,
hasAllAmount: exchangeViewModel.hasAllAmount, hasAllAmount: exchangeViewModel.hasAllAmount,
allAmount: exchangeViewModel.hasAllAmount allAmount: exchangeViewModel.hasAllAmount
@ -689,6 +695,7 @@ class ExchangePage extends BasePage {
final secondExchangeCard = Observer( final secondExchangeCard = Observer(
builder: (_) => ExchangeCard( builder: (_) => ExchangeCard(
cardInstanceName: 'receive_exchange_card',
onDispose: disposeBestRateSync, onDispose: disposeBestRateSync,
amountFocusNode: _receiveAmountFocus, amountFocusNode: _receiveAmountFocus,
addressFocusNode: _receiveAddressFocus, addressFocusNode: _receiveAddressFocus,

View file

@ -121,6 +121,7 @@ class ExchangeTemplatePage extends BasePage {
padding: EdgeInsets.fromLTRB(24, 100, 24, 32), padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
child: Observer( child: Observer(
builder: (_) => ExchangeCard( builder: (_) => ExchangeCard(
cardInstanceName: 'deposit_exchange_template_card',
amountFocusNode: _depositAmountFocus, amountFocusNode: _depositAmountFocus,
key: depositKey, key: depositKey,
title: S.of(context).you_will_send, title: S.of(context).you_will_send,
@ -157,6 +158,7 @@ class ExchangeTemplatePage extends BasePage {
padding: EdgeInsets.only(top: 29, left: 24, right: 24), padding: EdgeInsets.only(top: 29, left: 24, right: 24),
child: Observer( child: Observer(
builder: (_) => ExchangeCard( builder: (_) => ExchangeCard(
cardInstanceName: 'receive_exchange_template_card',
amountFocusNode: _receiveAmountFocus, amountFocusNode: _receiveAmountFocus,
key: receiveKey, key: receiveKey,
title: S.of(context).you_will_get, title: S.of(context).you_will_get,

View file

@ -12,7 +12,8 @@ class CurrencyPicker extends StatefulWidget {
this.title, this.title,
this.hintText, this.hintText,
this.isMoneroWallet = false, this.isMoneroWallet = false,
this.isConvertFrom = false}); this.isConvertFrom = false,
super.key});
final int selectedAtIndex; final int selectedAtIndex;
final List<Currency> items; final List<Currency> items;

View file

@ -44,8 +44,9 @@ class ExchangeCard extends StatefulWidget {
this.allAmount, this.allAmount,
this.onPushPasteButton, this.onPushPasteButton,
this.onPushAddressBookButton, this.onPushAddressBookButton,
this.onDispose}) this.onDispose,
: super(key: key); required this.cardInstanceName,
}) : super(key: key);
final List<CryptoCurrency> currencies; final List<CryptoCurrency> currencies;
final Function(CryptoCurrency) onCurrencySelected; final Function(CryptoCurrency) onCurrencySelected;
@ -73,6 +74,7 @@ class ExchangeCard extends StatefulWidget {
final void Function(BuildContext context)? onPushPasteButton; final void Function(BuildContext context)? onPushPasteButton;
final void Function(BuildContext context)? onPushAddressBookButton; final void Function(BuildContext context)? onPushAddressBookButton;
final Function()? onDispose; final Function()? onDispose;
final String cardInstanceName;
@override @override
ExchangeCardState createState() => ExchangeCardState(); ExchangeCardState createState() => ExchangeCardState();
@ -88,11 +90,13 @@ class ExchangeCardState extends State<ExchangeCard> {
_walletName = '', _walletName = '',
_selectedCurrency = CryptoCurrency.btc, _selectedCurrency = CryptoCurrency.btc,
_isAmountEstimated = false, _isAmountEstimated = false,
_isMoneroWallet = false; _isMoneroWallet = false,
_cardInstanceName = '';
final addressController = TextEditingController(); final addressController = TextEditingController();
final amountController = TextEditingController(); final amountController = TextEditingController();
String _cardInstanceName;
String _title; String _title;
String? _min; String? _min;
String? _max; String? _max;
@ -105,6 +109,7 @@ class ExchangeCardState extends State<ExchangeCard> {
@override @override
void initState() { void initState() {
_cardInstanceName = widget.cardInstanceName;
_title = widget.title; _title = widget.title;
_isAmountEditable = widget.initialIsAmountEditable; _isAmountEditable = widget.initialIsAmountEditable;
_isAddressEditable = widget.initialIsAddressEditable; _isAddressEditable = widget.initialIsAddressEditable;
@ -183,6 +188,7 @@ class ExchangeCardState extends State<ExchangeCard> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
key: ValueKey('${_cardInstanceName}_title_key'),
_title, _title,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
@ -200,6 +206,7 @@ class ExchangeCardState extends State<ExchangeCard> {
height: 32, height: 32,
color: widget.currencyButtonColor, color: widget.currencyButtonColor,
child: InkWell( child: InkWell(
key: ValueKey('${_cardInstanceName}_currency_picker_button_key'),
onTap: () => _presentPicker(context), onTap: () => _presentPicker(context),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -209,9 +216,12 @@ class ExchangeCardState extends State<ExchangeCard> {
padding: EdgeInsets.only(right: 5), padding: EdgeInsets.only(right: 5),
child: widget.imageArrow, child: widget.imageArrow,
), ),
Text(_selectedCurrency.toString(), Text(
style: TextStyle( key: ValueKey('${_cardInstanceName}_selected_currency_text_key'),
fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white)) _selectedCurrency.toString(),
style: TextStyle(
fontWeight: FontWeight.w600, fontSize: 16, color: Colors.white),
)
]), ]),
), ),
), ),
@ -227,13 +237,16 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Center( child: Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(6.0), padding: const EdgeInsets.all(6.0),
child: Text(_selectedCurrency.tag!, child: Text(
style: TextStyle( key: ValueKey('${_cardInstanceName}_selected_currency_tag_text_key'),
fontSize: 12, _selectedCurrency.tag!,
fontWeight: FontWeight.bold, style: TextStyle(
color: Theme.of(context) fontSize: 12,
.extension<SendPageTheme>()! fontWeight: FontWeight.bold,
.textFieldButtonIconColor)), color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor),
),
), ),
), ),
), ),
@ -252,6 +265,7 @@ class ExchangeCardState extends State<ExchangeCard> {
child: FocusTraversalOrder( child: FocusTraversalOrder(
order: NumericFocusOrder(1), order: NumericFocusOrder(1),
child: BaseTextFormField( child: BaseTextFormField(
key: ValueKey('${_cardInstanceName}_amount_textfield_key'),
focusNode: widget.amountFocusNode, focusNode: widget.amountFocusNode,
controller: amountController, controller: amountController,
enabled: _isAmountEditable, enabled: _isAmountEditable,
@ -272,9 +286,7 @@ class ExchangeCardState extends State<ExchangeCard> {
color: Theme.of(context) color: Theme.of(context)
.extension<ExchangePageTheme>()! .extension<ExchangePageTheme>()!
.hintTextColor), .hintTextColor),
validator: _isAmountEditable validator: _isAmountEditable ? widget.currencyValueValidator : null),
? widget.currencyValueValidator
: null),
), ),
), ),
if (widget.hasAllAmount) if (widget.hasAllAmount)
@ -287,6 +299,7 @@ class ExchangeCardState extends State<ExchangeCard> {
.textFieldButtonColor, .textFieldButtonColor,
borderRadius: BorderRadius.all(Radius.circular(6))), borderRadius: BorderRadius.all(Radius.circular(6))),
child: InkWell( child: InkWell(
key: ValueKey('${_cardInstanceName}_send_all_button_key'),
onTap: () => widget.allAmount?.call(), onTap: () => widget.allAmount?.call(),
child: Center( child: Center(
child: Text(S.of(context).all, child: Text(S.of(context).all,
@ -313,6 +326,7 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
_min != null _min != null
? Text( ? Text(
key: ValueKey('${_cardInstanceName}_min_limit_text_key'),
S.of(context).min_value(_min ?? '', _selectedCurrency.toString()), S.of(context).min_value(_min ?? '', _selectedCurrency.toString()),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
@ -322,11 +336,15 @@ class ExchangeCardState extends State<ExchangeCard> {
: Offstage(), : Offstage(),
_min != null ? SizedBox(width: 10) : Offstage(), _min != null ? SizedBox(width: 10) : Offstage(),
_max != null _max != null
? Text(S.of(context).max_value(_max ?? '', _selectedCurrency.toString()), ? Text(
key: ValueKey('${_cardInstanceName}_max_limit_text_key'),
S.of(context).max_value(_max ?? '', _selectedCurrency.toString()),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
height: 1.2, height: 1.2,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor)) color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor,
),
)
: Offstage(), : Offstage(),
])), ])),
), ),
@ -347,6 +365,7 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 20), padding: EdgeInsets.only(top: 20),
child: AddressTextField( child: AddressTextField(
key: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
focusNode: widget.addressFocusNode, focusNode: widget.addressFocusNode,
controller: addressController, controller: addressController,
onURIScanned: (uri) { onURIScanned: (uri) {
@ -387,6 +406,8 @@ class ExchangeCardState extends State<ExchangeCard> {
FocusTraversalOrder( FocusTraversalOrder(
order: NumericFocusOrder(3), order: NumericFocusOrder(3),
child: BaseTextFormField( child: BaseTextFormField(
key: ValueKey(
'${_cardInstanceName}_non_editable_address_textfield_key'),
controller: addressController, controller: addressController,
borderColor: Colors.transparent, borderColor: Colors.transparent,
suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36), suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36),
@ -410,6 +431,8 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Semantics( child: Semantics(
label: S.of(context).address_book, label: S.of(context).address_book,
child: InkWell( child: InkWell(
key: ValueKey(
'${_cardInstanceName}_address_book_button_key'),
onTap: () async { onTap: () async {
final contact = final contact =
await Navigator.of(context).pushNamed( await Navigator.of(context).pushNamed(
@ -447,6 +470,7 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Semantics( child: Semantics(
label: S.of(context).copy_address, label: S.of(context).copy_address,
child: InkWell( child: InkWell(
key: ValueKey('${_cardInstanceName}_copy_refund_address_button_key'),
onTap: () { onTap: () {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: addressController.text)); ClipboardData(text: addressController.text));
@ -470,6 +494,7 @@ class ExchangeCardState extends State<ExchangeCard> {
showPopUp<void>( showPopUp<void>(
context: context, context: context,
builder: (_) => CurrencyPicker( builder: (_) => CurrencyPicker(
key: ValueKey('${_cardInstanceName}_currency_picker_dialog_button_key'),
selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
items: widget.currencies, items: widget.currencies,
hintText: S.of(context).search_currency, hintText: S.of(context).search_currency,

View file

@ -32,7 +32,7 @@ class AddressTextField extends StatelessWidget {
this.onPushPasteButton, this.onPushPasteButton,
this.onPushAddressBookButton, this.onPushAddressBookButton,
this.onSelectedContact, this.onSelectedContact,
this.selectedCurrency}); this.selectedCurrency, super.key});
static const prefixIconWidth = 34.0; static const prefixIconWidth = 34.0;
static const prefixIconHeight = 34.0; static const prefixIconHeight = 34.0;

View file

@ -3,7 +3,12 @@ import 'package:cake_wallet/palette.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class AlertCloseButton extends StatelessWidget { class AlertCloseButton extends StatelessWidget {
AlertCloseButton({this.image, this.bottom, this.onTap}); AlertCloseButton({
this.image,
this.bottom,
this.onTap,
super.key,
});
final VoidCallback? onTap; final VoidCallback? onTap;

View file

@ -11,6 +11,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart';
import 'package:cake_wallet/themes/extensions/picker_theme.dart'; import 'package:cake_wallet/themes/extensions/picker_theme.dart';
//TODO(David): PickerWidget is intertwined and confusing as is, find a way to optimize?
class Picker<Item> extends StatefulWidget { class Picker<Item> extends StatefulWidget {
Picker({ Picker({
required this.selectedAtIndex, required this.selectedAtIndex,
@ -153,6 +154,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
Container( Container(
padding: EdgeInsets.symmetric(horizontal: padding), padding: EdgeInsets.symmetric(horizontal: padding),
child: Text( child: Text(
key: ValueKey('picker_title_text_key'),
widget.title!, widget.title!,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
@ -189,7 +191,10 @@ class _PickerState<Item> extends State<Picker<Item>> {
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: SearchBarWidget( child: SearchBarWidget(
searchController: searchController, hintText: widget.hintText), key: ValueKey('picker_search_bar_key'),
searchController: searchController,
hintText: widget.hintText,
),
), ),
Divider( Divider(
color: Theme.of(context).extension<PickerTheme>()!.dividerColor, color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
@ -203,6 +208,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
children: <Widget>[ children: <Widget>[
filteredItems.length > 3 filteredItems.length > 3
? Scrollbar( ? Scrollbar(
key: ValueKey('picker_scrollbar_key'),
controller: controller, controller: controller,
child: itemsList(), child: itemsList(),
) )
@ -213,6 +219,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
left: padding, left: padding,
right: padding, right: padding,
child: Text( child: Text(
key: ValueKey('picker_descriptinon_text_key'),
widget.description!, widget.description!,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
@ -242,6 +249,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
if (widget.isWrapped) { if (widget.isWrapped) {
return PickerWrapperWidget( return PickerWrapperWidget(
key: ValueKey('picker_wrapper_widget_key'),
hasTitle: widget.title?.isNotEmpty ?? false, hasTitle: widget.title?.isNotEmpty ?? false,
children: [content], children: [content],
); );
@ -260,6 +268,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
color: Theme.of(context).extension<PickerTheme>()!.dividerColor, color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
child: widget.isGridView child: widget.isGridView
? GridView.builder( ? GridView.builder(
key: ValueKey('picker_items_grid_view_key'),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
controller: controller, controller: controller,
shrinkWrap: true, shrinkWrap: true,
@ -275,6 +284,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
: buildItem(index), : buildItem(index),
) )
: ListView.separated( : ListView.separated(
key: ValueKey('picker_items_list_view_key'),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
controller: controller, controller: controller,
shrinkWrap: true, shrinkWrap: true,
@ -297,6 +307,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
final item = widget.headerEnabled ? filteredItems[index] : items[index]; final item = widget.headerEnabled ? filteredItems[index] : items[index];
final tag = item is Currency ? item.tag : null; final tag = item is Currency ? item.tag : null;
final currencyName = item is Currency ? item.name : '';
final icon = _getItemIcon(item); final icon = _getItemIcon(item);
final image = images.isNotEmpty ? filteredImages[index] : icon; final image = images.isNotEmpty ? filteredImages[index] : icon;
@ -316,6 +327,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
key: ValueKey('picker_items_index_${currencyName}_text_key'),
widget.displayItem?.call(item) ?? item.toString(), widget.displayItem?.call(item) ?? item.toString(),
softWrap: true, softWrap: true,
style: TextStyle( style: TextStyle(
@ -335,6 +347,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
height: 18.0, height: 18.0,
child: Center( child: Center(
child: Text( child: Text(
key: ValueKey('picker_items_index_${index}_tag_key'),
tag, tag,
style: TextStyle( style: TextStyle(
fontSize: 7.0, fontSize: 7.0,
@ -358,6 +371,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
); );
return GestureDetector( return GestureDetector(
key: ValueKey('picker_items_index_${currencyName}_button_key'),
onTap: () { onTap: () {
if (widget.closeOnItemSelected) Navigator.of(context).pop(); if (widget.closeOnItemSelected) Navigator.of(context).pop();
onItemSelected(item!); onItemSelected(item!);
@ -383,6 +397,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
final item = items[index]; final item = items[index];
final tag = item is Currency ? item.tag : null; final tag = item is Currency ? item.tag : null;
final currencyName = item is Currency ? item.name : '';
final icon = _getItemIcon(item); final icon = _getItemIcon(item);
final image = images.isNotEmpty ? images[index] : icon; final image = images.isNotEmpty ? images[index] : icon;
@ -390,6 +405,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex; final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex;
final itemContent = Row( final itemContent = Row(
key: ValueKey('picker_selected_item_row_key'),
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: widget.mainAxisAlignment, mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -402,6 +418,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
key: ValueKey('picker_items_index_${currencyName}_selected_item_text_key'),
widget.displayItem?.call(item) ?? item.toString(), widget.displayItem?.call(item) ?? item.toString(),
softWrap: true, softWrap: true,
style: TextStyle( style: TextStyle(
@ -445,6 +462,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
); );
return GestureDetector( return GestureDetector(
key: ValueKey('picker_items_index_${currencyName}_selected_item_button_key'),
onTap: () { onTap: () {
if (widget.closeOnItemSelected) Navigator.of(context).pop(); if (widget.closeOnItemSelected) Navigator.of(context).pop();
}, },

View file

@ -4,7 +4,12 @@ import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/src/widgets/alert_close_button.dart'; import 'package:cake_wallet/src/widgets/alert_close_button.dart';
class PickerWrapperWidget extends StatelessWidget { class PickerWrapperWidget extends StatelessWidget {
PickerWrapperWidget({required this.children, this.hasTitle = false, this.onClose}); PickerWrapperWidget({
required this.children,
this.hasTitle = false,
this.onClose,
super.key,
});
final List<Widget> children; final List<Widget> children;
final bool hasTitle; final bool hasTitle;
@ -29,8 +34,8 @@ class PickerWrapperWidget extends StatelessWidget {
final containerBottom = screenCenter - containerCenter; final containerBottom = screenCenter - containerCenter;
// position the close button right below the search container // position the close button right below the search container
closeButtonBottom = closeButtonBottom - closeButtonBottom =
containerBottom + (!hasTitle ? padding : padding / 1.5); closeButtonBottom - containerBottom + (!hasTitle ? padding : padding / 1.5);
} }
return AlertBackground( return AlertBackground(
@ -46,7 +51,11 @@ class PickerWrapperWidget extends StatelessWidget {
children: children, children: children,
), ),
SizedBox(height: ResponsiveLayoutUtilBase.kPopupSpaceHeight), SizedBox(height: ResponsiveLayoutUtilBase.kPopupSpaceHeight),
AlertCloseButton(bottom: closeButtonBottom, onTap: onClose), AlertCloseButton(
key: ValueKey('picker_wrapper_close_button_key'),
bottom: closeButtonBottom,
onTap: onClose,
),
], ],
), ),
), ),

View file

@ -7,6 +7,7 @@ class SearchBarWidget extends StatelessWidget {
required this.searchController, required this.searchController,
this.hintText, this.hintText,
this.borderRadius = 14, this.borderRadius = 14,
super.key,
}); });
final TextEditingController searchController; final TextEditingController searchController;