diff --git a/lib/pages/coin_control/coin_control_rules.dart b/lib/pages/coin_control/coin_control_rules.dart new file mode 100644 index 000000000..97b0471a9 --- /dev/null +++ b/lib/pages/coin_control/coin_control_rules.dart @@ -0,0 +1,24 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + */ + +import '../../db/isar/main_db.dart'; + +CCFilter coinControlFilter({ + required bool isSearching, + required bool showBlocked, +}) => isSearching + ? CCFilter.all + : showBlocked + ? CCFilter.frozen + : CCFilter.available; + +bool canSelectCoinControlOutput({ + required bool isManageMode, + required bool isBlocked, + required bool isConfirmed, +}) => isManageMode || (!isBlocked && isConfirmed); diff --git a/lib/pages/coin_control/coin_control_view.dart b/lib/pages/coin_control/coin_control_view.dart index e7e58beb4..55f4d4d99 100644 --- a/lib/pages/coin_control/coin_control_view.dart +++ b/lib/pages/coin_control/coin_control_view.dart @@ -42,6 +42,7 @@ import '../../widgets/icon_widgets/x_icon.dart'; import '../../widgets/rounded_container.dart'; import '../../widgets/rounded_white_container.dart'; import '../../widgets/toggle.dart'; +import 'coin_control_rules.dart'; import 'utxo_card.dart'; import 'utxo_details_view.dart'; @@ -124,23 +125,22 @@ class _CoinControlViewState extends ConsumerState { Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - final minConfirms = ref - .watch(pWallets) - .getWallet(widget.walletId) - .cryptoCurrency - .minConfirms; - final coin = ref.watch(pWalletCoin(widget.walletId)); final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); final locale = ref.watch( localeServiceChangeNotifierProvider.select((value) => value.locale), ); + final filter = coinControlFilter( + isSearching: _isSearching, + showBlocked: _showBlocked, + ); + if (_sort == CCSortDescriptor.address && !_isSearching) { _list = null; _map = MainDB.instance.queryUTXOsGroupedByAddressSync( walletId: widget.walletId, - filter: CCFilter.all, + filter: filter, sort: _sort, searchTerm: "", locale: locale, @@ -150,11 +150,7 @@ class _CoinControlViewState extends ConsumerState { _map = null; _list = MainDB.instance.queryUTXOsSync( walletId: widget.walletId, - filter: _isSearching - ? CCFilter.all - : _showBlocked - ? CCFilter.frozen - : CCFilter.available, + filter: filter, sort: _sort, searchTerm: _isSearching ? searchController.text : "", locale: locale, @@ -287,8 +283,8 @@ class _CoinControlViewState extends ConsumerState { RoundedWhiteContainer( child: Text( "This option allows you to control, freeze, and utilize " - "outputs at your discretion. Tap the output circle to " - "select.", + "outputs at your discretion. Tap an output to select it, " + "or use the options button for more actions.", style: STextStyles.w500_14(context).copyWith( color: Theme.of(context) .extension()! @@ -297,7 +293,7 @@ class _CoinControlViewState extends ConsumerState { ), ), if (!_isSearching) const SizedBox(height: 10), - if (!(_isSearching || _map != null)) + if (!_isSearching) SizedBox( height: 48, child: Toggle( @@ -347,22 +343,21 @@ class _CoinControlViewState extends ConsumerState { ), walletId: widget.walletId, utxo: utxo, - canSelect: - widget.type == - CoinControlViewType.manage || - (widget.type == CoinControlViewType.use && - !utxo.isBlocked && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), - ), - ), - )), + canSelect: canSelectCoinControlOutput( + isManageMode: + widget.type == + CoinControlViewType.manage, + isBlocked: utxo.isBlocked, + isConfirmed: _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet(widget.walletId), + ), + ), + ), + ), initialSelectedState: isSelected, onSelectedChanged: (value) { if (value) { @@ -376,7 +371,7 @@ class _CoinControlViewState extends ConsumerState { } setState(() {}); }, - onPressed: () async { + onOptionsPressed: () async { final result = await Navigator.of(context) .pushNamed( UtxoDetailsView.routeName, @@ -416,23 +411,23 @@ class _CoinControlViewState extends ConsumerState { ), walletId: widget.walletId, utxo: utxo, - canSelect: - widget.type == - CoinControlViewType.manage || - (widget.type == - CoinControlViewType.use && - !_showBlocked && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), - ), - ), - )), + canSelect: canSelectCoinControlOutput( + isManageMode: + widget.type == + CoinControlViewType.manage, + isBlocked: utxo.isBlocked, + isConfirmed: _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, + ), + ), + ), + ), + ), initialSelectedState: isSelected, onSelectedChanged: (value) { if (value) { @@ -448,7 +443,7 @@ class _CoinControlViewState extends ConsumerState { } setState(() {}); }, - onPressed: () async { + onOptionsPressed: () async { final result = await Navigator.of(context) .pushNamed( @@ -566,24 +561,24 @@ class _CoinControlViewState extends ConsumerState { walletId: widget.walletId, utxo: utxo, canSelect: - widget.type == - CoinControlViewType - .manage || - (widget.type == - CoinControlViewType - .use && - !utxo.isBlocked && - _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), + canSelectCoinControlOutput( + isManageMode: + widget.type == + CoinControlViewType + .manage, + isBlocked: utxo.isBlocked, + isConfirmed: _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select( + (s) => s.getWallet( + widget.walletId, ), ), - )), + ), + ), + ), initialSelectedState: isSelected, onSelectedChanged: (value) { if (value) { @@ -603,7 +598,7 @@ class _CoinControlViewState extends ConsumerState { } setState(() {}); }, - onPressed: () async { + onOptionsPressed: () async { final result = await Navigator.of( context, diff --git a/lib/pages/coin_control/selectable_utxo_surface.dart b/lib/pages/coin_control/selectable_utxo_surface.dart new file mode 100644 index 000000000..3c9505b34 --- /dev/null +++ b/lib/pages/coin_control/selectable_utxo_surface.dart @@ -0,0 +1,57 @@ +/* + * This file is part of Stack Wallet. + * + * Copyright (c) 2023 Cypher Stack + * All Rights Reserved. + * The code is distributed under GPLv3 license, see LICENSE file for details. + */ + +import 'package:flutter/material.dart'; + +import '../../utilities/constants.dart'; + +class SelectableUtxoSurface extends StatelessWidget { + const SelectableUtxoSurface({ + super.key, + required this.canSelect, + required this.selected, + required this.onToggle, + required this.child, + this.color, + }); + + final bool canSelect; + final bool selected; + final VoidCallback onToggle; + final Widget child; + final Color? color; + + @override + Widget build(BuildContext context) { + if (!canSelect) { + return child; + } + + return Semantics( + selected: selected, + child: MaterialButton( + minWidth: 0, + padding: EdgeInsets.zero, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + color: color, + elevation: 0, + disabledElevation: 0, + hoverElevation: 0, + focusElevation: 0, + highlightElevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + Constants.size.circularBorderRadius, + ), + ), + onPressed: onToggle, + child: child, + ), + ); + } +} diff --git a/lib/pages/coin_control/utxo_card.dart b/lib/pages/coin_control/utxo_card.dart index 624b41eee..7dc4280c4 100644 --- a/lib/pages/coin_control/utxo_card.dart +++ b/lib/pages/coin_control/utxo_card.dart @@ -10,6 +10,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import '../../db/isar/main_db.dart'; import '../../models/isar/models/isar_models.dart'; @@ -17,14 +18,15 @@ import '../../providers/global/wallets_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; -import '../../utilities/constants.dart'; +import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../wallets/wallet/wallet.dart'; -import '../../widgets/conditional_parent.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/icon_widgets/utxo_status_icon.dart'; import '../../widgets/rounded_container.dart'; +import 'selectable_utxo_surface.dart'; class UtxoCard extends ConsumerStatefulWidget { const UtxoCard({ @@ -34,14 +36,14 @@ class UtxoCard extends ConsumerStatefulWidget { required this.onSelectedChanged, required this.initialSelectedState, required this.canSelect, - this.onPressed, + this.onOptionsPressed, }); final String walletId; final UTXO utxo; final void Function(bool) onSelectedChanged; final bool initialSelectedState; - final VoidCallback? onPressed; + final VoidCallback? onOptionsPressed; final bool canSelect; @override @@ -75,6 +77,25 @@ class _UtxoCardState extends ConsumerState { super.initState(); } + void _setSelected(bool selected) { + if (_selected == selected) { + return; + } + _selected = selected; + widget.onSelectedChanged(_selected); + setState(() {}); + } + + void _toggleSelected() => _setSelected(!_selected); + + @override + void didUpdateWidget(covariant UtxoCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.initialSelectedState != widget.initialSelectedState) { + _selected = widget.initialSelectedState; + } + } + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); @@ -82,28 +103,13 @@ class _UtxoCardState extends ConsumerState { final coin = ref.watch(pWalletCoin(widget.walletId)); final currentHeight = ref.watch(pWalletChainHeight(widget.walletId)); - return ConditionalParent( - condition: widget.onPressed != null, - builder: (child) => MaterialButton( - padding: const EdgeInsets.all(0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - color: Theme.of(context).extension()!.popupBG, - elevation: 0, - disabledElevation: 0, - hoverElevation: 0, - focusElevation: 0, - highlightElevation: 0, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(Constants.size.circularBorderRadius), - ), - onPressed: widget.onPressed, - child: child, - ), + return SelectableUtxoSurface( + canSelect: widget.canSelect, + selected: _selected, + onToggle: _toggleSelected, + color: Theme.of(context).extension()!.popupBG, child: RoundedContainer( - color: widget.onPressed == null - ? Theme.of(context).extension()!.popupBG - : Colors.transparent, + color: Theme.of(context).extension()!.popupBG, child: StreamBuilder( stream: stream, builder: (context, snapshot) { @@ -112,57 +118,42 @@ class _UtxoCardState extends ConsumerState { } return Row( children: [ - ConditionalParent( - condition: widget.canSelect, - builder: (child) => GestureDetector( - onTap: () { - _selected = !_selected; - widget.onSelectedChanged(_selected); - setState(() {}); - }, - child: child, - ), - child: UTXOStatusIcon( - blocked: utxo.isBlocked, - status: _isConfirmed( - utxo, - currentHeight, - ref.watch( - pWallets.select( - (s) => s.getWallet( - widget.walletId, - ), + UTXOStatusIcon( + blocked: utxo.isBlocked, + status: + _isConfirmed( + utxo, + currentHeight, + ref.watch( + pWallets.select((s) => s.getWallet(widget.walletId)), ), - ), - ) - ? UTXOStatusIconStatus.confirmed - : UTXOStatusIconStatus.unconfirmed, - background: - Theme.of(context).extension()!.popupBG, - selected: _selected, - width: 32, - height: 32, - ), - ), - const SizedBox( - width: 10, + ) + ? UTXOStatusIconStatus.confirmed + : UTXOStatusIconStatus.unconfirmed, + background: Theme.of(context) + .extension()! + .popupBG, + selected: _selected, + width: 32, + height: 32, ), + const SizedBox(width: 10), Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ Text( - ref.watch(pAmountFormatter(coin)).format( + ref + .watch(pAmountFormatter(coin)) + .format( utxo.value.toAmountAsRaw( fractionDigits: coin.fractionDigits, ), ), style: STextStyles.w600_14(context), ), - const SizedBox( - height: 2, - ), + const SizedBox(height: 2), Row( children: [ Flexible( @@ -182,6 +173,27 @@ class _UtxoCardState extends ConsumerState { ], ), ), + if (widget.onOptionsPressed != null) const SizedBox(width: 10), + if (widget.onOptionsPressed != null) + AppBarIconButton( + semanticsLabel: "Output options", + tooltip: "Output options", + size: 36, + shadows: const [], + color: Theme.of(context).extension()!.popupBG, + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + colorFilter: ColorFilter.mode( + Theme.of(context) + .extension()! + .textSubtitle1, + BlendMode.srcIn, + ), + width: 20, + height: 20, + ), + onPressed: widget.onOptionsPressed, + ), ], ); }, diff --git a/lib/pages_desktop_specific/coin_control/utxo_row.dart b/lib/pages_desktop_specific/coin_control/utxo_row.dart index e27d970fc..797b13978 100644 --- a/lib/pages_desktop_specific/coin_control/utxo_row.dart +++ b/lib/pages_desktop_specific/coin_control/utxo_row.dart @@ -10,22 +10,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:isar_community/isar.dart'; import '../../db/isar/main_db.dart'; import '../../models/isar/models/isar_models.dart'; +import '../../pages/coin_control/selectable_utxo_surface.dart'; import '../../pages/coin_control/utxo_details_view.dart'; import '../../providers/global/wallets_provider.dart'; import '../../themes/stack_colors.dart'; import '../../utilities/amount/amount.dart'; import '../../utilities/amount/amount_formatter.dart'; +import '../../utilities/assets.dart'; import '../../utilities/text_styles.dart'; import '../../wallets/crypto_currency/coins/namecoin.dart'; import '../../wallets/isar/providers/wallet_info_provider.dart'; import '../../wallets/wallet/impl/namecoin_wallet.dart'; import '../../widgets/conditional_parent.dart'; -import '../../widgets/custom_buttons/blue_text_button.dart'; -import '../../widgets/desktop/secondary_button.dart'; +import '../../widgets/custom_buttons/app_bar_icon_button.dart'; import '../../widgets/icon_widgets/utxo_status_icon.dart'; import '../../widgets/rounded_container.dart'; @@ -78,19 +80,34 @@ class _UtxoRowState extends ConsumerState { void _details() async { await showDialog( context: context, - builder: - (context) => - UtxoDetailsView(utxoId: utxo.id, walletId: widget.walletId), + builder: (context) => + UtxoDetailsView(utxoId: utxo.id, walletId: widget.walletId), ); } + void _toggleSelected() { + if (widget.compact && utxo.isBlocked) { + return; + } + _setSelected(!widget.data.selected); + } + + void _setSelected(bool selected) { + if (widget.data.selected == selected) { + return; + } + setState(() { + widget.data.selected = selected; + }); + widget.onSelectionChanged?.call(widget.data); + } + @override void initState() { - utxo = - MainDB.instance.isar.utxos - .where() - .idEqualTo(widget.data.utxoId) - .findFirstSync()!; + utxo = MainDB.instance.isar.utxos + .where() + .idEqualTo(widget.data.utxoId) + .findFirstSync()!; stream = MainDB.instance.watchUTXO(id: utxo.id); super.initState(); @@ -109,32 +126,23 @@ class _UtxoRowState extends ConsumerState { utxo = snapshot.data!; } - return RoundedContainer( - borderColor: - widget.compact && widget.compactWithBorder - ? Theme.of( - context, - ).extension()!.textFieldDefaultBG - : null, + final content = RoundedContainer( + borderColor: widget.compact && widget.compactWithBorder + ? Theme.of(context).extension()!.textFieldDefaultBG + : null, color: Theme.of(context).extension()!.popupBG, - boxShadow: - widget.data.selected && widget.raiseOnSelected - ? [ - Theme.of( - context, - ).extension()!.standardBoxShadow, - ] - : null, + boxShadow: widget.data.selected && widget.raiseOnSelected + ? [Theme.of(context).extension()!.standardBoxShadow] + : null, child: Row( children: [ if (!(widget.compact && utxo.isBlocked)) Checkbox( value: widget.data.selected, onChanged: (value) { - setState(() { - widget.data.selected = value!; - }); - widget.onSelectionChanged?.call(widget.data); + if (value != null) { + _setSelected(value); + } }, ), if (!(widget.compact && utxo.isBlocked)) @@ -143,21 +151,19 @@ class _UtxoRowState extends ConsumerState { blocked: utxo.isBlocked, status: (coin is Namecoin - ? (ref.watch(pWallets).getWallet(widget.walletId) - as NamecoinWallet) - .checkUtxoConfirmed( - utxo, - ref.watch( - pWalletChainHeight(widget.walletId), - ), - ) - : utxo.isConfirmed( - ref.watch(pWalletChainHeight(widget.walletId)), - coin.minConfirms, - coin.minCoinbaseConfirms, - )) - ? UTXOStatusIconStatus.confirmed - : UTXOStatusIconStatus.unconfirmed, + ? (ref.watch(pWallets).getWallet(widget.walletId) + as NamecoinWallet) + .checkUtxoConfirmed( + utxo, + ref.watch(pWalletChainHeight(widget.walletId)), + ) + : utxo.isConfirmed( + ref.watch(pWalletChainHeight(widget.walletId)), + coin.minConfirms, + coin.minCoinbaseConfirms, + )) + ? UTXOStatusIconStatus.confirmed + : UTXOStatusIconStatus.unconfirmed, background: Theme.of(context).extension()!.popupBG, selected: false, width: 32, @@ -207,29 +213,45 @@ class _UtxoRowState extends ConsumerState { utxo.name.isNotEmpty ? utxo.name : utxo.address ?? utxo.txid, - textAlign: - widget.compact ? TextAlign.left : TextAlign.center, + textAlign: widget.compact + ? TextAlign.left + : TextAlign.center, style: STextStyles.w500_12(context).copyWith( - color: - Theme.of( - context, - ).extension()!.textSubtitle1, + color: Theme.of(context) + .extension()! + .textSubtitle1, ), ), ), ), const SizedBox(width: 10), - widget.compact - ? CustomTextButton(text: "Details", onTap: _details) - : SecondaryButton( - width: 120, - buttonHeight: ButtonHeight.xs, - label: "Details", - onPressed: _details, + AppBarIconButton( + semanticsLabel: "Output options", + tooltip: "Output options", + size: 36, + shadows: const [], + color: Theme.of(context).extension()!.popupBG, + icon: SvgPicture.asset( + Assets.svg.verticalEllipsis, + colorFilter: ColorFilter.mode( + Theme.of(context).extension()!.textSubtitle1, + BlendMode.srcIn, ), + width: 20, + height: 20, + ), + onPressed: _details, + ), ], ), ); + + return SelectableUtxoSurface( + canSelect: !(widget.compact && utxo.isBlocked), + selected: widget.data.selected, + onToggle: _toggleSelected, + child: content, + ); }, ); } diff --git a/test/pages/coin_control/coin_control_rules_test.dart b/test/pages/coin_control/coin_control_rules_test.dart new file mode 100644 index 000000000..f316b3fc4 --- /dev/null +++ b/test/pages/coin_control/coin_control_rules_test.dart @@ -0,0 +1,55 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/db/isar/main_db.dart'; +import 'package:stackwallet/pages/coin_control/coin_control_rules.dart'; + +void main() { + test("uses one filter contract for flat and grouped output lists", () { + expect( + coinControlFilter(isSearching: true, showBlocked: false), + CCFilter.all, + ); + expect( + coinControlFilter(isSearching: false, showBlocked: false), + CCFilter.available, + ); + expect( + coinControlFilter(isSearching: false, showBlocked: true), + CCFilter.frozen, + ); + }); + + test("use mode excludes blocked and unconfirmed outputs", () { + expect( + canSelectCoinControlOutput( + isManageMode: false, + isBlocked: false, + isConfirmed: true, + ), + isTrue, + ); + expect( + canSelectCoinControlOutput( + isManageMode: false, + isBlocked: true, + isConfirmed: true, + ), + isFalse, + ); + expect( + canSelectCoinControlOutput( + isManageMode: false, + isBlocked: false, + isConfirmed: false, + ), + isFalse, + ); + expect( + canSelectCoinControlOutput( + isManageMode: true, + isBlocked: true, + isConfirmed: false, + ), + isTrue, + ); + }); +} diff --git a/test/widget_tests/selectable_utxo_surface_test.dart b/test/widget_tests/selectable_utxo_surface_test.dart new file mode 100644 index 000000000..7bcb70c21 --- /dev/null +++ b/test/widget_tests/selectable_utxo_surface_test.dart @@ -0,0 +1,116 @@ +import 'dart:ui' show Tristate; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stackwallet/pages/coin_control/selectable_utxo_surface.dart'; + +void main() { + Widget testApp({ + required bool canSelect, + required bool selected, + required VoidCallback onToggle, + required VoidCallback onOptions, + }) => MaterialApp( + home: Scaffold( + body: SelectableUtxoSurface( + canSelect: canSelect, + selected: selected, + onToggle: onToggle, + child: SizedBox( + width: 300, + height: 64, + child: Row( + children: [ + const Expanded(child: Text("Output 1")), + IconButton( + key: const Key("options"), + onPressed: onOptions, + icon: const Icon(Icons.more_vert), + ), + ], + ), + ), + ), + ), + ); + + testWidgets("row and nested options have independent actions", ( + tester, + ) async { + var toggles = 0; + var options = 0; + + await tester.pumpWidget( + testApp( + canSelect: true, + selected: false, + onToggle: () => toggles++, + onOptions: () => options++, + ), + ); + + await tester.tap(find.text("Output 1")); + await tester.pump(); + expect(toggles, 1); + expect(options, 0); + + await tester.tap(find.byKey(const Key("options"))); + await tester.pump(); + expect(toggles, 1); + expect(options, 1); + }); + + testWidgets("row selection supports keyboard activation and semantics", ( + tester, + ) async { + final semantics = tester.ensureSemantics(); + var toggles = 0; + + await tester.pumpWidget( + testApp( + canSelect: true, + selected: true, + onToggle: () => toggles++, + onOptions: () {}, + ), + ); + + final selectedSurface = find.byWidgetPredicate( + (widget) => widget is Semantics && widget.properties.selected == true, + ); + expect( + tester.getSemantics(selectedSurface).flagsCollection.isSelected, + Tristate.isTrue, + ); + + await tester.sendKeyEvent(LogicalKeyboardKey.tab); + await tester.pump(); + await tester.sendKeyEvent(LogicalKeyboardKey.enter); + await tester.pump(); + expect(toggles, 1); + + semantics.dispose(); + }); + + testWidgets("nonselectable rows retain their options action", (tester) async { + var toggles = 0; + var options = 0; + + await tester.pumpWidget( + testApp( + canSelect: false, + selected: false, + onToggle: () => toggles++, + onOptions: () => options++, + ), + ); + + await tester.tap(find.text("Output 1")); + await tester.tap(find.byKey(const Key("options"))); + await tester.pump(); + + expect(toggles, 0); + expect(options, 1); + }); +}