diff --git a/dwds/debug_extension_mv3/web/chrome_api.dart b/dwds/debug_extension_mv3/web/chrome_api.dart index 803e13082..77fc960f8 100644 --- a/dwds/debug_extension_mv3/web/chrome_api.dart +++ b/dwds/debug_extension_mv3/web/chrome_api.dart @@ -170,6 +170,8 @@ class Runtime { external Object getManifest(); + external String getURL(String path); + // Note: Not checking the lastError when one occurs throws a runtime exception. external ChromeError? get lastError; diff --git a/dwds/debug_extension_mv3/web/debug_info.dart b/dwds/debug_extension_mv3/web/debug_info.dart new file mode 100644 index 000000000..b827a4160 --- /dev/null +++ b/dwds/debug_extension_mv3/web/debug_info.dart @@ -0,0 +1,33 @@ +// 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. + +@JS() +library debug_info; + +import 'dart:convert'; +import 'dart:html'; +import 'dart:js'; + +import 'package:dwds/data/debug_info.dart'; +import 'package:dwds/data/serializers.dart'; +import 'package:js/js.dart'; + +void main() { + final debugInfoJson = _readDartDebugInfo(); + document.dispatchEvent(CustomEvent('dart-app-ready', detail: debugInfoJson)); +} + +String _readDartDebugInfo() { + final windowContext = JsObject.fromBrowserObject(window); + + return jsonEncode(serializers.serialize(DebugInfo((b) => b + ..appEntrypointPath = windowContext['\$dartEntrypointPath'] + ..appId = windowContext['\$dartAppId'] + ..appInstanceId = windowContext['\$dartAppInstanceId'] + ..appOrigin = window.location.origin + ..appUrl = window.location.href + ..extensionUrl = windowContext['\$dartExtensionUri'] + ..isInternalBuild = windowContext['\$isInternalBuild'] + ..isFlutterApp = windowContext['\$isFlutterApp']))); +} diff --git a/dwds/debug_extension_mv3/web/detector.dart b/dwds/debug_extension_mv3/web/detector.dart index 0b9357844..76899834a 100644 --- a/dwds/debug_extension_mv3/web/detector.dart +++ b/dwds/debug_extension_mv3/web/detector.dart @@ -9,6 +9,7 @@ import 'dart:html'; import 'dart:js_util'; import 'package:js/js.dart'; +import 'chrome_api.dart'; import 'logger.dart'; import 'messaging.dart'; @@ -23,13 +24,26 @@ void _registerListeners() { void _onDartAppReadyEvent(Event event) { final debugInfo = getProperty(event, 'detail') as String?; if (debugInfo == null) { - debugError('Can\'t debug Dart app without debug info.', verbose: true); - return; + debugWarn( + 'No debug info sent with ready event, instead reading from Window.'); + _injectDebugInfoScript(); + } else { + _sendMessageToBackgroundScript( + type: MessageType.debugInfo, + body: debugInfo, + ); } - _sendMessageToBackgroundScript( - type: MessageType.debugInfo, - body: debugInfo, - ); +} + +// TODO(elliette): Remove once DWDS 17.0.0 is in Flutter stable. If we are on an +// older version of DWDS, then the debug info is not sent along with the ready +// event. Therefore we must read it from the Window object, which is slower. +void _injectDebugInfoScript() { + final script = document.createElement('script'); + final scriptSrc = chrome.runtime.getURL('debug_info.dart.js'); + script.setAttribute('src', scriptSrc); + script.setAttribute('defer', true); + document.head?.append(script); } void _sendMessageToBackgroundScript({ diff --git a/dwds/debug_extension_mv3/web/manifest.json b/dwds/debug_extension_mv3/web/manifest.json index 91efe797a..28c727f81 100644 --- a/dwds/debug_extension_mv3/web/manifest.json +++ b/dwds/debug_extension_mv3/web/manifest.json @@ -28,5 +28,11 @@ "run_at": "document_end" } ], + "web_accessible_resources": [ + { + "matches": [""], + "resources": ["debug_info.dart.js"] + } + ], "options_page": "static_assets/settings.html" } diff --git a/dwds/test/fixtures/utilities.dart b/dwds/test/fixtures/utilities.dart index dacb24412..1f153da02 100644 --- a/dwds/test/fixtures/utilities.dart +++ b/dwds/test/fixtures/utilities.dart @@ -125,3 +125,31 @@ Future retryFn( failureMessage: failureMessage, ); } + +/// Retries an asynchronous callback function with a delay until the result is +/// non-null. +Future retryFnAsync( + Future Function() callback, { + int retryCount = 3, + int delayInMs = 1000, + String failureMessage = 'Function did not succeed after retries.', +}) async { + if (retryCount == 0) { + throw Exception(failureMessage); + } + + await Future.delayed(Duration(milliseconds: delayInMs)); + try { + final result = await callback(); + if (result != null) return result; + } catch (_) { + // Ignore any exceptions. + } + + return retryFnAsync( + callback, + retryCount: retryCount - 1, + delayInMs: delayInMs, + failureMessage: failureMessage, + ); +} diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart index 065e50ceb..917c7e37e 100644 --- a/dwds/test/puppeteer/extension_test.dart +++ b/dwds/test/puppeteer/extension_test.dart @@ -22,6 +22,7 @@ import 'package:test/test.dart'; import '../../debug_extension_mv3/web/data_serializers.dart'; import '../../debug_extension_mv3/web/data_types.dart'; import '../fixtures/context.dart'; +import '../fixtures/utilities.dart'; import 'test_utils.dart'; final context = TestContext.withSoundNullSafety(); @@ -435,6 +436,74 @@ void main() async { }); } }); + + group('connected to a fake app', () { + final fakeAppPath = webCompatiblePath( + p.split( + absolutePath( + pathFromDwds: p.join( + 'test', + 'puppeteer', + 'fake_app', + 'index.html', + ), + ), + ), + ); + final fakeAppUrl = 'file://$fakeAppPath'; + late Browser browser; + late Worker worker; + + setUpAll(() async { + browser = await puppeteer.launch( + headless: false, + timeout: Duration(seconds: 60), + args: [ + '--load-extension=$extensionPath', + '--disable-extensions-except=$extensionPath', + '--disable-features=DialMediaRouteProvider', + ], + ); + worker = await getServiceWorker(browser); + // Navigate to the Chrome extension page instead of the blank tab + // opened by Chrome. This is helpful for local debugging. + final blankTab = await navigateToPage(browser, url: 'about:blank'); + await blankTab.goto('chrome://extensions/'); + }); + + tearDown(() async { + await workerEvalDelay(); + await worker.evaluate(_clearStorageJs()); + await workerEvalDelay(); + }); + + tearDownAll(() async { + await browser.close(); + }); + + // Note: This tests that the debug extension still works for DWDS versions + // <17.0.0. Those versions don't send the debug info with the ready event. + // Therefore the values are read from the Window object. + test('reads debug info from Window and saves to storage', () async { + // Navigate to the "Dart" app: + await navigateToPage(browser, url: fakeAppUrl, isNew: true); + // Verify that we have debug info for the fake "Dart" app: + final appTabId = await _getTabId(fakeAppUrl, worker: worker); + final debugInfoKey = '$appTabId-debugInfo'; + final debugInfo = await _fetchStorageObj( + debugInfoKey, + storageArea: 'session', + worker: worker, + ); + 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); + }); + }); }); } @@ -521,11 +590,13 @@ Future _fetchStorageObj( required String storageArea, required Worker worker, }) async { - final storageObj = await worker.evaluate(_fetchStorageObjJs( - storageKey, - storageArea: storageArea, - )); - final json = storageObj[storageKey]; + final json = await retryFnAsync(() async { + final storageObj = await worker.evaluate(_fetchStorageObjJs( + storageKey, + storageArea: storageArea, + )); + return storageObj[storageKey]; + }); return serializers.deserialize(jsonDecode(json)) as T; } diff --git a/dwds/test/puppeteer/fake_app/index.html b/dwds/test/puppeteer/fake_app/index.html new file mode 100644 index 000000000..00e64ae27 --- /dev/null +++ b/dwds/test/puppeteer/fake_app/index.html @@ -0,0 +1,9 @@ + + + + + + +

Fake app

+ + diff --git a/dwds/test/puppeteer/fake_app/main.js b/dwds/test/puppeteer/fake_app/main.js new file mode 100644 index 000000000..04548baa5 --- /dev/null +++ b/dwds/test/puppeteer/fake_app/main.js @@ -0,0 +1,11 @@ +document.addEventListener("DOMContentLoaded", function(event) { + window['$dartEntrypointPath'] = 'DART_ENTRYPOINT_PATH'; + window['$dartAppId'] = 'DART_APP_ID'; + window['$dartAppInstanceId'] = 'DART_APP_INSTANCE_ID'; + window['$dartExtensionUri'] = 'DART_EXTENSION_URI'; + window['$isInternalBuild'] = true; + window['$isFlutterApp'] = false; + setTimeout(() => { + window.top.document.dispatchEvent(new CustomEvent('dart-app-ready')); + }, 1000); + });