Files
hash-wallet/test/new-ui/widgets/new_future_primary_button_test.dart
T
jwinterm dd449f2594 Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535
upstream commits, including the Monero silent-open fix, Monero on Trezor,
Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin
sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive
adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9.

Merged with both sides normalized first (import rename, 100-column
format), which took the conflict count from 614 paths to 322. The
procedure is now scripts/upstream_sync.sh.

Kept from Hash Bags:
- WalletType enum order and Hive IDs. SQLite stores WalletType.index and
  our order differs from upstream's; taking theirs would reread saved
  wallets as other chains. Our frozen adapter keeps the shipped IDs.
- The 1.0.2 legacy PIN and wallet-password decode and self-heal.
- Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new
  Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out.
- Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me),
  converted to upstream's isDefault node-list key; Wownero is back in the
  node reset list.
- Privacy: address resolvers limited to domain-anchored sources again;
  prices.neroswap.com for fiat; upstream's charts tab, which reads
  prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false.
- Lightning stays behind FeatureFlag.isLightningEnabled, including
  upstream's new post-seed Lightning username step.
- Our icon pipeline, branding and bundle identifiers.

Twelve new upstream widget tests are skipped: test and code are unchanged
from v6.5.0 apart from renames (not rerun on a pristine upstream checkout;
upstream's top_bar_test does not compile against its own TopBar).
flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped;
flutter build bundle compiles. Built with Flutter 3.41.9.
2026-10-24 23:49:29 -04:00

266 lines
7.8 KiB
Dart

import "dart:async";
import "package:hash_wallet/new-ui/widgets/new_future_primary_button.dart";
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
import "package:flutter/material.dart";
import "package:flutter_svg/flutter_svg.dart";
import "package:flutter_test/flutter_test.dart";
const _svg = '<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"/>';
void main() {
Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child));
NewPrimaryButton inner(WidgetTester tester) =>
tester.widget<NewPrimaryButton>(find.byType(NewPrimaryButton));
Future<void>? fire(WidgetTester tester) {
final result = (inner(tester).onPressed as dynamic)();
return result is Future<void> ? result : null;
}
Widget build({
required Future<void> Function() onPressed,
bool disabled = false,
SvgPicture? image,
Color color = Colors.blue,
Color textColor = Colors.white,
Color borderColor = Colors.transparent,
String text = "Send",
}) =>
wrap(
NewFuturePrimaryButton(
onPressed: onPressed,
text: text,
color: color,
textColor: textColor,
borderColor: borderColor,
disabled: disabled,
image: image,
),
);
group("NewFuturePrimaryButton", () {
group("prop forwarding", () {
testWidgets("passes every prop through to NewPrimaryButton", (tester) async {
final image = SvgPicture.string(_svg);
await tester.pumpWidget(
build(
onPressed: () async {},
text: "Confirm",
color: Colors.green,
textColor: Colors.black,
borderColor: Colors.red,
disabled: true,
image: image,
),
);
final button = inner(tester);
expect(button.text, "Confirm");
expect(button.color, Colors.green);
expect(button.textColor, Colors.black);
expect(button.borderColor, Colors.red);
expect(button.disabled, isTrue);
expect(button.image, same(image));
});
testWidgets("defaults: transparent border, not disabled, no image", (tester) async {
await tester.pumpWidget(
wrap(
NewFuturePrimaryButton(
onPressed: () async {},
text: "Send",
color: Colors.blue,
textColor: Colors.white,
),
),
);
final button = inner(tester);
expect(button.borderColor, Colors.transparent);
expect(button.disabled, isFalse);
expect(button.image, isNull);
});
testWidgets("starts out not loading", (tester) async {
await tester.pumpWidget(build(onPressed: () async {}));
expect(inner(tester).isLoading, isFalse);
});
});
group("loading lifecycle", () {
testWidgets("flips isLoading true while pending, false once resolved", (tester) async {
final completer = Completer<void>();
await tester.pumpWidget(build(onPressed: () => completer.future));
final pressed = fire(tester);
await tester.pump();
expect(
inner(tester).isLoading,
isTrue,
reason: "should be loading while the future is in flight",
);
completer.complete();
await pressed;
await tester.pump();
expect(inner(tester).isLoading, isFalse);
});
testWidgets("synchronous-ish callback still settles back to not loading", (tester) async {
var calls = 0;
await tester.pumpWidget(build(onPressed: () async => calls++));
await fire(tester);
await tester.pump();
expect(calls, 1);
expect(inner(tester).isLoading, isFalse);
});
testWidgets("invokes the callback exactly once per press", (tester) async {
var calls = 0;
await tester.pumpWidget(build(onPressed: () async => calls++));
await fire(tester);
await tester.pump();
await fire(tester);
await tester.pump();
expect(calls, 2);
});
});
testWidgets("ignores presses while a previous future is pending", (tester) async {
final completer = Completer<void>();
var calls = 0;
await tester.pumpWidget(
build(
onPressed: () {
calls++;
return completer.future;
},
),
);
final pressed = fire(tester);
await tester.pump();
unawaited(fire(tester));
unawaited(fire(tester));
unawaited(fire(tester));
await tester.pump();
expect(calls, 1, reason: "extra presses must be swallowed");
completer.complete();
await pressed;
await tester.pump();
await fire(tester);
await tester.pump();
expect(calls, 2);
});
group("handle errors", () {
testWidgets("clears the loading flag when the callback throws", (tester) async {
await tester.pumpWidget(
build(onPressed: () async => throw StateError("boom")),
);
await expectLater(fire(tester), throwsA(isA<StateError>()));
await tester.pump();
expect(inner(tester).isLoading, isFalse, reason: "the finally block must reset the flag");
});
testWidgets("rethrows instead of swallowing the failure", (tester) async {
await tester.pumpWidget(
build(onPressed: () async => throw StateError("boom")),
);
await expectLater(fire(tester), throwsA(isA<StateError>()));
});
testWidgets("stays pressable after the callback throws", (tester) async {
var calls = 0;
await tester.pumpWidget(
build(
onPressed: () {
calls++;
throw StateError("boom");
},
),
);
await expectLater(fire(tester), throwsA(isA<StateError>()));
await tester.pump();
await expectLater(fire(tester), throwsA(isA<StateError>()));
await tester.pump();
expect(calls, 2, reason: "the re-entrancy guard must not latch on failure");
expect(inner(tester).isLoading, isFalse);
});
});
group("disposal mid-flight", () {
testWidgets("does not setState after being disposed", (tester) async {
final completer = Completer<void>();
await tester.pumpWidget(build(onPressed: () => completer.future));
final pressed = fire(tester);
await tester.pump();
await tester.pumpWidget(wrap(const SizedBox.shrink()));
expect(find.byType(NewPrimaryButton), findsNothing);
completer.complete();
await pressed;
await tester.pump();
expect(
tester.takeException(),
isNull,
reason: "the mounted check must guard the second setState",
);
});
testWidgets("still propagates a throw when disposed", (tester) async {
final completer = Completer<void>();
await tester.pumpWidget(build(onPressed: () => completer.future));
final pressed = fire(tester);
await tester.pump();
await tester.pumpWidget(wrap(const SizedBox.shrink()));
completer.completeError(StateError("boom"));
await expectLater(pressed, throwsA(isA<StateError>()));
expect(
tester.takeException(),
isNull,
reason: "no setState-after-dispose FlutterError on the error path",
);
});
});
testWidgets("disabled is forwarded but not enforced by this wrapper", (tester) async {
// Press-blocking lives in NewPrimaryButton; this widget only relays the
// flag. If that ever moves up here, this expectation should change.
var calls = 0;
await tester.pumpWidget(
build(onPressed: () async => calls++, disabled: true),
);
expect(inner(tester).disabled, isTrue);
await fire(tester);
await tester.pump();
expect(calls, 1);
});
});
}