UNPKG

@polymer/polymer

Version:

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

1,332 lines (1,133 loc) 68.8 kB
<!doctype html> <!-- @license Copyright (c) 2017 The Polymer Project Authors. All rights reserved. This code may only be used under the BSD style license found at http://polymer.github.io/LICENSE.txt The complete set of authors may be found at http://polymer.github.io/AUTHORS.txt The complete set of contributors may be found at http://polymer.github.io/CONTRIBUTORS.txt Code distributed by Google as part of the polymer project is also subject to an additional IP rights grant found at http://polymer.github.io/PATENTS.txt --> <html> <head> <meta charset="utf-8"> <script src="../../../webcomponentsjs/webcomponents-lite.js"></script> <script src="../../../web-component-tester/browser.js"></script> <link rel="import" href="../../polymer.html"> <link rel="import" href="property-effects-elements.html"> <body> <dom-repeat id="class-repeat" items='["class1", "class2"]'> <template> <div class$=[[item]] clazz$="[[item]]">[[item]]</div> </template> </dom-repeat> <script> suite('single-element binding effects', function() { var el; setup(function() { el = document.createElement('x-basic'); document.body.appendChild(el); }); teardown(function() { document.body.removeChild(el); }); test('undefined input value', function() { assert.equal(el.$.boundInput.value, '', 'undefined input value not blank'); el.text = 'this is a test'; assert.equal(el.$.boundInput.value, 'this is a test', 'binding to input didn\'t go'); el.text = undefined; assert.equal(el.$.boundInput.value, '', 'undefined input value not blank'); }); test('undefined textarea value', function() { assert.equal(el.$.boundTextArea.value, '', 'undefined textarea value not blank'); el.text = 'this is a test'; assert.equal(el.$.boundTextArea.value, 'this is a test', 'binding to textarea didn\'t go'); el.text = undefined; assert.equal(el.$.boundTextArea.value, '', 'undefined textarea value not blank'); }); test('id is bindable', function() { assert.equal(el.root.querySelector('span[idtest]').id, 'span', 'id bound to <span> not found'); }); test('textContent binding updates', function() { el.text = 'this is a test'; assert.equal(el.$.boundText.textContent, 'this is a test', 'Value not propagated to textContent'); }); test('textContent binding to undefined is empty string', function() { el.text = 'this is a test'; el.text = undefined; assert.equal(el.$.boundText.textContent, '', 'undefined bound to textContent should be empty string'); }); test('textContent binding to null is empty string', function() { el.text = null; assert.equal(el.$.boundText.textContent, '', 'null bound to textContent should be empty string'); }); test('textContent binding to zero is empty correct', function() { el.text = 0; assert.equal(el.$.boundText.textContent, '0', 'zero bound to textContent should be empty string'); }); test('camel-case binding updates', function() { el.value = 41; assert.equal(el.$.boundChild.camelCase, 41, 'Value not propagated to camelCase property'); }); test('annotation binding updates', function() { el.value = 42; assert.equal(el.$.boundChild.value, 42, 'Value not propagated to bound child'); }); test('negated annotation binding updates', function() { el.bool = true; assert.equal(el.$.boundChild.negvalue, false, 'Value not negated'); el.bool = false; assert.equal(el.$.boundChild.negvalue, true, 'Value not negated'); }); test('observer called', function() { assert.equal(el.observerCounts.valueChanged, 1, 'observer not called once for default value at configure'); el.value = 43; assert.equal(el.observerCounts.valueChanged, 2, 'observer not called after property change'); }); test('observer called only once for null', function() { el.clearObserverCounts(); assert.equal(el.observerCounts.valueChanged, 0); el.value = {}; assert.equal(el.observerCounts.valueChanged, 1); el.value = null; assert.equal(el.observerCounts.valueChanged, 2); el.value = null; assert.equal(el.observerCounts.valueChanged, 2); el.value = {}; assert.equal(el.observerCounts.valueChanged, 3); el.value = null; assert.equal(el.observerCounts.valueChanged, 4); el.value = null; assert.equal(el.observerCounts.valueChanged, 4); }); test('computed value updates', function() { el.value = 44; assert.equal(el.computedvalue, 45, 'Computed value not correct'); assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child'); }); test('computed value readOnly from imperative set', function() { el.value = 44; // Should have no effect el.computedvalue = 99; assert.equal(el.computedvalue, 45, 'Computed value not correct'); assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child'); }); test('computed values to same method updates', function() { el.value = 44; el.valuetwo = 144; assert.equal(el.computedvalue, 45, 'Computed value not correct'); assert.equal(el.computedvaluetwo, 145, 'Computed value not correct'); assert.equal(el.$.boundChild.computedvalue, 45, 'Computed value not propagated to bound child'); }); test('computed value using computing fn from behavior', function() { el.value = 44; assert.equal(el.$.boundChild.computedFromBehavior, 'computed:44'); el.computeFromBehavior = function(value) { return 'new:' + value; }; assert.equal(el.$.boundChild.computedFromBehavior, 'new:44'); }); test('notification sent', function() { var notified = 0; el.addEventListener('notifyingvalue-changed', function(e) { assert.equal(e.detail.value, 45); notified++; }); el.addEventListener('camel-notifying-value-changed', function(e) { assert.equal(e.detail.value, 45); notified++; }); el.notifyingvalue = 45; el.camelNotifyingValue = 45; assert.equal(notified, 2, 'Notification events not sent'); }); test('computed observer called', function() { el.clearObserverCounts(); el.value = 46; assert.equal(el.observerCounts.computedvalueChanged, 1, 'observer not called'); }); test('NaN does not loop observers', function() { el.clearObserverCounts(); el.addEventListener('notifierwithoutcomputing-changed', function() { if (el.observerCounts.notifierWithoutComputingChanged >= 3) { throw new Error('infinite loop!'); } }); el.notifierWithoutComputing = NaN; assert.equal(el.observerCounts.notifierWithoutComputingChanged, 1, 'NaN was not handled as expected'); }); test('computed notification sent', function() { var notified = 0; el.addEventListener('computednotifyingvalue-changed', function(e) { assert.equal(e.detail.value, 49); notified++; }); el.notifyingvalue = 47; assert.equal(notified, 1, 'Notification event not sent'); }); test('computed property with multiple dependencies', function() { var notified = 0; el.addEventListener('computed-from-multiple-values-changed', function() { notified++; }); el.sum1 = 10; el.sum2 = 20; el.divide = 2; assert.equal(el.computedFromMultipleValues, 15, 'Computed value wrong'); assert.equal(notified, 1, 'Notification event not sent'); assert.equal(el.observerCounts.computedFromMultipleValuesChanged, 1, 'observer not called'); }); test('computed annotation with literals', function() { el.bool = true; assert.equal(el.$.boundChild.computedFromMixedLiterals, '3foo', 'Wrong result from mixed literal arg computation'); assert.equal(el.$.boundChild.computedFromPureLiterals, '3foo', 'Wrong result from pure literal arg computation'); assert.equal(el.$.boundChild.computedFromTrickyFunction, '3foo', 'Wrong result from tricky function with pure literal arg computation'); assert.equal(el.$.boundChild.computedFromTrickyLiterals, '3tricky,\'zot\'', 'Wrong result from tricky literal arg computation'); assert.equal(el.$.boundChild.computedFromTrickyLiterals2, '3tricky,\'zot\'', 'Wrong result from tricky literal arg computation'); assert.equal(el.$.boundChild.computedFromTrickyLiterals3, '3tricky,\'zot\'', 'Wrong result from tricky literal arg computation'); assert.equal(el.$.computedContent.textContent, '3tricky,\'zot\'', 'Wrong textContent from tricky literal arg computation'); assert.equal(el.$.computedContent2.textContent, '(3', 'Wrong textContent from tricky literal arg computation'); }); test('computed annotation with no args', function() { assert.equal(el.$.boundChild.computedFromNoArgs, 'no args!', 'Wrong content when computed has no args'); }); test('no read-only observer called with assignment', function() { el.readonlyvalue = 46; assert.equal(el.observerCounts.readonlyvalueChanged, 0, 'observer should not be called for readOnly prop assignment'); }); test('read-only observer called with _setReadonlyvalue', function() { el._setReadonlyvalue(46); assert.equal(el.observerCounts.readonlyvalueChanged, 1, 'observer should be called'); assert(el.readonlyvalue == 46, 'value should be changed but was not'); }); test('no read-only notification sent with assignment', function() { var notified = 0; el.addEventListener('readonlyvalue-changed', function() { notified++; }); el.readonlyvalue = 47; assert.equal(notified, 0, 'Notification should not be called for readOnly prop assignment'); }); test('read-only notification sent with _setReadonlyvalue', function() { var notified = 0; el.addEventListener('readonlyvalue-changed', function(e) { assert.equal(e.detail.value, 47); notified++; }); el._setReadonlyvalue(47); assert.equal(notified, 1, 'Notification event not sent'); }); test('multiple dependency observer called once', function() { el.setProperties({ dep1: true, dep2: {}, dep3: 42 }); assert.equal(el.observerCounts.multipleDepChangeHandler, 1, 'observer not called once'); }); test('setProperties does not set readOnly by default', function() { assert.equal(el.observerCounts.valueChanged, 1); assert.equal(el.observerCounts.readonlyvalueChanged, 0); el.setProperties({ value: 'shouldChange', nofx: 'shouldChange', readonlyvalue: 'shouldNotChange' }); assert.equal(el.value, 'shouldChange'); assert.equal(el.nofx, 'shouldChange'); assert.equal(el.readonlyvalue, undefined); assert.equal(el.observerCounts.valueChanged, 2); assert.equal(el.observerCounts.readonlyvalueChanged, 0); }); test('setProperties sets readOnly using `setPrivate` arg', function() { assert.equal(el.observerCounts.valueChanged, 1); assert.equal(el.observerCounts.readonlyvalueChanged, 0); el.setProperties({ value: 'shouldChange', nofx: 'shouldChange', readonlyvalue: 'shouldChange' }, true); assert.equal(el.value, 'shouldChange'); assert.equal(el.nofx, 'shouldChange'); assert.equal(el.readonlyvalue, 'shouldChange'); assert.equal(el.observerCounts.valueChanged, 2); assert.equal(el.observerCounts.readonlyvalueChanged, 1); }); test('annotated computed property', function() { el.value = 20; el.add = 40; el.divide = 3; assert.equal(el.$.boundChild.computedInline, 20, 'computedInline not correct'); assert.equal(el.$.boundChild.computedInline2, 20, 'computedInline2 not correct'); assert.equal(el.$.boundChild.computedInline3, 20, 'computedInline3 not correct'); assert.equal(el.$.boundChild.negComputedInline, false, 'negComputedInline not correct'); }); test('annotated computed attribute', function() { el.value = 20; el.add = 40; el.divide = 3; assert.equal(el.$.boundChild.getAttribute('computedattribute'), 20, 'computed attribute not correct'); assert.equal(el.$.boundChild.getAttribute('computedattribute2'), 20, 'computed attribute not correct'); }); test('annotated style attribute binding', function() { el.boundStyle = 'padding: 37px;'; assert.equal(getComputedStyle(el.$.boundChild).paddingTop, '37px', 'style attribute binding not correct'); }); test('annotated dataset attribute binding', function() { if (el.$.boundChild.datast) { // IE10, sigh el.dataSetId = 'yeah'; assert.equal(el.$.boundChild.dataset.id, 'yeah', 'dataset.id dataset property not set correctly'); assert.equal(el.$.boundChild.getAttribute('data-id'), 'yeah', 'data-id attribute not set correctly'); } }); test('custom notification event to property', function() { el.$.boundChild.customEventValue = 42; el.fire('custom', null, {node: el.$.boundChild}); assert.equal(el.customEventValue, 42, 'custom bound property incorrect'); assert.equal(el.observerCounts.customEventValueChanged, 1, 'custom bound property observer not called'); }); test('custom notification bubbling event to property', function() { const child = document.createElement('div'); el.$.boundChild.appendChild(child); el.$.boundChild.customEventValue = 42; child.dispatchEvent(new Event('custom', {bubbles: true})); assert.equal(el.customEventValue, 42, 'custom bound property incorrect'); assert.equal(el.observerCounts.customEventValueChanged, 1, 'custom bound property observer not called'); }); test('custom notification event to path', function() { el.clearObserverCounts(); el.$.boundChild.customEventObjectValue = 84; el.$.boundChild.dispatchEvent(new Event('change')); assert.equal(el.customEventObject.value, 84, 'custom bound path incorrect'); assert.equal(el.observerCounts.customEventObjectValueChanged, 1, 'custom bound path observer not called'); }); test('computed property with negative number', function() { assert.equal(el.$.boundChild.computedNegativeNumber, -1); }); test('computed property with negative literal', function() { assert.equal(el.$.boundChild.computedNegativeLiteral, undefined); }); test('computed binding with wildcard', function() { el.a = 5; el.b = {value: 10}; assert.equal(el.$.boundChild.computedWildcard, 15); }); test('binding with dash', function() { el.objectWithDash = { 'binding-with-dash': 'yes' }; assert.equal(el.$.boundWithDash.textContent, 'yes'); }); test('class attribute without template scope not erased', function() { var el = document.querySelector('.class1'); assert.notEqual(el, null, 'class without template scope is undefined'); var el2 = document.querySelector('.class2'); assert.notEqual(el2, null, 'class without template scope is undefined'); }); test('property effect for native property (title)', function() { // NOTE: This will FAIL on browser on which `title` is an own property so // just let this pass. if (document.createElement('div').hasOwnProperty('title')) { this.skip(); } assert.isTrue(el.titleChanged.notCalled); el.title = 'a title'; assert.isTrue(el.titleChanged.calledOnce); assert.equal(el.titleChanged.firstCall.args[0], 'a title'); }); test('property effect added at instance time', function() { el.earlyBoundObserver = sinon.spy(); el.lateBoundObserver = sinon.spy(); el.earlyBound = 'early'; el._createPropertyObserver('earlyBound', 'earlyBoundObserver'); el._createPropertyObserver('lateBound', 'lateBoundObserver'); el._flushProperties(); el.lateBound = 'late'; assert.equal(el.earlyBound, 'early'); assert.equal(el.earlyBoundObserver.callCount, 1); assert.equal(el.earlyBoundObserver.firstCall.args[0], 'early'); assert.equal(el.lateBound, 'late'); assert.equal(el.lateBoundObserver.callCount, 1); assert.equal(el.lateBoundObserver.firstCall.args[0], 'late'); }); test('does not parse bindings inside <script>', function() { assert.include(el.$.scriptWithBinding.textContent, "{{binding}}"); }); test('does not parse bindings inside <style>', function() { var style = el.shadowRoot.querySelector('style'); // native shadow dom if (style) { assert.include(style.textContent, "[[binding]]"); // shady dom } else { assert.include(document.querySelector('[scope="x-basic"]').textContent, "[[binding]]"); } }); test('only calls dynamic functions once', function() { el.dynamicFn = function() { assert.isFalse(this._dynamicFnCalled); this._dynamicFnCalled = true; }; }); suite('observer inheritance', function() { setup(function() { el = document.createElement('sub-observer-element'); document.body.appendChild(el); }); test('does not invoke observer twice', function() { assert.equal(el.__observerCalled, 1); }); }); suite('automated attribute capitalization detection', function() { let el; setup(function() { el = document.createElement('svg-element'); document.body.appendChild(el); }); teardown(function() { document.body.removeChild(el); }); test('can handle capitalized HTML attribute', function() { assert.equal(el.$.svg.getAttribute('viewBox'), el.value); }); }); suite('can work with strict binding parser', function() { setup(function() { document.body.removeChild(el); el = document.createElement('x-basic-strict-binding-parser'); document.body.appendChild(el); }); test('binding with slash', function() { el.objectWithSlash = { 'binding/with/slash': 'yes' }; assert.equal(el.$.boundWithSlash.textContent, 'yes'); }); test('json should not be a binding', function() { assert.equal(el.$.jsonContent.textContent, '[["Jan", 31],["Feb", 28],["Mar", 31]]'); }); test('binding with non-English unicode', function() { el.objectWithNonEnglishUnicode = { 商品名: 'yes' }; assert.equal(el.$.nonEnglishUnicode.textContent, 'yes'); }); test('binding with booleans', function() { el.otherValue = 10; assert.equal(el.$.booleanTrue.textContent, 'foo(field, true): 10'); assert.equal(el.$.booleanFalse.textContent, 'foo(field, false): 20'); }); suite('equivalent behavior as regex', function() { // Loop over the suite "single-element binding effects" (parent of the parent of this suite) // And make sure that the tests there also pass on the binding-parser // // t.title is the name of the test and t.fn contains the test body this.parent.parent.tests.forEach(t => { test(t.title, t.fn); }); }); }); }); suite('computed bindings with dynamic functions', function() { var el; setup(function() { sinon.spy(console, 'warn'); }); teardown(function() { document.body.removeChild(el); console.warn.restore(); }); test('annotated computation with dynamic function', function() { el = document.createElement('x-bind-computed-property'); document.body.appendChild(el); assert.equal(el.$.check.textContent, 'translated: Hello World.'); el.translator = function(message) { return 'changed: ' + message; }; assert.equal(el.$.check.textContent, 'changed: Hello World.'); assert.equal(console.warn.callCount, 0); }); test('annotated computation / late resolved dynamic function', function() { el = document.createElement('x-bind-computed-property-late-translator'); document.body.appendChild(el); assert.equal(el.$.check.textContent.trim(), ''); el.translator = function(message) { return 'translated: ' + message; }; assert.equal(el.$.check.textContent, 'translated: Hello'); assert.equal(console.warn.callCount, 0); }); test('method observer with dynamic function', function() { Polymer({ is: 'x-method-observer-with-dynamic-function', properties: { translateMessage: { type: Function }, message: { type: String, value: 'Hello' } }, observers: ['translateMessage(message)'] }); el = document.createElement('x-method-observer-with-dynamic-function'); document.body.appendChild(el); el.translateMessage = sinon.spy(); assert.equal(el.translateMessage.callCount, 1); assert.equal(console.warn.callCount, 0); }); test('observer with dynamic function', function() { Polymer({ is: 'x-observer-with-dynamic-function', properties: { messageChanged: { type: Function }, message: { type: String, value: 'Hello', observer: 'messageChanged' } } }); el = document.createElement('x-observer-with-dynamic-function'); document.body.appendChild(el); el.messageChanged = sinon.spy(); assert.equal(el.messageChanged.callCount, 1); assert.equal(console.warn.callCount, 0); }); test('computed property with dynamic function', function() { Polymer({ is: 'x-computed-property-with-dynamic-function', properties: { computedValue: { computed: "translateMessage('Hello')" }, translateMessage: { type: Function } } }); el = document.createElement('x-computed-property-with-dynamic-function'); document.body.appendChild(el); assert.equal(el.computedValue, undefined); var called = 0; el.translateMessage = function(message) { called += 1; return 'translated: ' + message; }; assert.equal(called, 1); assert.equal(el.computedValue, 'translated: Hello'); assert.equal(console.warn.callCount, 0); }); test('ensure annotator can pass dynamic fn to parent props', function(done) { el = document.createElement('x-child-template-with-dynamic-fn'); document.body.appendChild(el); setTimeout(function() { var check = el.root.querySelector('p'); assert.equal(check.textContent, 'text'); assert.equal(console.warn.callCount, 0); done(); }); }); }); suite('2-way binding effects between elements', function() { var el; setup(function() { el = document.createElement('x-compose'); document.body.appendChild(el); }); teardown(function() { document.body.removeChild(el); }); test('binding to non-notifying property', function() { el.boundvalue = 42; assert.equal(el.$.basic1.value, 42, 'binding to child not updated'); el.$.basic1.value = 43; assert.equal(el.boundvalue, 42, 'binding to non-notifying property updated and should not have been'); }); test('observer for property bound to non-notifying property', function() { el.$.basic1.value = 44; assert.equal(el.observerCounts.boundvalueChanged, 0, 'observer for property bound to non-notifying property called and should not have been'); }); test('binding to non-notifying computed property', function() { el.boundcomputedvalue = 42; el.$.basic1.value = 43; assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been'); }); test('observer for property bound to non-notifying computed property', function() { el.$.basic1.value = 44; assert.equal(el.observerCounts.boundcomputedvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been'); }); test('computed value readOnly from downward binding', function() { el.$.basic3.value = 10; assert.equal(el.$.basic3.computedvalue, 11); // should have no effect el.value = 99; assert.equal(el.$.basic3.computedvalue, 11); }); test('computed value readOnly from upward notification', function() { assert.equal(el.computedValue, 30); // should have no effect el.$.basic3.notifyingvalue = 10; assert.equal(el.computedValue, 30); }); test('binding to notifying property', function() { el.boundnotifyingvalue = 42; assert.equal(el.$.basic1.notifyingvalue, 42, 'binding to child not updated'); assert.equal(el.$.basic1.camelNotifyingValue, 42, 'camel-case binding to child not updated'); el.$.basic1.notifyingvalue = 43; assert.equal(el.boundnotifyingvalue, 43, 'binding to notifying property not updated'); el.$.basic1.camelNotifyingValue = -43; assert.equal(el.boundnotifyingvalue, -43, 'camel-case binding to notifying property not updated'); }); test('binding to notifying property with default', function() { assert.equal(el.boundnotifyingvalueWithDefault, 99); }); test('observer for property bound to notifying property', function() { el.$.basic1.notifyingvalue = 45; assert.equal(el.observerCounts.boundnotifyingvalueChanged, 1, 'observer for property bound to notifying property not called'); }); test('binding to notifying computed property', function() { el.$.basic1.notifyingvalue = 43; assert.equal(el.boundcomputednotifyingvalue, 45, 'binding to notifying computed property not updated'); }); test('observer for property bound to notifying computed property', function() { el.$.basic1.notifyingvalue = 45; assert.equal(el.observerCounts.boundcomputednotifyingvalueChanged, 1, 'observer for property bound to non-notifying computed property not called'); }); test('no change for binding into read-only property', function() { el.$.basic1._setReadonlyvalue(45); el.$.basic1.clearObserverCounts(); el.boundreadonlyvalue = 46; assert.equal(el.$.basic1.observerCounts.readonlyvalueChanged, 0, 'observer for read-only property should not be called from change to bound value'); assert.equal(el.$.basic1.readonlyvalue, 45, 'read-only property should not change from change to bound value'); }); test('change for binding out of read-only property', function() { el.$.basic1._setReadonlyvalue(46); assert.equal(el.observerCounts.boundreadonlyvalueChanged, 1, 'observer for property bound to read-only property should be called from change to bound value'); assert.equal(el.boundreadonlyvalue, 46, 'property bound to read-only property should change from change to bound value'); }); test('negated binding update negates value for parent', function() { assert.equal(el.negatedValue, false); assert.equal(el.$.basic4.notifyingvalue, true); el.$.basic4.notifyingvalue = false; assert.equal(el.negatedValue, true); }); test('custom xxx-changed event notifies correctly', function() { assert.equal(el.boundCustomNotifyingValue, undefined); assert.equal(el.observerCounts.boundCustomNotifyingValueChanged, 0); el.$.basic1.fireCustomNotifyingEvent(); assert.equal(el.boundCustomNotifyingValue, 'changed!'); assert.equal(el.observerCounts.boundCustomNotifyingValueChanged, 1); }); }); suite('handling notifying evevnts', function() { test('handle notification event and set property with observer when connected', function() { var el = document.createElement('x-handle-notify-event'); document.body.appendChild(el); assert.ok(el.shadowRoot.querySelector('#before'), 'element not found before default notifying element'); assert.ok(el.shadowRoot.querySelector('#later'), 'element not found after default notifying element'); assert.isTrue(el.readySpy.calledOnce, 'ready called more than once'); assert.isTrue(el.handleNotify.calledOnce, 'listener called more than once'); assert.isTrue(el.propChanged.calledOnce, 'observer called more than once'); assert.isTrue(el.handleNotify.calledBefore(el.propChanged), 'observer called before event'); assert.isTrue(el.afterSettingProp.calledBefore(el.propChanged), 'accessor side effect processed during notifying event (before clients ready)'); assert.isTrue(el.propChanged.calledBefore(el.readySpy), 'observer called before ready'); document.body.removeChild(el); }); test('handle notification event and set property with observer when *not* connected and _enableProperties called', function() { var el = document.createElement('x-handle-notify-event'); el._enableProperties(); assert.ok(el.shadowRoot.querySelector('#before'), 'element not found before default notifying element'); assert.ok(el.shadowRoot.querySelector('#later'), 'element not found after default notifying element'); assert.isTrue(el.readySpy.calledOnce, 'ready called more than once'); assert.isTrue(el.handleNotify.calledOnce, 'listener called more than once'); assert.isTrue(el.propChanged.calledOnce, 'observer called more than once'); assert.isTrue(el.handleNotify.calledBefore(el.propChanged), 'observer called before event'); assert.isTrue(el.afterSettingProp.calledBefore(el.propChanged), 'accessor side effect processed during notifying event (before clients ready)'); assert.isTrue(el.propChanged.calledBefore(el.readySpy), 'observer called before ready'); }); }); suite('1-way binding effects between elements', function() { var el; setup(function() { el = document.createElement('x-compose'); document.body.appendChild(el); }); teardown(function() { document.body.removeChild(el); }); test('one-way binding to non-notifying property', function() { el.boundvalue = 42; assert.equal(el.$.basic1.value, 42, 'binding to child not updated'); el.$.basic2.value = 43; assert.equal(el.boundvalue, 42, 'binding to non-notifying property updated and should not have been'); }); test('observer for property one-way-bound to non-notifying property', function() { el.$.basic2.value = 44; assert.equal(el.observerCounts.boundvalueChanged, 0, 'observer for property one-way-bound to non-notifying property called and should not have been'); }); test('one-way binding to non-notifying computed property', function() { el.boundcomputedvalue = 42; el.$.basic2.value = 43; assert.equal(el.boundcomputedvalue, 42, 'binding to non-notifying computed property updated and should not have been'); }); test('observer for property one-way-bound to non-notifying computed property', function() { el.$.basic2.value = 44; assert.equal(el.observerCounts.boundcomputedvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been'); }); test('one-way binding to notifying property', function() { el.boundnotifyingvalue = 42; assert.equal(el.$.basic2.notifyingvalue, 42, 'binding to child not updated'); el.$.basic2.notifyingvalue = 43; assert.equal(el.boundnotifyingvalue, 42, 'binding to notifying property updated and should not have been'); }); test('observer for property one-way-bound to notifying property', function() { el.$.basic2.notifyingvalue = 45; assert.equal(el.observerCounts.boundnotifyingvalueChanged, 0, 'observer for property bound to notifying property called and should not have been'); }); test('one-way binding to notifying computed property', function() { el.boundcomputednotifyingvalue = 42; el.$.basic2.notifyingvalue = 43; assert.equal(el.boundcomputednotifyingvalue, 42, 'binding to notifying computed property updated and should not have been'); }); test('observer for property one-way-bound to notifying computed property', function() { el.$.basic2.notifyingvalue = 45; assert.equal(el.observerCounts.boundcomputednotifyingvalueChanged, 0, 'observer for property bound to non-notifying computed property called and should not have been'); }); }); suite('reflection to attribute', function() { var el; setup(function() { el = document.createElement('x-reflect'); document.body.appendChild(el); }); teardown(function() { document.body.removeChild(el); }); test('reflect object', function() { var obj = {foo: 'bar', array: [1, '2', {3:3}]}; el.reflectedobject = obj; assert.equal(el.getAttribute('reflectedobject'), '{"foo":"bar","array":[1,"2",{"3":3}]}'); // Ensure object wasn't re-deserialized assert.equal(el.reflectedobject, obj); el.reflectedobject = null; assert(!el.hasAttribute('reflectedobject')); }); test('reflect array', function() { var arr = [1, '2', {3:3}, {'four': 'four'}]; el.reflectedarray = arr; assert.equal(el.getAttribute('reflectedarray'), '[1,"2",{"3":3},{"four":"four"}]'); // Ensure array wasn't re-deserialized assert.equal(el.reflectedarray, arr); el.reflectedarray = null; assert(!el.hasAttribute('reflectedarray')); }); test('reflect string', function() { var str = '"polymer is grrrrreat, ain\'t it?"'; el.reflectedstring = str; assert.equal(el.getAttribute('reflectedstring'), str); assert.equal(el.reflectedstring, str); el.reflectedstring = ''; assert.equal(el.getAttribute('reflectedstring'), ''); assert.equal(el.reflectedstring, ''); el.reflectedstring = null; assert(!el.hasAttribute('reflectedstring')); assert.equal(el.reflectedstring, null); }); test('reflect number', function() { el.reflectedNumber = 765; assert.equal(el.getAttribute('reflected-number'), '765'); assert.equal(el.reflectedNumber, 765); el.reflectedNumber = 765.4321; assert.equal(el.getAttribute('reflected-number'), '765.4321'); assert.equal(el.reflectedNumber, 765.4321); el.reflectedNumber = null; assert(!el.hasAttribute('reflected-number')); assert.equal(el.reflectedNumber, null); }); test('reflect boolean', function() { el.reflectedboolean = true; assert(el.hasAttribute('reflectedboolean')); assert.equal(el.getAttribute('reflectedboolean'), ''); assert.equal(el.reflectedboolean, true); el.reflectedboolean = false; assert(!el.hasAttribute('reflectedboolean')); assert.equal(el.reflectedboolean, false); el.reflectedboolean = true; el.reflectedboolean = null; assert(!el.hasAttribute('reflectedboolean')); assert.equal(el.reflectedboolean, null); }); test('reflect date', function() { var date = new Date('Fri Jan 23 2015 17:40:29 GMT-0800 (PST)'); el.reflecteddate = date; assert(el.hasAttribute('reflecteddate')); assert.equal(Date.parse(el.getAttribute('reflecteddate')), el.reflecteddate.getTime()); assert.equal(el.reflecteddate, date); el.reflecteddate = null; assert(!el.hasAttribute('reflecteddate')); assert.equal(el.reflecteddate, null); }); test('reflect wrong type', function() { el.reflectedstring = true; assert(el.hasAttribute('reflectedstring')); assert.equal(el.getAttribute('reflectedstring'), ''); // Ensure value wasn't re-deserialized assert.strictEqual(el.reflectedstring, true); }); }); suite('binding to attribute', function() { var el; setup(function() { el = document.createElement('x-basic'); document.body.appendChild(el); }); teardown(function() { document.body.removeChild(el); }); test('bind object to attribute', function() { el.attrvalue = {foo: 'bar', array: [1, '2', {3:3}]}; assert.equal(el.$.boundChild.getAttribute('attrvalue'), '{"foo":"bar","array":[1,"2",{"3":3}]}'); el.attrvalue = null; assert(!el.$.boundChild.hasAttribute('attrvalue')); }); test('bind array to attribute', function() { el.attrvalue = [1, '2', {3:3}, {'four': 'four'}]; assert.equal(el.$.boundChild.getAttribute('attrvalue'), '[1,"2",{"3":3},{"four":"four"}]'); el.attrvalue = null; assert(!el.$.boundChild.hasAttribute('attrvalue')); }); test('bind string to attribute', function() { el.attrvalue = '"polymer is grrrrreat, ain\'t it?"'; assert.equal(el.$.boundChild.getAttribute('attrvalue'), '"polymer is grrrrreat, ain\'t it?"'); el.attrvalue = ''; assert.equal(el.$.boundChild.getAttribute('attrvalue'), ''); el.attrvalue = null; assert(!el.$.boundChild.hasAttribute('attrvalue')); }); test('bind number to attribute', function() { el.attrvalue = 765; assert.equal(el.$.boundChild.getAttribute('attrvalue'), '765'); el.attrvalue = 765.4321; assert.equal(el.$.boundChild.getAttribute('attrvalue'), '765.4321'); el.attrvalue = null; assert(!el.$.boundChild.hasAttribute('attrvalue')); }); test('bind boolean to attribute', function() { el.attrvalue = true; assert(el.$.boundChild.hasAttribute('attrvalue')); assert.equal(el.$.boundChild.getAttribute('attrvalue'), ''); el.attrvalue = false; assert(!el.$.boundChild.hasAttribute('attrvalue')); el.attrvalue = true; el.attrvalue = null; assert(!el.$.boundChild.hasAttribute('attrvalue')); }); test('bind date to attribute', function() { el.attrvalue = new Date('Fri Jan 23 2015 17:40:29 GMT-0800 (PST)'); assert(el.$.boundChild.hasAttribute('attrvalue')); assert.equal(Date.parse(el.$.boundChild.getAttribute('attrvalue')), el.attrvalue.getTime()); el.attrvalue = null; assert(!el.$.boundChild.hasAttribute('attrvalue')); }); test('bind to value attribute on input should not fail', function() { var el = document.createElement('x-input-value'); document.body.appendChild(el); el.inputValue = "the value"; assert.equal(el.$.input.value, "the value", "The value of the input is not propagated"); assert.equal(el.$.input.value$, undefined, "value$ should be removed from input"); document.body.removeChild(el); }); }); suite('avoid non-bubbling event gotchas', function() { var el; var container; setup(function() { container = document.createElement('div'); document.body.appendChild(container); container.innerHTML = '<x-notifies3></x-notifies3>'; el = container.firstChild; }); teardown(function() { document.body.removeChild(container); }); test('avoid non-bubbling event gotchas', function() { el.$.notifies2.$.notifies1.notifies = 'runtimeValue'; assert.equal(el.$.notifies2.$.notifies1.notifies, 'runtimeValue'); assert.equal(el.$.notifies2.shouldChange, 'runtimeValue'); assert.notEqual(el.shouldNotChange, 'runtimeValue'); }); test('avoid non-bubbling event gotchas at ready time', function() { assert.equal(el.$.notifies2.$.notifies1.notifies, 'readyValue'); assert.equal(el.$.notifies2.shouldChange, 'readyValue'); assert.notEqual(el.shouldNotChange, 'readyValue'); }); }); suite('warnings', function() { var el; var warn; //eslint-disable-line no-unused-vars setup(function() { sinon.spy(console, 'warn'); el = document.createElement('x-basic'); document.body.appendChild(el); }); teardown(function() { console.warn.restore(); document.body.removeChild(el); }); test('undefined observer', function() { el.noObserver = 42; assert.isTrue(console.warn.calledOnce, 'no warning for undefined observer'); }); test('undefined complex observer', function() { el.noComplexObserver = {}; assert.isTrue(console.warn.calledOnce, 'no warning for undefined complex observer'); }); test('undefined computed function', function() { el.noComputed = 99; assert.isTrue(console.warn.calledOnce, 'no warning for undefined computed function'); }); test('undefined inline computed function', function() { el.noInlineComputed = 99; assert.isTrue(console.warn.calledOnce, 'no warning for undefined computed function'); }); test('binding to a bad attribute warns', function() { document.createElement('x-bind-bad-attribute-name'); assert.isTrue(console.warn.calledOnce, 'no warning for setting a bad attribute'); }); }); suite('binding corner cases', function() { // IE can create adjacent text nodes that split bindings; this test // ensures the code that addresses this is functional test('text binding after entity', function() { var el = document.createElement('x-entity-and-binding'); document.body.appendChild(el); assert.equal(el.$.binding.textContent, 'binding'); document.body.removeChild(el); }); test('bind to isAttached', function() { var el = document.createElement('x-bind-is-attached'); sinon.spy(el, '_isAttachedChanged'); document.body.appendChild(el); assert.equal(el.$.check.textContent, 'true'); assert.isTrue(el._isAttachedChanged.calledOnce); document.body.removeChild(el); }); }); suite('binding interop', function() { test('do not set same value to a non-Polymer element', function() { var el = document.createElement('x-interop'); document.body.appendChild(el); assert.equal(el.$.raw.value, 10); assert.equal(el.$.raw.valueChanged.callCount, 1); assert.equal(el.$.raw.valueChanged.firstCall.args[0], 10); // Should set value el.value++; assert.equal(el.$.raw.value, 11); assert.equal(el.$.raw.valueChanged.callCount, 2); assert.equal(el.$.raw.valueChanged.secondCall.args[0], 11); // Notifies up to host, should not be re-set down to element el.$.raw.increment(); assert.equal(el.$.raw.value, 12); assert.equal(el.$.raw.valueChanged.callCount, 2); document.body.removeChild(el); }); test('path notification of object bound to textContent of a Polymer element', function() { var el = document.createElement('x-interop'); document.body.appendChild(el); // Initial state assert.equal(el.$.polymer.textContent, '1,2,3'); // Push el.push('array', 4); assert.equal(el.$.polymer.textContent, '1,2,3,4'); document.body.removeChild(el); }); test('path notification of array bound to setter of a Polymer element', function() { var el = document.createElement('x-interop'); document.body.appendChild(el); // Initial state assert.equal(el.$.polymer.array, el.array); assert.equal(el.$.polymer.arrayChanged.callCount, 1); assert.equal(el.$.polymer.arrayChanged.firstCall.args[0], el.array); // Push el.push('array', 4); assert.equal(el.$.polymer.array, el.array); // Gets called twice, once for splice info, once for length :( assert.equal(el.$.polymer.arrayChanged.callCount, 3); assert.equal(el.$.polymer.arrayChanged.secondCall.args[0], el.array); assert.equal(el.$.polymer.arrayChanged.thirdCall.args[0], el.array); document.body.removeChild(el); }); test('path notification of array in compound binding to property of Polymer element', function() { var el = document.createElement('x-interop'); document.body.appendChild(el); // Initial state assert.equal(el.$.polymer.compound, '**1,2,3**'); assert.equal(el.$.polymer.compoundChanged.callCount, 1); assert.equal(el.$.polymer.compoundChanged.firstCall.args[0], '**1,2,3**'); // Push el.push('array', 4); assert.equal(el.$.polymer.compound, '**1,2,3,4**'); assert.equal(el.$.polymer.compoundChanged.callCount, 2); assert.equal(el.$.polymer.compoundChanged.secondCall.args[0], '**1,2,3,4**'); document.body.removeChild(el); }); test('path notification of array bound to setter of a non-Polymer element', function() { var el = document.createElement('x-interop'); document.body.appendChild(el); // Initial state assert.equal(el.$.raw.textContent, '1,2,3'); assert.equal(el.$.raw.arrayChanged.callCount, 1); assert.equal(el.$.raw.arrayChanged.firstCall.args[0], el.array); // Push el.push('array', 4); assert.equal(el.$.raw.textContent, '1,2,3,4'); // Array change notifies once for splice info, once for length, and // since the property is a Array, it passes the dirty check and will go // through twice; this test also happens to not have a getter, so that // would cause the dirty check to pass even if it wasn't an Object assert.equal(el.$.raw.arrayChanged.callCount, 3); assert.equal(el.$.raw.arrayChanged.secondCall.args[0], el.array); assert.equal(el.$.raw.arrayChanged.thirdCall.args[0], el.array); document.body.removeChild(el); }); test('path notification of array in compound binding to property of non-Polymer element', function() { var el = document.createElement('x-interop'); document.body.appendChild(el); // Initial state assert.equal(el.$.raw.compound, '**1,2,3**'); // Raw elements with compound bindings will first see the // literals, then the properties, hence 2 sets initially assert.equal(el.$.raw.compoundChanged.callCount, 2); assert.equal(el.$.raw.compoundChanged.firstCall.args[0], '****'); assert.equal(el.$.raw.compoundChanged.secondCall.args[0], '**1,2,3**'); // Push el.push('array', 4); assert.equal(el.$.raw.compound, '**1,2,3,4**'); assert.equal(el.$.raw.compoundChanged.callCount, 3); assert.equal(el.$.raw.compoundChanged.thirdCall.args[0], '**1,2,3,4**'); document.body.removeChild(el); }); }); suite('compound binding / string interpolation', function() { var el; setup(function() { el = document.createElement('x-basic'); document.body.appendChild(el); }); teardown(function() { document.body.removeChild(el); }); test('compound adjacent property bindings', function() { // Adjacent compound binding with no literal do not override the default assert.equal(el.$.boundProps.prop1, 'default'); assert.isTrue(el.$.boundProps.prop1Changed.calledOnce); el.cpnd2 = 'cpnd2'; assert.equal(el.$.boundProps.prop1, 'cpnd2'); el.cpnd1 = 'cpnd1'; el.cpnd3 = {prop: 'cpnd3'}; assert.equal(el.$.boundProps.prop1, 'cpnd1cpnd2cpnd3'); el.cpnd4 = 'cpnd4'; assert.equal(el.$.boundProps.prop1, 'cpnd1cpnd2cpnd3literalComputedcpnd4'); el.cpnd5 = 'cpnd5'; assert.equal(el.$.boundProps.prop1, 'cpnd1cpnd2cpnd3literalComputedcpnd5cpnd4'); }); test('compound property bindings with literals', function() { assert.equal(el.$.boundProps.prop2, 'literal1 literal2 literal3 literal4'); assert.isTrue(el.$.boundProps.prop2Changed.calledOnce); el.cpnd1 = 'cpnd1'; el.cpnd2 = 'cpnd2'; el.cpnd3 = {prop: 'cpnd3'}; el.cpnd4 = 'cpnd4'; el.cpnd5 = 'cpnd5'; assert.equal(el.$.boundProps.prop2, 'literal1 cpnd1 literal2 cpnd2cpnd3 literal3 literalComputedcpnd5cpnd4 literal4'); el.cpnd1 = null; el.cpnd2 = undefined; el.cpnd3 = {}; el.cpnd4 = ''; el.cpnd5 = ''; assert.equal(el.$.boundProps.prop2, 'literal1 literal2 literal3 literalComputed literal4'); }); test('compound adjacent attribute bindings', function() { // Adjacent compound binding with no literal do not override the default assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), null); el.cpnd2 = 'cpnd2'; assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), 'cpnd2'); el.cpnd1 = 'cpnd1'; el.cpnd3 = {prop: 'cpnd3'}; assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), 'cpnd1cpnd2cpnd3'); el.cpnd4 = 'cpnd4'; assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), 'cpnd1cpnd2cpnd3literalComputedcpnd4'); el.cpnd5 = 'cpnd5'; assert.equal(el.$.boundChild.getAttribute('compoundAttr1'), 'cpnd1cpnd2cpnd3literalComputedcpnd5cpnd4'); }); test('compound property attribute with literals', function() { assert.equal(el.$.boundChild.getAttribute('compoundAttr2'), 'literal1 literal2 literal3 literal4'); el.cpnd1 = 'cpnd1'; el.cpnd2 = 'cpnd2'; el.cpnd3 = {prop: 'cpnd3'}; el.cpnd4 = 'cpnd4'; el.cpnd5 = 'cpnd5'; assert.equal(el.$.boundChild.getAttribute('compoundAttr2'), 'literal1 cpnd1 literal2 cpnd2cpnd3 literal3 literalComputedcpnd5cpnd4 literal4'); el.cpnd1 = null; el.cpnd2 = undefined; el.cpnd3 = {}; el.cpnd4 = ''; el.cpnd5 = ''; assert.equal(el.$.boundChild.getAttribute('compoundAttr2'), 'literal1 literal2 literal3 literalComputed literal4'); }); test('compound property attribute with {} and [] in text', function() { el.cpnd1 = 'cpnd1'; assert.equal(el.$.boundChild.getAttribute('compoundAttr3'), '[yes/no]: cpnd1, Hello {0} username world'); }); test('compound adjacent textNode bindings', function() { // The single space is due to the gambit to prevent empty text nodes // from being omitted by IE during importNode from the template; it will // only be there when in the virgin state after cloning the template assert.equal(el.$.compound1.textContent, ' '); el.cpnd2 = 'cpnd2'; assert.equal(el.$.compound1.textContent, 'cpnd2'); el.cpnd1 = 'cpnd1'; el.cpnd3 = {prop: 'cpnd3'}; assert.equal(el.$.compound1.textContent, 'cpnd1cpnd2cpnd3'); el.cpnd4 = 'cpnd4'; assert.equal(el.$.compound1.textContent, 'cpnd1cpnd2cpnd3literalComputedcpnd4'); el.cpnd5 = 'cpnd5'; assert.equal(el.$.compound1.textContent, 'cpnd1cpnd2cpnd3literalComputedcpnd5cpnd4'); // Once the binding evaluates back to '', it will in fact be '' el.computeCompound = function() { return ''; }; el.cpnd1 = null; el.cpnd2 = ''; el.cpnd3 = {prop: null}; el.cpnd4 = null; el.cpnd5 = ''; assert.equal(el.$.compound1.textContent, ''); }); test('compound textNode bindings with literals', function() { assert.equal(el.$.compound2.textContent.trim(), 'literal1 literal2 literal3 literal4'); el.cpnd1 = 'cpnd1'; el.cpnd2 = 'cpnd2'; el.cpnd3 = {prop: 'cpnd3'}; el.cpnd4 = 'cpnd4'; el.cpnd5 = 'cpnd5'; assert.equal(el.$.compound2.textContent.trim(), 'literal1 cpnd1 literal2 cpnd2cpnd3 literal3 literalComputedcpnd5cpnd4 literal4'); el.cpnd1 = null; el.cpnd2 = undefined; el.cpnd3 = {}; el.cpnd4 = ''; el.cpnd5 = ''; assert.equal(el.$.compound2.textContent.trim(), 'literal1 literal2 literal3 literalComputed literal4'); }); test('malformed bindings ignored', function() { el.bool = true; assert.isTrue(el.$.boundChild.textContent.indexOf('really.long.identifier.in.malformed.binding.should.be.ignored') >= 0, true); assert.isTrue(el.$.boundChild.textContent.indexOf('really.long.l