1 | // Copyright 2014 The Flutter Authors. All rights reserved. |
2 | // Use of this source code is governed by a BSD-style license that can be |
3 | // found in the LICENSE file. |
4 | |
5 | import 'dart:async'; |
6 | import 'dart:io'; |
7 | |
8 | import 'package:flutter/cupertino.dart'; |
9 | import 'package:flutter/foundation.dart'; |
10 | import 'package:flutter/gestures.dart'; |
11 | import 'package:flutter/material.dart'; |
12 | import 'package:flutter/rendering.dart'; |
13 | import 'package:flutter/scheduler.dart'; |
14 | import 'package:flutter/services.dart'; |
15 | import 'package:flutter_test/flutter_test.dart'; |
16 | import 'package:matcher/expect.dart' as matcher; |
17 | import 'package:matcher/src/expect/async_matcher.dart' ; |
18 | |
19 | import 'multi_view_testing.dart'; |
20 | |
21 | void main() { |
22 | group('expectLater' , () { |
23 | testWidgets('completes when matcher completes' , (WidgetTester tester) async { |
24 | final Completer<void> completer = Completer<void>(); |
25 | final Future<void> future = expectLater(null, FakeMatcher(completer)); |
26 | String? result; |
27 | future.then<void>((void value) { |
28 | result = '123' ; |
29 | }); |
30 | matcher.expect(result, isNull); |
31 | completer.complete(); |
32 | matcher.expect(result, isNull); |
33 | await future; |
34 | await tester.pump(); |
35 | matcher.expect(result, '123' ); |
36 | }); |
37 | |
38 | testWidgets('respects the skip flag' , (WidgetTester tester) async { |
39 | final Completer<void> completer = Completer<void>(); |
40 | final Future<void> future = expectLater( |
41 | null, |
42 | FakeMatcher(completer), |
43 | skip: 'testing skip' , |
44 | ); // [intended] API testing |
45 | bool completed = false; |
46 | future.then<void>((_) { |
47 | completed = true; |
48 | }); |
49 | matcher.expect(completed, isFalse); |
50 | await future; |
51 | matcher.expect(completed, isTrue); |
52 | }); |
53 | }); |
54 | |
55 | group('group retry flag allows test to run multiple times' , () { |
56 | bool retried = false; |
57 | group('the group with retry flag' , () { |
58 | testWidgets('the test inside it' , (WidgetTester tester) async { |
59 | addTearDown(() => retried = true); |
60 | if (!retried) { |
61 | debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED' ); |
62 | } |
63 | expect(retried, isTrue); |
64 | }); |
65 | }, retry: 1); |
66 | }); |
67 | |
68 | group('testWidget retry flag allows test to run multiple times' , () { |
69 | bool retried = false; |
70 | testWidgets('the test with retry flag' , (WidgetTester tester) async { |
71 | addTearDown(() => retried = true); |
72 | if (!retried) { |
73 | debugPrint('DISREGARD NEXT FAILURE, IT IS EXPECTED' ); |
74 | } |
75 | expect(retried, isTrue); |
76 | }, retry: 1); |
77 | }); |
78 | |
79 | group('respects the group skip flag' , () { |
80 | testWidgets('should be skipped' , (WidgetTester tester) async { |
81 | expect(false, true); |
82 | }); |
83 | }, skip: true); // [intended] API testing |
84 | |
85 | group('pumping' , () { |
86 | testWidgets('pumping' , (WidgetTester tester) async { |
87 | await tester.pumpWidget(const Text('foo' , textDirection: TextDirection.ltr)); |
88 | int count; |
89 | |
90 | final AnimationController test = AnimationController( |
91 | duration: const Duration(milliseconds: 5100), |
92 | vsync: tester, |
93 | ); |
94 | count = await tester.pumpAndSettle(const Duration(seconds: 1)); |
95 | expect(count, 1); // it always pumps at least one frame |
96 | |
97 | test.forward(from: 0.0); |
98 | count = await tester.pumpAndSettle(const Duration(seconds: 1)); |
99 | // 1 frame at t=0, starting the animation |
100 | // 1 frame at t=1 |
101 | // 1 frame at t=2 |
102 | // 1 frame at t=3 |
103 | // 1 frame at t=4 |
104 | // 1 frame at t=5 |
105 | // 1 frame at t=6, ending the animation |
106 | expect(count, 7); |
107 | |
108 | test.forward(from: 0.0); |
109 | await tester.pump(); // starts the animation |
110 | count = await tester.pumpAndSettle(const Duration(seconds: 1)); |
111 | expect(count, 6); |
112 | |
113 | test.forward(from: 0.0); |
114 | await tester.pump(); // starts the animation |
115 | await tester.pump(); // has no effect |
116 | count = await tester.pumpAndSettle(const Duration(seconds: 1)); |
117 | expect(count, 6); |
118 | }); |
119 | |
120 | testWidgets('pumpFrames' , (WidgetTester tester) async { |
121 | final List<int> logPaints = <int>[]; |
122 | int? initial; |
123 | |
124 | final Widget target = _AlwaysAnimating( |
125 | onPaint: () { |
126 | final int current = SchedulerBinding.instance.currentFrameTimeStamp.inMicroseconds; |
127 | initial ??= current; |
128 | logPaints.add(current - initial!); |
129 | }, |
130 | ); |
131 | |
132 | await tester.pumpFrames(target, const Duration(milliseconds: 55)); |
133 | |
134 | // `pumpframes` defaults to 16 milliseconds and 683 microseconds per pump, |
135 | // so we expect 4 pumps of 16683 microseconds each in the 55ms duration. |
136 | expect(logPaints, <int>[0, 16683, 33366, 50049]); |
137 | logPaints.clear(); |
138 | |
139 | await tester.pumpFrames( |
140 | target, |
141 | const Duration(milliseconds: 30), |
142 | const Duration(milliseconds: 10), |
143 | ); |
144 | |
145 | // Since `pumpFrames` was given a 10ms interval per pump, we expect the |
146 | // results to continue from 50049 with 10000 microseconds per pump over |
147 | // the 30ms duration. |
148 | expect(logPaints, <int>[60049, 70049, 80049]); |
149 | }); |
150 | }); |
151 | group('pageBack' , () { |
152 | testWidgets('fails when there are no back buttons' , (WidgetTester tester) async { |
153 | await tester.pumpWidget(Container()); |
154 | |
155 | expect(expectAsync0(tester.pageBack), throwsA(isA<TestFailure>())); |
156 | }); |
157 | |
158 | testWidgets('successfully taps material back buttons' , (WidgetTester tester) async { |
159 | await tester.pumpWidget( |
160 | MaterialApp( |
161 | home: Center( |
162 | child: Builder( |
163 | builder: (BuildContext context) { |
164 | return ElevatedButton( |
165 | child: const Text('Next' ), |
166 | onPressed: () { |
167 | Navigator.push<void>( |
168 | context, |
169 | MaterialPageRoute<void>( |
170 | builder: (BuildContext context) { |
171 | return Scaffold(appBar: AppBar(title: const Text('Page 2' ))); |
172 | }, |
173 | ), |
174 | ); |
175 | }, |
176 | ); |
177 | }, |
178 | ), |
179 | ), |
180 | ), |
181 | ); |
182 | |
183 | await tester.tap(find.text('Next' )); |
184 | await tester.pump(); |
185 | await tester.pump(const Duration(milliseconds: 400)); |
186 | |
187 | await tester.pageBack(); |
188 | await tester.pump(); |
189 | await tester.pump(const Duration(milliseconds: 400)); |
190 | |
191 | expect(find.text('Next' ), findsOneWidget); |
192 | expect(find.text('Page 2' ), findsNothing); |
193 | }); |
194 | |
195 | testWidgets('successfully taps cupertino back buttons' , (WidgetTester tester) async { |
196 | await tester.pumpWidget( |
197 | MaterialApp( |
198 | home: Center( |
199 | child: Builder( |
200 | builder: (BuildContext context) { |
201 | return CupertinoButton( |
202 | child: const Text('Next' ), |
203 | onPressed: () { |
204 | Navigator.push<void>( |
205 | context, |
206 | CupertinoPageRoute<void>( |
207 | builder: (BuildContext context) { |
208 | return CupertinoPageScaffold( |
209 | navigationBar: const CupertinoNavigationBar(middle: Text('Page 2' )), |
210 | child: Container(), |
211 | ); |
212 | }, |
213 | ), |
214 | ); |
215 | }, |
216 | ); |
217 | }, |
218 | ), |
219 | ), |
220 | ), |
221 | ); |
222 | |
223 | await tester.tap(find.text('Next' )); |
224 | await tester.pump(); |
225 | await tester.pump(const Duration(milliseconds: 400)); |
226 | |
227 | await tester.pageBack(); |
228 | await tester.pump(); |
229 | await tester.pumpAndSettle(); |
230 | |
231 | expect(find.text('Next' ), findsOneWidget); |
232 | expect(find.text('Page 2' ), findsNothing); |
233 | }); |
234 | }); |
235 | |
236 | testWidgets('hasRunningAnimations control test' , (WidgetTester tester) async { |
237 | final AnimationController controller = AnimationController( |
238 | duration: const Duration(seconds: 1), |
239 | vsync: const TestVSync(), |
240 | ); |
241 | expect(tester.hasRunningAnimations, isFalse); |
242 | controller.forward(); |
243 | expect(tester.hasRunningAnimations, isTrue); |
244 | controller.stop(); |
245 | expect(tester.hasRunningAnimations, isFalse); |
246 | controller.forward(); |
247 | expect(tester.hasRunningAnimations, isTrue); |
248 | await tester.pumpAndSettle(); |
249 | expect(tester.hasRunningAnimations, isFalse); |
250 | }); |
251 | |
252 | testWidgets('pumpAndSettle control test' , (WidgetTester tester) async { |
253 | final AnimationController controller = AnimationController( |
254 | duration: const Duration(minutes: 525600), |
255 | vsync: const TestVSync(), |
256 | ); |
257 | expect(await tester.pumpAndSettle(), 1); |
258 | controller.forward(); |
259 | try { |
260 | await tester.pumpAndSettle(); |
261 | expect(true, isFalse); |
262 | } catch (e) { |
263 | expect(e, isFlutterError); |
264 | } |
265 | controller.stop(); |
266 | expect(await tester.pumpAndSettle(), 1); |
267 | controller.duration = const Duration(seconds: 1); |
268 | controller.forward(); |
269 | expect( |
270 | await tester.pumpAndSettle(const Duration(milliseconds: 300)), |
271 | 5, |
272 | ); // 0, 300, 600, 900, 1200ms |
273 | }); |
274 | |
275 | testWidgets('Input event array' , (WidgetTester tester) async { |
276 | final List<String> logs = <String>[]; |
277 | |
278 | await tester.pumpWidget( |
279 | Directionality( |
280 | textDirection: TextDirection.ltr, |
281 | child: Listener( |
282 | onPointerDown: (PointerDownEvent event) => logs.add('down ${event.buttons}' ), |
283 | onPointerMove: (PointerMoveEvent event) => logs.add('move ${event.buttons}' ), |
284 | onPointerUp: (PointerUpEvent event) => logs.add('up ${event.buttons}' ), |
285 | child: const Text('test' ), |
286 | ), |
287 | ), |
288 | ); |
289 | |
290 | final Offset location = tester.getCenter(find.text('test' )); |
291 | final List<PointerEventRecord> records = <PointerEventRecord>[ |
292 | PointerEventRecord(Duration.zero, <PointerEvent>[ |
293 | // Typically PointerAddedEvent is not used in testers, but for records |
294 | // captured on a device it is usually what start a gesture. |
295 | PointerAddedEvent(position: location), |
296 | PointerDownEvent(position: location, buttons: kSecondaryMouseButton, pointer: 1), |
297 | ]), |
298 | ...<PointerEventRecord>[ |
299 | for ( |
300 | Duration t = const Duration(milliseconds: 5); |
301 | t < const Duration(milliseconds: 80); |
302 | t += const Duration(milliseconds: 16) |
303 | ) |
304 | PointerEventRecord(t, <PointerEvent>[ |
305 | PointerMoveEvent( |
306 | timeStamp: t - const Duration(milliseconds: 1), |
307 | position: location, |
308 | buttons: kSecondaryMouseButton, |
309 | pointer: 1, |
310 | ), |
311 | ]), |
312 | ], |
313 | PointerEventRecord(const Duration(milliseconds: 80), <PointerEvent>[ |
314 | PointerUpEvent( |
315 | timeStamp: const Duration(milliseconds: 79), |
316 | position: location, |
317 | buttons: kSecondaryMouseButton, |
318 | pointer: 1, |
319 | ), |
320 | ]), |
321 | ]; |
322 | final List<Duration> timeDiffs = await tester.handlePointerEventRecord(records); |
323 | expect(timeDiffs.length, records.length); |
324 | for (final Duration diff in timeDiffs) { |
325 | expect(diff, Duration.zero); |
326 | } |
327 | |
328 | const String b = ' $kSecondaryMouseButton' ; |
329 | expect(logs.first, 'down $b' ); |
330 | for (int i = 1; i < logs.length - 1; i++) { |
331 | expect(logs[i], 'move $b' ); |
332 | } |
333 | expect(logs.last, 'up $b' ); |
334 | }); |
335 | |
336 | group('runAsync' , () { |
337 | testWidgets('works with no async calls' , (WidgetTester tester) async { |
338 | String? value; |
339 | await tester.runAsync(() async { |
340 | value = '123' ; |
341 | }); |
342 | expect(value, '123' ); |
343 | }); |
344 | |
345 | testWidgets('works with real async calls' , (WidgetTester tester) async { |
346 | final StringBuffer buf = StringBuffer('1' ); |
347 | await tester.runAsync(() async { |
348 | buf.write('2' ); |
349 | //ignore: avoid_slow_async_io |
350 | await Directory.current.stat(); |
351 | buf.write('3' ); |
352 | }); |
353 | buf.write('4' ); |
354 | expect(buf.toString(), '1234' ); |
355 | }); |
356 | |
357 | testWidgets('propagates return values' , (WidgetTester tester) async { |
358 | final String? value = await tester.runAsync<String>(() async { |
359 | return '123' ; |
360 | }); |
361 | expect(value, '123' ); |
362 | }); |
363 | |
364 | testWidgets('reports errors via framework' , (WidgetTester tester) async { |
365 | final String? value = await tester.runAsync<String>(() async { |
366 | throw ArgumentError(); |
367 | }); |
368 | expect(value, isNull); |
369 | expect(tester.takeException(), isArgumentError); |
370 | }); |
371 | |
372 | testWidgets('disallows re-entry' , (WidgetTester tester) async { |
373 | final Completer<void> completer = Completer<void>(); |
374 | tester.runAsync<void>(() => completer.future); |
375 | expect(() => tester.runAsync(() async {}), throwsA(isA<TestFailure>())); |
376 | completer.complete(); |
377 | }); |
378 | |
379 | testWidgets('maintains existing zone values' , (WidgetTester tester) async { |
380 | final Object key = Object(); |
381 | await runZoned<Future<void>>(() { |
382 | expect(Zone.current[key], 'abczed' ); |
383 | return tester.runAsync<void>(() async { |
384 | expect(Zone.current[key], 'abczed' ); |
385 | }); |
386 | }, zoneValues: <dynamic, dynamic>{key: 'abczed' }); |
387 | }); |
388 | |
389 | testWidgets('control test (return value)' , (WidgetTester tester) async { |
390 | final String? result = await tester.binding.runAsync<String>(() async => 'Judy Turner' ); |
391 | expect(result, 'Judy Turner' ); |
392 | }); |
393 | |
394 | testWidgets('async throw' , (WidgetTester tester) async { |
395 | final String? result = await tester.binding.runAsync<Never>( |
396 | () async => throw Exception('Lois Dilettente' ), |
397 | ); |
398 | expect(result, isNull); |
399 | expect(tester.takeException(), isNotNull); |
400 | }); |
401 | |
402 | testWidgets('sync throw' , (WidgetTester tester) async { |
403 | final String? result = await tester.binding.runAsync<Never>( |
404 | () => throw Exception('Butch Barton' ), |
405 | ); |
406 | expect(result, isNull); |
407 | expect(tester.takeException(), isNotNull); |
408 | }); |
409 | |
410 | testWidgets('runs in original zone' , (WidgetTester tester) async { |
411 | final Zone testZone = Zone.current; |
412 | Zone? runAsyncZone; |
413 | Zone? timerZone; |
414 | Zone? periodicTimerZone; |
415 | Zone? microtaskZone; |
416 | |
417 | Zone? innerZone; |
418 | Zone? innerTimerZone; |
419 | Zone? innerPeriodicTimerZone; |
420 | Zone? innerMicrotaskZone; |
421 | |
422 | await tester.binding.runAsync<void>(() async { |
423 | final Zone currentZone = Zone.current; |
424 | runAsyncZone = currentZone; |
425 | |
426 | // Complete a future when all callbacks have completed. |
427 | int pendingCallbacks = 6; |
428 | final Completer<void> callbacksDone = Completer<void>(); |
429 | void onCallback() { |
430 | if (--pendingCallbacks == 0) { |
431 | testZone.run(() { |
432 | callbacksDone.complete(null); |
433 | }); |
434 | } |
435 | } |
436 | |
437 | // On the runAsync zone itself. |
438 | currentZone.createTimer(Duration.zero, () { |
439 | timerZone = Zone.current; |
440 | onCallback(); |
441 | }); |
442 | currentZone.createPeriodicTimer(Duration.zero, (Timer timer) { |
443 | timer.cancel(); |
444 | periodicTimerZone = Zone.current; |
445 | onCallback(); |
446 | }); |
447 | currentZone.scheduleMicrotask(() { |
448 | microtaskZone = Zone.current; |
449 | onCallback(); |
450 | }); |
451 | |
452 | // On a nested user-created zone. |
453 | final Zone inner = runZoned(() => Zone.current); |
454 | innerZone = inner; |
455 | inner.createTimer(Duration.zero, () { |
456 | innerTimerZone = Zone.current; |
457 | onCallback(); |
458 | }); |
459 | inner.createPeriodicTimer(Duration.zero, (Timer timer) { |
460 | timer.cancel(); |
461 | innerPeriodicTimerZone = Zone.current; |
462 | onCallback(); |
463 | }); |
464 | inner.scheduleMicrotask(() { |
465 | innerMicrotaskZone = Zone.current; |
466 | onCallback(); |
467 | }); |
468 | |
469 | await callbacksDone.future; |
470 | }); |
471 | expect(runAsyncZone, isNotNull); |
472 | expect(timerZone, same(runAsyncZone)); |
473 | expect(periodicTimerZone, same(runAsyncZone)); |
474 | expect(microtaskZone, same(runAsyncZone)); |
475 | |
476 | expect(innerZone, isNotNull); |
477 | expect(innerTimerZone, same(innerZone)); |
478 | expect(innerPeriodicTimerZone, same(innerZone)); |
479 | expect(innerMicrotaskZone, same(innerZone)); |
480 | |
481 | expect(runAsyncZone, isNot(same(testZone))); |
482 | expect(runAsyncZone, isNot(same(innerZone))); |
483 | expect(innerZone, isNot(same(testZone))); |
484 | }); |
485 | }); |
486 | |
487 | group('showKeyboard' , () { |
488 | testWidgets('can be called twice' , (WidgetTester tester) async { |
489 | await tester.pumpWidget(MaterialApp(home: Material(child: Center(child: TextFormField())))); |
490 | await tester.showKeyboard(find.byType(TextField)); |
491 | await tester.testTextInput.receiveAction(TextInputAction.done); |
492 | await tester.pump(); |
493 | await tester.showKeyboard(find.byType(TextField)); |
494 | await tester.testTextInput.receiveAction(TextInputAction.done); |
495 | await tester.pump(); |
496 | await tester.showKeyboard(find.byType(TextField)); |
497 | await tester.showKeyboard(find.byType(TextField)); |
498 | await tester.pump(); |
499 | }); |
500 | |
501 | testWidgets( |
502 | 'can focus on offstage text input field if finder says not to skip offstage nodes' , |
503 | (WidgetTester tester) async { |
504 | await tester.pumpWidget( |
505 | MaterialApp(home: Material(child: Offstage(child: TextFormField()))), |
506 | ); |
507 | await tester.showKeyboard(find.byType(TextField, skipOffstage: false)); |
508 | }, |
509 | ); |
510 | }); |
511 | |
512 | testWidgets('verifyTickersWereDisposed control test' , (WidgetTester tester) async { |
513 | late FlutterError error; |
514 | final Ticker ticker = tester.createTicker((Duration duration) {}); |
515 | ticker.start(); |
516 | try { |
517 | tester.verifyTickersWereDisposed('' ); |
518 | } on FlutterError catch (e) { |
519 | error = e; |
520 | } finally { |
521 | expect(error, isNotNull); |
522 | expect(error.diagnostics.length, 4); |
523 | expect(error.diagnostics[2].level, DiagnosticLevel.hint); |
524 | expect( |
525 | error.diagnostics[2].toStringDeep(), |
526 | 'Tickers used by AnimationControllers should be disposed by\n' |
527 | 'calling dispose() on the AnimationController itself. Otherwise,\n' |
528 | 'the ticker will leak.\n' , |
529 | ); |
530 | expect(error.diagnostics.last, isA<DiagnosticsProperty<Ticker>>()); |
531 | expect(error.diagnostics.last.value, ticker); |
532 | expect( |
533 | error.toStringDeep(), |
534 | startsWith( |
535 | 'FlutterError\n' |
536 | ' A Ticker was active .\n' |
537 | ' All Tickers must be disposed.\n' |
538 | ' Tickers used by AnimationControllers should be disposed by\n' |
539 | ' calling dispose() on the AnimationController itself. Otherwise,\n' |
540 | ' the ticker will leak.\n' |
541 | ' The offending ticker was:\n' |
542 | ' _TestTicker()\n' , |
543 | ), |
544 | ); |
545 | } |
546 | ticker.stop(); |
547 | }); |
548 | |
549 | group('testWidgets variants work' , () { |
550 | int numberOfVariationsRun = 0; |
551 | |
552 | testWidgets( |
553 | 'variant tests run all values provided' , |
554 | (WidgetTester tester) async { |
555 | if (debugDefaultTargetPlatformOverride == null) { |
556 | expect(numberOfVariationsRun, equals(TargetPlatform.values.length)); |
557 | } else { |
558 | numberOfVariationsRun += 1; |
559 | } |
560 | }, |
561 | variant: TargetPlatformVariant(TargetPlatform.values.toSet()), |
562 | ); |
563 | |
564 | testWidgets( |
565 | 'variant tests have descriptions with details' , |
566 | (WidgetTester tester) async { |
567 | if (debugDefaultTargetPlatformOverride == null) { |
568 | expect(tester.testDescription, equals('variant tests have descriptions with details' )); |
569 | } else { |
570 | expect( |
571 | tester.testDescription, |
572 | equals( |
573 | 'variant tests have descriptions with details (variant: $debugDefaultTargetPlatformOverride)' , |
574 | ), |
575 | ); |
576 | } |
577 | }, |
578 | variant: TargetPlatformVariant(TargetPlatform.values.toSet()), |
579 | ); |
580 | }); |
581 | |
582 | group('TargetPlatformVariant' , () { |
583 | int numberOfVariationsRun = 0; |
584 | TargetPlatform? origTargetPlatform; |
585 | |
586 | setUpAll(() { |
587 | origTargetPlatform = debugDefaultTargetPlatformOverride; |
588 | }); |
589 | |
590 | tearDownAll(() { |
591 | expect(debugDefaultTargetPlatformOverride, equals(origTargetPlatform)); |
592 | }); |
593 | |
594 | testWidgets( |
595 | 'TargetPlatformVariant.only tests given value' , |
596 | (WidgetTester tester) async { |
597 | expect(debugDefaultTargetPlatformOverride, equals(TargetPlatform.iOS)); |
598 | expect(defaultTargetPlatform, equals(TargetPlatform.iOS)); |
599 | }, |
600 | variant: TargetPlatformVariant.only(TargetPlatform.iOS), |
601 | ); |
602 | |
603 | group('all' , () { |
604 | testWidgets('TargetPlatformVariant.all tests run all variants' , (WidgetTester tester) async { |
605 | if (debugDefaultTargetPlatformOverride == null) { |
606 | expect(numberOfVariationsRun, equals(TargetPlatform.values.length)); |
607 | } else { |
608 | numberOfVariationsRun += 1; |
609 | } |
610 | }, variant: TargetPlatformVariant.all()); |
611 | |
612 | const Set<TargetPlatform> excludePlatforms = <TargetPlatform>{ |
613 | TargetPlatform.android, |
614 | TargetPlatform.linux, |
615 | }; |
616 | testWidgets( |
617 | 'TargetPlatformVariant.all, excluding runs an all variants except those provided in excluding' , |
618 | (WidgetTester tester) async { |
619 | if (debugDefaultTargetPlatformOverride == null) { |
620 | expect( |
621 | numberOfVariationsRun, |
622 | equals(TargetPlatform.values.length - excludePlatforms.length), |
623 | ); |
624 | expect( |
625 | excludePlatforms, |
626 | isNot(contains(debugDefaultTargetPlatformOverride)), |
627 | reason: 'this test should not run on any platform in excludePlatforms' , |
628 | ); |
629 | } else { |
630 | numberOfVariationsRun += 1; |
631 | } |
632 | }, |
633 | variant: TargetPlatformVariant.all(excluding: excludePlatforms), |
634 | ); |
635 | }); |
636 | |
637 | testWidgets('TargetPlatformVariant.desktop + mobile contains all TargetPlatform values' , ( |
638 | WidgetTester tester, |
639 | ) async { |
640 | final TargetPlatformVariant all = TargetPlatformVariant.all(); |
641 | final TargetPlatformVariant desktop = TargetPlatformVariant.all(); |
642 | final TargetPlatformVariant mobile = TargetPlatformVariant.all(); |
643 | expect(desktop.values.union(mobile.values), equals(all.values)); |
644 | }); |
645 | }); |
646 | |
647 | group('Pending timer' , () { |
648 | late TestExceptionReporter currentExceptionReporter; |
649 | setUp(() { |
650 | currentExceptionReporter = reportTestException; |
651 | }); |
652 | |
653 | tearDown(() { |
654 | reportTestException = currentExceptionReporter; |
655 | }); |
656 | |
657 | test('Throws assertion message without code' , () async { |
658 | late FlutterErrorDetails flutterErrorDetails; |
659 | reportTestException = (FlutterErrorDetails details, String testDescription) { |
660 | flutterErrorDetails = details; |
661 | }; |
662 | |
663 | final TestWidgetsFlutterBinding binding = TestWidgetsFlutterBinding.ensureInitialized(); |
664 | debugPrint('DISREGARD NEXT PENDING TIMER LIST, IT IS EXPECTED' ); |
665 | await binding.runTest(() async { |
666 | final Timer timer = Timer(const Duration(seconds: 1), () {}); |
667 | expect(timer.isActive, true); |
668 | }, () {}); |
669 | |
670 | expect(flutterErrorDetails.exception, isA<AssertionError>()); |
671 | expect( |
672 | (flutterErrorDetails.exception as AssertionError).message, |
673 | 'A Timer is still pending even after the widget tree was disposed.' , |
674 | ); |
675 | expect(binding.inTest, true); |
676 | binding.postTest(); |
677 | }); |
678 | }); |
679 | |
680 | group('Accessibility announcements testing API' , () { |
681 | testWidgets('Returns the list of announcements' , (WidgetTester tester) async { |
682 | // Make sure the handler is properly set |
683 | expect( |
684 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler( |
685 | SystemChannels.accessibility.name, |
686 | null, |
687 | ), |
688 | isFalse, |
689 | ); |
690 | |
691 | await SemanticsService.announce('announcement 1' , TextDirection.ltr); |
692 | await SemanticsService.announce( |
693 | 'announcement 2' , |
694 | TextDirection.rtl, |
695 | assertiveness: Assertiveness.assertive, |
696 | ); |
697 | await SemanticsService.announce('announcement 3' , TextDirection.rtl); |
698 | |
699 | final List<CapturedAccessibilityAnnouncement> list = tester.takeAnnouncements(); |
700 | expect(list, hasLength(3)); |
701 | final CapturedAccessibilityAnnouncement first = list[0]; |
702 | expect(first.message, 'announcement 1' ); |
703 | expect(first.textDirection, TextDirection.ltr); |
704 | |
705 | final CapturedAccessibilityAnnouncement second = list[1]; |
706 | expect(second.message, 'announcement 2' ); |
707 | expect(second.textDirection, TextDirection.rtl); |
708 | expect(second.assertiveness, Assertiveness.assertive); |
709 | |
710 | final CapturedAccessibilityAnnouncement third = list[2]; |
711 | expect(third.message, 'announcement 3' ); |
712 | expect(third.textDirection, TextDirection.rtl); |
713 | expect(third.assertiveness, Assertiveness.polite); |
714 | |
715 | final List<CapturedAccessibilityAnnouncement> emptyList = tester.takeAnnouncements(); |
716 | expect(emptyList, <CapturedAccessibilityAnnouncement>[]); |
717 | }); |
718 | |
719 | test('New test API is not breaking existing tests' , () async { |
720 | final List<Map<dynamic, dynamic>> log = <Map<dynamic, dynamic>>[]; |
721 | |
722 | Future<dynamic> handleMessage(dynamic mockMessage) async { |
723 | final Map<dynamic, dynamic> message = mockMessage as Map<dynamic, dynamic>; |
724 | log.add(message); |
725 | } |
726 | |
727 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger |
728 | .setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, handleMessage); |
729 | |
730 | await SemanticsService.announce( |
731 | 'announcement 1' , |
732 | TextDirection.rtl, |
733 | assertiveness: Assertiveness.assertive, |
734 | ); |
735 | expect( |
736 | log, |
737 | equals(<Map<String, dynamic>>[ |
738 | <String, dynamic>{ |
739 | 'type' : 'announce' , |
740 | 'data' : <String, dynamic>{ |
741 | 'message' : 'announcement 1' , |
742 | 'textDirection' : 0, |
743 | 'assertiveness' : 1, |
744 | }, |
745 | }, |
746 | ]), |
747 | ); |
748 | |
749 | // Remove the handler |
750 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger |
751 | .setMockDecodedMessageHandler<dynamic>(SystemChannels.accessibility, null); |
752 | }); |
753 | |
754 | tearDown(() { |
755 | // Make sure that the handler is removed in [TestWidgetsFlutterBinding.postTest] |
756 | expect( |
757 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.checkMockMessageHandler( |
758 | SystemChannels.accessibility.name, |
759 | null, |
760 | ), |
761 | isTrue, |
762 | ); |
763 | }); |
764 | }); |
765 | |
766 | testWidgets('wrapWithView: false does not include View' , (WidgetTester tester) async { |
767 | FlutterView? flutterView; |
768 | View? view; |
769 | int builderCount = 0; |
770 | await tester.pumpWidget( |
771 | wrapWithView: false, |
772 | Builder( |
773 | builder: (BuildContext context) { |
774 | builderCount++; |
775 | flutterView = View.maybeOf(context); |
776 | view = context.findAncestorWidgetOfExactType<View>(); |
777 | return const ViewCollection(views: <Widget>[]); |
778 | }, |
779 | ), |
780 | ); |
781 | |
782 | expect(builderCount, 1); |
783 | expect(view, isNull); |
784 | expect(flutterView, isNull); |
785 | expect(find.byType(View), findsNothing); |
786 | }); |
787 | |
788 | testWidgets('passing a view to pumpWidget with wrapWithView: true throws' , ( |
789 | WidgetTester tester, |
790 | ) async { |
791 | await tester.pumpWidget(View(view: FakeView(tester.view), child: const SizedBox.shrink())); |
792 | expect( |
793 | tester.takeException(), |
794 | isFlutterError.having( |
795 | (FlutterError e) => e.message, |
796 | 'message' , |
797 | contains('consider setting the "wrapWithView" parameter of that method to false' ), |
798 | ), |
799 | ); |
800 | }); |
801 | |
802 | testWidgets('can pass a View to pumpWidget when wrapWithView: false' , ( |
803 | WidgetTester tester, |
804 | ) async { |
805 | await tester.pumpWidget( |
806 | wrapWithView: false, |
807 | View(view: tester.view, child: const SizedBox.shrink()), |
808 | ); |
809 | expect(find.byType(View), findsOne); |
810 | }); |
811 | } |
812 | |
813 | class FakeMatcher extends AsyncMatcher { |
814 | FakeMatcher(this.completer); |
815 | |
816 | final Completer<void> completer; |
817 | |
818 | @override |
819 | Future<String?> matchAsync(dynamic object) { |
820 | return completer.future.then<String?>((void value) { |
821 | return object?.toString(); |
822 | }); |
823 | } |
824 | |
825 | @override |
826 | Description describe(Description description) => description.add('--fake--' ); |
827 | } |
828 | |
829 | class _AlwaysAnimating extends StatefulWidget { |
830 | const _AlwaysAnimating({required this.onPaint}); |
831 | |
832 | final VoidCallback onPaint; |
833 | |
834 | @override |
835 | State<StatefulWidget> createState() => _AlwaysAnimatingState(); |
836 | } |
837 | |
838 | class _AlwaysAnimatingState extends State<_AlwaysAnimating> with SingleTickerProviderStateMixin { |
839 | late AnimationController _controller; |
840 | |
841 | @override |
842 | void initState() { |
843 | super.initState(); |
844 | _controller = AnimationController(duration: const Duration(milliseconds: 100), vsync: this); |
845 | _controller.repeat(); |
846 | } |
847 | |
848 | @override |
849 | void dispose() { |
850 | _controller.dispose(); |
851 | super.dispose(); |
852 | } |
853 | |
854 | @override |
855 | Widget build(BuildContext context) { |
856 | return AnimatedBuilder( |
857 | animation: _controller.view, |
858 | builder: (BuildContext context, Widget? child) { |
859 | return CustomPaint(painter: _AlwaysRepaint(widget.onPaint)); |
860 | }, |
861 | ); |
862 | } |
863 | } |
864 | |
865 | class _AlwaysRepaint extends CustomPainter { |
866 | _AlwaysRepaint(this.onPaint); |
867 | |
868 | final VoidCallback onPaint; |
869 | |
870 | @override |
871 | bool shouldRepaint(CustomPainter oldDelegate) => true; |
872 | |
873 | @override |
874 | void paint(Canvas canvas, Size size) { |
875 | onPaint(); |
876 | } |
877 | } |
878 | |