CW-727/728-Automated-Integrated-Tests (#1514)

* feat: Integration tests setup and tests for Disclaimer, Welcome and Setup Pin Code pages * feat: Integration test flow from start to restoring a wallet successfully done * test: Dashboard view test and linking to flow * feat: Testing the Exchange flow section, selecting sending and receiving currencies * test: Successfully create an exchange section * feat: Implement flow up to sending section * test: Complete Exchange flow * fix dependency issue * test: Final cleanups * feat: Add CI to run automated integration tests withan android emulator * feat: Adjust Automated integration test CI to run on ubuntu 20.04-a * fix: Move integration test CI into PR test build CI * ci: Add automated test ci which is a streamlined replica of pr test build ci * ci: Re-add step to access branch name * ci: Add KVM * ci: Add filepath to trigger the test run from * ci: Add required key * ci: Add required key * ci: Add missing secret key * ci: Add missing secret key * ci: Add nano secrets to workflow * ci: Switch step to free space on runner * ci: Remove timeout from workflow * ci: Confirm impact that removing copy_monero_deps would have on entire workflow time * ci: Update CI and temporarily remove cache related to emulator * ci: Remove dynamic java version * ci: Temporarily switch CI * ci: Switch to 11.x jdk * ci: Temporarily switch CI * ci: Revert ubuntu version * ci: Add more api levels * ci: Add more target options * ci: Settled on stable emulator matrix options * ci: Add more target options * ci: Modify flow * ci: Streamline api levels to 28 and 29 * ci: One more trial * ci: Switch to flutter drive * ci: Reduce options * ci: Remove haven from test * ci: Check for solana in list * ci: Adjust amounts and currencies for exchange flow * ci: Set write response on failure to true * ci: Split ci to funds and non funds related tests * test: Test for Send flow scenario and minor restructuring for test folders and files * chore: cleanup * ci: Pause CI for now * ci: Pause CI for now * ci: Pause CI for now * Fix: Add keys back to currency amount textfield widget * fix: Switch variable name * fix: remove automation for now * test: Updating send page robot and also syncing branch with main --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>

David Adegoke committed Sep 22, 2024 at 03:46 UTC 4adb81c4dcf8e13665f9da8e5fff98a6a72df909
67 files changed +2381 -240
.github/workflows/pr_test_build_android.yml
+3
@@ -13,6 +13,9 @@ on:
13 jobs:
14 PR_test_build:
15 runs-on: ubuntu-20.04
16 + strategy:
17 + matrix:
18 + api-level: [29]
19 env:
20 STORE_PASS: test@cake_wallet
21 KEY_PASS: test@cake_wallet
.gitignore
+3
@@ -171,6 +171,9 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
171 macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
172 macos/Runner/Configs/AppInfo.xcconfig
173
174 +
175 +integration_test/playground.dart
176 +
177 # Monero.dart (Monero_C)
178 scripts/monero_c
179 # iOS generated framework bin
cw_nano/pubspec.lock
+2 -2
@@ -277,10 +277,10 @@ packages:
277 dependency: transitive
278 description:
279 name: file
280 - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
280 + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
281 url: "https://pub.dev"
282 source: hosted
283 - version: "6.1.4"
283 + version: "7.0.0"
284 fixnum:
285 dependency: transitive
286 description:
integration_test/components/common_test_cases.dart new
+96
@@ -0,0 +1,96 @@
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter_test/flutter_test.dart';
3 +
4 +class CommonTestCases {
5 + WidgetTester tester;
6 + CommonTestCases(this.tester);
7 +
8 + Future<void> isSpecificPage<T>() async {
9 + await tester.pumpAndSettle();
10 + hasType<T>();
11 + }
12 +
13 + Future<void> tapItemByKey(String key, {bool shouldPumpAndSettle = true}) async {
14 + final widget = find.byKey(ValueKey(key));
15 + await tester.tap(widget);
16 + shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump();
17 + }
18 +
19 + Future<void> tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async {
20 + await tester.tap(finder);
21 + shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump();
22 + }
23 +
24 + void hasText(String text, {bool hasWidget = true}) {
25 + final textWidget = find.text(text);
26 + expect(textWidget, hasWidget ? findsOneWidget : findsNothing);
27 + }
28 +
29 + void hasType<T>() {
30 + final typeWidget = find.byType(T);
31 + expect(typeWidget, findsOneWidget);
32 + }
33 +
34 + void hasValueKey(String key) {
35 + final typeWidget = find.byKey(ValueKey(key));
36 + expect(typeWidget, findsOneWidget);
37 + }
38 +
39 + Future<void> swipePage({bool swipeRight = true}) async {
40 + await tester.drag(find.byType(PageView), Offset(swipeRight ? -300 : 300, 0));
41 + await tester.pumpAndSettle();
42 + }
43 +
44 + Future<void> swipeByPageKey({required String key, bool swipeRight = true}) async {
45 + await tester.drag(find.byKey(ValueKey(key)), Offset(swipeRight ? -300 : 300, 0));
46 + await tester.pumpAndSettle();
47 + }
48 +
49 + Future<void> goBack() async {
50 + tester.printToConsole('Routing back to previous screen');
51 + final NavigatorState navigator = tester.state(find.byType(Navigator));
52 + navigator.pop();
53 + await tester.pumpAndSettle();
54 + }
55 +
56 + Future<void> scrollUntilVisible(String childKey, String parentScrollableKey,
57 + {double delta = 300}) async {
58 + final scrollableWidget = find.descendant(
59 + of: find.byKey(Key(parentScrollableKey)),
60 + matching: find.byType(Scrollable),
61 + );
62 +
63 + final isAlreadyVisibile = isWidgetVisible(find.byKey(ValueKey(childKey)));
64 +
65 + if (isAlreadyVisibile) return;
66 +
67 + await tester.scrollUntilVisible(
68 + find.byKey(ValueKey(childKey)),
69 + delta,
70 + scrollable: scrollableWidget,
71 + );
72 + }
73 +
74 + bool isWidgetVisible(Finder finder) {
75 + try {
76 + final Element element = finder.evaluate().single;
77 + final RenderBox renderBox = element.renderObject as RenderBox;
78 + return renderBox.paintBounds
79 + .shift(renderBox.localToGlobal(Offset.zero))
80 + .overlaps(tester.binding.renderViews.first.paintBounds);
81 + } catch (e) {
82 + return false;
83 + }
84 + }
85 +
86 + Future<void> enterText(String text, String editableTextKey) async {
87 + final editableTextWidget = find.byKey(ValueKey((editableTextKey)));
88 +
89 + await tester.enterText(editableTextWidget, text);
90 +
91 + await tester.pumpAndSettle();
92 + }
93 +
94 + Future<void> defaultSleepTime({int seconds = 2}) async =>
95 + await Future.delayed(Duration(seconds: seconds));
96 +}
integration_test/components/common_test_constants.dart new
+13
@@ -0,0 +1,13 @@
1 +import 'package:cw_core/crypto_currency.dart';
2 +import 'package:cw_core/wallet_type.dart';
3 +
4 +class CommonTestConstants {
5 + static final pin = [0, 8, 0, 1];
6 + static final String sendTestAmount = '0.00008';
7 + static final String exchangeTestAmount = '8';
8 + static final WalletType testWalletType = WalletType.solana;
9 + static final String testWalletName = 'Integrated Testing Wallet';
10 + static final CryptoCurrency testReceiveCurrency = CryptoCurrency.sol;
11 + static final CryptoCurrency testDepositCurrency = CryptoCurrency.usdtSol;
12 + static final String testWalletAddress = 'An2Y2fsUYKfYvN1zF89GAqR1e6GUMBg3qA83Y5ZWDf8L';
13 +}
integration_test/components/common_test_flows.dart new
+101
@@ -0,0 +1,101 @@
1 +import 'package:flutter/foundation.dart';
2 +import 'package:flutter_test/flutter_test.dart';
3 +
4 +import 'package:cake_wallet/.secrets.g.dart' as secrets;
5 +import 'package:cake_wallet/main.dart' as app;
6 +
7 +import '../robots/disclaimer_page_robot.dart';
8 +import '../robots/new_wallet_type_page_robot.dart';
9 +import '../robots/restore_from_seed_or_key_robot.dart';
10 +import '../robots/restore_options_page_robot.dart';
11 +import '../robots/setup_pin_code_robot.dart';
12 +import '../robots/welcome_page_robot.dart';
13 +import 'common_test_cases.dart';
14 +import 'common_test_constants.dart';
15 +
16 +class CommonTestFlows {
17 + CommonTestFlows(this._tester)
18 + : _commonTestCases = CommonTestCases(_tester),
19 + _welcomePageRobot = WelcomePageRobot(_tester),
20 + _setupPinCodeRobot = SetupPinCodeRobot(_tester),
21 + _disclaimerPageRobot = DisclaimerPageRobot(_tester),
22 + _newWalletTypePageRobot = NewWalletTypePageRobot(_tester),
23 + _restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester),
24 + _restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester);
25 +
26 + final WidgetTester _tester;
27 + final CommonTestCases _commonTestCases;
28 +
29 + final WelcomePageRobot _welcomePageRobot;
30 + final SetupPinCodeRobot _setupPinCodeRobot;
31 + final DisclaimerPageRobot _disclaimerPageRobot;
32 + final NewWalletTypePageRobot _newWalletTypePageRobot;
33 + final RestoreOptionsPageRobot _restoreOptionsPageRobot;
34 + final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
35 +
36 + Future<void> startAppFlow(Key key) async {
37 + await app.main(topLevelKey: ValueKey('send_flow_test_app_key'));
38 +
39 + await _tester.pumpAndSettle();
40 +
41 + // --------- Disclaimer Page ------------
42 + // Tap checkbox to accept disclaimer
43 + await _disclaimerPageRobot.tapDisclaimerCheckbox();
44 +
45 + // Tap accept button
46 + await _disclaimerPageRobot.tapAcceptButton();
47 + }
48 +
49 + Future<void> restoreWalletThroughSeedsFlow() async {
50 + await _welcomeToRestoreFromSeedsPath();
51 + await _restoreFromSeeds();
52 + }
53 +
54 + Future<void> restoreWalletThroughKeysFlow() async {
55 + await _welcomeToRestoreFromSeedsPath();
56 + await _restoreFromKeys();
57 + }
58 +
59 + Future<void> _welcomeToRestoreFromSeedsPath() async {
60 + // --------- Welcome Page ---------------
61 + await _welcomePageRobot.navigateToRestoreWalletPage();
62 +
63 + // ----------- Restore Options Page -----------
64 + // Route to restore from seeds page to continue flow
65 + await _restoreOptionsPageRobot.navigateToRestoreFromSeedsPage();
66 +
67 + // ----------- SetupPinCode Page -------------
68 + // Confirm initial defaults - Widgets to be displayed etc
69 + await _setupPinCodeRobot.isSetupPinCodePage();
70 +
71 + await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, true);
72 + await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, false);
73 + await _setupPinCodeRobot.tapSuccessButton();
74 +
75 + // ----------- NewWalletType Page -------------
76 + // Confirm scroll behaviour works properly
77 + await _newWalletTypePageRobot
78 + .findParticularWalletTypeInScrollableList(CommonTestConstants.testWalletType);
79 +
80 + // Select a wallet and route to next page
81 + await _newWalletTypePageRobot.selectWalletType(CommonTestConstants.testWalletType);
82 + await _newWalletTypePageRobot.onNextButtonPressed();
83 + }
84 +
85 + Future<void> _restoreFromSeeds() async {
86 + // ----------- RestoreFromSeedOrKeys Page -------------
87 + await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName);
88 + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(secrets.solanaTestWalletSeeds);
89 + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
90 + }
91 +
92 + Future<void> _restoreFromKeys() async {
93 + await _commonTestCases.swipePage();
94 + await _commonTestCases.defaultSleepTime();
95 +
96 + await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName);
97 +
98 + await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore('');
99 + await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
100 + }
101 +}
integration_test/funds_related_tests.dart new
+84
@@ -0,0 +1,84 @@
1 +import 'package:flutter/foundation.dart';
2 +import 'package:flutter_test/flutter_test.dart';
3 +import 'package:integration_test/integration_test.dart';
4 +
5 +import 'components/common_test_constants.dart';
6 +import 'components/common_test_flows.dart';
7 +import 'robots/auth_page_robot.dart';
8 +import 'robots/dashboard_page_robot.dart';
9 +import 'robots/exchange_confirm_page_robot.dart';
10 +import 'robots/exchange_page_robot.dart';
11 +import 'robots/exchange_trade_page_robot.dart';
12 +
13 +void main() {
14 + IntegrationTestWidgetsFlutterBinding.ensureInitialized();
15 +
16 + DashboardPageRobot dashboardPageRobot;
17 + ExchangePageRobot exchangePageRobot;
18 + ExchangeConfirmPageRobot exchangeConfirmPageRobot;
19 + AuthPageRobot authPageRobot;
20 + ExchangeTradePageRobot exchangeTradePageRobot;
21 + CommonTestFlows commonTestFlows;
22 +
23 + group('Startup Test', () {
24 + testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL',
25 + (tester) async {
26 + authPageRobot = AuthPageRobot(tester);
27 + exchangePageRobot = ExchangePageRobot(tester);
28 + dashboardPageRobot = DashboardPageRobot(tester);
29 + exchangeTradePageRobot = ExchangeTradePageRobot(tester);
30 + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
31 + commonTestFlows = CommonTestFlows(tester);
32 +
33 + await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key'));
34 +
35 + await commonTestFlows.restoreWalletThroughSeedsFlow();
36 +
37 + // ----------- RestoreFromSeedOrKeys Page -------------
38 + await dashboardPageRobot.navigateToExchangePage();
39 +
40 + // ----------- Exchange Page -------------
41 + await exchangePageRobot.isExchangePage();
42 + exchangePageRobot.hasResetButton();
43 + await exchangePageRobot.displayBothExchangeCards();
44 + exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards();
45 + exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards();
46 +
47 + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency);
48 + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency);
49 +
50 + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
51 + await exchangePageRobot.enterDepositRefundAddress(
52 + depositAddress: CommonTestConstants.testWalletAddress);
53 +
54 + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
55 +
56 + await exchangePageRobot.onExchangeButtonPressed();
57 +
58 + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
59 +
60 + final onAuthPage = authPageRobot.onAuthPage();
61 + if (onAuthPage) {
62 + await authPageRobot.enterPinCode(CommonTestConstants.pin, false);
63 + }
64 +
65 + // ----------- Exchange Confirm Page -------------
66 + await exchangeConfirmPageRobot.isExchangeConfirmPage();
67 +
68 + exchangeConfirmPageRobot.confirmComponentsOfTradeDisplayProperly();
69 + await exchangeConfirmPageRobot.confirmCopyTradeIdToClipBoardWorksProperly();
70 + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
71 +
72 + // ----------- Exchange Trade Page -------------
73 + await exchangeTradePageRobot.isExchangeTradePage();
74 + exchangeTradePageRobot.hasInformationDialog();
75 + await exchangeTradePageRobot.onGotItButtonPressed();
76 +
77 + await exchangeTradePageRobot.onConfirmSendingButtonPressed();
78 +
79 + await exchangeTradePageRobot.handleConfirmSendResult();
80 +
81 + await exchangeTradePageRobot.onSendButtonOnConfirmSendingDialogPressed();
82 + });
83 + });
84 +}
integration_test/helpers/mocks.dart new
+25
@@ -0,0 +1,25 @@
1 +import 'package:cake_wallet/core/auth_service.dart';
2 +import 'package:cake_wallet/core/secure_storage.dart';
3 +import 'package:cake_wallet/store/app_store.dart';
4 +import 'package:cake_wallet/store/authentication_store.dart';
5 +import 'package:cake_wallet/store/settings_store.dart';
6 +import 'package:cake_wallet/store/wallet_list_store.dart';
7 +import 'package:cake_wallet/view_model/link_view_model.dart';
8 +import 'package:hive/hive.dart';
9 +import 'package:mocktail/mocktail.dart';
10 +
11 +class MockAppStore extends Mock implements AppStore{}
12 +class MockAuthService extends Mock implements AuthService{}
13 +class MockSettingsStore extends Mock implements SettingsStore {}
14 +class MockAuthenticationStore extends Mock implements AuthenticationStore{}
15 +class MockWalletListStore extends Mock implements WalletListStore{}
16 +
17 +
18 +
19 +class MockLinkViewModel extends Mock implements LinkViewModel {}
20 +
21 +class MockHiveInterface extends Mock implements HiveInterface {}
22 +
23 +class MockHiveBox extends Mock implements Box<dynamic> {}
24 +
25 +class MockSecureStorage extends Mock implements SecureStorage{}
\ No newline at end of file
integration_test/helpers/test_helpers.dart new
+100
@@ -0,0 +1,100 @@
1 +import 'package:cake_wallet/core/auth_service.dart';
2 +import 'package:cake_wallet/core/secure_storage.dart';
3 +import 'package:cake_wallet/di.dart';
4 +import 'package:cake_wallet/store/app_store.dart';
5 +import 'package:cake_wallet/store/authentication_store.dart';
6 +import 'package:cake_wallet/store/settings_store.dart';
7 +import 'package:cake_wallet/store/wallet_list_store.dart';
8 +import 'package:cake_wallet/view_model/link_view_model.dart';
9 +import 'package:hive/hive.dart';
10 +import 'package:mocktail/mocktail.dart';
11 +
12 +import 'mocks.dart';
13 +
14 +class TestHelpers {
15 + static void setup() {
16 + // Fallback values can also be declared here
17 + registerDependencies();
18 + }
19 +
20 + static void registerDependencies() {
21 + getAndRegisterAppStore();
22 + getAndRegisterAuthService();
23 + getAndRegisterSettingsStore();
24 + getAndRegisterAuthenticationStore();
25 + getAndRegisterWalletListStore();
26 +
27 + getAndRegisterLinkViewModel();
28 + getAndRegisterSecureStorage();
29 + getAndRegisterHiveInterface();
30 + }
31 +
32 + static MockSettingsStore getAndRegisterSettingsStore() {
33 + _removeRegistrationIfExists<SettingsStore>();
34 + final service = MockSettingsStore();
35 + getIt.registerSingleton<SettingsStore>(service);
36 + return service;
37 + }
38 +
39 + static MockAppStore getAndRegisterAppStore() {
40 + _removeRegistrationIfExists<AppStore>();
41 + final service = MockAppStore();
42 + final settingsStore = getAndRegisterSettingsStore();
43 +
44 + when(() => service.settingsStore).thenAnswer((invocation) => settingsStore);
45 + getIt.registerSingleton<AppStore>(service);
46 + return service;
47 + }
48 +
49 + static MockAuthService getAndRegisterAuthService() {
50 + _removeRegistrationIfExists<AuthService>();
51 + final service = MockAuthService();
52 + getIt.registerSingleton<AuthService>(service);
53 + return service;
54 + }
55 +
56 + static MockAuthenticationStore getAndRegisterAuthenticationStore() {
57 + _removeRegistrationIfExists<AuthenticationStore>();
58 + final service = MockAuthenticationStore();
59 + when(() => service.state).thenReturn(AuthenticationState.uninitialized);
60 + getIt.registerSingleton<AuthenticationStore>(service);
61 + return service;
62 + }
63 +
64 + static MockWalletListStore getAndRegisterWalletListStore() {
65 + _removeRegistrationIfExists<WalletListStore>();
66 + final service = MockWalletListStore();
67 + getIt.registerSingleton<WalletListStore>(service);
68 + return service;
69 + }
70 +
71 + static MockLinkViewModel getAndRegisterLinkViewModel() {
72 + _removeRegistrationIfExists<LinkViewModel>();
73 + final service = MockLinkViewModel();
74 + getIt.registerSingleton<LinkViewModel>(service);
75 + return service;
76 + }
77 +
78 + static MockHiveInterface getAndRegisterHiveInterface() {
79 + _removeRegistrationIfExists<HiveInterface>();
80 + final service = MockHiveInterface();
81 + final box = MockHiveBox();
82 + getIt.registerSingleton<HiveInterface>(service);
83 + return service;
84 + }
85 +
86 + static MockSecureStorage getAndRegisterSecureStorage() {
87 + _removeRegistrationIfExists<SecureStorage>();
88 + final service = MockSecureStorage();
89 + getIt.registerSingleton<SecureStorage>(service);
90 + return service;
91 + }
92 +
93 + static void _removeRegistrationIfExists<T extends Object>() {
94 + if (getIt.isRegistered<T>()) {
95 + getIt.unregister<T>();
96 + }
97 + }
98 +
99 + static void tearDown() => getIt.reset();
100 +}
integration_test/integration_response_data.json new
+1
@@ -0,0 +1 @@
1 +null
\ No newline at end of file
integration_test/robots/auth_page_robot.dart new
+30
@@ -0,0 +1,30 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/auth/auth_page.dart';
3 +import 'package:flutter/widgets.dart';
4 +import 'package:flutter_test/flutter_test.dart';
5 +
6 +import '../components/common_test_cases.dart';
7 +import 'pin_code_widget_robot.dart';
8 +
9 +class AuthPageRobot extends PinCodeWidgetRobot {
10 + AuthPageRobot(this.tester)
11 + : commonTestCases = CommonTestCases(tester),
12 + super(tester);
13 +
14 + final WidgetTester tester;
15 + late CommonTestCases commonTestCases;
16 +
17 + bool onAuthPage() {
18 + final hasPinButtons = find.byKey(ValueKey('pin_code_button_3_key'));
19 + final hasPin = hasPinButtons.tryEvaluate();
20 + return hasPin;
21 + }
22 +
23 + Future<void> isAuthPage() async {
24 + await commonTestCases.isSpecificPage<AuthPage>();
25 + }
26 +
27 + void hasTitle() {
28 + commonTestCases.hasText(S.current.setup_pin);
29 + }
30 +}
integration_test/robots/dashboard_page_robot.dart new
+75
@@ -0,0 +1,75 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
3 +import 'package:cw_core/wallet_type.dart';
4 +import 'package:flutter_test/flutter_test.dart';
5 +
6 +import '../components/common_test_cases.dart';
7 +
8 +class DashboardPageRobot {
9 + DashboardPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
10 +
11 + final WidgetTester tester;
12 + late CommonTestCases commonTestCases;
13 +
14 + Future<void> isDashboardPage() async {
15 + await commonTestCases.isSpecificPage<DashboardPage>();
16 + }
17 +
18 + void confirmServiceUpdateButtonDisplays() {
19 + commonTestCases.hasValueKey('dashboard_page_services_update_button_key');
20 + }
21 +
22 + void confirmSyncIndicatorButtonDisplays() {
23 + commonTestCases.hasValueKey('dashboard_page_sync_indicator_button_key');
24 + }
25 +
26 + void confirmMenuButtonDisplays() {
27 + commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key');
28 + }
29 +
30 + Future<void> confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type,
31 + {bool isHaven = false}) async {
32 + //Balance Page
33 + final walletName = walletTypeToString(type);
34 + final assetName = isHaven ? '$walletName Assets' : walletName;
35 + commonTestCases.hasText(assetName);
36 +
37 + // Swipe to Cake features Page
38 + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false);
39 + await commonTestCases.defaultSleepTime();
40 + commonTestCases.hasText('Cake ${S.current.features}');
41 +
42 + // Swipe back to balance
43 + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key');
44 + await commonTestCases.defaultSleepTime();
45 +
46 + // Swipe to Transactions Page
47 + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key');
48 + await commonTestCases.defaultSleepTime();
49 + commonTestCases.hasText(S.current.transactions);
50 +
51 + // Swipe back to balance
52 + await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false);
53 + await commonTestCases.defaultSleepTime(seconds: 5);
54 + }
55 +
56 + Future<void> navigateToBuyPage() async {
57 + await commonTestCases.tapItemByKey('dashboard_page_${S.current.buy}_action_button_key');
58 + }
59 +
60 + Future<void> navigateToSendPage() async {
61 + await commonTestCases.tapItemByKey('dashboard_page_${S.current.send}_action_button_key');
62 + }
63 +
64 + Future<void> navigateToSellPage() async {
65 + await commonTestCases.tapItemByKey('dashboard_page_${S.current.sell}_action_button_key');
66 + }
67 +
68 + Future<void> navigateToReceivePage() async {
69 + await commonTestCases.tapItemByKey('dashboard_page_${S.current.receive}_action_button_key');
70 + }
71 +
72 + Future<void> navigateToExchangePage() async {
73 + await commonTestCases.tapItemByKey('dashboard_page_${S.current.exchange}_action_button_key');
74 + }
75 +}
integration_test/robots/disclaimer_page_robot.dart new
+39
@@ -0,0 +1,39 @@
1 +import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
2 +import 'package:flutter/material.dart';
3 +import 'package:flutter_test/flutter_test.dart';
4 +
5 +import '../components/common_test_cases.dart';
6 +
7 +class DisclaimerPageRobot {
8 + DisclaimerPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
9 +
10 + final WidgetTester tester;
11 + late CommonTestCases commonTestCases;
12 +
13 + Future<void> isDisclaimerPage() async {
14 + await commonTestCases.isSpecificPage<DisclaimerPage>();
15 + }
16 +
17 + void hasCheckIcon(bool hasBeenTapped) {
18 + // The checked Icon should not be available initially, until user taps the checkbox
19 + final checkIcon = find.byKey(ValueKey('disclaimer_check_icon_key'));
20 + expect(checkIcon, hasBeenTapped ? findsOneWidget : findsNothing);
21 + }
22 +
23 + void hasDisclaimerCheckbox() {
24 + final checkBox = find.byKey(ValueKey('disclaimer_check_key'));
25 + expect(checkBox, findsOneWidget);
26 + }
27 +
28 + Future<void> tapDisclaimerCheckbox() async {
29 + await commonTestCases.tapItemByKey('disclaimer_check_key');
30 +
31 + await commonTestCases.defaultSleepTime();
32 + }
33 +
34 + Future<void> tapAcceptButton() async {
35 + await commonTestCases.tapItemByKey('disclaimer_accept_button_key');
36 +
37 + await commonTestCases.defaultSleepTime();
38 + }
39 +}
integration_test/robots/exchange_confirm_page_robot.dart new
+45
@@ -0,0 +1,45 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart';
3 +import 'package:flutter/services.dart';
4 +import 'package:flutter_test/flutter_test.dart';
5 +
6 +import '../components/common_test_cases.dart';
7 +
8 +class ExchangeConfirmPageRobot {
9 + ExchangeConfirmPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
10 +
11 + final WidgetTester tester;
12 + late CommonTestCases commonTestCases;
13 +
14 + Future<void> isExchangeConfirmPage() async {
15 + await commonTestCases.isSpecificPage<ExchangeConfirmPage>();
16 + }
17 +
18 + void confirmComponentsOfTradeDisplayProperly() {
19 + final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage));
20 + final trade = exchangeConfirmPage.trade;
21 +
22 + commonTestCases.hasText(trade.id);
23 + commonTestCases.hasText('${trade.provider.title} ${S.current.trade_id}');
24 +
25 + commonTestCases.hasValueKey('exchange_confirm_page_saved_id_button_key');
26 + commonTestCases.hasValueKey('exchange_confirm_page_copy_to_clipboard_button_key');
27 + }
28 +
29 + Future<void> confirmCopyTradeIdToClipBoardWorksProperly() async {
30 + final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage));
31 + final trade = exchangeConfirmPage.trade;
32 +
33 + await commonTestCases.tapItemByKey('exchange_confirm_page_copy_to_clipboard_button_key');
34 +
35 + ClipboardData? clipboardData = await Clipboard.getData('text/plain');
36 +
37 + expect(clipboardData?.text, trade.id);
38 + }
39 +
40 + Future<void> onSavedTradeIdButtonPressed() async {
41 + await tester.pumpAndSettle();
42 + await commonTestCases.defaultSleepTime();
43 + await commonTestCases.tapItemByKey('exchange_confirm_page_saved_id_button_key');
44 + }
45 +}
integration_test/robots/exchange_page_robot.dart new
+330
@@ -0,0 +1,330 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
3 +import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
4 +import 'package:cw_core/crypto_currency.dart';
5 +import 'package:flutter/foundation.dart';
6 +import 'package:flutter_test/flutter_test.dart';
7 +
8 +import '../components/common_test_cases.dart';
9 +
10 +class ExchangePageRobot {
11 + ExchangePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
12 +
13 + final WidgetTester tester;
14 + late CommonTestCases commonTestCases;
15 +
16 + Future<void> isExchangePage() async {
17 + await commonTestCases.isSpecificPage<ExchangePage>();
18 + await commonTestCases.defaultSleepTime();
19 + }
20 +
21 + void hasResetButton() {
22 + commonTestCases.hasText(S.current.reset);
23 + }
24 +
25 + void displaysPresentProviderPicker() {
26 + commonTestCases.hasType<PresentProviderPicker>();
27 + }
28 +
29 + Future<void> displayBothExchangeCards() async {
30 + final ExchangePage exchangeCard = tester.widget<ExchangePage>(
31 + find.byType(ExchangePage),
32 + );
33 +
34 + final depositKey = exchangeCard.depositKey;
35 + final receiveKey = exchangeCard.receiveKey;
36 +
37 + final depositExchangeCard = find.byKey(depositKey);
38 + expect(depositExchangeCard, findsOneWidget);
39 +
40 + final receiveExchangeCard = find.byKey(receiveKey);
41 + expect(receiveExchangeCard, findsOneWidget);
42 + }
43 +
44 + void confirmRightComponentsDisplayOnDepositExchangeCards() {
45 + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
46 + final exchangeViewModel = exchangePage.exchangeViewModel;
47 + final depositCardPrefix = 'deposit_exchange_card';
48 +
49 + commonTestCases.hasValueKey('${depositCardPrefix}_title_key');
50 + commonTestCases.hasValueKey('${depositCardPrefix}_currency_picker_button_key');
51 + commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_text_key');
52 + commonTestCases.hasValueKey('${depositCardPrefix}_amount_textfield_key');
53 +
54 + exchangePage.depositKey.currentState!.changeLimits(min: '0.1');
55 +
56 + commonTestCases.hasValueKey('${depositCardPrefix}_min_limit_text_key');
57 +
58 + final initialCurrency = exchangeViewModel.depositCurrency;
59 + if (initialCurrency.tag != null) {
60 + commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_tag_text_key');
61 + }
62 +
63 + if (exchangeViewModel.hasAllAmount) {
64 + commonTestCases.hasValueKey('${depositCardPrefix}_send_all_button_key');
65 + }
66 +
67 + if (exchangeViewModel.isMoneroWallet) {
68 + commonTestCases.hasValueKey('${depositCardPrefix}_address_book_button_key');
69 + }
70 +
71 + if (exchangeViewModel.isDepositAddressEnabled) {
72 + commonTestCases.hasValueKey('${depositCardPrefix}_editable_address_textfield_key');
73 + } else {
74 + commonTestCases.hasValueKey('${depositCardPrefix}_non_editable_address_textfield_key');
75 + commonTestCases.hasValueKey('${depositCardPrefix}_copy_refund_address_button_key');
76 + }
77 +
78 + // commonTestCases.hasValueKey('${depositCardPrefix}_max_limit_text_key');
79 + }
80 +
81 + void confirmRightComponentsDisplayOnReceiveExchangeCards() {
82 + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
83 + final exchangeViewModel = exchangePage.exchangeViewModel;
84 + final receiveCardPrefix = 'receive_exchange_card';
85 +
86 + commonTestCases.hasValueKey('${receiveCardPrefix}_title_key');
87 + commonTestCases.hasValueKey('${receiveCardPrefix}_currency_picker_button_key');
88 + commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_text_key');
89 + commonTestCases.hasValueKey('${receiveCardPrefix}_amount_textfield_key');
90 + commonTestCases.hasValueKey('${receiveCardPrefix}_min_limit_text_key');
91 +
92 + final initialCurrency = exchangeViewModel.receiveCurrency;
93 + if (initialCurrency.tag != null) {
94 + commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_tag_text_key');
95 + }
96 +
97 + if (exchangeViewModel.hasAllAmount) {
98 + commonTestCases.hasValueKey('${receiveCardPrefix}_send_all_button_key');
99 + }
100 +
101 + if (exchangeViewModel.isMoneroWallet) {
102 + commonTestCases.hasValueKey('${receiveCardPrefix}_address_book_button_key');
103 + }
104 +
105 + commonTestCases.hasValueKey('${receiveCardPrefix}_editable_address_textfield_key');
106 + }
107 +
108 + Future<void> selectDepositCurrency(CryptoCurrency depositCurrency) async {
109 + final depositPrefix = 'deposit_exchange_card';
110 + final currencyPickerKey = '${depositPrefix}_currency_picker_button_key';
111 + final currencyPickerDialogKey = '${depositPrefix}_currency_picker_dialog_button_key';
112 +
113 + await commonTestCases.tapItemByKey(currencyPickerKey);
114 + commonTestCases.hasValueKey(currencyPickerDialogKey);
115 +
116 + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
117 + final exchangeViewModel = exchangePage.exchangeViewModel;
118 +
119 + if (depositCurrency == exchangeViewModel.depositCurrency) {
120 + await commonTestCases.defaultSleepTime();
121 + await commonTestCases
122 + .tapItemByKey('picker_items_index_${depositCurrency.name}_selected_item_button_key');
123 + return;
124 + }
125 +
126 + await commonTestCases.scrollUntilVisible(
127 + 'picker_items_index_${depositCurrency.name}_button_key',
128 + 'picker_scrollbar_key',
129 + );
130 + await commonTestCases.defaultSleepTime();
131 +
132 + await commonTestCases.tapItemByKey('picker_items_index_${depositCurrency.name}_button_key');
133 + }
134 +
135 + Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
136 + final receivePrefix = 'receive_exchange_card';
137 + final currencyPickerKey = '${receivePrefix}_currency_picker_button_key';
138 + final currencyPickerDialogKey = '${receivePrefix}_currency_picker_dialog_button_key';
139 +
140 + await commonTestCases.tapItemByKey(currencyPickerKey);
141 + commonTestCases.hasValueKey(currencyPickerDialogKey);
142 +
143 + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
144 + final exchangeViewModel = exchangePage.exchangeViewModel;
145 +
146 + if (receiveCurrency == exchangeViewModel.receiveCurrency) {
147 + await commonTestCases
148 + .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key');
149 + return;
150 + }
151 +
152 + await commonTestCases.scrollUntilVisible(
153 + 'picker_items_index_${receiveCurrency.name}_button_key',
154 + 'picker_scrollbar_key',
155 + );
156 + await commonTestCases.defaultSleepTime();
157 +
158 + await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
159 + }
160 +
161 + Future<void> enterDepositAmount(String amount) async {
162 + await commonTestCases.enterText(amount, 'deposit_exchange_card_amount_textfield_key');
163 + }
164 +
165 + Future<void> enterDepositRefundAddress({String? depositAddress}) async {
166 + ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
167 + final exchangeViewModel = exchangePage.exchangeViewModel;
168 +
169 + if (exchangeViewModel.isDepositAddressEnabled && depositAddress != null) {
170 + await commonTestCases.enterText(
171 + depositAddress, 'deposit_exchange_card_editable_address_textfield_key');
172 + }
173 + }
174 +
175 + Future<void> enterReceiveAddress(String receiveAddress) async {
176 + await commonTestCases.enterText(
177 + receiveAddress,
178 + 'receive_exchange_card_editable_address_textfield_key',
179 + );
180 + await commonTestCases.defaultSleepTime();
181 + }
182 +
183 + Future<void> onExchangeButtonPressed() async {
184 + await commonTestCases.tapItemByKey('exchange_page_exchange_button_key');
185 + await commonTestCases.defaultSleepTime();
186 + }
187 +
188 + bool hasMaxLimitError() {
189 + final maxErrorText = find.text(S.current.error_text_input_above_maximum_limit);
190 +
191 + bool hasMaxError = maxErrorText.tryEvaluate();
192 +
193 + return hasMaxError;
194 + }
195 +
196 + bool hasMinLimitError() {
197 + final minErrorText = find.text(S.current.error_text_input_below_minimum_limit);
198 +
199 + bool hasMinError = minErrorText.tryEvaluate();
200 +
201 + return hasMinError;
202 + }
203 +
204 + bool hasTradeCreationFailureError() {
205 + final tradeCreationFailureDialogButton =
206 + find.byKey(ValueKey('exchange_page_trade_creation_failure_dialog_button_key'));
207 +
208 + bool hasTradeCreationFailure = tradeCreationFailureDialogButton.tryEvaluate();
209 + tester.printToConsole('Trade not created error: $hasTradeCreationFailure');
210 + return hasTradeCreationFailure;
211 + }
212 +
213 + Future<void> onTradeCreationFailureDialogButtonPressed() async {
214 + await commonTestCases.tapItemByKey('exchange_page_trade_creation_failure_dialog_button_key');
215 + }
216 +
217 + /// Handling Trade Failure Errors or errors shown through the Failure Dialog.
218 + ///
219 + /// Simulating the user's flow and response when this error comes up.
220 + /// Examples are:
221 + /// - No provider can handle this trade error,
222 + /// - Trade amount below limit error.
223 + Future<void> _handleTradeCreationFailureErrors() async {
224 + bool isTradeCreationFailure = false;
225 +
226 + isTradeCreationFailure = hasTradeCreationFailureError();
227 +
228 + int maxRetries = 20;
229 + int retries = 0;
230 +
231 + while (isTradeCreationFailure && retries < maxRetries) {
232 + await tester.pump();
233 +
234 + await onTradeCreationFailureDialogButtonPressed();
235 +
236 + await commonTestCases.defaultSleepTime(seconds: 5);
237 +
238 + await onExchangeButtonPressed();
239 +
240 + isTradeCreationFailure = hasTradeCreationFailureError();
241 + retries++;
242 + }
243 + }
244 +
245 + /// Handles the min limit error.
246 + ///
247 + /// Simulates the user's flow and response when it comes up.
248 + ///
249 + /// Has a max retry of 20 times.
250 + Future<void> _handleMinLimitError(String initialAmount) async {
251 + bool isMinLimitError = false;
252 +
253 + isMinLimitError = hasMinLimitError();
254 +
255 + double amount;
256 +
257 + amount = double.parse(initialAmount);
258 +
259 + int maxRetries = 20;
260 + int retries = 0;
261 +
262 + while (isMinLimitError && retries < maxRetries) {
263 + amount++;
264 + tester.printToConsole('Amount: $amount');
265 +
266 + enterDepositAmount(amount.toString());
267 +
268 + await commonTestCases.defaultSleepTime();
269 +
270 + await onExchangeButtonPressed();
271 +
272 + isMinLimitError = hasMinLimitError();
273 +
274 + retries++;
275 + }
276 +
277 + if (retries >= maxRetries) {
278 + tester.printToConsole('Max retries reached for minLimit Error. Exiting loop.');
279 + }
280 + }
281 +
282 + /// Handles the max limit error.
283 + ///
284 + /// Simulates the user's flow and response when it comes up.
285 + ///
286 + /// Has a max retry of 20 times.
287 + Future<void> _handleMaxLimitError(String initialAmount) async {
288 + bool isMaxLimitError = false;
289 +
290 + isMaxLimitError = hasMaxLimitError();
291 +
292 + double amount;
293 +
294 + amount = double.parse(initialAmount);
295 +
296 + int maxRetries = 20;
297 + int retries = 0;
298 +
299 + while (isMaxLimitError && retries < maxRetries) {
300 + amount++;
301 + tester.printToConsole('Amount: $amount');
302 +
303 + enterDepositAmount(amount.toString());
304 +
305 + await commonTestCases.defaultSleepTime();
306 +
307 + await onExchangeButtonPressed();
308 +
309 + isMaxLimitError = hasMaxLimitError();
310 +
311 + retries++;
312 + }
313 +
314 + if (retries >= maxRetries) {
315 + tester.printToConsole('Max retries reached for maxLimit Error. Exiting loop.');
316 + }
317 + }
318 +
319 + Future<void> handleErrors(String initialAmount) async {
320 + await tester.pumpAndSettle();
321 +
322 + await _handleMinLimitError(initialAmount);
323 +
324 + await _handleMaxLimitError(initialAmount);
325 +
326 + await _handleTradeCreationFailureErrors();
327 +
328 + await commonTestCases.defaultSleepTime();
329 + }
330 +}
integration_test/robots/exchange_trade_page_robot.dart new
+152
@@ -0,0 +1,152 @@
1 +import 'dart:async';
2 +
3 +import 'package:cake_wallet/core/execution_state.dart';
4 +import 'package:cake_wallet/generated/i18n.dart';
5 +import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart';
6 +import 'package:flutter/foundation.dart';
7 +import 'package:flutter_test/flutter_test.dart';
8 +
9 +import '../components/common_test_cases.dart';
10 +
11 +class ExchangeTradePageRobot {
12 + ExchangeTradePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
13 +
14 + final WidgetTester tester;
15 + late CommonTestCases commonTestCases;
16 +
17 + Future<void> isExchangeTradePage() async {
18 + await commonTestCases.isSpecificPage<ExchangeTradePage>();
19 + }
20 +
21 + void hasInformationDialog() {
22 + commonTestCases.hasValueKey('information_page_dialog_key');
23 + }
24 +
25 + Future<void> onGotItButtonPressed() async {
26 + await commonTestCases.tapItemByKey('information_page_got_it_button_key');
27 + await commonTestCases.defaultSleepTime();
28 + }
29 +
30 + Future<void> onConfirmSendingButtonPressed() async {
31 + tester.printToConsole('Now confirming sending');
32 +
33 + await commonTestCases.tapItemByKey(
34 + 'exchange_trade_page_confirm_sending_button_key',
35 + shouldPumpAndSettle: false,
36 + );
37 +
38 + final Completer<void> completer = Completer<void>();
39 +
40 + // Loop to wait for the async operation to complete
41 + while (true) {
42 + await Future.delayed(Duration(seconds: 1));
43 +
44 + final ExchangeTradeState state = tester.state(find.byType(ExchangeTradeForm));
45 + final execState = state.widget.exchangeTradeViewModel.sendViewModel.state;
46 +
47 + bool isDone = execState is ExecutedSuccessfullyState;
48 + bool isFailed = execState is FailureState;
49 +
50 + tester.printToConsole('isDone: $isDone');
51 + tester.printToConsole('isFailed: $isFailed');
52 +
53 + if (isDone || isFailed) {
54 + tester.printToConsole(
55 + isDone ? 'Completer is done' : 'Completer is done though operation failed');
56 + completer.complete();
57 + await tester.pump();
58 + break;
59 + } else {
60 + tester.printToConsole('Completer is not done');
61 + await tester.pump();
62 + }
63 + }
64 +
65 + await expectLater(completer.future, completes);
66 +
67 + tester.printToConsole('Done confirming sending');
68 +
69 + await commonTestCases.defaultSleepTime(seconds: 4);
70 + }
71 +
72 + Future<void> onSendButtonOnConfirmSendingDialogPressed() async {
73 + tester.printToConsole('Send Button on Confirm Dialog Triggered');
74 + await commonTestCases.defaultSleepTime(seconds: 4);
75 +
76 + final sendText = find.text(S.current.send);
77 + bool hasText = sendText.tryEvaluate();
78 +
79 + if (hasText) {
80 + await commonTestCases.tapItemByFinder(sendText);
81 +
82 + await commonTestCases.defaultSleepTime(seconds: 4);
83 + }
84 + }
85 +
86 + Future<void> onCancelButtonOnConfirmSendingDialogPressed() async {
87 + tester.printToConsole('Cancel Button on Confirm Dialog Triggered');
88 +
89 + await commonTestCases.tapItemByKey(
90 + 'exchange_trade_page_confirm_sending_dialog_cancel_button_key',
91 + );
92 +
93 + await commonTestCases.defaultSleepTime();
94 + }
95 +
96 + Future<void> onSendFailureDialogButtonPressed() async {
97 + await commonTestCases.defaultSleepTime(seconds: 6);
98 +
99 + tester.printToConsole('Send Button Failure Dialog Triggered');
100 +
101 + await commonTestCases.tapItemByKey('exchange_trade_page_send_failure_dialog_button_key');
102 + }
103 +
104 + Future<bool> hasErrorWhileSending() async {
105 + await tester.pump();
106 +
107 + tester.printToConsole('Checking if there is an error');
108 +
109 + final errorDialog = find.byKey(
110 + ValueKey('exchange_trade_page_send_failure_dialog_button_key'),
111 + );
112 +
113 + bool hasError = errorDialog.tryEvaluate();
114 +
115 + tester.printToConsole('Has error: $hasError');
116 +
117 + return hasError;
118 + }
119 +
120 + Future<void> handleConfirmSendResult() async {
121 + bool hasError = false;
122 +
123 + hasError = await hasErrorWhileSending();
124 +
125 + int maxRetries = 20;
126 + int retries = 0;
127 +
128 + while (hasError && retries < maxRetries) {
129 + tester.printToConsole('hasErrorInLoop: $hasError');
130 + await tester.pump();
131 +
132 + await onSendFailureDialogButtonPressed();
133 + tester.printToConsole('Failure button tapped');
134 +
135 + await commonTestCases.defaultSleepTime();
136 +
137 + await onConfirmSendingButtonPressed();
138 + tester.printToConsole('Confirm sending button tapped');
139 +
140 + hasError = await hasErrorWhileSending();
141 +
142 + retries++;
143 + }
144 +
145 + if (!hasError) {
146 + tester.printToConsole('No error, proceeding with flow');
147 + await tester.pump();
148 + }
149 +
150 + await commonTestCases.defaultSleepTime();
151 + }
152 +}
integration_test/robots/new_wallet_type_page_robot.dart new
+59
@@ -0,0 +1,59 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart';
3 +import 'package:cake_wallet/themes/theme_base.dart';
4 +import 'package:cw_core/wallet_type.dart';
5 +import 'package:flutter/material.dart';
6 +import 'package:flutter_test/flutter_test.dart';
7 +
8 +import '../components/common_test_cases.dart';
9 +
10 +class NewWalletTypePageRobot {
11 + NewWalletTypePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
12 +
13 + final WidgetTester tester;
14 + late CommonTestCases commonTestCases;
15 +
16 + Future<void> isNewWalletTypePage() async {
17 + await commonTestCases.isSpecificPage<NewWalletTypePage>();
18 + }
19 +
20 + void displaysCorrectTitle(bool isCreate) {
21 + commonTestCases.hasText(
22 + isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet,
23 + );
24 + }
25 +
26 + void hasWalletTypeForm() {
27 + commonTestCases.hasType<WalletTypeForm>();
28 + }
29 +
30 + void displaysCorrectImage(ThemeType type) {
31 + final walletTypeImage = Image.asset('assets/images/wallet_type.png').image;
32 + final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png').image;
33 +
34 + find.image(
35 + type == ThemeType.dark ? walletTypeImage : walletTypeLightImage,
36 + );
37 + }
38 +
39 + Future<void> findParticularWalletTypeInScrollableList(WalletType type) async {
40 + final scrollableWidget = find.descendant(
41 + of: find.byKey(Key('new_wallet_type_scrollable_key')),
42 + matching: find.byType(Scrollable),
43 + );
44 +
45 + await tester.scrollUntilVisible(
46 + find.byKey(ValueKey('new_wallet_type_${type.name}_button_key')),
47 + 300,
48 + scrollable: scrollableWidget,
49 + );
50 + }
51 +
52 + Future<void> selectWalletType(WalletType type) async {
53 + await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key');
54 + }
55 +
56 + Future<void> onNextButtonPressed() async {
57 + await commonTestCases.tapItemByKey('new_wallet_type_next_button_key');
58 + }
59 +}
integration_test/robots/pin_code_widget_robot.dart new
+38
@@ -0,0 +1,38 @@
1 +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
2 +import 'package:flutter_test/flutter_test.dart';
3 +
4 +import '../components/common_test_cases.dart';
5 +
6 +class PinCodeWidgetRobot {
7 + PinCodeWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester);
8 +
9 + final WidgetTester tester;
10 + late CommonTestCases commonTestCases;
11 +
12 + void hasPinCodeWidget() {
13 + final pinCodeWidget = find.bySubtype<PinCodeWidget>();
14 + expect(pinCodeWidget, findsOneWidget);
15 + }
16 +
17 + void hasNumberButtonsVisible() {
18 + // Confirmation for buttons 1-9
19 + for (var i = 1; i < 10; i++) {
20 + commonTestCases.hasValueKey('pin_code_button_${i}_key');
21 + }
22 +
23 + // Confirmation for 0 button
24 + commonTestCases.hasValueKey('pin_code_button_0_key');
25 + }
26 +
27 + Future<void> pushPinButton(int index) async {
28 + await commonTestCases.tapItemByKey('pin_code_button_${index}_key');
29 + }
30 +
31 + Future<void> enterPinCode(List<int> pinCode, bool isFirstEntry) async {
32 + for (int pin in pinCode) {
33 + await pushPinButton(pin);
34 + }
35 +
36 + await commonTestCases.defaultSleepTime();
37 + }
38 +}
integration_test/robots/restore_from_seed_or_key_robot.dart new
+89
@@ -0,0 +1,89 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
3 +import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart';
4 +import 'package:flutter_test/flutter_test.dart';
5 +
6 +import '../components/common_test_cases.dart';
7 +
8 +class RestoreFromSeedOrKeysPageRobot {
9 + RestoreFromSeedOrKeysPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
10 +
11 + final WidgetTester tester;
12 + late CommonTestCases commonTestCases;
13 +
14 + Future<void> isRestoreFromSeedKeyPage() async {
15 + await commonTestCases.isSpecificPage<WalletRestorePage>();
16 + }
17 +
18 + Future<void> confirmViewComponentsDisplayProperlyPerPageView() async {
19 + commonTestCases.hasText(S.current.wallet_name);
20 + commonTestCases.hasText(S.current.enter_seed_phrase);
21 + commonTestCases.hasText(S.current.restore_title_from_seed);
22 +
23 + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_textfield_key');
24 + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key');
25 + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
26 + commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key');
27 +
28 + commonTestCases.hasText(S.current.private_key, hasWidget: false);
29 + commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false);
30 +
31 + await commonTestCases.swipePage();
32 + await commonTestCases.defaultSleepTime();
33 +
34 + commonTestCases.hasText(S.current.wallet_name);
35 + commonTestCases.hasText(S.current.private_key);
36 + commonTestCases.hasText(S.current.restore_title_from_keys);
37 +
38 + commonTestCases.hasText(S.current.enter_seed_phrase, hasWidget: false);
39 + commonTestCases.hasText(S.current.restore_title_from_seed, hasWidget: false);
40 +
41 + await commonTestCases.swipePage(swipeRight: false);
42 + }
43 +
44 + void confirmRestoreButtonDisplays() {
45 + commonTestCases.hasValueKey('wallet_restore_seed_or_key_restore_button_key');
46 + }
47 +
48 + void confirmAdvancedSettingButtonDisplays() {
49 + commonTestCases.hasValueKey('wallet_restore_advanced_settings_button_key');
50 + }
51 +
52 + Future<void> enterWalletNameText(String walletName, {bool isSeedFormEntry = true}) async {
53 + await commonTestCases.enterText(
54 + walletName,
55 + 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_textfield_key',
56 + );
57 + }
58 +
59 + Future<void> selectWalletNameFromAvailableOptions({bool isSeedFormEntry = true}) async {
60 + await commonTestCases.tapItemByKey(
61 + 'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_refresh_button_key',
62 + );
63 + }
64 +
65 + Future<void> enterSeedPhraseForWalletRestore(String text) async {
66 + ValidatableAnnotatedEditableTextState seedTextState =
67 + await tester.state(find.byType(ValidatableAnnotatedEditableText));
68 +
69 + seedTextState.widget.controller.text = text;
70 + await tester.pumpAndSettle();
71 + }
72 +
73 + Future<void> onPasteSeedPhraseButtonPressed() async {
74 + await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
75 + }
76 +
77 + Future<void> enterPrivateKeyForWalletRestore(String privateKey) async {
78 + await commonTestCases.enterText(
79 + privateKey,
80 + 'wallet_restore_from_key_private_key_textfield_key',
81 + );
82 + await tester.pumpAndSettle();
83 + }
84 +
85 + Future<void> onRestoreWalletButtonPressed() async {
86 + await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key');
87 + await commonTestCases.defaultSleepTime();
88 + }
89 +}
integration_test/robots/restore_options_page_robot.dart new
+42
@@ -0,0 +1,42 @@
1 +import 'package:cake_wallet/src/screens/restore/restore_options_page.dart';
2 +import 'package:flutter_test/flutter_test.dart';
3 +
4 +import '../components/common_test_cases.dart';
5 +
6 +class RestoreOptionsPageRobot {
7 + RestoreOptionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
8 +
9 + final WidgetTester tester;
10 + late CommonTestCases commonTestCases;
11 +
12 + Future<void> isRestoreOptionsPage() async {
13 + await commonTestCases.isSpecificPage<RestoreOptionsPage>();
14 + }
15 +
16 + void hasRestoreOptionsButton() {
17 + commonTestCases.hasValueKey('restore_options_from_seeds_button_key');
18 + commonTestCases.hasValueKey('restore_options_from_backup_button_key');
19 + commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key');
20 + commonTestCases.hasValueKey('restore_options_from_qr_button_key');
21 + }
22 +
23 + Future<void> navigateToRestoreFromSeedsPage() async {
24 + await commonTestCases.tapItemByKey('restore_options_from_seeds_button_key');
25 + await commonTestCases.defaultSleepTime();
26 + }
27 +
28 + Future<void> navigateToRestoreFromBackupPage() async {
29 + await commonTestCases.tapItemByKey('restore_options_from_backup_button_key');
30 + await commonTestCases.defaultSleepTime();
31 + }
32 +
33 + Future<void> navigateToRestoreFromHardwareWalletPage() async {
34 + await commonTestCases.tapItemByKey('restore_options_from_hardware_wallet_button_key');
35 + await commonTestCases.defaultSleepTime();
36 + }
37 +
38 + Future<void> backAndVerify() async {
39 + await commonTestCases.goBack();
40 + await isRestoreOptionsPage();
41 + }
42 +}
integration_test/robots/send_page_robot.dart new
+366
@@ -0,0 +1,366 @@
1 +import 'dart:async';
2 +
3 +import 'package:cake_wallet/core/execution_state.dart';
4 +import 'package:cake_wallet/generated/i18n.dart';
5 +import 'package:cake_wallet/src/screens/send/send_page.dart';
6 +import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
7 +import 'package:cw_core/crypto_currency.dart';
8 +import 'package:cw_core/transaction_priority.dart';
9 +import 'package:flutter/foundation.dart';
10 +import 'package:flutter_test/flutter_test.dart';
11 +
12 +import '../components/common_test_cases.dart';
13 +import '../components/common_test_constants.dart';
14 +import 'auth_page_robot.dart';
15 +
16 +class SendPageRobot {
17 + SendPageRobot({required this.tester})
18 + : commonTestCases = CommonTestCases(tester),
19 + authPageRobot = AuthPageRobot(tester);
20 +
21 + WidgetTester tester;
22 + CommonTestCases commonTestCases;
23 + AuthPageRobot authPageRobot;
24 +
25 + Future<void> isSendPage() async {
26 + await commonTestCases.isSpecificPage<SendPage>();
27 + }
28 +
29 + void hasTitle() {
30 + commonTestCases.hasText(S.current.send);
31 + }
32 +
33 + void confirmViewComponentsDisplayProperly() {
34 + SendPage sendPage = tester.widget(find.byType(SendPage));
35 + final sendViewModel = sendPage.sendViewModel;
36 +
37 + commonTestCases.hasValueKey('send_page_address_textfield_key');
38 + commonTestCases.hasValueKey('send_page_note_textfield_key');
39 + commonTestCases.hasValueKey('send_page_amount_textfield_key');
40 + commonTestCases.hasValueKey('send_page_add_template_button_key');
41 +
42 + if (sendViewModel.hasMultipleTokens) {
43 + commonTestCases.hasValueKey('send_page_currency_picker_button_key');
44 + }
45 +
46 + if (!sendViewModel.isBatchSending) {
47 + commonTestCases.hasValueKey('send_page_send_all_button_key');
48 + }
49 +
50 + if (!sendViewModel.isFiatDisabled) {
51 + commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key');
52 + }
53 +
54 + if (sendViewModel.hasFees) {
55 + commonTestCases.hasValueKey('send_page_select_fee_priority_button_key');
56 + }
57 +
58 + if (sendViewModel.hasCoinControl) {
59 + commonTestCases.hasValueKey('send_page_unspent_coin_button_key');
60 + }
61 +
62 + if (sendViewModel.hasCurrecyChanger) {
63 + commonTestCases.hasValueKey('send_page_change_asset_button_key');
64 + }
65 +
66 + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) {
67 + commonTestCases.hasValueKey('send_page_add_receiver_button_key');
68 + }
69 + }
70 +
71 + Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
72 + final currencyPickerKey = 'send_page_currency_picker_button_key';
73 + final currencyPickerDialogKey = 'send_page_currency_picker_dialog_button_key';
74 +
75 + await commonTestCases.tapItemByKey(currencyPickerKey);
76 + commonTestCases.hasValueKey(currencyPickerDialogKey);
77 +
78 + SendPage sendPage = tester.widget(find.byType(SendPage));
79 + final sendViewModel = sendPage.sendViewModel;
80 +
81 + if (receiveCurrency == sendViewModel.selectedCryptoCurrency) {
82 + await commonTestCases
83 + .tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key');
84 + return;
85 + }
86 +
87 + await commonTestCases.scrollUntilVisible(
88 + 'picker_items_index_${receiveCurrency.name}_button_key',
89 + 'picker_scrollbar_key',
90 + );
91 + await commonTestCases.defaultSleepTime();
92 +
93 + await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
94 + }
95 +
96 + Future<void> enterReceiveAddress(String receiveAddress) async {
97 + await commonTestCases.enterText(receiveAddress, 'send_page_address_textfield_key');
98 + await commonTestCases.defaultSleepTime();
99 + }
100 +
101 + Future<void> enterAmount(String amount) async {
102 + await commonTestCases.enterText(amount, 'send_page_amount_textfield_key');
103 + }
104 +
105 + Future<void> selectTransactionPriority({TransactionPriority? priority}) async {
106 + SendPage sendPage = tester.widget(find.byType(SendPage));
107 + final sendViewModel = sendPage.sendViewModel;
108 +
109 + if (!sendViewModel.hasFees || priority == null) return;
110 +
111 + final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key';
112 + await commonTestCases.tapItemByKey(transactionPriorityPickerKey);
113 +
114 + if (priority == sendViewModel.transactionPriority) {
115 + await commonTestCases
116 + .tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key');
117 + return;
118 + }
119 +
120 + await commonTestCases.scrollUntilVisible(
121 + 'picker_items_index_${priority.title}_button_key',
122 + 'picker_scrollbar_key',
123 + );
124 + await commonTestCases.defaultSleepTime();
125 +
126 + await commonTestCases.tapItemByKey('picker_items_index_${priority.title}_button_key');
127 + }
128 +
129 + Future<void> onSendButtonPressed() async {
130 + tester.printToConsole('Pressing send');
131 +
132 + await commonTestCases.tapItemByKey(
133 + 'send_page_send_button_key',
134 + shouldPumpAndSettle: false,
135 + );
136 +
137 + await _waitForSendTransactionCompletion();
138 +
139 + await commonTestCases.defaultSleepTime();
140 + }
141 +
142 + Future<void> _waitForSendTransactionCompletion() async {
143 + await tester.pump();
144 + final Completer<void> completer = Completer<void>();
145 +
146 + // Loop to wait for the async operation to complete
147 + while (true) {
148 + await Future.delayed(Duration(seconds: 1));
149 +
150 + tester.printToConsole('Before _handleAuth');
151 +
152 + await _handleAuthPage();
153 +
154 + tester.printToConsole('After _handleAuth');
155 +
156 + await tester.pump();
157 +
158 + final sendPage = tester.widget<SendPage>(find.byType(SendPage));
159 + final state = sendPage.sendViewModel.state;
160 +
161 + await tester.pump();
162 +
163 + bool isDone = state is ExecutedSuccessfullyState;
164 + bool isFailed = state is FailureState;
165 +
166 + tester.printToConsole('isDone: $isDone');
167 + tester.printToConsole('isFailed: $isFailed');
168 +
169 + if (isDone || isFailed) {
170 + tester.printToConsole(
171 + isDone ? 'Completer is done' : 'Completer is done though operation failed',
172 + );
173 + completer.complete();
174 + await tester.pump();
175 + break;
176 + } else {
177 + tester.printToConsole('Completer is not done');
178 + await tester.pump();
179 + }
180 + }
181 +
182 + await expectLater(completer.future, completes);
183 +
184 + tester.printToConsole('Done confirming sending operation');
185 + }
186 +
187 + Future<void> _handleAuthPage() async {
188 + tester.printToConsole('Inside _handleAuth');
189 + await tester.pump();
190 + tester.printToConsole('starting auth checks');
191 +
192 + final authPage = authPageRobot.onAuthPage();
193 +
194 + tester.printToConsole('hasAuth:$authPage');
195 +
196 + if (authPage) {
197 + await tester.pump();
198 + tester.printToConsole('Starting inner _handleAuth loop checks');
199 +
200 + try {
201 + await authPageRobot.enterPinCode(CommonTestConstants.pin, false);
202 + tester.printToConsole('Auth done');
203 +
204 + await tester.pump();
205 +
206 + tester.printToConsole('Auth pump done');
207 + } catch (e) {
208 + tester.printToConsole('Auth failed, retrying');
209 + await tester.pump();
210 + _handleAuthPage();
211 + }
212 + }
213 + }
214 +
215 + Future<void> handleSendResult() async {
216 + tester.printToConsole('Inside handle function');
217 +
218 + bool hasError = false;
219 +
220 + hasError = await hasErrorWhileSending();
221 +
222 + tester.printToConsole('Has an Error in the handle: $hasError');
223 +
224 + int maxRetries = 20;
225 + int retries = 0;
226 +
227 + while (hasError && retries < maxRetries) {
228 + tester.printToConsole('hasErrorInLoop: $hasError');
229 + await tester.pump();
230 +
231 + await onSendFailureDialogButtonPressed();
232 + tester.printToConsole('Failure button tapped');
233 +
234 + await commonTestCases.defaultSleepTime();
235 +
236 + await onSendButtonPressed();
237 + tester.printToConsole('Send button tapped');
238 +
239 + hasError = await hasErrorWhileSending();
240 +
241 + retries++;
242 + }
243 +
244 + if (!hasError) {
245 + tester.printToConsole('No error, proceeding with flow');
246 + await tester.pump();
247 + }
248 +
249 + await commonTestCases.defaultSleepTime();
250 + }
251 +
252 + //* ------ On Sending Failure ------------
253 + Future<bool> hasErrorWhileSending() async {
254 + await tester.pump();
255 +
256 + tester.printToConsole('Checking if there is an error');
257 +
258 + final errorDialog = find.byKey(ValueKey('send_page_send_failure_dialog_button_key'));
259 +
260 + bool hasError = errorDialog.tryEvaluate();
261 +
262 + tester.printToConsole('Has error: $hasError');
263 +
264 + return hasError;
265 + }
266 +
267 + Future<void> onSendFailureDialogButtonPressed() async {
268 + await commonTestCases.defaultSleepTime();
269 +
270 + tester.printToConsole('Send Button Failure Dialog Triggered');
271 +
272 + await commonTestCases.tapItemByKey('send_page_send_failure_dialog_button_key');
273 + }
274 +
275 + //* ------ On Sending Success ------------
276 + Future<void> onSendButtonOnConfirmSendingDialogPressed() async {
277 + tester.printToConsole('Inside confirm sending dialog: For sending');
278 + await commonTestCases.defaultSleepTime();
279 + await tester.pump();
280 +
281 + final sendText = find.text(S.current.send).last;
282 + bool hasText = sendText.tryEvaluate();
283 + tester.printToConsole('Has Text: $hasText');
284 +
285 + if (hasText) {
286 + await commonTestCases.tapItemByFinder(sendText, shouldPumpAndSettle: false);
287 + // Loop to wait for the operation to commit transaction
288 + await _waitForCommitTransactionCompletion();
289 +
290 + await commonTestCases.defaultSleepTime(seconds: 4);
291 + } else {
292 + await commonTestCases.defaultSleepTime();
293 + await tester.pump();
294 + onSendButtonOnConfirmSendingDialogPressed();
295 + }
296 + }
297 +
298 + Future<void> _waitForCommitTransactionCompletion() async {
299 + final Completer<void> completer = Completer<void>();
300 +
301 + while (true) {
302 + await Future.delayed(Duration(seconds: 1));
303 +
304 + final sendPage = tester.widget<SendPage>(find.byType(SendPage));
305 + final state = sendPage.sendViewModel.state;
306 +
307 + bool isDone = state is TransactionCommitted;
308 + bool isFailed = state is FailureState;
309 +
310 + tester.printToConsole('isDone: $isDone');
311 + tester.printToConsole('isFailed: $isFailed');
312 +
313 + if (isDone || isFailed) {
314 + tester.printToConsole(
315 + isDone ? 'Completer is done' : 'Completer is done though operation failed',
316 + );
317 + completer.complete();
318 + await tester.pump();
319 + break;
320 + } else {
321 + tester.printToConsole('Completer is not done');
322 + await tester.pump();
323 + }
324 + }
325 +
326 + await expectLater(completer.future, completes);
327 +
328 + tester.printToConsole('Done Committing Transaction');
329 + }
330 +
331 + Future<void> onCancelButtonOnConfirmSendingDialogPressed() async {
332 + tester.printToConsole('Inside confirm sending dialog: For canceling');
333 + await commonTestCases.defaultSleepTime(seconds: 4);
334 +
335 + final cancelText = find.text(S.current.cancel);
336 + bool hasText = cancelText.tryEvaluate();
337 +
338 + if (hasText) {
339 + await commonTestCases.tapItemByFinder(cancelText);
340 +
341 + await commonTestCases.defaultSleepTime(seconds: 4);
342 + }
343 + }
344 +
345 + //* ---- Add Contact Dialog On Send Successful Dialog -----
346 + Future<void> onSentDialogPopUp() async {
347 + SendPage sendPage = tester.widget(find.byType(SendPage));
348 + final sendViewModel = sendPage.sendViewModel;
349 +
350 + final newContactAddress = sendPage.newContactAddress ?? sendViewModel.newContactAddress();
351 + if (newContactAddress != null) {
352 + await _onAddContactButtonOnSentDialogPressed();
353 + }
354 +
355 + await commonTestCases.defaultSleepTime();
356 + }
357 +
358 + Future<void> _onAddContactButtonOnSentDialogPressed() async {
359 + await commonTestCases.tapItemByKey('send_page_sent_dialog_add_contact_button_key');
360 + }
361 +
362 + // ignore: unused_element
363 + Future<void> _onIgnoreButtonOnSentDialogPressed() async {
364 + await commonTestCases.tapItemByKey('send_page_sent_dialog_ignore_button_key');
365 + }
366 +}
\ No newline at end of file
integration_test/robots/setup_pin_code_robot.dart new
+28
@@ -0,0 +1,28 @@
1 +import 'package:cake_wallet/generated/i18n.dart';
2 +import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart';
3 +import 'package:flutter_test/flutter_test.dart';
4 +
5 +import '../components/common_test_cases.dart';
6 +import 'pin_code_widget_robot.dart';
7 +
8 +class SetupPinCodeRobot extends PinCodeWidgetRobot {
9 + SetupPinCodeRobot(this.tester)
10 + : commonTestCases = CommonTestCases(tester),
11 + super(tester);
12 +
13 + final WidgetTester tester;
14 + late CommonTestCases commonTestCases;
15 +
16 + Future<void> isSetupPinCodePage() async {
17 + await commonTestCases.isSpecificPage<SetupPinCodePage>();
18 + }
19 +
20 + void hasTitle() {
21 + commonTestCases.hasText(S.current.setup_pin);
22 + }
23 +
24 + Future<void> tapSuccessButton() async {
25 + await commonTestCases.tapItemByKey('setup_pin_code_success_button_key');
26 + await commonTestCases.defaultSleepTime();
27 + }
28 +}
integration_test/robots/welcome_page_robot.dart new
+40
@@ -0,0 +1,40 @@
1 +import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
2 +import 'package:flutter/material.dart';
3 +import 'package:flutter_test/flutter_test.dart';
4 +
5 +import '../components/common_test_cases.dart';
6 +
7 +class WelcomePageRobot {
8 + WelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
9 +
10 + final WidgetTester tester;
11 + late CommonTestCases commonTestCases;
12 +
13 + Future<void> isWelcomePage() async {
14 + await commonTestCases.isSpecificPage<WelcomePage>();
15 + }
16 +
17 + void confirmActionButtonsDisplay() {
18 + final createNewWalletButton = find.byKey(ValueKey('welcome_page_create_new_wallet_button_key'));
19 +
20 + final restoreWalletButton = find.byKey(ValueKey('welcome_page_restore_wallet_button_key'));
21 +
22 + expect(createNewWalletButton, findsOneWidget);
23 + expect(restoreWalletButton, findsOneWidget);
24 + }
25 +
26 + Future<void> navigateToCreateNewWalletPage() async {
27 + await commonTestCases.tapItemByKey('welcome_page_create_new_wallet_button_key');
28 + await commonTestCases.defaultSleepTime();
29 + }
30 +
31 + Future<void> navigateToRestoreWalletPage() async {
32 + await commonTestCases.tapItemByKey('welcome_page_restore_wallet_button_key');
33 + await commonTestCases.defaultSleepTime();
34 + }
35 +
36 + Future<void> backAndVerify() async {
37 + await commonTestCases.goBack();
38 + await isWelcomePage();
39 + }
40 +}
integration_test/test_suites/exchange_flow_test.dart new
+59
@@ -0,0 +1,59 @@
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter_test/flutter_test.dart';
3 +import 'package:integration_test/integration_test.dart';
4 +
5 +import '../components/common_test_constants.dart';
6 +import '../components/common_test_flows.dart';
7 +import '../robots/auth_page_robot.dart';
8 +import '../robots/dashboard_page_robot.dart';
9 +import '../robots/exchange_confirm_page_robot.dart';
10 +import '../robots/exchange_page_robot.dart';
11 +import '../robots/exchange_trade_page_robot.dart';
12 +
13 +void main() {
14 + IntegrationTestWidgetsFlutterBinding.ensureInitialized();
15 +
16 + AuthPageRobot authPageRobot;
17 + CommonTestFlows commonTestFlows;
18 + ExchangePageRobot exchangePageRobot;
19 + DashboardPageRobot dashboardPageRobot;
20 + ExchangeTradePageRobot exchangeTradePageRobot;
21 + ExchangeConfirmPageRobot exchangeConfirmPageRobot;
22 +
23 + group('Exchange Flow Tests', () {
24 + testWidgets('Exchange flow', (tester) async {
25 + authPageRobot = AuthPageRobot(tester);
26 + commonTestFlows = CommonTestFlows(tester);
27 + exchangePageRobot = ExchangePageRobot(tester);
28 + dashboardPageRobot = DashboardPageRobot(tester);
29 + exchangeTradePageRobot = ExchangeTradePageRobot(tester);
30 + exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
31 +
32 + await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key'));
33 + await commonTestFlows.restoreWalletThroughSeedsFlow();
34 + await dashboardPageRobot.navigateToExchangePage();
35 +
36 + // ----------- Exchange Page -------------
37 + await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency);
38 + await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency);
39 +
40 + await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
41 + await exchangePageRobot.enterDepositRefundAddress(
42 + depositAddress: CommonTestConstants.testWalletAddress,
43 + );
44 + await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
45 +
46 + await exchangePageRobot.onExchangeButtonPressed();
47 +
48 + await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
49 +
50 + final onAuthPage = authPageRobot.onAuthPage();
51 + if (onAuthPage) {
52 + await authPageRobot.enterPinCode(CommonTestConstants.pin, false);
53 + }
54 +
55 + await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
56 + await exchangeTradePageRobot.onGotItButtonPressed();
57 + });
58 + });
59 +}
integration_test/test_suites/send_flow_test.dart new
+41
@@ -0,0 +1,41 @@
1 +import 'package:flutter/material.dart';
2 +import 'package:flutter_test/flutter_test.dart';
3 +import 'package:integration_test/integration_test.dart';
4 +
5 +import '../components/common_test_constants.dart';
6 +import '../components/common_test_flows.dart';
7 +import '../robots/dashboard_page_robot.dart';
8 +import '../robots/send_page_robot.dart';
9 +
10 +void main() {
11 + IntegrationTestWidgetsFlutterBinding.ensureInitialized();
12 +
13 + SendPageRobot sendPageRobot;
14 + CommonTestFlows commonTestFlows;
15 + DashboardPageRobot dashboardPageRobot;
16 +
17 + group('Send Flow Tests', () {
18 + testWidgets('Send flow', (tester) async {
19 + commonTestFlows = CommonTestFlows(tester);
20 + sendPageRobot = SendPageRobot(tester: tester);
21 + dashboardPageRobot = DashboardPageRobot(tester);
22 +
23 + await commonTestFlows.startAppFlow(ValueKey('send_test_app_key'));
24 + await commonTestFlows.restoreWalletThroughSeedsFlow();
25 + await dashboardPageRobot.navigateToSendPage();
26 +
27 + await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
28 + await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency);
29 + await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount);
30 + await sendPageRobot.selectTransactionPriority();
31 +
32 + await sendPageRobot.onSendButtonPressed();
33 +
34 + await sendPageRobot.handleSendResult();
35 +
36 + await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed();
37 +
38 + await sendPageRobot.onSentDialogPopUp();
39 + });
40 + });
41 +}
ios/Podfile.lock
+9
@@ -66,6 +66,8 @@ PODS:
66 - Toast
67 - in_app_review (0.2.0):
68 - Flutter
69 + - integration_test (0.0.1):
70 + - Flutter
71 - MTBBarcodeScanner (5.0.11)
72 - OrderedSet (5.0.0)
73 - package_info_plus (0.4.5):
@@ -120,6 +122,8 @@ DEPENDENCIES:
122 - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
123 - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
124 - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
125 + - integration_test (from `.symlinks/plugins/integration_test/ios`)
126 + - package_info (from `.symlinks/plugins/package_info/ios`)
127 - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
128 - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
129 - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
@@ -174,6 +178,10 @@ EXTERNAL SOURCES:
178 :path: ".symlinks/plugins/fluttertoast/ios"
179 in_app_review:
180 :path: ".symlinks/plugins/in_app_review/ios"
181 + integration_test:
182 + :path: ".symlinks/plugins/integration_test/ios"
183 + package_info:
184 + :path: ".symlinks/plugins/package_info/ios"
185 package_info_plus:
186 :path: ".symlinks/plugins/package_info_plus/ios"
187 path_provider_foundation:
@@ -216,6 +224,7 @@ SPEC CHECKSUMS:
224 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
225 fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c
226 in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
227 + integration_test: 13825b8a9334a850581300559b8839134b124670
228 MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
229 OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
230 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c
lib/main.dart
+9 -5
@@ -47,11 +47,11 @@ final navigatorKey = GlobalKey<NavigatorState>();
47 final rootKey = GlobalKey<RootState>();
48 final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>();
49
50 -Future<void> main() async {
51 - await runAppWithZone();
50 +Future<void> main({Key? topLevelKey}) async {
51 + await runAppWithZone(topLevelKey: topLevelKey);
52 }
53
54 -Future<void> runAppWithZone() async {
54 +Future<void> runAppWithZone({Key? topLevelKey}) async {
55 bool isAppRunning = false;
56
57 await runZonedGuarded(() async {
@@ -67,7 +67,8 @@ Future<void> runAppWithZone() async {
67 };
68 await initializeAppAtRoot();
69
70 - runApp(App());
70 + runApp(App(key: topLevelKey));
71 +
72 isAppRunning = true;
73 }, (error, stackTrace) async {
74 if (!isAppRunning) {
@@ -236,6 +237,9 @@ Future<void> initialSetup(
237 }
238
239 class App extends StatefulWidget {
240 + App({this.key});
241 +
242 + final Key? key;
243 @override
244 AppState createState() => AppState();
245 }
@@ -264,7 +268,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
268 statusBarIconBrightness: statusBarIconBrightness));
269
270 return Root(
267 - key: rootKey,
271 + key: widget.key ?? rootKey,
272 appStore: appStore,
273 authenticationStore: authenticationStore,
274 navigatorKey: navigatorKey,
lib/src/screens/dashboard/dashboard_page.dart
+6
@@ -147,6 +147,7 @@ class _DashboardPageView extends BasePage {
147 return Observer(
148 builder: (context) {
149 return ServicesUpdatesWidget(
150 + key: ValueKey('dashboard_page_services_update_button_key'),
151 dashboardViewModel.getServicesStatus(),
152 enabled: dashboardViewModel.isEnabledBulletinAction,
153 );
@@ -157,6 +158,7 @@ class _DashboardPageView extends BasePage {
158 @override
159 Widget middle(BuildContext context) {
160 return SyncIndicator(
161 + key: ValueKey('dashboard_page_sync_indicator_button_key'),
162 dashboardViewModel: dashboardViewModel,
163 onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync),
164 );
@@ -173,6 +175,7 @@ class _DashboardPageView extends BasePage {
175 alignment: Alignment.centerRight,
176 width: 40,
177 child: TextButton(
178 + key: ValueKey('dashboard_page_wallet_menu_button_key'),
179 // FIX-ME: Style
180 //highlightColor: Colors.transparent,
181 //splashColor: Colors.transparent,
@@ -226,6 +229,7 @@ class _DashboardPageView extends BasePage {
229 child: Observer(
230 builder: (context) {
231 return PageView.builder(
232 + key: ValueKey('dashboard_page_view_key'),
233 controller: controller,
234 itemCount: pages.length,
235 itemBuilder: (context, index) => pages[index],
@@ -291,6 +295,8 @@ class _DashboardPageView extends BasePage {
295 button: true,
296 enabled: (action.isEnabled?.call(dashboardViewModel) ?? true),
297 child: ActionButton(
298 + key: ValueKey(
299 + 'dashboard_page_${action.name(context)}_action_button_key'),
300 image: Image.asset(
301 action.image,
302 height: 24,
lib/src/screens/dashboard/widgets/action_button.dart
+9 -7
@@ -2,13 +2,15 @@ import 'package:flutter/material.dart';
2 import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
3
4 class ActionButton extends StatelessWidget {
5 - ActionButton(
6 - {required this.image,
7 - required this.title,
8 - this.route,
9 - this.onClick,
10 - this.alignment = Alignment.center,
11 - this.textColor});
5 + ActionButton({
6 + required this.image,
7 + required this.title,
8 + this.route,
9 + this.onClick,
10 + this.alignment = Alignment.center,
11 + this.textColor,
12 + super.key,
13 + });
14
15 final Image image;
16 final String title;
lib/src/screens/dashboard/widgets/sync_indicator.dart
+5 -1
@@ -7,7 +7,11 @@ import 'package:cw_core/sync_status.dart';
7 import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
8
9 class SyncIndicator extends StatelessWidget {
10 - SyncIndicator({required this.dashboardViewModel, required this.onTap});
10 + SyncIndicator({
11 + required this.dashboardViewModel,
12 + required this.onTap,
13 + super.key,
14 + });
15
16 final DashboardViewModel dashboardViewModel;
17 final Function() onTap;
lib/src/screens/disclaimer/disclaimer_page.dart
+3
@@ -207,6 +207,7 @@ class DisclaimerBodyState extends State<DisclaimerPageBody> {
207 padding: EdgeInsets.only(
208 left: 24.0, top: 10.0, right: 24.0, bottom: 10.0),
209 child: InkWell(
210 + key: ValueKey('disclaimer_check_key'),
211 onTap: () {
212 setState(() {
213 _checked = !_checked;
@@ -230,6 +231,7 @@ class DisclaimerBodyState extends State<DisclaimerPageBody> {
231 color: Theme.of(context).colorScheme.background),
232 child: _checked
233 ? Icon(
234 + key: ValueKey('disclaimer_check_icon_key'),
235 Icons.check,
236 color: Colors.blue,
237 size: 20.0,
@@ -253,6 +255,7 @@ class DisclaimerBodyState extends State<DisclaimerPageBody> {
255 padding:
256 EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0),
257 child: PrimaryButton(
258 + key: ValueKey('disclaimer_accept_button_key'),
259 onPressed: _checked
260 ? () => Navigator.of(context)
261 .popAndPushNamed(Routes.welcome)
lib/src/screens/exchange/exchange_page.dart
+5
@@ -228,6 +228,7 @@ class ExchangePage extends BasePage {
228 ),
229 Observer(
230 builder: (_) => LoadingPrimaryButton(
231 + key: ValueKey('exchange_page_exchange_button_key'),
232 text: S.of(context).exchange,
233 onPressed: () {
234 if (_formKey.currentState != null &&
@@ -430,6 +431,8 @@ class ExchangePage extends BasePage {
431 context: context,
432 builder: (BuildContext context) {
433 return AlertWithOneAction(
434 + key: ValueKey('exchange_page_trade_creation_failure_dialog_key'),
435 + buttonKey: ValueKey('exchange_page_trade_creation_failure_dialog_button_key'),
436 alertTitle: S.of(context).provider_error(state.title),
437 alertContent: state.error,
438 buttonText: S.of(context).ok,
@@ -612,6 +615,7 @@ class ExchangePage extends BasePage {
615 Widget _exchangeCardsSection(BuildContext context) {
616 final firstExchangeCard = Observer(
617 builder: (_) => ExchangeCard(
618 + cardInstanceName: 'deposit_exchange_card',
619 onDispose: disposeBestRateSync,
620 hasAllAmount: exchangeViewModel.hasAllAmount,
621 allAmount: exchangeViewModel.hasAllAmount
@@ -681,6 +685,7 @@ class ExchangePage extends BasePage {
685
686 final secondExchangeCard = Observer(
687 builder: (_) => ExchangeCard(
688 + cardInstanceName: 'receive_exchange_card',
689 onDispose: disposeBestRateSync,
690 amountFocusNode: _receiveAmountFocus,
691 addressFocusNode: _receiveAddressFocus,
lib/src/screens/exchange/exchange_template_page.dart
+2
@@ -121,6 +121,7 @@ class ExchangeTemplatePage extends BasePage {
121 padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
122 child: Observer(
123 builder: (_) => ExchangeCard(
124 + cardInstanceName: 'deposit_exchange_template_card',
125 amountFocusNode: _depositAmountFocus,
126 key: depositKey,
127 title: S.of(context).you_will_send,
@@ -157,6 +158,7 @@ class ExchangeTemplatePage extends BasePage {
158 padding: EdgeInsets.only(top: 29, left: 24, right: 24),
159 child: Observer(
160 builder: (_) => ExchangeCard(
161 + cardInstanceName: 'receive_exchange_template_card',
162 amountFocusNode: _receiveAmountFocus,
163 key: receiveKey,
164 title: S.of(context).you_will_get,
lib/src/screens/exchange/widgets/currency_picker.dart
+2 -1
@@ -12,7 +12,8 @@ class CurrencyPicker extends StatefulWidget {
12 this.title,
13 this.hintText,
14 this.isMoneroWallet = false,
15 - this.isConvertFrom = false});
15 + this.isConvertFrom = false,
16 + super.key});
17
18 final int selectedAtIndex;
19 final List<Currency> items;
lib/src/screens/exchange/widgets/exchange_card.dart
+72 -44
@@ -19,34 +19,35 @@ import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
19 import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
20
21 class ExchangeCard extends StatefulWidget {
22 - ExchangeCard(
23 - {Key? key,
24 - required this.initialCurrency,
25 - required this.initialAddress,
26 - required this.initialWalletName,
27 - required this.initialIsAmountEditable,
28 - required this.isAmountEstimated,
29 - required this.currencies,
30 - required this.onCurrencySelected,
31 - this.imageArrow,
32 - this.currencyValueValidator,
33 - this.addressTextFieldValidator,
34 - this.title = '',
35 - this.initialIsAddressEditable = true,
36 - this.hasRefundAddress = false,
37 - this.isMoneroWallet = false,
38 - this.currencyButtonColor = Colors.transparent,
39 - this.addressButtonsColor = Colors.transparent,
40 - this.borderColor = Colors.transparent,
41 - this.hasAllAmount = false,
42 - this.isAllAmountEnabled = false,
43 - this.amountFocusNode,
44 - this.addressFocusNode,
45 - this.allAmount,
46 - this.onPushPasteButton,
47 - this.onPushAddressBookButton,
48 - this.onDispose})
49 - : super(key: key);
22 + ExchangeCard({
23 + Key? key,
24 + required this.initialCurrency,
25 + required this.initialAddress,
26 + required this.initialWalletName,
27 + required this.initialIsAmountEditable,
28 + required this.isAmountEstimated,
29 + required this.currencies,
30 + required this.onCurrencySelected,
31 + this.imageArrow,
32 + this.currencyValueValidator,
33 + this.addressTextFieldValidator,
34 + this.title = '',
35 + this.initialIsAddressEditable = true,
36 + this.hasRefundAddress = false,
37 + this.isMoneroWallet = false,
38 + this.currencyButtonColor = Colors.transparent,
39 + this.addressButtonsColor = Colors.transparent,
40 + this.borderColor = Colors.transparent,
41 + this.hasAllAmount = false,
42 + this.isAllAmountEnabled = false,
43 + this.amountFocusNode,
44 + this.addressFocusNode,
45 + this.allAmount,
46 + this.onPushPasteButton,
47 + this.onPushAddressBookButton,
48 + this.onDispose,
49 + required this.cardInstanceName,
50 + }) : super(key: key);
51
52 final List<CryptoCurrency> currencies;
53 final Function(CryptoCurrency) onCurrencySelected;
@@ -74,6 +75,7 @@ class ExchangeCard extends StatefulWidget {
75 final void Function(BuildContext context)? onPushPasteButton;
76 final void Function(BuildContext context)? onPushAddressBookButton;
77 final Function()? onDispose;
78 + final String cardInstanceName;
79
80 @override
81 ExchangeCardState createState() => ExchangeCardState();
@@ -89,11 +91,13 @@ class ExchangeCardState extends State<ExchangeCard> {
91 _walletName = '',
92 _selectedCurrency = CryptoCurrency.btc,
93 _isAmountEstimated = false,
92 - _isMoneroWallet = false;
94 + _isMoneroWallet = false,
95 + _cardInstanceName = '';
96
97 final addressController = TextEditingController();
98 final amountController = TextEditingController();
99
100 + String _cardInstanceName;
101 String _title;
102 String? _min;
103 String? _max;
@@ -106,6 +110,7 @@ class ExchangeCardState extends State<ExchangeCard> {
110
111 @override
112 void initState() {
113 + _cardInstanceName = widget.cardInstanceName;
114 _title = widget.title;
115 _isAmountEditable = widget.initialIsAmountEditable;
116 _isAddressEditable = widget.initialIsAddressEditable;
@@ -184,6 +189,7 @@ class ExchangeCardState extends State<ExchangeCard> {
189 mainAxisAlignment: MainAxisAlignment.start,
190 children: <Widget>[
191 Text(
192 + key: ValueKey('${_cardInstanceName}_title_key'),
193 _title,
194 style: TextStyle(
195 fontSize: 18,
@@ -193,17 +199,26 @@ class ExchangeCardState extends State<ExchangeCard> {
199 ],
200 ),
201 CurrencyAmountTextField(
196 - imageArrow: widget.imageArrow,
197 - selectedCurrency: _selectedCurrency.toString(),
198 - amountFocusNode: widget.amountFocusNode,
199 - amountController: amountController,
200 - onTapPicker: () => _presentPicker(context),
201 - isAmountEditable: _isAmountEditable,
202 - isPickerEnable: true,
203 - allAmountButton: widget.hasAllAmount,
204 - currencyValueValidator: widget.currencyValueValidator,
205 - tag: _selectedCurrency.tag,
206 - allAmountCallback: widget.allAmount),
202 + currencyPickerButtonKey: ValueKey('${_cardInstanceName}_currency_picker_button_key'),
203 + selectedCurrencyTextKey: ValueKey('${_cardInstanceName}_selected_currency_text_key'),
204 + selectedCurrencyTagTextKey:
205 + ValueKey('${_cardInstanceName}_selected_currency_tag_text_key'),
206 + amountTextfieldKey: ValueKey('${_cardInstanceName}_amount_textfield_key'),
207 + sendAllButtonKey: ValueKey('${_cardInstanceName}_send_all_button_key'),
208 + currencyAmountTextFieldWidgetKey:
209 + ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'),
210 + imageArrow: widget.imageArrow,
211 + selectedCurrency: _selectedCurrency.toString(),
212 + amountFocusNode: widget.amountFocusNode,
213 + amountController: amountController,
214 + onTapPicker: () => _presentPicker(context),
215 + isAmountEditable: _isAmountEditable,
216 + isPickerEnable: true,
217 + allAmountButton: widget.hasAllAmount,
218 + currencyValueValidator: widget.currencyValueValidator,
219 + tag: _selectedCurrency.tag,
220 + allAmountCallback: widget.allAmount,
221 + ),
222 Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
223 Padding(
224 padding: EdgeInsets.only(top: 5),
@@ -212,6 +227,7 @@ class ExchangeCardState extends State<ExchangeCard> {
227 child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
228 _min != null
229 ? Text(
230 + key: ValueKey('${_cardInstanceName}_min_limit_text_key'),
231 S.of(context).min_value(_min ?? '', _selectedCurrency.toString()),
232 style: TextStyle(
233 fontSize: 10,
@@ -221,11 +237,15 @@ class ExchangeCardState extends State<ExchangeCard> {
237 : Offstage(),
238 _min != null ? SizedBox(width: 10) : Offstage(),
239 _max != null
224 - ? Text(S.of(context).max_value(_max ?? '', _selectedCurrency.toString()),
240 + ? Text(
241 + key: ValueKey('${_cardInstanceName}_max_limit_text_key'),
242 + S.of(context).max_value(_max ?? '', _selectedCurrency.toString()),
243 style: TextStyle(
226 - fontSize: 10,
227 - height: 1.2,
228 - color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor))
244 + fontSize: 10,
245 + height: 1.2,
246 + color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor,
247 + ),
248 + )
249 : Offstage(),
250 ])),
251 ),
@@ -246,6 +266,7 @@ class ExchangeCardState extends State<ExchangeCard> {
266 child: Padding(
267 padding: EdgeInsets.only(top: 20),
268 child: AddressTextField(
269 + addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
270 focusNode: widget.addressFocusNode,
271 controller: addressController,
272 onURIScanned: (uri) {
@@ -286,6 +307,8 @@ class ExchangeCardState extends State<ExchangeCard> {
307 FocusTraversalOrder(
308 order: NumericFocusOrder(3),
309 child: BaseTextFormField(
310 + key: ValueKey(
311 + '${_cardInstanceName}_non_editable_address_textfield_key'),
312 controller: addressController,
313 borderColor: Colors.transparent,
314 suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36),
@@ -309,6 +332,8 @@ class ExchangeCardState extends State<ExchangeCard> {
332 child: Semantics(
333 label: S.of(context).address_book,
334 child: InkWell(
335 + key: ValueKey(
336 + '${_cardInstanceName}_address_book_button_key'),
337 onTap: () async {
338 final contact =
339 await Navigator.of(context).pushNamed(
@@ -346,6 +371,8 @@ class ExchangeCardState extends State<ExchangeCard> {
371 child: Semantics(
372 label: S.of(context).copy_address,
373 child: InkWell(
374 + key: ValueKey(
375 + '${_cardInstanceName}_copy_refund_address_button_key'),
376 onTap: () {
377 Clipboard.setData(
378 ClipboardData(text: addressController.text));
@@ -369,6 +396,7 @@ class ExchangeCardState extends State<ExchangeCard> {
396 showPopUp<void>(
397 context: context,
398 builder: (_) => CurrencyPicker(
399 + key: ValueKey('${_cardInstanceName}_currency_picker_dialog_button_key'),
400 selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
401 items: widget.currencies,
402 hintText: S.of(context).search_currency,
lib/src/screens/exchange_trade/exchange_confirm_page.dart
+2
@@ -83,6 +83,7 @@ class ExchangeConfirmPage extends BasePage {
83 padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
84 child: Builder(
85 builder: (context) => PrimaryButton(
86 + key: ValueKey('exchange_confirm_page_copy_to_clipboard_button_key'),
87 onPressed: () {
88 Clipboard.setData(ClipboardData(text: trade.id));
89 showBar<void>(
@@ -117,6 +118,7 @@ class ExchangeConfirmPage extends BasePage {
118 ],
119 )),
120 PrimaryButton(
121 + key: ValueKey('exchange_confirm_page_saved_id_button_key'),
122 onPressed: () => Navigator.of(context)
123 .pushReplacementNamed(Routes.exchangeTrade),
124 text: S.of(context).saved_the_trade_id,
lib/src/screens/exchange_trade/exchange_trade_page.dart
+10 -1
@@ -39,7 +39,9 @@ void showInformation(
39
40 showPopUp<void>(
41 context: context,
42 - builder: (_) => InformationPage(information: information));
42 + builder: (_) => InformationPage(
43 + key: ValueKey('information_page_dialog_key'),
44 + information: information));
45 }
46
47 class ExchangeTradePage extends BasePage {
@@ -215,6 +217,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
217 return widget.exchangeTradeViewModel.isSendable &&
218 !(sendingState is TransactionCommitted)
219 ? LoadingPrimaryButton(
220 + key: ValueKey('exchange_trade_page_confirm_sending_button_key'),
221 isDisabled: trade.inputAddress == null ||
222 trade.inputAddress!.isEmpty,
223 isLoading: sendingState is IsExecutingState,
@@ -241,6 +244,8 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
244 context: context,
245 builder: (BuildContext popupContext) {
246 return AlertWithOneAction(
247 + key: ValueKey('exchange_trade_page_send_failure_dialog_key'),
248 + buttonKey: ValueKey('exchange_trade_page_send_failure_dialog_button_key'),
249 alertTitle: S.of(popupContext).error,
250 alertContent: state.error,
251 buttonText: S.of(popupContext).ok,
@@ -255,6 +260,10 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
260 context: context,
261 builder: (BuildContext popupContext) {
262 return ConfirmSendingAlert(
263 + key: ValueKey('exchange_trade_page_confirm_sending_dialog_key'),
264 + alertLeftActionButtonKey: ValueKey('exchange_trade_page_confirm_sending_dialog_cancel_button_key'),
265 + alertRightActionButtonKey:
266 + ValueKey('exchange_trade_page_confirm_sending_dialog_send_button_key'),
267 alertTitle: S.of(popupContext).confirm_sending,
268 amount: S.of(popupContext).send_amount,
269 amountValue: widget.exchangeTradeViewModel.sendViewModel
lib/src/screens/exchange_trade/information_page.dart
+2 -1
@@ -10,7 +10,7 @@ import 'package:cake_wallet/src/widgets/alert_background.dart';
10 import 'package:cake_wallet/themes/extensions/menu_theme.dart';
11
12 class InformationPage extends StatelessWidget {
13 - InformationPage({required this.information});
13 + InformationPage({required this.information, super.key});
14
15 final String information;
16
@@ -47,6 +47,7 @@ class InformationPage extends StatelessWidget {
47 Padding(
48 padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
49 child: PrimaryButton(
50 + key: ValueKey('information_page_got_it_button_key'),
51 onPressed: () => Navigator.of(context).pop(),
52 text: S.of(context).got_it,
53 color: Theme.of(context).extension<ExchangePageTheme>()!.buttonBackgroundColor,
lib/src/screens/new_wallet/new_wallet_type_page.dart
+3
@@ -131,6 +131,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
131 Expanded(
132 child: ScrollableWithBottomSection(
133 contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
134 + scrollableKey: ValueKey('new_wallet_type_scrollable_key'),
135 content: Column(
136 crossAxisAlignment: CrossAxisAlignment.center,
137 children: <Widget>[
@@ -138,6 +139,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
139 (type) => Padding(
140 padding: EdgeInsets.only(top: 12),
141 child: SelectButton(
142 + key: ValueKey('new_wallet_type_${type.name}_button_key'),
143 image: Image.asset(
144 walletTypeToCryptoCurrency(type).iconPath ?? '',
145 height: 24,
@@ -158,6 +160,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
160 ),
161 bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
162 bottomSection: PrimaryButton(
163 + key: ValueKey('new_wallet_type_next_button_key'),
164 onPressed: () => onTypeSelected(),
165 text: S.of(context).seed_language_next,
166 color: Theme.of(context).primaryColor,
lib/src/screens/new_wallet/widgets/select_button.dart
+1
@@ -20,6 +20,7 @@ class SelectButton extends StatelessWidget {
20 this.deviceConnectionTypes,
21 this.borderRadius,
22 this.padding,
23 + super.key,
24 });
25
26 final Widget? image;
lib/src/screens/pin_code/pin_code_widget.dart
+1
@@ -240,6 +240,7 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
240 return Container(
241 margin: EdgeInsets.only(left: marginLeft, right: marginRight),
242 child: TextButton(
243 + key: ValueKey('pin_code_button_${index}_key'),
244 onPressed: () => _push(index),
245 style: TextButton.styleFrom(
246 backgroundColor: Theme.of(context).colorScheme.background,
lib/src/screens/receive/widgets/currency_input_field.dart
+22 -4
@@ -24,8 +24,20 @@ class CurrencyAmountTextField extends StatelessWidget {
24 this.tagBackgroundColor,
25 this.currencyValueValidator,
26 this.allAmountCallback,
27 - });
27 + this.sendAllButtonKey,
28 + this.amountTextfieldKey,
29 + this.currencyPickerButtonKey,
30 + this.selectedCurrencyTextKey,
31 + this.selectedCurrencyTagTextKey,
32 + this.currencyAmountTextFieldWidgetKey,
33 + }) : super(key: currencyAmountTextFieldWidgetKey);
34
35 + final Key? sendAllButtonKey;
36 + final Key? amountTextfieldKey;
37 + final Key? currencyPickerButtonKey;
38 + final Key? selectedCurrencyTextKey;
39 + final Key? selectedCurrencyTagTextKey;
40 + final Key? currencyAmountTextFieldWidgetKey;
41 final Widget? imageArrow;
42 final String selectedCurrency;
43 final String? tag;
@@ -54,6 +66,7 @@ class CurrencyAmountTextField extends StatelessWidget {
66 ? Container(
67 height: 32,
68 child: InkWell(
69 + key: currencyPickerButtonKey,
70 onTap: onTapPicker,
71 child: Row(
72 mainAxisAlignment: MainAxisAlignment.spaceBetween,
@@ -65,6 +78,7 @@ class CurrencyAmountTextField extends StatelessWidget {
78 Image.asset('assets/images/arrow_bottom_purple_icon.png',
79 color: textColor, height: 8)),
80 Text(
81 + key: selectedCurrencyTextKey,
82 selectedCurrency,
83 style: TextStyle(
84 fontWeight: FontWeight.w600,
@@ -77,6 +91,7 @@ class CurrencyAmountTextField extends StatelessWidget {
91 ),
92 )
93 : Text(
94 + key: selectedCurrencyTextKey,
95 selectedCurrency,
96 style: TextStyle(
97 fontWeight: FontWeight.w600,
@@ -98,6 +113,7 @@ class CurrencyAmountTextField extends StatelessWidget {
113 child: Padding(
114 padding: const EdgeInsets.all(6.0),
115 child: Text(
116 + key: selectedCurrencyTagTextKey,
117 tag!,
118 style: TextStyle(
119 fontSize: 12,
@@ -132,9 +148,9 @@ class CurrencyAmountTextField extends StatelessWidget {
148 padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
149 margin: const EdgeInsets.only(right: 3),
150 decoration: BoxDecoration(
135 - border: Border.all(
136 - color: textColor,
137 - ),
151 + border: Border.all(
152 + color: textColor,
153 + ),
154 borderRadius: BorderRadius.circular(26),
155 color: Theme.of(context).primaryColor))
156 : _prefixContent,
@@ -146,6 +162,7 @@ class CurrencyAmountTextField extends StatelessWidget {
162 child: FocusTraversalOrder(
163 order: NumericFocusOrder(1),
164 child: BaseTextFormField(
165 + key: amountTextfieldKey,
166 focusNode: amountFocusNode,
167 controller: amountController,
168 enabled: isAmountEditable,
@@ -184,6 +201,7 @@ class CurrencyAmountTextField extends StatelessWidget {
201 borderRadius: const BorderRadius.all(Radius.circular(6)),
202 ),
203 child: InkWell(
204 + key: sendAllButtonKey,
205 onTap: allAmountCallback,
206 child: Center(
207 child: Text(
lib/src/screens/restore/restore_options_page.dart
+14 -6
@@ -59,8 +59,12 @@ class RestoreOptionsPage extends BasePage {
59 child: Column(
60 children: <Widget>[
61 OptionTile(
62 - onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromSeedKeys,
63 - arguments: isNewInstall),
62 + key: ValueKey('restore_options_from_seeds_button_key'),
63 + onPressed: () => Navigator.pushNamed(
64 + context,
65 + Routes.restoreWalletFromSeedKeys,
66 + arguments: isNewInstall,
67 + ),
68 image: imageSeedKeys,
69 title: S.of(context).restore_title_from_seed_keys,
70 description: S.of(context).restore_description_from_seed_keys,
@@ -69,6 +73,7 @@ class RestoreOptionsPage extends BasePage {
73 Padding(
74 padding: EdgeInsets.only(top: 24),
75 child: OptionTile(
76 + key: ValueKey('restore_options_from_backup_button_key'),
77 onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup),
78 image: imageBackup,
79 title: S.of(context).restore_title_from_backup,
@@ -79,6 +84,7 @@ class RestoreOptionsPage extends BasePage {
84 Padding(
85 padding: EdgeInsets.only(top: 24),
86 child: OptionTile(
87 + key: ValueKey('restore_options_from_hardware_wallet_button_key'),
88 onPressed: () => Navigator.pushNamed(
89 context, Routes.restoreWalletFromHardwareWallet,
90 arguments: isNewInstall),
@@ -90,10 +96,12 @@ class RestoreOptionsPage extends BasePage {
96 Padding(
97 padding: EdgeInsets.only(top: 24),
98 child: OptionTile(
93 - onPressed: () => _onScanQRCode(context),
94 - image: qrCode,
95 - title: S.of(context).scan_qr_code,
96 - description: S.of(context).cold_or_recover_wallet),
99 + key: ValueKey('restore_options_from_qr_button_key'),
100 + onPressed: () => _onScanQRCode(context),
101 + image: qrCode,
102 + title: S.of(context).scan_qr_code,
103 + description: S.of(context).cold_or_recover_wallet,
104 + ),
105 )
106 ],
107 ),
lib/src/screens/restore/wallet_restore_from_keys_form.dart
+3
@@ -112,10 +112,12 @@ class WalletRestoreFromKeysFromState extends State<WalletRestoreFromKeysFrom> {
112 alignment: Alignment.centerRight,
113 children: [
114 BaseTextFormField(
115 + key: ValueKey('wallet_restore_from_keys_wallet_name_textfield_key'),
116 controller: nameTextEditingController,
117 hintText: S.of(context).wallet_name,
118 validator: WalletNameValidator(),
119 suffixIcon: IconButton(
120 + key: ValueKey('wallet_restore_from_keys_wallet_name_refresh_button_key'),
121 onPressed: () async {
122 final rName = await generateName();
123 FocusManager.instance.primaryFocus?.unfocus();
@@ -175,6 +177,7 @@ class WalletRestoreFromKeysFromState extends State<WalletRestoreFromKeysFrom> {
177 bool nanoBased = widget.walletRestoreViewModel.type == WalletType.nano ||
178 widget.walletRestoreViewModel.type == WalletType.banano;
179 return AddressTextField(
180 + addressKey: ValueKey('wallet_restore_from_key_private_key_textfield_key'),
181 controller: privateKeyController,
182 placeholder: nanoBased ? S.of(context).seed_hex_form : S.of(context).private_key,
183 options: [AddressTextFieldOption.paste],
lib/src/screens/restore/wallet_restore_from_seed_form.dart
+9 -4
@@ -151,11 +151,13 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
151 alignment: Alignment.centerRight,
152 children: [
153 BaseTextFormField(
154 + key: ValueKey('wallet_restore_from_seed_wallet_name_textfield_key'),
155 controller: nameTextEditingController,
156 hintText: S
157 .of(context)
158 .wallet_name,
159 suffixIcon: IconButton(
160 + key: ValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'),
161 onPressed: () async {
162 final rName = await generateName();
163 FocusManager.instance.primaryFocus?.unfocus();
@@ -190,10 +192,13 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
192 )),
193 Container(height: 20),
194 SeedWidget(
193 - key: seedWidgetStateKey,
194 - language: language,
195 - type: widget.type,
196 - onSeedChange: onSeedChange),
195 + key: seedWidgetStateKey,
196 + language: language,
197 + type: widget.type,
198 + onSeedChange: onSeedChange,
199 + seedTextFieldKey: ValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'),
200 + pasteButtonKey: ValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'),
201 + ),
202 if (widget.type == WalletType.monero || widget.type == WalletType.wownero)
203 GestureDetector(
204 onTap: () async {
lib/src/screens/restore/wallet_restore_page.dart
+2
@@ -213,6 +213,7 @@ class WalletRestorePage extends BasePage {
213 Observer(
214 builder: (context) {
215 return LoadingPrimaryButton(
216 + key: ValueKey('wallet_restore_seed_or_key_restore_button_key'),
217 onPressed: () async {
218 await _confirmForm(context);
219 },
@@ -230,6 +231,7 @@ class WalletRestorePage extends BasePage {
231 ),
232 const SizedBox(height: 25),
233 GestureDetector(
234 + key: ValueKey('wallet_restore_advanced_settings_button_key'),
235 onTap: () {
236 Navigator.of(context)
237 .pushNamed(Routes.advancedPrivacySettings, arguments: {
lib/src/screens/send/send_page.dart
+26 -9
@@ -250,6 +250,7 @@ class SendPage extends BasePage {
250 return Row(
251 children: <Widget>[
252 AddTemplateButton(
253 + key: ValueKey('send_page_add_template_button_key'),
254 onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate),
255 currentTemplatesLength: templates.length,
256 ),
@@ -339,19 +340,22 @@ class SendPage extends BasePage {
340 children: [
341 if (sendViewModel.hasCurrecyChanger)
342 Observer(
342 - builder: (_) => Padding(
343 - padding: EdgeInsets.only(bottom: 12),
344 - child: PrimaryButton(
345 - onPressed: () => presentCurrencyPicker(context),
346 - text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})',
347 - color: Colors.transparent,
348 - textColor:
349 - Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
350 - ))),
343 + builder: (_) => Padding(
344 + padding: EdgeInsets.only(bottom: 12),
345 + child: PrimaryButton(
346 + key: ValueKey('send_page_change_asset_button_key'),
347 + onPressed: () => presentCurrencyPicker(context),
348 + text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})',
349 + color: Colors.transparent,
350 + textColor: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
351 + ),
352 + ),
353 + ),
354 if (sendViewModel.sendTemplateViewModel.hasMultiRecipient)
355 Padding(
356 padding: EdgeInsets.only(bottom: 12),
357 child: PrimaryButton(
358 + key: ValueKey('send_page_add_receiver_button_key'),
359 onPressed: () {
360 sendViewModel.addOutput();
361 Future.delayed(const Duration(milliseconds: 250), () {
@@ -368,6 +372,7 @@ class SendPage extends BasePage {
372 Observer(
373 builder: (_) {
374 return LoadingPrimaryButton(
375 + key: ValueKey('send_page_send_button_key'),
376 onPressed: () async {
377 if (sendViewModel.state is IsExecutingState) return;
378 if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
@@ -451,6 +456,8 @@ class SendPage extends BasePage {
456 context: context,
457 builder: (BuildContext context) {
458 return AlertWithOneAction(
459 + key: ValueKey('send_page_send_failure_dialog_key'),
460 + buttonKey: ValueKey('send_page_send_failure_dialog_button_key'),
461 alertTitle: S.of(context).error,
462 alertContent: state.error,
463 buttonText: S.of(context).ok,
@@ -466,6 +473,7 @@ class SendPage extends BasePage {
473 context: context,
474 builder: (BuildContext _dialogContext) {
475 return ConfirmSendingAlert(
476 + key: ValueKey('send_page_confirm_sending_dialog_key'),
477 alertTitle: S.of(_dialogContext).confirm_sending,
478 amount: S.of(_dialogContext).send_amount,
479 amountValue: sendViewModel.pendingTransaction!.amountFormatted,
@@ -480,6 +488,10 @@ class SendPage extends BasePage {
488 change: sendViewModel.pendingTransaction!.change,
489 rightButtonText: S.of(_dialogContext).send,
490 leftButtonText: S.of(_dialogContext).cancel,
491 + alertRightActionButtonKey:
492 + ValueKey('send_page_confirm_sending_dialog_send_button_key'),
493 + alertLeftActionButtonKey:
494 + ValueKey('send_page_confirm_sending_dialog_cancel_button_key'),
495 actionRightButton: () async {
496 Navigator.of(_dialogContext).pop();
497 sendViewModel.commitTransaction();
@@ -513,10 +525,15 @@ class SendPage extends BasePage {
525
526 if (newContactAddress != null) {
527 return AlertWithTwoActions(
528 + alertDialogKey: ValueKey('send_page_sent_dialog_key'),
529 alertTitle: '',
530 alertContent: alertContent,
531 rightButtonText: S.of(_dialogContext).add_contact,
532 leftButtonText: S.of(_dialogContext).ignor,
533 + alertLeftActionButtonKey:
534 + ValueKey('send_page_sent_dialog_ignore_button_key'),
535 + alertRightActionButtonKey: ValueKey(
536 + 'send_page_sent_dialog_add_contact_button_key'),
537 actionRightButton: () {
538 Navigator.of(_dialogContext).pop();
539 RequestReviewHandler.requestReview();
lib/src/screens/send/widgets/confirm_sending_alert.dart
+37 -24
@@ -9,30 +9,34 @@ import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
9 import 'package:flutter/scheduler.dart';
10
11 class ConfirmSendingAlert extends BaseAlertDialog {
12 - ConfirmSendingAlert(
13 - {required this.alertTitle,
14 - this.paymentId,
15 - this.paymentIdValue,
16 - this.expirationTime,
17 - required this.amount,
18 - required this.amountValue,
19 - required this.fiatAmountValue,
20 - required this.fee,
21 - this.feeRate,
22 - required this.feeValue,
23 - required this.feeFiatAmount,
24 - required this.outputs,
25 - this.change,
26 - required this.leftButtonText,
27 - required this.rightButtonText,
28 - required this.actionLeftButton,
29 - required this.actionRightButton,
30 - this.alertBarrierDismissible = true,
31 - this.alertLeftActionButtonTextColor,
32 - this.alertRightActionButtonTextColor,
33 - this.alertLeftActionButtonColor,
34 - this.alertRightActionButtonColor,
35 - this.onDispose});
12 + ConfirmSendingAlert({
13 + required this.alertTitle,
14 + this.paymentId,
15 + this.paymentIdValue,
16 + this.expirationTime,
17 + required this.amount,
18 + required this.amountValue,
19 + required this.fiatAmountValue,
20 + required this.fee,
21 + this.feeRate,
22 + required this.feeValue,
23 + required this.feeFiatAmount,
24 + required this.outputs,
25 + this.change,
26 + required this.leftButtonText,
27 + required this.rightButtonText,
28 + required this.actionLeftButton,
29 + required this.actionRightButton,
30 + this.alertBarrierDismissible = true,
31 + this.alertLeftActionButtonTextColor,
32 + this.alertRightActionButtonTextColor,
33 + this.alertLeftActionButtonColor,
34 + this.alertRightActionButtonColor,
35 + this.onDispose,
36 + this.alertLeftActionButtonKey,
37 + this.alertRightActionButtonKey,
38 + Key? key,
39 + });
40
41 final String alertTitle;
42 final String? paymentId;
@@ -57,6 +61,8 @@ class ConfirmSendingAlert extends BaseAlertDialog {
61 final Color? alertLeftActionButtonColor;
62 final Color? alertRightActionButtonColor;
63 final Function? onDispose;
64 + final Key? alertRightActionButtonKey;
65 + final Key? alertLeftActionButtonKey;
66
67 @override
68 String get titleText => alertTitle;
@@ -91,6 +97,12 @@ class ConfirmSendingAlert extends BaseAlertDialog {
97 @override
98 Color? get rightActionButtonColor => alertRightActionButtonColor;
99
100 + @override
101 + Key? get leftActionButtonKey => alertLeftActionButtonKey;
102 +
103 + @override
104 + Key? get rightActionButtonKey => alertLeftActionButtonKey;
105 +
106 @override
107 Widget content(BuildContext context) => ConfirmSendingAlertContent(
108 paymentId: paymentId,
@@ -288,6 +300,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
300 crossAxisAlignment: CrossAxisAlignment.end,
301 children: [
302 Text(
303 + key: ValueKey('confirm_sending_dialog_amount_text_value_key'),
304 amountValue,
305 style: TextStyle(
306 fontSize: 18,
lib/src/screens/send/widgets/send_card.dart
+21 -6
@@ -158,6 +158,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
158 : sendViewModel.addressValidator;
159
160 return AddressTextField(
161 + addressKey: ValueKey('send_page_address_textfield_key'),
162 focusNode: addressFocusNode,
163 controller: addressController,
164 onURIScanned: (uri) {
@@ -209,6 +210,11 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
210 fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
211 validator: sendViewModel.addressValidator)),
212 CurrencyAmountTextField(
213 + currencyPickerButtonKey: ValueKey('send_page_currency_picker_button_key'),
214 + amountTextfieldKey: ValueKey('send_page_amount_textfield_key'),
215 + sendAllButtonKey: ValueKey('send_page_send_all_button_key'),
216 + currencyAmountTextFieldWidgetKey:
217 + ValueKey('send_page_crypto_currency_amount_textfield_widget_key'),
218 selectedCurrency: sendViewModel.selectedCryptoCurrency.title,
219 amountFocusNode: cryptoAmountFocus,
220 amountController: cryptoAmountController,
@@ -216,7 +222,8 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
222 onTapPicker: () => _presentPicker(context),
223 isPickerEnable: sendViewModel.hasMultipleTokens,
224 tag: sendViewModel.selectedCryptoCurrency.tag,
219 - allAmountButton: !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL,
225 + allAmountButton:
226 + !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL,
227 currencyValueValidator: output.sendAll
228 ? sendViewModel.allAmountValidator
229 : sendViewModel.amountValidator,
@@ -257,6 +264,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
264 ),
265 if (!sendViewModel.isFiatDisabled)
266 CurrencyAmountTextField(
267 + amountTextfieldKey: ValueKey('send_page_fiat_amount_textfield_key'),
268 + currencyAmountTextFieldWidgetKey:
269 + ValueKey('send_page_fiat_currency_amount_textfield_widget_key'),
270 selectedCurrency: sendViewModel.fiat.title,
271 amountFocusNode: fiatAmountFocus,
272 amountController: fiatAmountController,
@@ -269,6 +279,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
279 Padding(
280 padding: EdgeInsets.only(top: 20),
281 child: BaseTextFormField(
282 + key: ValueKey('send_page_note_textfield_key'),
283 controller: noteController,
284 keyboardType: TextInputType.multiline,
285 maxLines: null,
@@ -287,6 +298,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
298 if (sendViewModel.hasFees)
299 Observer(
300 builder: (_) => GestureDetector(
301 + key: ValueKey('send_page_select_fee_priority_button_key'),
302 onTap: sendViewModel.hasFeesPriority
303 ? () => pickTransactionPriority(context)
304 : () {},
@@ -360,6 +372,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
372 Padding(
373 padding: EdgeInsets.only(top: 6),
374 child: GestureDetector(
375 + key: ValueKey('send_page_unspent_coin_button_key'),
376 onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList),
377 child: Container(
378 color: Colors.transparent,
@@ -544,11 +557,13 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
557 showPopUp<void>(
558 context: context,
559 builder: (_) => CurrencyPicker(
547 - selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency),
548 - items: sendViewModel.currencies,
549 - hintText: S.of(context).search_currency,
550 - onItemSelected: (Currency cur) =>
551 - sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency)),
560 + key: ValueKey('send_page_currency_picker_dialog_button_key'),
561 + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency),
562 + items: sendViewModel.currencies,
563 + hintText: S.of(context).search_currency,
564 + onItemSelected: (Currency cur) =>
565 + sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency),
566 + ),
567 );
568 }
569
lib/src/screens/setup_pin_code/setup_pin_code.dart
+1
@@ -52,6 +52,7 @@ class SetupPinCodePage extends BasePage {
52 context: context,
53 builder: (BuildContext context) {
54 return AlertWithOneAction(
55 + buttonKey: ValueKey('setup_pin_code_success_button_key'),
56 alertTitle: S.current.setup_pin,
57 alertContent: S.of(context).setup_successful,
58 buttonText: S.of(context).ok,
lib/src/screens/welcome/welcome_page.dart
+2
@@ -133,6 +133,7 @@ class WelcomePage extends BasePage {
133 Padding(
134 padding: EdgeInsets.only(top: 24),
135 child: PrimaryImageButton(
136 + key: ValueKey('welcome_page_create_new_wallet_button_key'),
137 onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome),
138 image: newWalletImage,
139 text: S.of(context).create_new,
@@ -146,6 +147,7 @@ class WelcomePage extends BasePage {
147 Padding(
148 padding: EdgeInsets.only(top: 10),
149 child: PrimaryImageButton(
150 + key: ValueKey('welcome_page_restore_wallet_button_key'),
151 onPressed: () {
152 Navigator.pushNamed(context, Routes.restoreOptions, arguments: true);
153 },
lib/src/widgets/address_text_field.dart
+23 -22
@@ -15,28 +15,27 @@ import 'package:permission_handler/permission_handler.dart';
15 enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
16
17 class AddressTextField extends StatelessWidget {
18 - AddressTextField(
19 - {required this.controller,
20 - this.isActive = true,
21 - this.placeholder,
22 - this.options = const [
23 - AddressTextFieldOption.qrCode,
24 - AddressTextFieldOption.addressBook
25 - ],
26 - this.onURIScanned,
27 - this.focusNode,
28 - this.isBorderExist = true,
29 - this.buttonColor,
30 - this.borderColor,
31 - this.iconColor,
32 - this.textStyle,
33 - this.hintStyle,
34 - this.validator,
35 - this.onPushPasteButton,
36 - this.onPushAddressBookButton,
37 - this.onPushAddressPickerButton,
38 - this.onSelectedContact,
39 - this.selectedCurrency});
18 + AddressTextField({
19 + required this.controller,
20 + this.isActive = true,
21 + this.placeholder,
22 + this.options = const [AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook],
23 + this.onURIScanned,
24 + this.focusNode,
25 + this.isBorderExist = true,
26 + this.buttonColor,
27 + this.borderColor,
28 + this.iconColor,
29 + this.textStyle,
30 + this.hintStyle,
31 + this.validator,
32 + this.onPushPasteButton,
33 + this.onPushAddressBookButton,
34 + this.onPushAddressPickerButton,
35 + this.onSelectedContact,
36 + this.selectedCurrency,
37 + this.addressKey,
38 + });
39
40 static const prefixIconWidth = 34.0;
41 static const prefixIconHeight = 34.0;
@@ -60,12 +59,14 @@ class AddressTextField extends StatelessWidget {
59 final Function(BuildContext context)? onPushAddressPickerButton;
60 final Function(ContactBase contact)? onSelectedContact;
61 final CryptoCurrency? selectedCurrency;
62 + final Key? addressKey;
63
64 @override
65 Widget build(BuildContext context) {
66 return Stack(
67 children: <Widget>[
68 TextFormField(
69 + key: addressKey,
70 enableIMEPersonalizedLearning: false,
71 keyboardType: TextInputType.visiblePassword,
72 onFieldSubmitted: (_) => FocusScope.of(context).unfocus(),
lib/src/widgets/alert_close_button.dart
+6 -1
@@ -3,7 +3,12 @@ import 'package:cake_wallet/palette.dart';
3 import 'package:flutter/material.dart';
4
5 class AlertCloseButton extends StatelessWidget {
6 - AlertCloseButton({this.image, this.bottom, this.onTap});
6 + AlertCloseButton({
7 + this.image,
8 + this.bottom,
9 + this.onTap,
10 + super.key,
11 + });
12
13 final VoidCallback? onTap;
14
lib/src/widgets/alert_with_one_action.dart
+6 -2
@@ -9,7 +9,9 @@ class AlertWithOneAction extends BaseAlertDialog {
9 required this.buttonAction,
10 this.alertBarrierDismissible = true,
11 this.headerTitleText,
12 - this.headerImageProfileUrl
12 + this.headerImageProfileUrl,
13 + this.buttonKey,
14 + Key? key,
15 });
16
17 final String alertTitle;
@@ -19,6 +21,7 @@ class AlertWithOneAction extends BaseAlertDialog {
21 final bool alertBarrierDismissible;
22 final String? headerTitleText;
23 final String? headerImageProfileUrl;
24 + final Key? buttonKey;
25
26 @override
27 String get titleText => alertTitle;
@@ -45,6 +48,7 @@ class AlertWithOneAction extends BaseAlertDialog {
48 child: ButtonTheme(
49 minWidth: double.infinity,
50 child: TextButton(
51 + key: buttonKey,
52 onPressed: buttonAction,
53 // FIX-ME: Style
54 //highlightColor: Colors.transparent,
@@ -62,4 +66,4 @@ class AlertWithOneAction extends BaseAlertDialog {
66 ),
67 );
68 }
65 -}
\ No newline at end of file
69 +}
lib/src/widgets/alert_with_two_actions.dart
+15
@@ -14,6 +14,9 @@ class AlertWithTwoActions extends BaseAlertDialog {
14 this.isDividerExist = false,
15 // this.leftActionColor,
16 // this.rightActionColor,
17 + this.alertRightActionButtonKey,
18 + this.alertLeftActionButtonKey,
19 + this.alertDialogKey,
20 });
21
22 final String alertTitle;
@@ -26,6 +29,9 @@ class AlertWithTwoActions extends BaseAlertDialog {
29 // final Color leftActionColor;
30 // final Color rightActionColor;
31 final bool isDividerExist;
32 + final Key? alertRightActionButtonKey;
33 + final Key? alertLeftActionButtonKey;
34 + final Key? alertDialogKey;
35
36 @override
37 String get titleText => alertTitle;
@@ -47,4 +53,13 @@ class AlertWithTwoActions extends BaseAlertDialog {
53 // Color get rightButtonColor => rightActionColor;
54 @override
55 bool get isDividerExists => isDividerExist;
56 +
57 + @override
58 + Key? get dialogKey => alertDialogKey;
59 +
60 + @override
61 + Key? get leftActionButtonKey => alertLeftActionButtonKey;
62 +
63 + @override
64 + Key? get rightActionButtonKey => alertRightActionButtonKey;
65 }
lib/src/widgets/base_alert_dialog.dart
+9
@@ -33,6 +33,12 @@ class BaseAlertDialog extends StatelessWidget {
33
34 String? get headerImageUrl => null;
35
36 + Key? leftActionButtonKey;
37 +
38 + Key? rightActionButtonKey;
39 +
40 + Key? dialogKey;
41 +
42 Widget title(BuildContext context) {
43 return Text(
44 titleText,
@@ -87,6 +93,7 @@ class BaseAlertDialog extends StatelessWidget {
93 children: <Widget>[
94 Expanded(
95 child: TextButton(
96 + key: leftActionButtonKey,
97 onPressed: actionLeft,
98 style: TextButton.styleFrom(
99 backgroundColor:
@@ -109,6 +116,7 @@ class BaseAlertDialog extends StatelessWidget {
116 const VerticalSectionDivider(),
117 Expanded(
118 child: TextButton(
119 + key: rightActionButtonKey,
120 onPressed: actionRight,
121 style: TextButton.styleFrom(
122 backgroundColor:
@@ -152,6 +160,7 @@ class BaseAlertDialog extends StatelessWidget {
160 @override
161 Widget build(BuildContext context) {
162 return GestureDetector(
163 + key: key,
164 onTap: () => barrierDismissible ? Navigator.of(context).pop() : null,
165 child: Container(
166 color: Colors.transparent,
lib/src/widgets/base_text_form_field.dart
+2 -1
@@ -30,7 +30,8 @@ class BaseTextFormField extends StatelessWidget {
30 this.focusNode,
31 this.initialValue,
32 this.onSubmit,
33 - this.borderWidth = 1.0});
33 + this.borderWidth = 1.0,
34 + super.key});
35
36 final TextEditingController? controller;
37 final TextInputType? keyboardType;
lib/src/widgets/option_tile.dart
+2 -1
@@ -6,7 +6,8 @@ class OptionTile extends StatelessWidget {
6 {required this.onPressed,
7 required this.image,
8 required this.title,
9 - required this.description});
9 + required this.description,
10 + super.key});
11
12 final VoidCallback onPressed;
13 final Image image;
lib/src/widgets/picker.dart
+34 -1
@@ -4,6 +4,7 @@ import 'dart:math';
4
5 import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
6 import 'package:cake_wallet/utils/responsive_layout_util.dart';
7 +import 'package:cw_core/transaction_priority.dart';
8 import 'package:flutter/material.dart';
9 import 'package:cw_core/currency.dart';
10 import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
@@ -11,6 +12,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
12 import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart';
13 import 'package:cake_wallet/themes/extensions/picker_theme.dart';
14
15 +//TODO(David): PickerWidget is intertwined and confusing as is, find a way to optimize?
16 class Picker<Item> extends StatefulWidget {
17 Picker({
18 required this.selectedAtIndex,
@@ -153,6 +155,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
155 Container(
156 padding: EdgeInsets.symmetric(horizontal: padding),
157 child: Text(
158 + key: ValueKey('picker_title_text_key'),
159 widget.title!,
160 textAlign: TextAlign.center,
161 style: TextStyle(
@@ -189,7 +192,10 @@ class _PickerState<Item> extends State<Picker<Item>> {
192 Padding(
193 padding: const EdgeInsets.all(16),
194 child: SearchBarWidget(
192 - searchController: searchController, hintText: widget.hintText),
195 + key: ValueKey('picker_search_bar_key'),
196 + searchController: searchController,
197 + hintText: widget.hintText,
198 + ),
199 ),
200 Divider(
201 color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
@@ -203,6 +209,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
209 children: <Widget>[
210 filteredItems.length > 3
211 ? Scrollbar(
212 + key: ValueKey('picker_scrollbar_key'),
213 controller: controller,
214 child: itemsList(),
215 )
@@ -213,6 +220,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
220 left: padding,
221 right: padding,
222 child: Text(
223 + key: ValueKey('picker_descriptinon_text_key'),
224 widget.description!,
225 textAlign: TextAlign.center,
226 style: TextStyle(
@@ -242,6 +250,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
250
251 if (widget.isWrapped) {
252 return PickerWrapperWidget(
253 + key: ValueKey('picker_wrapper_widget_key'),
254 hasTitle: widget.title?.isNotEmpty ?? false,
255 children: [content],
256 );
@@ -260,6 +269,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
269 color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
270 child: widget.isGridView
271 ? GridView.builder(
272 + key: ValueKey('picker_items_grid_view_key'),
273 padding: EdgeInsets.zero,
274 controller: controller,
275 shrinkWrap: true,
@@ -275,6 +285,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
285 : buildItem(index),
286 )
287 : ListView.separated(
288 + key: ValueKey('picker_items_list_view_key'),
289 padding: EdgeInsets.zero,
290 controller: controller,
291 shrinkWrap: true,
@@ -293,10 +304,25 @@ class _PickerState<Item> extends State<Picker<Item>> {
304 );
305 }
306
307 + String _getItemName(Item item) {
308 + String itemName;
309 + if (item is Currency) {
310 + itemName = item.name;
311 + } else if (item is TransactionPriority) {
312 + itemName = item.title;
313 + } else {
314 + itemName = '';
315 + }
316 +
317 + return itemName;
318 + }
319 +
320 Widget buildItem(int index) {
321 final item = widget.headerEnabled ? filteredItems[index] : items[index];
322
323 final tag = item is Currency ? item.tag : null;
324 + final itemName = _getItemName(item);
325 +
326 final icon = _getItemIcon(item);
327
328 final image = images.isNotEmpty ? filteredImages[index] : icon;
@@ -316,6 +342,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
342 children: [
343 Flexible(
344 child: Text(
345 + key: ValueKey('picker_items_index_${itemName}_text_key'),
346 widget.displayItem?.call(item) ?? item.toString(),
347 softWrap: true,
348 style: TextStyle(
@@ -335,6 +362,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
362 height: 18.0,
363 child: Center(
364 child: Text(
365 + key: ValueKey('picker_items_index_${index}_tag_key'),
366 tag,
367 style: TextStyle(
368 fontSize: 7.0,
@@ -358,6 +386,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
386 );
387
388 return GestureDetector(
389 + key: ValueKey('picker_items_index_${itemName}_button_key'),
390 onTap: () {
391 if (widget.closeOnItemSelected) Navigator.of(context).pop();
392 onItemSelected(item!);
@@ -383,6 +412,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
412 final item = items[index];
413
414 final tag = item is Currency ? item.tag : null;
415 + final itemName = _getItemName(item);
416 final icon = _getItemIcon(item);
417
418 final image = images.isNotEmpty ? images[index] : icon;
@@ -390,6 +420,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
420 final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex;
421
422 final itemContent = Row(
423 + key: ValueKey('picker_selected_item_row_key'),
424 mainAxisSize: MainAxisSize.max,
425 mainAxisAlignment: widget.mainAxisAlignment,
426 crossAxisAlignment: CrossAxisAlignment.center,
@@ -402,6 +433,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
433 children: [
434 Flexible(
435 child: Text(
436 + key: ValueKey('picker_items_index_${itemName}_selected_item_text_key'),
437 widget.displayItem?.call(item) ?? item.toString(),
438 softWrap: true,
439 style: TextStyle(
@@ -445,6 +477,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
477 );
478
479 return GestureDetector(
480 + key: ValueKey('picker_items_index_${itemName}_selected_item_button_key'),
481 onTap: () {
482 if (widget.closeOnItemSelected) Navigator.of(context).pop();
483 },
lib/src/widgets/picker_wrapper_widget.dart
+13 -4
@@ -4,7 +4,12 @@ import 'package:cake_wallet/src/widgets/alert_background.dart';
4 import 'package:cake_wallet/src/widgets/alert_close_button.dart';
5
6 class PickerWrapperWidget extends StatelessWidget {
7 - PickerWrapperWidget({required this.children, this.hasTitle = false, this.onClose});
7 + PickerWrapperWidget({
8 + required this.children,
9 + this.hasTitle = false,
10 + this.onClose,
11 + super.key,
12 + });
13
14 final List<Widget> children;
15 final bool hasTitle;
@@ -29,8 +34,8 @@ class PickerWrapperWidget extends StatelessWidget {
34 final containerBottom = screenCenter - containerCenter;
35
36 // position the close button right below the search container
32 - closeButtonBottom = closeButtonBottom -
33 - containerBottom + (!hasTitle ? padding : padding / 1.5);
37 + closeButtonBottom =
38 + closeButtonBottom - containerBottom + (!hasTitle ? padding : padding / 1.5);
39 }
40
41 return AlertBackground(
@@ -46,7 +51,11 @@ class PickerWrapperWidget extends StatelessWidget {
51 children: children,
52 ),
53 SizedBox(height: ResponsiveLayoutUtilBase.kPopupSpaceHeight),
49 - AlertCloseButton(bottom: closeButtonBottom, onTap: onClose),
54 + AlertCloseButton(
55 + key: ValueKey('picker_wrapper_close_button_key'),
56 + bottom: closeButtonBottom,
57 + onTap: onClose,
58 + ),
59 ],
60 ),
61 ),
lib/src/widgets/primary_button.dart
+76 -84
@@ -4,15 +4,17 @@ import 'package:flutter/cupertino.dart';
4 import 'package:flutter/material.dart';
5
6 class PrimaryButton extends StatelessWidget {
7 - const PrimaryButton(
8 - {required this.text,
9 - required this.color,
10 - required this.textColor,
11 - this.onPressed,
12 - this.isDisabled = false,
13 - this.isDottedBorder = false,
14 - this.borderColor = Colors.black,
15 - this.onDisabledPressed});
7 + const PrimaryButton({
8 + required this.text,
9 + required this.color,
10 + required this.textColor,
11 + this.onPressed,
12 + this.isDisabled = false,
13 + this.isDottedBorder = false,
14 + this.borderColor = Colors.black,
15 + this.onDisabledPressed,
16 + super.key,
17 + });
18
19 final VoidCallback? onPressed;
20 final VoidCallback? onDisabledPressed;
@@ -31,23 +33,23 @@ class PrimaryButton extends StatelessWidget {
33 width: double.infinity,
34 height: 52.0,
35 child: TextButton(
34 - onPressed: isDisabled
35 - ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed,
36 - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color),
36 + onPressed:
37 + isDisabled ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed,
38 + style: ButtonStyle(
39 + backgroundColor:
40 + MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color),
41 shape: MaterialStateProperty.all<RoundedRectangleBorder>(
42 RoundedRectangleBorder(
43 borderRadius: BorderRadius.circular(26.0),
44 ),
45 ),
42 - overlayColor: MaterialStateProperty.all(Colors.transparent)),
46 + overlayColor: MaterialStateProperty.all(Colors.transparent)),
47 child: Text(text,
48 textAlign: TextAlign.center,
49 style: TextStyle(
50 fontSize: 15.0,
51 fontWeight: FontWeight.w600,
48 - color: isDisabled
49 - ? textColor.withOpacity(0.5)
50 - : textColor)),
52 + color: isDisabled ? textColor.withOpacity(0.5) : textColor)),
53 )),
54 );
55
@@ -64,13 +66,15 @@ class PrimaryButton extends StatelessWidget {
66 }
67
68 class LoadingPrimaryButton extends StatelessWidget {
67 - const LoadingPrimaryButton(
68 - {required this.onPressed,
69 - required this.text,
70 - required this.color,
71 - required this.textColor,
72 - this.isDisabled = false,
73 - this.isLoading = false});
69 + const LoadingPrimaryButton({
70 + required this.onPressed,
71 + required this.text,
72 + required this.color,
73 + required this.textColor,
74 + this.isDisabled = false,
75 + this.isLoading = false,
76 + super.key,
77 + });
78
79 final VoidCallback onPressed;
80 final Color color;
@@ -88,41 +92,38 @@ class LoadingPrimaryButton extends StatelessWidget {
92 height: 52.0,
93 child: TextButton(
94 onPressed: (isLoading || isDisabled) ? null : onPressed,
91 - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color),
92 - shape: MaterialStateProperty.all<RoundedRectangleBorder>(
93 - RoundedRectangleBorder(
94 - borderRadius: BorderRadius.circular(26.0),
95 - ),
96 - )),
97 -
95 + style: ButtonStyle(
96 + backgroundColor:
97 + MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color),
98 + shape: MaterialStateProperty.all<RoundedRectangleBorder>(
99 + RoundedRectangleBorder(
100 + borderRadius: BorderRadius.circular(26.0),
101 + ),
102 + )),
103 child: isLoading
104 ? CupertinoActivityIndicator(animating: true)
105 : Text(text,
101 - style: TextStyle(
102 - fontSize: 15.0,
103 - fontWeight: FontWeight.w600,
104 - color: isDisabled
105 - ? textColor.withOpacity(0.5)
106 - : textColor
107 - )),
106 + style: TextStyle(
107 + fontSize: 15.0,
108 + fontWeight: FontWeight.w600,
109 + color: isDisabled ? textColor.withOpacity(0.5) : textColor)),
110 )),
111 );
112 }
113 }
114
115 class PrimaryIconButton extends StatelessWidget {
114 - const PrimaryIconButton({
115 - required this.onPressed,
116 - required this.iconData,
117 - required this.text,
118 - required this.color,
119 - required this.borderColor,
120 - required this.iconColor,
121 - required this.iconBackgroundColor,
122 - required this.textColor,
123 - this.mainAxisAlignment = MainAxisAlignment.start,
124 - this.radius = 26
125 - });
116 + const PrimaryIconButton(
117 + {required this.onPressed,
118 + required this.iconData,
119 + required this.text,
120 + required this.color,
121 + required this.borderColor,
122 + required this.iconColor,
123 + required this.iconBackgroundColor,
124 + required this.textColor,
125 + this.mainAxisAlignment = MainAxisAlignment.start,
126 + this.radius = 26, super.key});
127
128 final VoidCallback onPressed;
129 final IconData iconData;
@@ -144,7 +145,8 @@ class PrimaryIconButton extends StatelessWidget {
145 height: 52.0,
146 child: TextButton(
147 onPressed: onPressed,
147 - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color),
148 + style: ButtonStyle(
149 + backgroundColor: MaterialStateProperty.all(color),
150 shape: MaterialStateProperty.all<RoundedRectangleBorder>(
151 RoundedRectangleBorder(
152 borderRadius: BorderRadius.circular(radius),
@@ -158,21 +160,15 @@ class PrimaryIconButton extends StatelessWidget {
160 Container(
161 width: 26.0,
162 height: 52.0,
161 - decoration: BoxDecoration(
162 - shape: BoxShape.circle, color: iconBackgroundColor),
163 - child: Center(
164 - child: Icon(iconData, color: iconColor, size: 22.0)
165 - ),
163 + decoration: BoxDecoration(shape: BoxShape.circle, color: iconBackgroundColor),
164 + child: Center(child: Icon(iconData, color: iconColor, size: 22.0)),
165 ),
166 ],
167 ),
168 Container(
169 height: 52.0,
170 child: Center(
172 - child: Text(text,
173 - style: TextStyle(
174 - fontSize: 16.0,
175 - color: textColor)),
171 + child: Text(text, style: TextStyle(fontSize: 16.0, color: textColor)),
172 ),
173 )
174 ],
@@ -189,7 +185,7 @@ class PrimaryImageButton extends StatelessWidget {
185 required this.text,
186 required this.color,
187 required this.textColor,
192 - this.borderColor = Colors.transparent});
188 + this.borderColor = Colors.transparent, super.key});
189
190 final VoidCallback onPressed;
191 final Image image;
@@ -206,31 +202,27 @@ class PrimaryImageButton extends StatelessWidget {
202 width: double.infinity,
203 height: 52.0,
204 child: TextButton(
209 - onPressed: onPressed,
210 - style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color),
211 - shape: MaterialStateProperty.all<RoundedRectangleBorder>(
212 - RoundedRectangleBorder(
213 - borderRadius: BorderRadius.circular(26.0),
214 - ),
215 - )),
216 - child:Center(
217 - child: Row(
218 - mainAxisSize: MainAxisSize.min,
219 - children: <Widget>[
220 - image,
221 - SizedBox(width: 15),
222 - Text(
223 - text,
224 - style: TextStyle(
225 - fontSize: 15,
226 - fontWeight: FontWeight.w600,
227 - color: textColor
205 + onPressed: onPressed,
206 + style: ButtonStyle(
207 + backgroundColor: MaterialStateProperty.all(color),
208 + shape: MaterialStateProperty.all<RoundedRectangleBorder>(
209 + RoundedRectangleBorder(
210 + borderRadius: BorderRadius.circular(26.0),
211 ),
229 - )
230 - ],
231 - ),
232 - )
233 - )),
212 + )),
213 + child: Center(
214 + child: Row(
215 + mainAxisSize: MainAxisSize.min,
216 + children: <Widget>[
217 + image,
218 + SizedBox(width: 15),
219 + Text(
220 + text,
221 + style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: textColor),
222 + )
223 + ],
224 + ),
225 + ))),
226 );
227 }
228 }
lib/src/widgets/scollable_with_bottom_section.dart
+3
@@ -9,6 +9,7 @@ class ScrollableWithBottomSection extends StatefulWidget {
9 this.contentPadding,
10 this.bottomSectionPadding,
11 this.topSectionPadding,
12 + this.scrollableKey,
13 });
14
15 final Widget content;
@@ -17,6 +18,7 @@ class ScrollableWithBottomSection extends StatefulWidget {
18 final EdgeInsets? contentPadding;
19 final EdgeInsets? bottomSectionPadding;
20 final EdgeInsets? topSectionPadding;
21 + final Key? scrollableKey;
22
23 @override
24 ScrollableWithBottomSectionState createState() => ScrollableWithBottomSectionState();
@@ -35,6 +37,7 @@ class ScrollableWithBottomSectionState extends State<ScrollableWithBottomSection
37 ),
38 Expanded(
39 child: SingleChildScrollView(
40 + key: widget.scrollableKey,
41 child: Padding(
42 padding: widget.contentPadding ?? EdgeInsets.only(left: 20, right: 20),
43 child: widget.content,
lib/src/widgets/search_bar_widget.dart
+1
@@ -7,6 +7,7 @@ class SearchBarWidget extends StatelessWidget {
7 required this.searchController,
8 this.hintText,
9 this.borderRadius = 14,
10 + super.key,
11 });
12
13 final TextEditingController searchController;
lib/src/widgets/seed_widget.dart
+14 -8
@@ -9,11 +9,15 @@ import 'package:flutter/services.dart';
9
10 class SeedWidget extends StatefulWidget {
11 SeedWidget({
12 - Key? key,
12 required this.language,
13 required this.type,
15 - this.onSeedChange}) : super(key: key);
16 -
14 + this.onSeedChange,
15 + this.pasteButtonKey,
16 + this.seedTextFieldKey,
17 + super.key,
18 + });
19 + final Key? seedTextFieldKey;
20 + final Key? pasteButtonKey;
21 final String language;
22 final WalletType type;
23 final void Function(String)? onSeedChange;
@@ -78,11 +82,11 @@ class SeedWidgetState extends State<SeedWidget> {
82 top: 10,
83 left: 0,
84 child: Text(S.of(context).enter_seed_phrase,
81 - style: TextStyle(
82 - fontSize: 16.0, color: Theme.of(context).hintColor))),
85 + style: TextStyle(fontSize: 16.0, color: Theme.of(context).hintColor))),
86 Padding(
87 padding: EdgeInsets.only(right: 40, top: 10),
88 child: ValidatableAnnotatedEditableText(
89 + key: widget.seedTextFieldKey,
90 cursorColor: Colors.blue,
91 backgroundCursorColor: Colors.blue,
92 validStyle: TextStyle(
@@ -112,15 +116,17 @@ class SeedWidgetState extends State<SeedWidget> {
116 width: 32,
117 height: 32,
118 child: InkWell(
119 + key: widget.pasteButtonKey,
120 onTap: () async => _pasteText(),
121 child: Container(
122 padding: EdgeInsets.all(8),
123 decoration: BoxDecoration(
124 color: Theme.of(context).hintColor,
120 - borderRadius:
121 - BorderRadius.all(Radius.circular(6))),
125 + borderRadius: BorderRadius.all(Radius.circular(6))),
126 child: Image.asset('assets/images/paste_ios.png',
123 - color: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonIconColor)),
127 + color: Theme.of(context)
128 + .extension<SendPageTheme>()!
129 + .textFieldButtonIconColor)),
130 )))
131 ]),
132 Container(
pubspec_base.yaml
+4 -1
@@ -107,11 +107,14 @@ dependencies:
107 dev_dependencies:
108 flutter_test:
109 sdk: flutter
110 + integration_test:
111 + sdk: flutter
112 + mocktail: ^1.0.4
113 build_runner: ^2.3.3
114 logging: ^1.2.0
115 mobx_codegen: ^2.1.1
116 build_resolvers: ^2.0.9
114 - hive_generator: ^1.1.3
117 + hive_generator: ^2.0.1
118 # flutter_launcher_icons: ^0.11.0
119 # check flutter_launcher_icons for usage
120 pedantic: ^1.8.0
test_driver/integration_test.dart new
+33
@@ -0,0 +1,33 @@
1 +import 'dart:convert';
2 +
3 +import 'package:integration_test/integration_test_driver.dart';
4 +import 'package:path/path.dart' as path;
5 +
6 +import 'package:flutter_driver/flutter_driver.dart';
7 +
8 +Future<void> main() async {
9 + integrationDriver(
10 + responseDataCallback: (Map<String, dynamic>? data) async {
11 + await fs.directory(_destinationDirectory).create(recursive: true);
12 +
13 + final file = fs.file(
14 + path.join(
15 + _destinationDirectory,
16 + '$_testOutputFilename.json',
17 + ),
18 + );
19 +
20 + final resultString = _encodeJson(data);
21 + await file.writeAsString(resultString);
22 + },
23 + writeResponseOnFailure: true,
24 + );
25 +}
26 +
27 +String _encodeJson(Map<String, dynamic>? jsonObject) {
28 + return _prettyEncoder.convert(jsonObject);
29 +}
30 +
31 +const _prettyEncoder = JsonEncoder.withIndent(' ');
32 +const _testOutputFilename = 'integration_response_data';
33 +const _destinationDirectory = 'integration_test';
tool/utils/secret_key.dart
+1
@@ -39,6 +39,7 @@ class SecretKey {
39 SecretKey('moralisApiKey', () => ''),
40 SecretKey('ankrApiKey', () => ''),
41 SecretKey('quantexExchangeMarkup', () => ''),
42 + SecretKey('seeds', () => ''),
43 SecretKey('testCakePayApiKey', () => ''),
44 SecretKey('cakePayApiKey', () => ''),
45 SecretKey('CSRFToken', () => ''),