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