ElementUI table not rendering in a Jest test - vue-cli

I have noticed that some of my components that use ElementUI tables are failing their tests, with Jest and Vue-test-utils.
Looking into it some more, when I reproduce a simple example and use console.log(wrapper.html) some of the inner parts of the table are simply not being rendered.
I am by no means a frontend expert (more of a Python backend person), and working on this to help another team out and so I am not 100% convinced that I'm not doing something stupid, but I have explored a range of possibilities and I can't figure it out. What's more, the tests are not working, but the component itself works absolutely fine within the application - the table is rendered properly. So it's frustrating because it appears to work, but the test suite disagrees....
This is not the actual code, but it's a trimmed down example to reproduce the issue.
BasicTable.vue
<template>
<div>
<el-table
:data="tdata"
>
<el-table-column #default="{ row }">
{{ row.item }}
</el-table-column>
<el-table-column #default="{ row }">
{{ row.size }}
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: 'BasicTable',
data () {
return {
tdata: [{ item: 'A', size: 'L' },
{ item: 'B', size: 'M' },
{ item: 'C', size: 'XS' },
{ item: 'D', size: 'XXL' },
],
}
},
}
</script>
BasicTable.spec.js
import BasicTable from './BasicTable.vue'
import ElementUI from 'element-ui'
import { mount, createLocalVue } from '#vue/test-utils'
test('Render for basic table', () => {
const localVue = createLocalVue()
localVue.use(ElementUI)
const wrapper = mount(BasicTable, {
localVue
}
console.log(wrapper.html())
})
So I would expect the output of the wrapper.html() to just render the table, with the four rows, and two columns, as would be expected from the data.
However, what I actually get is
<div>
<div class="el-table el-table--fit el-table--enable-row-hover el-table--enable-row-transition">
<div class="hidden-columns">
<div></div>
<div></div>
</div>
<div class="el-table__header-wrapper">
<table cellspacing="0" cellpadding="0" border="0" class="el-table__header">
<colgroup></colgroup>
<thead class="">
<tr class=""></tr>
</thead>
</table>
</div>
<div class="el-table__body-wrapper is-scrolling-none">
<table cellspacing="0" cellpadding="0" border="0" class="el-table__body">
<colgroup></colgroup>
<tbody>
<tr class="el-table__row"></tr>
<tr class="el-table__row"></tr>
<tr class="el-table__row"></tr>
<tr class="el-table__row"></tr>
<!---->
</tbody>
</table>
<!---->
<!---->
</div>
<!---->
<!---->
<!---->
<!---->
<div class="el-table__column-resize-proxy" style="display: none;"></div>
</div>
</div>
So between the <tbody></tbody> tags, the correct number of rows are present (4) but then they seem to have no internal content.
A few things I initially considered but discounted as the issue:
I made sure I was mounting and not shallowMounting
using LocalVue and using ElementUI does appear to be working, the ElementUI components are being rendered as basic html, with the relevant classes added (e.g. <table class="el-table">)
Some of the actual components use a computed() to calculate the data that goes into the table, or have it passed via a prop. I thought this might be causing some issue, but I set up this basic example with the most simple case I could think of (i.e just hardcoded data in the component) and i still see the same result.
I have struggled to find much specific ElementUI/Jest/Vue-test-utils help online, so any suggestions would be greatly appreciated! And if I have done something stupid, please let me know because as I said this is not my main area of expertise.
Versions of stuff I am using (from package.json)
"dependencies": {
"element-ui": "^2.4.5",
"vue": "^2.6.11",
"vue-router": "^3.2.0",
"core-js": "^3.6.5",
"#vue/cli-service": "~4.5.0"
},
Note: The project is setup with Vue-cli and I believe Jest is bundled with that.
"#vue/cli-plugin-unit-jest": "~4.5.0",
I run the tests with npm run test:unit

I seem to have found an answer, so will post it here if anyone else stumbles across this.
The solution appears to be waiting for nextTick() for the DOM elements to properly render. I'll be honest, I am not 100% sure why this is the reason - I might expect this if the data was changing during the test (and I had used that approach for other tests when updating props or triggering a click for example) but in this case the data is passed in while mounting... If you come across this answer and have a more concrete explanation, please feel free to add it here!
So for completeness, something like
test('Render for basic table', async () => {
const localVue = createLocalVue()
localVue.use(ElementUI)
const wrapper = mount(BasicTable, {
localVue
}
await wrapper.vm.$nextTick() <-- added this line (also made the test async)
console.log(wrapper.html())
})
Appears to solve my problem!

Related

Strange escaping behaviour with Vue.js

I'm using this code with the intent to create different tags, i.e. item.tag below:
<div v-for="item in items" :key="item.id">
<{{item.tag}}>
{{item.data}}
</{{item.tag}}>
</div>
With items defined as follows:
items: generateItems(2, i => ({
id: 'item' + i,
tag: 'hr',
data: ''
}))
But the HTML inside the div after the code runs has the < and > escaped, even though they aren't inside {{ }}, so it looks like this:
<hr> </hr>
But if I define the type explicitly:
<div v-for="item in items" :key="item.id">
<hr>
{{item.data}}
</hr>
</div>
The < and > are not escaped, and the horizontal rules display no problem.
I intend to use other tags besides hr so would like to be able to use item.tag some way.
Can anyone explain what is going on, and is there a workaround for this?
One way to do this is to use the <component :is="tag"> For example:
var demo = new Vue({
el: '#demo',
data() {
return {
tag: 'button',
othertag: 'hr'
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div v-html id="demo">
<component :is="tag">hello</component>
<component :is="othertag"></component>
</div>

A way to render multiple root elements on VueJS with v-for directive

Right now, I'm trying to make a website that shows recent news posts which is supplied my NodeJS API.
I've tried the following:
HTML
<div id="news" class="media" v-for="item in posts">
<div>
<h4 class="media-heading">{{item.title}}</h4>
<p>{{item.msg}}</p>
</div>
</div>
JavaScript
const news = new Vue({
el: '#news',
data: {
posts: [
{title: 'My First News post', msg: 'This is your fist news!'},
{title: 'Cakes are great food', msg: 'Yummy Yummy Yummy'},
{title: 'How to learnVueJS', msg: 'Start Learning!'},
]
}
})
Apparently, the above didn't work because Vue can't render multiple root elements.
I've looked up the VueJS's official manual and couldn't come up with a solution.
After googling a while, I've understood that it was impossible to render multiple root element, however, I yet to have been able to come up with a solution.
The simplest way I've found of adding multiple root elements is to add a single <div> wrapper element and make it disappear with some CSS magic for the purposes of rendering.
For this we can use the "display: contents" CSS property. The effect is that it makes the container disappear, making the child elements children of the element the next level up in the DOM.
Therefore, in your Vue component template you can have something like this:
<template>
<div style="display: contents"> <!-- my wrapper div is rendered invisible -->
<tr>...</tr>
<tr>...</tr>
<tr>...</tr>
</div>
</template>
I can now use my component without the browser messing up formatting because the wrapping <div> root element will be ignored by the browser for display purposes:
<table>
<my-component></my-component> <!-- the wrapping div will be ignored -->
</table>
Note however, that although this should work in most browsers, you may want to check here to make sure it can handle your target browser.
You can have multiple root elements (or components) using render functions
A simple example is having a component which renders multiple <li> elements:
<template>
<li>Item</li>
<li>Item2</li>
... etc
</template>
However the above will throw an error. To solve this error the above template can be converted to:
export default {
functional: true,
render(createElement) {
return [
createElement('li', 'Item'),
createElement('li', 'Item2'),
]
}
}
But again as you probably noticed this can get very tedious if for example you want to display 50 li items. So, eventually, to dynamically display elements you can do:
export default {
functional: true,
props: ['listItems'], //this is an array of `<li>` names (e.g. ['Item', 'Item2'])
render(createElement, { props }) {
return props.listItems.map(name => {
return createElement('li', name)
})
}
}
INFO in those examples i have used the property functional: true but it is not required of course to use "render functions". Please consider learning more about functional componentshere
Define a custom directive:
Vue.directive('fragments', {
inserted: function(el) {
const children = Array.from(el.children)
const parent = el.parentElement
children.forEach((item) => { parent.appendChild(item) })
parent.removeChild(el)
}
});
then you can use it in root element of a component
<div v-fragments>
<tr v-for="post in posts">...</tr>
</div>
The root element will not be rendered in DOM, which is especially effective when rendering table.
Vue requires that there be a single root node. However, try changing your html to this:
<div id="news" >
<div class="media" v-for="item in posts">
<h4 class="media-heading">{{item.title}}</h4>
<p>{{item.msg}}</p>
</div>
</div>
This change allows for a single root node id="news" and yet still allows for rendering the lists of recent posts.
In Vue 3, this is supported as you were trying:
In 3.x, components now can have multiple root nodes! However, this does require developers to explicitly define where attributes should be distributed.
<!-- Layout.vue -->
<template>
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
</template>
Multiple root elements are not supported by Vue (which caused by your v-for directive, beacause it may render more than 1 elements). And is also very simple to solve, just wrap your HTML into another Element will do.
For example:
<div id="app">
<!-- your HTML code -->
</div>
and the js:
var app = new Vue({
el: '#app', // it must be a single root!
// ...
})

ng-bind-html not recognizing ID names

I'm using ng-bind-html and it pulls in the HTML with class names, but the id names are all missing. What is causing this?
actual code example:
service partial example
var assets = {
'sampleInvoice': '<div id="invoice-div"><table id="invoice-table-id" cellspacing="0" cellpadding="0" border="0"><tbody>......
}
controller portion:
$scope.invoiceDetails = function(params) {
console.log("Here I am ");
var modalInstance = $uibModal.open({
templateUrl: 'invoice-details-modal',
controller: 'invoiceDetailsCtrl',
windowClass: 'invoice-details-modal',
resolve: {
invoice: function resolveInvoiceTemplate($q) {
return $q.resolve($templateCache.get("sampleInvoice"));
}
}
});
HTML Part
<div ng-bind-html="invoice"></div>
What it's producing:
<div><table cellspacing="0" cellpadding="0" border="0"><tbody><tr valign="top"><td><table cellspacing="0" cellpadding="0" border="0"><tbody><tr><td class="invoice-logo-td"><div class="invoice-logo">....
Notice the class names are there, but the ids are gone. Now, I can switch all the ids to classes as a work around, but it seems like it should be able to recognize id names as well.
Does anyone know why this is happening? (Yes, I injected ngSanitize)
Thanks in advance!
Stripping id's from elements in an intentional sanitation. You can see the original commit that caused this back in v1.0.0-rc2.
The reasoning isn't given in the commit, but a later issue filed against angularjs about striping id's from img tags explains this:
the reason why we strip out id and name is that browsers put them on window
<div id="angular"> would overwrite the window.angular with an element. This is considered a security issue and so we don't allow it.
— mhevery

When should we use Vue.js's component

When I study the feature of Vue.js's component system. I feel confused when and where should we use this? In Vue.js's doc they said
Vue.js allows you to treat extended Vue subclasses as reusable
components that are conceptually similar to Web Components, without
requiring any polyfills.
But based on their example it doesn't clear to me how does it help to reuse. I even think it complex the logic flow.
For example, you use "alerts" a lot in your app. If you have experienced bootstrap, the alert would be like:
<div class="alert alert-danger">
<button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button>
<strong>Title!</strong> Alert body ...
</div>
Instead of writing it over and over again, you can actually make it into a component in Vue:
Vue.component('alert', {
props: ['type','bold','msg'],
data : function() { return { isShown: true }; },
methods : {
closeAlert : function() {
this.isShown = false;
}
}
});
And the HTML template (just to make it clear, I separate this from the Vue Comp above):
<div class="alert alert-{{ type }}" v-show="isShown">
<button type="button" class="close" v-on="click: closeAlert()">×</button>
<strong>{{ bold }}</strong> {{ msg }}
</div>
Then you can just call it like this:
<alert type="success|danger|warning|success" bold="Oops!" msg="This is the message"></alert>
Note that this is just a 4-lines of template code, imagine when your app uses lot of "widgets" with 100++ lines of code
Hope this answers..

Nested ListView or Nested Repeater

I am trying to created a nested repeater or a nested list view using WinJS 4.0, but I am unable to figure out how to bind the data source of the inner listview/repeater.
Here is a sample of what I am trying to do (note that the control could be Repeater, which I would prefer):
HTML:
<div id="myList" data-win-control="WinJS.UI.ListView">
<span data-win-bind="innerText: title"></span>
<div data-win-control="WinJS.UI.ListView">
<span data-win-bind="innerText: name"></span>
</div>
</div>
JS:
var myList = element.querySelector('#myList).winControl;
var myData = [
{
title: "line 1",
items: [
{name: "item 1.1"},
{name: "item 1.2"}
]
},
{
title: "line 2",
items: [
{name: "item 2.1"},
{name: "item 2.2"}
]
}
];
myList.data = new WinJS.Binding.List(myData);
When I try this, nothing renders for the inner list. I have attempted trying to use this answer Nested Repeaters Using Table Tags and this one WinJS: Nested ListViews but I still seem to have the same problem and was hoping it was a little less complicated (like KnockOut).
I know it is mentioned that WinJS doesn't support nested ListViews, but that seems to be a few years ago and I am hoping that is still not the issue.
Update
I was able to get the nested repeater to work correctly, thanks to Kraig's answer. Here is what my code looks like:
HTML:
<div id="myTemplate" data-win-control="WinJS.Binding.Template">
<div
<span>Bucket:</span><span data-win-bind="innerText: name"></span>
<span>Amount:</span><input type="text" data-win-bind="value: amount" />
<button class="removeBucket">X</button>
<div id="bucketItems" data-win-control="WinJS.UI.Repeater"
data-win-options="{template: select('#myTemplate')}"
data-win-bind="winControl.data: lineItems">
</div>
</div>
</div>
<div id="budgetBuckets" data-win-control="WinJS.UI.Repeater"
data-win-options="{data: Data.buckets,template: select('#myTemplate')}">
</div>
JS: (after the "use strict" statement)
WinJS.Namespace.define("Data", {
buckets: new WinJS.Binding.List([
{
name: "A",
amount: 5,
lineItems: new WinJS.Binding.List( [
{ name: 'test item1', amount: 50 },
{ name: 'test item2', amount: 25 }
]
)
}
])
})
*Note that this answers part of my question, however, I would really like to do this all after a repo call and set the repeater data source programmatically. I am going to keep working towards that and if I get it I will post that as the accepted answer.
The HTML Repeater control sample for Windows 8.1 has an example in scenario 6 with a nested Repeater, and in this case the Repeater is created through a Template control. That's a good place to start. (I discuss this sample in Chapter 7 of Programming Windows Store Apps with HTML, CSS, and JavaScript, 2nd Edition, starting on page 372, or 374 for the nested part.)
Should still work with WinJS 4, though I haven't tried it.
Ok, so I have to give much credit to Kraig because he got me on the correct path to getting this worked out and the referenced book Programming Windows Store Apps with HTML, CSS, and JavaScript, 2nd Edition is amazing.
The original issue was a combination of not using templates correctly (using curly braces in the data-win-bind attribute), not structuring my HTML correctly and not setting the child lists as WinJS.Binding.List data source. Below is the final working code structure to created a nested repeater when binding the data from code only:
HTML:
This is the template for the child lists. It looks similar, but I plan on add more things so I wanted it separate instead of recursive as referenced in the book. Note that the inner div after the template control declaration was important for me.
<div id="bucketItemTemplate" data-win-control="WinJS.Binding.Template">
<div>
<span>Description:</span>
<span data-win-bind="innerText: description"></span>
<span>Amount:</span>
<input type="text" data-win-bind="value: amount" />
<button class="removeBucketItem">X</button>
</div>
</div>
This is the main repeater template for the lists. Note that the inner div after the template control declaration was important for me. Another key point was using the "winControl.data" property against the property name of the child lists.
<div id="bucketTemplate" data-win-control="WinJS.Binding.Template">
<div>
<span>Bucket:</span>
<span data-win-bind="innerText: bucket"></span>
<span>Amount:</span>
<input type="text" data-win-bind="value: amount" />
<button class="removeBucket">X</button>
<div id="bucketItems" data-win-control="WinJS.UI.Repeater"
data-win-options="{template: select('#bucketItemTemplate')}"
data-win-bind="winControl.data: lineItems">
</div>
</div>
</div>
This is the main control element for the nested repeater and it is pretty basic.
<div id="budgetBuckets" data-win-control="WinJS.UI.Repeater"
data-win-options="{template: select('#bucketTemplate')}">
</div>
JavaScript:
The JavaScript came down to a few simple steps:
Getting the winControl
var bucketsControl = element.querySelector('#budgetBuckets').winControl;
Looping through the elements and making the child lists into Binding Lists - the data here is made up but could have easily came from the repo:
var bucketsData = selectedBudget.buckets;
for (var i = 0; i < bucketsData.length; i++) {
bucketsData[i].lineItems =
new WinJS.Binding.List([{ description: i, amount: i * 10 }]);
}
Then finally converting the entire data into a Binding list and setting it to the "data" property of the winControl.
bucketsControl.data = new WinJS.Binding.List(bucketsData);
*Note that this is the entire JavaScript file, for clarity.
(function () {
"use strict";
var nav = WinJS.Navigation;
WinJS.UI.Pages.define("/pages/budget/budget.html", {
// This function is called whenever a user navigates to this page. It
// populates the page elements with the app's data.
ready: function (element, options) {
// TODO: Initialize the page here.
var bindableBuckets;
require(['repository'], function (repo) {
//we can setup our save button here
var appBar = document.getElementById('appBarBudget').winControl;
appBar.getCommandById('cmdSave').addEventListener('click', function () {
//do save work
}, false);
repo.getBudgets(nav.state.budgetSelectedIndex).done(function (selectedBudget) {
var budgetContainer = element.querySelector('#budgetContainer');
WinJS.Binding.processAll(budgetContainer, selectedBudget);
var bucketsControl = element.querySelector('#budgetBuckets').winControl;
var bucketsData = selectedBudget.buckets;
for (var i = 0; i < bucketsData.length; i++)
{
bucketsData[i].lineItems = new WinJS.Binding.List([{ description: i, amount: i * 10 }]);
}
bucketsControl.data = new WinJS.Binding.List(bucketsData);
});
});
WinJS.UI.processAll();
}
});
})();

Resources