blob: dd0cb61cb0035a019f0dec3acbcd075983cff413 [file] [log] [blame]
// 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 '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';
import 'package:test/test.dart';
import 'package:webdriver/sync_io.dart' show WebDriver;
const _chromeDriverPath = 'runtime_deps/chromedriver';
/// Defines a test utility class to drive Ermine during integration test using
/// Flutter Driver. This utility will grow with more convenience methods in the
/// future useful for testing.
class ErmineDriver {
/// The instance of [Sl4f] used to connect to Ermine flutter app.
final Sl4f sl4f;
final Component _component;
FlutterDriver _driver;
final FlutterDriverConnector _connector;
final _browserDrivers = <FlutterDriver>[];
final _browserConnectors = <FlutterDriverConnector>[];
final WebDriverConnector _webDriverConnector;
/// 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
/// of Ermine using FlutterDriver.
Future<void> setUp() async {
// TODO(http://fxbug.dev/66199): Uncomment once session restart is fixed.
// Restart the workstation session.
// final result = await sl4f.ssh.run('session_control restart');
// if (result.exitCode != 0) {
// fail('failed to restart workstation session.');
// }
// Initialize Ermine's flutter driver and web driver connectors.
await _connector.initialize();
await _webDriverConnector.initialize();
// Now connect to ermine.
_driver = await _connector.driverForIsolate('ermine');
if (_driver == null) {
fail('unable to connect to ermine.');
}
}
/// Closes [FlutterDriverConnector] and performs cleanup.
Future<void> tearDown() async {
await _driver?.close();
await _connector.tearDown();
for (final browserDriver in _browserDrivers) {
await browserDriver?.close();
}
for (final browserConnector in _browserConnectors) {
await browserConnector?.tearDown();
}
await _webDriverConnector?.tearDown();
}
/// Launch a component given its [componentUrl].
Future<void> launch(String componentUrl) async {
final result = await sl4f.ssh.run('session_control add $componentUrl');
if (result.exitCode != 0) {
fail('failed to launch component: $componentUrl.');
}
}
/// Got to the Overview screen.
Future<void> gotoOverview() async {
await _driver.requestData('overview');
await _driver.waitFor(find.byValueKey('overview'));
}
/// Launches a simple browser and returns a [FlutterDriver] connected to it.
Future<FlutterDriver> launchAndWaitForSimpleBrowser() async {
await launch(
'fuchsia-pkg://fuchsia.com/simple-browser#meta/simple-browser.cmx');
// Initilize the browser's flutter driver connector.
final browserConnector = FlutterDriverConnector(sl4f);
_browserConnectors.add(browserConnector);
await browserConnector.initialize();
// Checks if Simple Browser is running.
// TODO(fxb/66577): Get the last isolate once it's supported by
// [FlutterDriverConnector] in flutter_driver_sl4f.dart
final browserIsolate = await browserConnector.isolate('simple-browser');
if (browserIsolate == null) {
fail('couldn\'t find simple browser.');
}
// Connect to the browser.
// TODO(fxb/66577): Get the driver of the last isolate once it's supported by
// [FlutterDriverConnector] in flutter_driver_sl4f.dart
final browserDriver =
await browserConnector.driverForIsolate('simple-browser');
_browserDrivers.add(browserDriver);
if (browserDriver == null) {
fail('unable to connect to simple browser.');
}
await browserDriver.waitUntilNoTransientCallbacks();
return browserDriver;
}
/// Returns a web driver connected to a given URL.
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;
}
}