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
@@ -1,273 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
class CommonTestCases {
WidgetTester tester;
CommonTestCases(this.tester);
Future<void> isSpecificPage<T>() async {
await tester.pumpAndSettle();
hasType<T>();
}
Future<void> tapItemByKey(
String key, {
bool shouldPumpAndSettle = true,
int pumpDuration = 100,
Duration timeout = const Duration(seconds: 20),
}) async {
final widgetFinder = find.byKey(ValueKey(key));
// Wait for the widget to actually appear rather than pumping a flat 500ms
// and hoping. Screens that arrive behind an async transition (PIN setup's
// success button is the usual offender) are otherwise a coin flip, and the
// failure reads as "key missing" rather than "key not there YET".
//
// Deliberately a pump loop, not pumpAndSettle: the dashboard runs a
// perpetual sync-indicator animation, so settling never completes there.
final deadline = DateTime.now().add(timeout);
while (!tester.any(widgetFinder) && DateTime.now().isBefore(deadline)) {
await tester.pump(const Duration(milliseconds: 100));
}
expect(tester.any(widgetFinder), true,
reason: 'Widget with key "$key" should be visible '
'(waited ${timeout.inSeconds}s)');
final widget = widgetFinder.first;
await tester.tap(widget, warnIfMissed: false);
shouldPumpAndSettle
? await tester.pumpAndSettle(Duration(milliseconds: pumpDuration))
: await tester.pump();
}
Future<void> tapItemByFinder(Finder finder, {bool shouldPumpAndSettle = true}) async {
await tester.tap(finder);
shouldPumpAndSettle ? await tester.pumpAndSettle() : await tester.pump();
}
void hasText(String text, {bool hasWidget = true}) {
final textWidget = find.text(text);
expect(textWidget, hasWidget ? findsOneWidget : findsNothing);
}
void hasTextAtLestOnce(String text, {bool hasWidget = true}) {
final textWidget = find.text(text);
expect(textWidget, hasWidget ? findsAny : findsNothing);
}
void hasType<T>() {
final typeWidget = find.byType(T);
expect(typeWidget, findsOneWidget);
}
bool isKeyPresent(String key) {
final typeWidget = find.byKey(ValueKey(key));
return typeWidget.tryEvaluate();
}
void hasValueKey(String key) {
final typeWidget = find.byKey(ValueKey(key));
expect(typeWidget, findsOneWidget);
}
Future<void> swipePage({bool swipeRight = true}) async {
await tester.drag(find.byType(PageView), Offset(swipeRight ? -300 : 300, 0));
await tester.pumpAndSettle();
}
Future<void> swipeByPageKey({required String key, bool swipeRight = true}) async {
await tester.drag(find.byKey(ValueKey(key)), Offset(swipeRight ? -300 : 300, 0));
await tester.pumpAndSettle();
}
Future<void> goBack() async {
tester.printToConsole('Routing back to previous screen');
final NavigatorState navigator = tester.state(find.byType(Navigator));
navigator.pop();
await tester.pumpAndSettle();
}
Future<void> startGesture(String key, Offset gestureOffset) async {
await tester.pumpAndSettle();
final hasKey = isKeyPresent(key);
tester.printToConsole("Has gestureKey: $hasKey");
if (!hasKey) return;
final gesture = await tester.startGesture(tester.getCenter(find.byKey(ValueKey(key))));
// Drag to the left
await gesture.moveBy(gestureOffset);
// End the gesture
await gesture.up();
await tester.pump();
}
Future<void> dragUntilVisible(
String childKey,
String parentKey, {
int maxScrolls = 100,
double scrollStep = 50.0,
int maxReverseScrolls = 50,
}) async {
await tester.pumpAndSettle();
final itemFinder = find.byKey(ValueKey(childKey));
final listFinder = find.byKey(ValueKey(parentKey));
// Check if the widget is already in the widget tree
if (tester.any(itemFinder)) {
// Widget is already built and in the tree
tester.printToConsole('Child is already present');
return;
}
// Find the Scrollable associated with the Parent Ad
final scrollableFinder = find.descendant(
of: listFinder,
matching: find.byType(Scrollable),
);
// Ensure that the Scrollable is found
expect(
scrollableFinder,
findsOneWidget,
reason: 'Scrollable descendant of the Parent Widget not found.',
);
// Get the initial scroll position
final scrollableState = tester.state<ScrollableState>(scrollableFinder);
double previousScrollPosition = scrollableState.position.pixels;
bool scrollDown = true;
bool reversedDirection = false;
bool found = false;
int reverseScrollCount = 0;
for (int scrolls = 0; scrolls < maxScrolls; scrolls++) {
await tester.pumpAndSettle();
// Check if the widget is visible
if (tester.any(itemFinder)) {
found = true;
break;
}
// Log current state for debugging
tester.printToConsole('Scrolling ${scrollDown ? 'down' : 'up'}, attempt $scrolls');
// Stop if reverse scroll limit is exceeded
if (!scrollDown && reverseScrollCount >= maxReverseScrolls) {
tester.printToConsole('Maximum reverse scrolls reached. Widget not found.');
break;
}
// Perform scrolling in the current direction
await tester.drag(
scrollableFinder,
scrollDown ? Offset(0, -scrollStep) : Offset(0, scrollStep),
);
await tester.pumpAndSettle();
// Update the scroll position after the drag
final currentScrollPosition = scrollableState.position.pixels;
if (currentScrollPosition == previousScrollPosition) {
// Cannot scroll further in the current direction
if (reversedDirection) {
// We've already tried both directions
tester.printToConsole('Reached the scroll limit in both directions. Widget not found.');
break;
} else {
// Reverse the scroll direction and reset reverse scroll count
scrollDown = !scrollDown;
reversedDirection = true;
reverseScrollCount = 0;
tester.printToConsole('Reached the end, reversing direction');
}
} else {
// Update scroll position and reverse scroll count, incrementing only for reverse scrolling
previousScrollPosition = currentScrollPosition;
if (!scrollDown) reverseScrollCount++;
}
}
if (!found) {
tester.printToConsole('Widget not found after $maxScrolls scrolls.');
}
}
Future<void> scrollItemIntoView(
String itemKeyId,
double scrollPixels,
String scrollableFinderKey,
) async {
final Finder itemFinder = find.byKey(ValueKey(itemKeyId));
final scrollableFinder = find.descendant(
of: find.byKey(ValueKey(scrollableFinderKey)),
matching: find.byType(Scrollable),
);
// Check if the item is already visible
if (tester.any(itemFinder)) {
tester.printToConsole('Item $itemKeyId is already visible');
return;
}
// Check if scrollable exists
if (!tester.any(scrollableFinder)) {
tester.printToConsole('Scrollable not found for $itemKeyId');
return;
}
try {
await tester.scrollUntilVisible(
itemFinder,
scrollPixels,
scrollable: scrollableFinder,
maxScrolls: 10,
);
// Wait for the scroll to complete
await tester.pumpAndSettle(Duration(milliseconds: 500));
// Verify the item is now visible
if (!tester.any(itemFinder)) {
tester.printToConsole('Item $itemKeyId not found after scrolling');
}
} catch (e) {
tester.printToConsole('Could not scroll to $itemKeyId: $e');
}
}
Future<void> enterText(String text, String editableTextKey) async {
final editableTextWidget = find.byKey(ValueKey((editableTextKey)));
await tester.enterText(editableTextWidget, text);
await tester.pumpAndSettle();
}
void findWidgetViaDescendant({
required FinderBase<Element> of,
required FinderBase<Element> matching,
}) {
final textWidget = find.descendant(of: of, matching: matching);
expect(textWidget, findsOneWidget);
}
Future<void> defaultSleepTime({int seconds = 2}) async =>
await Future.delayed(Duration(seconds: seconds));
Future<void> takeScreenshots(String screenshotName) async {
// Pausing this for now
// await (tester.binding as IntegrationTestWidgetsFlutterBinding).takeScreenshot(screenshotName);
}
}
@@ -1,18 +0,0 @@
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/wallet_type.dart';
class CommonTestConstants {
static final pin = [0, 8, 0, 1];
static final String sendTestAmount = '0.00008';
static final String sendTestFiatAmount = '1.00';
static final String exchangeTestAmount = '0.01';
// Hash Bags is Wownero-first and no longer ships Solana, so the defaults
// moved off SOL. WalletType.solana does not exist in this fork.
static final WalletType testWalletType = WalletType.wownero;
static final String testWalletName = 'Integrated Testing Wallet';
static final CryptoCurrency sendTestReceiveCurrency = CryptoCurrency.wow;
static final CryptoCurrency exchangeTestReceiveCurrency = CryptoCurrency.btc;
static final CryptoCurrency exchangeTestDepositCurrency = CryptoCurrency.wow;
static final String testWalletAddress =
'Wo3MWeKwtA918DU4c69hVSNgejdWFCRCuWjShRY66mJkU2Hv58eygJWDJS1MNa2Ge5M1WjUkGHJLAjaZYLBZnDh42Q2u1Es9J';
}
@@ -1,404 +0,0 @@
import 'dart:io';
import 'package:hash_wallet/entities/seed_type.dart';
import 'package:hash_wallet/reactions/wallet_utils.dart';
import 'package:hash_wallet/wallet_types.g.dart';
import 'package:cw_core/currency_for_wallet_type.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:hash_wallet/main.dart' as app;
import '../robots/create_pin_welcome_page_robot.dart';
import '../robots/new_wallet_page_robot.dart';
import '../robots/new_wallet_type_page_robot.dart';
import '../robots/pre_seed_page_robot.dart';
import '../robots/restore_from_seed_or_key_robot.dart';
import '../robots/restore_options_page_robot.dart';
import '../robots/seed_verification_page_robot.dart';
import '../robots/setup_pin_code_robot.dart';
import '../robots/wallet_group_description_page_robot.dart';
import '../robots/wallet_list_page_robot.dart';
import '../robots/wallet_seed_page_robot.dart';
import '../robots/welcome_page_robot.dart';
import 'common_test_cases.dart';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
import 'common_test_constants.dart';
class CommonTestFlows {
CommonTestFlows(this._tester)
: _commonTestCases = CommonTestCases(_tester),
_welcomePageRobot = WelcomePageRobot(_tester),
_preSeedPageRobot = PreSeedPageRobot(_tester),
_setupPinCodeRobot = SetupPinCodeRobot(_tester),
_newWalletPageRobot = NewWalletPageRobot(_tester),
_walletSeedPageRobot = WalletSeedPageRobot(_tester),
_walletListPageRobot = WalletListPageRobot(_tester),
_newWalletTypePageRobot = NewWalletTypePageRobot(_tester),
_restoreOptionsPageRobot = RestoreOptionsPageRobot(_tester),
_seedVerificationPageRobot = SeedVerificationPageRobot(_tester),
_createPinWelcomePageRobot = CreatePinWelcomePageRobot(_tester),
_restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(_tester),
_walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(_tester);
final WidgetTester _tester;
final CommonTestCases _commonTestCases;
final WelcomePageRobot _welcomePageRobot;
final PreSeedPageRobot _preSeedPageRobot;
final SetupPinCodeRobot _setupPinCodeRobot;
final NewWalletPageRobot _newWalletPageRobot;
final WalletSeedPageRobot _walletSeedPageRobot;
final WalletListPageRobot _walletListPageRobot;
final NewWalletTypePageRobot _newWalletTypePageRobot;
final RestoreOptionsPageRobot _restoreOptionsPageRobot;
final CreatePinWelcomePageRobot _createPinWelcomePageRobot;
final SeedVerificationPageRobot _seedVerificationPageRobot;
final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot;
//* ========== Handles flow to start the app afresh and accept disclaimer =============
Future<void> startAppFlow(Key key) async {
await app.main(topLevelKey: key);
await _tester.pump(Duration(seconds: 2));
// // --------- Disclaimer Page ------------
// // Tap checkbox to accept disclaimer
// await _disclaimerPageRobot.tapDisclaimerCheckbox();
// // Tap accept button
// await _disclaimerPageRobot.tapAcceptButton();
}
//* ========== Handles flow from welcome to creating a new wallet ===============
Future<void> welcomePageToCreateNewWalletFlow(
WalletType walletTypeToCreate,
List<int> walletPin,
) async {
await _welcomeToCreateWalletPath(walletTypeToCreate, walletPin);
await _generateNewWalletDetails();
await _confirmPreSeedInfo();
await _confirmWalletDetails();
await _verifyWalletSeed();
}
//* ========== Handles flow from welcome to restoring wallet from seeds ===============
Future<void> welcomePageToRestoreWalletThroughSeedsFlow(
WalletType walletTypeToRestore,
String walletSeed,
List<int> walletPin,
) async {
await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin);
await _restoreFromSeeds(walletTypeToRestore, walletSeed);
}
//* ========== Handles flow from welcome to restoring wallet from keys ===============
Future<void> welcomePageToRestoreWalletThroughKeysFlow(
WalletType walletTypeToRestore,
List<int> walletPin,
) async {
await _welcomeToRestoreFromSeedsOrKeysPath(walletTypeToRestore, walletPin);
await _restoreFromKeys();
}
void confirmAllAvailableWalletTypeIconsDisplayCorrectly() {
for (var walletType in availableWalletTypes) {
final imageUrl = walletTypeToCryptoCurrency(walletType).iconPath;
final walletIconFinder = find.image(
Image.asset(
imageUrl!,
width: 32,
height: 32,
).image,
);
expect(walletIconFinder, findsAny);
}
}
//* ========== Handles creating new wallet flow from wallet list/menu ===============
Future<void> createNewWalletFromWalletMenu(WalletType walletTypeToCreate) async {
_tester.printToConsole('Creating ${walletTypeToCreate.name} Wallet');
await _tester.pumpAndSettle(Duration(milliseconds: 1000));
await _walletListPageRobot.navigateToCreateNewWalletPage();
await _commonTestCases.defaultSleepTime();
await _selectWalletTypeForWallet(walletTypeToCreate);
await _commonTestCases.defaultSleepTime();
// ---- Wallet Group/New Seed Implementation Comes here
await _walletGroupDescriptionPageFlow(true, walletTypeToCreate);
await _generateNewWalletDetails();
await _confirmPreSeedInfo();
await _confirmWalletDetails();
await _verifyWalletSeed();
await _commonTestCases.defaultSleepTime();
}
Future<void> _walletGroupDescriptionPageFlow(bool isNewSeed, WalletType walletType) async {
if (!isBIP39Wallet(walletType)) return;
await _walletGroupDescriptionPageRobot.isWalletGroupDescriptionPage();
if (isNewSeed) {
await _walletGroupDescriptionPageRobot.navigateToCreateNewSeedPage();
} else {
await _walletGroupDescriptionPageRobot.navigateToChooseWalletGroup();
}
}
//* ========== Handles restore wallet flow from wallet list/menu ===============
Future<void> restoreWalletFromWalletMenu(WalletType walletType, String walletSeed) async {
_tester.printToConsole('Restoring ${walletType.name} Wallet');
await _walletListPageRobot.navigateToRestoreWalletOptionsPage();
await _commonTestCases.defaultSleepTime();
if (!Platform.isLinux) await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
if (!Platform.isLinux) await _commonTestCases.defaultSleepTime();
await _selectWalletTypeForWallet(walletType);
await _commonTestCases.defaultSleepTime();
await _restoreFromSeeds(walletType, walletSeed);
await _commonTestCases.defaultSleepTime();
}
//* ========== Handles setting up pin code for wallet on first install ===============
Future<void> setupPinCodeForWallet(List<int> pin) async {
if (Platform.isLinux) return;
// ----------- SetupPinCode Page -------------
// Confirm initial defaults - Widgets to be displayed etc
await _setupPinCodeRobot.isSetupPinCodePage();
await _setupPinCodeRobot.enterPinCode(pin);
await _setupPinCodeRobot.enterPinCode(pin);
await _setupPinCodeRobot.tapSuccessButton();
}
Future<void> _welcomeToCreateWalletPath(
WalletType walletTypeToCreate,
List<int> pin,
) async {
await _createPinWelcomePageRobot.tapSetAPinButton();
await setupPinCodeForWallet(pin);
await _welcomePageRobot.navigateToCreateNewWalletPage();
await _selectWalletTypeForWallet(walletTypeToCreate);
if (_welcomePageRobot.hasNewSingleSeedButton()) {
await _welcomePageRobot.tapNewSingleSeed();
}
}
Future<void> _welcomeToRestoreFromSeedsOrKeysPath(
WalletType walletTypeToRestore,
List<int> pin,
) async {
await _createPinWelcomePageRobot.tapSetAPinButton();
await setupPinCodeForWallet(pin);
await _welcomePageRobot.navigateToRestoreWalletPage();
if (!Platform.isLinux) await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
await _selectWalletTypeForWallet(walletTypeToRestore);
}
//* ============ Handles New Wallet Type Page ==================
Future<void> _selectWalletTypeForWallet(WalletType type) async {
// ----------- NewWalletType Page -------------
_tester.printToConsole('Selecting wallet type: ${type.name}');
await _tester.pumpAndSettle(Duration(milliseconds: 1000));
await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type);
_tester.printToConsole('Tapping wallet type: ${type.name}');
await _newWalletTypePageRobot.selectWalletType(type);
await _tester.pumpAndSettle(Duration(milliseconds: 500));
_tester.printToConsole('Pressing next button');
await _newWalletTypePageRobot.onNextButtonPressed();
await _tester.pumpAndSettle(Duration(milliseconds: 2000));
_tester.printToConsole('Wallet type selection completed for: ${type.name}');
}
//* ============ Handles New Wallet Page ==================
Future<void> _generateNewWalletDetails() async {
_tester.printToConsole('Attempting to find NewWalletPage...');
await _tester.pumpAndSettle(Duration(milliseconds: 1000));
await _newWalletPageRobot.isNewWalletPage();
_tester.printToConsole('NewWalletPage found successfully');
await _newWalletPageRobot.generateWalletName();
if (Platform.isLinux) {
// manual pin input
await _restoreFromSeedOrKeysPageRobot
.enterPasswordForWalletRestore(CommonTestConstants.pin.join(""));
await _restoreFromSeedOrKeysPageRobot
.enterPasswordRepeatForWalletRestore(CommonTestConstants.pin.join(""));
}
await _newWalletPageRobot.onNextButtonPressed();
}
//* ============ Handles Pre Seed Page =====================
Future<void> _confirmPreSeedInfo() async {
await _preSeedPageRobot.isPreSeedPage();
await _preSeedPageRobot.onConfirmButtonPressed();
}
//* ============ Handles Wallet Seed Page ==================
Future<void> _confirmWalletDetails() async {
await _walletSeedPageRobot.isWalletSeedPage();
_walletSeedPageRobot.confirmWalletDetailsDisplayCorrectly();
_walletSeedPageRobot.confirmWalletSeedReminderDisplays();
// await _walletSeedPageRobot.onCopySeedsButtonPressed();
await _walletSeedPageRobot.onVerifySeedButtonPressed();
}
//* ============ Handles Wallet Seed Verification Page ==================
Future<void> _verifyWalletSeed() async {
await _seedVerificationPageRobot.isSeedVerificationPage();
_seedVerificationPageRobot.hasTitle();
await _seedVerificationPageRobot.verifyWalletSeeds();
}
//* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Seeds Action
Future<void> _restoreFromSeeds(WalletType type, String walletSeed) async {
// ----------- RestoreFromSeedOrKeys Page -------------
await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions();
await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(walletSeed);
final numberOfWords = walletSeed.split(' ').length;
if (numberOfWords == 25 && (type == WalletType.monero)) {
await _restoreFromSeedOrKeysPageRobot
.chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType.legacy);
// Using a constant value of 2831400 for the blockheight as its the restore blockheight for our testing wallet
await _restoreFromSeedOrKeysPageRobot
.enterBlockHeightForWalletRestore(secrets.moneroTestWalletBlockHeight);
}
if (Platform.isLinux) {
// manual pin input
await _restoreFromSeedOrKeysPageRobot
.enterPasswordForWalletRestore(CommonTestConstants.pin.join(""));
await _restoreFromSeedOrKeysPageRobot
.enterPasswordRepeatForWalletRestore(CommonTestConstants.pin.join(""));
}
await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
}
//* Main Restore Actions - On the RestoreFromSeed/Keys Page - Restore from Keys Action
Future<void> _restoreFromKeys() async {
await _commonTestCases.swipePage();
await _commonTestCases.defaultSleepTime();
await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions(
isSeedFormEntry: false,
);
await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore('');
await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
}
//* ====== Utility Function to get test wallet seeds for each wallet type ========
String getWalletSeedsByWalletType(WalletType walletType) {
switch (walletType) {
case WalletType.monero:
return secrets.moneroTestWalletSeeds;
case WalletType.bitcoin:
return secrets.bitcoinTestWalletSeeds;
case WalletType.ethereum:
return secrets.ethereumTestWalletSeeds;
case WalletType.litecoin:
return secrets.litecoinTestWalletSeeds;
case WalletType.bitcoinCash:
return secrets.bitcoinCashTestWalletSeeds;
case WalletType.polygon:
return secrets.polygonTestWalletSeeds;
case WalletType.base:
return secrets.baseTestWalletSeeds;
case WalletType.arbitrum:
return secrets.arbitrumTestWalletSeeds;
case WalletType.bsc:
return secrets.bscTestWalletSeeds;
case WalletType.nano:
return secrets.nanoTestWalletSeeds;
case WalletType.wownero:
return secrets.wowneroTestWalletSeeds;
// Hash Bags does not ship SOL/TRX/ZEC/DCR/ZANO/Haven — those WalletType
// members no longer exist. dogecoin ships, but has no test seed in
// .secrets.g.dart.
case WalletType.dogecoin:
case WalletType.none:
case WalletType.banano:
throw Exception("Unable to get seeds for ${walletType}");
}
}
//* ====== Utility Function to get test receive address for each wallet type ========
String getReceiveAddressByWalletType(WalletType walletType) {
switch (walletType) {
case WalletType.monero:
return secrets.moneroTestWalletReceiveAddress;
case WalletType.bitcoin:
return secrets.bitcoinTestWalletReceiveAddress;
case WalletType.ethereum:
return secrets.ethereumTestWalletReceiveAddress;
case WalletType.litecoin:
return secrets.litecoinTestWalletReceiveAddress;
case WalletType.bitcoinCash:
return secrets.bitcoinCashTestWalletReceiveAddress;
case WalletType.polygon:
return secrets.polygonTestWalletReceiveAddress;
case WalletType.base:
return secrets.baseTestWalletReceiveAddress;
case WalletType.arbitrum:
return secrets.arbitrumTestWalletReceiveAddress;
case WalletType.bsc:
return secrets.bscTestWalletReceiveAddress;
case WalletType.nano:
return secrets.nanoTestWalletReceiveAddress;
case WalletType.wownero:
return secrets.wowneroTestWalletReceiveAddress;
default:
return '';
}
}
}
+75
View File
@@ -0,0 +1,75 @@
import "dart:async";
import "package:hash_wallet/main.dart" as app;
import "package:flutter/foundation.dart";
import "package:flutter_test/flutter_test.dart";
import "benign_errors.dart";
void integrationTest(String description, Future<void> Function(WidgetTester tester) body) {
testWidgets(description, (tester) async {
final completer = Completer<void>();
final run = runZonedGuarded(() async {
try {
await body(tester);
if (!completer.isCompleted) {
completer.complete();
}
} catch (error, stack) {
if (!completer.isCompleted) {
completer.completeError(error, stack);
}
}
}, (error, stack) {
final kind = benignErrorKind(error.toString());
if (kind != null) {
debugPrint("Ignoring benign $kind background error: $error");
return;
}
if (completer.isCompleted) {
debugPrint("Background error after the test finished: $error");
return;
}
completer.completeError(error, stack);
});
unawaited(run);
await completer.future;
});
}
class AppLauncher {
AppLauncher(this.tester);
final WidgetTester tester;
Future<void> launchApp({required String testKey}) async {
final bindingHandler = FlutterError.onError;
await app.main(topLevelKey: ValueKey(testKey));
_installTestErrorHandler(bindingHandler);
await tester.pump(const Duration(seconds: 2));
}
void _installTestErrorHandler(FlutterExceptionHandler? bindingHandler) {
FlutterError.onError = (details) {
final reportedByTestFramework = details.library == "Flutter test framework";
final kind = reportedByTestFramework ? null : benignErrorKind(details.exceptionAsString());
if (kind != null) {
debugPrint("Ignoring benign $kind error: ${details.exceptionAsString()}");
return;
}
bindingHandler?.call(details);
};
}
}
+238
View File
@@ -0,0 +1,238 @@
import "dart:async";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "benign_errors.dart";
abstract class BaseRobot {
BaseRobot(this.tester);
final WidgetTester tester;
Future<void> isDisplayed();
void drainBackgroundErrors() {
final Object? pending = tester.takeException();
if (pending == null) {
return;
}
final kind = benignErrorKind(pending.toString());
if (kind != null) {
tester.printToConsole("Ignoring benign $kind background error: $pending");
return;
}
throw TestFailure("Unhandled background error: $pending");
}
Future<void> _pumpStep(Duration step) async {
await tester.pump(step);
drainBackgroundErrors();
}
Future<void> pumpUntilFound(
Finder finder, {
Duration timeout = const Duration(seconds: 30),
Duration step = const Duration(milliseconds: 100),
}) async {
final endTime = DateTime.now().add(timeout);
while (DateTime.now().isBefore(endTime)) {
await _pumpStep(step);
if (tester.any(finder)) {
return;
}
}
await takeScreenshot("${runtimeType}_pump_until_found_timeout");
throw TestFailure("Widget not found after ${timeout.inSeconds}s: $finder");
}
Future<void> pumpUntilGone(
Finder finder, {
Duration timeout = const Duration(seconds: 30),
Duration step = const Duration(milliseconds: 100),
}) async {
final endTime = DateTime.now().add(timeout);
while (DateTime.now().isBefore(endTime)) {
await _pumpStep(step);
if (!tester.any(finder)) {
return;
}
}
await takeScreenshot("${runtimeType}_pump_until_gone_timeout");
throw TestFailure("Widget still present after ${timeout.inSeconds}s: $finder");
}
Future<void> tapByKey(String key, {Duration timeout = const Duration(seconds: 30)}) async {
final finder = find.byKey(ValueKey(key));
await pumpUntilFound(finder, timeout: timeout);
// A tap during a route transition lands on the animating overlay instead of the widget
await settle(max: const Duration(seconds: 2));
await tester.tap(finder.first, warnIfMissed: false);
await tester.pump(const Duration(milliseconds: 300));
}
Future<void> tapWhenVisible(
Finder finder, {
Duration timeout = const Duration(seconds: 30),
}) async {
await pumpUntilFound(finder, timeout: timeout);
await settle(max: const Duration(seconds: 2));
await tester.tap(finder.first, warnIfMissed: false);
await tester.pump(const Duration(milliseconds: 300));
}
Future<void> enterTextByKey(String key, String text) async {
final finder = find.byKey(ValueKey(key));
await pumpUntilFound(finder);
await tester.enterText(finder.first, text);
await tester.pump(const Duration(milliseconds: 300));
}
Future<bool> pumpUntil(
bool Function() condition, {
Duration timeout = const Duration(seconds: 30),
Duration step = const Duration(milliseconds: 100),
}) async {
final endTime = DateTime.now().add(timeout);
while (DateTime.now().isBefore(endTime)) {
await _pumpStep(step);
if (condition()) {
return true;
}
}
return false;
}
Future<void> goBack() async {
final navigator = tester.state<NavigatorState>(find.byType(Navigator).last);
navigator.pop();
await tester.pump(const Duration(milliseconds: 500));
}
Future<void> dismissModal() async {
final navigator = tester.state<NavigatorState>(find.byType(Navigator).first);
navigator.pop();
await tester.pump(const Duration(milliseconds: 500));
}
// We make this bounded instead of pumpAndSettle
Future<void> settle({Duration max = const Duration(seconds: 3)}) async {
final endTime = DateTime.now().add(max);
while (DateTime.now().isBefore(endTime)) {
await _pumpStep(const Duration(milliseconds: 100));
if (!tester.binding.hasScheduledFrame) {
return;
}
}
}
Future<void> isSpecificPage<T>({Duration timeout = const Duration(seconds: 30)}) async {
await pumpUntilFound(find.byType(T), timeout: timeout);
await settle();
hasType<T>();
}
bool isKeyPresent(String key) => tester.any(find.byKey(ValueKey(key)));
String describeScreen() {
final names = tester.allWidgets
.map((widget) => widget.runtimeType.toString())
.where((name) => name.endsWith("Page") || name.endsWith("Sheet") || name.endsWith("Screen"))
.toSet()
.toList()
..sort();
return names.join(", ");
}
void hasType<T>() {
expect(find.byType(T), findsOneWidget);
}
void hasText(String text) {
expect(find.text(text), findsOneWidget);
}
void hasTextAtLeastOnce(String text) {
expect(find.text(text), findsAny);
}
String? textByKey(String key) {
final finder = find.byKey(ValueKey(key));
if (!tester.any(finder)) {
return null;
}
return tester.widget<Text>(finder.first).data;
}
static bool _surfaceConverted = false;
Future<void> takeScreenshot(String name) async {
try {
final binding = tester.binding;
if (binding is! IntegrationTestWidgetsFlutterBinding) {
return;
}
if (!_surfaceConverted) {
await binding.convertFlutterSurfaceToImage();
_surfaceConverted = true;
}
var finished = false;
unawaited(
binding.takeScreenshot("${name}_${DateTime.now().millisecondsSinceEpoch}").then(
(_) => finished = true,
onError: (Object error) {
finished = true;
tester.printToConsole("Screenshot $name failed: $error");
},
),
);
final deadline = DateTime.now().add(const Duration(seconds: 10));
while (!finished && DateTime.now().isBefore(deadline)) {
await tester.pump(const Duration(milliseconds: 100));
}
if (!finished) {
tester.printToConsole("Screenshot $name gave up after 10s");
}
} catch (e) {
tester.printToConsole("Screenshot $name failed: $e");
}
}
}
+51
View File
@@ -0,0 +1,51 @@
String? benignErrorKind(String message) {
if (_toleratedAssertions.any(message.contains)) {
return "assertion";
}
if (_toleratedAssets.any(message.contains)) {
return "asset";
}
if (_toleratedNetworkNoise.any(message.contains)) {
return "network";
}
return null;
}
// Debug build assertions. The semantics ones are real accessibility bugs that already
// exist on dev, take them off this list once they are fixed.
const List<String> _toleratedAssertions = [
"overflowed by",
"minValue, and maxValue must be valid numbers",
"node.parent?._dirty",
"RenderBox was not laid out",
];
// HashImageWidget reports these before falling back to the svg
const List<String> _toleratedAssets = [
"Unable to load asset",
"NetworkImageLoadException",
"Failed to load network image",
];
// The same failures exception_handler.dart ignores
const List<String> _toleratedNetworkNoise = [
"SocketException",
"HttpException",
"ClientException",
"HandshakeException",
"TimeoutException",
"Failed host lookup",
"Connection closed",
"Connection reset by peer",
"Connection refused",
"Connection timed out",
"Operation timed out",
"Network is unreachable",
"No route to host",
"Broken pipe",
"Software caused connection abort",
"Connection closed before full header",
];
+6
View File
@@ -0,0 +1,6 @@
class FundedWallet {
const FundedWallet({required this.seed, this.passphrase = ""});
final String seed;
final String passphrase;
}
@@ -0,0 +1,5 @@
import "funded_wallet.dart";
// Empty on purpose and it has to stay empty in git. The manual funds workflow overwrites
// this file from the FUNDS_SECRETS_FILE secret, keyed by WalletType name.
const Map<String, List<FundedWallet>> fundedWallets = {};
+46
View File
@@ -0,0 +1,46 @@
import "package:flutter_test/flutter_test.dart";
class FundsOutcome {
static const marker = "FUNDS_RESULT";
static void ok(WidgetTester tester, String chain, String detail) =>
_write(tester, chain, "ok", detail);
static void empty(WidgetTester tester, String chain, String detail) =>
_write(tester, chain, "empty", detail);
static void waiting(WidgetTester tester, String chain, String detail) =>
_write(tester, chain, "waiting", detail);
static void failed(WidgetTester tester, String chain, String detail) =>
_write(tester, chain, statusFor(detail), detail);
static String statusFor(String detail) {
if (_isWalletBalanceLow(detail)) {
return "empty";
}
return _isChainTooSlowToSync(detail) ? "waiting" : "failed";
}
static bool needsFurtherReview(String detail) => statusFor(detail) == "failed";
static bool _isChainTooSlowToSync(String detail) =>
detail.toLowerCase().contains("never became ready to send");
// Low wallet balance is not a failure, we just need to add fundsss
static bool _isWalletBalanceLow(String detail) {
final wording = detail.toLowerCase();
return wording.contains("not enough") ||
wording.contains("wrong balance") ||
wording.contains("insufficient") ||
wording.contains("no spendable");
}
static void _write(WidgetTester tester, String chain, String status, String detail) {
final clean = detail.replaceAll("\n", " ").replaceAll("|", "/").trim();
tester.printToConsole("$marker|$chain|$status|$clean");
}
}
+112
View File
@@ -0,0 +1,112 @@
import "package:hash_wallet/wallet_types.g.dart";
import "package:cw_core/wallet_type.dart";
import "test_wallets.dart";
class TestConfig {
static final List<int> pin = [0, 8, 0, 1];
static const bool isCIBuild = bool.fromEnvironment("CI_BUILD");
static const String _walletTypesOverride = String.fromEnvironment("TEST_WALLET_TYPES");
// Solana and ethereum cover the cheap key-derivation chains,
// bitcoin covers the electrum family and monero covers the native-library chains.
static final List<WalletType> _representativeWalletTypes = [
WalletType.solana,
WalletType.ethereum,
WalletType.bitcoin,
WalletType.monero,
];
static List<WalletType> get walletTypesUnderTest {
if (_walletTypesOverride == "all") {
return availableWalletTypes;
}
if (_walletTypesOverride.isNotEmpty) {
return _walletTypesOverride
.split(",")
.map((name) => WalletType.values.byName(name.trim()))
.where(availableWalletTypes.contains)
.toList();
}
return _representativeWalletTypes.where(availableWalletTypes.contains).toList();
}
static const String _fundsFlows = String.fromEnvironment("FLOWS", defaultValue: "all");
static const String _spend = String.fromEnvironment("SPEND", defaultValue: "false");
static const String _swapFrom = String.fromEnvironment("SWAP_FROM");
static const String swapReceive = String.fromEnvironment("SWAP_RECEIVE");
static WalletType swapDepositType(List<WalletType> funded) =>
_swapFrom.isEmpty ? funded.first : _walletTypeByName(_swapFrom, "SWAP_FROM");
static bool shouldRunFundsFlow(String flow) => _fundsFlows == "all" || _fundsFlows == flow;
static bool shouldDryRun(String flow) => shouldRunFundsFlow(flow);
static bool shouldSpend(String flow) => _spend == "true" && shouldRunFundsFlow(flow);
static const String _fundedChainsOverride = String.fromEnvironment(
"CHAINS",
defaultValue: "auto",
);
static List<WalletType> get fundedWalletTypesUnderTest {
if (_fundedChainsOverride == "auto") {
return TestWallets.fundedWalletTypes;
}
return _fundedChainsOverride
.split(",")
.map((name) => _walletTypeByName(name, "CHAINS"))
.where((type) => TestWallets.fundedWalletsFor(type).isNotEmpty)
.toList();
}
static const Map<String, String> _fundsSendAmounts = {
"solana": "0.0001",
"ethereum": "0.00001",
"polygon": "0.01",
"base": "0.00001",
"arbitrum": "0.00001",
"bsc": "0.000001",
"tron": "1",
"bitcoin": "0.00001",
"litecoin": "0.001",
"bitcoinCash": "0.0002",
"dogecoin": "2",
"monero": "0.0001",
"wownero": "0.1",
"zcash": "0.0001",
"nano": "0.000001",
"decred": "0.001",
"zano": "0.001",
};
static String fundsSendAmountFor(WalletType type) => _fundsSendAmounts[type.name] ?? "0.0001";
// Making this two hours to accommodate monero that takes a longgg time syncing
// also, this would only affect the funded tests which need actual funds to test
static const Duration walletSyncBudget = Duration(hours: 2);
static WalletType _walletTypeByName(String name, String input) {
final trimmed = name.trim();
final type = WalletType.values.where((value) => value.name == trimmed).toList();
if (type.isEmpty) {
throw ArgumentError(
'Unknown wallet type "$trimmed" in $input, '
"valid names: ${WalletType.values.map((value) => value.name).join(", ")}",
);
}
return type.first;
}
}
+106
View File
@@ -0,0 +1,106 @@
import "package:hash_wallet/.secrets.g.dart" as secrets;
import "package:hash_wallet/wallet_types.g.dart";
import "package:cw_core/wallet_type.dart";
import "funded_wallet.dart";
import "funded_wallets.dart";
class TestWallets {
static List<WalletType> get fundedWalletTypes =>
availableWalletTypes.where((type) => fundedWalletsFor(type).isNotEmpty).toList();
static List<FundedWallet> fundedWalletsFor(WalletType type) =>
(fundedWallets[type.name] ?? []).where((wallet) => wallet.seed.trim().isNotEmpty).toList();
static String seedFor(WalletType type) {
switch (type) {
case WalletType.monero:
return secrets.moneroTestWalletSeeds;
case WalletType.bitcoin:
return secrets.bitcoinTestWalletSeeds;
case WalletType.ethereum:
return secrets.ethereumTestWalletSeeds;
case WalletType.litecoin:
return secrets.litecoinTestWalletSeeds;
case WalletType.bitcoinCash:
return secrets.bitcoinCashTestWalletSeeds;
case WalletType.polygon:
return secrets.polygonTestWalletSeeds;
case WalletType.solana:
return secrets.solanaTestWalletSeeds;
case WalletType.base:
return secrets.baseTestWalletSeeds;
case WalletType.arbitrum:
return secrets.arbitrumTestWalletSeeds;
case WalletType.bsc:
return secrets.bscTestWalletSeeds;
case WalletType.robinhood:
return secrets.robinhoodTestWalletSeeds;
case WalletType.tron:
return secrets.tronTestWalletSeeds;
case WalletType.nano:
return secrets.nanoTestWalletSeeds;
case WalletType.wownero:
return secrets.wowneroTestWalletSeeds;
case WalletType.zano:
return secrets.zanoTestWalletSeeds;
case WalletType.decred:
return secrets.decredTestWalletSeeds;
case WalletType.dogecoin:
return secrets.dogeTestWalletSeeds;
case WalletType.zcash:
return secrets.zcashTestWalletSeeds;
case WalletType.none:
case WalletType.haven:
case WalletType.banano:
throw Exception("No test wallet seed available for ${type.name}");
}
}
static String receiveAddressFor(WalletType type) {
switch (type) {
case WalletType.monero:
return secrets.moneroTestWalletReceiveAddress;
case WalletType.bitcoin:
return secrets.bitcoinTestWalletReceiveAddress;
case WalletType.ethereum:
return secrets.ethereumTestWalletReceiveAddress;
case WalletType.litecoin:
return secrets.litecoinTestWalletReceiveAddress;
case WalletType.bitcoinCash:
return secrets.bitcoinCashTestWalletReceiveAddress;
case WalletType.polygon:
return secrets.polygonTestWalletReceiveAddress;
case WalletType.base:
return secrets.baseTestWalletReceiveAddress;
case WalletType.arbitrum:
return secrets.arbitrumTestWalletReceiveAddress;
case WalletType.bsc:
return secrets.bscTestWalletReceiveAddress;
case WalletType.robinhood:
return secrets.robinhoodTestWalletReceiveAddress;
case WalletType.solana:
return secrets.solanaTestWalletReceiveAddress;
case WalletType.tron:
return secrets.tronTestWalletReceiveAddress;
case WalletType.nano:
return secrets.nanoTestWalletReceiveAddress;
case WalletType.wownero:
return secrets.wowneroTestWalletReceiveAddress;
case WalletType.zano:
case WalletType.decred:
case WalletType.dogecoin:
case WalletType.zcash:
case WalletType.none:
case WalletType.haven:
case WalletType.banano:
return "";
}
}
static String get moneroLegacySeed => secrets.moneroLegacyTestWalletSeeds;
static String get moneroRestoreBlockHeight => secrets.moneroTestWalletBlockHeight;
static String get zcashRestoreHeight => secrets.zcashTestWalletRestoreHeight;
}
+41
View File
@@ -0,0 +1,41 @@
import "package:flutter_test/flutter_test.dart";
import "../core/test_config.dart";
import "../robots/auth_page_robot.dart";
class AuthFlows {
AuthFlows(this.tester) : _authPageRobot = AuthPageRobot(tester);
final WidgetTester tester;
final AuthPageRobot _authPageRobot;
Future<void> authenticateWithPin({List<int>? pin, bool required = true}) async {
final prompted = await _waitForPinWidget(
timeout: required ? const Duration(seconds: 30) : const Duration(seconds: 5),
);
if (!prompted) {
expect(required, false, reason: "The pin was never asked for");
return;
}
await _authPageRobot.enterPinCode(pin ?? TestConfig.pin);
}
bool isPinWidgetShown() => _authPageRobot.onAuthPage();
Future<bool> _waitForPinWidget({Duration timeout = const Duration(seconds: 30)}) async {
final endTime = DateTime.now().add(timeout);
while (DateTime.now().isBefore(endTime)) {
await tester.pump(const Duration(milliseconds: 100));
if (_authPageRobot.onAuthPage()) {
return true;
}
}
return false;
}
}
+155
View File
@@ -0,0 +1,155 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/sync_status.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/test_config.dart";
import "../core/test_wallets.dart";
import "../robots/home_page_robot.dart";
import "../robots/new_dashboard_robot.dart";
import "../robots/new_send_page_robot.dart";
import "onboarding_flows.dart";
class FundsFlows {
FundsFlows(this.tester)
: _onboardingFlows = OnboardingFlows(tester),
_dashboardRobot = NewDashboardRobot(tester),
_homePageRobot = HomePageRobot(tester),
_sendRobot = NewSendPageRobot(tester);
final WidgetTester tester;
final OnboardingFlows _onboardingFlows;
final NewDashboardRobot _dashboardRobot;
final HomePageRobot _homePageRobot;
final NewSendPageRobot _sendRobot;
bool _hasRestoredAnyWallet = false;
Future<bool> openFundedWallet(WalletType type) async {
final wallets = TestWallets.fundedWalletsFor(type);
for (int index = 0; index < wallets.length; index++) {
final wallet = wallets[index];
tester.printToConsole("Trying funded wallet ${index + 1}/${wallets.length} for ${type.name}");
if (_hasRestoredAnyWallet) {
await _dashboardRobot.openWalletsTab();
final isWalletListReady = await _dashboardRobot.pumpUntil(
() =>
tester.any(find.byKey(const ValueKey("wallet_list_page_restore_wallet_button_key"))),
timeout: const Duration(seconds: 60),
);
expect(
isWalletListReady,
true,
reason: "The wallet list restore button didn't show before ${type.name}, "
"Currently have: ${_dashboardRobot.describeScreen()} on screen",
);
await _onboardingFlows.restoreAdditionalWalletFromWalletList(
type,
seed: wallet.seed,
passphrase: wallet.passphrase,
);
} else {
await _onboardingFlows.restoreFirstWalletFromSeed(
type,
seed: wallet.seed,
passphrase: wallet.passphrase,
);
}
_hasRestoredAnyWallet = true;
await _waitForDashboard(type);
await _homePageRobot.isDisplayed();
if (await _hasSpendableBalance()) {
return true;
}
tester.printToConsole("Wallet ${index + 1} for ${type.name} has no spendable balance");
}
return false;
}
Future<bool> returnToHome({int maxPops = 6}) async {
for (int attempt = 0; attempt <= maxPops; attempt++) {
if (await _homePageRobot.pumpUntil(
() => _isHomeInFront,
timeout: const Duration(seconds: 3),
)) {
return true;
}
try {
await _homePageRobot.dismissModal();
} catch (e) {
tester.printToConsole("Nothing left to pop while returning home: $e");
break;
}
}
return _isHomeInFront;
}
Future<void> _waitForDashboard(WalletType type) async {
final reached = await _dashboardRobot.pumpUntil(
() => tester.any(find.byKey(const ValueKey("new_dashboard_page_key"))),
timeout: TestConfig.walletSyncBudget,
);
if (reached) {
return;
}
final status = getIt.get<AppStore>().wallet?.syncStatus;
final stillSyncing = status is SyncingSyncStatus || status is SyncronizingSyncStatus;
throw TestFailure(
stillSyncing
? "${type.name} never became ready to send, it is still syncing (${status.runtimeType})"
: "${type.name} never reached the dashboard, its status was ${status.runtimeType}",
);
}
bool get _isHomeInFront => _homePageRobot.isInFront && !_sendRobot.isSendFlowOpen;
Future<bool> _hasSpendableBalance({Duration timeout = TestConfig.walletSyncBudget}) async {
final appStore = getIt.get<AppStore>();
final endTime = DateTime.now().add(timeout);
DateTime? syncedSince;
while (DateTime.now().isBefore(endTime)) {
await tester.pump(const Duration(milliseconds: 250));
final wallet = appStore.wallet;
if (wallet == null) {
continue;
}
if (wallet.balance.values.any((balance) => balance.available.sign > 0)) {
return true;
}
if (wallet.syncStatus is SyncedSyncStatus) {
syncedSince ??= DateTime.now();
if (DateTime.now().difference(syncedSince) > const Duration(seconds: 15)) {
return false;
}
} else {
syncedSince = null;
}
}
return false;
}
}
@@ -0,0 +1,267 @@
import "dart:io";
import "package:hash_wallet/entities/seed_type.dart";
import "package:hash_wallet/reactions/wallet_utils.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "../core/test_config.dart";
import "../core/test_wallets.dart";
import "../robots/create_pin_welcome_page_robot.dart";
import "../robots/lightning_username_page_robot.dart";
import "../robots/new_wallet_page_robot.dart";
import "../robots/new_wallet_type_page_robot.dart";
import "../robots/pre_seed_page_robot.dart";
import "../robots/restore_from_seed_or_key_robot.dart";
import "../robots/restore_options_page_robot.dart";
import "../robots/seed_verification_page_robot.dart";
import "../robots/setup_pin_code_robot.dart";
import "../robots/wallet_group_description_page_robot.dart";
import "../robots/wallet_group_existing_seed_page_robot.dart";
import "../robots/wallet_groups_display_page_robot.dart";
import "../robots/wallet_list_page_robot.dart";
import "../robots/wallet_seed_page_robot.dart";
import "../robots/welcome_page_robot.dart";
class OnboardingFlows {
OnboardingFlows(this.tester)
: _welcomePageRobot = WelcomePageRobot(tester),
_preSeedPageRobot = PreSeedPageRobot(tester),
_setupPinCodeRobot = SetupPinCodeRobot(tester),
_newWalletPageRobot = NewWalletPageRobot(tester),
_walletSeedPageRobot = WalletSeedPageRobot(tester),
_walletListPageRobot = WalletListPageRobot(tester),
_newWalletTypePageRobot = NewWalletTypePageRobot(tester),
_seedVerificationPageRobot = SeedVerificationPageRobot(tester),
_createPinWelcomePageRobot = CreatePinWelcomePageRobot(tester),
_restoreOptionsPageRobot = RestoreOptionsPageRobot(tester),
_restoreFromSeedOrKeysPageRobot = RestoreFromSeedOrKeysPageRobot(tester),
_lightningUsernamePageRobot = LightningUsernamePageRobot(tester),
_walletGroupDescriptionPageRobot = WalletGroupDescriptionPageRobot(tester),
_walletGroupsDisplayPageRobot = WalletGroupsDisplayPageRobot(tester),
_walletGroupExistingSeedPageRobot = WalletGroupExistingSeedPageRobot(tester);
final WidgetTester tester;
final WelcomePageRobot _welcomePageRobot;
final PreSeedPageRobot _preSeedPageRobot;
final SetupPinCodeRobot _setupPinCodeRobot;
final NewWalletPageRobot _newWalletPageRobot;
final WalletSeedPageRobot _walletSeedPageRobot;
final WalletListPageRobot _walletListPageRobot;
final NewWalletTypePageRobot _newWalletTypePageRobot;
final SeedVerificationPageRobot _seedVerificationPageRobot;
final CreatePinWelcomePageRobot _createPinWelcomePageRobot;
final RestoreOptionsPageRobot _restoreOptionsPageRobot;
final RestoreFromSeedOrKeysPageRobot _restoreFromSeedOrKeysPageRobot;
final LightningUsernamePageRobot _lightningUsernamePageRobot;
final WalletGroupDescriptionPageRobot _walletGroupDescriptionPageRobot;
final WalletGroupsDisplayPageRobot _walletGroupsDisplayPageRobot;
final WalletGroupExistingSeedPageRobot _walletGroupExistingSeedPageRobot;
Future<void> createFirstWallet(WalletType type, {List<int>? pin}) async {
await _createPinWelcomePageRobot.tapSetAPinButton();
await setupPinCode(pin ?? TestConfig.pin);
await _welcomePageRobot.navigateToCreateNewWalletPage();
await _selectWalletType(type);
if (_welcomePageRobot.hasNewSingleSeedButton()) {
await _welcomePageRobot.tapNewSingleSeed();
}
await _completeWalletCreationSteps(type);
}
Future<void> createAdditionalWalletFromWalletList(WalletType type) async {
tester.printToConsole("Creating ${type.name} wallet");
await startCreatingWalletFromWalletList(type);
await _completeWalletCreationSteps(type);
}
Future<void> createWalletInGroupOf(WalletType type, String existingWalletName) async {
tester.printToConsole("Adding a ${type.name} wallet to the seed of $existingWalletName");
await _walletListPageRobot.navigateToCreateNewWalletPage();
await _selectWalletType(type);
await _walletGroupDescriptionPageRobot.isDisplayed();
await _walletGroupDescriptionPageRobot.navigateToChooseWalletGroup();
await _walletGroupsDisplayPageRobot.isDisplayed();
await _walletGroupsDisplayPageRobot.selectWallet(existingWalletName);
await _walletGroupsDisplayPageRobot.continueWithSelectedSeed();
await _newWalletPageRobot.isDisplayed();
await _newWalletPageRobot.generateWalletName();
await _newWalletPageRobot.onNextButtonPressed();
await _walletGroupExistingSeedPageRobot.isDisplayed();
await _walletGroupExistingSeedPageRobot.openWallet();
}
Future<void> startCreatingWalletFromWalletList(WalletType type) async {
await _walletListPageRobot.navigateToCreateNewWalletPage();
await _selectWalletType(type);
if (isBIP39Wallet(type)) {
await _walletGroupDescriptionPageRobot.isDisplayed();
await _walletGroupDescriptionPageRobot.navigateToCreateNewSeedPage();
}
await _newWalletPageRobot.isDisplayed();
}
Future<void> restoreFirstWalletFromSeed(
WalletType type, {
String? seed,
String passphrase = "",
List<int>? pin,
}) async {
await startRestoringFirstWallet(type, pin: pin);
await _restoreFromSeed(type, seed ?? TestWallets.seedFor(type), passphrase);
}
Future<void> startRestoringFirstWallet(WalletType type, {List<int>? pin}) async {
await _createPinWelcomePageRobot.tapSetAPinButton();
await setupPinCode(pin ?? TestConfig.pin);
await _welcomePageRobot.navigateToRestoreWalletPage();
// Desktop skips the restore options page and goes straight to the type list.
if (!Platform.isLinux) {
await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
}
await _selectWalletType(type);
await _restoreFromSeedOrKeysPageRobot.isDisplayed();
}
Future<void> restoreAdditionalWalletFromWalletList(
WalletType type, {
String? seed,
String passphrase = "",
}) async {
tester.printToConsole("Restoring ${type.name} wallet");
await _walletListPageRobot.navigateToRestoreWalletOptionsPage();
if (!Platform.isLinux) {
await _restoreOptionsPageRobot.navigateToRestoreFromSeedsOrKeysPage();
}
await _selectWalletType(type);
await _restoreFromSeed(type, seed ?? TestWallets.seedFor(type), passphrase);
}
Future<void> _restoreFromSeed(WalletType type, String seed, String passphrase) async {
await _restoreFromSeedOrKeysPageRobot.selectWalletNameFromAvailableOptions();
await _restoreFromSeedOrKeysPageRobot.enterSeedPhraseForWalletRestore(seed);
if (passphrase.isNotEmpty) {
await _restoreFromSeedOrKeysPageRobot.enablePassphrase();
}
// 25 word monero seeds are legacy seeds and need their restore block height entered.
if (seed.split(" ").length == 25 && type == WalletType.monero) {
await _restoreFromSeedOrKeysPageRobot
.chooseSeedTypeForMoneroOrWowneroWallets(MoneroSeedType.legacy);
await _restoreFromSeedOrKeysPageRobot
.enterBlockHeightForWalletRestore(TestWallets.moneroRestoreBlockHeight);
}
if (type == WalletType.zcash && TestWallets.zcashRestoreHeight.isNotEmpty) {
await _restoreFromSeedOrKeysPageRobot
.enterBlockHeightForWalletRestore(TestWallets.zcashRestoreHeight);
}
if (Platform.isLinux) {
final password = TestConfig.pin.join("");
await _restoreFromSeedOrKeysPageRobot.enterPasswordForWalletRestore(password);
await _restoreFromSeedOrKeysPageRobot.enterPasswordRepeatForWalletRestore(password);
}
await _restoreFromSeedOrKeysPageRobot.onRestoreWalletButtonPressed();
if (passphrase.isNotEmpty) {
await _restoreFromSeedOrKeysPageRobot.enterPassphrase(passphrase);
}
}
Future<void> setupPinCode(List<int> pin) async {
// Desktop builds take a wallet password on the new wallet form instead of a pin.
if (Platform.isLinux) {
return;
}
await _setupPinCodeRobot.isDisplayed();
await _setupPinCodeRobot.enterPinCode(pin);
await _setupPinCodeRobot.enterPinCode(pin);
await _setupPinCodeRobot.tapSuccessButton();
}
Future<void> _selectWalletType(WalletType type) async {
await _newWalletTypePageRobot.isDisplayed();
await _newWalletTypePageRobot.findParticularWalletTypeInScrollableList(type);
await _newWalletTypePageRobot.selectWalletType(type);
}
Future<void> _completeWalletCreationSteps(WalletType type) async {
await _generateNewWalletDetails();
await _preSeedPageRobot.isDisplayed();
await _preSeedPageRobot.onConfirmButtonPressed();
await _confirmWalletSeedDetails();
await _verifyWalletSeed();
if (type == WalletType.bitcoin) {
await _lightningUsernamePageRobot.isDisplayed();
await _lightningUsernamePageRobot.tapSkip();
}
}
Future<void> _generateNewWalletDetails() async {
await _newWalletPageRobot.isDisplayed();
await _newWalletPageRobot.generateWalletName();
if (Platform.isLinux) {
final password = TestConfig.pin.join("");
await _restoreFromSeedOrKeysPageRobot.enterPasswordForWalletRestore(password);
await _restoreFromSeedOrKeysPageRobot.enterPasswordRepeatForWalletRestore(password);
}
await _newWalletPageRobot.onNextButtonPressed();
}
Future<void> _confirmWalletSeedDetails() async {
await _walletSeedPageRobot.isDisplayed();
_walletSeedPageRobot.confirmWalletDetailsDisplayCorrectly();
_walletSeedPageRobot.confirmWalletSeedReminderDisplays();
await _walletSeedPageRobot.onVerifySeedButtonPressed();
}
Future<void> _verifyWalletSeed() async {
await _seedVerificationPageRobot.isDisplayed();
_seedVerificationPageRobot.hasTitle();
await _seedVerificationPageRobot.verifyWalletSeeds();
}
}
+59
View File
@@ -0,0 +1,59 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:flutter/material.dart";
import "package:flutter_test/flutter_test.dart";
import "../robots/new_dashboard_robot.dart";
import "auth_flows.dart";
class WalletFlows {
WalletFlows(this.tester)
: _authFlows = AuthFlows(tester),
_dashboardRobot = NewDashboardRobot(tester);
final WidgetTester tester;
final AuthFlows _authFlows;
final NewDashboardRobot _dashboardRobot;
Future<void> _expandWalletGroups({int maxGroups = 10}) async {
for (int index = 0; index < maxGroups; index++) {
final tile = find.byKey(ValueKey("group_wallets_expansion_tile_widget_$index"));
if (!tester.any(tile)) {
return;
}
await tester.tap(tile, warnIfMissed: false);
await tester.pump(const Duration(milliseconds: 300));
}
}
Future<void> switchToWallet(String name) async {
await _dashboardRobot.openWalletsTab();
final walletFinder = find.text(name);
// Wallets sharing a seed are listed under their group's name, so the wallet itself is
// not on screen until that group is opened.
if (!tester.any(walletFinder)) {
await _expandWalletGroups();
}
await _dashboardRobot.pumpUntilFound(walletFinder);
await tester.tap(walletFinder.first, warnIfMissed: false);
await tester.pump(const Duration(milliseconds: 500));
await _authFlows.authenticateWithPin(required: false);
// The row does nothing at all when that wallet is already the open one, and the load
// itself runs behind a progress overlay, so the store is the only thing that tells us
// the switch happened.
final switched = await _dashboardRobot.pumpUntil(
() => getIt.get<AppStore>().wallet?.name == name,
timeout: const Duration(minutes: 2),
);
expect(switched, true, reason: "Wallet never became $name after tapping its row");
}
}
-96
View File
@@ -1,96 +0,0 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'components/common_test_constants.dart';
import 'components/common_test_flows.dart';
import 'robots/auth_page_robot.dart';
import 'robots/dashboard_page_robot.dart';
import 'robots/exchange_confirm_page_robot.dart';
import 'robots/exchange_page_robot.dart';
import 'robots/exchange_trade_page_robot.dart';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
DashboardPageRobot dashboardPageRobot;
ExchangePageRobot exchangePageRobot;
ExchangeConfirmPageRobot exchangeConfirmPageRobot;
AuthPageRobot authPageRobot;
ExchangeTradePageRobot exchangeTradePageRobot;
CommonTestFlows commonTestFlows;
group('Startup Test', () {
testWidgets('Test for Exchange flow using Restore Wallet - Exchanging USDT(Sol) to SOL',
(tester) async {
authPageRobot = AuthPageRobot(tester);
exchangePageRobot = ExchangePageRobot(tester);
dashboardPageRobot = DashboardPageRobot(tester);
exchangeTradePageRobot = ExchangeTradePageRobot(tester);
exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
commonTestFlows = CommonTestFlows(tester);
await commonTestFlows.startAppFlow(ValueKey('funds_exchange_test_app_key'));
await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
CommonTestConstants.testWalletType,
secrets.solanaTestWalletSeeds,
CommonTestConstants.pin,
);
// ----------- RestoreFromSeedOrKeys Page -------------
await dashboardPageRobot.navigateToExchangePage();
// ----------- Exchange Page -------------
await exchangePageRobot.isExchangePage();
exchangePageRobot.hasResetButton();
await exchangePageRobot.displayBothExchangeCards();
exchangePageRobot.confirmRightComponentsDisplayOnDepositExchangeCards();
exchangePageRobot.confirmRightComponentsDisplayOnReceiveExchangeCards();
await exchangePageRobot
.selectDepositCurrency(CommonTestConstants.exchangeTestDepositCurrency);
await exchangePageRobot
.selectReceiveCurrency(CommonTestConstants.exchangeTestReceiveCurrency);
await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
await exchangePageRobot.enterDepositRefundAddress(
depositAddress: CommonTestConstants.testWalletAddress);
await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
await exchangePageRobot.onExchangeButtonPressed();
await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
final onAuthPage = authPageRobot.onAuthPage();
if (onAuthPage) {
await authPageRobot.enterPinCode(CommonTestConstants.pin);
}
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
if (onAuthPageDesktop) {
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
}
// ----------- Exchange Confirm Page -------------
await exchangeConfirmPageRobot.isExchangeConfirmPage();
exchangeConfirmPageRobot.confirmComponentsOfTradeDisplayProperly();
await exchangeConfirmPageRobot.confirmCopyTradeIdToClipBoardWorksProperly();
await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
// ----------- Exchange Trade Page -------------
await exchangeTradePageRobot.isExchangeTradePage();
exchangeTradePageRobot.hasInformationDialog();
await exchangeTradePageRobot.onGotItButtonPressed();
await exchangeTradePageRobot.onSendFromCakeButtonPressed();
await exchangeTradePageRobot.handleConfirmSendResult();
await exchangeTradePageRobot.onSendButtonOnConfirmSendingDialogPressed();
});
});
}
@@ -0,0 +1,107 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../core/app_launcher.dart";
import "../core/funds_outcome.dart";
import "../core/test_config.dart";
import "../flows/auth_flows.dart";
import "../flows/funds_flows.dart";
import "../robots/home_page_robot.dart";
import "../robots/new_send_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Every funded chain can build a transaction without sending it", (tester) async {
final appLauncher = AppLauncher(tester);
final authFlows = AuthFlows(tester);
final fundsFlows = FundsFlows(tester);
final homePageRobot = HomePageRobot(tester);
final sendRobot = NewSendPageRobot(tester);
if (!TestConfig.shouldDryRun("send")) {
tester.printToConsole("FLOWS excludes send, skipping");
return;
}
final walletTypes = TestConfig.fundedWalletTypesUnderTest;
expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
await appLauncher.launchApp(testKey: "send_dry_run_test_app_key");
final appStore = getIt.get<AppStore>();
final failures = <String>[];
for (final type in walletTypes) {
tester.printToConsole("Send dry run starting for ${type.name}");
try {
final opened = await fundsFlows.openFundedWallet(type);
if (!opened) {
tester.printToConsole("${type.name} has nothing spendable, top it up");
FundsOutcome.empty(tester, type.name, "every funded wallet is empty");
continue;
}
expect(appStore.wallet?.type, type);
final ownAddress = appStore.wallet!.walletAddresses.address;
await homePageRobot.openSendSheet();
await sendRobot.isDisplayed();
await sendRobot.enterAddress(ownAddress);
await sendRobot.enterAmount(TestConfig.fundsSendAmountFor(type));
await sendRobot.tapSendButtonWhenReady();
await authFlows.authenticateWithPin(required: false);
await sendRobot.confirmTransactionBuilt();
await sendRobot.confirmSendButtonReplacesSwiper();
tester.printToConsole("${type.name} built a transaction, leaving it unsent");
FundsOutcome.ok(tester, type.name, "built and signed, not broadcast");
expect(
await fundsFlows.returnToHome(),
true,
reason: "${type.name} finished but the send flow would not close",
);
} catch (e) {
if (FundsOutcome.needsFurtherReview(e.toString())) {
failures.add("${type.name}: $e");
} else {
tester.printToConsole("${type.name} needs attention rather than a fix: $e");
}
FundsOutcome.failed(tester, type.name, e.toString());
if (!await fundsFlows.returnToHome()) {
// The app is hanging or found itself somewhere we don't know,
// no need to continue running anything else
for (final skipped in walletTypes.skip(walletTypes.indexOf(type) + 1)) {
FundsOutcome.waiting(
tester,
skipped.name,
"never ran, ${type.name} left the app somewhere it could not come back from",
);
}
if (FundsOutcome.needsFurtherReview(e.toString())) {
failures.add("${type.name}: could not get back to the home page, "
"the chains after this one never ran");
}
break;
}
}
}
expect(failures, isEmpty, reason: "Failed chains:\n${failures.join("\n")}");
});
}
@@ -0,0 +1,109 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../core/app_launcher.dart";
import "../core/funds_outcome.dart";
import "../core/test_config.dart";
import "../flows/auth_flows.dart";
import "../flows/funds_flows.dart";
import "../robots/home_page_robot.dart";
import "../robots/new_send_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// tests everything up to the point of swiping
integrationTest("Send a real self transaction on every funded chain", (tester) async {
final appLauncher = AppLauncher(tester);
final fundsFlows = FundsFlows(tester);
final authFlows = AuthFlows(tester);
final homePageRobot = HomePageRobot(tester);
final sendRobot = NewSendPageRobot(tester);
if (!TestConfig.shouldSpend("send")) {
tester.printToConsole("FLOWS excludes send, skipping");
return;
}
final walletTypes = TestConfig.fundedWalletTypesUnderTest;
expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
await appLauncher.launchApp(testKey: "send_funds_test_app_key");
final appStore = getIt.get<AppStore>();
final failures = <String>[];
for (final type in walletTypes) {
tester.printToConsole("Funds send starting for ${type.name}");
try {
final opened = await fundsFlows.openFundedWallet(type);
if (!opened) {
tester.printToConsole("${type.name} has nothing spendable, top it up");
FundsOutcome.empty(tester, type.name, "every funded wallet is empty");
continue;
}
expect(appStore.wallet?.type, type);
// Making it a self send so the funds remain inside the funded wallet, only fees are spent.
final ownAddress = appStore.wallet!.walletAddresses.address;
await homePageRobot.openSendSheet();
await sendRobot.isDisplayed();
await sendRobot.enterAddress(ownAddress);
await sendRobot.enterAmount(TestConfig.fundsSendAmountFor(type));
await sendRobot.tapSendButtonWhenReady();
await authFlows.authenticateWithPin(required: false);
await sendRobot.swipeToConfirm();
await sendRobot.confirmTransactionCommitted();
// The send page sits under the confirm sheet, so one pop leaves it up and the next
// chain would try to restore from behind it.
expect(
await fundsFlows.returnToHome(),
true,
reason: "${type.name} broadcast but the send flow would not close",
);
tester.printToConsole("FUNDS_SEND_OK: ${type.name}");
FundsOutcome.ok(tester, type.name, "self send broadcast");
} catch (e) {
if (FundsOutcome.needsFurtherReview(e.toString())) {
failures.add("${type.name}: $e");
} else {
tester.printToConsole("${type.name} needs attention rather than a fix: $e");
}
FundsOutcome.failed(tester, type.name, e.toString());
tester.printToConsole("FUNDS_SEND_FAIL: ${type.name}: $e");
if (!await fundsFlows.returnToHome()) {
for (final skipped in walletTypes.skip(walletTypes.indexOf(type) + 1)) {
FundsOutcome.waiting(
tester,
skipped.name,
"never ran, ${type.name} left the app somewhere it could not come back from",
);
}
if (FundsOutcome.needsFurtherReview(e.toString())) {
failures.add("${type.name}: could not get back to the home page, "
"the chains after this one never ran");
}
break;
}
}
}
expect(failures, isEmpty, reason: "Failed chains:\n${failures.join("\n")}");
});
}
@@ -0,0 +1,69 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../core/app_launcher.dart";
import "../core/funds_outcome.dart";
import "../core/test_config.dart";
import "../flows/funds_flows.dart";
import "../robots/home_page_robot.dart";
import "../robots/new_swap_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
// tests everything up to the point of creating the trade
integrationTest("Swap prices a real deposit without creating a trade", (tester) async {
final appLauncher = AppLauncher(tester);
final fundsFlows = FundsFlows(tester);
final homePageRobot = HomePageRobot(tester);
final swapRobot = NewSwapPageRobot(tester);
if (!TestConfig.shouldDryRun("swap")) {
tester.printToConsole("FLOWS excludes swap, skipping");
return;
}
final walletTypes = TestConfig.fundedWalletTypesUnderTest;
expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
await appLauncher.launchApp(testKey: "swap_dry_run_test_app_key");
final appStore = getIt.get<AppStore>();
final type = TestConfig.swapDepositType(walletTypes);
expect(
walletTypes,
contains(type),
reason: "SWAP_FROM=${type.name} is not among the funded wallets under test",
);
final opened = await fundsFlows.openFundedWallet(type);
expect(opened, true, reason: "Every funded wallet for ${type.name} came up empty");
expect(appStore.wallet?.type, type);
await homePageRobot.openSwapSheet();
await swapRobot.isDisplayed();
if (TestConfig.swapReceive.isNotEmpty) {
await swapRobot.chooseReceiveCurrency(CryptoCurrency.fromString(TestConfig.swapReceive));
}
await swapRobot.enterMinimumViableDepositAmount();
await swapRobot.enterTestReceiveAddress();
await swapRobot.confirmQuoteReceived();
await swapRobot.confirmSwapButtonEnabled();
tester.printToConsole("${type.name} got a quote and the swap button enabled, no trade created");
FundsOutcome.ok(tester, type.name, "quoted, swap button enabled, no trade created");
await swapRobot.dismissModal();
await homePageRobot.isDisplayed();
});
}
@@ -0,0 +1,86 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/crypto_currency.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../core/app_launcher.dart";
import "../core/funds_outcome.dart";
import "../core/test_config.dart";
import "../core/test_wallets.dart";
import "../flows/auth_flows.dart";
import "../flows/funds_flows.dart";
import "../robots/home_page_robot.dart";
import "../robots/new_send_page_robot.dart";
import "../robots/new_swap_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Create a real swap and broadcast its deposit", (tester) async {
final appLauncher = AppLauncher(tester);
final fundsFlows = FundsFlows(tester);
final authFlows = AuthFlows(tester);
final homePageRobot = HomePageRobot(tester);
final sendRobot = NewSendPageRobot(tester);
final swapRobot = NewSwapPageRobot(tester);
if (!TestConfig.shouldSpend("swap")) {
tester.printToConsole("FLOWS excludes swap, skipping");
return;
}
final walletTypes = TestConfig.fundedWalletTypesUnderTest;
expect(walletTypes.isNotEmpty, true, reason: "No funded wallet seeds configured");
final type = TestConfig.swapDepositType(walletTypes);
expect(
walletTypes,
contains(type),
reason: "SWAP_FROM=${type.name} is not among the funded wallets under test",
);
await appLauncher.launchApp(testKey: "swap_funds_test_app_key");
final opened = await fundsFlows.openFundedWallet(type);
expect(
opened,
true,
reason: "${type.name}: none of the ${TestWallets.fundedWalletsFor(type).length} funded "
"wallets has a spendable balance, top them up before the next run",
);
final appStore = getIt.get<AppStore>();
expect(appStore.wallet?.type, type);
await homePageRobot.openSwapSheet();
await swapRobot.isDisplayed();
if (TestConfig.swapReceive.isNotEmpty) {
await swapRobot.chooseReceiveCurrency(CryptoCurrency.fromString(TestConfig.swapReceive));
}
await swapRobot.enterMinimumViableDepositAmount();
final payoutAddress = await swapRobot.enterTestReceiveAddress();
await swapRobot.confirmQuoteReceived();
await swapRobot.confirmSwapButtonEnabled();
await swapRobot.tapSwapButton();
await authFlows.authenticateWithPin(required: false);
await swapRobot.confirmTradeCreated();
swapRobot.confirmPayoutAddress(payoutAddress);
await sendRobot.swipeToConfirm();
await swapRobot.confirmDepositCommitted();
await homePageRobot.isDisplayed();
tester.printToConsole("FUNDS_SWAP_OK: ${type.name}");
FundsOutcome.ok(tester, type.name, "trade created and deposit broadcast");
});
}
-27
View File
@@ -1,27 +0,0 @@
import 'package:hash_wallet/core/auth_service.dart';
import 'package:hash_wallet/core/secure_storage.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/authentication_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/store/wallet_list_store.dart';
import 'package:hash_wallet/view_model/link_view_model.dart';
import 'package:hive/hive.dart';
import 'package:mocktail/mocktail.dart';
class MockAppStore extends Mock implements AppStore {}
class MockAuthService extends Mock implements AuthService {}
class MockSettingsStore extends Mock implements SettingsStore {}
class MockAuthenticationStore extends Mock implements AuthenticationStore {}
class MockWalletListStore extends Mock implements WalletListStore {}
class MockLinkViewModel extends Mock implements LinkViewModel {}
class MockHiveInterface extends Mock implements HiveInterface {}
class MockHiveBox extends Mock implements Box<dynamic> {}
class MockSecureStorage extends Mock implements SecureStorage {}
@@ -1,99 +0,0 @@
import 'package:hash_wallet/core/auth_service.dart';
import 'package:hash_wallet/core/secure_storage.dart';
import 'package:hash_wallet/di.dart';
import 'package:hash_wallet/store/app_store.dart';
import 'package:hash_wallet/store/authentication_store.dart';
import 'package:hash_wallet/store/settings_store.dart';
import 'package:hash_wallet/store/wallet_list_store.dart';
import 'package:hash_wallet/view_model/link_view_model.dart';
import 'package:hive/hive.dart';
import 'package:mocktail/mocktail.dart';
import 'mocks.dart';
class TestHelpers {
static void setup() {
// Fallback values can also be declared here
registerDependencies();
}
static void registerDependencies() {
getAndRegisterAppStore();
getAndRegisterAuthService();
getAndRegisterSettingsStore();
getAndRegisterAuthenticationStore();
getAndRegisterWalletListStore();
getAndRegisterLinkViewModel();
getAndRegisterSecureStorage();
getAndRegisterHiveInterface();
}
static MockSettingsStore getAndRegisterSettingsStore() {
_removeRegistrationIfExists<SettingsStore>();
final service = MockSettingsStore();
getIt.registerSingleton<SettingsStore>(service);
return service;
}
static MockAppStore getAndRegisterAppStore() {
_removeRegistrationIfExists<AppStore>();
final service = MockAppStore();
final settingsStore = getAndRegisterSettingsStore();
when(() => service.settingsStore).thenAnswer((invocation) => settingsStore);
getIt.registerSingleton<AppStore>(service);
return service;
}
static MockAuthService getAndRegisterAuthService() {
_removeRegistrationIfExists<AuthService>();
final service = MockAuthService();
getIt.registerSingleton<AuthService>(service);
return service;
}
static MockAuthenticationStore getAndRegisterAuthenticationStore() {
_removeRegistrationIfExists<AuthenticationStore>();
final service = MockAuthenticationStore();
when(() => service.state).thenReturn(AuthenticationState.uninitialized);
getIt.registerSingleton<AuthenticationStore>(service);
return service;
}
static MockWalletListStore getAndRegisterWalletListStore() {
_removeRegistrationIfExists<WalletListStore>();
final service = MockWalletListStore();
getIt.registerSingleton<WalletListStore>(service);
return service;
}
static MockLinkViewModel getAndRegisterLinkViewModel() {
_removeRegistrationIfExists<LinkViewModel>();
final service = MockLinkViewModel();
getIt.registerSingleton<LinkViewModel>(service);
return service;
}
static MockHiveInterface getAndRegisterHiveInterface() {
_removeRegistrationIfExists<HiveInterface>();
final service = MockHiveInterface();
getIt.registerSingleton<HiveInterface>(service);
return service;
}
static MockSecureStorage getAndRegisterSecureStorage() {
_removeRegistrationIfExists<SecureStorage>();
final service = MockSecureStorage();
getIt.registerSingleton<SecureStorage>(service);
return service;
}
static void _removeRegistrationIfExists<T extends Object>() {
if (getIt.isRegistered<T>()) {
getIt.unregister<T>();
}
}
static void tearDown() => getIt.reset();
}
@@ -1 +0,0 @@
null
+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");
}
}
@@ -0,0 +1,46 @@
import "package:cw_core/wallet_type.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../core/test_wallets.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/contact_robot.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("A saved contact keeps the address it was given", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final contactRobot = ContactRobot(tester);
const contactName = "Integration Test Contact";
final contactAddress = TestWallets.receiveAddressFor(WalletType.solana);
await appLauncher.launchApp(testKey: "address_book_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await dashboardRobot.openContactsTab();
await contactRobot.isDisplayed();
await contactRobot.openAddContact();
await contactRobot.enterName(contactName);
await contactRobot.chooseCurrency("sol");
await contactRobot.enterAddress(contactAddress);
await contactRobot.save();
await contactRobot.isDisplayed();
contactRobot.expectContactSaved(contactName, contactAddress);
});
}
@@ -0,0 +1,73 @@
import "package:hash_wallet/routes.dart";
import "package:hash_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../core/test_config.dart";
import "../../flows/auth_flows.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_settings_page_robot.dart";
import "../../robots/security_backup_page_robot.dart";
import "../../robots/setup_pin_code_robot.dart";
import "../../robots/show_keys_disclaimer_page_robot.dart";
import "../../robots/wallet_keys_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Changing the pin replaces the one that unlocks the wallet", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final authFlows = AuthFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final settingsRobot = NewSettingsPageRobot(tester);
final securityRobot = SecurityBackupPageRobot(tester);
final setupPinRobot = SetupPinCodeRobot(tester);
final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
final newPin = [1, 2, 3, 4];
expect(newPin, isNot(TestConfig.pin), reason: "The new pin has to differ from the old one");
await appLauncher.launchApp(testKey: "change_pin_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.openSettingsSheet();
await settingsRobot.isDisplayed();
await settingsRobot.openRow(Routes.securityBackupPage);
await securityRobot.isDisplayed();
await securityRobot.openChangePin();
await authFlows.authenticateWithPin();
await setupPinRobot.isDisplayed();
await setupPinRobot.enterPinCode(newPin);
await setupPinRobot.enterPinCode(newPin);
await setupPinRobot.tapSuccessButton();
await securityRobot.isDisplayed();
await securityRobot.goBack();
await settingsRobot.isDisplayed();
await settingsRobot.openRow(Routes.showKeysDisclaimer);
await disclaimerRobot.isDisplayed();
await disclaimerRobot.tapShowKeys();
await authFlows.authenticateWithPin(pin: newPin);
await walletKeysRobot.isDisplayed();
expect(find.byType(WalletKeysPage), findsOneWidget);
});
}
@@ -0,0 +1,42 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_wallet_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("A wallet name already in use is refused", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final newWalletPageRobot = NewWalletPageRobot(tester);
const walletType = WalletType.solana;
await appLauncher.launchApp(testKey: "duplicate_wallet_name_test_app_key");
await onboardingFlows.createFirstWallet(walletType);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final existingName = getIt.get<AppStore>().wallet!.name;
await dashboardRobot.openWalletsTab();
await onboardingFlows.startCreatingWalletFromWalletList(walletType);
await newWalletPageRobot.enterWalletName(existingName);
await newWalletPageRobot.onNextButtonPressed();
await newWalletPageRobot.expectNameRejected();
});
}
@@ -0,0 +1,107 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/entities/fiat_api_mode.dart";
import "package:hash_wallet/entities/fiat_currency.dart";
import "package:hash_wallet/routes.dart";
import "package:hash_wallet/store/settings_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/connection_sync_page_robot.dart";
import "../../robots/display_settings_page_robot.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_settings_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Turning the fiat api off hides the currency setting, turning it on returns it",
(tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final settingsRobot = NewSettingsPageRobot(tester);
final connectionRobot = ConnectionSyncPageRobot(tester);
final displayRobot = DisplaySettingsPageRobot(tester);
const wanted = FiatCurrency.eur;
await appLauncher.launchApp(testKey: "fiat_currency_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final settingsStore = getIt.get<SettingsStore>();
expect(
settingsStore.fiatCurrency,
isNot(wanted),
reason: "The wallet already starts on $wanted, so changing to it would prove nothing",
);
await homePageRobot.openSettingsSheet();
await settingsRobot.isDisplayed();
if (settingsStore.fiatApiMode != FiatApiMode.disabled) {
await settingsRobot.openRow(Routes.connectionSync);
await connectionRobot.isDisplayed();
await connectionRobot.setFiatApiMode(FiatApiMode.disabled);
connectionRobot.hasFiatApiMode(FiatApiMode.disabled);
expect(settingsStore.fiatApiMode, FiatApiMode.disabled);
await connectionRobot.goBack();
await settingsRobot.isDisplayed();
}
await settingsRobot.openRow(Routes.displaySettingsPage);
await displayRobot.isDisplayed();
expect(
displayRobot.hasFiatCurrencyRow(),
false,
reason: "The currency setting is still showing while the fiat api is off",
);
await displayRobot.goBack();
await settingsRobot.isDisplayed();
await settingsRobot.openRow(Routes.connectionSync);
await connectionRobot.isDisplayed();
await connectionRobot.setFiatApiMode(FiatApiMode.enabled);
connectionRobot.hasFiatApiMode(FiatApiMode.enabled);
expect(settingsStore.fiatApiMode, FiatApiMode.enabled);
await connectionRobot.goBack();
await settingsRobot.isDisplayed();
await settingsRobot.openRow(Routes.displaySettingsPage);
await displayRobot.isDisplayed();
expect(
displayRobot.hasFiatCurrencyRow(),
true,
reason: "The currency setting did not come back after the fiat api was turned on",
);
await displayRobot.openFiatCurrencyPicker();
await displayRobot.chooseFiatCurrency(wanted);
displayRobot.expectFiatCurrencyShown(wanted);
expect(
settingsStore.fiatCurrency,
wanted,
reason: "The row shows ${wanted.title} but the setting behind it did not change",
);
});
}
@@ -0,0 +1,51 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../core/test_wallets.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/restore_from_seed_or_key_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("A seed the wallet cannot parse restores nothing", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final restoreRobot = RestoreFromSeedOrKeysPageRobot(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
const walletType = WalletType.solana;
await appLauncher.launchApp(testKey: "invalid_seed_test_app_key");
await onboardingFlows.startRestoringFirstWallet(walletType);
// Twelve words that are not in the wordlist. Restoring from this would either fail
// outright or, worse it derives some other wallet the user has no keys for.
await restoreRobot.enterSeedPhraseForWalletRestore(
"alpha bravo charlie delta echo foxtrot golf hotel india juliet kilo lima",
);
await restoreRobot.onRestoreWalletButtonPressed();
await restoreRobot.expectRestoreRefused();
// The same form with the real seed goes through. Without this the test above would pass
// just as happily if the form were broken and restored nothing for any seed at all.
await restoreRobot.selectWalletNameFromAvailableOptions();
await restoreRobot.enterSeedPhraseForWalletRestore(TestWallets.seedFor(walletType));
await restoreRobot.onRestoreWalletButtonPressed();
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
expect(getIt.get<AppStore>().wallet?.type, walletType);
});
}
@@ -0,0 +1,46 @@
import "package:hash_wallet/routes.dart";
import "package:cw_core/wallet_type.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/display_settings_page_robot.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_settings_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Changing the language changes what the settings screen shows", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final settingsRobot = NewSettingsPageRobot(tester);
final displayRobot = DisplaySettingsPageRobot(tester);
// Not the default, so seeing it afterwards means the change took rather than the screen
// having always said so.
const languageCode = "de";
const languageName = "Deutsch (German)";
await appLauncher.launchApp(testKey: "language_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.openSettingsSheet();
await settingsRobot.isDisplayed();
await settingsRobot.openRow(Routes.displaySettingsPage);
await displayRobot.isDisplayed();
await displayRobot.openLanguagePicker();
await displayRobot.chooseLanguage(languageCode);
displayRobot.expectLanguageShown(languageName);
});
}
@@ -0,0 +1,53 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../core/test_config.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Create a wallet for each configured wallet type", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
await appLauncher.launchApp(testKey: "onboarding_create_test_app_key");
final walletTypes = TestConfig.walletTypesUnderTest;
final firstType = walletTypes.first;
tester.printToConsole("Creating first wallet: ${firstType.name}");
await onboardingFlows.createFirstWallet(firstType);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final appStore = getIt.get<AppStore>();
expect(appStore.wallet?.type, firstType);
await homePageRobot.hasWalletName(appStore.wallet!.name);
for (final type in walletTypes.skip(1)) {
tester.printToConsole("Creating additional wallet: ${type.name}");
await dashboardRobot.openWalletsTab();
await onboardingFlows.createAdditionalWalletFromWalletList(type);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
expect(appStore.wallet?.type, type);
await homePageRobot.hasWalletName(appStore.wallet!.name);
}
});
}
@@ -0,0 +1,94 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../core/test_config.dart";
import "../../core/test_wallets.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Restore a wallet from seed for each configured wallet type", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
await appLauncher.launchApp(testKey: "onboarding_restore_test_app_key");
final walletTypes = TestConfig.walletTypesUnderTest;
final firstType = walletTypes.first;
tester.printToConsole("Restoring first wallet: ${firstType.name}");
await onboardingFlows.restoreFirstWalletFromSeed(firstType);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final appStore = getIt.get<AppStore>();
expect(appStore.wallet?.type, firstType);
await homePageRobot.hasWalletName(appStore.wallet!.name);
await _expectRestoredTheRightWallet(homePageRobot, appStore, firstType);
for (final type in walletTypes.skip(1)) {
tester.printToConsole("Restoring additional wallet: ${type.name}");
await dashboardRobot.openWalletsTab();
await onboardingFlows.restoreAdditionalWalletFromWalletList(type);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
expect(appStore.wallet?.type, type);
await homePageRobot.hasWalletName(appStore.wallet!.name);
await _expectRestoredTheRightWallet(homePageRobot, appStore, type);
}
});
}
Future<void> _expectRestoredTheRightWallet(
HomePageRobot robot,
AppStore appStore,
WalletType type,
) async {
final expectedAddress = TestWallets.receiveAddressFor(type);
if (expectedAddress.isEmpty) {
robot.tester.printToConsole("No known address for ${type.name}, derivation not verified");
return;
}
final settled = await robot.pumpUntil(
() {
final addresses = appStore.wallet?.walletAddresses;
if (addresses == null) {
return false;
}
return addresses.address == expectedAddress ||
addresses.addressesMap.containsKey(expectedAddress) ||
addresses.allAddressesMap.containsKey(expectedAddress);
},
timeout: const Duration(seconds: 60),
);
expect(
settled,
true,
reason: "${type.name} restored from a known seed derived "
"${appStore.wallet?.walletAddresses.address} and does not know $expectedAddress",
);
}
@@ -0,0 +1,43 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_receive_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Receive sheet shows the opened wallet's own address", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final receiveRobot = NewReceivePageRobot(tester);
const walletType = WalletType.solana;
await appLauncher.launchApp(testKey: "receive_address_test_app_key");
await onboardingFlows.restoreFirstWalletFromSeed(walletType);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final expectedAddress = getIt.get<AppStore>().wallet!.walletAddresses.address;
expect(expectedAddress.isNotEmpty, true);
await homePageRobot.openReceiveSheet();
await receiveRobot.isDisplayed();
await receiveRobot.confirmAddressMatches(expectedAddress);
await receiveRobot.dismissModal();
await homePageRobot.isDisplayed();
});
}
@@ -0,0 +1,70 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/routes.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/auth_flows.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_settings_page_robot.dart";
import "../../robots/show_keys_disclaimer_page_robot.dart";
import "../../robots/wallet_keys_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Displayed seed and keys match the opened wallet", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final authFlows = AuthFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final settingsRobot = NewSettingsPageRobot(tester);
final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
// These three cover the different key display paths on the wallet keys page.
final walletTypes = [WalletType.solana, WalletType.bitcoin, WalletType.monero];
await appLauncher.launchApp(testKey: "seed_confirmation_test_app_key");
await onboardingFlows.createFirstWallet(walletTypes.first);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final appStore = getIt.get<AppStore>();
for (final type in walletTypes) {
if (type != walletTypes.first) {
await dashboardRobot.openWalletsTab();
await onboardingFlows.createAdditionalWalletFromWalletList(type);
await homePageRobot.isDisplayed();
}
expect(appStore.wallet?.type, type);
tester.printToConsole("Confirming displayed credentials for ${type.name}");
await homePageRobot.openSettingsSheet();
await settingsRobot.isDisplayed();
await settingsRobot.openRow(Routes.showKeysDisclaimer);
await disclaimerRobot.isDisplayed();
await disclaimerRobot.tapShowKeys();
await authFlows.authenticateWithPin();
await walletKeysRobot.isDisplayed();
await walletKeysRobot.confirmWalletCredentials(type);
await settingsRobot.goBack();
await settingsRobot.dismissModal();
await homePageRobot.isDisplayed();
}
});
}
@@ -0,0 +1,59 @@
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../core/test_wallets.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/contact_robot.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_send_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("A contact picked on the send screen fills in its own address", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final contactRobot = ContactRobot(tester);
final sendRobot = NewSendPageRobot(tester);
const contactName = "Send Book Contact";
final contactAddress = TestWallets.receiveAddressFor(WalletType.solana);
await appLauncher.launchApp(testKey: "send_from_address_book_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await dashboardRobot.openContactsTab();
await contactRobot.isDisplayed();
await contactRobot.openAddContact();
await contactRobot.enterName(contactName);
await contactRobot.chooseCurrency("sol");
await contactRobot.enterAddress(contactAddress);
await contactRobot.save();
await contactRobot.isDisplayed();
await dashboardRobot.openHomeTab();
await homePageRobot.isDisplayed();
await homePageRobot.openSendSheet();
await sendRobot.isDisplayed();
await sendRobot.pickContactFromAddressBook(contactName);
expect(
sendRobot.enteredAddress(),
contactAddress,
reason: "The send screen was filled with an address the contact does not hold",
);
});
}
@@ -0,0 +1,53 @@
import "package:cw_core/wallet_type.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../core/test_wallets.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_send_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Send screen refuses what it cannot send", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final sendRobot = NewSendPageRobot(tester);
const walletType = WalletType.solana;
await appLauncher.launchApp(testKey: "send_validation_test_app_key");
await onboardingFlows.createFirstWallet(walletType);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.openSendSheet();
await sendRobot.isDisplayed();
await sendRobot.tapSendButtonWhenReady(timeout: const Duration(minutes: 5));
await sendRobot.expectNoTransactionBuilt();
// test that no transaction is built when the address is wrong or cannot be parsed
await sendRobot.enterAddress("not-a-real-address");
await sendRobot.enterAmount("0.0001");
await sendRobot.tapSendButton();
await sendRobot.expectNoTransactionBuilt();
// test that no transaction is built even if the address is valid but no balance
await sendRobot.enterAddress(TestWallets.receiveAddressFor(walletType));
await sendRobot.enterAmount("0.0001");
await sendRobot.tapSendButton();
await sendRobot.expectNoTransactionBuilt();
await sendRobot.dismissModal();
await homePageRobot.isDisplayed();
});
}
@@ -0,0 +1,61 @@
import "package:hash_wallet/new-ui/pages/about_page.dart";
import "package:hash_wallet/routes.dart";
import "package:hash_wallet/src/screens/settings/connection_sync_page.dart";
import "package:hash_wallet/src/screens/settings/display_settings_page.dart";
import "package:hash_wallet/src/screens/settings/manage_nodes_page.dart";
import "package:hash_wallet/src/screens/settings/other_settings_page.dart";
import "package:hash_wallet/src/screens/settings/privacy_page.dart";
import "package:hash_wallet/src/screens/settings/security_backup_page.dart";
import "package:cw_core/wallet_type.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_settings_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Every settings row opens its page and backs out", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final settingsRobot = NewSettingsPageRobot(tester);
final rowsUnderTest = <String, Type>{
Routes.manageNodes: ManageNodesPage,
Routes.privacyPage: PrivacyPage,
Routes.otherSettingsPage: OtherSettingsPage,
Routes.connectionSync: ConnectionSyncPage,
Routes.displaySettingsPage: DisplaySettingsPage,
Routes.securityBackupPage: SecurityBackupPage,
Routes.aboutPage: AboutPage,
};
await appLauncher.launchApp(testKey: "settings_nav_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.openSettingsSheet();
await settingsRobot.isDisplayed();
for (final entry in rowsUnderTest.entries) {
tester.printToConsole("Opening settings row ${entry.key}");
await settingsRobot.openRow(entry.key);
await settingsRobot.confirmLeafPageDisplayed(entry.value);
await settingsRobot.goBack();
await settingsRobot.isDisplayed();
}
await settingsRobot.dismissModal();
await homePageRobot.isDisplayed();
});
}
@@ -0,0 +1,61 @@
import "package:hash_wallet/routes.dart";
import "package:hash_wallet/src/screens/wallet_keys/wallet_keys_page.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/auth_flows.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_settings_page_robot.dart";
import "../../robots/show_keys_disclaimer_page_robot.dart";
import "../../robots/wallet_keys_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Seed and keys page rejects a wrong pin and accepts the right one",
(tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final authFlows = AuthFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final settingsRobot = NewSettingsPageRobot(tester);
final walletKeysRobot = WalletKeysAndSeedPageRobot(tester);
final disclaimerRobot = ShowKeysDisclaimerPageRobot(tester);
final wrongPin = [9, 9, 9, 9];
await appLauncher.launchApp(testKey: "show_keys_auth_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.openSettingsSheet();
await settingsRobot.isDisplayed();
await settingsRobot.openRow(Routes.showKeysDisclaimer);
await disclaimerRobot.isDisplayed();
await disclaimerRobot.tapShowKeys();
await authFlows.authenticateWithPin(pin: wrongPin);
// A wrong pin must keep the keys page locked away.
await settingsRobot.settle(max: const Duration(seconds: 3));
expect(find.byType(WalletKeysPage), findsNothing);
expect(authFlows.isPinWidgetShown(), true);
await authFlows.authenticateWithPin();
await walletKeysRobot.isDisplayed();
await settingsRobot.goBack();
await settingsRobot.dismissModal();
await homePageRobot.isDisplayed();
});
}
@@ -0,0 +1,85 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/auth_flows.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/wallet_edit_page_robot.dart";
import "../../robots/wallet_list_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Deleting a wallet takes it off the list and out of storage", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final authFlows = AuthFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final walletListRobot = WalletListPageRobot(tester);
final walletEditRobot = WalletEditPageRobot(tester);
await appLauncher.launchApp(testKey: "wallet_delete_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final appStore = getIt.get<AppStore>();
final deletedWallet = appStore.wallet!.name;
await dashboardRobot.openWalletsTab();
await onboardingFlows.createAdditionalWalletFromWalletList(WalletType.ethereum);
await homePageRobot.isDisplayed();
final keptWallet = appStore.wallet!.name;
expect(keptWallet, isNot(deletedWallet), reason: "The second wallet never became the open one");
await dashboardRobot.openWalletsTab();
await walletListRobot.isDisplayed();
await walletListRobot.openEditFor(deletedWallet);
await walletEditRobot.isDisplayed();
await walletEditRobot.tapDelete();
await authFlows.authenticateWithPin(required: false);
await walletEditRobot.confirmDelete();
await walletListRobot.isDisplayed();
final removed = await walletListRobot.pumpUntil(
() => !walletListRobot.hasWallet(deletedWallet),
timeout: const Duration(minutes: 1),
);
expect(removed, true, reason: "$deletedWallet is still on the list after being deleted");
expect(
walletListRobot.hasWallet(keptWallet),
true,
reason: "Deleting $deletedWallet took $keptWallet with it",
);
final stored = await WalletInfo.getAll();
final names = stored.map((wallet) => wallet.name).toList();
expect(names, isNot(contains(deletedWallet)));
expect(names, contains(keptWallet));
expect(
appStore.wallet?.name,
keptWallet,
reason: "Deleting another wallet changed which one is open",
);
});
}
@@ -0,0 +1,70 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../flows/wallet_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("A wallet added to an existing seed shares it and both stay reachable",
(tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final walletFlows = WalletFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
const firstType = WalletType.solana;
const secondType = WalletType.ethereum;
await appLauncher.launchApp(testKey: "wallet_group_test_app_key");
await onboardingFlows.createFirstWallet(firstType);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final appStore = getIt.get<AppStore>();
final firstWalletName = appStore.wallet!.name;
final sharedSeed = appStore.wallet!.seed;
expect(sharedSeed, isNotNull, reason: "The first wallet has no seed to share");
await dashboardRobot.openWalletsTab();
await onboardingFlows.createWalletInGroupOf(secondType, firstWalletName);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final secondWalletName = appStore.wallet!.name;
expect(
secondWalletName,
isNot(firstWalletName),
reason: "Adding to a group opened the wallet that was already there",
);
expect(appStore.wallet?.type, secondType);
expect(
appStore.wallet!.seed,
sharedSeed,
reason: "The wallet added to the group derived its own seed instead of sharing one",
);
await walletFlows.switchToWallet(firstWalletName);
await homePageRobot.isDisplayed();
await homePageRobot.hasWalletName(firstWalletName);
expect(appStore.wallet?.seed, sharedSeed);
});
}
@@ -0,0 +1,86 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_info.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/wallet_edit_page_robot.dart";
import "../../robots/wallet_list_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("A wallet can be renamed, but not onto a name already in use", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final walletListRobot = WalletListPageRobot(tester);
final walletEditRobot = WalletEditPageRobot(tester);
const renamedTo = "Renamed Test Wallet";
await appLauncher.launchApp(testKey: "wallet_rename_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final appStore = getIt.get<AppStore>();
final renamedWallet = appStore.wallet!.name;
await dashboardRobot.openWalletsTab();
await onboardingFlows.createAdditionalWalletFromWalletList(WalletType.ethereum);
await homePageRobot.isDisplayed();
final openWallet = appStore.wallet!.name;
expect(openWallet, isNot(renamedWallet), reason: "The second wallet never became the open one");
await dashboardRobot.openWalletsTab();
await walletListRobot.isDisplayed();
await walletListRobot.openEditFor(renamedWallet);
await walletEditRobot.isDisplayed();
await walletEditRobot.enterName(openWallet);
await walletEditRobot.save();
await walletEditRobot.expectNameTakenRefusal();
await walletEditRobot.dismissNameTakenRefusal();
await walletEditRobot.enterName(renamedTo);
await walletEditRobot.save();
await walletListRobot.isDisplayed();
final renamed = await walletListRobot.pumpUntil(() => walletListRobot.hasWallet(renamedTo));
expect(renamed, true, reason: "The list never showed $renamedTo after the rename was saved");
expect(
walletListRobot.hasWallet(renamedWallet),
false,
reason: "The list still shows $renamedWallet next to the name it was renamed to",
);
final stored = await WalletInfo.getAll();
final names = stored.map((wallet) => wallet.name).toList();
expect(names, contains(renamedTo));
expect(names, isNot(contains(renamedWallet)));
expect(
appStore.wallet?.name,
openWallet,
reason: "Renaming another wallet changed which one is open",
);
});
}
@@ -0,0 +1,47 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../core/test_wallets.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("A 25 word monero seed restores through the legacy path", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final legacySeed = TestWallets.moneroLegacySeed;
expect(
legacySeed.split(" ").length,
25,
reason: "The legacy seed secret is not a 25 word seed, this suite would restore a polyseed",
);
await appLauncher.launchApp(testKey: "monero_legacy_seed_test_app_key");
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.monero, seed: legacySeed);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
final appStore = getIt.get<AppStore>();
expect(appStore.wallet?.type, WalletType.monero);
expect(
appStore.wallet?.seed?.split(" ").length,
25,
reason: "The wallet came back with a seed of a different length than the one restored",
);
});
}
@@ -0,0 +1,49 @@
import "package:hash_wallet/routes.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/manage_nodes_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_settings_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Switching nodes changes which one the wallet is pointed at", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final settingsRobot = NewSettingsPageRobot(tester);
final nodesRobot = ManageNodesPageRobot(tester);
await appLauncher.launchApp(testKey: "node_switching_test_app_key");
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.openSettingsSheet();
await settingsRobot.isDisplayed();
await settingsRobot.openRow(Routes.manageNodes);
await nodesRobot.isDisplayed();
final initialNodeUri = nodesRobot.currentNodeUri();
expect(initialNodeUri, isNotEmpty, reason: "The wallet started with no node at all");
await nodesRobot.switchToAnotherNode();
expect(
nodesRobot.currentNodeUri(),
isNot(initialNodeUri),
reason: "The node was confirmed but the wallet is still pointed at $initialNodeUri",
);
});
}
@@ -0,0 +1,37 @@
import "package:cw_core/wallet_type.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
import "../../robots/new_swap_page_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Swap sheet fetches a provider quote, no trade is created", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
final swapRobot = NewSwapPageRobot(tester);
await appLauncher.launchApp(testKey: "swap_quote_test_app_key");
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.openSwapSheet();
await swapRobot.isDisplayed();
await swapRobot.enterDepositAmount("1");
await swapRobot.confirmQuoteReceived();
await swapRobot.dismissModal();
await homePageRobot.isDisplayed();
});
}
@@ -0,0 +1,27 @@
import "package:cw_core/wallet_type.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Restored wallet connects to a node and starts syncing", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
await appLauncher.launchApp(testKey: "sync_status_test_app_key");
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.bitcoin);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.confirmSyncIndicatorShown();
});
}
@@ -0,0 +1,47 @@
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Tapping a transaction opens the details of that transaction", (tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
await appLauncher.launchApp(testKey: "transaction_details_test_app_key");
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
await homePageRobot.confirmTransactionHistoryVisible();
await homePageRobot.openAllTransactions();
await homePageRobot.confirmAllTransactionsVisible();
final tapped = homePageRobot.firstTransactionIdInAllView();
expect(tapped, isNotEmpty, reason: "The tile carries no transaction id to check against");
await homePageRobot.openFirstTransactionDetails();
// A list that hands the wrong transaction to the details screen shows one person's
// amounts under another's id, and nothing on the screen would say so.
expect(
homePageRobot.openedTransactionId(),
tapped,
reason: "The details opened for a different transaction than the one tapped",
);
homePageRobot.hasTransactionIdRow();
});
}
@@ -0,0 +1,37 @@
import "package:hash_wallet/di.dart";
import "package:hash_wallet/store/app_store.dart";
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("Transaction history renders for a wallet with known transactions",
(tester) async {
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
await appLauncher.launchApp(testKey: "transaction_history_test_app_key");
await onboardingFlows.restoreFirstWalletFromSeed(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
expect(getIt.get<AppStore>().wallet?.type, WalletType.solana);
await homePageRobot.confirmTransactionHistoryVisible();
await homePageRobot.openAllTransactions();
await homePageRobot.confirmAllTransactionsVisible();
});
}
@@ -0,0 +1,55 @@
// Copy this file into integration_test/suites/tier0/ or tier1/, rename it to
// <feature>_test.dart and replace the example steps with your journey.
// The runner only picks up files ending in _test.dart, this template never runs.
import "package:cw_core/wallet_type.dart";
import "package:flutter_test/flutter_test.dart";
import "package:integration_test/integration_test.dart";
import "../../core/app_launcher.dart";
import "../../flows/onboarding_flows.dart";
import "../../robots/home_page_robot.dart";
import "../../robots/new_dashboard_robot.dart";
void main() {
// Required first line in every suite, integrationTest contains background app errors
// that testWidgets would turn into failures.
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
integrationTest("One sentence describing the journey under test", (tester) async {
// Robots and flows take the tester, create the ones your journey needs.
final appLauncher = AppLauncher(tester);
final onboardingFlows = OnboardingFlows(tester);
final dashboardRobot = NewDashboardRobot(tester);
final homePageRobot = HomePageRobot(tester);
// Boots the real app, the key names the suite in failure output.
await appLauncher.launchApp(testKey: "example_feature_test_app_key");
// Every suite starts from a fresh install, so get to a working wallet first.
// Use restoreFirstWalletFromSeed instead when your feature needs a known wallet.
await onboardingFlows.createFirstWallet(WalletType.solana);
await dashboardRobot.isDisplayed();
await homePageRobot.isDisplayed();
// Drive your feature through its robot from here. Robots own the finders and
// assertions for their screen, the suite only sequences them:
//
// await homePageRobot.openSettingsSheet();
// await mySettingsRobot.isDisplayed();
// await mySettingsRobot.doTheThing();
//
// Rules of thumb:
// - Wait with pumpUntilFound and pumpUntil, never with sleeps or pumpAndSettle.
// - Tap and read widgets by ValueKey, add keys to lib/ widgets when they are missing.
// - Assert app state through getIt (AppStore) or the page widget's view model.
// - A widget being in the tree does not mean it is on screen. Every dashboard tab
// stays mounted in an IndexedStack, so find.byType on a tab passes from any other
// tab. Check what is in front, the way WalletListPageRobot does.
// - A button being findable does not mean it is enabled. Tapping a disabled button
// does nothing at all and reads later as a missing widget somewhere else, so wait
// on whatever the screen itself gates on.
// - Add the finished suite to the table in docs/INTEGRATION_TESTS.md.
});
}
@@ -1,122 +0,0 @@
import 'package:hash_wallet/wallet_types.g.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../components/common_test_constants.dart';
import '../components/common_test_flows.dart';
import '../robots/auth_page_robot.dart';
import '../robots/dashboard_page_robot.dart';
import '../robots/security_and_backup_page_robot.dart';
import '../robots/wallet_keys_robot.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
AuthPageRobot authPageRobot;
CommonTestFlows commonTestFlows;
DashboardPageRobot dashboardPageRobot;
WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot;
SecurityAndBackupPageRobot securityAndBackupPageRobot;
testWidgets(
'Confirm if the seeds display properly',
(tester) async {
// Store the original FlutterError.onError handler
final originalOnError = FlutterError.onError;
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('FlutterError caught: ${details.exception}');
};
try {
authPageRobot = AuthPageRobot(tester);
commonTestFlows = CommonTestFlows(tester);
dashboardPageRobot = DashboardPageRobot(tester);
walletKeysAndSeedPageRobot = WalletKeysAndSeedPageRobot(tester);
securityAndBackupPageRobot = SecurityAndBackupPageRobot(tester);
await commonTestFlows.startAppFlow(
ValueKey('confirm_creds_display_correctly_flow_app_key'),
);
await commonTestFlows.welcomePageToCreateNewWalletFlow(
WalletType.solana,
CommonTestConstants.pin,
);
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
await _confirmSeedsFlowForWalletType(
WalletType.solana,
authPageRobot,
dashboardPageRobot,
securityAndBackupPageRobot,
walletKeysAndSeedPageRobot,
tester,
);
// Do the same for other available wallet types
for (var walletType in availableWalletTypes) {
if (walletType == WalletType.solana) {
continue;
}
await dashboardPageRobot.navigateToWalletsListPage(isDrawer: true);
await commonTestFlows.createNewWalletFromWalletMenu(walletType);
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
await _confirmSeedsFlowForWalletType(
walletType,
authPageRobot,
dashboardPageRobot,
securityAndBackupPageRobot,
walletKeysAndSeedPageRobot,
tester,
);
}
await Future.delayed(Duration(seconds: 15));
} finally {
FlutterError.onError = originalOnError;
}
},
);
}
Future<void> _confirmSeedsFlowForWalletType(
WalletType walletType,
AuthPageRobot authPageRobot,
DashboardPageRobot dashboardPageRobot,
SecurityAndBackupPageRobot securityAndBackupPageRobot,
WalletKeysAndSeedPageRobot walletKeysAndSeedPageRobot,
WidgetTester tester,
) async {
await dashboardPageRobot.openDrawerMenu();
await dashboardPageRobot.dashboardMenuWidgetRobot.navigateToSecurityAndBackupPage();
await securityAndBackupPageRobot.navigateToShowKeysPage();
final onAuthPage = authPageRobot.onAuthPage();
if (onAuthPage) {
await authPageRobot.enterPinCode(CommonTestConstants.pin);
}
final onAuthPageDesktop = authPageRobot.onAuthPageDesktop();
if (onAuthPageDesktop) {
await authPageRobot.enterPassword(CommonTestConstants.pin.join(""));
}
await tester.pumpAndSettle();
await walletKeysAndSeedPageRobot.isWalletKeysAndSeedPage();
walletKeysAndSeedPageRobot.hasTitle();
walletKeysAndSeedPageRobot.hasShareWarning();
await walletKeysAndSeedPageRobot.confirmWalletCredentials(walletType);
await walletKeysAndSeedPageRobot.backToDashboard();
}
@@ -1,60 +0,0 @@
import 'package:hash_wallet/wallet_types.g.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../components/common_test_constants.dart';
import '../components/common_test_flows.dart';
import '../robots/dashboard_page_robot.dart';
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
CommonTestFlows commonTestFlows;
DashboardPageRobot dashboardPageRobot;
testWidgets(
'Create Wallet Flow',
(tester) async {
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('FlutterError caught: ${details.exception}');
};
commonTestFlows = CommonTestFlows(tester);
dashboardPageRobot = DashboardPageRobot(tester);
// Start the app
await commonTestFlows.startAppFlow(
ValueKey('create_wallets_through_seeds_test_app_key'),
);
await commonTestFlows.welcomePageToCreateNewWalletFlow(
WalletType.solana,
CommonTestConstants.pin,
);
// Confirm it actually restores a solana wallet
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
// Do the same for other available wallet types
for (var walletType in availableWalletTypes) {
if (walletType == WalletType.solana) {
continue;
}
await dashboardPageRobot.navigateToWalletsListPage();
await commonTestFlows.createNewWalletFromWalletMenu(walletType);
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
}
// Goes to the wallet menu and provides a confirmation that all the wallets were correctly restored
await dashboardPageRobot.navigateToWalletsListPage();
commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly();
await Future.delayed(Duration(seconds: 5));
},
);
}
@@ -1,65 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../components/common_test_constants.dart';
import '../components/common_test_flows.dart';
import '../robots/dashboard_page_robot.dart';
import '../robots/exchange_confirm_page_robot.dart';
import '../robots/exchange_page_robot.dart';
import '../robots/exchange_trade_external_send_page_robot.dart';
import '../robots/exchange_trade_page_robot.dart';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
CommonTestFlows commonTestFlows;
ExchangePageRobot exchangePageRobot;
DashboardPageRobot dashboardPageRobot;
ExchangeTradePageRobot exchangeTradePageRobot;
ExchangeConfirmPageRobot exchangeConfirmPageRobot;
ExchangeTradeExternalSendPageRobot exchangeTradeExternalSendPageRobot;
testWidgets('Exchange flow', (tester) async {
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('FlutterError caught: ${details.exception}');
};
commonTestFlows = CommonTestFlows(tester);
exchangePageRobot = ExchangePageRobot(tester);
dashboardPageRobot = DashboardPageRobot(tester);
exchangeTradePageRobot = ExchangeTradePageRobot(tester);
exchangeConfirmPageRobot = ExchangeConfirmPageRobot(tester);
exchangeTradeExternalSendPageRobot = ExchangeTradeExternalSendPageRobot(tester);
await commonTestFlows.startAppFlow(ValueKey('exchange_app_test_key'));
await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
CommonTestConstants.testWalletType,
secrets.solanaTestWalletSeeds,
CommonTestConstants.pin,
);
await dashboardPageRobot.navigateToExchangePage();
// ----------- Exchange Page -------------
await exchangePageRobot.selectDepositCurrency(CommonTestConstants.exchangeTestDepositCurrency);
await exchangePageRobot.selectReceiveCurrency(CommonTestConstants.exchangeTestReceiveCurrency);
await exchangePageRobot.enterDepositAmount(CommonTestConstants.exchangeTestAmount);
await exchangePageRobot.enterDepositRefundAddress(
depositAddress: CommonTestConstants.testWalletAddress,
);
await exchangePageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
await exchangePageRobot.onExchangeButtonPressed();
await exchangePageRobot.handleErrors(CommonTestConstants.exchangeTestAmount);
await exchangeConfirmPageRobot.onSavedTradeIdButtonPressed();
await exchangeTradePageRobot.onGotItButtonPressed();
await exchangeTradePageRobot.onSendFromExternalButtonPressed();
await exchangeTradeExternalSendPageRobot.isExchangeTradeExternalSendPage();
await exchangeTradeExternalSendPageRobot.verifySendDetailsItemsDisplayProperly();
await exchangeTradeExternalSendPageRobot.onContinueButtonPressed();
});
}
@@ -1,75 +0,0 @@
import 'package:hash_wallet/wallet_types.g.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../components/common_test_constants.dart';
import '../components/common_test_flows.dart';
import '../robots/dashboard_page_robot.dart';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
CommonTestFlows commonTestFlows;
DashboardPageRobot dashboardPageRobot;
testWidgets(
'Restoring Wallets Through Seeds',
(tester) async {
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('FlutterError caught: ${details.exception}');
};
commonTestFlows = CommonTestFlows(tester);
dashboardPageRobot = DashboardPageRobot(tester);
// Start the app
await commonTestFlows.startAppFlow(
ValueKey('restore_wallets_through_seeds_test_app_key'),
);
// Restore the first wallet type: Solana
await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
WalletType.solana,
secrets.solanaTestWalletSeeds,
CommonTestConstants.pin,
);
// Confirm it actually restores a solana wallet
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
// Do the same for other available wallet types
for (var walletType in availableWalletTypes) {
if (walletType == WalletType.solana) {
continue;
}
final seed = commonTestFlows.getWalletSeedsByWalletType(walletType);
if (seed.isEmpty) {
tester.printToConsole("----------------------------");
tester.printToConsole("- Skipped wallet: ${walletType}");
tester.printToConsole("- Make sure to add seed to secrets");
tester.printToConsole("----------------------------");
continue;
}
await dashboardPageRobot.navigateToWalletsListPage();
await commonTestFlows.restoreWalletFromWalletMenu(
walletType,
seed,
);
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(walletType);
}
// Goes to the wallet menu and provides a visual confirmation that all the wallets were correctly restored
await dashboardPageRobot.navigateToWalletsListPage();
commonTestFlows.confirmAllAvailableWalletTypeIconsDisplayCorrectly();
await Future.delayed(Duration(seconds: 5));
},
);
}
@@ -1,133 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import 'package:cw_core/wallet_type.dart';
import '../components/common_test_constants.dart';
import '../components/common_test_flows.dart';
import '../components/common_test_cases.dart';
import '../robots/dashboard_page_robot.dart';
import '../robots/send_page_robot.dart';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
SendPageRobot sendPageRobot;
CommonTestFlows commonTestFlows;
DashboardPageRobot dashboardPageRobot;
CommonTestCases commonTestCases;
testWidgets('Send flow with wallet switching for insufficient balance - testing \$1 send',
(tester) async {
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('FlutterError caught: ${details.exception}');
};
commonTestFlows = CommonTestFlows(tester);
sendPageRobot = SendPageRobot(tester: tester);
dashboardPageRobot = DashboardPageRobot(tester);
commonTestCases = CommonTestCases(tester);
await commonTestFlows.startAppFlow(ValueKey('send_test_app_key'));
// Various wallet options we want to use to test the send flow
final walletConfigs = [
{
'type': WalletType.solana,
'seed': secrets.solanaTestWalletSeeds2,
'name': 'Solana Wallet 1',
},
{
'type': WalletType.solana,
'seed': secrets.solanaTestWalletSeeds,
'name': 'Solana Wallet 2',
},
{
'type': WalletType.ethereum,
'seed': secrets.ethereumTestWalletSeeds,
'name': 'Ethereum Wallet',
},
];
bool hasSufficientBalance = false;
WalletType? successfulWalletType;
// We try each wallet configuration until we find one with sufficient balance
for (int i = 0; i < walletConfigs.length; i++) {
final config = walletConfigs[i];
if ((config['seed'] as String).isEmpty) {
tester.printToConsole('Skipping ${config['name']}, seed is empty');
continue;
}
tester.printToConsole('Trying ${config['name']}');
// For first wallet, we restore wallet afresh, for subsequent ones, we do a wallet switch
if (i == 0) {
await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
config['type'] as WalletType,
config['seed'] as String,
CommonTestConstants.pin,
);
} else {
await commonTestCases.goBack();
await commonTestCases.defaultSleepTime();
await dashboardPageRobot.navigateToWalletsListPage();
await commonTestCases.defaultSleepTime();
await commonTestFlows.restoreWalletFromWalletMenu(
config['type'] as WalletType,
config['seed'] as String,
);
}
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(config['type'] as WalletType);
// Navigate to send page
await dashboardPageRobot.navigateToSendPage();
await sendPageRobot.checkIfSendPageIsVisible();
await sendPageRobot.enterReceiveAddress(CommonTestConstants.testWalletAddress);
await sendPageRobot.selectReceiveCurrency(CommonTestConstants.sendTestReceiveCurrency);
await sendPageRobot.selectTransactionPriority();
// Main check to see if this wallet has sufficient balance for $1 send
final hasBalance = await sendPageRobot.validateWalletBalanceForOneDollarSend();
if (hasBalance) {
hasSufficientBalance = true;
successfulWalletType = config['type'] as WalletType;
tester.printToConsole(
'${config['name']} has sufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send, proceeding with send',
);
break;
} else {
tester.printToConsole(
'${config['name']} has insufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send',
);
if (i < walletConfigs.length - 1) {
tester.printToConsole('Switching to next wallet...');
}
}
}
// We only proceed with the send if we have sufficient balance
if (hasSufficientBalance && successfulWalletType != null) {
tester.printToConsole('Performing send transaction with $successfulWalletType wallet');
await sendPageRobot.testFiatAmountEntry();
await sendPageRobot.testCryptoAmountEntry();
await sendPageRobot.onSendButtonPressed();
await sendPageRobot.onSendSliderOnConfirmSendingBottomSheetDragged();
await sendPageRobot.handleTransactionSuccessFlow();
} else {
tester.printToConsole(
'Test completed without sending - all available wallets have insufficient balance for \$${CommonTestConstants.sendTestFiatAmount} send',
);
}
});
}
@@ -1,82 +0,0 @@
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:integration_test/integration_test.dart';
import '../components/common_test_constants.dart';
import '../components/common_test_flows.dart';
import '../robots/dashboard_page_robot.dart';
import '../robots/transactions_page_robot.dart';
import 'package:hash_wallet/.secrets.g.dart' as secrets;
void main() {
IntegrationTestWidgetsFlutterBinding.ensureInitialized();
CommonTestFlows commonTestFlows;
DashboardPageRobot dashboardPageRobot;
TransactionsPageRobot transactionsPageRobot;
/// Two Test Scenarios
/// - Fully Synchronizes and display the transaction history either immediately or few seconds after fully synchronizing
/// - Displays the transaction history progressively as synchronizing happens
testWidgets('Transaction history flow', (tester) async {
FlutterError.onError = (FlutterErrorDetails details) {
debugPrint('FlutterError caught: ${details.exception}');
};
commonTestFlows = CommonTestFlows(tester);
dashboardPageRobot = DashboardPageRobot(tester);
transactionsPageRobot = TransactionsPageRobot(tester);
await commonTestFlows.startAppFlow(
ValueKey('confirm_creds_display_correctly_flow_app_key'),
);
/// Test Scenario 1 - Displays transaction history list while synchronizing.
///
/// For Solana/Tron WalletTypes.
await commonTestFlows.welcomePageToRestoreWalletThroughSeedsFlow(
WalletType.solana,
secrets.solanaTestWalletSeeds,
CommonTestConstants.pin,
);
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.solana);
await dashboardPageRobot.swipeDashboardTab(true);
await transactionsPageRobot.isTransactionsPage();
await transactionsPageRobot.confirmTransactionsPageConstantsDisplayProperly();
// Wait time for the first scenario to ensure proper loading
await tester.pump(Duration(seconds: 3));
await tester.pumpAndSettle();
await transactionsPageRobot.confirmTransactionHistoryListDisplaysCorrectly(true);
/// Test Scenario 2 - Displays transaction history list after fully synchronizing.
///
/// For Bitcoin/Monero/Wownero WalletTypes.
await dashboardPageRobot.navigateToWalletsListPage();
await commonTestFlows.restoreWalletFromWalletMenu(
WalletType.bitcoin,
secrets.bitcoinTestWalletSeeds,
);
await dashboardPageRobot.confirmWalletTypeIsDisplayedCorrectly(WalletType.bitcoin);
await dashboardPageRobot.swipeDashboardTab(true);
await transactionsPageRobot.isTransactionsPage();
await transactionsPageRobot.confirmTransactionsPageConstantsDisplayProperly();
// Wait time for the second scenario to ensure proper loading
await tester.pump(Duration(seconds: 3));
await tester.pumpAndSettle();
await transactionsPageRobot.confirmTransactionHistoryListDisplaysCorrectly(false);
});
}