CW-727/728-Automated-Integrated-Tests (#1514)
Some checks are pending
Cache Dependencies / test (push) Waiting to run

* 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>
This commit is contained in:
David Adegoke 2024-09-22 03:46:51 +01:00 committed by GitHub
parent 32e119e24f
commit 4adb81c4dc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
67 changed files with 2381 additions and 240 deletions

View file

@ -13,6 +13,9 @@ on:
jobs: jobs:
PR_test_build: PR_test_build:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
strategy:
matrix:
api-level: [29]
env: env:
STORE_PASS: test@cake_wallet STORE_PASS: test@cake_wallet
KEY_PASS: test@cake_wallet KEY_PASS: test@cake_wallet

3
.gitignore vendored
View file

@ -171,6 +171,9 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
macos/Runner/Configs/AppInfo.xcconfig macos/Runner/Configs/AppInfo.xcconfig
integration_test/playground.dart
# Monero.dart (Monero_C) # Monero.dart (Monero_C)
scripts/monero_c scripts/monero_c
# iOS generated framework bin # iOS generated framework bin

View file

@ -277,10 +277,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: file name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.1.4" version: "7.0.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:

View file

@ -0,0 +1,96 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class CommonTestCases {
WidgetTester tester;
CommonTestCases(this.tester);
Future<void> isSpecificPage<T>() async {
await tester.pumpAndSettle();
hasType<T>();
}
Future<void> tapItemByKey(String key, {bool shouldPumpAndSettle = true}) async {
final widget = find.byKey(ValueKey(key));
await tester.tap(widget);
shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump();
}
Future<void> tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async {
await tester.tap(finder);
shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump();
}
void hasText(String text, {bool hasWidget = true}) {
final textWidget = find.text(text);
expect(textWidget, hasWidget ? findsOneWidget : findsNothing);
}
void hasType<T>() {
final typeWidget = find.byType(T);
expect(typeWidget, findsOneWidget);
}
void hasValueKey(String key) {
final typeWidget = find.byKey(ValueKey(key));
expect(typeWidget, findsOneWidget);
}
Future<void> swipePage({bool swipeRight = true}) async {
await tester.drag(find.byType(PageView), Offset(swipeRight ? -300 : 300, 0));
await tester.pumpAndSettle();
}
Future<void> swipeByPageKey({required String key, bool swipeRight = true}) async {
await tester.drag(find.byKey(ValueKey(key)), Offset(swipeRight ? -300 : 300, 0));
await tester.pumpAndSettle();
}
Future<void> goBack() async {
tester.printToConsole('Routing back to previous screen');
final NavigatorState navigator = tester.state(find.byType(Navigator));
navigator.pop();
await tester.pumpAndSettle();
}
Future<void> scrollUntilVisible(String childKey, String parentScrollableKey,
{double delta = 300}) async {
final scrollableWidget = find.descendant(
of: find.byKey(Key(parentScrollableKey)),
matching: find.byType(Scrollable),
);
final isAlreadyVisibile = isWidgetVisible(find.byKey(ValueKey(childKey)));
if (isAlreadyVisibile) return;
await tester.scrollUntilVisible(
find.byKey(ValueKey(childKey)),
delta,
scrollable: scrollableWidget,
);
}
bool isWidgetVisible(Finder finder) {
try {
final Element element = finder.evaluate().single;
final RenderBox renderBox = element.renderObject as RenderBox;
return renderBox.paintBounds
.shift(renderBox.localToGlobal(Offset.zero))
.overlaps(tester.binding.renderViews.first.paintBounds);
} catch (e) {
return false;
}
}
Future<void> enterText(String text, String editableTextKey) async {
final editableTextWidget = find.byKey(ValueKey((editableTextKey)));
await tester.enterText(editableTextWidget, text);
await tester.pumpAndSettle();
}
Future<void> defaultSleepTime({int seconds = 2}) async =>
await Future.delayed(Duration(seconds: seconds));
}

View file

@ -0,0 +1,13 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
class CommonTestConstants {
static final pin = [0, 8, 0, 1];
static final String sendTestAmount = '0.00008';
static final String exchangeTestAmount = '8';
static final WalletType testWalletType = WalletType.solana;
static final String testWalletName = 'Integrated Testing Wallet';
static final CryptoCurrency testReceiveCurrency = CryptoCurrency.sol;
static final CryptoCurrency testDepositCurrency = CryptoCurrency.usdtSol;
static final String testWalletAddress = 'An2Y2fsUYKfYvN1zF89GAqR1e6GUMBg3qA83Y5ZWDf8L';
}

View file

@ -0,0 +1,101 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
import 'package:cake_wallet/main.dart' as app;
import '../robots/disclaimer_page_robot.dart';
import '../robots/new_wallet_type_page_robot.dart';
import '../robots/restore_from_seed_or_key_robot.dart';
import '../robots/restore_options_page_robot.dart';
import '../robots/setup_pin_code_robot.dart';
import '../robots/welcome_page_robot.dart';
import 'common_test_cases.dart';
import 'common_test_constants.dart';
class CommonTestFlows {
CommonTestFlows(this._tester)
: _commonTestCases = CommonTestCases(_tester),
_welcomePageRobot = WelcomePageRobot(_tester),
_setupPinCodeRobot = SetupPinCodeRobot(_tester),
_disclaimerPageRobot = DisclaimerPageRobot(_tester),
_newWalletTypePageRobot = NewWalletTypePageRobot(_tester),
_restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester),
_restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester);
final WidgetTester _tester;
final CommonTestCases _commonTestCases;
final WelcomePageRobot _welcomePageRobot;
final SetupPinCodeRobot _setupPinCodeRobot;
final DisclaimerPageRobot _disclaimerPageRobot;
final NewWalletTypePageRobot _newWalletTypePageRobot;
final RestoreOptionsPageRobot _restoreOptionsPageRobot;
final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
Future<void> startAppFlow(Key key) async {
await app.main(topLevelKey: ValueKey('send_flow_test_app_key'));
await _tester.pumpAndSettle();
// --------- Disclaimer Page ------------
// Tap checkbox to accept disclaimer
await _disclaimerPageRobot.tapDisclaimerCheckbox();
// Tap accept button
await _disclaimerPageRobot.tapAcceptButton();
}
Future<void> restoreWalletThroughSeedsFlow() async {
await _welcomeToRestoreFromSeedsPath();
await _restoreFromSeeds();
}
Future<void> restoreWalletThroughKeysFlow() async {
await _welcomeToRestoreFromSeedsPath();
await _restoreFromKeys();
}
Future<void> _welcomeToRestoreFromSeedsPath() async {
// --------- Welcome Page ---------------
await _welcomePageRobot.navigateToRestoreWalletPage();
// ----------- Restore Options Page -----------
// Route to restore from seeds page to continue flow
await _restoreOptionsPageRobot.navigateToRestoreFromSeedsPage();
// ----------- SetupPinCode Page -------------
// Confirm initial defaults - Widgets to be displayed etc
await _setupPinCodeRobot.isSetupPinCodePage();
await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, true);
await _setupPinCodeRobot.enterPinCode(CommonTestConstants.pin, false);
await _setupPinCodeRobot.tapSuccessButton();
// ----------- NewWalletType Page -------------
// Confirm scroll behaviour works properly
await _newWalletTypePageRobot
.findParticularWalletTypeInScrollableList(CommonTestConstants.testWalletType);
// Select a wallet and route to next page
await _newWalletTypePageRobot.selectWalletType(CommonTestConstants.testWalletType);
await _newWalletTypePageRobot.onNextButtonPressed();
}
Future<void> _restoreFromSeeds() async {
// ----------- RestoreFromSeedOrKeys Page -------------
await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName);
await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(secrets.solanaTestWalletSeeds);
await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
}
Future<void> _restoreFromKeys() async {
await _commonTestCases.swipePage();
await _commonTestCases.defaultSleepTime();
await _restoreFromSeedOrKeysPageRobot.enterWalletNameText(CommonTestConstants.testWalletName);
await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore('');
await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
}
}

View file

@ -0,0 +1,84 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'components/common_test_constants.dart';
import 'components/common_test_flows.dart';
import 'robots/auth_page_robot.dart';
import 'robots/dashboard_page_robot.dart';
import 'robots/exchange_confirm_page_robot.dart';
import 'robots/exchange_page_robot.dart';
import 'robots/exchange_trade_page_robot.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
DashboardPageRobot dashboardPageRobot;
ExchangePageRobot exchangePageRobot;
ExchangeConfirmPageRobot exchangeConfirmPageRobot;
AuthPageRobot authPageRobot;
ExchangeTradePageRobot exchangeTradePageRobot;
CommonTestFlows commonTestFlows;
group('Startup Test', () {
testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL',
(tester) async {
authPageRobot = AuthPageRobot(tester);
exchangePageRobot = ExchangePageRobot(tester);
dashboardPageRobot = DashboardPageRobot(tester);
exchangeTradePageRobot = ExchangeTradePageRobot(tester);
exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
commonTestFlows = CommonTestFlows(tester);
await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key'));
await commonTestFlows.restoreWalletThroughSeedsFlow();
// ----------- RestoreFromSeedOrKeys Page -------------
await dashboardPageRobot.navigateToExchangePage();
// ----------- Exchange Page -------------
await exchangePageRobot.isExchangePage();
exchangePageRobot.hasResetButton();
await exchangePageRobot.displayBothExchangeCards();
exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards();
exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards();
await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency);
await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency);
await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
await exchangePageRobot.enterDepositRefundAddress(
depositAddress: CommonTestConstants.testWalletAddress);
await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
await exchangePageRobot.onExchangeButtonPressed();
await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
final onAuthPage = authPageRobot.onAuthPage();
if (onAuthPage) {
await authPageRobot.enterPinCode(CommonTestConstants.pin, false);
}
// ----------- Exchange Confirm Page -------------
await exchangeConfirmPageRobot.isExchangeConfirmPage();
exchangeConfirmPageRobot.confirmComponentsOfTradeDisplayProperly();
await exchangeConfirmPageRobot.confirmCopyTradeIdToClipBoardWorksProperly();
await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
// ----------- Exchange Trade Page -------------
await exchangeTradePageRobot.isExchangeTradePage();
exchangeTradePageRobot.hasInformationDialog();
await exchangeTradePageRobot.onGotItButtonPressed();
await exchangeTradePageRobot.onConfirmSendingButtonPressed();
await exchangeTradePageRobot.handleConfirmSendResult();
await exchangeTradePageRobot.onSendButtonOnConfirmSendingDialogPressed();
});
});
}

View file

@ -0,0 +1,25 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:hive/hive.dart';
import 'package:mocktail/mocktail.dart';
class MockAppStore extends Mock implements AppStore{}
class MockAuthService extends Mock implements AuthService{}
class MockSettingsStore extends Mock implements SettingsStore {}
class MockAuthenticationStore extends Mock implements AuthenticationStore{}
class MockWalletListStore extends Mock implements WalletListStore{}
class MockLinkViewModel extends Mock implements LinkViewModel {}
class MockHiveInterface extends Mock implements HiveInterface {}
class MockHiveBox extends Mock implements Box<dynamic> {}
class MockSecureStorage extends Mock implements SecureStorage{}

View file

@ -0,0 +1,100 @@
import 'package:cake_wallet/core/auth_service.dart';
import 'package:cake_wallet/core/secure_storage.dart';
import 'package:cake_wallet/di.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/view_model/link_view_model.dart';
import 'package:hive/hive.dart';
import 'package:mocktail/mocktail.dart';
import 'mocks.dart';
class TestHelpers {
static void setup() {
// Fallback values can also be declared here
registerDependencies();
}
static void registerDependencies() {
getAndRegisterAppStore();
getAndRegisterAuthService();
getAndRegisterSettingsStore();
getAndRegisterAuthenticationStore();
getAndRegisterWalletListStore();
getAndRegisterLinkViewModel();
getAndRegisterSecureStorage();
getAndRegisterHiveInterface();
}
static MockSettingsStore getAndRegisterSettingsStore() {
_removeRegistrationIfExists<SettingsStore>();
final service = MockSettingsStore();
getIt.registerSingleton<SettingsStore>(service);
return service;
}
static MockAppStore getAndRegisterAppStore() {
_removeRegistrationIfExists<AppStore>();
final service = MockAppStore();
final settingsStore = getAndRegisterSettingsStore();
when(() => service.settingsStore).thenAnswer((invocation) => settingsStore);
getIt.registerSingleton<AppStore>(service);
return service;
}
static MockAuthService getAndRegisterAuthService() {
_removeRegistrationIfExists<AuthService>();
final service = MockAuthService();
getIt.registerSingleton<AuthService>(service);
return service;
}
static MockAuthenticationStore getAndRegisterAuthenticationStore() {
_removeRegistrationIfExists<AuthenticationStore>();
final service = MockAuthenticationStore();
when(() => service.state).thenReturn(AuthenticationState.uninitialized);
getIt.registerSingleton<AuthenticationStore>(service);
return service;
}
static MockWalletListStore getAndRegisterWalletListStore() {
_removeRegistrationIfExists<WalletListStore>();
final service = MockWalletListStore();
getIt.registerSingleton<WalletListStore>(service);
return service;
}
static MockLinkViewModel getAndRegisterLinkViewModel() {
_removeRegistrationIfExists<LinkViewModel>();
final service = MockLinkViewModel();
getIt.registerSingleton<LinkViewModel>(service);
return service;
}
static MockHiveInterface getAndRegisterHiveInterface() {
_removeRegistrationIfExists<HiveInterface>();
final service = MockHiveInterface();
final box = MockHiveBox();
getIt.registerSingleton<HiveInterface>(service);
return service;
}
static MockSecureStorage getAndRegisterSecureStorage() {
_removeRegistrationIfExists<SecureStorage>();
final service = MockSecureStorage();
getIt.registerSingleton<SecureStorage>(service);
return service;
}
static void _removeRegistrationIfExists<T extends Object>() {
if (getIt.isRegistered<T>()) {
getIt.unregister<T>();
}
}
static void tearDown() => getIt.reset();
}

View file

@ -0,0 +1 @@
null

View file

@ -0,0 +1,30 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/auth/auth_page.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
import 'pin_code_widget_robot.dart';
class AuthPageRobot extends PinCodeWidgetRobot {
AuthPageRobot(this.tester)
: commonTestCases = CommonTestCases(tester),
super(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
bool onAuthPage() {
final hasPinButtons = find.byKey(ValueKey('pin_code_button_3_key'));
final hasPin = hasPinButtons.tryEvaluate();
return hasPin;
}
Future<void> isAuthPage() async {
await commonTestCases.isSpecificPage<AuthPage>();
}
void hasTitle() {
commonTestCases.hasText(S.current.setup_pin);
}
}

View file

@ -0,0 +1,75 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/dashboard/dashboard_page.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class DashboardPageRobot {
DashboardPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isDashboardPage() async {
await commonTestCases.isSpecificPage<DashboardPage>();
}
void confirmServiceUpdateButtonDisplays() {
commonTestCases.hasValueKey('dashboard_page_services_update_button_key');
}
void confirmSyncIndicatorButtonDisplays() {
commonTestCases.hasValueKey('dashboard_page_sync_indicator_button_key');
}
void confirmMenuButtonDisplays() {
commonTestCases.hasValueKey('dashboard_page_wallet_menu_button_key');
}
Future<void> confirmRightCryptoAssetTitleDisplaysPerPageView(WalletType type,
{bool isHaven = false}) async {
//Balance Page
final walletName = walletTypeToString(type);
final assetName = isHaven ? '$walletName Assets' : walletName;
commonTestCases.hasText(assetName);
// Swipe to Cake features Page
await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false);
await commonTestCases.defaultSleepTime();
commonTestCases.hasText('Cake ${S.current.features}');
// Swipe back to balance
await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key');
await commonTestCases.defaultSleepTime();
// Swipe to Transactions Page
await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key');
await commonTestCases.defaultSleepTime();
commonTestCases.hasText(S.current.transactions);
// Swipe back to balance
await commonTestCases.swipeByPageKey(key: 'dashboard_page_view_key', swipeRight: false);
await commonTestCases.defaultSleepTime(seconds: 5);
}
Future<void> navigateToBuyPage() async {
await commonTestCases.tapItemByKey('dashboard_page_${S.current.buy}_action_button_key');
}
Future<void> navigateToSendPage() async {
await commonTestCases.tapItemByKey('dashboard_page_${S.current.send}_action_button_key');
}
Future<void> navigateToSellPage() async {
await commonTestCases.tapItemByKey('dashboard_page_${S.current.sell}_action_button_key');
}
Future<void> navigateToReceivePage() async {
await commonTestCases.tapItemByKey('dashboard_page_${S.current.receive}_action_button_key');
}
Future<void> navigateToExchangePage() async {
await commonTestCases.tapItemByKey('dashboard_page_${S.current.exchange}_action_button_key');
}
}

View file

@ -0,0 +1,39 @@
import 'package:cake_wallet/src/screens/disclaimer/disclaimer_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class DisclaimerPageRobot {
DisclaimerPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isDisclaimerPage() async {
await commonTestCases.isSpecificPage<DisclaimerPage>();
}
void hasCheckIcon(bool hasBeenTapped) {
// The checked Icon should not be available initially, until user taps the checkbox
final checkIcon = find.byKey(ValueKey('disclaimer_check_icon_key'));
expect(checkIcon, hasBeenTapped ? findsOneWidget : findsNothing);
}
void hasDisclaimerCheckbox() {
final checkBox = find.byKey(ValueKey('disclaimer_check_key'));
expect(checkBox, findsOneWidget);
}
Future<void> tapDisclaimerCheckbox() async {
await commonTestCases.tapItemByKey('disclaimer_check_key');
await commonTestCases.defaultSleepTime();
}
Future<void> tapAcceptButton() async {
await commonTestCases.tapItemByKey('disclaimer_accept_button_key');
await commonTestCases.defaultSleepTime();
}
}

View file

@ -0,0 +1,45 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_confirm_page.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class ExchangeConfirmPageRobot {
ExchangeConfirmPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isExchangeConfirmPage() async {
await commonTestCases.isSpecificPage<ExchangeConfirmPage>();
}
void confirmComponentsOfTradeDisplayProperly() {
final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage));
final trade = exchangeConfirmPage.trade;
commonTestCases.hasText(trade.id);
commonTestCases.hasText('${trade.provider.title} ${S.current.trade_id}');
commonTestCases.hasValueKey('exchange_confirm_page_saved_id_button_key');
commonTestCases.hasValueKey('exchange_confirm_page_copy_to_clipboard_button_key');
}
Future<void> confirmCopyTradeIdToClipBoardWorksProperly() async {
final ExchangeConfirmPage exchangeConfirmPage = tester.widget(find.byType(ExchangeConfirmPage));
final trade = exchangeConfirmPage.trade;
await commonTestCases.tapItemByKey('exchange_confirm_page_copy_to_clipboard_button_key');
ClipboardData? clipboardData = await Clipboard.getData('text/plain');
expect(clipboardData?.text, trade.id);
}
Future<void> onSavedTradeIdButtonPressed() async {
await tester.pumpAndSettle();
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('exchange_confirm_page_saved_id_button_key');
}
}

View file

@ -0,0 +1,330 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange/exchange_page.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class ExchangePageRobot {
ExchangePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isExchangePage() async {
await commonTestCases.isSpecificPage<ExchangePage>();
await commonTestCases.defaultSleepTime();
}
void hasResetButton() {
commonTestCases.hasText(S.current.reset);
}
void displaysPresentProviderPicker() {
commonTestCases.hasType<PresentProviderPicker>();
}
Future<void> displayBothExchangeCards() async {
final ExchangePage exchangeCard = tester.widget<ExchangePage>(
find.byType(ExchangePage),
);
final depositKey = exchangeCard.depositKey;
final receiveKey = exchangeCard.receiveKey;
final depositExchangeCard = find.byKey(depositKey);
expect(depositExchangeCard, findsOneWidget);
final receiveExchangeCard = find.byKey(receiveKey);
expect(receiveExchangeCard, findsOneWidget);
}
void confirmRightComponentsDisplayOnDepositExchangeCards() {
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
final depositCardPrefix = 'deposit_exchange_card';
commonTestCases.hasValueKey('${depositCardPrefix}_title_key');
commonTestCases.hasValueKey('${depositCardPrefix}_currency_picker_button_key');
commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_text_key');
commonTestCases.hasValueKey('${depositCardPrefix}_amount_textfield_key');
exchangePage.depositKey.currentState!.changeLimits(min: '0.1');
commonTestCases.hasValueKey('${depositCardPrefix}_min_limit_text_key');
final initialCurrency = exchangeViewModel.depositCurrency;
if (initialCurrency.tag != null) {
commonTestCases.hasValueKey('${depositCardPrefix}_selected_currency_tag_text_key');
}
if (exchangeViewModel.hasAllAmount) {
commonTestCases.hasValueKey('${depositCardPrefix}_send_all_button_key');
}
if (exchangeViewModel.isMoneroWallet) {
commonTestCases.hasValueKey('${depositCardPrefix}_address_book_button_key');
}
if (exchangeViewModel.isDepositAddressEnabled) {
commonTestCases.hasValueKey('${depositCardPrefix}_editable_address_textfield_key');
} else {
commonTestCases.hasValueKey('${depositCardPrefix}_non_editable_address_textfield_key');
commonTestCases.hasValueKey('${depositCardPrefix}_copy_refund_address_button_key');
}
// commonTestCases.hasValueKey('${depositCardPrefix}_max_limit_text_key');
}
void confirmRightComponentsDisplayOnReceiveExchangeCards() {
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
final receiveCardPrefix = 'receive_exchange_card';
commonTestCases.hasValueKey('${receiveCardPrefix}_title_key');
commonTestCases.hasValueKey('${receiveCardPrefix}_currency_picker_button_key');
commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_text_key');
commonTestCases.hasValueKey('${receiveCardPrefix}_amount_textfield_key');
commonTestCases.hasValueKey('${receiveCardPrefix}_min_limit_text_key');
final initialCurrency = exchangeViewModel.receiveCurrency;
if (initialCurrency.tag != null) {
commonTestCases.hasValueKey('${receiveCardPrefix}_selected_currency_tag_text_key');
}
if (exchangeViewModel.hasAllAmount) {
commonTestCases.hasValueKey('${receiveCardPrefix}_send_all_button_key');
}
if (exchangeViewModel.isMoneroWallet) {
commonTestCases.hasValueKey('${receiveCardPrefix}_address_book_button_key');
}
commonTestCases.hasValueKey('${receiveCardPrefix}_editable_address_textfield_key');
}
Future<void> selectDepositCurrency(CryptoCurrency depositCurrency) async {
final depositPrefix = 'deposit_exchange_card';
final currencyPickerKey = '${depositPrefix}_currency_picker_button_key';
final currencyPickerDialogKey = '${depositPrefix}_currency_picker_dialog_button_key';
await commonTestCases.tapItemByKey(currencyPickerKey);
commonTestCases.hasValueKey(currencyPickerDialogKey);
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
if (depositCurrency == exchangeViewModel.depositCurrency) {
await commonTestCases.defaultSleepTime();
await commonTestCases
.tapItemByKey('picker_items_index_${depositCurrency.name}_selected_item_button_key');
return;
}
await commonTestCases.scrollUntilVisible(
'picker_items_index_${depositCurrency.name}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${depositCurrency.name}_button_key');
}
Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
final receivePrefix = 'receive_exchange_card';
final currencyPickerKey = '${receivePrefix}_currency_picker_button_key';
final currencyPickerDialogKey = '${receivePrefix}_currency_picker_dialog_button_key';
await commonTestCases.tapItemByKey(currencyPickerKey);
commonTestCases.hasValueKey(currencyPickerDialogKey);
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
if (receiveCurrency == exchangeViewModel.receiveCurrency) {
await commonTestCases
.tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key');
return;
}
await commonTestCases.scrollUntilVisible(
'picker_items_index_${receiveCurrency.name}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
}
Future<void> enterDepositAmount(String amount) async {
await commonTestCases.enterText(amount, 'deposit_exchange_card_amount_textfield_key');
}
Future<void> enterDepositRefundAddress({String? depositAddress}) async {
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
if (exchangeViewModel.isDepositAddressEnabled && depositAddress != null) {
await commonTestCases.enterText(
depositAddress, 'deposit_exchange_card_editable_address_textfield_key');
}
}
Future<void> enterReceiveAddress(String receiveAddress) async {
await commonTestCases.enterText(
receiveAddress,
'receive_exchange_card_editable_address_textfield_key',
);
await commonTestCases.defaultSleepTime();
}
Future<void> onExchangeButtonPressed() async {
await commonTestCases.tapItemByKey('exchange_page_exchange_button_key');
await commonTestCases.defaultSleepTime();
}
bool hasMaxLimitError() {
final maxErrorText = find.text(S.current.error_text_input_above_maximum_limit);
bool hasMaxError = maxErrorText.tryEvaluate();
return hasMaxError;
}
bool hasMinLimitError() {
final minErrorText = find.text(S.current.error_text_input_below_minimum_limit);
bool hasMinError = minErrorText.tryEvaluate();
return hasMinError;
}
bool hasTradeCreationFailureError() {
final tradeCreationFailureDialogButton =
find.byKey(ValueKey('exchange_page_trade_creation_failure_dialog_button_key'));
bool hasTradeCreationFailure = tradeCreationFailureDialogButton.tryEvaluate();
tester.printToConsole('Trade not created error: $hasTradeCreationFailure');
return hasTradeCreationFailure;
}
Future<void> onTradeCreationFailureDialogButtonPressed() async {
await commonTestCases.tapItemByKey('exchange_page_trade_creation_failure_dialog_button_key');
}
/// Handling Trade Failure Errors or errors shown through the Failure Dialog.
///
/// Simulating the user's flow and response when this error comes up.
/// Examples are:
/// - No provider can handle this trade error,
/// - Trade amount below limit error.
Future<void> _handleTradeCreationFailureErrors() async {
bool isTradeCreationFailure = false;
isTradeCreationFailure = hasTradeCreationFailureError();
int maxRetries = 20;
int retries = 0;
while (isTradeCreationFailure && retries < maxRetries) {
await tester.pump();
await onTradeCreationFailureDialogButtonPressed();
await commonTestCases.defaultSleepTime(seconds: 5);
await onExchangeButtonPressed();
isTradeCreationFailure = hasTradeCreationFailureError();
retries++;
}
}
/// Handles the min limit error.
///
/// Simulates the user's flow and response when it comes up.
///
/// Has a max retry of 20 times.
Future<void> _handleMinLimitError(String initialAmount) async {
bool isMinLimitError = false;
isMinLimitError = hasMinLimitError();
double amount;
amount = double.parse(initialAmount);
int maxRetries = 20;
int retries = 0;
while (isMinLimitError && retries < maxRetries) {
amount++;
tester.printToConsole('Amount: $amount');
enterDepositAmount(amount.toString());
await commonTestCases.defaultSleepTime();
await onExchangeButtonPressed();
isMinLimitError = hasMinLimitError();
retries++;
}
if (retries >= maxRetries) {
tester.printToConsole('Max retries reached for minLimit Error. Exiting loop.');
}
}
/// Handles the max limit error.
///
/// Simulates the user's flow and response when it comes up.
///
/// Has a max retry of 20 times.
Future<void> _handleMaxLimitError(String initialAmount) async {
bool isMaxLimitError = false;
isMaxLimitError = hasMaxLimitError();
double amount;
amount = double.parse(initialAmount);
int maxRetries = 20;
int retries = 0;
while (isMaxLimitError && retries < maxRetries) {
amount++;
tester.printToConsole('Amount: $amount');
enterDepositAmount(amount.toString());
await commonTestCases.defaultSleepTime();
await onExchangeButtonPressed();
isMaxLimitError = hasMaxLimitError();
retries++;
}
if (retries >= maxRetries) {
tester.printToConsole('Max retries reached for maxLimit Error. Exiting loop.');
}
}
Future<void> handleErrors(String initialAmount) async {
await tester.pumpAndSettle();
await _handleMinLimitError(initialAmount);
await _handleMaxLimitError(initialAmount);
await _handleTradeCreationFailureErrors();
await commonTestCases.defaultSleepTime();
}
}

View file

@ -0,0 +1,152 @@
import 'dart:async';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_page.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class ExchangeTradePageRobot {
ExchangeTradePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isExchangeTradePage() async {
await commonTestCases.isSpecificPage<ExchangeTradePage>();
}
void hasInformationDialog() {
commonTestCases.hasValueKey('information_page_dialog_key');
}
Future<void> onGotItButtonPressed() async {
await commonTestCases.tapItemByKey('information_page_got_it_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> onConfirmSendingButtonPressed() async {
tester.printToConsole('Now confirming sending');
await commonTestCases.tapItemByKey(
'exchange_trade_page_confirm_sending_button_key',
shouldPumpAndSettle: false,
);
final Completer<void> completer = Completer<void>();
// Loop to wait for the async operation to complete
while (true) {
await Future.delayed(Duration(seconds: 1));
final ExchangeTradeState state = tester.state(find.byType(ExchangeTradeForm));
final execState = state.widget.exchangeTradeViewModel.sendViewModel.state;
bool isDone = execState is ExecutedSuccessfullyState;
bool isFailed = execState is FailureState;
tester.printToConsole('isDone: $isDone');
tester.printToConsole('isFailed: $isFailed');
if (isDone || isFailed) {
tester.printToConsole(
isDone ? 'Completer is done' : 'Completer is done though operation failed');
completer.complete();
await tester.pump();
break;
} else {
tester.printToConsole('Completer is not done');
await tester.pump();
}
}
await expectLater(completer.future, completes);
tester.printToConsole('Done confirming sending');
await commonTestCases.defaultSleepTime(seconds: 4);
}
Future<void> onSendButtonOnConfirmSendingDialogPressed() async {
tester.printToConsole('Send Button on Confirm Dialog Triggered');
await commonTestCases.defaultSleepTime(seconds: 4);
final sendText = find.text(S.current.send);
bool hasText = sendText.tryEvaluate();
if (hasText) {
await commonTestCases.tapItemByFinder(sendText);
await commonTestCases.defaultSleepTime(seconds: 4);
}
}
Future<void> onCancelButtonOnConfirmSendingDialogPressed() async {
tester.printToConsole('Cancel Button on Confirm Dialog Triggered');
await commonTestCases.tapItemByKey(
'exchange_trade_page_confirm_sending_dialog_cancel_button_key',
);
await commonTestCases.defaultSleepTime();
}
Future<void> onSendFailureDialogButtonPressed() async {
await commonTestCases.defaultSleepTime(seconds: 6);
tester.printToConsole('Send Button Failure Dialog Triggered');
await commonTestCases.tapItemByKey('exchange_trade_page_send_failure_dialog_button_key');
}
Future<bool> hasErrorWhileSending() async {
await tester.pump();
tester.printToConsole('Checking if there is an error');
final errorDialog = find.byKey(
ValueKey('exchange_trade_page_send_failure_dialog_button_key'),
);
bool hasError = errorDialog.tryEvaluate();
tester.printToConsole('Has error: $hasError');
return hasError;
}
Future<void> handleConfirmSendResult() async {
bool hasError = false;
hasError = await hasErrorWhileSending();
int maxRetries = 20;
int retries = 0;
while (hasError && retries < maxRetries) {
tester.printToConsole('hasErrorInLoop: $hasError');
await tester.pump();
await onSendFailureDialogButtonPressed();
tester.printToConsole('Failure button tapped');
await commonTestCases.defaultSleepTime();
await onConfirmSendingButtonPressed();
tester.printToConsole('Confirm sending button tapped');
hasError = await hasErrorWhileSending();
retries++;
}
if (!hasError) {
tester.printToConsole('No error, proceeding with flow');
await tester.pump();
}
await commonTestCases.defaultSleepTime();
}
}

View file

@ -0,0 +1,59 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/new_wallet/new_wallet_type_page.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class NewWalletTypePageRobot {
NewWalletTypePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isNewWalletTypePage() async {
await commonTestCases.isSpecificPage<NewWalletTypePage>();
}
void displaysCorrectTitle(bool isCreate) {
commonTestCases.hasText(
isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet,
);
}
void hasWalletTypeForm() {
commonTestCases.hasType<WalletTypeForm>();
}
void displaysCorrectImage(ThemeType type) {
final walletTypeImage = Image.asset('assets/images/wallet_type.png').image;
final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png').image;
find.image(
type == ThemeType.dark ? walletTypeImage : walletTypeLightImage,
);
}
Future<void> findParticularWalletTypeInScrollableList(WalletType type) async {
final scrollableWidget = find.descendant(
of: find.byKey(Key('new_wallet_type_scrollable_key')),
matching: find.byType(Scrollable),
);
await tester.scrollUntilVisible(
find.byKey(ValueKey('new_wallet_type_${type.name}_button_key')),
300,
scrollable: scrollableWidget,
);
}
Future<void> selectWalletType(WalletType type) async {
await commonTestCases.tapItemByKey('new_wallet_type_${type.name}_button_key');
}
Future<void> onNextButtonPressed() async {
await commonTestCases.tapItemByKey('new_wallet_type_next_button_key');
}
}

View file

@ -0,0 +1,38 @@
import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class PinCodeWidgetRobot {
PinCodeWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
void hasPinCodeWidget() {
final pinCodeWidget = find.bySubtype<PinCodeWidget>();
expect(pinCodeWidget, findsOneWidget);
}
void hasNumberButtonsVisible() {
// Confirmation for buttons 1-9
for (var i = 1; i < 10; i++) {
commonTestCases.hasValueKey('pin_code_button_${i}_key');
}
// Confirmation for 0 button
commonTestCases.hasValueKey('pin_code_button_0_key');
}
Future<void> pushPinButton(int index) async {
await commonTestCases.tapItemByKey('pin_code_button_${index}_key');
}
Future<void> enterPinCode(List<int> pinCode, bool isFirstEntry) async {
for (int pin in pinCode) {
await pushPinButton(pin);
}
await commonTestCases.defaultSleepTime();
}
}

View file

@ -0,0 +1,89 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/restore/wallet_restore_page.dart';
import 'package:cake_wallet/src/widgets/validable_annotated_editable_text.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class RestoreFromSeedOrKeysPageRobot {
RestoreFromSeedOrKeysPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isRestoreFromSeedKeyPage() async {
await commonTestCases.isSpecificPage<WalletRestorePage>();
}
Future<void> confirmViewComponentsDisplayProperlyPerPageView() async {
commonTestCases.hasText(S.current.wallet_name);
commonTestCases.hasText(S.current.enter_seed_phrase);
commonTestCases.hasText(S.current.restore_title_from_seed);
commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_textfield_key');
commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key');
commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
commonTestCases.hasValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key');
commonTestCases.hasText(S.current.private_key, hasWidget: false);
commonTestCases.hasText(S.current.restore_title_from_keys, hasWidget: false);
await commonTestCases.swipePage();
await commonTestCases.defaultSleepTime();
commonTestCases.hasText(S.current.wallet_name);
commonTestCases.hasText(S.current.private_key);
commonTestCases.hasText(S.current.restore_title_from_keys);
commonTestCases.hasText(S.current.enter_seed_phrase, hasWidget: false);
commonTestCases.hasText(S.current.restore_title_from_seed, hasWidget: false);
await commonTestCases.swipePage(swipeRight: false);
}
void confirmRestoreButtonDisplays() {
commonTestCases.hasValueKey('wallet_restore_seed_or_key_restore_button_key');
}
void confirmAdvancedSettingButtonDisplays() {
commonTestCases.hasValueKey('wallet_restore_advanced_settings_button_key');
}
Future<void> enterWalletNameText(String walletName, {bool isSeedFormEntry = true}) async {
await commonTestCases.enterText(
walletName,
'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_textfield_key',
);
}
Future<void> selectWalletNameFromAvailableOptions({bool isSeedFormEntry = true}) async {
await commonTestCases.tapItemByKey(
'wallet_restore_from_${isSeedFormEntry ? 'seed' : 'keys'}_wallet_name_refresh_button_key',
);
}
Future<void> enterSeedPhraseForWalletRestore(String text) async {
ValidatableAnnotatedEditableTextState seedTextState =
await tester.state(find.byType(ValidatableAnnotatedEditableText));
seedTextState.widget.controller.text = text;
await tester.pumpAndSettle();
}
Future<void> onPasteSeedPhraseButtonPressed() async {
await commonTestCases.tapItemByKey('wallet_restore_from_seed_wallet_seeds_paste_button_key');
}
Future<void> enterPrivateKeyForWalletRestore(String privateKey) async {
await commonTestCases.enterText(
privateKey,
'wallet_restore_from_key_private_key_textfield_key',
);
await tester.pumpAndSettle();
}
Future<void> onRestoreWalletButtonPressed() async {
await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key');
await commonTestCases.defaultSleepTime();
}
}

View file

@ -0,0 +1,42 @@
import 'package:cake_wallet/src/screens/restore/restore_options_page.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class RestoreOptionsPageRobot {
RestoreOptionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isRestoreOptionsPage() async {
await commonTestCases.isSpecificPage<RestoreOptionsPage>();
}
void hasRestoreOptionsButton() {
commonTestCases.hasValueKey('restore_options_from_seeds_button_key');
commonTestCases.hasValueKey('restore_options_from_backup_button_key');
commonTestCases.hasValueKey('restore_options_from_hardware_wallet_button_key');
commonTestCases.hasValueKey('restore_options_from_qr_button_key');
}
Future<void> navigateToRestoreFromSeedsPage() async {
await commonTestCases.tapItemByKey('restore_options_from_seeds_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> navigateToRestoreFromBackupPage() async {
await commonTestCases.tapItemByKey('restore_options_from_backup_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> navigateToRestoreFromHardwareWalletPage() async {
await commonTestCases.tapItemByKey('restore_options_from_hardware_wallet_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> backAndVerify() async {
await commonTestCases.goBack();
await isRestoreOptionsPage();
}
}

View file

@ -0,0 +1,366 @@
import 'dart:async';
import 'package:cake_wallet/core/execution_state.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/send/send_page.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
import '../components/common_test_constants.dart';
import 'auth_page_robot.dart';
class SendPageRobot {
SendPageRobot({required this.tester})
: commonTestCases = CommonTestCases(tester),
authPageRobot = AuthPageRobot(tester);
WidgetTester tester;
CommonTestCases commonTestCases;
AuthPageRobot authPageRobot;
Future<void> isSendPage() async {
await commonTestCases.isSpecificPage<SendPage>();
}
void hasTitle() {
commonTestCases.hasText(S.current.send);
}
void confirmViewComponentsDisplayProperly() {
SendPage sendPage = tester.widget(find.byType(SendPage));
final sendViewModel = sendPage.sendViewModel;
commonTestCases.hasValueKey('send_page_address_textfield_key');
commonTestCases.hasValueKey('send_page_note_textfield_key');
commonTestCases.hasValueKey('send_page_amount_textfield_key');
commonTestCases.hasValueKey('send_page_add_template_button_key');
if (sendViewModel.hasMultipleTokens) {
commonTestCases.hasValueKey('send_page_currency_picker_button_key');
}
if (!sendViewModel.isBatchSending) {
commonTestCases.hasValueKey('send_page_send_all_button_key');
}
if (!sendViewModel.isFiatDisabled) {
commonTestCases.hasValueKey('send_page_fiat_amount_textfield_key');
}
if (sendViewModel.hasFees) {
commonTestCases.hasValueKey('send_page_select_fee_priority_button_key');
}
if (sendViewModel.hasCoinControl) {
commonTestCases.hasValueKey('send_page_unspent_coin_button_key');
}
if (sendViewModel.hasCurrecyChanger) {
commonTestCases.hasValueKey('send_page_change_asset_button_key');
}
if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) {
commonTestCases.hasValueKey('send_page_add_receiver_button_key');
}
}
Future<void> selectReceiveCurrency(CryptoCurrency receiveCurrency) async {
final currencyPickerKey = 'send_page_currency_picker_button_key';
final currencyPickerDialogKey = 'send_page_currency_picker_dialog_button_key';
await commonTestCases.tapItemByKey(currencyPickerKey);
commonTestCases.hasValueKey(currencyPickerDialogKey);
SendPage sendPage = tester.widget(find.byType(SendPage));
final sendViewModel = sendPage.sendViewModel;
if (receiveCurrency == sendViewModel.selectedCryptoCurrency) {
await commonTestCases
.tapItemByKey('picker_items_index_${receiveCurrency.name}_selected_item_button_key');
return;
}
await commonTestCases.scrollUntilVisible(
'picker_items_index_${receiveCurrency.name}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.name}_button_key');
}
Future<void> enterReceiveAddress(String receiveAddress) async {
await commonTestCases.enterText(receiveAddress, 'send_page_address_textfield_key');
await commonTestCases.defaultSleepTime();
}
Future<void> enterAmount(String amount) async {
await commonTestCases.enterText(amount, 'send_page_amount_textfield_key');
}
Future<void> selectTransactionPriority({TransactionPriority? priority}) async {
SendPage sendPage = tester.widget(find.byType(SendPage));
final sendViewModel = sendPage.sendViewModel;
if (!sendViewModel.hasFees || priority == null) return;
final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key';
await commonTestCases.tapItemByKey(transactionPriorityPickerKey);
if (priority == sendViewModel.transactionPriority) {
await commonTestCases
.tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key');
return;
}
await commonTestCases.scrollUntilVisible(
'picker_items_index_${priority.title}_button_key',
'picker_scrollbar_key',
);
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${priority.title}_button_key');
}
Future<void> onSendButtonPressed() async {
tester.printToConsole('Pressing send');
await commonTestCases.tapItemByKey(
'send_page_send_button_key',
shouldPumpAndSettle: false,
);
await _waitForSendTransactionCompletion();
await commonTestCases.defaultSleepTime();
}
Future<void> _waitForSendTransactionCompletion() async {
await tester.pump();
final Completer<void> completer = Completer<void>();
// Loop to wait for the async operation to complete
while (true) {
await Future.delayed(Duration(seconds: 1));
tester.printToConsole('Before _handleAuth');
await _handleAuthPage();
tester.printToConsole('After _handleAuth');
await tester.pump();
final sendPage = tester.widget<SendPage>(find.byType(SendPage));
final state = sendPage.sendViewModel.state;
await tester.pump();
bool isDone = state is ExecutedSuccessfullyState;
bool isFailed = state is FailureState;
tester.printToConsole('isDone: $isDone');
tester.printToConsole('isFailed: $isFailed');
if (isDone || isFailed) {
tester.printToConsole(
isDone ? 'Completer is done' : 'Completer is done though operation failed',
);
completer.complete();
await tester.pump();
break;
} else {
tester.printToConsole('Completer is not done');
await tester.pump();
}
}
await expectLater(completer.future, completes);
tester.printToConsole('Done confirming sending operation');
}
Future<void> _handleAuthPage() async {
tester.printToConsole('Inside _handleAuth');
await tester.pump();
tester.printToConsole('starting auth checks');
final authPage = authPageRobot.onAuthPage();
tester.printToConsole('hasAuth:$authPage');
if (authPage) {
await tester.pump();
tester.printToConsole('Starting inner _handleAuth loop checks');
try {
await authPageRobot.enterPinCode(CommonTestConstants.pin, false);
tester.printToConsole('Auth done');
await tester.pump();
tester.printToConsole('Auth pump done');
} catch (e) {
tester.printToConsole('Auth failed, retrying');
await tester.pump();
_handleAuthPage();
}
}
}
Future<void> handleSendResult() async {
tester.printToConsole('Inside handle function');
bool hasError = false;
hasError = await hasErrorWhileSending();
tester.printToConsole('Has an Error in the handle: $hasError');
int maxRetries = 20;
int retries = 0;
while (hasError && retries < maxRetries) {
tester.printToConsole('hasErrorInLoop: $hasError');
await tester.pump();
await onSendFailureDialogButtonPressed();
tester.printToConsole('Failure button tapped');
await commonTestCases.defaultSleepTime();
await onSendButtonPressed();
tester.printToConsole('Send button tapped');
hasError = await hasErrorWhileSending();
retries++;
}
if (!hasError) {
tester.printToConsole('No error, proceeding with flow');
await tester.pump();
}
await commonTestCases.defaultSleepTime();
}
//* ------ On Sending Failure ------------
Future<bool> hasErrorWhileSending() async {
await tester.pump();
tester.printToConsole('Checking if there is an error');
final errorDialog = find.byKey(ValueKey('send_page_send_failure_dialog_button_key'));
bool hasError = errorDialog.tryEvaluate();
tester.printToConsole('Has error: $hasError');
return hasError;
}
Future<void> onSendFailureDialogButtonPressed() async {
await commonTestCases.defaultSleepTime();
tester.printToConsole('Send Button Failure Dialog Triggered');
await commonTestCases.tapItemByKey('send_page_send_failure_dialog_button_key');
}
//* ------ On Sending Success ------------
Future<void> onSendButtonOnConfirmSendingDialogPressed() async {
tester.printToConsole('Inside confirm sending dialog: For sending');
await commonTestCases.defaultSleepTime();
await tester.pump();
final sendText = find.text(S.current.send).last;
bool hasText = sendText.tryEvaluate();
tester.printToConsole('Has Text: $hasText');
if (hasText) {
await commonTestCases.tapItemByFinder(sendText, shouldPumpAndSettle: false);
// Loop to wait for the operation to commit transaction
await _waitForCommitTransactionCompletion();
await commonTestCases.defaultSleepTime(seconds: 4);
} else {
await commonTestCases.defaultSleepTime();
await tester.pump();
onSendButtonOnConfirmSendingDialogPressed();
}
}
Future<void> _waitForCommitTransactionCompletion() async {
final Completer<void> completer = Completer<void>();
while (true) {
await Future.delayed(Duration(seconds: 1));
final sendPage = tester.widget<SendPage>(find.byType(SendPage));
final state = sendPage.sendViewModel.state;
bool isDone = state is TransactionCommitted;
bool isFailed = state is FailureState;
tester.printToConsole('isDone: $isDone');
tester.printToConsole('isFailed: $isFailed');
if (isDone || isFailed) {
tester.printToConsole(
isDone ? 'Completer is done' : 'Completer is done though operation failed',
);
completer.complete();
await tester.pump();
break;
} else {
tester.printToConsole('Completer is not done');
await tester.pump();
}
}
await expectLater(completer.future, completes);
tester.printToConsole('Done Committing Transaction');
}
Future<void> onCancelButtonOnConfirmSendingDialogPressed() async {
tester.printToConsole('Inside confirm sending dialog: For canceling');
await commonTestCases.defaultSleepTime(seconds: 4);
final cancelText = find.text(S.current.cancel);
bool hasText = cancelText.tryEvaluate();
if (hasText) {
await commonTestCases.tapItemByFinder(cancelText);
await commonTestCases.defaultSleepTime(seconds: 4);
}
}
//* ---- Add Contact Dialog On Send Successful Dialog -----
Future<void> onSentDialogPopUp() async {
SendPage sendPage = tester.widget(find.byType(SendPage));
final sendViewModel = sendPage.sendViewModel;
final newContactAddress = sendPage.newContactAddress ?? sendViewModel.newContactAddress();
if (newContactAddress != null) {
await _onAddContactButtonOnSentDialogPressed();
}
await commonTestCases.defaultSleepTime();
}
Future<void> _onAddContactButtonOnSentDialogPressed() async {
await commonTestCases.tapItemByKey('send_page_sent_dialog_add_contact_button_key');
}
// ignore: unused_element
Future<void> _onIgnoreButtonOnSentDialogPressed() async {
await commonTestCases.tapItemByKey('send_page_sent_dialog_ignore_button_key');
}
}

View file

@ -0,0 +1,28 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
import 'pin_code_widget_robot.dart';
class SetupPinCodeRobot extends PinCodeWidgetRobot {
SetupPinCodeRobot(this.tester)
: commonTestCases = CommonTestCases(tester),
super(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isSetupPinCodePage() async {
await commonTestCases.isSpecificPage<SetupPinCodePage>();
}
void hasTitle() {
commonTestCases.hasText(S.current.setup_pin);
}
Future<void> tapSuccessButton() async {
await commonTestCases.tapItemByKey('setup_pin_code_success_button_key');
await commonTestCases.defaultSleepTime();
}
}

View file

@ -0,0 +1,40 @@
import 'package:cake_wallet/src/screens/welcome/welcome_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class WelcomePageRobot {
WelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isWelcomePage() async {
await commonTestCases.isSpecificPage<WelcomePage>();
}
void confirmActionButtonsDisplay() {
final createNewWalletButton = find.byKey(ValueKey('welcome_page_create_new_wallet_button_key'));
final restoreWalletButton = find.byKey(ValueKey('welcome_page_restore_wallet_button_key'));
expect(createNewWalletButton, findsOneWidget);
expect(restoreWalletButton, findsOneWidget);
}
Future<void> navigateToCreateNewWalletPage() async {
await commonTestCases.tapItemByKey('welcome_page_create_new_wallet_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> navigateToRestoreWalletPage() async {
await commonTestCases.tapItemByKey('welcome_page_restore_wallet_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> backAndVerify() async {
await commonTestCases.goBack();
await isWelcomePage();
}
}

View file

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../components/common_test_constants.dart';
import '../components/common_test_flows.dart';
import '../robots/auth_page_robot.dart';
import '../robots/dashboard_page_robot.dart';
import '../robots/exchange_confirm_page_robot.dart';
import '../robots/exchange_page_robot.dart';
import '../robots/exchange_trade_page_robot.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
AuthPageRobot authPageRobot;
CommonTestFlows commonTestFlows;
ExchangePageRobot exchangePageRobot;
DashboardPageRobot dashboardPageRobot;
ExchangeTradePageRobot exchangeTradePageRobot;
ExchangeConfirmPageRobot exchangeConfirmPageRobot;
group('Exchange Flow Tests', () {
testWidgets('Exchange flow', (tester) async {
authPageRobot = AuthPageRobot(tester);
commonTestFlows = CommonTestFlows(tester);
exchangePageRobot = ExchangePageRobot(tester);
dashboardPageRobot = DashboardPageRobot(tester);
exchangeTradePageRobot = ExchangeTradePageRobot(tester);
exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key'));
await commonTestFlows.restoreWalletThroughSeedsFlow();
await dashboardPageRobot.navigateToExchangePage();
// ----------- Exchange Page -------------
await exchangePageRobot.selectDepositCurrency(CommonTestConstants.testDepositCurrency);
await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency);
await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
await exchangePageRobot.enterDepositRefundAddress(
depositAddress: CommonTestConstants.testWalletAddress,
);
await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
await exchangePageRobot.onExchangeButtonPressed();
await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
final onAuthPage = authPageRobot.onAuthPage();
if (onAuthPage) {
await authPageRobot.enterPinCode(CommonTestConstants.pin, false);
}
await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
await exchangeTradePageRobot.onGotItButtonPressed();
});
});
}

View file

@ -0,0 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../components/common_test_constants.dart';
import '../components/common_test_flows.dart';
import '../robots/dashboard_page_robot.dart';
import '../robots/send_page_robot.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
SendPageRobot sendPageRobot;
CommonTestFlows commonTestFlows;
DashboardPageRobot dashboardPageRobot;
group('Send Flow Tests', () {
testWidgets('Send flow', (tester) async {
commonTestFlows = CommonTestFlows(tester);
sendPageRobot = SendPageRobot(tester: tester);
dashboardPageRobot = DashboardPageRobot(tester);
await commonTestFlows.startAppFlow(ValueKey('send_test_app_key'));
await commonTestFlows.restoreWalletThroughSeedsFlow();
await dashboardPageRobot.navigateToSendPage();
await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
await sendPageRobot.selectReceiveCurrency(CommonTestConstants.testReceiveCurrency);
await sendPageRobot.enterAmount(CommonTestConstants.sendTestAmount);
await sendPageRobot.selectTransactionPriority();
await sendPageRobot.onSendButtonPressed();
await sendPageRobot.handleSendResult();
await sendPageRobot.onSendButtonOnConfirmSendingDialogPressed();
await sendPageRobot.onSentDialogPopUp();
});
});
}

View file

@ -66,6 +66,8 @@ PODS:
- Toast - Toast
- in_app_review (0.2.0): - in_app_review (0.2.0):
- Flutter - Flutter
- integration_test (0.0.1):
- Flutter
- MTBBarcodeScanner (5.0.11) - MTBBarcodeScanner (5.0.11)
- OrderedSet (5.0.0) - OrderedSet (5.0.0)
- package_info_plus (0.4.5): - package_info_plus (0.4.5):
@ -120,6 +122,8 @@ DEPENDENCIES:
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`) - in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- integration_test (from `.symlinks/plugins/integration_test/ios`)
- package_info (from `.symlinks/plugins/package_info/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`)
@ -174,6 +178,10 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/fluttertoast/ios" :path: ".symlinks/plugins/fluttertoast/ios"
in_app_review: in_app_review:
:path: ".symlinks/plugins/in_app_review/ios" :path: ".symlinks/plugins/in_app_review/ios"
integration_test:
:path: ".symlinks/plugins/integration_test/ios"
package_info:
:path: ".symlinks/plugins/package_info/ios"
package_info_plus: package_info_plus:
:path: ".symlinks/plugins/package_info_plus/ios" :path: ".symlinks/plugins/package_info_plus/ios"
path_provider_foundation: path_provider_foundation:
@ -216,6 +224,7 @@ SPEC CHECKSUMS:
flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be
fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c fluttertoast: 48c57db1b71b0ce9e6bba9f31c940ff4b001293c
in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d
integration_test: 13825b8a9334a850581300559b8839134b124670
MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb
OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c
package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c

View file

@ -47,11 +47,11 @@ final navigatorKey = GlobalKey<NavigatorState>();
final rootKey = GlobalKey<RootState>(); final rootKey = GlobalKey<RootState>();
final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>(); final RouteObserver<PageRoute<dynamic>> routeObserver = RouteObserver<PageRoute<dynamic>>();
Future<void> main() async { Future<void> main({Key? topLevelKey}) async {
await runAppWithZone(); await runAppWithZone(topLevelKey: topLevelKey);
} }
Future<void> runAppWithZone() async { Future<void> runAppWithZone({Key? topLevelKey}) async {
bool isAppRunning = false; bool isAppRunning = false;
await runZonedGuarded(() async { await runZonedGuarded(() async {
@ -67,7 +67,8 @@ Future<void> runAppWithZone() async {
}; };
await initializeAppAtRoot(); await initializeAppAtRoot();
runApp(App()); runApp(App(key: topLevelKey));
isAppRunning = true; isAppRunning = true;
}, (error, stackTrace) async { }, (error, stackTrace) async {
if (!isAppRunning) { if (!isAppRunning) {
@ -236,6 +237,9 @@ Future<void> initialSetup(
} }
class App extends StatefulWidget { class App extends StatefulWidget {
App({this.key});
final Key? key;
@override @override
AppState createState() => AppState(); AppState createState() => AppState();
} }
@ -264,7 +268,7 @@ class AppState extends State<App> with SingleTickerProviderStateMixin {
statusBarIconBrightness: statusBarIconBrightness)); statusBarIconBrightness: statusBarIconBrightness));
return Root( return Root(
key: rootKey, key: widget.key ?? rootKey,
appStore: appStore, appStore: appStore,
authenticationStore: authenticationStore, authenticationStore: authenticationStore,
navigatorKey: navigatorKey, navigatorKey: navigatorKey,

View file

@ -147,6 +147,7 @@ class _DashboardPageView extends BasePage {
return Observer( return Observer(
builder: (context) { builder: (context) {
return ServicesUpdatesWidget( return ServicesUpdatesWidget(
key: ValueKey('dashboard_page_services_update_button_key'),
dashboardViewModel.getServicesStatus(), dashboardViewModel.getServicesStatus(),
enabled: dashboardViewModel.isEnabledBulletinAction, enabled: dashboardViewModel.isEnabledBulletinAction,
); );
@ -157,6 +158,7 @@ class _DashboardPageView extends BasePage {
@override @override
Widget middle(BuildContext context) { Widget middle(BuildContext context) {
return SyncIndicator( return SyncIndicator(
key: ValueKey('dashboard_page_sync_indicator_button_key'),
dashboardViewModel: dashboardViewModel, dashboardViewModel: dashboardViewModel,
onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync), onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync),
); );
@ -173,6 +175,7 @@ class _DashboardPageView extends BasePage {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
width: 40, width: 40,
child: TextButton( child: TextButton(
key: ValueKey('dashboard_page_wallet_menu_button_key'),
// FIX-ME: Style // FIX-ME: Style
//highlightColor: Colors.transparent, //highlightColor: Colors.transparent,
//splashColor: Colors.transparent, //splashColor: Colors.transparent,
@ -226,6 +229,7 @@ class _DashboardPageView extends BasePage {
child: Observer( child: Observer(
builder: (context) { builder: (context) {
return PageView.builder( return PageView.builder(
key: ValueKey('dashboard_page_view_key'),
controller: controller, controller: controller,
itemCount: pages.length, itemCount: pages.length,
itemBuilder: (context, index) => pages[index], itemBuilder: (context, index) => pages[index],
@ -291,6 +295,8 @@ class _DashboardPageView extends BasePage {
button: true, button: true,
enabled: (action.isEnabled?.call(dashboardViewModel) ?? true), enabled: (action.isEnabled?.call(dashboardViewModel) ?? true),
child: ActionButton( child: ActionButton(
key: ValueKey(
'dashboard_page_${action.name(context)}_action_button_key'),
image: Image.asset( image: Image.asset(
action.image, action.image,
height: 24, height: 24,

View file

@ -2,13 +2,15 @@ import 'package:flutter/material.dart';
import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart';
class ActionButton extends StatelessWidget { class ActionButton extends StatelessWidget {
ActionButton( ActionButton({
{required this.image, required this.image,
required this.title, required this.title,
this.route, this.route,
this.onClick, this.onClick,
this.alignment = Alignment.center, this.alignment = Alignment.center,
this.textColor}); this.textColor,
super.key,
});
final Image image; final Image image;
final String title; final String title;

View file

@ -7,7 +7,11 @@ import 'package:cw_core/sync_status.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
class SyncIndicator extends StatelessWidget { class SyncIndicator extends StatelessWidget {
SyncIndicator({required this.dashboardViewModel, required this.onTap}); SyncIndicator({
required this.dashboardViewModel,
required this.onTap,
super.key,
});
final DashboardViewModel dashboardViewModel; final DashboardViewModel dashboardViewModel;
final Function() onTap; final Function() onTap;

View file

@ -207,6 +207,7 @@ class DisclaimerBodyState extends State<DisclaimerPageBody> {
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 24.0, top: 10.0, right: 24.0, bottom: 10.0), left: 24.0, top: 10.0, right: 24.0, bottom: 10.0),
child: InkWell( child: InkWell(
key: ValueKey('disclaimer_check_key'),
onTap: () { onTap: () {
setState(() { setState(() {
_checked = !_checked; _checked = !_checked;
@ -230,6 +231,7 @@ class DisclaimerBodyState extends State<DisclaimerPageBody> {
color: Theme.of(context).colorScheme.background), color: Theme.of(context).colorScheme.background),
child: _checked child: _checked
? Icon( ? Icon(
key: ValueKey('disclaimer_check_icon_key'),
Icons.check, Icons.check,
color: Colors.blue, color: Colors.blue,
size: 20.0, size: 20.0,
@ -253,6 +255,7 @@ class DisclaimerBodyState extends State<DisclaimerPageBody> {
padding: padding:
EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0), EdgeInsets.only(left: 24.0, right: 24.0, bottom: 24.0),
child: PrimaryButton( child: PrimaryButton(
key: ValueKey('disclaimer_accept_button_key'),
onPressed: _checked onPressed: _checked
? () => Navigator.of(context) ? () => Navigator.of(context)
.popAndPushNamed(Routes.welcome) .popAndPushNamed(Routes.welcome)

View file

@ -228,6 +228,7 @@ class ExchangePage extends BasePage {
), ),
Observer( Observer(
builder: (_) => LoadingPrimaryButton( builder: (_) => LoadingPrimaryButton(
key: ValueKey('exchange_page_exchange_button_key'),
text: S.of(context).exchange, text: S.of(context).exchange,
onPressed: () { onPressed: () {
if (_formKey.currentState != null && if (_formKey.currentState != null &&
@ -430,6 +431,8 @@ class ExchangePage extends BasePage {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertWithOneAction( return AlertWithOneAction(
key: ValueKey('exchange_page_trade_creation_failure_dialog_key'),
buttonKey: ValueKey('exchange_page_trade_creation_failure_dialog_button_key'),
alertTitle: S.of(context).provider_error(state.title), alertTitle: S.of(context).provider_error(state.title),
alertContent: state.error, alertContent: state.error,
buttonText: S.of(context).ok, buttonText: S.of(context).ok,
@ -612,6 +615,7 @@ class ExchangePage extends BasePage {
Widget _exchangeCardsSection(BuildContext context) { Widget _exchangeCardsSection(BuildContext context) {
final firstExchangeCard = Observer( final firstExchangeCard = Observer(
builder: (_) => ExchangeCard( builder: (_) => ExchangeCard(
cardInstanceName: 'deposit_exchange_card',
onDispose: disposeBestRateSync, onDispose: disposeBestRateSync,
hasAllAmount: exchangeViewModel.hasAllAmount, hasAllAmount: exchangeViewModel.hasAllAmount,
allAmount: exchangeViewModel.hasAllAmount allAmount: exchangeViewModel.hasAllAmount
@ -681,6 +685,7 @@ class ExchangePage extends BasePage {
final secondExchangeCard = Observer( final secondExchangeCard = Observer(
builder: (_) => ExchangeCard( builder: (_) => ExchangeCard(
cardInstanceName: 'receive_exchange_card',
onDispose: disposeBestRateSync, onDispose: disposeBestRateSync,
amountFocusNode: _receiveAmountFocus, amountFocusNode: _receiveAmountFocus,
addressFocusNode: _receiveAddressFocus, addressFocusNode: _receiveAddressFocus,

View file

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

View file

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

View file

@ -19,34 +19,35 @@ import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
class ExchangeCard extends StatefulWidget { class ExchangeCard extends StatefulWidget {
ExchangeCard( ExchangeCard({
{Key? key, Key? key,
required this.initialCurrency, required this.initialCurrency,
required this.initialAddress, required this.initialAddress,
required this.initialWalletName, required this.initialWalletName,
required this.initialIsAmountEditable, required this.initialIsAmountEditable,
required this.isAmountEstimated, required this.isAmountEstimated,
required this.currencies, required this.currencies,
required this.onCurrencySelected, required this.onCurrencySelected,
this.imageArrow, this.imageArrow,
this.currencyValueValidator, this.currencyValueValidator,
this.addressTextFieldValidator, this.addressTextFieldValidator,
this.title = '', this.title = '',
this.initialIsAddressEditable = true, this.initialIsAddressEditable = true,
this.hasRefundAddress = false, this.hasRefundAddress = false,
this.isMoneroWallet = false, this.isMoneroWallet = false,
this.currencyButtonColor = Colors.transparent, this.currencyButtonColor = Colors.transparent,
this.addressButtonsColor = Colors.transparent, this.addressButtonsColor = Colors.transparent,
this.borderColor = Colors.transparent, this.borderColor = Colors.transparent,
this.hasAllAmount = false, this.hasAllAmount = false,
this.isAllAmountEnabled = false, this.isAllAmountEnabled = false,
this.amountFocusNode, this.amountFocusNode,
this.addressFocusNode, this.addressFocusNode,
this.allAmount, this.allAmount,
this.onPushPasteButton, this.onPushPasteButton,
this.onPushAddressBookButton, this.onPushAddressBookButton,
this.onDispose}) this.onDispose,
: super(key: key); required this.cardInstanceName,
}) : super(key: key);
final List<CryptoCurrency> currencies; final List<CryptoCurrency> currencies;
final Function(CryptoCurrency) onCurrencySelected; final Function(CryptoCurrency) onCurrencySelected;
@ -74,6 +75,7 @@ class ExchangeCard extends StatefulWidget {
final void Function(BuildContext context)? onPushPasteButton; final void Function(BuildContext context)? onPushPasteButton;
final void Function(BuildContext context)? onPushAddressBookButton; final void Function(BuildContext context)? onPushAddressBookButton;
final Function()? onDispose; final Function()? onDispose;
final String cardInstanceName;
@override @override
ExchangeCardState createState() => ExchangeCardState(); ExchangeCardState createState() => ExchangeCardState();
@ -89,11 +91,13 @@ class ExchangeCardState extends State<ExchangeCard> {
_walletName = '', _walletName = '',
_selectedCurrency = CryptoCurrency.btc, _selectedCurrency = CryptoCurrency.btc,
_isAmountEstimated = false, _isAmountEstimated = false,
_isMoneroWallet = false; _isMoneroWallet = false,
_cardInstanceName = '';
final addressController = TextEditingController(); final addressController = TextEditingController();
final amountController = TextEditingController(); final amountController = TextEditingController();
String _cardInstanceName;
String _title; String _title;
String? _min; String? _min;
String? _max; String? _max;
@ -106,6 +110,7 @@ class ExchangeCardState extends State<ExchangeCard> {
@override @override
void initState() { void initState() {
_cardInstanceName = widget.cardInstanceName;
_title = widget.title; _title = widget.title;
_isAmountEditable = widget.initialIsAmountEditable; _isAmountEditable = widget.initialIsAmountEditable;
_isAddressEditable = widget.initialIsAddressEditable; _isAddressEditable = widget.initialIsAddressEditable;
@ -184,6 +189,7 @@ class ExchangeCardState extends State<ExchangeCard> {
mainAxisAlignment: MainAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Text(
key: ValueKey('${_cardInstanceName}_title_key'),
_title, _title,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
@ -193,17 +199,26 @@ class ExchangeCardState extends State<ExchangeCard> {
], ],
), ),
CurrencyAmountTextField( CurrencyAmountTextField(
imageArrow: widget.imageArrow, currencyPickerButtonKey: ValueKey('${_cardInstanceName}_currency_picker_button_key'),
selectedCurrency: _selectedCurrency.toString(), selectedCurrencyTextKey: ValueKey('${_cardInstanceName}_selected_currency_text_key'),
amountFocusNode: widget.amountFocusNode, selectedCurrencyTagTextKey:
amountController: amountController, ValueKey('${_cardInstanceName}_selected_currency_tag_text_key'),
onTapPicker: () => _presentPicker(context), amountTextfieldKey: ValueKey('${_cardInstanceName}_amount_textfield_key'),
isAmountEditable: _isAmountEditable, sendAllButtonKey: ValueKey('${_cardInstanceName}_send_all_button_key'),
isPickerEnable: true, currencyAmountTextFieldWidgetKey:
allAmountButton: widget.hasAllAmount, ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'),
currencyValueValidator: widget.currencyValueValidator, imageArrow: widget.imageArrow,
tag: _selectedCurrency.tag, selectedCurrency: _selectedCurrency.toString(),
allAmountCallback: widget.allAmount), amountFocusNode: widget.amountFocusNode,
amountController: amountController,
onTapPicker: () => _presentPicker(context),
isAmountEditable: _isAmountEditable,
isPickerEnable: true,
allAmountButton: widget.hasAllAmount,
currencyValueValidator: widget.currencyValueValidator,
tag: _selectedCurrency.tag,
allAmountCallback: widget.allAmount,
),
Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor), Divider(height: 1, color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
Padding( Padding(
padding: EdgeInsets.only(top: 5), padding: EdgeInsets.only(top: 5),
@ -212,6 +227,7 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ child: Row(mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[
_min != null _min != null
? Text( ? Text(
key: ValueKey('${_cardInstanceName}_min_limit_text_key'),
S.of(context).min_value(_min ?? '', _selectedCurrency.toString()), S.of(context).min_value(_min ?? '', _selectedCurrency.toString()),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
@ -221,11 +237,15 @@ class ExchangeCardState extends State<ExchangeCard> {
: Offstage(), : Offstage(),
_min != null ? SizedBox(width: 10) : Offstage(), _min != null ? SizedBox(width: 10) : Offstage(),
_max != null _max != null
? Text(S.of(context).max_value(_max ?? '', _selectedCurrency.toString()), ? Text(
key: ValueKey('${_cardInstanceName}_max_limit_text_key'),
S.of(context).max_value(_max ?? '', _selectedCurrency.toString()),
style: TextStyle( style: TextStyle(
fontSize: 10, fontSize: 10,
height: 1.2, height: 1.2,
color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor)) color: Theme.of(context).extension<ExchangePageTheme>()!.hintTextColor,
),
)
: Offstage(), : Offstage(),
])), ])),
), ),
@ -246,6 +266,7 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Padding( child: Padding(
padding: EdgeInsets.only(top: 20), padding: EdgeInsets.only(top: 20),
child: AddressTextField( child: AddressTextField(
addressKey: ValueKey('${_cardInstanceName}_editable_address_textfield_key'),
focusNode: widget.addressFocusNode, focusNode: widget.addressFocusNode,
controller: addressController, controller: addressController,
onURIScanned: (uri) { onURIScanned: (uri) {
@ -286,6 +307,8 @@ class ExchangeCardState extends State<ExchangeCard> {
FocusTraversalOrder( FocusTraversalOrder(
order: NumericFocusOrder(3), order: NumericFocusOrder(3),
child: BaseTextFormField( child: BaseTextFormField(
key: ValueKey(
'${_cardInstanceName}_non_editable_address_textfield_key'),
controller: addressController, controller: addressController,
borderColor: Colors.transparent, borderColor: Colors.transparent,
suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36), suffixIcon: SizedBox(width: _isMoneroWallet ? 80 : 36),
@ -309,6 +332,8 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Semantics( child: Semantics(
label: S.of(context).address_book, label: S.of(context).address_book,
child: InkWell( child: InkWell(
key: ValueKey(
'${_cardInstanceName}_address_book_button_key'),
onTap: () async { onTap: () async {
final contact = final contact =
await Navigator.of(context).pushNamed( await Navigator.of(context).pushNamed(
@ -346,6 +371,8 @@ class ExchangeCardState extends State<ExchangeCard> {
child: Semantics( child: Semantics(
label: S.of(context).copy_address, label: S.of(context).copy_address,
child: InkWell( child: InkWell(
key: ValueKey(
'${_cardInstanceName}_copy_refund_address_button_key'),
onTap: () { onTap: () {
Clipboard.setData( Clipboard.setData(
ClipboardData(text: addressController.text)); ClipboardData(text: addressController.text));
@ -369,6 +396,7 @@ class ExchangeCardState extends State<ExchangeCard> {
showPopUp<void>( showPopUp<void>(
context: context, context: context,
builder: (_) => CurrencyPicker( builder: (_) => CurrencyPicker(
key: ValueKey('${_cardInstanceName}_currency_picker_dialog_button_key'),
selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), selectedAtIndex: widget.currencies.indexOf(_selectedCurrency),
items: widget.currencies, items: widget.currencies,
hintText: S.of(context).search_currency, hintText: S.of(context).search_currency,

View file

@ -83,6 +83,7 @@ class ExchangeConfirmPage extends BasePage {
padding: EdgeInsets.fromLTRB(10, 0, 10, 10), padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
child: Builder( child: Builder(
builder: (context) => PrimaryButton( builder: (context) => PrimaryButton(
key: ValueKey('exchange_confirm_page_copy_to_clipboard_button_key'),
onPressed: () { onPressed: () {
Clipboard.setData(ClipboardData(text: trade.id)); Clipboard.setData(ClipboardData(text: trade.id));
showBar<void>( showBar<void>(
@ -117,6 +118,7 @@ class ExchangeConfirmPage extends BasePage {
], ],
)), )),
PrimaryButton( PrimaryButton(
key: ValueKey('exchange_confirm_page_saved_id_button_key'),
onPressed: () => Navigator.of(context) onPressed: () => Navigator.of(context)
.pushReplacementNamed(Routes.exchangeTrade), .pushReplacementNamed(Routes.exchangeTrade),
text: S.of(context).saved_the_trade_id, text: S.of(context).saved_the_trade_id,

View file

@ -39,7 +39,9 @@ void showInformation(
showPopUp<void>( showPopUp<void>(
context: context, context: context,
builder: (_) => InformationPage(information: information)); builder: (_) => InformationPage(
key: ValueKey('information_page_dialog_key'),
information: information));
} }
class ExchangeTradePage extends BasePage { class ExchangeTradePage extends BasePage {
@ -215,6 +217,7 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
return widget.exchangeTradeViewModel.isSendable && return widget.exchangeTradeViewModel.isSendable &&
!(sendingState is TransactionCommitted) !(sendingState is TransactionCommitted)
? LoadingPrimaryButton( ? LoadingPrimaryButton(
key: ValueKey('exchange_trade_page_confirm_sending_button_key'),
isDisabled: trade.inputAddress == null || isDisabled: trade.inputAddress == null ||
trade.inputAddress!.isEmpty, trade.inputAddress!.isEmpty,
isLoading: sendingState is IsExecutingState, isLoading: sendingState is IsExecutingState,
@ -241,6 +244,8 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
context: context, context: context,
builder: (BuildContext popupContext) { builder: (BuildContext popupContext) {
return AlertWithOneAction( return AlertWithOneAction(
key: ValueKey('exchange_trade_page_send_failure_dialog_key'),
buttonKey: ValueKey('exchange_trade_page_send_failure_dialog_button_key'),
alertTitle: S.of(popupContext).error, alertTitle: S.of(popupContext).error,
alertContent: state.error, alertContent: state.error,
buttonText: S.of(popupContext).ok, buttonText: S.of(popupContext).ok,
@ -255,6 +260,10 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
context: context, context: context,
builder: (BuildContext popupContext) { builder: (BuildContext popupContext) {
return ConfirmSendingAlert( return ConfirmSendingAlert(
key: ValueKey('exchange_trade_page_confirm_sending_dialog_key'),
alertLeftActionButtonKey: ValueKey('exchange_trade_page_confirm_sending_dialog_cancel_button_key'),
alertRightActionButtonKey:
ValueKey('exchange_trade_page_confirm_sending_dialog_send_button_key'),
alertTitle: S.of(popupContext).confirm_sending, alertTitle: S.of(popupContext).confirm_sending,
amount: S.of(popupContext).send_amount, amount: S.of(popupContext).send_amount,
amountValue: widget.exchangeTradeViewModel.sendViewModel amountValue: widget.exchangeTradeViewModel.sendViewModel

View file

@ -10,7 +10,7 @@ import 'package:cake_wallet/src/widgets/alert_background.dart';
import 'package:cake_wallet/themes/extensions/menu_theme.dart'; import 'package:cake_wallet/themes/extensions/menu_theme.dart';
class InformationPage extends StatelessWidget { class InformationPage extends StatelessWidget {
InformationPage({required this.information}); InformationPage({required this.information, super.key});
final String information; final String information;
@ -47,6 +47,7 @@ class InformationPage extends StatelessWidget {
Padding( Padding(
padding: EdgeInsets.fromLTRB(10, 0, 10, 10), padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
child: PrimaryButton( child: PrimaryButton(
key: ValueKey('information_page_got_it_button_key'),
onPressed: () => Navigator.of(context).pop(), onPressed: () => Navigator.of(context).pop(),
text: S.of(context).got_it, text: S.of(context).got_it,
color: Theme.of(context).extension<ExchangePageTheme>()!.buttonBackgroundColor, color: Theme.of(context).extension<ExchangePageTheme>()!.buttonBackgroundColor,

View file

@ -131,6 +131,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
Expanded( Expanded(
child: ScrollableWithBottomSection( child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
scrollableKey: ValueKey('new_wallet_type_scrollable_key'),
content: Column( content: Column(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[ children: <Widget>[
@ -138,6 +139,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
(type) => Padding( (type) => Padding(
padding: EdgeInsets.only(top: 12), padding: EdgeInsets.only(top: 12),
child: SelectButton( child: SelectButton(
key: ValueKey('new_wallet_type_${type.name}_button_key'),
image: Image.asset( image: Image.asset(
walletTypeToCryptoCurrency(type).iconPath ?? '', walletTypeToCryptoCurrency(type).iconPath ?? '',
height: 24, height: 24,
@ -158,6 +160,7 @@ class WalletTypeFormState extends State<WalletTypeForm> {
), ),
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
bottomSection: PrimaryButton( bottomSection: PrimaryButton(
key: ValueKey('new_wallet_type_next_button_key'),
onPressed: () => onTypeSelected(), onPressed: () => onTypeSelected(),
text: S.of(context).seed_language_next, text: S.of(context).seed_language_next,
color: Theme.of(context).primaryColor, color: Theme.of(context).primaryColor,

View file

@ -20,6 +20,7 @@ class SelectButton extends StatelessWidget {
this.deviceConnectionTypes, this.deviceConnectionTypes,
this.borderRadius, this.borderRadius,
this.padding, this.padding,
super.key,
}); });
final Widget? image; final Widget? image;

View file

@ -240,6 +240,7 @@ class PinCodeState<T extends PinCodeWidget> extends State<T> {
return Container( return Container(
margin: EdgeInsets.only(left: marginLeft, right: marginRight), margin: EdgeInsets.only(left: marginLeft, right: marginRight),
child: TextButton( child: TextButton(
key: ValueKey('pin_code_button_${index}_key'),
onPressed: () => _push(index), onPressed: () => _push(index),
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.background, backgroundColor: Theme.of(context).colorScheme.background,

View file

@ -24,8 +24,20 @@ class CurrencyAmountTextField extends StatelessWidget {
this.tagBackgroundColor, this.tagBackgroundColor,
this.currencyValueValidator, this.currencyValueValidator,
this.allAmountCallback, this.allAmountCallback,
}); this.sendAllButtonKey,
this.amountTextfieldKey,
this.currencyPickerButtonKey,
this.selectedCurrencyTextKey,
this.selectedCurrencyTagTextKey,
this.currencyAmountTextFieldWidgetKey,
}) : super(key: currencyAmountTextFieldWidgetKey);
final Key? sendAllButtonKey;
final Key? amountTextfieldKey;
final Key? currencyPickerButtonKey;
final Key? selectedCurrencyTextKey;
final Key? selectedCurrencyTagTextKey;
final Key? currencyAmountTextFieldWidgetKey;
final Widget? imageArrow; final Widget? imageArrow;
final String selectedCurrency; final String selectedCurrency;
final String? tag; final String? tag;
@ -54,6 +66,7 @@ class CurrencyAmountTextField extends StatelessWidget {
? Container( ? Container(
height: 32, height: 32,
child: InkWell( child: InkWell(
key: currencyPickerButtonKey,
onTap: onTapPicker, onTap: onTapPicker,
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
@ -65,6 +78,7 @@ class CurrencyAmountTextField extends StatelessWidget {
Image.asset('assets/images/arrow_bottom_purple_icon.png', Image.asset('assets/images/arrow_bottom_purple_icon.png',
color: textColor, height: 8)), color: textColor, height: 8)),
Text( Text(
key: selectedCurrencyTextKey,
selectedCurrency, selectedCurrency,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -77,6 +91,7 @@ class CurrencyAmountTextField extends StatelessWidget {
), ),
) )
: Text( : Text(
key: selectedCurrencyTextKey,
selectedCurrency, selectedCurrency,
style: TextStyle( style: TextStyle(
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
@ -98,6 +113,7 @@ class CurrencyAmountTextField extends StatelessWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.all(6.0), padding: const EdgeInsets.all(6.0),
child: Text( child: Text(
key: selectedCurrencyTagTextKey,
tag!, tag!,
style: TextStyle( style: TextStyle(
fontSize: 12, fontSize: 12,
@ -132,9 +148,9 @@ class CurrencyAmountTextField extends StatelessWidget {
padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8),
margin: const EdgeInsets.only(right: 3), margin: const EdgeInsets.only(right: 3),
decoration: BoxDecoration( decoration: BoxDecoration(
border: Border.all( border: Border.all(
color: textColor, color: textColor,
), ),
borderRadius: BorderRadius.circular(26), borderRadius: BorderRadius.circular(26),
color: Theme.of(context).primaryColor)) color: Theme.of(context).primaryColor))
: _prefixContent, : _prefixContent,
@ -146,6 +162,7 @@ class CurrencyAmountTextField extends StatelessWidget {
child: FocusTraversalOrder( child: FocusTraversalOrder(
order: NumericFocusOrder(1), order: NumericFocusOrder(1),
child: BaseTextFormField( child: BaseTextFormField(
key: amountTextfieldKey,
focusNode: amountFocusNode, focusNode: amountFocusNode,
controller: amountController, controller: amountController,
enabled: isAmountEditable, enabled: isAmountEditable,
@ -184,6 +201,7 @@ class CurrencyAmountTextField extends StatelessWidget {
borderRadius: const BorderRadius.all(Radius.circular(6)), borderRadius: const BorderRadius.all(Radius.circular(6)),
), ),
child: InkWell( child: InkWell(
key: sendAllButtonKey,
onTap: allAmountCallback, onTap: allAmountCallback,
child: Center( child: Center(
child: Text( child: Text(

View file

@ -59,8 +59,12 @@ class RestoreOptionsPage extends BasePage {
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
OptionTile( OptionTile(
onPressed: () => Navigator.pushNamed(context, Routes.restoreWalletFromSeedKeys, key: ValueKey('restore_options_from_seeds_button_key'),
arguments: isNewInstall), onPressed: () => Navigator.pushNamed(
context,
Routes.restoreWalletFromSeedKeys,
arguments: isNewInstall,
),
image: imageSeedKeys, image: imageSeedKeys,
title: S.of(context).restore_title_from_seed_keys, title: S.of(context).restore_title_from_seed_keys,
description: S.of(context).restore_description_from_seed_keys, description: S.of(context).restore_description_from_seed_keys,
@ -69,6 +73,7 @@ class RestoreOptionsPage extends BasePage {
Padding( Padding(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: OptionTile( child: OptionTile(
key: ValueKey('restore_options_from_backup_button_key'),
onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup), onPressed: () => Navigator.pushNamed(context, Routes.restoreFromBackup),
image: imageBackup, image: imageBackup,
title: S.of(context).restore_title_from_backup, title: S.of(context).restore_title_from_backup,
@ -79,6 +84,7 @@ class RestoreOptionsPage extends BasePage {
Padding( Padding(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: OptionTile( child: OptionTile(
key: ValueKey('restore_options_from_hardware_wallet_button_key'),
onPressed: () => Navigator.pushNamed( onPressed: () => Navigator.pushNamed(
context, Routes.restoreWalletFromHardwareWallet, context, Routes.restoreWalletFromHardwareWallet,
arguments: isNewInstall), arguments: isNewInstall),
@ -90,10 +96,12 @@ class RestoreOptionsPage extends BasePage {
Padding( Padding(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: OptionTile( child: OptionTile(
onPressed: () => _onScanQRCode(context), key: ValueKey('restore_options_from_qr_button_key'),
image: qrCode, onPressed: () => _onScanQRCode(context),
title: S.of(context).scan_qr_code, image: qrCode,
description: S.of(context).cold_or_recover_wallet), title: S.of(context).scan_qr_code,
description: S.of(context).cold_or_recover_wallet,
),
) )
], ],
), ),

View file

@ -112,10 +112,12 @@ class WalletRestoreFromKeysFromState extends State<WalletRestoreFromKeysFrom> {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
children: [ children: [
BaseTextFormField( BaseTextFormField(
key: ValueKey('wallet_restore_from_keys_wallet_name_textfield_key'),
controller: nameTextEditingController, controller: nameTextEditingController,
hintText: S.of(context).wallet_name, hintText: S.of(context).wallet_name,
validator: WalletNameValidator(), validator: WalletNameValidator(),
suffixIcon: IconButton( suffixIcon: IconButton(
key: ValueKey('wallet_restore_from_keys_wallet_name_refresh_button_key'),
onPressed: () async { onPressed: () async {
final rName = await generateName(); final rName = await generateName();
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -175,6 +177,7 @@ class WalletRestoreFromKeysFromState extends State<WalletRestoreFromKeysFrom> {
bool nanoBased = widget.walletRestoreViewModel.type == WalletType.nano || bool nanoBased = widget.walletRestoreViewModel.type == WalletType.nano ||
widget.walletRestoreViewModel.type == WalletType.banano; widget.walletRestoreViewModel.type == WalletType.banano;
return AddressTextField( return AddressTextField(
addressKey: ValueKey('wallet_restore_from_key_private_key_textfield_key'),
controller: privateKeyController, controller: privateKeyController,
placeholder: nanoBased ? S.of(context).seed_hex_form : S.of(context).private_key, placeholder: nanoBased ? S.of(context).seed_hex_form : S.of(context).private_key,
options: [AddressTextFieldOption.paste], options: [AddressTextFieldOption.paste],

View file

@ -151,11 +151,13 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
children: [ children: [
BaseTextFormField( BaseTextFormField(
key: ValueKey('wallet_restore_from_seed_wallet_name_textfield_key'),
controller: nameTextEditingController, controller: nameTextEditingController,
hintText: S hintText: S
.of(context) .of(context)
.wallet_name, .wallet_name,
suffixIcon: IconButton( suffixIcon: IconButton(
key: ValueKey('wallet_restore_from_seed_wallet_name_refresh_button_key'),
onPressed: () async { onPressed: () async {
final rName = await generateName(); final rName = await generateName();
FocusManager.instance.primaryFocus?.unfocus(); FocusManager.instance.primaryFocus?.unfocus();
@ -190,10 +192,13 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
)), )),
Container(height: 20), Container(height: 20),
SeedWidget( SeedWidget(
key: seedWidgetStateKey, key: seedWidgetStateKey,
language: language, language: language,
type: widget.type, type: widget.type,
onSeedChange: onSeedChange), onSeedChange: onSeedChange,
seedTextFieldKey: ValueKey('wallet_restore_from_seed_wallet_seeds_textfield_key'),
pasteButtonKey: ValueKey('wallet_restore_from_seed_wallet_seeds_paste_button_key'),
),
if (widget.type == WalletType.monero || widget.type == WalletType.wownero) if (widget.type == WalletType.monero || widget.type == WalletType.wownero)
GestureDetector( GestureDetector(
onTap: () async { onTap: () async {

View file

@ -213,6 +213,7 @@ class WalletRestorePage extends BasePage {
Observer( Observer(
builder: (context) { builder: (context) {
return LoadingPrimaryButton( return LoadingPrimaryButton(
key: ValueKey('wallet_restore_seed_or_key_restore_button_key'),
onPressed: () async { onPressed: () async {
await _confirmForm(context); await _confirmForm(context);
}, },
@ -230,6 +231,7 @@ class WalletRestorePage extends BasePage {
), ),
const SizedBox(height: 25), const SizedBox(height: 25),
GestureDetector( GestureDetector(
key: ValueKey('wallet_restore_advanced_settings_button_key'),
onTap: () { onTap: () {
Navigator.of(context) Navigator.of(context)
.pushNamed(Routes.advancedPrivacySettings, arguments: { .pushNamed(Routes.advancedPrivacySettings, arguments: {

View file

@ -250,6 +250,7 @@ class SendPage extends BasePage {
return Row( return Row(
children: <Widget>[ children: <Widget>[
AddTemplateButton( AddTemplateButton(
key: ValueKey('send_page_add_template_button_key'),
onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate), onTap: () => Navigator.of(context).pushNamed(Routes.sendTemplate),
currentTemplatesLength: templates.length, currentTemplatesLength: templates.length,
), ),
@ -339,19 +340,22 @@ class SendPage extends BasePage {
children: [ children: [
if (sendViewModel.hasCurrecyChanger) if (sendViewModel.hasCurrecyChanger)
Observer( Observer(
builder: (_) => Padding( builder: (_) => Padding(
padding: EdgeInsets.only(bottom: 12), padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton( child: PrimaryButton(
onPressed: () => presentCurrencyPicker(context), key: ValueKey('send_page_change_asset_button_key'),
text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', onPressed: () => presentCurrencyPicker(context),
color: Colors.transparent, text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})',
textColor: color: Colors.transparent,
Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor, textColor: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
))), ),
),
),
if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) if (sendViewModel.sendTemplateViewModel.hasMultiRecipient)
Padding( Padding(
padding: EdgeInsets.only(bottom: 12), padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton( child: PrimaryButton(
key: ValueKey('send_page_add_receiver_button_key'),
onPressed: () { onPressed: () {
sendViewModel.addOutput(); sendViewModel.addOutput();
Future.delayed(const Duration(milliseconds: 250), () { Future.delayed(const Duration(milliseconds: 250), () {
@ -368,6 +372,7 @@ class SendPage extends BasePage {
Observer( Observer(
builder: (_) { builder: (_) {
return LoadingPrimaryButton( return LoadingPrimaryButton(
key: ValueKey('send_page_send_button_key'),
onPressed: () async { onPressed: () async {
if (sendViewModel.state is IsExecutingState) return; if (sendViewModel.state is IsExecutingState) return;
if (_formKey.currentState != null && !_formKey.currentState!.validate()) { if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
@ -451,6 +456,8 @@ class SendPage extends BasePage {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertWithOneAction( return AlertWithOneAction(
key: ValueKey('send_page_send_failure_dialog_key'),
buttonKey: ValueKey('send_page_send_failure_dialog_button_key'),
alertTitle: S.of(context).error, alertTitle: S.of(context).error,
alertContent: state.error, alertContent: state.error,
buttonText: S.of(context).ok, buttonText: S.of(context).ok,
@ -466,6 +473,7 @@ class SendPage extends BasePage {
context: context, context: context,
builder: (BuildContext _dialogContext) { builder: (BuildContext _dialogContext) {
return ConfirmSendingAlert( return ConfirmSendingAlert(
key: ValueKey('send_page_confirm_sending_dialog_key'),
alertTitle: S.of(_dialogContext).confirm_sending, alertTitle: S.of(_dialogContext).confirm_sending,
amount: S.of(_dialogContext).send_amount, amount: S.of(_dialogContext).send_amount,
amountValue: sendViewModel.pendingTransaction!.amountFormatted, amountValue: sendViewModel.pendingTransaction!.amountFormatted,
@ -480,6 +488,10 @@ class SendPage extends BasePage {
change: sendViewModel.pendingTransaction!.change, change: sendViewModel.pendingTransaction!.change,
rightButtonText: S.of(_dialogContext).send, rightButtonText: S.of(_dialogContext).send,
leftButtonText: S.of(_dialogContext).cancel, leftButtonText: S.of(_dialogContext).cancel,
alertRightActionButtonKey:
ValueKey('send_page_confirm_sending_dialog_send_button_key'),
alertLeftActionButtonKey:
ValueKey('send_page_confirm_sending_dialog_cancel_button_key'),
actionRightButton: () async { actionRightButton: () async {
Navigator.of(_dialogContext).pop(); Navigator.of(_dialogContext).pop();
sendViewModel.commitTransaction(); sendViewModel.commitTransaction();
@ -513,10 +525,15 @@ class SendPage extends BasePage {
if (newContactAddress != null) { if (newContactAddress != null) {
return AlertWithTwoActions( return AlertWithTwoActions(
alertDialogKey: ValueKey('send_page_sent_dialog_key'),
alertTitle: '', alertTitle: '',
alertContent: alertContent, alertContent: alertContent,
rightButtonText: S.of(_dialogContext).add_contact, rightButtonText: S.of(_dialogContext).add_contact,
leftButtonText: S.of(_dialogContext).ignor, leftButtonText: S.of(_dialogContext).ignor,
alertLeftActionButtonKey:
ValueKey('send_page_sent_dialog_ignore_button_key'),
alertRightActionButtonKey: ValueKey(
'send_page_sent_dialog_add_contact_button_key'),
actionRightButton: () { actionRightButton: () {
Navigator.of(_dialogContext).pop(); Navigator.of(_dialogContext).pop();
RequestReviewHandler.requestReview(); RequestReviewHandler.requestReview();

View file

@ -9,30 +9,34 @@ import 'package:cake_wallet/src/widgets/cake_scrollbar.dart';
import 'package:flutter/scheduler.dart'; import 'package:flutter/scheduler.dart';
class ConfirmSendingAlert extends BaseAlertDialog { class ConfirmSendingAlert extends BaseAlertDialog {
ConfirmSendingAlert( ConfirmSendingAlert({
{required this.alertTitle, required this.alertTitle,
this.paymentId, this.paymentId,
this.paymentIdValue, this.paymentIdValue,
this.expirationTime, this.expirationTime,
required this.amount, required this.amount,
required this.amountValue, required this.amountValue,
required this.fiatAmountValue, required this.fiatAmountValue,
required this.fee, required this.fee,
this.feeRate, this.feeRate,
required this.feeValue, required this.feeValue,
required this.feeFiatAmount, required this.feeFiatAmount,
required this.outputs, required this.outputs,
this.change, this.change,
required this.leftButtonText, required this.leftButtonText,
required this.rightButtonText, required this.rightButtonText,
required this.actionLeftButton, required this.actionLeftButton,
required this.actionRightButton, required this.actionRightButton,
this.alertBarrierDismissible = true, this.alertBarrierDismissible = true,
this.alertLeftActionButtonTextColor, this.alertLeftActionButtonTextColor,
this.alertRightActionButtonTextColor, this.alertRightActionButtonTextColor,
this.alertLeftActionButtonColor, this.alertLeftActionButtonColor,
this.alertRightActionButtonColor, this.alertRightActionButtonColor,
this.onDispose}); this.onDispose,
this.alertLeftActionButtonKey,
this.alertRightActionButtonKey,
Key? key,
});
final String alertTitle; final String alertTitle;
final String? paymentId; final String? paymentId;
@ -57,6 +61,8 @@ class ConfirmSendingAlert extends BaseAlertDialog {
final Color? alertLeftActionButtonColor; final Color? alertLeftActionButtonColor;
final Color? alertRightActionButtonColor; final Color? alertRightActionButtonColor;
final Function? onDispose; final Function? onDispose;
final Key? alertRightActionButtonKey;
final Key? alertLeftActionButtonKey;
@override @override
String get titleText => alertTitle; String get titleText => alertTitle;
@ -91,6 +97,12 @@ class ConfirmSendingAlert extends BaseAlertDialog {
@override @override
Color? get rightActionButtonColor => alertRightActionButtonColor; Color? get rightActionButtonColor => alertRightActionButtonColor;
@override
Key? get leftActionButtonKey => alertLeftActionButtonKey;
@override
Key? get rightActionButtonKey => alertLeftActionButtonKey;
@override @override
Widget content(BuildContext context) => ConfirmSendingAlertContent( Widget content(BuildContext context) => ConfirmSendingAlertContent(
paymentId: paymentId, paymentId: paymentId,
@ -288,6 +300,7 @@ class ConfirmSendingAlertContentState extends State<ConfirmSendingAlertContent>
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
children: [ children: [
Text( Text(
key: ValueKey('confirm_sending_dialog_amount_text_value_key'),
amountValue, amountValue,
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,

View file

@ -158,6 +158,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
: sendViewModel.addressValidator; : sendViewModel.addressValidator;
return AddressTextField( return AddressTextField(
addressKey: ValueKey('send_page_address_textfield_key'),
focusNode: addressFocusNode, focusNode: addressFocusNode,
controller: addressController, controller: addressController,
onURIScanned: (uri) { onURIScanned: (uri) {
@ -209,6 +210,11 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white),
validator: sendViewModel.addressValidator)), validator: sendViewModel.addressValidator)),
CurrencyAmountTextField( CurrencyAmountTextField(
currencyPickerButtonKey: ValueKey('send_page_currency_picker_button_key'),
amountTextfieldKey: ValueKey('send_page_amount_textfield_key'),
sendAllButtonKey: ValueKey('send_page_send_all_button_key'),
currencyAmountTextFieldWidgetKey:
ValueKey('send_page_crypto_currency_amount_textfield_widget_key'),
selectedCurrency: sendViewModel.selectedCryptoCurrency.title, selectedCurrency: sendViewModel.selectedCryptoCurrency.title,
amountFocusNode: cryptoAmountFocus, amountFocusNode: cryptoAmountFocus,
amountController: cryptoAmountController, amountController: cryptoAmountController,
@ -216,7 +222,8 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
onTapPicker: () => _presentPicker(context), onTapPicker: () => _presentPicker(context),
isPickerEnable: sendViewModel.hasMultipleTokens, isPickerEnable: sendViewModel.hasMultipleTokens,
tag: sendViewModel.selectedCryptoCurrency.tag, tag: sendViewModel.selectedCryptoCurrency.tag,
allAmountButton: !sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL, allAmountButton:
!sendViewModel.isBatchSending && sendViewModel.shouldDisplaySendALL,
currencyValueValidator: output.sendAll currencyValueValidator: output.sendAll
? sendViewModel.allAmountValidator ? sendViewModel.allAmountValidator
: sendViewModel.amountValidator, : sendViewModel.amountValidator,
@ -257,6 +264,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
), ),
if (!sendViewModel.isFiatDisabled) if (!sendViewModel.isFiatDisabled)
CurrencyAmountTextField( CurrencyAmountTextField(
amountTextfieldKey: ValueKey('send_page_fiat_amount_textfield_key'),
currencyAmountTextFieldWidgetKey:
ValueKey('send_page_fiat_currency_amount_textfield_widget_key'),
selectedCurrency: sendViewModel.fiat.title, selectedCurrency: sendViewModel.fiat.title,
amountFocusNode: fiatAmountFocus, amountFocusNode: fiatAmountFocus,
amountController: fiatAmountController, amountController: fiatAmountController,
@ -269,6 +279,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
Padding( Padding(
padding: EdgeInsets.only(top: 20), padding: EdgeInsets.only(top: 20),
child: BaseTextFormField( child: BaseTextFormField(
key: ValueKey('send_page_note_textfield_key'),
controller: noteController, controller: noteController,
keyboardType: TextInputType.multiline, keyboardType: TextInputType.multiline,
maxLines: null, maxLines: null,
@ -287,6 +298,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
if (sendViewModel.hasFees) if (sendViewModel.hasFees)
Observer( Observer(
builder: (_) => GestureDetector( builder: (_) => GestureDetector(
key: ValueKey('send_page_select_fee_priority_button_key'),
onTap: sendViewModel.hasFeesPriority onTap: sendViewModel.hasFeesPriority
? () => pickTransactionPriority(context) ? () => pickTransactionPriority(context)
: () {}, : () {},
@ -360,6 +372,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
Padding( Padding(
padding: EdgeInsets.only(top: 6), padding: EdgeInsets.only(top: 6),
child: GestureDetector( child: GestureDetector(
key: ValueKey('send_page_unspent_coin_button_key'),
onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList), onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList),
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,
@ -544,11 +557,13 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
showPopUp<void>( showPopUp<void>(
context: context, context: context,
builder: (_) => CurrencyPicker( builder: (_) => CurrencyPicker(
selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), key: ValueKey('send_page_currency_picker_dialog_button_key'),
items: sendViewModel.currencies, selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency),
hintText: S.of(context).search_currency, items: sendViewModel.currencies,
onItemSelected: (Currency cur) => hintText: S.of(context).search_currency,
sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency)), onItemSelected: (Currency cur) =>
sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency),
),
); );
} }

View file

@ -52,6 +52,7 @@ class SetupPinCodePage extends BasePage {
context: context, context: context,
builder: (BuildContext context) { builder: (BuildContext context) {
return AlertWithOneAction( return AlertWithOneAction(
buttonKey: ValueKey('setup_pin_code_success_button_key'),
alertTitle: S.current.setup_pin, alertTitle: S.current.setup_pin,
alertContent: S.of(context).setup_successful, alertContent: S.of(context).setup_successful,
buttonText: S.of(context).ok, buttonText: S.of(context).ok,

View file

@ -133,6 +133,7 @@ class WelcomePage extends BasePage {
Padding( Padding(
padding: EdgeInsets.only(top: 24), padding: EdgeInsets.only(top: 24),
child: PrimaryImageButton( child: PrimaryImageButton(
key: ValueKey('welcome_page_create_new_wallet_button_key'),
onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome), onPressed: () => Navigator.pushNamed(context, Routes.newWalletFromWelcome),
image: newWalletImage, image: newWalletImage,
text: S.of(context).create_new, text: S.of(context).create_new,
@ -146,6 +147,7 @@ class WelcomePage extends BasePage {
Padding( Padding(
padding: EdgeInsets.only(top: 10), padding: EdgeInsets.only(top: 10),
child: PrimaryImageButton( child: PrimaryImageButton(
key: ValueKey('welcome_page_restore_wallet_button_key'),
onPressed: () { onPressed: () {
Navigator.pushNamed(context, Routes.restoreOptions, arguments: true); Navigator.pushNamed(context, Routes.restoreOptions, arguments: true);
}, },

View file

@ -15,28 +15,27 @@ import 'package:permission_handler/permission_handler.dart';
enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses } enum AddressTextFieldOption { paste, qrCode, addressBook, walletAddresses }
class AddressTextField extends StatelessWidget { class AddressTextField extends StatelessWidget {
AddressTextField( AddressTextField({
{required this.controller, required this.controller,
this.isActive = true, this.isActive = true,
this.placeholder, this.placeholder,
this.options = const [ this.options = const [AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook],
AddressTextFieldOption.qrCode, this.onURIScanned,
AddressTextFieldOption.addressBook this.focusNode,
], this.isBorderExist = true,
this.onURIScanned, this.buttonColor,
this.focusNode, this.borderColor,
this.isBorderExist = true, this.iconColor,
this.buttonColor, this.textStyle,
this.borderColor, this.hintStyle,
this.iconColor, this.validator,
this.textStyle, this.onPushPasteButton,
this.hintStyle, this.onPushAddressBookButton,
this.validator, this.onPushAddressPickerButton,
this.onPushPasteButton, this.onSelectedContact,
this.onPushAddressBookButton, this.selectedCurrency,
this.onPushAddressPickerButton, this.addressKey,
this.onSelectedContact, });
this.selectedCurrency});
static const prefixIconWidth = 34.0; static const prefixIconWidth = 34.0;
static const prefixIconHeight = 34.0; static const prefixIconHeight = 34.0;
@ -60,12 +59,14 @@ class AddressTextField extends StatelessWidget {
final Function(BuildContext context)? onPushAddressPickerButton; final Function(BuildContext context)? onPushAddressPickerButton;
final Function(ContactBase contact)? onSelectedContact; final Function(ContactBase contact)? onSelectedContact;
final CryptoCurrency? selectedCurrency; final CryptoCurrency? selectedCurrency;
final Key? addressKey;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Stack( return Stack(
children: <Widget>[ children: <Widget>[
TextFormField( TextFormField(
key: addressKey,
enableIMEPersonalizedLearning: false, enableIMEPersonalizedLearning: false,
keyboardType: TextInputType.visiblePassword, keyboardType: TextInputType.visiblePassword,
onFieldSubmitted: (_) => FocusScope.of(context).unfocus(), onFieldSubmitted: (_) => FocusScope.of(context).unfocus(),

View file

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

View file

@ -9,7 +9,9 @@ class AlertWithOneAction extends BaseAlertDialog {
required this.buttonAction, required this.buttonAction,
this.alertBarrierDismissible = true, this.alertBarrierDismissible = true,
this.headerTitleText, this.headerTitleText,
this.headerImageProfileUrl this.headerImageProfileUrl,
this.buttonKey,
Key? key,
}); });
final String alertTitle; final String alertTitle;
@ -19,6 +21,7 @@ class AlertWithOneAction extends BaseAlertDialog {
final bool alertBarrierDismissible; final bool alertBarrierDismissible;
final String? headerTitleText; final String? headerTitleText;
final String? headerImageProfileUrl; final String? headerImageProfileUrl;
final Key? buttonKey;
@override @override
String get titleText => alertTitle; String get titleText => alertTitle;
@ -45,6 +48,7 @@ class AlertWithOneAction extends BaseAlertDialog {
child: ButtonTheme( child: ButtonTheme(
minWidth: double.infinity, minWidth: double.infinity,
child: TextButton( child: TextButton(
key: buttonKey,
onPressed: buttonAction, onPressed: buttonAction,
// FIX-ME: Style // FIX-ME: Style
//highlightColor: Colors.transparent, //highlightColor: Colors.transparent,

View file

@ -14,6 +14,9 @@ class AlertWithTwoActions extends BaseAlertDialog {
this.isDividerExist = false, this.isDividerExist = false,
// this.leftActionColor, // this.leftActionColor,
// this.rightActionColor, // this.rightActionColor,
this.alertRightActionButtonKey,
this.alertLeftActionButtonKey,
this.alertDialogKey,
}); });
final String alertTitle; final String alertTitle;
@ -26,6 +29,9 @@ class AlertWithTwoActions extends BaseAlertDialog {
// final Color leftActionColor; // final Color leftActionColor;
// final Color rightActionColor; // final Color rightActionColor;
final bool isDividerExist; final bool isDividerExist;
final Key? alertRightActionButtonKey;
final Key? alertLeftActionButtonKey;
final Key? alertDialogKey;
@override @override
String get titleText => alertTitle; String get titleText => alertTitle;
@ -47,4 +53,13 @@ class AlertWithTwoActions extends BaseAlertDialog {
// Color get rightButtonColor => rightActionColor; // Color get rightButtonColor => rightActionColor;
@override @override
bool get isDividerExists => isDividerExist; bool get isDividerExists => isDividerExist;
@override
Key? get dialogKey => alertDialogKey;
@override
Key? get leftActionButtonKey => alertLeftActionButtonKey;
@override
Key? get rightActionButtonKey => alertRightActionButtonKey;
} }

View file

@ -33,6 +33,12 @@ class BaseAlertDialog extends StatelessWidget {
String? get headerImageUrl => null; String? get headerImageUrl => null;
Key? leftActionButtonKey;
Key? rightActionButtonKey;
Key? dialogKey;
Widget title(BuildContext context) { Widget title(BuildContext context) {
return Text( return Text(
titleText, titleText,
@ -87,6 +93,7 @@ class BaseAlertDialog extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
child: TextButton( child: TextButton(
key: leftActionButtonKey,
onPressed: actionLeft, onPressed: actionLeft,
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: backgroundColor:
@ -109,6 +116,7 @@ class BaseAlertDialog extends StatelessWidget {
const VerticalSectionDivider(), const VerticalSectionDivider(),
Expanded( Expanded(
child: TextButton( child: TextButton(
key: rightActionButtonKey,
onPressed: actionRight, onPressed: actionRight,
style: TextButton.styleFrom( style: TextButton.styleFrom(
backgroundColor: backgroundColor:
@ -152,6 +160,7 @@ class BaseAlertDialog extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return GestureDetector( return GestureDetector(
key: key,
onTap: () => barrierDismissible ? Navigator.of(context).pop() : null, onTap: () => barrierDismissible ? Navigator.of(context).pop() : null,
child: Container( child: Container(
color: Colors.transparent, color: Colors.transparent,

View file

@ -30,7 +30,8 @@ class BaseTextFormField extends StatelessWidget {
this.focusNode, this.focusNode,
this.initialValue, this.initialValue,
this.onSubmit, this.onSubmit,
this.borderWidth = 1.0}); this.borderWidth = 1.0,
super.key});
final TextEditingController? controller; final TextEditingController? controller;
final TextInputType? keyboardType; final TextInputType? keyboardType;

View file

@ -6,7 +6,8 @@ class OptionTile extends StatelessWidget {
{required this.onPressed, {required this.onPressed,
required this.image, required this.image,
required this.title, required this.title,
required this.description}); required this.description,
super.key});
final VoidCallback onPressed; final VoidCallback onPressed;
final Image image; final Image image;

View file

@ -4,6 +4,7 @@ import 'dart:math';
import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:cw_core/currency.dart'; import 'package:cw_core/currency.dart';
import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart'; import 'package:cake_wallet/src/widgets/picker_wrapper_widget.dart';
@ -11,6 +12,7 @@ import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart'; import 'package:cake_wallet/themes/extensions/cake_scrollbar_theme.dart';
import 'package:cake_wallet/themes/extensions/picker_theme.dart'; import 'package:cake_wallet/themes/extensions/picker_theme.dart';
//TODO(David): PickerWidget is intertwined and confusing as is, find a way to optimize?
class Picker<Item> extends StatefulWidget { class Picker<Item> extends StatefulWidget {
Picker({ Picker({
required this.selectedAtIndex, required this.selectedAtIndex,
@ -153,6 +155,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
Container( Container(
padding: EdgeInsets.symmetric(horizontal: padding), padding: EdgeInsets.symmetric(horizontal: padding),
child: Text( child: Text(
key: ValueKey('picker_title_text_key'),
widget.title!, widget.title!,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
@ -189,7 +192,10 @@ class _PickerState<Item> extends State<Picker<Item>> {
Padding( Padding(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
child: SearchBarWidget( child: SearchBarWidget(
searchController: searchController, hintText: widget.hintText), key: ValueKey('picker_search_bar_key'),
searchController: searchController,
hintText: widget.hintText,
),
), ),
Divider( Divider(
color: Theme.of(context).extension<PickerTheme>()!.dividerColor, color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
@ -203,6 +209,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
children: <Widget>[ children: <Widget>[
filteredItems.length > 3 filteredItems.length > 3
? Scrollbar( ? Scrollbar(
key: ValueKey('picker_scrollbar_key'),
controller: controller, controller: controller,
child: itemsList(), child: itemsList(),
) )
@ -213,6 +220,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
left: padding, left: padding,
right: padding, right: padding,
child: Text( child: Text(
key: ValueKey('picker_descriptinon_text_key'),
widget.description!, widget.description!,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
@ -242,6 +250,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
if (widget.isWrapped) { if (widget.isWrapped) {
return PickerWrapperWidget( return PickerWrapperWidget(
key: ValueKey('picker_wrapper_widget_key'),
hasTitle: widget.title?.isNotEmpty ?? false, hasTitle: widget.title?.isNotEmpty ?? false,
children: [content], children: [content],
); );
@ -260,6 +269,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
color: Theme.of(context).extension<PickerTheme>()!.dividerColor, color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
child: widget.isGridView child: widget.isGridView
? GridView.builder( ? GridView.builder(
key: ValueKey('picker_items_grid_view_key'),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
controller: controller, controller: controller,
shrinkWrap: true, shrinkWrap: true,
@ -275,6 +285,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
: buildItem(index), : buildItem(index),
) )
: ListView.separated( : ListView.separated(
key: ValueKey('picker_items_list_view_key'),
padding: EdgeInsets.zero, padding: EdgeInsets.zero,
controller: controller, controller: controller,
shrinkWrap: true, shrinkWrap: true,
@ -293,10 +304,25 @@ class _PickerState<Item> extends State<Picker<Item>> {
); );
} }
String _getItemName(Item item) {
String itemName;
if (item is Currency) {
itemName = item.name;
} else if (item is TransactionPriority) {
itemName = item.title;
} else {
itemName = '';
}
return itemName;
}
Widget buildItem(int index) { Widget buildItem(int index) {
final item = widget.headerEnabled ? filteredItems[index] : items[index]; final item = widget.headerEnabled ? filteredItems[index] : items[index];
final tag = item is Currency ? item.tag : null; final tag = item is Currency ? item.tag : null;
final itemName = _getItemName(item);
final icon = _getItemIcon(item); final icon = _getItemIcon(item);
final image = images.isNotEmpty ? filteredImages[index] : icon; final image = images.isNotEmpty ? filteredImages[index] : icon;
@ -316,6 +342,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
key: ValueKey('picker_items_index_${itemName}_text_key'),
widget.displayItem?.call(item) ?? item.toString(), widget.displayItem?.call(item) ?? item.toString(),
softWrap: true, softWrap: true,
style: TextStyle( style: TextStyle(
@ -335,6 +362,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
height: 18.0, height: 18.0,
child: Center( child: Center(
child: Text( child: Text(
key: ValueKey('picker_items_index_${index}_tag_key'),
tag, tag,
style: TextStyle( style: TextStyle(
fontSize: 7.0, fontSize: 7.0,
@ -358,6 +386,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
); );
return GestureDetector( return GestureDetector(
key: ValueKey('picker_items_index_${itemName}_button_key'),
onTap: () { onTap: () {
if (widget.closeOnItemSelected) Navigator.of(context).pop(); if (widget.closeOnItemSelected) Navigator.of(context).pop();
onItemSelected(item!); onItemSelected(item!);
@ -383,6 +412,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
final item = items[index]; final item = items[index];
final tag = item is Currency ? item.tag : null; final tag = item is Currency ? item.tag : null;
final itemName = _getItemName(item);
final icon = _getItemIcon(item); final icon = _getItemIcon(item);
final image = images.isNotEmpty ? images[index] : icon; final image = images.isNotEmpty ? images[index] : icon;
@ -390,6 +420,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex; final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex;
final itemContent = Row( final itemContent = Row(
key: ValueKey('picker_selected_item_row_key'),
mainAxisSize: MainAxisSize.max, mainAxisSize: MainAxisSize.max,
mainAxisAlignment: widget.mainAxisAlignment, mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
@ -402,6 +433,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
children: [ children: [
Flexible( Flexible(
child: Text( child: Text(
key: ValueKey('picker_items_index_${itemName}_selected_item_text_key'),
widget.displayItem?.call(item) ?? item.toString(), widget.displayItem?.call(item) ?? item.toString(),
softWrap: true, softWrap: true,
style: TextStyle( style: TextStyle(
@ -445,6 +477,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
); );
return GestureDetector( return GestureDetector(
key: ValueKey('picker_items_index_${itemName}_selected_item_button_key'),
onTap: () { onTap: () {
if (widget.closeOnItemSelected) Navigator.of(context).pop(); if (widget.closeOnItemSelected) Navigator.of(context).pop();
}, },

View file

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

View file

@ -4,15 +4,17 @@ import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
class PrimaryButton extends StatelessWidget { class PrimaryButton extends StatelessWidget {
const PrimaryButton( const PrimaryButton({
{required this.text, required this.text,
required this.color, required this.color,
required this.textColor, required this.textColor,
this.onPressed, this.onPressed,
this.isDisabled = false, this.isDisabled = false,
this.isDottedBorder = false, this.isDottedBorder = false,
this.borderColor = Colors.black, this.borderColor = Colors.black,
this.onDisabledPressed}); this.onDisabledPressed,
super.key,
});
final VoidCallback? onPressed; final VoidCallback? onPressed;
final VoidCallback? onDisabledPressed; final VoidCallback? onDisabledPressed;
@ -31,23 +33,23 @@ class PrimaryButton extends StatelessWidget {
width: double.infinity, width: double.infinity,
height: 52.0, height: 52.0,
child: TextButton( child: TextButton(
onPressed: isDisabled onPressed:
? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed, isDisabled ? (onDisabledPressed != null ? onDisabledPressed : null) : onPressed,
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), style: ButtonStyle(
backgroundColor:
MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color),
shape: MaterialStateProperty.all<RoundedRectangleBorder>( shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26.0), borderRadius: BorderRadius.circular(26.0),
), ),
), ),
overlayColor: MaterialStateProperty.all(Colors.transparent)), overlayColor: MaterialStateProperty.all(Colors.transparent)),
child: Text(text, child: Text(text,
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
fontSize: 15.0, fontSize: 15.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: isDisabled color: isDisabled ? textColor.withOpacity(0.5) : textColor)),
? textColor.withOpacity(0.5)
: textColor)),
)), )),
); );
@ -64,13 +66,15 @@ class PrimaryButton extends StatelessWidget {
} }
class LoadingPrimaryButton extends StatelessWidget { class LoadingPrimaryButton extends StatelessWidget {
const LoadingPrimaryButton( const LoadingPrimaryButton({
{required this.onPressed, required this.onPressed,
required this.text, required this.text,
required this.color, required this.color,
required this.textColor, required this.textColor,
this.isDisabled = false, this.isDisabled = false,
this.isLoading = false}); this.isLoading = false,
super.key,
});
final VoidCallback onPressed; final VoidCallback onPressed;
final Color color; final Color color;
@ -88,41 +92,38 @@ class LoadingPrimaryButton extends StatelessWidget {
height: 52.0, height: 52.0,
child: TextButton( child: TextButton(
onPressed: (isLoading || isDisabled) ? null : onPressed, onPressed: (isLoading || isDisabled) ? null : onPressed,
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color), style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>( backgroundColor:
RoundedRectangleBorder( MaterialStateProperty.all(isDisabled ? color.withOpacity(0.5) : color),
borderRadius: BorderRadius.circular(26.0), shape: MaterialStateProperty.all<RoundedRectangleBorder>(
), RoundedRectangleBorder(
)), borderRadius: BorderRadius.circular(26.0),
),
)),
child: isLoading child: isLoading
? CupertinoActivityIndicator(animating: true) ? CupertinoActivityIndicator(animating: true)
: Text(text, : Text(text,
style: TextStyle( style: TextStyle(
fontSize: 15.0, fontSize: 15.0,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
color: isDisabled color: isDisabled ? textColor.withOpacity(0.5) : textColor)),
? textColor.withOpacity(0.5)
: textColor
)),
)), )),
); );
} }
} }
class PrimaryIconButton extends StatelessWidget { class PrimaryIconButton extends StatelessWidget {
const PrimaryIconButton({ const PrimaryIconButton(
required this.onPressed, {required this.onPressed,
required this.iconData, required this.iconData,
required this.text, required this.text,
required this.color, required this.color,
required this.borderColor, required this.borderColor,
required this.iconColor, required this.iconColor,
required this.iconBackgroundColor, required this.iconBackgroundColor,
required this.textColor, required this.textColor,
this.mainAxisAlignment = MainAxisAlignment.start, this.mainAxisAlignment = MainAxisAlignment.start,
this.radius = 26 this.radius = 26, super.key});
});
final VoidCallback onPressed; final VoidCallback onPressed;
final IconData iconData; final IconData iconData;
@ -144,7 +145,8 @@ class PrimaryIconButton extends StatelessWidget {
height: 52.0, height: 52.0,
child: TextButton( child: TextButton(
onPressed: onPressed, onPressed: onPressed,
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(color),
shape: MaterialStateProperty.all<RoundedRectangleBorder>( shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder( RoundedRectangleBorder(
borderRadius: BorderRadius.circular(radius), borderRadius: BorderRadius.circular(radius),
@ -158,21 +160,15 @@ class PrimaryIconButton extends StatelessWidget {
Container( Container(
width: 26.0, width: 26.0,
height: 52.0, height: 52.0,
decoration: BoxDecoration( decoration: BoxDecoration(shape: BoxShape.circle, color: iconBackgroundColor),
shape: BoxShape.circle, color: iconBackgroundColor), child: Center(child: Icon(iconData, color: iconColor, size: 22.0)),
child: Center(
child: Icon(iconData, color: iconColor, size: 22.0)
),
), ),
], ],
), ),
Container( Container(
height: 52.0, height: 52.0,
child: Center( child: Center(
child: Text(text, child: Text(text, style: TextStyle(fontSize: 16.0, color: textColor)),
style: TextStyle(
fontSize: 16.0,
color: textColor)),
), ),
) )
], ],
@ -189,7 +185,7 @@ class PrimaryImageButton extends StatelessWidget {
required this.text, required this.text,
required this.color, required this.color,
required this.textColor, required this.textColor,
this.borderColor = Colors.transparent}); this.borderColor = Colors.transparent, super.key});
final VoidCallback onPressed; final VoidCallback onPressed;
final Image image; final Image image;
@ -206,31 +202,27 @@ class PrimaryImageButton extends StatelessWidget {
width: double.infinity, width: double.infinity,
height: 52.0, height: 52.0,
child: TextButton( child: TextButton(
onPressed: onPressed, onPressed: onPressed,
style: ButtonStyle(backgroundColor: MaterialStateProperty.all(color), style: ButtonStyle(
shape: MaterialStateProperty.all<RoundedRectangleBorder>( backgroundColor: MaterialStateProperty.all(color),
RoundedRectangleBorder( shape: MaterialStateProperty.all<RoundedRectangleBorder>(
borderRadius: BorderRadius.circular(26.0), RoundedRectangleBorder(
), borderRadius: BorderRadius.circular(26.0),
)),
child:Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
image,
SizedBox(width: 15),
Text(
text,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.w600,
color: textColor
), ),
) )),
], child: Center(
), child: Row(
) mainAxisSize: MainAxisSize.min,
)), children: <Widget>[
image,
SizedBox(width: 15),
Text(
text,
style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: textColor),
)
],
),
))),
); );
} }
} }

View file

@ -9,6 +9,7 @@ class ScrollableWithBottomSection extends StatefulWidget {
this.contentPadding, this.contentPadding,
this.bottomSectionPadding, this.bottomSectionPadding,
this.topSectionPadding, this.topSectionPadding,
this.scrollableKey,
}); });
final Widget content; final Widget content;
@ -17,6 +18,7 @@ class ScrollableWithBottomSection extends StatefulWidget {
final EdgeInsets? contentPadding; final EdgeInsets? contentPadding;
final EdgeInsets? bottomSectionPadding; final EdgeInsets? bottomSectionPadding;
final EdgeInsets? topSectionPadding; final EdgeInsets? topSectionPadding;
final Key? scrollableKey;
@override @override
ScrollableWithBottomSectionState createState() => ScrollableWithBottomSectionState(); ScrollableWithBottomSectionState createState() => ScrollableWithBottomSectionState();
@ -35,6 +37,7 @@ class ScrollableWithBottomSectionState extends State<ScrollableWithBottomSection
), ),
Expanded( Expanded(
child: SingleChildScrollView( child: SingleChildScrollView(
key: widget.scrollableKey,
child: Padding( child: Padding(
padding: widget.contentPadding ?? EdgeInsets.only(left: 20, right: 20), padding: widget.contentPadding ?? EdgeInsets.only(left: 20, right: 20),
child: widget.content, child: widget.content,

View file

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

View file

@ -9,11 +9,15 @@ import 'package:flutter/services.dart';
class SeedWidget extends StatefulWidget { class SeedWidget extends StatefulWidget {
SeedWidget({ SeedWidget({
Key? key,
required this.language, required this.language,
required this.type, required this.type,
this.onSeedChange}) : super(key: key); this.onSeedChange,
this.pasteButtonKey,
this.seedTextFieldKey,
super.key,
});
final Key? seedTextFieldKey;
final Key? pasteButtonKey;
final String language; final String language;
final WalletType type; final WalletType type;
final void Function(String)? onSeedChange; final void Function(String)? onSeedChange;
@ -78,11 +82,11 @@ class SeedWidgetState extends State<SeedWidget> {
top: 10, top: 10,
left: 0, left: 0,
child: Text(S.of(context).enter_seed_phrase, child: Text(S.of(context).enter_seed_phrase,
style: TextStyle( style: TextStyle(fontSize: 16.0, color: Theme.of(context).hintColor))),
fontSize: 16.0, color: Theme.of(context).hintColor))),
Padding( Padding(
padding: EdgeInsets.only(right: 40, top: 10), padding: EdgeInsets.only(right: 40, top: 10),
child: ValidatableAnnotatedEditableText( child: ValidatableAnnotatedEditableText(
key: widget.seedTextFieldKey,
cursorColor: Colors.blue, cursorColor: Colors.blue,
backgroundCursorColor: Colors.blue, backgroundCursorColor: Colors.blue,
validStyle: TextStyle( validStyle: TextStyle(
@ -112,15 +116,17 @@ class SeedWidgetState extends State<SeedWidget> {
width: 32, width: 32,
height: 32, height: 32,
child: InkWell( child: InkWell(
key: widget.pasteButtonKey,
onTap: () async => _pasteText(), onTap: () async => _pasteText(),
child: Container( child: Container(
padding: EdgeInsets.all(8), padding: EdgeInsets.all(8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Theme.of(context).hintColor, color: Theme.of(context).hintColor,
borderRadius: borderRadius: BorderRadius.all(Radius.circular(6))),
BorderRadius.all(Radius.circular(6))),
child: Image.asset('assets/images/paste_ios.png', child: Image.asset('assets/images/paste_ios.png',
color: Theme.of(context).extension<SendPageTheme>()!.textFieldButtonIconColor)), color: Theme.of(context)
.extension<SendPageTheme>()!
.textFieldButtonIconColor)),
))) )))
]), ]),
Container( Container(

View file

@ -107,11 +107,14 @@ dependencies:
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
sdk: flutter sdk: flutter
integration_test:
sdk: flutter
mocktail: ^1.0.4
build_runner: ^2.3.3 build_runner: ^2.3.3
logging: ^1.2.0 logging: ^1.2.0
mobx_codegen: ^2.1.1 mobx_codegen: ^2.1.1
build_resolvers: ^2.0.9 build_resolvers: ^2.0.9
hive_generator: ^1.1.3 hive_generator: ^2.0.1
# flutter_launcher_icons: ^0.11.0 # flutter_launcher_icons: ^0.11.0
# check flutter_launcher_icons for usage # check flutter_launcher_icons for usage
pedantic: ^1.8.0 pedantic: ^1.8.0

View file

@ -0,0 +1,33 @@
import 'dart:convert';
import 'package:integration_test/integration_test_driver.dart';
import 'package:path/path.dart' as path;
import 'package:flutter_driver/flutter_driver.dart';
Future<void> main() async {
integrationDriver(
responseDataCallback: (Map<String, dynamic>? data) async {
await fs.directory(_destinationDirectory).create(recursive: true);
final file = fs.file(
path.join(
_destinationDirectory,
'$_testOutputFilename.json',
),
);
final resultString = _encodeJson(data);
await file.writeAsString(resultString);
},
writeResponseOnFailure: true,
);
}
String _encodeJson(Map<String, dynamic>? jsonObject) {
return _prettyEncoder.convert(jsonObject);
}
const _prettyEncoder = JsonEncoder.withIndent(' ');
const _testOutputFilename = 'integration_response_data';
const _destinationDirectory = 'integration_test';

View file

@ -39,6 +39,7 @@ class SecretKey {
SecretKey('moralisApiKey', () => ''), SecretKey('moralisApiKey', () => ''),
SecretKey('ankrApiKey', () => ''), SecretKey('ankrApiKey', () => ''),
SecretKey('quantexExchangeMarkup', () => ''), SecretKey('quantexExchangeMarkup', () => ''),
SecretKey('seeds', () => ''),
SecretKey('testCakePayApiKey', () => ''), SecretKey('testCakePayApiKey', () => ''),
SecretKey('cakePayApiKey', () => ''), SecretKey('cakePayApiKey', () => ''),
SecretKey('CSRFToken', () => ''), SecretKey('CSRFToken', () => ''),