blob: 4b9a82dfe14928e8cc28fb7b4f6719a587400658 [file] [log] [blame] [edit]
// 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,
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<DebugInfo>(
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<String>(
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>(
'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 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(
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(
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<DebugInfo>(
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<DebugInfo>(
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<String>(
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<String>(
storageKey,
storageArea: 'session',
worker: worker,
backgroundPage: backgroundPage,
);
expect(multipleAppsDetected, equals('true'));
// Expect there to be a warning banner:
warningMsg = await _evaluateInPanel<String>(
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<DebugInfo>(
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<DebugInfo>(
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<String>(
authKey,
storageArea: 'session',
worker: worker,
backgroundPage: backgroundPage,
);
expect(isAuthenticated, equals('true'));
// Close the tab:
await appTab.close();
});
});
});
}
Future<bool> _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<Page> _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<T> _evaluateInPanel<T>(
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<ElementHandle?> _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<void> _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<int> _getCurrentTabId({
Worker? worker,
Page? backgroundPage,
}) async {
return (await evaluate(
_currentTabIdJs,
worker: worker,
backgroundPage: backgroundPage,
)) as int;
}
Future<int?> _getCurrentWindowId({
Worker? worker,
Page? backgroundPage,
}) async {
return (await evaluate(
_currentWindowIdJs,
worker: worker,
backgroundPage: backgroundPage,
)) as int?;
}
Future<T> _fetchStorageObj<T>(
String storageKey, {
required String storageArea,
Worker? worker,
Page? backgroundPage,
}) async {
final json = await retryFnAsync<String>(() 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<void> _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<HttpServer> _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');
}