diff --git a/dwds/debug_extension_mv3/tool/build_extension.dart b/dwds/debug_extension_mv3/tool/build_extension.dart index 4ed884ed9..05b35a1dc 100644 --- a/dwds/debug_extension_mv3/tool/build_extension.dart +++ b/dwds/debug_extension_mv3/tool/build_extension.dart @@ -10,6 +10,7 @@ // Run from the extension root directory: // - For dev: dart run tool/build_extension.dart // - For prod: dart run tool/build_extension.dart prod +// - For MV3: dart run tool/build_extension.dart --mv3 import 'dart:async'; import 'dart:convert'; @@ -19,20 +20,26 @@ import 'package:args/args.dart'; import 'package:path/path.dart' as p; const _prodFlag = 'prod'; +const _mv3Flag = 'mv3'; void main(List arguments) async { final parser = ArgParser() - ..addFlag(_prodFlag, negatable: true, defaultsTo: false); + ..addFlag(_prodFlag, negatable: true, defaultsTo: false) + ..addFlag(_mv3Flag, negatable: true, defaultsTo: false); final argResults = parser.parse(arguments); - exitCode = await run(isProd: argResults[_prodFlag] as bool); + exitCode = await run( + isProd: argResults[_prodFlag] as bool, + isMV3: argResults[_mv3Flag] as bool, + ); if (exitCode != 0) { _logWarning('Run terminated unexpectedly with exit code: $exitCode'); } } -Future run({required bool isProd}) async { - _logInfo('Building extension for ${isProd ? 'prod' : 'dev'}'); +Future run({required bool isProd, required bool isMV3}) async { + _logInfo( + 'Building ${isMV3 ? 'MV3' : 'MV2'} extension for ${isProd ? 'prod' : 'dev'}'); _logInfo('Compiling extension with dart2js to /compiled directory'); final compileStep = await Process.start( 'dart', @@ -43,6 +50,17 @@ Future run({required bool isProd}) async { if (compileExitCode != 0) { return compileExitCode; } + final manifestFileName = isMV3 ? 'manifest_mv3' : 'manifest_mv2'; + _logInfo('Copying manifest.json to /compiled directory'); + try { + File(p.join('web', '$manifestFileName.json')).copySync( + p.join('compiled', 'manifest.json'), + ); + } catch (error) { + _logWarning('Copying manifest file failed: $error'); + // Return non-zero exit code to indicate failure: + return 1; + } _logInfo('Updating manifest.json in /compiled directory.'); final updateStep = await Process.start( 'dart', diff --git a/dwds/debug_extension_mv3/tool/update_dev_files.dart b/dwds/debug_extension_mv3/tool/update_dev_files.dart index 7cc2f4838..af4176069 100644 --- a/dwds/debug_extension_mv3/tool/update_dev_files.dart +++ b/dwds/debug_extension_mv3/tool/update_dev_files.dart @@ -22,7 +22,7 @@ Future _updateManifestJson() async { _newKeyValue( oldLine: line, newKey: 'name', - newValue: '[DEV] MV3 Dart Debug Extension', + newValue: '[DEV] Dart Debug Extension', ), if (extensionKey != null) _newKeyValue( diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart index 9d1a913f0..52c398393 100644 --- a/dwds/debug_extension_mv3/web/chrome_api.dart +++ b/dwds/debug_extension_mv3/web/chrome_api.dart @@ -12,42 +12,16 @@ external Chrome get chrome; @JS() @anonymous class Chrome { - external Action get action; external Debugger get debugger; external Devtools get devtools; external Notifications get notifications; external Runtime get runtime; - external Scripting get scripting; external Storage get storage; external Tabs get tabs; external WebNavigation get webNavigation; external Windows get windows; } -/// chrome.action APIs -/// https://developer.chrome.com/docs/extensions/reference/action - -@JS() -@anonymous -class Action { - external void setIcon(IconInfo iconInfo, Function? callback); - - external OnClickedHandler get onClicked; -} - -@JS() -@anonymous -class OnClickedHandler { - external void addListener(void Function(Tab tab) callback); -} - -@JS() -@anonymous -class IconInfo { - external String get path; - external factory IconInfo({String path}); -} - /// chrome.debugger APIs: /// https://developer.chrome.com/docs/extensions/reference/debugger @@ -57,7 +31,7 @@ class Debugger { external void attach( Debuggee target, String requiredVersion, Function? callback); - external Object detach(Debuggee target); + external void detach(Debuggee target, Function? callback); external void sendCommand(Debuggee target, String method, Object? commandParams, Function? callback); @@ -224,30 +198,6 @@ class MessageSender { external factory MessageSender({String? id, String? url, Tab? tab}); } -/// chrome.scripting APIs -/// https://developer.chrome.com/docs/extensions/reference/scripting - -@JS() -@anonymous -class Scripting { - external Object executeScript(InjectDetails details); -} - -@JS() -@anonymous -class InjectDetails { - external Target get target; - external T? get func; - external List? get args; - external List? get files; - external factory InjectDetails({ - Target target, - T? func, - List? args, - List? files, - }); -} - @JS() @anonymous class Target { diff --git a/dwds/debug_extension_mv3/web/debug_session.dart b/dwds/debug_extension_mv3/web/debug_session.dart index cd6a8fb88..50e289384 100644 --- a/dwds/debug_extension_mv3/web/debug_session.dart +++ b/dwds/debug_extension_mv3/web/debug_session.dart @@ -7,7 +7,6 @@ library debug_session; import 'dart:async'; import 'dart:convert'; -import 'dart:html'; import 'package:built_collection/built_collection.dart'; import 'package:collection/collection.dart' show IterableExtension; @@ -136,15 +135,15 @@ void detachDebugger( final debugSession = _debugSessionForTab(tabId, type: type); if (debugSession == null) return; final debuggee = Debuggee(tabId: debugSession.appTabId); - final detachPromise = chrome.debugger.detach(debuggee); - await promiseToFuture(detachPromise); - final error = chrome.runtime.lastError; - if (error != null) { - debugWarn( - 'Error detaching tab for reason: $reason. Error: ${error.message}'); - } else { - _handleDebuggerDetach(debuggee, reason); - } + chrome.debugger.detach(debuggee, allowInterop(() { + final error = chrome.runtime.lastError; + if (error != null) { + debugWarn( + 'Error detaching tab for reason: $reason. Error: ${error.message}'); + } else { + _handleDebuggerDetach(debuggee, reason); + } + })); } void _registerDebugEventListeners() { diff --git a/dwds/debug_extension_mv3/web/devtools.dart b/dwds/debug_extension_mv3/web/devtools.dart index 68fb0930b..68521425f 100644 --- a/dwds/debug_extension_mv3/web/devtools.dart +++ b/dwds/debug_extension_mv3/web/devtools.dart @@ -27,7 +27,6 @@ void _registerListeners() { Object _, String storageArea, ) { - if (storageArea != 'session') return; _maybeCreatePanels(); })); } diff --git a/dwds/debug_extension_mv3/web/lifeline_ports.dart b/dwds/debug_extension_mv3/web/lifeline_ports.dart index b0820026d..d43ad9b4f 100644 --- a/dwds/debug_extension_mv3/web/lifeline_ports.dart +++ b/dwds/debug_extension_mv3/web/lifeline_ports.dart @@ -20,6 +20,8 @@ Port? _lifelinePort; int? _lifelineTab; Future maybeCreateLifelinePort(int tabId) async { + // This is only necessary for Manifest V3 extensions: + if (!isMV3) return; // Don't create a lifeline port if we already have one (meaning another Dart // app is currently being debugged): if (_lifelinePort != null) { @@ -36,6 +38,8 @@ Future maybeCreateLifelinePort(int tabId) async { } void maybeRemoveLifelinePort(int removedTabId) { + // This is only necessary for Manifest V3 extensions: + if (!isMV3) return; // If the removed Dart tab hosted the lifeline port connection, see if there // are any other Dart tabs to connect to. Otherwise disconnect the port. if (_lifelineTab == removedTabId) { diff --git a/dwds/debug_extension_mv3/web/manifest_mv2.json b/dwds/debug_extension_mv3/web/manifest_mv2.json new file mode 100644 index 000000000..4bc9e4117 --- /dev/null +++ b/dwds/debug_extension_mv3/web/manifest_mv2.json @@ -0,0 +1,32 @@ +{ + "name": "Dart Debug Extension", + "version": "1.31", + "manifest_version": 2, + "devtools_page": "static_assets/devtools.html", + "browser_action": { + "default_icon": "static_assets/dart_dev.png" + }, + "externally_connectable": { + "ids": ["nbkbficgbembimioedhceniahniffgpl"] + }, + "permissions": [ + "debugger", + "notifications", + "storage", + "tabs", + "webNavigation" + ], + "host_permissions": [""], + "background": { + "scripts": ["background.dart.js"] + }, + "content_scripts": [ + { + "matches": [""], + "js": ["detector.dart.js"], + "run_at": "document_end" + } + ], + "web_accessible_resources": ["debug_info.dart.js"], + "options_page": "static_assets/settings.html" +} diff --git a/dwds/debug_extension_mv3/web/manifest.json b/dwds/debug_extension_mv3/web/manifest_mv3.json similarity index 100% rename from dwds/debug_extension_mv3/web/manifest.json rename to dwds/debug_extension_mv3/web/manifest_mv3.json diff --git a/dwds/debug_extension_mv3/web/panel.dart b/dwds/debug_extension_mv3/web/panel.dart index 7acc3c820..6deb642f9 100644 --- a/dwds/debug_extension_mv3/web/panel.dart +++ b/dwds/debug_extension_mv3/web/panel.dart @@ -97,9 +97,6 @@ void _handleRuntimeMessages( } void _handleStorageChanges(Object storageObj, String storageArea) { - // We only care about session storage objects: - if (storageArea != 'session') return; - interceptStorageChange( storageObj: storageObj, expectedType: StorageObject.debugInfo, diff --git a/dwds/debug_extension_mv3/web/storage.dart b/dwds/debug_extension_mv3/web/storage.dart index 4e89cd090..05b7b0d4f 100644 --- a/dwds/debug_extension_mv3/web/storage.dart +++ b/dwds/debug_extension_mv3/web/storage.dart @@ -14,6 +14,7 @@ import 'package:js/js.dart'; import 'chrome_api.dart'; import 'data_serializers.dart'; import 'logger.dart'; +import 'utils.dart'; enum StorageObject { debugInfo, @@ -131,6 +132,9 @@ void interceptStorageChange({ } StorageArea _getStorageArea(Persistance persistance) { + // MV2 extensions don't have access to session storage: + if (!isMV3) return chrome.storage.local; + switch (persistance) { case Persistance.acrossSessions: return chrome.storage.local; diff --git a/dwds/debug_extension_mv3/web/utils.dart b/dwds/debug_extension_mv3/web/utils.dart index 451a66fc6..1f7aef1ae 100644 --- a/dwds/debug_extension_mv3/web/utils.dart +++ b/dwds/debug_extension_mv3/web/utils.dart @@ -11,6 +11,7 @@ import 'dart:js_util'; import 'package:js/js.dart'; import 'chrome_api.dart'; +import 'logger.dart'; Future createTab(String url, {bool inNewWindow = false}) { final completer = Completer(); @@ -69,19 +70,32 @@ Future removeTab(int tabId) { } Future injectScript(String scriptName, {required int tabId}) async { - await promiseToFuture(chrome.scripting.executeScript(InjectDetails( - target: Target(tabId: tabId), - files: [scriptName], - ))); - return true; + if (isMV3) { + await promiseToFuture(_executeScriptMV3(_InjectDetails( + target: Target(tabId: tabId), + files: [scriptName], + ))); + return true; + } else { + debugWarn('Script injection is only supported in Manifest V3.'); + return false; + } } void onExtensionIconClicked(void Function(Tab) callback) { - chrome.action.onClicked.addListener(callback); + if (isMV3) { + _onExtensionIconClickedMV3(callback); + } else { + _onExtensionIconClickedMV2(callback); + } } void setExtensionIcon(IconInfo info) { - chrome.action.setIcon(info, /*callback*/ null); + if (isMV3) { + _setExtensionIconMV3(info, /*callback*/ null); + } else { + _setExtensionIconMV2(info, /*callback*/ null); + } } bool? _isDevMode; @@ -97,6 +111,20 @@ bool get isDevMode { return isDevMode; } +bool? _isMV3; + +bool get isMV3 { + if (_isMV3 != null) { + return _isMV3!; + } + final extensionManifest = chrome.runtime.getManifest(); + final manifestVersion = + getProperty(extensionManifest, 'manifest_version') ?? 2; + final isMV3 = manifestVersion == 3; + _isMV3 = isMV3; + return isMV3; +} + String addQueryParameters( String uri, { required Map queryParameters, @@ -108,3 +136,36 @@ String addQueryParameters( }); return newUri.toString(); } + +@JS('chrome.browserAction.onClicked.addListener') +external void _onExtensionIconClickedMV2(void Function(Tab tab) callback); + +@JS('chrome.action.onClicked.addListener') +external void _onExtensionIconClickedMV3(void Function(Tab tab) callback); + +@JS('chrome.browserAction.setIcon') +external void _setExtensionIconMV2(IconInfo iconInfo, Function? callback); + +@JS('chrome.action.setIcon') +external void _setExtensionIconMV3(IconInfo iconInfo, Function? callback); + +@JS() +@anonymous +class IconInfo { + external String get path; + external factory IconInfo({required String path}); +} + +@JS('chrome.scripting.executeScript') +external Object _executeScriptMV3(_InjectDetails details); + +@JS() +@anonymous +class _InjectDetails { + external Target get target; + external List? get files; + external factory _InjectDetails({ + Target target, + List? files, + }); +} diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart index de4756674..2c8e08c68 100644 --- a/dwds/test/puppeteer/extension_test.dart +++ b/dwds/test/puppeteer/extension_test.dart @@ -8,7 +8,7 @@ // TODO(elliette): Enable on Linux. 'linux': Skip('https://github.com/dart-lang/webdev/issues/1787'), }) -@Timeout(Duration(minutes: 2)) +@Timeout(Duration(minutes: 5)) import 'dart:async'; import 'dart:convert'; import 'dart:io'; @@ -25,334 +25,67 @@ import '../fixtures/context.dart'; import '../fixtures/utilities.dart'; import 'test_utils.dart'; -final context = TestContext.withSoundNullSafety(); - -enum Panel { debugger, inspector } - -void main() async { - group('MV3 Debug Extension', () { - late String extensionPath; +// To run all tests: +// dart test test/puppeteer/extension_test.dart --r=expanded --no-retry - setUpAll(() async { - extensionPath = await buildDebugExtension(); - }); - - for (var useSse in [true, false]) { - group(useSse ? 'connected with SSE:' : 'connected with WebSockets:', () { - late Browser browser; - late Worker worker; +// To run the MV3 tests only: +// dart test test/puppeteer/extension_test.dart --r=expanded --no-retry --n="MV3 Debug Extension" - setUpAll(() async { - browser = await setUpExtensionTest( - context, - extensionPath: extensionPath, - serveDevTools: true, - useSse: useSse, - ); - worker = await getServiceWorker(browser); - - // Navigate to the Chrome extension page instead of the blank tab - // opened by Chrome. This is helpful for local debugging. - final blankTab = await navigateToPage(browser, url: 'about:blank'); - await blankTab.goto('chrome://extensions/'); - }); - - tearDown(() async { - await tearDownHelper(worker: worker); - }); +// To run the MV2 tests only: +// dart test test/puppeteer/extension_test.dart --r=expanded --no-retry --n="MV2 Debug Extension" - tearDownAll(() async { - await browser.close(); - }); - - test('the debug info for a Dart app is saved in session storage', - () async { - final appUrl = context.appUrl; - // Navigate to the Dart app: - final appTab = - await navigateToPage(browser, url: appUrl, isNew: true); - // Verify that we have debug info for the Dart app: - await workerEvalDelay(); - final appTabId = await _getTabId(appUrl, worker: worker); - final debugInfoKey = '$appTabId-debugInfo'; - final debugInfo = await _fetchStorageObj( - debugInfoKey, - storageArea: 'session', - worker: worker, - ); - expect(debugInfo.appId, isNotNull); - expect(debugInfo.appEntrypointPath, isNotNull); - expect(debugInfo.appInstanceId, isNotNull); - expect(debugInfo.appOrigin, isNotNull); - expect(debugInfo.appUrl, isNotNull); - expect(debugInfo.isInternalBuild, isNotNull); - expect(debugInfo.isFlutterApp, isNotNull); - await appTab.close(); - }); - - test('the auth status is saved in session storage', () async { - final appUrl = context.appUrl; - // Navigate to the Dart app: - final appTab = - await navigateToPage(browser, url: appUrl, isNew: true); - // Verify that we have debug info for the Dart app: - await workerEvalDelay(); - final appTabId = await _getTabId(appUrl, worker: worker); - final authKey = '$appTabId-isAuthenticated'; - final authenticated = await _fetchStorageObj( - authKey, - storageArea: 'session', - worker: worker, - ); - expect(authenticated, isNotNull); - expect(authenticated, equals('true')); - await appTab.close(); - }); - - test('whether to open in a new tab or window is saved in local storage', - () async { - // Navigate to the extension settings page: - final extensionOrigin = getExtensionOrigin(browser); - final settingsTab = await navigateToPage( - browser, - url: '$extensionOrigin/static_assets/settings.html', - isNew: true, - ); - // Set the settings to open DevTools in a new window: - await settingsTab.tap('#windowOpt'); - await settingsTab.tap('#saveButton'); - // Wait for the saved message to verify settings have been saved: - await settingsTab.waitForSelector('.show'); - // Close the settings tab: - await settingsTab.close(); - // Check that is has been saved in local storage: - final devToolsOpener = await _fetchStorageObj( - 'devToolsOpener', - storageArea: 'local', - worker: worker, - ); - expect(devToolsOpener.newWindow, isTrue); - }); - - test( - 'can configure opening DevTools in a tab/window with extension settings', - () async { - final appUrl = context.appUrl; - final devToolsUrlFragment = - useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; - // Navigate to the Dart app: - final appTab = - await navigateToPage(browser, url: appUrl, isNew: true); - // Click on the Dart Debug Extension icon: - await workerEvalDelay(); - await clickOnExtensionIcon(worker); - // Verify the extension opened DevTools in the same window: - var devToolsTabTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment)); - var devToolsTab = await devToolsTabTarget.page; - var devToolsWindowId = await _getWindowId( - devToolsTab.url!, - worker: worker, - ); - var appWindowId = await _getWindowId(appUrl, worker: worker); - expect(devToolsWindowId == appWindowId, isTrue); - // Close the DevTools tab: - devToolsTab = await devToolsTabTarget.page; - await devToolsTab.close(); - // Navigate to the extension settings page: - final extensionOrigin = getExtensionOrigin(browser); - final settingsTab = await navigateToPage( - browser, - url: '$extensionOrigin/static_assets/settings.html', - isNew: true, - ); - // Set the settings to open DevTools in a new window: - await settingsTab.tap('#windowOpt'); - await settingsTab.tap('#saveButton'); - // Wait for the saved message to verify settings have been saved: - await settingsTab.waitForSelector('.show'); - // Close the settings tab: - await settingsTab.close(); - // Navigate to the Dart app: - await navigateToPage(browser, url: appUrl); - // Click on the Dart Debug Extension icon: - await clickOnExtensionIcon(worker); - // Verify the extension opened DevTools in a different window: - devToolsTabTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment)); - devToolsTab = await devToolsTabTarget.page; - devToolsWindowId = await _getWindowId( - devToolsTab.url!, - worker: worker, - ); - appWindowId = await _getWindowId(appUrl, worker: worker); - expect(devToolsWindowId == appWindowId, isFalse); - // Close the DevTools tab: - devToolsTab = await devToolsTabTarget.page; - await devToolsTab.close(); - await appTab.close(); - }); - - test('DevTools is opened with the correct query parameters', () async { - final appUrl = context.appUrl; - final devToolsUrlFragment = - useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; - // Navigate to the Dart app: - final appTab = - await navigateToPage(browser, url: appUrl, isNew: true); - // Click on the Dart Debug Extension icon: - await workerEvalDelay(); - await clickOnExtensionIcon(worker); - // Wait for DevTools to open: - final devToolsTabTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment)); - final devToolsUrl = devToolsTabTarget.url; - // Expect the correct query parameters to be on the DevTools url: - final uri = Uri.parse(devToolsUrl); - final queryParameters = uri.queryParameters; - expect(queryParameters.keys, unorderedMatches(['uri', 'ide'])); - expect(queryParameters, containsPair('ide', 'DebugExtension')); - expect(queryParameters, containsPair('uri', isNotEmpty)); - // Close the DevTools tab: - final devToolsTab = await devToolsTabTarget.page; - await devToolsTab.close(); - await appTab.close(); - }); - - test( - 'navigating away from the Dart app while debugging closes DevTools', - () async { - final appUrl = context.appUrl; - final devToolsUrlFragment = - useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; - // Navigate to the Dart app: - final appTab = - await navigateToPage(browser, url: appUrl, isNew: true); - // Click on the Dart Debug Extension icon: - await workerEvalDelay(); - await clickOnExtensionIcon(worker); - // Verify that the Dart DevTools tab is open: - final devToolsTabTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment)); - expect(devToolsTabTarget.type, equals('page')); - // Navigate away from the Dart app: - await appTab.goto('https://dart.dev/', wait: Until.domContentLoaded); - await appTab.bringToFront(); - // Verify that the Dart DevTools tab closes: - await devToolsTabTarget.onClose; - await appTab.close(); - }); +final context = TestContext.withSoundNullSafety(); - test('closing the Dart app while debugging closes DevTools', () async { - final appUrl = context.appUrl; - final devToolsUrlFragment = - useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; - // Navigate to the Dart app: - final appTab = - await navigateToPage(browser, url: appUrl, isNew: true); - // Click on the Dart Debug Extension icon: - await workerEvalDelay(); - await clickOnExtensionIcon(worker); - // Verify that the Dart DevTools tab is open: - final devToolsTabTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment)); - expect(devToolsTabTarget.type, equals('page')); - // Close the Dart app: - await appTab.close(); - // Verify that the Dart DevTools tab closes: - await devToolsTabTarget.onClose; - }); +enum Panel { debugger, inspector } - test('Clicking extension icon while debugging shows warning', () async { - final appUrl = context.appUrl; - final devToolsUrlFragment = - useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; - // Navigate to the Dart app: - final appTab = - await navigateToPage(browser, url: appUrl, isNew: true); - // Click on the Dart Debug Extension icon: - await workerEvalDelay(); - await clickOnExtensionIcon(worker); - // Wait for Dart Devtools to open: - final devToolsTabTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment)); - // There should be no warning notifications: - var chromeNotifications = await evaluate( - _getNotifications(), - worker: worker, - ); - expect(chromeNotifications, isEmpty); - // Navigate back to Dart app: - await navigateToPage(browser, url: appUrl, isNew: false); - // Click on the Dart Debug Extension icon again: - await workerEvalDelay(); - await clickOnExtensionIcon(worker); - await workerEvalDelay(); - // There should now be a warning notificiation: - chromeNotifications = - await evaluate(_getNotifications(), worker: worker); - expect(chromeNotifications, isNotEmpty); - // Close the Dart app and the associated Dart DevTools: - await appTab.close(); - await devToolsTabTarget.onClose; - }); +void main() async { + for (var isMV3 in [true, false]) { + group('${isMV3 ? 'MV3' : 'MV2'} Debug Extension', () { + late String extensionPath; - test('Refreshing the Dart app does not open a new Dart DevTools', - () async { - final appUrl = context.appUrl; - final devToolsUrlFragment = - useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; - // Navigate to the Dart app: - final appTab = - await navigateToPage(browser, url: appUrl, isNew: true); - // Click on the Dart Debug Extension icon: - await workerEvalDelay(); - await clickOnExtensionIcon(worker); - // Verify that the Dart DevTools tab is open: - final devToolsTabTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment)); - expect(devToolsTabTarget.type, equals('page')); - // Refresh the app tab: - await appTab.reload(); - // Verify that we don't open a new Dart DevTools on page refresh: - final devToolsTargets = browser.targets - .where((target) => target.url.contains(devToolsUrlFragment)); - expect(devToolsTargets.length, equals(1)); - // Close the Dart app and the associated Dart DevTools: - await appTab.close(); - await devToolsTabTarget.onClose; - }); + setUpAll(() async { + extensionPath = await buildDebugExtension(isMV3: isMV3); }); - } - group('connected to an externally-built', () { - for (var isFlutterApp in [true, false]) { - group(isFlutterApp ? 'Flutter app:' : 'Dart app:', () { + for (var useSse in [true, false]) { + group(useSse ? 'connected with SSE:' : 'connected with WebSockets:', + () { late Browser browser; - late Worker worker; + Worker? worker; + Page? backgroundPage; setUpAll(() async { browser = await setUpExtensionTest( context, extensionPath: extensionPath, serveDevTools: true, - isInternalBuild: false, - isFlutterApp: isFlutterApp, - openChromeDevTools: true, + useSse: useSse, ); - worker = await getServiceWorker(browser); + if (isMV3) { + worker = await getServiceWorker(browser); + } else { + backgroundPage = await getBackgroundPage(browser); + } + + // Navigate to the Chrome extension page instead of the blank tab + // opened by Chrome. This is helpful for local debugging. + final blankTab = await navigateToPage(browser, url: 'about:blank'); + await blankTab.goto('chrome://extensions/'); }); tearDown(() async { - await tearDownHelper(worker: worker); + await tearDownHelper( + worker: worker, + backgroundPage: backgroundPage, + ); }); tearDownAll(() async { await browser.close(); }); - test( - 'isFlutterApp=$isFlutterApp and isInternalBuild=false are saved in storage', + + test('the debug info for a Dart app is saved in session storage', () async { final appUrl = context.appUrl; // Navigate to the Dart app: @@ -360,293 +93,674 @@ void main() async { await navigateToPage(browser, url: appUrl, isNew: true); // Verify that we have debug info for the Dart app: await workerEvalDelay(); - final appTabId = await _getTabId(appUrl, worker: worker); + final appTabId = await _getTabId( + appUrl, + worker: worker, + backgroundPage: backgroundPage, + ); final debugInfoKey = '$appTabId-debugInfo'; final debugInfo = await _fetchStorageObj( debugInfoKey, storageArea: 'session', worker: worker, + backgroundPage: backgroundPage, ); - expect(debugInfo.isInternalBuild, equals(false)); - expect(debugInfo.isFlutterApp, equals(isFlutterApp)); + expect(debugInfo.appId, isNotNull); + expect(debugInfo.appEntrypointPath, isNotNull); + expect(debugInfo.appInstanceId, isNotNull); + expect(debugInfo.appOrigin, isNotNull); + expect(debugInfo.appUrl, isNotNull); + expect(debugInfo.isInternalBuild, isNotNull); + expect(debugInfo.isFlutterApp, isNotNull); await appTab.close(); }); - test('no additional panels are added in Chrome DevTools', () async { + test('the auth status is saved in session storage', () async { final appUrl = context.appUrl; - // This is the blank page automatically opened by Chrome: - final blankTab = await navigateToPage(browser, url: 'about:blank'); // Navigate to the Dart app: - await blankTab.goto(appUrl, wait: Until.domContentLoaded); - final appTab = blankTab; - await appTab.bringToFront(); - final chromeDevToolsTarget = browser.targets.firstWhere( - (target) => target.url.startsWith('devtools://devtools')); - chromeDevToolsTarget.type = 'page'; - final chromeDevToolsPage = await chromeDevToolsTarget.page; - _tabLeft(chromeDevToolsPage); - await _takeScreenshot(chromeDevToolsPage, - screenshotName: 'chromeDevTools_externalBuild'); - final inspectorPanelTarget = browser.targets - .firstWhereOrNull((target) => target.url == 'inspector_panel'); - expect(inspectorPanelTarget, isNull); - final debuggerPanelTarget = browser.targets - .firstWhereOrNull((target) => target.url == 'debugger_panel'); - expect(debuggerPanelTarget, isNull); + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Verify that we have debug info for the Dart app: + await workerEvalDelay(); + final appTabId = await _getTabId( + appUrl, + worker: worker, + backgroundPage: backgroundPage, + ); + final authKey = '$appTabId-isAuthenticated'; + final authenticated = await _fetchStorageObj( + authKey, + storageArea: 'session', + worker: worker, + backgroundPage: backgroundPage, + ); + expect(authenticated, isNotNull); + expect(authenticated, equals('true')); + await appTab.close(); }); - }); - } - }); - group('connected to an internally-built', () { - late Page appTab; - - for (var isFlutterApp in [true, false]) { - group(isFlutterApp ? 'Flutter app:' : 'Dart app:', () { - late Browser browser; - late Worker worker; - - setUpAll(() async { - browser = await setUpExtensionTest( - context, - extensionPath: extensionPath, - serveDevTools: true, - isInternalBuild: true, - isFlutterApp: isFlutterApp, - // TODO(elliette): Figure out if there is a way to close and then - // re-open Chrome DevTools. That way we can test that a debug - // session lasts across Chrome DevTools being opened and closed. - openChromeDevTools: true, + test( + 'whether to open in a new tab or window is saved in local storage', + () async { + // Navigate to the extension settings page: + final extensionOrigin = getExtensionOrigin(browser); + final settingsTab = await navigateToPage( + browser, + url: '$extensionOrigin/static_assets/settings.html', + isNew: true, ); - - worker = await getServiceWorker(browser); + // Set the settings to open DevTools in a new window: + await settingsTab.tap('#windowOpt'); + await settingsTab.tap('#saveButton'); + // Wait for the saved message to verify settings have been saved: + await settingsTab.waitForSelector('.show'); + // Close the settings tab: + await settingsTab.close(); + // Check that is has been saved in local storage: + final devToolsOpener = await _fetchStorageObj( + 'devToolsOpener', + storageArea: 'local', + worker: worker, + backgroundPage: backgroundPage, + ); + expect(devToolsOpener.newWindow, isTrue); }); - setUp(() async { - for (final page in await browser.pages) { - await page.close().catchError((_) {}); - } - appTab = await navigateToPage( + test( + 'can configure opening DevTools in a tab/window with extension settings', + () async { + final appUrl = context.appUrl; + final devToolsUrlFragment = + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Click on the Dart Debug Extension icon: + await workerEvalDelay(); + await clickOnExtensionIcon( + worker: worker, + backgroundPage: backgroundPage, + ); + // Verify the extension opened DevTools in the same window: + var devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + var devToolsTab = await devToolsTabTarget.page; + var devToolsWindowId = await _getWindowId( + devToolsTab.url!, + worker: worker, + backgroundPage: backgroundPage, + ); + var appWindowId = await _getWindowId( + appUrl, + worker: worker, + backgroundPage: backgroundPage, + ); + expect(devToolsWindowId == appWindowId, isTrue); + // Close the DevTools tab: + devToolsTab = await devToolsTabTarget.page; + await devToolsTab.close(); + // Navigate to the extension settings page: + final extensionOrigin = getExtensionOrigin(browser); + final settingsTab = await navigateToPage( browser, - url: context.appUrl, + url: '$extensionOrigin/static_assets/settings.html', isNew: true, ); + // Set the settings to open DevTools in a new window: + await settingsTab.tap('#windowOpt'); + await settingsTab.tap('#saveButton'); + // Wait for the saved message to verify settings have been saved: + await settingsTab.waitForSelector('.show'); + // Close the settings tab: + await settingsTab.close(); + // Navigate to the Dart app: + await navigateToPage(browser, url: appUrl); + // Click on the Dart Debug Extension icon: + await clickOnExtensionIcon( + worker: worker, + backgroundPage: backgroundPage, + ); + // Verify the extension opened DevTools in a different window: + devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + devToolsTab = await devToolsTabTarget.page; + devToolsWindowId = await _getWindowId( + devToolsTab.url!, + worker: worker, + backgroundPage: backgroundPage, + ); + appWindowId = await _getWindowId( + appUrl, + worker: worker, + backgroundPage: backgroundPage, + ); + expect(devToolsWindowId == appWindowId, isFalse); + // Close the DevTools tab: + devToolsTab = await devToolsTabTarget.page; + await devToolsTab.close(); + await appTab.close(); }); - tearDown(() async { - await tearDownHelper(worker: worker); + test('DevTools is opened with the correct query parameters', + () async { + final appUrl = context.appUrl; + final devToolsUrlFragment = + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Click on the Dart Debug Extension icon: + await workerEvalDelay(); + await clickOnExtensionIcon( + worker: worker, + backgroundPage: backgroundPage, + ); + // Wait for DevTools to open: + final devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + final devToolsUrl = devToolsTabTarget.url; + // Expect the correct query parameters to be on the DevTools url: + final uri = Uri.parse(devToolsUrl); + final queryParameters = uri.queryParameters; + expect(queryParameters.keys, unorderedMatches(['uri', 'ide'])); + expect(queryParameters, containsPair('ide', 'DebugExtension')); + expect(queryParameters, containsPair('uri', isNotEmpty)); + // Close the DevTools tab: + final devToolsTab = await devToolsTabTarget.page; + await devToolsTab.close(); + await appTab.close(); }); - tearDownAll(() async { - await browser.close(); - }); test( - 'isFlutterApp=$isFlutterApp and isInternalBuild=true are saved in storage', + 'navigating away from the Dart app while debugging closes DevTools', () async { final appUrl = context.appUrl; - // Verify that we have debug info for the Dart app: + final devToolsUrlFragment = + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Click on the Dart Debug Extension icon: await workerEvalDelay(); - final appTabId = await _getTabId(appUrl, worker: worker); - final debugInfoKey = '$appTabId-debugInfo'; - final debugInfo = await _fetchStorageObj( - debugInfoKey, - storageArea: 'session', + await clickOnExtensionIcon( worker: worker, + backgroundPage: backgroundPage, ); - expect(debugInfo.isInternalBuild, equals(true)); - expect(debugInfo.isFlutterApp, equals(isFlutterApp)); + // Verify that the Dart DevTools tab is open: + final devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + expect(devToolsTabTarget.type, equals('page')); + // Navigate away from the Dart app: + await appTab.goto('https://dart.dev/', + wait: Until.domContentLoaded); + await appTab.bringToFront(); + // Verify that the Dart DevTools tab closes: + await devToolsTabTarget.onClose; + await appTab.close(); }); - test('the correct extension panels are added to Chrome DevTools', + test('closing the Dart app while debugging closes DevTools', () async { - final chromeDevToolsPage = await getChromeDevToolsPage(browser); - // There are no hooks for when a panel is added to Chrome DevTools, - // therefore we rely on a slight delay: - await Future.delayed(Duration(seconds: 1)); - if (isFlutterApp) { - _tabLeft(chromeDevToolsPage); - final inspectorPanelElement = await _getPanelElement( - browser, - panel: Panel.inspector, - elementSelector: '#panelBody', - ); - expect(inspectorPanelElement, isNotNull); - await _takeScreenshot( - chromeDevToolsPage, - screenshotName: 'inspectorPanelLandingPage_flutterApp', - ); - } - _tabLeft(chromeDevToolsPage); - final debuggerPanelElement = await _getPanelElement( - browser, - panel: Panel.debugger, - elementSelector: '#panelBody', - ); - expect(debuggerPanelElement, isNotNull); - await _takeScreenshot( - chromeDevToolsPage, - screenshotName: - 'debuggerPanelLandingPage_${isFlutterApp ? 'flutterApp' : 'dartApp'}', + final appUrl = context.appUrl; + final devToolsUrlFragment = + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Click on the Dart Debug Extension icon: + await workerEvalDelay(); + await clickOnExtensionIcon( + worker: worker, + backgroundPage: backgroundPage, ); + // Verify that the Dart DevTools tab is open: + final devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + expect(devToolsTabTarget.type, equals('page')); + // Close the Dart app: + await appTab.close(); + // Verify that the Dart DevTools tab closes: + await devToolsTabTarget.onClose; }); - test('Dart DevTools is embedded for debug session lifetime', + test('Clicking extension icon while debugging shows warning', () async { - final chromeDevToolsPage = await getChromeDevToolsPage(browser); - // There are no hooks for when a panel is added to Chrome DevTools, - // therefore we rely on a slight delay: - await Future.delayed(Duration(seconds: 1)); - // Navigate to the Dart Debugger panel: - _tabLeft(chromeDevToolsPage); - if (isFlutterApp) { - _tabLeft(chromeDevToolsPage); - } - await _clickLaunchButton( - browser, - panel: Panel.debugger, - ); - // Expect the Dart DevTools IFRAME to be added: + final appUrl = context.appUrl; final devToolsUrlFragment = - 'ide=ChromeDevTools&embed=true&page=debugger'; - final iframeTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment), + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Click on the Dart Debug Extension icon: + await workerEvalDelay(); + await clickOnExtensionIcon( + worker: worker, + backgroundPage: backgroundPage, ); - var iframeDestroyed = false; - unawaited(iframeTarget.onClose.whenComplete(() { - iframeDestroyed = true; - })); - // TODO(elliette): Figure out how to reliably verify that Dart - // DevTools has loaded, and take screenshot. - expect(iframeTarget, isNotNull); - // Navigate away from the Dart app: - await appTab.goto('https://dart.dev/', - wait: Until.domContentLoaded); - // Expect the Dart DevTools IFRAME to be destroyed: - expect(iframeDestroyed, isTrue); - // Expect the connection lost banner to be visible: - final connectionLostBanner = await _getPanelElement( - browser, - panel: Panel.debugger, - elementSelector: '#warningBanner', + // Wait for Dart Devtools to open: + final devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + // There should be no warning notifications: + var chromeNotifications = await evaluate( + _getNotifications(), + worker: worker, + backgroundPage: backgroundPage, + ); + expect(chromeNotifications, isEmpty); + // Navigate back to Dart app: + await navigateToPage(browser, url: appUrl, isNew: false); + // Click on the Dart Debug Extension icon again: + await workerEvalDelay(); + await clickOnExtensionIcon( + worker: worker, + backgroundPage: backgroundPage, ); - expect(connectionLostBanner, isNotNull); - await _takeScreenshot( - chromeDevToolsPage, - screenshotName: - 'debuggerPanelDisconnected_${isFlutterApp ? 'flutterApp' : 'dartApp'}', + await workerEvalDelay(); + // There should now be a warning notificiation: + chromeNotifications = await evaluate( + _getNotifications(), + worker: worker, + backgroundPage: backgroundPage, ); + expect(chromeNotifications, isNotEmpty); + // Close the Dart app and the associated Dart DevTools: + await appTab.close(); + await devToolsTabTarget.onClose; }); - test('The Dart DevTools IFRAME has the correct query parameters', + test('Refreshing the Dart app does not open a new Dart DevTools', () async { - final chromeDevToolsPage = await getChromeDevToolsPage(browser); - // There are no hooks for when a panel is added to Chrome DevTools, - // therefore we rely on a slight delay: - await Future.delayed(Duration(seconds: 1)); - // Navigate to the Dart Debugger panel: - _tabLeft(chromeDevToolsPage); - if (isFlutterApp) { - _tabLeft(chromeDevToolsPage); - } - await _clickLaunchButton( - browser, - panel: Panel.debugger, - ); - // Expect the Dart DevTools IFRAME to be added: + final appUrl = context.appUrl; final devToolsUrlFragment = - 'ide=ChromeDevTools&embed=true&page=debugger'; - final iframeTarget = await browser.waitForTarget( - (target) => target.url.contains(devToolsUrlFragment), + useSse ? 'debugger?uri=sse' : 'debugger?uri=ws'; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Click on the Dart Debug Extension icon: + await workerEvalDelay(); + await clickOnExtensionIcon( + worker: worker, + backgroundPage: backgroundPage, ); - final iframeUrl = iframeTarget.url; - // Expect the correct query parameters to be on the IFRAME url: - final uri = Uri.parse(iframeUrl); - final queryParameters = uri.queryParameters; - expect( - queryParameters.keys, - unorderedMatches([ - 'uri', - 'ide', - 'embed', - 'page', - 'backgroundColor', - ])); - expect(queryParameters, containsPair('ide', 'ChromeDevTools')); - expect(queryParameters, containsPair('uri', isNotEmpty)); - expect(queryParameters, containsPair('page', isNotEmpty)); - expect( - queryParameters, containsPair('backgroundColor', isNotEmpty)); + // Verify that the Dart DevTools tab is open: + final devToolsTabTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment)); + expect(devToolsTabTarget.type, equals('page')); + // Refresh the app tab: + await appTab.reload(); + // Verify that we don't open a new Dart DevTools on page refresh: + final devToolsTargets = browser.targets + .where((target) => target.url.contains(devToolsUrlFragment)); + expect(devToolsTargets.length, equals(1)); + // Close the Dart app and the associated Dart DevTools: + await appTab.close(); + await devToolsTabTarget.onClose; }); }); } - }); - group('connected to a fake app', () { - final fakeAppPath = webCompatiblePath( - p.split( - absolutePath( - pathFromDwds: p.join( - 'test', - 'puppeteer', - 'fake_app', - 'index.html', - ), - ), - ), - ); - final fakeAppUrl = 'file://$fakeAppPath'; - late Browser browser; - late Worker worker; + group('connected to an externally-built', () { + for (var isFlutterApp in [true, false]) { + group(isFlutterApp ? 'Flutter app:' : 'Dart app:', () { + late Browser browser; + Worker? worker; + Page? backgroundPage; + + setUpAll(() async { + browser = await setUpExtensionTest( + context, + extensionPath: extensionPath, + serveDevTools: true, + isInternalBuild: false, + isFlutterApp: isFlutterApp, + openChromeDevTools: true, + ); - setUpAll(() async { - browser = await puppeteer.launch( - headless: false, - timeout: Duration(seconds: 60), - args: [ - '--load-extension=$extensionPath', - '--disable-extensions-except=$extensionPath', - '--disable-features=DialMediaRouteProvider', - ], - ); - worker = await getServiceWorker(browser); - // Navigate to the Chrome extension page instead of the blank tab - // opened by Chrome. This is helpful for local debugging. - final blankTab = await navigateToPage(browser, url: 'about:blank'); - await blankTab.goto('chrome://extensions/'); + if (isMV3) { + worker = await getServiceWorker(browser); + } else { + backgroundPage = await getBackgroundPage(browser); + } + }); + + tearDown(() async { + await tearDownHelper( + worker: worker, + backgroundPage: backgroundPage, + ); + }); + + tearDownAll(() async { + await browser.close(); + }); + test( + 'isFlutterApp=$isFlutterApp and isInternalBuild=false are saved in storage', + () async { + final appUrl = context.appUrl; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + // Verify that we have debug info for the Dart app: + await workerEvalDelay(); + final appTabId = await _getTabId( + appUrl, + worker: worker, + backgroundPage: backgroundPage, + ); + final debugInfoKey = '$appTabId-debugInfo'; + final debugInfo = await _fetchStorageObj( + debugInfoKey, + storageArea: 'session', + worker: worker, + backgroundPage: backgroundPage, + ); + expect(debugInfo.isInternalBuild, equals(false)); + expect(debugInfo.isFlutterApp, equals(isFlutterApp)); + await appTab.close(); + }); + + test('no additional panels are added in Chrome DevTools', () async { + final appUrl = context.appUrl; + // This is the blank page automatically opened by Chrome: + final blankTab = + await navigateToPage(browser, url: 'about:blank'); + // Navigate to the Dart app: + await blankTab.goto(appUrl, wait: Until.domContentLoaded); + final appTab = blankTab; + await appTab.bringToFront(); + final chromeDevToolsTarget = browser.targets.firstWhere( + (target) => target.url.startsWith('devtools://devtools')); + chromeDevToolsTarget.type = 'page'; + final chromeDevToolsPage = await chromeDevToolsTarget.page; + _tabLeft(chromeDevToolsPage); + await _takeScreenshot(chromeDevToolsPage, + screenshotName: 'chromeDevTools_externalBuild'); + final inspectorPanelTarget = browser.targets.firstWhereOrNull( + (target) => target.url == 'inspector_panel'); + expect(inspectorPanelTarget, isNull); + final debuggerPanelTarget = browser.targets + .firstWhereOrNull((target) => target.url == 'debugger_panel'); + expect(debuggerPanelTarget, isNull); + }); + }); + } }); - tearDown(() async { - await tearDownHelper(worker: worker); - }); + group('connected to an internally-built', () { + late Page appTab; + + for (var isFlutterApp in [true, false]) { + group(isFlutterApp ? 'Flutter app:' : 'Dart app:', () { + late Browser browser; + Worker? worker; + Page? backgroundPage; + + setUpAll(() async { + browser = await setUpExtensionTest( + context, + extensionPath: extensionPath, + serveDevTools: true, + isInternalBuild: true, + isFlutterApp: isFlutterApp, + // TODO(elliette): Figure out if there is a way to close and then + // re-open Chrome DevTools. That way we can test that a debug + // session lasts across Chrome DevTools being opened and closed. + openChromeDevTools: true, + ); + if (isMV3) { + worker = await getServiceWorker(browser); + } else { + backgroundPage = await getBackgroundPage(browser); + } + }); + + setUp(() async { + for (final page in await browser.pages) { + await page.close().catchError((_) {}); + } + appTab = await navigateToPage( + browser, + url: context.appUrl, + isNew: true, + ); + }); - tearDownAll(() async { - await browser.close(); + tearDown(() async { + await tearDownHelper( + worker: worker, + backgroundPage: backgroundPage, + ); + }); + + tearDownAll(() async { + await browser.close(); + }); + test( + 'isFlutterApp=$isFlutterApp and isInternalBuild=true are saved in storage', + () async { + final appUrl = context.appUrl; + // Verify that we have debug info for the Dart app: + await workerEvalDelay(); + final appTabId = await _getTabId( + appUrl, + worker: worker, + backgroundPage: backgroundPage, + ); + final debugInfoKey = '$appTabId-debugInfo'; + final debugInfo = await _fetchStorageObj( + debugInfoKey, + storageArea: 'session', + worker: worker, + backgroundPage: backgroundPage, + ); + expect(debugInfo.isInternalBuild, equals(true)); + expect(debugInfo.isFlutterApp, equals(isFlutterApp)); + }); + + test('the correct extension panels are added to Chrome DevTools', + () async { + final chromeDevToolsPage = await getChromeDevToolsPage(browser); + // There are no hooks for when a panel is added to Chrome DevTools, + // therefore we rely on a slight delay: + await Future.delayed(Duration(seconds: 1)); + if (isFlutterApp) { + _tabLeft(chromeDevToolsPage); + final inspectorPanelElement = await _getPanelElement( + browser, + panel: Panel.inspector, + elementSelector: '#panelBody', + ); + expect(inspectorPanelElement, isNotNull); + await _takeScreenshot( + chromeDevToolsPage, + screenshotName: 'inspectorPanelLandingPage_flutterApp', + ); + } + _tabLeft(chromeDevToolsPage); + final debuggerPanelElement = await _getPanelElement( + browser, + panel: Panel.debugger, + elementSelector: '#panelBody', + ); + expect(debuggerPanelElement, isNotNull); + await _takeScreenshot( + chromeDevToolsPage, + screenshotName: + 'debuggerPanelLandingPage_${isFlutterApp ? 'flutterApp' : 'dartApp'}', + ); + }); + + test('Dart DevTools is embedded for debug session lifetime', + () async { + final chromeDevToolsPage = await getChromeDevToolsPage(browser); + // There are no hooks for when a panel is added to Chrome DevTools, + // therefore we rely on a slight delay: + await Future.delayed(Duration(seconds: 1)); + // Navigate to the Dart Debugger panel: + _tabLeft(chromeDevToolsPage); + if (isFlutterApp) { + _tabLeft(chromeDevToolsPage); + } + await _clickLaunchButton( + browser, + panel: Panel.debugger, + ); + // Expect the Dart DevTools IFRAME to be added: + final devToolsUrlFragment = + 'ide=ChromeDevTools&embed=true&page=debugger'; + final iframeTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment), + ); + var iframeDestroyed = false; + unawaited(iframeTarget.onClose.whenComplete(() { + iframeDestroyed = true; + })); + // TODO(elliette): Figure out how to reliably verify that Dart + // DevTools has loaded, and take screenshot. + expect(iframeTarget, isNotNull); + // Navigate away from the Dart app: + await appTab.goto('https://dart.dev/', + wait: Until.domContentLoaded); + // Expect the Dart DevTools IFRAME to be destroyed: + expect(iframeDestroyed, isTrue); + // Expect the connection lost banner to be visible: + final connectionLostBanner = await _getPanelElement( + browser, + panel: Panel.debugger, + elementSelector: '#warningBanner', + ); + expect(connectionLostBanner, isNotNull); + await _takeScreenshot( + chromeDevToolsPage, + screenshotName: + 'debuggerPanelDisconnected_${isFlutterApp ? 'flutterApp' : 'dartApp'}', + ); + }); + + test('The Dart DevTools IFRAME has the correct query parameters', + () async { + final chromeDevToolsPage = await getChromeDevToolsPage(browser); + // There are no hooks for when a panel is added to Chrome DevTools, + // therefore we rely on a slight delay: + await Future.delayed(Duration(seconds: 1)); + // Navigate to the Dart Debugger panel: + _tabLeft(chromeDevToolsPage); + if (isFlutterApp) { + _tabLeft(chromeDevToolsPage); + } + await _clickLaunchButton( + browser, + panel: Panel.debugger, + ); + // Expect the Dart DevTools IFRAME to be added: + final devToolsUrlFragment = + 'ide=ChromeDevTools&embed=true&page=debugger'; + final iframeTarget = await browser.waitForTarget( + (target) => target.url.contains(devToolsUrlFragment), + ); + final iframeUrl = iframeTarget.url; + // Expect the correct query parameters to be on the IFRAME url: + final uri = Uri.parse(iframeUrl); + final queryParameters = uri.queryParameters; + expect( + queryParameters.keys, + unorderedMatches([ + 'uri', + 'ide', + 'embed', + 'page', + 'backgroundColor', + ])); + expect(queryParameters, containsPair('ide', 'ChromeDevTools')); + expect(queryParameters, containsPair('uri', isNotEmpty)); + expect(queryParameters, containsPair('page', isNotEmpty)); + expect( + queryParameters, containsPair('backgroundColor', isNotEmpty)); + }); + }); + } }); - // Note: This tests that the debug extension still works for DWDS versions - // <17.0.0. Those versions don't send the debug info with the ready event. - // Therefore the values are read from the Window object. - test('reads debug info from Window and saves to storage', () async { - // Navigate to the "Dart" app: - await navigateToPage(browser, url: fakeAppUrl, isNew: true); - // Verify that we have debug info for the fake "Dart" app: - final appTabId = await _getTabId(fakeAppUrl, worker: worker); - final debugInfoKey = '$appTabId-debugInfo'; - final debugInfo = await _fetchStorageObj( - debugInfoKey, - storageArea: 'session', - worker: worker, + group('connected to a fake app', () { + final fakeAppPath = webCompatiblePath( + p.split( + absolutePath( + pathFromDwds: p.join( + 'test', + 'puppeteer', + 'fake_app', + 'index.html', + ), + ), + ), ); - expect(debugInfo.appId, equals('DART_APP_ID')); - expect(debugInfo.appEntrypointPath, equals('DART_ENTRYPOINT_PATH')); - expect(debugInfo.appInstanceId, equals('DART_APP_INSTANCE_ID')); - expect(debugInfo.isInternalBuild, isTrue); - expect(debugInfo.isFlutterApp, isFalse); - expect(debugInfo.appOrigin, isNotNull); - expect(debugInfo.appUrl, isNotNull); + final fakeAppUrl = 'file://$fakeAppPath'; + late Browser browser; + Worker? worker; + Page? backgroundPage; + + setUpAll(() async { + browser = await puppeteer.launch( + headless: false, + timeout: Duration(seconds: 60), + args: [ + '--load-extension=$extensionPath', + '--disable-extensions-except=$extensionPath', + '--disable-features=DialMediaRouteProvider', + ], + ); + if (isMV3) { + worker = await getServiceWorker(browser); + } else { + backgroundPage = await getBackgroundPage(browser); + } + // Navigate to the Chrome extension page instead of the blank tab + // opened by Chrome. This is helpful for local debugging. + final blankTab = await navigateToPage(browser, url: 'about:blank'); + await blankTab.goto('chrome://extensions/'); + }); + + tearDown(() async { + await tearDownHelper( + worker: worker, + backgroundPage: backgroundPage, + ); + }); + + tearDownAll(() async { + await browser.close(); + }); + + // Note: This tests that the debug extension still works for DWDS versions + // <17.0.0. Those versions don't send the debug info with the ready event. + // Therefore the values are read from the Window object. + test('reads debug info from Window and saves to storage', () async { + // Navigate to the "Dart" app: + await navigateToPage(browser, url: fakeAppUrl, isNew: true); + // Verify that we have debug info for the fake "Dart" app: + final appTabId = await _getTabId( + fakeAppUrl, + worker: worker, + backgroundPage: backgroundPage, + ); + final debugInfoKey = '$appTabId-debugInfo'; + final debugInfo = await _fetchStorageObj( + debugInfoKey, + storageArea: 'session', + worker: worker, + backgroundPage: backgroundPage, + ); + expect(debugInfo.appId, equals('DART_APP_ID')); + expect(debugInfo.appEntrypointPath, equals('DART_ENTRYPOINT_PATH')); + expect(debugInfo.appInstanceId, equals('DART_APP_INSTANCE_ID')); + expect(debugInfo.isInternalBuild, isTrue); + expect(debugInfo.isFlutterApp, isFalse); + expect(debugInfo.appOrigin, isNotNull); + expect(debugInfo.appUrl, isNotNull); + }); }); }); - }); + } } Future _clickLaunchButton( @@ -706,38 +820,45 @@ void _tabLeft(Page chromeDevToolsPage) async { Future _getTabId( String url, { - required Worker worker, + Worker? worker, + Page? backgroundPage, }) async { final jsExpression = _tabIdForTabJs(url); return (await evaluate( jsExpression, worker: worker, + backgroundPage: backgroundPage, )) as int; } Future _getWindowId( String url, { - required Worker worker, + Worker? worker, + Page? backgroundPage, }) async { final jsExpression = _windowIdForTabJs(url); return (await evaluate( jsExpression, worker: worker, + backgroundPage: backgroundPage, )) as int?; } Future _fetchStorageObj( String storageKey, { required String storageArea, - required Worker worker, + Worker? worker, + Page? backgroundPage, }) async { final json = await retryFnAsync(() async { final storageObj = await evaluate( _fetchStorageObjJs( storageKey, - storageArea: storageArea, + // Only local storage exists for MV2: + storageArea: worker != null ? storageArea : 'local', ), worker: worker, + backgroundPage: backgroundPage, ); return storageObj[storageKey]; }); @@ -748,9 +869,12 @@ Future _fetchStorageObj( String _tabIdForTabJs(String tabUrl) { return ''' async () => { - const matchingTabs = await chrome.tabs.query({ url: "$tabUrl" }); - const tab = matchingTabs[0]; - return tab.id; + return new Promise((resolve, reject) => { + chrome.tabs.query({url: "$tabUrl"}, (tabs) => { + const tab = tabs[0]; + resolve(tab.id); + }); + }); } '''; } @@ -758,9 +882,12 @@ String _tabIdForTabJs(String tabUrl) { String _windowIdForTabJs(String tabUrl) { return ''' async () => { - const matchingTabs = await chrome.tabs.query({ url: "$tabUrl" }); - const tab = matchingTabs[0]; - return tab.windowId; + return new Promise((resolve, reject) => { + chrome.tabs.query({url: "$tabUrl"}, (tabs) => { + const tab = tabs[0]; + resolve(tab.windowId); + }); + }); } '''; } diff --git a/dwds/test/puppeteer/lifeline_test.dart b/dwds/test/puppeteer/lifeline_test.dart index bd920dcc6..13a0359fd 100644 --- a/dwds/test/puppeteer/lifeline_test.dart +++ b/dwds/test/puppeteer/lifeline_test.dart @@ -23,7 +23,7 @@ void main() async { group('MV3 Debug Extension Lifeline Connection', () { setUpAll(() async { - extensionPath = await buildDebugExtension(); + extensionPath = await buildDebugExtension(isMV3: true); browser = await setUpExtensionTest( context, extensionPath: extensionPath, @@ -52,7 +52,7 @@ void main() async { } }); // Click on the Dart Debug Extension icon to intiate a debug session: - await clickOnExtensionIcon(worker); + await clickOnExtensionIcon(worker: worker, backgroundPage: null); final connectedToPort = await portConnectionFuture; // Verify that we have connected to the port: expect(connectedToPort, isTrue); diff --git a/dwds/test/puppeteer/test_utils.dart b/dwds/test/puppeteer/test_utils.dart index c1ed0b525..5583e34b4 100644 --- a/dwds/test/puppeteer/test_utils.dart +++ b/dwds/test/puppeteer/test_utils.dart @@ -13,18 +13,23 @@ import '../fixtures/context.dart'; import '../fixtures/utilities.dart'; enum ConsoleSource { + background, devTools, worker, } +final _backgroundLogs = []; final _devToolsLogs = []; final _workerLogs = []; -Future buildDebugExtension() async { +Future buildDebugExtension({required bool isMV3}) async { final extensionDir = absolutePath(pathFromDwds: 'debug_extension_mv3'); await Process.run( 'dart', - [p.join('tool', 'build_extension.dart')], + [ + p.join('tool', 'build_extension.dart'), + if (isMV3) '--mv3', + ], workingDirectory: extensionDir, ); return p.join(extensionDir, 'compiled'); @@ -61,11 +66,18 @@ Future setUpExtensionTest( ); } -Future tearDownHelper({required Worker worker}) async { +Future tearDownHelper({ + Worker? worker, + Page? backgroundPage, +}) async { _logConsoleMsgsOnFailure(); + _backgroundLogs.clear(); _workerLogs.clear(); _devToolsLogs.clear(); - await _clearStorage(worker: worker); + await _clearStorage( + worker: worker, + backgroundPage: backgroundPage, + ); } Future getServiceWorker(Browser browser) async { @@ -85,6 +97,20 @@ Future getServiceWorker(Browser browser) async { ); } +Future getBackgroundPage(Browser browser) async { + final backgroundPageTarget = + await browser.waitForTarget((target) => target.type == 'background_page'); + final backgroundPage = await backgroundPageTarget.page; + backgroundPage.onConsole.listen((msg) { + _saveConsoleMsg( + source: ConsoleSource.background, + type: '${msg.type}', + msg: msg.text ?? '', + ); + }); + return backgroundPage; +} + Future getChromeDevToolsPage(Browser browser) async { final chromeDevToolsTarget = browser.targets .firstWhere((target) => target.url.startsWith('devtools://devtools')); @@ -100,12 +126,28 @@ Future getChromeDevToolsPage(Browser browser) async { return chromeDevToolsPage; } -Future evaluate(String jsExpression, {required Worker worker}) async { - return worker.evaluate(jsExpression); +Future evaluate( + String jsExpression, { + Worker? worker, + Page? backgroundPage, +}) async { + if (worker != null) { + assert(backgroundPage == null); + return worker.evaluate(jsExpression); + } else { + return backgroundPage!.evaluate(jsExpression); + } } -Future clickOnExtensionIcon(Worker worker) async { - return evaluate(_clickIconJs, worker: worker); +Future clickOnExtensionIcon({ + Worker? worker, + Page? backgroundPage, +}) async { + return evaluate( + _clickIconJs(isMV3: worker != null), + worker: worker, + backgroundPage: backgroundPage, + ); } // Note: The following delay is required to reduce flakiness. It makes @@ -151,6 +193,9 @@ void _saveConsoleMsg({ final consiseMsg = msg.startsWith('JSHandle:') ? msg.substring(9) : msg; final formatted = 'console.$type: $consiseMsg'; switch (source) { + case ConsoleSource.background: + _backgroundLogs.add(formatted); + break; case ConsoleSource.devTools: _devToolsLogs.add(formatted); break; @@ -161,6 +206,9 @@ void _saveConsoleMsg({ } void _logConsoleMsgsOnFailure() { + if (_backgroundLogs.isNotEmpty) { + printOnFailure(['Background Page logs:', ..._backgroundLogs].join('\n')); + } if (_workerLogs.isNotEmpty) { printOnFailure(['Service Worker logs:', ..._workerLogs].join('\n')); } @@ -179,26 +227,29 @@ Future _getPageForUrl(Browser browser, {required String url}) { } Future _clearStorage({ - required Worker worker, + Worker? worker, + Page? backgroundPage, }) async { return evaluate( - _clearStorageJs, + _clearStorageJs(isMV3: worker != null), worker: worker, + backgroundPage: backgroundPage, ).catchError((_) {}); } -final _clickIconJs = ''' +String _clickIconJs({bool isMV3 = false}) => ''' async () => { - const activeTabs = await chrome.tabs.query({ active: true }); - const tab = activeTabs[0]; - chrome.action.onClicked.dispatch(tab); + const activeTabs = await chrome.tabs.query({ active: true }, (tabs) => { + const tab = tabs[0]; + chrome.${isMV3 ? 'action' : 'browserAction'}.onClicked.dispatch(tab); + }); } '''; -final _clearStorageJs = ''' +String _clearStorageJs({required bool isMV3}) => ''' async () => { await chrome.storage.local.clear(); - await chrome.storage.session.clear(); + ${isMV3 ? 'await chrome.storage.session.clear();' : ''} return true; } ''';