diff --git a/engine/src/flutter/lib/web_ui/dev/browser.dart b/engine/src/flutter/lib/web_ui/dev/browser.dart index ed40d295ca..79c328661c 100644 --- a/engine/src/flutter/lib/web_ui/dev/browser.dart +++ b/engine/src/flutter/lib/web_ui/dev/browser.dart @@ -75,6 +75,12 @@ abstract class Browser { /// with an error. Future get onExit; + /// A future that completes if the browser is notified about an uncaught + /// exception. + /// + /// Returns `null` if the browser does not support this. + Future? get onUncaughtException => null; + /// Closes the browser /// /// Returns the same [Future] as [onExit], except that it won't emit diff --git a/engine/src/flutter/lib/web_ui/dev/chrome.dart b/engine/src/flutter/lib/web_ui/dev/chrome.dart index d6ecd3d2da..c3b5d922d6 100644 --- a/engine/src/flutter/lib/web_ui/dev/chrome.dart +++ b/engine/src/flutter/lib/web_ui/dev/chrome.dart @@ -20,6 +20,8 @@ import 'common.dart'; import 'environment.dart'; import 'package_lock.dart'; +const String kBlankPageUrl = 'about:blank'; + /// Provides an environment for desktop Chrome. class ChromeEnvironment implements BrowserEnvironment { ChromeEnvironment({ @@ -82,6 +84,7 @@ class Chrome extends Browser { required bool useDwarf, }) { final Completer remoteDebuggerCompleter = Completer.sync(); + final Completer exceptionCompleter = Completer(); return Chrome._(BrowserProcess(() async { // A good source of various Chrome CLI options: // https://peter.sh/experiments/chromium-command-line-switches/ @@ -98,7 +101,7 @@ class Chrome extends Browser { final String dir = await generateUserDirectory(installation, useDwarf); final List args = [ '--user-data-dir=$dir', - url.toString(), + kBlankPageUrl, if (!debug) '--headless', if (isChromeNoSandbox) @@ -139,6 +142,8 @@ class Chrome extends Browser { final Process process = await _spawnChromiumProcess(installation.executable, args); + await setupChromiumTab(url, exceptionCompleter); + remoteDebuggerCompleter.complete( getRemoteDebuggerUrl(Uri.parse('http://localhost:$kDevtoolsPort'))); @@ -146,10 +151,10 @@ class Chrome extends Browser { .then((_) => Directory(dir).deleteSync(recursive: true))); return process; - }), remoteDebuggerCompleter.future); + }), remoteDebuggerCompleter.future, exceptionCompleter.future); } - Chrome._(this._process, this.remoteDebuggerUrl); + Chrome._(this._process, this.remoteDebuggerUrl, this._onUncaughtException); static Future generateUserDirectory( BrowserInstallation installation, @@ -211,12 +216,17 @@ class Chrome extends Browser { final BrowserProcess _process; + final Future _onUncaughtException; + @override final Future remoteDebuggerUrl; @override Future get onExit => _process.onExit; + @override + Future? get onUncaughtException => _onUncaughtException; + @override Future close() => _process.close(); @@ -378,3 +388,28 @@ Future getRemoteDebuggerUrl(Uri base) async { return base; } } + +Future setupChromiumTab( + Uri url, Completer exceptionCompleter) async { + final wip.ChromeConnection chromeConnection = + wip.ChromeConnection('localhost', kDevtoolsPort); + final wip.ChromeTab? chromeTab = await chromeConnection.getTab( + (wip.ChromeTab chromeTab) => chromeTab.url == kBlankPageUrl); + final wip.WipConnection wipConnection = await chromeTab!.connect(); + + await wipConnection.runtime.enable(); + + wipConnection.runtime.onExceptionThrown.listen( + (wip.ExceptionThrownEvent event) { + if (!exceptionCompleter.isCompleted) { + final String text = event.exceptionDetails.text; + final String? description = event.exceptionDetails.exception?.description; + exceptionCompleter.complete('$text: $description'); + } + } + ); + + await wipConnection.page.enable(); + + await wipConnection.page.navigate(url.toString()); +} diff --git a/engine/src/flutter/lib/web_ui/dev/test_platform.dart b/engine/src/flutter/lib/web_ui/dev/test_platform.dart index 8bc3bc39fd..d2bae4ec18 100644 --- a/engine/src/flutter/lib/web_ui/dev/test_platform.dart +++ b/engine/src/flutter/lib/web_ui/dev/test_platform.dart @@ -1059,7 +1059,18 @@ class BrowserManager { } _controllers.add(controller!); - return await controller!.suite; + + final List> futures = >[ + controller!.suite + ]; + if (_browser.onUncaughtException != null) { + futures.add(_browser.onUncaughtException!.then( + (String error) => + throw Exception('Exception while loading suite: $error'))); + } + + final RunnerSuite suite = await Future.any(futures); + return suite; } catch (_) { closeIframe(); rethrow;