mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
CW-1513/CW-1610: Add account archival and UI improvements (#3600)
* add ability to hide accounts * remove import * merge * merge * fix loading accounts when styleSettings are not saved yet * merge * auto-reformat * small fixes * proper reload on card unhide * block hiding last card * fixup * revert formatting-only changes * liner fixes * fix overflows * card layout as separate class to prevent index errors * fix mini card on addresses page * block opening customizers if wallet is syncing * switch to BlocProvider.value to prevent auto dispose * imports * fix fiat balance * Improve homescreen layout and account access * Add homescreen layout regression tests * improve account management w/ education + archival flows * reorganize wallet-specific settings * preserve account archival during dismissal * fix: align account visuals with design feedback * minor account & archival visual corrections * simplify wallet settings + account UI state * generalise education carousel & dismissal tracking * refactor account UI flows + minor improvements * minor refactors and improvement * pass account balances as Money and format at display * use fresh full balance for account archival check * refactor BalanceCard w/ MoneyText + cleanup * minor card_customizer cleanup * rework card customizer flow --------- Co-authored-by: Robert Malikowski <malikowskirobert@gmail.com> Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
Robert Malikowski
Omar Hatem
parent
9f1f82fdd4
commit
20f1ceb651
@@ -0,0 +1,177 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:cake_wallet/core/execution_state.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/locales/locale.dart";
|
||||
import "package:cake_wallet/src/screens/settings/widgets/account_creation_modal.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_edit_or_create_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mocktail/mocktail.dart";
|
||||
|
||||
class _MockAccountEditOrCreateViewModel extends Mock
|
||||
implements MoneroAccountEditOrCreateViewModel {}
|
||||
|
||||
void main() {
|
||||
late _MockAccountEditOrCreateViewModel viewModel;
|
||||
bool? routeResult;
|
||||
|
||||
setUp(() {
|
||||
viewModel = _MockAccountEditOrCreateViewModel();
|
||||
routeResult = null;
|
||||
});
|
||||
|
||||
Widget creationLauncher() => MaterialApp(
|
||||
localizationsDelegates: localizationDelegates,
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
home: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () async {
|
||||
routeResult = await Navigator.of(context).push<bool>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: AccountCreationModal(
|
||||
viewModel: viewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: const Text("Open account creation"),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
Future<void> openModal(WidgetTester tester) async {
|
||||
await tester.pumpWidget(creationLauncher());
|
||||
await tester.tap(find.text("Open account creation"));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets("shows the Figma account-creation presentation", (tester) async {
|
||||
await openModal(tester);
|
||||
|
||||
expect(find.text("Create Account"), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
"Accounts let you organize your funds into different compartments without needing to create a separate wallet",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
expect(find.text("Continue"), findsOneWidget);
|
||||
expect(find.bySemanticsLabel("Close"), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("passes the entered name to the existing account save operation", (tester) async {
|
||||
when(() => viewModel.save()).thenAnswer((_) async {});
|
||||
when(() => viewModel.state).thenReturn(ExecutedSuccessfullyState());
|
||||
await openModal(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextField), "Savings");
|
||||
await tester.pump();
|
||||
await tester.tap(find.text("Continue"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => viewModel.label = "Savings").called(1);
|
||||
verify(() => viewModel.save()).called(1);
|
||||
expect(routeResult, isTrue);
|
||||
expect(find.byType(AccountCreationModal), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("cannot be dismissed while account creation is in progress", (tester) async {
|
||||
final saveCompleter = Completer<void>();
|
||||
when(() => viewModel.save()).thenAnswer((_) => saveCompleter.future);
|
||||
when(() => viewModel.state).thenReturn(ExecutedSuccessfullyState());
|
||||
await openModal(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextField), "Savings");
|
||||
await tester.pump();
|
||||
await tester.tap(find.text("Continue"));
|
||||
await tester.pump();
|
||||
|
||||
await tester.tap(find.bySemanticsLabel("Close"));
|
||||
await tester.pump();
|
||||
expect(find.byType(AccountCreationModal), findsOneWidget);
|
||||
|
||||
await tester.binding.handlePopRoute();
|
||||
await tester.pump();
|
||||
expect(find.byType(AccountCreationModal), findsOneWidget);
|
||||
|
||||
saveCompleter.complete();
|
||||
await tester.pumpAndSettle();
|
||||
expect(routeResult, isTrue);
|
||||
expect(find.byType(AccountCreationModal), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("shows a save failure and restores safe dismissal", (tester) async {
|
||||
when(() => viewModel.save()).thenAnswer((_) async {});
|
||||
when(() => viewModel.state).thenReturn(FailureState("Could not create account"));
|
||||
await openModal(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextField), "Savings");
|
||||
await tester.pump();
|
||||
await tester.tap(find.text("Continue"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Could not create account"), findsOneWidget);
|
||||
expect(find.byType(AccountCreationModal), findsOneWidget);
|
||||
expect(routeResult, isNull);
|
||||
|
||||
await tester.tap(find.text("OK"));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.bySemanticsLabel("Close"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => viewModel.save()).called(1);
|
||||
expect(find.byType(AccountCreationModal), findsNothing);
|
||||
expect(routeResult, isNull);
|
||||
});
|
||||
|
||||
testWidgets("shows a thrown save error and restores safe dismissal", (tester) async {
|
||||
when(() => viewModel.save()).thenThrow(Exception("Card design failed"));
|
||||
await openModal(tester);
|
||||
|
||||
await tester.enterText(find.byType(TextField), "Savings");
|
||||
await tester.pump();
|
||||
await tester.tap(find.text("Continue"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Exception: Card design failed"), findsOneWidget);
|
||||
expect(find.byType(AccountCreationModal), findsOneWidget);
|
||||
expect(routeResult, isNull);
|
||||
|
||||
await tester.tap(find.text("OK"));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.bySemanticsLabel("Close"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => viewModel.save()).called(1);
|
||||
expect(find.byType(AccountCreationModal), findsNothing);
|
||||
expect(routeResult, isNull);
|
||||
});
|
||||
|
||||
testWidgets("requires a non-empty account name before continuing", (tester) async {
|
||||
await openModal(tester);
|
||||
|
||||
TextButton continueButton() => tester.widget<TextButton>(
|
||||
find.ancestor(
|
||||
of: find.text("Continue"),
|
||||
matching: find.byType(TextButton),
|
||||
),
|
||||
);
|
||||
|
||||
expect(continueButton().onPressed, isNull);
|
||||
|
||||
await tester.enterText(find.byType(TextField), " ");
|
||||
await tester.pump();
|
||||
expect(continueButton().onPressed, isNull);
|
||||
verifyNever(() => viewModel.save());
|
||||
|
||||
await tester.enterText(find.byType(TextField), "Savings");
|
||||
await tester.pump();
|
||||
expect(continueButton().onPressed, isNotNull);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,206 @@
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/locales/locale.dart";
|
||||
import "package:cake_wallet/new-ui/pages/account_education_page.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cake_wallet/themes/core/theme_store.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mocktail/mocktail.dart";
|
||||
|
||||
class _MockSettingsStore extends Mock implements SettingsStore {}
|
||||
|
||||
void main() {
|
||||
late bool registeredThemeStore;
|
||||
late _MockSettingsStore settingsStore;
|
||||
|
||||
setUpAll(() {
|
||||
registeredThemeStore = !getIt.isRegistered<ThemeStore>();
|
||||
if (registeredThemeStore) {
|
||||
getIt.registerSingleton(ThemeStore());
|
||||
}
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
if (registeredThemeStore) {
|
||||
await getIt.unregister<ThemeStore>();
|
||||
}
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
settingsStore = _MockSettingsStore();
|
||||
when(() => settingsStore.dismissEducation(any())).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
Future<void> openEducation(WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: localizationDelegates,
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: Builder(
|
||||
builder: (context) => TextButton(
|
||||
onPressed: () => AccountEducationPage(settingsStore: settingsStore).show(context),
|
||||
child: const Text("Open education"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text("Open education"));
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
Future<void> continueToNextPage(WidgetTester tester) async {
|
||||
await tester.tap(find.text("Continue"));
|
||||
await tester.pump();
|
||||
await tester.pumpAndSettle();
|
||||
}
|
||||
|
||||
testWidgets("first page explains account organization and exposes progress", (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
|
||||
await openEducation(tester);
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
"Accounts let you organize your funds by acting as separate pockets within one wallet",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.text(
|
||||
"You can create one for your regular spending, another for your savings... all accessible from the same homescreen",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.bySemanticsLabel(RegExp("Page 1 of 4")), findsOneWidget);
|
||||
expect(find.text("Continue"), findsOneWidget);
|
||||
verifyNever(() => settingsStore.dismissEducation(any()));
|
||||
|
||||
final title = find.text(
|
||||
"Accounts let you organize your funds by acting as separate pockets within one wallet",
|
||||
);
|
||||
final image = find.byKey(const ValueKey("accounts-education-overview-image"));
|
||||
final description = find.text(
|
||||
"You can create one for your regular spending, another for your savings... all accessible from the same homescreen",
|
||||
);
|
||||
expect(tester.getBottomLeft(title).dy, lessThan(tester.getTopLeft(image).dy));
|
||||
expect(tester.getBottomLeft(image).dy, lessThan(tester.getTopLeft(description).dy));
|
||||
expect(
|
||||
tester.getTopLeft(image).dy - tester.getBottomLeft(title).dy,
|
||||
greaterThanOrEqualTo(24),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(description).dy - tester.getBottomLeft(image).dy,
|
||||
greaterThanOrEqualTo(24),
|
||||
);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets("Continue advances to the recovery education page", (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
|
||||
await openEducation(tester);
|
||||
await continueToNextPage(tester);
|
||||
|
||||
expect(
|
||||
find.text(
|
||||
"Accounts belong together in one wallet, they can all be restored from just one Recovery Phrase",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.bySemanticsLabel(RegExp("Page 2 of 4")), findsOneWidget);
|
||||
verifyNever(() => settingsStore.dismissEducation(any()));
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets("account-order education follows the Figma content sequence", (tester) async {
|
||||
await openEducation(tester);
|
||||
await continueToNextPage(tester);
|
||||
await continueToNextPage(tester);
|
||||
|
||||
final orderImage = find.byKey(const ValueKey("accounts-education-order-image"));
|
||||
final title = find.text(
|
||||
"Accounts are created in order, and they are restored in the same order",
|
||||
);
|
||||
final accountImage = find.byKey(
|
||||
const ValueKey("accounts-education-ordered-account-image"),
|
||||
);
|
||||
final description = find.byWidgetPredicate(
|
||||
(widget) =>
|
||||
widget is RichText &&
|
||||
widget.text.toPlainText() ==
|
||||
"The order of creation is shown alongside the account name, in the Accounts menu",
|
||||
);
|
||||
final restoreDescription = find.text(
|
||||
"This is useful to know if you want to restore your accounts, both in Cake Wallet or elsewhere",
|
||||
);
|
||||
|
||||
expect(tester.getBottomLeft(orderImage).dy, lessThan(tester.getTopLeft(title).dy));
|
||||
expect(tester.getBottomLeft(title).dy, lessThan(tester.getTopLeft(accountImage).dy));
|
||||
expect(tester.getBottomLeft(accountImage).dy, lessThan(tester.getTopLeft(description).dy));
|
||||
expect(
|
||||
tester.getBottomLeft(description).dy,
|
||||
lessThan(tester.getTopLeft(restoreDescription).dy),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(title).dy - tester.getBottomLeft(orderImage).dy,
|
||||
moreOrLessEquals(51, epsilon: 0.1),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(accountImage).dy - tester.getBottomLeft(title).dy,
|
||||
moreOrLessEquals(51, epsilon: 0.1),
|
||||
);
|
||||
expect(
|
||||
tester.getTopLeft(description).dy - tester.getBottomLeft(accountImage).dy,
|
||||
moreOrLessEquals(51, epsilon: 0.1),
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets("final acknowledgement marks education as seen", (tester) async {
|
||||
await openEducation(tester);
|
||||
|
||||
await continueToNextPage(tester);
|
||||
await continueToNextPage(tester);
|
||||
await continueToNextPage(tester);
|
||||
|
||||
expect(
|
||||
find.text("Accounts cannot be deleted, only archived"),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.text(
|
||||
"If you deposit funds to an account, they will still be there even if you archive it",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text("I understand. Continue"), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text("I understand. Continue"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => settingsStore.dismissEducation("accounts")).called(1);
|
||||
});
|
||||
|
||||
testWidgets("closing education also marks it as seen", (tester) async {
|
||||
final semantics = tester.ensureSemantics();
|
||||
|
||||
await openEducation(tester);
|
||||
await tester.tap(find.bySemanticsLabel("Close"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
verify(() => settingsStore.dismissEducation("accounts")).called(1);
|
||||
semantics.dispose();
|
||||
});
|
||||
|
||||
testWidgets("dismissing the modal without completing education marks it as seen", (tester) async {
|
||||
await openEducation(tester);
|
||||
|
||||
Navigator.of(tester.element(find.byType(AccountEducationPage))).pop();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AccountEducationPage), findsNothing);
|
||||
verify(() => settingsStore.dismissEducation("accounts")).called(1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:cake_wallet/core/amount_parsing_proxy.dart";
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/entities/balance_display_mode.dart";
|
||||
import "package:cake_wallet/entities/fiat_api_mode.dart";
|
||||
import "package:cake_wallet/entities/fiat_currency.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
|
||||
import "package:cake_wallet/locales/locale.dart";
|
||||
import "package:cake_wallet/new-ui/pages/wallet_accounts_page.dart";
|
||||
import "package:cake_wallet/new-ui/pages/account_education_page.dart";
|
||||
import "package:cake_wallet/new-ui/pages/card_customizer.dart";
|
||||
import "package:cake_wallet/new-ui/pages/hidden_accounts.dart";
|
||||
import "package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_provider.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/fiat_conversion_store.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cake_wallet/themes/core/theme_store.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_list_view_model.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/balance_card_layout.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart" as sqlite;
|
||||
import "package:cw_core/sync_status.dart";
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mobx/mobx.dart" show Observable, runInAction;
|
||||
import "package:mocktail/mocktail.dart";
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
class _MockAccountEditOrCreateViewModel extends Mock implements WalletAccountEditOrCreateViewModel {}
|
||||
|
||||
class _MockDashboardViewModel extends Mock implements DashboardViewModel {}
|
||||
|
||||
class _MockBalanceViewModel extends Mock implements BalanceViewModel {}
|
||||
|
||||
class _MockAppStore extends Mock implements AppStore {}
|
||||
|
||||
class _MockAccountListViewModel extends Mock implements MoneroAccountListViewModel {}
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWalletInfo extends Mock implements WalletInfo {}
|
||||
|
||||
class _MockCardCustomizerBloc extends Mock implements CardCustomizerBloc {}
|
||||
|
||||
class _MockSettingsStore extends Mock implements SettingsStore {}
|
||||
|
||||
Future<void> _waitForArchivePage(WidgetTester tester) async {
|
||||
for (var attempt = 0; attempt < 50; attempt++) {
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 10)),
|
||||
);
|
||||
await tester.pump();
|
||||
if (find.byType(CupertinoActivityIndicator).evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw TestFailure("Archived accounts did not finish loading");
|
||||
}
|
||||
|
||||
Future<void> _waitForTextToDisappear(WidgetTester tester, String text) async {
|
||||
for (var attempt = 0; attempt < 50; attempt++) {
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 10)),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 20));
|
||||
if (find.text(text).evaluate().isEmpty) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw TestFailure("$text did not disappear");
|
||||
}
|
||||
|
||||
Future<void> _pumpUntil(WidgetTester tester, bool Function() done) async {
|
||||
for (var attempt = 0; attempt < 100; attempt++) {
|
||||
await tester.runAsync(
|
||||
() => Future<void>.delayed(const Duration(milliseconds: 10)),
|
||||
);
|
||||
await tester.pump(const Duration(milliseconds: 10));
|
||||
if (done()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw TestFailure("Timed out waiting for WalletAccountsPage");
|
||||
}
|
||||
|
||||
void main() {
|
||||
late bool registeredThemeStore;
|
||||
late DatabaseFactory? originalDatabaseFactory;
|
||||
late Database? originalDatabase;
|
||||
late _MockDashboardViewModel dashboardViewModel;
|
||||
late _MockBalanceViewModel balanceViewModel;
|
||||
late _MockAccountListViewModel accountListViewModel;
|
||||
late _MockWallet wallet;
|
||||
late _MockWalletInfo walletInfo;
|
||||
|
||||
final fundedAccount =
|
||||
AccountListItem(id: 0, label: "Savings", balance: Money.parse("1.25", CryptoCurrency.xmr));
|
||||
final activeAccount =
|
||||
AccountListItem(id: 1, label: "Primary", balance: Money.parse("0", CryptoCurrency.xmr));
|
||||
final emptyAccount =
|
||||
AccountListItem(id: 2, label: "Travel", balance: Money.parse("0", CryptoCurrency.xmr));
|
||||
|
||||
setUpAll(() {
|
||||
registerFallbackValue(DesignSaved());
|
||||
registerFallbackValue(
|
||||
AccountListItem(id: -1, label: "fallback", balance: Money.zero(CryptoCurrency.xmr)),
|
||||
);
|
||||
|
||||
registeredThemeStore = !getIt.isRegistered<ThemeStore>();
|
||||
if (registeredThemeStore) {
|
||||
getIt.registerSingleton(ThemeStore());
|
||||
}
|
||||
|
||||
originalDatabaseFactory = databaseFactoryOrNull;
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
databaseFactoryOrNull = originalDatabaseFactory;
|
||||
if (registeredThemeStore) {
|
||||
await getIt.unregister<ThemeStore>();
|
||||
}
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
originalDatabase = sqlite.db;
|
||||
sqlite.db = await databaseFactory.openDatabase(inMemoryDatabasePath);
|
||||
await sqlite.db!.execute("""
|
||||
CREATE TABLE ${BalanceCardStyleSettings.tableName} (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
gradientIndex INTEGER DEFAULT -1,
|
||||
useSpecialDesign BOOLEAN DEFAULT FALSE,
|
||||
hidden BOOLEAN DEFAULT FALSE,
|
||||
backgroundImagePath TEXT DEFAULT "",
|
||||
iconStyleIndex INTEGER DEFAULT 0,
|
||||
isGradientOnly BOOLEAN DEFAULT FALSE,
|
||||
cardOrder INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (walletInfoId, accountIndex)
|
||||
)
|
||||
""");
|
||||
|
||||
dashboardViewModel = _MockDashboardViewModel();
|
||||
balanceViewModel = _MockBalanceViewModel();
|
||||
accountListViewModel = _MockAccountListViewModel();
|
||||
wallet = _MockWallet();
|
||||
walletInfo = _MockWalletInfo();
|
||||
final settingsStore = _MockSettingsStore();
|
||||
when(() => settingsStore.balanceDisplayMode).thenReturn(BalanceDisplayMode.displayableBalance);
|
||||
when(() => settingsStore.displayAmountsInSatoshi).thenReturn(BitcoinAmountDisplayMode.bitcoin);
|
||||
when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
|
||||
when(() => accountListViewModel.select(any())).thenAnswer((_) async {});
|
||||
when(() => accountListViewModel.reload()).thenAnswer((_) async {});
|
||||
|
||||
when(() => dashboardViewModel.balanceViewModel).thenReturn(balanceViewModel);
|
||||
when(() => balanceViewModel.accountFiatBalance(any())).thenReturn(null);
|
||||
when(() => dashboardViewModel.isMultiAccountsEnabled).thenReturn(true);
|
||||
when(() => dashboardViewModel.canToggleMultiAccounts).thenReturn(false);
|
||||
when(() => dashboardViewModel.wallet).thenReturn(wallet);
|
||||
when(() => dashboardViewModel.loadCardDesigns()).thenAnswer((_) async {});
|
||||
when(() => wallet.walletInfo).thenReturn(walletInfo);
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.xmr);
|
||||
when(() => walletInfo.internalId).thenReturn(42);
|
||||
when(() => accountListViewModel.currency).thenReturn(CryptoCurrency.xmr);
|
||||
when(() => accountListViewModel.accounts).thenReturn([activeAccount]);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await sqlite.db?.close();
|
||||
sqlite.db = originalDatabase;
|
||||
});
|
||||
|
||||
Future<void> hideAccount(int accountId, int order) => BalanceCardStyleSettings.fromCardDesign(
|
||||
walletInfoId: 42,
|
||||
accountIndex: accountId,
|
||||
cardOrder: order,
|
||||
design: CardDesign.gradientOnlyDesign,
|
||||
hidden: true,
|
||||
).insert();
|
||||
|
||||
Widget testApp(Widget home) => MaterialApp(
|
||||
localizationsDelegates: localizationDelegates,
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (_, child) => MoneySettingsProvider(
|
||||
settingsStore: dashboardViewModel.settingsStore,
|
||||
child: child!,
|
||||
),
|
||||
home: home,
|
||||
);
|
||||
|
||||
testWidgets("empty archive explains where archived accounts will appear", (tester) async {
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
HiddenAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
await _waitForArchivePage(tester);
|
||||
|
||||
expect(find.text("Archived Accounts"), findsOneWidget);
|
||||
expect(find.text("No Archived Accounts"), findsOneWidget);
|
||||
expect(find.text("When you Archive an Account, it will show up here"), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("archived accounts use the persisted hidden layout and Figma groups", (tester) async {
|
||||
when(() => accountListViewModel.accounts)
|
||||
.thenReturn([fundedAccount, activeAccount, emptyAccount]);
|
||||
await tester.runAsync(() async {
|
||||
await hideAccount(fundedAccount.id, 0);
|
||||
await hideAccount(emptyAccount.id, 1);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
HiddenAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
await _waitForArchivePage(tester);
|
||||
|
||||
expect(find.text("Funded accounts"), findsOneWidget);
|
||||
expect(find.text("Unarchive an account to access its funds"), findsOneWidget);
|
||||
expect(find.text("1. Savings"), findsOneWidget);
|
||||
expect(find.text("1.25 XMR"), findsOneWidget);
|
||||
expect(find.text("Empty Accounts"), findsOneWidget);
|
||||
expect(find.text("3. Travel"), findsOneWidget);
|
||||
expect(find.text("0 XMR"), findsNothing);
|
||||
});
|
||||
|
||||
for (final currency in [CryptoCurrency.xmr, CryptoCurrency.btc]) {
|
||||
testWidgets("archived ${currency.title} balances respect price, units, and privacy",
|
||||
(tester) async {
|
||||
final settingsStore = _MockSettingsStore();
|
||||
final displayMode = Observable(BalanceDisplayMode.displayableBalance);
|
||||
final amountDisplayMode = Observable(BitcoinAmountDisplayMode.bitcoin);
|
||||
when(() => settingsStore.balanceDisplayMode).thenAnswer((_) => displayMode.value);
|
||||
when(() => settingsStore.displayAmountsInSatoshi).thenAnswer((_) => amountDisplayMode.value);
|
||||
final fundedAccount = AccountListItem(
|
||||
id: 0,
|
||||
label: "Savings",
|
||||
balance: Money.parse("1.25", currency),
|
||||
);
|
||||
when(() => wallet.currency).thenReturn(currency);
|
||||
when(() => wallet.type)
|
||||
.thenReturn(currency == CryptoCurrency.btc ? WalletType.bitcoin : WalletType.monero);
|
||||
when(() => accountListViewModel.currency).thenReturn(currency);
|
||||
|
||||
when(() => accountListViewModel.accounts)
|
||||
.thenReturn([fundedAccount, activeAccount, emptyAccount]);
|
||||
when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
|
||||
when(() => settingsStore.fiatCurrency).thenReturn(FiatCurrency.usd);
|
||||
when(() => settingsStore.languageCode).thenReturn("de_DE");
|
||||
when(() => settingsStore.fiatApiMode).thenReturn(FiatApiMode.enabled);
|
||||
when(() => settingsStore.mwebAlwaysScan).thenReturn(false);
|
||||
when(() => walletInfo.isShowIntroCakePayCard).thenReturn(false);
|
||||
final appStore = _MockAppStore();
|
||||
when(() => appStore.wallet).thenReturn(wallet);
|
||||
when(() => appStore.amountParsingProxy)
|
||||
.thenAnswer((_) => AmountParsingProxy(amountDisplayMode.value));
|
||||
when(() => dashboardViewModel.balanceViewModel).thenReturn(
|
||||
BalanceViewModel(
|
||||
appStore: appStore,
|
||||
settingsStore: settingsStore,
|
||||
fiatConversionStore: FiatConversionStore()..prices[currency] = 20,
|
||||
),
|
||||
);
|
||||
await tester.runAsync(() async {
|
||||
await hideAccount(fundedAccount.id, 0);
|
||||
await hideAccount(emptyAccount.id, 1);
|
||||
});
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
HiddenAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
await _waitForArchivePage(tester);
|
||||
|
||||
expect(find.text("1.25 ${currency.title}"), findsOneWidget);
|
||||
expect(find.text("25,00 USD"), findsOneWidget);
|
||||
expect(find.text("3. Travel"), findsOneWidget);
|
||||
expect(find.text("0,00 USD"), findsNothing);
|
||||
|
||||
if (currency == CryptoCurrency.btc) {
|
||||
runInAction(() => amountDisplayMode.value = BitcoinAmountDisplayMode.satoshi);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text("125,000,000 sats"), findsOneWidget);
|
||||
expect(find.text("25,00 USD"), findsOneWidget);
|
||||
}
|
||||
|
||||
runInAction(() => displayMode.value = BalanceDisplayMode.hiddenBalance);
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text("●●●●●●"), findsOneWidget);
|
||||
expect(find.text("●●●●● USD"), findsOneWidget);
|
||||
expect(find.text("25,00 USD"), findsNothing);
|
||||
});
|
||||
}
|
||||
|
||||
testWidgets("WalletAccountsPage shows education only until it has been seen", (tester) async {
|
||||
var cardDesignLoads = 0;
|
||||
var educationDismissed = false;
|
||||
final settingsStore = _MockSettingsStore();
|
||||
when(() => settingsStore.balanceDisplayMode).thenReturn(BalanceDisplayMode.displayableBalance);
|
||||
when(() => settingsStore.displayAmountsInSatoshi).thenReturn(BitcoinAmountDisplayMode.bitcoin);
|
||||
|
||||
when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
|
||||
when(() => settingsStore.isEducationDismissed("accounts"))
|
||||
.thenAnswer((_) => educationDismissed);
|
||||
when(() => settingsStore.dismissEducation("accounts")).thenAnswer((_) async {
|
||||
educationDismissed = true;
|
||||
});
|
||||
|
||||
when(() => dashboardViewModel.loadCardDesigns()).thenAnswer((_) async {
|
||||
cardDesignLoads++;
|
||||
});
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.xmr);
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
WalletAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
accountEditOrCreateViewModel: _MockAccountEditOrCreateViewModel(),
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
await _pumpUntil(tester, () => find.byType(AccountEducationPage).evaluate().isNotEmpty);
|
||||
|
||||
expect(find.byType(AccountEducationPage), findsOneWidget);
|
||||
await tester.tap(
|
||||
find.descendant(
|
||||
of: find.byType(AccountEducationPage),
|
||||
matching: find.bySemanticsLabel("Close"),
|
||||
),
|
||||
);
|
||||
await tester.pumpAndSettle();
|
||||
expect(settingsStore.isEducationDismissed("accounts"), isTrue);
|
||||
|
||||
final loadsBeforeFirstDispose = cardDesignLoads;
|
||||
await tester.pumpWidget(testApp(const SizedBox.shrink()));
|
||||
await _pumpUntil(tester, () => cardDesignLoads > loadsBeforeFirstDispose);
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
WalletAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
accountEditOrCreateViewModel: _MockAccountEditOrCreateViewModel(),
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
await _pumpUntil(tester, () => find.text("Add Account").evaluate().isNotEmpty);
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(AccountEducationPage), findsNothing);
|
||||
|
||||
final loadsBeforeSecondDispose = cardDesignLoads;
|
||||
await tester.pumpWidget(testApp(const SizedBox.shrink()));
|
||||
await _pumpUntil(tester, () => cardDesignLoads > loadsBeforeSecondDispose);
|
||||
});
|
||||
|
||||
testWidgets("unnamed archived accounts get the Figma display fallback", (tester) async {
|
||||
final unnamedAccount =
|
||||
AccountListItem(id: 4, label: "", balance: Money.parse("0", CryptoCurrency.xmr));
|
||||
when(() => accountListViewModel.accounts).thenReturn([activeAccount, unnamedAccount]);
|
||||
await tester.runAsync(() => hideAccount(unnamedAccount.id, 0));
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
HiddenAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
await _waitForArchivePage(tester);
|
||||
|
||||
expect(find.text("5. Unnamed Account"), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("unarchiving uses the latest account state for confirmation and selection",
|
||||
(tester) async {
|
||||
when(() => accountListViewModel.accounts).thenReturn([emptyAccount, activeAccount]);
|
||||
await tester.runAsync(() => hideAccount(emptyAccount.id, 0));
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
HiddenAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
await _waitForArchivePage(tester);
|
||||
|
||||
final accountForConfirmation = AccountListItem(
|
||||
id: emptyAccount.id,
|
||||
label: emptyAccount.label,
|
||||
balance: Money.parse("1.5", CryptoCurrency.xmr),
|
||||
);
|
||||
when(() => accountListViewModel.accounts).thenReturn([accountForConfirmation, activeAccount]);
|
||||
await tester.tap(find.text("3. Travel"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Unarchive this account?"), findsOneWidget);
|
||||
expect(
|
||||
find.text("This account will show up again on your wallet, letting you access your funds"),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text("1.5 XMR"), findsOneWidget);
|
||||
|
||||
final latestAccount = AccountListItem(
|
||||
id: emptyAccount.id,
|
||||
label: "Updated",
|
||||
balance: Money.parse("2.5", CryptoCurrency.xmr),
|
||||
);
|
||||
when(() => accountListViewModel.accounts).thenReturn([latestAccount, activeAccount]);
|
||||
await tester.tap(find.text("Continue"));
|
||||
await tester.pump();
|
||||
await _waitForTextToDisappear(tester, "3. Travel");
|
||||
|
||||
final setting = await tester.runAsync(
|
||||
() => BalanceCardStyleSettings.get(42, emptyAccount.id),
|
||||
);
|
||||
final settings = await tester.runAsync(() => BalanceCardStyleSettings.getAll(42));
|
||||
final layout = BalanceCardLayout.resolve(
|
||||
accountIndices: [emptyAccount.id, activeAccount.id],
|
||||
settings: settings!,
|
||||
);
|
||||
|
||||
expect(setting?.hidden, isFalse);
|
||||
expect(layout.hidden, isEmpty);
|
||||
expect(layout.visible, contains(emptyAccount.id));
|
||||
verify(() => accountListViewModel.select(latestAccount)).called(1);
|
||||
verify(() => dashboardViewModel.loadCardDesigns()).called(1);
|
||||
expect(find.text("3. Travel"), findsNothing);
|
||||
expect(find.text("No Archived Accounts"), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets("archive flow survives dismissal and protects the final visible account",
|
||||
(tester) async {
|
||||
final remainingAccount =
|
||||
AccountListItem(id: 0, label: "Primary", balance: Money.parse("0", CryptoCurrency.xmr));
|
||||
final archivedAccount = AccountListItem(
|
||||
id: 1,
|
||||
label: "Savings",
|
||||
balance: Money.parse("0", CryptoCurrency.xmr),
|
||||
isSelected: true,
|
||||
);
|
||||
final bloc = _MockCardCustomizerBloc();
|
||||
final settingsStore = _MockSettingsStore();
|
||||
when(() => settingsStore.balanceDisplayMode).thenReturn(BalanceDisplayMode.displayableBalance);
|
||||
when(() => settingsStore.displayAmountsInSatoshi).thenReturn(BitcoinAmountDisplayMode.bitcoin);
|
||||
final states = StreamController<CardCustomizerState>.broadcast();
|
||||
final events = <Type>[];
|
||||
final selections = <int>[];
|
||||
final canHideValues = <bool>[];
|
||||
final finishArchiveReload = Completer<void>();
|
||||
var canHide = false;
|
||||
var cardDesignLoads = 0;
|
||||
var pauseCardDesignLoad = false;
|
||||
|
||||
when(() => settingsStore.isEducationDismissed("accounts")).thenReturn(true);
|
||||
var customizerState = CardCustomizerInitial(
|
||||
0,
|
||||
0,
|
||||
const <CardDesign>[CardDesign.gradientOnlyDesign],
|
||||
const <Gradient>[CardDesign.gradientBlue],
|
||||
"Savings",
|
||||
archivedAccount.id,
|
||||
false,
|
||||
1,
|
||||
);
|
||||
final savedState = CardCustomizerSaved(
|
||||
0,
|
||||
0,
|
||||
const <CardDesign>[CardDesign.gradientOnlyDesign],
|
||||
const <Gradient>[CardDesign.gradientBlue],
|
||||
"Savings",
|
||||
archivedAccount.id,
|
||||
false,
|
||||
1,
|
||||
);
|
||||
|
||||
final accounts = <AccountListItem>[remainingAccount, archivedAccount];
|
||||
when(() => accountListViewModel.accounts).thenAnswer((_) => accounts);
|
||||
when(() => accountListViewModel.select(any())).thenAnswer((invocation) async {
|
||||
selections.add((invocation.positionalArguments.single as AccountListItem).id);
|
||||
});
|
||||
when(() => dashboardViewModel.status).thenReturn(SyncedSyncStatus());
|
||||
when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
|
||||
when(() => dashboardViewModel.loadCardDesigns()).thenAnswer((_) async {
|
||||
cardDesignLoads++;
|
||||
if (pauseCardDesignLoad) {
|
||||
await finishArchiveReload.future;
|
||||
}
|
||||
});
|
||||
when(() => balanceViewModel.accountFiatBalance(any())).thenReturn("0,00 USD");
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.xmr);
|
||||
when(() => bloc.state).thenAnswer((_) => customizerState);
|
||||
when(() => bloc.close()).thenAnswer((_) async {});
|
||||
when(() => bloc.stream).thenAnswer((_) => states.stream);
|
||||
when(() => bloc.canHide).thenAnswer((_) => canHide);
|
||||
when(() => bloc.add(any())).thenAnswer((invocation) {
|
||||
final event = invocation.positionalArguments.single as CardCustomizerEvent;
|
||||
if (event is AccountNameChanged) {
|
||||
customizerState = customizerState.copyWith(accountName: event.newAccountName);
|
||||
return;
|
||||
}
|
||||
events.add(event.runtimeType);
|
||||
|
||||
if (event is DesignSaved) {
|
||||
scheduleMicrotask(() => states.add(savedState));
|
||||
} else if (event is AccountHidden) {
|
||||
// Simulate AccountHidden's persisted output so WalletAccountsPage reloads visible cards.
|
||||
scheduleMicrotask(() {
|
||||
unawaited(
|
||||
BalanceCardStyleSettings.fromCardDesign(
|
||||
walletInfoId: 42,
|
||||
accountIndex: archivedAccount.id,
|
||||
cardOrder: 1,
|
||||
design: CardDesign.gradientOnlyDesign,
|
||||
hidden: true,
|
||||
).insert().then((_) => states.add(savedState)),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
var scopeOpen = true;
|
||||
getIt.pushNewScope();
|
||||
addTearDown(() async {
|
||||
if (!finishArchiveReload.isCompleted) {
|
||||
finishArchiveReload.complete();
|
||||
}
|
||||
if (!states.isClosed) {
|
||||
await states.close();
|
||||
}
|
||||
if (scopeOpen) {
|
||||
await getIt.popScope();
|
||||
}
|
||||
});
|
||||
getIt.registerFactoryParam<CardCustomizerBloc, CardCustomizerBlocParams, void>(
|
||||
(params, _) {
|
||||
canHideValues.add(canHide = params.canHide);
|
||||
return bloc;
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
Scaffold(
|
||||
body: WalletAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
accountEditOrCreateViewModel: _MockAccountEditOrCreateViewModel(),
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await _pumpUntil(tester, () => find.text("Add Account").evaluate().isNotEmpty);
|
||||
|
||||
ModernButton archiveButton() => tester.widget<ModernButton>(
|
||||
find.byWidgetPredicate(
|
||||
(widget) => widget is ModernButton && widget.semanticLabel == "Archived Accounts",
|
||||
),
|
||||
);
|
||||
|
||||
expect(archiveButton().backgroundColor, isNull);
|
||||
expect(archiveButton().iconColor, isNull);
|
||||
|
||||
final frontCard = tester
|
||||
.widgetList<BalanceCard>(find.byType(BalanceCard))
|
||||
.singleWhere((card) => card.onCustomizeTapped != null);
|
||||
expect(frontCard.fiatBalance, "0,00 USD");
|
||||
frontCard.onCustomizeTapped!();
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.byType(CardCustomizer), findsOneWidget);
|
||||
expect(canHideValues, <bool>[true]);
|
||||
expect(find.text("Archive Account"), findsOneWidget);
|
||||
|
||||
await tester.enterText(find.byType(TextField), "Long-term Savings");
|
||||
await tester.ensureVisible(find.text("Archive Account"));
|
||||
await tester.tap(find.text("Archive Account"));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text("2. Long-term Savings"), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text("Cancel"));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text("Archive Account"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final loadsBeforeArchive = cardDesignLoads;
|
||||
pauseCardDesignLoad = true;
|
||||
await tester.tap(find.text("Continue"));
|
||||
await tester.pump();
|
||||
await _pumpUntil(
|
||||
tester,
|
||||
() => events.length == 2 && selections.length == 2 && cardDesignLoads > loadsBeforeArchive,
|
||||
);
|
||||
|
||||
expect(events, <Type>[DesignSaved, AccountHidden]);
|
||||
expect(selections, <int>[archivedAccount.id, remainingAccount.id]);
|
||||
await tester.pumpWidget(testApp(const SizedBox.shrink()));
|
||||
pauseCardDesignLoad = false;
|
||||
finishArchiveReload.complete();
|
||||
await tester.pump();
|
||||
final archivedSetting = await tester.runAsync(() async {
|
||||
// Queue the final read behind both disposal writes from the unfixed flow.
|
||||
await BalanceCardStyleSettings.getAll(42);
|
||||
await BalanceCardStyleSettings.getAll(42);
|
||||
return BalanceCardStyleSettings.get(42, archivedAccount.id);
|
||||
});
|
||||
expect(archivedSetting?.hidden, isTrue);
|
||||
expect(selections, <int>[archivedAccount.id, remainingAccount.id]);
|
||||
|
||||
await tester.pumpWidget(
|
||||
testApp(
|
||||
Scaffold(
|
||||
body: WalletAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
accountEditOrCreateViewModel: _MockAccountEditOrCreateViewModel(),
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await _pumpUntil(tester, () => find.text("Add Account").evaluate().isNotEmpty);
|
||||
|
||||
final colorScheme = Theme.of(tester.element(find.byType(WalletAccountsPage))).colorScheme;
|
||||
expect(archiveButton().backgroundColor, colorScheme.primary);
|
||||
expect(archiveButton().iconColor, colorScheme.onPrimary);
|
||||
|
||||
final remainingFrontCard = tester
|
||||
.widgetList<BalanceCard>(find.byType(BalanceCard))
|
||||
.singleWhere((card) => card.onCustomizeTapped != null);
|
||||
remainingFrontCard.onCustomizeTapped!();
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(canHideValues, <bool>[true, false]);
|
||||
expect(find.text("Archive Account"), findsNothing);
|
||||
|
||||
Navigator.of(tester.element(find.byType(CardCustomizer))).pop(false);
|
||||
await tester.pump();
|
||||
await _pumpUntil(tester, () => events.length == 3 && selections.length == 3);
|
||||
expect(events.last, DesignSaved);
|
||||
expect(selections.last, remainingAccount.id);
|
||||
|
||||
final loadsBeforeDispose = cardDesignLoads;
|
||||
await tester.pumpWidget(testApp(const SizedBox.shrink()));
|
||||
await _pumpUntil(tester, () => cardDesignLoads > loadsBeforeDispose);
|
||||
await states.close();
|
||||
await getIt.popScope();
|
||||
scopeOpen = false;
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/entities/balance_display_mode.dart";
|
||||
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/locales/locale.dart";
|
||||
import "package:cake_wallet/new-ui/pages/card_customizer.dart";
|
||||
import "package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/cards/balance_card.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_provider.dart";
|
||||
import "package:cake_wallet/themes/core/theme_store.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_list_item.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/card_design.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mocktail/mocktail.dart";
|
||||
|
||||
class _MockCardCustomizerBloc extends Mock implements CardCustomizerBloc {}
|
||||
|
||||
class _MockDashboardViewModel extends Mock implements DashboardViewModel {}
|
||||
|
||||
class _MockSettingsStore extends Mock implements SettingsStore {}
|
||||
|
||||
class _RouteTracker {
|
||||
bool completed = false;
|
||||
bool? result;
|
||||
}
|
||||
|
||||
CardCustomizerState _accountState(String accountName) => CardCustomizerInitial(
|
||||
0,
|
||||
0,
|
||||
const <CardDesign>[CardDesign.gradientOnlyDesign],
|
||||
const <Gradient>[CardDesign.gradientBlue],
|
||||
accountName,
|
||||
0,
|
||||
false,
|
||||
0,
|
||||
);
|
||||
|
||||
Future<_RouteTracker> _openCustomizer(
|
||||
WidgetTester tester,
|
||||
CardCustomizerBloc bloc, {
|
||||
required DashboardViewModel dashboardViewModel,
|
||||
required AccountListItem? account,
|
||||
String fiatBalance = "",
|
||||
}) async {
|
||||
final tracker = _RouteTracker();
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: localizationDelegates,
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
builder: (_, child) => MoneySettingsProvider(
|
||||
settingsStore: dashboardViewModel.settingsStore,
|
||||
child: child!,
|
||||
),
|
||||
home: Builder(
|
||||
builder: (context) => Scaffold(
|
||||
body: TextButton(
|
||||
onPressed: () {
|
||||
Navigator.of(context)
|
||||
.push<bool>(
|
||||
MaterialPageRoute(
|
||||
builder: (_) => BlocProvider<CardCustomizerBloc>.value(
|
||||
value: bloc,
|
||||
child: Material(
|
||||
child: CardCustomizer(
|
||||
cryptoTitle: "Monero",
|
||||
cryptoName: "xmr",
|
||||
account: account,
|
||||
fiatBalance: fiatBalance,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.then((result) {
|
||||
tracker
|
||||
..completed = true
|
||||
..result = result;
|
||||
});
|
||||
},
|
||||
child: const Text("Open card customizer"),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await tester.tap(find.text("Open card customizer"));
|
||||
await tester.pumpAndSettle();
|
||||
return tracker;
|
||||
}
|
||||
|
||||
void main() {
|
||||
late _MockCardCustomizerBloc bloc;
|
||||
late _MockDashboardViewModel dashboardViewModel;
|
||||
late AccountListItem account;
|
||||
late StreamController<CardCustomizerState> stateController;
|
||||
|
||||
setUpAll(() {
|
||||
getIt.registerSingleton(ThemeStore());
|
||||
});
|
||||
|
||||
tearDownAll(() async {
|
||||
await getIt.unregister<ThemeStore>();
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
bloc = _MockCardCustomizerBloc();
|
||||
dashboardViewModel = _MockDashboardViewModel();
|
||||
account =
|
||||
AccountListItem(id: 0, label: "Savings", balance: Money.parse("1.25", CryptoCurrency.xmr));
|
||||
final settingsStore = _MockSettingsStore();
|
||||
when(() => settingsStore.balanceDisplayMode).thenReturn(BalanceDisplayMode.displayableBalance);
|
||||
when(() => dashboardViewModel.settingsStore).thenReturn(settingsStore);
|
||||
when(() => settingsStore.displayAmountsInSatoshi).thenReturn(BitcoinAmountDisplayMode.bitcoin);
|
||||
stateController = StreamController<CardCustomizerState>.broadcast();
|
||||
when(() => bloc.stream).thenAnswer((_) => stateController.stream);
|
||||
when(() => bloc.canHide).thenReturn(true);
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await stateController.close();
|
||||
});
|
||||
|
||||
testWidgets("account mode renders edit controls and cancels funded archival", (tester) async {
|
||||
when(() => bloc.state).thenReturn(_accountState(""));
|
||||
final tracker = await _openCustomizer(
|
||||
tester,
|
||||
bloc,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
account: account,
|
||||
fiatBalance: "2.50 USD",
|
||||
);
|
||||
|
||||
expect(find.text("Edit Account"), findsOneWidget);
|
||||
expect(find.text("#1"), findsOneWidget);
|
||||
expect(find.text("Unnamed Account"), findsOneWidget);
|
||||
expect(find.text("1.25"), findsWidgets);
|
||||
expect(find.text("2.50 USD"), findsOneWidget);
|
||||
expect(find.text("Account name"), findsOneWidget);
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
expect(find.text("Archive Account"), findsOneWidget);
|
||||
expect(find.text("This change can be reverted"), findsOneWidget);
|
||||
expect(
|
||||
tester.getTopLeft(find.byType(BalanceCard).first).dy,
|
||||
lessThan(tester.getTopLeft(find.text("Account name")).dy),
|
||||
);
|
||||
|
||||
await tester.ensureVisible(find.text("Archive Account"));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text("Archive Account"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Are you sure you want to Archive this account?"), findsOneWidget);
|
||||
expect(find.text("This account has the following funds:"), findsOneWidget);
|
||||
expect(find.text("1.25 XMR"), findsOneWidget);
|
||||
expect(
|
||||
find.text("Before proceeding, it is recommended you move them to an account you will use"),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.text(
|
||||
"Archiving an account does not delete any funds or activity. You can reverse this action",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
|
||||
await tester.tap(find.text("Cancel"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(CardCustomizer), findsOneWidget);
|
||||
expect(tracker.completed, isFalse);
|
||||
});
|
||||
|
||||
testWidgets("confirming hidden empty-account archival returns true to the parent",
|
||||
(tester) async {
|
||||
account =
|
||||
AccountListItem(id: 0, label: "Savings", balance: Money.parse("0", CryptoCurrency.xmr));
|
||||
final settingsStore = dashboardViewModel.settingsStore;
|
||||
when(() => settingsStore.balanceDisplayMode).thenReturn(BalanceDisplayMode.hiddenBalance);
|
||||
when(() => bloc.state).thenReturn(_accountState("Savings"));
|
||||
final tracker = await _openCustomizer(
|
||||
tester,
|
||||
bloc,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
account: account,
|
||||
);
|
||||
|
||||
await tester.ensureVisible(find.text("Archive Account"));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text("Archive Account"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("1. Savings"), findsOneWidget);
|
||||
expect(find.text("This account has the following funds:"), findsNothing);
|
||||
expect(
|
||||
find.text(
|
||||
"This action won’t delete the account or its past activity, but only hide it inside Cake Wallet",
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.text("You can reverse this action from Accounts settings"), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text("Continue"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.byType(CardCustomizer), findsNothing);
|
||||
expect(tracker.completed, isTrue);
|
||||
expect(tracker.result, isTrue);
|
||||
});
|
||||
|
||||
testWidgets("account editing remains available when archival is disabled", (tester) async {
|
||||
when(() => bloc.state).thenReturn(_accountState("Savings"));
|
||||
when(() => bloc.canHide).thenReturn(false);
|
||||
|
||||
await _openCustomizer(
|
||||
tester,
|
||||
bloc,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
account: account,
|
||||
);
|
||||
|
||||
expect(find.text("Edit Account"), findsOneWidget);
|
||||
expect(find.text("Account name"), findsOneWidget);
|
||||
expect(find.byType(TextField), findsOneWidget);
|
||||
expect(find.text("Savings"), findsWidgets);
|
||||
expect(find.text("Archive Account"), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("wallet card editing does not require an account", (tester) async {
|
||||
when(() => bloc.state).thenReturn(_accountState(""));
|
||||
when(() => bloc.canHide).thenReturn(false);
|
||||
|
||||
await _openCustomizer(
|
||||
tester,
|
||||
bloc,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
account: null,
|
||||
);
|
||||
|
||||
expect(find.text("Edit Card"), findsOneWidget);
|
||||
expect(find.byType(BalanceCard), findsWidgets);
|
||||
expect(find.byType(TextField), findsNothing);
|
||||
expect(find.text("Archive Account"), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -1,125 +1,329 @@
|
||||
import "package:cake_wallet/core/auth_service.dart";
|
||||
import "package:cake_wallet/di.dart";
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/account_edit_or_create_view_model.dart";
|
||||
import "package:cake_wallet/locales/locale.dart";
|
||||
import "package:cake_wallet/new-ui/pages/account_customizer.dart";
|
||||
import "package:cake_wallet/new-ui/pages/wallet_accounts_page.dart";
|
||||
import "package:cake_wallet/new-ui/pages/settings_page.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/modern_button.dart";
|
||||
import "package:cake_wallet/routes.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/view_model/monero_account_list/account_list_item.dart";
|
||||
import "package:cake_wallet/view_model/monero_account_list/monero_account_list_view_model.dart";
|
||||
import "package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_list_view_model.dart";
|
||||
import "package:cake_wallet/wallet_types.g.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart" as sqlite;
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter/services.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mocktail/mocktail.dart";
|
||||
import "package:sqflite/sqflite.dart";
|
||||
|
||||
class MockBalanceViewModel extends Mock implements BalanceViewModel {}
|
||||
|
||||
class MockDashboardViewModel extends Mock implements DashboardViewModel {}
|
||||
|
||||
class _MockAccountEditOrCreateViewModel extends Mock implements WalletAccountEditOrCreateViewModel {}
|
||||
|
||||
class _MockAccountListViewModel extends Mock implements MoneroAccountListViewModel {}
|
||||
|
||||
class _MockAuthService extends Mock implements AuthService {}
|
||||
|
||||
class _MockDatabase extends Mock implements Database {}
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWalletInfo extends Mock implements WalletInfo {}
|
||||
|
||||
void main() {
|
||||
late S english;
|
||||
late S french;
|
||||
late S portuguese;
|
||||
late MockBalanceViewModel balanceViewModel;
|
||||
late MockDashboardViewModel dashboardViewModel;
|
||||
late _MockWallet wallet;
|
||||
late _MockAuthService authService;
|
||||
|
||||
setUpAll(() => S.delegate.load(const Locale("en")));
|
||||
setUpAll(() async {
|
||||
english = await S.delegate.load(const Locale("en"));
|
||||
french = await S.delegate.load(const Locale("fr"));
|
||||
portuguese = await S.delegate.load(const Locale("pt"));
|
||||
await S.delegate.load(const Locale("en"));
|
||||
});
|
||||
|
||||
setUp(() {
|
||||
balanceViewModel = MockBalanceViewModel();
|
||||
dashboardViewModel = MockDashboardViewModel();
|
||||
wallet = _MockWallet();
|
||||
authService = _MockAuthService();
|
||||
|
||||
when(() => dashboardViewModel.balanceViewModel).thenReturn(balanceViewModel);
|
||||
});
|
||||
|
||||
SettingsListItem accountsItem() => SettingsSectionData.walletSettings.items.singleWhere(
|
||||
(item) => item.route == Routes.accountCustomizer,
|
||||
);
|
||||
|
||||
test("Accounts is the first Wallet Settings item and routes with the dashboard", () {
|
||||
final item = accountsItem();
|
||||
|
||||
expect(SettingsSectionData.walletSettings.items.first, same(item));
|
||||
expect(item.iconPath, "assets/new-ui/settings_row_icons/accounts.svg");
|
||||
expect(item.title, S.current.accounts);
|
||||
expect(item.route, Routes.accountCustomizer);
|
||||
expect(item.routeArgs, isNull);
|
||||
expect(item.routeArgsBuilder, isNotNull);
|
||||
expect(item.routeArgsBuilder!(dashboardViewModel), same(dashboardViewModel));
|
||||
});
|
||||
|
||||
test("Accounts visibility follows account support", () {
|
||||
final item = accountsItem();
|
||||
|
||||
when(() => balanceViewModel.hasAccounts).thenReturn(true);
|
||||
expect(item.condition(dashboardViewModel), isTrue);
|
||||
|
||||
when(() => balanceViewModel.hasAccounts).thenReturn(false);
|
||||
expect(item.condition(dashboardViewModel), isFalse);
|
||||
});
|
||||
|
||||
testWidgets("Accounts opens through the settings modal with the active dashboard",
|
||||
(tester) async {
|
||||
final accountListViewModel = _MockAccountListViewModel();
|
||||
final authService = _MockAuthService();
|
||||
final wallet = _MockWallet();
|
||||
DashboardViewModel? routedDashboardViewModel;
|
||||
|
||||
when(() => balanceViewModel.hasAccounts).thenReturn(true);
|
||||
when(() => dashboardViewModel.hasLightning).thenReturn(false);
|
||||
when(() => dashboardViewModel.hasWalletConnect).thenReturn(false);
|
||||
when(() => dashboardViewModel.isMultiAccountsEnabled).thenReturn(true);
|
||||
when(() => dashboardViewModel.canToggleMultiAccounts).thenReturn(false);
|
||||
when(() => dashboardViewModel.wallet).thenReturn(wallet);
|
||||
when(() => dashboardViewModel.loadCardDesigns()).thenAnswer((_) async {});
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
when(() => dashboardViewModel.hasLightning).thenReturn(false);
|
||||
when(() => dashboardViewModel.hasSilentPayments).thenReturn(false);
|
||||
when(() => dashboardViewModel.hasMweb).thenReturn(false);
|
||||
when(() => dashboardViewModel.hasWalletConnect).thenReturn(false);
|
||||
when(() => wallet.hasAccountsSupport).thenReturn(false);
|
||||
when(() => wallet.hasCoinControl).thenReturn(false);
|
||||
when(() => wallet.name).thenReturn("My Cake Wallet");
|
||||
when(() => wallet.hardwareWalletType).thenReturn(null);
|
||||
when(() => accountListViewModel.accounts).thenReturn(const []);
|
||||
when(() => accountListViewModel.selected).thenReturn(
|
||||
AccountListItem(label: "Primary", id: 0, isSelected: true),
|
||||
);
|
||||
});
|
||||
|
||||
getIt.pushNewScope(scopeName: "settings-accounts-route-test");
|
||||
getIt.registerFactoryParam<AccountCustomizer, DashboardViewModel, void>(
|
||||
(routedViewModel, _) {
|
||||
routedDashboardViewModel = routedViewModel;
|
||||
return AccountCustomizer(
|
||||
accountListViewModel: accountListViewModel,
|
||||
dashboardViewModel: routedViewModel,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
localizationsDelegates: localizationDelegates,
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
home: NewSettingsPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
authService: authService,
|
||||
),
|
||||
Widget settingsApp({Locale locale = const Locale("en")}) => MaterialApp(
|
||||
locale: locale,
|
||||
localizationsDelegates: localizationDelegates,
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
home: NewSettingsPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
authService: authService,
|
||||
),
|
||||
);
|
||||
|
||||
test("wallet settings follow capabilities rather than wallet type", () {
|
||||
const resolver = WalletSettingsResolver();
|
||||
when(() => wallet.type).thenReturn(WalletType.solana);
|
||||
when(() => wallet.hasCoinControl).thenReturn(true);
|
||||
when(() => dashboardViewModel.hasWalletConnect).thenReturn(true);
|
||||
|
||||
expect(
|
||||
resolver.resolveSections(dashboardViewModel).map(
|
||||
(section) => section.map((item) => item.route),
|
||||
),
|
||||
[
|
||||
[Routes.manageNodes, Routes.unspentCoinsList],
|
||||
[Routes.walletConnectConnectionsListing],
|
||||
],
|
||||
);
|
||||
|
||||
when(() => wallet.hasCoinControl).thenReturn(false);
|
||||
when(() => dashboardViewModel.hasWalletConnect).thenReturn(false);
|
||||
|
||||
expect(
|
||||
resolver.resolveSections(dashboardViewModel).single.map((item) => item.route),
|
||||
[Routes.manageNodes],
|
||||
);
|
||||
});
|
||||
|
||||
test("Monero resync and Litecoin MWEB retain their feature section", () {
|
||||
const resolver = WalletSettingsResolver();
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
when(() => wallet.hardwareWalletType).thenReturn(HardwareWalletType.trezor);
|
||||
|
||||
expect(
|
||||
resolver.resolveSections(dashboardViewModel).last.single.route,
|
||||
Routes.syncKeyImagesDevices,
|
||||
);
|
||||
|
||||
when(() => wallet.type).thenReturn(WalletType.litecoin);
|
||||
when(() => wallet.hardwareWalletType).thenReturn(null);
|
||||
when(() => dashboardViewModel.hasMweb).thenReturn(true);
|
||||
|
||||
expect(
|
||||
resolver.resolveSections(dashboardViewModel).last.single.route,
|
||||
Routes.mwebSettings,
|
||||
);
|
||||
});
|
||||
|
||||
test("settings icons are bundled for available wallet types", () async {
|
||||
const resolver = WalletSettingsResolver();
|
||||
for (final type in availableWalletTypes) {
|
||||
final path = resolver.iconPathFor(type);
|
||||
final asset = await rootBundle.load("$path.vec");
|
||||
expect(asset.lengthInBytes, greaterThan(0), reason: "$type: $path");
|
||||
}
|
||||
});
|
||||
|
||||
test("app settings expose only implemented destinations in the Figma order", () {
|
||||
final items = const SettingsPageSectionsResolver().appSettings();
|
||||
|
||||
expect(
|
||||
items.map((item) => item.route),
|
||||
[
|
||||
Routes.connectionSync,
|
||||
Routes.displaySettingsPage,
|
||||
Routes.securityBackupPage,
|
||||
Routes.backup,
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test("runtime wallet capabilities filter supported Bitcoin settings", () {
|
||||
when(() => wallet.type).thenReturn(WalletType.bitcoin);
|
||||
when(() => wallet.hasCoinControl).thenReturn(true);
|
||||
when(() => dashboardViewModel.hasLightning).thenReturn(true);
|
||||
|
||||
final sections = const WalletSettingsResolver().resolveSections(dashboardViewModel);
|
||||
final routes = sections.expand((section) => section).map((item) => item.route);
|
||||
|
||||
expect(routes, contains(Routes.manageNodes));
|
||||
expect(routes, contains(Routes.unspentCoinsList));
|
||||
expect(routes, contains(Routes.lightningUsernamePage));
|
||||
expect(routes, isNot(contains(Routes.walletAccountsPage)));
|
||||
expect(routes, isNot(contains(Routes.silentPaymentsSettings)));
|
||||
});
|
||||
|
||||
testWidgets("main Settings shows the active wallet and wallet-general/app groups",
|
||||
(tester) async {
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
|
||||
await tester.pumpWidget(settingsApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("My Cake Wallet"), findsOneWidget);
|
||||
expect(find.text("Monero ${english.settings_title}"), findsOneWidget);
|
||||
expect(find.text(english.privacy), findsOneWidget);
|
||||
expect(find.text(english.recovery_and_keys), findsOneWidget);
|
||||
expect(find.text(english.other), findsOneWidget);
|
||||
expect(find.text(english.app_settings), findsOneWidget);
|
||||
expect(find.text(english.connections), findsOneWidget);
|
||||
expect(find.text(english.display), findsOneWidget);
|
||||
expect(find.text(english.security), findsOneWidget);
|
||||
expect(find.text(english.backup), findsOneWidget);
|
||||
expect(find.text(english.accounts), findsNothing);
|
||||
expect(find.text(english.nodes), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("wallet settings title and rows follow the active Bitcoin capabilities",
|
||||
(tester) async {
|
||||
when(() => wallet.type).thenReturn(WalletType.bitcoin);
|
||||
when(() => wallet.hasCoinControl).thenReturn(true);
|
||||
when(() => dashboardViewModel.hasLightning).thenReturn(true);
|
||||
when(() => dashboardViewModel.hasSilentPayments).thenReturn(true);
|
||||
|
||||
await tester.pumpWidget(settingsApp());
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text("Bitcoin ${english.settings_title}"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Bitcoin ${english.settings_title}"), findsOneWidget);
|
||||
expect(find.text(english.nodes), findsOneWidget);
|
||||
expect(find.text(english.coin_control_settings), findsOneWidget);
|
||||
expect(find.text("Lightning ${english.username}"), findsOneWidget);
|
||||
expect(find.text(english.silent_payments), findsOneWidget);
|
||||
expect(find.text(english.accounts), findsNothing);
|
||||
expect(find.text(english.privacy), findsNothing);
|
||||
});
|
||||
|
||||
testWidgets("long localized wallet settings title stays clear of the back control",
|
||||
(tester) async {
|
||||
await tester.binding.setSurfaceSize(const Size(320, 640));
|
||||
addTearDown(() => tester.binding.setSurfaceSize(null));
|
||||
|
||||
when(() => wallet.type).thenReturn(WalletType.bsc);
|
||||
|
||||
await tester.pumpWidget(settingsApp(locale: const Locale("pt")));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final title = "BNB Smart Chain ${portuguese.settings_title}";
|
||||
await tester.tap(find.text(title));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
final titleFinder = find.text(title);
|
||||
final titleText = tester.widget<Text>(titleFinder);
|
||||
final backButton = find.byType(ModernButton);
|
||||
final titleGroup = find.byKey(ValueKey(title));
|
||||
|
||||
expect(titleText.maxLines, 1);
|
||||
expect(titleText.overflow, TextOverflow.ellipsis);
|
||||
expect(tester.getRect(titleGroup).left, greaterThanOrEqualTo(tester.getRect(backButton).right));
|
||||
expect(tester.getRect(titleGroup).center.dx, 160);
|
||||
expect(tester.takeException(), isNull);
|
||||
});
|
||||
|
||||
testWidgets("settings labels rebuild when the locale changes", (tester) async {
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
|
||||
await tester.pumpWidget(settingsApp());
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text(english.privacy), findsOneWidget);
|
||||
|
||||
await tester.pumpWidget(settingsApp(locale: const Locale("fr")));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text(french.privacy), findsOneWidget);
|
||||
if (english.privacy != french.privacy) {
|
||||
expect(find.text(english.privacy), findsNothing);
|
||||
}
|
||||
});
|
||||
|
||||
testWidgets("Accounts opens through nested wallet settings with the active dashboard",
|
||||
(tester) async {
|
||||
final accountListViewModel = _MockAccountListViewModel();
|
||||
final database = _MockDatabase();
|
||||
final walletInfo = _MockWalletInfo();
|
||||
final originalDatabase = sqlite.db;
|
||||
var scopeOpen = false;
|
||||
DashboardViewModel? routedDashboardViewModel;
|
||||
|
||||
when(() => wallet.hasAccountsSupport).thenReturn(true);
|
||||
when(() => dashboardViewModel.loadCardDesigns()).thenAnswer((_) async {});
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.xmr);
|
||||
when(() => wallet.walletInfo).thenReturn(walletInfo);
|
||||
when(() => walletInfo.internalId).thenReturn(42);
|
||||
when(() => accountListViewModel.accounts).thenReturn(const []);
|
||||
when(
|
||||
() => database.query(
|
||||
any(),
|
||||
where: any(named: "where"),
|
||||
whereArgs: any(named: "whereArgs"),
|
||||
),
|
||||
).thenAnswer((_) async => const []);
|
||||
|
||||
try {
|
||||
sqlite.db = database;
|
||||
getIt.pushNewScope(scopeName: "settings-accounts-route-test");
|
||||
scopeOpen = true;
|
||||
getIt.registerFactoryParam<WalletAccountsPage, DashboardViewModel, void>(
|
||||
(routedViewModel, _) {
|
||||
routedDashboardViewModel = routedViewModel;
|
||||
return WalletAccountsPage(
|
||||
accountListViewModel: accountListViewModel,
|
||||
accountEditOrCreateViewModel: _MockAccountEditOrCreateViewModel(),
|
||||
dashboardViewModel: routedViewModel,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
await tester.pumpWidget(settingsApp());
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
await tester.tap(find.text(S.current.accounts));
|
||||
await tester.tap(find.text("Monero ${english.settings_title}"));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text(english.accounts));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(routedDashboardViewModel, same(dashboardViewModel));
|
||||
expect(find.byType(AccountCustomizer), findsOneWidget);
|
||||
expect(find.byType(WalletAccountsPage), findsOneWidget);
|
||||
expect(tester.takeException(), isNull);
|
||||
} finally {
|
||||
await tester.pumpWidget(const SizedBox.shrink());
|
||||
await tester.pump();
|
||||
await getIt.popScope();
|
||||
if (scopeOpen) {
|
||||
await getIt.popScope();
|
||||
}
|
||||
sqlite.db = originalDatabase;
|
||||
}
|
||||
});
|
||||
|
||||
test("Bitcoin account copy is explicitly onchain when the capability is available", () {
|
||||
when(() => wallet.hasAccountsSupport).thenReturn(true);
|
||||
when(() => wallet.type).thenReturn(WalletType.bitcoin);
|
||||
|
||||
final accountItem = const WalletSettingsResolver()
|
||||
.resolveSections(dashboardViewModel)
|
||||
.expand((section) => section)
|
||||
.singleWhere((item) => item.route == Routes.walletAccountsPage);
|
||||
|
||||
expect(accountItem.title, english.accounts_onchain);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
import "package:cake_wallet/new-ui/viewmodels/card_customizer/card_customizer_bloc.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/account.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart" as sqlite;
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mocktail/mocktail.dart";
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWalletInfo extends Mock implements WalletInfo {}
|
||||
|
||||
class _MockWownero extends Mock implements wow.Wownero {}
|
||||
|
||||
class _MockWowneroAccountList extends Mock implements wow.WowneroAccountList {}
|
||||
|
||||
void main() {
|
||||
late DatabaseFactory? originalDatabaseFactory;
|
||||
late Database? originalDatabase;
|
||||
late wow.Wownero? originalWownero;
|
||||
late _MockWallet wallet;
|
||||
late _MockWownero wowneroAdapter;
|
||||
late _MockWowneroAccountList accountList;
|
||||
|
||||
setUpAll(() {
|
||||
originalDatabaseFactory = databaseFactoryOrNull;
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
databaseFactoryOrNull = originalDatabaseFactory;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
originalDatabase = sqlite.db;
|
||||
originalWownero = wow.wownero;
|
||||
sqlite.db = await databaseFactory.openDatabase(inMemoryDatabasePath);
|
||||
await sqlite.db!.execute("""
|
||||
CREATE TABLE ${BalanceCardStyleSettings.tableName} (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
gradientIndex INTEGER DEFAULT -1,
|
||||
useSpecialDesign BOOLEAN DEFAULT FALSE,
|
||||
hidden BOOLEAN DEFAULT FALSE,
|
||||
backgroundImagePath TEXT DEFAULT "",
|
||||
iconStyleIndex INTEGER DEFAULT 0,
|
||||
isGradientOnly BOOLEAN DEFAULT FALSE,
|
||||
cardOrder INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (walletInfoId, accountIndex)
|
||||
)
|
||||
""");
|
||||
|
||||
wallet = _MockWallet();
|
||||
final walletInfo = _MockWalletInfo();
|
||||
wowneroAdapter = _MockWownero();
|
||||
accountList = _MockWowneroAccountList();
|
||||
wow.wownero = wowneroAdapter;
|
||||
|
||||
when(() => wallet.hasNativeAccounts).thenReturn(true);
|
||||
when(() => wallet.type).thenReturn(WalletType.wownero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.wow);
|
||||
when(() => wallet.walletInfo).thenReturn(walletInfo);
|
||||
when(() => walletInfo.internalId).thenReturn(42);
|
||||
when(() => wallet.save()).thenAnswer((_) async {});
|
||||
when(() => wowneroAdapter.getCurrentAccount(wallet))
|
||||
.thenReturn(Account(id: 3, label: "Savings", balance: "2.0"));
|
||||
when(() => wowneroAdapter.getAccountList(wallet)).thenReturn(accountList);
|
||||
when(
|
||||
() => accountList.setLabelAccount(
|
||||
wallet,
|
||||
accountIndex: any(named: "accountIndex"),
|
||||
label: any(named: "label"),
|
||||
),
|
||||
).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
wow.wownero = originalWownero;
|
||||
await sqlite.db?.close();
|
||||
sqlite.db = originalDatabase;
|
||||
});
|
||||
|
||||
test("Wownero customization saves the selected account identity and name", () async {
|
||||
final bloc = CardCustomizerBloc(wallet);
|
||||
addTearDown(bloc.close);
|
||||
|
||||
final initial = await bloc.stream.firstWhere((state) => state is CardCustomizerInitial);
|
||||
expect(initial.accountIndex, 3);
|
||||
expect(initial.accountName, "Savings");
|
||||
expect(bloc.canHide, isFalse);
|
||||
|
||||
final renamed = bloc.stream.firstWhere((state) => state.accountName == "Long-term Savings");
|
||||
bloc.add(AccountNameChanged("Long-term Savings"));
|
||||
await renamed;
|
||||
|
||||
final saved = bloc.stream.firstWhere((state) => state is CardCustomizerSaved);
|
||||
bloc.add(DesignSaved());
|
||||
await saved;
|
||||
|
||||
verify(
|
||||
() => accountList.setLabelAccount(
|
||||
wallet,
|
||||
accountIndex: 3,
|
||||
label: "Long-term Savings",
|
||||
),
|
||||
).called(1);
|
||||
verify(() => wallet.save()).called(1);
|
||||
|
||||
final persisted = await BalanceCardStyleSettings.get(42, 3);
|
||||
expect(persisted, isNotNull);
|
||||
expect(await BalanceCardStyleSettings.get(42, -1), isNull);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import "package:cake_wallet/generated/i18n.dart";
|
||||
import "package:cake_wallet/locales/locale.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/accounts_promo.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mobx/mobx.dart" show Observable, runInAction;
|
||||
import "package:mocktail/mocktail.dart";
|
||||
|
||||
class _MockSettingsStore extends Mock implements SettingsStore {
|
||||
final _dismissed = Observable(false);
|
||||
|
||||
@override
|
||||
bool get accountsHomePromoDismissed => _dismissed.value;
|
||||
|
||||
@override
|
||||
set accountsHomePromoDismissed(bool value) => runInAction(() => _dismissed.value = value);
|
||||
}
|
||||
|
||||
void main() {
|
||||
late _MockSettingsStore settingsStore;
|
||||
late int openCount;
|
||||
|
||||
setUp(() {
|
||||
settingsStore = _MockSettingsStore();
|
||||
openCount = 0;
|
||||
});
|
||||
|
||||
Widget testApp() => MaterialApp(
|
||||
localizationsDelegates: localizationDelegates,
|
||||
supportedLocales: S.delegate.supportedLocales,
|
||||
home: Scaffold(
|
||||
body: AccountsPromo(
|
||||
settingsStore: settingsStore,
|
||||
walletName: "Bitcoin",
|
||||
onTap: () => openCount++,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
test("accounts promo is only eligible for Bitcoin", () {
|
||||
expect(AccountsPromo.supportsWallet(WalletType.bitcoin), isTrue);
|
||||
expect(AccountsPromo.supportsWallet(WalletType.monero), isFalse);
|
||||
expect(AccountsPromo.supportsWallet(WalletType.wownero), isFalse);
|
||||
});
|
||||
|
||||
testWidgets("shows generic current-wallet copy and opens Accounts", (tester) async {
|
||||
await tester.pumpWidget(testApp());
|
||||
|
||||
expect(find.text("Accounts for Bitcoin are here!"), findsOneWidget);
|
||||
expect(find.text("Manage all your assets in a unified interface"), findsOneWidget);
|
||||
expect(find.text("Don’t show this anymore"), findsOneWidget);
|
||||
|
||||
await tester.tap(find.text("Accounts for Bitcoin are here!"));
|
||||
expect(openCount, 1);
|
||||
});
|
||||
|
||||
testWidgets("explicit dismissal updates the store and hides only the promo", (tester) async {
|
||||
await tester.pumpWidget(testApp());
|
||||
|
||||
await tester.tap(find.text("Don’t show this anymore"));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(find.text("Accounts for Bitcoin are here!"), findsNothing);
|
||||
expect(settingsStore.accountsHomePromoDismissed, isTrue);
|
||||
expect(openCount, 0);
|
||||
});
|
||||
|
||||
testWidgets("a previously dismissed promo stays hidden", (tester) async {
|
||||
settingsStore.accountsHomePromoDismissed = true;
|
||||
await tester.pumpWidget(testApp());
|
||||
|
||||
expect(find.text("Accounts for Bitcoin are here!"), findsNothing);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_history_section.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/coins_page/assets_history/assets_top_bar.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mocktail/mocktail.dart";
|
||||
|
||||
class _MockDashboardViewModel extends Mock implements DashboardViewModel {}
|
||||
|
||||
void main() {
|
||||
testWidgets("uses the same assets gap for EVM and non-EVM wallets", (tester) async {
|
||||
final dashboardViewModel = _MockDashboardViewModel();
|
||||
const tabs = [AssetsHistorySectionTab("Assets", SizedBox.shrink(), null)];
|
||||
|
||||
Future<EdgeInsetsGeometry> pumpTopBar({required bool isEVMWallet}) async {
|
||||
when(() => dashboardViewModel.isEVMWallet).thenReturn(isEVMWallet);
|
||||
|
||||
await tester.pumpWidget(
|
||||
MaterialApp(
|
||||
home: CustomScrollView(
|
||||
slivers: [
|
||||
AssetsTopBar(
|
||||
onTabChange: (_) {},
|
||||
onTransactionHistoryOpened: () {},
|
||||
selectedTab: 0,
|
||||
tabs: tabs,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final sliver = tester.widget<SliverToBoxAdapter>(
|
||||
find.descendant(
|
||||
of: find.byType(AssetsTopBar),
|
||||
matching: find.byType(SliverToBoxAdapter),
|
||||
),
|
||||
);
|
||||
|
||||
return (sliver.child! as Padding).padding;
|
||||
}
|
||||
|
||||
expect(
|
||||
await pumpTopBar(isEVMWallet: true),
|
||||
const EdgeInsets.only(top: 20, left: 12, right: 18),
|
||||
);
|
||||
expect(
|
||||
await pumpTopBar(isEVMWallet: false),
|
||||
const EdgeInsets.only(top: 20, left: 12, right: 18),
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
import "package:cake_wallet/core/key_service.dart";
|
||||
import "package:cake_wallet/core/trade_monitor.dart";
|
||||
import "package:cake_wallet/store/anonpay/anonpay_transactions_store.dart";
|
||||
import "package:cake_wallet/store/app_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/order_filter_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/orders_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/payjoin_transactions_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/trade_filter_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/trades_store.dart";
|
||||
import "package:cake_wallet/store/dashboard/transaction_filter_store.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/balance_view_model.dart";
|
||||
import "package:cake_wallet/view_model/dashboard/dashboard_view_model.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/account.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart" as sqlite;
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mobx/mobx.dart" show ObservableList, ObservableMap;
|
||||
import "package:mocktail/mocktail.dart";
|
||||
import "package:shared_preferences/shared_preferences.dart";
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
class _MockBalanceViewModel extends Mock implements BalanceViewModel {}
|
||||
|
||||
class _MockTradeMonitor extends Mock implements TradeMonitor {}
|
||||
|
||||
class _MockAppStore extends Mock implements AppStore {}
|
||||
|
||||
class _MockTradesStore extends Mock implements TradesStore {}
|
||||
|
||||
class _MockTradeFilterStore extends Mock implements TradeFilterStore {}
|
||||
|
||||
class _MockOrderFilterStore extends Mock implements OrderFilterStore {}
|
||||
|
||||
class _MockTransactionFilterStore extends Mock implements TransactionFilterStore {}
|
||||
|
||||
class _MockSettingsStore extends Mock implements SettingsStore {}
|
||||
|
||||
class _MockOrdersStore extends Mock implements OrdersStore {}
|
||||
|
||||
class _MockAnonpayTransactionsStore extends Mock implements AnonpayTransactionsStore {}
|
||||
|
||||
class _MockPayjoinTransactionsStore extends Mock implements PayjoinTransactionsStore {}
|
||||
|
||||
class _MockKeyService extends Mock implements KeyService {}
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWalletInfo extends Mock implements WalletInfo {}
|
||||
|
||||
class _MockTransactionHistory extends Mock implements TransactionHistoryBase<TransactionInfo> {}
|
||||
|
||||
class _MockWownero extends Mock implements wow.Wownero {}
|
||||
|
||||
class _MockWowneroAccountList extends Mock implements wow.WowneroAccountList {}
|
||||
|
||||
class _MockWowneroWalletDetails extends Mock implements wow.WowneroWalletDetails {}
|
||||
|
||||
class _MockWowneroBalance extends Mock implements wow.WowneroBalance {}
|
||||
|
||||
class _DashboardViewModelProbe extends DashboardViewModelBase {
|
||||
_DashboardViewModelProbe({
|
||||
required super.balanceViewModel,
|
||||
required super.accountListViewModelFactory,
|
||||
required super.tradeMonitor,
|
||||
required super.appStore,
|
||||
required super.tradesStore,
|
||||
required super.tradeFilterStore,
|
||||
required super.orderFilterStore,
|
||||
required super.transactionFilterStore,
|
||||
required super.settingsStore,
|
||||
required super.ordersStore,
|
||||
required super.anonpayTransactionsStore,
|
||||
required super.payjoinTransactionsStore,
|
||||
required super.sharedPreferences,
|
||||
required super.keyService,
|
||||
});
|
||||
|
||||
bool _loadRealCards = false;
|
||||
|
||||
void enableRealCardLoading() => _loadRealCards = true;
|
||||
|
||||
@override
|
||||
void loadFilterItems() {}
|
||||
|
||||
@override
|
||||
Future<void> loadCardDesigns() => _loadRealCards ? super.loadCardDesigns() : Future<void>.value();
|
||||
|
||||
@override
|
||||
Future<bool> isBackgroundSyncEnabled() async => false;
|
||||
|
||||
@override
|
||||
Future<bool> isBatteryOptimizationEnabled() async => false;
|
||||
}
|
||||
|
||||
void main() {
|
||||
late DatabaseFactory? originalDatabaseFactory;
|
||||
late Database? originalDatabase;
|
||||
late wow.Wownero? originalWownero;
|
||||
|
||||
setUpAll(() {
|
||||
originalDatabaseFactory = databaseFactoryOrNull;
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
databaseFactoryOrNull = originalDatabaseFactory;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
originalDatabase = sqlite.db;
|
||||
originalWownero = wow.wownero;
|
||||
sqlite.db = await databaseFactory.openDatabase(inMemoryDatabasePath);
|
||||
await sqlite.db!.execute("""
|
||||
CREATE TABLE ${BalanceCardStyleSettings.tableName} (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
gradientIndex INTEGER DEFAULT -1,
|
||||
useSpecialDesign BOOLEAN DEFAULT FALSE,
|
||||
hidden BOOLEAN DEFAULT FALSE,
|
||||
backgroundImagePath TEXT DEFAULT "",
|
||||
iconStyleIndex INTEGER DEFAULT 0,
|
||||
isGradientOnly BOOLEAN DEFAULT FALSE,
|
||||
cardOrder INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (walletInfoId, accountIndex)
|
||||
)
|
||||
""");
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
wow.wownero = originalWownero;
|
||||
await sqlite.db?.close();
|
||||
sqlite.db = originalDatabase;
|
||||
});
|
||||
|
||||
test("Wownero keeps every account card and maps the selected account", () async {
|
||||
SharedPreferences.setMockInitialValues(<String, Object>{});
|
||||
final sharedPreferences = await SharedPreferences.getInstance();
|
||||
final wallet = _MockWallet();
|
||||
final walletInfo = _MockWalletInfo();
|
||||
final transactionHistory = _MockTransactionHistory();
|
||||
final appStore = _MockAppStore();
|
||||
final settingsStore = _MockSettingsStore();
|
||||
final tradesStore = _MockTradesStore();
|
||||
final tradeMonitor = _MockTradeMonitor();
|
||||
final wowneroAdapter = _MockWownero();
|
||||
final accountList = _MockWowneroAccountList();
|
||||
final walletDetails = _MockWowneroWalletDetails();
|
||||
final wowneroBalance = _MockWowneroBalance();
|
||||
final accounts = ObservableList.of(<Account>[
|
||||
Account(id: 0, label: "Primary", balance: "1.0"),
|
||||
Account(id: 1, label: "Savings", balance: "2.0"),
|
||||
]);
|
||||
|
||||
wow.wownero = wowneroAdapter;
|
||||
when(() => appStore.wallet).thenReturn(wallet);
|
||||
when(() => wallet.hasNativeAccounts).thenReturn(true);
|
||||
when(() => wallet.name).thenReturn("Wownero Wallet");
|
||||
when(() => wallet.id).thenReturn("wownero-wallet");
|
||||
when(() => wallet.type).thenReturn(WalletType.wownero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.wow);
|
||||
when(() => wallet.walletInfo).thenReturn(walletInfo);
|
||||
when(() => walletInfo.internalId).thenReturn(42);
|
||||
when(() => walletInfo.showSeedBackupReminder).thenReturn(false);
|
||||
when(() => wallet.transactionHistory).thenReturn(transactionHistory);
|
||||
when(() => transactionHistory.transactions)
|
||||
.thenReturn(ObservableMap<String, TransactionInfo>());
|
||||
when(() => wowneroAdapter.getAccountList(wallet)).thenReturn(accountList);
|
||||
when(() => accountList.accounts).thenReturn(accounts);
|
||||
when(() => wowneroAdapter.getCurrentAccount(wallet)).thenReturn(accounts[1]);
|
||||
when(() => wowneroAdapter.getWowneroWalletDetails(wallet)).thenReturn(walletDetails);
|
||||
when(() => walletDetails.account).thenReturn(accounts[1]);
|
||||
when(() => walletDetails.balance).thenReturn(wowneroBalance);
|
||||
when(() => settingsStore.mwebAlwaysScan).thenReturn(false);
|
||||
when(() => tradesStore.trades).thenReturn(const []);
|
||||
when(() => tradeMonitor.monitorActiveTrades(any())).thenReturn(null);
|
||||
|
||||
final viewModel = _DashboardViewModelProbe(
|
||||
balanceViewModel: _MockBalanceViewModel(),
|
||||
accountListViewModelFactory: () => null,
|
||||
tradeMonitor: tradeMonitor,
|
||||
appStore: appStore,
|
||||
tradesStore: tradesStore,
|
||||
tradeFilterStore: _MockTradeFilterStore(),
|
||||
orderFilterStore: _MockOrderFilterStore(),
|
||||
transactionFilterStore: _MockTransactionFilterStore(),
|
||||
settingsStore: settingsStore,
|
||||
ordersStore: _MockOrdersStore(),
|
||||
anonpayTransactionsStore: _MockAnonpayTransactionsStore(),
|
||||
payjoinTransactionsStore: _MockPayjoinTransactionsStore(),
|
||||
sharedPreferences: sharedPreferences,
|
||||
keyService: _MockKeyService(),
|
||||
);
|
||||
|
||||
expect(viewModel.cardAccountIndices, <int>[0, 1]);
|
||||
expect(viewModel.currentCardAccountIndex, 1);
|
||||
|
||||
viewModel.enableRealCardLoading();
|
||||
await viewModel.loadCardDesigns();
|
||||
|
||||
expect(viewModel.cardOrder, <int, int>{0: 0, 1: 1});
|
||||
expect(viewModel.cardDesigns, hasLength(2));
|
||||
expect(viewModel.currentCardDesign, same(viewModel.cardDesigns[1]));
|
||||
});
|
||||
}
|
||||
+136
@@ -0,0 +1,136 @@
|
||||
import "package:cake_wallet/core/execution_state.dart";
|
||||
import "package:cake_wallet/monero/monero.dart" as xmr;
|
||||
import "package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_edit_or_create_view_model.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/account.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/balance_card_style_settings.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/db/sqlite.dart" as sqlite;
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mobx/mobx.dart" show ObservableList;
|
||||
import "package:mocktail/mocktail.dart";
|
||||
import "package:sqflite_common_ffi/sqflite_ffi.dart";
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWalletInfo extends Mock implements WalletInfo {}
|
||||
|
||||
class _MockMoneroAccountList extends Mock implements xmr.MoneroAccountList {}
|
||||
|
||||
class _MockWowneroAccountList extends Mock implements wow.WowneroAccountList {}
|
||||
|
||||
void main() {
|
||||
late DatabaseFactory? originalDatabaseFactory;
|
||||
late Database? originalDatabase;
|
||||
late _MockWallet wallet;
|
||||
late _MockWalletInfo walletInfo;
|
||||
|
||||
setUpAll(() {
|
||||
originalDatabaseFactory = databaseFactoryOrNull;
|
||||
sqfliteFfiInit();
|
||||
databaseFactory = databaseFactoryFfi;
|
||||
});
|
||||
|
||||
tearDownAll(() {
|
||||
databaseFactoryOrNull = originalDatabaseFactory;
|
||||
});
|
||||
|
||||
setUp(() async {
|
||||
originalDatabase = sqlite.db;
|
||||
|
||||
sqlite.db = await databaseFactory.openDatabase(inMemoryDatabasePath);
|
||||
await sqlite.db!.execute("""
|
||||
CREATE TABLE ${BalanceCardStyleSettings.tableName} (
|
||||
walletInfoId INTEGER,
|
||||
accountIndex INTEGER DEFAULT -1,
|
||||
gradientIndex INTEGER DEFAULT -1,
|
||||
hidden BOOLEAN DEFAULT FALSE,
|
||||
useSpecialDesign BOOLEAN DEFAULT FALSE,
|
||||
backgroundImagePath TEXT DEFAULT "",
|
||||
iconStyleIndex INTEGER DEFAULT 0,
|
||||
isGradientOnly BOOLEAN DEFAULT FALSE,
|
||||
cardOrder INTEGER DEFAULT 0,
|
||||
PRIMARY KEY (walletInfoId, accountIndex)
|
||||
)
|
||||
""");
|
||||
|
||||
wallet = _MockWallet();
|
||||
walletInfo = _MockWalletInfo();
|
||||
when(() => wallet.walletInfo).thenReturn(walletInfo);
|
||||
when(() => walletInfo.internalId).thenReturn(42);
|
||||
when(() => wallet.save()).thenAnswer((_) async {});
|
||||
});
|
||||
|
||||
tearDown(() async {
|
||||
await sqlite.db?.close();
|
||||
sqlite.db = originalDatabase;
|
||||
});
|
||||
|
||||
test("creates a Wownero account and stores its card design at the next Wownero index", () async {
|
||||
final accountList = _MockWowneroAccountList();
|
||||
final accounts = ObservableList<Account>.of([
|
||||
Account(id: 0, label: "Primary"),
|
||||
Account(id: 1, label: "Savings"),
|
||||
]);
|
||||
when(() => wallet.type).thenReturn(WalletType.wownero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.wow);
|
||||
when(() => accountList.accounts).thenReturn(accounts);
|
||||
when(() => accountList.addAccount(wallet, label: "Spending")).thenAnswer((_) async {
|
||||
accounts.add(Account(id: 2, label: "Spending"));
|
||||
});
|
||||
|
||||
final viewModel = MoneroAccountEditOrCreateViewModel(
|
||||
null,
|
||||
accountList,
|
||||
wallet: wallet,
|
||||
)..label = "Spending";
|
||||
|
||||
await viewModel.save();
|
||||
|
||||
expect(viewModel.state, isA<ExecutedSuccessfullyState>());
|
||||
verify(() => accountList.addAccount(wallet, label: "Spending")).called(1);
|
||||
verify(() => wallet.save()).called(1);
|
||||
|
||||
final cardDesign = await BalanceCardStyleSettings.get(42, 2);
|
||||
expect(cardDesign, isNotNull);
|
||||
expect(cardDesign!.accountIndex, 2);
|
||||
expect(cardDesign.cardOrder, 2);
|
||||
});
|
||||
|
||||
test("creates a Monero account and stores its card design at the next Monero index", () async {
|
||||
final accountList = _MockMoneroAccountList();
|
||||
final accounts = ObservableList<Account>.of([
|
||||
Account(id: 0, label: "Primary"),
|
||||
]);
|
||||
when(() => wallet.type).thenReturn(WalletType.monero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.xmr);
|
||||
when(() => accountList.accounts).thenReturn(accounts);
|
||||
when(() => accountList.addAccount(wallet, label: "Savings")).thenAnswer((_) async {
|
||||
accounts.add(Account(id: 1, label: "Savings"));
|
||||
});
|
||||
|
||||
final viewModel = MoneroAccountEditOrCreateViewModel(
|
||||
accountList,
|
||||
null,
|
||||
wallet: wallet,
|
||||
)..label = "Savings";
|
||||
|
||||
await viewModel.save();
|
||||
|
||||
expect(viewModel.state, isA<ExecutedSuccessfullyState>());
|
||||
verify(() => accountList.addAccount(wallet, label: "Savings")).called(1);
|
||||
verify(() => wallet.save()).called(1);
|
||||
|
||||
final cardDesign = await BalanceCardStyleSettings.get(42, 1);
|
||||
expect(cardDesign, isNotNull);
|
||||
expect(cardDesign!.accountIndex, 1);
|
||||
expect(cardDesign.cardOrder, 1);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import "package:cake_wallet/monero/monero.dart" as xmr;
|
||||
import "package:cake_wallet/view_model/wallet_account_list/monero_account_list/monero_account_list_view_model.dart";
|
||||
import "package:cake_wallet/wownero/wownero.dart" as wow;
|
||||
import "package:cw_core/account.dart";
|
||||
import "package:cw_core/amount/money.dart";
|
||||
import "package:cw_core/balance.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/transaction_history.dart";
|
||||
import "package:cw_core/transaction_info.dart";
|
||||
import "package:cw_core/wallet_base.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
import "package:mobx/mobx.dart" show ObservableList;
|
||||
import "package:mocktail/mocktail.dart";
|
||||
|
||||
class _MockWallet extends Mock
|
||||
implements WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> {}
|
||||
|
||||
class _MockWownero extends Mock implements wow.Wownero {}
|
||||
|
||||
class _MockWowneroAccountList extends Mock implements wow.WowneroAccountList {}
|
||||
|
||||
void main() {
|
||||
late xmr.Monero? originalMonero;
|
||||
late wow.Wownero? originalWownero;
|
||||
late _MockWallet wallet;
|
||||
late _MockWownero wowneroAdapter;
|
||||
late _MockWowneroAccountList accountList;
|
||||
|
||||
setUp(() {
|
||||
originalMonero = xmr.monero;
|
||||
originalWownero = wow.wownero;
|
||||
|
||||
wallet = _MockWallet();
|
||||
wowneroAdapter = _MockWownero();
|
||||
accountList = _MockWowneroAccountList();
|
||||
|
||||
xmr.monero = null;
|
||||
wow.wownero = wowneroAdapter;
|
||||
|
||||
when(() => wallet.type).thenReturn(WalletType.wownero);
|
||||
when(() => wallet.currency).thenReturn(CryptoCurrency.wow);
|
||||
when(() => wowneroAdapter.getAccountList(wallet)).thenReturn(accountList);
|
||||
when(() => wowneroAdapter.getCurrentAccount(wallet))
|
||||
.thenReturn(Account(id: 1, label: "Selected", balance: "2.00000000001"));
|
||||
when(() => accountList.accounts).thenReturn(
|
||||
ObservableList.of([
|
||||
Account(id: 0, label: "Primary", balance: "0.0"),
|
||||
Account(id: 1, label: "Selected", balance: "2.00000000001"),
|
||||
]),
|
||||
);
|
||||
when(() => wowneroAdapter.getAccountFullBalance(0)).thenReturn(Money.zero(CryptoCurrency.wow));
|
||||
when(() => wowneroAdapter.getAccountFullBalance(1))
|
||||
.thenReturn(Money.fromInt(200000000001, CryptoCurrency.wow));
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
xmr.monero = originalMonero;
|
||||
wow.wownero = originalWownero;
|
||||
});
|
||||
|
||||
test("Wownero accounts retain exact amounts and select through the Wownero adapter", () async {
|
||||
final viewModel = MoneroAccountListViewModel(wallet);
|
||||
|
||||
expect(viewModel.selected.id, 1);
|
||||
expect(viewModel.selected.label, "Selected");
|
||||
expect(viewModel.accounts.first.isFunded, isFalse);
|
||||
expect(viewModel.selected.balance.amount, BigInt.from(200000000001));
|
||||
await viewModel.select(viewModel.selected);
|
||||
verify(() => wowneroAdapter.setCurrentAccount(wallet, 1, "Selected", "2.00000000001"))
|
||||
.called(1);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user