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/utils/debounce.js b/lib/utils/debounce.js index 6cafd8cb47..6fb246052b 100644 --- a/lib/utils/debounce.js +++ b/lib/utils/debounce.js @@ -47,7 +47,6 @@ export class Debouncer { cancel() { if (this.isActive()) { this._cancelAsync(); - this._timer = null; // 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) @@ -61,7 +60,10 @@ export class Debouncer { * @return {void} */ _cancelAsync() { - this._asyncModule.cancel(/** @type {number} */(this._timer)); + if (this.isActive()) { + this._asyncModule.cancel(/** @type {number} */(this._timer)); + this._timer = null; + } } /** * Flushes an active debouncer and returns a reference to itself. @@ -144,7 +146,7 @@ export const enqueueDebouncer = function(debouncer) { /** * Flushes any enqueued debouncers * - * @return {void} + * @return {boolean} Returns whether any debouncers were flushed */ export const flushDebouncers = function() { const didFlush = Boolean(debouncerQueue.size); 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/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 9f528b6ac6..5d20439147 100644 --- a/test/unit/debounce.html +++ b/test/unit/debounce.html @@ -36,6 +36,61 @@ 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 occured 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); + }); + + 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); + }); + +}); + suite('debounce', function() { var element; @@ -211,43 +266,6 @@ }); - suite('enqueueDebouncer & flush', function() { - - 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); - }); - - }); });