diff --git a/dev/devicelab/lib/tasks/integration_ui.dart b/dev/devicelab/lib/tasks/integration_ui.dart index 694be05c828..0d1eec8d65f 100644 --- a/dev/devicelab/lib/tasks/integration_ui.dart +++ b/dev/devicelab/lib/tasks/integration_ui.dart @@ -24,6 +24,7 @@ Future runEndToEndTests() async { const List entryPoints = const [ 'lib/keyboard_resize.dart', 'lib/driver.dart', + 'lib/screenshot.dart', ]; for (final String entryPoint in entryPoints) { diff --git a/dev/integration_tests/ui/lib/screenshot.dart b/dev/integration_tests/ui/lib/screenshot.dart new file mode 100644 index 00000000000..34bdcbf0cc5 --- /dev/null +++ b/dev/integration_tests/ui/lib/screenshot.dart @@ -0,0 +1,80 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter/material.dart'; +import 'package:flutter_driver/driver_extension.dart'; + +/// This sample application creates a hard to render frame, causing the +/// driver script to race the GPU thread. If the driver script wins the +/// race, it will screenshot the previous frame. If the GPU thread wins +/// it, it will screenshot the latest frame. +void main() { + enableFlutterDriverExtension(); + + runApp(new Toggler()); +} + +class Toggler extends StatefulWidget { + @override + State createState() => new TogglerState(); +} + +class TogglerState extends State { + bool _visible = false; + + @override + Widget build(BuildContext context) { + return new MaterialApp( + home: new Scaffold( + appBar: new AppBar( + title: const Text('FlutterDriver test'), + ), + body: new Material( + child: new Column( + children: [ + new FlatButton( + key: const ValueKey('toggle'), + child: const Text('Toggle visibility'), + onPressed: () { + setState(() { + _visible = !_visible; + }); + }, + ), + new Expanded( + child: new ListView( + children: _buildRows(_visible ? 10 : 0), + ), + ), + ], + ), + ), + ), + ); + } +} + +List _buildRows(int count) { + return new List.generate(count, (int i) { + return new Row( + children: _buildCells(i / count), + ); + }); +} + +/// Builds cells that are known to take time to render causing a delay on the +/// GPU thread. +List _buildCells(double epsilon) { + return new List.generate(15, (int i) { + return new Expanded( + child: new Material( + // A magic color that the test will be looking for on the screenshot. + color: const Color(0xffff0102), + borderRadius: new BorderRadius.all(new Radius.circular(i.toDouble() + epsilon)), + elevation: 5.0, + child: const SizedBox(height: 10.0, width: 10.0), + ), + ); + }); +} diff --git a/dev/integration_tests/ui/pubspec.yaml b/dev/integration_tests/ui/pubspec.yaml index 82f5c16cef9..06b0996574e 100644 --- a/dev/integration_tests/ui/pubspec.yaml +++ b/dev/integration_tests/ui/pubspec.yaml @@ -2,6 +2,7 @@ name: integration_ui description: Flutter non-plugin UI integration tests. dependencies: + image: 1.1.29 flutter: sdk: flutter flutter_driver: diff --git a/dev/integration_tests/ui/test_driver/screenshot_test.dart b/dev/integration_tests/ui/test_driver/screenshot_test.dart new file mode 100644 index 00000000000..9ec40018f2c --- /dev/null +++ b/dev/integration_tests/ui/test_driver/screenshot_test.dart @@ -0,0 +1,46 @@ +// Copyright 2017 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:flutter_driver/flutter_driver.dart'; +import 'package:image/image.dart'; +import 'package:test/test.dart'; + +void main() { + group('FlutterDriver', () { + FlutterDriver driver; + + setUpAll(() async { + driver = await FlutterDriver.connect(); + }); + + tearDownAll(() async { + await driver.close(); + }); + + test('should take screenshot', () async { + final SerializableFinder toggleBtn = find.byValueKey('toggle'); + // Cards use a magic background color that we look for in the screenshots. + final Matcher cardsAreVisible = contains(0xff0201ff); + await driver.waitFor(toggleBtn); + + bool cardsShouldBeVisible = false; + Image imageBefore = decodePng(await driver.screenshot()); + for (int i = 0; i < 10; i += 1) { + await driver.tap(toggleBtn); + cardsShouldBeVisible = !cardsShouldBeVisible; + final Image imageAfter = decodePng(await driver.screenshot()); + + if (cardsShouldBeVisible) { + expect(imageBefore.data, isNot(cardsAreVisible)); + expect(imageAfter.data, cardsAreVisible); + } else { + expect(imageBefore.data, cardsAreVisible); + expect(imageAfter.data, isNot(cardsAreVisible)); + } + + imageBefore = imageAfter; + } + }, timeout: const Timeout(const Duration(minutes: 2))); + }); +} diff --git a/packages/flutter_driver/lib/src/driver.dart b/packages/flutter_driver/lib/src/driver.dart index a9ce328e234..b89877b82bf 100644 --- a/packages/flutter_driver/lib/src/driver.dart +++ b/packages/flutter_driver/lib/src/driver.dart @@ -435,6 +435,42 @@ class FlutterDriver { /// Take a screenshot. The image will be returned as a PNG. Future> screenshot({ Duration timeout }) async { timeout ??= _kLongTimeout; + + // HACK: this artificial delay here is to deal with a race between the + // driver script and the GPU thread. The issue is that driver API + // synchronizes with the framework based on transient callbacks, which + // are out of sync with the GPU thread. Here's the timeline of events + // in ASCII art: + // + // ------------------------------------------------------------------- + // Before this change: + // ------------------------------------------------------------------- + // UI : <-- build --> + // GPU : <-- rasterize --> + // Gap : | random | + // Driver: <-- screenshot --> + // + // In the diagram above, the gap is the time between the last driver + // action taken, such as a `tap()`, and the subsequent call to + // `screenshot()`. The gap is random because it is determined by the + // unpredictable network communication between the driver process and + // the application. If this gap is too short, the screenshot is taken + // before the GPU thread is done rasterizing the frame, so the + // screenshot of the previous frame is taken, which is wrong. + // + // ------------------------------------------------------------------- + // After this change: + // ------------------------------------------------------------------- + // UI : <-- build --> + // GPU : <-- rasterize --> + // Gap : | 2 seconds or more | + // Driver: <-- screenshot --> + // + // The two-second gap should be long enough for the GPU thread to + // finish rasterizing the frame, but not longer than necessary to keep + // driver tests as fast a possible. + await new Future.delayed(const Duration(seconds: 2)); + final Map result = await _peer.sendRequest('_flutter.screenshot').timeout(timeout); return BASE64.decode(result['screenshot']); }