[e2e] Add screenshot capability to ermine driver.
This change adds the capability to grab a screenshot to a launched view.
Test: Add e2e test for spinning_square_view
Change-Id: I71942ddbfbdbb4d5244584187bd2fa743b3e4a06
Reviewed-on: https://fuchsia-review.googlesource.com/c/experiences/+/470567
Commit-Queue: Sanjay Chouksey <sanjayc@google.com>
Reviewed-by: Charles Whitten <cwhitten@google.com>
diff --git a/tests/e2e/BUILD.gn b/tests/e2e/BUILD.gn
index 4ec221d..3fe0928 100644
--- a/tests/e2e/BUILD.gn
+++ b/tests/e2e/BUILD.gn
@@ -15,12 +15,14 @@
"ermine_session_shell_ask_test.dart",
"ermine_session_shell_simple_browser_test.dart",
"ermine_smoke_test.dart",
+ "ermine_spinning_square_view_test.dart",
]
deps = [
"//sdk/testing/sl4f/client",
"//sdk/testing/sl4f/flutter_driver_sl4f",
"//third_party/dart-pkg/git/flutter/packages/flutter_driver",
+ "//third_party/dart-pkg/pub/image",
"//third_party/dart-pkg/pub/test",
"//third_party/dart-pkg/pub/webdriver",
]
diff --git a/tests/e2e/test/ermine_driver.dart b/tests/e2e/test/ermine_driver.dart
index 4e7dfd1..dd0cb61 100644
--- a/tests/e2e/test/ermine_driver.dart
+++ b/tests/e2e/test/ermine_driver.dart
@@ -2,6 +2,9 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:math';
+
+import 'package:image/image.dart';
import 'package:flutter_driver/flutter_driver.dart';
import 'package:flutter_driver_sl4f/flutter_driver_sl4f.dart';
import 'package:sl4f/sl4f.dart';
@@ -16,6 +19,7 @@
class ErmineDriver {
/// The instance of [Sl4f] used to connect to Ermine flutter app.
final Sl4f sl4f;
+ final Component _component;
FlutterDriver _driver;
final FlutterDriverConnector _connector;
@@ -26,11 +30,15 @@
/// Constructor.
ErmineDriver(this.sl4f)
: _connector = FlutterDriverConnector(sl4f),
+ _component = Component(sl4f),
_webDriverConnector = WebDriverConnector(_chromeDriverPath, sl4f);
/// The instance of [FlutterDriver] that is connected to Ermine flutter app.
FlutterDriver get driver => _driver;
+ /// The instance of [Component] that is connected to the DUT.
+ Component get component => _component;
+
/// Set up the test environment for Ermine.
///
/// This restarts the workstation session and connects to the running instance
@@ -99,7 +107,7 @@
// [FlutterDriverConnector] in flutter_driver_sl4f.dart
final browserIsolate = await browserConnector.isolate('simple-browser');
if (browserIsolate == null) {
- fail("couldn't find simple browser.");
+ fail('couldn\'t find simple browser.');
}
// Connect to the browser.
@@ -121,4 +129,75 @@
Future<WebDriver> getWebDriverFor(String hostUrl) async {
return (await _webDriverConnector.webDriversForHost(hostUrl)).single;
}
+
+ Future<Rectangle> getViewRect(String viewUrl,
+ [Duration timeout = const Duration(seconds: 30)]) async {
+ final component = await waitForView(viewUrl, timeout);
+ final viewport = component['viewport'];
+ final viewRect =
+ viewport.split(',').map((e) => double.parse(e).round()).toList();
+
+ return Rectangle(viewRect[0], viewRect[1], viewRect[2], viewRect[3]);
+ }
+
+ /// Finds the first launched component given its [viewUrl] and returns it's
+ /// Inspect data. Waits for [timeout] duration for view to launch.
+ Future<Map<String, dynamic>> waitForView(String viewUrl,
+ [Duration timeout = const Duration(seconds: 30)]) async {
+ final end = DateTime.now().add(timeout);
+ while (DateTime.now().isBefore(end)) {
+ final views = await launchedViews();
+ final view = views.firstWhere((view) => view['url'] == viewUrl,
+ orElse: () => null);
+ if (view != null) {
+ return view;
+ }
+ // Wait a second to query inspect again.
+ await Future.delayed(Duration(seconds: 1));
+ }
+ fail('Failed to find component: $viewUrl');
+ // ignore: dead_code
+ return null;
+ }
+
+ /// Returns [Inspect] data for all launched views.
+ Future<List<Map<String, dynamic>>> launchedViews() async {
+ final snapshot = await Inspect(sl4f).snapshotRoot('ermine.cmx');
+ final workspace = snapshot['workspaces'];
+
+ final clusters = <Map<String, dynamic>>[];
+ int instance = 0;
+ while (workspace['cluster-$instance'] != null) {
+ clusters.add(workspace['cluster-${instance++}']);
+ }
+
+ final views = <Map<String, dynamic>>[];
+ for (final cluster in clusters) {
+ int instance = 0;
+ while (cluster['component-$instance'] != null) {
+ views.add(cluster['component-${instance++}']);
+ }
+ }
+ return views;
+ }
+
+ /// Take a screenshot of a View given its screen co-ordinates.
+ Future<Image> screenshot(Rectangle rect) async {
+ final scenic = Scenic(sl4f);
+ final image = await scenic.takeScreenshot();
+ return copyCrop(image, rect.left, rect.top, rect.width, rect.height);
+ }
+
+ /// Returns a histogram, i.e. occurences of colors, in an image.
+ /// [Color] is encoded as 0xAABBGGRR.
+ Map<int, int> histogram(Image image) {
+ final colors = <int, int>{};
+ for (int j = 0; j < image.height; j++) {
+ for (int i = 0; i < image.width; i++) {
+ final color = image.getPixel(i, j);
+ colors[color] = (colors[color] ?? 0) + 1;
+ }
+ }
+ return colors;
+ }
}
diff --git a/tests/e2e/test/ermine_smoke_test.dart b/tests/e2e/test/ermine_smoke_test.dart
index 1067d91..2686216 100644
--- a/tests/e2e/test/ermine_smoke_test.dart
+++ b/tests/e2e/test/ermine_smoke_test.dart
@@ -2,9 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
-import 'dart:math';
-
-import 'package:flutter_driver/flutter_driver.dart';
import 'package:sl4f/sl4f.dart';
import 'package:test/test.dart';
@@ -42,17 +39,3 @@
expect(isAllBlack, false);
});
}
-
-Future<Point<int>> centerFromFinder(
- FlutterDriver driver, SerializableFinder finder) async {
- // Get the bottom right of the main screen.
- final mainScreenFinder = find.byValueKey('main');
- final bottomRight = await driver.getBottomRight(mainScreenFinder);
-
- // The `input` utility expects screen coordinates to be scaled 0 - 1000.
- final center = await driver.getCenter(finder);
- int x = (center.dx / bottomRight.dx * 1000).toInt();
- int y = (center.dy / bottomRight.dy * 1000).toInt();
-
- return Point<int>(x, y);
-}
diff --git a/tests/e2e/test/ermine_spinning_square_view_test.dart b/tests/e2e/test/ermine_spinning_square_view_test.dart
new file mode 100644
index 0000000..f631658
--- /dev/null
+++ b/tests/e2e/test/ermine_spinning_square_view_test.dart
@@ -0,0 +1,57 @@
+// Copyright 2019 The Fuchsia 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:sl4f/sl4f.dart';
+import 'package:test/test.dart';
+
+import 'ermine_driver.dart';
+
+/// Tests that the DUT running ermine can do the following:
+/// - Launch `spinning_square_view` ephemeral package.
+/// - Verify it is show by taking its screenshot.
+void main() {
+ Sl4f sl4f;
+ ErmineDriver ermine;
+
+ setUpAll(() async {
+ sl4f = Sl4f.fromEnvironment();
+ await sl4f.startServer();
+
+ ermine = ErmineDriver(sl4f);
+ await ermine.setUp();
+ });
+
+ tearDownAll(() async {
+ // Any of these may end up being null if the test fails in setup.
+ await ermine.tearDown();
+ await sl4f?.stopServer();
+ sl4f?.close();
+ });
+
+ test('Verify spinning square view is shown', () async {
+ const componentUrl =
+ 'fuchsia-pkg://fuchsia.com/spinning_square_view#meta/spinning_square_view.cmx';
+ await ermine.launch(componentUrl);
+ await ermine.component.search('spinning_square_view.cmx');
+ // Get the view rect.
+ final viewRect = await ermine.getViewRect(componentUrl);
+ // Give the view couple of seconds to draw before taking its screenshot.
+ await Future.delayed(Duration(seconds: 2));
+ final screenshot = await ermine.screenshot(viewRect);
+ final histogram = ermine.histogram(screenshot);
+
+ // spinning_square_view displays a red square on purple background.
+ const purple = 0xffb73a67; // (0xAABBGGRR)
+ const red = 0xff5700f5; // (0xAABBGGRR)
+ expect(histogram[purple], isNotNull);
+ expect(histogram[red], isNotNull);
+ expect(histogram[purple] > histogram[red], isTrue);
+
+ // Close the view.
+ await ermine.driver.requestData('close');
+ // Verify the view is closed.
+ await ermine.driver.waitForAbsent(find.text('spinning_square_view'));
+ });
+}