expose more methods; change API; rev version (#58)

expose more methods; change API; rev version
diff --git a/changelog.md b/changelog.md
index ec149a6..8ed2a8c 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,5 +1,13 @@
 # webkit_inspection_protocol.dart
 
+## 0.7.0
+- Normalized all objects to expose a `json` field for raw access to the protocol information.
+- Exposed Runtime.getProperties, Runtime.getHeapUsage, and Runtime.getIsolateId
+- Exposed `DebuggerPausedEvent.hitBreakpoints` and `DebuggerPausedEvent.asyncStackTrace`
+- Exposed `WipCallFrame.returnValue`
+- Removed `WrappedWipEvent` (in favor of just using `WipEvent`)
+- Removed `WipRemoteObject` (in favor of just using `RemoteObject`)
+
 ## 0.6.0
 - Add `onSend` and `onReceive` in `WipConnection` 
 - Expose `onExecutionContextCreated`, `onExecutionContextDestroyed`,
diff --git a/lib/src/console.dart b/lib/src/console.dart
index 72d9e27..e1500d7 100644
--- a/lib/src/console.dart
+++ b/lib/src/console.dart
@@ -17,15 +17,15 @@
 
   Stream<ConsoleMessageEvent> get onMessage => eventStream(
       'Console.messageAdded',
-      (WipEvent event) => new ConsoleMessageEvent(event));
+      (WipEvent event) => new ConsoleMessageEvent(event.json));
 
   Stream<ConsoleClearedEvent> get onCleared => eventStream(
       'Console.messagesCleared',
-      (WipEvent event) => new ConsoleClearedEvent(event));
+      (WipEvent event) => new ConsoleClearedEvent(event.json));
 }
 
-class ConsoleMessageEvent extends WrappedWipEvent {
-  ConsoleMessageEvent(WipEvent event) : super(event);
+class ConsoleMessageEvent extends WipEvent {
+  ConsoleMessageEvent(Map<String, dynamic> json) : super(json);
 
   Map get _message => params['message'] as Map;
 
@@ -47,22 +47,22 @@
   String toString() => text;
 }
 
-class ConsoleClearedEvent extends WrappedWipEvent {
-  ConsoleClearedEvent(WipEvent event) : super(event);
+class ConsoleClearedEvent extends WipEvent {
+  ConsoleClearedEvent(Map<String, dynamic> json) : super(json);
 }
 
 class WipConsoleCallFrame {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  WipConsoleCallFrame.fromMap(this._map);
+  WipConsoleCallFrame.fromMap(this.json);
 
-  int get columnNumber => _map['columnNumber'] as int;
+  int get columnNumber => json['columnNumber'] as int;
 
-  String get functionName => _map['functionName'] as String;
+  String get functionName => json['functionName'] as String;
 
-  int get lineNumber => _map['lineNumber'] as int;
+  int get lineNumber => json['lineNumber'] as int;
 
-  String get scriptId => _map['scriptId'] as String;
+  String get scriptId => json['scriptId'] as String;
 
-  String get url => _map['url'] as String;
+  String get url => json['url'] as String;
 }
diff --git a/lib/src/debugger.dart b/lib/src/debugger.dart
index 1c928cd..ae7cb62 100644
--- a/lib/src/debugger.dart
+++ b/lib/src/debugger.dart
@@ -22,10 +22,11 @@
 
   Future<WipResponse> disable() => sendCommand('Debugger.disable');
 
-  Future<String> getScriptSource(String scriptId) async =>
-      (await sendCommand('Debugger.getScriptSource',
-              params: {'scriptId': scriptId}))
-          .result['scriptSource'] as String;
+  Future<String> getScriptSource(String scriptId) async {
+    return (await sendCommand('Debugger.getScriptSource',
+            params: {'scriptId': scriptId}))
+        .result['scriptSource'] as String;
+  }
 
   Future<WipResponse> pause() => sendCommand('Debugger.pause');
 
@@ -142,19 +143,19 @@
     }
   }
 
-  Stream<DebuggerPausedEvent> get onPaused => eventStream(
-      'Debugger.paused', (WipEvent event) => new DebuggerPausedEvent(event));
+  Stream<DebuggerPausedEvent> get onPaused => eventStream('Debugger.paused',
+      (WipEvent event) => new DebuggerPausedEvent(event.json));
 
   Stream<GlobalObjectClearedEvent> get onGlobalObjectCleared => eventStream(
       'Debugger.globalObjectCleared',
       (WipEvent event) => new GlobalObjectClearedEvent(event));
 
-  Stream<DebuggerResumedEvent> get onResumed => eventStream(
-      'Debugger.resumed', (WipEvent event) => new DebuggerResumedEvent(event));
+  Stream<DebuggerResumedEvent> get onResumed => eventStream('Debugger.resumed',
+      (WipEvent event) => new DebuggerResumedEvent(event.json));
 
   Stream<ScriptParsedEvent> get onScriptParsed => eventStream(
       'Debugger.scriptParsed',
-      (WipEvent event) => new ScriptParsedEvent(event));
+      (WipEvent event) => new ScriptParsedEvent(event.json));
 
   Map<String, WipScript> get scripts => new UnmodifiableMapView(_scripts);
 }
@@ -174,158 +175,177 @@
 
 enum PauseState { all, none, uncaught }
 
-class ScriptParsedEvent extends WrappedWipEvent {
-  final WipScript script;
+class ScriptParsedEvent extends WipEvent {
+  ScriptParsedEvent(Map<String, dynamic> json) : super(json);
 
-  ScriptParsedEvent(WipEvent event)
-      : this.script = new WipScript(event.params),
-        super(event);
+  WipScript get script => new WipScript(params);
 
   String toString() => script.toString();
 }
 
-class GlobalObjectClearedEvent extends WrappedWipEvent {
-  GlobalObjectClearedEvent(WipEvent event) : super(event);
+class GlobalObjectClearedEvent extends WipEvent {
+  GlobalObjectClearedEvent(json) : super(json);
 }
 
-class DebuggerResumedEvent extends WrappedWipEvent {
-  DebuggerResumedEvent(WipEvent event) : super(event);
+class DebuggerResumedEvent extends WipEvent {
+  DebuggerResumedEvent(Map<String, dynamic> json) : super(json);
 }
 
-class DebuggerPausedEvent extends WrappedWipEvent {
-  DebuggerPausedEvent(WipEvent event) : super(event);
+/// Fired when the virtual machine stopped on breakpoint or exception or any
+/// other stop criteria.
+class DebuggerPausedEvent extends WipEvent {
+  DebuggerPausedEvent(Map<String, dynamic> json) : super(json);
 
+  /// Call stack the virtual machine stopped on.
+  List<WipCallFrame> getCallFrames() => (params['callFrames'] as List)
+      .map((frame) => new WipCallFrame(frame as Map<String, dynamic>))
+      .toList();
+
+  /// Pause reason.
+  ///
+  /// Allowed Values: ambiguous, assert, debugCommand, DOM, EventListener,
+  /// exception, instrumentation, OOM, other, promiseRejection, XHR.
   String get reason => params['reason'] as String;
 
+  /// Object containing break-specific auxiliary properties.
   Object get data => params['data'];
 
-  Iterable<WipCallFrame> getCallFrames() => (params['callFrames'] as List)
-      .map((frame) => new WipCallFrame(frame as Map<String, dynamic>));
+  /// Hit breakpoints IDs (optional).
+  List<String> get hitBreakpoints {
+    if (params['hitBreakpoints'] == null) return null;
+    return (params['hitBreakpoints'] as List).cast<String>();
+  }
+
+  /// Async stack trace, if any.
+  StackTrace get asyncStackTrace => params['asyncStackTrace'] == null
+      ? null
+      : StackTrace(params['asyncStackTrace']);
 
   String toString() => 'paused: ${reason}';
 }
 
 class WipCallFrame {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  WipCallFrame(this._map);
+  WipCallFrame(this.json);
 
-  String get callFrameId => _map['callFrameId'] as String;
+  /// Call frame identifier.
+  ///
+  /// This identifier is only valid while the virtual machine is paused.
+  String get callFrameId => json['callFrameId'] as String;
 
-  String get functionName => _map['functionName'] as String;
+  /// Name of the JavaScript function called on this call frame.
+  String get functionName => json['functionName'] as String;
 
+  /// Location in the source code.
   WipLocation get location =>
-      new WipLocation(_map['location'] as Map<String, dynamic>);
+      new WipLocation(json['location'] as Map<String, dynamic>);
 
-  WipRemoteObject get thisObject =>
-      new WipRemoteObject(_map['this'] as Map<String, dynamic>);
+  /// JavaScript script name or url.
+  String get url => json['url'] as String;
 
-  Iterable<WipScope> getScopeChain() => (_map['scopeChain'] as List)
+  /// Scope chain for this call frame.
+  Iterable<WipScope> getScopeChain() => (json['scopeChain'] as List)
       .map((scope) => new WipScope(scope as Map<String, dynamic>));
 
+  /// `this` object for this call frame.
+  RemoteObject get thisObject =>
+      new RemoteObject(json['this'] as Map<String, dynamic>);
+
+  /// The value being returned, if the function is at return point.
+  ///
+  /// (optional)
+  RemoteObject get returnValue {
+    return json.containsKey('returnValue')
+        ? new RemoteObject(json['returnValue'] as Map<String, dynamic>)
+        : null;
+  }
+
   String toString() => '[${functionName}]';
 }
 
 class WipLocation {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  WipLocation(this._map);
+  WipLocation(this.json);
 
   WipLocation.fromValues(String scriptId, int lineNumber, {int columnNumber})
-      : _map = {} {
-    _map['scriptId'] = scriptId;
-    _map['lineNumber'] = lineNumber;
+      : json = {} {
+    json['scriptId'] = scriptId;
+    json['lineNumber'] = lineNumber;
     if (columnNumber != null) {
-      _map['columnNumber'] = columnNumber;
+      json['columnNumber'] = columnNumber;
     }
   }
 
-  String get scriptId => _map['scriptId'];
+  String get scriptId => json['scriptId'];
 
-  int get lineNumber => _map['lineNumber'];
+  int get lineNumber => json['lineNumber'];
 
-  int get columnNumber => _map['columnNumber'];
+  int get columnNumber => json['columnNumber'];
 
   Map<String, dynamic> toJsonMap() {
-    return _map;
+    return json;
   }
 
   String toString() => '[${scriptId}:${lineNumber}:${columnNumber}]';
 }
 
-class WipRemoteObject {
-  final Map<String, dynamic> _map;
-
-  WipRemoteObject(this._map);
-
-  String get className => _map['className'] as String;
-
-  String get description => _map['description'] as String;
-
-  String get objectId => _map['objectId'] as String;
-
-  String get subtype => _map['subtype'] as String;
-
-  String get type => _map['type'] as String;
-
-  Object get value => _map['value'];
-}
-
 class WipScript {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  WipScript(this._map);
+  WipScript(this.json);
 
-  String get scriptId => _map['scriptId'] as String;
+  String get scriptId => json['scriptId'] as String;
 
-  String get url => _map['url'] as String;
+  String get url => json['url'] as String;
 
-  int get startLine => _map['startLine'] as int;
+  int get startLine => json['startLine'] as int;
 
-  int get startColumn => _map['startColumn'] as int;
+  int get startColumn => json['startColumn'] as int;
 
-  int get endLine => _map['endLine'] as int;
+  int get endLine => json['endLine'] as int;
 
-  int get endColumn => _map['endColumn'] as int;
+  int get endColumn => json['endColumn'] as int;
 
-  bool get isContentScript => _map['isContentScript'] as bool;
+  bool get isContentScript => json['isContentScript'] as bool;
 
-  String get sourceMapURL => _map['sourceMapURL'] as String;
+  String get sourceMapURL => json['sourceMapURL'] as String;
 
   String toString() => '[script ${scriptId}: ${url}]';
 }
 
 class WipScope {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  WipScope(this._map);
+  WipScope(this.json);
 
   // "catch", "closure", "global", "local", "with"
-  String get scope => _map['type'] as String;
+  String get scope => json['type'] as String;
 
   /// Name of the scope, null if unnamed closure or global scope
-  String get name => _map['name'] as String;
+  String get name => json['name'] as String;
 
   /// Object representing the scope. For global and with scopes it represents
   /// the actual object; for the rest of the scopes, it is artificial transient
   /// object enumerating scope variables as its properties.
-  WipRemoteObject get object =>
-      new WipRemoteObject(_map['object'] as Map<String, dynamic>);
+  RemoteObject get object =>
+      new RemoteObject(json['object'] as Map<String, dynamic>);
 }
 
 class WipBreakLocation extends WipLocation {
-  WipBreakLocation(Map<String, dynamic> map) : super(map);
+  WipBreakLocation(Map<String, dynamic> json) : super(json);
 
   WipBreakLocation.fromValues(String scriptId, int lineNumber,
       {int columnNumber, String type})
       : super.fromValues(scriptId, lineNumber, columnNumber: columnNumber) {
     if (type != null) {
-      _map['type'] = type;
+      json['type'] = type;
     }
   }
 
   /// Allowed Values: `debuggerStatement`, `call`, `return`.
-  String get type => _map['type'];
+  String get type => json['type'];
 }
 
 /// The response from [WipDebugger.setBreakpoint].
diff --git a/lib/src/dom.dart b/lib/src/dom.dart
index e8929ff..d1fd1e5 100644
--- a/lib/src/dom.dart
+++ b/lib/src/dom.dart
@@ -118,14 +118,14 @@
     return resp.result['nodeId'] as int;
   }
 
-  Future<WipRemoteObject> resolveNode(int nodeId, {String objectGroup}) async {
+  Future<RemoteObject> resolveNode(int nodeId, {String objectGroup}) async {
     var params = <String, dynamic>{'nodeId': nodeId};
     if (objectGroup != null) {
       params['objectGroup'] = objectGroup;
     }
 
     var resp = await sendCommand('DOM.resolveNode', params: params);
-    return new WipRemoteObject(resp.result['object'] as Map<String, dynamic>);
+    return new RemoteObject(resp.result['object'] as Map<String, dynamic>);
   }
 
   Future setAttributeValue(int nodeId, String name, String value) =>
@@ -156,64 +156,79 @@
 
   Stream<AttributeModifiedEvent> get onAttributeModified => eventStream(
       'DOM.attributeModified',
-      (WipEvent event) => new AttributeModifiedEvent(event));
+      (WipEvent event) => new AttributeModifiedEvent(event.json));
+
   Stream<AttributeRemovedEvent> get onAttributeRemoved => eventStream(
       'DOM.attributeRemoved',
-      (WipEvent event) => new AttributeRemovedEvent(event));
+      (WipEvent event) => new AttributeRemovedEvent(event.json));
+
   Stream<CharacterDataModifiedEvent> get onCharacterDataModified => eventStream(
       'DOM.characterDataModified',
-      (WipEvent event) => new CharacterDataModifiedEvent(event));
+      (WipEvent event) => new CharacterDataModifiedEvent(event.json));
+
   Stream<ChildNodeCountUpdatedEvent> get onChildNodeCountUpdated => eventStream(
       'DOM.childNodeCountUpdated',
-      (WipEvent event) => new ChildNodeCountUpdatedEvent(event));
+      (WipEvent event) => new ChildNodeCountUpdatedEvent(event.json));
+
   Stream<ChildNodeInsertedEvent> get onChildNodeInserted => eventStream(
       'DOM.childNodeInserted',
-      (WipEvent event) => new ChildNodeInsertedEvent(event));
+      (WipEvent event) => new ChildNodeInsertedEvent(event.json));
+
   Stream<ChildNodeRemovedEvent> get onChildNodeRemoved => eventStream(
       'DOM.childNodeRemoved',
-      (WipEvent event) => new ChildNodeRemovedEvent(event));
+      (WipEvent event) => new ChildNodeRemovedEvent(event.json));
+
   Stream<DocumentUpdatedEvent> get onDocumentUpdated => eventStream(
       'DOM.documentUpdated',
-      (WipEvent event) => new DocumentUpdatedEvent(event));
+      (WipEvent event) => new DocumentUpdatedEvent(event.json));
+
   Stream<SetChildNodesEvent> get onSetChildNodes => eventStream(
-      'DOM.setChildNodes', (WipEvent event) => new SetChildNodesEvent(event));
+      'DOM.setChildNodes',
+      (WipEvent event) => new SetChildNodesEvent(event.json));
 }
 
-class AttributeModifiedEvent extends WrappedWipEvent {
-  AttributeModifiedEvent(WipEvent event) : super(event);
+class AttributeModifiedEvent extends WipEvent {
+  AttributeModifiedEvent(Map<String, dynamic> json) : super(json);
 
   int get nodeId => params['nodeId'] as int;
+
   String get name => params['name'] as String;
+
   String get value => params['value'] as String;
 }
 
-class AttributeRemovedEvent extends WrappedWipEvent {
-  AttributeRemovedEvent(WipEvent event) : super(event);
+class AttributeRemovedEvent extends WipEvent {
+  AttributeRemovedEvent(Map<String, dynamic> json) : super(json);
 
   int get nodeId => params['nodeId'] as int;
+
   String get name => params['name'] as String;
 }
 
-class CharacterDataModifiedEvent extends WrappedWipEvent {
-  CharacterDataModifiedEvent(WipEvent event) : super(event);
+class CharacterDataModifiedEvent extends WipEvent {
+  CharacterDataModifiedEvent(Map<String, dynamic> json) : super(json);
 
   int get nodeId => params['nodeId'] as int;
+
   String get characterData => params['characterData'] as String;
 }
 
-class ChildNodeCountUpdatedEvent extends WrappedWipEvent {
-  ChildNodeCountUpdatedEvent(WipEvent event) : super(event);
+class ChildNodeCountUpdatedEvent extends WipEvent {
+  ChildNodeCountUpdatedEvent(Map<String, dynamic> json) : super(json);
 
   int get nodeId => params['nodeId'] as int;
+
   int get childNodeCount => params['childNodeCount'] as int;
 }
 
-class ChildNodeInsertedEvent extends WrappedWipEvent {
-  ChildNodeInsertedEvent(WipEvent event) : super(event);
+class ChildNodeInsertedEvent extends WipEvent {
+  ChildNodeInsertedEvent(Map<String, dynamic> json) : super(json);
 
   int get parentNodeId => params['parentNodeId'] as int;
+
   int get previousNodeId => params['previousNodeId'] as int;
   Node _node;
+
   Node get node {
     if (_node == null) {
       _node = new Node(params['node'] as Map<String, dynamic>);
@@ -222,21 +237,23 @@
   }
 }
 
-class ChildNodeRemovedEvent extends WrappedWipEvent {
-  ChildNodeRemovedEvent(WipEvent event) : super(event);
+class ChildNodeRemovedEvent extends WipEvent {
+  ChildNodeRemovedEvent(Map<String, dynamic> json) : super(json);
 
   int get parentNodeId => params['parentNodeId'] as int;
+
   int get nodeId => params['nodeId'] as int;
 }
 
-class DocumentUpdatedEvent extends WrappedWipEvent {
-  DocumentUpdatedEvent(WipEvent event) : super(event);
+class DocumentUpdatedEvent extends WipEvent {
+  DocumentUpdatedEvent(Map<String, dynamic> json) : super(json);
 }
 
-class SetChildNodesEvent extends WrappedWipEvent {
-  SetChildNodesEvent(WipEvent event) : super(event);
+class SetChildNodesEvent extends WipEvent {
+  SetChildNodesEvent(Map<String, dynamic> json) : super(json);
 
   int get nodeId => params['parentId'] as int;
+
   Iterable<Node> get nodes sync* {
     for (Map node in params['nodes']) {
       yield new Node(node as Map<String, dynamic>);
@@ -255,6 +272,7 @@
   Node(this._map);
 
   Map<String, String> _attributes;
+
   Map<String, String> get attributes {
     if (_attributes == null && _map.containsKey('attributes')) {
       _attributes = _attributeListToMap((_map['attributes'] as List).cast());
@@ -265,6 +283,7 @@
   int get childNodeCount => _map['childNodeCount'] as int;
 
   List<Node> _children;
+
   List<Node> get children {
     if (_children == null && _map.containsKey('children')) {
       _children = new UnmodifiableListView((_map['children'] as List)
diff --git a/lib/src/log.dart b/lib/src/log.dart
index 772d453..6dc5b46 100644
--- a/lib/src/log.dart
+++ b/lib/src/log.dart
@@ -12,12 +12,12 @@
 
   Future<WipResponse> disable() => sendCommand('Log.disable');
 
-  Stream<LogEntry> get onEntryAdded =>
-      eventStream('Log.entryAdded', (WipEvent event) => new LogEntry(event));
+  Stream<LogEntry> get onEntryAdded => eventStream(
+      'Log.entryAdded', (WipEvent event) => new LogEntry(event.json));
 }
 
-class LogEntry extends WrappedWipEvent {
-  LogEntry(WipEvent event) : super(event);
+class LogEntry extends WipEvent {
+  LogEntry(Map<String, dynamic> json) : super(json);
 
   Map<String, dynamic> get _entry => params['entry'] as Map<String, dynamic>;
 
diff --git a/lib/src/runtime.dart b/lib/src/runtime.dart
index bd6d647..b802289 100644
--- a/lib/src/runtime.dart
+++ b/lib/src/runtime.dart
@@ -64,8 +64,8 @@
   /// object (int, String, double, bool).
   Future<RemoteObject> callFunctionOn(
     String functionDeclaration, {
-    List<dynamic> arguments,
     String objectId,
+    List<dynamic> arguments,
     bool returnByValue,
     int executionContextId,
   }) async {
@@ -104,13 +104,57 @@
     }
   }
 
+  /// Returns the JavaScript heap usage. It is the total usage of the
+  /// corresponding isolate not scoped to a particular Runtime.
+  @experimental
+  Future<HeapUsage> getHeapUsage() async {
+    final WipResponse response = await sendCommand('Runtime.getHeapUsage');
+    return HeapUsage(response.result);
+  }
+
+  /// Returns the isolate id.
+  @experimental
+  Future<String> getIsolateId() async {
+    return (await sendCommand('Runtime.getIsolateId')).result['id'] as String;
+  }
+
+  /// Returns properties of a given object. Object group of the result is
+  /// inherited from the target object.
+  ///
+  /// objectId: Identifier of the object to return properties for.
+  ///
+  /// ownProperties: If true, returns properties belonging only to the element
+  /// itself, not to its prototype chain.
+  Future<List<PropertyDescriptor>> getProperties(
+    RemoteObject object, {
+    bool ownProperties,
+  }) async {
+    Map<String, dynamic> params = {
+      'objectId': object.objectId,
+    };
+    if (ownProperties != null) {
+      params['ownProperties'] = ownProperties;
+    }
+
+    final WipResponse response =
+        await sendCommand('Runtime.getProperties', params: params);
+
+    if (response.result.containsKey('exceptionDetails')) {
+      throw new ExceptionDetails(
+          response.result['exceptionDetails'] as Map<String, dynamic>);
+    } else {
+      List locations = response.result['result'];
+      return List.from(locations.map((map) => PropertyDescriptor(map)));
+    }
+  }
+
   Stream<ConsoleAPIEvent> get onConsoleAPICalled => eventStream(
       'Runtime.consoleAPICalled',
-      (WipEvent event) => new ConsoleAPIEvent(event));
+      (WipEvent event) => new ConsoleAPIEvent(event.json));
 
   Stream<ExceptionThrownEvent> get onExceptionThrown => eventStream(
       'Runtime.exceptionThrown',
-      (WipEvent event) => new ExceptionThrownEvent(event));
+      (WipEvent event) => new ExceptionThrownEvent(event.json));
 
   /// Issued when new execution context is created.
   Stream<ExecutionContextDescription> get onExecutionContextCreated =>
@@ -129,44 +173,43 @@
       'Runtime.executionContextsCleared', (WipEvent event) => event);
 }
 
-class ConsoleAPIEvent extends WrappedWipEvent {
-  ConsoleAPIEvent(WipEvent event) : super(event);
+// TODO: stackTrace, StackTrace, Stack trace captured when the call was made.
+class ConsoleAPIEvent extends WipEvent {
+  ConsoleAPIEvent(Map<String, dynamic> json) : super(json);
 
   /// Type of the call. Allowed values: log, debug, info, error, warning, dir,
   /// dirxml, table, trace, clear, startGroup, startGroupCollapsed, endGroup,
   /// assert, profile, profileEnd.
   String get type => params['type'] as String;
 
-  // Call timestamp.
+  /// Call timestamp.
   num get timestamp => params['timestamp'] as num;
 
   /// Call arguments.
   List<RemoteObject> get args => (params['args'] as List)
       .map((m) => new RemoteObject(m as Map<String, dynamic>))
       .toList();
-
-// TODO: stackTrace, StackTrace, Stack trace captured when the call was made.
 }
 
 /// Description of an isolated world.
 class ExecutionContextDescription {
-  final Map<String, dynamic> map;
+  final Map<String, dynamic> json;
 
-  ExecutionContextDescription(this.map);
+  ExecutionContextDescription(this.json);
 
   /// Unique id of the execution context. It can be used to specify in which
   /// execution context script evaluation should be performed.
-  int get id => map['id'] as int;
+  int get id => json['id'] as int;
 
   /// Execution context origin.
-  String get origin => map['origin'];
+  String get origin => json['origin'];
 
   /// Human readable name describing given context.
-  String get name => map['name'];
+  String get name => json['name'];
 }
 
-class ExceptionThrownEvent extends WrappedWipEvent {
-  ExceptionThrownEvent(WipEvent event) : super(event);
+class ExceptionThrownEvent extends WipEvent {
+  ExceptionThrownEvent(Map<String, dynamic> json) : super(json);
 
   /// Timestamp of the exception.
   int get timestamp => params['timestamp'] as int;
@@ -176,60 +219,59 @@
 }
 
 class ExceptionDetails implements Exception {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  ExceptionDetails(this._map);
-
-  Map<String, dynamic> get json => _map;
+  ExceptionDetails(this.json);
 
   /// Exception id.
-  int get exceptionId => _map['exceptionId'] as int;
+  int get exceptionId => json['exceptionId'] as int;
 
   /// Exception text, which should be used together with exception object when
   /// available.
-  String get text => _map['text'] as String;
+  String get text => json['text'] as String;
 
   /// Line number of the exception location (0-based).
-  int get lineNumber => _map['lineNumber'] as int;
+  int get lineNumber => json['lineNumber'] as int;
 
   /// Column number of the exception location (0-based).
-  int get columnNumber => _map['columnNumber'] as int;
+  int get columnNumber => json['columnNumber'] as int;
 
   /// URL of the exception location, to be used when the script was not
   /// reported.
   @optional
-  String get url => _map['url'] as String;
+  String get url => json['url'] as String;
 
   /// Script ID of the exception location.
   @optional
-  String get scriptId => _map['scriptId'] as String;
+  String get scriptId => json['scriptId'] as String;
 
   /// JavaScript stack trace if available.
   @optional
-  StackTrace get stackTrace => _map['stackTrace'] == null
+  StackTrace get stackTrace => json['stackTrace'] == null
       ? null
-      : new StackTrace(_map['stackTrace'] as Map<String, dynamic>);
+      : new StackTrace(json['stackTrace'] as Map<String, dynamic>);
 
   /// Exception object if available.
   @optional
-  RemoteObject get exception => _map['exception'] == null
+  RemoteObject get exception => json['exception'] == null
       ? null
-      : new RemoteObject(_map['exception'] as Map<String, dynamic>);
+      : new RemoteObject(json['exception'] as Map<String, dynamic>);
 
   String toString() => '$text, $url, $scriptId, $lineNumber, $exception';
 }
 
+/// Call frames for assertions or error messages.
 class StackTrace {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  StackTrace(this._map);
+  StackTrace(this.json);
 
   /// String label of this stack trace. For async traces this may be a name of
   /// the function that initiated the async call.
   @optional
-  String get description => _map['description'] as String;
+  String get description => json['description'] as String;
 
-  List<CallFrame> get callFrames => (_map['callFrames'] as List)
+  List<CallFrame> get callFrames => (json['callFrames'] as List)
       .map((m) => new CallFrame(m as Map<String, dynamic>))
       .toList();
 
@@ -252,48 +294,113 @@
   String toString() => callFrames.map((f) => '  $f').join('\n');
 }
 
+/// Stack entry for runtime errors and assertions.
 class CallFrame {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  CallFrame(this._map);
+  CallFrame(this.json);
 
   /// JavaScript function name.
-  String get functionName => _map['functionName'] as String;
+  String get functionName => json['functionName'] as String;
 
   /// JavaScript script id.
-  String get scriptId => _map['scriptId'] as String;
+  String get scriptId => json['scriptId'] as String;
 
   /// JavaScript script name or url.
-  String get url => _map['url'] as String;
+  String get url => json['url'] as String;
 
   /// JavaScript script line number (0-based).
-  int get lineNumber => _map['lineNumber'] as int;
+  int get lineNumber => json['lineNumber'] as int;
 
   /// JavaScript script column number (0-based).
-  int get columnNumber => _map['columnNumber'] as int;
+  int get columnNumber => json['columnNumber'] as int;
 
   String toString() => '$functionName() ($url $lineNumber:$columnNumber)';
 }
 
 /// Mirror object referencing original JavaScript object.
 class RemoteObject {
-  final Map<String, dynamic> _map;
+  final Map<String, dynamic> json;
 
-  RemoteObject(this._map);
+  RemoteObject(this.json);
 
-  /// Object type.object, function, undefined, string, number, boolean, symbol,
-  /// bigint.
-  String get type => _map['type'] as String;
+  /// Object type.
+  ///
+  /// Allowed Values: object, function, undefined, string, number, boolean,
+  /// symbol, bigint, wasm.
+  String get type => json['type'] as String;
+
+  /// Object subtype hint. Specified for object or wasm type values only.
+  ///
+  /// Allowed Values: array, null, node, regexp, date, map, set, weakmap,
+  /// weakset, iterator, generator, error, proxy, promise, typedarray,
+  /// arraybuffer, dataview, i32, i64, f32, f64, v128, anyref.
+  String get subtype => json['subtype'] as String;
+
+  /// Object class (constructor) name.
+  ///
+  /// Specified for object type values only.
+  String get className => json['className'] as String;
 
   /// Remote object value in case of primitive values or JSON values (if it was
   /// requested). (optional)
-  Object get value => _map['value'];
+  Object get value => json['value'];
 
   /// String representation of the object. (optional)
-  String get description => _map['description'] as String;
+  String get description => json['description'] as String;
 
   /// Unique object identifier (for non-primitive values). (optional)
-  String get objectId => _map['objectId'] as String;
+  String get objectId => json['objectId'] as String;
 
+  @override
   String toString() => '$type $value';
 }
+
+/// Returns the JavaScript heap usage. It is the total usage of the
+/// corresponding isolate not scoped to a particular Runtime.
+class HeapUsage {
+  final Map<String, dynamic> json;
+
+  HeapUsage(this.json);
+
+  /// Used heap size in bytes.
+  int get usedSize => json['usedSize'];
+
+  /// Allocated heap size in bytes.
+  int get totalSize => json['totalSize'];
+
+  @override
+  String toString() => '$usedSize of $totalSize';
+}
+
+/// Object property descriptor.
+class PropertyDescriptor {
+  final Map<String, dynamic> json;
+
+  PropertyDescriptor(this.json);
+
+  /// Property name or symbol description.
+  String get name => json['name'];
+
+  /// The value associated with the property.
+  RemoteObject get value =>
+      json['value'] != null ? RemoteObject(json['value']) : null;
+
+  /// True if the value associated with the property may be changed (data
+  /// descriptors only).
+  bool get writable => json['writable'];
+
+  /// True if the type of this property descriptor may be changed and if the
+  /// property may be deleted from the corresponding object.
+  bool get configurable => json['configurable'];
+
+  /// True if this property shows up during enumeration of the properties on the
+  /// corresponding object.
+  bool get enumerable => json['enumerable'];
+
+  /// True if the result was thrown during the evaluation.
+  bool get wasThrown => json['wasThrown'];
+
+  /// True if the property is owned for the object.
+  bool get isOwn => json['isOwn'];
+}
diff --git a/lib/webkit_inspection_protocol.dart b/lib/webkit_inspection_protocol.dart
index 3618974..1a0330e 100644
--- a/lib/webkit_inspection_protocol.dart
+++ b/lib/webkit_inspection_protocol.dart
@@ -239,12 +239,14 @@
 }
 
 class WipEvent {
+  final Map<String, dynamic> json;
+
   final String method;
   final Map<String, dynamic> params;
 
-  WipEvent(Map<String, dynamic> map)
-      : method = map['method'] as String,
-        params = map['params'] as Map<String, dynamic>;
+  WipEvent(this.json)
+      : method = json['method'] as String,
+        params = json['params'] as Map<String, dynamic>;
 
   String toString() => 'WipEvent: $method($params)';
 }
@@ -322,18 +324,6 @@
   }
 }
 
-class WrappedWipEvent implements WipEvent {
-  final WipEvent _wrapped;
-
-  WrappedWipEvent(this._wrapped);
-
-  @override
-  String get method => _wrapped.method;
-
-  @override
-  Map<String, dynamic> get params => _wrapped.params;
-}
-
 const _Experimental experimental = const _Experimental();
 
 class _Experimental {
diff --git a/pubspec.yaml b/pubspec.yaml
index 76174a9..9dc68a5 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: webkit_inspection_protocol
-version: 0.6.0
+version: 0.7.0
 description: A client for the Chrome DevTools Protocol (previously called the Webkit Inspection Protocol).
 homepage: https://github.com/google/webkit_inspection_protocol.dart
 
diff --git a/test/data/runtime_test.html b/test/data/runtime_test.html
new file mode 100644
index 0000000..3f9d708
--- /dev/null
+++ b/test/data/runtime_test.html
@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html>
+<head lang="en">
+    <meta charset="UTF-8">
+    <title></title>
+</head>
+<body>
+<script language="JavaScript">
+    console.clear();
+    console.log('message 1');
+</script>
+
+</body>
+</html>
diff --git a/test/dom_model_test.dart b/test/dom_model_test.dart
index c7b63f1..762d128 100644
--- a/test/dom_model_test.dart
+++ b/test/dom_model_test.dart
@@ -124,6 +124,6 @@
 
       expect(getAttributes, attributes);
       expect(bodyNode.attributes, attributes);
-    });
+    }, skip: 'google/webkit_inspection_protocol.dart/issues/52');
   });
 }
diff --git a/test/runtime_test.dart b/test/runtime_test.dart
new file mode 100644
index 0000000..4f4b78a
--- /dev/null
+++ b/test/runtime_test.dart
@@ -0,0 +1,96 @@
+// Copyright 2020 Google. All rights reserved. Use of this source code is
+// governed by a BSD-style license that can be found in the LICENSE file.
+
+@TestOn('vm')
+library wip.runtime_test;
+
+import 'dart:async';
+
+import 'package:test/test.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import 'test_setup.dart';
+
+void main() {
+  group('WipRuntime', () {
+    WipRuntime runtime;
+    List<StreamSubscription> subs = [];
+
+    setUp(() async {
+      runtime = (await wipConnection).runtime;
+    });
+
+    tearDown(() async {
+      await runtime.disable();
+      runtime = null;
+
+      await closeConnection();
+      subs.forEach((s) => s.cancel());
+      subs.clear();
+    });
+
+    test('getIsolateId', () async {
+      await runtime.enable();
+      await navigateToPage('runtime_test.html');
+
+      expect(await runtime.getIsolateId(), isNotEmpty);
+    });
+
+    test('getHeapUsage', () async {
+      await runtime.enable();
+      await navigateToPage('runtime_test.html');
+
+      HeapUsage usage = await runtime.getHeapUsage();
+
+      expect(usage.usedSize, greaterThan(0));
+      expect(usage.totalSize, greaterThan(0));
+    });
+
+    test('evaluate', () async {
+      await runtime.enable();
+      await navigateToPage('runtime_test.html');
+
+      RemoteObject result = await runtime.evaluate('1+1');
+      expect(result.type, 'number');
+      expect(result.value, 2);
+    });
+
+    test('callFunctionOn', () async {
+      await runtime.enable();
+      await navigateToPage('runtime_test.html');
+
+      RemoteObject console = await runtime.evaluate('console');
+      RemoteObject result = await runtime.callFunctionOn(
+        '''
+        function(msg) {
+          console.log(msg);
+          return msg;
+        }''',
+        objectId: console.objectId,
+        arguments: [
+          'foo',
+        ],
+      );
+
+      expect(result.type, 'string');
+      expect(result.value, 'foo');
+    });
+
+    test('getProperties', () async {
+      await runtime.enable();
+      await navigateToPage('runtime_test.html');
+
+      RemoteObject console = await runtime.evaluate('console');
+
+      List<PropertyDescriptor> properties = await runtime.getProperties(
+        console,
+        ownProperties: true,
+      );
+
+      expect(properties, isNotEmpty);
+
+      PropertyDescriptor property = properties.first;
+      expect(property.name, isNotEmpty);
+    });
+  });
+}
diff --git a/test/test_setup.dart b/test/test_setup.dart
index 5f478b2..5a5cfbe 100644
--- a/test/test_setup.dart
+++ b/test/test_setup.dart
@@ -34,6 +34,10 @@
 /// Starts ChromeDriver and returns the listening port.
 Future<int> _startChromeDriver() async {
   var chromeDriverPort = await findUnusedPort();
+
+  // Delay a small amount to allow us to close the above port.
+  await Future.delayed(const Duration(milliseconds: 25));
+
   try {
     var _exeExt = Platform.isWindows ? '.exe' : '';
     _chromeDriver = await Process.start('chromedriver$_exeExt',