// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file // for details. 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 'package:collection/collection.dart'; import 'package:dwds/data/debug_info.dart'; import 'package:dwds/data/extension_request.dart'; import 'package:dwds/src/servers/extension_backend.dart'; import 'package:dwds/src/utilities/server.dart'; import 'package:path/path.dart' as p; import 'package:puppeteer/puppeteer.dart' hide Response; import 'package:shelf/shelf.dart'; import 'package:shelf_static/shelf_static.dart'; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; import 'package:test_common/utilities.dart'; import '../../debug_extension/web/data_serializers.dart'; import '../fixtures/context.dart'; import '../fixtures/project.dart'; import '../fixtures/utilities.dart'; import 'test_utils.dart'; enum Panel { debugger, inspector } late bool screenshotsEnabled; void testAll({required bool isMV3, required bool screenshotsEnabled}) { screenshotsEnabled = screenshotsEnabled; final provider = TestSdkConfigurationProvider(); final context = TestContext(TestProject.test, provider); tearDownAll(provider.dispose); group('${isMV3 ? 'MV3' : 'MV2'} Debug Extension', () { late String extensionPath; setUpAll(() async { extensionPath = await buildDebugExtension(isMV3: isMV3); }); for (final useSse in [true, false]) { group(useSse ? 'connected with SSE:' : 'connected with WebSockets:', () { late Browser browser; Worker? worker; Page? backgroundPage; setUpAll(() async { browser = await setUpExtensionTest( context, extensionPath: extensionPath, serveDevTools: true, useSse: useSse, workspaceName: 'test-workspace', ); 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(); await context.tearDown(); }); 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 _getCurrentTabId( worker: worker, backgroundPage: backgroundPage, ); final debugInfoKey = '$appTabId-debugInfo'; final debugInfo = await _fetchStorageObj( debugInfoKey, storageArea: 'session', worker: worker, backgroundPage: backgroundPage, ); 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); expect(debugInfo.workspaceName, 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 _getCurrentTabId( 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(); }); 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( browser: browser, 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(); }); 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( browser: browser, 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')); // 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('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( browser: browser, 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('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( browser: browser, worker: worker, backgroundPage: backgroundPage, ); // 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( browser: browser, worker: worker, backgroundPage: backgroundPage, ); await workerEvalDelay(); // There should now be a warning notification: 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( 'Clicking extension icon for a non Dart app shows warning', () async { // Navigate to a page that doesn't contain a Dart app: final tab = await navigateToPage( browser, url: 'https://dart.dev', isNew: true, ); // Click on the Dart Debug Extension icon: await workerEvalDelay(); await clickOnExtensionIcon( browser: browser, worker: worker, backgroundPage: backgroundPage, ); // There should now be a warning notification: final chromeNotifications = await evaluate( _getNotifications(), worker: worker, backgroundPage: backgroundPage, ); await workerEvalDelay(); expect(chromeNotifications, isNotEmpty); // Close the tab: await tab.close(); }, ); 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( browser: browser, 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')); // 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 an externally-built', () { for (final 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, ); 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 _getCurrentTabId( 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; await _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); }); }); } }); group('connected to an internally-built', () { late Page appTab; for (final 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, ); }); tearDown(() async { await tearDownHelper( worker: worker, backgroundPage: backgroundPage, ); }); tearDownAll(() async { await browser.close(); }); test( 'isFlutterApp=$isFlutterApp and isInternalBuild=true are saved in storage', () async { // Verify that we have debug info for the Dart app: await workerEvalDelay(); final appTabId = await _getCurrentTabId( 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) { await _tabLeft(chromeDevToolsPage); final inspectorPanelElement = await _getPanelElement( browser, panel: Panel.inspector, elementSelector: '#panelBody', ); expect(inspectorPanelElement, isNotNull); await _takeScreenshot( chromeDevToolsPage, screenshotName: 'inspectorPanelLandingPage_flutterApp', ); } await _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: await _tabLeft(chromeDevToolsPage); if (isFlutterApp) { await _tabLeft(chromeDevToolsPage); } await _clickLaunchButton(browser, panel: Panel.debugger); // Expect the Dart DevTools IFRAME to be added: final devToolsUrlFragment = 'ide=ChromeDevTools&embed=true&page=debugger'; var 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'}', ); // Navigate back to the Dart app: await appTab.goto(context.appUrl, wait: Until.domContentLoaded); // Click the launch button again await _clickLaunchButton(browser, panel: Panel.debugger); // Expect the Dart DevTools IFRAME to be added again: iframeTarget = await browser.waitForTarget( (target) => target.url.contains(devToolsUrlFragment), ); expect(iframeTarget, isNotNull); }); // TODO(elliette): Pull TestServer out of TestContext, so we can add: // 1. a test case for starting another test app, loading that app in // the tab we were debugging, and being able to reconnect to that // one. // 2. a test case for embedding a Dart app in a tab with the same // origin, and being able to connect to the embedded Dart app. // See https://github.com/dart-lang/webdev/issues/1779 test( 'The Dart DevTools IFRAME has the correct query parameters and path', () 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: await _tabLeft(chromeDevToolsPage); if (isFlutterApp) { await _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), ); expect(uri.path, equals('/')); }, ); test( 'Trying to debug a page with multiple Dart apps 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: await _tabLeft(chromeDevToolsPage); if (isFlutterApp) { await _tabLeft(chromeDevToolsPage); } // Expect there to be no warning banner: var warningMsg = await _evaluateInPanel( browser, panel: Panel.debugger, jsExpression: 'document.querySelector("#warningMsg").innerHTML', ); expect( warningMsg == 'Cannot debug multiple apps in a page.', isFalse, ); // Set the 'data-multiple-dart-apps' attribute on the DOM. await appTab.evaluate(_setMultipleAppsAttributeJs); final appTabId = await _getCurrentTabId( worker: worker, backgroundPage: backgroundPage, ); // Expect multiple apps info to be saved in storage: final storageKey = '$appTabId-multipleAppsDetected'; final multipleAppsDetected = await _fetchStorageObj( storageKey, storageArea: 'session', worker: worker, backgroundPage: backgroundPage, ); expect(multipleAppsDetected, equals('true')); // Expect there to be a warning banner: warningMsg = await _evaluateInPanel( browser, panel: Panel.debugger, jsExpression: 'document.querySelector("#warningMsg").innerHTML', ); await _takeScreenshot( chromeDevToolsPage, screenshotName: 'debuggerMultipleAppsDetected_${isFlutterApp ? 'flutterApp' : 'dartApp'}', ); expect( warningMsg, equals('Cannot debug multiple apps in a page.'), ); }, ); }); } }); group('Backwards-compatible with older DWDS versions', () { final port = 8080; final hostname = 'localhost'; final fakeAppUrl = 'http://$hostname:$port/index.html'; final fakeAppDir = webCompatiblePath( p.split( absolutePath(pathFromDwds: p.join('test', 'puppeteer', 'fake_app')), ), ); late Browser browser; late HttpServer server; Worker? worker; Page? backgroundPage; setUpAll(() async { server = await _fakeServer( hostname: hostname, port: port, assetPath: fakeAppDir, ); 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 server.close(); await browser.close(); }); // Note: This tests that the extension works for DWDS versions <17.0.0. // Those versions don't send the debug info with the ready event // (https://github.com/dart-lang/webdev/pull/1772). 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: final appTab = await navigateToPage( browser, url: fakeAppUrl, isNew: true, ); // Verify that we have debug info for the fake "Dart" app: final appTabId = await _getCurrentTabId( 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); // Close the tab: await appTab.close(); }); // Note: This tests that the extension works for DWDS versions <18.0.0. // Those versions don't support authentication from the injected client // (https://github.com/dart-lang/webdev/pull/1916). Therefore the auth // request is sent from the extension itself. test('clicking on extension icon authenticates the user', () async { // Navigate to the "Dart" app: final appTab = await navigateToPage( browser, url: fakeAppUrl, isNew: true, ); // Wait for debug info to be saved: final appTabId = await _getCurrentTabId( worker: worker, backgroundPage: backgroundPage, ); final debugInfoKey = '$appTabId-debugInfo'; final debugInfo = await _fetchStorageObj( debugInfoKey, storageArea: 'session', worker: worker, backgroundPage: backgroundPage, ); // Verify that the extension URL has been saved (this is what is used // by the extension to create the auth url): expect(debugInfo.extensionUrl, isNotNull); // Click on the extension icon: await clickOnExtensionIcon( browser: browser, worker: worker, backgroundPage: backgroundPage, ); // Verify that the user is now authenticated: final authKey = '$appTabId-isAuthenticated'; final isAuthenticated = await _fetchStorageObj( authKey, storageArea: 'session', worker: worker, backgroundPage: backgroundPage, ); expect(isAuthenticated, equals('true')); // Close the tab: await appTab.close(); }); }); }); } Future _clickLaunchButton(Browser browser, {required Panel panel}) async { try { final launchButton = await _getPanelElement( browser, panel: panel, elementSelector: '#launchDebugConnectionButton', ); // Slight delay to guarantee button is clickable: await Future.delayed(Duration(seconds: 1)); await launchButton!.click(); return true; } catch (_) { return false; } } Future _getPanelPage(Browser browser, {required Panel panel}) async { final panelName = panel == Panel.inspector ? 'inspector_panel' : 'debugger_panel'; var panelTarget = browser.targets.firstWhereOrNull( (target) => target.url.contains(panelName), ); panelTarget ??= await browser.waitForTarget( (target) => target.url.contains(panelName), ); panelTarget.type = 'page'; return await panelTarget.page; } Future _evaluateInPanel( Browser browser, { required Panel panel, required String jsExpression, }) async { final panelPage = await _getPanelPage(browser, panel: panel); final frames = panelPage.frames; final mainFrame = frames[0]; return mainFrame.evaluate(jsExpression); } Future _getPanelElement( Browser browser, { required Panel panel, required String elementSelector, }) async { final panelPage = await _getPanelPage(browser, panel: panel); final frames = panelPage.frames; final mainFrame = frames[0]; final panelElement = await mainFrame.$OrNull(elementSelector); return panelElement; } Future _tabLeft(Page chromeDevToolsPage) async { // TODO(elliette): Detect which environment we are OS we are running // in and update modifier key accordingly. Meta key for MacOs and // Ctrl key for Linux/Windows. final modifierKey = Key.meta; await chromeDevToolsPage.keyboard.down(modifierKey); await chromeDevToolsPage.keyboard.press(Key.bracketLeft); await chromeDevToolsPage.keyboard.up(modifierKey); } Future _getCurrentTabId({Worker? worker, Page? backgroundPage}) async { return (await evaluate( _currentTabIdJs, worker: worker, backgroundPage: backgroundPage, )) as int; } Future _fetchStorageObj( String storageKey, { required String storageArea, Worker? worker, Page? backgroundPage, }) async { final json = await retryFnAsync(() async { final storageObj = await evaluate( _fetchStorageObjJs( storageKey, // Only local storage exists for MV2: storageArea: worker != null ? storageArea : 'local', ), worker: worker, backgroundPage: backgroundPage, ); return storageObj[storageKey]; }); if (T == String) return json as T; return serializers.deserialize(jsonDecode(json)) as T; } String _currentTabIdJs = ''' async () => { return new Promise((resolve, reject) => { chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { const tab = tabs[0]; resolve(tab.id); }); }); } '''; String _fetchStorageObjJs(String storageKey, {required String storageArea}) { return ''' async () => { const storageKey = "$storageKey"; return new Promise((resolve, reject) => { chrome.storage.$storageArea.get(storageKey, (storageObj) => { if (storageObj != null) { resolve(storageObj); } else { resolve(null); } }); }); } '''; } String _getNotifications() { return ''' async () => { return new Promise((resolve, reject) => { chrome.notifications.getAll((notifications) => { resolve(notifications); }); }); } '''; } String _setMultipleAppsAttributeJs = ''' document.documentElement.setAttribute("data-multiple-dart-apps", true); '''; // TODO(https://github.com/dart-lang/webdev/issues/1787): Compare to golden // images. Currently golden comparison is not set up, since this is only run // locally, not as part of our CI test suite. Future _takeScreenshot( Page page, { required String screenshotName, }) async { if (!screenshotsEnabled) return; // Since the DevTools panels are not real "pages" but merely targets we have // coerced into having a "page" type, there doesn't seem to be a way to verify // that the DOM has been loaded. Therefore we use a slight delay before taking // a screenshot. See https://github.com/puppeteer/puppeteer/issues/9371. await Future.delayed(Duration(seconds: 1)); final screenshot = await page.screenshot(); final screenshotPath = p.join( 'test', 'puppeteer', 'test_images', '$screenshotName.png', ); await File(screenshotPath).writeAsBytes(screenshot); } Future _fakeServer({ required String hostname, required int port, required String assetPath, }) async { final server = await startHttpServer(hostname, port: port); final staticHandler = createStaticHandler(assetPath); final cascade = Cascade().add(staticHandler).add(_fakeAuthHandler); serveHttpRequests(server, cascade.handler, (e, s) {}); return server; } Response _fakeAuthHandler(request) { if (request.url.path == authenticationPath) { return Response.ok(authenticationResponse); } return Response.notFound('Not found'); }