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:io'; |
6 | |
7 | import 'package:flutter/material.dart'; |
8 | import 'package:flutter/rendering.dart'; |
9 | import 'package:flutter_test/flutter_test.dart'; |
10 | |
11 | const List<Widget> fooBarTexts = <Text>[ |
12 | Text('foo', textDirection: TextDirection.ltr), |
13 | Text('bar', textDirection: TextDirection.ltr), |
14 | ]; |
15 | |
16 | void main() { |
17 | group('image', () { |
18 | testWidgets('finds Image widgets', (WidgetTester tester) async { |
19 | await tester.pumpWidget(_boilerplate(Image(image: FileImage(File('test'))))); |
20 | expect(find.image(FileImage(File('test'))), findsOneWidget); |
21 | }); |
22 | |
23 | testWidgets('finds Button widgets with Image', (WidgetTester tester) async { |
24 | await tester.pumpWidget( |
25 | _boilerplate(ElevatedButton(onPressed: null, child: Image(image: FileImage(File('test'))))), |
26 | ); |
27 | expect(find.widgetWithImage(ElevatedButton, FileImage(File('test'))), findsOneWidget); |
28 | }); |
29 | }); |
30 | |
31 | group('text', () { |
32 | testWidgets('finds Text widgets', (WidgetTester tester) async { |
33 | await tester.pumpWidget(_boilerplate(const Text('test'))); |
34 | expect(find.text('test'), findsOneWidget); |
35 | }); |
36 | |
37 | testWidgets('finds Text.rich widgets', (WidgetTester tester) async { |
38 | await tester.pumpWidget( |
39 | _boilerplate( |
40 | const Text.rich( |
41 | TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'e'), TextSpan(text: 'st')]), |
42 | ), |
43 | ), |
44 | ); |
45 | |
46 | expect(find.text('test'), findsOneWidget); |
47 | }); |
48 | |
49 | group('findRichText', () { |
50 | testWidgets('finds RichText widgets when enabled', (WidgetTester tester) async { |
51 | await tester.pumpWidget( |
52 | _boilerplate( |
53 | RichText(text: const TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'est')])), |
54 | ), |
55 | ); |
56 | |
57 | expect(find.text('test', findRichText: true), findsOneWidget); |
58 | }); |
59 | |
60 | testWidgets('finds Text widgets once when enabled', (WidgetTester tester) async { |
61 | await tester.pumpWidget(_boilerplate(const Text('test2'))); |
62 | |
63 | expect(find.text('test2', findRichText: true), findsOneWidget); |
64 | }); |
65 | |
66 | testWidgets('does not find RichText widgets when disabled', (WidgetTester tester) async { |
67 | await tester.pumpWidget( |
68 | _boilerplate( |
69 | RichText(text: const TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'est')])), |
70 | ), |
71 | ); |
72 | |
73 | expect(find.text('test'), findsNothing); |
74 | }); |
75 | |
76 | testWidgets('does not find Text and RichText separated by semantics widgets twice', ( |
77 | WidgetTester tester, |
78 | ) async { |
79 | // If rich: true found both Text and RichText, this would find two widgets. |
80 | await tester.pumpWidget(_boilerplate(const Text('test', semanticsLabel: 'foo'))); |
81 | |
82 | expect(find.text('test'), findsOneWidget); |
83 | }); |
84 | |
85 | testWidgets('finds Text.rich widgets when enabled', (WidgetTester tester) async { |
86 | await tester.pumpWidget( |
87 | _boilerplate( |
88 | const Text.rich( |
89 | TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'est'), TextSpan(text: '3')]), |
90 | ), |
91 | ), |
92 | ); |
93 | |
94 | expect(find.text('test3', findRichText: true), findsOneWidget); |
95 | }); |
96 | |
97 | testWidgets('finds Text.rich widgets when disabled', (WidgetTester tester) async { |
98 | await tester.pumpWidget( |
99 | _boilerplate( |
100 | const Text.rich( |
101 | TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'est'), TextSpan(text: '3')]), |
102 | ), |
103 | ), |
104 | ); |
105 | |
106 | expect(find.text('test3'), findsOneWidget); |
107 | }); |
108 | }); |
109 | }); |
110 | |
111 | group('textContaining', () { |
112 | testWidgets('finds Text widgets', (WidgetTester tester) async { |
113 | await tester.pumpWidget(_boilerplate(const Text('this is a test'))); |
114 | expect(find.textContaining(RegExp(r'test')), findsOneWidget); |
115 | expect(find.textContaining('test'), findsOneWidget); |
116 | expect(find.textContaining('a'), findsOneWidget); |
117 | expect(find.textContaining('s'), findsOneWidget); |
118 | }); |
119 | |
120 | testWidgets('finds Text.rich widgets', (WidgetTester tester) async { |
121 | await tester.pumpWidget( |
122 | _boilerplate( |
123 | const Text.rich( |
124 | TextSpan( |
125 | text: 'this', |
126 | children: <TextSpan>[ |
127 | TextSpan(text: 'is'), |
128 | TextSpan(text: 'a'), |
129 | TextSpan(text: 'test'), |
130 | ], |
131 | ), |
132 | ), |
133 | ), |
134 | ); |
135 | |
136 | expect(find.textContaining(RegExp(r'isatest')), findsOneWidget); |
137 | expect(find.textContaining('isatest'), findsOneWidget); |
138 | }); |
139 | |
140 | testWidgets('finds EditableText widgets', (WidgetTester tester) async { |
141 | await tester.pumpWidget( |
142 | MaterialApp( |
143 | home: Scaffold( |
144 | body: _boilerplate( |
145 | TextField(controller: TextEditingController()..text = 'this is test'), |
146 | ), |
147 | ), |
148 | ), |
149 | ); |
150 | |
151 | expect(find.textContaining(RegExp(r'test')), findsOneWidget); |
152 | expect(find.textContaining('test'), findsOneWidget); |
153 | }); |
154 | |
155 | group('findRichText', () { |
156 | testWidgets('finds RichText widgets when enabled', (WidgetTester tester) async { |
157 | await tester.pumpWidget( |
158 | _boilerplate( |
159 | RichText(text: const TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'est')])), |
160 | ), |
161 | ); |
162 | |
163 | expect(find.textContaining('te', findRichText: true), findsOneWidget); |
164 | }); |
165 | |
166 | testWidgets('finds Text widgets once when enabled', (WidgetTester tester) async { |
167 | await tester.pumpWidget(_boilerplate(const Text('test2'))); |
168 | |
169 | expect(find.textContaining('tes', findRichText: true), findsOneWidget); |
170 | }); |
171 | |
172 | testWidgets('does not find RichText widgets when disabled', (WidgetTester tester) async { |
173 | await tester.pumpWidget( |
174 | _boilerplate( |
175 | RichText(text: const TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'est')])), |
176 | ), |
177 | ); |
178 | |
179 | expect(find.textContaining('te'), findsNothing); |
180 | }); |
181 | |
182 | testWidgets('does not find Text and RichText separated by semantics widgets twice', ( |
183 | WidgetTester tester, |
184 | ) async { |
185 | // If rich: true found both Text and RichText, this would find two widgets. |
186 | await tester.pumpWidget(_boilerplate(const Text('test', semanticsLabel: 'foo'))); |
187 | |
188 | expect(find.textContaining('tes'), findsOneWidget); |
189 | }); |
190 | |
191 | testWidgets('finds Text.rich widgets when enabled', (WidgetTester tester) async { |
192 | await tester.pumpWidget( |
193 | _boilerplate( |
194 | const Text.rich( |
195 | TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'est'), TextSpan(text: '3')]), |
196 | ), |
197 | ), |
198 | ); |
199 | |
200 | expect(find.textContaining('t3', findRichText: true), findsOneWidget); |
201 | }); |
202 | |
203 | testWidgets('finds Text.rich widgets when disabled', (WidgetTester tester) async { |
204 | await tester.pumpWidget( |
205 | _boilerplate( |
206 | const Text.rich( |
207 | TextSpan(text: 't', children: <TextSpan>[TextSpan(text: 'est'), TextSpan(text: '3')]), |
208 | ), |
209 | ), |
210 | ); |
211 | |
212 | expect(find.textContaining('t3'), findsOneWidget); |
213 | }); |
214 | }); |
215 | }); |
216 | |
217 | group('semantics', () { |
218 | testWidgets('Throws StateError if semantics are not enabled', (WidgetTester tester) async { |
219 | expect(() => find.bySemanticsLabel('Add'), throwsStateError); |
220 | }, semanticsEnabled: false); |
221 | |
222 | testWidgets('finds Semantically labeled widgets', (WidgetTester tester) async { |
223 | final SemanticsHandle semanticsHandle = tester.ensureSemantics(); |
224 | await tester.pumpWidget( |
225 | _boilerplate( |
226 | Semantics( |
227 | label: 'Add', |
228 | button: true, |
229 | child: const TextButton(onPressed: null, child: Text('+')), |
230 | ), |
231 | ), |
232 | ); |
233 | expect(find.bySemanticsLabel('Add'), findsOneWidget); |
234 | semanticsHandle.dispose(); |
235 | }); |
236 | |
237 | testWidgets('finds Semantically labeled widgets by RegExp', (WidgetTester tester) async { |
238 | final SemanticsHandle semanticsHandle = tester.ensureSemantics(); |
239 | await tester.pumpWidget( |
240 | _boilerplate( |
241 | Semantics( |
242 | container: true, |
243 | child: const Row(children: <Widget>[Text('Hello'), Text( 'World')]), |
244 | ), |
245 | ), |
246 | ); |
247 | expect(find.bySemanticsLabel('Hello'), findsNothing); |
248 | expect(find.bySemanticsLabel(RegExp(r'^Hello')), findsOneWidget); |
249 | semanticsHandle.dispose(); |
250 | }); |
251 | |
252 | testWidgets('finds Semantically labeled widgets without explicit Semantics', ( |
253 | WidgetTester tester, |
254 | ) async { |
255 | final SemanticsHandle semanticsHandle = tester.ensureSemantics(); |
256 | await tester.pumpWidget(_boilerplate(const SimpleCustomSemanticsWidget('Foo'))); |
257 | expect(find.bySemanticsLabel('Foo'), findsOneWidget); |
258 | semanticsHandle.dispose(); |
259 | }); |
260 | |
261 | testWidgets( |
262 | 'Throws StateError if semantics are not enabled (bySemanticsIdentifier)', |
263 | (WidgetTester tester) async { |
264 | expect( |
265 | () => find.bySemanticsIdentifier('Add'), |
266 | throwsA( |
267 | isA<StateError>().having( |
268 | (StateError e) => e.message, |
269 | 'message', |
270 | contains('Semantics are not enabled'), |
271 | ), |
272 | ), |
273 | ); |
274 | }, |
275 | semanticsEnabled: false, |
276 | ); |
277 | |
278 | testWidgets('finds Semantically labeled widgets by identifier', (WidgetTester tester) async { |
279 | final SemanticsHandle semanticsHandle = tester.ensureSemantics(); |
280 | await tester.pumpWidget( |
281 | _boilerplate( |
282 | Semantics( |
283 | identifier: 'Add', |
284 | button: true, |
285 | child: const TextButton(onPressed: null, child: Text('+')), |
286 | ), |
287 | ), |
288 | ); |
289 | expect(find.bySemanticsIdentifier('Add'), findsOneWidget); |
290 | semanticsHandle.dispose(); |
291 | }); |
292 | |
293 | testWidgets('finds Semantically labeled widgets by identifier RegExp', ( |
294 | WidgetTester tester, |
295 | ) async { |
296 | final SemanticsHandle semanticsHandle = tester.ensureSemantics(); |
297 | // list of elements with a prefixed identifier |
298 | await tester.pumpWidget( |
299 | _boilerplate( |
300 | Row( |
301 | children: <Widget>[ |
302 | Semantics(identifier: 'item-1', child: const Text( 'Item 1')), |
303 | Semantics(identifier: 'item-2', child: const Text( 'Item 2')), |
304 | ], |
305 | ), |
306 | ), |
307 | ); |
308 | expect(find.bySemanticsIdentifier('item'), findsNothing); |
309 | expect(find.bySemanticsIdentifier(RegExp(r'^item-')), findsNWidgets(2)); |
310 | semanticsHandle.dispose(); |
311 | }); |
312 | }); |
313 | |
314 | group('byTooltip', () { |
315 | testWidgets('finds widgets by tooltip', (WidgetTester tester) async { |
316 | await tester.pumpWidget( |
317 | _boilerplate(const Tooltip(message: 'Tooltip Message', child: Text( '+'))), |
318 | ); |
319 | expect(find.byTooltip('Tooltip Message'), findsOneWidget); |
320 | }); |
321 | |
322 | testWidgets('finds widgets with tooltip by RegExp', (WidgetTester tester) async { |
323 | await tester.pumpWidget( |
324 | _boilerplate(const Tooltip(message: 'Tooltip Message', child: Text( '+'))), |
325 | ); |
326 | expect(find.byTooltip('Tooltip'), findsNothing); |
327 | expect(find.byTooltip(RegExp(r'^Tooltip')), findsOneWidget); |
328 | }); |
329 | |
330 | testWidgets('finds widgets by rich text tooltip', (WidgetTester tester) async { |
331 | await tester.pumpWidget( |
332 | _boilerplate( |
333 | const Tooltip( |
334 | richMessage: TextSpan( |
335 | children: <InlineSpan>[TextSpan(text: 'Tooltip '), TextSpan(text: 'Message')], |
336 | ), |
337 | child: Text('+'), |
338 | ), |
339 | ), |
340 | ); |
341 | expect(find.byTooltip('Tooltip Message'), findsOneWidget); |
342 | }); |
343 | |
344 | testWidgets('finds widgets with rich text tooltip by RegExp', (WidgetTester tester) async { |
345 | await tester.pumpWidget( |
346 | _boilerplate( |
347 | const Tooltip( |
348 | richMessage: TextSpan( |
349 | children: <InlineSpan>[TextSpan(text: 'Tooltip '), TextSpan(text: 'Message')], |
350 | ), |
351 | child: Text('+'), |
352 | ), |
353 | ), |
354 | ); |
355 | expect(find.byTooltip('Tooltip M'), findsNothing); |
356 | expect(find.byTooltip(RegExp(r'^Tooltip M')), findsOneWidget); |
357 | }); |
358 | |
359 | testWidgets('finds empty string with tooltip', (WidgetTester tester) async { |
360 | await tester.pumpWidget(_boilerplate(const Tooltip(message: '', child: Text( '+')))); |
361 | expect(find.byTooltip(''), findsOneWidget); |
362 | |
363 | await tester.pumpWidget( |
364 | _boilerplate( |
365 | const Tooltip( |
366 | richMessage: TextSpan(children: <InlineSpan>[TextSpan(text: '')]), |
367 | child: Text('+'), |
368 | ), |
369 | ), |
370 | ); |
371 | expect(find.byTooltip(''), findsOneWidget); |
372 | |
373 | await tester.pumpWidget(_boilerplate(const Tooltip(message: '', child: Text( '+')))); |
374 | expect(find.byTooltip(RegExp(r'^$')), findsOneWidget); |
375 | |
376 | await tester.pumpWidget( |
377 | _boilerplate( |
378 | const Tooltip( |
379 | richMessage: TextSpan(children: <InlineSpan>[TextSpan(text: '')]), |
380 | child: Text('+'), |
381 | ), |
382 | ), |
383 | ); |
384 | expect(find.byTooltip(RegExp(r'^$')), findsOneWidget); |
385 | }); |
386 | }); |
387 | |
388 | group('hitTestable', () { |
389 | testWidgets('excludes non-hit-testable widgets', (WidgetTester tester) async { |
390 | await tester.pumpWidget( |
391 | _boilerplate( |
392 | IndexedStack( |
393 | sizing: StackFit.expand, |
394 | children: <Widget>[ |
395 | GestureDetector( |
396 | key: const ValueKey<int>(0), |
397 | behavior: HitTestBehavior.opaque, |
398 | onTap: () {}, |
399 | child: const SizedBox.expand(), |
400 | ), |
401 | GestureDetector( |
402 | key: const ValueKey<int>(1), |
403 | behavior: HitTestBehavior.opaque, |
404 | onTap: () {}, |
405 | child: const SizedBox.expand(), |
406 | ), |
407 | ], |
408 | ), |
409 | ), |
410 | ); |
411 | expect(find.byType(GestureDetector), findsOneWidget); |
412 | expect(find.byType(GestureDetector, skipOffstage: false), findsNWidgets(2)); |
413 | final Finder hitTestable = find.byType(GestureDetector, skipOffstage: false).hitTestable(); |
414 | expect(hitTestable, findsOneWidget); |
415 | expect(tester.widget(hitTestable).key, const ValueKey<int>(0)); |
416 | }); |
417 | }); |
418 | |
419 | group('text range finders', () { |
420 | testWidgets('basic text span test', (WidgetTester tester) async { |
421 | await tester.pumpWidget( |
422 | _boilerplate( |
423 | const IndexedStack( |
424 | sizing: StackFit.expand, |
425 | children: <Widget>[ |
426 | Text.rich( |
427 | TextSpan( |
428 | text: 'sub', |
429 | children: <InlineSpan>[ |
430 | TextSpan(text: 'stringsub'), |
431 | TextSpan(text: 'stringsub'), |
432 | TextSpan(text: 'stringsub'), |
433 | ], |
434 | ), |
435 | ), |
436 | Text('substringsub'), |
437 | ], |
438 | ), |
439 | ), |
440 | ); |
441 | |
442 | expect( |
443 | find.textRange.ofSubstring('substringsub'), |
444 | findsExactly(2), |
445 | ); // Pattern skips overlapping matches. |
446 | expect( |
447 | find.textRange.ofSubstring('substringsub').first.evaluate().single.textRange, |
448 | const TextRange(start: 0, end: 12), |
449 | ); |
450 | expect( |
451 | find.textRange.ofSubstring('substringsub').last.evaluate().single.textRange, |
452 | const TextRange(start: 18, end: 30), |
453 | ); |
454 | |
455 | expect( |
456 | find.textRange.ofSubstring('substringsub').first.evaluate().single.renderObject, |
457 | find.textRange.ofSubstring('substringsub').last.evaluate().single.renderObject, |
458 | ); |
459 | |
460 | expect(find.textRange.ofSubstring('substringsub', skipOffstage: false), findsExactly(3)); |
461 | }); |
462 | |
463 | testWidgets('basic text span test', (WidgetTester tester) async { |
464 | await tester.pumpWidget( |
465 | _boilerplate( |
466 | const IndexedStack( |
467 | sizing: StackFit.expand, |
468 | children: <Widget>[ |
469 | Text.rich( |
470 | TextSpan( |
471 | text: 'sub', |
472 | children: <InlineSpan>[ |
473 | TextSpan(text: 'stringsub'), |
474 | TextSpan(text: 'stringsub'), |
475 | TextSpan(text: 'stringsub'), |
476 | ], |
477 | ), |
478 | ), |
479 | Text('substringsub'), |
480 | ], |
481 | ), |
482 | ), |
483 | ); |
484 | |
485 | expect( |
486 | find.textRange.ofSubstring('substringsub'), |
487 | findsExactly(2), |
488 | ); // Pattern skips overlapping matches. |
489 | expect( |
490 | find.textRange.ofSubstring('substringsub').first.evaluate().single.textRange, |
491 | const TextRange(start: 0, end: 12), |
492 | ); |
493 | expect( |
494 | find.textRange.ofSubstring('substringsub').last.evaluate().single.textRange, |
495 | const TextRange(start: 18, end: 30), |
496 | ); |
497 | |
498 | expect( |
499 | find.textRange.ofSubstring('substringsub').first.evaluate().single.renderObject, |
500 | find.textRange.ofSubstring('substringsub').last.evaluate().single.renderObject, |
501 | ); |
502 | |
503 | expect(find.textRange.ofSubstring('substringsub', skipOffstage: false), findsExactly(3)); |
504 | }); |
505 | |
506 | testWidgets('descendentOf', (WidgetTester tester) async { |
507 | await tester.pumpWidget( |
508 | _boilerplate( |
509 | const Column( |
510 | children: <Widget>[ |
511 | Text.rich(TextSpan(text: 'text')), |
512 | Text.rich(TextSpan(text: 'text')), |
513 | ], |
514 | ), |
515 | ), |
516 | ); |
517 | |
518 | expect(find.textRange.ofSubstring('text'), findsExactly(2)); |
519 | expect(find.textRange.ofSubstring('text', descendentOf: find.text( 'text').first), findsOne); |
520 | }); |
521 | |
522 | testWidgets('finds only static text for now', (WidgetTester tester) async { |
523 | await tester.pumpWidget( |
524 | _boilerplate( |
525 | EditableText( |
526 | controller: TextEditingController(text: 'text'), |
527 | focusNode: FocusNode(), |
528 | style: const TextStyle(), |
529 | cursorColor: const Color(0x00000000), |
530 | backgroundCursorColor: const Color(0x00000000), |
531 | ), |
532 | ), |
533 | ); |
534 | |
535 | expect(find.textRange.ofSubstring('text'), findsNothing); |
536 | }); |
537 | }); |
538 | |
539 | testWidgets('ChainedFinders chain properly', (WidgetTester tester) async { |
540 | final GlobalKey key1 = GlobalKey(); |
541 | await tester.pumpWidget( |
542 | _boilerplate( |
543 | Column(children: <Widget>[Container(key: key1, child: const Text('1')), const Text( '2')]), |
544 | ), |
545 | ); |
546 | |
547 | // Get the text back. By correctly chaining the descendant finder's |
548 | // candidates, it should find 1 instead of 2. If the _LastFinder wasn't |
549 | // correctly chained after the descendant's candidates, the last element |
550 | // with a Text widget would have been 2. |
551 | final Text text = |
552 | find |
553 | .descendant(of: find.byKey(key1), matching: find.byType(Text)) |
554 | .last |
555 | .evaluate() |
556 | .single |
557 | .widget |
558 | as Text; |
559 | |
560 | expect(text.data, '1'); |
561 | }); |
562 | |
563 | testWidgets('finds multiple subtypes', (WidgetTester tester) async { |
564 | await tester.pumpWidget( |
565 | _boilerplate( |
566 | Row( |
567 | children: <Widget>[ |
568 | const Column(children: <Widget>[Text('Hello'), Text( 'World')]), |
569 | Column(children: <Widget>[Image(image: FileImage(File('test')))]), |
570 | const Column( |
571 | children: <Widget>[ |
572 | SimpleGenericWidget<int>(child: Text('one')), |
573 | SimpleGenericWidget<double>(child: Text('pi')), |
574 | SimpleGenericWidget<String>(child: Text('two')), |
575 | ], |
576 | ), |
577 | ], |
578 | ), |
579 | ), |
580 | ); |
581 | |
582 | expect(find.bySubtype<Row>(), findsOneWidget); |
583 | expect(find.bySubtype<Column>(), findsNWidgets(3)); |
584 | // Finds both rows and columns. |
585 | expect(find.bySubtype<Flex>(), findsNWidgets(4)); |
586 | |
587 | // Finds only the requested generic subtypes. |
588 | expect(find.bySubtype<SimpleGenericWidget<int>>(), findsOneWidget); |
589 | expect(find.bySubtype<SimpleGenericWidget<num>>(), findsNWidgets(2)); |
590 | expect(find.bySubtype<SimpleGenericWidget<Object>>(), findsNWidgets(3)); |
591 | |
592 | // Finds all widgets. |
593 | final int totalWidgetCount = find.byWidgetPredicate((_) => true).evaluate().length; |
594 | expect(find.bySubtype<Widget>(), findsNWidgets(totalWidgetCount)); |
595 | }); |
596 | |
597 | group('find.byElementPredicate', () { |
598 | testWidgets('fails with a custom description in the message', (WidgetTester tester) async { |
599 | await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); |
600 | |
601 | const String customDescription = 'custom description'; |
602 | late TestFailure failure; |
603 | try { |
604 | expect( |
605 | find.byElementPredicate((_) => false, description: customDescription), |
606 | findsOneWidget, |
607 | ); |
608 | } on TestFailure catch (e) { |
609 | failure = e; |
610 | } |
611 | |
612 | expect(failure, isNotNull); |
613 | expect( |
614 | failure.message, |
615 | contains('Actual: _ElementPredicateWidgetFinder:<Found 0 widgets with$customDescription '), |
616 | ); |
617 | }); |
618 | }); |
619 | |
620 | group('find.byWidgetPredicate', () { |
621 | testWidgets('fails with a custom description in the message', (WidgetTester tester) async { |
622 | await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); |
623 | |
624 | const String customDescription = 'custom description'; |
625 | late TestFailure failure; |
626 | try { |
627 | expect( |
628 | find.byWidgetPredicate((_) => false, description: customDescription), |
629 | findsOneWidget, |
630 | ); |
631 | } on TestFailure catch (e) { |
632 | failure = e; |
633 | } |
634 | |
635 | expect(failure, isNotNull); |
636 | expect( |
637 | failure.message, |
638 | contains('Actual: _WidgetPredicateWidgetFinder:<Found 0 widgets with$customDescription '), |
639 | ); |
640 | }); |
641 | }); |
642 | |
643 | group('find.descendant', () { |
644 | testWidgets('finds one descendant', (WidgetTester tester) async { |
645 | await tester.pumpWidget( |
646 | const Row( |
647 | textDirection: TextDirection.ltr, |
648 | children: <Widget>[Column(children: fooBarTexts)], |
649 | ), |
650 | ); |
651 | |
652 | expect( |
653 | find.descendant(of: find.widgetWithText(Row, 'foo'), matching: find.text( 'bar')), |
654 | findsOneWidget, |
655 | ); |
656 | }); |
657 | |
658 | testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async { |
659 | await tester.pumpWidget( |
660 | const Row( |
661 | textDirection: TextDirection.ltr, |
662 | children: <Widget>[Column(children: fooBarTexts), Column(children: fooBarTexts)], |
663 | ), |
664 | ); |
665 | |
666 | expect( |
667 | find.descendant(of: find.widgetWithText(Column, 'foo'), matching: find.text( 'bar')), |
668 | findsNWidgets(2), |
669 | ); |
670 | }); |
671 | |
672 | testWidgets('fails with a descriptive message', (WidgetTester tester) async { |
673 | await tester.pumpWidget( |
674 | const Row( |
675 | textDirection: TextDirection.ltr, |
676 | children: <Widget>[ |
677 | Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]), |
678 | Text('bar', textDirection: TextDirection.ltr), |
679 | ], |
680 | ), |
681 | ); |
682 | |
683 | late TestFailure failure; |
684 | try { |
685 | expect( |
686 | find.descendant(of: find.widgetWithText(Column, 'foo'), matching: find.text( 'bar')), |
687 | findsOneWidget, |
688 | ); |
689 | } on TestFailure catch (e) { |
690 | failure = e; |
691 | } |
692 | |
693 | expect(failure, isNotNull); |
694 | expect( |
695 | failure.message, |
696 | contains( |
697 | 'Actual: _DescendantWidgetFinder:<Found 0 widgets with text "bar" descending from widgets with type "Column" that are ancestors of widgets with text "foo"', |
698 | ), |
699 | ); |
700 | }); |
701 | }); |
702 | |
703 | group('find.ancestor', () { |
704 | testWidgets('finds one ancestor', (WidgetTester tester) async { |
705 | await tester.pumpWidget( |
706 | const Row( |
707 | textDirection: TextDirection.ltr, |
708 | children: <Widget>[Column(children: fooBarTexts)], |
709 | ), |
710 | ); |
711 | |
712 | expect( |
713 | find.ancestor(of: find.text('bar'), matching: find.widgetWithText(Row, 'foo')), |
714 | findsOneWidget, |
715 | ); |
716 | }); |
717 | |
718 | testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async { |
719 | await tester.pumpWidget( |
720 | const Directionality( |
721 | textDirection: TextDirection.ltr, |
722 | child: Row(children: <Widget>[Row(children: fooBarTexts)]), |
723 | ), |
724 | ); |
725 | |
726 | expect(find.ancestor(of: find.text('bar'), matching: find.byType(Row)), findsNWidgets(2)); |
727 | }); |
728 | |
729 | testWidgets('fails with a descriptive message', (WidgetTester tester) async { |
730 | await tester.pumpWidget( |
731 | const Row( |
732 | textDirection: TextDirection.ltr, |
733 | children: <Widget>[ |
734 | Column(children: <Text>[Text('foo', textDirection: TextDirection.ltr)]), |
735 | Text('bar', textDirection: TextDirection.ltr), |
736 | ], |
737 | ), |
738 | ); |
739 | |
740 | late TestFailure failure; |
741 | try { |
742 | expect( |
743 | find.ancestor(of: find.text('bar'), matching: find.widgetWithText(Column, 'foo')), |
744 | findsOneWidget, |
745 | ); |
746 | } on TestFailure catch (e) { |
747 | failure = e; |
748 | } |
749 | |
750 | expect(failure, isNotNull); |
751 | expect( |
752 | failure.message, |
753 | contains( |
754 | 'Actual: _AncestorWidgetFinder:<Found 0 widgets with type "Column" that are ancestors of widgets with text "foo" that are ancestors of widgets with text "bar"', |
755 | ), |
756 | ); |
757 | }); |
758 | |
759 | testWidgets('Root not matched by default', (WidgetTester tester) async { |
760 | await tester.pumpWidget( |
761 | const Row( |
762 | textDirection: TextDirection.ltr, |
763 | children: <Widget>[Column(children: fooBarTexts)], |
764 | ), |
765 | ); |
766 | |
767 | expect( |
768 | find.ancestor(of: find.byType(Column), matching: find.widgetWithText(Column, 'foo')), |
769 | findsNothing, |
770 | ); |
771 | }); |
772 | |
773 | testWidgets('Match the root', (WidgetTester tester) async { |
774 | await tester.pumpWidget( |
775 | const Row( |
776 | textDirection: TextDirection.ltr, |
777 | children: <Widget>[Column(children: fooBarTexts)], |
778 | ), |
779 | ); |
780 | |
781 | expect( |
782 | find.descendant( |
783 | of: find.byType(Column), |
784 | matching: find.widgetWithText(Column, 'foo'), |
785 | matchRoot: true, |
786 | ), |
787 | findsOneWidget, |
788 | ); |
789 | }); |
790 | |
791 | testWidgets('is fast in deep tree', (WidgetTester tester) async { |
792 | await tester.pumpWidget( |
793 | Directionality( |
794 | textDirection: TextDirection.ltr, |
795 | child: _deepWidgetTree( |
796 | depth: 500, |
797 | child: Row( |
798 | children: <Widget>[ |
799 | _deepWidgetTree(depth: 500, child: const Column(children: fooBarTexts)), |
800 | ], |
801 | ), |
802 | ), |
803 | ), |
804 | ); |
805 | |
806 | expect(find.ancestor(of: find.text('bar'), matching: find.byType(Row)), findsOneWidget); |
807 | }); |
808 | }); |
809 | |
810 | group('CommonSemanticsFinders', () { |
811 | final Widget semanticsTree = _boilerplate( |
812 | Semantics( |
813 | container: true, |
814 | header: true, |
815 | readOnly: true, |
816 | onCopy: () {}, |
817 | onLongPress: () {}, |
818 | value: 'value1', |
819 | hint: 'hint1', |
820 | label: 'label1', |
821 | child: Semantics( |
822 | container: true, |
823 | textField: true, |
824 | onSetText: (_) {}, |
825 | onPaste: () {}, |
826 | onLongPress: () {}, |
827 | value: 'value2', |
828 | hint: 'hint2', |
829 | label: 'label2', |
830 | child: Semantics( |
831 | container: true, |
832 | readOnly: true, |
833 | onCopy: () {}, |
834 | value: 'value3', |
835 | hint: 'hint3', |
836 | label: 'label3', |
837 | child: Semantics( |
838 | container: true, |
839 | readOnly: true, |
840 | onLongPress: () {}, |
841 | value: 'value4', |
842 | hint: 'hint4', |
843 | label: 'label4', |
844 | child: Semantics( |
845 | container: true, |
846 | onLongPress: () {}, |
847 | onCopy: () {}, |
848 | value: 'value5', |
849 | hint: 'hint5', |
850 | label: 'label5', |
851 | ), |
852 | ), |
853 | ), |
854 | ), |
855 | ), |
856 | ); |
857 | |
858 | group('ancestor', () { |
859 | testWidgets('finds matching ancestor nodes', (WidgetTester tester) async { |
860 | await tester.pumpWidget(semanticsTree); |
861 | |
862 | final FinderBase<SemanticsNode> finder = find.semantics.ancestor( |
863 | of: find.semantics.byLabel('label4'), |
864 | matching: find.semantics.byAction(SemanticsAction.copy), |
865 | ); |
866 | |
867 | expect(finder, findsExactly(2)); |
868 | }); |
869 | |
870 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
871 | late TestFailure failure; |
872 | await tester.pumpWidget(semanticsTree); |
873 | |
874 | final FinderBase<SemanticsNode> finder = find.semantics.ancestor( |
875 | of: find.semantics.byLabel('label4'), |
876 | matching: find.semantics.byAction(SemanticsAction.copy), |
877 | ); |
878 | |
879 | try { |
880 | expect(finder, findsExactly(3)); |
881 | } on TestFailure catch (e) { |
882 | failure = e; |
883 | } |
884 | |
885 | expect( |
886 | failure.message, |
887 | contains( |
888 | 'Actual: _AncestorSemanticsFinder:<Found 2 SemanticsNodes with action "SemanticsAction.copy" that are ancestors of SemanticsNodes with label "label4"', |
889 | ), |
890 | ); |
891 | }); |
892 | }); |
893 | |
894 | group('descendant', () { |
895 | testWidgets('finds matching descendant nodes', (WidgetTester tester) async { |
896 | await tester.pumpWidget(semanticsTree); |
897 | |
898 | final FinderBase<SemanticsNode> finder = find.semantics.descendant( |
899 | of: find.semantics.byLabel('label4'), |
900 | matching: find.semantics.byAction(SemanticsAction.copy), |
901 | ); |
902 | |
903 | expect(finder, findsOne); |
904 | }); |
905 | |
906 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
907 | late TestFailure failure; |
908 | await tester.pumpWidget(semanticsTree); |
909 | |
910 | final FinderBase<SemanticsNode> finder = find.semantics.descendant( |
911 | of: find.semantics.byLabel('label4'), |
912 | matching: find.semantics.byAction(SemanticsAction.copy), |
913 | ); |
914 | |
915 | try { |
916 | expect(finder, findsNothing); |
917 | } on TestFailure catch (e) { |
918 | failure = e; |
919 | } |
920 | |
921 | expect( |
922 | failure.message, |
923 | contains( |
924 | 'Actual: _DescendantSemanticsFinder:<Found 1 SemanticsNode with action "SemanticsAction.copy" descending from SemanticsNode with label "label4"', |
925 | ), |
926 | ); |
927 | }); |
928 | }); |
929 | |
930 | group('byPredicate', () { |
931 | testWidgets('finds nodes matching given predicate', (WidgetTester tester) async { |
932 | final RegExp replaceRegExp = RegExp(r'^[^\d]+'); |
933 | await tester.pumpWidget(semanticsTree); |
934 | |
935 | final SemanticsFinder finder = find.semantics.byPredicate((SemanticsNode node) { |
936 | final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; |
937 | return labelNum > 1; |
938 | }); |
939 | |
940 | expect(finder, findsExactly(4)); |
941 | }); |
942 | |
943 | testWidgets('fails with default message', (WidgetTester tester) async { |
944 | late TestFailure failure; |
945 | final RegExp replaceRegExp = RegExp(r'^[^\d]+'); |
946 | await tester.pumpWidget(semanticsTree); |
947 | |
948 | final SemanticsFinder finder = find.semantics.byPredicate((SemanticsNode node) { |
949 | final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; |
950 | return labelNum > 1; |
951 | }); |
952 | try { |
953 | expect(finder, findsExactly(5)); |
954 | } on TestFailure catch (e) { |
955 | failure = e; |
956 | } |
957 | |
958 | expect( |
959 | failure.message, |
960 | contains('Actual: _PredicateSemanticsFinder:<Found 4 matching semantics predicate'), |
961 | ); |
962 | }); |
963 | |
964 | testWidgets('fails with given message', (WidgetTester tester) async { |
965 | late TestFailure failure; |
966 | const String expected = 'custom error message'; |
967 | final RegExp replaceRegExp = RegExp(r'^[^\d]+'); |
968 | await tester.pumpWidget(semanticsTree); |
969 | |
970 | final SemanticsFinder finder = find.semantics.byPredicate((SemanticsNode node) { |
971 | final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; |
972 | return labelNum > 1; |
973 | }, describeMatch: (_) => expected); |
974 | try { |
975 | expect(finder, findsExactly(5)); |
976 | } on TestFailure catch (e) { |
977 | failure = e; |
978 | } |
979 | |
980 | expect(failure.message, contains(expected)); |
981 | }); |
982 | }); |
983 | |
984 | group('byLabel', () { |
985 | testWidgets('finds nodes with matching label using String', (WidgetTester tester) async { |
986 | await tester.pumpWidget(semanticsTree); |
987 | |
988 | final SemanticsFinder finder = find.semantics.byLabel('label3'); |
989 | |
990 | expect(finder, findsOne); |
991 | expect(finder.found.first.label, 'label3'); |
992 | }); |
993 | |
994 | testWidgets('finds nodes with matching label using RegEx', (WidgetTester tester) async { |
995 | await tester.pumpWidget(semanticsTree); |
996 | |
997 | final SemanticsFinder finder = find.semantics.byLabel(RegExp('^label.*')); |
998 | |
999 | expect(finder, findsExactly(5)); |
1000 | expect(finder.found.every((SemanticsNode node) => node.label.startsWith('label')), isTrue); |
1001 | }); |
1002 | |
1003 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
1004 | late TestFailure failure; |
1005 | await tester.pumpWidget(semanticsTree); |
1006 | |
1007 | final SemanticsFinder finder = find.semantics.byLabel('label3'); |
1008 | |
1009 | try { |
1010 | expect(finder, findsNothing); |
1011 | } on TestFailure catch (e) { |
1012 | failure = e; |
1013 | } |
1014 | |
1015 | expect( |
1016 | failure.message, |
1017 | contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with label "label3"'), |
1018 | ); |
1019 | }); |
1020 | }); |
1021 | |
1022 | group('byValue', () { |
1023 | testWidgets('finds nodes with matching value using String', (WidgetTester tester) async { |
1024 | await tester.pumpWidget(semanticsTree); |
1025 | |
1026 | final SemanticsFinder finder = find.semantics.byValue('value3'); |
1027 | |
1028 | expect(finder, findsOne); |
1029 | expect(finder.found.first.value, 'value3'); |
1030 | }); |
1031 | |
1032 | testWidgets('finds nodes with matching value using RegEx', (WidgetTester tester) async { |
1033 | await tester.pumpWidget(semanticsTree); |
1034 | |
1035 | final SemanticsFinder finder = find.semantics.byValue(RegExp('^value.*')); |
1036 | |
1037 | expect(finder, findsExactly(5)); |
1038 | expect(finder.found.every((SemanticsNode node) => node.value.startsWith('value')), isTrue); |
1039 | }); |
1040 | |
1041 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
1042 | late TestFailure failure; |
1043 | await tester.pumpWidget(semanticsTree); |
1044 | |
1045 | final SemanticsFinder finder = find.semantics.byValue('value3'); |
1046 | |
1047 | try { |
1048 | expect(finder, findsNothing); |
1049 | } on TestFailure catch (e) { |
1050 | failure = e; |
1051 | } |
1052 | |
1053 | expect( |
1054 | failure.message, |
1055 | contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with value "value3"'), |
1056 | ); |
1057 | }); |
1058 | }); |
1059 | |
1060 | group('byHint', () { |
1061 | testWidgets('finds nodes with matching hint using String', (WidgetTester tester) async { |
1062 | await tester.pumpWidget(semanticsTree); |
1063 | |
1064 | final SemanticsFinder finder = find.semantics.byHint('hint3'); |
1065 | |
1066 | expect(finder, findsOne); |
1067 | expect(finder.found.first.hint, 'hint3'); |
1068 | }); |
1069 | |
1070 | testWidgets('finds nodes with matching hint using RegEx', (WidgetTester tester) async { |
1071 | await tester.pumpWidget(semanticsTree); |
1072 | |
1073 | final SemanticsFinder finder = find.semantics.byHint(RegExp('^hint.*')); |
1074 | |
1075 | expect(finder, findsExactly(5)); |
1076 | expect(finder.found.every((SemanticsNode node) => node.hint.startsWith('hint')), isTrue); |
1077 | }); |
1078 | |
1079 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
1080 | late TestFailure failure; |
1081 | await tester.pumpWidget(semanticsTree); |
1082 | |
1083 | final SemanticsFinder finder = find.semantics.byHint('hint3'); |
1084 | |
1085 | try { |
1086 | expect(finder, findsNothing); |
1087 | } on TestFailure catch (e) { |
1088 | failure = e; |
1089 | } |
1090 | |
1091 | expect( |
1092 | failure.message, |
1093 | contains('Actual: _PredicateSemanticsFinder:<Found 1 SemanticsNode with hint "hint3"'), |
1094 | ); |
1095 | }); |
1096 | }); |
1097 | |
1098 | group('byAction', () { |
1099 | testWidgets('finds nodes with matching action', (WidgetTester tester) async { |
1100 | await tester.pumpWidget(semanticsTree); |
1101 | |
1102 | final SemanticsFinder finder = find.semantics.byAction(SemanticsAction.copy); |
1103 | |
1104 | expect(finder, findsExactly(3)); |
1105 | }); |
1106 | |
1107 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
1108 | late TestFailure failure; |
1109 | await tester.pumpWidget(semanticsTree); |
1110 | |
1111 | final SemanticsFinder finder = find.semantics.byAction(SemanticsAction.copy); |
1112 | |
1113 | try { |
1114 | expect(finder, findsExactly(4)); |
1115 | } on TestFailure catch (e) { |
1116 | failure = e; |
1117 | } |
1118 | |
1119 | expect( |
1120 | failure.message, |
1121 | contains( |
1122 | 'Actual: _PredicateSemanticsFinder:<Found 3 SemanticsNodes with action "SemanticsAction.copy"', |
1123 | ), |
1124 | ); |
1125 | }); |
1126 | }); |
1127 | |
1128 | group('byAnyAction', () { |
1129 | testWidgets('finds nodes with any matching actions', (WidgetTester tester) async { |
1130 | await tester.pumpWidget(semanticsTree); |
1131 | |
1132 | final SemanticsFinder finder = find.semantics.byAnyAction(<SemanticsAction>[ |
1133 | SemanticsAction.paste, |
1134 | SemanticsAction.longPress, |
1135 | ]); |
1136 | |
1137 | expect(finder, findsExactly(4)); |
1138 | }); |
1139 | |
1140 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
1141 | late TestFailure failure; |
1142 | await tester.pumpWidget(semanticsTree); |
1143 | |
1144 | final SemanticsFinder finder = find.semantics.byAnyAction(<SemanticsAction>[ |
1145 | SemanticsAction.paste, |
1146 | SemanticsAction.longPress, |
1147 | ]); |
1148 | |
1149 | try { |
1150 | expect(finder, findsExactly(5)); |
1151 | } on TestFailure catch (e) { |
1152 | failure = e; |
1153 | } |
1154 | |
1155 | expect( |
1156 | failure.message, |
1157 | contains( |
1158 | 'Actual: _PredicateSemanticsFinder:<Found 4 SemanticsNodes with any of the following actions: [SemanticsAction.paste, SemanticsAction.longPress]:', |
1159 | ), |
1160 | ); |
1161 | }); |
1162 | }); |
1163 | |
1164 | group('byFlag', () { |
1165 | testWidgets('finds nodes with matching flag', (WidgetTester tester) async { |
1166 | await tester.pumpWidget(semanticsTree); |
1167 | |
1168 | final SemanticsFinder finder = find.semantics.byFlag(SemanticsFlag.isReadOnly); |
1169 | |
1170 | expect(finder, findsExactly(3)); |
1171 | }); |
1172 | |
1173 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
1174 | late TestFailure failure; |
1175 | await tester.pumpWidget(semanticsTree); |
1176 | |
1177 | final SemanticsFinder finder = find.semantics.byFlag(SemanticsFlag.isReadOnly); |
1178 | |
1179 | try { |
1180 | expect(finder, findsExactly(4)); |
1181 | } on TestFailure catch (e) { |
1182 | failure = e; |
1183 | } |
1184 | |
1185 | expect( |
1186 | failure.message, |
1187 | contains( |
1188 | '_PredicateSemanticsFinder:<Found 3 SemanticsNodes with flag "SemanticsFlag.isReadOnly":', |
1189 | ), |
1190 | ); |
1191 | }); |
1192 | }); |
1193 | |
1194 | group('byAnyFlag', () { |
1195 | testWidgets('finds nodes with any matching flag', (WidgetTester tester) async { |
1196 | await tester.pumpWidget(semanticsTree); |
1197 | |
1198 | final SemanticsFinder finder = find.semantics.byAnyFlag(<SemanticsFlag>[ |
1199 | SemanticsFlag.isHeader, |
1200 | SemanticsFlag.isTextField, |
1201 | ]); |
1202 | |
1203 | expect(finder, findsExactly(2)); |
1204 | }); |
1205 | |
1206 | testWidgets('fails with descriptive message', (WidgetTester tester) async { |
1207 | late TestFailure failure; |
1208 | await tester.pumpWidget(semanticsTree); |
1209 | |
1210 | final SemanticsFinder finder = find.semantics.byAnyFlag(<SemanticsFlag>[ |
1211 | SemanticsFlag.isHeader, |
1212 | SemanticsFlag.isTextField, |
1213 | ]); |
1214 | |
1215 | try { |
1216 | expect(finder, findsExactly(3)); |
1217 | } on TestFailure catch (e) { |
1218 | failure = e; |
1219 | } |
1220 | |
1221 | expect( |
1222 | failure.message, |
1223 | contains( |
1224 | 'Actual: _PredicateSemanticsFinder:<Found 2 SemanticsNodes with any of the following flags: [SemanticsFlag.isHeader, SemanticsFlag.isTextField]:', |
1225 | ), |
1226 | ); |
1227 | }); |
1228 | }); |
1229 | |
1230 | group('scrollable', () { |
1231 | testWidgets('can find node that can scroll up', (WidgetTester tester) async { |
1232 | final ScrollController controller = ScrollController(); |
1233 | await tester.pumpWidget( |
1234 | MaterialApp( |
1235 | home: SingleChildScrollView( |
1236 | controller: controller, |
1237 | child: const SizedBox(width: 100, height: 1000), |
1238 | ), |
1239 | ), |
1240 | ); |
1241 | |
1242 | expect( |
1243 | find.semantics.scrollable(), |
1244 | containsSemantics(hasScrollUpAction: true, hasScrollDownAction: false), |
1245 | ); |
1246 | }); |
1247 | |
1248 | testWidgets('can find node that can scroll down', (WidgetTester tester) async { |
1249 | final ScrollController controller = ScrollController(initialScrollOffset: 400); |
1250 | await tester.pumpWidget( |
1251 | MaterialApp( |
1252 | home: SingleChildScrollView( |
1253 | controller: controller, |
1254 | child: const SizedBox(width: 100, height: 1000), |
1255 | ), |
1256 | ), |
1257 | ); |
1258 | |
1259 | expect( |
1260 | find.semantics.scrollable(), |
1261 | containsSemantics(hasScrollUpAction: false, hasScrollDownAction: true), |
1262 | ); |
1263 | }); |
1264 | |
1265 | testWidgets('can find node that can scroll left', (WidgetTester tester) async { |
1266 | final ScrollController controller = ScrollController(); |
1267 | await tester.pumpWidget( |
1268 | MaterialApp( |
1269 | home: SingleChildScrollView( |
1270 | scrollDirection: Axis.horizontal, |
1271 | controller: controller, |
1272 | child: const SizedBox(width: 1000, height: 100), |
1273 | ), |
1274 | ), |
1275 | ); |
1276 | |
1277 | expect( |
1278 | find.semantics.scrollable(), |
1279 | containsSemantics(hasScrollLeftAction: true, hasScrollRightAction: false), |
1280 | ); |
1281 | }); |
1282 | |
1283 | testWidgets('can find node that can scroll right', (WidgetTester tester) async { |
1284 | final ScrollController controller = ScrollController(initialScrollOffset: 200); |
1285 | await tester.pumpWidget( |
1286 | MaterialApp( |
1287 | home: SingleChildScrollView( |
1288 | scrollDirection: Axis.horizontal, |
1289 | controller: controller, |
1290 | child: const SizedBox(width: 1000, height: 100), |
1291 | ), |
1292 | ), |
1293 | ); |
1294 | |
1295 | expect( |
1296 | find.semantics.scrollable(), |
1297 | containsSemantics(hasScrollLeftAction: false, hasScrollRightAction: true), |
1298 | ); |
1299 | }); |
1300 | |
1301 | testWidgets('can exclusively find node that scrolls horizontally', ( |
1302 | WidgetTester tester, |
1303 | ) async { |
1304 | await tester.pumpWidget( |
1305 | const MaterialApp( |
1306 | home: Column( |
1307 | children: <Widget>[ |
1308 | SingleChildScrollView( |
1309 | scrollDirection: Axis.horizontal, |
1310 | child: SizedBox(width: 1000, height: 100), |
1311 | ), |
1312 | Expanded(child: SingleChildScrollView(child: SizedBox(width: 100, height: 1000))), |
1313 | ], |
1314 | ), |
1315 | ), |
1316 | ); |
1317 | |
1318 | expect(find.semantics.scrollable(axis: Axis.horizontal), findsOne); |
1319 | }); |
1320 | |
1321 | testWidgets('can exclusively find node that scrolls vertically', (WidgetTester tester) async { |
1322 | await tester.pumpWidget( |
1323 | const MaterialApp( |
1324 | home: Column( |
1325 | children: <Widget>[ |
1326 | SingleChildScrollView( |
1327 | scrollDirection: Axis.horizontal, |
1328 | child: SizedBox(width: 1000, height: 100), |
1329 | ), |
1330 | Expanded(child: SingleChildScrollView(child: SizedBox(width: 100, height: 1000))), |
1331 | ], |
1332 | ), |
1333 | ), |
1334 | ); |
1335 | |
1336 | expect(find.semantics.scrollable(axis: Axis.vertical), findsOne); |
1337 | }); |
1338 | }); |
1339 | }); |
1340 | |
1341 | group('FinderBase', () { |
1342 | group('describeMatch', () { |
1343 | test('is used for Finder and results', () { |
1344 | const String expected = 'Fake finder describe match'; |
1345 | final _FakeFinder finder = _FakeFinder( |
1346 | describeMatchCallback: (_) { |
1347 | return expected; |
1348 | }, |
1349 | ); |
1350 | |
1351 | expect(finder.evaluate().toString(), contains(expected)); |
1352 | expect(finder.toString(describeSelf: true), contains(expected)); |
1353 | }); |
1354 | |
1355 | for (int i = 0; i < 4; i++) { |
1356 | test('gets expected plurality for$i when reporting results from find', () { |
1357 | final Plurality expected = switch (i) { |
1358 | 0 => Plurality.zero, |
1359 | 1 => Plurality.one, |
1360 | _ => Plurality.many, |
1361 | }; |
1362 | late final Plurality actual; |
1363 | final _FakeFinder finder = _FakeFinder( |
1364 | describeMatchCallback: (Plurality plurality) { |
1365 | actual = plurality; |
1366 | return 'Fake description'; |
1367 | }, |
1368 | findInCandidatesCallback: |
1369 | (_) => Iterable<String>.generate(i, (int index) => index.toString()), |
1370 | ); |
1371 | finder.evaluate().toString(); |
1372 | |
1373 | expect(actual, expected); |
1374 | }); |
1375 | |
1376 | test('gets expected plurality for$i when reporting results from toString', () { |
1377 | final Plurality expected = switch (i) { |
1378 | 0 => Plurality.zero, |
1379 | 1 => Plurality.one, |
1380 | _ => Plurality.many, |
1381 | }; |
1382 | late final Plurality actual; |
1383 | final _FakeFinder finder = _FakeFinder( |
1384 | describeMatchCallback: (Plurality plurality) { |
1385 | actual = plurality; |
1386 | return 'Fake description'; |
1387 | }, |
1388 | findInCandidatesCallback: |
1389 | (_) => Iterable<String>.generate(i, (int index) => index.toString()), |
1390 | ); |
1391 | finder.toString(); |
1392 | |
1393 | expect(actual, expected); |
1394 | }); |
1395 | |
1396 | test('always gets many when describing finder', () { |
1397 | const Plurality expected = Plurality.many; |
1398 | late final Plurality actual; |
1399 | final _FakeFinder finder = _FakeFinder( |
1400 | describeMatchCallback: (Plurality plurality) { |
1401 | actual = plurality; |
1402 | return 'Fake description'; |
1403 | }, |
1404 | findInCandidatesCallback: |
1405 | (_) => Iterable<String>.generate(i, (int index) => index.toString()), |
1406 | ); |
1407 | finder.toString(describeSelf: true); |
1408 | |
1409 | expect(actual, expected); |
1410 | }); |
1411 | } |
1412 | }); |
1413 | |
1414 | test('findInCandidates gets allCandidates', () { |
1415 | final List<String> expected = <String>['Test1', 'Test2', 'Test3', 'Test4']; |
1416 | late final List<String> actual; |
1417 | final _FakeFinder finder = _FakeFinder( |
1418 | allCandidatesCallback: () => expected, |
1419 | findInCandidatesCallback: (Iterable<String> candidates) { |
1420 | actual = candidates.toList(); |
1421 | return candidates; |
1422 | }, |
1423 | ); |
1424 | finder.evaluate(); |
1425 | |
1426 | expect(actual, expected); |
1427 | }); |
1428 | |
1429 | test('allCandidates calculated for each find', () { |
1430 | const int expectedCallCount = 3; |
1431 | int actualCallCount = 0; |
1432 | final _FakeFinder finder = _FakeFinder( |
1433 | allCandidatesCallback: () { |
1434 | actualCallCount++; |
1435 | return <String>['test']; |
1436 | }, |
1437 | ); |
1438 | for (int i = 0; i < expectedCallCount; i++) { |
1439 | finder.evaluate(); |
1440 | } |
1441 | |
1442 | expect(actualCallCount, expectedCallCount); |
1443 | }); |
1444 | |
1445 | test('allCandidates only called once while caching', () { |
1446 | int actualCallCount = 0; |
1447 | final _FakeFinder finder = _FakeFinder( |
1448 | allCandidatesCallback: () { |
1449 | actualCallCount++; |
1450 | return <String>['test']; |
1451 | }, |
1452 | ); |
1453 | finder.runCached(() { |
1454 | for (int i = 0; i < 5; i++) { |
1455 | finder.evaluate(); |
1456 | finder.tryEvaluate(); |
1457 | final FinderResult<String> _ = finder.found; |
1458 | } |
1459 | }); |
1460 | |
1461 | expect(actualCallCount, 1); |
1462 | }); |
1463 | |
1464 | group('tryFind', () { |
1465 | test('returns false if no results', () { |
1466 | final _FakeFinder finder = _FakeFinder(findInCandidatesCallback: (_) => <String>[]); |
1467 | |
1468 | expect(finder.tryEvaluate(), false); |
1469 | }); |
1470 | |
1471 | test('returns true if results are available', () { |
1472 | final _FakeFinder finder = _FakeFinder( |
1473 | findInCandidatesCallback: (_) => <String>['Results'], |
1474 | ); |
1475 | |
1476 | expect(finder.tryEvaluate(), true); |
1477 | }); |
1478 | }); |
1479 | |
1480 | group('found', () { |
1481 | test('throws before any calls to evaluate or tryEvaluate', () { |
1482 | final _FakeFinder finder = _FakeFinder(); |
1483 | |
1484 | expect(finder.hasFound, false); |
1485 | expect(() => finder.found, throwsAssertionError); |
1486 | }); |
1487 | |
1488 | test('has same results as evaluate after call to evaluate', () { |
1489 | final _FakeFinder finder = _FakeFinder(); |
1490 | final FinderResult<String> expected = finder.evaluate(); |
1491 | |
1492 | expect(finder.hasFound, true); |
1493 | expect(finder.found, expected); |
1494 | }); |
1495 | |
1496 | test('has expected results after call to tryFind', () { |
1497 | final Iterable<String> expected = Iterable<String>.generate(10, (int i) => i.toString()); |
1498 | final _FakeFinder finder = _FakeFinder(findInCandidatesCallback: (_) => expected); |
1499 | finder.tryEvaluate(); |
1500 | |
1501 | expect(finder.hasFound, true); |
1502 | expect(finder.found, orderedEquals(expected)); |
1503 | }); |
1504 | }); |
1505 | }); |
1506 | } |
1507 | |
1508 | Widget _boilerplate(Widget child) { |
1509 | return Directionality( |
1510 | textDirection: TextDirection.ltr, |
1511 | child: Navigator( |
1512 | onGenerateRoute: (RouteSettings settings) { |
1513 | return MaterialPageRoute<void>(builder: (BuildContext context) => child); |
1514 | }, |
1515 | ), |
1516 | ); |
1517 | } |
1518 | |
1519 | class SimpleCustomSemanticsWidget extends LeafRenderObjectWidget { |
1520 | const SimpleCustomSemanticsWidget(this.label, {super.key}); |
1521 | |
1522 | final String label; |
1523 | |
1524 | @override |
1525 | RenderObject createRenderObject(BuildContext context) => SimpleCustomSemanticsRenderObject(label); |
1526 | } |
1527 | |
1528 | class SimpleCustomSemanticsRenderObject extends RenderBox { |
1529 | SimpleCustomSemanticsRenderObject(this.label); |
1530 | |
1531 | final String label; |
1532 | |
1533 | @override |
1534 | bool get sizedByParent => true; |
1535 | |
1536 | @override |
1537 | Size computeDryLayout(BoxConstraints constraints) { |
1538 | return constraints.smallest; |
1539 | } |
1540 | |
1541 | @override |
1542 | void describeSemanticsConfiguration(SemanticsConfiguration config) { |
1543 | super.describeSemanticsConfiguration(config); |
1544 | config |
1545 | ..label = label |
1546 | ..textDirection = TextDirection.ltr; |
1547 | } |
1548 | } |
1549 | |
1550 | class SimpleGenericWidget<T> extends StatelessWidget { |
1551 | const SimpleGenericWidget({required Widget child, super.key}) : _child = child; |
1552 | |
1553 | final Widget _child; |
1554 | |
1555 | @override |
1556 | Widget build(BuildContext context) { |
1557 | return _child; |
1558 | } |
1559 | } |
1560 | |
1561 | /// Wraps [child] in [depth] layers of [SizedBox] |
1562 | Widget _deepWidgetTree({required int depth, required Widget child}) { |
1563 | Widget tree = child; |
1564 | for (int i = 0; i < depth; i += 1) { |
1565 | tree = SizedBox(child: tree); |
1566 | } |
1567 | return tree; |
1568 | } |
1569 | |
1570 | class _FakeFinder extends FinderBase<String> { |
1571 | _FakeFinder({ |
1572 | this.allCandidatesCallback, |
1573 | this.describeMatchCallback, |
1574 | this.findInCandidatesCallback, |
1575 | }); |
1576 | |
1577 | final Iterable<String> Function()? allCandidatesCallback; |
1578 | final DescribeMatchCallback? describeMatchCallback; |
1579 | final Iterable<String> Function(Iterable<String> candidates)? findInCandidatesCallback; |
1580 | |
1581 | @override |
1582 | Iterable<String> get allCandidates { |
1583 | return allCandidatesCallback?.call() ?? <String>['String 1', 'String 2', 'String 3']; |
1584 | } |
1585 | |
1586 | @override |
1587 | String describeMatch(Plurality plurality) { |
1588 | return describeMatchCallback?.call(plurality) ?? |
1589 | switch (plurality) { |
1590 | Plurality.one => 'String', |
1591 | Plurality.many || Plurality.zero => 'Strings', |
1592 | }; |
1593 | } |
1594 | |
1595 | @override |
1596 | Iterable<String> findInCandidates(Iterable<String> candidates) { |
1597 | return findInCandidatesCallback?.call(candidates) ?? candidates; |
1598 | } |
1599 | } |
1600 |
Definitions
- fooBarTexts
- main
- _boilerplate
- SimpleCustomSemanticsWidget
- SimpleCustomSemanticsWidget
- createRenderObject
- SimpleCustomSemanticsRenderObject
- SimpleCustomSemanticsRenderObject
- sizedByParent
- computeDryLayout
- describeSemanticsConfiguration
- SimpleGenericWidget
- SimpleGenericWidget
- build
- _deepWidgetTree
- _FakeFinder
- _FakeFinder
- allCandidates
- describeMatch
Learn more about Flutter for embedded and desktop on industrialflutter.com