diff --git a/dwds/test/puppeteer/extension_common.dart b/dwds/test/puppeteer/extension_common.dart new file mode 100644 index 000000000..478095adf --- /dev/null +++ b/dwds/test/puppeteer/extension_common.dart @@ -0,0 +1,1139 @@ +// 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_mv3/web/data_serializers.dart'; +import '../../debug_extension_mv3/web/data_types.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.testWithSoundNullSafety, provider); + tearDownAll(provider.dispose); + + group('${isMV3 ? 'MV3' : 'MV2'} Debug Extension', () { + late String extensionPath; + + setUpAll(() async { + extensionPath = await buildDebugExtension(isMV3: isMV3); + }); + + for (var 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, + ); + + 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); + 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('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, + backgroundPage: backgroundPage, + ); + 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); + final appWindowId = await _getCurrentWindowId( + worker: worker, + backgroundPage: backgroundPage, + ); + // 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; + // Navigate to the newly opened DevTools tab: + await navigateToPage( + browser, + url: devToolsTabTarget.url, + ); + var devToolsWindowId = await _getCurrentWindowId( + 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: '$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; + // Navigate to the newly opened DevTools tab: + await navigateToPage( + browser, + url: devToolsTabTarget.url, + ); + devToolsWindowId = await _getCurrentWindowId( + worker: worker, + backgroundPage: backgroundPage, + ); + 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: 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( + 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( + 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( + 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( + worker: worker, + backgroundPage: backgroundPage, + ); + 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('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( + worker: worker, + backgroundPage: backgroundPage, + ); + // There should now be a warning notificiation: + 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( + 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 (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, + ); + + 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 (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, + ); + }); + + 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', + () 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), + ); + }); + + 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( + 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 enviroment 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 _getCurrentWindowId({ + Worker? worker, + Page? backgroundPage, +}) async { + return (await evaluate( + _currentWindowIdJs, + 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 _currentWindowIdJs = ''' + async () => { + return new Promise((resolve, reject) => { + chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { + const tab = tabs[0]; + resolve(tab.windowId); + }); + }); + } +'''; + +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'); +} diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart deleted file mode 100644 index ad641a13c..000000000 --- a/dwds/test/puppeteer/extension_test.dart +++ /dev/null @@ -1,1155 +0,0 @@ -// 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. - -@OnPlatform({ - // TODO(elliette): Enable on Windows. - 'windows': Skip('https://github.com/dart-lang/webdev/issues/1724'), - // TODO(elliette): Enable on Linux. - 'linux': Skip('https://github.com/dart-lang/webdev/issues/1787'), -}) -@Timeout(Duration(minutes: 5)) -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_mv3/web/data_serializers.dart'; -import '../../debug_extension_mv3/web/data_types.dart'; -import '../fixtures/context.dart'; -import '../fixtures/project.dart'; -import '../fixtures/utilities.dart'; -import 'test_utils.dart'; - -// To run all tests: -// dart test test/puppeteer/extension_test.dart --r=expanded --no-retry - -// To run the MV3 tests only: -// dart test test/puppeteer/extension_test.dart --r=expanded --no-retry --n="MV3 Debug Extension" - -// To run the MV2 tests only: -// dart test test/puppeteer/extension_test.dart --r=expanded --no-retry --n="MV2 Debug Extension" - -enum Panel { debugger, inspector } - -void main() async { - final provider = TestSdkConfigurationProvider(); - final context = TestContext(TestProject.testWithSoundNullSafety, provider); - tearDownAll(provider.dispose); - - for (var isMV3 in [true, false]) { - group('${isMV3 ? 'MV3' : 'MV2'} Debug Extension', () { - late String extensionPath; - - setUpAll(() async { - extensionPath = await buildDebugExtension(isMV3: isMV3); - }); - - for (var 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, - ); - - 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); - 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( - '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, - backgroundPage: backgroundPage, - ); - 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); - final appWindowId = await _getCurrentWindowId( - worker: worker, - backgroundPage: backgroundPage, - ); - // 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; - // Navigate to the newly opened DevTools tab: - await navigateToPage( - browser, - url: devToolsTabTarget.url, - ); - var devToolsWindowId = await _getCurrentWindowId( - 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: '$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; - // Navigate to the newly opened DevTools tab: - await navigateToPage( - browser, - url: devToolsTabTarget.url, - ); - devToolsWindowId = await _getCurrentWindowId( - worker: worker, - backgroundPage: backgroundPage, - ); - 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: 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( - 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( - 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( - 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( - worker: worker, - backgroundPage: backgroundPage, - ); - 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('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( - worker: worker, - backgroundPage: backgroundPage, - ); - // There should now be a warning notificiation: - 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( - 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 (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, - ); - - 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 (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, - ); - }); - - 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', - () 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), - ); - }); - - 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( - 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 enviroment 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 _getCurrentWindowId({ - Worker? worker, - Page? backgroundPage, -}) async { - return (await evaluate( - _currentWindowIdJs, - 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 _currentWindowIdJs = ''' - async () => { - return new Promise((resolve, reject) => { - chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { - const tab = tabs[0]; - resolve(tab.windowId); - }); - }); - } -'''; - -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 { - // 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'); -} diff --git a/dwds/test/puppeteer/extension_test_mv2.dart b/dwds/test/puppeteer/extension_test_mv2.dart new file mode 100644 index 000000000..aefa21867 --- /dev/null +++ b/dwds/test/puppeteer/extension_test_mv2.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2023, 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. + +@OnPlatform({ + // TODO(elliette): Enable CI testing. + 'linux': Skip('https://github.com/dart-lang/webdev/issues/1787'), +}) +@Timeout(Duration(minutes: 5)) +import 'package:test/test.dart'; + +import 'extension_common.dart'; + +void main() { + testAll( + isMV3: false, + // TODO(elliette): Configure screenshot diffing. + screenshotsEnabled: false, + ); +} diff --git a/dwds/test/puppeteer/extension_test_mv3.dart b/dwds/test/puppeteer/extension_test_mv3.dart new file mode 100644 index 000000000..730d6e03f --- /dev/null +++ b/dwds/test/puppeteer/extension_test_mv3.dart @@ -0,0 +1,20 @@ +// Copyright (c) 2023, 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. + +@OnPlatform({ + // TODO(elliette): Enable CI testing. + 'linux': Skip('https://github.com/dart-lang/webdev/issues/1787'), +}) +@Timeout(Duration(minutes: 5)) +import 'package:test/test.dart'; + +import 'extension_common.dart'; + +void main() { + testAll( + isMV3: true, + // TODO(elliette): Configure screenshot diffing. + screenshotsEnabled: false, + ); +}