| // Copyright 2013 The Flutter 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:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:math'; |
| |
| import 'package:image/image.dart'; |
| import 'package:webdriver/async_io.dart' show WebDriver, createDriver; |
| |
| import 'browser.dart'; |
| |
| abstract class WebDriverBrowserEnvironment extends BrowserEnvironment { |
| late int portNumber; |
| late final Process _driverProcess; |
| |
| Future<Process> spawnDriverProcess(); |
| Uri get driverUri; |
| |
| /// Finds and returns an unused port on the test host in the local port range. |
| Future<int> pickUnusedPort() async { |
| // Use bind to allocate an unused port, then unbind from that port to |
| // make it available for use. |
| final ServerSocket socket = await ServerSocket.bind('localhost', 0); |
| final int port = socket.port; |
| await socket.close(); |
| |
| return port; |
| } |
| |
| |
| @override |
| Future<void> prepare() async { |
| portNumber = await pickUnusedPort(); |
| |
| _driverProcess = await spawnDriverProcess(); |
| |
| _driverProcess.stderr |
| .transform(utf8.decoder) |
| .transform(const LineSplitter()) |
| .listen((String error) { |
| print('[Webdriver][Error] $error'); |
| }); |
| |
| _driverProcess.stdout |
| .transform(utf8.decoder) |
| .transform(const LineSplitter()) |
| .listen((String log) { |
| print('[Webdriver] $log'); |
| }); |
| } |
| |
| @override |
| Future<void> cleanup() async { |
| _driverProcess.kill(); |
| } |
| |
| @override |
| Future<Browser> launchBrowserInstance(Uri url, {bool debug = false}) async { |
| while (true) { |
| try { |
| final WebDriver driver = await createDriver( |
| uri: driverUri, desired: <String, dynamic>{'browserName': packageTestRuntime.identifier}); |
| return WebDriverBrowser(driver, url); |
| } on SocketException { |
| // Sometimes we may try to connect before the web driver port is ready. |
| // So we should retry here. Note that if there was some issue with the |
| // webdriver process, we may loop infinitely here, so we're relying on |
| // the test timeout to kill us if it takes too long to connect. |
| print('Failed to connect to webdriver process. Retrying in 100 ms'); |
| await Future<void>.delayed(const Duration(milliseconds: 100)); |
| } catch (exception) { |
| rethrow; |
| } |
| } |
| } |
| } |
| |
| class WebDriverBrowser extends Browser { |
| WebDriverBrowser(this._driver, this._url) { |
| _driver.get(_url); |
| _activateLoopFuture = () async { |
| // Some browsers (i.e. Safari) stop actually executing our unit tests if |
| // their window is occluded or non-visible. This hacky solution of |
| // re-activating the window every two seconds prevents our unit tests from |
| // stalling out if the window becomes obscured by some other thing that |
| // may appear on the system. |
| while (!_shouldStopActivating) { |
| await (await _driver.window).setAsActive(); |
| await Future<void>.delayed(const Duration(seconds: 2)); |
| } |
| }(); |
| } |
| |
| final WebDriver _driver; |
| final Uri _url; |
| final Completer<void> _onExitCompleter = Completer<void>(); |
| bool _shouldStopActivating = false; |
| late final Future<void> _activateLoopFuture; |
| |
| @override |
| Future<void> close() async { |
| _shouldStopActivating = true; |
| await _activateLoopFuture; |
| |
| await (await _driver.window).close(); |
| if (!_onExitCompleter.isCompleted) { |
| _onExitCompleter.complete(); |
| } |
| } |
| |
| @override |
| Future<void> get onExit => _onExitCompleter.future; |
| |
| @override |
| bool get supportsScreenshots => true; |
| |
| @override |
| Future<Image> captureScreenshot(Rectangle<num> region) async { |
| final Image image = decodePng(await _driver.captureScreenshotAsList())!; |
| return copyCrop(image, region.left.round(), region.top.round(), |
| region.width.round(), region.height.round()); |
| } |
| } |