Save debug information in `chrome.storage` after a Dart app loads (#1791)
diff --git a/dwds/debug_extension_mv3/web/background.dart b/dwds/debug_extension_mv3/web/background.dart index 60d2477..b749136 100644 --- a/dwds/debug_extension_mv3/web/background.dart +++ b/dwds/debug_extension_mv3/web/background.dart
@@ -53,11 +53,19 @@ final currentTab = await _getTab(); final currentUrl = currentTab?.url ?? ''; final appUrl = debugInfo.appUrl ?? ''; - if (currentUrl.isEmpty || appUrl.isEmpty || currentUrl != appUrl) { + if (currentTab == null || + currentUrl.isEmpty || + appUrl.isEmpty || + currentUrl != appUrl) { console.warn( 'Dart app detected at $appUrl but current tab is $currentUrl.'); return; } + // Save the debug info for the Dart app in storage: + await setStorageObject<DebugInfo>( + type: StorageObject.debugInfo, + value: debugInfo, + tabId: currentTab.id); // Update the icon to show that a Dart app has been detected: chrome.action.setIcon(IconInfo(path: 'dart.png'), /*callback*/ null); });
diff --git a/dwds/debug_extension_mv3/web/data_serializers.dart b/dwds/debug_extension_mv3/web/data_serializers.dart index 3692027..20552e6 100644 --- a/dwds/debug_extension_mv3/web/data_serializers.dart +++ b/dwds/debug_extension_mv3/web/data_serializers.dart
@@ -3,6 +3,7 @@ // BSD-style license that can be found in the LICENSE file. import 'package:built_value/serializer.dart'; +import 'package:dwds/data/debug_info.dart'; import 'data_types.dart'; @@ -10,6 +11,7 @@ /// Serializers for all the data types used in the Dart Debug Extension. @SerializersFor([ + DebugInfo, DevToolsOpener, ]) final Serializers serializers = _$serializers;
diff --git a/dwds/debug_extension_mv3/web/data_serializers.g.dart b/dwds/debug_extension_mv3/web/data_serializers.g.dart index 0b4f692..2d11b95 100644 --- a/dwds/debug_extension_mv3/web/data_serializers.g.dart +++ b/dwds/debug_extension_mv3/web/data_serializers.g.dart
@@ -6,7 +6,9 @@ // BuiltValueGenerator // ************************************************************************** -Serializers _$serializers = - (new Serializers().toBuilder()..add(DevToolsOpener.serializer)).build(); +Serializers _$serializers = (new Serializers().toBuilder() + ..add(DebugInfo.serializer) + ..add(DevToolsOpener.serializer)) + .build(); // ignore_for_file: always_put_control_body_on_new_line,always_specify_types,annotate_overrides,avoid_annotating_with_dynamic,avoid_as,avoid_catches_without_on_clauses,avoid_returning_this,deprecated_member_use_from_same_package,lines_longer_than_80_chars,no_leading_underscores_for_local_identifiers,omit_local_variable_types,prefer_expression_function_bodies,sort_constructors_first,test_types_in_equals,unnecessary_const,unnecessary_new,unnecessary_lambdas
diff --git a/dwds/debug_extension_mv3/web/storage.dart b/dwds/debug_extension_mv3/web/storage.dart index 7e8aa5f..a989e63 100644 --- a/dwds/debug_extension_mv3/web/storage.dart +++ b/dwds/debug_extension_mv3/web/storage.dart
@@ -13,12 +13,19 @@ import 'chrome_api.dart'; import 'data_serializers.dart'; +import 'web_api.dart'; + +/// Switch to true for debug logging. +bool enableDebugLogging = true; enum StorageObject { + debugInfo, devToolsOpener; String get keyName { switch (this) { + case StorageObject.debugInfo: + return 'debugInfo'; case StorageObject.devToolsOpener: return 'devToolsOpener'; } @@ -28,9 +35,10 @@ Future<bool> setStorageObject<T>({ required StorageObject type, required T value, + int? tabId, void Function()? callback, }) { - final storageKey = type.keyName; + final storageKey = _createStorageKey(type, tabId); final json = jsonEncode(serializers.serialize(value)); final storageObj = <String, String>{storageKey: json}; final completer = Completer<bool>(); @@ -38,22 +46,42 @@ if (callback != null) { callback(); } + _debugLog(storageKey, 'Set: $json'); completer.complete(true); })); return completer.future; } -Future<T?> fetchStorageObject<T>({required StorageObject type}) { - final storageKey = type.keyName; +Future<T?> fetchStorageObject<T>({required StorageObject type, int? tabId}) { + final storageKey = _createStorageKey(type, tabId); final completer = Completer<T?>(); chrome.storage.local.get([storageKey], allowInterop((Object storageObj) { final json = getProperty(storageObj, storageKey) as String?; if (json == null) { + _debugWarn(storageKey, 'Does not exist.'); completer.complete(null); } else { final value = serializers.deserialize(jsonDecode(json)) as T; + _debugLog(storageKey, 'Fetched: $json'); completer.complete(value); } })); return completer.future; } + +String _createStorageKey(StorageObject type, int? tabId) { + if (tabId == null) return type.keyName; + return '$tabId-${type.keyName}'; +} + +void _debugLog(String storageKey, String msg) { + if (enableDebugLogging) { + console.log('[$storageKey] $msg'); + } +} + +void _debugWarn(String storageKey, String msg) { + if (enableDebugLogging) { + console.warn('[$storageKey] $msg'); + } +}
diff --git a/dwds/lib/src/injected/client.js b/dwds/lib/src/injected/client.js index 794e091..777c1d6 100644 --- a/dwds/lib/src/injected/client.js +++ b/dwds/lib/src/injected/client.js
@@ -25172,11 +25172,10 @@ A.main__closure8.prototype = { call$1(b) { var t2, t3, - _s17_ = "$dartExtensionUri", t1 = A._asStringQ(self.$dartEntrypointPath); b.get$_$this()._appEntrypointPath = t1; t1 = this.windowContext; - t2 = A._asStringQ(t1.$index(0, _s17_)); + t2 = A._asStringQ(t1.$index(0, "$dartAppId")); b.get$_$this()._appId = t2; t2 = A._asStringQ(self.$dartAppInstanceId); b.get$_$this()._appInstanceId = t2; @@ -25186,7 +25185,7 @@ t2 = t2._as(window.location).href; t2.toString; b.get$_$this()._appUrl = t2; - t2 = A._asStringQ(t1.$index(0, _s17_)); + t2 = A._asStringQ(t1.$index(0, "$dartExtensionUri")); b.get$_$this()._extensionUrl = t2; t1 = A._asBoolQ(t1.$index(0, "$isInternalDartBuild")); b.get$_$this()._isInternalBuild = t1;
diff --git a/dwds/test/puppeteer/extension_test.dart b/dwds/test/puppeteer/extension_test.dart index 016f8dd..78ec470 100644 --- a/dwds/test/puppeteer/extension_test.dart +++ b/dwds/test/puppeteer/extension_test.dart
@@ -10,7 +10,10 @@ }) @Timeout(Duration(seconds: 60)) import 'dart:async'; +import 'dart:convert'; +import 'package:dwds/data/debug_info.dart'; +import 'package:dwds/data/serializers.dart'; import 'package:puppeteer/puppeteer.dart'; import 'package:test/test.dart'; @@ -19,6 +22,10 @@ final context = TestContext(); +// Note: The following delay is required to reduce flakiness. It makes +// sure the service worker execution context is ready. +const executionContextDelay = 1; + void main() async { late Target serviceWorkerTarget; late Browser browser; @@ -53,6 +60,31 @@ await browser.close(); }); + test('the debug info for a Dart app is saved in the extension storage', + () async { + final appUrl = context.appUrl; + // Navigate to the Dart app: + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); + final worker = (await serviceWorkerTarget.worker)!; + await Future.delayed(Duration(seconds: executionContextDelay)); + // Verify that we have debug info for the Dart app: + final tabIdForAppJs = _tabIdForTabJs(appUrl); + final appTabId = (await worker.evaluate(tabIdForAppJs)) as int; + final debugInfoKey = '$appTabId-debugInfo'; + final storageObj = + await worker.evaluate(_fetchStorageObjJs(debugInfoKey)); + final json = storageObj[debugInfoKey]; + final debugInfo = + serializers.deserialize(jsonDecode(json)) as DebugInfo; + expect(debugInfo.appId, isNotNull); + expect(debugInfo.appEntrypointPath, isNotNull); + expect(debugInfo.appInstanceId, isNotNull); + expect(debugInfo.appOrigin, isNotNull); + expect(debugInfo.appUrl, isNotNull); + await appTab.close(); + }); + test( 'can configure opening DevTools in a tab/window with extension settings', () async { @@ -62,12 +94,11 @@ final windowIdForAppJs = _windowIdForTabJs(appUrl); final windowIdForDevToolsJs = _windowIdForTabJs(devToolsUrl); // Navigate to the Dart app: - await navigateToPage(browser, url: appUrl, isNew: true); + final appTab = + await navigateToPage(browser, url: appUrl, isNew: true); // Click on the Dart Debug Extension icon: final worker = (await serviceWorkerTarget.worker)!; - // Note: The following delay is required to reduce flakiness (it makes - // sure the execution context is ready): - await Future.delayed(Duration(seconds: 1)); + await Future.delayed(Duration(seconds: executionContextDelay)); await worker.evaluate(clickIconJs); // Verify the extension opened the Dart docs in the same window: var devToolsTabTarget = await browser @@ -107,12 +138,23 @@ // Close the DevTools tab: devToolsTab = await devToolsTabTarget.page; await devToolsTab.close(); + await appTab.close(); }); }); } }); } +String _tabIdForTabJs(String tabUrl) { + return ''' + async () => { + const matchingTabs = await chrome.tabs.query({ url: "$tabUrl" }); + const tab = matchingTabs[0]; + return tab.id; + } +'''; +} + String _windowIdForTabJs(String tabUrl) { return ''' async () => { @@ -122,3 +164,20 @@ } '''; } + +String _fetchStorageObjJs(String storageKey) { + return ''' + async () => { + const storageKey = "$storageKey"; + return new Promise((resolve, reject) => { + chrome.storage.local.get(storageKey, (storageObj) => { + if (storageObj != null) { + resolve(storageObj); + } else { + resolve(null); + } + }); + }); + } +'''; +}
diff --git a/dwds/web/client.dart b/dwds/web/client.dart index 79da73e..ac430dc 100644 --- a/dwds/web/client.dart +++ b/dwds/web/client.dart
@@ -176,7 +176,7 @@ final windowContext = JsObject.fromBrowserObject(window); final debugInfoJson = jsonEncode(serializers.serialize(DebugInfo((b) => b ..appEntrypointPath = dartEntrypointPath - ..appId = windowContext['\$dartExtensionUri'] + ..appId = windowContext['\$dartAppId'] ..appInstanceId = dartAppInstanceId ..appOrigin = window.location.origin ..appUrl = window.location.href