dev
dart 265 lines 7.81 KB
Raw
1 import "dart:async";
2
3 import "package:cake_wallet/new-ui/widgets/new_future_primary_button.dart";
4 import "package:cake_wallet/new-ui/widgets/new_primary_button.dart";
5 import "package:flutter/material.dart";
6 import "package:flutter_svg/flutter_svg.dart";
7 import "package:flutter_test/flutter_test.dart";
8
9 const _svg = '<svg xmlns="http://www.w3.org/2000/svg" width="8" height="8"/>';
10
11 void main() {
12 Widget wrap(Widget child) => MaterialApp(home: Scaffold(body: child));
13
14 NewPrimaryButton inner(WidgetTester tester) =>
15 tester.widget<NewPrimaryButton>(find.byType(NewPrimaryButton));
16
17 Future<void>? fire(WidgetTester tester) {
18 final result = (inner(tester).onPressed as dynamic)();
19 return result is Future<void> ? result : null;
20 }
21
22 Widget build({
23 required Future<void> Function() onPressed,
24 bool disabled = false,
25 SvgPicture? image,
26 Color color = Colors.blue,
27 Color textColor = Colors.white,
28 Color borderColor = Colors.transparent,
29 String text = "Send",
30 }) =>
31 wrap(
32 NewFuturePrimaryButton(
33 onPressed: onPressed,
34 text: text,
35 color: color,
36 textColor: textColor,
37 borderColor: borderColor,
38 disabled: disabled,
39 image: image,
40 ),
41 );
42
43 group("NewFuturePrimaryButton", () {
44 group("prop forwarding", () {
45 testWidgets("passes every prop through to NewPrimaryButton", (tester) async {
46 final image = SvgPicture.string(_svg);
47
48 await tester.pumpWidget(
49 build(
50 onPressed: () async {},
51 text: "Confirm",
52 color: Colors.green,
53 textColor: Colors.black,
54 borderColor: Colors.red,
55 disabled: true,
56 image: image,
57 ),
58 );
59
60 final button = inner(tester);
61 expect(button.text, "Confirm");
62 expect(button.color, Colors.green);
63 expect(button.textColor, Colors.black);
64 expect(button.borderColor, Colors.red);
65 expect(button.disabled, isTrue);
66 expect(button.image, same(image));
67 });
68
69 testWidgets("defaults: transparent border, not disabled, no image", (tester) async {
70 await tester.pumpWidget(
71 wrap(
72 NewFuturePrimaryButton(
73 onPressed: () async {},
74 text: "Send",
75 color: Colors.blue,
76 textColor: Colors.white,
77 ),
78 ),
79 );
80
81 final button = inner(tester);
82 expect(button.borderColor, Colors.transparent);
83 expect(button.disabled, isFalse);
84 expect(button.image, isNull);
85 });
86
87 testWidgets("starts out not loading", (tester) async {
88 await tester.pumpWidget(build(onPressed: () async {}));
89
90 expect(inner(tester).isLoading, isFalse);
91 });
92 });
93
94 group("loading lifecycle", () {
95 testWidgets("flips isLoading true while pending, false once resolved", (tester) async {
96 final completer = Completer<void>();
97 await tester.pumpWidget(build(onPressed: () => completer.future));
98
99 final pressed = fire(tester);
100 await tester.pump();
101 expect(
102 inner(tester).isLoading,
103 isTrue,
104 reason: "should be loading while the future is in flight",
105 );
106
107 completer.complete();
108 await pressed;
109 await tester.pump();
110 expect(inner(tester).isLoading, isFalse);
111 });
112
113 testWidgets("synchronous-ish callback still settles back to not loading", (tester) async {
114 var calls = 0;
115 await tester.pumpWidget(build(onPressed: () async => calls++));
116
117 await fire(tester);
118 await tester.pump();
119
120 expect(calls, 1);
121 expect(inner(tester).isLoading, isFalse);
122 });
123
124 testWidgets("invokes the callback exactly once per press", (tester) async {
125 var calls = 0;
126 await tester.pumpWidget(build(onPressed: () async => calls++));
127
128 await fire(tester);
129 await tester.pump();
130 await fire(tester);
131 await tester.pump();
132
133 expect(calls, 2);
134 });
135 });
136
137 testWidgets("ignores presses while a previous future is pending", (tester) async {
138 final completer = Completer<void>();
139 var calls = 0;
140 await tester.pumpWidget(
141 build(
142 onPressed: () {
143 calls++;
144 return completer.future;
145 },
146 ),
147 );
148
149 final pressed = fire(tester);
150 await tester.pump();
151
152 unawaited(fire(tester));
153 unawaited(fire(tester));
154 unawaited(fire(tester));
155 await tester.pump();
156
157 expect(calls, 1, reason: "extra presses must be swallowed");
158
159 completer.complete();
160 await pressed;
161 await tester.pump();
162
163 await fire(tester);
164 await tester.pump();
165 expect(calls, 2);
166 });
167
168 group("handle errors", () {
169 testWidgets("clears the loading flag when the callback throws", (tester) async {
170 await tester.pumpWidget(
171 build(onPressed: () async => throw StateError("boom")),
172 );
173
174 await expectLater(fire(tester), throwsA(isA<StateError>()));
175 await tester.pump();
176
177 expect(inner(tester).isLoading, isFalse, reason: "the finally block must reset the flag");
178 });
179
180 testWidgets("rethrows instead of swallowing the failure", (tester) async {
181 await tester.pumpWidget(
182 build(onPressed: () async => throw StateError("boom")),
183 );
184
185 await expectLater(fire(tester), throwsA(isA<StateError>()));
186 });
187
188 testWidgets("stays pressable after the callback throws", (tester) async {
189 var calls = 0;
190 await tester.pumpWidget(
191 build(
192 onPressed: () {
193 calls++;
194 throw StateError("boom");
195 },
196 ),
197 );
198
199 await expectLater(fire(tester), throwsA(isA<StateError>()));
200 await tester.pump();
201 await expectLater(fire(tester), throwsA(isA<StateError>()));
202 await tester.pump();
203
204 expect(calls, 2, reason: "the re-entrancy guard must not latch on failure");
205 expect(inner(tester).isLoading, isFalse);
206 });
207 });
208
209 group("disposal mid-flight", () {
210 testWidgets("does not setState after being disposed", (tester) async {
211 final completer = Completer<void>();
212 await tester.pumpWidget(build(onPressed: () => completer.future));
213
214 final pressed = fire(tester);
215 await tester.pump();
216
217 await tester.pumpWidget(wrap(const SizedBox.shrink()));
218 expect(find.byType(NewPrimaryButton), findsNothing);
219
220 completer.complete();
221 await pressed;
222 await tester.pump();
223
224 expect(
225 tester.takeException(),
226 isNull,
227 reason: "the mounted check must guard the second setState",
228 );
229 });
230
231 testWidgets("still propagates a throw when disposed", (tester) async {
232 final completer = Completer<void>();
233 await tester.pumpWidget(build(onPressed: () => completer.future));
234
235 final pressed = fire(tester);
236 await tester.pump();
237 await tester.pumpWidget(wrap(const SizedBox.shrink()));
238
239 completer.completeError(StateError("boom"));
240
241 await expectLater(pressed, throwsA(isA<StateError>()));
242 expect(
243 tester.takeException(),
244 isNull,
245 reason: "no setState-after-dispose FlutterError on the error path",
246 );
247 });
248 });
249
250 testWidgets("disabled is forwarded but not enforced by this wrapper", (tester) async {
251 // Press-blocking lives in NewPrimaryButton; this widget only relays the
252 // flag. If that ever moves up here, this expectation should change.
253 var calls = 0;
254 await tester.pumpWidget(
255 build(onPressed: () async => calls++, disabled: true),
256 );
257
258 expect(inner(tester).disabled, isTrue);
259
260 await fire(tester);
261 await tester.pump();
262 expect(calls, 1);
263 });
264 });
265 }