Files
hash-wallet/integration_test/robots/transactions_page_robot.dart
T
jwinterm 04203654f1 Rename Flutter project + binary: cake_wallet -> hash_wallet
The big one. Touches 760+ files. Locks in the user's decision to break
import-line compatibility with upstream cake-tech/cake_wallet in exchange
for a binary named hash_wallet on every platform.

Changes:
- pubspec_description.yaml: project name cake_wallet -> hash_wallet
- 762 Dart files: every 'package:cake_wallet/...' import rewritten to
  'package:hash_wallet/...' (lib/, cw_*/lib/, tool/, test/, integration_test/)
- linux/CMakeLists.txt: BINARY_NAME cake_wallet -> hash_wallet
- windows/CMakeLists.txt: project + BINARY_NAME (CakeWallet -> HashWallet)
- windows/runner/Runner.rc: company / file / product strings to Hash Wallet
  + Such Software LLC copyright
- macos/Runner.xcodeproj/project_base.pbxproj: cake_wallet_new.app refs
  -> hash_wallet.app (macOS output bundle name)
- com.suchsoftware.HashWallet.yml (Flatpak): all binary path refs
  cake_wallet -> hash_wallet (e.g. /app/hash_wallet/hash_wallet)
- linux/com.suchsoftware.HashWallet.desktop: Exec=hash_wallet
- scripts/android/copy_monero_deps.sh: CW_DIR target path
- configure_hash_wallet.sh: error message typo (old filename)

INTENTIONALLY UNCHANGED:
- cw_* subpackage names (cw_core, cw_bitcoin, cw_wownero, etc.) — they
  remain as the historical 'cake wallet' naming; renaming them would
  cascade through every sub-package's internal structure
- Method channel name 'com.cake_wallet/native_utils' in iOS/macOS/Android
  AppDelegates and MainActivity — it's a string identifier that has to
  match the Dart side exactly. Renaming requires both-sides change; it's
  internal-only and not user-visible.
- ghcr.io/cake-tech/cake_wallet:debian13-... docker image references in
  build scripts — that's Cake Labs' published image we use as the build
  environment, not ours.

User data directory now follows the new binary name:
  ~/.local/share/hash_wallet/  (was ~/.local/share/cake_wallet/)
  ~/.config/hash_wallet/       (was ~/.config/cake_wallet/)
Existing users on the old name will need migration if any exist.
2026-05-16 11:33:25 -04:00

440 lines
16 KiB
Dart

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);
}
}