Overview
DOM elements that are dynamically-rendered within dom-if, dom-repeat <templates> seem to be rendered asynchronously thus making unit-testing a bit of a pain.
The Polymer Component
template(is='dom-if', if='{{!defaultPrintAll}}')
template(is='dom-repeat', items='{{_pageBucketItems}}')
button(type='button', class$='{{_computeDefaultClass(item)}}', on-tap='_togglePageClick') {{item}}
The Test
test("Clicking 'Export All' to off, reveals board-selection tiles", function() {
$("#export-pdf-checkbox-all").siblings(".checkbox").trigger("click");
Polymer.dom.flush()
expect($(".board-range__button")).to.be.visible;
});
Why it seems to fail:
When clicking a button which triggers the dom-if/dom-repeat the elements don't render in a synchronous order.
The dom-if and it's subsequent/nested dom-repeat render asynchronously.
To make matters worse, the button itself get's it's class in a computed/binded manner (mind the class$= on the button).
So the question boils down to this:
Is it possible to force render the dom-if, dom-repeat, and the computed-binding of the class in a synchronous order after I simulate the click to the button which activates all 3 of those conditions?
Notes:
I'm using Polymer's official WCT as the test harness.
I'm also using chai-jquery.
I've also used Polymer.dom.flush() but it still doesn't, ahem.. flush.
I'm aware that I can use chai-as-promised.js instead but it adds unnecessary complexity to my tests for a trivial matter such as this, so I'd like to avoid it.
Rather than using Polymer.dom.flush(), try using the flush function that WCT puts on the window. This will enqueue a callback function to be executed, in theory, after the template has rendered.
test("Clicking 'Export All' to off, reveals board-selection tiles", function(done) {
$("#export-pdf-checkbox-all").siblings(".checkbox").trigger("click");
flush(function () {
expect($(".board-range__button")).to.be.visible;
done();
}
});
Important to notice: Asynchronous tests require the done function to be passed into the test callback, and require done to be called after your conditions have been evaluated.
Related
Following the question I would like to ask about the appropriate way to check the initial render status of a component (not the update status) in shadow DOM. Is there any similar to document.readyState or a promise?
I have also tried to do:
getItems() {
this.updateComplete
.then(() => {
this.nodesLists = this.shadowRoot.querySelectorAll(".name");
})
.then(...)
}
which also failed.
Tia
await this.updateComplete (or this.updateComplete.then(...)) is the correct way to wait until the element has no pending render work before e.g. querying the state of the element's rendering, so your code should generally work as long as the element is connected to the document before running getItems.
Example: https://jsbin.com/jiquhez/edit?html,console,output
Note however, that if you await updateComplete before the element is connected and the element has no properties set that would trigger a render, then updateComplete currently resolves before the first render. This may be considered an unintended bug, filed at lit-element/#594.
Note you may also want to look into using the firstUpdated lifecycle method, depending on your use case. This is a method you can implement on your class to perform one-time work following the first update/render cycle for the element (useful for e.g. selecting static nodes that won't change based on rendering).
Example: https://jsbin.com/limikas/edit?html,console,output
With Polymer 1.* and WCT, when testing my element <sp-veteran></sp-veteran> I am not able to stub out the methods ._getSpComboBox() and ._getItems() in the ready function. I get Error thrown outside of test function: this._getSpComboBox(...)._getItems is not a function.
Since it is in the ready function, I need to use the WCT api stub instead of sinon.stub since the later requires me to grab the element which I can not do before fixture().
Any suggestions?
original code:
_getSpComboBox: function() {
return Polymer.dom(this.$.veteran.root).querySelector('sp-combo-box');
},
ready: function() {
if (this.editMode) {
this._getSpComboBox()._getItems();
}
this.$.veteranNoAjax.read();
this._setStyle();
}
test:
<test-fixture id="sp-veteran">
<template>
<h2>edit veteran in edit mode</h2>
<sp-app>
<sp-toast></sp-toast>
<sp-veteran edit-mode></sp-veteran>
</sp-app>
</template>
</test-fixture>
before(() => {
replace('sp-app').with('fake-sp-app');
stub('sp-ajax', {read: ()=> entitiesMock});
const _getItems = ()=> entitiesMock;
stub('sp-veteran', {_getSpComboBox: ()=> _getItems});
Unfortunately testing ready in Polymer1 is kind of a pain, or at least I haven't found an easy way that doesn't have odd side-effects. Calling the ready method after you've attached your stubs/spies is always an option but as I mentioned it can cause some odd issues. This was alleviated in Polymer2 as ready is called by the first call of connectedCallback for your element, so you can create the element then bind your spies and manually add to trigger it, just don't forget to remove after.
In the case of DOM manipulation in a Polymer element, you should be using the attached lifecycle instead, this will solve your issue as I mentioned above for testing, but it also saves you a weird potential usage case in the future. Since ready only runs once for an instance of an element, any logic in your ready statement won't get re-run if that element is re-used later, instead if you put the logic in your attached lifecycle if that element is removed from the DOM then added again later in another location it will rerun it's logic to fetch it's new children.
I have a component that contains video. My component is nested in a dom-if and can disappear. When this happens the video (and it sound) keep playing.
Is there a way in which my component can detect that is has disappeared from the DOM? I have tried to use the 'detached' callback as described here: https://www.polymer-project.org/1.0/docs/devguide/registering-elements
Polymer({
is: 'my-component-with-video',
properties: {
// some properties
},
detached: function() {
console.log('Component detached');
// more code to stop video
},
but when my element is removed by the dom-if nothing happens, I don't see the console.log message. What am I doing wrong?
There are two scenarios possible here:
You want your element to be discarded and recreated fresh when the condition changes.
You want to keep it in the dom but freeze it.
In the first case you need to add the restamp attribute to the dom-if to make sure the template DOM is destroyed, not hidden. By default the dom-if stamps the template at first initialization and then hides it from the view if the condition becomes falsy.
In the second case, the suggestion given by Intervalia will not work, because the dom-if in "hide" mode does not detach anything from the DOM. Setting restamp attribute will make the detached callback run but then no point in pausing anything since the element will be discarded.
If you want to keep it in the DOM and freeze it's state you need to listen to dom-change event on the dom-if and run the .pause() accordingly.
No need for any workaround other than simply using your dom-if and rather than
<dom-if if="[[condietionBoolean]]">
<your-video-element id="giveanId"></your-video-element>
</dom-if>
write the if statement like below and so each time your condition changes, you check and make sure the video is paused if when you like. See below.
...
<dom-if if="[[_shouldShowVideo(conditionBoolean)]]">
<your-video-element id="giveanId"></your-video-element>
</dom-if>
...
Polymer({
is: 'my-component-with-video',
properties: {
conditionBoolean : {
type: Boolean,
},
},
_shouldShowVideo: function(conditionBoolean ) {
if (!conditionBoolean) this.$$(#yourVideoElementId).pause();
return conditionBoolean ;
}
});
In your detached function you need to get the Video Element and call .pause() on it.
You will probably also need to call .pause() when your condition changes that would cause the dom-if to remove the player.
I'm starting to learn angularJS better, and I've noticed that AngularJS tries to make strong emphasis on separating the view from the controller and encapsulation. One example of this is people telling me DOM manipulation should go in directives. I kinda got the hang of it now, and how using link functions that inject the current element allow for great behavior functionality, but this doesn't explain a problem I always encounter.
Example:
I have a sidebar I want to open by clicking a button. There is no way to do this in button's directive link function without using a hard-coded javascript/jquery selector to grab the sidebar, something I've seen very frowned upon in angularJS (hard-coding dom selectors) since it breaks separation of concerns. I guess one way of getting around this is making each element I wish to manipulate an attribute directive and on it's link function, saving a reference it's element property into a dom-factory so that whenever a directive needs to access an element other than itself, it can call the dom-factory which returns the element, even if it knows nothing where it came from. But is this the "Angular way"?
I say this because in my current project I'm using hard-coded selectors which are already a pain to mantain because I'm constantly changing my css. There must be a better way to access multiple DOM elements. Any ideas?
There are a number of ways to approach this.
One approach, is to create a create a sidebar directive that responds to "well-defined" broadcasted messages to open/close the sidebar.
.directive("sidebar", function(){
return {
templateUrl: "sidebar.template.html",
link: function(scope, element){
scope.$root.$on("openSidebar", function(){
// whatever you do to actually show the sidebar DOM content
// e.x. element.show();
});
}
}
});
Then, a button could invoke a function in some controller to open a sidebar:
$scope.openSidebar = function(){
$scope.$root.$emit("openSidebar");
}
Another approach is to use a $sidebar service - this is somewhat similar to how $modal works in angularui-bootstrap, but could be more simplified.
Well, if you have a directive on a button and the element you need is outside the directive, you could pass the class of the element you need to toggle as an attribute
<button my-directive data-toggle-class="sidebar">open</button>
Then in your directive
App.directive('myDirective', function() {
return {
restrict: 'A',
link: function(scope, element, attrs) {
angular.element('.' + attrs.toggleClass).toggleClass('active');
}
};
}
You won't always have the link element argument match up with what you need to manipulate unfortunately. There are many "angular ways" to solve this though.
You could even do something like:
<div ng-init="isOpen = false" class="sidebar" ng-class="{'active': isOpen}" ng-click="isOpen = !isOpen">
...
</div>
The best way for directive to communicate with each other is through events. It also keeps with the separation of concerns. Your button could $broadcast on the $rootScope so that all scopes hear it. You would emit and event such as sidebar.open. Then the sidebar directive would listen for that event and act upon it.
Element needs some time for template-repeat to render all content, so paper-spinner is used to notify the user to wait.
How can I know that template-repeat has finished so I can turn off the spinner?
And related question: how can inner element "item-details" be selected? Again, template-repeat has to be finished first.
Here's the code I am using:
<polymer-element name="item-list">
<template>
<paper-spinner active></paper-spinner>
<template id="repeat_items" repeat="{{ item in car.items }}">
<item-details id="item_details" item="{{item}}"></item-details>
</template>....
This is some simulation of the problem: plnkr.co
Edit
links from research:
spinner example
why does onmutation disconnect after first mutation?
polymer-how-to-watch-for-change-in-content-properties
There are component lifecycle hooks.
You are probably looking for domReady.
Called when the element’s initial set of children are guaranteed to exist. This is an appropriate time to poke at the element’s parent or light DOM children. Another use is when you have sibling custom elements (e.g. they’re .innerHTML‘d together, at the same time). Before element A can use B’s API/properties, element B needs to be upgraded. The domReady callback ensures both elements exist.
Polymer('tag-name', {
domReady: function() {
// hide the spinner
// select the first item details element
}
});
As for selecting elements, you can traverse the component's shadow dom like so:
this.shadowRoot.querySelector(selector);
EDIT...
The domReady hook is great if you have all of your data up-front. If you get data asynchronously, then you can use a change watcher.
Here's is a fork of your plunkr that successfully selects the child components after the data changes. Notice the setTimeout(f, 1) that defers selection until after the DOM updates.
carsChanged: function(){
var _this = this;
setTimeout(function(){
console.log(_this.shadowRoot.querySelectorAll('item-details'))
},1)
}
I suggest something like this - http://jsbin.com/bifene/4/edit
Leverages Polymer's onMutation function to watch for changes to a DOM node. Note that it only gets called once so you'll need to re-register it every time you load new items & restart the spinner.