Vue.js - Vuetify - Hard time to test a menu component - unit-testing

Given the following Toolbar component, I am trying to test the locale changing upon click on menu item ... but the wrapper content is not changing after a mouseover event on the menu selector ...
Toolbar.vue
<template>
<v-toolbar height="80px" fixed>
<v-toolbar-title>
<img src="#/assets/images/app_logo.png" alt="">
<v-menu bottom offset-y open-on-hover class="btn btn--flat" style="margin-bottom: 12px;">
<v-btn id="current-flag" flat slot="activator">
<img :src="flagImage(currentLocaleIndex)" width="24px">
</v-btn>
<v-list>
<v-list-tile v-for="(locale, index) in locales" :key="index" #click="switchLocale(index)">
<div class="list__tile__avatar avatar--tile" #click="switchLocale(index)">
<img :src="flagImage(index)" width="24px">
</div>
<div class="list__tile__title" v-html="flagTitle(index)"></div>
</v-list-tile>
</v-list>
</v-menu>
</v-toolbar-title>
<v-spacer></v-spacer>
<v-toolbar-items>
<!-- MENU HOME-->
<v-btn flat #click="$router.push(menuItems.home.link)">
<v-icon left>{{ menuItems.home.icon }}</v-icon>
<span>{{ menuItems.home.title | translate }}</span>
</v-btn>
<!-- ABOUT HOME-->
<v-btn flat #click="$router.push(menuItems.about.link)">
<v-icon left>{{ menuItems.about.icon }}</v-icon>
<span>{{ menuItems.about.title | translate }}</span>
</v-btn>
</v-toolbar-items>
</v-toolbar>
</template>
<script>
// import i18n from '#/locales'
export default {
name: "Toolbar",
props: ["appName"],
data() {
return {
menuItems: {
home: { icon: "home", title: "Home", link: "/" },
about: { icon: "info", title: "About", link: "/about" }
},
locales: [
{ id: "en", title: "English", flag: "#/assets/images/flag_en_24.png" },
{ id: "fr", title: "Français", flag: "#/assets/images/flag_fr_24.png" },
{ id: "br", title: "Português", flag: "#/assets/images/flag_br_24.png" }
]
};
},
computed: {
currentLocaleIndex: function() {
let index = this.locales.findIndex(o => o.id === this.$i18n.locale);
return index;
},
currentLocaleTitle: function() {
let obj = this.locales.find(o => o.id === this.$i18n.locale);
return obj.title;
},
currentLocaleFlag: function() {
let obj = this.locales.find(o => o.id === this.$i18n.locale);
return obj.flag;
}
},
methods: {
flagImage: function(index) {
return require("#/assets/images/flag_" +
this.locales[index].id +
"_24.png");
},
flagTitle: function(index) {
return this.locales[index].title;
},
switchLocale: function(index) {
this.$i18n.locale = this.locales[index].id;
}
},
mounted() {}
};
</script>
Toobar.spec.je
import Vue from "vue";
import router from "#/router";
import Vuetify from "vuetify";
import i18n from "#/locales";
import { mount, shallowMount } from "#vue/test-utils";
import Toolbar from "#/components/shared/Toolbar.vue";
describe("App.vue", () => {
let wrapper;
beforeEach(() => {
Vue.use(Vuetify);
Vue.filter("translate", function(value) {
if (!value) return "";
value = "lang.views.global." + value.toString();
return i18n.t(value);
});
const el = document.createElement('div');
el.setAttribute('data-app', true);
document.body.appendChild(el);
});
it("should change locale", () => {
// given
wrapper = mount(Toolbar, { router, i18n });
console.log('CURRENT LOCALE INDEX: ', wrapper.vm.currentLocaleIndex);
// console.log(wrapper.html());
const currentFlagBtn = wrapper.find("#current-flag");
console.log(currentFlagBtn.html())
currentFlagBtn.trigger('mouseover');
wrapper.vm.$nextTick( () => {
console.log(wrapper.html());
// const localesBtn = wrapper.findAll("btn");
});
// when
// localesBtn.at(1).trigger("click"); // French locale
// then
// expect(wrapper.wrapper.vm.currentLocaleIndex).toBe(1);
});
});
console.log
console.log tests/unit/Toolbar.spec.js:32
CURRENT LOCALE INDEX: 0
console.log tests/unit/Toolbar.spec.js:35
<button type="button" class="v-btn v-btn--flat" id="current-flag"><div class="v-btn__content"><img src="[object Object]" width="24px"></div></button>
console.log tests/unit/Toolbar.spec.js:38
<nav class="v-toolbar v-toolbar--fixed" style="margin-top: 0px; padding-right: 0px; padding-left: 0px; transform: translateY(0px);">
<div class="v-toolbar__content" style="height: 80px;">
<div class="v-toolbar__title">
<img src="#/assets/images/app_logo.png" alt="">
<div class="v-menu btn btn--flat v-menu--inline" style="margin-bottom: 12px;">
<div class="v-menu__activator">
<button type="button" class="v-btn v-btn--flat" id="current-flag">
<div class="v-btn__content">
<img src="[object Object]" width="24px">
</div>
</button>
</div>
</div>
</div>
<div class="spacer"></div>
<div class="v-toolbar__items">
<button type="button" class="v-btn v-btn--flat">
<div class="v-btn__content">
<i aria-hidden="true"class="v-icon v-icon--left material-icons">home</i>
<span>Home</span>
</div>
</button>
<button type="button" class="v-btn v-btn--flat">
<div class="v-btn__content">
<i aria-hidden="true" class="v-icon v-icon--left material-icons">info</i>
<span>About</span>
</div>
</button>
</div>
</div>
</nav>

I found a solution, but maybe someone can explain why :
btnFlags.at(1).vm.$emit('click'); // OK
and
btnFlags.at(1).trigger('click'); // NOT OK
this is the spec which is running fine :
import Vue from "vue";
import router from "#/router";
import Vuetify from "vuetify";
import i18n from "#/locales";
import { mount, shallowMount } from "#vue/test-utils";
import Toolbar from "#/components/shared/Toolbar.vue";
describe("Toolbar.vue", () => {
let wrapper;
beforeEach(() => {
Vue.use(Vuetify);
Vue.filter("translate", function(value) {
if (!value) return "";
value = "lang.views.global." + value.toString();
return i18n.t(value);
});
const el = document.createElement('div');
el.setAttribute('data-app', true);
document.body.appendChild(el);
});
it("should change locale", async () => {
// given
wrapper = shallowMount(Toolbar, { router, i18n });
console.log('CURRENT LOCALE INDEX: ', wrapper.vm.currentLocaleIndex);
const btnFlags = wrapper.findAll("v-list-tile-stub");
// when
btnFlags.at(1).vm.$emit('click');
await wrapper.vm.$nextTick();
// then
console.log('NEW LOCALE INDEX: ', wrapper.vm.currentLocaleIndex);
expect(wrapper.vm.currentLocaleIndex).toBe(1);
});
});

Related

braintree hosted payment fields client undefined Ember 3.25

Ember and Braintree Hosted Fields are not a good mix so far, Braintree Support are out of ideas on this one. When the form renders on the page it calls the action to create the client. The client is undefined.
picture-this-44ac48bef9f8df633632a4d202da2379.js:57 Uncaught TypeError: Cannot read property 'client' of undefined
component hbs
<script src="https://js.braintreegateway.com/web/3.81.0/js/client.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.81.0/js/hosted-fields.min.js"></script>
<div class="demo-frame" {{did-insert this.setupBraintreeHostedFields}}>
<form action="/" method="post" id="cardForm" >
<label class="hosted-fields--label" for="card-number">Card Number</label>
<div id="card-number" class="hosted-field"></div>
<label class="hosted-fields--label" for="expiration-date">Expiration Date</label>
<div id="expiration-date" class="hosted-field"></div>
<label class="hosted-fields--label" for="cvv">CVV</label>
<div id="cvv" class="hosted-field"></div>
<label class="hosted-fields--label" for="postal-code">Postal Code</label>
<div id="postal-code" class="hosted-field"></div>
<div class="button-container">
<input type="submit" class="button button--small button--green" value="Purchase" id="submit"/>
</div>
</form>
</div>
component class
import Component from '#glimmer/component';
import { action } from '#ember/object';
import { inject as service } from '#ember/service';
import { tracked } from '#glimmer/tracking';
import { braintree } from 'braintree-web';
export default class CardPaymentComponent extends Component {
#action
setupBraintreeHostedFields() {
alert('booh');
var form = document.querySelector('#cardForm');
var authorization = 'sandbox_24nzd6x7_gyvpsk2myght4c2p';
braintree.client.create({
authorization: authorization
}, function(err, clientInstance) {
if (err) {
console.error(err);
return;
}
createHostedFields(clientInstance);
});
function createHostedFields(clientInstance) {
braintree.hostedFields.create({
client: clientInstance,
styles: {
'input': {
'font-size': '16px',
'font-family': 'courier, monospace',
'font-weight': 'lighter',
'color': '#ccc'
},
':focus': {
'color': 'black'
},
'.valid': {
'color': '#8bdda8'
}
},
fields: {
number: {
selector: '#card-number',
placeholder: '4111 1111 1111 1111'
},
cvv: {
selector: '#cvv',
placeholder: '123'
},
expirationDate: {
selector: '#expiration-date',
placeholder: 'MM/YYYY'
},
postalCode: {
selector: '#postal-code',
placeholder: '11111'
}
}
}, function (err, hostedFieldsInstance) {
var tokenize = function (event) {
event.preventDefault();
hostedFieldsInstance.tokenize(function (err, payload) {
if (err) {
alert('Something went wrong. Check your card details and try again.');
return;
}
alert('Submit your nonce (' + payload.nonce + ') to your server here!');
});
};
form.addEventListener('submit', tokenize, false);
});
}
}
}
package.json
...
"ember-cli": "^3.25.2",
"braintree-web": "^3.81.0",
...
** Final Solution **
NPM braintree-web not required. Component class does not have access to the Braintree Window object. Move the tags to the app/index.html as outlined in the accepted answer.
component hbs
<article class="rental">
<form action="/" method="post" id="cardForm">
<label class="hosted-fields--label" for="card-number">Cardholder Name</label>
<div id="card-holder-name" class="hosted-field payment"></div>
<label class="hosted-fields--label" for="card-number">Email</label>
<div id="email" class="hosted-field payment"></div>
<label class="hosted-fields--label" for="card-number">Card Number</label>
<div id="card-number" class="hosted-field payment"></div>
<label class="hosted-fields--label" for="expiration-date">Expiration Date</label>
<div id="expiration-date" class="hosted-field payment"></div>
<label class="hosted-fields--label" for="cvv">CVV</label>
<div id="cvv" class="hosted-field payment"></div>
<label class="hosted-fields--label" for="postal-code">Postal Code</label>
<div id="postal-code" class="hosted-field payment"></div>
<div class="button-container">
<input type="submit" class="button" value="Purchase" id="submit"/>
</div>
</form>
</article>
<script>
var form = document.querySelector('#cardForm');
var authorization = 'sandbox_24nzd6x7_gyvpsk2myght4c2p';
braintree.client.create({
authorization: authorization
}, function(err, clientInstance) {
if (err) {
console.error(err);
return;
}
createHostedFields(clientInstance);
});
function createHostedFields(clientInstance) {
braintree.hostedFields.create({
client: clientInstance,
styles: {
'input': {
'font-size': '1.2em',
'font-family': 'courier, monospace',
'font-weight': 'lighter',
'color': '#ccc'
},
':focus': {
'color': 'black'
},
'.valid': {
'color': '#8bdda8'
}
},
fields: {
number: {
selector: '#card-number',
placeholder: '4111 1111 1111 1111'
},
cvv: {
selector: '#cvv',
placeholder: '123'
},
expirationDate: {
selector: '#expiration-date',
placeholder: 'MM/YYYY'
},
postalCode: {
selector: '#postal-code',
placeholder: '11111'
}
}
}, function (err, hostedFieldsInstance) {
var tokenize = function (event) {
event.preventDefault();
hostedFieldsInstance.tokenize(function (err, payload) {
if (err) {
alert('Something went wrong. Check your card details and try again.');
return;
}
alert('Submit your nonce (' + payload.nonce + ') to your server here!');
});
};
form.addEventListener('submit', tokenize, false);
});
}
</script>
You can use Braintree SDK via either the direct script tag or using the npm module with the help of ember-auto-import. In your case, you are using both.
For simplicity, let's use the script tag to inject the SDK. The issue in your snippet is that you are trying to load the script tag inside a component handlebar file. the handlebars (.hbs file) cannot load scripts using a <script> tag. We need to move the script tag to the index.html file present inside the app folder. This will load the SDK properly to be used inside a component.
app/index.html:
<body>
...
<script src="https://js.braintreegateway.com/web/3.81.0/js/client.min.js"></script>
<script src="https://js.braintreegateway.com/web/3.81.0/js/hosted-fields.min.js"></script>
{{content-for "body-footer"}}
</body>
Once you inject the SDK properly, you can use the braintree window object without any issue.

Unit Test using Jest. TypeError: Cannot read property 'getters' of undefined

I have a component that I want to test that uses Vuex.
components/Main/Header.vue
<template>
<v-container fluid grid-list-xl>
<v-card flat class="header" color="transparent">
<v-layout align-center justify-start row fill-height class="content">
<v-flex xs5>
<v-img :src="avatar" class="avatar" aspect-ratio="1" contain></v-img>
</v-flex>
<v-flex xs7>
<div class="main-text font-weight-black">
WELCOME
</div>
<div class="sub-text">
{{currentLocation.description}}
</div>
<div #click="showStory" class="show-more font-weight-bold">
Explore More
</div>
</v-flex>
</v-layout>
</v-card>
</v-container>
</template>
<script>
import avatar from '../../assets/images/home/avatar.png';
export default {
name: 'Header',
computed: {
currentLocation() {
return this.$store.getters.getCurrentLocation;
},
avatar() {
return avatar;
}
},
methods: {
showStory() {
this.$router.push( { name: 'Stories', params: { name: 'Our Story' } });
}
}
}
</script>
and from /test/unit/components/Main/Header.spec.js
import Vuex from 'vuex'
import { shallowMount, createLocalVue } from "#vue/test-utils"
import Header from "#/components/Main/Header.vue"
const localVue = createLocalVue()
localVue.use(Vuex)
const store = new Vuex.Store({
state: {
currentLocation: {
name: 'this is a name',
description: "lorem ipsum",
latitude: '1.123123',
longitude: '103.123123',
radius: '5000'
}
},
getters: {
getCurrentLocation: (state) => state.currentLocation
}
})
describe('Header', () => {
const wrapper = shallowMount(Header, {
store,
localVue
})
it('should render a computed property currentLocation', () => {
expect(Header.computed.currentLocation()).toBe(store.getters.getCurrentLocation)
});
});
The error I'm getting is from the computed property currentLocation
TypeError: Cannot read property 'getters' of undefined

NuxtJS , Unit Test language picker with Jest and nuxt-i18n

I have a component that switch Language of a nuxtjs application using nuxt-i18n as follows
<template>
<div class="navbar-item has-dropdown is-hoverable">
<a class="navbar-link langpicker">{{ $t("language_picker") }} </a>
<div class="navbar-dropdown is-hidden-mobile">
<div>
<nuxt-link
v-if="currentLanguage != 'en'"
class="navbar-item"
:to="switchLocalePath('en')"
>
<img src="~/static/flags/us.svg" class="flagIcon" /> English
</nuxt-link>
<nuxt-link
v-if="currentLanguage != 'el'"
class="navbar-item"
:to="switchLocalePath('el')"
>
<img src="~/static/flags/el.svg" class="flagIcon" /> Ελληνικά
</nuxt-link>
</div>
</div>
</div>
</template>
<script>
export default {
name: "LangPicker",
computed: {
currentLanguage() {
return this.$i18n.locale || "en";
}
}
};
</script>
I want to write a Unit Test that test the correct language switch on 'nuxt-link' click.
So far I have the following
import { mount, RouterLinkStub } from "#vue/test-utils";
import LangPicker from "#/components/layout/LangPicker";
describe("LangPicker with locale en", () => {
let cmp;
beforeEach(() => {
cmp = mount(LangPicker, {
mocks: {
$t: msg => msg,
$i18n: { locale: "en" },
switchLocalePath: msg => msg
},
stubs: {
NuxtLink: RouterLinkStub
}
});
});
it("Trigger language", () => {
const el = cmp.findAll(".navbar-item")
});
});
cmp.find(".navbar-item") return an empty object.
I don't know how I must set up to "trigger" the click event.
const el = cmp.findAll(".navbar-item")[1].trigger("click");
make sure your find selector is correct.
const comp = cmp.find(".navbar-item");
comp.trigger('click');
you can use chrome dev tools selector utility.
Refer this link for detailed information.

Vue.js unit test w avoriaz , how to test submit event

I am trying to test a form submit.. it seems that trigger is not appropriate
1) calls store action login when the form is submitted
LoginPage.vue
TypeError: wrapper.find(...).trigger is not a function
at Context.<anonymous> (webpack:///test/unit/specs/pages/LoginPage.spec.js:35:28 <- index.js:50826:29)
my vue component to be tested
LoginPage.vue
<template>
<div class="container">
<div class="login-page">
<h1 class="title">Login to existing account</h1>
<form #submit.prevent="submit()" class="form form--login grid">
<div class="row">
<label for="login__email">Email</label>
<input type="text" id="login__email" class="input--block" v-model="email" v-on:keyup="clearErrorMessage" />
</div>
<div class="row">
<label for="login__password">Password</label>
<input type="password" id="login__password" class="input--block" v-model="password" v-on:keyup="clearErrorMessage" />
</div><!-- /.row -->
<div class="row">
<label></label>
<button id="submit" type="submit">Login</button>
</div><!-- /.row -->
<div v-show='hasError' class="row">
<label></label>
<p class="error">Invalid credentials</p>
</div>
</form>
</div><!-- /.login-page -->
</div>
</template>
<script>
import store from '#/vuex/store'
import { mapActions } from 'vuex'
import _ from 'underscore'
export default {
name: 'loginPage',
data () {
return {
email: 'john.doe#domain.com',
password: 'john123',
hasError: false
}
},
methods: _.extend({}, mapActions(['login']), {
clearErrorMessage () {
this.hasError = false
},
submit () {
return this.login({user: { email: this.email, password: this.password }})
.then((logged) => {
if (logged) {
this.$router.push('shoppinglists')
} else {
this.hasError = true
}
})
}
}),
store
}
</script>
LoginPage.spec.js
import LoginPage from '#/pages/LoginPage'
import Vue from 'vue'
import Vuex from 'vuex'
import sinon from 'sinon'
import { mount } from 'avoriaz'
Vue.use(Vuex)
describe('LoginPage.vue', () => {
let actions
let getters
let store
beforeEach(() => {
actions = {
login: sinon.stub()
}
getters = {
isAuthenticated: () => {
state => state.isAuthenticated
}
}
store = new Vuex.Store({
actions,
getters,
state: {
isAuthenticated: false,
currentUserId: ''
}
})
})
it('calls store action login when the form is submitted', (done) => {
const wrapper = mount(LoginPage, { store })
wrapper.find('#submit').trigger('submit')
wrapper.vm.$nextTick(() => {
expect(actions.login.calledOnce).to.equal(true)
done()
})
})
})
Should be trigger 'click' on the form tag !
it('calls store action login when the form is submitted', (done) => {
const wrapper = mount(LoginPage, { store })
const form = wrapper.find('form')[0]
form.trigger('submit')
wrapper.vm.$nextTick(() => {
expect(actions.login.calledOnce).to.equal(true)
done()
})
})

Emberjs: How to refresh parent template after model create

I'm building an app that has the following code:
Routes:
App.Router.map(function() {
this.resource('gradebooks', function() {
this.resource('gradebook', { path: ':gradebook_id' });
});
});
App.IndexRoute = Em.Route.extend({
redirect: function() {
this.transitionTo('gradebooks');
}
})
App.GradebooksRoute = Em.Route.extend({
setupController: function(controller, model) {
Em.$.getJSON('/data/gradebooks/get', function(data) {
controller.set('model', data);
});
}
});
App.GradebookRoute = Em.Route.extend({
model: function(params) {
var id = params.gradebook_id;
return Em.$.getJSON('/data/gradebooks/get/' + id);
}
})
Controllers:
App.GradebooksController = Em.ArrayController.extend({
isActive: false,
actions: {
createGradebook: function(newTitle) {
var self = this;
if(newTitle.trim()) {
Em.$.get('/data/gradebooks/add/' + newTitle, {}, function(json) {
Em.$('#create-gradebook-modal').modal('hide').find('input').val('');
self.transitionToRoute('gradebook', json.id);
}, 'json');
}
}
}
});
Templates (gradebooks.hbs):
<div class="sidebar">
<div class="btn-container">
<button class="btn btn-block btn-info" data-toggle="modal" data-target="#create-gradebook-modal">
<i class="fa fa-book"></i>
Create Gradebook
</button>
</div>
<ul>
{{#each}}
<li {{bind-attr class="isActive:active"}}>
{{#link-to "gradebook" _id}}
{{title}}
{{/link-to}}
</li>
{{/each}}
</ul>
</div>
<div class="content">
<div class="container-fluid">
{{outlet}}
</div>
</div>
Templates (gradebook.hbs):
<h1>{{title}} <small>{{_id}}</small></h1>
<div class="btn-container">
<button class="btn btn-default"><i class="fa fa-pencil"></i> Rename</button>
<button class="btn btn-danger"><i class="fa fa-trash-o"></i> Trash</button>
</div>
What I'm having trouble with is under the GradebooksController.actions.createGradebook, I send a request to create the model and then I transition to the gradebook I just created. The creation and transition works just fine, but I want the sidebar (gradebooks.hbs) to show the newly created gradebook.
I think it has something to do with updating the model in the GradebooksRoute. How can I make it fetch for the models from the server again after creating a gradebook.
To elaborate on #fanta's comment, you'll want to do something like this:
App.GradebooksController = Em.ArrayController.extend({
isActive: false,
actions: {
createGradebook: function(newTitle) {
var self = this;
if(newTitle.trim()) {
var gradebook = Em.$.getJSON('/data/gradebooks/add/' + newTitle, {}, function(json) {
Em.$('#create-gradebook-modal').modal('hide').find('input').val('');
self.transitionToRoute('gradebook', json.id);
}, 'json');
self.get('content').pushObject(gradebook);
}
}
}
});