[web][identity] Test utility for driving Chrome instances from host
This will be used for driving Google login tests.
Testing: Running with WIP Google login test, which tests a single Chrome
instance displaying authentication for OAuth. fx run-host-tests
WEB-40 #comment
AUTH-198 #comment
Change-Id: Ia013d2a11d80d2a03550f783007af7ce1e04dd8c
diff --git a/sdk/testing/sl4f/client/BUILD.gn b/sdk/testing/sl4f/client/BUILD.gn
index 24648f4..97e847b5 100644
--- a/sdk/testing/sl4f/client/BUILD.gn
+++ b/sdk/testing/sl4f/client/BUILD.gn
@@ -24,6 +24,7 @@
"src/sl4f_client.dart",
"src/storage.dart",
"src/traceutil.dart",
+ "src/webdriver.dart",
]
deps = [
@@ -31,6 +32,7 @@
"//third_party/dart-pkg/pub/image",
"//third_party/dart-pkg/pub/logging",
"//third_party/dart-pkg/pub/meta",
+ "//third_party/dart-pkg/pub/webdriver",
]
}
@@ -43,6 +45,7 @@
"performance_test.dart",
"scenic_test.dart",
"storage_test.dart",
+ "webdriver_test.dart",
]
deps = [
diff --git a/sdk/testing/sl4f/client/lib/sl4f.dart b/sdk/testing/sl4f/client/lib/sl4f.dart
index 84a7ff1..396d7c6 100644
--- a/sdk/testing/sl4f/client/lib/sl4f.dart
+++ b/sdk/testing/sl4f/client/lib/sl4f.dart
@@ -17,3 +17,4 @@
export 'src/sl4f_client.dart';
export 'src/storage.dart';
export 'src/traceutil.dart';
+export 'src/webdriver.dart';
diff --git a/sdk/testing/sl4f/client/lib/src/webdriver.dart b/sdk/testing/sl4f/client/lib/src/webdriver.dart
new file mode 100644
index 0000000..7c6ddc1
--- /dev/null
+++ b/sdk/testing/sl4f/client/lib/src/webdriver.dart
@@ -0,0 +1,180 @@
+// 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:io' as io;
+
+import 'package:sl4f/sl4f.dart';
+import 'package:webdriver/sync_core.dart' show WebDriver;
+import 'package:webdriver/sync_io.dart' as sync_io;
+
+/// Port Chromedriver listens on.
+const _chromedriverPort = 9072;
+
+/// `WebDriverConnector` is a utility for host-driven tests that control Chrome
+/// contexts running on a remote device under test(DuT). `WebDriverConnector`
+/// vends `WebDriver` objects connected to remote Chrome instances.
+/// Check the [webdriver package](https://pub.dev/documentation/webdriver/)
+/// documentation for details on using `WebDriver`.
+///
+/// `WebDriverConnector` additionally starts an instance of the ChromeDriver
+/// binary that runs locally on the test host. `WebDriver` instances
+/// communicate with ChromeDriver, which in turn communicates with Chrome
+/// instances on the DuT.
+///
+/// Note that the latest version of Chromedriver currently needs to be manually
+/// downloaded and placed in the location passed when constructing
+/// `WebDriverConnector`. This is necessary as the automatically downloaded
+/// version in prebuilt is not kept up to date with the Chrome version. Effort
+/// for this is tracked in IN-1321.
+/// TODO(satsukiu): Remove notice and add e2e test for facade functionality
+/// on completion of IN-1321
+class WebDriverConnector {
+ /// Relative path of chromedriver binary.
+ final String _chromedriverPath;
+
+ /// SL4F client.
+ final Sl4f _sl4f;
+
+ /// Helper for starting processes.
+ final ProcessHelper _processHelper;
+
+ /// Helper for instantiating WebDriver objects.
+ final WebDriverHelper _webDriverHelper;
+
+ /// A handle to the process running Chromedriver.
+ io.Process _chromedriverProcess;
+
+ /// A mapping from an exposed port number to an open WebDriver session.
+ Map<int, _WebDriverSession> _webDriverSessions;
+
+ WebDriverConnector(String chromeDriverPath, Sl4f sl4f,
+ {ProcessHelper processHelper, WebDriverHelper webDriverHelper})
+ : _chromedriverPath = chromeDriverPath,
+ _sl4f = sl4f,
+ _processHelper = processHelper ?? ProcessHelper(),
+ _webDriverHelper = webDriverHelper ?? WebDriverHelper(),
+ _webDriverSessions = {};
+
+ /// Starts ChromeDriver and enables DevTools for any future created Chrome
+ /// contexts. As this will not enable DevTools on any already opened
+ /// contexts, `initialize` must be called prior to the instantiation of the
+ /// Chrome context that needs to be driven.
+ Future<void> initialize() async {
+ await _sl4f.request('webdriver_facade.EnableDevTools');
+ await _startChromedriver();
+ }
+
+ /// Stops Chromedriver and removes any connections that are still open.
+ void tearDown() {
+ _chromedriverProcess?.kill();
+ _chromedriverProcess = null;
+ for (var session in _webDriverSessions.values) {
+ session.portForwardingProcess.kill();
+ }
+ _webDriverSessions = {};
+ }
+
+ /// Searches for Chrome contexts based on the host of the currently displayed
+ /// page, and returns `WebDriver` connections to the found contexts.
+ Future<List<WebDriver>> webDriversForHost(String host) async {
+ await _updateWebDriverSessions();
+
+ return List.from(_webDriverSessions.values
+ .where(
+ (session) => Uri.parse(session.webDriver.currentUrl).host == host)
+ .map((session) => session.webDriver));
+ }
+
+ /// Starts Chromedriver on the host.
+ Future<void> _startChromedriver() async {
+ if (_chromedriverProcess == null) {
+ final chromedriver =
+ io.Platform.script.resolve(_chromedriverPath).toFilePath();
+ final args = ['--port=$_chromedriverPort'];
+ _chromedriverProcess = await _processHelper.start(chromedriver, args);
+ }
+ }
+
+ /// Updates the set of open WebDriver connections.
+ Future<void> _updateWebDriverSessions() async {
+ var portsResult = await _sl4f.request('webdriver_facade.GetDevToolsPorts');
+ var ports = Set.from(portsResult['ports']);
+
+ // terminate and remove port forwarding for any ports that aren't open anymore
+ _webDriverSessions.removeWhere((port, session) {
+ if (!ports.contains(port)) {
+ session.portForwardingProcess.kill();
+ return true;
+ } else {
+ return false;
+ }
+ });
+
+ // Add new sessions for new ports. For a given Chrome context listening on
+ // port p on the DuT, we forward localhost:p to DuT:p, and create a
+ // WebDriver instance pointing to localhost:p.
+ for (var port in ports) {
+ if (!_webDriverSessions.containsKey(port)) {
+ var forwardProcess = await _processHelper.start(
+ 'ssh',
+ [
+ '-i',
+ _sl4f.sshKeyPath,
+ '-N',
+ '-o',
+ 'UserKnownHostsFile=/dev/null',
+ '-o',
+ 'StrictHostKeyChecking=no',
+ '-L',
+ '$port:localhost:$port',
+ _sl4f.target,
+ ],
+ runInShell: true);
+
+ var webDriver = _webDriverHelper.createDriver(port, _chromedriverPort);
+
+ _webDriverSessions.putIfAbsent(
+ port, () => _WebDriverSession(webDriver, forwardProcess));
+ }
+ }
+ }
+}
+
+/// A wrapper around static dart:io Process methods.
+class ProcessHelper {
+ ProcessHelper();
+
+ /// Start a new process.
+ Future<io.Process> start(String cmd, List<String> args,
+ {bool runInShell = false}) async {
+ return await io.Process.start(cmd, args, runInShell: runInShell);
+ }
+}
+
+/// A wrapper around static WebDriver creation methods.
+class WebDriverHelper {
+ WebDriverHelper();
+
+ /// Create a new WebDriver pointing to Chromedriver on the given uri and with
+ /// given desired capabilities.
+ WebDriver createDriver(int devToolsPort, int chromedriverPort) {
+ var chromeOptions = {'debuggerAddress': 'localhost:$devToolsPort'};
+ var capabilities = sync_io.Capabilities.chrome;
+ capabilities[sync_io.Capabilities.chromeOptions] = chromeOptions;
+ return sync_io.createDriver(
+ desired: capabilities,
+ uri: Uri.parse('http://localhost:$_chromedriverPort'));
+ }
+}
+
+/// A wrapper around WebDriver object that also holds the port forwarding process.
+class _WebDriverSession {
+ /// WebDriver connection.
+ final WebDriver webDriver;
+
+ /// Handle to the process forwarding the port from the host to DuT.
+ final io.Process portForwardingProcess;
+
+ _WebDriverSession(this.webDriver, this.portForwardingProcess);
+}
diff --git a/sdk/testing/sl4f/client/test/webdriver_test.dart b/sdk/testing/sl4f/client/test/webdriver_test.dart
new file mode 100644
index 0000000..46875c8
--- /dev/null
+++ b/sdk/testing/sl4f/client/test/webdriver_test.dart
@@ -0,0 +1,69 @@
+// 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:mockito/mockito.dart';
+import 'package:test/test.dart';
+import 'package:webdriver/sync_core.dart';
+
+import 'package:sl4f/sl4f.dart';
+
+class MockSl4f extends Mock implements Sl4f {}
+
+class MockProcessHelper extends Mock implements ProcessHelper {}
+
+class MockWebDriverHelper extends Mock implements WebDriverHelper {}
+
+class MockWebDriver extends Mock implements WebDriver {}
+
+void main(List<String> args) {
+ MockSl4f sl4f;
+ MockProcessHelper processHelper;
+ MockWebDriverHelper webDriverHelper;
+ WebDriverConnector webDriverConnector;
+
+ setUp(() {
+ sl4f = MockSl4f();
+ processHelper = MockProcessHelper();
+ webDriverHelper = MockWebDriverHelper();
+ webDriverConnector = WebDriverConnector('path/to/chromedriver', sl4f,
+ processHelper: processHelper, webDriverHelper: webDriverHelper);
+ });
+
+ test('webDriversForHost filters by host', () async {
+ var openContexts = {
+ 20000: 'https://www.test.com/path/1',
+ 20001: 'https://www.example.com/path/1',
+ 20002: 'https://www.test.com/path/2',
+ 20003: 'https://www.example.com/path/2'
+ };
+ mockAvailableWebDrivers(webDriverHelper, sl4f, openContexts);
+ var webDrivers = await webDriverConnector.webDriversForHost('www.test.com');
+ expect(webDrivers.length, 2);
+ var webDriverCurrentUrls =
+ Set.from(webDrivers.map((webDriver) => webDriver.currentUrl));
+ expect(webDriverCurrentUrls,
+ {'https://www.test.com/path/1', 'https://www.test.com/path/2'});
+ });
+
+ test('webDriversForHost no contexts', () async {
+ mockAvailableWebDrivers(webDriverHelper, sl4f, {});
+ var webDrivers = await webDriverConnector.webDriversForHost('www.test.com');
+ expect(webDrivers.length, 0);
+ });
+}
+
+/// Set up mocks as if there are chrome contexts with the given ports exposing a url.
+void mockAvailableWebDrivers(MockWebDriverHelper webDriverHelper, MockSl4f sl4f,
+ Map<int, String> portToUrl) {
+ var portList = {'ports': List.from(portToUrl.keys)};
+ print(portList);
+ when(sl4f.request('webdriver_facade.GetDevToolsPorts'))
+ .thenAnswer((_) => Future.value(portList));
+ when(webDriverHelper.createDriver(any, any)).thenAnswer((invocation) {
+ var devToolsPort = invocation.positionalArguments.first;
+ WebDriver webDriver = MockWebDriver();
+ when(webDriver.currentUrl).thenReturn(portToUrl[devToolsPort]);
+ return webDriver;
+ });
+}