extension_test.dart 45.3 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 7 8 9
// TODO(gspencergoog): Remove this tag once this test's state leaks/test
// dependencies have been fixed.
// https://github.com/flutter/flutter/issues/85160
// Fails with "flutter test --test-randomize-ordering-seed=20210721"
@Tags(<String>['no-shuffle'])
10
library;
11

12
import 'package:flutter/foundation.dart';
13
import 'package:flutter/material.dart';
14
import 'package:flutter/rendering.dart';
15
import 'package:flutter/scheduler.dart';
16
import 'package:flutter/services.dart';
17
import 'package:flutter_driver/flutter_driver.dart';
18
import 'package:flutter_driver/src/extension/extension.dart';
19 20
import 'package:flutter_test/flutter_test.dart';

21 22
import 'stubs/stub_command.dart';
import 'stubs/stub_command_extension.dart';
23 24 25
import 'stubs/stub_finder.dart';
import 'stubs/stub_finder_extension.dart';

26 27 28 29 30 31 32 33 34 35
Future<void> silenceDriverLogger(AsyncCallback callback) async {
  final DriverLogCallback oldLogger = driverLog;
  driverLog = (String source, String message) { };
  try {
    await callback();
  } finally {
    driverLog = oldLogger;
  }
}

36 37
void main() {
  group('waitUntilNoTransientCallbacks', () {
38 39
    late FlutterDriverExtension driverExtension;
    Map<String, dynamic>? result;
40
    int messageId = 0;
41
    final List<String?> log = <String?>[];
42 43 44

    setUp(() {
      result = null;
45
      driverExtension = FlutterDriverExtension((String? message) async { log.add(message); return (messageId += 1).toString(); }, false, true);
46 47 48
    });

    testWidgets('returns immediately when transient callback queue is empty', (WidgetTester tester) async {
49
      driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize())
50 51 52
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));
53 54 55

      await tester.idle();
      expect(
56 57 58
        result,
        <String, dynamic>{
          'isError': false,
59
          'response': <String, dynamic>{},
60
        },
61 62 63 64
      );
    });

    testWidgets('waits until no transient callbacks', (WidgetTester tester) async {
65
      SchedulerBinding.instance.scheduleFrameCallback((_) {
66 67 68
        // Intentionally blank. We only care about existence of a callback.
      });

69
      driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize())
70 71 72
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));
73 74 75 76 77 78 79 80

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(
81 82 83
        result,
        <String, dynamic>{
          'isError': false,
84
          'response': <String, dynamic>{},
85
        },
86 87
      );
    });
88 89 90

    testWidgets('handler', (WidgetTester tester) async {
      expect(log, isEmpty);
91
      final Map<String, dynamic> response = await driverExtension.call(const RequestData('hello').serialize());
92
      final RequestDataResult result = RequestDataResult.fromJson(response['response'] as Map<String, dynamic>);
93 94 95
      expect(log, <String>['hello']);
      expect(result.message, '1');
    });
96
  });
97

98
  group('waitForCondition', () {
99 100
    late FlutterDriverExtension driverExtension;
    Map<String, dynamic>? result;
101
    int messageId = 0;
102
    final List<String?> log = <String?>[];
103 104 105

    setUp(() {
      result = null;
106
      driverExtension = FlutterDriverExtension((String? message) async { log.add(message); return (messageId += 1).toString(); }, false, true);
107 108 109
    });

    testWidgets('waiting for NoTransientCallbacks returns immediately when transient callback queue is empty', (WidgetTester tester) async {
110
      driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize())
111 112 113 114 115 116 117 118 119
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      await tester.idle();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
120
          'response': <String, dynamic>{},
121 122 123 124 125
        },
      );
    });

    testWidgets('waiting for NoTransientCallbacks returns until no transient callbacks', (WidgetTester tester) async {
126
      SchedulerBinding.instance.scheduleFrameCallback((_) {
127 128 129
        // Intentionally blank. We only care about existence of a callback.
      });

130
      driverExtension.call(const WaitForCondition(NoTransientCallbacks()).serialize())
131 132 133 134 135 136 137 138 139 140 141 142 143 144
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
145
          'response': <String, dynamic>{},
146 147 148 149 150 151
        },
      );
    });

    testWidgets('waiting for NoPendingFrame returns immediately when frame is synced', (
        WidgetTester tester) async {
152
      driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize())
153 154 155 156 157 158 159 160 161
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      await tester.idle();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
162
          'response': <String, dynamic>{},
163 164 165 166 167
        },
      );
    });

    testWidgets('waiting for NoPendingFrame returns until no pending scheduled frame', (WidgetTester tester) async {
168
      SchedulerBinding.instance.scheduleFrame();
169

170
      driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize())
171 172 173 174 175 176 177 178 179 180 181 182 183 184
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
185
          'response': <String, dynamic>{},
186 187 188 189 190 191 192 193
        },
      );
    });

    testWidgets(
        'waiting for combined conditions returns immediately', (WidgetTester tester) async {
      const SerializableWaitCondition combinedCondition =
          CombinedCondition(<SerializableWaitCondition>[NoTransientCallbacks(), NoPendingFrame()]);
194
      driverExtension.call(const WaitForCondition(combinedCondition).serialize())
195 196 197 198 199 200 201 202 203
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      await tester.idle();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
204
          'response': <String, dynamic>{},
205 206 207 208 209 210
        },
      );
    });

    testWidgets(
        'waiting for combined conditions returns until no transient callbacks', (WidgetTester tester) async {
211 212
      SchedulerBinding.instance.scheduleFrame();
      SchedulerBinding.instance.scheduleFrameCallback((_) {
213 214 215 216 217
        // Intentionally blank. We only care about existence of a callback.
      });

      const SerializableWaitCondition combinedCondition =
          CombinedCondition(<SerializableWaitCondition>[NoTransientCallbacks(), NoPendingFrame()]);
218
      driverExtension.call(const WaitForCondition(combinedCondition).serialize())
219 220 221 222 223 224 225 226 227 228 229 230 231 232
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
233
          'response': <String, dynamic>{},
234 235 236 237 238 239
        },
      );
    });

    testWidgets(
        'waiting for combined conditions returns until no pending scheduled frame', (WidgetTester tester) async {
240 241
      SchedulerBinding.instance.scheduleFrame();
      SchedulerBinding.instance.scheduleFrameCallback((_) {
242 243 244 245 246
        // Intentionally blank. We only care about existence of a callback.
      });

      const SerializableWaitCondition combinedCondition =
          CombinedCondition(<SerializableWaitCondition>[NoPendingFrame(), NoTransientCallbacks()]);
247
      driverExtension.call(const WaitForCondition(combinedCondition).serialize())
248 249 250 251 252 253 254 255 256 257 258 259 260 261
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
262
          'response': <String, dynamic>{},
263 264 265
        },
      );
    });
266 267

    testWidgets(
268
        'waiting for NoPendingPlatformMessages returns immediately when there are no platform messages', (WidgetTester tester) async {
269
      driverExtension
270 271 272 273 274 275 276 277 278 279
          .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      await tester.idle();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
280
          'response': <String, dynamic>{},
281 282 283 284 285 286 287 288
        },
      );
    });

    testWidgets(
        'waiting for NoPendingPlatformMessages returns until a single method channel call returns', (WidgetTester tester) async {
      const MethodChannel channel = MethodChannel('helloChannel', JSONMethodCodec());
      const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
289
      tester.binding.defaultBinaryMessenger.setMockMessageHandler(
290
          'helloChannel', (ByteData? message) {
291 292
            return Future<ByteData>.delayed(
                const Duration(milliseconds: 10),
293
                () => jsonMessage.encodeMessage(<dynamic>['hello world'])!);
294 295 296
          });
      channel.invokeMethod<String>('sayHello', 'hello');

297
      driverExtension
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
          .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // The channel message are delayed for 10 milliseconds, so nothing happens yet.
      await tester.pump(const Duration(milliseconds: 5));
      expect(result, isNull);

      // Now we receive the result.
      await tester.pump(const Duration(milliseconds: 5));
      expect(
        result,
        <String, dynamic>{
          'isError': false,
313
          'response': <String, dynamic>{},
314 315 316 317 318 319 320 321 322
        },
      );
    });

    testWidgets(
        'waiting for NoPendingPlatformMessages returns until both method channel calls return', (WidgetTester tester) async {
      const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
      // Configures channel 1
      const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
323
      tester.binding.defaultBinaryMessenger.setMockMessageHandler(
324
          'helloChannel1', (ByteData? message) {
325 326
            return Future<ByteData>.delayed(
                const Duration(milliseconds: 10),
327
                () => jsonMessage.encodeMessage(<dynamic>['hello world'])!);
328 329 330 331
          });

      // Configures channel 2
      const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
332
      tester.binding.defaultBinaryMessenger.setMockMessageHandler(
333
          'helloChannel2', (ByteData? message) {
334 335
            return Future<ByteData>.delayed(
                const Duration(milliseconds: 20),
336
                () => jsonMessage.encodeMessage(<dynamic>['hello world'])!);
337 338 339 340 341
          });

      channel1.invokeMethod<String>('sayHello', 'hello');
      channel2.invokeMethod<String>('sayHello', 'hello');

342
      driverExtension
343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361
          .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // Neither of the channel responses is received, so nothing happens yet.
      await tester.pump(const Duration(milliseconds: 5));
      expect(result, isNull);

      // Result of channel 1 is received, but channel 2 is still pending, so still waiting.
      await tester.pump(const Duration(milliseconds: 10));
      expect(result, isNull);

      // Both of the results are received. Now we receive the result.
      await tester.pump(const Duration(milliseconds: 30));
      expect(
        result,
        <String, dynamic>{
          'isError': false,
362
          'response': <String, dynamic>{},
363 364 365 366 367 368 369 370 371
        },
      );
    });

    testWidgets(
        'waiting for NoPendingPlatformMessages returns until new method channel call returns', (WidgetTester tester) async {
      const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
      // Configures channel 1
      const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
372
      tester.binding.defaultBinaryMessenger.setMockMessageHandler(
373
          'helloChannel1', (ByteData? message) {
374 375
            return Future<ByteData>.delayed(
                const Duration(milliseconds: 10),
376
                () => jsonMessage.encodeMessage(<dynamic>['hello world'])!);
377 378 379 380
          });

      // Configures channel 2
      const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
381
      tester.binding.defaultBinaryMessenger.setMockMessageHandler(
382
          'helloChannel2', (ByteData? message) {
383 384
            return Future<ByteData>.delayed(
                const Duration(milliseconds: 20),
385
                () => jsonMessage.encodeMessage(<dynamic>['hello world'])!);
386 387 388 389 390
          });

      channel1.invokeMethod<String>('sayHello', 'hello');

      // Calls the waiting API before the second channel message is sent.
391
      driverExtension
392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412
          .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // The first channel message is not received, so nothing happens yet.
      await tester.pump(const Duration(milliseconds: 5));
      expect(result, isNull);

      channel2.invokeMethod<String>('sayHello', 'hello');

      // Result of channel 1 is received, but channel 2 is still pending, so still waiting.
      await tester.pump(const Duration(milliseconds: 15));
      expect(result, isNull);

      // Both of the results are received. Now we receive the result.
      await tester.pump(const Duration(milliseconds: 10));
      expect(
        result,
        <String, dynamic>{
          'isError': false,
413
          'response': <String, dynamic>{},
414 415 416 417 418 419 420 421 422
        },
      );
    });

    testWidgets(
        'waiting for NoPendingPlatformMessages returns until both old and new method channel calls return', (WidgetTester tester) async {
      const MessageCodec<dynamic> jsonMessage = JSONMessageCodec();
      // Configures channel 1
      const MethodChannel channel1 = MethodChannel('helloChannel1', JSONMethodCodec());
423
      tester.binding.defaultBinaryMessenger.setMockMessageHandler(
424
          'helloChannel1', (ByteData? message) {
425 426
            return Future<ByteData>.delayed(
                const Duration(milliseconds: 20),
427
                () => jsonMessage.encodeMessage(<dynamic>['hello world'])!);
428 429 430 431
          });

      // Configures channel 2
      const MethodChannel channel2 = MethodChannel('helloChannel2', JSONMethodCodec());
432
      tester.binding.defaultBinaryMessenger.setMockMessageHandler(
433
          'helloChannel2', (ByteData? message) {
434 435
            return Future<ByteData>.delayed(
                const Duration(milliseconds: 10),
436
                () => jsonMessage.encodeMessage(<dynamic>['hello world'])!);
437 438 439 440
          });

      channel1.invokeMethod<String>('sayHello', 'hello');

441
      driverExtension
442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462
          .call(const WaitForCondition(NoPendingPlatformMessages()).serialize())
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // The first channel message is not received, so nothing happens yet.
      await tester.pump(const Duration(milliseconds: 5));
      expect(result, isNull);

      channel2.invokeMethod<String>('sayHello', 'hello');

      // Result of channel 2 is received, but channel 1 is still pending, so still waiting.
      await tester.pump(const Duration(milliseconds: 10));
      expect(result, isNull);

      // Now we receive the result.
      await tester.pump(const Duration(milliseconds: 5));
      expect(
        result,
        <String, dynamic>{
          'isError': false,
463
          'response': <String, dynamic>{},
464 465 466
        },
      );
    });
467 468
  });

469
  group('getSemanticsId', () {
470
    late FlutterDriverExtension driverExtension;
471
    setUp(() {
472
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
473 474 475
    });

    testWidgets('works when semantics are enabled', (WidgetTester tester) async {
476
      final SemanticsHandle semantics = tester.ensureSemantics();
477 478
      await tester.pumpWidget(
        const Text('hello', textDirection: TextDirection.ltr));
479

480
      final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize();
481
      final Map<String, dynamic> response = await driverExtension.call(arguments);
482
      final GetSemanticsIdResult result = GetSemanticsIdResult.fromJson(response['response'] as Map<String, dynamic>);
483

484 485 486 487 488 489 490
      expect(result.id, 1);
      semantics.dispose();
    });

    testWidgets('throws state error if no data is found', (WidgetTester tester) async {
      await tester.pumpWidget(
        const Text('hello', textDirection: TextDirection.ltr));
491

492
      final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize();
493
      final Map<String, dynamic> response = await driverExtension.call(arguments);
494

495 496
      expect(response['isError'], true);
      expect(response['response'], contains('Bad state: No semantics data found'));
497
    }, semanticsEnabled: false);
498 499

    testWidgets('throws state error multiple matches are found', (WidgetTester tester) async {
500
      final SemanticsHandle semantics = tester.ensureSemantics();
501
      await tester.pumpWidget(
502
        Directionality(
503
          textDirection: TextDirection.ltr,
504
          child: ListView(children: const <Widget>[
505 506
            SizedBox(width: 100.0, height: 100.0, child: Text('hello')),
            SizedBox(width: 100.0, height: 100.0, child: Text('hello')),
507 508 509
          ]),
        ),
      );
510

511
      final Map<String, String> arguments = GetSemanticsId(const ByText('hello')).serialize();
512
      final Map<String, dynamic> response = await driverExtension.call(arguments);
513

514
      expect(response['isError'], true);
515
      expect(response['response'], contains('Bad state: Found more than one element with the same ID'));
516 517 518
      semantics.dispose();
    });
  });
519 520

  testWidgets('getOffset', (WidgetTester tester) async {
521
    final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
522 523

    Future<Offset> getOffset(OffsetType offset) async {
524
      final Map<String, String> arguments = GetOffset(ByValueKey(1), offset).serialize();
525
      final Map<String, dynamic> response = await driverExtension.call(arguments);
526
      final GetOffsetResult result = GetOffsetResult.fromJson(response['response'] as Map<String, dynamic>);
527 528 529 530 531 532 533 534
      return Offset(result.dx, result.dy);
    }

    await tester.pumpWidget(
      Align(
        alignment: Alignment.topLeft,
        child: Transform.translate(
          offset: const Offset(40, 30),
535 536
          child: const SizedBox(
            key: ValueKey<int>(1),
537 538 539 540 541 542 543 544 545 546 547 548 549
            width: 100,
            height: 120,
          ),
        ),
      ),
    );

    expect(await getOffset(OffsetType.topLeft), const Offset(40, 30));
    expect(await getOffset(OffsetType.topRight), const Offset(40 + 100.0, 30));
    expect(await getOffset(OffsetType.bottomLeft), const Offset(40, 30 + 120.0));
    expect(await getOffset(OffsetType.bottomRight), const Offset(40 + 100.0, 30 + 120.0));
    expect(await getOffset(OffsetType.center), const Offset(40 + (100 / 2), 30 + (120 / 2)));
  });
550

551 552
  testWidgets('getText', (WidgetTester tester) async {
    await silenceDriverLogger(() async {
553
      final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
554

555
      Future<String?> getTextInternal(SerializableFinder search) async {
556
        final Map<String, String> arguments = GetText(search, timeout: const Duration(seconds: 1)).serialize();
557
        final Map<String, dynamic> result = await driverExtension.call(arguments);
558 559 560 561 562 563 564 565 566 567 568 569
        if (result['isError'] as bool) {
          return null;
        }
        return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text;
      }

      await tester.pumpWidget(
          MaterialApp(
              home: Scaffold(body:Column(
                key: const ValueKey<String>('column'),
                children: <Widget>[
                  const Text('Hello1', key: ValueKey<String>('text1')),
570
                  SizedBox(
Dan Field's avatar
Dan Field committed
571 572 573 574 575
                    height: 25.0,
                    child: RichText(
                      key: const ValueKey<String>('text2'),
                      text: const TextSpan(text: 'Hello2'),
                    ),
576
                  ),
577
                  SizedBox(
578
                    height: 25.0,
Dan Field's avatar
Dan Field committed
579 580 581 582 583 584 585 586
                    child: EditableText(
                      key: const ValueKey<String>('text3'),
                      controller: TextEditingController(text: 'Hello3'),
                      focusNode: FocusNode(),
                      style: const TextStyle(),
                      cursorColor: Colors.red,
                      backgroundCursorColor: Colors.black,
                    ),
587
                  ),
588
                  SizedBox(
589
                    height: 25.0,
Dan Field's avatar
Dan Field committed
590 591 592 593
                    child: TextField(
                      key: const ValueKey<String>('text4'),
                      controller: TextEditingController(text: 'Hello4'),
                    ),
594
                  ),
595
                  SizedBox(
596
                    height: 25.0,
Dan Field's avatar
Dan Field committed
597 598 599 600
                    child: TextFormField(
                      key: const ValueKey<String>('text5'),
                      controller: TextEditingController(text: 'Hello5'),
                    ),
601
                  ),
602
                  SizedBox(
603 604 605 606 607 608 609 610 611 612 613
                    height: 25.0,
                    child: RichText(
                      key: const ValueKey<String>('text6'),
                      text: const TextSpan(children: <TextSpan>[
                        TextSpan(text: 'Hello'),
                        TextSpan(text: ', '),
                        TextSpan(text: 'World'),
                        TextSpan(text: '!'),
                      ]),
                    ),
                  ),
614 615 616 617 618 619 620 621 622 623
                ],
              ))
          )
      );

      expect(await getTextInternal(ByValueKey('text1')), 'Hello1');
      expect(await getTextInternal(ByValueKey('text2')), 'Hello2');
      expect(await getTextInternal(ByValueKey('text3')), 'Hello3');
      expect(await getTextInternal(ByValueKey('text4')), 'Hello4');
      expect(await getTextInternal(ByValueKey('text5')), 'Hello5');
624
      expect(await getTextInternal(ByValueKey('text6')), 'Hello, World!');
625 626 627

      // Check if error thrown for other types
      final Map<String, String> arguments = GetText(ByValueKey('column'), timeout: const Duration(seconds: 1)).serialize();
628
      final Map<String, dynamic> response = await driverExtension.call(arguments);
629 630 631 632 633
      expect(response['isError'], true);
      expect(response['response'], contains('is currently not supported by getText'));
    });
  });

634
  testWidgets('descendant finder', (WidgetTester tester) async {
635
    await silenceDriverLogger(() async {
636
      final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
637

638
      Future<String?> getDescendantText({ String? of, bool matchRoot = false}) async {
639
        final Map<String, String> arguments = GetText(Descendant(
640 641 642 643
          of: ByValueKey(of),
          matching: ByValueKey('text2'),
          matchRoot: matchRoot,
        ), timeout: const Duration(seconds: 1)).serialize();
644
        final Map<String, dynamic> result = await driverExtension.call(arguments);
645
        if (result['isError'] as bool) {
646 647
          return null;
        }
648
        return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text;
649 650
      }

651
      await tester.pumpWidget(
652
          const MaterialApp(
653
              home: Column(
654 655
                key: ValueKey<String>('column'),
                children: <Widget>[
656 657 658 659 660 661 662
                  Text('Hello1', key: ValueKey<String>('text1')),
                  Text('Hello2', key: ValueKey<String>('text2')),
                  Text('Hello3', key: ValueKey<String>('text3')),
                ],
              )
          )
      );
663

664 665 666
      expect(await getDescendantText(of: 'column'), 'Hello2');
      expect(await getDescendantText(of: 'column', matchRoot: true), 'Hello2');
      expect(await getDescendantText(of: 'text2', matchRoot: true), 'Hello2');
667

668
      // Find nothing
669
      Future<String?> result = getDescendantText(of: 'text1', matchRoot: true);
670 671
      await tester.pump(const Duration(seconds: 2));
      expect(await result, null);
672

673 674 675 676
      result = getDescendantText(of: 'text2');
      await tester.pump(const Duration(seconds: 2));
      expect(await result, null);
    });
677 678
  });

679
  testWidgets('descendant finder firstMatchOnly', (WidgetTester tester) async {
680
    await silenceDriverLogger(() async {
681
      final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
682

683
      Future<String?> getDescendantText() async {
684
        final Map<String, String> arguments = GetText(Descendant(
685 686 687 688
          of: ByValueKey('column'),
          matching: const ByType('Text'),
          firstMatchOnly: true,
        ), timeout: const Duration(seconds: 1)).serialize();
689
        final Map<String, dynamic> result = await driverExtension.call(arguments);
690
        if (result['isError'] as bool) {
691 692
          return null;
        }
693
        return GetTextResult.fromJson(result['response'] as Map<String, dynamic>).text;
694 695
      }

696
      await tester.pumpWidget(
697
        const MaterialApp(
698
          home: Column(
699 700
            key: ValueKey<String>('column'),
            children: <Widget>[
701 702 703 704 705
              Text('Hello1', key: ValueKey<String>('text1')),
              Text('Hello2', key: ValueKey<String>('text2')),
              Text('Hello3', key: ValueKey<String>('text3')),
            ],
          ),
706
        ),
707
      );
708

709 710
      expect(await getDescendantText(), 'Hello1');
    });
711 712
  });

713
  testWidgets('ancestor finder', (WidgetTester tester) async {
714
    await silenceDriverLogger(() async {
715
      final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
716

717
      Future<Offset?> getAncestorTopLeft({ String? of, String? matching, bool matchRoot = false}) async {
718
        final Map<String, String> arguments = GetOffset(Ancestor(
719 720 721 722
          of: ByValueKey(of),
          matching: ByValueKey(matching),
          matchRoot: matchRoot,
        ), OffsetType.topLeft, timeout: const Duration(seconds: 1)).serialize();
723
        final Map<String, dynamic> response = await driverExtension.call(arguments);
724
        if (response['isError'] as bool) {
725 726
          return null;
        }
727
        final GetOffsetResult result = GetOffsetResult.fromJson(response['response'] as Map<String, dynamic>);
728
        return Offset(result.dx, result.dy);
729 730
      }

731
      await tester.pumpWidget(
732
          const MaterialApp(
733
            home: Center(
734
                child: SizedBox(
735
                  key: ValueKey<String>('parent'),
736 737 738 739
                  height: 100,
                  width: 100,
                  child: Center(
                    child: Row(
740
                      children: <Widget>[
741 742
                        SizedBox(
                          key: ValueKey<String>('leftchild'),
743 744 745
                          width: 25,
                          height: 25,
                        ),
746 747
                        SizedBox(
                          key: ValueKey<String>('righttchild'),
748 749 750 751 752
                          width: 25,
                          height: 25,
                        ),
                      ],
                    ),
753
                  ),
754 755 756 757
                )
            ),
          )
      );
758

759 760 761 762 763 764 765 766 767 768 769 770
      expect(
        await getAncestorTopLeft(of: 'leftchild', matching: 'parent'),
        const Offset((800 - 100) / 2, (600 - 100) / 2),
      );
      expect(
        await getAncestorTopLeft(of: 'leftchild', matching: 'parent', matchRoot: true),
        const Offset((800 - 100) / 2, (600 - 100) / 2),
      );
      expect(
        await getAncestorTopLeft(of: 'parent', matching: 'parent', matchRoot: true),
        const Offset((800 - 100) / 2, (600 - 100) / 2),
      );
771

772
      // Find nothing
773
      Future<Offset?> result = getAncestorTopLeft(of: 'leftchild', matching: 'leftchild');
774 775
      await tester.pump(const Duration(seconds: 2));
      expect(await result, null);
776

777 778 779 780
      result = getAncestorTopLeft(of: 'leftchild', matching: 'righttchild');
      await tester.pump(const Duration(seconds: 2));
      expect(await result, null);
    });
781
  });
782

783
  testWidgets('ancestor finder firstMatchOnly', (WidgetTester tester) async {
784
    await silenceDriverLogger(() async {
785
      final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
786

787
      Future<Offset?> getAncestorTopLeft() async {
788
        final Map<String, String> arguments = GetOffset(Ancestor(
789
          of: ByValueKey('leaf'),
790
          matching: const ByType('SizedBox'),
791 792
          firstMatchOnly: true,
        ), OffsetType.topLeft, timeout: const Duration(seconds: 1)).serialize();
793
        final Map<String, dynamic> response = await driverExtension.call(arguments);
794
        if (response['isError'] as bool) {
795 796
          return null;
        }
797
        final GetOffsetResult result = GetOffsetResult.fromJson(response['response'] as Map<String, dynamic>);
798
        return Offset(result.dx, result.dy);
799 800
      }

801
      await tester.pumpWidget(
802
        const MaterialApp(
803
          home: Center(
804
            child: SizedBox(
805 806 807
              height: 200,
              width: 200,
              child: Center(
808
                child: SizedBox(
809 810 811
                  height: 100,
                  width: 100,
                  child: Center(
812 813
                    child: SizedBox(
                      key: ValueKey<String>('leaf'),
814 815 816
                      height: 50,
                      width: 50,
                    ),
817 818 819 820 821 822
                  ),
                ),
              ),
            ),
          ),
        ),
823
      );
824

825 826 827 828 829
      expect(
        await getAncestorTopLeft(),
        const Offset((800 - 100) / 2, (600 - 100) / 2),
      );
    });
830 831
  });

832
  testWidgets('GetDiagnosticsTree', (WidgetTester tester) async {
833
    final FlutterDriverExtension driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
834

835
    Future<Map<String, dynamic>> getDiagnosticsTree(DiagnosticsType type, SerializableFinder finder, { int depth = 0, bool properties = true }) async {
836
      final Map<String, String> arguments = GetDiagnosticsTree(finder, type, subtreeDepth: depth, includeProperties: properties).serialize();
837
      final Map<String, dynamic> response = await driverExtension.call(arguments);
838
      final DiagnosticsTreeResult result = DiagnosticsTreeResult(response['response'] as Map<String, dynamic>);
839 840 841 842
      return result.json;
    }

    await tester.pumpWidget(
843
      const Directionality(
844 845
        textDirection: TextDirection.ltr,
        child: Center(
846
            child: Text('Hello World', key: ValueKey<String>('Text'))
847 848 849 850 851
        ),
      ),
    );

    // Widget
852
    Map<String, dynamic> result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'));
853 854 855
    expect(result['children'], isNull); // depth: 0
    expect(result['widgetRuntimeType'], 'Text');

856 857
    List<Map<String, dynamic>> properties = (result['properties']! as List<Object>).cast<Map<String, dynamic>>();
    Map<String, dynamic> stringProperty = properties.singleWhere((Map<String, dynamic> property) => property['name'] == 'data');
858 859 860
    expect(stringProperty['description'], '"Hello World"');
    expect(stringProperty['propertyType'], 'String');

861
    result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'), properties: false);
862 863 864 865
    expect(result['widgetRuntimeType'], 'Text');
    expect(result['properties'], isNull); // properties: false

    result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'), depth: 1);
866
    List<Map<String, dynamic>> children = (result['children']! as List<Object>).cast<Map<String, dynamic>>();
867 868 869
    expect(children.single['children'], isNull);

    result = await getDiagnosticsTree(DiagnosticsType.widget, ByValueKey('Text'), depth: 100);
870
    children = (result['children']! as List<Object>).cast<Map<String, dynamic>>();
871 872 873
    expect(children.single['children'], isEmpty);

    // RenderObject
874
    result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'));
875 876 877 878
    expect(result['children'], isNull); // depth: 0
    expect(result['properties'], isNotNull);
    expect(result['description'], startsWith('RenderParagraph'));

879
    result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'), properties: false);
880 881 882 883
    expect(result['properties'], isNull); // properties: false
    expect(result['description'], startsWith('RenderParagraph'));

    result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'), depth: 1);
884 885
    children = (result['children']! as List<Object>).cast<Map<String, dynamic>>();
    final Map<String, dynamic> textSpan = children.single;
886
    expect(textSpan['description'], 'TextSpan');
887 888
    properties = (textSpan['properties']! as List<Object>).cast<Map<String, dynamic>>();
    stringProperty = properties.singleWhere((Map<String, dynamic> property) => property['name'] == 'text');
889 890 891 892 893
    expect(stringProperty['description'], '"Hello World"');
    expect(stringProperty['propertyType'], 'String');
    expect(children.single['children'], isNull);

    result = await getDiagnosticsTree(DiagnosticsType.renderObject, ByValueKey('Text'), depth: 100);
894
    children = (result['children']! as List<Object>).cast<Map<String, dynamic>>();
895 896
    expect(children.single['children'], isEmpty);
  });
897

898
  group('enableTextEntryEmulation', () {
899
    late FlutterDriverExtension driverExtension;
900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918

    Future<Map<String, dynamic>> enterText() async {
      final Map<String, String> arguments = const EnterText('foo').serialize();
      final Map<String, dynamic> result = await driverExtension.call(arguments);
      return result;
    }

    const Widget testWidget = MaterialApp(
      home: Material(
        child: Center(
          child: TextField(
            key: ValueKey<String>('foo'),
            autofocus: true,
          ),
        ),
      ),
    );

    testWidgets('enableTextEntryEmulation false', (WidgetTester tester) async {
919
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, false);
920 921 922 923 924 925 926 927

      await tester.pumpWidget(testWidget);

      final Map<String, dynamic> enterTextResult = await enterText();
      expect(enterTextResult['isError'], isTrue);
    });

    testWidgets('enableTextEntryEmulation true', (WidgetTester tester) async {
928
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
929 930 931 932 933 934 935 936

      await tester.pumpWidget(testWidget);

      final Map<String, dynamic> enterTextResult = await enterText();
      expect(enterTextResult['isError'], isFalse);
    });
  });

937 938 939 940 941 942 943 944 945
  group('extension finders', () {
    final Widget debugTree = Directionality(
      textDirection: TextDirection.ltr,
      child: Center(
        child: Column(
          key: const ValueKey<String>('Column'),
          children: <Widget>[
            const Text('Foo', key: ValueKey<String>('Text1')),
            const Text('Bar', key: ValueKey<String>('Text2')),
946
            TextButton(
947 948
              key: const ValueKey<String>('Button'),
              onPressed: () {},
949
              child: const Text('Whatever'),
950 951 952 953 954 955 956 957
            ),
          ],
        ),
      ),
    );

    testWidgets('unknown extension finder', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
958
        (String? arg) async => '',
959
        true,
960
        true,
961 962 963 964 965
        finders: <FinderExtension>[],
      );

      Future<Map<String, dynamic>> getText(SerializableFinder finder) async {
        final Map<String, String> arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize();
966
        return driverExtension.call(arguments);
967 968 969 970 971 972 973
      }

      await tester.pumpWidget(debugTree);

      final Map<String, dynamic> result = await getText(StubFinder('Text1'));
      expect(result['isError'], true);
      expect(result['response'] is String, true);
974
      expect(result['response'] as String?, contains('Unsupported search specification type Stub'));
975 976 977 978
    });

    testWidgets('simple extension finder', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
979
        (String? arg) async => '',
980
        true,
981
        true,
982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000
        finders: <FinderExtension>[
          StubFinderExtension(),
        ],
      );

      Future<GetTextResult> getText(SerializableFinder finder) async {
        final Map<String, String> arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        return GetTextResult.fromJson(response['response'] as Map<String, dynamic>);
      }

      await tester.pumpWidget(debugTree);

      final GetTextResult result = await getText(StubFinder('Text1'));
      expect(result.text, 'Foo');
    });

    testWidgets('complex extension finder', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
1001
        (String? arg) async => '',
1002
        true,
1003
        true,
1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022
        finders: <FinderExtension>[
          StubFinderExtension(),
        ],
      );

      Future<GetTextResult> getText(SerializableFinder finder) async {
        final Map<String, String> arguments = GetText(finder, timeout: const Duration(seconds: 1)).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        return GetTextResult.fromJson(response['response'] as Map<String, dynamic>);
      }

      await tester.pumpWidget(debugTree);

      final GetTextResult result = await getText(Descendant(of: StubFinder('Column'), matching: StubFinder('Text1')));
      expect(result.text, 'Foo');
    });

    testWidgets('extension finder with command', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
1023
        (String? arg) async => '',
1024
        true,
1025
        true,
1026 1027 1028 1029 1030 1031 1032
        finders: <FinderExtension>[
          StubFinderExtension(),
        ],
      );

      Future<Map<String, dynamic>> tap(SerializableFinder finder) async {
        final Map<String, String> arguments = Tap(finder, timeout: const Duration(seconds: 1)).serialize();
1033
        return driverExtension.call(arguments);
1034 1035 1036 1037 1038 1039 1040 1041
      }

      await tester.pumpWidget(debugTree);

      final Map<String, dynamic> result = await tap(StubFinder('Button'));
      expect(result['isError'], false);
    });
  });
1042 1043 1044

  group('extension commands', () {
    int invokes = 0;
1045
    void stubCallback() => invokes++;
1046 1047 1048 1049 1050 1051

    final Widget debugTree = Directionality(
      textDirection: TextDirection.ltr,
      child: Center(
        child: Column(
          children: <Widget>[
1052
            TextButton(
1053 1054
              key: const ValueKey<String>('Button'),
              onPressed: stubCallback,
1055
              child: const Text('Whatever'),
1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067
            ),
          ],
        ),
      ),
    );

    setUp(() {
      invokes = 0;
    });

    testWidgets('unknown extension command', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
1068
        (String? arg) async => '',
1069
        true,
1070
        true,
1071 1072 1073 1074 1075
        commands: <CommandExtension>[],
      );

      Future<Map<String, dynamic>> invokeCommand(SerializableFinder finder, int times) async {
        final Map<String, String> arguments = StubNestedCommand(finder, times).serialize();
1076
        return driverExtension.call(arguments);
1077 1078 1079 1080 1081 1082 1083
      }

      await tester.pumpWidget(debugTree);

      final Map<String, dynamic> result = await invokeCommand(ByValueKey('Button'), 10);
      expect(result['isError'], true);
      expect(result['response'] is String, true);
1084
      expect(result['response'] as String?, contains('Unsupported command kind StubNestedCommand'));
1085 1086 1087 1088
    });

    testWidgets('nested command', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
1089
        (String? arg) async => '',
1090
        true,
1091
        true,
1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114
        commands: <CommandExtension>[
          StubNestedCommandExtension(),
        ],
      );

      Future<StubCommandResult> invokeCommand(SerializableFinder finder, int times) async {
        await driverExtension.call(const SetFrameSync(false).serialize()); // disable frame sync for test to avoid lock
        final Map<String, String> arguments = StubNestedCommand(finder, times, timeout: const Duration(seconds: 1)).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        final Map<String, dynamic> commandResponse = response['response'] as Map<String, dynamic>;
        return StubCommandResult(commandResponse['resultParam'] as String);
      }

      await tester.pumpWidget(debugTree);

      const int times = 10;
      final StubCommandResult result = await invokeCommand(ByValueKey('Button'), times);
      expect(result.resultParam, 'stub response');
      expect(invokes, times);
    });

    testWidgets('prober command', (WidgetTester tester) async {
      final FlutterDriverExtension driverExtension = FlutterDriverExtension(
1115
        (String? arg) async => '',
1116
        true,
1117
        true,
1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138
        commands: <CommandExtension>[
          StubProberCommandExtension(),
        ],
      );

      Future<StubCommandResult> invokeCommand(SerializableFinder finder, int times) async {
        await driverExtension.call(const SetFrameSync(false).serialize()); // disable frame sync for test to avoid lock
        final Map<String, String> arguments = StubProberCommand(finder, times, timeout: const Duration(seconds: 1)).serialize();
        final Map<String, dynamic> response = await driverExtension.call(arguments);
        final Map<String, dynamic> commandResponse = response['response'] as Map<String, dynamic>;
        return StubCommandResult(commandResponse['resultParam'] as String);
      }

      await tester.pumpWidget(debugTree);

      const int times = 10;
      final StubCommandResult result = await invokeCommand(ByValueKey('Button'), times);
      expect(result.resultParam, 'stub response');
      expect(invokes, times);
    });
  });
1139

1140 1141 1142 1143 1144 1145 1146 1147 1148 1149
  group('waitForTappable', () {
    late FlutterDriverExtension driverExtension;

    Future<Map<String, dynamic>> waitForTappable() async {
      final SerializableFinder finder = ByValueKey('widgetOne');
      final Map<String, String> arguments = WaitForTappable(finder).serialize();
      final Map<String, dynamic> result = await driverExtension.call(arguments);
      return result;
    }

1150
    const Widget testWidget = MaterialApp(
1151
      home: Material(
1152
        child: Column(children: <Widget> [
1153
          Text('Hello ', key: Key('widgetOne')),
1154
          SizedBox.shrink(
1155
            child: Text('World!', key: Key('widgetTwo')),
1156 1157
          ),
        ]),
1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171
      ),
    );

    testWidgets('returns true when widget is tappable', (
        WidgetTester tester) async {
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, false);

      await tester.pumpWidget(testWidget);

      final Map<String, dynamic> waitForTappableResult = await waitForTappable();
      expect(waitForTappableResult['isError'], isFalse);
    });
  });

1172
  group('waitUntilFrameSync', () {
1173 1174
    late FlutterDriverExtension driverExtension;
    Map<String, dynamic>? result;
1175 1176

    setUp(() {
1177
      driverExtension = FlutterDriverExtension((String? arg) async => '', true, true);
1178 1179 1180 1181 1182
      result = null;
    });

    testWidgets('returns immediately when frame is synced', (
        WidgetTester tester) async {
1183
      driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize())
1184 1185 1186 1187 1188 1189 1190 1191 1192
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      await tester.idle();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
1193
          'response': <String, dynamic>{},
1194 1195 1196 1197 1198 1199
        },
      );
    });

    testWidgets(
        'waits until no transient callbacks', (WidgetTester tester) async {
1200
      SchedulerBinding.instance.scheduleFrameCallback((_) {
1201 1202 1203
        // Intentionally blank. We only care about existence of a callback.
      });

1204
      driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize())
1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
1219
          'response': <String, dynamic>{},
1220 1221 1222 1223 1224 1225
        },
      );
    });

    testWidgets(
        'waits until no pending scheduled frame', (WidgetTester tester) async {
1226
      SchedulerBinding.instance.scheduleFrame();
1227

1228
      driverExtension.call(const WaitForCondition(NoPendingFrame()).serialize())
1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242
          .then<void>(expectAsync1((Map<String, dynamic> r) {
        result = r;
      }));

      // Nothing should happen until the next frame.
      await tester.idle();
      expect(result, isNull);

      // NOW we should receive the result.
      await tester.pump();
      expect(
        result,
        <String, dynamic>{
          'isError': false,
1243
          'response': <String, dynamic>{},
1244 1245 1246 1247
        },
      );
    });
  });
1248
}