diff --git a/build.json b/build.json
index 1468013..12ec786 100644
--- a/build.json
+++ b/build.json
@@ -19,5 +19,7 @@
"src/wrappers/Document.js",
"src/wrappers/Window.js",
"src/wrappers/MutationObserver.js",
+ "src/wrappers/Range.js",
+ "src/wrappers/elements-with-form-property.js",
"src/wrappers/override-constructors.js"
]
diff --git a/shadowdom.js b/shadowdom.js
index b697830..55691ef 100644
--- a/shadowdom.js
+++ b/shadowdom.js
@@ -33,9 +33,11 @@
'wrappers/generic.js',
'wrappers/ShadowRoot.js',
'ShadowRenderer.js',
+ 'wrappers/elements-with-form-property.js',
'wrappers/Document.js',
'wrappers/Window.js',
'wrappers/MutationObserver.js',
+ 'wrappers/Range.js',
'wrappers/override-constructors.js'
].forEach(function(src) {
document.write('');
diff --git a/src/wrappers.js b/src/wrappers.js
index da76593..ec37611 100644
--- a/src/wrappers.js
+++ b/src/wrappers.js
@@ -9,6 +9,7 @@ var ShadowDOMPolyfill = {};
var wrapperTable = new SideTable();
var constructorTable = new SideTable();
+ var nativePrototypeTable = new SideTable();
var wrappers = Object.create(null);
function assert(b) {
@@ -140,9 +141,8 @@ var ShadowDOMPolyfill = {};
/**
* @param {Function} nativeConstructor
* @param {Function} wrapperConstructor
- * @param {string|Object=} opt_instance If present, this is used to extract
- * properties from an instance object. If this is a string
- * |document.createElement| is used to create an instance.
+ * @param {Object=} opt_instance If present, this is used to extract
+ * properties from an instance object.
*/
function register(nativeConstructor, wrapperConstructor, opt_instance) {
var nativePrototype = nativeConstructor.prototype;
@@ -153,7 +153,10 @@ var ShadowDOMPolyfill = {};
function registerInternal(nativePrototype, wrapperConstructor, opt_instance) {
var wrapperPrototype = wrapperConstructor.prototype;
assert(constructorTable.get(nativePrototype) === undefined);
+
constructorTable.set(nativePrototype, wrapperConstructor);
+ nativePrototypeTable.set(wrapperPrototype, nativePrototype);
+
addForwardingProperties(nativePrototype, wrapperPrototype);
if (opt_instance)
registerInstanceProperties(wrapperPrototype, opt_instance);
@@ -199,10 +202,12 @@ var ShadowDOMPolyfill = {};
var OriginalEvent = Event;
var OriginalNode = Node;
var OriginalWindow = Window;
+ var OriginalRange = Range;
function isWrapper(object) {
return object instanceof wrappers.EventTarget ||
object instanceof wrappers.Event ||
+ object instanceof wrappers.Range ||
object instanceof wrappers.DOMImplementation;
}
@@ -210,6 +215,7 @@ var ShadowDOMPolyfill = {};
return object instanceof OriginalNode ||
object instanceof OriginalEvent ||
object instanceof OriginalWindow ||
+ object instanceof OriginalRange ||
object instanceof OriginalDOMImplementation;
}
@@ -310,11 +316,13 @@ var ShadowDOMPolyfill = {};
}
scope.assert = assert;
+ scope.constructorTable = constructorTable;
scope.defineGetter = defineGetter;
scope.defineWrapGetter = defineWrapGetter;
scope.forwardMethodsToWrapper = forwardMethodsToWrapper;
scope.isWrapperFor = isWrapperFor;
scope.mixin = mixin;
+ scope.nativePrototypeTable = nativePrototypeTable;
scope.registerObject = registerObject;
scope.registerWrapper = register;
scope.rewrap = rewrap;
diff --git a/src/wrappers/Document.js b/src/wrappers/Document.js
index 6a31289..bd6def3 100644
--- a/src/wrappers/Document.js
+++ b/src/wrappers/Document.js
@@ -45,13 +45,15 @@
}
[
- 'getElementById',
+ 'createComment',
+ 'createDocumentFragment',
'createElement',
'createElementNS',
- 'createTextNode',
- 'createDocumentFragment',
'createEvent',
'createEventNS',
+ 'createRange',
+ 'createTextNode',
+ 'getElementById',
].forEach(wrapMethod);
var originalAdoptNode = document.adoptNode;
@@ -97,15 +99,99 @@
}
});
+ if (document.register) {
+ var originalRegister = document.register;
+ Document.prototype.register = function(tagName, object) {
+ var prototype = object.prototype;
+
+ // If we already used the object as a prototype for another custom
+ // element.
+ if (scope.nativePrototypeTable.get(prototype)) {
+ // TODO(arv): DOMException
+ throw new Error('NotSupportedError');
+ }
+
+ // Find first object on the prototype chain that already have a native
+ // prototype. Keep track of all the objects before that so we can create
+ // a similar structure for the native case.
+ var proto = Object.getPrototypeOf(prototype);
+ var nativePrototype;
+ var prototypes = [];
+ while (proto) {
+ nativePrototype = scope.nativePrototypeTable.get(proto);
+ if (nativePrototype)
+ break;
+ prototypes.push(proto);
+ proto = Object.getPrototypeOf(proto);
+ }
+
+ if (!nativePrototype) {
+ // TODO(arv): DOMException
+ throw new Error('NotSupportedError');
+ }
+
+ // This works by creating a new prototype object that is empty, but has
+ // the native prototype as its proto. The original prototype object
+ // passed into register is used as the wrapper prototype.
+
+ var newPrototype = Object.create(nativePrototype);
+ for (var i = prototypes.length - 1; i >= 0; i--) {
+ newPrototype = Object.create(newPrototype);
+ }
+
+ // Add callbacks if present.
+ // Names are taken from:
+ // https://code.google.com/p/chromium/codesearch#chromium/src/third_party/WebKit/Source/bindings/v8/CustomElementConstructorBuilder.cpp&sq=package:chromium&type=cs&l=156
+ // and not from the spec since the spec is out of date.
+ [
+ 'createdCallback',
+ 'enteredDocumentCallback',
+ 'leftDocumentCallback',
+ 'attributeChangedCallback',
+ ].forEach(function(name) {
+ var f = prototype[name];
+ if (!f)
+ return;
+ newPrototype[name] = function() {
+ f.apply(wrap(this), arguments);
+ };
+ });
+
+ var nativeConstructor = originalRegister.call(unwrap(this), tagName,
+ {prototype: newPrototype});
+
+ function GeneratedWrapper(node) {
+ if (!node)
+ return document.createElement(tagName);
+ this.impl = node;
+ }
+ GeneratedWrapper.prototype = prototype;
+ GeneratedWrapper.prototype.constructor = GeneratedWrapper;
+
+ scope.constructorTable.set(newPrototype, GeneratedWrapper);
+ scope.nativePrototypeTable.set(prototype, newPrototype);
+
+ return GeneratedWrapper;
+ };
+
+ forwardMethodsToWrapper([
+ window.HTMLDocument || window.Document, // Gecko adds these to HTMLDocument
+ ], [
+ 'register',
+ ]);
+ }
+
// We also override some of the methods on document.body and document.head
// for convenience.
forwardMethodsToWrapper([
window.HTMLBodyElement,
window.HTMLDocument || window.Document, // Gecko adds these to HTMLDocument
window.HTMLHeadElement,
+ window.HTMLHtmlElement,
], [
'appendChild',
'compareDocumentPosition',
+ 'contains',
'getElementsByClassName',
'getElementsByTagName',
'getElementsByTagNameNS',
@@ -120,11 +206,14 @@
window.HTMLDocument || window.Document, // Gecko adds these to HTMLDocument
], [
'adoptNode',
+ 'contains',
+ 'createComment',
'createDocumentFragment',
'createElement',
'createElementNS',
'createEvent',
'createEventNS',
+ 'createRange',
'createTextNode',
'elementFromPoint',
'getElementById',
diff --git a/src/wrappers/Node.js b/src/wrappers/Node.js
index 2b4d9fc..2bc3e6d 100644
--- a/src/wrappers/Node.js
+++ b/src/wrappers/Node.js
@@ -13,6 +13,7 @@
var registerWrapper = scope.registerWrapper;
var unwrap = scope.unwrap;
var wrap = scope.wrap;
+ var wrapIfNeeded = scope.wrapIfNeeded;
function assertIsNodeWrapper(node) {
assert(node instanceof Node);
@@ -363,6 +364,8 @@
if (!child)
return false;
+ child = wrapIfNeeded(child);
+
// TODO(arv): Optimize using ownerDocument etc.
if (child === this)
return true;
diff --git a/src/wrappers/Range.js b/src/wrappers/Range.js
new file mode 100644
index 0000000..019805e
--- /dev/null
+++ b/src/wrappers/Range.js
@@ -0,0 +1,86 @@
+// 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 registerWrapper = scope.registerWrapper;
+ var unwrap = scope.unwrap;
+ var unwrapIfNeeded = scope.unwrapIfNeeded;
+ var wrap = scope.wrap;
+
+ function Range(impl) {
+ this.impl = impl;
+ }
+ Range.prototype = {
+ get startContainer() {
+ return wrap(this.impl.startContainer);
+ },
+ get endContainer() {
+ return wrap(this.impl.endContainer);
+ },
+ get commonAncestorContainer() {
+ return wrap(this.impl.commonAncestorContainer);
+ },
+ setStart: function(refNode,offset) {
+ this.impl.setStart(unwrapIfNeeded(refNode), offset);
+ },
+ setEnd: function(refNode,offset) {
+ this.impl.setEnd(unwrapIfNeeded(refNode), offset);
+ },
+ setStartBefore: function(refNode) {
+ this.impl.setStartBefore(unwrapIfNeeded(refNode));
+ },
+ setStartAfter: function(refNode) {
+ this.impl.setStartAfter(unwrapIfNeeded(refNode));
+ },
+ setEndBefore: function(refNode) {
+ this.impl.setEndBefore(unwrapIfNeeded(refNode));
+ },
+ setEndAfter: function(refNode) {
+ this.impl.setEndAfter(unwrapIfNeeded(refNode));
+ },
+ selectNode: function(refNode) {
+ this.impl.selectNode(unwrapIfNeeded(refNode));
+ },
+ selectNodeContents: function(refNode) {
+ this.impl.selectNodeContents(unwrapIfNeeded(refNode));
+ },
+ compareBoundaryPoints: function(how, sourceRange) {
+ return this.impl.compareBoundaryPoints(how, unwrap(sourceRange));
+ },
+ extractContents: function() {
+ return wrap(this.impl.extractContents());
+ },
+ cloneContents: function() {
+ return wrap(this.impl.cloneContents());
+ },
+ insertNode: function(node) {
+ this.impl.insertNode(unwrapIfNeeded(node));
+ },
+ surroundContents: function(newParent) {
+ this.impl.surroundContents(unwrapIfNeeded(newParent));
+ },
+ cloneRange: function() {
+ return wrap(this.impl.cloneRange());
+ },
+ isPointInRange: function(node, offset) {
+ return this.impl.isPointInRange(unwrapIfNeeded(node), offset);
+ },
+ comparePoint: function(node, offset) {
+ return this.impl.comparePoint(unwrapIfNeeded(node), offset);
+ },
+ intersectsNode: function(node) {
+ return this.impl.intersectsNode(unwrapIfNeeded(node));
+ },
+ createContextualFragment: function(html) {
+ return wrap(this.impl.createContextualFragment(html));
+ }
+ };
+
+ registerWrapper(window.Range, Range);
+
+ scope.wrappers.Range = Range;
+
+})(this.ShadowDOMPolyfill);
diff --git a/src/wrappers/elements-with-form-property.js b/src/wrappers/elements-with-form-property.js
new file mode 100644
index 0000000..1617ada
--- /dev/null
+++ b/src/wrappers/elements-with-form-property.js
@@ -0,0 +1,54 @@
+// 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 HTMLElement = scope.wrappers.HTMLElement;
+ var assert = scope.assert;
+ var mixin = scope.mixin;
+ var registerWrapper = scope.registerWrapper;
+ var unwrap = scope.unwrap;
+ var wrap = scope.wrap;
+
+ var elementsWithFormProperty = [
+ 'HTMLButtonElement',
+ 'HTMLFieldSetElement',
+ 'HTMLInputElement',
+ 'HTMLKeygenElement',
+ 'HTMLLabelElement',
+ 'HTMLLegendElement',
+ 'HTMLObjectElement',
+ 'HTMLOptionElement',
+ 'HTMLOutputElement',
+ 'HTMLSelectElement',
+ 'HTMLTextAreaElement',
+ ];
+
+ function createWrapperConstructor(name) {
+ if (!window[name])
+ return;
+
+ // Ensure we are not overriding an already existing constructor.
+ assert(!scope.wrappers[name]);
+
+ var GeneratedWrapper = function(node) {
+ // At this point all of them extend HTMLElement.
+ HTMLElement.call(this, node);
+ }
+ GeneratedWrapper.prototype = Object.create(HTMLElement.prototype);
+ mixin(GeneratedWrapper.prototype, {
+ get form() {
+ return wrap(unwrap(this).form);
+ },
+ });
+
+ registerWrapper(window[name], GeneratedWrapper,
+ document.createElement(name.slice(4, -7)));
+ scope.wrappers[name] = GeneratedWrapper;
+ }
+
+ elementsWithFormProperty.forEach(createWrapperConstructor);
+
+})(this.ShadowDOMPolyfill);
diff --git a/test/js/Document.js b/test/js/Document.js
index 35f9a94..e4517ee 100644
--- a/test/js/Document.js
+++ b/test/js/Document.js
@@ -248,5 +248,204 @@ htmlSuite('Document', function() {
assert.equal(doc.elementFromPoint(5, 5), div);
});
+ test('document.contains', function() {
+ assert.isTrue(document.contains(document.body));
+ assert.isTrue(document.contains(document.querySelector('body')));
+
+ assert.isTrue(document.contains(document.head));
+ assert.isTrue(document.contains(document.querySelector('head')));
+
+ assert.isTrue(document.contains(document.documentElement));
+ assert.isTrue(document.contains(document.querySelector('html')));
+ });
+
+ test('document.register', function() {
+ if (!document.register)
+ return;
+
+ var aPrototype = Object.create(HTMLElement.prototype);
+ aPrototype.getName = function() {
+ return 'a';
+ };
+
+ var A = document.register('x-a', {prototype: aPrototype});
+
+ var a1 = document.createElement('x-a');
+ assert.equal('x-a', a1.localName);
+ assert.equal(Object.getPrototypeOf(a1), aPrototype);
+ assert.instanceOf(a1, A);
+ assert.instanceOf(a1, HTMLElement);
+ assert.equal(a1.getName(), 'a');
+
+ var a2 = new A();
+ assert.equal('x-a', a2.localName);
+ assert.equal(Object.getPrototypeOf(a2), aPrototype);
+ assert.instanceOf(a2, A);
+ assert.instanceOf(a2, HTMLElement);
+ assert.equal(a2.getName(), 'a');
+
+ //////////////////////////////////////////////////////////////////////
+
+ var bPrototype = Object.create(A.prototype);
+ bPrototype.getName = function() {
+ return 'b';
+ };
+
+ var B = document.register('x-b', {prototype: bPrototype});
+
+ var b1 = document.createElement('x-b');
+ assert.equal('x-b', b1.localName);
+ assert.equal(Object.getPrototypeOf(b1), bPrototype);
+ assert.instanceOf(b1, B);
+ assert.instanceOf(b1, A);
+ assert.instanceOf(b1, HTMLElement);
+ assert.equal(b1.getName(), 'b');
+
+ var b2 = new B();
+ assert.equal('x-b', b2.localName);
+ assert.equal(Object.getPrototypeOf(b2), bPrototype);
+ assert.instanceOf(b2, B);
+ assert.instanceOf(b2, A);
+ assert.instanceOf(b2, HTMLElement);
+ assert.equal(b2.getName(), 'b');
+ });
+
+ test('document.register deeper', function() {
+ if (!document.register)
+ return;
+
+ function C() {}
+ C.prototype = {
+ __proto__: HTMLElement.prototype
+ };
+
+ function B() {}
+ B.prototype = {
+ __proto__: C.prototype
+ };
+
+ function A() {}
+ A.prototype = {
+ __proto__: B.prototype
+ };
+
+ A = document.register('x-a5', A);
+
+ var a1 = document.createElement('x-a5');
+ assert.equal('x-a5', a1.localName);
+ assert.equal(a1.__proto__, A.prototype);
+ assert.equal(a1.__proto__.__proto__, B.prototype);
+ assert.equal(a1.__proto__.__proto__.__proto__, C.prototype);
+ assert.equal(a1.__proto__.__proto__.__proto__.__proto__,
+ HTMLElement.prototype);
+
+ var a2 = new A();
+ assert.equal('x-a5', a2.localName);
+ assert.equal(a2.__proto__, A.prototype);
+ assert.equal(a2.__proto__.__proto__, B.prototype);
+ assert.equal(a2.__proto__.__proto__.__proto__, C.prototype);
+ assert.equal(a2.__proto__.__proto__.__proto__.__proto__,
+ HTMLElement.prototype);
+ });
+
+ test('document.register createdCallback', function() {
+ if (!document.register)
+ return;
+
+ var self;
+ var createdCalls = 0;
+
+ function A() {}
+ A.prototype = {
+ __proto__: HTMLElement.prototype,
+ createdCallback: function() {
+ createdCalls++;
+ assert.isUndefined(a);
+ assert.instanceOf(this, A);
+ self = this;
+ }
+ }
+
+ A = document.register('x-a2', A);
+
+ var a = new A;
+ assert.equal(createdCalls, 1);
+ assert.equal(self, a);
+ });
+
+ test('document.register enteredDocumentCallback, leftDocumentCallback',
+ function() {
+ if (!document.register)
+ return;
+
+ var enteredDocumentCalls = 0;
+ var leftDocumentCalls = 0;
+
+ function A() {}
+ A.prototype = {
+ __proto__: HTMLElement.prototype,
+ enteredDocumentCallback: function() {
+ enteredDocumentCalls++;
+ assert.instanceOf(this, A);
+ assert.equal(a, this);
+ },
+ leftDocumentCallback: function() {
+ leftDocumentCalls++;
+ assert.instanceOf(this, A);
+ assert.equal(a, this);
+ }
+ }
+
+ A = document.register('x-a3', A);
+
+ var a = new A;
+ document.body.appendChild(a);
+ assert.equal(enteredDocumentCalls, 1);
+ document.body.removeChild(a);
+ assert.equal(leftDocumentCalls, 1);
+ });
+
+ test('document.register attributeChangedCallback', function() {
+ if (!document.register)
+ return;
+
+ var attributeChangedCalls = 0;
+
+ function A() {}
+ A.prototype = {
+ __proto__: HTMLElement.prototype,
+ attributeChangedCallback: function(name, oldValue, newValue) {
+ attributeChangedCalls++;
+ assert.equal(name, 'foo');
+ switch (attributeChangedCalls) {
+ case 1:
+ assert.isNull(oldValue);
+ assert.equal(newValue, 'bar');
+ break;
+ case 2:
+ assert.equal(oldValue, 'bar');
+ assert.equal(newValue, 'baz');
+ break;
+ case 3:
+ assert.equal(oldValue, 'baz');
+ assert.isNull(newValue);
+ break;
+ }
+ console.log(arguments);
+ }
+ }
+
+ A = document.register('x-a4', A);
+
+ var a = new A;
+ assert.equal(attributeChangedCalls, 0);
+ a.setAttribute('foo', 'bar');
+ assert.equal(attributeChangedCalls, 1);
+ a.setAttribute('foo', 'baz');
+ assert.equal(attributeChangedCalls, 2);
+ a.removeAttribute('foo');
+ assert.equal(attributeChangedCalls, 3);
+ });
+
htmlTest('html/document-write.html');
});
diff --git a/test/js/HTMLBodyElement.js b/test/js/HTMLBodyElement.js
index fb9bdbc..ac02abc 100644
--- a/test/js/HTMLBodyElement.js
+++ b/test/js/HTMLBodyElement.js
@@ -105,5 +105,13 @@ htmlSuite('HTMLBodyElement', function() {
assert.equal(calls, 2);
});
+ test('document.body.contains', function() {
+ var doc = wrap(document);
+ assert.isTrue(doc.body.contains(doc.body.firstChild));
+ assert.isTrue(doc.body.contains(document.body.firstChild));
+ assert.isTrue(document.body.contains(doc.body.firstChild));
+ assert.isTrue(document.body.contains(document.body.firstChild));
+ });
+
htmlTest('html/document-body-inner-html.html');
});
diff --git a/test/js/HTMLButtonElement.js b/test/js/HTMLButtonElement.js
new file mode 100644
index 0000000..ede1c15
--- /dev/null
+++ b/test/js/HTMLButtonElement.js
@@ -0,0 +1,16 @@
+/*
+ * 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('HTMLButtonElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var button = document.createElement('button');
+ form.appendChild(button);
+ assert.equal(button.form, form);
+ });
+
+});
diff --git a/test/js/HTMLFieldSetElement.js b/test/js/HTMLFieldSetElement.js
new file mode 100644
index 0000000..cc6f5df
--- /dev/null
+++ b/test/js/HTMLFieldSetElement.js
@@ -0,0 +1,16 @@
+/*
+ * 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('HTMLFieldSetElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var fieldSet = document.createElement('fieldset');
+ form.appendChild(fieldSet);
+ assert.equal(fieldSet.form, form);
+ });
+
+});
diff --git a/test/js/HTMLHeadElement.js b/test/js/HTMLHeadElement.js
index d0ad020..4e0bd1d 100644
--- a/test/js/HTMLHeadElement.js
+++ b/test/js/HTMLHeadElement.js
@@ -85,4 +85,12 @@ suite('HTMLHeadElement', function() {
doc.body.removeChild(div);
assert.isNull(div.parentNode);
});
+
+ test('document.head.contains', function() {
+ var doc = wrap(document);
+ assert.isTrue(doc.head.contains(doc.head.firstChild));
+ assert.isTrue(doc.head.contains(document.head.firstChild));
+ assert.isTrue(document.head.contains(doc.head.firstChild));
+ assert.isTrue(document.head.contains(document.head.firstChild));
+ });
});
diff --git a/test/js/HTMLHtmlElement.js b/test/js/HTMLHtmlElement.js
new file mode 100644
index 0000000..3955917
--- /dev/null
+++ b/test/js/HTMLHtmlElement.js
@@ -0,0 +1,30 @@
+/*
+ * 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('HTMLHtmlElement', function() {
+
+ var wrap = ShadowDOMPolyfill.wrap;
+
+ test('instanceof', function() {
+ var doc = wrap(document);
+ assert.instanceOf(doc.documentElement, HTMLHtmlElement);
+ assert.equal(wrap(document.documentElement), doc.documentElement);
+ });
+
+ test('appendChild', function() {
+ var doc = wrap(document);
+
+ var a = document.createComment('a');
+ var b = document.createComment('b');
+
+ document.documentElement.appendChild(a);
+ assert.equal(doc.documentElement.lastChild, a);
+
+ doc.documentElement.appendChild(b);
+ assert.equal(doc.documentElement.lastChild, b);
+ });
+
+});
diff --git a/test/js/HTMLInputElement.js b/test/js/HTMLInputElement.js
new file mode 100644
index 0000000..c5ee42a
--- /dev/null
+++ b/test/js/HTMLInputElement.js
@@ -0,0 +1,16 @@
+/*
+ * 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('HTMLInputElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var input = document.createElement('input');
+ form.appendChild(input);
+ assert.equal(input.form, form);
+ });
+
+});
diff --git a/test/js/HTMLKeygenElement.js b/test/js/HTMLKeygenElement.js
new file mode 100644
index 0000000..609d1c6
--- /dev/null
+++ b/test/js/HTMLKeygenElement.js
@@ -0,0 +1,19 @@
+/*
+ * 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('HTMLKeygenElement', function() {
+ // Not implemented in Firefox.
+ if (!window.HTMLKeygenElement)
+ return;
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var keygen = document.createElement('keygen');
+ form.appendChild(keygen);
+ assert.equal(keygen.form, form);
+ });
+
+});
diff --git a/test/js/HTMLLabelElement.js b/test/js/HTMLLabelElement.js
new file mode 100644
index 0000000..b059704
--- /dev/null
+++ b/test/js/HTMLLabelElement.js
@@ -0,0 +1,16 @@
+/*
+ * 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('HTMLLabelElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var label = document.createElement('label');
+ form.appendChild(label);
+ assert.equal(label.form, form);
+ });
+
+});
diff --git a/test/js/HTMLLegendElement.js b/test/js/HTMLLegendElement.js
new file mode 100644
index 0000000..09ac37c
--- /dev/null
+++ b/test/js/HTMLLegendElement.js
@@ -0,0 +1,18 @@
+/*
+ * 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('HTMLLegendElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var fieldSet = document.createElement('fieldset');
+ var legend = document.createElement('legend');
+ form.appendChild(fieldSet);
+ fieldSet.appendChild(legend);
+ assert.equal(legend.form, form);
+ });
+
+});
diff --git a/test/js/HTMLObjectElement.js b/test/js/HTMLObjectElement.js
new file mode 100644
index 0000000..8b1ba08
--- /dev/null
+++ b/test/js/HTMLObjectElement.js
@@ -0,0 +1,16 @@
+/*
+ * 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('HTMLObjectElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var object = document.createElement('object');
+ form.appendChild(object);
+ assert.equal(object.form, form);
+ });
+
+});
diff --git a/test/js/HTMLOptionElement.js b/test/js/HTMLOptionElement.js
new file mode 100644
index 0000000..3823a35
--- /dev/null
+++ b/test/js/HTMLOptionElement.js
@@ -0,0 +1,18 @@
+/*
+ * 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('HTMLOptionElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var select = document.createElement('select');
+ var option = document.createElement('option');
+ form.appendChild(select);
+ select.appendChild(option);
+ assert.equal(option.form, form);
+ });
+
+});
diff --git a/test/js/HTMLOutputElement.js b/test/js/HTMLOutputElement.js
new file mode 100644
index 0000000..ebe3dcd
--- /dev/null
+++ b/test/js/HTMLOutputElement.js
@@ -0,0 +1,19 @@
+/*
+ * 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('HTMLOutputElement', function() {
+ // Not implemented in IE10.
+ if (!window.HTMLOutputElement)
+ return;
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var output = document.createElement('output');
+ form.appendChild(output);
+ assert.equal(output.form, form);
+ });
+
+});
diff --git a/test/js/HTMLSelectElement.js b/test/js/HTMLSelectElement.js
new file mode 100644
index 0000000..f9e3792
--- /dev/null
+++ b/test/js/HTMLSelectElement.js
@@ -0,0 +1,16 @@
+/*
+ * 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('HTMLSelectElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var select = document.createElement('select');
+ form.appendChild(select);
+ assert.equal(select.form, form);
+ });
+
+});
diff --git a/test/js/HTMLTextAreaElement.js b/test/js/HTMLTextAreaElement.js
new file mode 100644
index 0000000..a40b63b
--- /dev/null
+++ b/test/js/HTMLTextAreaElement.js
@@ -0,0 +1,16 @@
+/*
+ * 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('HTMLTextAreaElement', function() {
+
+ test('form', function() {
+ var form = document.createElement('form');
+ var textArea = document.createElement('textarea');
+ form.appendChild(textArea);
+ assert.equal(textArea.form, form);
+ });
+
+});
diff --git a/test/js/Range.js b/test/js/Range.js
new file mode 100644
index 0000000..84617bd
--- /dev/null
+++ b/test/js/Range.js
@@ -0,0 +1,31 @@
+/*
+ * 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('Range', function() {
+
+ var wrap = ShadowDOMPolyfill.wrap;
+
+ test('instanceof', function() {
+ var range = document.createRange();
+ assert.instanceOf(range, Range);
+
+ var range2 = wrap(document).createRange();
+ assert.instanceOf(range2, Range);
+ });
+
+ test('createContextualFragment', function() {
+ var range = document.createRange();
+ var container = document.body || document.head;
+
+ range.selectNode(container);
+ var fragment = range.createContextualFragment('');
+
+ assert.instanceOf(fragment, DocumentFragment);
+ assert.equal(fragment.firstChild.localName, 'b');
+ assert.equal(fragment.childNodes.length, 1);
+ });
+
+});
diff --git a/test/test.main.js b/test/test.main.js
index 92fa4af..2a91cde 100644
--- a/test/test.main.js
+++ b/test/test.main.js
@@ -55,16 +55,29 @@ var modules = [
'Document.js',
'Element.js',
'HTMLBodyElement.js',
+ 'HTMLButtonElement.js',
'HTMLContentElement.js',
+ 'HTMLFieldSetElement.js',
'HTMLHeadElement.js',
+ 'HTMLHtmlElement.js',
+ 'HTMLInputElement.js',
+ 'HTMLKeygenElement.js',
+ 'HTMLLabelElement.js',
+ 'HTMLLegendElement.js',
+ 'HTMLObjectElement.js',
+ 'HTMLOptionElement.js',
+ 'HTMLOutputElement.js',
+ 'HTMLSelectElement.js',
'HTMLShadowElement.js',
'HTMLTemplateElement.js',
+ 'HTMLTextAreaElement.js',
'MutationObserver.js',
'Node.js',
'ParentNodeInterface.js',
'ShadowRoot.js',
'Text.js',
'Window.js',
+ 'Range.js',
'custom-element.js',
'events.js',
'paralleltrees.js',