mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-26 06:08:40 +01:00
Use baseunit provider (#3582)
* feat: introduce BaseUnit provider for money settings with aspect-based notifications and add unit tests * test: expand use_base_unit_provider_test with comprehensive scenarios for BaseUnitConfig behaviors
This commit is contained in:
@@ -1,15 +1,20 @@
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/use_base_unit_provider.dart";
|
||||
import "package:cake_wallet/store/settings_store.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
class MoneySettingsProvider extends StatelessWidget {
|
||||
const MoneySettingsProvider({required this.settingsStore, this.child, super.key});
|
||||
const MoneySettingsProvider({required this.settingsStore, required this.child, super.key});
|
||||
|
||||
final Widget? child;
|
||||
final Widget child;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) =>
|
||||
BlocProvider(create: (_) => MoneySettingsCubit(settingsStore), child: child);
|
||||
Widget build(BuildContext context) => BlocProvider(
|
||||
create: (_) => MoneySettingsCubit(settingsStore),
|
||||
child: BaseUnitScope(
|
||||
child: child,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import "dart:async";
|
||||
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:flutter/widgets.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
|
||||
/// Read-only view of the money display settings for the current subtree.
|
||||
///
|
||||
/// ```dart
|
||||
/// final useBaseUnit = BaseUnit.useBaseUnitOf(context, CryptoCurrency.btc);
|
||||
/// ```
|
||||
class BaseUnit extends InheritedModel<Currency> {
|
||||
const BaseUnit({required this.state, required super.child});
|
||||
|
||||
final MoneySettingsState state;
|
||||
|
||||
static bool useBaseUnitOf(BuildContext context, Currency currency) =>
|
||||
InheritedModel.inheritFrom<BaseUnit>(context, aspect: currency)!.state.useBaseUnit(currency);
|
||||
|
||||
static String getSymbolOf(BuildContext context, Currency currency) =>
|
||||
InheritedModel.inheritFrom<BaseUnit>(context, aspect: currency)!.state.getSymbol(currency);
|
||||
|
||||
@override
|
||||
bool updateShouldNotify(BaseUnit old) =>
|
||||
state.bitcoinAmountDisplayMode != old.state.bitcoinAmountDisplayMode;
|
||||
|
||||
@override
|
||||
bool updateShouldNotifyDependent(BaseUnit old, Set<Currency> aspects) => aspects.any((c) =>
|
||||
state.useBaseUnit(c) != old.state.useBaseUnit(c) ||
|
||||
state.getSymbol(c) != old.state.getSymbol(c));
|
||||
}
|
||||
|
||||
class BaseUnitScope extends StatefulWidget {
|
||||
const BaseUnitScope({required this.child, super.key});
|
||||
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
State<BaseUnitScope> createState() => _BaseUnitScopeState();
|
||||
}
|
||||
|
||||
class _BaseUnitScopeState extends State<BaseUnitScope> {
|
||||
late MoneySettingsState _state;
|
||||
late final StreamSubscription<MoneySettingsState> _sub;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final cubit = context.read<MoneySettingsCubit>();
|
||||
_state = cubit.state;
|
||||
_sub = cubit.stream.listen((s) => setState(() => _state = s));
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_sub.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => BaseUnit(state: _state, child: widget.child);
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
import "package:cake_wallet/entities/balance_display_mode.dart";
|
||||
import "package:cake_wallet/entities/bitcoin_amount_display_mode.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/money_settings_cubit.dart";
|
||||
import "package:cake_wallet/new-ui/widgets/money/use_base_unit_provider.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/currency.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_bloc/flutter_bloc.dart";
|
||||
import "package:flutter_test/flutter_test.dart";
|
||||
|
||||
class FakeMoneySettingsCubit extends Cubit<MoneySettingsState> implements MoneySettingsCubit {
|
||||
FakeMoneySettingsCubit(super.initialState);
|
||||
|
||||
void setState(MoneySettingsState state) => emit(state);
|
||||
}
|
||||
|
||||
MoneySettingsState _moneySettingsState({
|
||||
BitcoinAmountDisplayMode bitcoin = BitcoinAmountDisplayMode.bitcoin,
|
||||
BalanceDisplayMode display = BalanceDisplayMode.availableBalance,
|
||||
}) =>
|
||||
MoneySettingsState(
|
||||
bitcoinAmountDisplayMode: bitcoin,
|
||||
displayMode: display,
|
||||
);
|
||||
|
||||
Future<void> _pumpEmission(WidgetTester tester) => tester.pump(Duration.zero);
|
||||
|
||||
class _Probe extends StatelessWidget {
|
||||
const _Probe({required this.currencies, required this.log});
|
||||
|
||||
_Probe.single(Currency currency, {required this.log, super.key})
|
||||
: currencies = <Currency>[currency];
|
||||
|
||||
final List<Currency> currencies;
|
||||
final List<List<bool>> log;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final observed =
|
||||
currencies.map((c) => BaseUnit.useBaseUnitOf(context, c)).toList(growable: false);
|
||||
log.add(observed);
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class _SymbolProbe extends StatelessWidget {
|
||||
const _SymbolProbe(this.currency, this.log, {super.key});
|
||||
|
||||
final Currency currency;
|
||||
final List<String> log;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
log.add(BaseUnit.getSymbolOf(context, currency));
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
}
|
||||
|
||||
class _SubtreeCounter extends StatelessWidget {
|
||||
const _SubtreeCounter(this.log, {required this.child});
|
||||
|
||||
final List<void> log;
|
||||
final Widget child;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
log.add(null);
|
||||
return child;
|
||||
}
|
||||
}
|
||||
|
||||
Widget _host(FakeMoneySettingsCubit cubit, Widget child) => MaterialApp(
|
||||
home:
|
||||
BlocProvider<MoneySettingsCubit>.value(value: cubit, child: BaseUnitScope(child: child)),
|
||||
);
|
||||
|
||||
void main() {
|
||||
group("BaseUnit", () {
|
||||
group("BaseUnit.useBaseUnitOf", () {
|
||||
testWidgets("reads the value from the nearest ancestor", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi),
|
||||
);
|
||||
final log = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
_host(cubit, _Probe.single(CryptoCurrency.btc, log: log)),
|
||||
);
|
||||
|
||||
expect(log, [[true]]);
|
||||
});
|
||||
|
||||
testWidgets("nearest ancestor wins when models are nested", (tester) async {
|
||||
final log = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
BaseUnit(
|
||||
state: _moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi),
|
||||
child: BaseUnit(
|
||||
state: _moneySettingsState(bitcoin: BitcoinAmountDisplayMode.bitcoin),
|
||||
child: _Probe.single(CryptoCurrency.btc, log: log),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
expect(log.single, [false]);
|
||||
});
|
||||
|
||||
testWidgets("throws when no BaseUnit ancestor exists", (tester) async {
|
||||
await tester.pumpWidget(_Probe.single(CryptoCurrency.btc, log: <List<bool>>[]));
|
||||
|
||||
expect(tester.takeException(), isA<TypeError>());
|
||||
});
|
||||
});
|
||||
|
||||
group("BaseUnit.getSymbolOf", () {
|
||||
testWidgets("returns the ticker under whole-coin mode", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.bitcoin),
|
||||
);
|
||||
final log = <String>[];
|
||||
|
||||
await tester.pumpWidget(_host(cubit, _SymbolProbe(CryptoCurrency.btc, log)));
|
||||
|
||||
expect(log.single, CryptoCurrency.btc.symbol);
|
||||
});
|
||||
|
||||
testWidgets("flips to sats when the mode changes", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.bitcoin),
|
||||
);
|
||||
final log = <String>[];
|
||||
|
||||
await tester.pumpWidget(_host(cubit, _SymbolProbe(CryptoCurrency.btc, log)));
|
||||
|
||||
cubit.setState(_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi));
|
||||
await _pumpEmission(tester);
|
||||
|
||||
expect(log, [CryptoCurrency.btc.symbol, "sats"]);
|
||||
});
|
||||
|
||||
testWidgets("spares a symbol reader whose currency did not change", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi),
|
||||
);
|
||||
final btcLog = <String>[];
|
||||
final btclnLog = <String>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
cubit,
|
||||
Row(
|
||||
children: [
|
||||
_SymbolProbe(CryptoCurrency.btc, btcLog, key: const ValueKey("btc")),
|
||||
_SymbolProbe(CryptoCurrency.btcln, btclnLog, key: const ValueKey("btcln")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(btcLog.single, "sats");
|
||||
expect(btclnLog.single, "sats");
|
||||
|
||||
cubit.setState(_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshiForLightning));
|
||||
await _pumpEmission(tester);
|
||||
|
||||
expect(btcLog.length, 2);
|
||||
expect(btcLog.last, CryptoCurrency.btc.symbol);
|
||||
|
||||
expect(btclnLog.length, 1, reason: "btcln symbol did not change");
|
||||
});
|
||||
|
||||
testWidgets("a non-bitcoin symbol is stable across every mode", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.bitcoin),
|
||||
);
|
||||
final log = <String>[];
|
||||
|
||||
await tester.pumpWidget(_host(cubit, _SymbolProbe(CryptoCurrency.xmr, log)));
|
||||
|
||||
for (final mode in BitcoinAmountDisplayMode.all) {
|
||||
cubit.setState(_moneySettingsState(bitcoin: mode));
|
||||
await _pumpEmission(tester);
|
||||
}
|
||||
|
||||
expect(log.single, CryptoCurrency.xmr.symbol);
|
||||
});
|
||||
});
|
||||
|
||||
group("BaseUnit aspect notification", () {
|
||||
testWidgets("rebuilds a dependent whose currency flipped", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.bitcoin),
|
||||
);
|
||||
final log = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(_host(cubit, _Probe.single(CryptoCurrency.btc, log: log)));
|
||||
expect(log.length, 1);
|
||||
|
||||
cubit.setState(_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi));
|
||||
await _pumpEmission(tester);
|
||||
|
||||
expect(log.length, 2);
|
||||
expect(log.last, [true]);
|
||||
});
|
||||
|
||||
testWidgets("satoshi -> satoshiForLightning rebuilds btc but not btcln", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi),
|
||||
);
|
||||
final btcLog = <List<bool>>[];
|
||||
final btclnLog = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
cubit,
|
||||
Row(
|
||||
children: [
|
||||
_Probe.single(CryptoCurrency.btc, log: btcLog, key: const ValueKey("btc")),
|
||||
_Probe.single(CryptoCurrency.btcln, log: btclnLog, key: const ValueKey("btcln")),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(btcLog.single, [true]);
|
||||
expect(btclnLog.single, [true]);
|
||||
|
||||
cubit.setState(_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshiForLightning));
|
||||
await _pumpEmission(tester);
|
||||
|
||||
// btc: true -> false, so it must rebuild.
|
||||
expect(btcLog.length, 2);
|
||||
expect(btcLog.last, [false]);
|
||||
|
||||
// btcln: true -> true. The aspect filter should have spared it.
|
||||
expect(btclnLog.length, 1, reason: "btcln value did not change");
|
||||
});
|
||||
|
||||
testWidgets("a currency outside the bitcoin family never rebuilds", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.bitcoin),
|
||||
);
|
||||
final log = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(_host(cubit, _Probe.single(CryptoCurrency.xmr, log: log)));
|
||||
|
||||
for (final mode in BitcoinAmountDisplayMode.all) {
|
||||
cubit.setState(_moneySettingsState(bitcoin: mode));
|
||||
await _pumpEmission(tester);
|
||||
}
|
||||
|
||||
expect(log.length, 1);
|
||||
});
|
||||
|
||||
testWidgets("a dependent on several currencies rebuilds if any changed", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi),
|
||||
);
|
||||
final log = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
cubit,
|
||||
_Probe(currencies: <Currency>[CryptoCurrency.xmr, CryptoCurrency.btc], log: log),
|
||||
),
|
||||
);
|
||||
expect(log.single, [false, true]);
|
||||
|
||||
cubit.setState(_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshiForLightning));
|
||||
await _pumpEmission(tester);
|
||||
|
||||
expect(log.length, 2);
|
||||
expect(log.last, [false, false]);
|
||||
});
|
||||
|
||||
testWidgets("a value-equal but distinct state does not reach dependents", (tester) async {
|
||||
final initial = _moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi);
|
||||
final cubit = FakeMoneySettingsCubit(initial);
|
||||
final log = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(_host(cubit, _Probe.single(CryptoCurrency.btc, log: log)));
|
||||
expect(log.length, 1);
|
||||
|
||||
cubit.setState(initial.copyWith());
|
||||
await _pumpEmission(tester);
|
||||
|
||||
expect(log.length, 1);
|
||||
});
|
||||
|
||||
testWidgets("changing only displayMode does not reach useBaseUnit dependents",
|
||||
(tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(
|
||||
bitcoin: BitcoinAmountDisplayMode.satoshi,
|
||||
display: BalanceDisplayMode.availableBalance,
|
||||
),
|
||||
);
|
||||
final log = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
_host(cubit, _Probe.single(CryptoCurrency.btc, log: log)),
|
||||
);
|
||||
|
||||
cubit.setState(
|
||||
_moneySettingsState(
|
||||
bitcoin: BitcoinAmountDisplayMode.satoshi,
|
||||
display: BalanceDisplayMode.hiddenBalance,
|
||||
),
|
||||
);
|
||||
await _pumpEmission(tester);
|
||||
|
||||
expect(log.length, 1);
|
||||
});
|
||||
});
|
||||
|
||||
group("BaseUnitScope subtree isolation", () {
|
||||
testWidgets("an emission does not rebuild the subtree", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.bitcoin),
|
||||
);
|
||||
final subtreeLog = <void>[];
|
||||
final probeLog = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
cubit,
|
||||
_SubtreeCounter(
|
||||
subtreeLog,
|
||||
child: _Probe.single(CryptoCurrency.btc, log: probeLog),
|
||||
),
|
||||
),
|
||||
);
|
||||
expect(subtreeLog.length, 1);
|
||||
expect(probeLog.length, 1);
|
||||
|
||||
cubit.setState(_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi));
|
||||
await _pumpEmission(tester);
|
||||
|
||||
expect(probeLog.length, 2);
|
||||
expect(probeLog.last, [true]);
|
||||
|
||||
expect(subtreeLog.length, 1, reason: "the subtree was rebuilt");
|
||||
});
|
||||
|
||||
testWidgets("nested subtrees below the boundary are untouched", (tester) async {
|
||||
final cubit = FakeMoneySettingsCubit(
|
||||
_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.bitcoin),
|
||||
);
|
||||
final outerLog = <void>[];
|
||||
final innerLog = <void>[];
|
||||
final probeLog = <List<bool>>[];
|
||||
|
||||
await tester.pumpWidget(
|
||||
_host(
|
||||
cubit,
|
||||
_SubtreeCounter(
|
||||
outerLog,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.zero,
|
||||
child: _SubtreeCounter(
|
||||
innerLog,
|
||||
child: _Probe.single(CryptoCurrency.btc, log: probeLog),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
cubit.setState(_moneySettingsState(bitcoin: BitcoinAmountDisplayMode.satoshi));
|
||||
await _pumpEmission(tester);
|
||||
|
||||
expect(probeLog.length, 2);
|
||||
expect(outerLog.length, 1);
|
||||
expect(innerLog.length, 1);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user