[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'));
+  });
+}