Merge upstream Cake Wallet v6.5.0

Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
+10 -26
View File
@@ -1,36 +1,20 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/auth/auth_page.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_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';
import "pin_code_widget_robot.dart";
class AuthPageRobot extends PinCodeWidgetRobot {
AuthPageRobot(this.tester)
: commonTestCases = CommonTestCases(tester),
super(tester);
AuthPageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
@override
Future<void> isDisplayed() async {
await isSpecificPage<AuthPage>();
}
bool onAuthPage() {
final hasPinButtons = find.byKey(ValueKey('pin_code_button_3_key'));
final hasPinButtons = find.byKey(const ValueKey("pin_code_button_3_key"));
final hasPin = hasPinButtons.tryEvaluate();
return hasPin;
}
bool onAuthPageDesktop() {
final hasWalletPasswordInput = find.byKey(ValueKey('enter_wallet_password'));
return hasWalletPasswordInput.tryEvaluate();
}
Future<void> isAuthPage() async {
await commonTestCases.isSpecificPage<AuthPage>();
await commonTestCases.takeScreenshots('auth_page');
}
void hasTitle() {
commonTestCases.hasText(S.current.setup_pin);
}
}
@@ -0,0 +1,40 @@
import "package:hash_wallet/entities/fiat_api_mode.dart";
import "package:hash_wallet/src/screens/settings/connection_sync_page.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class ConnectionSyncPageRobot extends BaseRobot {
ConnectionSyncPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await isSpecificPage<ConnectionSyncPage>();
}
Future<void> setFiatApiMode(FiatApiMode mode) async {
await tapByKey("fiat_api");
final unselected = find.byKey(ValueKey("picker_items_index_${mode.title}_button_key"));
final selected =
find.byKey(ValueKey("picker_items_index_${mode.title}_selected_item_button_key"));
await pumpUntil(() => tester.any(unselected) || tester.any(selected));
await tester.tap(tester.any(unselected) ? unselected : selected);
await settle();
}
void hasFiatApiMode(FiatApiMode mode) {
expect(
find.descendant(
of: find.byKey(const ValueKey("fiat_api")),
matching: find.text(mode.title),
),
findsOneWidget,
reason: "The fiat api row does not show ${mode.title}",
);
}
}
@@ -0,0 +1,66 @@
import "package:hash_wallet/src/screens/contact/contact_list_page.dart";
import "package:hash_wallet/src/screens/contact/contact_page.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class ContactRobot extends BaseRobot {
ContactRobot(super.tester);
@override
Future<void> isDisplayed() async {
await pumpUntilFound(find.byType(ContactListPage));
}
Future<void> openAddContact() async {
await tapByKey("contact_list_page_add_contact_button_key");
await pumpUntilFound(find.byType(ContactPage));
}
Future<void> enterName(String name) async {
await enterTextByKey("contact_page_name_textfield_key", name);
}
Future<void> chooseCurrency(String currencyName) async {
await tapByKey("contact_page_currency_picker_button_key");
await tapByKey("picker_items_index_${currencyName}_button_key");
await settle();
}
Future<void> enterAddress(String address) async {
final input = find.byKey(const ValueKey("contact_page_address_input_key"));
await pumpUntilFound(input);
final field = find.descendant(of: input, matching: find.byType(EditableText));
await tester.enterText(field.first, address);
await tester.pump(const Duration(milliseconds: 300));
}
Future<void> save() async {
await tapByKey("contact_page_save_button_key");
await pumpUntilGone(find.byType(ContactPage));
await settle();
}
void expectContactSaved(String name, String address) {
final page = tester.widget<ContactListPage>(find.byType(ContactListPage));
final saved = page.contactListViewModel.contacts.where((c) => c.name == name).toList();
expect(saved, isNotEmpty, reason: "The contact $name was never saved");
expect(
saved.first.address,
address,
reason: "The address book stored a different address than it was given",
);
}
}
@@ -1,46 +1,16 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/welcome/create_pin_welcome_page.dart';
import 'package:hash_wallet/wallet_type_utils.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/welcome/create_pin_welcome_page.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class CreatePinWelcomePageRobot {
CreatePinWelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class CreatePinWelcomePageRobot extends BaseRobot {
CreatePinWelcomePageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isCreatePinWelcomePage() async {
await commonTestCases.isSpecificPage<CreatePinWelcomePage>();
await commonTestCases.takeScreenshots('create_pin_welcome_page');
}
void hasTitle() {
String title;
if (isMoneroOnly) {
title = S.current.monero_com;
}
title = S.current.cake_wallet;
commonTestCases.hasText(title);
}
void hasDescription() {
String description;
if (isMoneroOnly) {
description = S.current.monero_com_wallet_text;
}
description = S.current.new_first_wallet_text;
commonTestCases.hasText(description);
@override
Future<void> isDisplayed() async {
await isSpecificPage<CreatePinWelcomePage>();
}
Future<void> tapSetAPinButton() async {
await commonTestCases.tapItemByKey('create_pin_welcome_page_create_a_pin_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("create_pin_welcome_page_create_a_pin_button_key");
}
}
@@ -1,40 +0,0 @@
import 'package:hash_wallet/src/screens/dashboard/widgets/menu_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class DashboardMenuWidgetRobot {
DashboardMenuWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> hasMenuWidget() async {
await commonTestCases.takeScreenshots('menu_widget_page');
commonTestCases.hasType<MenuWidget>();
}
void displaysTheCorrectWalletNameAndSubName() {
final menuWidgetState = tester.state<MenuWidgetState>(find.byType(MenuWidget));
final walletName = menuWidgetState.widget.dashboardViewModel.name;
commonTestCases.hasText(walletName);
final walletSubName = menuWidgetState.widget.dashboardViewModel.subname;
if (walletSubName.isNotEmpty) {
commonTestCases.hasText(walletSubName);
}
}
Future<void> navigateToWalletMenu() async {
await commonTestCases.tapItemByKey('dashboard_page_menu_widget_wallet_menu_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> navigateToSecurityAndBackupPage() async {
await commonTestCases.tapItemByKey(
'dashboard_page_menu_widget_security_and_backup_button_key',
);
await commonTestCases.defaultSleepTime();
}
}
@@ -1,122 +0,0 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/dashboard/dashboard_page.dart';
import 'package:hash_wallet/src/screens/dashboard/pages/balance/crypto_balance_widget.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
import 'dashboard_menu_widget_robot.dart';
class DashboardPageRobot {
DashboardPageRobot(this.tester)
: commonTestCases = CommonTestCases(tester),
dashboardMenuWidgetRobot = DashboardMenuWidgetRobot(tester);
final WidgetTester tester;
final DashboardMenuWidgetRobot dashboardMenuWidgetRobot;
late CommonTestCases commonTestCases;
Future<void> isDashboardPage() async {
await commonTestCases.isSpecificPage<DashboardPage>();
await commonTestCases.takeScreenshots('dashboard_page');
}
Future<void> confirmWalletTypeIsDisplayedCorrectly(
WalletType type, {
bool isHaven = false,
}) async {
final cryptoBalanceWidget =
tester.widget<CryptoBalanceWidget>(find.byType(CryptoBalanceWidget));
final hasAccounts = cryptoBalanceWidget.dashboardViewModel.balanceViewModel.hasAccounts;
if (hasAccounts) {
final walletName = cryptoBalanceWidget.dashboardViewModel.name;
commonTestCases.hasText(walletName);
} else {
final walletName = walletTypeToString(type);
final assetName = isHaven ? '$walletName Assets' : walletName;
commonTestCases.hasText(assetName);
}
await commonTestCases.defaultSleepTime(seconds: 5);
}
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
await confirmWalletTypeIsDisplayedCorrectly(type, isHaven: isHaven);
// Swipe to Cake features Page
await swipeDashboardTab(false);
commonTestCases.hasText('Cake ${S.current.features}');
// Swipe back to balance
await swipeDashboardTab(true);
// Swipe to Transactions Page
await swipeDashboardTab(true);
commonTestCases.hasText(S.current.transactions);
// Swipe back to balance
await swipeDashboardTab(false);
await commonTestCases.defaultSleepTime(seconds: 3);
}
Future<void> swipeDashboardTab(bool swipeRight) async {
await commonTestCases.swipeByPageKey(
key: 'dashboard_page_view_key',
swipeRight: swipeRight,
);
await commonTestCases.defaultSleepTime();
}
Future<void> openDrawerMenu() async {
await commonTestCases.tapItemByKey('dashboard_page_wallet_menu_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> navigateToBuyPage() async {
await commonTestCases.tapItemByKey('dashboard_page_buy_action_button_key');
}
Future<void> navigateToWalletsListPage({bool isDrawer = false}) async {
await tester.pumpAndSettle(Duration(milliseconds: 1000));
await commonTestCases.tapItemByKey(
isDrawer
? 'dashboard_page_menu_widget_wallet_menu_button_key'
: 'dashboard_page__wallet_list_button_key',
);
await tester.pumpAndSettle(Duration(milliseconds: 2000));
}
Future<void> navigateToSendPage() async {
await commonTestCases.tapItemByKey('dashboard_page_send_action_button_key');
}
Future<void> navigateToSellPage() async {
await commonTestCases.tapItemByKey('dashboard_page_sell_action_button_key');
}
Future<void> navigateToReceivePage() async {
await commonTestCases.tapItemByKey('dashboard_page_receive_action_button_key');
}
Future<void> navigateToExchangePage() async {
await commonTestCases.tapItemByKey('dashboard_page_swap_action_button_key');
}
}
@@ -1,40 +0,0 @@
import 'package:hash_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>();
await commonTestCases.takeScreenshots('disclaimer_page');
}
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();
}
}
@@ -0,0 +1,78 @@
import "package:hash_wallet/entities/fiat_currency.dart";
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart";
import "package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_picker_sheet.dart";
import "package:hash_wallet/new-ui/widgets/currency_picker/fiat_currency_row.dart";
import "package:hash_wallet/src/screens/settings/display_settings_page.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class DisplaySettingsPageRobot extends BaseRobot {
DisplaySettingsPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await isSpecificPage<DisplaySettingsPage>();
}
Future<void> openLanguagePicker() async {
await tapByKey("display_settings_language");
}
Future<void> chooseLanguage(String code) async {
await tapByKey("picker_items_index_${code}_button_key");
await settle();
}
bool hasFiatCurrencyRow() =>
tester.any(find.byKey(const ValueKey("display_settings_fiat_currency")));
Future<void> openFiatCurrencyPicker() async {
await tapByKey("display_settings_fiat_currency");
await pumpUntilFound(find.byType(FiatCurrencyPickerSheet));
}
Future<void> chooseFiatCurrency(FiatCurrency currency) async {
final searchField = find.descendant(
of: find.byType(CurrencyPickerSearchField),
matching: find.byType(TextFormField),
);
await tester.enterText(searchField, currency.fullName);
final row = find.byWidgetPredicate(
(widget) => widget is FiatCurrencyRow && widget.currency == currency,
);
await pumpUntilFound(row);
await tester.tap(row.first);
await pumpUntilGone(find.byType(FiatCurrencyPickerSheet));
}
void expectFiatCurrencyShown(FiatCurrency currency) {
expect(
find.descendant(
of: find.byKey(const ValueKey("display_settings_fiat_currency")),
matching: find.text(currency.title),
),
findsOneWidget,
reason: "The currency row still does not show ${currency.title}",
);
}
void expectLanguageShown(String displayName) {
expect(
find.descendant(
of: find.byKey(const ValueKey("display_settings_language")),
matching: find.text(displayName),
),
findsOneWidget,
reason: "The language row still does not show $displayName",
);
}
}
@@ -1,46 +0,0 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_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>();
await commonTestCases.takeScreenshots('exchange_confirm_page');
}
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');
}
}
@@ -1,338 +0,0 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/exchange/exchange_page.dart';
import 'package:hash_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';
import '../components/common_test_constants.dart';
import 'auth_page_robot.dart';
class ExchangePageRobot {
ExchangePageRobot(this.tester)
: commonTestCases = CommonTestCases(tester),
authPageRobot = AuthPageRobot(tester);
WidgetTester tester;
AuthPageRobot authPageRobot;
CommonTestCases commonTestCases;
Future<void> isExchangePage() async {
await commonTestCases.isSpecificPage<ExchangePage>();
await commonTestCases.takeScreenshots('exchange_page');
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.enterText(depositCurrency.name, 'search_bar_widget_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.enterText(receiveCurrency.name, 'search_bar_widget_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 {
tester.printToConsole('Inside trade creation failure handle');
bool isTradeCreationFailure = false;
isTradeCreationFailure = hasTradeCreationFailureError();
int maxRetries = 3;
int retries = 0;
while (isTradeCreationFailure && retries < maxRetries) {
await tester.pumpAndSettle();
await onTradeCreationFailureDialogButtonPressed();
await commonTestCases.defaultSleepTime(seconds: 5);
await onExchangeButtonPressed();
await _handleAuthPage();
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 {
double amount;
amount = double.parse(initialAmount);
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
amount = (exchangeViewModel.limits.min ?? 0.0) + 1.0;
await enterDepositAmount(amount.toString());
await tester.pumpAndSettle();
await onExchangeButtonPressed();
await tester.pumpAndSettle();
}
/// 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 {
double amount;
amount = double.parse(initialAmount);
ExchangePage exchangePage = tester.widget(find.byType(ExchangePage));
final exchangeViewModel = exchangePage.exchangeViewModel;
amount = (exchangeViewModel.limits.max ?? 0.0) - 1.0;
await enterDepositAmount(amount.toString());
await tester.pumpAndSettle();
await onExchangeButtonPressed();
await tester.pumpAndSettle();
}
Future<void> handleErrors(String initialAmount) async {
await tester.pumpAndSettle();
bool isMinLimitError = hasMinLimitError();
if (isMinLimitError) {
tester.printToConsole('Has min limit error');
await _handleMinLimitError(initialAmount);
}
bool isMaxLimitError = hasMaxLimitError();
if (isMaxLimitError) {
await _handleMaxLimitError(initialAmount);
}
tester.printToConsole('No limits error, proceeding with flow');
await _handleAuthPage();
await tester.pumpAndSettle();
await _handleTradeCreationFailureErrors();
await tester.pumpAndSettle();
await commonTestCases.defaultSleepTime();
}
Future<void> _handleAuthPage() async {
final onAuthPage = authPageRobot.onAuthPage();
if (onAuthPage) {
await authPageRobot.enterPinCode(CommonTestConstants.pin);
}
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
if (onAuthPageDesktop) {
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
}
}
}
@@ -1,35 +0,0 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:hash_wallet/src/screens/exchange_trade/exchange_trade_external_send_page.dart';
import '../components/common_test_cases.dart';
class ExchangeTradeExternalSendPageRobot {
ExchangeTradeExternalSendPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isExchangeTradeExternalSendPage() async {
await commonTestCases.isSpecificPage<ExchangeTradeExternalSendPage>();
await commonTestCases.takeScreenshots('exchange_trade_external_send_page');
}
Future<void> verifySendDetailsItemsDisplayProperly() async {
final widget =
tester.widget<ExchangeTradeExternalSendPage>(find.byType(ExchangeTradeExternalSendPage));
final exchangeTradeViewModel = widget.exchangeTradeViewModel;
final items = exchangeTradeViewModel.items.where((item) => item.isExternalSendDetail).toList();
for (var item in items) {
commonTestCases.hasValueKey('exchange_trade_external_send_page_send_item_${item.title}_key');
tester.printToConsole('${item.title} present on screen');
}
commonTestCases.defaultSleepTime();
}
Future<void> onContinueButtonPressed() async {
await commonTestCases.tapItemByKey('exchange_trade_external_send_page_continue_button_key');
}
}
@@ -1,161 +0,0 @@
import 'dart:async';
import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_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>();
await commonTestCases.takeScreenshots('exchange_trade_page');
}
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> onSendFromExternalButtonPressed() async {
tester.printToConsole('Routing to send from external details page');
await commonTestCases.tapItemByKey(
'exchange_trade_page_send_from_external_button_key',
);
}
Future<void> onSendFromCakeButtonPressed() async {
tester.printToConsole('Now sending from cake');
await commonTestCases.tapItemByKey(
'exchange_trade_page_send_from_cake_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 onSendFromCakeButtonPressed();
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();
}
}
@@ -0,0 +1,227 @@
import "package:hash_wallet/new-ui/pages/home_page.dart";
import "package:hash_wallet/new-ui/widgets/coins_page/assets_history/history_tile.dart";
import "package:hash_wallet/new-ui/widgets/coins_page/assets_history/transaction_details_modal.dart";
import "package:hash_wallet/new-ui/widgets/coins_page/top_bar_widget/sync_bar.dart";
import "package:cw_core/sync_status.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class HomePageRobot extends BaseRobot {
HomePageRobot(super.tester);
@override
Future<void> isDisplayed() async {
final shown = await pumpUntil(() => isInFront);
expect(shown, true, reason: "The home tab never came to the front");
}
bool get isInFront {
final stackFinder = find.byType(IndexedStack);
if (!tester.any(stackFinder)) {
return false;
}
final stack = tester.widget<IndexedStack>(stackFinder.first);
final index = stack.children.indexWhere((child) => child is NewHomePage);
return index >= 0 && stack.index == index;
}
Future<void> hasWalletName(String name) async {
final nameFinder = find.byKey(const ValueKey("home_page_wallet_name_text_key"));
final syncBarFinder = find.byType(SyncBar);
final shown = await pumpUntil(() => tester.any(nameFinder) || tester.any(syncBarFinder));
expect(shown, true, reason: "Top bar showed neither the wallet name nor the sync bar");
if (!tester.any(nameFinder)) {
tester.printToConsole("Sync bar is in place of the wallet name, not checking it on screen");
return;
}
expect(textByKey("home_page_wallet_name_text_key"), name);
}
Future<void> openSendSheet() async {
await tapByKey("home_page_send_button_key");
}
Future<void> openReceiveSheet() async {
await tapByKey("home_page_receive_button_key");
}
Future<void> openSwapSheet() async {
await tapByKey("home_page_swap_button_key");
}
Future<void> openSettingsSheet() async {
await tapByKey("home_page_settings_button_key");
}
Future<void> openHistoryTab() async {
final tabFinder = find.byKey(const ValueKey("line_tab_switcher_1_key"));
if (!tester.any(tabFinder)) {
return;
}
await tapWhenVisible(tabFinder);
await settle();
}
Future<void> confirmTransactionHistoryVisible({
Duration timeout = const Duration(minutes: 3),
}) async {
final hasTransactions = await pumpUntil(
() => _dashboardTransactionCount() > 0,
timeout: timeout,
);
expect(
hasTransactions,
true,
reason: "No transactions arrived within ${timeout.inSeconds}s",
);
await openHistoryTab();
await pumpUntilFound(find.byType(HistoryTile));
expect(
tester.widgetList(find.byType(HistoryTile)).length,
lessThanOrEqualTo(3),
reason: "The home page preview should never render more than the short history",
);
}
Future<void> openAllTransactions() async {
final actionButton = find.byKey(const ValueKey("assets_history_action_button_key"));
final historyBar = find.byKey(const ValueKey("history_top_bar_key"));
await tapWhenVisible(tester.any(actionButton) ? actionButton : historyBar);
await pumpUntilFound(find.byKey(const ValueKey("history_modal_key")));
}
Future<void> confirmAllTransactionsVisible() async {
await pumpUntilFound(_allViewTiles);
final rendered = tester.widgetList(_allViewTiles).length;
if (_dashboardTransactionCount() > 3) {
expect(
rendered,
greaterThan(3),
reason: "The All view rendered $rendered tiles, no more than the home preview does",
);
return;
}
expect(_allViewTiles, findsWidgets);
}
String firstTransactionIdInAllView() {
final tile = tester.widgetList<HistoryTile>(_allViewTiles).first;
final key = tile.key! as ValueKey<String>;
return key.value.substring("home_page_transaction_".length, key.value.length - "_key".length);
}
Future<void> openFirstTransactionDetails() async {
await pumpUntilFound(_allViewTiles);
await tester.tap(_allViewTiles.first);
await pumpUntilFound(find.byType(TransactionDetailsModal));
await settle();
}
String openedTransactionId() {
final modal = tester.widget<TransactionDetailsModal>(find.byType(TransactionDetailsModal));
return modal.transactionDetailsViewModel.transactionInfo.id;
}
void hasTransactionIdRow() {
expect(
find.byKey(const ValueKey("standard_list_item_transaction_details_id_key")),
findsOneWidget,
reason: "The details opened without showing the transaction id",
);
}
// The home preview stays mounted underneath the modal, so its tiles have to be left out.
Finder get _allViewTiles => find.descendant(
of: find.byKey(const ValueKey("history_modal_key")),
matching: find.byType(HistoryTile),
);
Future<void> confirmSyncIndicatorShown({Duration timeout = const Duration(minutes: 3)}) async {
final syncBar = find.byType(SyncBar);
final tick = find.descendant(of: syncBar, matching: find.byIcon(Icons.check));
SyncStatus? lastStatus;
var sawIndicator = false;
final synced = await pumpUntil(
() {
lastStatus = _dashboardStatus();
if (lastStatus == null) {
return false;
}
final keyed = find.descendant(
of: syncBar,
matching: find.byKey(ValueKey(lastStatus.runtimeType)),
);
sawIndicator = sawIndicator || tester.any(keyed) || tester.any(tick);
return lastStatus is SyncedSyncStatus;
},
timeout: timeout,
);
expect(
synced,
true,
reason: "The wallet never synced within ${timeout.inMinutes}m, "
"it reported ${lastStatus.runtimeType}",
);
expect(
sawIndicator,
true,
reason: "The sync bar never showed a status while the wallet synced",
);
}
SyncStatus? _dashboardStatus() {
final finder = find.byType(NewHomePage);
if (!tester.any(finder)) {
return null;
}
return tester.widget<NewHomePage>(finder.first).dashboardViewModel.status;
}
int _dashboardTransactionCount() {
final finder = find.byType(NewHomePage);
if (!tester.any(finder)) {
return 0;
}
return tester.widget<NewHomePage>(finder.first).dashboardViewModel.transactions.length;
}
}
@@ -0,0 +1,17 @@
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class LightningUsernamePageRobot extends BaseRobot {
LightningUsernamePageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await pumpUntilFound(find.byKey(const ValueKey("lightning_username_page_skip_button_key")));
}
Future<void> tapSkip() async {
await tapByKey("lightning_username_page_skip_button_key");
}
}
@@ -0,0 +1,39 @@
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/src/screens/nodes/widgets/node_list_row.dart";
import "package:hash_wallet/src/screens/settings/manage_nodes_page.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class ManageNodesPageRobot extends BaseRobot {
ManageNodesPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await isSpecificPage<ManageNodesPage>();
}
String currentNodeUri() {
final page = tester.widget<ManageNodesPage>(find.byType(ManageNodesPage));
return page.nodeListViewModel.currentNode.uriRaw;
}
Future<void> switchToAnotherNode() async {
final rows = find.byType(NodeListRow);
await pumpUntilFound(rows.first);
expect(
tester.widgetList(rows).length,
greaterThan(1),
reason: "Only the current node is listed, there is nothing to switch to",
);
await tapWhenVisible(rows.at(1));
await tapWhenVisible(find.text(S.current.change));
await settle();
}
}
@@ -0,0 +1,25 @@
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class NewDashboardRobot extends BaseRobot {
NewDashboardRobot(super.tester);
@override
Future<void> isDisplayed({Duration timeout = const Duration(seconds: 30)}) async {
await pumpUntilFound(find.byKey(const ValueKey("new_dashboard_page_key")), timeout: timeout);
}
Future<void> openHomeTab() async {
await tapByKey("dashboard_page_home_action_button_key");
}
Future<void> openWalletsTab() async {
await tapByKey("dashboard_page_wallets_action_button_key");
}
Future<void> openContactsTab() async {
await tapByKey("dashboard_page_contacts_action_button_key");
}
}
@@ -0,0 +1,57 @@
import "package:hash_wallet/new-ui/pages/receive_page.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class NewReceivePageRobot extends BaseRobot {
NewReceivePageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await pumpUntilFound(find.byType(NewReceivePage));
}
Future<void> confirmAddressMatches(String expected) async {
final wanted = _normalise(expected);
await pumpUntilFound(find.byKey(const ValueKey("receive_page_address_key")));
final matched = await pumpUntil(() => _renderedAddress() == wanted);
expect(
matched,
true,
reason: "Receive page should show $wanted but showed ${_renderedAddress()}",
);
}
String _normalise(String address) =>
address.replaceAll("bitcoincash:", "").replaceAll(RegExp(r"\s"), "");
String? _renderedAddress() {
final root = find.byKey(const ValueKey("receive_page_address_key"));
if (!tester.any(root)) {
return null;
}
final rich = find.descendant(of: root, matching: find.byType(RichText));
if (tester.any(rich)) {
final text = _normalise(tester.widget<RichText>(rich.first).text.toPlainText());
if (text.isNotEmpty) {
return text;
}
}
final plain = find.descendant(of: root, matching: find.byType(Text));
if (tester.any(plain)) {
return _normalise(tester.widget<Text>(plain.first).data ?? "");
}
return null;
}
}
@@ -0,0 +1,205 @@
import "package:hash_wallet/core/execution_state.dart";
import "package:hash_wallet/new-ui/pages/send_page.dart";
import "package:hash_wallet/new-ui/widgets/confirm_swiper.dart";
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
import "package:hash_wallet/new-ui/widgets/send_page/send_confirm_bottom_widget.dart";
import "package:hash_wallet/new-ui/widgets/send_page/send_confirm_sheet.dart";
import "package:hash_wallet/view_model/send/send_view_model.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
import "../core/test_config.dart";
class NewSendPageRobot extends BaseRobot {
NewSendPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await pumpUntilFound(find.byType(NewSendPage));
}
Future<void> enterAddress(String address) async {
await _enterTextInInput("send_page_address_input_key", address);
}
Future<void> enterAmount(String amount) async {
await _enterTextInInput("send_page_amount_input_key", amount);
}
String enteredAddress() => _textInInput("send_page_address_input_key");
Future<void> pickContactFromAddressBook(String contactName) async {
await tapByKey("send_page_address_book_button_key");
final tabs = find.byType(Tab);
await pumpUntilFound(tabs);
await tester.tap(tabs.at(1));
await settle();
final contactFinder = find.byKey(ValueKey(contactName));
await pumpUntilFound(contactFinder);
await tester.tap(contactFinder.first);
await pumpUntilGone(contactFinder);
}
Future<void> tapSendButton() async {
await tapByKey("send_page_send_button_key");
}
Future<void> tapSendButtonWhenReady({Duration timeout = TestConfig.walletSyncBudget}) async {
final ready = await pumpUntil(() => _model()?.isReadyForSend ?? false, timeout: timeout);
expect(
ready,
true,
reason: "The wallet never became ready to send within ${timeout.inMinutes}m, "
"so the send button stayed disabled",
);
await tapSendButton();
}
bool get isSendFlowOpen =>
tester.any(find.byType(NewSendPage)) || tester.any(find.byType(SendConfirmSheet));
Future<void> swipeToConfirm({Duration timeout = const Duration(seconds: 90)}) async {
final finder = find.byKey(const ValueKey("send_page_confirm_swiper_key"));
await pumpUntilFound(finder, timeout: timeout);
await settle(max: const Duration(seconds: 2));
final width = tester.view.physicalSize.width / tester.view.devicePixelRatio;
await tester.drag(finder.first, Offset(width, 0));
await tester.pump(const Duration(milliseconds: 500));
}
Future<void> confirmSendButtonReplacesSwiper() async {
final swiper = find.byKey(const ValueKey("send_page_confirm_swiper_key"));
await pumpUntilFound(swiper);
tester.binding.platformDispatcher.accessibilityFeaturesTestValue =
const FakeAccessibilityFeatures(accessibleNavigation: true);
addTearDown(tester.binding.platformDispatcher.clearAccessibilityFeaturesTestValue);
await settle(max: const Duration(seconds: 2));
final button = find.descendant(of: swiper, matching: find.byType(NewPrimaryButton));
expect(
tester.any(button),
true,
reason: "Accessible navigation is on but the sheet still only offers the swiper",
);
expect(
tester.widget<NewPrimaryButton>(button.first).onPressed,
isNotNull,
reason: "The send button a screen reader gets is disabled",
);
}
Future<void> confirmTransactionBuilt({Duration timeout = const Duration(minutes: 3)}) async {
await pumpUntil(
() => tester.any(find.byType(ConfirmSwiper)) || _model()?.state is FailureState,
timeout: timeout,
);
if (tester.any(find.byType(ConfirmSwiper))) {
return;
}
fail("No confirm sheet after ${timeout.inSeconds}s. ${_describeSendState()}");
}
Future<void> confirmTransactionCommitted({
Duration timeout = const Duration(minutes: 3),
}) async {
await pumpUntilFound(find.byType(TransactionCommittedScreenActionButton), timeout: timeout);
}
Future<void> expectNoTransactionBuilt({
Duration window = const Duration(seconds: 15),
}) async {
final offered = await pumpUntil(
() => tester.any(find.byType(ConfirmSwiper)),
timeout: window,
);
expect(
offered,
false,
reason:
"The send screen showed the swiper, wanting to broadcast a transaction it should have refused",
);
}
SendViewModel? _model() {
final sheet = find.byType(SendConfirmSheet);
if (tester.any(sheet)) {
return tester.widget<SendConfirmSheet>(sheet.first).sendViewModel;
}
final page = find.byType(NewSendPage);
if (tester.any(page)) {
return tester.widget<NewSendPage>(page.first).sendViewModel;
}
return null;
}
String _describeSendState() {
final model = _model();
final available = model?.wallet.balance.values
.map((balance) => balance.available)
.where((amount) => amount.sign > 0)
.join(", ");
final errorFinder = find.byType(TransactionErrorActions);
final error = tester.any(errorFinder)
? tester.widget<TransactionErrorActions>(errorFinder.first).errorText
: model?.state is FailureState
? (model!.state as FailureState).error
: null;
return "state=${model?.state.runtimeType}, "
"wallet holds ${available == null || available.isEmpty ? "nothing" : available}"
"${error == null ? "" : ", the chain said: $error"}";
}
String _textInInput(String inputKey) {
final inputFinder = find.byKey(ValueKey(inputKey));
if (!tester.any(inputFinder)) {
return "unknown";
}
final field = find.descendant(of: inputFinder, matching: find.byType(EditableText));
return tester.widget<EditableText>(field.first).controller.text;
}
Future<void> _enterTextInInput(String inputKey, String text) async {
final inputFinder = find.byKey(ValueKey(inputKey));
await pumpUntilFound(inputFinder);
final fieldFinder = find.descendant(of: inputFinder, matching: find.byType(EditableText));
await tester.enterText(fieldFinder.first, text);
await tester.pump(const Duration(milliseconds: 300));
}
}
@@ -0,0 +1,45 @@
import "package:hash_wallet/new-ui/pages/settings_page.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class NewSettingsPageRobot extends BaseRobot {
NewSettingsPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await pumpUntilFound(find.byType(SettingsMainPage));
}
Future<void> openRow(String route) async {
await scrollUntilRowVisible(route);
await tapByKey(route);
}
Future<void> scrollUntilRowVisible(String route) async {
final rowFinder = find.byKey(ValueKey(route));
if (tester.any(rowFinder)) {
return;
}
final scrollableFinder = find.descendant(
of: find.byType(SettingsMainPage),
matching: find.byType(Scrollable),
);
await tester.scrollUntilVisible(
rowFinder,
200,
scrollable: scrollableFinder.first,
maxScrolls: 30,
);
await tester.pump(const Duration(milliseconds: 300));
}
Future<void> confirmLeafPageDisplayed(Type pageType) async {
await pumpUntilFound(find.byType(pageType));
}
}
@@ -0,0 +1,211 @@
import "package:hash_wallet/core/execution_state.dart";
import "package:hash_wallet/exchange/limits_state.dart";
import "package:hash_wallet/new-ui/pages/swap_page.dart";
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_row.dart";
import "package:hash_wallet/new-ui/widgets/currency_picker/currency_picker_search_field.dart";
import "package:hash_wallet/new-ui/widgets/swap_page/swap_amount_box.dart";
import "package:hash_wallet/new-ui/widgets/swap_page/swap_confirm_sheet.dart";
import "package:hash_wallet/src/widgets/primary_button.dart";
import "package:hash_wallet/view_model/send/send_view_model_state.dart";
import "package:cw_core/crypto_currency.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
import "../core/test_config.dart";
import "../core/test_wallets.dart";
class NewSwapPageRobot extends BaseRobot {
NewSwapPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await pumpUntilFound(find.byType(NewSwapPage));
}
Future<void> enterDepositAmount(String amount) async {
await enterTextByKey("swap_page_deposit_amount_field_key", amount);
}
Future<void> chooseReceiveCurrency(CryptoCurrency currency) async {
await tapByKey("swap_amount_box_receive_currency_button_key");
final search = find.descendant(
of: find.byType(CurrencyPickerSearchField),
matching: find.byType(EditableText),
);
await pumpUntilFound(search);
await tester.enterText(search.first, currency.title);
await settle(max: const Duration(seconds: 2));
await tapWhenVisible(
find.byWidgetPredicate(
(widget) => widget is CurrencyPickerRow && _isSameCurrency(widget.currency, currency),
),
);
final chosen = await pumpUntil(
() => _isSameCurrency(
tester.widget<NewSwapPage>(find.byType(NewSwapPage)).exchangeViewModel.receiveCurrency,
currency,
),
);
expect(
chosen,
true,
reason: "The picker did not switch the receive currency to ${currency.title}",
);
}
static bool _isSameCurrency(CryptoCurrency one, CryptoCurrency other) =>
one.title.toLowerCase() == other.title.toLowerCase() &&
(one.tag ?? "").toLowerCase() == (other.tag ?? "").toLowerCase();
Future<void> confirmQuoteReceived({Duration timeout = const Duration(seconds: 90)}) async {
final received = await pumpUntil(() => _bestRate() > 0, timeout: timeout);
expect(received, true, reason: "No provider quote arrived within ${timeout.inSeconds}s");
}
Future<void> enterMinimumViableDepositAmount() async {
final limitsLoaded = await pumpUntil(
() => _limitsState() is LimitsLoadedSuccessfully,
timeout: const Duration(seconds: 90),
);
expect(limitsLoaded, true, reason: "Provider limits never loaded");
final min = _limitsMin() ?? 0;
// 5 percent over the minimum, so a small rate move does not invalidate the trade.
final amount = min > 0 ? (min * 1.05).toStringAsFixed(8) : "0.01";
await enterDepositAmount(amount);
}
Future<String> enterTestReceiveAddress() async {
final exchangeViewModel =
tester.widget<NewSwapPage>(find.byType(NewSwapPage)).exchangeViewModel;
final receiveType = cryptoCurrencyOrTokenToWalletType(exchangeViewModel.receiveCurrency);
final address = receiveType == null ? "" : TestWallets.receiveAddressFor(receiveType);
expect(
address.isNotEmpty,
true,
reason: "No test receive address for ${exchangeViewModel.receiveCurrency.title}",
);
final receiveCard = find.byWidgetPredicate(
(widget) => widget is SwapAmountBox && widget.isReceiverCard,
);
tester.state<SwapAmountBoxState>(receiveCard).addressController.text = address;
await settle();
return address;
}
Future<void> confirmSwapButtonEnabled({Duration timeout = TestConfig.walletSyncBudget}) async {
final button = find.byKey(const ValueKey("exchange_page_exchange_button_key"));
final enabled = await pumpUntil(
() => tester.any(button) && !tester.widget<LoadingPrimaryButton>(button).isDisabled,
timeout: timeout,
);
expect(
enabled,
true,
reason: "The swap button stayed disabled with an amount, a quote and a receive address",
);
}
Future<void> tapSwapButton() async {
await tapByKey("exchange_page_exchange_button_key");
}
Future<void> confirmTradeCreated({Duration timeout = const Duration(minutes: 2)}) async {
await pumpUntilFound(find.byType(SwapConfirmSheet), timeout: timeout);
}
void confirmPayoutAddress(String expected) {
final sheet = tester.widget<SwapConfirmSheet>(find.byType(SwapConfirmSheet));
final payout = sheet.exchangeTradeViewModel.trade.payoutAddress ?? "";
final isHex = expected.startsWith("0x");
expect(
isHex ? payout.toLowerCase() : payout,
isHex ? expected.toLowerCase() : expected,
reason: "The trade pays out somewhere other than the test wallet",
);
}
Future<void> confirmDepositCommitted({Duration timeout = const Duration(minutes: 3)}) async {
ExecutionState? lastState;
final committed = await pumpUntil(
() {
final sheet = find.byType(SwapConfirmSheet);
if (!tester.any(sheet)) {
return true;
}
lastState =
tester.widget<SwapConfirmSheet>(sheet).exchangeTradeViewModel.sendViewModel.state;
return lastState is TransactionCommitted;
},
timeout: timeout,
);
expect(
committed,
true,
reason: "The deposit never committed within ${timeout.inMinutes}m, "
"the sheet reported ${lastState.runtimeType}",
);
expect(
lastState,
isA<TransactionCommitted>(),
reason: "The confirm sheet closed before the deposit committed, "
"it last reported ${lastState.runtimeType}",
);
await pumpUntilGone(find.byType(NewSwapPage));
}
double? _limitsMin() {
final finder = find.byType(NewSwapPage);
if (!tester.any(finder)) {
return null;
}
return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.limits.min;
}
LimitsState? _limitsState() {
final finder = find.byType(NewSwapPage);
if (!tester.any(finder)) {
return null;
}
return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.limitsState;
}
double _bestRate() {
final finder = find.byType(NewSwapPage);
if (!tester.any(finder)) {
return 0;
}
return tester.widget<NewSwapPage>(finder.first).exchangeViewModel.bestRate;
}
}
@@ -1,36 +1,32 @@
import 'package:hash_wallet/src/screens/new_wallet/new_wallet_page.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/new_wallet/new_wallet_page.dart";
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
import "package:flutter_test/flutter_test.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class NewWalletPageRobot {
NewWalletPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class NewWalletPageRobot extends BaseRobot {
NewWalletPageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isNewWalletPage() async {
await commonTestCases.isSpecificPage<NewWalletPage>();
await commonTestCases.takeScreenshots('new_wallet_page');
@override
Future<void> isDisplayed() async {
await isSpecificPage<NewWalletPage>();
}
Future<void> enterWalletName(String walletName) async {
await commonTestCases.enterText(
walletName,
'new_wallet_page_wallet_name_textformfield_key',
);
await commonTestCases.defaultSleepTime();
await enterTextByKey("new_wallet_page_wallet_name_textformfield_key", walletName);
}
Future<void> generateWalletName() async {
await commonTestCases.tapItemByKey(
'new_wallet_page_wallet_name_textformfield_generate_name_button_key',
);
await commonTestCases.defaultSleepTime();
await tapByKey("new_wallet_page_wallet_name_textformfield_generate_name_button_key");
}
Future<void> expectNameRejected() async {
await pumpUntilFound(find.byType(AlertWithOneAction));
hasType<NewWalletPage>();
}
Future<void> onNextButtonPressed() async {
await commonTestCases.tapItemByKey('new_wallet_page_confirm_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("new_wallet_page_confirm_button_key");
}
}
@@ -1,55 +1,29 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/new_wallet/new_wallet_type_page.dart';
import 'package:hash_wallet/themes/core/material_base_theme.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/new_wallet/new_wallet_type_page.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';
import "../core/base_robot.dart";
class NewWalletTypePageRobot {
NewWalletTypePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class NewWalletTypePageRobot extends BaseRobot {
NewWalletTypePageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isNewWalletTypePage() async {
await commonTestCases.isSpecificPage<NewWalletTypePage>();
await commonTestCases.takeScreenshots('new_wallet_type_page');
}
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,
);
@override
Future<void> isDisplayed() async {
await isSpecificPage<NewWalletTypePage>();
}
Future<void> findParticularWalletTypeInScrollableList(WalletType type) async {
await tester.pump(Duration(milliseconds: 1000));
final scrollableWidget = find.descendant(
of: find.byKey(Key('new_wallet_type_scrollable_key')),
of: find.byKey(const Key("new_wallet_type_scrollable_key")),
matching: find.byType(Scrollable),
);
final targetWidget = find.byKey(ValueKey('new_wallet_type_${type.name}_button_key'));
final targetWidget = find.byKey(ValueKey("new_wallet_type_${type.name}_button_key"));
tester.printToConsole('Attempting to scroll to wallet type ${type.name}');
tester.printToConsole("Attempting to scroll to wallet type ${type.name}");
await tester.pump(Duration(milliseconds: 500));
await pumpUntilFound(scrollableWidget.first);
await tester.scrollUntilVisible(
targetWidget,
@@ -58,12 +32,14 @@ class NewWalletTypePageRobot {
maxScrolls: 20,
);
await tester.pumpAndSettle(Duration(milliseconds: 1000));
await settle();
expect(tester.any(targetWidget), true,
reason: 'Wallet type ${type.name} should be visible after scrolling');
expect(
tester.any(targetWidget),
true,
reason: "Wallet type ${type.name} should be visible after scrolling",
);
// Additional check to ensure the widget is actually on screen
final widgetRect = tester.getRect(targetWidget);
final screenSize = tester.view.physicalSize;
final screenRect = Rect.fromLTWH(0, 0, screenSize.width, screenSize.height);
@@ -71,17 +47,13 @@ class NewWalletTypePageRobot {
expect(
screenRect.overlaps(widgetRect),
true,
reason: 'Wallet type ${type.name} should be within screen bounds',
reason: "Wallet type ${type.name} should be within screen bounds",
);
tester.printToConsole('Wallet type ${type.name} is now visible and tappable');
tester.printToConsole("Wallet type ${type.name} is now visible and tappable");
}
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');
await tapByKey("new_wallet_type_${type.name}_button_key");
}
}
@@ -1,52 +1,24 @@
import 'package:hash_wallet/src/screens/pin_code/pin_code_widget.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/pin_code/pin_code_widget.dart";
import "package:flutter_test/flutter_test.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class PinCodeWidgetRobot {
PinCodeWidgetRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class PinCodeWidgetRobot extends BaseRobot {
PinCodeWidgetRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
@override
Future<void> isDisplayed() async => hasPinCodeWidget();
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');
Future<void> enterPinCode(List<int> pinCode) async {
for (final pin in pinCode) {
await tapByKey("pin_code_button_${pin}_key");
}
// Confirmation for 0 button
commonTestCases.hasValueKey('pin_code_button_0_key');
}
Future<void> enterPassword(String password, {int pumpDuration = 100}) async {
await commonTestCases.enterText(
password,
'enter_wallet_password',
);
await tester.pumpAndSettle();
await commonTestCases.tapItemByKey(
'unlock',
);
await tester.pumpAndSettle();
await commonTestCases.defaultSleepTime();
}
Future<void> enterPinCode(List<int> pinCode, {int pumpDuration = 100}) async {
for (int pin in pinCode) {
await commonTestCases.tapItemByKey(
'pin_code_button_${pin}_key',
pumpDuration: pumpDuration,
);
}
await commonTestCases.takeScreenshots('pin_code_widget');
await commonTestCases.defaultSleepTime();
await settle();
}
}
@@ -1,21 +1,20 @@
import 'package:hash_wallet/src/screens/seed/pre_seed_page.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/new-ui/pages/seed/pre_seed_page.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class PreSeedPageRobot {
PreSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class PreSeedPageRobot extends BaseRobot {
PreSeedPageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isPreSeedPage() async {
await commonTestCases.isSpecificPage<PreSeedPage>();
await commonTestCases.takeScreenshots('pre_seed_page');
@override
Future<void> isDisplayed() async {
await isSpecificPage<PreSeedPage>();
}
// The button is only built once all three boxes are ticked
Future<void> onConfirmButtonPressed() async {
await commonTestCases.tapItemByKey('pre_seed_page_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("pre_seed_page_only_way_checkbox_key");
await tapByKey("pre_seed_page_write_down_checkbox_key");
await tapByKey("pre_seed_page_never_share_checkbox_key");
await tapByKey("pre_seed_page_button_key");
}
}
@@ -1,123 +1,101 @@
import 'package:hash_wallet/entities/seed_type.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/restore/wallet_restore_page.dart';
import 'package:hash_wallet/src/widgets/validable_annotated_editable_text.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/entities/seed_type.dart";
import "package:hash_wallet/src/screens/restore/wallet_restore_page.dart";
import "package:hash_wallet/src/widgets/primary_button.dart";
import "package:hash_wallet/src/widgets/validable_annotated_editable_text.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class RestoreFromSeedOrKeysPageRobot {
RestoreFromSeedOrKeysPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class RestoreFromSeedOrKeysPageRobot extends BaseRobot {
RestoreFromSeedOrKeysPageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isRestoreFromSeedKeyPage() async {
await commonTestCases.isSpecificPage<WalletRestorePage>();
await commonTestCases.takeScreenshots('wallet_restore_page');
}
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',
);
@override
Future<void> isDisplayed() async {
await isSpecificPage<WalletRestorePage>();
}
Future<void> selectWalletNameFromAvailableOptions({bool isSeedFormEntry = true}) async {
await commonTestCases.tapItemByKey(
await tapByKey(
'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));
final ValidatableAnnotatedEditableTextState seedTextState =
tester.state(find.byType(ValidatableAnnotatedEditableText));
seedTextState.widget.controller.text = text;
await tester.pumpAndSettle();
await settle();
}
Future<void> enterPasswordForWalletRestore(String text) async {
await commonTestCases.enterText(
text,
'password',
);
await tester.pumpAndSettle();
await enterTextByKey("password", text);
await settle();
}
Future<void> enterPasswordRepeatForWalletRestore(String text) async {
await commonTestCases.enterText(
text,
'repeat_wallet_password',
);
await tester.pumpAndSettle();
await enterTextByKey("repeat_wallet_password", text);
await settle();
}
Future<void> enterBlockHeightForWalletRestore(String blockHeight) async {
await commonTestCases.enterText(
blockHeight,
'wallet_restore_from_seed_blockheight_textfield_key',
);
await tester.pumpAndSettle();
await enterTextByKey("wallet_restore_from_seed_blockheight_textfield_key", blockHeight);
await settle();
}
Future<void> chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType selectedType) async {
await commonTestCases.tapItemByKey('wallet_restore_from_seed_seedtype_picker_button_key');
await tapByKey("wallet_restore_from_seed_seedtype_picker_button_key");
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${selectedType.title}_button_key');
await tapByKey("picker_items_index_${selectedType.title}_button_key");
}
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',
Future<void> expectRestoreRefused({Duration window = const Duration(seconds: 10)}) async {
final button = tester.widget<LoadingPrimaryButton>(
find.byKey(const ValueKey("wallet_restore_seed_or_key_restore_button_key")),
);
await tester.pumpAndSettle();
expect(
button.isDisabled,
true,
reason: "The restore button accepted a seed the wallet cannot parse",
);
final left = await pumpUntil(
() => !tester.any(find.byType(WalletRestorePage)),
timeout: window,
);
expect(left, false, reason: "A seed the wallet cannot parse still restored something");
}
Future<void> onRestoreWalletButtonPressed() async {
await commonTestCases.tapItemByKey('wallet_restore_seed_or_key_restore_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("wallet_restore_seed_or_key_restore_button_key");
}
Future<void> enablePassphrase() async {
await tapByKey("wallet_restore_has_passphrase_checkbox_key");
await settle();
final page = tester.widget<WalletRestorePage>(find.byType(WalletRestorePage));
expect(
page.walletRestoreViewModel.hasPassphrase,
true,
reason: "The passphrase box did not tick, the wallet would restore without it",
);
}
Future<void> enterPassphrase(String passphrase) async {
await enterTextByKey(
"add_passphrase_bottom_sheet_widget_passphrase_textfield_key",
passphrase,
);
await enterTextByKey(
"add_passphrase_bottom_sheet_widget_confirm_passphrase_textfield_key",
passphrase,
);
await tapByKey("add_passphrase_bottom_sheet_widget_restore_button_key");
}
}
@@ -1,43 +1,16 @@
import 'package:hash_wallet/src/screens/restore/restore_options_page.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/restore/restore_options_page.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class RestoreOptionsPageRobot {
RestoreOptionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class RestoreOptionsPageRobot extends BaseRobot {
RestoreOptionsPageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isRestoreOptionsPage() async {
await commonTestCases.isSpecificPage<RestoreOptionsPage>();
await commonTestCases.takeScreenshots('restore_options');
}
void hasRestoreOptionsButton() {
commonTestCases.hasValueKey('restore_options_from_seeds_or_keys_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');
@override
Future<void> isDisplayed() async {
await isSpecificPage<RestoreOptionsPage>();
}
Future<void> navigateToRestoreFromSeedsOrKeysPage() async {
await commonTestCases.tapItemByKey('restore_options_from_seeds_or_keys_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();
await tapByKey("restore_options_from_seeds_or_keys_button_key");
}
}
@@ -1,28 +0,0 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/settings/security_backup_page.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class SecurityAndBackupPageRobot {
SecurityAndBackupPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
final CommonTestCases commonTestCases;
Future<void> isSecurityAndBackupPage() async {
await commonTestCases.isSpecificPage<SecurityBackupPage>();
await commonTestCases.takeScreenshots('security_backup_page');
}
void hasTitle() {
commonTestCases.hasText(S.current.security_and_backup);
}
Future<void> navigateToShowKeysPage() async {
await tester.pumpAndSettle(Duration(milliseconds: 1000));
await commonTestCases.tapItemByKey('security_backup_page_show_keys_button_key');
await tester.pumpAndSettle(Duration(milliseconds: 1000));
}
}
@@ -0,0 +1,16 @@
import "package:hash_wallet/src/screens/settings/security_backup_page.dart";
import "../core/base_robot.dart";
class SecurityBackupPageRobot extends BaseRobot {
SecurityBackupPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await isSpecificPage<SecurityBackupPage>();
}
Future<void> openChangePin() async {
await tapByKey("security_backup_page_change_pin_button_key");
}
}
@@ -1,22 +1,19 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/seed/seed_verification/seed_verification_page.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/src/screens/seed/seed_verification/seed_verification_page.dart";
import "package:flutter_test/flutter_test.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class SeedVerificationPageRobot {
SeedVerificationPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class SeedVerificationPageRobot extends BaseRobot {
SeedVerificationPageRobot(super.tester);
final WidgetTester tester;
final CommonTestCases commonTestCases;
Future<void> isSeedVerificationPage() async {
await commonTestCases.isSpecificPage<SeedVerificationPage>();
await commonTestCases.takeScreenshots('seed_verification_page');
@override
Future<void> isDisplayed() async {
await isSpecificPage<SeedVerificationPage>();
}
void hasTitle() {
commonTestCases.hasText(S.current.verify_seed);
hasText(S.current.verify_seed);
}
Future<void> verifyWalletSeeds() async {
@@ -28,18 +25,23 @@ class SeedVerificationPageRobot {
while (!walletSeedViewModel.isVerificationComplete &&
walletSeedViewModel.verificationWordCount != 0) {
final currentCorrectWord = walletSeedViewModel.currentCorrectWord;
final currentStep = walletSeedViewModel.currentStepIndex;
commonTestCases.hasTextAtLestOnce(currentCorrectWord);
hasTextAtLeastOnce(currentCorrectWord);
await commonTestCases.tapItemByKey(
'seed_verification_option_${currentCorrectWord}_button_key',
await tapByKey("seed_verification_option_${currentCorrectWord}_button_key");
final hasMovedOn = await pumpUntil(
() =>
walletSeedViewModel.isVerificationComplete ||
walletSeedViewModel.currentStepIndex != currentStep,
);
await commonTestCases.defaultSleepTime(seconds: 1);
if (!hasMovedOn) {
throw TestFailure("Seed verification never moved past step $currentStep");
}
}
await commonTestCases.tapItemByKey('wallet_seed_page_open_wallet_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("wallet_seed_page_open_wallet_button_key");
}
}
@@ -1,716 +0,0 @@
import 'dart:async';
import 'package:hash_wallet/core/execution_state.dart';
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/send/send_page.dart';
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
import 'package:hash_wallet/src/widgets/primary_button.dart';
import 'package:hash_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/material.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';
import 'package:hash_wallet/src/widgets/standard_slide_button_widget.dart';
import 'package:hash_wallet/src/screens/dashboard/dashboard_page.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>();
await commonTestCases.takeScreenshots('send_page');
}
Future<void> waitForSendPage() async {
tester.printToConsole('Waiting for SendPage to be available');
final stopwatch = Stopwatch()..start();
final maxDuration = Duration(seconds: 30);
while (stopwatch.elapsed < maxDuration) {
await tester.pump(Duration(milliseconds: 500));
// Check if we're still on auth page
if (authPageRobot.onAuthPage()) {
tester.printToConsole('Still on auth page, waiting...');
continue;
}
// Check if SendPage is available
final sendPageFinder = find.byType(SendPage);
if (sendPageFinder.tryEvaluate()) {
tester.printToConsole('SendPage found!');
return;
}
tester.printToConsole('SendPage not found yet, waiting...');
}
throw Exception('SendPage not found after ${maxDuration.inSeconds} seconds');
}
Future<void> checkIfSendPageIsVisible() async {
tester.printToConsole('Confirming SendPage is visible');
if (authPageRobot.onAuthPage()) {
tester.printToConsole('Auth page is currently visible');
await _handleAuthPage();
await commonTestCases.defaultSleepTime();
}
await waitForSendPage();
}
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.feesViewModel.hasFees) {
commonTestCases.hasValueKey('send_page_select_fee_priority_button_key');
}
if (sendViewModel.hasCoinControl) {
commonTestCases.hasValueKey('send_page_unspent_coin_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);
await commonTestCases.defaultSleepTime();
// Check if picker dialog is present
if (!commonTestCases.isKeyPresent(currencyPickerDialogKey)) {
tester.printToConsole('Currency picker dialog not found, may already be selected');
return;
}
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.enterText(receiveCurrency.title, 'search_bar_widget_key');
await commonTestCases.defaultSleepTime();
await commonTestCases.tapItemByKey('picker_items_index_${receiveCurrency.fullName}_button_key');
}
Future<void> enterReceiveAddress(String receiveAddress) async {
await commonTestCases.enterText(receiveAddress, 'send_page_address_textfield_key');
await commonTestCases.defaultSleepTime();
}
Future<void> enterSendAmount(String amount, {bool isFiat = false}) async {
await commonTestCases.enterText(
amount,
isFiat ? 'send_page_fiat_amount_textfield_key' : 'send_page_amount_textfield_key',
);
}
String _getTextFromField(ValueKey<String> key) {
final field = find.byKey(key);
if (!field.tryEvaluate()) return '';
final baseTextFormField = tester.widget<BaseTextFormField>(field);
return baseTextFormField.controller?.text ?? '';
}
/// Validates wallet balance for $1 send by checking against the converted crypto amount
/// Returns true if wallet has sufficient balance, false otherwise
Future<bool> validateWalletBalanceForOneDollarSend() async {
SendPage sendPage = tester.widget(find.byType(SendPage));
final sendViewModel = sendPage.sendViewModel;
final balance = await sendViewModel.sendingBalance;
await setupOneDollarSend();
// Get the crypto amount that was set for the $1 send
String cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
if (cryptoAmount.isEmpty || cryptoAmount == '0' || cryptoAmount == '0.0') {
cryptoAmount = '0.001'; // fallback amount
}
final amount = double.tryParse(cryptoAmount) ?? 0.0;
tester.printToConsole(
'Wallet balance: $balance, sending amount for \$${CommonTestConstants.sendTestFiatAmount}: $cryptoAmount',
);
if (balance.isEmpty || double.tryParse(balance) == null) {
tester.printToConsole('Invalid wallet balance: $balance');
return false;
}
final balanceValue = double.parse(balance);
if (balanceValue < amount) {
tester.printToConsole(
'Insufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send: $balanceValue < $amount',
);
return false;
}
tester.printToConsole(
'Wallet has sufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send',
);
return true;
}
Future<void> selectTransactionPriority({TransactionPriority? priority}) async {
SendPage sendPage = tester.widget(find.byType(SendPage));
final sendViewModel = sendPage.sendViewModel;
if (!sendViewModel.feesViewModel.hasFees || priority == null) return;
final transactionPriorityPickerKey = 'send_page_select_fee_priority_button_key';
await commonTestCases.tapItemByKey(transactionPriorityPickerKey);
if (priority == sendViewModel.feesViewModel.transactionPriority) {
await commonTestCases
.tapItemByKey('picker_items_index_${priority.title}_selected_item_button_key');
return;
}
await commonTestCases.dragUntilVisible(
'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 checkIfSendPageIsVisible();
await tester.pumpAndSettle();
final sendPage = tester.widget<SendPage>(find.byType(SendPage));
while (true) {
bool isReadyForSend = sendPage.sendViewModel.isReadyForSend;
await tester.pump();
if (isReadyForSend) {
tester.printToConsole('Is ready for send');
break;
} else {
await commonTestCases.defaultSleepTime();
await tester.pumpAndSettle();
tester.printToConsole('not yet ready for 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();
await commonTestCases.defaultSleepTime();
tester.printToConsole('After _handleAuth');
await tester.pump();
if (authPageRobot.onAuthPage()) {
tester.printToConsole('Still on auth page, continuing to wait');
continue;
}
final sendPageFinder = find.byType(SendPage);
if (!sendPageFinder.tryEvaluate()) {
tester.printToConsole('SendPage not found yet, continuing to wait');
continue;
}
final sendPage = tester.widget<SendPage>(sendPageFinder);
final state = sendPage.sendViewModel.state;
await tester.pump();
bool isDone = state is ExecutedSuccessfullyState || 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 confirming sending operation');
}
Future<void> _handleAuthPage() async {
tester.printToConsole('Inside _handleAuth');
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
if (onAuthPageDesktop) {
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
await commonTestCases.defaultSleepTime();
return;
}
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, pumpDuration: 500);
tester.printToConsole('Auth done');
await tester.pump(Duration(seconds: 3));
tester.printToConsole('Auth pump done');
await commonTestCases.defaultSleepTime();
} catch (e) {
tester.printToConsole('Auth failed, retrying: $e');
await tester.pump();
await commonTestCases.defaultSleepTime(seconds: 1);
await _handleAuthPage();
}
} else {
tester.printToConsole('No auth page detected, proceeding');
}
await tester.pump();
}
//* ------ 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> onSendSliderOnConfirmSendingBottomSheetDragged() async {
await commonTestCases.defaultSleepTime();
await tester.pump();
if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
tester.printToConsole('Found confirm sending bottom sheet, starting slider drag');
final accessibleButton = find.byType(PrimaryButton);
if (accessibleButton.tryEvaluate()) {
tester.printToConsole('Found accessible navigation button, tapping it');
await tester.tap(accessibleButton);
await tester.pumpAndSettle();
} else {
await _performSendSliderDrag();
}
tester.printToConsole('Slider/button action completed, waiting for transaction completion');
// Check if bottom sheet is dismissed
if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
tester.printToConsole('Bottom sheet still present, waiting a bit more for dismissal');
await commonTestCases.defaultSleepTime(seconds: 3);
// If still present, try one more manual dismissal attempt
if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
tester.printToConsole('Bottom sheet still present, attempting final manual dismissal');
await tester.tapAt(Offset(200, 200)); // Tap outside
await tester.pumpAndSettle();
}
}
// Wait for transaction completion
await _waitForCommitTransactionCompletion();
await commonTestCases.defaultSleepTime(seconds: 2);
} else {
tester.printToConsole('Confirm sending bottom sheet not found, waiting and retrying');
await commonTestCases.defaultSleepTime();
await tester.pump();
await onSendSliderOnConfirmSendingBottomSheetDragged();
}
}
Future<void> _performSendSliderDrag() async {
final sliderFinder = find.byKey(const ValueKey('standard_slide_button_widget_slider_key'));
expect(sliderFinder, findsOneWidget, reason: 'Slider should be found');
// Find the StandardSlideButton widget to get the main container
final slideButtonFinder = find.byType(StandardSlideButton);
expect(slideButtonFinder, findsOneWidget, reason: 'StandardSlideButton should be found');
// Get the main container bounds (the outer container, not the slider container)
final mainContainerRect = tester.getRect(slideButtonFinder);
final containerWidth = mainContainerRect.width;
final sideMargin = 4.0;
final sliderWidth = 42.0;
final effectiveMaxWidth = containerWidth - 2 * sideMargin;
final threshold = effectiveMaxWidth - sliderWidth - 10;
// Add a small buffer to ensure we exceed the threshold
final dragDistance = threshold + 20;
tester.printToConsole(
'Main container width: $containerWidth, Threshold: $threshold, Drag distance: $dragDistance',
);
// Start the drag
await tester.drag(sliderFinder, Offset(dragDistance, 0));
// Wait for the drag to complete and trigger onHorizontalDragEnd
await tester.pump(Duration(milliseconds: 100));
// Release the drag (this should trigger onHorizontalDragEnd)
await tester.pump(Duration(seconds: 2));
tester.printToConsole('Drag completed, waiting for callback');
// Wait for the slide completion callback to trigger and bottom sheet to dismiss
await _waitForBottomSheetDismissal();
}
Future<void> _waitForBottomSheetDismissal() async {
tester.printToConsole('Waiting for bottom sheet dismissal');
final stopwatch = Stopwatch()..start();
final maxDuration = Duration(seconds: 10);
while (stopwatch.elapsed < maxDuration) {
await tester.pump(Duration(milliseconds: 500));
// Check if the confirm bottom sheet is still present
if (!commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
tester.printToConsole('Bottom sheet dismissed successfully!');
return;
}
// Check if transaction has started (this indicates slider was successful)
if (_isTransactionStarted()) {
tester.printToConsole(
'Transaction started, slider was successful even if bottom sheet is still visible');
return;
}
tester.printToConsole('Bottom sheet still present, waiting');
}
tester.printToConsole('Bottom sheet dismissal timeout reached, trying manual dismissal');
// If the bottom sheet is still present after timeout, try to manually dismiss it
// This might happen if the slider drag didn't trigger the onSlideComplete properly
if (commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
tester.printToConsole('Attempting manual bottom sheet dismissal');
// Try to find and tap a close button or back button
final closeButton = find.byIcon(Icons.close);
if (closeButton.tryEvaluate()) {
await tester.tap(closeButton);
await tester.pumpAndSettle();
tester.printToConsole('Manual close button tapped');
} else {
// Try to pop the bottom sheet by tapping outside or using back gesture
await tester.tapAt(Offset(100, 100)); // Tap outside the bottom sheet
await tester.pumpAndSettle();
tester.printToConsole('Tapped outside bottom sheet');
}
}
}
bool _isTransactionStarted() {
try {
// Check if SendPage is available and transaction state has changed
final sendPageFinder = find.byType(SendPage);
if (!sendPageFinder.tryEvaluate()) {
return false;
}
final sendPage = tester.widget<SendPage>(sendPageFinder);
final state = sendPage.sendViewModel.state;
// Check if we're in a transaction-related state
return state is TransactionCommitting ||
state is IsExecutingState ||
state is TransactionCommitted ||
state is ExecutedSuccessfullyState;
} catch (e) {
return false;
}
}
Future<void> _waitForCommitTransactionCompletion() async {
tester.printToConsole('Starting to wait for transaction completion');
final stopwatch = Stopwatch()..start();
final maxDuration = Duration(seconds: 60);
while (stopwatch.elapsed < maxDuration) {
await Future.delayed(Duration(seconds: 1));
// Check if the confirm bottom sheet is gone (transaction started)
if (!commonTestCases.isKeyPresent('send_page_confirm_sending_bottom_sheet_key')) {
tester.printToConsole('Confirm bottom sheet disappeared, transaction may be processing');
}
// Check if we've navigated back to the dashboard (for cases of successful transaction)
final dashboardFinder = find.byType(DashboardPage);
if (dashboardFinder.tryEvaluate()) {
tester.printToConsole('Dashboard detected, transaction completed successfully!');
await tester.pump();
return;
}
// Check if SendPage is still available (transaction still processing)
final sendPageFinder = find.byType(SendPage);
if (sendPageFinder.tryEvaluate()) {
final sendPage = tester.widget<SendPage>(sendPageFinder);
final state = sendPage.sendViewModel.state;
bool isDone = state is ExecutedSuccessfullyState || state is TransactionCommitted;
bool isFailed = state is FailureState;
tester.printToConsole('Transaction state: $state');
tester.printToConsole('isDone: $isDone');
tester.printToConsole('isFailed: $isFailed');
if (isDone) {
tester.printToConsole('Transaction committed successfully!');
await tester.pump();
return;
} else if (isFailed) {
tester.printToConsole('Transaction failed: $state');
await tester.pump();
return;
} else {
tester.printToConsole('Transaction still processing');
await tester.pump();
}
} else {
// SendPage not found, check if we're on dashboard or still processing
if (dashboardFinder.tryEvaluate()) {
tester.printToConsole('Dashboard detected - transaction completed successfully!');
await tester.pump();
return;
} else {
tester.printToConsole(
'SendPage not found, but dashboard not yet visible - continuing to wait...');
}
}
}
if (stopwatch.elapsed >= maxDuration) {
tester.printToConsole('Transaction completion timeout reached');
}
tester.printToConsole('Done waiting for transaction completion');
}
//* ---- Handle Transaction Success Flow -----
Future<void> handleTransactionSuccessFlow() async {
await commonTestCases.defaultSleepTime();
// Wait for any success dialogs to appear
await tester.pump(Duration(seconds: 2));
// Check for contact addition dialog first (if new contact address exists)
final contactDialog = find.byKey(ValueKey('send_page_add_contact_bottom_sheet_yes_button_key'));
if (contactDialog.tryEvaluate()) {
tester.printToConsole('Found contact addition dialog, selecting Yes');
// Check if the button is actually visible and tappable
final buttonRect = tester.getRect(contactDialog);
final screenSize = tester.view.physicalSize / tester.view.devicePixelRatio;
if (buttonRect.bottom <= screenSize.height && buttonRect.top >= 0) {
await commonTestCases.tapItemByKey('send_page_add_contact_bottom_sheet_yes_button_key');
await commonTestCases.defaultSleepTime();
} else {
tester.printToConsole('Contact dialog button is off-screen, skipping');
}
}
// Check for the main success dialog
final successDialog = find.byKey(ValueKey('send_page_sent_dialog_ok_button_key'));
if (successDialog.tryEvaluate()) {
tester.printToConsole('Found transaction success dialog, closing it');
await commonTestCases.tapItemByKey('send_page_sent_dialog_ok_button_key');
await commonTestCases.defaultSleepTime();
}
}
//* ---- Fiat/Crypto Amount Validation -----
Future<void> testFiatAmountEntry() async {
tester.printToConsole('Testing fiat amount entry...');
await enterSendAmount('');
await enterSendAmount('', isFiat: true);
await commonTestCases.defaultSleepTime();
await enterSendAmount(CommonTestConstants.sendTestFiatAmount, isFiat: true);
await commonTestCases.defaultSleepTime();
// Wait for conversion to complete
await tester.pump(Duration(seconds: 3));
await tester.pumpAndSettle();
// Next we get the crypto amount value and validate it's not 0
final cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
tester.printToConsole(
'Crypto amount after entering \$${CommonTestConstants.sendTestFiatAmount}: $cryptoAmount',
);
if (cryptoAmount.isNotEmpty && cryptoAmount != '0' && cryptoAmount != '0.0') {
tester.printToConsole('Fiat to crypto conversion working - crypto amount: $cryptoAmount');
} else {
tester.printToConsole(
'Fiat to crypto conversion may not be working - crypto amount: $cryptoAmount',
);
}
}
Future<void> testCryptoAmountEntry() async {
tester.printToConsole('Testing crypto amount entry...');
await enterSendAmount('');
await enterSendAmount('', isFiat: true);
await commonTestCases.defaultSleepTime();
String cryptoAmount = '0.001';
await enterSendAmount(cryptoAmount);
await commonTestCases.defaultSleepTime();
// Wait for conversion to complete
await tester.pump(Duration(seconds: 3));
await tester.pumpAndSettle();
// Get the fiat amount value and validate it's not 0
final fiatAmount = _getTextFromField(ValueKey('send_page_fiat_amount_textfield_key'));
tester.printToConsole('Fiat amount after entering $cryptoAmount: $fiatAmount');
if (fiatAmount.isNotEmpty && fiatAmount != '0' && fiatAmount != '0.0') {
tester.printToConsole('Crypto to fiat conversion working, fiat amount: $fiatAmount');
} else {
tester.printToConsole(
'Crypto to fiat conversion may not be working - fiat amount: $fiatAmount');
}
}
Future<void> setupOneDollarSend() async {
// Clear existing amounts
await enterSendAmount('');
await enterSendAmount('', isFiat: true);
await commonTestCases.defaultSleepTime();
await enterSendAmount(CommonTestConstants.sendTestFiatAmount, isFiat: true);
await commonTestCases.defaultSleepTime();
// Wait for conversion to complete
await tester.pump(Duration(seconds: 3));
await tester.pumpAndSettle();
// Get the converted crypto amount
String cryptoAmount = _getTextFromField(ValueKey('send_page_amount_textfield_key'));
if (cryptoAmount.isEmpty || cryptoAmount == '0' || cryptoAmount == '0.0') {
cryptoAmount = '0.001'; // fallback amount
}
tester.printToConsole(
'Sending $cryptoAmount (equivalent to \$${CommonTestConstants.sendTestFiatAmount}) to test wallet',
);
// Update the amount field with the converted value
await enterSendAmount(cryptoAmount);
await commonTestCases.defaultSleepTime();
}
}
@@ -1,29 +1,16 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/setup_pin_code/setup_pin_code.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/setup_pin_code/setup_pin_code.dart";
import '../components/common_test_cases.dart';
import 'pin_code_widget_robot.dart';
import "pin_code_widget_robot.dart";
class SetupPinCodeRobot extends PinCodeWidgetRobot {
SetupPinCodeRobot(this.tester)
: commonTestCases = CommonTestCases(tester),
super(tester);
SetupPinCodeRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isSetupPinCodePage() async {
await commonTestCases.isSpecificPage<SetupPinCodePage>();
await commonTestCases.takeScreenshots('setup_pin_code_page');
}
void hasTitle() {
commonTestCases.hasText(S.current.setup_pin);
@override
Future<void> isDisplayed() async {
await isSpecificPage<SetupPinCodePage>();
}
Future<void> tapSuccessButton() async {
await commonTestCases.tapItemByKey('setup_pin_code_success_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("setup_pin_code_success_button_key");
}
}
@@ -0,0 +1,16 @@
import "package:hash_wallet/new-ui/pages/seed/show_keys_disclaimer_page.dart";
import "../core/base_robot.dart";
class ShowKeysDisclaimerPageRobot extends BaseRobot {
ShowKeysDisclaimerPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await isSpecificPage<ShowKeysDisclaimerPage>();
}
Future<void> tapShowKeys() async {
await tapByKey("show_keys_disclaimer_page_button_key");
}
}
@@ -1,21 +0,0 @@
import 'package:hash_wallet/src/screens/send/transaction_success_info_page.dart';
import 'package:flutter_test/flutter_test.dart';
import '../components/common_test_cases.dart';
class TransactionSuccessInfoRobot {
TransactionSuccessInfoRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isTransactionSuccessInfoPage() async {
await commonTestCases.isSpecificPage<TransactionSuccessPage>();
await commonTestCases.takeScreenshots('transaction_success_info_page');
}
Future<void> onConfirmButtonPressed() async {
await commonTestCases.tapItemByKey('transaction_success_info_page_button_key');
await commonTestCases.defaultSleepTime();
}
}
@@ -1,439 +0,0 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/dashboard/pages/transactions_page.dart';
import 'package:hash_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
import 'package:hash_wallet/view_model/dashboard/order_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/trade_list_item.dart';
import 'package:hash_wallet/view_model/dashboard/transaction_list_item.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/sync_status.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:intl/intl.dart';
import '../components/common_test_cases.dart';
class TransactionsPageRobot {
TransactionsPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isTransactionsPage() async {
await commonTestCases.isSpecificPage<TransactionsPage>();
await commonTestCases.takeScreenshots('transactions_page');
}
Future<void> confirmTransactionsPageConstantsDisplayProperly() async {
await commonTestCases.defaultSleepTime();
final transactionsPage = tester.widget<TransactionsPage>(find.byType(TransactionsPage));
final dashboardViewModel = transactionsPage.dashboardViewModel;
if (dashboardViewModel.status is SyncingSyncStatus) {
commonTestCases.hasValueKey('transactions_page_syncing_alert_card_key');
commonTestCases.hasText(S.current.syncing_wallet_alert_title);
commonTestCases.hasText(S.current.syncing_wallet_alert_content);
}
commonTestCases.hasValueKey('transactions_page_header_row_key');
commonTestCases.hasText(S.current.transactions);
commonTestCases.hasValueKey('transactions_page_header_row_transaction_filter_button_key');
}
Future<void> confirmTransactionHistoryListDisplaysCorrectly(bool hasTxHistoryWhileSyncing) async {
try {
final transactionsPage = tester.widget<TransactionsPage>(find.byType(TransactionsPage));
final dashboardViewModel = transactionsPage.dashboardViewModel;
await _waitForListToBeReady();
if (!hasTxHistoryWhileSyncing) {
await _waitForSyncToComplete(dashboardViewModel);
} else {
await tester.pump(Duration(seconds: 2));
await tester.pumpAndSettle();
}
await _performComprehensiveItemCheck(dashboardViewModel);
} catch (e) {
tester.printToConsole('Error in transaction history check: $e');
}
}
Future<void> _waitForSyncToComplete(DashboardViewModel dashboardViewModel) async {
const maxWaitTime = Duration(minutes: 2);
final endTime = DateTime.now().add(maxWaitTime);
tester.printToConsole('Waiting for wallet to sync...');
while (DateTime.now().isBefore(endTime)) {
await tester.pump(Duration(seconds: 2));
await tester.pumpAndSettle();
if (dashboardViewModel.status is SyncedSyncStatus) {
tester.printToConsole('Wallet synced successfully');
return;
}
// Check if we have now have transaction items available
if (dashboardViewModel.items.isNotEmpty) {
tester.printToConsole('Items available while syncing, proceeding with test');
return;
}
tester.printToConsole('Sync status: ${dashboardViewModel.status.runtimeType}');
}
tester.printToConsole('Warning: Wallet did not sync within expected time, proceeding anyway');
}
Future<void> _performComprehensiveItemCheck(DashboardViewModel dashboardViewModel) async {
try {
await _waitForItemsToLoad(dashboardViewModel);
final itemsToProcess = dashboardViewModel.items.where((item) {
if (item is DateSectionItem) return false;
if (item is TransactionListItem) {
return !(item.hasTokens && item.assetOfTransaction == null);
}
return true;
}).toList();
if (itemsToProcess.isEmpty) {
tester.printToConsole('No transaction items to process - checking for placeholder');
_verifyPlaceholder();
return;
}
// This is a temporary limit to prevent the test from taking too long
final maxItemsToProcess = 100;
final itemsToCheck = itemsToProcess.take(maxItemsToProcess).toList();
tester.printToConsole(
'Processing ${itemsToCheck.length} items out of ${itemsToProcess.length} total items',
);
await _processVisibleItems(itemsToCheck, dashboardViewModel);
// Try to scroll and process more items if needed
await _processItemsWithScrolling(itemsToCheck, dashboardViewModel);
} catch (e) {
tester.printToConsole('Error in comprehensive item check: $e');
try {
_verifyPlaceholder();
} catch (placeholderError) {
tester.printToConsole('Could not verify placeholder either: $placeholderError');
}
}
}
Future<void> _waitForItemsToLoad(DashboardViewModel dashboardViewModel) async {
const maxWaitTime = Duration(seconds: 30);
final endTime = DateTime.now().add(maxWaitTime);
while (DateTime.now().isBefore(endTime)) {
if (dashboardViewModel.items.isNotEmpty) {
tester.printToConsole('Items loaded: ${dashboardViewModel.items.length}');
return;
}
// Check if placeholder is shown
if (tester.any(find.byKey(ValueKey('transactions_page_placeholder_transactions_text_key')))) {
tester.printToConsole('Placeholder is shown - no items to load');
return;
}
await tester.pump(Duration(seconds: 1));
await tester.pumpAndSettle();
}
tester.printToConsole('Warning: No items loaded and no placeholder shown within expected time');
}
Future<void> _processVisibleItems(
List<dynamic> items,
DashboardViewModel dashboardViewModel,
) async {
try {
int processedCount = 0;
for (var item in items) {
try {
final keyId = (item.key as ValueKey<String>).value;
// Check if item is already visible
if (tester.any(find.byKey(ValueKey(keyId)))) {
tester.printToConsole('Processing visible item: $keyId');
await _verifyItemDisplay(item, dashboardViewModel);
processedCount++;
}
} catch (itemError) {
tester.printToConsole('Error processing item: $itemError');
continue;
}
}
tester.printToConsole('Processed $processedCount visible items\n');
} catch (e) {
tester.printToConsole('Error in visible items processing: $e');
}
}
Future<void> _processItemsWithScrolling(
List<dynamic> items,
DashboardViewModel dashboardViewModel,
) async {
try {
final scrollableFinder = find.descendant(
of: find.byKey(ValueKey('transactions_page_list_view_builder_key')),
matching: find.byType(Scrollable),
);
if (!tester.any(scrollableFinder)) {
tester.printToConsole('No scrollable found, skipping scroll processing');
return;
}
int processedCount = 0;
const maxScrollAttempts = 10;
for (var item in items) {
try {
final keyId = (item.key as ValueKey<String>).value;
// Skip if already processed
if (tester.any(find.byKey(ValueKey(keyId)))) {
continue;
}
// Try to scroll to the item
bool found = false;
for (int attempt = 0; attempt < maxScrollAttempts; attempt++) {
await tester.pumpAndSettle(Duration(milliseconds: 200));
if (tester.any(find.byKey(ValueKey(keyId)))) {
tester.printToConsole('Found item after scrolling: $keyId');
await _verifyItemDisplay(item, dashboardViewModel);
processedCount++;
found = true;
break;
}
// Perform scroll
try {
await tester.drag(scrollableFinder, Offset(0, -100));
await tester.pumpAndSettle(Duration(milliseconds: 300));
} catch (scrollError) {
tester.printToConsole('Scroll failed: $scrollError');
break;
}
}
if (!found) {
tester.printToConsole('Could not find item after scrolling: $keyId');
}
} catch (itemError) {
tester.printToConsole('Error processing item with scrolling: $itemError');
continue;
}
}
tester.printToConsole('Processed $processedCount items with scrolling');
} catch (e) {
tester.printToConsole('Error in scroll processing: $e');
}
}
void _verifyPlaceholder() {
commonTestCases.hasValueKey('transactions_page_placeholder_transactions_text_key');
commonTestCases.hasText(S.current.placeholder_transactions);
}
Future<void> _waitForListToBeReady() async {
const maxWaitAttempts = 10;
int attempts = 0;
while (attempts < maxWaitAttempts) {
await tester.pumpAndSettle(Duration(milliseconds: 500));
// Check if the list view is present
final listViewFinder = find.byKey(ValueKey('transactions_page_list_view_builder_key'));
if (tester.any(listViewFinder)) {
tester.printToConsole('List view is ready');
return;
}
attempts++;
tester.printToConsole('Waiting for list view to be ready, attempt $attempts\n');
}
tester.printToConsole('List view did not become ready within expected time');
}
Future<void> _verifyItemDisplay(dynamic item, DashboardViewModel dashboardViewModel) async {
// Execute the proper check depending on item type.
switch (item.runtimeType) {
case TransactionListItem:
final transactionItem = item as TransactionListItem;
tester.printToConsole(transactionItem.formattedTitle);
tester.printToConsole(transactionItem.formattedFiatAmount);
tester.printToConsole('\n');
await _verifyTransactionListItemDisplay(transactionItem, dashboardViewModel);
break;
case AnonpayTransactionListItem:
await _verifyAnonpayTransactionListItemDisplay(item as AnonpayTransactionListItem);
break;
case TradeListItem:
await _verifyTradeListItemDisplay(item as TradeListItem);
break;
case OrderListItem:
await _verifyOrderListItemDisplay(item as OrderListItem);
break;
default:
tester.printToConsole('Unhandled item type: ${item.runtimeType}');
}
}
Future<void> _verifyTransactionListItemDisplay(
TransactionListItem item,
DashboardViewModel dashboardViewModel,
) async {
final keyId =
'${dashboardViewModel.type.name}_transaction_history_item_${item.transaction.id}_key';
if (!tester.any(find.byKey(ValueKey(keyId)))) {
tester.printToConsole(
'Could not find transaction item with key: $keyId for transaction ${item.transaction.id}',
);
return;
}
if (item.hasTokens && item.assetOfTransaction == null) return;
try {
//* ==============Confirm it has the right key for this item ========
commonTestCases.hasValueKey(keyId);
//* ======Confirm it displays the properly formatted amount==========
commonTestCases.findWidgetViaDescendant(
of: find.byKey(ValueKey(keyId)),
matching: find.text(item.formattedCryptoAmount),
);
//* ======Confirm it displays the properly formatted date============
final formattedDate = DateFormat('HH:mm').format(item.transaction.date);
commonTestCases.findWidgetViaDescendant(
of: find.byKey(ValueKey(keyId)),
matching: find.text(formattedDate),
);
//* ======Confirm it displays the properly formatted fiat amount=====
final formattedFiatAmount =
dashboardViewModel.balanceViewModel.isFiatDisabled ? '' : item.formattedFiatAmount;
if (formattedFiatAmount.isNotEmpty) {
commonTestCases.findWidgetViaDescendant(
of: find.byKey(ValueKey(keyId)),
matching: find.text(formattedFiatAmount),
);
}
//* ======Confirm it displays the right image based on the transaction direction=====
final imageToUse = item.transaction.direction == TransactionDirection.incoming
? 'assets/images/down_arrow.png'
: 'assets/images/up_arrow.png';
find.widgetWithImage(Container, AssetImage(imageToUse));
} catch (e) {
tester.printToConsole('Error verifying transaction item ${item.transaction.id}: $e');
}
}
Future<void> _verifyAnonpayTransactionListItemDisplay(AnonpayTransactionListItem item) async {
final keyId = 'anonpay_invoice_transaction_list_item_${item.transaction.invoiceId}_key';
//* ==============Confirm it has the right key for this item ========
commonTestCases.hasValueKey(keyId);
//* ==============Confirm it displays the correct provider =========================
commonTestCases.hasText(item.transaction.provider);
//* ===========Confirm it displays the properly formatted amount with currency ========
final currency = item.transaction.fiatAmount != null
? item.transaction.fiatEquiv ?? ''
: CryptoCurrency.fromFullName(item.transaction.coinTo).name.toUpperCase();
final amount =
item.transaction.fiatAmount?.toString() ?? (item.transaction.amountTo?.toString() ?? '');
final amountCurrencyText = amount + ' ' + currency;
commonTestCases.hasText(amountCurrencyText);
//* ======Confirm it displays the properly formatted date=================
final formattedDate = DateFormat('HH:mm').format(item.transaction.createdAt);
commonTestCases.hasText(formattedDate);
//* ===============Confirm it displays the right image====================
find.widgetWithImage(ClipRRect, AssetImage('assets/images/trocador.png'));
}
Future<void> _verifyTradeListItemDisplay(TradeListItem item) async {
final keyId = 'trade_list_item_${item.trade.id}_key';
final from = item.trade.from?.toString() ?? '';
final to = item.trade.to?.toString() ?? '';
//* ==============Confirm it has the right key for this item ========
commonTestCases.hasValueKey(keyId);
//* ==============Confirm it displays the correct provider =========================
final conversionFlow = '$from → $to';
commonTestCases.hasText(conversionFlow);
//* ===========Confirm it displays the properly formatted amount with its crypto tag ========
final amountCryptoText = item.tradeFormattedAmount + ' ' + from;
commonTestCases.hasText(amountCryptoText);
//* ======Confirm it displays the properly formatted date=================
final createdAtFormattedDate =
item.trade.createdAt != null ? DateFormat('HH:mm').format(item.trade.createdAt!) : null;
if (createdAtFormattedDate != null) {
commonTestCases.hasText(createdAtFormattedDate);
}
//* ===============Confirm it displays the right image====================
commonTestCases.hasValueKey(item.trade.provider.image);
}
Future<void> _verifyOrderListItemDisplay(OrderListItem item) async {
final keyId = 'order_list_item_${item.order.id}_key';
//* ==============Confirm it has the right key for this item ========
commonTestCases.hasValueKey(keyId);
//* ==============Confirm it displays the correct provider =========================
final orderFlow = '${item.order.from!} → ${item.order.to}';
commonTestCases.hasText(orderFlow);
//* ===========Confirm it displays the properly formatted amount with its crypto tag ========
final amountCryptoText = item.orderFormattedAmount + ' ' + item.order.to!;
commonTestCases.hasText(amountCryptoText);
//* ======Confirm it displays the properly formatted date=================
final createdAtFormattedDate = DateFormat('HH:mm').format(item.order.createdAt);
commonTestCases.hasText(createdAtFormattedDate);
}
}
@@ -0,0 +1,50 @@
import "package:hash_wallet/src/screens/wallet/wallet_edit_page.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class WalletEditPageRobot extends BaseRobot {
WalletEditPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await isSpecificPage<WalletEditPage>();
}
Future<void> enterName(String name) async {
final input = find.byKey(const ValueKey("wallet_edit_page_name_input_key"));
await pumpUntilFound(input);
final field = find.descendant(of: input, matching: find.byType(EditableText));
await tester.enterText(field.first, name);
await tester.pump(const Duration(milliseconds: 300));
}
Future<void> save() async {
await tapByKey("wallet_edit_page_save_button_key");
}
Future<void> tapDelete() async {
await tapByKey("wallet_edit_page_delete_button_key");
}
Future<void> confirmDelete() async {
await tapByKey("wallet_edit_page_confirm_delete_button_key");
}
Future<void> expectNameTakenRefusal() async {
await pumpUntilFound(find.byKey(const ValueKey("wallet_edit_page_name_taken_ok_button_key")));
hasType<WalletEditPage>();
}
Future<void> dismissNameTakenRefusal() async {
await tapByKey("wallet_edit_page_name_taken_ok_button_key");
await settle();
}
}
@@ -1,39 +1,25 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/new_wallet/wallet_group_description_page.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/new_wallet/wallet_group_description_page.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class WalletGroupDescriptionPageRobot {
WalletGroupDescriptionPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class WalletGroupDescriptionPageRobot extends BaseRobot {
WalletGroupDescriptionPageRobot(super.tester);
final WidgetTester tester;
final CommonTestCases commonTestCases;
Future<void> isWalletGroupDescriptionPage() async {
await commonTestCases.isSpecificPage<WalletGroupDescriptionPage>();
await commonTestCases.takeScreenshots('wallet_group_description_page');
@override
Future<void> isDisplayed() async {
await isSpecificPage<WalletGroupDescriptionPage>();
}
void hasTitle() {
commonTestCases.hasText(S.current.wallet_group);
}
bool hasNewSingleSeedButton() {
return commonTestCases.isKeyPresent('wallet_group_description_page_create_new_seed_button_key');
}
bool hasNewSingleSeedButton() =>
isKeyPresent("wallet_group_description_page_create_new_seed_button_key");
Future<void> navigateToCreateNewSeedPage() async {
if (hasNewSingleSeedButton()) {
await commonTestCases.tapItemByKey(
'wallet_group_description_page_create_new_seed_button_key',
);
await tapByKey("wallet_group_description_page_create_new_seed_button_key");
}
}
Future<void> navigateToChooseWalletGroup() async {
await commonTestCases.tapItemByKey(
'wallet_group_description_page_choose_wallet_group_button_key',
);
await tapByKey("wallet_group_description_page_choose_wallet_group_button_key");
}
}
@@ -0,0 +1,16 @@
import "package:hash_wallet/src/screens/new_wallet/wallet_group_existing_seed_description_page.dart";
import "../core/base_robot.dart";
class WalletGroupExistingSeedPageRobot extends BaseRobot {
WalletGroupExistingSeedPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await isSpecificPage<WalletGroupExistingSeedDescriptionPage>();
}
Future<void> openWallet() async {
await tapByKey("wallet_group_existing_seed_description_page_open_wallet_button_key");
}
}
@@ -0,0 +1,21 @@
import "package:hash_wallet/src/screens/new_wallet/wallet_group_display_page.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/base_robot.dart";
class WalletGroupsDisplayPageRobot extends BaseRobot {
WalletGroupsDisplayPageRobot(super.tester);
@override
Future<void> isDisplayed() async {
await isSpecificPage<WalletGroupsDisplayPage>();
}
Future<void> selectWallet(String name) async {
await tapWhenVisible(find.text(name));
}
Future<void> continueWithSelectedSeed() async {
await tapByKey("wallet_group_display_page_next_button_key");
}
}
+114 -112
View File
@@ -1,48 +1,38 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/reactions/wallet_connect.dart';
import 'package:hash_wallet/src/screens/wallet_keys/wallet_keys_page.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:cw_core/monero_wallet_keys.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cw_monero/monero_wallet.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:polyseed/polyseed.dart';
import "package:hash_wallet/reactions/wallet_connect.dart";
import "package:hash_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/monero_wallet_keys.dart";
import "package:cw_core/wallet_type.dart";
import "package:cw_monero/monero_wallet.dart";
import "package:flutter_test/flutter_test.dart";
import "package:polyseed/polyseed.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class WalletKeysAndSeedPageRobot {
WalletKeysAndSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class WalletKeysAndSeedPageRobot extends BaseRobot {
WalletKeysAndSeedPageRobot(super.tester);
final WidgetTester tester;
final CommonTestCases commonTestCases;
Future<void> isWalletKeysAndSeedPage() async {
await commonTestCases.isSpecificPage<WalletKeysPage>();
await commonTestCases.takeScreenshots('wallet_keys_page');
@override
Future<void> isDisplayed() async {
await isSpecificPage<WalletKeysPage>();
}
void hasTitle() {
final walletKeysPage = tester.widget<WalletKeysPage>(find.byType(WalletKeysPage));
final walletKeysViewModel = walletKeysPage.walletKeysViewModel;
commonTestCases.hasText(walletKeysViewModel.title);
}
void hasShareWarning() {
commonTestCases.hasText(S.current.do_not_share_warning_text.toUpperCase());
}
int _verifiedCredentials = 0;
Future<void> confirmWalletCredentials(WalletType walletType) async {
_verifiedCredentials = 0;
final walletKeysPage = tester.widget<WalletKeysPage>(find.byType(WalletKeysPage));
final walletKeysViewModel = walletKeysPage.walletKeysViewModel;
final appStore = walletKeysViewModel.appStore;
final walletName = walletType.name;
bool hasSeed = appStore.wallet!.seed != null;
bool hasHexSeed = appStore.wallet!.hexSeed != null;
bool hasPrivateKey = appStore.wallet!.privateKey != null;
final hasSeed = appStore.wallet!.seed != null;
final hasHexSeed = appStore.wallet!.hexSeed != null;
final hasPrivateKey = appStore.wallet!.privateKey != null;
if (walletType == WalletType.monero) {
final moneroWallet = appStore.wallet as MoneroWalletBase;
final moneroWallet = appStore.wallet! as MoneroWalletBase;
final lang = PolyseedLang.getByPhrase(moneroWallet.seed);
final legacySeed = moneroWallet.seedLegacy(lang.nameEnglish);
@@ -57,51 +47,89 @@ class WalletKeysAndSeedPageRobot {
if (walletType == WalletType.bitcoin ||
walletType == WalletType.litecoin ||
walletType == WalletType.bitcoinCash) {
final seedWords = appStore.wallet!.seed!.split(" ");
for (var seedWord in seedWords) {
commonTestCases.hasTextAtLestOnce(seedWord);
}
tester.printToConsole('$walletName wallet has seeds properly displayed');
_showsSeedWords(appStore.wallet!.seed!);
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has seeds properly displayed");
}
// Hash Bags does not ship Solana or Tron; those WalletType members were
// removed with the chains, so only the EVM chains remain here.
if (isEVMCompatibleChain(walletType)) {
if (hasSeed) {
final seedWords = appStore.wallet!.seed!.split(" ");
for (var seedWord in seedWords) {
commonTestCases.hasTextAtLestOnce(seedWord);
}
tester.printToConsole('$walletName wallet has seeds properly displayed');
_showsSeedWords(appStore.wallet!.seed!);
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has seeds properly displayed");
}
if (hasPrivateKey) {
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
commonTestCases.hasText(appStore.wallet!.privateKey!);
tester.printToConsole('$walletName wallet has private key properly displayed');
await _openKeysTab();
_showsCredentialOnce(appStore.wallet!.privateKey!, "private key");
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has private key properly displayed");
}
}
if (walletType == WalletType.nano || walletType == WalletType.banano) {
if (hasSeed) {
final seedWords = appStore.wallet!.seed!.split(" ");
for (var seedWord in seedWords) {
commonTestCases.hasTextAtLestOnce(seedWord);
}
tester.printToConsole('$walletName wallet has seeds properly displayed');
_showsSeedWords(appStore.wallet!.seed!);
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has seeds properly displayed");
}
if (hasHexSeed) {
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
commonTestCases.hasText(appStore.wallet!.hexSeed!);
tester.printToConsole('$walletName wallet has hexSeed properly displayed');
await _openKeysTab();
_showsCredentialOnce(appStore.wallet!.hexSeed!, "hex seed");
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has hexSeed properly displayed");
}
if (hasPrivateKey) {
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
commonTestCases.hasText(appStore.wallet!.privateKey!);
tester.printToConsole('$walletName wallet has private key properly displayed');
await _openKeysTab();
_showsCredentialOnce(appStore.wallet!.privateKey!, "private key");
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has private key properly displayed");
}
}
await commonTestCases.defaultSleepTime(seconds: 5);
_expectSomethingWasVerified(walletType);
}
void _expectSomethingWasVerified(WalletType walletType) {
expect(
_verifiedCredentials,
greaterThan(0),
reason: "Nothing was verified for ${walletType.name}, this suite covers no credential "
"for that type so it cannot tell a working keys page from a broken one",
);
}
Future<void> _openKeysTab() async {
await tapByKey("wallet_keys_page_keys");
await settle();
}
void _showsCredential(String value, String label) {
expect(
tester.any(find.text(value, skipOffstage: false)),
true,
reason: "The keys page did not show the wallet's $label",
);
}
void _showsCredentialOnce(String value, String label) {
final rows = tester.widgetList(find.text(value, skipOffstage: false)).length;
expect(
rows,
1,
reason: "The keys page showed $rows rows for the wallet's $label, expected one",
);
}
void _showsSeedWords(String seed) {
final words = seed.split(" ");
for (var index = 0; index < words.length; index++) {
_showsCredential(words[index], "seed word ${index + 1}");
}
}
Future<void> _confirmMoneroWalletCredentials(
@@ -111,68 +139,42 @@ class WalletKeysAndSeedPageRobot {
String legacySeed,
) async {
final keys = appStore.wallet!.keys as MoneroWalletKeys;
final hasPublicSpendKey = commonTestCases.isKeyPresent(
'${walletName}_wallet_public_spend_key_item_key',
);
final hasPrivateSpendKey = commonTestCases.isKeyPresent(
'${walletName}_wallet_private_spend_key_item_key',
);
final hasPublicViewKey = commonTestCases.isKeyPresent(
'${walletName}_wallet_public_view_key_item_key',
);
final hasPrivateViewKey = commonTestCases.isKeyPresent(
'${walletName}_wallet_private_view_key_item_key',
);
final hasSeeds = seed.isNotEmpty;
final hasSeedLegacy = Polyseed.isValidSeed(seed);
if (hasPublicSpendKey) {
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
commonTestCases.hasText(keys.publicSpendKey);
tester.printToConsole('$walletName wallet has public spend key properly displayed');
}
if (hasPrivateSpendKey) {
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
commonTestCases.hasText(keys.privateSpendKey);
tester.printToConsole('$walletName wallet has private spend key properly displayed');
}
if (hasPublicViewKey) {
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
commonTestCases.hasText(keys.publicViewKey);
tester.printToConsole('$walletName wallet has public view key properly displayed');
}
if (hasPrivateViewKey) {
await commonTestCases.tapItemByKey('wallet_keys_page_keys');
commonTestCases.hasText(keys.privateViewKey);
tester.printToConsole('$walletName wallet has private view key properly displayed');
}
await _openKeysTab();
_showsCredentialOnce(keys.publicSpendKey, "public spend key");
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has public spend key properly displayed");
_showsCredentialOnce(keys.privateSpendKey, "private spend key");
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has private spend key properly displayed");
_showsCredentialOnce(keys.publicViewKey, "public view key");
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has public view key properly displayed");
_showsCredentialOnce(keys.privateViewKey, "private view key");
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has private view key properly displayed");
if (hasSeeds) {
await commonTestCases.tapItemByKey('wallet_keys_page_seed');
final seedWords = seed.split(" ");
for (var seedWord in seedWords) {
commonTestCases.hasTextAtLestOnce(seedWord);
}
tester.printToConsole('$walletName wallet has seeds properly displayed');
await tapByKey("wallet_keys_page_seed");
await settle();
_showsSeedWords(seed);
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has seeds properly displayed");
}
if (hasSeedLegacy) {
await commonTestCases.tapItemByKey('wallet_keys_page_seed_legacy');
final seedWords = legacySeed.split(" ");
for (var seedWord in seedWords) {
commonTestCases.hasTextAtLestOnce(seedWord);
}
tester.printToConsole('$walletName wallet has legacy seeds properly displayed');
await tapByKey("wallet_keys_page_seed_legacy");
await settle();
_showsSeedWords(legacySeed);
_verifiedCredentials++;
tester.printToConsole("$walletName wallet has legacy seeds properly displayed");
}
}
Future<void> backToDashboard() async {
tester.printToConsole('Going back to dashboard from credentials page');
await tester.pumpAndSettle(Duration(milliseconds: 1000));
await commonTestCases.goBack();
await tester.pumpAndSettle(Duration(milliseconds: 1000));
await commonTestCases.goBack();
await tester.pumpAndSettle(Duration(milliseconds: 2000));
}
}
@@ -1,28 +1,48 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/wallet_list/wallet_list_page.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class WalletListPageRobot {
WalletListPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class WalletListPageRobot extends BaseRobot {
WalletListPageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
@override
Future<void> isDisplayed() async {
final shown = await pumpUntil(_isShowing);
Future<void> isWalletListPage() async {
await commonTestCases.isSpecificPage<WalletListPageRobot>();
await commonTestCases.takeScreenshots('wallet_list_page');
expect(shown, true, reason: "The wallets tab never came to the front");
}
void displaysCorrectTitle() {
commonTestCases.hasText(S.current.wallets);
Future<void> openEditFor(String walletName) async {
await tapByKey("wallet_list_edit_${walletName}_button_key");
}
bool hasWallet(String walletName) => tester.any(
find.descendant(
of: find.byType(WalletListPage),
matching: find.text(walletName),
),
);
Future<void> navigateToCreateNewWalletPage() async {
await commonTestCases.tapItemByKey('wallet_list_page_create_new_wallet_button_key');
await tapByKey("wallet_list_page_create_new_wallet_button_key");
}
Future<void> navigateToRestoreWalletOptionsPage() async {
await commonTestCases.tapItemByKey('wallet_list_page_restore_wallet_button_key');
await tapByKey("wallet_list_page_restore_wallet_button_key");
}
bool _isShowing() {
final stackFinder = find.byType(IndexedStack);
if (!tester.any(stackFinder)) {
return false;
}
final stack = tester.widget<IndexedStack>(stackFinder.first);
final index = stack.children.indexWhere((child) => child is WalletListPage);
return index >= 0 && stack.index == index;
}
}
@@ -1,38 +1,19 @@
import 'package:hash_wallet/generated/i18n.dart';
import 'package:hash_wallet/src/screens/seed/wallet_seed_page.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/generated/i18n.dart";
import "package:hash_wallet/src/screens/seed/wallet_seed_page.dart";
import "package:flutter_test/flutter_test.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class WalletSeedPageRobot {
WalletSeedPageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class WalletSeedPageRobot extends BaseRobot {
WalletSeedPageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isWalletSeedPage() async {
await commonTestCases.isSpecificPage<WalletSeedPage>();
await commonTestCases.takeScreenshots('wallet_seed_page');
@override
Future<void> isDisplayed() async {
await isSpecificPage<WalletSeedPage>();
}
Future<void> onVerifySeedButtonPressed() async {
await commonTestCases.tapItemByKey('wallet_seed_page_verify_seed_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> onSaveSeedButtonPressed() async {
await commonTestCases.tapItemByKey('wallet_seed_page_save_seeds_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> onConfirmButtonOnSeedAlertDialogPressed() async {
await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_confirm_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> onBackButtonOnSeedAlertDialogPressed() async {
await commonTestCases.tapItemByKey('wallet_seed_page_seed_alert_back_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("wallet_seed_page_verify_seed_button_key");
}
void confirmWalletDetailsDisplayCorrectly() {
@@ -42,23 +23,18 @@ class WalletSeedPageRobot {
final walletName = walletSeedViewModel.name;
final walletSeeds = walletSeedViewModel.seedSplit;
commonTestCases.hasText(walletName);
for (var seed in walletSeeds) {
commonTestCases.hasTextAtLestOnce(seed);
hasText(walletName);
for (var index = 0; index < walletSeeds.length; index++) {
expect(
tester.any(find.text(walletSeeds[index])),
true,
reason: "The seed page did not show seed word ${index + 1}",
);
}
}
void confirmWalletSeedReminderDisplays() {
commonTestCases.hasText(S.current.cake_seeds_save_disclaimer);
}
Future<void> onSaveSeedsButtonPressed() async {
await commonTestCases.tapItemByKey('wallet_seed_page_save_seeds_button_key');
await commonTestCases.defaultSleepTime();
}
Future<void> onCopySeedsButtonPressed() async {
await commonTestCases.tapItemByKey('wallet_seed_page_copy_seeds_button_key');
await commonTestCases.defaultSleepTime();
hasText(S.current.cake_seeds_save_disclaimer);
}
}
+13 -35
View File
@@ -1,50 +1,28 @@
import 'package:hash_wallet/src/screens/welcome/welcome_page.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import "package:hash_wallet/src/screens/welcome/welcome_page.dart";
import "package:flutter_test/flutter_test.dart";
import '../components/common_test_cases.dart';
import "../core/base_robot.dart";
class WelcomePageRobot {
WelcomePageRobot(this.tester) : commonTestCases = CommonTestCases(tester);
class WelcomePageRobot extends BaseRobot {
WelcomePageRobot(super.tester);
final WidgetTester tester;
late CommonTestCases commonTestCases;
Future<void> isWelcomePage() async {
await commonTestCases.isSpecificPage<WelcomePage>();
await commonTestCases.takeScreenshots('welcome_page');
}
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);
@override
Future<void> isDisplayed() async {
await isSpecificPage<WelcomePage>();
}
Future<void> navigateToCreateNewWalletPage() async {
await commonTestCases.tapItemByKey('welcome_page_create_new_wallet_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("welcome_page_create_new_wallet_button_key");
}
bool hasNewSingleSeedButton() {
return commonTestCases.isKeyPresent('wallet_group_description_page_create_new_seed_button_key');
}
bool hasNewSingleSeedButton() =>
isKeyPresent("wallet_group_description_page_create_new_seed_button_key");
Future<void> tapNewSingleSeed() async {
await commonTestCases.tapItemByKey('wallet_group_description_page_create_new_seed_button_key');
await commonTestCases.defaultSleepTime();
await tapByKey("wallet_group_description_page_create_new_seed_button_key");
}
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();
await tapByKey("welcome_page_restore_wallet_button_key");
}
}