// Copyright 2014 The Flutter Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter_test/flutter_test.dart'; const List fooBarTexts = [ Text('foo', textDirection: TextDirection.ltr), Text('bar', textDirection: TextDirection.ltr), ]; void main() { group('image', () { testWidgets('finds Image widgets', (WidgetTester tester) async { await tester .pumpWidget(_boilerplate(Image(image: FileImage(File('test'))))); expect(find.image(FileImage(File('test'))), findsOneWidget); }); testWidgets('finds Button widgets with Image', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(ElevatedButton( onPressed: null, child: Image(image: FileImage(File('test'))), ))); expect(find.widgetWithImage(ElevatedButton, FileImage(File('test'))), findsOneWidget); }); }); group('text', () { testWidgets('finds Text widgets', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Text('test'), )); expect(find.text('test'), findsOneWidget); }); testWidgets('finds Text.rich widgets', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 't', children: [ TextSpan(text: 'e'), TextSpan(text: 'st'), ], ), ))); expect(find.text('test'), findsOneWidget); }); group('findRichText', () { testWidgets('finds RichText widgets when enabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(RichText( text: const TextSpan( text: 't', children: [ TextSpan(text: 'est'), ], ), ))); expect(find.text('test', findRichText: true), findsOneWidget); }); testWidgets('finds Text widgets once when enabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text('test2'))); expect(find.text('test2', findRichText: true), findsOneWidget); }); testWidgets('does not find RichText widgets when disabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(RichText( text: const TextSpan( text: 't', children: [ TextSpan(text: 'est'), ], ), ))); expect(find.text('test'), findsNothing); }); testWidgets( 'does not find Text and RichText separated by semantics widgets twice', (WidgetTester tester) async { // If rich: true found both Text and RichText, this would find two widgets. await tester.pumpWidget(_boilerplate( const Text('test', semanticsLabel: 'foo'), )); expect(find.text('test'), findsOneWidget); }); testWidgets('finds Text.rich widgets when enabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 't', children: [ TextSpan(text: 'est'), TextSpan(text: '3'), ], ), ))); expect(find.text('test3', findRichText: true), findsOneWidget); }); testWidgets('finds Text.rich widgets when disabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 't', children: [ TextSpan(text: 'est'), TextSpan(text: '3'), ], ), ))); expect(find.text('test3'), findsOneWidget); }); }); }); group('textContaining', () { testWidgets('finds Text widgets', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Text('this is a test'), )); expect(find.textContaining(RegExp(r'test')), findsOneWidget); expect(find.textContaining('test'), findsOneWidget); expect(find.textContaining('a'), findsOneWidget); expect(find.textContaining('s'), findsOneWidget); }); testWidgets('finds Text.rich widgets', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 'this', children: [ TextSpan(text: 'is'), TextSpan(text: 'a'), TextSpan(text: 'test'), ], ), ))); expect(find.textContaining(RegExp(r'isatest')), findsOneWidget); expect(find.textContaining('isatest'), findsOneWidget); }); testWidgets('finds EditableText widgets', (WidgetTester tester) async { await tester.pumpWidget(MaterialApp( home: Scaffold( body: _boilerplate(TextField( controller: TextEditingController()..text = 'this is test', )), ), )); expect(find.textContaining(RegExp(r'test')), findsOneWidget); expect(find.textContaining('test'), findsOneWidget); }); group('findRichText', () { testWidgets('finds RichText widgets when enabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(RichText( text: const TextSpan( text: 't', children: [ TextSpan(text: 'est'), ], ), ))); expect(find.textContaining('te', findRichText: true), findsOneWidget); }); testWidgets('finds Text widgets once when enabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text('test2'))); expect(find.textContaining('tes', findRichText: true), findsOneWidget); }); testWidgets('does not find RichText widgets when disabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(RichText( text: const TextSpan( text: 't', children: [ TextSpan(text: 'est'), ], ), ))); expect(find.textContaining('te'), findsNothing); }); testWidgets( 'does not find Text and RichText separated by semantics widgets twice', (WidgetTester tester) async { // If rich: true found both Text and RichText, this would find two widgets. await tester.pumpWidget(_boilerplate( const Text('test', semanticsLabel: 'foo'), )); expect(find.textContaining('tes'), findsOneWidget); }); testWidgets('finds Text.rich widgets when enabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 't', children: [ TextSpan(text: 'est'), TextSpan(text: '3'), ], ), ))); expect(find.textContaining('t3', findRichText: true), findsOneWidget); }); testWidgets('finds Text.rich widgets when disabled', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate(const Text.rich( TextSpan( text: 't', children: [ TextSpan(text: 'est'), TextSpan(text: '3'), ], ), ))); expect(find.textContaining('t3'), findsOneWidget); }); }); }); group('semantics', () { testWidgets('Throws StateError if semantics are not enabled', (WidgetTester tester) async { expect(() => find.bySemanticsLabel('Add'), throwsStateError); }, semanticsEnabled: false); testWidgets('finds Semantically labeled widgets', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester.pumpWidget(_boilerplate( Semantics( label: 'Add', button: true, child: const TextButton( onPressed: null, child: Text('+'), ), ), )); expect(find.bySemanticsLabel('Add'), findsOneWidget); semanticsHandle.dispose(); }); testWidgets('finds Semantically labeled widgets by RegExp', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester.pumpWidget(_boilerplate( Semantics( container: true, child: const Row(children: [ Text('Hello'), Text('World'), ]), ), )); expect(find.bySemanticsLabel('Hello'), findsNothing); expect(find.bySemanticsLabel(RegExp(r'^Hello')), findsOneWidget); semanticsHandle.dispose(); }); testWidgets('finds Semantically labeled widgets without explicit Semantics', (WidgetTester tester) async { final SemanticsHandle semanticsHandle = tester.ensureSemantics(); await tester .pumpWidget(_boilerplate(const SimpleCustomSemanticsWidget('Foo'))); expect(find.bySemanticsLabel('Foo'), findsOneWidget); semanticsHandle.dispose(); }); }); group('byTooltip', () { testWidgets('finds widgets by tooltip', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Tooltip( message: 'Tooltip Message', child: Text('+'), ), )); expect(find.byTooltip('Tooltip Message'), findsOneWidget); }); testWidgets('finds widgets with tooltip by RegExp', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Tooltip( message: 'Tooltip Message', child: Text('+'), ), )); expect(find.byTooltip('Tooltip'), findsNothing); expect(find.byTooltip(RegExp(r'^Tooltip')), findsOneWidget); }); testWidgets('finds widgets by rich text tooltip', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Tooltip( richMessage: TextSpan( children: [ TextSpan(text: 'Tooltip '), TextSpan(text: 'Message'), ]), child: Text('+'), ), )); expect(find.byTooltip('Tooltip Message'), findsOneWidget); }); testWidgets('finds widgets with rich text tooltip by RegExp', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Tooltip( richMessage: TextSpan( children: [ TextSpan(text: 'Tooltip '), TextSpan(text: 'Message'), ]), child: Text('+'), ), )); expect(find.byTooltip('Tooltip M'), findsNothing); expect(find.byTooltip(RegExp(r'^Tooltip M')), findsOneWidget); }); testWidgets('finds empty string with tooltip', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( const Tooltip( message: '', child: Text('+'), ), )); expect(find.byTooltip(''), findsOneWidget); await tester.pumpWidget(_boilerplate( const Tooltip( richMessage: TextSpan( children: [ TextSpan(text: ''), ]), child: Text('+'), ), )); expect(find.byTooltip(''), findsOneWidget); await tester.pumpWidget(_boilerplate( const Tooltip( message: '', child: Text('+'), ), )); expect(find.byTooltip(RegExp(r'^$')), findsOneWidget); await tester.pumpWidget(_boilerplate( const Tooltip( richMessage: TextSpan( children: [ TextSpan(text: ''), ]), child: Text('+'), ), )); expect(find.byTooltip(RegExp(r'^$')), findsOneWidget); }); }); group('hitTestable', () { testWidgets('excludes non-hit-testable widgets', (WidgetTester tester) async { await tester.pumpWidget( _boilerplate(IndexedStack( sizing: StackFit.expand, children: [ GestureDetector( key: const ValueKey(0), behavior: HitTestBehavior.opaque, onTap: () {}, child: const SizedBox.expand(), ), GestureDetector( key: const ValueKey(1), behavior: HitTestBehavior.opaque, onTap: () {}, child: const SizedBox.expand(), ), ], )), ); expect(find.byType(GestureDetector), findsOneWidget); expect(find.byType(GestureDetector, skipOffstage: false), findsNWidgets(2)); final Finder hitTestable = find.byType(GestureDetector, skipOffstage: false).hitTestable(); expect(hitTestable, findsOneWidget); expect(tester.widget(hitTestable).key, const ValueKey(0)); }); }); group('text range finders', () { testWidgets('basic text span test', (WidgetTester tester) async { await tester.pumpWidget( _boilerplate(const IndexedStack( sizing: StackFit.expand, children: [ Text.rich(TextSpan( text: 'sub', children: [ TextSpan(text: 'stringsub'), TextSpan(text: 'stringsub'), TextSpan(text: 'stringsub'), ], )), Text('substringsub'), ], )), ); expect(find.textRange.ofSubstring('substringsub'), findsExactly(2)); // Pattern skips overlapping matches. expect(find.textRange.ofSubstring('substringsub').first.evaluate().single.textRange, const TextRange(start: 0, end: 12)); expect(find.textRange.ofSubstring('substringsub').last.evaluate().single.textRange, const TextRange(start: 18, end: 30)); expect( find.textRange.ofSubstring('substringsub').first.evaluate().single.renderObject, find.textRange.ofSubstring('substringsub').last.evaluate().single.renderObject, ); expect(find.textRange.ofSubstring('substringsub', skipOffstage: false), findsExactly(3)); }); testWidgets('basic text span test', (WidgetTester tester) async { await tester.pumpWidget( _boilerplate(const IndexedStack( sizing: StackFit.expand, children: [ Text.rich(TextSpan( text: 'sub', children: [ TextSpan(text: 'stringsub'), TextSpan(text: 'stringsub'), TextSpan(text: 'stringsub'), ], )), Text('substringsub'), ], )), ); expect(find.textRange.ofSubstring('substringsub'), findsExactly(2)); // Pattern skips overlapping matches. expect(find.textRange.ofSubstring('substringsub').first.evaluate().single.textRange, const TextRange(start: 0, end: 12)); expect(find.textRange.ofSubstring('substringsub').last.evaluate().single.textRange, const TextRange(start: 18, end: 30)); expect( find.textRange.ofSubstring('substringsub').first.evaluate().single.renderObject, find.textRange.ofSubstring('substringsub').last.evaluate().single.renderObject, ); expect(find.textRange.ofSubstring('substringsub', skipOffstage: false), findsExactly(3)); }); testWidgets('descendentOf', (WidgetTester tester) async { await tester.pumpWidget( _boilerplate( const Column( children: [ Text.rich(TextSpan(text: 'text')), Text.rich(TextSpan(text: 'text')), ], ), ), ); expect(find.textRange.ofSubstring('text'), findsExactly(2)); expect(find.textRange.ofSubstring('text', descendentOf: find.text('text').first), findsOne); }); testWidgets('finds only static text for now', (WidgetTester tester) async { await tester.pumpWidget( _boilerplate( EditableText( controller: TextEditingController(text: 'text'), focusNode: FocusNode(), style: const TextStyle(), cursorColor: const Color(0x00000000), backgroundCursorColor: const Color(0x00000000), ) ), ); expect(find.textRange.ofSubstring('text'), findsNothing); }); }); testWidgets('ChainedFinders chain properly', (WidgetTester tester) async { final GlobalKey key1 = GlobalKey(); await tester.pumpWidget( _boilerplate(Column( children: [ Container( key: key1, child: const Text('1'), ), const Text('2'), ], )), ); // Get the text back. By correctly chaining the descendant finder's // candidates, it should find 1 instead of 2. If the _LastFinder wasn't // correctly chained after the descendant's candidates, the last element // with a Text widget would have been 2. final Text text = find .descendant( of: find.byKey(key1), matching: find.byType(Text), ) .last .evaluate() .single .widget as Text; expect(text.data, '1'); }); testWidgets('finds multiple subtypes', (WidgetTester tester) async { await tester.pumpWidget(_boilerplate( Row(children: [ const Column(children: [ Text('Hello'), Text('World'), ]), Column(children: [ Image(image: FileImage(File('test'))), ]), const Column(children: [ SimpleGenericWidget(child: Text('one')), SimpleGenericWidget(child: Text('pi')), SimpleGenericWidget(child: Text('two')), ]), ]), )); expect(find.bySubtype(), findsOneWidget); expect(find.bySubtype(), findsNWidgets(3)); // Finds both rows and columns. expect(find.bySubtype(), findsNWidgets(4)); // Finds only the requested generic subtypes. expect(find.bySubtype>(), findsOneWidget); expect(find.bySubtype>(), findsNWidgets(2)); expect(find.bySubtype>(), findsNWidgets(3)); // Finds all widgets. final int totalWidgetCount = find.byWidgetPredicate((_) => true).evaluate().length; expect(find.bySubtype(), findsNWidgets(totalWidgetCount)); }); group('find.byElementPredicate', () { testWidgets('fails with a custom description in the message', (WidgetTester tester) async { await tester.pumpWidget(const Text('foo', textDirection: TextDirection.ltr)); const String customDescription = 'custom description'; late TestFailure failure; try { expect(find.byElementPredicate((_) => false, description: customDescription), findsOneWidget); } on TestFailure catch (e) { failure = e; } expect(failure, isNotNull); expect(failure.message, contains('Actual: _ElementPredicateWidgetFinder: false, description: customDescription), findsOneWidget); } on TestFailure catch (e) { failure = e; } expect(failure, isNotNull); expect(failure.message, contains('Actual: _WidgetPredicateWidgetFinder:[ Column(children: fooBarTexts), ], )); expect(find.descendant( of: find.widgetWithText(Row, 'foo'), matching: find.text('bar'), ), findsOneWidget); }); testWidgets('finds two descendants with different ancestors', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: [ Column(children: fooBarTexts), Column(children: fooBarTexts), ], )); expect(find.descendant( of: find.widgetWithText(Column, 'foo'), matching: find.text('bar'), ), findsNWidgets(2)); }); testWidgets('fails with a descriptive message', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: [ Column(children: [Text('foo', textDirection: TextDirection.ltr)]), Text('bar', textDirection: TextDirection.ltr), ], )); late TestFailure failure; try { expect(find.descendant( of: find.widgetWithText(Column, 'foo'), matching: find.text('bar'), ), findsOneWidget); } on TestFailure catch (e) { failure = e; } expect(failure, isNotNull); expect( failure.message, contains( 'Actual: _DescendantWidgetFinder:[ Column(children: fooBarTexts), ], )); expect(find.ancestor( of: find.text('bar'), matching: find.widgetWithText(Row, 'foo'), ), findsOneWidget); }); testWidgets('finds two matching ancestors, one descendant', (WidgetTester tester) async { await tester.pumpWidget( const Directionality( textDirection: TextDirection.ltr, child: Row( children: [ Row(children: fooBarTexts), ], ), ), ); expect(find.ancestor( of: find.text('bar'), matching: find.byType(Row), ), findsNWidgets(2)); }); testWidgets('fails with a descriptive message', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: [ Column(children: [Text('foo', textDirection: TextDirection.ltr)]), Text('bar', textDirection: TextDirection.ltr), ], )); late TestFailure failure; try { expect(find.ancestor( of: find.text('bar'), matching: find.widgetWithText(Column, 'foo'), ), findsOneWidget); } on TestFailure catch (e) { failure = e; } expect(failure, isNotNull); expect( failure.message, contains( 'Actual: _AncestorWidgetFinder:[ Column(children: fooBarTexts), ], )); expect(find.ancestor( of: find.byType(Column), matching: find.widgetWithText(Column, 'foo'), ), findsNothing); }); testWidgets('Match the root', (WidgetTester tester) async { await tester.pumpWidget(const Row( textDirection: TextDirection.ltr, children: [ Column(children: fooBarTexts), ], )); expect(find.descendant( of: find.byType(Column), matching: find.widgetWithText(Column, 'foo'), matchRoot: true, ), findsOneWidget); }); testWidgets('is fast in deep tree', (WidgetTester tester) async { await tester.pumpWidget( Directionality( textDirection: TextDirection.ltr, child: _deepWidgetTree( depth: 1000, child: Row( children: [ _deepWidgetTree( depth: 1000, child: const Column(children: fooBarTexts), ), ], ), ), ), ); expect(find.ancestor( of: find.text('bar'), matching: find.byType(Row), ), findsOneWidget); }); }); group('CommonSemanticsFinders', () { final Widget semanticsTree = _boilerplate( Semantics( container: true, header: true, readOnly: true, onCopy: () {}, onLongPress: () {}, value: 'value1', hint: 'hint1', label: 'label1', child: Semantics( container: true, textField: true, onSetText: (_) { }, onPaste: () { }, onLongPress: () { }, value: 'value2', hint: 'hint2', label: 'label2', child: Semantics( container: true, readOnly: true, onCopy: () {}, value: 'value3', hint: 'hint3', label: 'label3', child: Semantics( container: true, readOnly: true, onLongPress: () { }, value: 'value4', hint: 'hint4', label: 'label4', child: Semantics( container: true, onLongPress: () { }, onCopy: () {}, value: 'value5', hint: 'hint5', label: 'label5' ), ), ) ), ), ); group('ancestor', () { testWidgets('finds matching ancestor nodes', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final FinderBase finder = find.semantics.ancestor( of: find.semantics.byLabel('label4'), matching: find.semantics.byAction(SemanticsAction.copy), ); expect(finder, findsExactly(2)); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final FinderBase finder = find.semantics.ancestor( of: find.semantics.byLabel('label4'), matching: find.semantics.byAction(SemanticsAction.copy), ); try { expect(finder, findsExactly(3)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _AncestorSemanticsFinder: finder = find.semantics.descendant( of: find.semantics.byLabel('label4'), matching: find.semantics.byAction(SemanticsAction.copy), ); expect(finder, findsOne); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final FinderBase finder = find.semantics.descendant( of: find.semantics.byLabel('label4'), matching: find.semantics.byAction(SemanticsAction.copy), ); try { expect(finder, findsNothing); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _DescendantSemanticsFinder: 1; }, ); expect(finder, findsExactly(4)); }); testWidgets('fails with default message', (WidgetTester tester) async { late TestFailure failure; final RegExp replaceRegExp = RegExp(r'^[^\d]+'); await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byPredicate( (SemanticsNode node) { final int labelNum = int.tryParse(node.label.replaceAll(replaceRegExp, '')) ?? -1; return labelNum > 1; }, ); try { expect(finder, findsExactly(5)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder: 1; }, describeMatch: (_) => expected, ); try { expect(finder, findsExactly(5)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains(expected)); }); }); group('byLabel', () { testWidgets('finds nodes with matching label using String', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byLabel('label3'); expect(finder, findsOne); expect(finder.found.first.label, 'label3'); }); testWidgets('finds nodes with matching label using RegEx', (WidgetTester tester) async { await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byLabel(RegExp('^label.*')); expect(finder, findsExactly(5)); expect(finder.found.every((SemanticsNode node) => node.label.startsWith('label')), isTrue); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byLabel('label3'); try { expect(finder, findsNothing); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder: node.value.startsWith('value')), isTrue); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byValue('value3'); try { expect(finder, findsNothing); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder: node.hint.startsWith('hint')), isTrue); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byHint('hint3'); try { expect(finder, findsNothing); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:[ SemanticsAction.paste, SemanticsAction.longPress, ]); expect(finder, findsExactly(4)); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byAnyAction([ SemanticsAction.paste, SemanticsAction.longPress, ]); try { expect(finder, findsExactly(5)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:[ SemanticsFlag.isHeader, SemanticsFlag.isTextField, ]); expect(finder, findsExactly(2)); }); testWidgets('fails with descriptive message', (WidgetTester tester) async { late TestFailure failure; await tester.pumpWidget(semanticsTree); final SemanticsFinder finder = find.semantics.byAnyFlag([ SemanticsFlag.isHeader, SemanticsFlag.isTextField, ]); try { expect(finder, findsExactly(3)); } on TestFailure catch (e) { failure = e; } expect(failure.message, contains('Actual: _PredicateSemanticsFinder:[ SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox(width: 1000, height: 100), ), Expanded( child: SingleChildScrollView( child: SizedBox(width: 100, height: 1000), ), ), ], ) )); expect(find.semantics.scrollable(axis: Axis.horizontal), findsOne); }); testWidgets('can exclusively find node that scrolls vertically', (WidgetTester tester) async { await tester.pumpWidget(const MaterialApp( home: Column( children: [ SingleChildScrollView( scrollDirection: Axis.horizontal, child: SizedBox(width: 1000, height: 100), ), Expanded( child: SingleChildScrollView( child: SizedBox(width: 100, height: 1000), ), ), ], ) )); expect(find.semantics.scrollable(axis: Axis.vertical), findsOne); }); }); }); group('FinderBase', () { group('describeMatch', () { test('is used for Finder and results', () { const String expected = 'Fake finder describe match'; final _FakeFinder finder = _FakeFinder(describeMatchCallback: (_) { return expected; }); expect(finder.evaluate().toString(), contains(expected)); expect(finder.toString(describeSelf: true), contains(expected)); }); for (int i = 0; i < 4; i++) { test('gets expected plurality for $i when reporting results from find', () { final Plurality expected = switch (i) { 0 => Plurality.zero, 1 => Plurality.one, _ => Plurality.many, }; late final Plurality actual; final _FakeFinder finder = _FakeFinder( describeMatchCallback: (Plurality plurality) { actual = plurality; return 'Fake description'; }, findInCandidatesCallback: (_) => Iterable.generate(i, (int index) => index.toString()), ); finder.evaluate().toString(); expect(actual, expected); }); test('gets expected plurality for $i when reporting results from toString', () { final Plurality expected = switch (i) { 0 => Plurality.zero, 1 => Plurality.one, _ => Plurality.many, }; late final Plurality actual; final _FakeFinder finder = _FakeFinder( describeMatchCallback: (Plurality plurality) { actual = plurality; return 'Fake description'; }, findInCandidatesCallback: (_) => Iterable.generate(i, (int index) => index.toString()), ); finder.toString(); expect(actual, expected); }); test('always gets many when describing finder', () { const Plurality expected = Plurality.many; late final Plurality actual; final _FakeFinder finder = _FakeFinder( describeMatchCallback: (Plurality plurality) { actual = plurality; return 'Fake description'; }, findInCandidatesCallback: (_) => Iterable.generate(i, (int index) => index.toString()), ); finder.toString(describeSelf: true); expect(actual, expected); }); } }); test('findInCandidates gets allCandidates', () { final List expected = ['Test1', 'Test2', 'Test3', 'Test4']; late final List actual; final _FakeFinder finder = _FakeFinder( allCandidatesCallback: () => expected, findInCandidatesCallback: (Iterable candidates) { actual = candidates.toList(); return candidates; }, ); finder.evaluate(); expect(actual, expected); }); test('allCandidates calculated for each find', () { const int expectedCallCount = 3; int actualCallCount = 0; final _FakeFinder finder = _FakeFinder( allCandidatesCallback: () { actualCallCount++; return ['test']; }, ); for (int i = 0; i < expectedCallCount; i++) { finder.evaluate(); } expect(actualCallCount, expectedCallCount); }); test('allCandidates only called once while caching', () { int actualCallCount = 0; final _FakeFinder finder = _FakeFinder( allCandidatesCallback: () { actualCallCount++; return ['test']; }, ); finder.runCached(() { for (int i = 0; i < 5; i++) { finder.evaluate(); finder.tryEvaluate(); final FinderResult _ = finder.found; } }); expect(actualCallCount, 1); }); group('tryFind', () { test('returns false if no results', () { final _FakeFinder finder = _FakeFinder( findInCandidatesCallback: (_) => [], ); expect(finder.tryEvaluate(), false); }); test('returns true if results are available', () { final _FakeFinder finder = _FakeFinder( findInCandidatesCallback: (_) => ['Results'], ); expect(finder.tryEvaluate(), true); }); }); group('found', () { test('throws before any calls to evaluate or tryEvaluate', () { final _FakeFinder finder = _FakeFinder(); expect(finder.hasFound, false); expect(() => finder.found, throwsAssertionError); }); test('has same results as evaluate after call to evaluate', () { final _FakeFinder finder = _FakeFinder(); final FinderResult expected = finder.evaluate(); expect(finder.hasFound, true); expect(finder.found, expected); }); test('has expected results after call to tryFind', () { final Iterable expected = Iterable.generate(10, (int i) => i.toString()); final _FakeFinder finder = _FakeFinder(findInCandidatesCallback: (_) => expected); finder.tryEvaluate(); expect(finder.hasFound, true); expect(finder.found, orderedEquals(expected)); }); }); }); } Widget _boilerplate(Widget child) { return Directionality( textDirection: TextDirection.ltr, child: Navigator( onGenerateRoute: (RouteSettings settings) { return MaterialPageRoute( builder: (BuildContext context) => child, ); }, ), ); } class SimpleCustomSemanticsWidget extends LeafRenderObjectWidget { const SimpleCustomSemanticsWidget(this.label, {super.key}); final String label; @override RenderObject createRenderObject(BuildContext context) => SimpleCustomSemanticsRenderObject(label); } class SimpleCustomSemanticsRenderObject extends RenderBox { SimpleCustomSemanticsRenderObject(this.label); final String label; @override bool get sizedByParent => true; @override Size computeDryLayout(BoxConstraints constraints) { return constraints.smallest; } @override void describeSemanticsConfiguration(SemanticsConfiguration config) { super.describeSemanticsConfiguration(config); config ..label = label ..textDirection = TextDirection.ltr; } } class SimpleGenericWidget extends StatelessWidget { const SimpleGenericWidget({required Widget child, super.key}) : _child = child; final Widget _child; @override Widget build(BuildContext context) { return _child; } } /// Wraps [child] in [depth] layers of [SizedBox] Widget _deepWidgetTree({required int depth, required Widget child}) { Widget tree = child; for (int i = 0; i < depth; i += 1) { tree = SizedBox(child: tree); } return tree; } class _FakeFinder extends FinderBase { _FakeFinder({ this.allCandidatesCallback, this.describeMatchCallback, this.findInCandidatesCallback, }); final Iterable Function()? allCandidatesCallback; final DescribeMatchCallback? describeMatchCallback; final Iterable Function(Iterable candidates)? findInCandidatesCallback; @override Iterable get allCandidates { return allCandidatesCallback?.call() ?? [ 'String 1', 'String 2', 'String 3', ]; } @override String describeMatch(Plurality plurality) { return describeMatchCallback?.call(plurality) ?? switch (plurality) { Plurality.one => 'String', Plurality.many || Plurality.zero => 'Strings', }; } @override Iterable findInCandidates(Iterable candidates) { return findInCandidatesCallback?.call(candidates) ?? candidates; } }