mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-28 06:28:41 +01:00
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.
266 lines
7.8 KiB
Dart
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);
|
|
});
|
|
});
|
|
}
|