diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d99bb10b79..d2e675bab4 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -10,7 +10,6 @@ Beyond GitHub, we try to have a variety of different lines of communication open * [Blog](https://blog.polymer-project.org/) * [Twitter](https://twitter.com/polymer) -* [Google+ Community](https://plus.sandbox.google.com/u/0/communities/115626364525706131031?cfem=1) * [Mailing list](https://groups.google.com/forum/#!forum/polymer-dev) * [Slack channel](https://bit.ly/polymerslack) diff --git a/README.md b/README.md index 58a54a8a2d..b65a090bd4 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,6 @@ Beyond GitHub, we try to have a variety of different lines of communication avai * [Blog](https://blog.polymer-project.org/) * [Twitter](https://twitter.com/polymer) -* [Google+ community](https://plus.google.com/communities/115626364525706131031) * [Mailing list](https://groups.google.com/forum/#!forum/polymer-dev) * [Slack channel](https://bit.ly/polymerslack) diff --git a/lib/elements/array-selector.js b/lib/elements/array-selector.js index c4a9816749..158d242cea 100644 --- a/lib/elements/array-selector.js +++ b/lib/elements/array-selector.js @@ -107,7 +107,7 @@ let ArraySelectorMixin = dedupingMixin(superClass => { __updateSelection(multi, itemsInfo) { let path = itemsInfo.path; - if (path == 'items') { + if (path == JSCompiler_renameProperty('items', this)) { // Case 1 - items array changed, so diff against previous array and // deselect any removed items and adjust selected indices let newItems = itemsInfo.base || []; @@ -122,14 +122,14 @@ let ArraySelectorMixin = dedupingMixin(superClass => { } this.__lastItems = newItems; this.__lastMulti = multi; - } else if (itemsInfo.path == 'items.splices') { + } else if (itemsInfo.path == `${JSCompiler_renameProperty('items', this)}.splices`) { // Case 2 - got specific splice information describing the array mutation: // deselect any removed items and adjust selected indices this.__applySplices(itemsInfo.value.indexSplices); } else { // Case 3 - an array element was changed, so deselect the previous // item for that index if it was previously selected - let part = path.slice('items.'.length); + let part = path.slice(`${JSCompiler_renameProperty('items', this)}.`.length); let idx = parseInt(part, 10); if ((part.indexOf('.') < 0) && part == idx) { this.__deselectChangedIdx(idx); @@ -167,7 +167,7 @@ let ArraySelectorMixin = dedupingMixin(superClass => { selected.forEach((idx, item) => { if (idx < 0) { if (this.multi) { - this.splice('selected', sidx, 1); + this.splice(JSCompiler_renameProperty('selected', this), sidx, 1); } else { this.selected = this.selectedItem = null; } @@ -184,13 +184,19 @@ let ArraySelectorMixin = dedupingMixin(superClass => { let sidx = 0; this.__selectedMap.forEach(idx => { if (idx >= 0) { - this.linkPaths('items.' + idx, 'selected.' + sidx++); + this.linkPaths( + `${JSCompiler_renameProperty('items', this)}.${idx}`, + `${JSCompiler_renameProperty('selected', this)}.${sidx++}`); } }); } else { this.__selectedMap.forEach(idx => { - this.linkPaths('selected', 'items.' + idx); - this.linkPaths('selectedItem', 'items.' + idx); + this.linkPaths( + JSCompiler_renameProperty('selected', this), + `${JSCompiler_renameProperty('items', this)}.${idx}`); + this.linkPaths( + JSCompiler_renameProperty('selectedItem', this), + `${JSCompiler_renameProperty('items', this)}.${idx}`); }); } } @@ -248,9 +254,9 @@ let ArraySelectorMixin = dedupingMixin(superClass => { } __selectedIndexForItemIndex(idx) { - let selected = this.__dataLinkedPaths['items.' + idx]; + let selected = this.__dataLinkedPaths[`${JSCompiler_renameProperty('items', this)}.${idx}`]; if (selected) { - return parseInt(selected.slice('selected.'.length), 10); + return parseInt(selected.slice(`${JSCompiler_renameProperty('selected', this)}.`.length), 10); } } @@ -271,7 +277,7 @@ let ArraySelectorMixin = dedupingMixin(superClass => { } this.__updateLinks(); if (this.multi) { - this.splice('selected', sidx, 1); + this.splice(JSCompiler_renameProperty('selected', this), sidx, 1); } else { this.selected = this.selectedItem = null; } @@ -318,7 +324,7 @@ let ArraySelectorMixin = dedupingMixin(superClass => { this.__selectedMap.set(item, idx); this.__updateLinks(); if (this.multi) { - this.push('selected', item); + this.push(JSCompiler_renameProperty('selected', this), item); } else { this.selected = this.selectedItem = item; } diff --git a/lib/elements/dom-repeat.js b/lib/elements/dom-repeat.js index e4ca665939..74f12659d9 100644 --- a/lib/elements/dom-repeat.js +++ b/lib/elements/dom-repeat.js @@ -382,7 +382,7 @@ export class DomRepeat extends domRepeatBase { if (prop == this.as) { this.items[idx] = value; } - let path = translate(this.as, 'items.' + idx, prop); + let path = translate(this.as, `${JSCompiler_renameProperty('items', this)}.${idx}`, prop); this.notifyPath(path, value); } } diff --git a/lib/legacy/class.js b/lib/legacy/class.js index bb4326e370..a35c0c5c84 100644 --- a/lib/legacy/class.js +++ b/lib/legacy/class.js @@ -155,6 +155,26 @@ function flattenBehaviors(behaviors, list, exclude) { return list; } +/** + * Copies property descriptors from source to target, overwriting all fields + * of any previous descriptor for a property *except* for `value`, which is + * merged in from the target if it does not exist on the source. + * + * @param {*} target Target properties object + * @param {*} source Source properties object + */ +function mergeProperties(target, source) { + for (const p in source) { + const targetInfo = target[p]; + const sourceInfo = source[p]; + if (!('value' in sourceInfo) && targetInfo && ('value' in targetInfo)) { + target[p] = Object.assign({value: targetInfo.value}, sourceInfo); + } else { + target[p] = sourceInfo; + } + } +} + /* Note about construction and extension of legacy classes. [Changed in Q4 2018 to optimize performance.] @@ -227,10 +247,10 @@ function GenerateClassFromInfo(info, Base, behaviors) { const properties = {}; if (behaviorList) { for (let i=0; i < behaviorList.length; i++) { - Object.assign(properties, behaviorList[i].properties); + mergeProperties(properties, behaviorList[i].properties); } } - Object.assign(properties, info.properties); + mergeProperties(properties, info.properties); return properties; } diff --git a/lib/legacy/polymer.dom.js b/lib/legacy/polymer.dom.js index 442d428147..6d7f0eb667 100644 --- a/lib/legacy/polymer.dom.js +++ b/lib/legacy/polymer.dom.js @@ -395,6 +395,12 @@ if (window['ShadyDOM'] && window['ShadyDOM']['inUse'] && window['ShadyDOM']['noP } }); + // Note, `classList` is here only for legacy compatibility since it does not + // trigger distribution in v1 Shadow DOM. + forwardReadOnlyProperties(Wrapper.prototype, [ + 'classList' + ]); + DomApiImpl = Wrapper; Object.defineProperties(EventApi.prototype, { @@ -416,12 +422,17 @@ if (window['ShadyDOM'] && window['ShadyDOM']['inUse'] && window['ShadyDOM']['noP } else { + // Methods that can provoke distribution or must return the logical, not + // composed tree. forwardMethods(DomApiNative.prototype, [ 'cloneNode', 'appendChild', 'insertBefore', 'removeChild', 'replaceChild', 'setAttribute', 'removeAttribute', 'querySelector', 'querySelectorAll' ]); + // Properties that should return the logical, not composed tree. Note, `classList` + // is here only for legacy compatibility since it does not trigger distribution + // in v1 Shadow DOM. forwardReadOnlyProperties(DomApiNative.prototype, [ 'parentNode', 'firstChild', 'lastChild', 'nextSibling', 'previousSibling', 'firstElementChild', diff --git a/lib/mixins/element-mixin.js b/lib/mixins/element-mixin.js index 8ba0fab5a4..e2e6eebc5e 100644 --- a/lib/mixins/element-mixin.js +++ b/lib/mixins/element-mixin.js @@ -24,7 +24,7 @@ import { wrap } from '../utils/wrap.js'; * Current Polymer version in Semver notation. * @type {string} Semver notation of the current version of Polymer. */ -export const version = '3.0.5'; +export const version = '3.2.0'; const builtCSS = window.ShadyCSS && window.ShadyCSS['cssBuild']; diff --git a/lib/mixins/template-stamp.js b/lib/mixins/template-stamp.js index 167fbe530a..3ae0607e01 100644 --- a/lib/mixins/template-stamp.js +++ b/lib/mixins/template-stamp.js @@ -11,9 +11,6 @@ import '../utils/boot.js'; import { dedupingMixin } from '../utils/mixin.js'; -const walker = document.createTreeWalker(document, NodeFilter.SHOW_ALL, - null, false); - // 1.x backwards-compatible auto-wrapper for template type extensions // This is a clear layering violation and gives favored-nation status to // dom-if and dom-repeat templates. This is a conceit we're choosing to keep @@ -48,8 +45,7 @@ function findTemplateNode(root, nodeInfo) { if (parent) { // note: marginally faster than indexing via childNodes // (http://jsperf.com/childnodes-lookup) - walker.currentNode = parent; - for (let n=walker.firstChild(), i=0; n; n=walker.nextSibling()) { + for (let n=parent.firstChild, i=0; n; n=n.nextSibling) { if (nodeInfo.parentIndex === i++) { return n; } @@ -238,8 +234,7 @@ export const TemplateStamp = dedupingMixin( // For ShadyDom optimization, indicating there is an insertion point templateInfo.hasInsertionPoint = true; } - walker.currentNode = element; - if (walker.firstChild()) { + if (element.firstChild) { noted = this._parseTemplateChildNodes(element, templateInfo, nodeInfo) || noted; } if (element.hasAttributes && element.hasAttributes()) { @@ -265,8 +260,7 @@ export const TemplateStamp = dedupingMixin( if (root.localName === 'script' || root.localName === 'style') { return; } - walker.currentNode = root; - for (let node=walker.firstChild(), parentIndex=0, next; node; node=next) { + for (let node=root.firstChild, parentIndex=0, next; node; node=next) { // Wrap templates if (node.localName == 'template') { node = wrapTemplateExtension(node); @@ -275,13 +269,12 @@ export const TemplateStamp = dedupingMixin( // text nodes to be inexplicably split =( // note that root.normalize() should work but does not so we do this // manually. - walker.currentNode = node; - next = walker.nextSibling(); + next = node.nextSibling; if (node.nodeType === Node.TEXT_NODE) { let /** Node */ n = next; while (n && (n.nodeType === Node.TEXT_NODE)) { node.textContent += n.textContent; - next = walker.nextSibling(); + next = n.nextSibling; root.removeChild(n); n = next; } @@ -296,8 +289,7 @@ export const TemplateStamp = dedupingMixin( childInfo.infoIndex = templateInfo.nodeInfoList.push(/** @type {!NodeInfo} */(childInfo)) - 1; } // Increment if not removed - walker.currentNode = node; - if (walker.parentNode()) { + if (node.parentNode) { parentIndex++; } } diff --git a/lib/utils/debounce.js b/lib/utils/debounce.js index eac7dd0ee8..cf4311abbc 100644 --- a/lib/utils/debounce.js +++ b/lib/utils/debounce.js @@ -35,6 +35,7 @@ export class Debouncer { this._callback = callback; this._timer = this._asyncModule.run(() => { this._timer = null; + debouncerQueue.delete(this); this._callback(); }); } @@ -44,6 +45,21 @@ export class Debouncer { * @return {void} */ cancel() { + if (this.isActive()) { + this._cancelAsync(); + // Canceling a debouncer removes its spot from the flush queue, + // so if a debouncer is manually canceled and re-debounced, it + // will reset its flush order (this is a very minor difference from 1.x) + // Re-debouncing via the `debounce` API retains the 1.x FIFO flush order + debouncerQueue.delete(this); + } + } + /** + * Cancels a debouncer's async callback. + * + * @return {void} + */ + _cancelAsync() { if (this.isActive()) { this._asyncModule.cancel(/** @type {number} */(this._timer)); this._timer = null; @@ -104,7 +120,9 @@ export class Debouncer { */ static debounce(debouncer, asyncModule, callback) { if (debouncer instanceof Debouncer) { - debouncer.cancel(); + // Cancel the async callback, but leave in debouncerQueue if it was + // enqueued, to maintain 1.x flush order + debouncer._cancelAsync(); } else { debouncer = new Debouncer(); } @@ -112,3 +130,36 @@ export class Debouncer { return debouncer; } } + +let debouncerQueue = new Set(); + +/** + * Adds a `Debouncer` to a list of globally flushable tasks. + * + * @param {!Debouncer} debouncer Debouncer to enqueue + * @return {void} + */ +export const enqueueDebouncer = function(debouncer) { + debouncerQueue.add(debouncer); +}; + +/** + * Flushes any enqueued debouncers + * + * @return {boolean} Returns whether any debouncers were flushed + */ +export const flushDebouncers = function() { + const didFlush = Boolean(debouncerQueue.size); + // If new debouncers are added while flushing, Set.forEach will ensure + // newly added ones are also flushed + debouncerQueue.forEach(debouncer => { + try { + debouncer.flush(); + } catch(e) { + setTimeout(() => { + throw e; + }); + } + }); + return didFlush; +}; \ No newline at end of file diff --git a/lib/utils/flush.js b/lib/utils/flush.js index 065a58a901..a719047010 100644 --- a/lib/utils/flush.js +++ b/lib/utils/flush.js @@ -11,32 +11,8 @@ import './boot.js'; /* eslint-disable no-unused-vars */ import { Debouncer } from '../utils/debounce.js'; // used in type annotations /* eslint-enable no-unused-vars */ - -let debouncerQueue = []; - -/** - * Adds a `Debouncer` to a list of globally flushable tasks. - * - * @param {!Debouncer} debouncer Debouncer to enqueue - * @return {void} - */ -export const enqueueDebouncer = function(debouncer) { - debouncerQueue.push(debouncer); -}; - -function flushDebouncers() { - const didFlush = Boolean(debouncerQueue.length); - while (debouncerQueue.length) { - try { - debouncerQueue.shift().flush(); - } catch(e) { - setTimeout(() => { - throw e; - }); - } - } - return didFlush; -} +import { flushDebouncers } from '../utils/debounce.js'; // used in type annotations +export { enqueueDebouncer } from '../utils/debounce.js'; // used in type annotations /** * Forces several classes of asynchronously queued tasks to flush: diff --git a/lib/utils/gestures.js b/lib/utils/gestures.js index 54b2173717..f5f4bfd680 100644 --- a/lib/utils/gestures.js +++ b/lib/utils/gestures.js @@ -178,9 +178,10 @@ let mouseCanceller = function(mouseEvent) { for (let i = 0; i < path.length; i++) { if (path[i].nodeType === Node.ELEMENT_NODE) { if (path[i].localName === 'label') { - clickedLabels.push(path[i]); - } else if (canBeLabelled(path[i])) { - let ownerLabels = matchingLabels(path[i]); + clickedLabels.push(/** @type {!HTMLLabelElement} */ (path[i])); + } else if (canBeLabelled(/** @type {!HTMLElement} */ (path[i]))) { + let ownerLabels = + matchingLabels(/** @type {!HTMLElement} */ (path[i])); // check if one of the clicked labels is labelling this element for (let j = 0; j < ownerLabels.length; j++) { clickFromLabel = clickFromLabel || clickedLabels.indexOf(ownerLabels[j]) > -1; @@ -386,7 +387,7 @@ export function deepTargetFind(x, y) { * @return {EventTarget} Returns the event target. */ function _findOriginalTarget(ev) { - const path = getComposedPath(ev); + const path = getComposedPath(/** @type {?Event} */ (ev)); // It shouldn't be, but sometimes path is empty (window on Safari). return path.length > 0 ? path[0] : ev.target; } diff --git a/package-lock.json b/package-lock.json index 18b8846d8b..1ac0035ba0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "@polymer/polymer", - "version": "3.1.0", + "version": "3.2.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 1ccba888ec..6b89e07f24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@polymer/polymer", - "version": "3.1.0", + "version": "3.2.0", "description": "The Polymer library makes it easy to create your own web components. Give your element some markup and properties, and then use it on a site. Polymer provides features like dynamic templates and data binding to reduce the amount of boilerplate you need to write", "main": "polymer-element.js", "directories": { diff --git a/test/unit/behaviors.html b/test/unit/behaviors.html index 33bdc92497..6af4b6512b 100644 --- a/test/unit/behaviors.html +++ b/test/unit/behaviors.html @@ -402,15 +402,17 @@ behaviors: [ { properties: { - foo: { value: true }, - bar: { value: true} + foo: { value: 'a' }, + bar: { value: 'a' }, + ziz: { value: 'a' } } }, { properties: { - foo: { value: true }, + foo: { value: 'b' }, bar: String, - zot: {value: true} + zot: { value: 'b' }, + ziz: { value: 'b' } } }, @@ -418,7 +420,8 @@ properties: { foo: String, - zot: String + zot: String, + ziz: { value: 'c' } } }); @@ -587,9 +590,10 @@ test('behavior default values can be overridden', function() { const el = fixture('override-default-value'); - assert.notOk(el.foo); - assert.notOk(el.bar); - assert.notOk(el.zot); + assert.equal(el.foo, 'b'); + assert.equal(el.bar, 'a'); + assert.equal(el.zot, 'b'); + assert.equal(el.ziz, 'c'); }); test('readOnly not applied when property was previously observed', function() { diff --git a/test/unit/debounce.html b/test/unit/debounce.html index ece33b94a4..a5a97d599c 100644 --- a/test/unit/debounce.html +++ b/test/unit/debounce.html @@ -33,8 +33,89 @@ import { Polymer } from '../../polymer-legacy.js'; import { Debouncer } from '../../lib/utils/debounce.js'; import { microTask, timeOut, animationFrame, idlePeriod } from '../../lib/utils/async.js'; +import { enqueueDebouncer, flush } from '../../lib/utils/flush.js'; Polymer({is: 'x-basic'}); +suite('enqueueDebouncer & flush', function() { + + // NOTE: This is a regression test; the bug it fixed only occurred if the + // debouncer was flushed before any microtasks run, hence it should be + // first in this file + test('re-enqueue canceled debouncer', function() { + const cb = sinon.spy(); + let db; + db = Debouncer.debounce(null, microTask, cb); + enqueueDebouncer(db); + db.cancel(); + assert.equal(db.isActive(), false); + assert.equal(cb.callCount, 0); + db = Debouncer.debounce(db, microTask, cb); + enqueueDebouncer(db); + flush(); + assert.isTrue(cb.calledOnce); + }); + + test('flushDebouncers from enqueued debouncer', function(done) { + const cb = sinon.spy(() => flush()); + let db = Debouncer.debounce(null, microTask, cb); + enqueueDebouncer(db); + setTimeout(() => { + assert.isTrue(cb.calledOnce); + done(); + }); + }); + + const testEnqueue = (shouldFlush, done) => { + const actualOrder = []; + const enqueue = (type, {db, cb} = {}) => { + cb = cb || (() => actualOrder.push(cb)); + db = Debouncer.debounce(db, type, cb); + enqueueDebouncer(db); + return {db, cb}; + }; + const db1 = enqueue(microTask); + const db2 = enqueue(microTask); + const db3 = enqueue(timeOut); + const db4 = enqueue(microTask); + enqueue(microTask, db2); + enqueue(microTask, db1); + if (shouldFlush) { + flush(); + assert.deepEqual(actualOrder, [db1.cb, db2.cb, db3.cb, db4.cb]); + done(); + } else { + timeOut.run(() => { + assert.deepEqual(actualOrder, [db4.cb, db2.cb, db1.cb, db3.cb]); + done(); + }); + } + }; + + test('non-flushed', function(done) { + testEnqueue(false, done); + }); + + test('flushed', function(done) { + testEnqueue(true, done); + }); + + test('reentrant flush', function() { + const cb2 = sinon.spy(); + let db2; + const cb1 = sinon.spy(() => { + flush(); + db2 = Debouncer.debounce(null, microTask, cb2); + enqueueDebouncer(db2); + }); + const db1 = Debouncer.debounce(null, microTask, cb1); + enqueueDebouncer(db1); + flush(); + assert.isTrue(cb1.calledOnce); + assert.isTrue(cb2.calledOnce); + }); + +}); + suite('debounce', function() { var element; @@ -209,6 +290,7 @@ }); }); + }); diff --git a/test/unit/polymer-dom-nopatch.html b/test/unit/polymer-dom-nopatch.html index 54017de804..6a7e697110 100644 --- a/test/unit/polymer-dom-nopatch.html +++ b/test/unit/polymer-dom-nopatch.html @@ -355,6 +355,144 @@ assert.equal(useNativeCSSProperties, Boolean(!window.ShadyCSS || window.ShadyCSS.nativeCss)); }); +}); + +suite('forwarded native api', function() { + + let el; + + setup(function() { + el = document.createElement('x-container-slot'); + document.body.appendChild(el); + }); + + teardown(function() { + document.body.removeChild(el); + }); + + test('accessors are available', function() { + const d = dom(el); + assert.isDefined(d.parentNode); + assert.isDefined(d.firstChild); + assert.isDefined(d.lastChild); + assert.isDefined(d.nextSibling); + assert.isDefined(d.previousSibling); + assert.isDefined(d.firstElementChild); + assert.isDefined(d.lastElementChild); + assert.isDefined(d.nextElementSibling); + assert.isDefined(d.previousElementSibling); + assert.isDefined(d.childNodes); + assert.isDefined(d.children); + assert.isDefined(d.classList); + assert.isDefined(d.textContent); + assert.isDefined(d.innerHTML); + }); + + test('cloneNode', function() { + const clone = dom(el).cloneNode(el); + assert.ok(clone); + assert.equal(clone.localName, 'x-container-slot'); + }); + + test('appendChild', function() { + const d1 = document.createElement('div'); + dom(el).appendChild(d1); + assert.equal(dom(el).firstChild, d1); + assert.equal(dom(d1).parentNode, el); + }); + + test('insertBefore', function() { + const d1 = document.createElement('div'); + const d2 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).insertBefore(d2, d1); + assert.equal(dom(d2).nextSibling, d1); + }); + + test('removeChild', function() { + const d1 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).removeChild(d1); + assert.equal(dom(d1).parentNode, null); + }); + + test('replaceChild', function() { + const d1 = document.createElement('div'); + const d2 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).replaceChild(d2, d1); + assert.equal(dom(d1).parentNode, null); + assert.equal(dom(el).firstChild, d2); + assert.equal(dom(d2).parentNode, el); + }); + + test('replaceChild', function() { + dom(el).setAttribute('foo', 'foo'); + assert.equal(el.getAttribute('foo'), 'foo'); + }); + + test('replaceChild', function() { + dom(el).setAttribute('foo', 'foo'); + dom(el).removeAttribute('foo'); + assert.isFalse(el.hasAttribute('foo')); + }); + + test('querySelector', function() { + const d1 = document.createElement('div'); + dom(el).appendChild(d1); + const query = dom(el).querySelector('div'); + assert.equal(query, d1); + }); + + test('querySelectorAll', function() { + const d1 = document.createElement('div'); + const d2 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).appendChild(d2); + const query = dom(el).querySelectorAll('div'); + assert.equal(query[0], d1); + assert.equal(query[1], d2); + assert.equal(query.length, 2); + }); + + test('tree accessors', function() { + const d1 = document.createElement('div'); + const d2 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).appendChild(d2); + const pel = dom(el); + const pd1 = dom(d1); + const pd2 = dom(d2); + assert.equal(pd1.parentNode, el); + assert.equal(pel.firstChild, d1); + assert.equal(pel.lastChild, d2); + assert.equal(pel.firstElementChild, d1); + assert.equal(pel.lastElementChild, d2); + assert.equal(pd1.nextSibling, d2); + assert.equal(pd2.previousSibling, d1); + assert.equal(pd1.nextElementSibling, d2); + assert.equal(pd2.previousElementSibling, d1); + assert.equal(pel.childNodes[0], d1); + assert.equal(pel.childNodes[1], d2); + assert.equal(pel.children[0], d1); + assert.equal(pel.children[1], d2); + }); + + test('innerHTML', function() { + dom(el).innerHTML = '
'; + assert.equal(dom(el).firstChild.localName, 'div'); + }); + + test('textContent', function() { + dom(el).innerHTML = 'hi'; + assert.equal(dom(dom(el).firstChild).textContent, 'hi'); + }); + + test('classList', function() { + dom(el).classList.add('foo'); + assert.equal(el.className, 'foo'); + }); + }); diff --git a/test/unit/polymer-dom.html b/test/unit/polymer-dom.html index 8bea7e1c1f..30cc025654 100644 --- a/test/unit/polymer-dom.html +++ b/test/unit/polymer-dom.html @@ -342,6 +342,144 @@ assert.equal(useNativeCSSProperties, Boolean(!window.ShadyCSS || window.ShadyCSS.nativeCss)); }); +}); + +suite('forwarded native api', function() { + + let el; + + setup(function() { + el = document.createElement('x-container-slot'); + document.body.appendChild(el); + }); + + teardown(function() { + document.body.removeChild(el); + }); + + test('accessors are available', function() { + const d = dom(el); + assert.isDefined(d.parentNode); + assert.isDefined(d.firstChild); + assert.isDefined(d.lastChild); + assert.isDefined(d.nextSibling); + assert.isDefined(d.previousSibling); + assert.isDefined(d.firstElementChild); + assert.isDefined(d.lastElementChild); + assert.isDefined(d.nextElementSibling); + assert.isDefined(d.previousElementSibling); + assert.isDefined(d.childNodes); + assert.isDefined(d.children); + assert.isDefined(d.classList); + assert.isDefined(d.textContent); + assert.isDefined(d.innerHTML); + }); + + test('cloneNode', function() { + const clone = dom(el).cloneNode(el); + assert.ok(clone); + assert.equal(clone.localName, 'x-container-slot'); + }); + + test('appendChild', function() { + const d1 = document.createElement('div'); + dom(el).appendChild(d1); + assert.equal(dom(el).firstChild, d1); + assert.equal(dom(d1).parentNode, el); + }); + + test('insertBefore', function() { + const d1 = document.createElement('div'); + const d2 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).insertBefore(d2, d1); + assert.equal(dom(d2).nextSibling, d1); + }); + + test('removeChild', function() { + const d1 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).removeChild(d1); + assert.equal(dom(d1).parentNode, null); + }); + + test('replaceChild', function() { + const d1 = document.createElement('div'); + const d2 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).replaceChild(d2, d1); + assert.equal(dom(d1).parentNode, null); + assert.equal(dom(el).firstChild, d2); + assert.equal(dom(d2).parentNode, el); + }); + + test('replaceChild', function() { + dom(el).setAttribute('foo', 'foo'); + assert.equal(el.getAttribute('foo'), 'foo'); + }); + + test('replaceChild', function() { + dom(el).setAttribute('foo', 'foo'); + dom(el).removeAttribute('foo'); + assert.isFalse(el.hasAttribute('foo')); + }); + + test('querySelector', function() { + const d1 = document.createElement('div'); + dom(el).appendChild(d1); + const query = dom(el).querySelector('div'); + assert.equal(query, d1); + }); + + test('querySelectorAll', function() { + const d1 = document.createElement('div'); + const d2 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).appendChild(d2); + const query = dom(el).querySelectorAll('div'); + assert.equal(query[0], d1); + assert.equal(query[1], d2); + assert.equal(query.length, 2); + }); + + test('tree accessors', function() { + const d1 = document.createElement('div'); + const d2 = document.createElement('div'); + dom(el).appendChild(d1); + dom(el).appendChild(d2); + const pel = dom(el); + const pd1 = dom(d1); + const pd2 = dom(d2); + assert.equal(pd1.parentNode, el); + assert.equal(pel.firstChild, d1); + assert.equal(pel.lastChild, d2); + assert.equal(pel.firstElementChild, d1); + assert.equal(pel.lastElementChild, d2); + assert.equal(pd1.nextSibling, d2); + assert.equal(pd2.previousSibling, d1); + assert.equal(pd1.nextElementSibling, d2); + assert.equal(pd2.previousElementSibling, d1); + assert.equal(pel.childNodes[0], d1); + assert.equal(pel.childNodes[1], d2); + assert.equal(pel.children[0], d1); + assert.equal(pel.children[1], d2); + }); + + test('innerHTML', function() { + dom(el).innerHTML = '
'; + assert.equal(dom(el).firstChild.localName, 'div'); + }); + + test('textContent', function() { + dom(el).innerHTML = 'hi'; + assert.equal(dom(dom(el).firstChild).textContent, 'hi'); + }); + + test('classList', function() { + dom(el).classList.add('foo'); + assert.equal(el.className, 'foo'); + }); + });