diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index ac2480cb0..e096545c2 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -13,6 +13,9 @@ on: jobs: PR_test_build: runs-on: ubuntu-20.04 + strategy: + matrix: + api-level: [29] env: STORE_PASS: test@cake_wallet KEY_PASS: test@cake_wallet diff --git a/.gitignore b/.gitignore index 8336ca512..970241189 100644 --- a/.gitignore +++ b/.gitignore @@ -171,6 +171,9 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Configs/AppInfo.xcconfig + +integration_test/playground.dart + # Monero.dart (Monero_C) scripts/monero_c # iOS generated framework bin diff --git a/cw_nano/pubspec.lock b/cw_nano/pubspec.lock index e641024f7..ad1885d8b 100644 --- a/cw_nano/pubspec.lock +++ b/cw_nano/pubspec.lock @@ -277,10 +277,10 @@ packages: dependency: transitive description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.0" fixnum: dependency: transitive description: diff --git a/integration_test/components/common_test_cases.dart b/integration_test/components/common_test_cases.dart new file mode 100644 index 000000000..2e2991804 --- /dev/null +++ b/integration_test/components/common_test_cases.dart @@ -0,0 +1,96 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +class CommonTestCases { + WidgetTester tester; + CommonTestCases(this.tester); + + Future isSpecificPage() async { + await tester.pumpAndSettle(); + hasType(); + } + + Future tapItemByKey(String key, {bool shouldPumpAndSettle = true}) async { + final widget = find.byKey(ValueKey(key)); + await tester.tap(widget); + shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump(); + } + + Future tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async { + await tester.tap(finder); + shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump(); + } + + void hasText(String text, {bool hasWidget = true}) { + final textWidget = find.text(text); + expect(textWidget, hasWidget ? findsOneWidget : findsNothing); + } + + void hasType() { + final typeWidget = find.byType(T); + expect(typeWidget, findsOneWidget); + } + + void hasValueKey(String key) { + final typeWidget = find.byKey(ValueKey(key)); + expect(typeWidget, findsOneWidget); + } + + Future swipePage({bool swipeRight = true}) async { + await tester.drag(find.byType(PageView), Offset(swipeRight ? -300 : 300, 0)); + await tester.pumpAndSettle(); + } + + Future swipeByPageKey({required String key, bool swipeRight = true}) async { + await tester.drag(find.byKey(ValueKey(key)), Offset(swipeRight ? -300 : 300, 0)); + await tester.pumpAndSettle(); + } + + Future goBack() async { + tester.printToConsole('Routing back to previous screen'); + final NavigatorState navigator = tester.state(find.byType(Navigator)); + navigator.pop(); + await tester.pumpAndSettle(); + } + + Future scrollUntilVisible(String childKey, String parentScrollableKey, + {double delta = 300}) async { + final scrollableWidget = find.descendant( + of: find.byKey(Key(parentScrollableKey)), + matching: find.byType(Scrollable), + ); + + final isAlreadyVisibile = isWidgetVisible(find.byKey(ValueKey(childKey))); + + if (isAlreadyVisibile) return; + + await tester.scrollUntilVisible( + find.byKey(ValueKey(childKey)), + delta, + scrollable: scrollableWidget, + ); + } + + bool isWidgetVisible(Finder finder) { + try { + final Element element = finder.evaluate().single; + final RenderBox renderBox = element.renderObject as RenderBox; + return renderBox.paintBounds + .shift(renderBox.localToGlobal(Offset.zero)) + .overlaps(tester.binding.renderViews.first.paintBounds); + } catch (e) { + return false; + } + } + + Future enterText(String text, String editableTextKey) async { + final editableTextWidget = find.byKey(ValueKey((editableTextKey))); + + await tester.enterText(editableTextWidget, text); + + await tester.pumpAndSettle(); + } + + Future defaultSleepTime({int seconds = 2}) async => + await Future.delayed(Duration(seconds: seconds)); +} diff --git a/integration_test/components/common_test_constants.dart b/integration_test/components/common_test_constants.dart new file mode 100644 index 000000000..d8381973e --- /dev/null +++ b/integration_test/components/common_test_constants.dart @@ -0,0 +1,13 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_type.dart'; + +class CommonTestConstants { + static final pin = [0, 8, 0, 1]; + static final String sendTestAmount = '0.00008'; + static final String exchangeTestAmount = '8'; + static final WalletType testWalletType = WalletType.solana; + static final String testWalletName = 'Integrated Testing Wallet'; + static final CryptoCurrency testReceiveCurrency = CryptoCurrency.sol; + static final CryptoCurrency testDepositCurrency = CryptoCurrency.usdtSol; + static final String testWalletAddress = 'An2Y2fsUYKfYvN1zF89GAqR1e6GUMBg3qA83Y5ZWDf8L'; +} diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart new file mode 100644 index 000000000..807509de9 --- /dev/null +++ b/integration_test/components/common_test_flows.dart @@ -0,0 +1,101 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/main.dart' as app; + +import '../robots/disclaimer_page_robot.dart'; +import '../robots/new_wallet_type_page_robot.dart'; +import '../robots/restore_from_seed_or_key_robot.dart'; +import '../robots/restore_options_page_robot.dart'; +import '../robots/setup_pin_code_robot.dart'; +import '../robots/welcome_page_robot.dart'; +import 'common_test_cases.dart'; +import 'common_test_constants.dart'; + +class CommonTestFlows { + CommonTestFlows(this._tester) + : _commonTestCases = CommonTestCases(_tester), + _welcomePageRobot = WelcomePageRobot(_tester), + _setupPinCodeRobot = SetupPinCodeRobot(_tester), + _disclaimerPageRobot = DisclaimerPageRobot(_tester), + _newWalletTypePageRobot = NewWalletTypePageRobot(_tester), + _restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester), + _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester); + + final WidgetTester _tester; + final CommonTestCases _commonTestCases; + + final WelcomePageRobot _welcomePageRobot; + final SetupPinCodeRobot _setupPinCodeRobot; + final DisclaimerPageRobot _disclaimerPageRobot; + final NewWalletTypePageRobot _newWalletTypePageRobot; + final RestoreOptionsPageRobot _restoreOptionsPageRobot; + final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot; + + Future startAppFlow(Key key) async { + await app.main(topLevelKey: ValueKey('send_flow_test_app_key')); + + await _tester.pumpAndSettle(); + + // --------- Disclaimer Page ------------ + // Tap checkbox to accept disclaimer + await _disclaimerPageRobot.tapDisclaimerCheckbox(); + + // Tap accept button + await _disclaimerPageRobot.tapAcceptButton(); + } + + Future restoreWalletThroughSeedsFlow() async { + await _welcomeToRestoreFromSeedsPath(); + await _restoreFromSeeds(); + } + + Future restoreWalletThroughKeysFlow() async { + await _welcomeToRestoreFromSeedsPath(); + await _restoreFromKeys(); + } + + Future _welcomeToRestoreFromSeedsPath() async { + // --------- Welcome Page --------------- + await _welcomePageRobot.navigateToRestoreWalletPage(); + + // ----------- Restore Options Page ----------- + // Route to restore from seeds page to continue flow + await _restoreOptionsPageRobot.navigateToRestoreFromSeedsPage(); + + // ----------- SetupPinCode Page ------------- + // Confirm initial defaults - Widgets to be displayed etc + await _setupPinCodeRobot.isSetupPinCodePage(); + + await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, true); + await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, false); + await _setupPinCodeRobot.tapSuccessButton(); + + // ----------- NewWalletType Page ------------- + // Confirm scroll behaviour works properly + await _newWalletTypePageRobot + .findParticularWalletTypeInScrollableList(CommonTestConstants.testWalletType); + + // Select a wallet and route to next page + await _newWalletTypePageRobot.selectWalletType(CommonTestConstants.testWalletType); + await _newWalletTypePageRobot.onNextButtonPressed(); + } + + Future _restoreFromSeeds() async { + // ----------- RestoreFromSeedOrKeys Page ------------- + await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(secrets.solanaTestWalletSeeds); + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); + } + + Future _restoreFromKeys() async { + await _commonTestCases.swipePage(); + await _commonTestCases.defaultSleepTime(); + + await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName); + + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(''); + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed(); + } +} diff --git a/integration_test/funds_related_tests.dart b/integration_test/funds_related_tests.dart new file mode 100644 index 000000000..9d97d47f8 --- /dev/null +++ b/integration_test/funds_related_tests.dart @@ -0,0 +1,84 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'components/common_test_constants.dart'; +import 'components/common_test_flows.dart'; +import 'robots/auth_page_robot.dart'; +import 'robots/dashboard_page_robot.dart'; +import 'robots/exchange_confirm_page_robot.dart'; +import 'robots/exchange_page_robot.dart'; +import 'robots/exchange_trade_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + DashboardPageRobot dashboardPageRobot; + ExchangePageRobot exchangePageRobot; + ExchangeConfirmPageRobot exchangeConfirmPageRobot; + AuthPageRobot authPageRobot; + ExchangeTradePageRobot exchangeTradePageRobot; + CommonTestFlows commonTestFlows; + + group('Startup Test', () { + testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL', + (tester) async { + authPageRobot = AuthPageRobot(tester); + exchangePageRobot = ExchangePageRobot(tester); + dashboardPageRobot = DashboardPageRobot(tester); + exchangeTradePageRobot = ExchangeTradePageRobot(tester); + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + + await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key')); + + await commonTestFlows.restoreWalletThroughSeedsFlow(); + + // ----------- RestoreFromSeedOrKeys Page ------------- + await dashboardPageRobot.navigateToExchangePage(); + + // ----------- Exchange Page ------------- + await exchangePageRobot.isExchangePage(); + exchangePageRobot.hasResetButton(); + await exchangePageRobot.displayBothExchangeCards(); + exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards(); + exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards(); + + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.enterDepositRefundAddress( + depositAddress: CommonTestConstants.testWalletAddress); + + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + + await exchangePageRobot.onExchangeButtonPressed(); + + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); + + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + } + + // ----------- Exchange Confirm Page ------------- + await exchangeConfirmPageRobot.isExchangeConfirmPage(); + + exchangeConfirmPageRobot.confirmComponentsOfTradeDisplayProperly(); + await exchangeConfirmPageRobot.confirmCopyTradeIdToClipBoardWorksProperly(); + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); + + // ----------- Exchange Trade Page ------------- + await exchangeTradePageRobot.isExchangeTradePage(); + exchangeTradePageRobot.hasInformationDialog(); + await exchangeTradePageRobot.onGotItButtonPressed(); + + await exchangeTradePageRobot.onConfirmSendingButtonPressed(); + + await exchangeTradePageRobot.handleConfirmSendResult(); + + await exchangeTradePageRobot.onSendButtonOnConfirmSendingDialogPressed(); + }); + }); +} diff --git a/integration_test/helpers/mocks.dart b/integration_test/helpers/mocks.dart new file mode 100644 index 000000000..01259bcc8 --- /dev/null +++ b/integration_test/helpers/mocks.dart @@ -0,0 +1,25 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:hive/hive.dart'; +import 'package:mocktail/mocktail.dart'; + +class MockAppStore extends Mock implements AppStore{} +class MockAuthService extends Mock implements AuthService{} +class MockSettingsStore extends Mock implements SettingsStore {} +class MockAuthenticationStore extends Mock implements AuthenticationStore{} +class MockWalletListStore extends Mock implements WalletListStore{} + + + +class MockLinkViewModel extends Mock implements LinkViewModel {} + +class MockHiveInterface extends Mock implements HiveInterface {} + +class MockHiveBox extends Mock implements Box {} + +class MockSecureStorage extends Mock implements SecureStorage{} \ No newline at end of file diff --git a/integration_test/helpers/test_helpers.dart b/integration_test/helpers/test_helpers.dart new file mode 100644 index 000000000..979ff60ff --- /dev/null +++ b/integration_test/helpers/test_helpers.dart @@ -0,0 +1,100 @@ +import 'package:cake_wallet/core/auth_service.dart'; +import 'package:cake_wallet/core/secure_storage.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/store/app_store.dart'; +import 'package:cake_wallet/store/authentication_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/store/wallet_list_store.dart'; +import 'package:cake_wallet/view_model/link_view_model.dart'; +import 'package:hive/hive.dart'; +import 'package:mocktail/mocktail.dart'; + +import 'mocks.dart'; + +class TestHelpers { + static void setup() { + // Fallback values can also be declared here + registerDependencies(); + } + + static void registerDependencies() { + getAndRegisterAppStore(); + getAndRegisterAuthService(); + getAndRegisterSettingsStore(); + getAndRegisterAuthenticationStore(); + getAndRegisterWalletListStore(); + + getAndRegisterLinkViewModel(); + getAndRegisterSecureStorage(); + getAndRegisterHiveInterface(); + } + + static MockSettingsStore getAndRegisterSettingsStore() { + _removeRegistrationIfExists(); + final service = MockSettingsStore(); + getIt.registerSingleton(service); + return service; + } + + static MockAppStore getAndRegisterAppStore() { + _removeRegistrationIfExists(); + final service = MockAppStore(); + final settingsStore = getAndRegisterSettingsStore(); + + when(() => service.settingsStore).thenAnswer((invocation) => settingsStore); + getIt.registerSingleton(service); + return service; + } + + static MockAuthService getAndRegisterAuthService() { + _removeRegistrationIfExists(); + final service = MockAuthService(); + getIt.registerSingleton(service); + return service; + } + + static MockAuthenticationStore getAndRegisterAuthenticationStore() { + _removeRegistrationIfExists(); + final service = MockAuthenticationStore(); + when(() => service.state).thenReturn(AuthenticationState.uninitialized); + getIt.registerSingleton(service); + return service; + } + + static MockWalletListStore getAndRegisterWalletListStore() { + _removeRegistrationIfExists(); + final service = MockWalletListStore(); + getIt.registerSingleton(service); + return service; + } + + static MockLinkViewModel getAndRegisterLinkViewModel() { + _removeRegistrationIfExists(); + final service = MockLinkViewModel(); + getIt.registerSingleton(service); + return service; + } + + static MockHiveInterface getAndRegisterHiveInterface() { + _removeRegistrationIfExists(); + final service = MockHiveInterface(); + final box = MockHiveBox(); + getIt.registerSingleton(service); + return service; + } + + static MockSecureStorage getAndRegisterSecureStorage() { + _removeRegistrationIfExists(); + final service = MockSecureStorage(); + getIt.registerSingleton(service); + return service; + } + + static void _removeRegistrationIfExists() { + if (getIt.isRegistered()) { + getIt.unregister(); + } + } + + static void tearDown() => getIt.reset(); +} diff --git a/integration_test/integration_response_data.json b/integration_test/integration_response_data.json new file mode 100644 index 000000000..ec747fa47 --- /dev/null +++ b/integration_test/integration_response_data.json @@ -0,0 +1 @@ +null \ No newline at end of file diff --git a/integration_test/robots/auth_page_robot.dart b/integration_test/robots/auth_page_robot.dart new file mode 100644 index 000000000..6358d4398 --- /dev/null +++ b/integration_test/robots/auth_page_robot.dart @@ -0,0 +1,30 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import 'pin_code_widget_robot.dart'; + +class AuthPageRobot extends PinCodeWidgetRobot { + AuthPageRobot(this.tester) + : commonTestCases = CommonTestCases(tester), + super(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + bool onAuthPage() { + final hasPinButtons = find.byKey(ValueKey('pin_code_button_3_key')); + final hasPin = hasPinButtons.tryEvaluate(); + return hasPin; + } + + Future isAuthPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.setup_pin); + } +} diff --git a/integration_test/robots/dashboard_page_robot.dart b/integration_test/robots/dashboard_page_robot.dart new file mode 100644 index 000000000..fc917c3b2 --- /dev/null +++ b/integration_test/robots/dashboard_page_robot.dart @@ -0,0 +1,75 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class DashboardPageRobot { + DashboardPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isDashboardPage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmServiceUpdateButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_services_update_button_key'); + } + + void confirmSyncIndicatorButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_sync_indicator_button_key'); + } + + void confirmMenuButtonDisplays() { + commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key'); + } + + Future confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type, + {bool isHaven = false}) async { + //Balance Page + final walletName = walletTypeToString(type); + final assetName = isHaven ? '$walletName Assets' : walletName; + commonTestCases.hasText(assetName); + + // Swipe to Cake features Page + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); + await commonTestCases.defaultSleepTime(); + commonTestCases.hasText('Cake ${S.current.features}'); + + // Swipe back to balance + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); + await commonTestCases.defaultSleepTime(); + + // Swipe to Transactions Page + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key'); + await commonTestCases.defaultSleepTime(); + commonTestCases.hasText(S.current.transactions); + + // Swipe back to balance + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false); + await commonTestCases.defaultSleepTime(seconds: 5); + } + + Future navigateToBuyPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.buy}_action_button_key'); + } + + Future navigateToSendPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.send}_action_button_key'); + } + + Future navigateToSellPage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.sell}_action_button_key'); + } + + Future navigateToReceivePage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.receive}_action_button_key'); + } + + Future navigateToExchangePage() async { + await commonTestCases.tapItemByKey('dashboard_page_${S.current.exchange}_action_button_key'); + } +} diff --git a/integration_test/robots/disclaimer_page_robot.dart b/integration_test/robots/disclaimer_page_robot.dart new file mode 100644 index 000000000..18861fc29 --- /dev/null +++ b/integration_test/robots/disclaimer_page_robot.dart @@ -0,0 +1,39 @@ +import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class DisclaimerPageRobot { + DisclaimerPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isDisclaimerPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasCheckIcon(bool hasBeenTapped) { + // The checked Icon should not be available initially, until user taps the checkbox + final checkIcon = find.byKey(ValueKey('disclaimer_check_icon_key')); + expect(checkIcon, hasBeenTapped ? findsOneWidget : findsNothing); + } + + void hasDisclaimerCheckbox() { + final checkBox = find.byKey(ValueKey('disclaimer_check_key')); + expect(checkBox, findsOneWidget); + } + + Future tapDisclaimerCheckbox() async { + await commonTestCases.tapItemByKey('disclaimer_check_key'); + + await commonTestCases.defaultSleepTime(); + } + + Future tapAcceptButton() async { + await commonTestCases.tapItemByKey('disclaimer_accept_button_key'); + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/exchange_confirm_page_robot.dart b/integration_test/robots/exchange_confirm_page_robot.dart new file mode 100644 index 000000000..160fd9dfb --- /dev/null +++ b/integration_test/robots/exchange_confirm_page_robot.dart @@ -0,0 +1,45 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangeConfirmPageRobot { + ExchangeConfirmPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangeConfirmPage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmComponentsOfTradeDisplayProperly() { + final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage)); + final trade = exchangeConfirmPage.trade; + + commonTestCases.hasText(trade.id); + commonTestCases.hasText('${trade.provider.title} ${S.current.trade_id}'); + + commonTestCases.hasValueKey('exchange_confirm_page_saved_id_button_key'); + commonTestCases.hasValueKey('exchange_confirm_page_copy_to_clipboard_button_key'); + } + + Future confirmCopyTradeIdToClipBoardWorksProperly() async { + final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage)); + final trade = exchangeConfirmPage.trade; + + await commonTestCases.tapItemByKey('exchange_confirm_page_copy_to_clipboard_button_key'); + + ClipboardData? clipboardData = await Clipboard.getData('text/plain'); + + expect(clipboardData?.text, trade.id); + } + + Future onSavedTradeIdButtonPressed() async { + await tester.pumpAndSettle(); + await commonTestCases.defaultSleepTime(); + await commonTestCases.tapItemByKey('exchange_confirm_page_saved_id_button_key'); + } +} diff --git a/integration_test/robots/exchange_page_robot.dart b/integration_test/robots/exchange_page_robot.dart new file mode 100644 index 000000000..b439e4791 --- /dev/null +++ b/integration_test/robots/exchange_page_robot.dart @@ -0,0 +1,330 @@ +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/present_provider_picker.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangePageRobot { + ExchangePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangePage() async { + await commonTestCases.isSpecificPage(); + await commonTestCases.defaultSleepTime(); + } + + void hasResetButton() { + commonTestCases.hasText(S.current.reset); + } + + void displaysPresentProviderPicker() { + commonTestCases.hasType(); + } + + Future displayBothExchangeCards() async { + final ExchangePage exchangeCard = tester.widget( + 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'); + + exchangePage.depositKey.currentState!.changeLimits(min: '0.1'); + + 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 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); + + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (depositCurrency == exchangeViewModel.depositCurrency) { + await commonTestCases.defaultSleepTime(); + await commonTestCases + .tapItemByKey('picker_items_index_${depositCurrency.name}_selected_item_button_key'); + return; + } + + 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'); + } + + Future 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); + + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (receiveCurrency == exchangeViewModel.receiveCurrency) { + await commonTestCases + .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key'); + return; + } + + 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'); + } + + Future enterDepositAmount(String amount) async { + await commonTestCases.enterText(amount, 'deposit_exchange_card_amount_textfield_key'); + } + + Future enterDepositRefundAddress({String? depositAddress}) async { + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage)); + final exchangeViewModel = exchangePage.exchangeViewModel; + + if (exchangeViewModel.isDepositAddressEnabled && depositAddress != null) { + await commonTestCases.enterText( + depositAddress, 'deposit_exchange_card_editable_address_textfield_key'); + } + } + + Future enterReceiveAddress(String receiveAddress) async { + await commonTestCases.enterText( + receiveAddress, + 'receive_exchange_card_editable_address_textfield_key', + ); + await commonTestCases.defaultSleepTime(); + } + + Future onExchangeButtonPressed() async { + await commonTestCases.tapItemByKey('exchange_page_exchange_button_key'); + await commonTestCases.defaultSleepTime(); + } + + bool hasMaxLimitError() { + final maxErrorText = find.text(S.current.error_text_input_above_maximum_limit); + + bool hasMaxError = maxErrorText.tryEvaluate(); + + return hasMaxError; + } + + bool hasMinLimitError() { + final minErrorText = find.text(S.current.error_text_input_below_minimum_limit); + + bool hasMinError = minErrorText.tryEvaluate(); + + return hasMinError; + } + + bool hasTradeCreationFailureError() { + final tradeCreationFailureDialogButton = + find.byKey(ValueKey('exchange_page_trade_creation_failure_dialog_button_key')); + + bool hasTradeCreationFailure = tradeCreationFailureDialogButton.tryEvaluate(); + tester.printToConsole('Trade not created error: $hasTradeCreationFailure'); + return hasTradeCreationFailure; + } + + Future onTradeCreationFailureDialogButtonPressed() async { + await commonTestCases.tapItemByKey('exchange_page_trade_creation_failure_dialog_button_key'); + } + + /// Handling Trade Failure Errors or errors shown through the Failure Dialog. + /// + /// Simulating the user's flow and response when this error comes up. + /// Examples are: + /// - No provider can handle this trade error, + /// - Trade amount below limit error. + Future _handleTradeCreationFailureErrors() async { + bool isTradeCreationFailure = false; + + isTradeCreationFailure = hasTradeCreationFailureError(); + + int maxRetries = 20; + int retries = 0; + + while (isTradeCreationFailure && retries < maxRetries) { + await tester.pump(); + + await onTradeCreationFailureDialogButtonPressed(); + + await commonTestCases.defaultSleepTime(seconds: 5); + + await onExchangeButtonPressed(); + + isTradeCreationFailure = hasTradeCreationFailureError(); + retries++; + } + } + + /// Handles the min limit error. + /// + /// Simulates the user's flow and response when it comes up. + /// + /// Has a max retry of 20 times. + Future _handleMinLimitError(String initialAmount) async { + bool isMinLimitError = false; + + isMinLimitError = hasMinLimitError(); + + double amount; + + amount = double.parse(initialAmount); + + int maxRetries = 20; + int retries = 0; + + while (isMinLimitError && retries < maxRetries) { + amount++; + tester.printToConsole('Amount: $amount'); + + enterDepositAmount(amount.toString()); + + await commonTestCases.defaultSleepTime(); + + await onExchangeButtonPressed(); + + isMinLimitError = hasMinLimitError(); + + retries++; + } + + if (retries >= maxRetries) { + tester.printToConsole('Max retries reached for minLimit Error. Exiting loop.'); + } + } + + /// Handles the max limit error. + /// + /// Simulates the user's flow and response when it comes up. + /// + /// Has a max retry of 20 times. + Future _handleMaxLimitError(String initialAmount) async { + bool isMaxLimitError = false; + + isMaxLimitError = hasMaxLimitError(); + + double amount; + + amount = double.parse(initialAmount); + + int maxRetries = 20; + int retries = 0; + + while (isMaxLimitError && retries < maxRetries) { + amount++; + tester.printToConsole('Amount: $amount'); + + enterDepositAmount(amount.toString()); + + await commonTestCases.defaultSleepTime(); + + await onExchangeButtonPressed(); + + isMaxLimitError = hasMaxLimitError(); + + retries++; + } + + if (retries >= maxRetries) { + tester.printToConsole('Max retries reached for maxLimit Error. Exiting loop.'); + } + } + + Future handleErrors(String initialAmount) async { + await tester.pumpAndSettle(); + + await _handleMinLimitError(initialAmount); + + await _handleMaxLimitError(initialAmount); + + await _handleTradeCreationFailureErrors(); + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/exchange_trade_page_robot.dart b/integration_test/robots/exchange_trade_page_robot.dart new file mode 100644 index 000000000..5708b6fae --- /dev/null +++ b/integration_test/robots/exchange_trade_page_robot.dart @@ -0,0 +1,152 @@ +import 'dart:async'; + +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class ExchangeTradePageRobot { + ExchangeTradePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isExchangeTradePage() async { + await commonTestCases.isSpecificPage(); + } + + void hasInformationDialog() { + commonTestCases.hasValueKey('information_page_dialog_key'); + } + + Future onGotItButtonPressed() async { + await commonTestCases.tapItemByKey('information_page_got_it_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future onConfirmSendingButtonPressed() async { + tester.printToConsole('Now confirming sending'); + + await commonTestCases.tapItemByKey( + 'exchange_trade_page_confirm_sending_button_key', + shouldPumpAndSettle: false, + ); + + final Completer completer = Completer(); + + // Loop to wait for the async operation to complete + while (true) { + await Future.delayed(Duration(seconds: 1)); + + final ExchangeTradeState state = tester.state(find.byType(ExchangeTradeForm)); + final execState = state.widget.exchangeTradeViewModel.sendViewModel.state; + + bool isDone = execState is ExecutedSuccessfullyState; + bool isFailed = execState is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed'); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done confirming sending'); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + + Future onSendButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Send Button on Confirm Dialog Triggered'); + await commonTestCases.defaultSleepTime(seconds: 4); + + final sendText = find.text(S.current.send); + bool hasText = sendText.tryEvaluate(); + + if (hasText) { + await commonTestCases.tapItemByFinder(sendText); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + } + + Future onCancelButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Cancel Button on Confirm Dialog Triggered'); + + await commonTestCases.tapItemByKey( + 'exchange_trade_page_confirm_sending_dialog_cancel_button_key', + ); + + await commonTestCases.defaultSleepTime(); + } + + Future onSendFailureDialogButtonPressed() async { + await commonTestCases.defaultSleepTime(seconds: 6); + + tester.printToConsole('Send Button Failure Dialog Triggered'); + + await commonTestCases.tapItemByKey('exchange_trade_page_send_failure_dialog_button_key'); + } + + Future hasErrorWhileSending() async { + await tester.pump(); + + tester.printToConsole('Checking if there is an error'); + + final errorDialog = find.byKey( + ValueKey('exchange_trade_page_send_failure_dialog_button_key'), + ); + + bool hasError = errorDialog.tryEvaluate(); + + tester.printToConsole('Has error: $hasError'); + + return hasError; + } + + Future handleConfirmSendResult() async { + bool hasError = false; + + hasError = await hasErrorWhileSending(); + + int maxRetries = 20; + int retries = 0; + + while (hasError && retries < maxRetries) { + tester.printToConsole('hasErrorInLoop: $hasError'); + await tester.pump(); + + await onSendFailureDialogButtonPressed(); + tester.printToConsole('Failure button tapped'); + + await commonTestCases.defaultSleepTime(); + + await onConfirmSendingButtonPressed(); + tester.printToConsole('Confirm sending button tapped'); + + hasError = await hasErrorWhileSending(); + + retries++; + } + + if (!hasError) { + tester.printToConsole('No error, proceeding with flow'); + await tester.pump(); + } + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/new_wallet_type_page_robot.dart b/integration_test/robots/new_wallet_type_page_robot.dart new file mode 100644 index 000000000..89fc8d390 --- /dev/null +++ b/integration_test/robots/new_wallet_type_page_robot.dart @@ -0,0 +1,59 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class NewWalletTypePageRobot { + NewWalletTypePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isNewWalletTypePage() async { + await commonTestCases.isSpecificPage(); + } + + void displaysCorrectTitle(bool isCreate) { + commonTestCases.hasText( + isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet, + ); + } + + void hasWalletTypeForm() { + commonTestCases.hasType(); + } + + void displaysCorrectImage(ThemeType type) { + final walletTypeImage = Image.asset('assets/images/wallet_type.png').image; + final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png').image; + + find.image( + type == ThemeType.dark ? walletTypeImage : walletTypeLightImage, + ); + } + + Future findParticularWalletTypeInScrollableList(WalletType type) async { + final scrollableWidget = find.descendant( + of: find.byKey(Key('new_wallet_type_scrollable_key')), + matching: find.byType(Scrollable), + ); + + await tester.scrollUntilVisible( + find.byKey(ValueKey('new_wallet_type_${type.name}_button_key')), + 300, + scrollable: scrollableWidget, + ); + } + + Future selectWalletType(WalletType type) async { + await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key'); + } + + Future onNextButtonPressed() async { + await commonTestCases.tapItemByKey('new_wallet_type_next_button_key'); + } +} diff --git a/integration_test/robots/pin_code_widget_robot.dart b/integration_test/robots/pin_code_widget_robot.dart new file mode 100644 index 000000000..b6805e9e0 --- /dev/null +++ b/integration_test/robots/pin_code_widget_robot.dart @@ -0,0 +1,38 @@ +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class PinCodeWidgetRobot { + PinCodeWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + void hasPinCodeWidget() { + final pinCodeWidget = find.bySubtype(); + expect(pinCodeWidget, findsOneWidget); + } + + void hasNumberButtonsVisible() { + // Confirmation for buttons 1-9 + for (var i = 1; i < 10; i++) { + commonTestCases.hasValueKey('pin_code_button_${i}_key'); + } + + // Confirmation for 0 button + commonTestCases.hasValueKey('pin_code_button_0_key'); + } + + Future pushPinButton(int index) async { + await commonTestCases.tapItemByKey('pin_code_button_${index}_key'); + } + + Future enterPinCode(List pinCode, bool isFirstEntry) async { + for (int pin in pinCode) { + await pushPinButton(pin); + } + + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/restore_from_seed_or_key_robot.dart b/integration_test/robots/restore_from_seed_or_key_robot.dart new file mode 100644 index 000000000..43a65095d --- /dev/null +++ b/integration_test/robots/restore_from_seed_or_key_robot.dart @@ -0,0 +1,89 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart'; +import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class RestoreFromSeedOrKeysPageRobot { + RestoreFromSeedOrKeysPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isRestoreFromSeedKeyPage() async { + await commonTestCases.isSpecificPage(); + } + + Future confirmViewComponentsDisplayProperlyPerPageView() async { + commonTestCases.hasText(S.current.wallet_name); + commonTestCases.hasText(S.current.enter_seed_phrase); + commonTestCases.hasText(S.current.restore_title_from_seed); + + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_textfield_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'); + + commonTestCases.hasText(S.current.private_key, hasWidget: false); + commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false); + + await commonTestCases.swipePage(); + await commonTestCases.defaultSleepTime(); + + commonTestCases.hasText(S.current.wallet_name); + commonTestCases.hasText(S.current.private_key); + commonTestCases.hasText(S.current.restore_title_from_keys); + + commonTestCases.hasText(S.current.enter_seed_phrase, hasWidget: false); + commonTestCases.hasText(S.current.restore_title_from_seed, hasWidget: false); + + await commonTestCases.swipePage(swipeRight: false); + } + + void confirmRestoreButtonDisplays() { + commonTestCases.hasValueKey('wallet_restore_seed_or_key_restore_button_key'); + } + + void confirmAdvancedSettingButtonDisplays() { + commonTestCases.hasValueKey('wallet_restore_advanced_settings_button_key'); + } + + Future enterWalletNameText(String walletName, {bool isSeedFormEntry = true}) async { + await commonTestCases.enterText( + walletName, + 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_textfield_key', + ); + } + + Future selectWalletNameFromAvailableOptions({bool isSeedFormEntry = true}) async { + await commonTestCases.tapItemByKey( + 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_refresh_button_key', + ); + } + + Future enterSeedPhraseForWalletRestore(String text) async { + ValidatableAnnotatedEditableTextState seedTextState = + await tester.state(find.byType(ValidatableAnnotatedEditableText)); + + seedTextState.widget.controller.text = text; + await tester.pumpAndSettle(); + } + + Future onPasteSeedPhraseButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'); + } + + Future enterPrivateKeyForWalletRestore(String privateKey) async { + await commonTestCases.enterText( + privateKey, + 'wallet_restore_from_key_private_key_textfield_key', + ); + await tester.pumpAndSettle(); + } + + Future onRestoreWalletButtonPressed() async { + await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/restore_options_page_robot.dart b/integration_test/robots/restore_options_page_robot.dart new file mode 100644 index 000000000..b3cefc90c --- /dev/null +++ b/integration_test/robots/restore_options_page_robot.dart @@ -0,0 +1,42 @@ +import 'package:cake_wallet/src/screens/restore/restore_options_page.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class RestoreOptionsPageRobot { + RestoreOptionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isRestoreOptionsPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasRestoreOptionsButton() { + commonTestCases.hasValueKey('restore_options_from_seeds_button_key'); + commonTestCases.hasValueKey('restore_options_from_backup_button_key'); + commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key'); + commonTestCases.hasValueKey('restore_options_from_qr_button_key'); + } + + Future navigateToRestoreFromSeedsPage() async { + await commonTestCases.tapItemByKey('restore_options_from_seeds_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreFromBackupPage() async { + await commonTestCases.tapItemByKey('restore_options_from_backup_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreFromHardwareWalletPage() async { + await commonTestCases.tapItemByKey('restore_options_from_hardware_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future backAndVerify() async { + await commonTestCases.goBack(); + await isRestoreOptionsPage(); + } +} diff --git a/integration_test/robots/send_page_robot.dart b/integration_test/robots/send_page_robot.dart new file mode 100644 index 000000000..971556620 --- /dev/null +++ b/integration_test/robots/send_page_robot.dart @@ -0,0 +1,366 @@ +import 'dart:async'; + +import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/send/send_page.dart'; +import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import '../components/common_test_constants.dart'; +import 'auth_page_robot.dart'; + +class SendPageRobot { + SendPageRobot({required this.tester}) + : commonTestCases = CommonTestCases(tester), + authPageRobot = AuthPageRobot(tester); + + WidgetTester tester; + CommonTestCases commonTestCases; + AuthPageRobot authPageRobot; + + Future isSendPage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.send); + } + + void confirmViewComponentsDisplayProperly() { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + commonTestCases.hasValueKey('send_page_address_textfield_key'); + commonTestCases.hasValueKey('send_page_note_textfield_key'); + commonTestCases.hasValueKey('send_page_amount_textfield_key'); + commonTestCases.hasValueKey('send_page_add_template_button_key'); + + if (sendViewModel.hasMultipleTokens) { + commonTestCases.hasValueKey('send_page_currency_picker_button_key'); + } + + if (!sendViewModel.isBatchSending) { + commonTestCases.hasValueKey('send_page_send_all_button_key'); + } + + if (!sendViewModel.isFiatDisabled) { + commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key'); + } + + if (sendViewModel.hasFees) { + commonTestCases.hasValueKey('send_page_select_fee_priority_button_key'); + } + + if (sendViewModel.hasCoinControl) { + commonTestCases.hasValueKey('send_page_unspent_coin_button_key'); + } + + if (sendViewModel.hasCurrecyChanger) { + commonTestCases.hasValueKey('send_page_change_asset_button_key'); + } + + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) { + commonTestCases.hasValueKey('send_page_add_receiver_button_key'); + } + } + + Future selectReceiveCurrency(CryptoCurrency receiveCurrency) async { + final currencyPickerKey = 'send_page_currency_picker_button_key'; + final currencyPickerDialogKey = 'send_page_currency_picker_dialog_button_key'; + + await commonTestCases.tapItemByKey(currencyPickerKey); + commonTestCases.hasValueKey(currencyPickerDialogKey); + + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + if (receiveCurrency == sendViewModel.selectedCryptoCurrency) { + await commonTestCases + .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key'); + return; + } + + 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'); + } + + Future enterReceiveAddress(String receiveAddress) async { + await commonTestCases.enterText(receiveAddress, 'send_page_address_textfield_key'); + await commonTestCases.defaultSleepTime(); + } + + Future enterAmount(String amount) async { + await commonTestCases.enterText(amount, 'send_page_amount_textfield_key'); + } + + Future selectTransactionPriority({TransactionPriority? priority}) async { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + if (!sendViewModel.hasFees || priority == null) return; + + final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key'; + await commonTestCases.tapItemByKey(transactionPriorityPickerKey); + + if (priority == sendViewModel.transactionPriority) { + await commonTestCases + .tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key'); + return; + } + + await commonTestCases.scrollUntilVisible( + 'picker_items_index_${priority.title}_button_key', + 'picker_scrollbar_key', + ); + await commonTestCases.defaultSleepTime(); + + await commonTestCases.tapItemByKey('picker_items_index_${priority.title}_button_key'); + } + + Future onSendButtonPressed() async { + tester.printToConsole('Pressing send'); + + await commonTestCases.tapItemByKey( + 'send_page_send_button_key', + shouldPumpAndSettle: false, + ); + + await _waitForSendTransactionCompletion(); + + await commonTestCases.defaultSleepTime(); + } + + Future _waitForSendTransactionCompletion() async { + await tester.pump(); + final Completer completer = Completer(); + + // Loop to wait for the async operation to complete + while (true) { + await Future.delayed(Duration(seconds: 1)); + + tester.printToConsole('Before _handleAuth'); + + await _handleAuthPage(); + + tester.printToConsole('After _handleAuth'); + + await tester.pump(); + + final sendPage = tester.widget(find.byType(SendPage)); + final state = sendPage.sendViewModel.state; + + await tester.pump(); + + bool isDone = state is ExecutedSuccessfullyState; + bool isFailed = state is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed', + ); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done confirming sending operation'); + } + + Future _handleAuthPage() async { + tester.printToConsole('Inside _handleAuth'); + await tester.pump(); + tester.printToConsole('starting auth checks'); + + final authPage = authPageRobot.onAuthPage(); + + tester.printToConsole('hasAuth:$authPage'); + + if (authPage) { + await tester.pump(); + tester.printToConsole('Starting inner _handleAuth loop checks'); + + try { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + tester.printToConsole('Auth done'); + + await tester.pump(); + + tester.printToConsole('Auth pump done'); + } catch (e) { + tester.printToConsole('Auth failed, retrying'); + await tester.pump(); + _handleAuthPage(); + } + } + } + + Future handleSendResult() async { + tester.printToConsole('Inside handle function'); + + bool hasError = false; + + hasError = await hasErrorWhileSending(); + + tester.printToConsole('Has an Error in the handle: $hasError'); + + int maxRetries = 20; + int retries = 0; + + while (hasError && retries < maxRetries) { + tester.printToConsole('hasErrorInLoop: $hasError'); + await tester.pump(); + + await onSendFailureDialogButtonPressed(); + tester.printToConsole('Failure button tapped'); + + await commonTestCases.defaultSleepTime(); + + await onSendButtonPressed(); + tester.printToConsole('Send button tapped'); + + hasError = await hasErrorWhileSending(); + + retries++; + } + + if (!hasError) { + tester.printToConsole('No error, proceeding with flow'); + await tester.pump(); + } + + await commonTestCases.defaultSleepTime(); + } + + //* ------ On Sending Failure ------------ + Future hasErrorWhileSending() async { + await tester.pump(); + + tester.printToConsole('Checking if there is an error'); + + final errorDialog = find.byKey(ValueKey('send_page_send_failure_dialog_button_key')); + + bool hasError = errorDialog.tryEvaluate(); + + tester.printToConsole('Has error: $hasError'); + + return hasError; + } + + Future onSendFailureDialogButtonPressed() async { + await commonTestCases.defaultSleepTime(); + + tester.printToConsole('Send Button Failure Dialog Triggered'); + + await commonTestCases.tapItemByKey('send_page_send_failure_dialog_button_key'); + } + + //* ------ On Sending Success ------------ + Future onSendButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Inside confirm sending dialog: For sending'); + await commonTestCases.defaultSleepTime(); + await tester.pump(); + + final sendText = find.text(S.current.send).last; + bool hasText = sendText.tryEvaluate(); + tester.printToConsole('Has Text: $hasText'); + + if (hasText) { + await commonTestCases.tapItemByFinder(sendText, shouldPumpAndSettle: false); + // Loop to wait for the operation to commit transaction + await _waitForCommitTransactionCompletion(); + + await commonTestCases.defaultSleepTime(seconds: 4); + } else { + await commonTestCases.defaultSleepTime(); + await tester.pump(); + onSendButtonOnConfirmSendingDialogPressed(); + } + } + + Future _waitForCommitTransactionCompletion() async { + final Completer completer = Completer(); + + while (true) { + await Future.delayed(Duration(seconds: 1)); + + final sendPage = tester.widget(find.byType(SendPage)); + final state = sendPage.sendViewModel.state; + + bool isDone = state is TransactionCommitted; + bool isFailed = state is FailureState; + + tester.printToConsole('isDone: $isDone'); + tester.printToConsole('isFailed: $isFailed'); + + if (isDone || isFailed) { + tester.printToConsole( + isDone ? 'Completer is done' : 'Completer is done though operation failed', + ); + completer.complete(); + await tester.pump(); + break; + } else { + tester.printToConsole('Completer is not done'); + await tester.pump(); + } + } + + await expectLater(completer.future, completes); + + tester.printToConsole('Done Committing Transaction'); + } + + Future onCancelButtonOnConfirmSendingDialogPressed() async { + tester.printToConsole('Inside confirm sending dialog: For canceling'); + await commonTestCases.defaultSleepTime(seconds: 4); + + final cancelText = find.text(S.current.cancel); + bool hasText = cancelText.tryEvaluate(); + + if (hasText) { + await commonTestCases.tapItemByFinder(cancelText); + + await commonTestCases.defaultSleepTime(seconds: 4); + } + } + + //* ---- Add Contact Dialog On Send Successful Dialog ----- + Future onSentDialogPopUp() async { + SendPage sendPage = tester.widget(find.byType(SendPage)); + final sendViewModel = sendPage.sendViewModel; + + final newContactAddress = sendPage.newContactAddress ?? sendViewModel.newContactAddress(); + if (newContactAddress != null) { + await _onAddContactButtonOnSentDialogPressed(); + } + + await commonTestCases.defaultSleepTime(); + } + + Future _onAddContactButtonOnSentDialogPressed() async { + await commonTestCases.tapItemByKey('send_page_sent_dialog_add_contact_button_key'); + } + + // ignore: unused_element + Future _onIgnoreButtonOnSentDialogPressed() async { + await commonTestCases.tapItemByKey('send_page_sent_dialog_ignore_button_key'); + } +} \ No newline at end of file diff --git a/integration_test/robots/setup_pin_code_robot.dart b/integration_test/robots/setup_pin_code_robot.dart new file mode 100644 index 000000000..0888aac30 --- /dev/null +++ b/integration_test/robots/setup_pin_code_robot.dart @@ -0,0 +1,28 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; +import 'pin_code_widget_robot.dart'; + +class SetupPinCodeRobot extends PinCodeWidgetRobot { + SetupPinCodeRobot(this.tester) + : commonTestCases = CommonTestCases(tester), + super(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isSetupPinCodePage() async { + await commonTestCases.isSpecificPage(); + } + + void hasTitle() { + commonTestCases.hasText(S.current.setup_pin); + } + + Future tapSuccessButton() async { + await commonTestCases.tapItemByKey('setup_pin_code_success_button_key'); + await commonTestCases.defaultSleepTime(); + } +} diff --git a/integration_test/robots/welcome_page_robot.dart b/integration_test/robots/welcome_page_robot.dart new file mode 100644 index 000000000..510f63556 --- /dev/null +++ b/integration_test/robots/welcome_page_robot.dart @@ -0,0 +1,40 @@ +import 'package:cake_wallet/src/screens/welcome/welcome_page.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../components/common_test_cases.dart'; + +class WelcomePageRobot { + WelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester); + + final WidgetTester tester; + late CommonTestCases commonTestCases; + + Future isWelcomePage() async { + await commonTestCases.isSpecificPage(); + } + + void confirmActionButtonsDisplay() { + final createNewWalletButton = find.byKey(ValueKey('welcome_page_create_new_wallet_button_key')); + + final restoreWalletButton = find.byKey(ValueKey('welcome_page_restore_wallet_button_key')); + + expect(createNewWalletButton, findsOneWidget); + expect(restoreWalletButton, findsOneWidget); + } + + Future navigateToCreateNewWalletPage() async { + await commonTestCases.tapItemByKey('welcome_page_create_new_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future navigateToRestoreWalletPage() async { + await commonTestCases.tapItemByKey('welcome_page_restore_wallet_button_key'); + await commonTestCases.defaultSleepTime(); + } + + Future backAndVerify() async { + await commonTestCases.goBack(); + await isWelcomePage(); + } +} diff --git a/integration_test/test_suites/exchange_flow_test.dart b/integration_test/test_suites/exchange_flow_test.dart new file mode 100644 index 000000000..6c993634c --- /dev/null +++ b/integration_test/test_suites/exchange_flow_test.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/auth_page_robot.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/exchange_confirm_page_robot.dart'; +import '../robots/exchange_page_robot.dart'; +import '../robots/exchange_trade_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + AuthPageRobot authPageRobot; + CommonTestFlows commonTestFlows; + ExchangePageRobot exchangePageRobot; + DashboardPageRobot dashboardPageRobot; + ExchangeTradePageRobot exchangeTradePageRobot; + ExchangeConfirmPageRobot exchangeConfirmPageRobot; + + group('Exchange Flow Tests', () { + testWidgets('Exchange flow', (tester) async { + authPageRobot = AuthPageRobot(tester); + commonTestFlows = CommonTestFlows(tester); + exchangePageRobot = ExchangePageRobot(tester); + dashboardPageRobot = DashboardPageRobot(tester); + exchangeTradePageRobot = ExchangeTradePageRobot(tester); + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester); + + await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key')); + await commonTestFlows.restoreWalletThroughSeedsFlow(); + await dashboardPageRobot.navigateToExchangePage(); + + // ----------- Exchange Page ------------- + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency); + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount); + await exchangePageRobot.enterDepositRefundAddress( + depositAddress: CommonTestConstants.testWalletAddress, + ); + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + + await exchangePageRobot.onExchangeButtonPressed(); + + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount); + + final onAuthPage = authPageRobot.onAuthPage(); + if (onAuthPage) { + await authPageRobot.enterPinCode(CommonTestConstants.pin, false); + } + + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed(); + await exchangeTradePageRobot.onGotItButtonPressed(); + }); + }); +} diff --git a/integration_test/test_suites/send_flow_test.dart b/integration_test/test_suites/send_flow_test.dart new file mode 100644 index 000000000..38ac1574f --- /dev/null +++ b/integration_test/test_suites/send_flow_test.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import '../components/common_test_constants.dart'; +import '../components/common_test_flows.dart'; +import '../robots/dashboard_page_robot.dart'; +import '../robots/send_page_robot.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + SendPageRobot sendPageRobot; + CommonTestFlows commonTestFlows; + DashboardPageRobot dashboardPageRobot; + + group('Send Flow Tests', () { + testWidgets('Send flow', (tester) async { + commonTestFlows = CommonTestFlows(tester); + sendPageRobot = SendPageRobot(tester: tester); + dashboardPageRobot = DashboardPageRobot(tester); + + await commonTestFlows.startAppFlow(ValueKey('send_test_app_key')); + await commonTestFlows.restoreWalletThroughSeedsFlow(); + await dashboardPageRobot.navigateToSendPage(); + + await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress); + await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency); + await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount); + await sendPageRobot.selectTransactionPriority(); + + await sendPageRobot.onSendButtonPressed(); + + await sendPageRobot.handleSendResult(); + + await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed(); + + await sendPageRobot.onSentDialogPopUp(); + }); + }); +} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 39dcd8b80..847769cba 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -66,6 +66,8 @@ PODS: - Toast - in_app_review (0.2.0): - Flutter + - integration_test (0.0.1): + - Flutter - MTBBarcodeScanner (5.0.11) - OrderedSet (5.0.0) - package_info_plus (0.4.5): @@ -120,6 +122,8 @@ DEPENDENCIES: - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`) + - integration_test (from `.symlinks/plugins/integration_test/ios`) + - package_info (from `.symlinks/plugins/package_info/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) @@ -174,6 +178,10 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/fluttertoast/ios" in_app_review: :path: ".symlinks/plugins/in_app_review/ios" + integration_test: + :path: ".symlinks/plugins/integration_test/ios" + package_info: + :path: ".symlinks/plugins/package_info/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: @@ -216,6 +224,7 @@ SPEC CHECKSUMS: flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d + integration_test: 13825b8a9334a850581300559b8839134b124670 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c diff --git a/lib/main.dart b/lib/main.dart index cae528210..32a6397c2 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -47,11 +47,11 @@ final navigatorKey = GlobalKey(); final rootKey = GlobalKey(); final RouteObserver> routeObserver = RouteObserver>(); -Future main() async { - await runAppWithZone(); +Future main({Key? topLevelKey}) async { + await runAppWithZone(topLevelKey: topLevelKey); } -Future runAppWithZone() async { +Future runAppWithZone({Key? topLevelKey}) async { bool isAppRunning = false; await runZonedGuarded(() async { @@ -67,7 +67,8 @@ Future runAppWithZone() async { }; await initializeAppAtRoot(); - runApp(App()); + runApp(App(key: topLevelKey)); + isAppRunning = true; }, (error, stackTrace) async { if (!isAppRunning) { @@ -236,6 +237,9 @@ Future initialSetup( } class App extends StatefulWidget { + App({this.key}); + + final Key? key; @override AppState createState() => AppState(); } @@ -264,7 +268,7 @@ class AppState extends State with SingleTickerProviderStateMixin { statusBarIconBrightness: statusBarIconBrightness)); return Root( - key: rootKey, + key: widget.key ?? rootKey, appStore: appStore, authenticationStore: authenticationStore, navigatorKey: navigatorKey, diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index ad6e68cd8..953463269 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -147,6 +147,7 @@ class _DashboardPageView extends BasePage { return Observer( builder: (context) { return ServicesUpdatesWidget( + key: ValueKey('dashboard_page_services_update_button_key'), dashboardViewModel.getServicesStatus(), enabled: dashboardViewModel.isEnabledBulletinAction, ); @@ -157,6 +158,7 @@ class _DashboardPageView extends BasePage { @override Widget middle(BuildContext context) { return SyncIndicator( + key: ValueKey('dashboard_page_sync_indicator_button_key'), dashboardViewModel: dashboardViewModel, onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync), ); @@ -173,6 +175,7 @@ class _DashboardPageView extends BasePage { alignment: Alignment.centerRight, width: 40, child: TextButton( + key: ValueKey('dashboard_page_wallet_menu_button_key'), // FIX-ME: Style //highlightColor: Colors.transparent, //splashColor: Colors.transparent, @@ -226,6 +229,7 @@ class _DashboardPageView extends BasePage { child: Observer( builder: (context) { return PageView.builder( + key: ValueKey('dashboard_page_view_key'), controller: controller, itemCount: pages.length, itemBuilder: (context, index) => pages[index], @@ -291,6 +295,8 @@ class _DashboardPageView extends BasePage { button: true, enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), child: ActionButton( + key: ValueKey( + 'dashboard_page_${action.name(context)}_action_button_key'), image: Image.asset( action.image, height: 24, diff --git a/lib/src/screens/dashboard/widgets/action_button.dart b/lib/src/screens/dashboard/widgets/action_button.dart index 23f5c2f93..49ebab3cd 100644 --- a/lib/src/screens/dashboard/widgets/action_button.dart +++ b/lib/src/screens/dashboard/widgets/action_button.dart @@ -2,13 +2,15 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; class ActionButton extends StatelessWidget { - ActionButton( - {required this.image, - required this.title, - this.route, - this.onClick, - this.alignment = Alignment.center, - this.textColor}); + ActionButton({ + required this.image, + required this.title, + this.route, + this.onClick, + this.alignment = Alignment.center, + this.textColor, + super.key, + }); final Image image; final String title; diff --git a/lib/src/screens/dashboard/widgets/sync_indicator.dart b/lib/src/screens/dashboard/widgets/sync_indicator.dart index 52e596a83..27b3d0109 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator.dart @@ -7,7 +7,11 @@ import 'package:cw_core/sync_status.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; class SyncIndicator extends StatelessWidget { - SyncIndicator({required this.dashboardViewModel, required this.onTap}); + SyncIndicator({ + required this.dashboardViewModel, + required this.onTap, + super.key, + }); final DashboardViewModel dashboardViewModel; final Function() onTap; diff --git a/lib/src/screens/disclaimer/disclaimer_page.dart b/lib/src/screens/disclaimer/disclaimer_page.dart index f82a9efbe..c9d959b40 100644 --- a/lib/src/screens/disclaimer/disclaimer_page.dart +++ b/lib/src/screens/disclaimer/disclaimer_page.dart @@ -207,6 +207,7 @@ class DisclaimerBodyState extends State { padding: EdgeInsets.only( left: 24.0, top: 10.0, right: 24.0, bottom: 10.0), child: InkWell( + key: ValueKey('disclaimer_check_key'), onTap: () { setState(() { _checked = !_checked; @@ -230,6 +231,7 @@ class DisclaimerBodyState extends State { color: Theme.of(context).colorScheme.background), child: _checked ? Icon( + key: ValueKey('disclaimer_check_icon_key'), Icons.check, color: Colors.blue, size: 20.0, @@ -253,6 +255,7 @@ class DisclaimerBodyState extends State { padding: EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0), child: PrimaryButton( + key: ValueKey('disclaimer_accept_button_key'), onPressed: _checked ? () => Navigator.of(context) .popAndPushNamed(Routes.welcome) diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 2c717a3c8..78b4d0db8 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -228,6 +228,7 @@ class ExchangePage extends BasePage { ), Observer( builder: (_) => LoadingPrimaryButton( + key: ValueKey('exchange_page_exchange_button_key'), text: S.of(context).exchange, onPressed: () { if (_formKey.currentState != null && @@ -430,6 +431,8 @@ class ExchangePage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + key: ValueKey('exchange_page_trade_creation_failure_dialog_key'), + buttonKey: ValueKey('exchange_page_trade_creation_failure_dialog_button_key'), alertTitle: S.of(context).provider_error(state.title), alertContent: state.error, buttonText: S.of(context).ok, @@ -612,6 +615,7 @@ class ExchangePage extends BasePage { Widget _exchangeCardsSection(BuildContext context) { final firstExchangeCard = Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'deposit_exchange_card', onDispose: disposeBestRateSync, hasAllAmount: exchangeViewModel.hasAllAmount, allAmount: exchangeViewModel.hasAllAmount @@ -681,6 +685,7 @@ class ExchangePage extends BasePage { final secondExchangeCard = Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'receive_exchange_card', onDispose: disposeBestRateSync, amountFocusNode: _receiveAmountFocus, addressFocusNode: _receiveAddressFocus, diff --git a/lib/src/screens/exchange/exchange_template_page.dart b/lib/src/screens/exchange/exchange_template_page.dart index d24c91dad..4edc9095a 100644 --- a/lib/src/screens/exchange/exchange_template_page.dart +++ b/lib/src/screens/exchange/exchange_template_page.dart @@ -121,6 +121,7 @@ class ExchangeTemplatePage extends BasePage { padding: EdgeInsets.fromLTRB(24, 100, 24, 32), child: Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'deposit_exchange_template_card', amountFocusNode: _depositAmountFocus, key: depositKey, title: S.of(context).you_will_send, @@ -157,6 +158,7 @@ class ExchangeTemplatePage extends BasePage { padding: EdgeInsets.only(top: 29, left: 24, right: 24), child: Observer( builder: (_) => ExchangeCard( + cardInstanceName: 'receive_exchange_template_card', amountFocusNode: _receiveAmountFocus, key: receiveKey, title: S.of(context).you_will_get, diff --git a/lib/src/screens/exchange/widgets/currency_picker.dart b/lib/src/screens/exchange/widgets/currency_picker.dart index 0fe1d4e67..8c6f5e214 100644 --- a/lib/src/screens/exchange/widgets/currency_picker.dart +++ b/lib/src/screens/exchange/widgets/currency_picker.dart @@ -12,7 +12,8 @@ class CurrencyPicker extends StatefulWidget { this.title, this.hintText, this.isMoneroWallet = false, - this.isConvertFrom = false}); + this.isConvertFrom = false, + super.key}); final int selectedAtIndex; final List items; diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index 02218f848..75a2eadd7 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -19,34 +19,35 @@ import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; class ExchangeCard extends StatefulWidget { - ExchangeCard( - {Key? key, - required this.initialCurrency, - required this.initialAddress, - required this.initialWalletName, - required this.initialIsAmountEditable, - required this.isAmountEstimated, - required this.currencies, - required this.onCurrencySelected, - this.imageArrow, - this.currencyValueValidator, - this.addressTextFieldValidator, - this.title = '', - this.initialIsAddressEditable = true, - this.hasRefundAddress = false, - this.isMoneroWallet = false, - this.currencyButtonColor = Colors.transparent, - this.addressButtonsColor = Colors.transparent, - this.borderColor = Colors.transparent, - this.hasAllAmount = false, - this.isAllAmountEnabled = false, - this.amountFocusNode, - this.addressFocusNode, - this.allAmount, - this.onPushPasteButton, - this.onPushAddressBookButton, - this.onDispose}) - : super(key: key); + ExchangeCard({ + Key? key, + required this.initialCurrency, + required this.initialAddress, + required this.initialWalletName, + required this.initialIsAmountEditable, + required this.isAmountEstimated, + required this.currencies, + required this.onCurrencySelected, + this.imageArrow, + this.currencyValueValidator, + this.addressTextFieldValidator, + this.title = '', + this.initialIsAddressEditable = true, + this.hasRefundAddress = false, + this.isMoneroWallet = false, + this.currencyButtonColor = Colors.transparent, + this.addressButtonsColor = Colors.transparent, + this.borderColor = Colors.transparent, + this.hasAllAmount = false, + this.isAllAmountEnabled = false, + this.amountFocusNode, + this.addressFocusNode, + this.allAmount, + this.onPushPasteButton, + this.onPushAddressBookButton, + this.onDispose, + required this.cardInstanceName, + }) : super(key: key); final List currencies; final Function(CryptoCurrency) onCurrencySelected; @@ -74,6 +75,7 @@ class ExchangeCard extends StatefulWidget { final void Function(BuildContext context)? onPushPasteButton; final void Function(BuildContext context)? onPushAddressBookButton; final Function()? onDispose; + final String cardInstanceName; @override ExchangeCardState createState() => ExchangeCardState(); @@ -89,11 +91,13 @@ class ExchangeCardState extends State { _walletName = '', _selectedCurrency = CryptoCurrency.btc, _isAmountEstimated = false, - _isMoneroWallet = false; + _isMoneroWallet = false, + _cardInstanceName = ''; final addressController = TextEditingController(); final amountController = TextEditingController(); + String _cardInstanceName; String _title; String? _min; String? _max; @@ -106,6 +110,7 @@ class ExchangeCardState extends State { @override void initState() { + _cardInstanceName = widget.cardInstanceName; _title = widget.title; _isAmountEditable = widget.initialIsAmountEditable; _isAddressEditable = widget.initialIsAddressEditable; @@ -184,6 +189,7 @@ class ExchangeCardState extends State { mainAxisAlignment: MainAxisAlignment.start, children: [ Text( + key: ValueKey('${_cardInstanceName}_title_key'), _title, style: TextStyle( fontSize: 18, @@ -193,17 +199,26 @@ class ExchangeCardState extends State { ], ), CurrencyAmountTextField( - imageArrow: widget.imageArrow, - selectedCurrency: _selectedCurrency.toString(), - amountFocusNode: widget.amountFocusNode, - amountController: amountController, - onTapPicker: () => _presentPicker(context), - isAmountEditable: _isAmountEditable, - isPickerEnable: true, - allAmountButton: widget.hasAllAmount, - currencyValueValidator: widget.currencyValueValidator, - tag: _selectedCurrency.tag, - allAmountCallback: widget.allAmount), + currencyPickerButtonKey: ValueKey('${_cardInstanceName}_currency_picker_button_key'), + selectedCurrencyTextKey: ValueKey('${_cardInstanceName}_selected_currency_text_key'), + selectedCurrencyTagTextKey: + ValueKey('${_cardInstanceName}_selected_currency_tag_text_key'), + amountTextfieldKey: ValueKey('${_cardInstanceName}_amount_textfield_key'), + sendAllButtonKey: ValueKey('${_cardInstanceName}_send_all_button_key'), + currencyAmountTextFieldWidgetKey: + ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'), + imageArrow: widget.imageArrow, + selectedCurrency: _selectedCurrency.toString(), + amountFocusNode: widget.amountFocusNode, + amountController: amountController, + onTapPicker: () => _presentPicker(context), + isAmountEditable: _isAmountEditable, + isPickerEnable: true, + allAmountButton: widget.hasAllAmount, + currencyValueValidator: widget.currencyValueValidator, + tag: _selectedCurrency.tag, + allAmountCallback: widget.allAmount, + ), Divider(height: 1, color: Theme.of(context).extension()!.textFieldHintColor), Padding( padding: EdgeInsets.only(top: 5), @@ -212,6 +227,7 @@ class ExchangeCardState extends State { child: Row(mainAxisAlignment: MainAxisAlignment.start, children: [ _min != null ? Text( + key: ValueKey('${_cardInstanceName}_min_limit_text_key'), S.of(context).min_value(_min ?? '', _selectedCurrency.toString()), style: TextStyle( fontSize: 10, @@ -221,11 +237,15 @@ class ExchangeCardState extends State { : Offstage(), _min != null ? SizedBox(width: 10) : Offstage(), _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( - fontSize: 10, - height: 1.2, - color: Theme.of(context).extension()!.hintTextColor)) + fontSize: 10, + height: 1.2, + color: Theme.of(context).extension()!.hintTextColor, + ), + ) : Offstage(), ])), ), @@ -246,6 +266,7 @@ class ExchangeCardState extends State { child: Padding( padding: EdgeInsets.only(top: 20), child: AddressTextField( + addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'), focusNode: widget.addressFocusNode, controller: addressController, onURIScanned: (uri) { @@ -286,6 +307,8 @@ class ExchangeCardState extends State { FocusTraversalOrder( order: NumericFocusOrder(3), child: BaseTextFormField( + key: ValueKey( + '${_cardInstanceName}_non_editable_address_textfield_key'), controller: addressController, borderColor: Colors.transparent, suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36), @@ -309,6 +332,8 @@ class ExchangeCardState extends State { child: Semantics( label: S.of(context).address_book, child: InkWell( + key: ValueKey( + '${_cardInstanceName}_address_book_button_key'), onTap: () async { final contact = await Navigator.of(context).pushNamed( @@ -346,6 +371,8 @@ class ExchangeCardState extends State { child: Semantics( label: S.of(context).copy_address, child: InkWell( + key: ValueKey( + '${_cardInstanceName}_copy_refund_address_button_key'), onTap: () { Clipboard.setData( ClipboardData(text: addressController.text)); @@ -369,6 +396,7 @@ class ExchangeCardState extends State { showPopUp( context: context, builder: (_) => CurrencyPicker( + key: ValueKey('${_cardInstanceName}_currency_picker_dialog_button_key'), selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), items: widget.currencies, hintText: S.of(context).search_currency, diff --git a/lib/src/screens/exchange_trade/exchange_confirm_page.dart b/lib/src/screens/exchange_trade/exchange_confirm_page.dart index bf307dce6..a179ee473 100644 --- a/lib/src/screens/exchange_trade/exchange_confirm_page.dart +++ b/lib/src/screens/exchange_trade/exchange_confirm_page.dart @@ -83,6 +83,7 @@ class ExchangeConfirmPage extends BasePage { padding: EdgeInsets.fromLTRB(10, 0, 10, 10), child: Builder( builder: (context) => PrimaryButton( + key: ValueKey('exchange_confirm_page_copy_to_clipboard_button_key'), onPressed: () { Clipboard.setData(ClipboardData(text: trade.id)); showBar( @@ -117,6 +118,7 @@ class ExchangeConfirmPage extends BasePage { ], )), PrimaryButton( + key: ValueKey('exchange_confirm_page_saved_id_button_key'), onPressed: () => Navigator.of(context) .pushReplacementNamed(Routes.exchangeTrade), text: S.of(context).saved_the_trade_id, diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 0766a4562..0f3cc7bd9 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -39,7 +39,9 @@ void showInformation( showPopUp( context: context, - builder: (_) => InformationPage(information: information)); + builder: (_) => InformationPage( + key: ValueKey('information_page_dialog_key'), + information: information)); } class ExchangeTradePage extends BasePage { @@ -215,6 +217,7 @@ class ExchangeTradeState extends State { return widget.exchangeTradeViewModel.isSendable && !(sendingState is TransactionCommitted) ? LoadingPrimaryButton( + key: ValueKey('exchange_trade_page_confirm_sending_button_key'), isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty, isLoading: sendingState is IsExecutingState, @@ -241,6 +244,8 @@ class ExchangeTradeState extends State { context: context, builder: (BuildContext popupContext) { return AlertWithOneAction( + key: ValueKey('exchange_trade_page_send_failure_dialog_key'), + buttonKey: ValueKey('exchange_trade_page_send_failure_dialog_button_key'), alertTitle: S.of(popupContext).error, alertContent: state.error, buttonText: S.of(popupContext).ok, @@ -255,6 +260,10 @@ class ExchangeTradeState extends State { context: context, builder: (BuildContext popupContext) { return ConfirmSendingAlert( + key: ValueKey('exchange_trade_page_confirm_sending_dialog_key'), + alertLeftActionButtonKey: ValueKey('exchange_trade_page_confirm_sending_dialog_cancel_button_key'), + alertRightActionButtonKey: + ValueKey('exchange_trade_page_confirm_sending_dialog_send_button_key'), alertTitle: S.of(popupContext).confirm_sending, amount: S.of(popupContext).send_amount, amountValue: widget.exchangeTradeViewModel.sendViewModel diff --git a/lib/src/screens/exchange_trade/information_page.dart b/lib/src/screens/exchange_trade/information_page.dart index eed124b97..114a4824d 100644 --- a/lib/src/screens/exchange_trade/information_page.dart +++ b/lib/src/screens/exchange_trade/information_page.dart @@ -10,7 +10,7 @@ import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart'; class InformationPage extends StatelessWidget { - InformationPage({required this.information}); + InformationPage({required this.information, super.key}); final String information; @@ -47,6 +47,7 @@ class InformationPage extends StatelessWidget { Padding( padding: EdgeInsets.fromLTRB(10, 0, 10, 10), child: PrimaryButton( + key: ValueKey('information_page_got_it_button_key'), onPressed: () => Navigator.of(context).pop(), text: S.of(context).got_it, color: Theme.of(context).extension()!.buttonBackgroundColor, diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index faef4b479..6cf21ae58 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -131,6 +131,7 @@ class WalletTypeFormState extends State { Expanded( child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + scrollableKey: ValueKey('new_wallet_type_scrollable_key'), content: Column( crossAxisAlignment: CrossAxisAlignment.center, children: [ @@ -138,6 +139,7 @@ class WalletTypeFormState extends State { (type) => Padding( padding: EdgeInsets.only(top: 12), child: SelectButton( + key: ValueKey('new_wallet_type_${type.name}_button_key'), image: Image.asset( walletTypeToCryptoCurrency(type).iconPath ?? '', height: 24, @@ -158,6 +160,7 @@ class WalletTypeFormState extends State { ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: PrimaryButton( + key: ValueKey('new_wallet_type_next_button_key'), onPressed: () => onTypeSelected(), text: S.of(context).seed_language_next, color: Theme.of(context).primaryColor, diff --git a/lib/src/screens/new_wallet/widgets/select_button.dart b/lib/src/screens/new_wallet/widgets/select_button.dart index 834d85986..87015b89e 100644 --- a/lib/src/screens/new_wallet/widgets/select_button.dart +++ b/lib/src/screens/new_wallet/widgets/select_button.dart @@ -20,6 +20,7 @@ class SelectButton extends StatelessWidget { this.deviceConnectionTypes, this.borderRadius, this.padding, + super.key, }); final Widget? image; diff --git a/lib/src/screens/pin_code/pin_code_widget.dart b/lib/src/screens/pin_code/pin_code_widget.dart index 36328aee2..d39c88cc9 100644 --- a/lib/src/screens/pin_code/pin_code_widget.dart +++ b/lib/src/screens/pin_code/pin_code_widget.dart @@ -240,6 +240,7 @@ class PinCodeState extends State { return Container( margin: EdgeInsets.only(left: marginLeft, right: marginRight), child: TextButton( + key: ValueKey('pin_code_button_${index}_key'), onPressed: () => _push(index), style: TextButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.background, diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index ce3de9a6c..382c53f9d 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -24,8 +24,20 @@ class CurrencyAmountTextField extends StatelessWidget { this.tagBackgroundColor, this.currencyValueValidator, this.allAmountCallback, - }); + this.sendAllButtonKey, + this.amountTextfieldKey, + this.currencyPickerButtonKey, + this.selectedCurrencyTextKey, + this.selectedCurrencyTagTextKey, + this.currencyAmountTextFieldWidgetKey, + }) : super(key: currencyAmountTextFieldWidgetKey); + final Key? sendAllButtonKey; + final Key? amountTextfieldKey; + final Key? currencyPickerButtonKey; + final Key? selectedCurrencyTextKey; + final Key? selectedCurrencyTagTextKey; + final Key? currencyAmountTextFieldWidgetKey; final Widget? imageArrow; final String selectedCurrency; final String? tag; @@ -54,6 +66,7 @@ class CurrencyAmountTextField extends StatelessWidget { ? Container( height: 32, child: InkWell( + key: currencyPickerButtonKey, onTap: onTapPicker, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -65,6 +78,7 @@ class CurrencyAmountTextField extends StatelessWidget { Image.asset('assets/images/arrow_bottom_purple_icon.png', color: textColor, height: 8)), Text( + key: selectedCurrencyTextKey, selectedCurrency, style: TextStyle( fontWeight: FontWeight.w600, @@ -77,6 +91,7 @@ class CurrencyAmountTextField extends StatelessWidget { ), ) : Text( + key: selectedCurrencyTextKey, selectedCurrency, style: TextStyle( fontWeight: FontWeight.w600, @@ -98,6 +113,7 @@ class CurrencyAmountTextField extends StatelessWidget { child: Padding( padding: const EdgeInsets.all(6.0), child: Text( + key: selectedCurrencyTagTextKey, tag!, style: TextStyle( fontSize: 12, @@ -132,9 +148,9 @@ class CurrencyAmountTextField extends StatelessWidget { padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), margin: const EdgeInsets.only(right: 3), decoration: BoxDecoration( - border: Border.all( - color: textColor, - ), + border: Border.all( + color: textColor, + ), borderRadius: BorderRadius.circular(26), color: Theme.of(context).primaryColor)) : _prefixContent, @@ -146,6 +162,7 @@ class CurrencyAmountTextField extends StatelessWidget { child: FocusTraversalOrder( order: NumericFocusOrder(1), child: BaseTextFormField( + key: amountTextfieldKey, focusNode: amountFocusNode, controller: amountController, enabled: isAmountEditable, @@ -184,6 +201,7 @@ class CurrencyAmountTextField extends StatelessWidget { borderRadius: const BorderRadius.all(Radius.circular(6)), ), child: InkWell( + key: sendAllButtonKey, onTap: allAmountCallback, child: Center( child: Text( diff --git a/lib/src/screens/restore/restore_options_page.dart b/lib/src/screens/restore/restore_options_page.dart index a703c9f9e..cb5086fe1 100644 --- a/lib/src/screens/restore/restore_options_page.dart +++ b/lib/src/screens/restore/restore_options_page.dart @@ -59,8 +59,12 @@ class RestoreOptionsPage extends BasePage { child: Column( children: [ OptionTile( - onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromSeedKeys, - arguments: isNewInstall), + key: ValueKey('restore_options_from_seeds_button_key'), + onPressed: () => Navigator.pushNamed( + context, + Routes.restoreWalletFromSeedKeys, + arguments: isNewInstall, + ), image: imageSeedKeys, title: S.of(context).restore_title_from_seed_keys, description: S.of(context).restore_description_from_seed_keys, @@ -69,6 +73,7 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( + key: ValueKey('restore_options_from_backup_button_key'), onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), image: imageBackup, title: S.of(context).restore_title_from_backup, @@ -79,6 +84,7 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( + key: ValueKey('restore_options_from_hardware_wallet_button_key'), onPressed: () => Navigator.pushNamed( context, Routes.restoreWalletFromHardwareWallet, arguments: isNewInstall), @@ -90,10 +96,12 @@ class RestoreOptionsPage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: OptionTile( - onPressed: () => _onScanQRCode(context), - image: qrCode, - title: S.of(context).scan_qr_code, - description: S.of(context).cold_or_recover_wallet), + key: ValueKey('restore_options_from_qr_button_key'), + onPressed: () => _onScanQRCode(context), + image: qrCode, + title: S.of(context).scan_qr_code, + description: S.of(context).cold_or_recover_wallet, + ), ) ], ), diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 56e49b087..83772f866 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -112,10 +112,12 @@ class WalletRestoreFromKeysFromState extends State { alignment: Alignment.centerRight, children: [ BaseTextFormField( + key: ValueKey('wallet_restore_from_keys_wallet_name_textfield_key'), controller: nameTextEditingController, hintText: S.of(context).wallet_name, validator: WalletNameValidator(), suffixIcon: IconButton( + key: ValueKey('wallet_restore_from_keys_wallet_name_refresh_button_key'), onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); @@ -175,6 +177,7 @@ class WalletRestoreFromKeysFromState extends State { bool nanoBased = widget.walletRestoreViewModel.type == WalletType.nano || widget.walletRestoreViewModel.type == WalletType.banano; return AddressTextField( + addressKey: ValueKey('wallet_restore_from_key_private_key_textfield_key'), controller: privateKeyController, placeholder: nanoBased ? S.of(context).seed_hex_form : S.of(context).private_key, options: [AddressTextFieldOption.paste], diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index f295aab13..67576144c 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -151,11 +151,13 @@ class WalletRestoreFromSeedFormState extends State { alignment: Alignment.centerRight, children: [ BaseTextFormField( + key: ValueKey('wallet_restore_from_seed_wallet_name_textfield_key'), controller: nameTextEditingController, hintText: S .of(context) .wallet_name, suffixIcon: IconButton( + key: ValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'), onPressed: () async { final rName = await generateName(); FocusManager.instance.primaryFocus?.unfocus(); @@ -190,10 +192,13 @@ class WalletRestoreFromSeedFormState extends State { )), Container(height: 20), SeedWidget( - key: seedWidgetStateKey, - language: language, - type: widget.type, - onSeedChange: onSeedChange), + key: seedWidgetStateKey, + language: language, + type: widget.type, + onSeedChange: onSeedChange, + seedTextFieldKey: ValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'), + pasteButtonKey: ValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'), + ), if (widget.type == WalletType.monero || widget.type == WalletType.wownero) GestureDetector( onTap: () async { diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index a855088e4..c8e109860 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -213,6 +213,7 @@ class WalletRestorePage extends BasePage { Observer( builder: (context) { return LoadingPrimaryButton( + key: ValueKey('wallet_restore_seed_or_key_restore_button_key'), onPressed: () async { await _confirmForm(context); }, @@ -230,6 +231,7 @@ class WalletRestorePage extends BasePage { ), const SizedBox(height: 25), GestureDetector( + key: ValueKey('wallet_restore_advanced_settings_button_key'), onTap: () { Navigator.of(context) .pushNamed(Routes.advancedPrivacySettings, arguments: { diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 3e906048d..ccf4a1dc4 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -250,6 +250,7 @@ class SendPage extends BasePage { return Row( children: [ AddTemplateButton( + key: ValueKey('send_page_add_template_button_key'), onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate), currentTemplatesLength: templates.length, ), @@ -339,19 +340,22 @@ class SendPage extends BasePage { children: [ if (sendViewModel.hasCurrecyChanger) Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - onPressed: () => presentCurrencyPicker(context), - text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', - color: Colors.transparent, - textColor: - Theme.of(context).extension()!.hintTextColor, - ))), + builder: (_) => Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + key: ValueKey('send_page_change_asset_button_key'), + onPressed: () => presentCurrencyPicker(context), + text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', + color: Colors.transparent, + textColor: Theme.of(context).extension()!.hintTextColor, + ), + ), + ), if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) Padding( padding: EdgeInsets.only(bottom: 12), child: PrimaryButton( + key: ValueKey('send_page_add_receiver_button_key'), onPressed: () { sendViewModel.addOutput(); Future.delayed(const Duration(milliseconds: 250), () { @@ -368,6 +372,7 @@ class SendPage extends BasePage { Observer( builder: (_) { return LoadingPrimaryButton( + key: ValueKey('send_page_send_button_key'), onPressed: () async { if (sendViewModel.state is IsExecutingState) return; if (_formKey.currentState != null && !_formKey.currentState!.validate()) { @@ -451,6 +456,8 @@ class SendPage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + key: ValueKey('send_page_send_failure_dialog_key'), + buttonKey: ValueKey('send_page_send_failure_dialog_button_key'), alertTitle: S.of(context).error, alertContent: state.error, buttonText: S.of(context).ok, @@ -466,6 +473,7 @@ class SendPage extends BasePage { context: context, builder: (BuildContext _dialogContext) { return ConfirmSendingAlert( + key: ValueKey('send_page_confirm_sending_dialog_key'), alertTitle: S.of(_dialogContext).confirm_sending, amount: S.of(_dialogContext).send_amount, amountValue: sendViewModel.pendingTransaction!.amountFormatted, @@ -480,6 +488,10 @@ class SendPage extends BasePage { change: sendViewModel.pendingTransaction!.change, rightButtonText: S.of(_dialogContext).send, leftButtonText: S.of(_dialogContext).cancel, + alertRightActionButtonKey: + ValueKey('send_page_confirm_sending_dialog_send_button_key'), + alertLeftActionButtonKey: + ValueKey('send_page_confirm_sending_dialog_cancel_button_key'), actionRightButton: () async { Navigator.of(_dialogContext).pop(); sendViewModel.commitTransaction(); @@ -513,10 +525,15 @@ class SendPage extends BasePage { if (newContactAddress != null) { return AlertWithTwoActions( + alertDialogKey: ValueKey('send_page_sent_dialog_key'), alertTitle: '', alertContent: alertContent, rightButtonText: S.of(_dialogContext).add_contact, leftButtonText: S.of(_dialogContext).ignor, + alertLeftActionButtonKey: + ValueKey('send_page_sent_dialog_ignore_button_key'), + alertRightActionButtonKey: ValueKey( + 'send_page_sent_dialog_add_contact_button_key'), actionRightButton: () { Navigator.of(_dialogContext).pop(); RequestReviewHandler.requestReview(); diff --git a/lib/src/screens/send/widgets/confirm_sending_alert.dart b/lib/src/screens/send/widgets/confirm_sending_alert.dart index c7b6d3407..83866a73c 100644 --- a/lib/src/screens/send/widgets/confirm_sending_alert.dart +++ b/lib/src/screens/send/widgets/confirm_sending_alert.dart @@ -9,30 +9,34 @@ import 'package:cake_wallet/src/widgets/cake_scrollbar.dart'; import 'package:flutter/scheduler.dart'; class ConfirmSendingAlert extends BaseAlertDialog { - ConfirmSendingAlert( - {required this.alertTitle, - this.paymentId, - this.paymentIdValue, - this.expirationTime, - required this.amount, - required this.amountValue, - required this.fiatAmountValue, - required this.fee, - this.feeRate, - required this.feeValue, - required this.feeFiatAmount, - required this.outputs, - this.change, - required this.leftButtonText, - required this.rightButtonText, - required this.actionLeftButton, - required this.actionRightButton, - this.alertBarrierDismissible = true, - this.alertLeftActionButtonTextColor, - this.alertRightActionButtonTextColor, - this.alertLeftActionButtonColor, - this.alertRightActionButtonColor, - this.onDispose}); + ConfirmSendingAlert({ + required this.alertTitle, + this.paymentId, + this.paymentIdValue, + this.expirationTime, + required this.amount, + required this.amountValue, + required this.fiatAmountValue, + required this.fee, + this.feeRate, + required this.feeValue, + required this.feeFiatAmount, + required this.outputs, + this.change, + required this.leftButtonText, + required this.rightButtonText, + required this.actionLeftButton, + required this.actionRightButton, + this.alertBarrierDismissible = true, + this.alertLeftActionButtonTextColor, + this.alertRightActionButtonTextColor, + this.alertLeftActionButtonColor, + this.alertRightActionButtonColor, + this.onDispose, + this.alertLeftActionButtonKey, + this.alertRightActionButtonKey, + Key? key, + }); final String alertTitle; final String? paymentId; @@ -57,6 +61,8 @@ class ConfirmSendingAlert extends BaseAlertDialog { final Color? alertLeftActionButtonColor; final Color? alertRightActionButtonColor; final Function? onDispose; + final Key? alertRightActionButtonKey; + final Key? alertLeftActionButtonKey; @override String get titleText => alertTitle; @@ -91,6 +97,12 @@ class ConfirmSendingAlert extends BaseAlertDialog { @override Color? get rightActionButtonColor => alertRightActionButtonColor; + @override + Key? get leftActionButtonKey => alertLeftActionButtonKey; + + @override + Key? get rightActionButtonKey => alertLeftActionButtonKey; + @override Widget content(BuildContext context) => ConfirmSendingAlertContent( paymentId: paymentId, @@ -288,6 +300,7 @@ class ConfirmSendingAlertContentState extends State crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( + key: ValueKey('confirm_sending_dialog_amount_text_value_key'), amountValue, style: TextStyle( fontSize: 18, diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 214d162ed..2a14da305 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -158,6 +158,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin _presentPicker(context), isPickerEnable: sendViewModel.hasMultipleTokens, tag: sendViewModel.selectedCryptoCurrency.tag, - allAmountButton: !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, + allAmountButton: + !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, currencyValueValidator: output.sendAll ? sendViewModel.allAmountValidator : sendViewModel.amountValidator, @@ -257,6 +264,9 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin with AutomaticKeepAliveClientMixin GestureDetector( + key: ValueKey('send_page_select_fee_priority_button_key'), onTap: sendViewModel.hasFeesPriority ? () => pickTransactionPriority(context) : () {}, @@ -360,6 +372,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin Navigator.of(context).pushNamed(Routes.unspentCoinsList), child: Container( color: Colors.transparent, @@ -544,11 +557,13 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin( context: context, builder: (_) => CurrencyPicker( - selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), - items: sendViewModel.currencies, - hintText: S.of(context).search_currency, - onItemSelected: (Currency cur) => - sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency)), + key: ValueKey('send_page_currency_picker_dialog_button_key'), + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + items: sendViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) => + sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency), + ), ); } diff --git a/lib/src/screens/setup_pin_code/setup_pin_code.dart b/lib/src/screens/setup_pin_code/setup_pin_code.dart index 833fd9b60..d78a2df63 100644 --- a/lib/src/screens/setup_pin_code/setup_pin_code.dart +++ b/lib/src/screens/setup_pin_code/setup_pin_code.dart @@ -52,6 +52,7 @@ class SetupPinCodePage extends BasePage { context: context, builder: (BuildContext context) { return AlertWithOneAction( + buttonKey: ValueKey('setup_pin_code_success_button_key'), alertTitle: S.current.setup_pin, alertContent: S.of(context).setup_successful, buttonText: S.of(context).ok, diff --git a/lib/src/screens/welcome/welcome_page.dart b/lib/src/screens/welcome/welcome_page.dart index defc8e2c8..5b7b7f06d 100644 --- a/lib/src/screens/welcome/welcome_page.dart +++ b/lib/src/screens/welcome/welcome_page.dart @@ -133,6 +133,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 24), child: PrimaryImageButton( + key: ValueKey('welcome_page_create_new_wallet_button_key'), onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome), image: newWalletImage, text: S.of(context).create_new, @@ -146,6 +147,7 @@ class WelcomePage extends BasePage { Padding( padding: EdgeInsets.only(top: 10), child: PrimaryImageButton( + key: ValueKey('welcome_page_restore_wallet_button_key'), onPressed: () { Navigator.pushNamed(context, Routes.restoreOptions, arguments: true); }, diff --git a/lib/src/widgets/address_text_field.dart b/lib/src/widgets/address_text_field.dart index f229ea8ef..0b1ef4796 100644 --- a/lib/src/widgets/address_text_field.dart +++ b/lib/src/widgets/address_text_field.dart @@ -15,28 +15,27 @@ import 'package:permission_handler/permission_handler.dart'; enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses } class AddressTextField extends StatelessWidget { - AddressTextField( - {required this.controller, - this.isActive = true, - this.placeholder, - this.options = const [ - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook - ], - this.onURIScanned, - this.focusNode, - this.isBorderExist = true, - this.buttonColor, - this.borderColor, - this.iconColor, - this.textStyle, - this.hintStyle, - this.validator, - this.onPushPasteButton, - this.onPushAddressBookButton, - this.onPushAddressPickerButton, - this.onSelectedContact, - this.selectedCurrency}); + AddressTextField({ + required this.controller, + this.isActive = true, + this.placeholder, + this.options = const [AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook], + this.onURIScanned, + this.focusNode, + this.isBorderExist = true, + this.buttonColor, + this.borderColor, + this.iconColor, + this.textStyle, + this.hintStyle, + this.validator, + this.onPushPasteButton, + this.onPushAddressBookButton, + this.onPushAddressPickerButton, + this.onSelectedContact, + this.selectedCurrency, + this.addressKey, + }); static const prefixIconWidth = 34.0; static const prefixIconHeight = 34.0; @@ -60,12 +59,14 @@ class AddressTextField extends StatelessWidget { final Function(BuildContext context)? onPushAddressPickerButton; final Function(ContactBase contact)? onSelectedContact; final CryptoCurrency? selectedCurrency; + final Key? addressKey; @override Widget build(BuildContext context) { return Stack( children: [ TextFormField( + key: addressKey, enableIMEPersonalizedLearning: false, keyboardType: TextInputType.visiblePassword, onFieldSubmitted: (_) => FocusScope.of(context).unfocus(), diff --git a/lib/src/widgets/alert_close_button.dart b/lib/src/widgets/alert_close_button.dart index e3ff037a9..6ef0bdaa5 100644 --- a/lib/src/widgets/alert_close_button.dart +++ b/lib/src/widgets/alert_close_button.dart @@ -3,7 +3,12 @@ import 'package:cake_wallet/palette.dart'; import 'package:flutter/material.dart'; class AlertCloseButton extends StatelessWidget { - AlertCloseButton({this.image, this.bottom, this.onTap}); + AlertCloseButton({ + this.image, + this.bottom, + this.onTap, + super.key, + }); final VoidCallback? onTap; diff --git a/lib/src/widgets/alert_with_one_action.dart b/lib/src/widgets/alert_with_one_action.dart index 7ad0ac1af..6f0ba5e8b 100644 --- a/lib/src/widgets/alert_with_one_action.dart +++ b/lib/src/widgets/alert_with_one_action.dart @@ -9,7 +9,9 @@ class AlertWithOneAction extends BaseAlertDialog { required this.buttonAction, this.alertBarrierDismissible = true, this.headerTitleText, - this.headerImageProfileUrl + this.headerImageProfileUrl, + this.buttonKey, + Key? key, }); final String alertTitle; @@ -19,6 +21,7 @@ class AlertWithOneAction extends BaseAlertDialog { final bool alertBarrierDismissible; final String? headerTitleText; final String? headerImageProfileUrl; + final Key? buttonKey; @override String get titleText => alertTitle; @@ -45,6 +48,7 @@ class AlertWithOneAction extends BaseAlertDialog { child: ButtonTheme( minWidth: double.infinity, child: TextButton( + key: buttonKey, onPressed: buttonAction, // FIX-ME: Style //highlightColor: Colors.transparent, @@ -62,4 +66,4 @@ class AlertWithOneAction extends BaseAlertDialog { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/alert_with_two_actions.dart b/lib/src/widgets/alert_with_two_actions.dart index ddb11c3ee..e3d4408a6 100644 --- a/lib/src/widgets/alert_with_two_actions.dart +++ b/lib/src/widgets/alert_with_two_actions.dart @@ -14,6 +14,9 @@ class AlertWithTwoActions extends BaseAlertDialog { this.isDividerExist = false, // this.leftActionColor, // this.rightActionColor, + this.alertRightActionButtonKey, + this.alertLeftActionButtonKey, + this.alertDialogKey, }); final String alertTitle; @@ -26,6 +29,9 @@ class AlertWithTwoActions extends BaseAlertDialog { // final Color leftActionColor; // final Color rightActionColor; final bool isDividerExist; + final Key? alertRightActionButtonKey; + final Key? alertLeftActionButtonKey; + final Key? alertDialogKey; @override String get titleText => alertTitle; @@ -47,4 +53,13 @@ class AlertWithTwoActions extends BaseAlertDialog { // Color get rightButtonColor => rightActionColor; @override bool get isDividerExists => isDividerExist; + + @override + Key? get dialogKey => alertDialogKey; + + @override + Key? get leftActionButtonKey => alertLeftActionButtonKey; + + @override + Key? get rightActionButtonKey => alertRightActionButtonKey; } diff --git a/lib/src/widgets/base_alert_dialog.dart b/lib/src/widgets/base_alert_dialog.dart index 5c1111740..2e6f1571e 100644 --- a/lib/src/widgets/base_alert_dialog.dart +++ b/lib/src/widgets/base_alert_dialog.dart @@ -33,6 +33,12 @@ class BaseAlertDialog extends StatelessWidget { String? get headerImageUrl => null; + Key? leftActionButtonKey; + + Key? rightActionButtonKey; + + Key? dialogKey; + Widget title(BuildContext context) { return Text( titleText, @@ -87,6 +93,7 @@ class BaseAlertDialog extends StatelessWidget { children: [ Expanded( child: TextButton( + key: leftActionButtonKey, onPressed: actionLeft, style: TextButton.styleFrom( backgroundColor: @@ -109,6 +116,7 @@ class BaseAlertDialog extends StatelessWidget { const VerticalSectionDivider(), Expanded( child: TextButton( + key: rightActionButtonKey, onPressed: actionRight, style: TextButton.styleFrom( backgroundColor: @@ -152,6 +160,7 @@ class BaseAlertDialog extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( + key: key, onTap: () => barrierDismissible ? Navigator.of(context).pop() : null, child: Container( color: Colors.transparent, diff --git a/lib/src/widgets/base_text_form_field.dart b/lib/src/widgets/base_text_form_field.dart index 534e6dae2..4648b88cc 100644 --- a/lib/src/widgets/base_text_form_field.dart +++ b/lib/src/widgets/base_text_form_field.dart @@ -30,7 +30,8 @@ class BaseTextFormField extends StatelessWidget { this.focusNode, this.initialValue, this.onSubmit, - this.borderWidth = 1.0}); + this.borderWidth = 1.0, + super.key}); final TextEditingController? controller; final TextInputType? keyboardType; diff --git a/lib/src/widgets/option_tile.dart b/lib/src/widgets/option_tile.dart index 8b46641fb..f7811a888 100644 --- a/lib/src/widgets/option_tile.dart +++ b/lib/src/widgets/option_tile.dart @@ -6,7 +6,8 @@ class OptionTile extends StatelessWidget { {required this.onPressed, required this.image, required this.title, - required this.description}); + required this.description, + super.key}); final VoidCallback onPressed; final Image image; diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index a7cb03a4e..801a79595 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/transaction_priority.dart'; import 'package:flutter/material.dart'; import 'package:cw_core/currency.dart'; import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart'; @@ -11,6 +12,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/picker_theme.dart'; +//TODO(David): PickerWidget is intertwined and confusing as is, find a way to optimize? class Picker extends StatefulWidget { Picker({ required this.selectedAtIndex, @@ -153,6 +155,7 @@ class _PickerState extends State> { Container( padding: EdgeInsets.symmetric(horizontal: padding), child: Text( + key: ValueKey('picker_title_text_key'), widget.title!, textAlign: TextAlign.center, style: TextStyle( @@ -189,7 +192,10 @@ class _PickerState extends State> { Padding( padding: const EdgeInsets.all(16), child: SearchBarWidget( - searchController: searchController, hintText: widget.hintText), + key: ValueKey('picker_search_bar_key'), + searchController: searchController, + hintText: widget.hintText, + ), ), Divider( color: Theme.of(context).extension()!.dividerColor, @@ -203,6 +209,7 @@ class _PickerState extends State> { children: [ filteredItems.length > 3 ? Scrollbar( + key: ValueKey('picker_scrollbar_key'), controller: controller, child: itemsList(), ) @@ -213,6 +220,7 @@ class _PickerState extends State> { left: padding, right: padding, child: Text( + key: ValueKey('picker_descriptinon_text_key'), widget.description!, textAlign: TextAlign.center, style: TextStyle( @@ -242,6 +250,7 @@ class _PickerState extends State> { if (widget.isWrapped) { return PickerWrapperWidget( + key: ValueKey('picker_wrapper_widget_key'), hasTitle: widget.title?.isNotEmpty ?? false, children: [content], ); @@ -260,6 +269,7 @@ class _PickerState extends State> { color: Theme.of(context).extension()!.dividerColor, child: widget.isGridView ? GridView.builder( + key: ValueKey('picker_items_grid_view_key'), padding: EdgeInsets.zero, controller: controller, shrinkWrap: true, @@ -275,6 +285,7 @@ class _PickerState extends State> { : buildItem(index), ) : ListView.separated( + key: ValueKey('picker_items_list_view_key'), padding: EdgeInsets.zero, controller: controller, shrinkWrap: true, @@ -293,10 +304,25 @@ class _PickerState extends State> { ); } + String _getItemName(Item item) { + String itemName; + if (item is Currency) { + itemName = item.name; + } else if (item is TransactionPriority) { + itemName = item.title; + } else { + itemName = ''; + } + + return itemName; + } + Widget buildItem(int index) { final item = widget.headerEnabled ? filteredItems[index] : items[index]; final tag = item is Currency ? item.tag : null; + final itemName = _getItemName(item); + final icon = _getItemIcon(item); final image = images.isNotEmpty ? filteredImages[index] : icon; @@ -316,6 +342,7 @@ class _PickerState extends State> { children: [ Flexible( child: Text( + key: ValueKey('picker_items_index_${itemName}_text_key'), widget.displayItem?.call(item) ?? item.toString(), softWrap: true, style: TextStyle( @@ -335,6 +362,7 @@ class _PickerState extends State> { height: 18.0, child: Center( child: Text( + key: ValueKey('picker_items_index_${index}_tag_key'), tag, style: TextStyle( fontSize: 7.0, @@ -358,6 +386,7 @@ class _PickerState extends State> { ); return GestureDetector( + key: ValueKey('picker_items_index_${itemName}_button_key'), onTap: () { if (widget.closeOnItemSelected) Navigator.of(context).pop(); onItemSelected(item!); @@ -383,6 +412,7 @@ class _PickerState extends State> { final item = items[index]; final tag = item is Currency ? item.tag : null; + final itemName = _getItemName(item); final icon = _getItemIcon(item); final image = images.isNotEmpty ? images[index] : icon; @@ -390,6 +420,7 @@ class _PickerState extends State> { final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex; final itemContent = Row( + key: ValueKey('picker_selected_item_row_key'), mainAxisSize: MainAxisSize.max, mainAxisAlignment: widget.mainAxisAlignment, crossAxisAlignment: CrossAxisAlignment.center, @@ -402,6 +433,7 @@ class _PickerState extends State> { children: [ Flexible( child: Text( + key: ValueKey('picker_items_index_${itemName}_selected_item_text_key'), widget.displayItem?.call(item) ?? item.toString(), softWrap: true, style: TextStyle( @@ -445,6 +477,7 @@ class _PickerState extends State> { ); return GestureDetector( + key: ValueKey('picker_items_index_${itemName}_selected_item_button_key'), onTap: () { if (widget.closeOnItemSelected) Navigator.of(context).pop(); }, diff --git a/lib/src/widgets/picker_wrapper_widget.dart b/lib/src/widgets/picker_wrapper_widget.dart index f4e52c5cd..ac863ac5d 100644 --- a/lib/src/widgets/picker_wrapper_widget.dart +++ b/lib/src/widgets/picker_wrapper_widget.dart @@ -4,7 +4,12 @@ import 'package:cake_wallet/src/widgets/alert_background.dart'; import 'package:cake_wallet/src/widgets/alert_close_button.dart'; 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 children; final bool hasTitle; @@ -29,8 +34,8 @@ class PickerWrapperWidget extends StatelessWidget { final containerBottom = screenCenter - containerCenter; // position the close button right below the search container - closeButtonBottom = closeButtonBottom - - containerBottom + (!hasTitle ? padding : padding / 1.5); + closeButtonBottom = + closeButtonBottom - containerBottom + (!hasTitle ? padding : padding / 1.5); } return AlertBackground( @@ -46,7 +51,11 @@ class PickerWrapperWidget extends StatelessWidget { children: children, ), SizedBox(height: ResponsiveLayoutUtilBase.kPopupSpaceHeight), - AlertCloseButton(bottom: closeButtonBottom, onTap: onClose), + AlertCloseButton( + key: ValueKey('picker_wrapper_close_button_key'), + bottom: closeButtonBottom, + onTap: onClose, + ), ], ), ), diff --git a/lib/src/widgets/primary_button.dart b/lib/src/widgets/primary_button.dart index 5f6b50f8b..06bfda157 100644 --- a/lib/src/widgets/primary_button.dart +++ b/lib/src/widgets/primary_button.dart @@ -4,15 +4,17 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class PrimaryButton extends StatelessWidget { - const PrimaryButton( - {required this.text, - required this.color, - required this.textColor, - this.onPressed, - this.isDisabled = false, - this.isDottedBorder = false, - this.borderColor = Colors.black, - this.onDisabledPressed}); + const PrimaryButton({ + required this.text, + required this.color, + required this.textColor, + this.onPressed, + this.isDisabled = false, + this.isDottedBorder = false, + this.borderColor = Colors.black, + this.onDisabledPressed, + super.key, + }); final VoidCallback? onPressed; final VoidCallback? onDisabledPressed; @@ -31,23 +33,23 @@ class PrimaryButton extends StatelessWidget { width: double.infinity, height: 52.0, child: TextButton( - onPressed: isDisabled - ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + onPressed: + isDisabled ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(26.0), ), ), - overlayColor: MaterialStateProperty.all(Colors.transparent)), + overlayColor: MaterialStateProperty.all(Colors.transparent)), child: Text(text, textAlign: TextAlign.center, style: TextStyle( fontSize: 15.0, fontWeight: FontWeight.w600, - color: isDisabled - ? textColor.withOpacity(0.5) - : textColor)), + color: isDisabled ? textColor.withOpacity(0.5) : textColor)), )), ); @@ -64,13 +66,15 @@ class PrimaryButton extends StatelessWidget { } class LoadingPrimaryButton extends StatelessWidget { - const LoadingPrimaryButton( - {required this.onPressed, - required this.text, - required this.color, - required this.textColor, - this.isDisabled = false, - this.isLoading = false}); + const LoadingPrimaryButton({ + required this.onPressed, + required this.text, + required this.color, + required this.textColor, + this.isDisabled = false, + this.isLoading = false, + super.key, + }); final VoidCallback onPressed; final Color color; @@ -88,41 +92,38 @@ class LoadingPrimaryButton extends StatelessWidget { height: 52.0, child: TextButton( onPressed: (isLoading || isDisabled) ? null : onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), - ), - )), - + style: ButtonStyle( + backgroundColor: + MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), + ), + )), child: isLoading ? CupertinoActivityIndicator(animating: true) : Text(text, - style: TextStyle( - fontSize: 15.0, - fontWeight: FontWeight.w600, - color: isDisabled - ? textColor.withOpacity(0.5) - : textColor - )), + style: TextStyle( + fontSize: 15.0, + fontWeight: FontWeight.w600, + color: isDisabled ? textColor.withOpacity(0.5) : textColor)), )), ); } } class PrimaryIconButton extends StatelessWidget { - const PrimaryIconButton({ - required this.onPressed, - required this.iconData, - required this.text, - required this.color, - required this.borderColor, - required this.iconColor, - required this.iconBackgroundColor, - required this.textColor, - this.mainAxisAlignment = MainAxisAlignment.start, - this.radius = 26 - }); + const PrimaryIconButton( + {required this.onPressed, + required this.iconData, + required this.text, + required this.color, + required this.borderColor, + required this.iconColor, + required this.iconBackgroundColor, + required this.textColor, + this.mainAxisAlignment = MainAxisAlignment.start, + this.radius = 26, super.key}); final VoidCallback onPressed; final IconData iconData; @@ -144,7 +145,8 @@ class PrimaryIconButton extends StatelessWidget { height: 52.0, child: TextButton( onPressed: onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(color), shape: MaterialStateProperty.all( RoundedRectangleBorder( borderRadius: BorderRadius.circular(radius), @@ -158,21 +160,15 @@ class PrimaryIconButton extends StatelessWidget { Container( width: 26.0, height: 52.0, - decoration: BoxDecoration( - shape: BoxShape.circle, color: iconBackgroundColor), - child: Center( - child: Icon(iconData, color: iconColor, size: 22.0) - ), + decoration: BoxDecoration(shape: BoxShape.circle, color: iconBackgroundColor), + child: Center(child: Icon(iconData, color: iconColor, size: 22.0)), ), ], ), Container( height: 52.0, child: Center( - child: Text(text, - style: TextStyle( - fontSize: 16.0, - color: textColor)), + child: Text(text, style: TextStyle(fontSize: 16.0, color: textColor)), ), ) ], @@ -189,7 +185,7 @@ class PrimaryImageButton extends StatelessWidget { required this.text, required this.color, required this.textColor, - this.borderColor = Colors.transparent}); + this.borderColor = Colors.transparent, super.key}); final VoidCallback onPressed; final Image image; @@ -206,31 +202,27 @@ class PrimaryImageButton extends StatelessWidget { width: double.infinity, height: 52.0, child: TextButton( - onPressed: onPressed, - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), - shape: MaterialStateProperty.all( - RoundedRectangleBorder( - borderRadius: BorderRadius.circular(26.0), - ), - )), - child:Center( - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - image, - SizedBox(width: 15), - Text( - text, - style: TextStyle( - fontSize: 15, - fontWeight: FontWeight.w600, - color: textColor + onPressed: onPressed, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(color), + shape: MaterialStateProperty.all( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26.0), ), - ) - ], - ), - ) - )), + )), + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + image, + SizedBox(width: 15), + Text( + text, + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: textColor), + ) + ], + ), + ))), ); } } diff --git a/lib/src/widgets/scollable_with_bottom_section.dart b/lib/src/widgets/scollable_with_bottom_section.dart index e15be610e..07a070204 100644 --- a/lib/src/widgets/scollable_with_bottom_section.dart +++ b/lib/src/widgets/scollable_with_bottom_section.dart @@ -9,6 +9,7 @@ class ScrollableWithBottomSection extends StatefulWidget { this.contentPadding, this.bottomSectionPadding, this.topSectionPadding, + this.scrollableKey, }); final Widget content; @@ -17,6 +18,7 @@ class ScrollableWithBottomSection extends StatefulWidget { final EdgeInsets? contentPadding; final EdgeInsets? bottomSectionPadding; final EdgeInsets? topSectionPadding; + final Key? scrollableKey; @override ScrollableWithBottomSectionState createState() => ScrollableWithBottomSectionState(); @@ -35,6 +37,7 @@ class ScrollableWithBottomSectionState extends State { top: 10, left: 0, child: Text(S.of(context).enter_seed_phrase, - style: TextStyle( - fontSize: 16.0, color: Theme.of(context).hintColor))), + style: TextStyle(fontSize: 16.0, color: Theme.of(context).hintColor))), Padding( padding: EdgeInsets.only(right: 40, top: 10), child: ValidatableAnnotatedEditableText( + key: widget.seedTextFieldKey, cursorColor: Colors.blue, backgroundCursorColor: Colors.blue, validStyle: TextStyle( @@ -112,15 +116,17 @@ class SeedWidgetState extends State { width: 32, height: 32, child: InkWell( + key: widget.pasteButtonKey, onTap: () async => _pasteText(), child: Container( padding: EdgeInsets.all(8), decoration: BoxDecoration( color: Theme.of(context).hintColor, - borderRadius: - BorderRadius.all(Radius.circular(6))), + borderRadius: BorderRadius.all(Radius.circular(6))), child: Image.asset('assets/images/paste_ios.png', - color: Theme.of(context).extension()!.textFieldButtonIconColor)), + color: Theme.of(context) + .extension()! + .textFieldButtonIconColor)), ))) ]), Container( diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 54bd54f53..98661f7c1 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -107,11 +107,14 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter + mocktail: ^1.0.4 build_runner: ^2.3.3 logging: ^1.2.0 mobx_codegen: ^2.1.1 build_resolvers: ^2.0.9 - hive_generator: ^1.1.3 + hive_generator: ^2.0.1 # flutter_launcher_icons: ^0.11.0 # check flutter_launcher_icons for usage pedantic: ^1.8.0 diff --git a/test_driver/integration_test.dart b/test_driver/integration_test.dart new file mode 100644 index 000000000..2e67d866f --- /dev/null +++ b/test_driver/integration_test.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +import 'package:integration_test/integration_test_driver.dart'; +import 'package:path/path.dart' as path; + +import 'package:flutter_driver/flutter_driver.dart'; + +Future main() async { + integrationDriver( + responseDataCallback: (Map? data) async { + await fs.directory(_destinationDirectory).create(recursive: true); + + final file = fs.file( + path.join( + _destinationDirectory, + '$_testOutputFilename.json', + ), + ); + + final resultString = _encodeJson(data); + await file.writeAsString(resultString); + }, + writeResponseOnFailure: true, + ); +} + +String _encodeJson(Map? jsonObject) { + return _prettyEncoder.convert(jsonObject); +} + +const _prettyEncoder = JsonEncoder.withIndent(' '); +const _testOutputFilename = 'integration_response_data'; +const _destinationDirectory = 'integration_test'; diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 3084af07c..d3b652935 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -39,6 +39,7 @@ class SecretKey { SecretKey('moralisApiKey', () => ''), SecretKey('ankrApiKey', () => ''), SecretKey('quantexExchangeMarkup', () => ''), + SecretKey('seeds', () => ''), SecretKey('testCakePayApiKey', () => ''), SecretKey('cakePayApiKey', () => ''), SecretKey('CSRFToken', () => ''),