Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a simulated DevTools environment for developing extensions #6251

Merged
merged 8 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions packages/devtools_extensions/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
## 0.0.3-wip
* Add a simulated DevTools environment that for easier development.

## 0.0.2-dev.0

* Add missing dependency on `package:devtools_shared`.
Expand Down
20 changes: 11 additions & 9 deletions packages/devtools_extensions/lib/src/api/model.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,18 @@ import 'api.dart';
/// See [DevToolsExtensionEventType] for different types of events that are
/// supported over this communication channel.
class DevToolsExtensionEvent {
DevToolsExtensionEvent(this.type, {this.data});
DevToolsExtensionEvent(
this.type, {
this.data,
this.source,
});

factory DevToolsExtensionEvent.parse(Map<String, Object?> json) {
final eventType =
DevToolsExtensionEventType.from(json[_typeKey]! as String);
final data = (json[_dataKey] as Map?)?.cast<String, Object?>();
return DevToolsExtensionEvent(eventType, data: data);
final source = json[sourceKey] as String?;
return DevToolsExtensionEvent(eventType, data: data, source: source);
}

static DevToolsExtensionEvent? tryParse(Object data) {
Expand All @@ -30,17 +35,14 @@ class DevToolsExtensionEvent {

static const _typeKey = 'type';
static const _dataKey = 'data';

static DevToolsExtensionEvent ping =
DevToolsExtensionEvent(DevToolsExtensionEventType.ping);

static DevToolsExtensionEvent pong =
DevToolsExtensionEvent(DevToolsExtensionEventType.pong);
static const sourceKey = 'source';

final DevToolsExtensionEventType type;

final Map<String, Object?>? data;

/// Optional field to describe the source that created and sent this event.
final String? source;

Map<String, Object?> toJson() {
return {
_typeKey: type.name,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

part of '_simulated_devtools_environment.dart';

class _VmServiceConnection extends StatelessWidget {
const _VmServiceConnection({
required this.simController,
required this.connected,
});

static const _totalControlsHeight = 45.0;
static const _totalControlsWidth = 415.0;

final _SimulatedDevToolsController simController;
final bool connected;

@override
Widget build(BuildContext context) {
return SizedBox(
height: _totalControlsHeight,
child: connected
? const _ConnectedVmServiceDisplay()
: _DisconnectedVmServiceDisplay(
simController: simController,
),
);
}
}

class _ConnectedVmServiceDisplay extends StatelessWidget {
const _ConnectedVmServiceDisplay();

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Debugging:',
style: theme.regularTextStyle,
),
const Text('<connected uri>'),
// TODO(kenz): uncomment once we can bump to vm_service ^11.10.0
// Text(
// serviceManager.service!.wsUri ?? '--',
// style: theme.boldTextStyle,
// ),
],
),
const Expanded(
child: SizedBox(width: denseSpacing),
),
DevToolsButton(
elevated: true,
label: 'Disconnect',
onPressed: serviceManager.manuallyDisconnect,
),
],
);
}
}

class _DisconnectedVmServiceDisplay extends StatefulWidget {
const _DisconnectedVmServiceDisplay({required this.simController});

final _SimulatedDevToolsController simController;

@override
State<_DisconnectedVmServiceDisplay> createState() =>
_DisconnectedVmServiceDisplayState();
}

class _DisconnectedVmServiceDisplayState
extends State<_DisconnectedVmServiceDisplay> {
static const _connectFieldWidth = 300.0;

late final TextEditingController _connectTextFieldController;

@override
void initState() {
super.initState();
_connectTextFieldController = TextEditingController();
}

@override
void dispose() {
_connectTextFieldController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
final theme = Theme.of(context);
return Row(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
width: _connectFieldWidth,
child: TextField(
autofocus: true,
style: theme.regularTextStyle,
decoration: InputDecoration(
// contentPadding: const EdgeInsets.all(denseSpacing),
isDense: true,
border: const OutlineInputBorder(),
enabledBorder: OutlineInputBorder(
borderSide: BorderSide(width: 0.5, color: theme.focusColor),
),
labelText: 'Dart VM Service URL',
labelStyle: theme.regularTextStyle,
hintText: '(e.g., http://127.0.0.1:60851/fH-kAEXc7MQ=/)',
hintStyle: theme.regularTextStyle,
),
onSubmitted: (value) =>
widget.simController.vmServiceConnectionChanged(uri: value),
controller: _connectTextFieldController,
),
),
const SizedBox(width: denseSpacing),
DevToolsButton(
elevated: true,
label: 'Connect',
onPressed: () => widget.simController.vmServiceConnectionChanged(
uri: _connectTextFieldController.text,
),
),
],
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
// Copyright 2023 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

part of '_simulated_devtools_environment.dart';

class _SimulatedDevToolsController extends DisposableController
implements DevToolsExtensionHostInterface {
/// Logs of the post message communication that goes back and forth between
/// the extension and the simulated DevTools environment.
final messageLogs = ListValueNotifier<_PostMessageLogEntry>([]);

void init() {
html.window.addEventListener('message', _handleMessage);
}

void _handleMessage(html.Event e) {
if (e is html.MessageEvent) {
final extensionEvent = DevToolsExtensionEvent.tryParse(e.data);
if (extensionEvent != null) {
// Do not handle messages that come from the
// [_SimulatedDevToolsController] itself.
if (extensionEvent.source == '$_SimulatedDevToolsController') return;

onEventReceived(extensionEvent);
}
}
}

@override
void dispose() {
html.window.removeEventListener('message', _handleMessage);
super.dispose();
}

@override
void ping() {
_postMessageToExtension(
DevToolsExtensionEvent(DevToolsExtensionEventType.ping),
);
}

@override
void vmServiceConnectionChanged({String? uri}) {
uri = 'http://127.0.0.1:60851/fH-kAEXc7MQ=/';
// TODO(kenz): add some validation and error handling if [uri] is bad input.
final normalizedUri = normalizeVmServiceUri(uri!);
final event = DevToolsExtensionEvent(
DevToolsExtensionEventType.vmServiceConnection,
data: {'uri': normalizedUri.toString()},
);
_postMessageToExtension(event);
}

@override
void onEventReceived(
DevToolsExtensionEvent event, {
void Function()? onUnknownEvent,
}) {
messageLogs.add(
_PostMessageLogEntry(
source: _PostMessageSource.extension,
data: event.toJson(),
),
);
}

void _postMessageToExtension(DevToolsExtensionEvent event) {
final eventJson = event.toJson();
html.window.postMessage(
{
...eventJson,
DevToolsExtensionEvent.sourceKey: '$_SimulatedDevToolsController',
},
html.window.origin!,
);
messageLogs.add(
_PostMessageLogEntry(
source: _PostMessageSource.devtools,
data: eventJson,
),
);
}
}

class _PostMessageLogEntry {
_PostMessageLogEntry({required this.source, required this.data})
: timestamp = DateTime.now();

final _PostMessageSource source;
final Map<String, Object?> data;
final DateTime timestamp;
}

enum _PostMessageSource {
devtools,
extension;

String get display {
return name.toUpperCase();
}
}
Loading
Loading