finders_test.dart 10.9 KB
Newer Older
Ian Hickson's avatar
Ian Hickson committed
1
// Copyright 2014 The Flutter Authors. All rights reserved.
2 3 4
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5 6
import 'dart:io';

7
import 'package:flutter/material.dart';
8
import 'package:flutter/rendering.dart';
9 10 11
import 'package:flutter_test/flutter_test.dart';

void main() {
12 13
  group('image', () {
    testWidgets('finds Image widgets', (WidgetTester tester) async {
14 15
      await tester
          .pumpWidget(_boilerplate(Image(image: FileImage(File('test')))));
16
      expect(find.image(FileImage(File('test'))), findsOneWidget);
17
    });
18 19

    testWidgets('finds Button widgets with Image', (WidgetTester tester) async {
20 21 22 23 24 25
      await tester.pumpWidget(_boilerplate(ElevatedButton(
        onPressed: null,
        child: Image(image: FileImage(File('test'))),
      )));
      expect(find.widgetWithImage(ElevatedButton, FileImage(File('test'))),
          findsOneWidget);
26
    });
27 28
  });

29 30 31 32 33 34 35
  group('text', () {
    testWidgets('finds Text widgets', (WidgetTester tester) async {
      await tester.pumpWidget(_boilerplate(
        const Text('test'),
      ));
      expect(find.text('test'), findsOneWidget);
    });
36

37
    testWidgets('finds Text.rich widgets', (WidgetTester tester) async {
38 39 40 41
      await tester.pumpWidget(_boilerplate(const Text.rich(
        TextSpan(
          text: 't',
          children: <TextSpan>[
42 43
            TextSpan(text: 'e'),
            TextSpan(text: 'st'),
44
          ],
45 46 47 48 49
        ),
      )));

      expect(find.text('test'), findsOneWidget);
    });
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83

    group('findRichText', () {
      testWidgets('finds RichText widgets when enabled',
          (WidgetTester tester) async {
        await tester.pumpWidget(_boilerplate(RichText(
          text: const TextSpan(
            text: 't',
            children: <TextSpan>[
              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>[
              TextSpan(text: 'est'),
            ],
          ),
        )));

84
        expect(find.text('test'), findsNothing);
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124
      });

      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>[
              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>[
              TextSpan(text: 'est'),
              TextSpan(text: '3'),
            ],
          ),
        )));

125
        expect(find.text('test3'), findsOneWidget);
126 127
      });
    });
128 129
  });

130 131 132 133 134 135 136 137 138 139 140 141
  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 {
142 143 144 145 146 147 148 149 150 151
      await tester.pumpWidget(_boilerplate(const Text.rich(
        TextSpan(
          text: 'this',
          children: <TextSpan>[
            TextSpan(text: 'is'),
            TextSpan(text: 'a'),
            TextSpan(text: 'test'),
          ],
        ),
      )));
152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170

      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);
    });
  });

171
  group('semantics', () {
172 173
    testWidgets('Throws StateError if semantics are not enabled',
        (WidgetTester tester) async {
174
      expect(() => find.bySemanticsLabel('Add'), throwsStateError);
175
    }, semanticsEnabled: false);
176

177 178
    testWidgets('finds Semantically labeled widgets',
        (WidgetTester tester) async {
179 180 181 182 183
      final SemanticsHandle semanticsHandle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
        Semantics(
          label: 'Add',
          button: true,
184
          child: const TextButton(
185
            onPressed: null,
186
            child: Text('+'),
187 188 189 190 191 192 193
          ),
        ),
      ));
      expect(find.bySemanticsLabel('Add'), findsOneWidget);
      semanticsHandle.dispose();
    });

194 195
    testWidgets('finds Semantically labeled widgets by RegExp',
        (WidgetTester tester) async {
196 197 198 199 200 201 202 203 204 205 206 207 208 209 210
      final SemanticsHandle semanticsHandle = tester.ensureSemantics();
      await tester.pumpWidget(_boilerplate(
        Semantics(
          container: true,
          child: Row(children: const <Widget>[
            Text('Hello'),
            Text('World'),
          ]),
        ),
      ));
      expect(find.bySemanticsLabel('Hello'), findsNothing);
      expect(find.bySemanticsLabel(RegExp(r'^Hello')), findsOneWidget);
      semanticsHandle.dispose();
    });

211 212
    testWidgets('finds Semantically labeled widgets without explicit Semantics',
        (WidgetTester tester) async {
213
      final SemanticsHandle semanticsHandle = tester.ensureSemantics();
214 215
      await tester
          .pumpWidget(_boilerplate(const SimpleCustomSemanticsWidget('Foo')));
216 217 218 219 220
      expect(find.bySemanticsLabel('Foo'), findsOneWidget);
      semanticsHandle.dispose();
    });
  });

221
  group('hitTestable', () {
222 223
    testWidgets('excludes non-hit-testable widgets',
        (WidgetTester tester) async {
224
      await tester.pumpWidget(
225
        _boilerplate(IndexedStack(
226 227
          sizing: StackFit.expand,
          children: <Widget>[
228
            GestureDetector(
229 230
              key: const ValueKey<int>(0),
              behavior: HitTestBehavior.opaque,
231
              onTap: () {},
232 233
              child: const SizedBox.expand(),
            ),
234
            GestureDetector(
235 236
              key: const ValueKey<int>(1),
              behavior: HitTestBehavior.opaque,
237
              onTap: () {},
238 239 240 241 242 243
              child: const SizedBox.expand(),
            ),
          ],
        )),
      );
      expect(find.byType(GestureDetector), findsNWidgets(2));
244
      final Finder hitTestable = find.byType(GestureDetector).hitTestable();
245 246 247 248
      expect(hitTestable, findsOneWidget);
      expect(tester.widget(hitTestable).key, const ValueKey<int>(0));
    });
  });
249 250

  testWidgets('ChainedFinders chain properly', (WidgetTester tester) async {
251
    final GlobalKey key1 = GlobalKey();
252
    await tester.pumpWidget(
253
      _boilerplate(Column(
254
        children: <Widget>[
255
          Container(
256 257 258
            key: key1,
            child: const Text('1'),
          ),
259
          const Text('2'),
260 261 262 263 264 265 266 267
        ],
      )),
    );

    // 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.
268 269 270 271 272 273 274 275 276
    final Text text = find
        .descendant(
          of: find.byKey(key1),
          matching: find.byType(Text),
        )
        .last
        .evaluate()
        .single
        .widget as Text;
277 278 279

    expect(text.data, '1');
  });
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313

  testWidgets('finds multiple subtypes', (WidgetTester tester) async {
    await tester.pumpWidget(_boilerplate(
      Row(children: <Widget>[
        Column(children: const <Widget>[
          Text('Hello'),
          Text('World'),
        ]),
        Column(children: <Widget>[
          Image(image: FileImage(File('test'))),
        ]),
        Column(children: const <Widget>[
          SimpleGenericWidget<int>(child: Text('one')),
          SimpleGenericWidget<double>(child: Text('pi')),
          SimpleGenericWidget<String>(child: Text('two')),
        ]),
      ]),
    ));

    expect(find.bySubtype<Row>(), findsOneWidget);
    expect(find.bySubtype<Column>(), findsNWidgets(3));
    // Finds both rows and columns.
    expect(find.bySubtype<Flex>(), findsNWidgets(4));

    // Finds only the requested generic subtypes.
    expect(find.bySubtype<SimpleGenericWidget<int>>(), findsOneWidget);
    expect(find.bySubtype<SimpleGenericWidget<num>>(), findsNWidgets(2));
    expect(find.bySubtype<SimpleGenericWidget<Object>>(), findsNWidgets(3));

    // Finds all widgets.
    final int totalWidgetCount =
        find.byWidgetPredicate((_) => true).evaluate().length;
    expect(find.bySubtype<Widget>(), findsNWidgets(totalWidgetCount));
  });
314 315 316
}

Widget _boilerplate(Widget child) {
317
  return Directionality(
318 319 320 321
    textDirection: TextDirection.ltr,
    child: child,
  );
}
322 323

class SimpleCustomSemanticsWidget extends LeafRenderObjectWidget {
324
  const SimpleCustomSemanticsWidget(this.label, {Key? key}) : super(key: key);
325 326 327 328

  final String label;

  @override
329 330
  RenderObject createRenderObject(BuildContext context) =>
      SimpleCustomSemanticsRenderObject(label);
331 332 333 334 335 336 337 338 339 340
}

class SimpleCustomSemanticsRenderObject extends RenderBox {
  SimpleCustomSemanticsRenderObject(this.label);

  final String label;

  @override
  bool get sizedByParent => true;

341 342 343 344 345
  @override
  Size computeDryLayout(BoxConstraints constraints) {
    return constraints.smallest;
  }

346 347 348
  @override
  void describeSemanticsConfiguration(SemanticsConfiguration config) {
    super.describeSemanticsConfiguration(config);
349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364
    config
      ..label = label
      ..textDirection = TextDirection.ltr;
  }
}

class SimpleGenericWidget<T> extends StatelessWidget {
  const SimpleGenericWidget({required Widget child, Key? key})
      : _child = child,
        super(key: key);

  final Widget _child;

  @override
  Widget build(BuildContext context) {
    return _child;
365
  }
366
}