Add dom_model and allow multiplexor to use it so that multiple connected WIP clients don't stomp on each other when working with the DOM.
diff --git a/bin/multiplex.dart b/bin/multiplex.dart
index 56899ed..2ea011c 100644
--- a/bin/multiplex.dart
+++ b/bin/multiplex.dart
@@ -3,21 +3,25 @@
library wip.multiplex;
-import 'dart:async';
+import 'dart:async' show Future;
import 'dart:convert' show JSON;
import 'dart:io' show HttpClientResponse, HttpServer, InternetAddress, stderr;
-import 'package:args/args.dart';
-import 'package:logging/logging.dart';
+import 'package:args/args.dart' show ArgParser;
+import 'package:logging/logging.dart'
+ show hierarchicalLoggingEnabled, Level, Logger, LogRecord;
import 'package:shelf/shelf.dart' as shelf;
import 'package:shelf/shelf_io.dart' as io;
import 'package:shelf_web_socket/shelf_web_socket.dart' as ws;
-import 'package:webkit_inspection_protocol/forwarder.dart';
-import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+import 'package:webkit_inspection_protocol/dom_model.dart' show WipDomModel;
+import 'package:webkit_inspection_protocol/forwarder.dart' show WipForwarder;
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart'
+ show ChromeConnection, ChromeTab, WipConnection;
main(List<String> argv) async {
var args = (new ArgParser()
..addFlag('verbose', abbr: 'v', defaultsTo: false, negatable: false)
+ ..addFlag('model_dom', defaultsTo: true, negatable: true)
..addOption('chrome_host', defaultsTo: 'localhost')
..addOption('chrome_port', defaultsTo: '9222')
..addOption('listen_port', defaultsTo: '9223')).parse(argv);
@@ -36,7 +40,7 @@
var cr =
new ChromeConnection(args['chrome_host'], int.parse(args['chrome_port']));
- new Server(int.parse(args['listen_port']), cr);
+ new Server(int.parse(args['listen_port']), cr, modelDom: args['model_dom']);
}
class Server {
@@ -45,10 +49,12 @@
Future<HttpServer> _server;
final ChromeConnection chrome;
final int port;
+ final bool modelDom;
final _connections = <String, Future<WipConnection>>{};
+ final _modelDoms = <String, WipDomModel>{};
- Server(this.port, this.chrome) {
+ Server(this.port, this.chrome, {this.modelDom}) {
_server = io.serve(_handler, InternetAddress.ANY_IP_V4, port);
}
@@ -130,6 +136,7 @@
Future<shelf.Response> _forward(shelf.Request request) async {
_log.info('forwarding: ${request.url}');
var dtResp = await chrome.getUrl(request.url.path);
+
if (dtResp.statusCode == 200) {
return new shelf.Response.ok(dtResp,
headers: {'Content-Type': dtResp.headers.contentType.toString()});
@@ -151,9 +158,16 @@
var tab = await chrome.getTab((tab) => tab.id == path[2]);
return WipConnection.connect(tab.webSocketDebuggerUrl);
});
- var forwarder = new ChromeForwarder(debugger, ws);
+ var dom;
+ if (modelDom) {
+ dom = await _modelDoms.putIfAbsent(path[2], () {
+ return new WipDomModel(debugger.dom);
+ });
+ }
+ var forwarder = new WipForwarder(debugger, ws, domModel: dom);
debugger.onClose.listen((_) {
_connections.remove(path[2]);
+ _modelDoms.remove(path[2]);
forwarder.stop();
});
})(request);
diff --git a/lib/dom_model.dart b/lib/dom_model.dart
new file mode 100644
index 0000000..0ddea83
--- /dev/null
+++ b/lib/dom_model.dart
@@ -0,0 +1,333 @@
+// Copyright 2015 Google. All rights reserved. Use of this source code is
+// governed by a BSD-style license that can be found in the LICENSE file.
+
+library wip.dom_model;
+
+import 'dart:async' show EventSink, Future, Stream, StreamTransformer;
+import 'dart:collection' show UnmodifiableListView, UnmodifiableMapView;
+import 'dart:mirrors' show reflect;
+
+import 'package:logging/logging.dart' show Logger;
+
+import 'webkit_inspection_protocol.dart'
+ show
+ AttributeModifiedEvent,
+ AttributeRemovedEvent,
+ CharacterDataModifiedEvent,
+ ChildNodeCountUpdatedEvent,
+ ChildNodeInsertedEvent,
+ ChildNodeRemovedEvent,
+ DocumentUpdatedEvent,
+ Node,
+ SetChildNodesEvent,
+ WipDom,
+ WipEvent;
+
+/// Implementation of WipDom that maintains and updates a model of the DOM
+/// based on incoming events.
+class WipDomModel implements WipDom {
+ static final _log = new Logger('WipDomModel');
+
+ final WipDom _dom;
+
+ Map<int, _Node> _nodeCache = {};
+ Future<_Node> _root;
+
+ Stream<AttributeModifiedEvent> onAttributeModified;
+ Stream<AttributeRemovedEvent> onAttributeRemoved;
+ Stream<CharacterDataModifiedEvent> onCharacterDataModified;
+ Stream<ChildNodeCountUpdatedEvent> onChildNodeCountUpdated;
+ Stream<ChildNodeInsertedEvent> onChildNodeInserted;
+ Stream<ChildNodeRemovedEvent> onChildNodeRemoved;
+ Stream<DocumentUpdatedEvent> onDocumentUpdated;
+ Stream<SetChildNodesEvent> onSetChildNodes;
+
+ WipDomModel(this._dom) {
+ onAttributeModified = new StreamTransformer.fromHandlers(
+ handleData: _onAttributeModified).bind(_dom.onAttributeModified)
+ ..listen(_logEvent);
+ onAttributeRemoved = new StreamTransformer.fromHandlers(
+ handleData: _onAttributeRemoved).bind(_dom.onAttributeRemoved)
+ ..listen(_logEvent);
+ onCharacterDataModified = new StreamTransformer.fromHandlers(
+ handleData: _onCharacterDataModified).bind(_dom.onCharacterDataModified)
+ ..listen(_logEvent);
+ onChildNodeCountUpdated = new StreamTransformer.fromHandlers(
+ handleData: _onChildNodeCountUpdated).bind(_dom.onChildNodeCountUpdated)
+ ..listen(_logEvent);
+ onChildNodeInserted = new StreamTransformer.fromHandlers(
+ handleData: _onChildNodeInserted).bind(_dom.onChildNodeInserted)
+ ..listen(_logEvent);
+ onChildNodeRemoved = new StreamTransformer.fromHandlers(
+ handleData: _onChildNodeRemoved).bind(_dom.onChildNodeRemoved)
+ ..listen(_logEvent);
+ onDocumentUpdated = new StreamTransformer.fromHandlers(
+ handleData: _onDocumentUpdated).bind(_dom.onDocumentUpdated)
+ ..listen(_logEvent);
+ onSetChildNodes = new StreamTransformer.fromHandlers(
+ handleData: _onSetChildNodes).bind(_dom.onSetChildNodes)
+ ..listen(_logEvent);
+ }
+
+ _logEvent(WipEvent event) {
+ _log.finest('Event $event');
+ }
+
+ _onAttributeModified(
+ AttributeModifiedEvent event, EventSink<AttributeModifiedEvent> sink) {
+ var node = _getOrCreateNode(event.nodeId);
+ node._attributes[event.name] = event.value;
+ sink.add(event);
+ }
+
+ _onAttributeRemoved(
+ AttributeRemovedEvent event, EventSink<AttributeRemovedEvent> sink) {
+ var node = _getOrCreateNode(event.nodeId);
+ node._attributes.remove(event.name);
+ sink.add(event);
+ }
+
+ _onCharacterDataModified(CharacterDataModifiedEvent event,
+ EventSink<CharacterDataModifiedEvent> sink) {
+ var node = _getOrCreateNode(event.nodeId);
+ node._nodeValue = event.characterData;
+ sink.add(event);
+ }
+
+ _onChildNodeCountUpdated(ChildNodeCountUpdatedEvent event,
+ EventSink<ChildNodeCountUpdatedEvent> sink) {
+ var node = _getOrCreateNode(event.nodeId);
+ node._childNodeCount = event.childNodeCount;
+ sink.add(event);
+ }
+
+ _onChildNodeInserted(
+ ChildNodeInsertedEvent event, EventSink<ChildNodeInsertedEvent> sink) {
+ var parent = _getOrCreateNode(event.parentNodeId);
+ int index = 0;
+ if (event.previousNodeId != null) {
+ index =
+ parent._children.indexOf(_getOrCreateNode(event.previousNodeId)) + 1;
+ }
+ var node = _getOrCreateNodeFromNode(event.node);
+ parent._children.insert(index, node);
+ parent._childNodeCount = parent._children.length;
+ sink.add(event);
+ }
+
+ _onChildNodeRemoved(
+ ChildNodeRemovedEvent event, EventSink<ChildNodeRemovedEvent> sink) {
+ var parent = _getOrCreateNode(event.parentNodeId);
+ var node = _nodeCache.remove(event.nodeId);
+ parent._children.remove(node);
+ parent._childNodeCount = parent._children.length;
+ sink.add(event);
+ }
+
+ _onDocumentUpdated(
+ DocumentUpdatedEvent event, EventSink<DocumentUpdatedEvent> sink) {
+ _nodeCache.clear();
+ _root = null;
+ sink.add(event);
+ }
+
+ _onSetChildNodes(
+ SetChildNodesEvent event, EventSink<SetChildNodesEvent> sink) {
+ var parent = _getOrCreateNode(event.nodeId);
+ parent._children =
+ event.nodes.map(_getOrCreateNodeFromNode).toList(growable: true);
+ parent._childNodeCount = parent._children.length;
+ sink.add(event);
+ }
+
+ @override
+ Future<Map<String, String>> getAttributes(int nodeId) async {
+ Map<String, String> attributes = await _dom.getAttributes(nodeId);
+ var node = _getOrCreateNode(nodeId);
+ node._attributes = new Map.from(attributes);
+ return attributes;
+ }
+
+ /// Unlike the standard [WipDom.getDocument] call, this will not
+ /// reset the internal state of the debugger remote end when called
+ /// multiple times on the same page.
+ @override
+ Future<Node> getDocument() {
+ if (_root == null) {
+ _root = _dom.getDocument().then((n) => _getOrCreateNodeFromNode(n));
+ }
+ return _root;
+ }
+
+ _Node _getOrCreateNode(int nodeId) =>
+ _nodeCache.putIfAbsent(nodeId, () => new _Node(nodeId));
+
+ _Node _getOrCreateNodeFromNode(Node src) {
+ try {
+ var node = _getOrCreateNode(src.nodeId);
+ if (src.attributes != null) {
+ node._attributes = new Map.from(src.attributes);
+ }
+ if (src.children != null) {
+ node._children =
+ src.children.map(_getOrCreateNodeFromNode).toList(growable: true);
+ }
+ node._childNodeCount = src.childNodeCount;
+ if (src.contentDocument != null) {
+ node._contentDocument = _getOrCreateNodeFromNode(src.contentDocument);
+ }
+ node._documentUrl = src.documentUrl;
+ node._internalSubset = src.internalSubset;
+ node._localName = src.localName;
+ node._name = src.name;
+ node._nodeName = src.nodeName;
+ node._nodeType = src.nodeType;
+ node._nodeValue = src.nodeValue;
+ node._publicId = src.publicId;
+ node._systemId = src.systemId;
+ node._value = src.value;
+ node._xmlVersion = src.xmlVersion;
+ return node;
+ } catch (e, s) {
+ _log.severe('Error parsing: $src.nodeId', e, s);
+ rethrow;
+ }
+ }
+
+ noSuchMethod(Invocation invocation) => reflect(_dom).delegate(invocation);
+}
+
+class _Node implements Node {
+ Map<String, String> _attributes;
+ @override
+ Map<String, String> get attributes =>
+ _attributes != null ? new UnmodifiableMapView(_attributes) : null;
+
+ int _childNodeCount;
+ @override
+ int get childNodeCount => _childNodeCount;
+
+ List<_Node> _children;
+ @override
+ List<Node> get children =>
+ _children != null ? new UnmodifiableListView(_children) : null;
+
+ _Node _contentDocument;
+ @override
+ Node get contentDocument => _contentDocument;
+
+ String _documentUrl;
+ @override
+ String get documentUrl => _documentUrl;
+
+ String _internalSubset;
+ @override
+ String get internalSubset => _internalSubset;
+
+ String _localName;
+ @override
+ String get localName => _localName;
+
+ String _name;
+ @override
+ String get name => _name;
+
+ @override
+ final int nodeId;
+
+ String _nodeName;
+ @override
+ String get nodeName => _nodeName;
+
+ int _nodeType;
+ @override
+ int get nodeType => _nodeType;
+
+ String _nodeValue;
+ @override
+ String get nodeValue => _nodeValue;
+
+ String _publicId;
+ @override
+ String get publicId => _publicId;
+
+ String _systemId;
+ @override
+ String get systemId => _systemId;
+
+ String _value;
+ @override
+ String get value => _value;
+
+ String _xmlVersion;
+ @override
+ String get xmlVersion => _xmlVersion;
+
+ _Node(this.nodeId);
+
+ Map toJson() => _toJsonInternal(new Set());
+
+ Map _toJsonInternal(Set visited) {
+ var map = {
+ 'localName': localName,
+ 'nodeId': nodeId,
+ 'nodeName': nodeName,
+ 'nodeType': nodeType,
+ 'nodeValue': nodeValue
+ };
+ if (visited.add(nodeId)) {
+ if (attributes != null && attributes.isNotEmpty) {
+ map['attributes'] = flattenAttributesMap(attributes);
+ }
+ if (childNodeCount != null) {
+ map['childNodeCount'] = childNodeCount;
+ }
+ if (children != null && children.isNotEmpty) {
+ var newChildren = [];
+ children.forEach((child) {
+ if (child != null) {
+ newChildren.add(child._toJsonInternal(visited));
+ }
+ });
+ map['children'] = newChildren;
+ }
+ if (_contentDocument != null) {
+ map['contentDocument'] = _contentDocument._toJsonInternal(visited);
+ }
+ if (documentUrl != null) {
+ map['documentUrl'] = documentUrl;
+ }
+ if (internalSubset != null) {
+ map['internalSubset'] = internalSubset;
+ }
+ if (name != null) {
+ map['name'] = name;
+ }
+ if (publicId != null) {
+ map['publicId'] = publicId;
+ }
+ if (systemId != null) {
+ map['systemId'] = systemId;
+ }
+ if (value != null) {
+ map['value'] = value;
+ }
+ if (xmlVersion != null) {
+ map['xmlVersion'] = xmlVersion;
+ }
+ }
+ return map;
+ }
+}
+
+List<String> flattenAttributesMap(Map<String, String> attributes) {
+ var result = <String>[];
+ attributes.forEach((k, v) {
+ if (k != null) {
+ result
+ ..add(k)
+ ..add(v);
+ }
+ });
+ return result;
+}
diff --git a/lib/forwarder.dart b/lib/forwarder.dart
index 8dc158d..cb165b6 100644
--- a/lib/forwarder.dart
+++ b/lib/forwarder.dart
@@ -3,35 +3,45 @@
library crmux.forwarder;
-import 'dart:async';
-import 'dart:convert';
+import 'dart:async'
+ show
+ Completer,
+ Future,
+ Stream,
+ StreamController,
+ StreamSink,
+ StreamSubscription;
+import 'dart:convert' show JSON;
-import 'package:logging/logging.dart';
+import 'package:logging/logging.dart' show Logger;
-import 'webkit_inspection_protocol.dart';
+import 'dom_model.dart' show flattenAttributesMap;
+import 'webkit_inspection_protocol.dart'
+ show WipConnection, WipDom, WipError, WipEvent, WipResponse;
/// Forwards a [Stream] to a [WipConnection] and events
/// from a [WipConnection] to a [StreamSink].
-class ChromeForwarder {
+class WipForwarder {
static final _log = new Logger('ChromeForwarder');
final Stream _in;
final StreamSink _out;
final WipConnection _debugger;
+ final WipDom domModel;
final _subscriptions = <StreamSubscription>[];
final _closedController = new StreamController.broadcast();
- factory ChromeForwarder(WipConnection debugger, Stream stream,
- [StreamSink sink]) {
+ factory WipForwarder(WipConnection debugger, Stream stream,
+ {StreamSink sink, WipDom domModel}) {
if (sink == null) {
sink = stream as StreamSink;
}
- return new ChromeForwarder._(debugger, stream, sink);
+ return new WipForwarder._(debugger, stream, sink, domModel);
}
- ChromeForwarder._(this._debugger, this._in, this._out) {
+ WipForwarder._(this._debugger, this._in, this._out, this.domModel) {
_subscriptions.add(_in.listen(_onClientDataHandler,
onError: _onClientErrorHandler, onDone: _onClientDoneHandler));
_subscriptions.add(_debugger.onNotification.listen(_onDebuggerDataHandler,
@@ -43,13 +53,37 @@
var response = {'id': json['id']};
_log.info('Forwarding to debugger: $data');
try {
- var resp = await _debugger.sendCommand(json['method'], json['params']);
- if (resp.result != null) {
- response['result'] = resp.result;
+ String method = json['method'];
+ Map<String, dynamic> params = json['params'];
+ bool processed = false;
+
+ if (domModel != null) {
+ switch (method) {
+ case 'DOM.getDocument':
+ response['result'] = {'root': (await domModel.getDocument())};
+ processed = true;
+ break;
+ case 'DOM.getAttributes':
+ var attributes = flattenAttributesMap(
+ await domModel.getAttributes(params['nodeId']));
+ response['result'] = {'attributes': attributes};
+ processed = true;
+ break;
+ }
+ }
+ if (!processed) {
+ WipResponse resp = await _debugger.sendCommand(method, params);
+ if (resp.result != null) {
+ response['result'] = resp.result;
+ }
}
} on WipError catch (e) {
response['error'] = e.error;
+ } catch (e, s) {
+ _log.severe(json['id'], e.toString(), s);
+ response['error'] = e.toString();
}
+ _log.info('forwarding response: $response');
_out.add(JSON.encode(response));
}
diff --git a/lib/src/console.dart b/lib/src/console.dart
index 1b2e256..df87c3c 100644
--- a/lib/src/console.dart
+++ b/lib/src/console.dart
@@ -29,7 +29,6 @@
String get text => _message['text'];
String get level => _message['level'];
String get url => _message['url'];
- int get repeatCount => _message.putIfAbsent('repeatCount', () => 1);
Iterable<WipConsoleCallFrame> getStackTrace() {
if (_message.containsKey('stackTrace')) {
diff --git a/lib/src/dom.dart b/lib/src/dom.dart
index db0bf05..c7c5a49 100644
--- a/lib/src/dom.dart
+++ b/lib/src/dom.dart
@@ -11,12 +11,7 @@
Future<Map<String, String>> getAttributes(int nodeId) async {
WipResponse resp =
await _sendCommand('DOM.getAttributes', {'nodeId': nodeId});
- var attributes = {};
- for (int i = 0; i < resp.result['attributes'].length; i += 2) {
- attributes[resp.result['attributes'][i]] =
- attributes[resp.result['attributes'][i + 1]];
- }
- return new UnmodifiableMapView<String, String>(attributes);
+ return _attributeListToMap(resp.result['attributes']);
}
Future<Node> getDocument() async =>
@@ -232,6 +227,8 @@
yield new Node(node);
}
}
+
+ String toString() => 'SetChildNodes $nodeId: $nodes';
}
/// The backend keeps track of which DOM nodes have been sent,
@@ -245,24 +242,20 @@
var _attributes;
Map<String, String> get attributes {
if (_attributes == null && _map.containsKey('attributes')) {
- var attributes = {};
- for (int i = 0; i < _map['attributes'].length; i += 2) {
- attributes[_map['attributes'][i]] =
- attributes[_map['attributes'][i + 1]];
- }
- _attributes = new UnmodifiableMapView<String, String>(attributes);
+ _attributes = _attributeListToMap(_map['attributes']);
}
return _attributes;
}
int get childNodeCount => _map['childNodeCount'];
- Iterable<Node> get children sync* {
- if (_map.containsKey('children')) {
- for (var child in _map['children']) {
- yield new Node(child);
- }
+ var _children;
+ List<Node> get children {
+ if (_children == null && _map.containsKey('children')) {
+ _children =
+ new UnmodifiableListView(_map['children'].map((c) => new Node(c)));
}
+ return _children;
}
Node get contentDocument {
@@ -295,6 +288,8 @@
String get value => _map['value'];
String get xmlVersion => _map['xmlVersion'];
+
+ String toString() => '$nodeName: $nodeId $attributes';
}
class Rgba {
@@ -313,3 +308,11 @@
return json;
}
}
+
+Map<String, String> _attributeListToMap(List<String> attrList) {
+ var attributes = {};
+ for (int i = 0; i < attrList.length; i += 2) {
+ attributes[attrList[i]] = attrList[i + 1];
+ }
+ return new UnmodifiableMapView(attributes);
+}
diff --git a/lib/webkit_inspection_protocol.dart b/lib/webkit_inspection_protocol.dart
index 4966fae..7c00e60 100644
--- a/lib/webkit_inspection_protocol.dart
+++ b/lib/webkit_inspection_protocol.dart
@@ -14,12 +14,14 @@
Stream,
StreamController,
StreamTransformer;
-import 'dart:collection' show UnmodifiableMapView;
+import 'dart:collection' show UnmodifiableListView, UnmodifiableMapView;
import 'dart:convert' show JSON, UTF8;
import 'dart:io' show HttpClient, HttpClientResponse, WebSocket;
import 'package:logging/logging.dart' show Logger;
+import 'package:logging/logging.dart';
+
part 'src/console.dart';
part 'src/debugger.dart';
part 'src/dom.dart';
diff --git a/pubspec.yaml b/pubspec.yaml
index 4ae29a8..657b976 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -6,7 +6,7 @@
author: Devon Carew <devoncarew@google.com>
environment:
- sdk: '>=1.9.0 <2.0.0'
+ sdk: '>=1.10.0 <2.0.0'
dependencies:
args: '^0.13.1'
logging: '^0.11.0'
diff --git a/test/data/dom_model_test.html b/test/data/dom_model_test.html
new file mode 100644
index 0000000..6be274a
--- /dev/null
+++ b/test/data/dom_model_test.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head lang="en">
+ <meta charset="UTF-8">
+ <title></title>
+</head>
+<body test-attr="test-attr-value">
+ <div>
+ <h1>test</h1>
+ </div>
+ <div></div>
+</body>
+</html>
diff --git a/test/dom_model_test.dart b/test/dom_model_test.dart
new file mode 100644
index 0000000..d514243
--- /dev/null
+++ b/test/dom_model_test.dart
@@ -0,0 +1,129 @@
+// Copyright 2015 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.console_test;
+
+import 'package:test/test.dart';
+import 'package:webkit_inspection_protocol/dom_model.dart';
+import 'package:webkit_inspection_protocol/webkit_inspection_protocol.dart';
+
+import 'test_setup.dart';
+
+main() {
+ group('WipDomModel', () {
+ WipDom dom;
+
+ setUp(() async {
+ dom = new WipDomModel((await navigateToPage('dom_model_test.html')).dom);
+ });
+
+ tearDown(() async {
+ dom = null;
+ await closeConnection();
+ });
+
+ test('maintains model across getDocument calls', () async {
+ var document1 = await dom.getDocument();
+ var document2 = await dom.getDocument();
+ expect(document2.nodeId, document1.nodeId);
+ });
+
+ test('requestChildNodes updates children', () async {
+ Node htmlNode = (await dom.getDocument()).children[1];
+ for (var child in htmlNode.children) {
+ expect(child.children, isNull);
+ await dom.requestChildNodes(child.nodeId);
+ }
+ // wait for children to be updated
+ for (var child in htmlNode.children) {
+ expect(child.children, isNotNull);
+ }
+ });
+
+ test('removing a node updates children', () async {
+ Node bodyNode = (await dom.getDocument()).children[1].children[1];
+ await dom.requestChildNodes(bodyNode.nodeId);
+ var childCount = bodyNode.childNodeCount;
+ await dom.removeNode(bodyNode.children.first.nodeId);
+
+ expect(bodyNode.children, hasLength(childCount - 1));
+ expect(bodyNode.childNodeCount, childCount - 1);
+ });
+
+ test('Moving a node updates children', () async {
+ Node bodyNode = (await dom.getDocument()).children[1].children[1];
+ await dom.requestChildNodes(bodyNode.nodeId);
+ Node div1 = bodyNode.children[0];
+ Node div2 = bodyNode.children[1];
+
+ expect(div1.childNodeCount, 1);
+ expect(div2.childNodeCount, 0);
+
+ await dom.requestChildNodes(div1.nodeId);
+ await dom.requestChildNodes(div2.nodeId);
+
+ await dom.moveTo(div1.children.first.nodeId, div2.nodeId);
+
+ expect(div1.childNodeCount, 0);
+ expect(div2.childNodeCount, 1);
+ expect(div2.children, hasLength(1));
+ });
+
+ test('Setting node value updates value', () async {
+ Node bodyNode = (await dom.getDocument()).children[1].children[1];
+ await dom.requestChildNodes(bodyNode.nodeId);
+
+ Node div1 = bodyNode.children[0];
+ await dom.requestChildNodes(div1.nodeId);
+
+ Node h1 = div1.children[0];
+ await dom.requestChildNodes(h1.nodeId);
+
+ Node text = h1.children[0];
+
+ expect(text.nodeValue, 'test');
+
+ await dom.setNodeValue(text.nodeId, 'some new text');
+
+ expect(text.nodeValue, 'some new text');
+ });
+
+ test('Adding attribute updates attributes', () async {
+ Node bodyNode = (await dom.getDocument()).children[1].children[1];
+ expect(bodyNode.attributes.containsKey('my-attr'), isFalse);
+ await dom.setAttributeValue(bodyNode.nodeId, 'my-attr', 'my-value');
+ expect(bodyNode.attributes['my-attr'], 'my-value');
+ });
+
+ test('Changing attribute updates attributes', () async {
+ Node bodyNode = (await dom.getDocument()).children[1].children[1];
+ expect(bodyNode.attributes['test-attr'], 'test-attr-value');
+ await dom.setAttributeValue(bodyNode.nodeId, 'test-attr', 'my-value');
+ expect(bodyNode.attributes['test-attr'], 'my-value');
+ });
+
+ test('Removing attribute updates attributes', () async {
+ Node bodyNode = (await dom.getDocument()).children[1].children[1];
+ expect(bodyNode.attributes['test-attr'], 'test-attr-value');
+ await dom.removeAttribute(bodyNode.nodeId, 'test-attr');
+ expect(bodyNode.attributes.containsKey('test-attr'), isFalse);
+ });
+
+ test('refreshing resets document', () async {
+ var document1 = await dom.getDocument();
+ await navigateToPage('dom_model_test.html');
+ var document2 = await dom.getDocument();
+ expect(document2.nodeId, isNot(document1.nodeId));
+ });
+
+ test('getting attributes works', () async {
+ Node bodyNode = (await dom.getDocument()).children[1].children[1];
+ var attributes = bodyNode.attributes;
+ var getAttributes = await dom.getAttributes(bodyNode.nodeId);
+
+ expect(getAttributes, attributes);
+ expect(bodyNode.attributes, attributes);
+ });
+ });
+}