mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
204 lines
7.2 KiB
Dart
204 lines
7.2 KiB
Dart
// 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' as math;
|
|
|
|
import 'package:image/image.dart';
|
|
import 'package:pedantic/pedantic.dart';
|
|
import 'package:stack_trace/stack_trace.dart';
|
|
import 'package:test_api/src/backend/runtime.dart';
|
|
import 'package:typed_data/typed_buffers.dart';
|
|
|
|
/// Provides the environment for a specific web browser.
|
|
abstract class BrowserEnvironment {
|
|
/// The [Runtime] used by `package:test` to identify this browser type.
|
|
Runtime get packageTestRuntime;
|
|
|
|
/// The name of the configuration YAML file used to configure `package:test`.
|
|
///
|
|
/// The configuration file is expected to be a direct child of the `web_ui`
|
|
/// directory.
|
|
String get packageTestConfigurationYamlFile;
|
|
|
|
/// Prepares the OS environment to run tests for this browser.
|
|
///
|
|
/// This may include things like staring web drivers, iOS Simulators, and/or
|
|
/// Android emulators.
|
|
///
|
|
/// Typically the browser environment is prepared once and supports multiple
|
|
/// browser instances.
|
|
Future<void> prepareEnvironment();
|
|
|
|
/// Launches a browser instance.
|
|
///
|
|
/// The browser will be directed to open the provided [url].
|
|
///
|
|
/// If [debug] is true and the browser supports debugging, launches the
|
|
/// browser in debug mode by pausing test execution after the code is loaded
|
|
/// but before calling the `main()` function of the test, giving the
|
|
/// developer a chance to set breakpoints.
|
|
Browser launchBrowserInstance(Uri url, {bool debug = false});
|
|
|
|
/// Returns the screenshot manager used by this browser.
|
|
///
|
|
/// If the browser does not support screenshots, returns null.
|
|
ScreenshotManager? getScreenshotManager();
|
|
}
|
|
|
|
/// An interface for running browser instances.
|
|
///
|
|
/// This is intentionally coarse-grained: browsers are controlled primary from
|
|
/// inside a single tab. Thus this interface only provides support for closing
|
|
/// the browser and seeing if it closes itself.
|
|
///
|
|
/// Any errors starting or running the browser process are reported through
|
|
/// [onExit].
|
|
abstract class Browser {
|
|
String get name;
|
|
|
|
/// The Observatory URL for this browser.
|
|
///
|
|
/// Returns `null` for browsers that aren't running the Dart VM, or
|
|
/// if the Observatory URL can't be found.
|
|
Future<Uri>? get observatoryUrl => null;
|
|
|
|
/// The remote debugger URL for this browser.
|
|
///
|
|
/// Returns `null` for browsers that don't support remote debugging,
|
|
/// or if the remote debugging URL can't be found.
|
|
Future<Uri>? get remoteDebuggerUrl => null;
|
|
|
|
/// The underlying process.
|
|
///
|
|
/// This will fire once the process has started successfully.
|
|
Future<Process> get _process => _processCompleter.future;
|
|
final Completer<Process> _processCompleter = Completer<Process>();
|
|
|
|
/// Whether [close] has been called.
|
|
bool _closed = false;
|
|
|
|
/// A future that completes when the browser exits.
|
|
///
|
|
/// If there's a problem starting or running the browser, this will complete
|
|
/// with an error.
|
|
Future<void> get onExit => _onExitCompleter.future;
|
|
final Completer<void> _onExitCompleter = Completer<void>();
|
|
|
|
/// Standard IO streams for the underlying browser process.
|
|
final List<StreamSubscription<void>> _ioSubscriptions = <StreamSubscription<void>>[];
|
|
|
|
/// Creates a new browser.
|
|
///
|
|
/// This is intended to be called by subclasses. They pass in [startBrowser],
|
|
/// which asynchronously returns the browser process. Any errors in
|
|
/// [startBrowser] (even those raised asynchronously after it returns) are
|
|
/// piped to [onExit] and will cause the browser to be killed.
|
|
Browser(Future<Process> startBrowser()) {
|
|
// Don't return a Future here because there's no need for the caller to wait
|
|
// for the process to actually start. They should just wait for the HTTP
|
|
// request instead.
|
|
runZonedGuarded(() async {
|
|
final Process process = await startBrowser();
|
|
_processCompleter.complete(process);
|
|
|
|
final Uint8Buffer output = Uint8Buffer();
|
|
void drainOutput(Stream<List<int>> stream) {
|
|
try {
|
|
_ioSubscriptions
|
|
.add(stream.listen(output.addAll, cancelOnError: true));
|
|
} on StateError catch (_) {}
|
|
}
|
|
|
|
// If we don't drain the stdout and stderr the process can hang.
|
|
drainOutput(process.stdout);
|
|
drainOutput(process.stderr);
|
|
|
|
final int exitCode = await process.exitCode;
|
|
|
|
// This hack dodges an otherwise intractable race condition. When the user
|
|
// presses Control-C, the signal is sent to the browser and the test
|
|
// runner at the same time. It's possible for the browser to exit before
|
|
// the [Browser.close] is called, which would trigger the error below.
|
|
//
|
|
// A negative exit code signals that the process exited due to a signal.
|
|
// However, it's possible that this signal didn't come from the user's
|
|
// Control-C, in which case we do want to throw the error. The only way to
|
|
// resolve the ambiguity is to wait a brief amount of time and see if this
|
|
// browser is actually closed.
|
|
if (!_closed && exitCode < 0) {
|
|
await Future<void>.delayed(Duration(milliseconds: 200));
|
|
}
|
|
|
|
if (!_closed && exitCode != 0) {
|
|
final String outputString = utf8.decode(output);
|
|
String message = '$name failed with exit code $exitCode.';
|
|
if (outputString.isNotEmpty) {
|
|
message += '\nStandard output:\n$outputString';
|
|
}
|
|
|
|
throw Exception(message);
|
|
}
|
|
|
|
_onExitCompleter.complete();
|
|
}, (dynamic error, StackTrace? stackTrace) {
|
|
// Ignore any errors after the browser has been closed.
|
|
if (_closed) {
|
|
return;
|
|
}
|
|
|
|
// Make sure the process dies even if the error wasn't fatal.
|
|
_process.then((Process process) => process.kill());
|
|
|
|
if (stackTrace == null) {
|
|
stackTrace = Trace.current();
|
|
}
|
|
if (_onExitCompleter.isCompleted) {
|
|
return;
|
|
}
|
|
_onExitCompleter.completeError(
|
|
Exception('Failed to run $name: $error.'),
|
|
stackTrace,
|
|
);
|
|
});
|
|
}
|
|
|
|
/// Kills the browser process.
|
|
///
|
|
/// Returns the same [Future] as [onExit], except that it won't emit
|
|
/// exceptions.
|
|
Future<void> close() async {
|
|
_closed = true;
|
|
|
|
// If we don't manually close the stream the test runner can hang.
|
|
// For example this happens with Chrome Headless.
|
|
// See SDK issue: https://github.com/dart-lang/sdk/issues/31264
|
|
for (final StreamSubscription<void> stream in _ioSubscriptions) {
|
|
unawaited(stream.cancel());
|
|
}
|
|
|
|
(await _process).kill();
|
|
|
|
// Swallow exceptions. The user should explicitly use [onExit] for these.
|
|
return onExit.catchError((dynamic _) {});
|
|
}
|
|
}
|
|
|
|
/// Interface for capturing screenshots from a browser.
|
|
abstract class ScreenshotManager {
|
|
/// Capture a screenshot.
|
|
///
|
|
/// Please read the details for the implementing classes.
|
|
Future<Image> capture(math.Rectangle<num> region);
|
|
|
|
/// Suffix to be added to the end of the filename.
|
|
///
|
|
/// Example file names:
|
|
/// - Chrome, no-suffix: backdrop_filter_clip_moved.actual.png
|
|
/// - iOS Safari: backdrop_filter_clip_moved.iOS_Safari.actual.png
|
|
String get filenameSuffix;
|
|
}
|