Matthew Hardin Matthew Hardin - 5 months ago 7
Javascript Question

How can I pass a wct test while rearranging children spans in a Polymer element?

I am creating a polymer element called "price-text" that displays a monetary value and some text associated with it. I am also adding the ability to reverse their positions when the "swap" attribute is present. I have performed a large amount of testing on this element using wct with no issue until I set up a fixture using the swap attribute, despite being able to successfully use the swap attribute in a browser demo to reverse the order in which the spans (inside "price-text") are displayed.

I can only observe the error when wct opens the browsers to run the test. This is the error I see in the browser:

Error: the string "console.error:Error stamping [object HTMLUnknownElement] TypeError: Unable to get property 'parentNode' of undefined or null reference" was thrown, throw an Error :)


After wct completes all of the suties it will still return "Tests Passed" to the terminal.

I have determined that the the setup function will run, and the error occurs as soon as the test function is called. To restate my goal, I would like to be able to use wct to pass the two simple "expect" tests in this suite where spans change positions.

Here is the test fixture.

<test-fixture id="price-text-swapd">
<template>
<price-text price="6.99" text="meal" swap></price-text>
</template>
</test-fixture>


Here is the script associated with this test fixture.

suite('price and text are swapd', function() {

var el;
var priceTextLineEl;

setup(function() {
el = fixture('price-text-swapd');
priceTextLineEl = Polymer.dom(el.root).querySelector("#price-text-line");
});

test('defines the swap property', function(done) {
flush(function() {
expect(el.swap).to.be.a('boolean');
expect(el.swap).to.equal(true);
done();
});
});
});


Here is the price-text element.

<span id="price-text-line">
<span id="price" class="left border">{{_formattedPrice}}</span><span id="text" class="right">{{_formattedText}}</span>
</span>


Here is the function controlling the spans in the shadow DOM

_swapPriceText: function() {
var textEl = document.getElementById("text");
var priceEl = document.getElementById("price")
if (this.swap) {
textEl.parentNode.insertBefore(textEl, textEl.parentNode.firstChild);
textEl.classList.add("left");
textEl.classList.remove("right");
priceEl.classList.add("right");
priceEl.classList.remove("left");
}
}


I've posted the test and element file in their entirety in the following snippets.



<!--
@license
Copyright (c) 2015 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
-->

<link rel="import" href="../polymer/polymer.html">

<!--
Variable price and text sparated by a border.

Example:

<price-text price="6.99" text="meal"></price-text>

@demo demo/index.html
@hero hero.svg
-->

<dom-module id="price-text">
<template>
<style>
:root {
--font-family: 'Helvetica', arial;
--font-size: 32px;
--text-font-size: 21px;
--price-padding-left: 0;
--price-padding-right: 4px;
--price-border-right: 1px solid #000;
--price-border-left: 1px solid #000;
--text-padding-left: 8px;
--text-padding-right: 0;
}

:host {
display: inline-block;
box-sizing: border-box;
}

#price-text-line {
font-family: var(--font-family);
@apply(--price-text-line);
}

#price {
display: inline-block;
font-size: var(--font-size);
@apply(--price-text-line--price);
}

#text {
display: inline-block;
font-size: var(--text-font-size);
@apply(--price-text-line--text);
}

.left {
padding-left: var(--price-padding-left);
padding-right: var(--price-padding-right);;
@apply(--price-text-line--left);
}

.right {
padding-left: var(--text-padding-left);
padding-right: var(--text-padding-right);
@apply(--price-text-line--right);
}

.border.right {
border-left: var(--price-border-right);
}

.border.left {
border-right: var(--price-border-left);
}



</style>

<span id="price-text-line">
<span id="price" class="left border">{{_formattedPrice}}</span><span id="text" class="right">{{_formattedText}}</span>
</span>
</template>

<script>
Polymer({
is: 'price-text',

properties: {
/**
* `price` accepts input in the form of numbers and passes the data to the `formatCurrency()` method.
*
* @type {{price: Number}}
*/
price: {
type: Number,
value: 0,
observer: '_formatCurrency'
},
/**
* `formattedPrice` accepts formatted data from the `formatCurrency()` method to be displayed in .price.
*
* @type {{formattedPrice: String}}
*/
_formattedPrice: {
type: String,
value: ""
},
/**
* `text` accepts input in the form of strings and passes the data to the `formatText()` method.
*
* @type {{text: String}}
*/
text: {
type: String,
value: "",
observer: '_formatText'
},
/**
* formattedText accepts formatted data from the `formatText()` method to be displayed in .text.
*
* @type {{formattedText: String}}
*/
_formattedText: {
type: String,
value: ""
},

swap: {
type: Boolean,
observer: '_swapPriceText'
}

},

/**
* `formatCurrency()` accepts data from `price`, checks to see if it is a number greater than zero, rounds the number to two decimal places, and passes the data to `formattedPrice`. If the data is not a number it will set `formattedCurrency` to an empty string.
*
*/
_formatCurrency: function() {
if (this.price > 0) {
this._formattedPrice = '$' + (this.price).toFixed(2);
} else {
this._formattedPrice = '';
}
},
/**
*`formatText()` accepts data from `text`, checks to see if it is not a number, and passes the data to `formattedText`. If the data is a number, it will set `formattedText` to an empty string.
*
*/
_formatText: function() {
if (isNaN(this.text)) {
this._formattedText = this.text;
} else {
this._formattedText = '';
}
},

/**
*`swapPriceText` swaps the position of the spans containing the `price` and `text` data.
*
*/

_swapPriceText: function() {
var textEl = document.getElementById("text");
var priceEl = document.getElementById("price")
if (this.swap) {
textEl.parentNode.insertBefore(textEl, textEl.parentNode.firstChild);
textEl.classList.add("left");
textEl.classList.remove("right");
priceEl.classList.add("right");
priceEl.classList.remove("left");
} else {
priceEl.parentNode.insertBefore(priceEl, priceEl.parentNode.firstChild);
textEl.classList.add("right");
textEl.classList.remove("left");
priceEl.classList.add("left");
priceEl.classList.remove("right");

}

}





});
</script>
</dom-module>







<!doctype html>
<!--
@license
Copyright (c) 2015 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">
<meta name="viewport" content="width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes">

<script src="../../webcomponentsjs/webcomponents-lite.js"></script>
<script src="../../web-component-tester/browser.js"></script>

<!-- Step 1: import the element to test -->
<link rel="import" href="../price-text.html">
</head>
<body>

<!-- You can use the document as a place to set up your fixtures. -->
<test-fixture id="has-price-and-text">
<template>
<price-text price="6.99" text="meals"></price-text>
</template>
</test-fixture>
<test-fixture id="has-price-not-text">
<template>
<price-text price="42.42"></price-text>
</template>
</test-fixture>
<test-fixture id="has-text-not-price">
<template>
<price-text text="sandwich"></price-text>
</template>
</test-fixture>
<test-fixture id="has-wrong-price-data">
<template>
<price-text price="shake" text="fries"></price-text>
</template>
</test-fixture>
<test-fixture id="has-wrong-text-data">
<template>
<price-text price="42.00" text="0.42"></price-text>
</template>
</test-fixture>
<test-fixture id="price-text-undefined">
<template>
<price-text></price-text>
</template>
</test-fixture>
<test-fixture id="price-text-swapd">
<template>
<price-text price="6.99" text="meal" swap></price-text>
</template>
</test-fixture>

<script>
suite('define price and text', function() {

var el;
var priceEl;
var textEl;

setup(function() {
el = fixture('has-price-and-text');
priceEl = Polymer.dom(el.root).querySelector('#price');
textEl = Polymer.dom(el.root).querySelector('#text')
});

test('defines the price property', function() {
expect(el.price).to.be.a('number');
expect(el.price).to.equal(6.99);
});

test('defines the text properly', function() {
expect(el.text).to.be.a('string');
expect(el.text).to.equal('meals');
});

test('is price displaying properly', function() {
expect(priceEl.innerHTML).to.equal('$6.99');
});

test('is text displaying properly', function() {
expect(textEl.innerHTML).to.equal('meals')
});

});

suite('define price but not text', function() {

var el;
var priceEl;
var textEl;

setup(function() {
el = fixture('has-price-not-text');
priceEl = Polymer.dom(el.root).querySelector('#price');
textEl = Polymer.dom(el.root).querySelector('#text');
});

test('defines the price property', function() {
expect(el.price).to.be.a('number');
expect(el.price).to.equal(42.42);
});

test('defines the text property', function() {
expect(el.text).to.be.a('string');
expect(el.text).to.equal('');
});

test('is price displaying properly', function() {
expect(priceEl.innerHTML).to.equal('$42.42');
});

test('is text displaying properly', function() {
expect(textEl.innerHTML).to.equal('')
});

});

suite('price data is wrong', function() {

var el;
var priceEl;
var textEl;

setup(function() {
el = fixture('has-wrong-price-data');
priceEl = Polymer.dom(el.root).querySelector('#price');
textEl = Polymer.dom(el.root).querySelector('#text');
});

test('defines the price property', function() {
expect(el.price).to.be.a('number');
expect(isNaN(el.price)).to.be.ok;
});

test('defines the text property', function() {
expect(el.text).to.be.a('string');
expect(el.text).to.equal('fries');
});

test('is price displaying properly', function() {
expect(priceEl.innerHTML).to.equal('');
});

test('is text displaying properly', function() {
expect(textEl.innerHTML).to.equal('fries')
});

});

suite('define text but not price', function() {

var el;
var priceEl;
var textEl;

setup(function() {
el = fixture('has-text-not-price');
priceEl = Polymer.dom(el.root).querySelector('#price');
textEl = Polymer.dom(el.root).querySelector('#text');
});

test('defines the price property', function() {
expect(el.price).to.be.a('number');
expect(el.price).to.equal(0);
});

test('defines the text property', function() {
expect(el.text).to.be.a('string');
expect(el.text).to.equal('sandwich');
});

test('is price displaying properly', function() {
expect(priceEl.innerHTML).to.equal('');
});

test('is text displaying properly', function() {
expect(textEl.innerHTML).to.equal('sandwich')
});

});

suite('text data is wrong', function() {

var el;
var priceEl;
var textEl;

setup(function() {
el = fixture('has-wrong-text-data');
priceEl = Polymer.dom(el.root).querySelector('#price');
textEl = Polymer.dom(el.root).querySelector('#text');
});

test('defines the price property', function() {
expect(el.price).to.be.a('number');
expect(el.price).to.equal(42.00);
});

test('defines the text property', function() {
expect(el.text).to.be.a('string');
expect(!isNaN(el.text)).to.be.ok;
});

test('is price displaying properly', function() {
expect(priceEl.innerHTML).to.equal('$42.00');
});

test('is text displaying properly', function() {
expect(textEl.innerHTML).to.equal('')
});

});

suite('price and text are not defined', function() {

var el;
var priceEl;
var textEl;

setup(function() {
el = fixture('price-text-undefined');
priceEl = Polymer.dom(el.root).querySelector('#price');
textEl = Polymer.dom(el.root).querySelector('#text');
});

test('defines the price property', function() {
expect(el.price).to.be.a('number');
expect(el.price).to.equal(0);
});

test('defines the text property', function() {
expect(el.text).to.be.a('string');
expect(el.text).to.equal('');
});

test('is price displaying properly', function() {
expect(priceEl.innerHTML).to.equal('');
});

test('is text displaying properly', function() {
expect(textEl.innerHTML).to.equal('')
});

});

suite('price and text are swapd', function() {

var el;
var priceTextLineEl;

setup(function() {
el = fixture('price-text-swapd');
priceTextLineEl = Polymer.dom(el.root).querySelector("#price-text-line");
});
/*
test('defines the swap, price, and text properties', function(done) {
flush(function() {
expect(el.swap).to.be.a('boolean');
expect(el.swap).to.equal(true);
expect(el.price).to.be.a('number');
expect(el.price).to.equal(6.99);
expect(el.text).to.be.a('string');
expect(el.text).to.equal('meal');
done();
});
});*/

/* test('is swap displaying properly', function() {
expect(priceTextLineEl.innerHTML).to.equal('<span id="text" class="right">meal</span><span id="price" class="left border">meal</span>');
});
*/
});
</script>

</body>
</html>




Answer

Use this.$.text and this.$.price in _swapPriceText method. This will solve your issue. Always prefer to use above mentioned method or this.$$('#id') in case of dom-repeat in Polymer to get nodes. It is recommended method by polymer.

Also you don't need flush method that is needed in case of dom-repeat