Skip to content

Commit e134e5b

Browse files
authored
[MV3] Dart debug extension supports DWDS versions < 17.0.0 (#1882)
1 parent ed80c94 commit e134e5b

File tree

8 files changed

+185
-11
lines changed

8 files changed

+185
-11
lines changed

dwds/debug_extension_mv3/web/chrome_api.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@ class Runtime {
170170

171171
external Object getManifest();
172172

173+
external String getURL(String path);
174+
173175
// Note: Not checking the lastError when one occurs throws a runtime exception.
174176
external ChromeError? get lastError;
175177

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
@JS()
6+
library debug_info;
7+
8+
import 'dart:convert';
9+
import 'dart:html';
10+
import 'dart:js';
11+
12+
import 'package:dwds/data/debug_info.dart';
13+
import 'package:dwds/data/serializers.dart';
14+
import 'package:js/js.dart';
15+
16+
void main() {
17+
final debugInfoJson = _readDartDebugInfo();
18+
document.dispatchEvent(CustomEvent('dart-app-ready', detail: debugInfoJson));
19+
}
20+
21+
String _readDartDebugInfo() {
22+
final windowContext = JsObject.fromBrowserObject(window);
23+
24+
return jsonEncode(serializers.serialize(DebugInfo((b) => b
25+
..appEntrypointPath = windowContext['\$dartEntrypointPath']
26+
..appId = windowContext['\$dartAppId']
27+
..appInstanceId = windowContext['\$dartAppInstanceId']
28+
..appOrigin = window.location.origin
29+
..appUrl = window.location.href
30+
..extensionUrl = windowContext['\$dartExtensionUri']
31+
..isInternalBuild = windowContext['\$isInternalBuild']
32+
..isFlutterApp = windowContext['\$isFlutterApp'])));
33+
}

dwds/debug_extension_mv3/web/detector.dart

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import 'dart:html';
99
import 'dart:js_util';
1010
import 'package:js/js.dart';
1111

12+
import 'chrome_api.dart';
1213
import 'logger.dart';
1314
import 'messaging.dart';
1415

@@ -23,13 +24,26 @@ void _registerListeners() {
2324
void _onDartAppReadyEvent(Event event) {
2425
final debugInfo = getProperty(event, 'detail') as String?;
2526
if (debugInfo == null) {
26-
debugError('Can\'t debug Dart app without debug info.', verbose: true);
27-
return;
27+
debugWarn(
28+
'No debug info sent with ready event, instead reading from Window.');
29+
_injectDebugInfoScript();
30+
} else {
31+
_sendMessageToBackgroundScript(
32+
type: MessageType.debugInfo,
33+
body: debugInfo,
34+
);
2835
}
29-
_sendMessageToBackgroundScript(
30-
type: MessageType.debugInfo,
31-
body: debugInfo,
32-
);
36+
}
37+
38+
// TODO(elliette): Remove once DWDS 17.0.0 is in Flutter stable. If we are on an
39+
// older version of DWDS, then the debug info is not sent along with the ready
40+
// event. Therefore we must read it from the Window object, which is slower.
41+
void _injectDebugInfoScript() {
42+
final script = document.createElement('script');
43+
final scriptSrc = chrome.runtime.getURL('debug_info.dart.js');
44+
script.setAttribute('src', scriptSrc);
45+
script.setAttribute('defer', true);
46+
document.head?.append(script);
3347
}
3448

3549
void _sendMessageToBackgroundScript({

dwds/debug_extension_mv3/web/manifest.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,11 @@
2828
"run_at": "document_end"
2929
}
3030
],
31+
"web_accessible_resources": [
32+
{
33+
"matches": ["<all_urls>"],
34+
"resources": ["debug_info.dart.js"]
35+
}
36+
],
3137
"options_page": "static_assets/settings.html"
3238
}

dwds/test/fixtures/utilities.dart

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,3 +125,31 @@ Future<T> retryFn<T>(
125125
failureMessage: failureMessage,
126126
);
127127
}
128+
129+
/// Retries an asynchronous callback function with a delay until the result is
130+
/// non-null.
131+
Future<T> retryFnAsync<T>(
132+
Future<T> Function() callback, {
133+
int retryCount = 3,
134+
int delayInMs = 1000,
135+
String failureMessage = 'Function did not succeed after retries.',
136+
}) async {
137+
if (retryCount == 0) {
138+
throw Exception(failureMessage);
139+
}
140+
141+
await Future.delayed(Duration(milliseconds: delayInMs));
142+
try {
143+
final result = await callback();
144+
if (result != null) return result;
145+
} catch (_) {
146+
// Ignore any exceptions.
147+
}
148+
149+
return retryFnAsync<T>(
150+
callback,
151+
retryCount: retryCount - 1,
152+
delayInMs: delayInMs,
153+
failureMessage: failureMessage,
154+
);
155+
}

dwds/test/puppeteer/extension_test.dart

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import 'package:test/test.dart';
2222
import '../../debug_extension_mv3/web/data_serializers.dart';
2323
import '../../debug_extension_mv3/web/data_types.dart';
2424
import '../fixtures/context.dart';
25+
import '../fixtures/utilities.dart';
2526
import 'test_utils.dart';
2627

2728
final context = TestContext.withSoundNullSafety();
@@ -435,6 +436,74 @@ void main() async {
435436
});
436437
}
437438
});
439+
440+
group('connected to a fake app', () {
441+
final fakeAppPath = webCompatiblePath(
442+
p.split(
443+
absolutePath(
444+
pathFromDwds: p.join(
445+
'test',
446+
'puppeteer',
447+
'fake_app',
448+
'index.html',
449+
),
450+
),
451+
),
452+
);
453+
final fakeAppUrl = 'file://$fakeAppPath';
454+
late Browser browser;
455+
late Worker worker;
456+
457+
setUpAll(() async {
458+
browser = await puppeteer.launch(
459+
headless: false,
460+
timeout: Duration(seconds: 60),
461+
args: [
462+
'--load-extension=$extensionPath',
463+
'--disable-extensions-except=$extensionPath',
464+
'--disable-features=DialMediaRouteProvider',
465+
],
466+
);
467+
worker = await getServiceWorker(browser);
468+
// Navigate to the Chrome extension page instead of the blank tab
469+
// opened by Chrome. This is helpful for local debugging.
470+
final blankTab = await navigateToPage(browser, url: 'about:blank');
471+
await blankTab.goto('chrome://extensions/');
472+
});
473+
474+
tearDown(() async {
475+
await workerEvalDelay();
476+
await worker.evaluate(_clearStorageJs());
477+
await workerEvalDelay();
478+
});
479+
480+
tearDownAll(() async {
481+
await browser.close();
482+
});
483+
484+
// Note: This tests that the debug extension still works for DWDS versions
485+
// <17.0.0. Those versions don't send the debug info with the ready event.
486+
// Therefore the values are read from the Window object.
487+
test('reads debug info from Window and saves to storage', () async {
488+
// Navigate to the "Dart" app:
489+
await navigateToPage(browser, url: fakeAppUrl, isNew: true);
490+
// Verify that we have debug info for the fake "Dart" app:
491+
final appTabId = await _getTabId(fakeAppUrl, worker: worker);
492+
final debugInfoKey = '$appTabId-debugInfo';
493+
final debugInfo = await _fetchStorageObj<DebugInfo>(
494+
debugInfoKey,
495+
storageArea: 'session',
496+
worker: worker,
497+
);
498+
expect(debugInfo.appId, equals('DART_APP_ID'));
499+
expect(debugInfo.appEntrypointPath, equals('DART_ENTRYPOINT_PATH'));
500+
expect(debugInfo.appInstanceId, equals('DART_APP_INSTANCE_ID'));
501+
expect(debugInfo.isInternalBuild, isTrue);
502+
expect(debugInfo.isFlutterApp, isFalse);
503+
expect(debugInfo.appOrigin, isNotNull);
504+
expect(debugInfo.appUrl, isNotNull);
505+
});
506+
});
438507
});
439508
}
440509

@@ -521,11 +590,13 @@ Future<T> _fetchStorageObj<T>(
521590
required String storageArea,
522591
required Worker worker,
523592
}) async {
524-
final storageObj = await worker.evaluate(_fetchStorageObjJs(
525-
storageKey,
526-
storageArea: storageArea,
527-
));
528-
final json = storageObj[storageKey];
593+
final json = await retryFnAsync<String>(() async {
594+
final storageObj = await worker.evaluate(_fetchStorageObjJs(
595+
storageKey,
596+
storageArea: storageArea,
597+
));
598+
return storageObj[storageKey];
599+
});
529600
return serializers.deserialize(jsonDecode(json)) as T;
530601
}
531602

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
<html>
2+
<head>
3+
<script defer src="main.js"></script>
4+
</head>
5+
6+
<body>
7+
<h1>Fake app</h1>
8+
</body>
9+
</html>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
document.addEventListener("DOMContentLoaded", function(event) {
2+
window['$dartEntrypointPath'] = 'DART_ENTRYPOINT_PATH';
3+
window['$dartAppId'] = 'DART_APP_ID';
4+
window['$dartAppInstanceId'] = 'DART_APP_INSTANCE_ID';
5+
window['$dartExtensionUri'] = 'DART_EXTENSION_URI';
6+
window['$isInternalBuild'] = true;
7+
window['$isFlutterApp'] = false;
8+
setTimeout(() => {
9+
window.top.document.dispatchEvent(new CustomEvent('dart-app-ready'));
10+
}, 1000);
11+
});

0 commit comments

Comments
 (0)