// Copyright (c) 2019, 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. // When run locally this test may require a manifest key. This makes it easy to // just skip it. @Tags(['extension']) @Timeout(Duration(minutes: 2)) @OnPlatform({ 'windows': Skip('https://github.com/dart-lang/webdev/issues/711'), 'linux': Skip('https://github.com/dart-lang/webdev/issues/2114'), }) library; import 'package:dwds/src/connections/debug_connection.dart'; import 'package:dwds/src/handlers/injector.dart'; import 'package:http/http.dart' as http; import 'package:test/test.dart'; import 'package:test_common/test_sdk_configuration.dart'; // ignore: deprecated_member_use import 'package:webdriver/io.dart'; import 'fixtures/context.dart'; import 'fixtures/project.dart'; import 'fixtures/utilities.dart'; // Instructions for running: // * From the /dwds, run: dart test test/debug_extension_test.dart // * See note for Googlers below as well // [For Googlers] // A whitelisted developer key is needed to run these tests locally. // Add a developer key to dwds/debug_extension/build/web_prod/manifest.json. // Otherwise, you will get 'Error Loading Extension' alert. // Remove the key before pushing code to GitHub. // See go/extension-identification. void main() async { final provider = TestSdkConfigurationProvider(); tearDownAll(provider.dispose); final context = TestContext(TestProject.test, provider); Future waitForDartDevToolsWithRetry({ int retryCount = 6, Duration retryWait = const Duration(seconds: 1), }) async { if (retryCount == 0) return; final windows = await context.webDriver.windows.toList(); await context.webDriver.driver.switchTo.window(windows.last); final title = await context.webDriver.title; if (title == 'Dart DevTools') return; await Future.delayed(retryWait); return waitForDartDevToolsWithRetry( retryCount: retryCount--, retryWait: retryWait, ); } for (final useSse in [true, false]) { group(useSse ? 'SSE' : 'WebSockets', () { group('Without encoding', () { setUp(() async { await context.setUp( debugSettings: TestDebugSettings.withDevToolsLaunch( context, ).copyWith(enableDebugExtension: true, useSse: useSse), ); await context.extensionConnection.sendCommand('Runtime.evaluate', { 'expression': 'fakeClick()', }); // Wait for DevTools to actually open. await waitForDartDevToolsWithRetry(); }); tearDown(() async { await context.tearDown(); }); test('can launch DevTools', () async { final windows = await context.webDriver.windows.toList(); await context.webDriver.driver.switchTo.window(windows.last); expect(await context.webDriver.title, contains('Dart DevTools')); expect( await context.webDriver.currentUrl, contains('ide=DebugExtension'), ); }); test('can close DevTools and relaunch', () async { for (final window in await context.webDriver.windows.toList()) { await context.webDriver.driver.switchTo.window(window); if (await context.webDriver.title == 'Dart DevTools') { await window.close(); break; } } // Relaunch DevTools by (fake) clicking the extension. await context.extensionConnection.sendCommand('Runtime.evaluate', { 'expression': 'fakeClick()', }); await waitForDartDevToolsWithRetry(); expect(await context.webDriver.title, 'Dart DevTools'); }); test('sends script parsed events', () async { // Check if the extension debugger receives Debugger.ScriptParsed // events for some important scripts. final service = fetchChromeProxyService(context.debugConnection); final scripts = service.remoteDebugger.scripts; expect( scripts.values.map((s) => s.url), containsAllInOrder([ contains('stack_trace_mapper.dart.js'), contains('hello_world/main.ddc.js'), contains('packages/path/path.ddc.js'), contains('dev_compiler/dart_sdk.js'), contains('dwds/src/injected/client.js'), ]), ); }); }); group('With a sharded Dart app', () { setUp(() async { await context.setUp( debugSettings: TestDebugSettings.withDevToolsLaunch( context, ).copyWith(enableDebugExtension: true, useSse: useSse), ); final htmlTag = await context.webDriver.findElement( const By.tagName('html'), ); await context.webDriver.execute( "arguments[0].setAttribute('data-multiple-dart-apps', 'true');", [htmlTag], ); }); tearDown(() async { await context.tearDown(); }); test('opens an alert', () async { await context.extensionConnection.sendCommand('Runtime.evaluate', { 'expression': 'fakeClick()', }); // Wait for the alert to open. final alert = await retryFn( () => context.webDriver.switchTo.alert, ); expect(alert, isNotNull); }); }); // TODO(elliette): Figure out a way to verify that the Dart panel is added // to Chrome DevTools. This might not be possible to test with WebDriver, // because WebDriver doesn't allow you to interact with Chrome DevTools. group('With an internal Dart app', () { setUp(() async { await context.setUp( debugSettings: TestDebugSettings.withDevToolsLaunch( context, ).copyWith(enableDebugExtension: true, useSse: false), ); final htmlTag = await context.webDriver.findElement( const By.tagName('html'), ); await context.webDriver.execute( "arguments[0].setAttribute('data-ddr-dart-app', 'true');", [htmlTag], ); await context.extensionConnection.sendCommand('Runtime.evaluate', { 'expression': 'fakeClick()', }); // Wait for DevTools to actually open. await waitForDartDevToolsWithRetry(); }); tearDown(() async { await context.tearDown(); }); test('can launch DevTools', () async { final windows = await context.webDriver.windows.toList(); await context.webDriver.driver.switchTo.window(windows.last); expect(await context.webDriver.title, 'Dart DevTools'); }); test('can close DevTools and relaunch', () async { for (final window in await context.webDriver.windows.toList()) { await context.webDriver.driver.switchTo.window(window); if (await context.webDriver.title == 'Dart DevTools') { await window.close(); break; } } // Relaunch DevTools by (fake) clicking the extension. await context.extensionConnection.sendCommand('Runtime.evaluate', { 'expression': 'fakeClick()', }); await waitForDartDevToolsWithRetry(); expect(await context.webDriver.title, 'Dart DevTools'); }); test('sends script parsed events', () async { // Check if the extension debugger receives Debugger.ScriptParsed // events for some important scripts. final service = fetchChromeProxyService(context.debugConnection); final scripts = service.remoteDebugger.scripts; expect( scripts.values.map((s) => s.url), containsAllInOrder([ contains('stack_trace_mapper.dart.js'), contains('hello_world/main.ddc.js'), contains('packages/path/path.ddc.js'), contains('dev_compiler/dart_sdk.js'), contains('dwds/src/injected/client.js'), ]), ); }); }); }); } group('With encoding', () { setUp(() async { await context.setUp( debugSettings: TestDebugSettings.noDevToolsLaunch().copyWith( enableDebugExtension: true, urlEncoder: (url) async => url.endsWith(r'/$debug') ? 'http://some-encoded-url:8081/' : url, ), ); }); tearDown(() async { await context.tearDown(); }); test('uses the encoded URI', () async { final result = await http.get( Uri.parse( 'http://localhost:${context.port}/hello_world/main.dart$bootstrapJsExtension', ), ); expect(result.body.contains('dartExtensionUri'), isTrue); expect(result.body.contains('http://some-encoded-url:8081/'), isTrue); }); }); group('With "any" hostname', () { final uriPattern = RegExp(r'dartExtensionUri = "([^"]+)";'); setUp(() async { await context.setUp( appMetadata: TestAppMetadata.externalApp().copyWith(hostname: 'any'), debugSettings: TestDebugSettings.noDevToolsLaunch().copyWith( enableDebugExtension: true, ), ); }); tearDown(() async { await context.tearDown(); }); test('generates an extensionUri with a valid valid hostname', () async { final result = await http.get( Uri.parse( 'http://localhost:${context.port}/hello_world/main.dart$bootstrapJsExtension', ), ); expect(result.body.contains('dartExtensionUri'), isTrue); final extensionUri = Uri.parse( uriPattern.firstMatch(result.body)!.group(1)!, ); expect( extensionUri.host, anyOf( // The hostname should've been mapped from "any" to one of the local // loopback addresses/IPs. equals('localhost'), equals('127.0.0.1'), equals('::'), equals('::1'), ), ); }); }); }