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

import 'dart:async';
6
import 'dart:convert';
7

8 9
import 'package:flutter_driver/src/common/error.dart';
import 'package:flutter_driver/src/common/health.dart';
10
import 'package:flutter_driver/src/common/layer_tree.dart';
11
import 'package:flutter_driver/src/common/wait.dart';
12 13
import 'package:flutter_driver/src/driver/driver.dart';
import 'package:flutter_driver/src/driver/timeline.dart';
14
import 'package:fake_async/fake_async.dart';
15
import 'package:vm_service/vm_service.dart' as vms;
16

17
import '../../common.dart';
18

19
/// Magical timeout value that's different from the default.
20
const Duration _kTestTimeout = Duration(milliseconds: 1234);
21
const String _kSerializedTestTimeout = '1234';
22
const String _kWebScriptPrefix = r"window.$flutterDriver('";
23
const String _kWebScriptSuffix = "')";
24

25
void main() {
26 27 28 29 30
  final List<String> log = <String>[];
  driverLog = (String source, String message) {
    log.add('$source: $message');
  };

31
  group('VMServiceFlutterDriver.connect', () {
32 33 34
    late FakeVmService fakeClient;
    late FakeVM fakeVM;
    late FakeIsolate fakeIsolate;
35

36
    void expectLogContains(String message) {
37
      expect(log, anyElement(contains(message)));
38
    }
39

40
    setUp(() {
41
      log.clear();
42 43 44
      fakeIsolate = FakeIsolate();
      fakeVM = FakeVM(fakeIsolate);
      fakeClient = FakeVmService(fakeVM);
45
      vmServiceConnectFunction = (String url, Map<String, dynamic>? headers) async {
46
        return fakeClient;
47
      };
48
      fakeClient.responses['get_health'] = makeFakeResponse(<String, dynamic>{'status': 'ok'});
49 50 51
    });

    tearDown(() async {
yjbanov's avatar
yjbanov committed
52
      restoreVmServiceConnectFunction();
53 54
    });

55 56 57 58 59 60 61 62 63 64 65 66 67
    test('Retries while Dart VM service is not available', () async {
      // This test case will test the real implementation of `_waitAndConnect`.
      restoreVmServiceConnectFunction();

      // The actual behavior is to retry indefinitely until the Dart VM service
      // becomes available. `.timeout` is used here to exit the infinite loop,
      // expecting that no other types of error are thrown during the process.
      expect(
        vmServiceConnectFunction('http://foo.bar', <String, dynamic>{})
            .timeout(const Duration(seconds: 1)),
        throwsA(isA<TimeoutException>()),
      );
    });
68 69 70 71 72 73

    test('throws after retries if no isolate', () async {
      fakeVM.numberOfTriesBeforeResolvingIsolate = 10000;
      FakeAsync().run((FakeAsync time) {
        FlutterDriver.connect(dartVmServiceUrl: '');
        time.elapse(kUnusuallyLongTimeout);
74
      });
75 76 77 78 79 80 81 82 83 84 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
      expect(log, <String>[
        'VMServiceFlutterDriver: Connecting to Flutter application at ',
        'VMServiceFlutterDriver: The root isolate is taking an unuusally long time to start.',
      ]);
    });

    test('Retries connections if isolate is not available', () async {
      fakeIsolate.pauseEvent = vms.Event(kind: vms.EventKind.kPauseStart, timestamp: 0);
      fakeVM.numberOfTriesBeforeResolvingIsolate = 5;
      final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
      expect(driver, isNotNull);
      expect(
        fakeClient.connectionLog,
        <String>[
          'getIsolate',
          'setFlag pause_isolates_on_start false',
          'resume',
          'streamListen Isolate',
          'getIsolate',
          'onIsolateEvent',
          'streamCancel Isolate',
        ],
      );
    });

    test('Connects to isolate number', () async {
      fakeIsolate.pauseEvent = vms.Event(kind: vms.EventKind.kPauseStart, timestamp: 0);
      final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '', isolateNumber: int.parse(fakeIsolate.number));
      expect(driver, isNotNull);
      expect(
        fakeClient.connectionLog,
        <String>[
          'getIsolate',
          'setFlag pause_isolates_on_start false',
          'resume',
          'streamListen Isolate',
          'getIsolate',
          'onIsolateEvent',
          'streamCancel Isolate',
        ],
      );
    });

    test('connects to isolate paused at start', () async {
      fakeIsolate.pauseEvent = vms.Event(kind: vms.EventKind.kPauseStart, timestamp: 0);
120

121
      final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
122
      expect(driver, isNotNull);
123
      expectLogContains('Isolate is paused at start');
124 125 126 127 128 129 130 131 132 133 134 135
      expect(
        fakeClient.connectionLog,
        <String>[
          'getIsolate',
          'setFlag pause_isolates_on_start false',
          'resume',
          'streamListen Isolate',
          'getIsolate',
          'onIsolateEvent',
          'streamCancel Isolate',
        ],
      );
136 137
    });

138
    test('ignores setFlag failure', () async {
139 140
      fakeIsolate.pauseEvent = vms.Event(kind: vms.EventKind.kPauseStart, timestamp: 0);
      fakeClient.failOnSetFlag = true;
141 142 143 144 145 146 147 148

      final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
      expectLogContains('Failed to set pause_isolates_on_start=false, proceeding. '
                        'Error: Exception: setFlag failed');
      expect(driver, isNotNull);
    });


149
    test('connects to isolate paused mid-flight', () async {
150
      fakeIsolate.pauseEvent = vms.Event(kind: vms.EventKind.kPauseBreakpoint, timestamp: 0);
151

152
      final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
153
      expect(driver, isNotNull);
154
      expectLogContains('Isolate is paused mid-flight');
155 156 157 158 159 160 161
    });

    // This test simulates a situation when we believe that the isolate is
    // currently paused, but something else (e.g. a debugger) resumes it before
    // we do. There's no need to fail as we should be able to drive the app
    // just fine.
    test('connects despite losing the race to resume isolate', () async {
162 163
      fakeIsolate.pauseEvent = vms.Event(kind: vms.EventKind.kPauseBreakpoint, timestamp: 0);
      fakeClient.failOnResumeWith101 = true;
164

165
      final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
166 167 168 169 170
      expect(driver, isNotNull);
      expectLogContains('Attempted to resume an already resumed isolate');
    });

    test('connects to unpaused isolate', () async {
171 172
      fakeIsolate.pauseEvent = vms.Event(kind: vms.EventKind.kResume, timestamp: 0);

173
      final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
174 175 176
      expect(driver, isNotNull);
      expectLogContains('Isolate is not paused. Assuming application is ready.');
    });
177

178 179
    test('connects to unpaused when onExtensionAdded does not contain the '
      'driver extension', () async {
180 181 182
      fakeIsolate.pauseEvent = vms.Event(kind: vms.EventKind.kResume, timestamp: 0);
      fakeIsolate.extensionRPCs.add('ext.flutter.driver');

183 184 185 186
      final FlutterDriver driver = await FlutterDriver.connect(dartVmServiceUrl: '');
      expect(driver, isNotNull);
      expectLogContains('Isolate is not paused. Assuming application is ready.');
    });
187 188
  });

189
  group('VMServiceFlutterDriver', () {
190
    late FakeVmService fakeClient;
191 192
    FakeVM fakeVM;
    FakeIsolate fakeIsolate;
193
    late VMServiceFlutterDriver driver;
194 195

    setUp(() {
196 197 198 199 200
      fakeIsolate = FakeIsolate();
      fakeVM = FakeVM(fakeIsolate);
      fakeClient = FakeVmService(fakeVM);
      driver = VMServiceFlutterDriver.connectedTo(fakeClient, fakeIsolate);
      fakeClient.responses['tap'] = makeFakeResponse(<String, dynamic>{});
201 202 203
    });

    test('checks the health of the driver extension', () async {
204
      fakeClient.responses['get_health'] = makeFakeResponse(<String, dynamic>{'status': 'ok'});
205
      final Health result = await driver.checkHealth();
206 207 208 209 210
      expect(result.status, HealthStatus.ok);
    });

    test('closes connection', () async {
      await driver.close();
211
      expect(fakeClient.connectionLog.last, 'dispose');
212 213
    });

214
    group('ByValueKey', () {
215
      test('restricts value types', () async {
Dan Field's avatar
Dan Field committed
216
        expect(() => find.byValueKey(null), throwsDriverError);
217 218 219
      });

      test('finds by ValueKey', () async {
220
        await driver.tap(find.byValueKey('foo'), timeout: _kTestTimeout);
221 222 223
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: tap, timeout: $_kSerializedTestTimeout, finderType: ByValueKey, keyValueString: foo, keyValueType: String}',
        ]);
224 225 226
      });
    });

227 228 229
    group('BySemanticsLabel', () {
      test('finds by Semantic label using String', () async {
        await driver.tap(find.bySemanticsLabel('foo'), timeout: _kTestTimeout);
230 231 232
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: tap, timeout: $_kSerializedTestTimeout, finderType: BySemanticsLabel, label: foo}',
        ]);
233 234 235 236
      });

      test('finds by Semantic label using RegExp', () async {
        await driver.tap(find.bySemanticsLabel(RegExp('^foo')), timeout: _kTestTimeout);
237 238 239
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: tap, timeout: $_kSerializedTestTimeout, finderType: BySemanticsLabel, label: ^foo, isRegExp: true}',
        ]);
240 241 242
      });
    });

243 244
    group('tap', () {
      test('sends the tap command', () async {
245
        await driver.tap(find.text('foo'), timeout: _kTestTimeout);
246 247 248
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: tap, timeout: $_kSerializedTestTimeout, finderType: ByText, text: foo}',
        ]);
249 250 251 252 253
      });
    });

    group('getText', () {
      test('sends the getText command', () async {
254
        fakeClient.responses['get_text'] = makeFakeResponse(<String, dynamic>{'text': 'hello'});
255
        final String result = await driver.getText(find.byValueKey(123), timeout: _kTestTimeout);
256
        expect(result, 'hello');
257 258 259
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: get_text, timeout: $_kSerializedTestTimeout, finderType: ByValueKey, keyValueString: 123, keyValueType: int}',
        ]);
260 261
      });
    });
262

263 264
    group('getLayerTree', () {
      test('sends the getLayerTree command', () async {
265 266
        fakeClient.responses['get_layer_tree'] = makeFakeResponse(<String, String>{
          'tree': 'hello',
267 268 269 270 271 272
        });
        final LayerTree result = await driver.getLayerTree(timeout: _kTestTimeout);
        final LayerTree referenceTree = LayerTree.fromJson(<String, String>{
            'tree': 'hello',
          });
        expect(result.tree, referenceTree.tree);
273 274 275
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: get_layer_tree, timeout: $_kSerializedTestTimeout}',
        ]);
276 277 278
      });
    });

279
    group('waitFor', () {
280
      test('sends the waitFor command', () async {
281
        fakeClient.responses['waitFor'] = makeFakeResponse(<String, dynamic>{});
282
        await driver.waitFor(find.byTooltip('foo'), timeout: _kTestTimeout);
283 284 285
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: waitFor, timeout: $_kSerializedTestTimeout, finderType: ByTooltipMessage, text: foo}',
        ]);
286 287
      });
    });
288

289 290
    group('getWidgetDiagnostics', () {
      test('sends the getWidgetDiagnostics command', () async {
291
        fakeClient.responses['get_diagnostics_tree'] = makeFakeResponse(<String, dynamic>{});
292
        await driver.getWidgetDiagnostics(find.byTooltip('foo'), timeout: _kTestTimeout);
293 294 295
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: get_diagnostics_tree, timeout: $_kSerializedTestTimeout, finderType: ByTooltipMessage, text: foo, subtreeDepth: 0, includeProperties: true, diagnosticsType: widget}',
        ]);
296 297 298 299 300
      });
    });

    group('getRenderObjectDiagnostics', () {
      test('sends the getRenderObjectDiagnostics command', () async {
301
        fakeClient.responses['get_diagnostics_tree'] = makeFakeResponse(<String, dynamic>{});
302
        await driver.getRenderObjectDiagnostics(find.byTooltip('foo'), timeout: _kTestTimeout);
303 304 305
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: get_diagnostics_tree, timeout: $_kSerializedTestTimeout, finderType: ByTooltipMessage, text: foo, subtreeDepth: 0, includeProperties: true, diagnosticsType: renderObject}',
        ]);
306 307 308
      });
    });

309 310
    group('waitForCondition', () {
      test('sends the wait for NoPendingFrameCondition command', () async {
311
        fakeClient.responses['waitForCondition'] = makeFakeResponse(<String, dynamic>{});
312
        await driver.waitForCondition(const NoPendingFrame(), timeout: _kTestTimeout);
313 314 315
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: waitForCondition, timeout: $_kSerializedTestTimeout, conditionName: NoPendingFrameCondition}',
        ]);
316 317
      });

318
      test('sends the wait for NoPendingPlatformMessages command', () async {
319
        fakeClient.responses['waitForCondition'] = makeFakeResponse(<String, dynamic>{});
320
        await driver.waitForCondition(const NoPendingPlatformMessages(), timeout: _kTestTimeout);
321 322 323
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: waitForCondition, timeout: $_kSerializedTestTimeout, conditionName: NoPendingPlatformMessagesCondition}',
        ]);
324 325
      });

326
      test('sends the waitForCondition of combined conditions command', () async {
327
        fakeClient.responses['waitForCondition'] = makeFakeResponse(<String, dynamic>{});
328 329 330
        const SerializableWaitCondition combinedCondition =
            CombinedCondition(<SerializableWaitCondition>[NoPendingFrame(), NoTransientCallbacks()]);
        await driver.waitForCondition(combinedCondition, timeout: _kTestTimeout);
331 332 333
         expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: waitForCondition, timeout: $_kSerializedTestTimeout, conditionName: CombinedCondition, conditions: [{"conditionName":"NoPendingFrameCondition"},{"conditionName":"NoTransientCallbacksCondition"}]}',
        ]);
334 335 336
      });
    });

337 338
    group('waitUntilNoTransientCallbacks', () {
      test('sends the waitUntilNoTransientCallbacks command', () async {
339
        fakeClient.responses['waitForCondition'] = makeFakeResponse(<String, dynamic>{});
340
        await driver.waitUntilNoTransientCallbacks(timeout: _kTestTimeout);
341 342 343
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: waitForCondition, timeout: $_kSerializedTestTimeout, conditionName: NoTransientCallbacksCondition}',
        ]);
344 345
      });
    });
346 347 348

    group('waitUntilFirstFrameRasterized', () {
      test('sends the waitUntilFirstFrameRasterized command', () async {
349
        fakeClient.responses['waitForCondition'] = makeFakeResponse(<String, dynamic>{});
350
        await driver.waitUntilFirstFrameRasterized();
351 352 353
        expect(fakeClient.commandLog, <String>[
          'ext.flutter.driver {command: waitForCondition, conditionName: FirstFrameRasterizedCondition}',
        ]);
354 355
      });
    });
356

357
    group('getOffset', () {
358 359 360 361 362 363 364
      setUp(() {
        fakeClient.responses['get_offset'] = makeFakeResponse(<String, double>{
          'dx': 11,
          'dy': 12,
        });
      });

365 366 367
      test('sends the getCenter command', () async {
        final DriverOffset result = await driver.getCenter(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
368 369 370
        expect(fakeClient.commandLog, <String>[
           'ext.flutter.driver {command: get_offset, timeout: 1234, finderType: ByValueKey, keyValueString: 123, keyValueType: int, offsetType: center}',
        ]);
371 372 373 374 375
      });

      test('sends the getTopLeft command', () async {
        final DriverOffset result = await driver.getTopLeft(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
376 377 378
        expect(fakeClient.commandLog, <String>[
           'ext.flutter.driver {command: get_offset, timeout: 1234, finderType: ByValueKey, keyValueString: 123, keyValueType: int, offsetType: topLeft}',
        ]);
379 380 381 382 383
      });

      test('sends the getTopRight command', () async {
        final DriverOffset result = await driver.getTopRight(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
384 385 386
        expect(fakeClient.commandLog, <String>[
           'ext.flutter.driver {command: get_offset, timeout: 1234, finderType: ByValueKey, keyValueString: 123, keyValueType: int, offsetType: topRight}',
        ]);
387 388 389 390 391
      });

      test('sends the getBottomLeft command', () async {
        final DriverOffset result = await driver.getBottomLeft(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
392 393 394
        expect(fakeClient.commandLog, <String>[
           'ext.flutter.driver {command: get_offset, timeout: 1234, finderType: ByValueKey, keyValueString: 123, keyValueType: int, offsetType: bottomLeft}',
        ]);
395 396 397 398 399
      });

      test('sends the getBottomRight command', () async {
        final DriverOffset result = await driver.getBottomRight(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
400 401 402
        expect(fakeClient.commandLog, <String>[
           'ext.flutter.driver {command: get_offset, timeout: 1234, finderType: ByValueKey, keyValueString: 123, keyValueType: int, offsetType: bottomRight}',
        ]);
403 404 405
      });
    });

406 407 408
    group('clearTimeline', () {
      test('clears timeline', () async {
        await driver.clearTimeline();
409
        expect(fakeClient.connectionLog, contains('clearVMTimeline'));
410 411 412
      });
    });

413
    group('traceAction', () {
414
      test('without clearing timeline', () async {
415
        final Timeline timeline = await driver.traceAction(() async {
416
          fakeClient.connectionLog.add('action');
417 418
        }, retainPriorEvents: true);

419 420
        expect(fakeClient.connectionLog, const <String>[
          'setVMTimelineFlags [all]',
421
          'action',
422 423 424
          'getFlagList',
          'setVMTimelineFlags []',
          'getVMTimeline null null',
425
        ]);
426
        expect(timeline.events!.single.name, 'test event');
427 428 429
      });

      test('with clearing timeline', () async {
430
        final Timeline timeline = await driver.traceAction(() async {
431
          fakeClient.connectionLog.add('action');
432 433
        });

434 435
        expect(fakeClient.connectionLog, const <String>[
          'clearVMTimeline',
436
          'getVMTimelineMicros',
437
          'setVMTimelineFlags [all]',
438
          'action',
439
          'getVMTimelineMicros',
440 441 442
          'getFlagList',
          'setVMTimelineFlags []',
          'getVMTimeline 1 999999',
443
        ]);
444
        expect(timeline.events!.single.name, 'test event');
445
      });
446 447

      test('with time interval', () async {
448 449 450 451 452 453 454
        fakeClient.incrementMicros = true;
        fakeClient.timelineResponses[1000001] = vms.Timeline.parse(<String, dynamic>{
          'traceEvents': <dynamic>[
            <String, dynamic>{
              'name': 'test event 2',
            },
          ],
455 456
          'timeOriginMicros': 1000000,
          'timeExtentMicros': 999999,
457
        });
458
        final Timeline timeline = await driver.traceAction(() async {
459
          fakeClient.connectionLog.add('action');
460 461
        });

462 463
        expect(fakeClient.connectionLog, const <String>[
          'clearVMTimeline',
464
          'getVMTimelineMicros',
465
          'setVMTimelineFlags [all]',
466 467
          'action',
          'getVMTimelineMicros',
468 469 470 471
          'getFlagList',
          'setVMTimelineFlags []',
          'getVMTimeline 1 999999',
          'getVMTimeline 1000001 999999',
472
        ]);
473
        expect(timeline.events!.map((TimelineEvent event) => event.name), <String>[
474
          'test event',
475 476 477
          'test event 2',
        ]);
      });
478
    });
479

480 481
    group('traceAction with timeline streams', () {
      test('specify non-default timeline streams', () async {
482 483
        bool actionCalled = false;

484
        final Timeline timeline = await driver.traceAction(() async {
485 486
          actionCalled = true;
        },
487 488 489
        streams: const <TimelineStream>[
          TimelineStream.dart,
          TimelineStream.gc,
490
          TimelineStream.compiler,
491 492
        ],
        retainPriorEvents: true);
493 494

        expect(actionCalled, isTrue);
495 496 497 498 499 500 501
        expect(fakeClient.connectionLog, <String>[
          'setVMTimelineFlags [Dart, GC, Compiler]',
          'getFlagList',
          'setVMTimelineFlags []',
          'getVMTimeline null null'
        ]);

502
        expect(timeline.events!.single.name, 'test event');
503 504
      });
    });
505 506

    group('sendCommand error conditions', () {
507
      test('local default timeout', () async {
508
        log.clear();
509
        fakeClient.artificialExtensionDelay = Completer<void>().future;
510 511 512
        FakeAsync().run((FakeAsync time) {
          driver.waitFor(find.byTooltip('foo'));
          expect(log, <String>[]);
513 514
          time.elapse(kUnusuallyLongTimeout);
        });
515
        expect(log, <String>['VMServiceFlutterDriver: waitFor message is taking a long time to complete...']);
516 517 518
      });

      test('local custom timeout', () async {
519
        log.clear();
520
        fakeClient.artificialExtensionDelay = Completer<void>().future;
521 522 523 524 525
        FakeAsync().run((FakeAsync time) {
          final Duration customTimeout = kUnusuallyLongTimeout - const Duration(seconds: 1);
          driver.waitFor(find.byTooltip('foo'), timeout: customTimeout);
          expect(log, <String>[]);
          time.elapse(customTimeout);
526
        });
527
        expect(log, <String>['VMServiceFlutterDriver: waitFor message is taking a long time to complete...']);
528 529 530
      });

      test('remote error', () async {
531 532 533
        fakeClient.responses['waitFor'] = makeFakeResponse(<String, dynamic>{
          'message': 'This is a failure',
        }, isError: true);
534 535 536
        try {
          await driver.waitFor(find.byTooltip('foo'));
          fail('expected an exception');
537
        } catch (error) {
Dan Field's avatar
Dan Field committed
538
          expect(error, isA<DriverError>());
539
          expect((error as DriverError).message, 'Error in Flutter application: {message: This is a failure}');
540 541
        }
      });
542 543

      test('uncaught remote error', () async {
544 545 546
        fakeClient.artificialExtensionDelay = Future<void>.error(
          vms.RPCError('callServiceExtension', 9999, 'test error'),
        );
547 548 549

        expect(driver.waitFor(find.byTooltip('foo')), throwsDriverError);
      });
550
    });
551 552 553 554 555 556 557 558 559 560

    group('VMServiceFlutterDriver Unsupported error', () {
      test('enableAccessibility', () async {
        expect(driver.enableAccessibility(), throwsA(isA<UnsupportedError>()));
      });

      test('webDriver', () async {
        expect(() => driver.webDriver, throwsA(isA<UnsupportedError>()));
      });
    });
561
  });
562

563
  group('VMServiceFlutterDriver with custom timeout', () {
564
    late FakeVmService fakeClient;
565 566
    FakeVM fakeVM;
    FakeIsolate fakeIsolate;
567
    late VMServiceFlutterDriver driver;
568 569

    setUp(() {
570 571 572 573 574
      fakeIsolate = FakeIsolate();
      fakeVM = FakeVM(fakeIsolate);
      fakeClient = FakeVmService(fakeVM);
      driver = VMServiceFlutterDriver.connectedTo(fakeClient, fakeIsolate);
      fakeClient.responses['get_health'] = makeFakeResponse(<String, dynamic>{'status': 'ok'});
575 576
    });

577
    test('GetHealth has no default timeout', () async {
578
      await driver.checkHealth();
579 580 581 582
      expect(
        fakeClient.commandLog,
        <String>['ext.flutter.driver {command: get_health}'],
      );
583 584
    });

585
    test('does not interfere with explicit timeouts', () async {
586
      await driver.checkHealth(timeout: _kTestTimeout);
587 588 589 590
      expect(
        fakeClient.commandLog,
        <String>['ext.flutter.driver {command: get_health, timeout: $_kSerializedTestTimeout}'],
      );
591 592
    });
  });
593 594

  group('WebFlutterDriver', () {
595 596
    late FakeFlutterWebConnection fakeConnection;
    late WebFlutterDriver driver;
597 598

    setUp(() {
599 600 601
      fakeConnection = FakeFlutterWebConnection();
      fakeConnection.supportsTimelineAction = true;
      driver = WebFlutterDriver.connectedTo(fakeConnection);
602 603 604 605 606 607 608 609 610
    });

    test('closes connection', () async {
      await driver.close();
    });

    group('ByValueKey', () {
      test('restricts value types', () async {
        expect(() => find.byValueKey(null),
Dan Field's avatar
Dan Field committed
611
            throwsDriverError);
612 613 614
      });

      test('finds by ValueKey', () async {
615
        fakeConnection.responses['tap'] = jsonEncode(makeFakeResponse(<String, dynamic>{}));
616
        await driver.tap(find.byValueKey('foo'), timeout: _kTestTimeout);
617 618 619
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"tap","timeout":"1234","finderType":"ByValueKey","keyValueString":"foo","keyValueType":"String"}') 0:00:01.234000''',
        ]);
620 621 622 623 624
      });
    });

    group('BySemanticsLabel', () {
      test('finds by Semantic label using String', () async {
625
        fakeConnection.responses['tap'] = jsonEncode(makeFakeResponse(<String, dynamic>{}));
626
        await driver.tap(find.bySemanticsLabel('foo'), timeout: _kTestTimeout);
627 628 629
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"tap","timeout":"1234","finderType":"BySemanticsLabel","label":"foo"}') 0:00:01.234000''',
        ]);
630 631 632
      });

      test('finds by Semantic label using RegExp', () async {
633
        fakeConnection.responses['tap'] = jsonEncode(makeFakeResponse(<String, dynamic>{}));
634
        await driver.tap(find.bySemanticsLabel(RegExp('^foo')), timeout: _kTestTimeout);
635 636 637
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"tap","timeout":"1234","finderType":"BySemanticsLabel","label":"^foo","isRegExp":"true"}') 0:00:01.234000''',
        ]);
638 639 640 641 642
      });
    });

    group('tap', () {
      test('sends the tap command', () async {
643
        fakeConnection.responses['tap'] = jsonEncode(makeFakeResponse(<String, dynamic>{}));
644
        await driver.tap(find.text('foo'), timeout: _kTestTimeout);
645 646 647
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"tap","timeout":"1234","finderType":"ByText","text":"foo"}') 0:00:01.234000''',
        ]);
648 649 650 651 652
      });
    });

    group('getText', () {
      test('sends the getText command', () async {
653
        fakeConnection.responses['get_text'] = jsonEncode(makeFakeResponse(<String, dynamic>{'text': 'hello'}));
654 655
        final String result = await driver.getText(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, 'hello');
656 657 658
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"get_text","timeout":"1234","finderType":"ByValueKey","keyValueString":"123","keyValueType":"int"}') 0:00:01.234000''',
        ]);
659 660 661 662 663
      });
    });

    group('waitFor', () {
      test('sends the waitFor command', () async {
664
        fakeConnection.responses['waitFor'] = jsonEncode(makeFakeResponse(<String, dynamic>{'text': 'hello'}));
665
        await driver.waitFor(find.byTooltip('foo'), timeout: _kTestTimeout);
666 667 668
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"waitFor","timeout":"1234","finderType":"ByTooltipMessage","text":"foo"}') 0:00:01.234000''',
        ]);
669 670 671 672
      });
    });

    group('waitForCondition', () {
673 674 675 676
      setUp(() {
        fakeConnection.responses['waitForCondition'] = jsonEncode(makeFakeResponse(<String, dynamic>{'text': 'hello'}));
      });

677 678
      test('sends the wait for NoPendingFrameCondition command', () async {
        await driver.waitForCondition(const NoPendingFrame(), timeout: _kTestTimeout);
679 680 681
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"waitForCondition","timeout":"1234","conditionName":"NoPendingFrameCondition"}') 0:00:01.234000''',
        ]);
682 683 684 685
      });

      test('sends the wait for NoPendingPlatformMessages command', () async {
        await driver.waitForCondition(const NoPendingPlatformMessages(), timeout: _kTestTimeout);
686 687 688
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"waitForCondition","timeout":"1234","conditionName":"NoPendingPlatformMessagesCondition"}') 0:00:01.234000''',
        ]);
689 690 691
      });

      test('sends the waitForCondition of combined conditions command', () async {
692 693 694
        const SerializableWaitCondition combinedCondition = CombinedCondition(
          <SerializableWaitCondition>[NoPendingFrame(), NoTransientCallbacks()],
        );
695
        await driver.waitForCondition(combinedCondition, timeout: _kTestTimeout);
696 697 698
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"waitForCondition","timeout":"1234","conditionName":"CombinedCondition","conditions":"[{\"conditionName\":\"NoPendingFrameCondition\"},{\"conditionName\":\"NoTransientCallbacksCondition\"}]"}') 0:00:01.234000''',
        ]);
699 700 701 702 703
      });
    });

    group('waitUntilNoTransientCallbacks', () {
      test('sends the waitUntilNoTransientCallbacks command', () async {
704
        fakeConnection.responses['waitForCondition'] = jsonEncode(makeFakeResponse(<String, dynamic>{}));
705
        await driver.waitUntilNoTransientCallbacks(timeout: _kTestTimeout);
706 707 708
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"waitForCondition","timeout":"1234","conditionName":"NoTransientCallbacksCondition"}') 0:00:01.234000''',
        ]);
709 710 711 712
      });
    });

    group('getOffset', () {
713 714 715 716 717 718
      setUp(() {
        fakeConnection.responses['get_offset'] = jsonEncode(makeFakeResponse(<String, double>{
          'dx': 11,
          'dy': 12,
        }));
      });
719 720 721 722

      test('sends the getCenter command', () async {
        final DriverOffset result = await driver.getCenter(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
723 724 725
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"get_offset","timeout":"1234","finderType":"ByValueKey","keyValueString":"123","keyValueType":"int","offsetType":"center"}') 0:00:01.234000''',
        ]);
726 727 728 729 730
      });

      test('sends the getTopLeft command', () async {
        final DriverOffset result = await driver.getTopLeft(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
731 732 733
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"get_offset","timeout":"1234","finderType":"ByValueKey","keyValueString":"123","keyValueType":"int","offsetType":"topLeft"}') 0:00:01.234000''',
        ]);
734 735 736 737 738
      });

      test('sends the getTopRight command', () async {
        final DriverOffset result = await driver.getTopRight(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
739 740 741
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"get_offset","timeout":"1234","finderType":"ByValueKey","keyValueString":"123","keyValueType":"int","offsetType":"topRight"}') 0:00:01.234000''',
        ]);
742 743 744 745 746
      });

      test('sends the getBottomLeft command', () async {
        final DriverOffset result = await driver.getBottomLeft(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
747 748 749
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"get_offset","timeout":"1234","finderType":"ByValueKey","keyValueString":"123","keyValueType":"int","offsetType":"bottomLeft"}') 0:00:01.234000''',
        ]);
750 751 752 753 754
      });

      test('sends the getBottomRight command', () async {
        final DriverOffset result = await driver.getBottomRight(find.byValueKey(123), timeout: _kTestTimeout);
        expect(result, const DriverOffset(11, 12));
755 756 757
        expect(fakeConnection.commandLog, <String>[
          r'''window.$flutterDriver('{"command":"get_offset","timeout":"1234","finderType":"ByValueKey","keyValueString":"123","keyValueType":"int","offsetType":"bottomRight"}') 0:00:01.234000''',
        ]);
758 759 760 761
      });
    });

    test('checks the health of the driver extension', () async {
762
      fakeConnection.responses['get_health'] = jsonEncode(makeFakeResponse(<String, dynamic>{'status': 'ok'}));
763
      await driver.checkHealth();
764 765 766
      expect(fakeConnection.commandLog, <String>[
        r'''window.$flutterDriver('{"command":"get_health"}') null''',
      ]);
767 768
    });

769
    group('WebFlutterDriver Unimplemented/Unsupported error', () {
770 771
      test('forceGC', () async {
        expect(driver.forceGC(),
Dan Field's avatar
Dan Field committed
772
            throwsA(isA<UnimplementedError>()));
773 774 775 776
      });

      test('getVmFlags', () async {
        expect(driver.getVmFlags(),
Dan Field's avatar
Dan Field committed
777
            throwsA(isA<UnimplementedError>()));
778 779 780 781
      });

      test('waitUntilFirstFrameRasterized', () async {
        expect(driver.waitUntilFirstFrameRasterized(),
Dan Field's avatar
Dan Field committed
782
            throwsA(isA<UnimplementedError>()));
783 784 785
      });

      test('appIsoloate', () async {
786
        expect(() => driver.appIsolate.extensionRPCs,
Dan Field's avatar
Dan Field committed
787
            throwsA(isA<UnsupportedError>()));
788 789 790 791
      });

      test('serviceClient', () async {
        expect(() => driver.serviceClient.getVM(),
Dan Field's avatar
Dan Field committed
792
            throwsA(isA<UnsupportedError>()));
793 794 795 796 797
      });
    });
  });

  group('WebFlutterDriver with non-chrome browser', () {
798
    FakeFlutterWebConnection fakeConnection;
799
    late WebFlutterDriver driver;
800 801

    setUp(() {
802 803
      fakeConnection = FakeFlutterWebConnection();
      driver = WebFlutterDriver.connectedTo(fakeConnection);
804 805 806 807
    });

    test('tracing', () async {
      expect(driver.traceAction(() async { return Future<dynamic>.value(); }),
808
          throwsA(isA<UnsupportedError>()));
809
      expect(driver.startTracing(),
810
          throwsA(isA<UnsupportedError>()));
811
      expect(driver.stopTracingAndDownloadTimeline(),
812
          throwsA(isA<UnsupportedError>()));
813
      expect(driver.clearTimeline(),
814
          throwsA(isA<UnsupportedError>()));
815 816 817 818 819 820 821 822
    });
  });
}

/// This function will verify the format of the script
/// and return the actual script.
/// script will be in the following format:
//   window.flutterDriver('[actual script]')
823
String? _checkAndEncode(dynamic script) {
Dan Field's avatar
Dan Field committed
824
  expect(script, isA<String>());
825 826 827
  expect(script.startsWith(_kWebScriptPrefix), isTrue);
  expect(script.endsWith(_kWebScriptSuffix), isTrue);
  // Strip prefix and suffix
828
  return script.substring(_kWebScriptPrefix.length, script.length - 2) as String?;
829 830
}

831
vms.Response? makeFakeResponse(
832 833 834
  Map<String, dynamic> response, {
  bool isError = false,
}) {
835
  return vms.Response.parse(<String, dynamic>{
836
    'isError': isError,
837
    'response': response,
838 839 840
  });
}

841 842 843 844 845 846 847
class FakeFlutterWebConnection extends Fake implements FlutterWebConnection {
  @override
  bool supportsTimelineAction = false;

  Map<String, dynamic> responses = <String, dynamic>{};
  List<String> commandLog = <String>[];
  @override
848
  Future<dynamic> sendCommand(String script, Duration? duration) async {
849
    commandLog.add('$script $duration');
850
    final Map<String, dynamic> decoded = jsonDecode(_checkAndEncode(script)!) as Map<String, dynamic>;
851 852 853 854 855 856 857 858 859 860 861 862 863 864
    final dynamic response = responses[decoded['command']];
    assert(response != null, 'Missing ${decoded['command']} in responses.');
    return response;
  }

  @override
  Future<void> close() async {
    return;
  }
}

class FakeVmService extends Fake implements vms.VmService {
  FakeVmService(this.vm);

865
  FakeVM? vm;
866 867 868 869 870 871
  bool failOnSetFlag = false;
  bool failOnResumeWith101 = false;

  final List<String> connectionLog = <String>[];

  @override
872
  Future<vms.VM> getVM() async => vm!;
873 874 875 876

  @override
  Future<vms.Isolate> getIsolate(String isolateId) async {
    connectionLog.add('getIsolate');
877 878
    if (isolateId == vm!.isolate!.id) {
      return vm!.isolate!;
879
    }
880
    throw UnimplementedError('getIsolate called with unrecognized $isolateId');
881 882 883
  }

  @override
884 885
  Future<vms.Success> resume(String isolateId, {String? step, int? frameIndex}) async {
    assert(isolateId == vm!.isolate!.id);
886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924
    connectionLog.add('resume');
    if (failOnResumeWith101) {
      throw vms.RPCError('resume', 101, '');
    }
    return vms.Success();
  }

  @override
  Future<vms.Success> streamListen(String streamId) async {
    connectionLog.add('streamListen $streamId');
    return vms.Success();
  }

  @override
  Future<vms.Success> streamCancel(String streamId) async {
    connectionLog.add('streamCancel $streamId');
    return vms.Success();
  }

  @override
  Future<vms.Response> setFlag(String name, String value) async {
    connectionLog.add('setFlag $name $value');
    if (failOnSetFlag) {
      throw Exception('setFlag failed');
    }
    return vms.Success();
  }

  @override
  Stream<vms.Event> get onIsolateEvent async* {
    connectionLog.add('onIsolateEvent');
    yield vms.Event(
      kind: vms.EventKind.kServiceExtensionAdded,
      extensionRPC: 'ext.flutter.driver',
      timestamp: 0,
    );
  }

  List<String> commandLog = <String>[];
925 926
  Map<String, vms.Response?> responses = <String, vms.Response?>{};
  Future<void>? artificialExtensionDelay;
927 928

  @override
929
  Future<vms.Response> callServiceExtension(String method, {Map<dynamic, dynamic>? args, String? isolateId}) async {
930 931 932
    commandLog.add('$method $args');
    await artificialExtensionDelay;

933
    final vms.Response response = responses[args!['command']]!;
934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967
    assert(response != null, 'Failed to create a response for ${args['command']}');
    return response;
  }

  @override
  Future<vms.Success> clearVMTimeline() async {
    connectionLog.add('clearVMTimeline');
    return vms.Success();
  }

  @override
  Future<vms.FlagList> getFlagList() async {
    connectionLog.add('getFlagList');
    return vms.FlagList(flags: <vms.Flag>[]);
  }

  int vmTimelineMicros = -1000000;
  bool incrementMicros = false;

  @override
  Future<vms.Timestamp> getVMTimelineMicros() async {
    connectionLog.add('getVMTimelineMicros');
    if (incrementMicros || vmTimelineMicros < 0) {
      vmTimelineMicros = vmTimelineMicros + 1000001;
    }
    return vms.Timestamp(timestamp: vmTimelineMicros);
  }

  @override
  Future<vms.Success> setVMTimelineFlags(List<String> recordedStreams) async {
    connectionLog.add('setVMTimelineFlags $recordedStreams');
    return vms.Success();
  }

968
  final Map<int, vms.Timeline?> timelineResponses = <int, vms.Timeline?>{
969 970 971 972 973 974 975 976 977 978
    1: vms.Timeline.parse(<String, dynamic>{
      'traceEvents': <dynamic>[
        <String, dynamic>{
          'name': 'test event',
        },
      ],
      'timeOriginMicros': 0,
      'timeExtentMicros': 999999,
    }),
  };
979

980
  @override
981
  Future<vms.Timeline> getVMTimeline({int? timeOriginMicros, int? timeExtentMicros}) async {
982
    connectionLog.add('getVMTimeline $timeOriginMicros $timeExtentMicros');
983
    final vms.Timeline timeline = timelineResponses[timeOriginMicros ?? 1]!;
984 985 986
    assert(timeline != null, 'Missing entry in timelineResponses[$timeOriginMicros]');
    return timeline;
  }
987

988
  @override
989
  Future<void> dispose() async {
990 991
    connectionLog.add('dispose');
  }
992

993 994 995
  @override
  Future<void> get onDone async {}
}
996

997 998
class FakeVM extends Fake implements vms.VM {
  FakeVM(this.isolate);
999

1000
  vms.Isolate? isolate;
1001

1002
  int numberOfTriesBeforeResolvingIsolate = 0;
1003

1004
  @override
1005 1006 1007 1008
  List<vms.IsolateRef> get isolates {
    numberOfTriesBeforeResolvingIsolate -= 1;
    return <vms.Isolate>[
      if (numberOfTriesBeforeResolvingIsolate <= 0)
1009
        isolate!,
1010 1011
    ];
  }
1012
}
1013 1014 1015 1016 1017 1018 1019 1020 1021

class FakeIsolate extends Fake implements vms.Isolate {
  @override
  String get number => '123';

  @override
  String get id => number;

  @override
1022
  vms.Event? pauseEvent;
1023 1024 1025

  @override
  List<String> get extensionRPCs => <String>[];
1026
}