From 56e3d82e63b498c5469cbcb2b0b782813cbf98d0 Mon Sep 17 00:00:00 2001 From: Erik Arvidsson Date: Wed, 6 Nov 2013 11:41:05 -0500 Subject: [PATCH] Implement MutationObserver from scratch This is an implementation of the MutationObserver without relying on native MutationEvents nor native MutationObserver. --- build.json | 3 +- shadowdom.js | 3 +- src/MutationObserver.js | 372 +++++++++++++++ src/ShadowRenderer.js | 4 +- src/microtask.js | 49 ++ src/wrappers/CharacterData.js | 11 + src/wrappers/Element.js | 16 + src/wrappers/HTMLElement.js | 15 + src/wrappers/MutationObserver.js | 87 ---- src/wrappers/Node.js | 193 +++++--- test/js/MutationObserver.js | 6 +- test/js/MutationObserver/attributes.js | 280 +++++++++++ test/js/MutationObserver/callback.js | 76 +++ test/js/MutationObserver/characterData.js | 100 ++++ test/js/MutationObserver/childList.js | 556 ++++++++++++++++++++++ test/js/MutationObserver/mixed.js | 40 ++ test/js/MutationObserver/options.js | 110 +++++ test/js/MutationObserver/transient.js | 278 +++++++++++ test/js/microtask.js | 46 ++ test/test.main.js | 33 +- 20 files changed, 2124 insertions(+), 154 deletions(-) create mode 100644 src/MutationObserver.js create mode 100644 src/microtask.js delete mode 100644 src/wrappers/MutationObserver.js create mode 100644 test/js/MutationObserver/attributes.js create mode 100644 test/js/MutationObserver/callback.js create mode 100644 test/js/MutationObserver/characterData.js create mode 100644 test/js/MutationObserver/childList.js create mode 100644 test/js/MutationObserver/mixed.js create mode 100644 test/js/MutationObserver/options.js create mode 100644 test/js/MutationObserver/transient.js create mode 100644 test/js/microtask.js diff --git a/build.json b/build.json index c959cec..e3bea05 100644 --- a/build.json +++ b/build.json @@ -2,6 +2,8 @@ "../observe-js/src/observe.js", "../WeakMap/weakmap.js", "src/wrappers.js", + "src/microtask.js", + "src/MutationObserver.js", "src/wrappers/events.js", "src/wrappers/NodeList.js", "src/wrappers/Node.js", @@ -27,7 +29,6 @@ "src/wrappers/elements-with-form-property.js", "src/wrappers/Document.js", "src/wrappers/Window.js", - "src/wrappers/MutationObserver.js", "src/wrappers/Range.js", "src/wrappers/override-constructors.js" ] diff --git a/shadowdom.js b/shadowdom.js index 52e44dc..3808328 100644 --- a/shadowdom.js +++ b/shadowdom.js @@ -18,6 +18,8 @@ '../observe-js/src/observe.js', '../WeakMap/weakmap.js', 'src/wrappers.js', + 'src/microtask.js', + 'src/MutationObserver.js', 'src/wrappers/events.js', 'src/wrappers/NodeList.js', 'src/wrappers/Node.js', @@ -43,7 +45,6 @@ 'src/wrappers/elements-with-form-property.js', 'src/wrappers/Document.js', 'src/wrappers/Window.js', - 'src/wrappers/MutationObserver.js', 'src/wrappers/Range.js', 'src/wrappers/override-constructors.js' ].forEach(function(src) { diff --git a/src/MutationObserver.js b/src/MutationObserver.js new file mode 100644 index 0000000..a060172 --- /dev/null +++ b/src/MutationObserver.js @@ -0,0 +1,372 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(scope) { + 'use strict'; + + var setEndOfMicrotask = scope.setEndOfMicrotask + var wrapIfNeeded = scope.wrapIfNeeded + var wrappers = scope.wrappers; + + var registrationsTable = new WeakMap(); + var globalMutationObservers = []; + var isScheduled = false; + + function scheduleCallback(observer) { + if (isScheduled) + return; + setEndOfMicrotask(notifyObservers); + isScheduled = true; + } + + // http://dom.spec.whatwg.org/#mutation-observers + function notifyObservers() { + isScheduled = false; + + do { + var notifyList = globalMutationObservers.slice(); + var anyNonEmpty = false; + for (var i = 0; i < notifyList.length; i++) { + var mo = notifyList[i]; + var queue = mo.takeRecords(); + removeTransientObserversFor(mo); + if (queue.length) { + mo.callback_(queue, mo); + anyNonEmpty = true; + } + } + } while (anyNonEmpty); + } + + /** + * @param {string} type + * @param {Node} target + * @constructor + */ + function MutationRecord(type, target) { + this.type = type; + this.target = target; + this.addedNodes = new wrappers.NodeList(); + this.removedNodes = new wrappers.NodeList(); + this.previousSibling = null; + this.nextSibling = null; + this.attributeName = null; + this.attributeNamespace = null; + this.oldValue = null; + } + + /** + * Registers transient observers to ancestor and its ancesors for the node + * which was removed. + * @param {!Node} ancestor + * @param {!Node} node + */ + function registerTransientObservers(ancestor, node) { + for (; ancestor; ancestor = ancestor.parentNode) { + var registrations = registrationsTable.get(ancestor); + if (!registrations) + continue; + for (var i = 0; i < registrations.length; i++) { + var registration = registrations[i]; + if (registration.options.subtree) + registration.addTransientObserver(node); + } + } + } + + function removeTransientObserversFor(observer) { + for (var i = 0; i < observer.nodes_.length; i++) { + var node = observer.nodes_[i]; + var registrations = registrationsTable.get(node); + if (!registrations) + return; + for (var j = 0; j < registrations.length; j++) { + var registration = registrations[j]; + if (registration.observer === observer) + registration.removeTransientObservers(); + } + } + } + + // http://dom.spec.whatwg.org/#queue-a-mutation-record + function enqueueMutation(target, type, data) { + // 1. + var interestedObservers = Object.create(null); + var associatedStrings = Object.create(null); + + // 2. + for (var node = target; node; node = node.parentNode) { + // 3. + var registrations = registrationsTable.get(node); + if (!registrations) + continue; + for (var j = 0; j < registrations.length; j++) { + var registration = registrations[j]; + var options = registration.options; + // 1. + if (node !== target && !options.subtree) + continue; + + // 2. + if (type === 'attributes' && !options.attributes) + continue; + + // 3. If type is "attributes", options's attributeFilter is present, and + // either options's attributeFilter does not contain name or namespace + // is non-null, continue. + if (type === 'attributes' && options.attributeFilter && + (data.namespace !== null || + options.attributeFilter.indexOf(data.name) === -1)) { + continue; + } + + // 4. + if (type === 'characterData' && !options.characterData) + continue; + + // 5. + if (type === 'childList' && !options.childList) + continue; + + // 6. + var observer = registration.observer; + interestedObservers[observer.uid_] = observer; + + // 7. If either type is "attributes" and options's attributeOldValue is + // true, or type is "characterData" and options's characterDataOldValue + // is true, set the paired string of registered observer's observer in + // interested observers to oldValue. + if (type === 'attributes' && options.attributeOldValue || + type === 'characterData' && options.characterDataOldValue) { + associatedStrings[observer.uid_] = data.oldValue; + } + } + } + + var anyRecordsEnqueued = false; + + // 4. + for (var uid in interestedObservers) { + var observer = interestedObservers[uid]; + var record = new MutationRecord(type, target); + + // 2. + if ('name' in data && 'namespace' in data) { + record.attributeName = data.name; + record.attributeNamespace = data.namespace; + } + + // 3. + if (data.addedNodes) + record.addedNodes = data.addedNodes; + + // 4. + if (data.removedNodes) + record.removedNodes = data.removedNodes; + + // 5. + if (data.previousSibling) + record.previousSibling = data.previousSibling; + + // 6. + if (data.nextSibling) + record.nextSibling = data.nextSibling; + + // 7. + if (associatedStrings[uid] !== undefined) + record.oldValue = associatedStrings[uid]; + + // 8. + observer.records_.push(record); + + anyRecordsEnqueued = true; + } + + if (anyRecordsEnqueued) + scheduleCallback(); + } + + var slice = Array.prototype.slice; + + /** + * @param {!Object} options + * @constructor + */ + function MutationObserverOptions(options) { + this.childList = !!options.childList; + this.subtree = !!options.subtree; + + // 1. If either options' attributeOldValue or attributeFilter is present + // and options' attributes is omitted, set options' attributes to true. + if (!('attributes' in options) && + ('attributeOldValue' in options || 'attributeFilter' in options)) { + this.attributes = true; + } else { + this.attributes = !!options.attributes; + } + + // 2. If options' characterDataOldValue is present and options' + // characterData is omitted, set options' characterData to true. + if ('characterDataOldValue' in options && !('characterData' in options)) + this.characterData = true; + else + this.characterData = !!options.characterData; + + // 3. & 4. + if (!this.attributes && + (options.attributeOldValue || 'attributeFilter' in options) || + // 5. + !this.characterData && options.characterDataOldValue) { + throw new TypeError(); + } + + this.characterData = !!options.characterData; + this.attributeOldValue = !!options.attributeOldValue; + this.characterDataOldValue = !!options.characterDataOldValue; + if ('attributeFilter' in options) { + if (options.attributeFilter == null || + typeof options.attributeFilter !== 'object') { + throw new TypeError(); + } + this.attributeFilter = slice.call(options.attributeFilter); + } else { + this.attributeFilter = null; + } + } + + var uidCounter = 0; + + /** + * The class that maps to the DOM MutationObserver interface. + * @param {Function} callback. + * @constructor + */ + function MutationObserver(callback) { + this.callback_ = callback; + this.nodes_ = []; + this.records_ = []; + this.uid_ = ++uidCounter; + + // This will leak. There is no way to implement this without WeakRefs :'( + globalMutationObservers.push(this); + } + + MutationObserver.prototype = { + // http://dom.spec.whatwg.org/#dom-mutationobserver-observe + observe: function(target, options) { + target = wrapIfNeeded(target); + + var newOptions = new MutationObserverOptions(options); + + // 6. + var registration; + var registrations = registrationsTable.get(target); + if (!registrations) + registrationsTable.set(target, registrations = []); + + for (var i = 0; i < registrations.length; i++) { + if (registrations[i].observer === this) { + registration = registrations[i]; + // 6.1. + registration.removeTransientObservers(); + // 6.2. + registration.options = newOptions; + } + } + + // 7. + if (!registration) { + registration = new Registration(this, target, newOptions); + registrations.push(registration); + this.nodes_.push(target); + } + }, + + // http://dom.spec.whatwg.org/#dom-mutationobserver-disconnect + disconnect: function() { + this.nodes_.forEach(function(node) { + var registrations = registrationsTable.get(node); + for (var i = 0; i < registrations.length; i++) { + var registration = registrations[i]; + if (registration.observer === this) { + registrations.splice(i, 1); + // Each node can only have one registered observer associated with + // this observer. + break; + } + } + }, this); + this.records_ = []; + }, + + takeRecords: function() { + var copyOfRecords = this.records_; + this.records_ = []; + return copyOfRecords; + } + }; + + /** + * Class used to represent a registered observer. + * @param {MutationObserver} observer + * @param {Node} target + * @param {MutationObserverOptions} options + * @constructor + */ + function Registration(observer, target, options) { + this.observer = observer; + this.target = target; + this.options = options; + this.transientObservedNodes = []; + } + + Registration.prototype = { + /** + * Adds a transient observer on node. The transient observer gets removed + * next time we deliver the change records. + * @param {Node} node + */ + addTransientObserver: function(node) { + // Don't add transient observers on the target itself. We already have all + // the required listeners set up on the target. + if (node === this.target) + return; + + this.transientObservedNodes.push(node); + var registrations = registrationsTable.get(node); + if (!registrations) + registrationsTable.set(node, registrations = []); + + // We know that registrations does not contain this because we already + // checked if node === this.target. + registrations.push(this); + }, + + removeTransientObservers: function() { + var transientObservedNodes = this.transientObservedNodes; + this.transientObservedNodes = []; + + for (var i = 0; i < transientObservedNodes.length; i++) { + var node = transientObservedNodes[i]; + var registrations = registrationsTable.get(node); + for (var j = 0; j < registrations.length; j++) { + if (registrations[j] === this) { + registrations.splice(j, 1); + // Each node can only have one registered observer associated with + // this observer. + break; + } + } + } + } + }; + + scope.enqueueMutation = enqueueMutation; + scope.registerTransientObservers = registerTransientObservers; + scope.wrappers.MutationObserver = MutationObserver; + scope.wrappers.MutationRecord = MutationRecord; + +})(window.ShadowDOMPolyfill); diff --git a/src/ShadowRenderer.js b/src/ShadowRenderer.js index 36a9b8c..56afed3 100644 --- a/src/ShadowRenderer.js +++ b/src/ShadowRenderer.js @@ -632,8 +632,8 @@ return getDistributedChildNodes(this); }; - HTMLShadowElement.prototype.nodeWasAdded_ = - HTMLContentElement.prototype.nodeWasAdded_ = function() { + HTMLShadowElement.prototype.nodeIsInserted_ = + HTMLContentElement.prototype.nodeIsInserted_ = function() { // Invalidate old renderer if any. this.invalidateShadowRenderer(); diff --git a/src/microtask.js b/src/microtask.js new file mode 100644 index 0000000..c0e35b7 --- /dev/null +++ b/src/microtask.js @@ -0,0 +1,49 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +(function(context) { + 'use strict'; + + var OriginalMutationObserver = window.MutationObserver; + var callbacks = []; + var pending = false; + var timerFunc; + + function handle() { + pending = false; + var copies = callbacks.slice(0); + callbacks = []; + for (var i = 0; i < copies.length; i++) { + (0, copies[i])(); + } + } + + if (OriginalMutationObserver) { + var counter = 1; + var observer = new OriginalMutationObserver(handle); + var textNode = document.createTextNode(counter); + observer.observe(textNode, {characterData: true}); + + timerFunc = function() { + counter = (counter + 1) % 2; + textNode.data = counter; + }; + + } else { + timerFunc = window.setImmediate || window.setTimeout; + } + + function setEndOfMicrotask(func) { + callbacks.push(func); + if (pending) + return; + pending = true; + timerFunc(handle, 0); + } + + context.setEndOfMicrotask = setEndOfMicrotask; + +})(window.ShadowDOMPolyfill); diff --git a/src/wrappers/CharacterData.js b/src/wrappers/CharacterData.js index 146b9bd..e2655ff 100644 --- a/src/wrappers/CharacterData.js +++ b/src/wrappers/CharacterData.js @@ -7,6 +7,7 @@ var ChildNodeInterface = scope.ChildNodeInterface; var Node = scope.wrappers.Node; + var enqueueMutation = scope.enqueueMutation; var mixin = scope.mixin; var registerWrapper = scope.registerWrapper; @@ -22,6 +23,16 @@ }, set textContent(value) { this.data = value; + }, + get data() { + return this.impl.data; + }, + set data(value) { + var oldValue = this.impl.data; + enqueueMutation(this, 'characterData', { + oldValue: oldValue + }); + this.impl.data = value; } }); diff --git a/src/wrappers/Element.js b/src/wrappers/Element.js index 02e0ce0..5e76c0d 100644 --- a/src/wrappers/Element.js +++ b/src/wrappers/Element.js @@ -11,6 +11,7 @@ var ParentNodeInterface = scope.ParentNodeInterface; var SelectorsInterface = scope.SelectorsInterface; var addWrapNodeListMethod = scope.addWrapNodeListMethod; + var enqueueMutation = scope.enqueueMutation; var mixin = scope.mixin; var oneOf = scope.oneOf; var registerWrapper = scope.registerWrapper; @@ -38,6 +39,17 @@ renderer.invalidate(); } + function enqueAttributeChange(element, name, oldValue) { + // This is not fully spec compliant. We should use localName (which might + // have a different case than name) and the namespace (which requires us + // to get the Attr object). + enqueueMutation(element, 'attributes', { + name: name, + namespace: null, + oldValue: oldValue + }); + } + function Element(node) { Node.call(this, node); } @@ -58,12 +70,16 @@ }, setAttribute: function(name, value) { + var oldValue = this.impl.getAttribute(name); this.impl.setAttribute(name, value); + enqueAttributeChange(this, name, oldValue); invalidateRendererBasedOnAttribute(this, name); }, removeAttribute: function(name) { + var oldValue = this.impl.getAttribute(name); this.impl.removeAttribute(name); + enqueAttributeChange(this, name, oldValue); invalidateRendererBasedOnAttribute(this, name); }, diff --git a/src/wrappers/HTMLElement.js b/src/wrappers/HTMLElement.js index 1bae2cc..7fc1f91 100644 --- a/src/wrappers/HTMLElement.js +++ b/src/wrappers/HTMLElement.js @@ -7,8 +7,12 @@ var Element = scope.wrappers.Element; var defineGetter = scope.defineGetter; + var enqueueMutation = scope.enqueueMutation; var mixin = scope.mixin; + var nodesWereAdded = scope.nodesWereAdded; + var nodesWereRemoved = scope.nodesWereRemoved; var registerWrapper = scope.registerWrapper; + var snapshotNodeList = scope.snapshotNodeList; var unwrap = scope.unwrap; var wrap = scope.wrap; @@ -110,10 +114,21 @@ return getInnerHTML(this); }, set innerHTML(value) { + var removedNodes = snapshotNodeList(this.childNodes); + if (this.invalidateShadowRenderer()) setInnerHTML(this, value, this.tagName); else this.impl.innerHTML = value; + var addedNodes = snapshotNodeList(this.childNodes); + + enqueueMutation(this, 'childList', { + addedNodes: addedNodes, + removedNodes: removedNodes + }); + + nodesWereRemoved(removedNodes); + nodesWereAdded(addedNodes); }, get outerHTML() { diff --git a/src/wrappers/MutationObserver.js b/src/wrappers/MutationObserver.js deleted file mode 100644 index ee10d95..0000000 --- a/src/wrappers/MutationObserver.js +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright 2013 The Polymer Authors. All rights reserved. -// Use of this source code is goverened by a BSD-style -// license that can be found in the LICENSE file. - -(function(scope) { - 'use strict'; - - var defineGetter = scope.defineGetter; - var defineWrapGetter = scope.defineWrapGetter; - var registerWrapper = scope.registerWrapper; - var unwrapIfNeeded = scope.unwrapIfNeeded; - var wrapNodeList = scope.wrapNodeList; - var wrappers = scope.wrappers; - - var OriginalMutationObserver = window.MutationObserver || - window.WebKitMutationObserver; - - if (!OriginalMutationObserver) - return; - - var OriginalMutationRecord = window.MutationRecord; - - function MutationRecord(impl) { - this.impl = impl; - } - - MutationRecord.prototype = { - get addedNodes() { - return wrapNodeList(this.impl.addedNodes); - }, - get removedNodes() { - return wrapNodeList(this.impl.removedNodes); - } - }; - - ['target', 'previousSibling', 'nextSibling'].forEach(function(name) { - defineWrapGetter(MutationRecord, name); - }); - - // WebKit/Blink treats these as instance properties so we override - [ - 'type', - 'attributeName', - 'attributeNamespace', - 'oldValue' - ].forEach(function(name) { - defineGetter(MutationRecord, name, function() { - return this.impl[name]; - }); - }); - - if (OriginalMutationRecord) - registerWrapper(OriginalMutationRecord, MutationRecord); - - function wrapRecord(record) { - return new MutationRecord(record); - } - - function wrapRecords(records) { - return records.map(wrapRecord); - } - - function MutationObserver(callback) { - var self = this; - this.impl = new OriginalMutationObserver(function(mutations, observer) { - callback.call(self, wrapRecords(mutations), self); - }); - } - - var OriginalNode = window.Node; - - MutationObserver.prototype = { - observe: function(target, options) { - this.impl.observe(unwrapIfNeeded(target), options); - }, - disconnect: function() { - this.impl.disconnect(); - }, - takeRecords: function() { - return wrapRecords(this.impl.takeRecords()); - } - }; - - scope.wrappers.MutationObserver = MutationObserver; - scope.wrappers.MutationRecord = MutationRecord; - -})(window.ShadowDOMPolyfill); diff --git a/src/wrappers/Node.js b/src/wrappers/Node.js index 55d981d..85ed559 100644 --- a/src/wrappers/Node.js +++ b/src/wrappers/Node.js @@ -7,9 +7,11 @@ var EventTarget = scope.wrappers.EventTarget; var NodeList = scope.wrappers.NodeList; - var defineWrapGetter = scope.defineWrapGetter; var assert = scope.assert; + var defineWrapGetter = scope.defineWrapGetter; + var enqueueMutation = scope.enqueueMutation; var mixin = scope.mixin; + var registerTransientObservers = scope.registerTransientObservers; var registerWrapper = scope.registerWrapper; var unwrap = scope.unwrap; var wrap = scope.wrap; @@ -63,23 +65,46 @@ } function collectNodesNoNeedToUpdatePointers(node) { + var nodes = new NodeList(); if (node instanceof DocumentFragment) { var nodes = []; var i = 0; for (var child = node.firstChild; child; child = child.nextSibling) { nodes[i++] = child; } + nodes.length = i; return nodes; } - return [node]; + nodes[0] = node; + nodes.length = 1; + return nodes; + } + + function snapshotNodeList(nodeList) { + // NodeLists are not live at the moment so just return the same object. + return nodeList; + } + + // http://dom.spec.whatwg.org/#node-is-inserted + function nodeWasAdded(node) { + node.nodeIsInserted_(); } function nodesWereAdded(nodes) { for (var i = 0; i < nodes.length; i++) { - nodes[i].nodeWasAdded_(); + nodeWasAdded(nodes[i]); } } + // http://dom.spec.whatwg.org/#node-is-removed + function nodeWasRemoved(node) { + // Nothing at this point in time. + } + + function nodesWereRemoved(nodes) { + // Nothing at this point in time. + } + function ensureSameOwnerDocument(parent, child) { var ownerDoc = parent.nodeType === Node.DOCUMENT_NODE ? parent : parent.ownerDocument; @@ -148,6 +173,27 @@ return p && p.invalidateShadowRenderer(); } + /** + * Called before node is inserted into a node to enqueue its removal from its + * old parent. + * @param {!Node} node The node that is about to be removed. + * @param {!NodeList} nodes The collected nodes. + */ + function enqueueRemovalForInsertedNodes(node, nodes) { + var parent; + if (node instanceof DocumentFragment) { + enqueueMutation(node, 'childList', { + removedNodes: nodes + }); + } else if (parent = node.parentNode) { + enqueueMutation(parent, 'childList', { + removedNodes: nodes, + previousSibling: node.previousSibling, + nextSibling: node.nextSibling + }); + } + } + var OriginalNode = window.Node; /** @@ -222,69 +268,58 @@ Node.prototype = Object.create(EventTarget.prototype); mixin(Node.prototype, { appendChild: function(childWrapper) { - assertIsNodeWrapper(childWrapper); - - var nodes; - - if (this.invalidateShadowRenderer() || invalidateParent(childWrapper)) { - var previousNode = this.lastChild; - var nextNode = null; - nodes = collectNodes(childWrapper, this, previousNode, nextNode); - - this.lastChild_ = nodes[nodes.length - 1]; - if (!previousNode) - this.firstChild_ = nodes[0]; - - originalAppendChild.call(this.impl, unwrapNodesForInsertion(this, nodes)); - } else { - nodes = collectNodesNoNeedToUpdatePointers(childWrapper) - ensureSameOwnerDocument(this, childWrapper); - originalAppendChild.call(this.impl, unwrap(childWrapper)); - } - - nodesWereAdded(nodes); - - return childWrapper; + return this.insertBefore(childWrapper, null); }, insertBefore: function(childWrapper, refWrapper) { - // TODO(arv): Unify with appendChild - if (!refWrapper) - return this.appendChild(childWrapper); - assertIsNodeWrapper(childWrapper); - assertIsNodeWrapper(refWrapper); - assert(refWrapper.parentNode === this); + + refWrapper = refWrapper || null; + refWrapper && assertIsNodeWrapper(refWrapper); + refWrapper && assert(refWrapper.parentNode === this); var nodes; + var previousNode = + refWrapper ? refWrapper.previousSibling : this.lastChild; - if (this.invalidateShadowRenderer() || invalidateParent(childWrapper)) { - var previousNode = refWrapper.previousSibling; - var nextNode = refWrapper; - nodes = collectNodes(childWrapper, this, previousNode, nextNode); + var useNative = !this.invalidateShadowRenderer() && + !invalidateParent(childWrapper); - if (this.firstChild === refWrapper) + if (useNative) + nodes = collectNodesNoNeedToUpdatePointers(childWrapper); + else + nodes = collectNodes(childWrapper, this, previousNode, refWrapper); + + enqueueRemovalForInsertedNodes(childWrapper, nodes); + + if (useNative) { + ensureSameOwnerDocument(this, childWrapper); + originalInsertBefore.call(this.impl, unwrap(childWrapper), + unwrap(refWrapper)); + } else { + if (!previousNode) this.firstChild_ = nodes[0]; + if (!refWrapper) + this.lastChild_ = nodes[nodes.length - 1]; - // insertBefore refWrapper no matter what the parent is? var refNode = unwrap(refWrapper); - var parentNode = refNode.parentNode; + var parentNode = refNode ? refNode.parentNode : this.impl; + // insertBefore refWrapper no matter what the parent is? if (parentNode) { - originalInsertBefore.call( - parentNode, - unwrapNodesForInsertion(this, nodes), - refNode); + originalInsertBefore.call(parentNode, + unwrapNodesForInsertion(this, nodes), refNode); } else { adoptNodesIfNeeded(this, nodes); } - } else { - nodes = collectNodesNoNeedToUpdatePointers(childWrapper); - ensureSameOwnerDocument(this, childWrapper); - originalInsertBefore.call(this.impl, unwrap(childWrapper), - unwrap(refWrapper)); } + enqueueMutation(this, 'childList', { + addedNodes: nodes, + nextSibling: refWrapper, + previousSibling: previousNode + }); + nodesWereAdded(nodes); return childWrapper; @@ -310,15 +345,15 @@ } var childNode = unwrap(childWrapper); - if (this.invalidateShadowRenderer()) { + var childWrapperNextSibling = childWrapper.nextSibling; + var childWrapperPreviousSibling = childWrapper.previousSibling; + if (this.invalidateShadowRenderer()) { // We need to remove the real node from the DOM before updating the // pointers. This is so that that mutation event is dispatched before // the pointers have changed. var thisFirstChild = this.firstChild; var thisLastChild = this.lastChild; - var childWrapperNextSibling = childWrapper.nextSibling; - var childWrapperPreviousSibling = childWrapper.previousSibling; var parentNode = childNode.parentNode; if (parentNode) @@ -341,6 +376,14 @@ removeChildOriginalHelper(this.impl, childNode); } + enqueueMutation(this, 'childList', { + removedNodes: [childWrapper], + nextSibling: childWrapperNextSibling, + previousSibling: childWrapperPreviousSibling + }); + + registerTransientObservers(this, childWrapper); + return childWrapper; }, @@ -354,16 +397,24 @@ } var oldChildNode = unwrap(oldChildWrapper); + var nextNode = oldChildWrapper.nextSibling; + var previousNode = oldChildWrapper.previousSibling; var nodes; - if (this.invalidateShadowRenderer() || - invalidateParent(newChildWrapper)) { - var previousNode = oldChildWrapper.previousSibling; - var nextNode = oldChildWrapper.nextSibling; + var useNative = !this.invalidateShadowRenderer() && + !invalidateParent(newChildWrapper); + + if (useNative) { + nodes = collectNodesNoNeedToUpdatePointers(newChildWrapper); + } else { if (nextNode === newChildWrapper) nextNode = newChildWrapper.nextSibling; nodes = collectNodes(newChildWrapper, this, previousNode, nextNode); + } + + enqueueRemovalForInsertedNodes(newChildWrapper, nodes); + if (!useNative) { if (this.firstChild === oldChildWrapper) this.firstChild_ = nodes[0]; if (this.lastChild === oldChildWrapper) @@ -380,25 +431,32 @@ oldChildNode); } } else { - nodes = collectNodesNoNeedToUpdatePointers(newChildWrapper); ensureSameOwnerDocument(this, newChildWrapper); originalReplaceChild.call(this.impl, unwrap(newChildWrapper), oldChildNode); } + enqueueMutation(this, 'childList', { + addedNodes: nodes, + removedNodes: [oldChildWrapper], + nextSibling: nextNode, + previousSibling: previousNode + }); + + nodeWasRemoved(oldChildWrapper); nodesWereAdded(nodes); return oldChildWrapper; }, /** - * Called after a node was added. Subclasses override this to invalidate + * Called after a node was inserted. Subclasses override this to invalidate * the renderer as needed. * @private */ - nodeWasAdded_: function() { + nodeIsInserted_: function() { for (var child = this.firstChild; child; child = child.nextSibling) { - child.nodeWasAdded_(); + child.nodeIsInserted_(); } }, @@ -455,6 +513,8 @@ return s; }, set textContent(textContent) { + var removedNodes = snapshotNodeList(this.childNodes); + if (this.invalidateShadowRenderer()) { removeAllChildNodes(this); if (textContent !== '') { @@ -464,6 +524,16 @@ } else { this.impl.textContent = textContent; } + + var addedNodes = snapshotNodeList(this.childNodes); + + enqueueMutation(this, 'childList', { + addedNodes: addedNodes, + removedNodes: removedNodes + }); + + nodesWereRemoved(removedNodes); + nodesWereAdded(addedNodes); }, get childNodes() { @@ -522,6 +592,11 @@ delete Node.prototype.querySelectorAll; Node.prototype = mixin(Object.create(EventTarget.prototype), Node.prototype); + scope.nodeWasAdded = nodeWasAdded; + scope.nodeWasRemoved = nodeWasRemoved; + scope.nodesWereAdded = nodesWereAdded; + scope.nodesWereRemoved = nodesWereRemoved; + scope.snapshotNodeList = snapshotNodeList; scope.wrappers.Node = Node; })(window.ShadowDOMPolyfill); diff --git a/test/js/MutationObserver.js b/test/js/MutationObserver.js index 5f44a38..670e127 100644 --- a/test/js/MutationObserver.js +++ b/test/js/MutationObserver.js @@ -209,7 +209,7 @@ suite('MutationObserver', function() { subtree: true }); - document.body.setAttribute('a', newValue()); + wrap(document).body.setAttribute('a', newValue()); }); test('observe document.body', function(done) { @@ -230,7 +230,7 @@ suite('MutationObserver', function() { attributes: true }); - document.body.setAttribute('a', newValue()); + wrap(document.body).setAttribute('a', newValue()); }); test('observe document.head', function(done) { @@ -251,7 +251,7 @@ suite('MutationObserver', function() { attributes: true }); - document.head.setAttribute('a', newValue()); + wrap(document.head).setAttribute('a', newValue()); }); test('observe text node', function(done) { diff --git a/test/js/MutationObserver/attributes.js b/test/js/MutationObserver/attributes.js new file mode 100644 index 0000000..0bc5ae8 --- /dev/null +++ b/test/js/MutationObserver/attributes.js @@ -0,0 +1,280 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('attributes', function() { + + test('attr', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true + }); + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + }); + + test('attr with oldValue', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + attributeOldValue: true + }); + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null, + oldValue: null + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null, + oldValue: 'A' + }); + }); + + test('attr change in subtree should not genereate a record', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true + }); + child.setAttribute('a', 'A'); + child.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 0); + }); + + test('attr change, subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + subtree: true + }); + child.setAttribute('a', 'A'); + child.setAttribute('a', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + }); + + + test('multiple observers on same target', function() { + var div = document.createElement('div'); + var observer1 = new MutationObserver(function() {}); + observer1.observe(div, { + attributes: true, + attributeOldValue: true + }); + var observer2 = new MutationObserver(function() {}); + observer2.observe(div, { + attributes: true, + attributeFilter: ['b'] + }); + + div.setAttribute('a', 'A'); + div.setAttribute('a', 'A2'); + div.setAttribute('b', 'B'); + + var records = observer1.takeRecords(); + assert.equal(records.length, 3); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a' + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + oldValue: 'A' + }); + expectMutationRecord(records[2], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + + records = observer2.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + + test('observer observes on different target', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + + var observer = new MutationObserver(function() {}); + observer.observe(child, { + attributes: true + }); + observer.observe(div, { + attributes: true, + subtree: true, + attributeOldValue: true + }); + + child.setAttribute('a', 'A'); + child.setAttribute('a', 'A2'); + child.setAttribute('b', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 3); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a' + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: child, + attributeName: 'a', + oldValue: 'A' + }); + expectMutationRecord(records[2], { + type: 'attributes', + target: child, + attributeName: 'b' + }); + }); + + test('observing on the same node should update the options', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + attributeFilter: ['a'] + }); + observer.observe(div, { + attributes: true, + attributeFilter: ['b'] + }); + + div.setAttribute('a', 'A'); + div.setAttribute('b', 'B'); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + + test('disconnect should stop all events and empty the records', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + }); + + div.setAttribute('a', 'A'); + + observer.disconnect(); + var records = observer.takeRecords(); + assert.equal(records.length, 0); + + div.setAttribute('b', 'B'); + + records = observer.takeRecords(); + assert.equal(records.length, 0); + }); + + test('disconnect should not affect other observers', function() { + var div = document.createElement('div'); + var observer1 = new MutationObserver(function() {}); + observer1.observe(div, { + attributes: true, + }); + var observer2 = new MutationObserver(function() {}); + observer2.observe(div, { + attributes: true, + }); + + div.setAttribute('a', 'A'); + + observer1.disconnect(); + var records1 = observer1.takeRecords(); + assert.equal(records1.length, 0); + + var records2 = observer2.takeRecords(); + assert.equal(records2.length, 1); + expectMutationRecord(records2[0], { + type: 'attributes', + target: div, + attributeName: 'a' + }); + + div.setAttribute('b', 'B'); + + records1 = observer1.takeRecords(); + assert.equal(records1.length, 0); + + records2 = observer2.takeRecords(); + assert.equal(records2.length, 1); + expectMutationRecord(records2[0], { + type: 'attributes', + target: div, + attributeName: 'b' + }); + }); + + }); + +}); \ No newline at end of file diff --git a/test/js/MutationObserver/callback.js b/test/js/MutationObserver/callback.js new file mode 100644 index 0000000..7f84844 --- /dev/null +++ b/test/js/MutationObserver/callback.js @@ -0,0 +1,76 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('callback', function() { + + test('One observer, two attribute changes', function(done) { + var div = document.createElement('div'); + var observer = new MutationObserver(function(records) { + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectMutationRecord(records[1], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + + done(); + }); + + observer.observe(div, { + attributes: true + }); + + div.setAttribute('a', 'A'); + div.setAttribute('a', 'B'); + }); + + test('nested changes', function(done) { + var div = document.createElement('div'); + var i = 0; + var observer = new MutationObserver(function(records) { + assert.equal(records.length, 1); + + if (i === 0) { + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + div.setAttribute('b', 'B'); + i++; + } else { + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'b', + attributeNamespace: null + }); + + done(); + } + }); + + observer.observe(div, { + attributes: true + }); + + div.setAttribute('a', 'A'); + }); + + }); + +}); \ No newline at end of file diff --git a/test/js/MutationObserver/characterData.js b/test/js/MutationObserver/characterData.js new file mode 100644 index 0000000..cc462b2 --- /dev/null +++ b/test/js/MutationObserver/characterData.js @@ -0,0 +1,100 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('characterData', function() { + + test('characterData', function() { + var text = document.createTextNode('abc'); + var observer = new MutationObserver(function() {}); + observer.observe(text, { + characterData: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'characterData', + target: text + }); + expectMutationRecord(records[1], { + type: 'characterData', + target: text + }); + }); + + test('characterData with old value', function() { + var text = document.createTextNode('abc'); + var observer = new MutationObserver(function() {}); + observer.observe(text, { + characterData: true, + characterDataOldValue: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'characterData', + target: text, + oldValue: 'abc' + }); + expectMutationRecord(records[1], { + type: 'characterData', + target: text, + oldValue: 'def' + }); + }); + + test('characterData change in subtree should not generate a record', + function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('abc')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + characterData: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.equal(records.length, 0); + }); + + test('characterData change in subtree', + function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('abc')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + characterData: true, + subtree: true + }); + text.data = 'def'; + text.data = 'ghi'; + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'characterData', + target: text + }); + expectMutationRecord(records[1], { + type: 'characterData', + target: text + }); + }); + + }); + +}); \ No newline at end of file diff --git a/test/js/MutationObserver/childList.js b/test/js/MutationObserver/childList.js new file mode 100644 index 0000000..7b3b440 --- /dev/null +++ b/test/js/MutationObserver/childList.js @@ -0,0 +1,556 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('childList', function() { + + var NodeList = ShadowDOMPolyfill.wrappers.NodeList; + + function makeNodeList(/* ...args */) { + var nodeList = new NodeList; + for (var i = 0; i < arguments.length; i++) { + nodeList[i] = arguments[i]; + } + nodeList.length = i; + return nodeList; + } + + test('appendChild', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + var a = document.createElement('a'); + var b = document.createElement('b'); + + div.appendChild(a); + div.appendChild(b); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [a] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [b], + previousSibling: a + }); + }); + + test('insertBefore', function() { + var div = document.createElement('div'); + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('c'); + div.appendChild(a); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.insertBefore(b, a); + div.insertBefore(c, a); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [b], + nextSibling: a + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [c], + nextSibling: a, + previousSibling: b + }); + }); + + test('replaceChild', function() { + var div = document.createElement('div'); + var a = document.createElement('a'); + var b = document.createElement('b'); + div.appendChild(a); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.replaceChild(b, a); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [b], + removedNodes: [a] + }); + }); + + test('removeChild', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createElement('a')); + var b = div.appendChild(document.createElement('b')); + var c = div.appendChild(document.createElement('c')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.removeChild(b); + div.removeChild(a); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [b], + nextSibling: c, + previousSibling: a + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + removedNodes: [a], + nextSibling: c + }); + }); + + test('Direct children', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + var a = document.createElement('a'); + var b = document.createElement('b'); + + div.appendChild(a); + div.insertBefore(b, a); + div.removeChild(b); + + var records = observer.takeRecords(); + assert.equal(records.length, 3); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: [a] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + nextSibling: a, + addedNodes: [b] + }); + + expectMutationRecord(records[2], { + type: 'childList', + target: div, + nextSibling: a, + removedNodes: [b] + }); + }); + + test('subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new MutationObserver(function() {}); + observer.observe(child, { + childList: true + }); + var a = document.createTextNode('a'); + var b = document.createTextNode('b'); + + child.appendChild(a); + child.insertBefore(b, a); + child.removeChild(b); + + var records = observer.takeRecords(); + assert.equal(records.length, 3); + + expectMutationRecord(records[0], { + type: 'childList', + target: child, + addedNodes: [a] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: child, + nextSibling: a, + addedNodes: [b] + }); + + expectMutationRecord(records[2], { + type: 'childList', + target: child, + nextSibling: a, + removedNodes: [b] + }); + }); + + test('both direct and subtree', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true, + subtree: true + }); + observer.observe(child, { + childList: true + }); + + var a = document.createTextNode('a'); + var b = document.createTextNode('b'); + + child.appendChild(a); + div.appendChild(b); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: child, + addedNodes: [a] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: div, + addedNodes: [b], + previousSibling: child + }); + }); + + test('Append multiple at once at the end', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var b = df.appendChild(document.createTextNode('b')); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.appendChild(df); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(b, c, d), + removedNodes: makeNodeList(), + previousSibling: a, + nextSibling: null + }); + }); + + test('Append multiple at once at the front', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var b = df.appendChild(document.createTextNode('b')); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.insertBefore(df, a); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(b, c, d), + removedNodes: makeNodeList(), + previousSibling: null, + nextSibling: a + }); + }); + + test('Append multiple at once in the middle', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + var df = document.createDocumentFragment(); + var c = df.appendChild(document.createTextNode('c')); + var d = df.appendChild(document.createTextNode('d')); + + div.insertBefore(df, b); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(c, d), + removedNodes: makeNodeList(), + previousSibling: a, + nextSibling: b + }); + }); + + test('Remove all children using innerHTML', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + var c = div.appendChild(document.createTextNode('c')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.innerHTML = ''; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(), + removedNodes: makeNodeList(a, b, c), + previousSibling: null, + nextSibling: null + }); + }); + + test('Replace all children using innerHTML', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.innerHTML = ''; + var c = div.firstChild; + var d = div.lastChild; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(c, d), + removedNodes: makeNodeList(a, b), + previousSibling: null, + nextSibling: null + }); + }); + + test('Remove all children using textContent', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + var c = div.appendChild(document.createTextNode('c')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.textContent = ''; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(), + removedNodes: makeNodeList(a, b, c), + previousSibling: null, + nextSibling: null + }); + }); + + test('Replace all children using textContent', function() { + var div = document.createElement('div'); + var a = div.appendChild(document.createTextNode('a')); + var b = div.appendChild(document.createTextNode('b')); + + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true + }); + + div.textContent = 'text'; + var text = div.firstChild; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + expectMutationRecord(records[0], { + type: 'childList', + target: div, + addedNodes: makeNodeList(text), + removedNodes: makeNodeList(a, b), + previousSibling: null, + nextSibling: null + }); + }); + + test('appendChild removal', function() { + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('c'); + + a.appendChild(c); + + var observerA = new MutationObserver(function() {}); + observerA.observe(a, { + childList: true + }); + + var observerB = new MutationObserver(function() {}); + observerB.observe(b, { + childList: true + }); + + b.appendChild(c); + + var recordsA = observerA.takeRecords(); + + assert.equal(recordsA.length, 1); + expectMutationRecord(recordsA[0], { + type: 'childList', + target: a, + removedNodes: [c] + }); + + var recordsB = observerB.takeRecords(); + assert.equal(recordsB.length, 1); + expectMutationRecord(recordsB[0], { + type: 'childList', + target: b, + addedNodes: [c] + }); + }); + + test('insertBefore removal', function() { + var a = document.createElement('a'); + var b = document.createElement('b'); + var c = document.createElement('c'); + var d = document.createElement('d'); + var e = document.createElement('e'); + + a.appendChild(c); + a.appendChild(d); + b.appendChild(e); + + var observerA = new MutationObserver(function() {}); + observerA.observe(a, { + childList: true + }); + + var observerB = new MutationObserver(function() {}); + observerB.observe(b, { + childList: true + }); + + b.insertBefore(d, e); + + var recordsA = observerA.takeRecords(); + + assert.equal(recordsA.length, 1); + expectMutationRecord(recordsA[0], { + type: 'childList', + target: a, + removedNodes: [d], + previousSibling: c + }); + + var recordsB = observerB.takeRecords(); + assert.equal(recordsB.length, 1); + expectMutationRecord(recordsB[0], { + type: 'childList', + target: b, + addedNodes: [d], + nextSibling: e + }); + }); + + test('appendChild removal document fragment', function() { + var df = document.createDocumentFragment(); + var b = document.createElement('b'); + var c = document.createElement('c'); + + df.appendChild(c); + + var observerDf = new MutationObserver(function() {}); + observerDf.observe(df, { + childList: true + }); + + var observerB = new MutationObserver(function() {}); + observerB.observe(b, { + childList: true + }); + + b.appendChild(df); + + var recordsDf = observerDf.takeRecords(); + + assert.equal(recordsDf.length, 1); + expectMutationRecord(recordsDf[0], { + type: 'childList', + target: df, + removedNodes: [c] + }); + + var recordsB = observerB.takeRecords(); + assert.equal(recordsB.length, 1); + expectMutationRecord(recordsB[0], { + type: 'childList', + target: b, + addedNodes: [c] + }); + }); + + }); + +}); diff --git a/test/js/MutationObserver/mixed.js b/test/js/MutationObserver/mixed.js new file mode 100644 index 0000000..afc75a9 --- /dev/null +++ b/test/js/MutationObserver/mixed.js @@ -0,0 +1,40 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('mixed', function() { + + test('attr and characterData', function() { + var div = document.createElement('div'); + var text = div.appendChild(document.createTextNode('text')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + characterData: true, + subtree: true + }); + div.setAttribute('a', 'A'); + div.firstChild.data = 'changed'; + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'attributes', + target: div, + attributeName: 'a', + attributeNamespace: null + }); + expectMutationRecord(records[1], { + type: 'characterData', + target: div.firstChild + }); + }); + + }); + +}); \ No newline at end of file diff --git a/test/js/MutationObserver/options.js b/test/js/MutationObserver/options.js new file mode 100644 index 0000000..bea4c5d --- /dev/null +++ b/test/js/MutationObserver/options.js @@ -0,0 +1,110 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('options', function() { + + test('attributeOldValue and attributes', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + + assert.throws(function() { + observer.observe(div, { + attributeOldValue: true, + attributes: false + }); + }, TypeError); + + observer.observe(div, { + attributeOldValue: true, + }); + + observer.observe(div, { + attributeOldValue: false, + attributes: false + }); + + observer.observe(div, { + attributeOldValue: false, + attributes: true + }); + + observer.observe(div, { + attributeOldValue: true, + attributes: true + }); + }); + + test('attributeFilter and attributes', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + + assert.throws(function() { + observer.observe(div, { + attributeFilter: ['name'], + attributes: false + }); + }, TypeError); + + observer.observe(div, { + attributeFilter: ['name'], + }); + + assert.throws(function() { + observer.observe(div, { + attributeFilter: null, + }); + }, TypeError); + + observer.observe(div, { + attributeFilter: ['name'], + attributes: true + }); + + observer.observe(div, { + attributes: false + }); + + observer.observe(div, { + attributes: true + }); + }); + + test('characterDataOldValue and characterData', function() { + var div = document.createElement('div'); + var observer = new MutationObserver(function() {}); + + assert.throws(function() { + observer.observe(div, { + characterDataOldValue: true, + characterData: false + }); + }, TypeError); + + observer.observe(div, { + characterDataOldValue: true + }); + + observer.observe(div, { + characterDataOldValue: false, + characterData: false + }); + + observer.observe(div, { + characterDataOldValue: false, + characterData: true + }); + + observer.observe(div, { + characterDataOldValue: true, + characterData: true + }); + }); + + }); + +}); \ No newline at end of file diff --git a/test/js/MutationObserver/transient.js b/test/js/MutationObserver/transient.js new file mode 100644 index 0000000..1f85cdd --- /dev/null +++ b/test/js/MutationObserver/transient.js @@ -0,0 +1,278 @@ +/* + * Copyright 2012 The Polymer Authors. All rights reserved. + * Use of this source code is governed by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('MutationObserver', function() { + + suite('transient', function() { + + test('attr', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + attributes: true, + subtree: true + }); + div.removeChild(child); + child.setAttribute('a', 'A'); + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + child.setAttribute('b', 'B'); + + records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'b', + attributeNamespace: null + }); + }); + + test('attr callback', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new MutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + // The transient observers are removed before the callback is called. + child.setAttribute('b', 'B'); + records = observer.takeRecords(); + assert.equal(records.length, 0); + + cont(); + }); + + observer.observe(div, { + attributes: true, + subtree: true + }); + + div.removeChild(child); + child.setAttribute('a', 'A'); + }); + + test('attr, make sure transient gets removed', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new MutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'a', + attributeNamespace: null + }); + + step2(); + }); + + observer.observe(div, { + attributes: true, + subtree: true + }); + + div.removeChild(child); + child.setAttribute('a', 'A'); + + function step2() { + var div2 = document.createElement('div'); + var observer2 = new MutationObserver(function(records) { + i++; + if (i > 2) + expect().fail(); + + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'attributes', + target: child, + attributeName: 'b', + attributeNamespace: null + }); + + cont(); + }); + + observer2.observe(div2, { + attributes: true, + subtree: true, + }); + + div2.appendChild(child); + child.setAttribute('b', 'B'); + } + }); + + test('characterData', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createTextNode('text')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + characterData: true, + subtree: true + }); + div.removeChild(child); + child.data = 'changed'; + + var records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'characterData', + target: child + }); + + child.data += ' again'; + + records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'characterData', + target: child + }); + }); + + test('characterData callback', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createTextNode('text')); + var i = 0; + var observer = new MutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'characterData', + target: child + }); + + // The transient observers are removed before the callback is called. + child.data += ' again'; + records = observer.takeRecords(); + assert.equal(records.length, 0); + + cont(); + }); + observer.observe(div, { + characterData: true, + subtree: true + }); + div.removeChild(child); + child.data = 'changed'; + }); + + test('childList', function() { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var observer = new MutationObserver(function() {}); + observer.observe(div, { + childList: true, + subtree: true + }); + div.removeChild(child); + var grandChild = child.appendChild(document.createElement('span')); + + var records = observer.takeRecords(); + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [child] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: child, + addedNodes: [grandChild] + }); + + child.removeChild(grandChild); + + records = observer.takeRecords(); + assert.equal(records.length, 1); + + expectMutationRecord(records[0], { + type: 'childList', + target: child, + removedNodes: [grandChild] + }); + }); + + test('childList callback', function(cont) { + var div = document.createElement('div'); + var child = div.appendChild(document.createElement('div')); + var i = 0; + var observer = new MutationObserver(function(records) { + i++; + if (i > 1) + expect().fail(); + + assert.equal(records.length, 2); + + expectMutationRecord(records[0], { + type: 'childList', + target: div, + removedNodes: [child] + }); + + expectMutationRecord(records[1], { + type: 'childList', + target: child, + addedNodes: [grandChild] + }); + + // The transient observers are removed before the callback is called. + child.removeChild(grandChild); + + records = observer.takeRecords(); + assert.equal(records.length, 0); + + cont(); + }); + observer.observe(div, { + childList: true, + subtree: true + }); + div.removeChild(child); + var grandChild = child.appendChild(document.createElement('span')); + }); + + }); + +}); \ No newline at end of file diff --git a/test/js/microtask.js b/test/js/microtask.js new file mode 100644 index 0000000..179057b --- /dev/null +++ b/test/js/microtask.js @@ -0,0 +1,46 @@ +/* + * Copyright 2013 The Polymer Authors. All rights reserved. + * Use of this source code is goverened by a BSD-style + * license that can be found in the LICENSE file. + */ + +suite('Microtask', function() { + + var setEndOfMicrotask = ShadowDOMPolyfill.setEndOfMicrotask; + + test('single', function(done) { + setEndOfMicrotask(done); + }); + + test('multiple', function(done) { + var count = 0; + setEndOfMicrotask(function() { + count++; + assert.equal(2, count); + }); + setEndOfMicrotask(function() { + count++; + assert.equal(3, count); + }); + setEndOfMicrotask(function() { + count++; + assert.equal(4, count); + done(); + }); + count++; + }); + + test('nested', function(done) { + var count = 0; + setEndOfMicrotask(function() { + assert.equal(1, count); + setEndOfMicrotask(function() { + assert.equal(2, count); + done(); + }); + count++; + }); + count++; + }); + +}); diff --git a/test/test.main.js b/test/test.main.js index 3f2da9a..08662f6 100644 --- a/test/test.main.js +++ b/test/test.main.js @@ -44,6 +44,29 @@ function assertArrayEqual(a, b, msg) { assert.equal(a.length, b.length, msg); } +function expectMutationRecord(record, expected) { + assert.equal(record.type, + expected.type === undefined ? null : expected.type); + assert.equal(record.target, + expected.target === undefined ? null : expected.target); + assertArrayEqual(record.addedNodes, + expected.addedNodes === undefined ? [] : expected.addedNodes); + assertArrayEqual(record.removedNodes, + expected.removedNodes === undefined ? [] : expected.removedNodes); + assert.equal(record.previousSibling, + expected.previousSibling === undefined ? + null : expected.previousSibling); + assert.equal(record.nextSibling, + expected.nextSibling === undefined ? null : expected.nextSibling); + assert.equal(record.attributeName, + expected.attributeName === undefined ? null : expected.attributeName); + assert.equal(record.attributeNamespace, + expected.attributeNamespace === undefined ? + null : expected.attributeNamespace); + assert.equal(record.oldValue, + expected.oldValue === undefined ? null : expected.oldValue); +} + mocha.setup({ ui: 'tdd', globals: ['console', 'getInterface'] @@ -76,14 +99,22 @@ var modules = [ 'HTMLTemplateElement.js', 'HTMLTextAreaElement.js', 'MutationObserver.js', + 'MutationObserver/attributes.js', + 'MutationObserver/callback.js', + 'MutationObserver/characterData.js', + 'MutationObserver/childList.js', + 'MutationObserver/mixed.js', + 'MutationObserver/transient.js', + 'MutationObserver/options.js', 'Node.js', 'ParentNodeInterface.js', 'ShadowRoot.js', 'Text.js', - 'Window.js', 'Range.js', + 'Window.js', 'custom-element.js', 'events.js', + 'microtask.js', 'paralleltrees.js', 'reprojection.js', 'rerender.js',