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,8 +19,8 @@ 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,
@ -45,8 +45,9 @@ class ExchangeCard extends StatefulWidget {
this.allAmount, this.allAmount,
this.onPushPasteButton, this.onPushPasteButton,
this.onPushAddressBookButton, this.onPushAddressBookButton,
this.onDispose}) this.onDispose,
: super(key: key); required this.cardInstanceName,
}) : super(key: key);
final List<CryptoCurrency> currencies; final List<CryptoCurrency> currencies;
final Function(CryptoCurrency) onCurrencySelected; final Function(CryptoCurrency) onCurrencySelected;
@ -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,6 +199,14 @@ class ExchangeCardState extends State<ExchangeCard> {
], ],
), ),
CurrencyAmountTextField( CurrencyAmountTextField(
currencyPickerButtonKey: ValueKey('${_cardInstanceName}_currency_picker_button_key'),
selectedCurrencyTextKey: ValueKey('${_cardInstanceName}_selected_currency_text_key'),
selectedCurrencyTagTextKey:
ValueKey('${_cardInstanceName}_selected_currency_tag_text_key'),
amountTextfieldKey: ValueKey('${_cardInstanceName}_amount_textfield_key'),
sendAllButtonKey: ValueKey('${_cardInstanceName}_send_all_button_key'),
currencyAmountTextFieldWidgetKey:
ValueKey('${_cardInstanceName}_currency_amount_textfield_widget_key'),
imageArrow: widget.imageArrow, imageArrow: widget.imageArrow,
selectedCurrency: _selectedCurrency.toString(), selectedCurrency: _selectedCurrency.toString(),
amountFocusNode: widget.amountFocusNode, amountFocusNode: widget.amountFocusNode,
@ -203,7 +217,8 @@ class ExchangeCardState extends State<ExchangeCard> {
allAmountButton: widget.hasAllAmount, allAmountButton: widget.hasAllAmount,
currencyValueValidator: widget.currencyValueValidator, currencyValueValidator: widget.currencyValueValidator,
tag: _selectedCurrency.tag, tag: _selectedCurrency.tag,
allAmountCallback: widget.allAmount), 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,
@ -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(
key: ValueKey('restore_options_from_qr_button_key'),
onPressed: () => _onScanQRCode(context), onPressed: () => _onScanQRCode(context),
image: qrCode, image: qrCode,
title: S.of(context).scan_qr_code, title: S.of(context).scan_qr_code,
description: S.of(context).cold_or_recover_wallet), 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();
@ -193,7 +195,10 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
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,
), ),
@ -342,16 +343,19 @@ class SendPage extends BasePage {
builder: (_) => Padding( builder: (_) => Padding(
padding: EdgeInsets.only(bottom: 12), padding: EdgeInsets.only(bottom: 12),
child: PrimaryButton( child: PrimaryButton(
key: ValueKey('send_page_change_asset_button_key'),
onPressed: () => presentCurrencyPicker(context), onPressed: () => presentCurrencyPicker(context),
text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})', text: 'Change your asset (${sendViewModel.selectedCryptoCurrency})',
color: Colors.transparent, color: Colors.transparent,
textColor: textColor: Theme.of(context).extension<SeedWidgetTheme>()!.hintTextColor,
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,8 +9,8 @@ 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,
@ -32,7 +32,11 @@ class ConfirmSendingAlert extends BaseAlertDialog {
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(
key: ValueKey('send_page_currency_picker_dialog_button_key'),
selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency),
items: sendViewModel.currencies, items: sendViewModel.currencies,
hintText: S.of(context).search_currency, hintText: S.of(context).search_currency,
onItemSelected: (Currency cur) => onItemSelected: (Currency cur) =>
sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency)), 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,14 +15,11 @@ 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,
AddressTextFieldOption.addressBook
],
this.onURIScanned, this.onURIScanned,
this.focusNode, this.focusNode,
this.isBorderExist = true, this.isBorderExist = true,
@ -36,7 +33,9 @@ class AddressTextField extends StatelessWidget {
this.onPushAddressBookButton, this.onPushAddressBookButton,
this.onPushAddressPickerButton, this.onPushAddressPickerButton,
this.onSelectedContact, this.onSelectedContact,
this.selectedCurrency}); this.selectedCurrency,
this.addressKey,
});
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,9 +33,11 @@ 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),
@ -45,9 +49,7 @@ class PrimaryButton extends StatelessWidget {
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,31 +92,29 @@ 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(
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),
), ),
)), )),
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,
@ -121,8 +123,7 @@ class PrimaryIconButton extends StatelessWidget {
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;
@ -207,13 +203,14 @@ class PrimaryImageButton 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(26.0), borderRadius: BorderRadius.circular(26.0),
), ),
)), )),
child:Center( child: Center(
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
@ -221,16 +218,11 @@ class PrimaryImageButton extends StatelessWidget {
SizedBox(width: 15), SizedBox(width: 15),
Text( Text(
text, text,
style: TextStyle( style: TextStyle(fontSize: 15, fontWeight: FontWeight.w600, color: textColor),
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', () => ''),