How to test plain Vue components (not single file components) - javascript

All of these tries are throwing an error:
var testUtils=require('#vue/test-utils'), Vue=require('vue');
require('jsdom-global')();
testUtils.mount(Vue.component('test', {
template:'<div>test</div>'
}));
testUtils.mount(Vue.component('test', {
render:function(el) { return el('div', 'test'); }
}));
testUtils.mount({
template:'<div>test</div>'
});
#vue/test-utils/dist/vue-test-utils.js:2471
var componentInstance = node.child;
TypeError: Cannot read property 'child' of undefined
I have also tried to use localVue, to use shallowMount instead of mount and tried to pass Vue.options.components.test after registrating it globally (and some other things that came to my mind) but nothing works.
Isn't there any way to test vue components without using single file components, webpack and/or other technologies that are making things unnecessary complicated and require a build process? Or is this just a lack of documentation?

You need to load the DOM before requiring #vue/test-utils. Change your code to this:
require('jsdom-global')();
var testUtils=require('#vue/test-utils'), Vue=require('vue');
// ...
Likely Jest loads JSDOM in some script before Vue is required, which is why it works there.

There's a simple guide for that in their GitHub repo.
https://github.com/vuejs/vue-test-utils/pull/1373/files#diff-b64ec6abf844db0ffa41aaf83deb3043f880b0beb988a14e0b2ace310848a335
require('jsdom-global')()
const assert = require('assert')
const Vue = require('vue')
const VueTestUtils = require('#vue/test-utils')
const App = Vue.component('app', {
data() {
return {
msg: 'Hello Vue Test Utils'
}
},
template: `
<div>{{ msg }}</div>
`
})
const wrapper = VueTestUtils.shallowMount(App)
assert.strictEqual('Hello Vue Test Utils', wrapper.text())

Related

Gatsby Plug-in: How Can I Take a Component as a Plug-In Option?

I'm trying to make an improvement to an existing Gatsby plug-in, and I want to pass a React Component to the plug-in, through its configuration entry in gatsby-config.js:
plugins: [
{
resolve: `gatsby-plugin-modal-routing`,
options: { someComponent: SomeComponentClassOrFunction }
},
However, the problem I'm running into is that I can't figure out how to make it work.
If I try to pass the component itself as part of the plug-in's configuration, it seems to get serialized to/from JSON, resulting in the class becoming a useless object. So it seems I have to pass a path string instead.
plugins: [
{
resolve: `gatsby-plugin-modal-routing`,
options: {
modalComponentPath: path.join(__dirname, 'src/components/SomeComponent.js')
}
},
However, if I try to pass the path instead, I can't figure out how to use it to load the component inside the plug-in. I've tried using a dynamic Node import (ie. import(path).then(component => ...)) ...
with a path that's path.join-ed with __dirname
with a relative path (src/components/SomeComponent)
with a local-path-relative path (./src/components/SomeComponent)
with and without a trailing .js
I'm not sure if this is some sort of issue with the different paths of the app vs. the plug-in or whether there's some other problem, but using import seems like an un-Gatsby-like solution anyway.
So, then I discovered the loadPage and loadPageSync functions which are passed into the plug-in ... but those failed also. Every path I try results in component coming back ... but it's a "page not found" component (presumably because the component I'm trying to pass in hasn't been added as a page).
This seems like it should be a simple question, at least to anyone who has worked on Gatsby plug-ins before: if you want a plug-in to take a component as an input (either as a function/class or as a string of a path to a module) ... how can you actually use that component in your plug-in?
All I'm looking for is a basic pattern or reference to a line in an existing Gatsby plugin that takes a component, or something simple like that (I can look up any details).
This seems like it should be a simple question
I had the same thought while trying this out myself. Oh boy.
TL:DR
// gatsby-node.js
const { DefinePlugin } = require('webpack')
const path = require('path')
exports.onCreateWebpackConfig = ({ actions }, { componentPath }) => {
actions.setWebpackConfig({
plugins: [
new DefinePlugin({
'___COMPONENT___': JSON.stringify(componentPath)
})
]
})
}
// gatsby-ssr
export const onRenderBody = ({ setPreBodyComponents }) => {
const Component = require(___COMPONENT___).default
setPreBodyComponents([<Component />])
}
Long read
Gatsby config doesn't seem to pass functions around (I could have sworn it used to), so passing a React component directly to your custom plugin is out the window. It has to be a path to your component.
// gatsby-config.js
{
resolve: 'my-custom-plugin',
options: {
componentPath: path.join(__dirname, './my-component.js')
}
}
You didn't say if you're using the component in gatsby-node or gatsby-browser/ssr, but I assume it's the later since requiring stuff dynamically in Node is dead simple:
Gatsby Node
// gatsby-node.js
function consume(component) {
const Component = require(component)
}
...although it doesn't understand JSX or ESM, but that's a different problem.
Gatsby Browser
gatsby-browser/ssr is run with webpack, so the module format is not a problem. But import(componentPath) won't work:
Dynamic expressions in import()
It is not possible to use a fully dynamic import statement, such as import(foo). Because foo could potentially be any path to any file in your system or project.
webpack doc
Ok, I suppose so something like this should work:
// gatsby-browser
import('./my-dir' + componentPath)
Nope, because webpack will try to resolve this from wherever the plugin live, i.e node_modules or plugins directory & we're not about to ask our users to put their custom components in node_modules.
What about this, then?
// gatsby-browser
import(process.cwd() + componentPath) // nope
We're right back at the beginning — webpack doesn't like full dynamic path! And also even if this works, this is a terrible idea since webpack will try to bundle the whole working directory.
Only if we could encode the path as a static string beforehand, so webpack can just read that code — like using webpack.DefinePlugin to define environment variables. Fortunately we can do that in gatsby-node.js:
// gatsby-node.js
const { DefinePlugin } = require('webpack')
const path = require('path')
exports.onCreateWebpackConfig = ({ actions }) => {
actions.setWebpackConfig({
plugins: [
new DefinePlugin({
'___CURRENT_DIR___': JSON.stringify(process.cwd())
})
]
})
}
And finally
// gatsby-browser
// eslint throw error for unknown var, so disable it
// eslint-disable-next-line
import(___CURRENT_DIR___ + componentPath) // works, but don't do this
But since we can access user options right in gatsby-node, let's just encode the whole path:
// gatsby-node.js
const { DefinePlugin } = require('webpack')
- const path = require('path')
- exports.onCreateWebpackConfig = ({ actions }) => {
+ exports.onCreateWebpackConfig = ({ actions }, { componentPath }) => {
actions.setWebpackConfig({
plugins: [
new DefinePlugin({
- '___CURRENT_DIR___': JSON.stringify(process.cwd())
+ '___COMPONENT___': JSON.stringify(componentPath)
})
]
})
}
Back in gatsby-browser.js:
// gatsby-browser
// I pick a random API to test, can't imagine why one would import a module in this API
export const onRouteUpdate = async () => {
// eslint-disable-next-line
const { default: Component } = await import(___COMPONENT___)
console.log(Component) // works
}
Gatsby SSR
For the sake of completeness, let's try the same trick in gatby-ssr:
// gatsby-ssr
export const onRenderBody = async ({ setPreBodyComponents }) => {
// const Component = require(___COMPONENT___).default
const { default: Component } = await import(___COMPONENT___)
setPreBodyComponents([<Component />])
}
...and it failed.
Why? If one's curious enough they might go and dig around Gatsby code to see how gatsby-ssr is treated differently than gatsby-browser, but alas I just don't feel like doing that.
Fear not, we still have one trick up our sleeve. Webpack's require can import module dynamically too, though not asynchronously. Since gatsby-ssr doesn't run in the browser, I couldn't care less about asynchronicity.
export const onRenderBody = ({ setPreBodyComponents }) => {
const Component = require(___COMPONENT___).default
setPreBodyComponents([<Component />]) // works
}
And now it works.
Sharing code between gatsby-ssr & gatsby-browser
Let's just say we need this component in both gatsby-ssr and gatsby-browser — would require(...) works in gatsby-browser too?
export const onRouteUpdate = async () => {
// eslint-disable-next-line
const { default: Component } = require(___COMPONENT___)
console.log(Component) // yes
}
It works.
import(..) vs require()
While import() does load stuff dynamically, it is more of a tool for code-splitting. Here's some different, other than asynchronicity:
using import('./my-dir' + componentPath) will bundle all files inside ./my-dir into a chunk. There's magic comment we can use to exclude/include stuff.
require(...) will just inline the required component into whatever chunk's calling it.

Testing Vuetify (Vue.js) - Second call on mount throws error

I am currently experiencing a behaviour when testing my Vue Application (specifically when vuetify is included). I am using Jest as Test Runner but experienced the same behaviour with mocha.
The first thing to notice is that the problem only occurs with mount from the #vue/test-utils and not shallowMount. Also it only occurs if you use mount twice (I guess the reason is the pollution of the Vue Object but more on that later).
Now my component is manly just a wrapper around a basic v-data-table with the property value bound to its items and some custom slots for checkboxes instead of text.
Now the problem. First this is what the first variant of my test looks like (it's basically how it's recommended by vuetify. take a look here. As the test itsself doesn't really matter I'll just expect true to be true here
import Vue from 'vue';
import Vuetify from 'vuetify';
import { mount, createLocalVue, shallowMount } from '#vue/test-utils';
import PermissionTable from '#/components/PermissionTable.vue';
import { expect } from 'chai';
const localVue = createLocalVue();
// Vue.use is not in the example but leaving it will cause the error that
// the data table is not registered
Vue.use(Vuetify);
describe('Permissiontable.vue', () => {
let vuetify;
let tableItems;
beforeEach(() => {
tableItems = [];
vuetify = new Vuetify();
});
it('will test the first thing',() => {
const wrapper = mount(PermissionTable, {
localVue,
vuetify,
propsData: {
value: tableItems
}
});
expect(true).to.be(true);
});
it('will test the second thing',() => {
const wrapper = mount(PermissionTable, {
localVue,
vuetify,
propsData: {
value: tableItems
}
});
expect(true).to.be(true);
});
});
Now as already commented without using Vue.use(Vuetify) I'll get the error that the component v-data-table is not registered. With it I'm left with the following behaviour
Test the first thing runs as expected and succeeds
The the second thing fails the following Error
Type Error: Cannot read property '$scopedSlots' of undefined
and fails at mount(....). To make the behaviour even weirder, if I debug and stop at this line,
run the mount manually in the debug console it fails as well on the first time with the same error.
If I run it again it works. If I for example execute this test 4 times this will happen. 1st will succeed -> 2nd fails -> 3rd and 4th will succeed again.
Now I am sure that functions behave the same way if they get the same input. So the Input to the mount must be altered by the first call. My guess is that the Vue class gets polluted somehow. So if I look at the documentation for localVue this utility is made to prevent pollution of the global Vue class. So I altered my code to
import Vue from 'vue';
import Vuetify from 'vuetify';
import { mount, createLocalVue, shallowMount } from '#vue/test-utils';
import PermissionTable from '#/components/PermissionTable.vue';
import { expect } from 'chai';
describe('Permissiontable.vue', () => {
let vuetify;
let tableItems;
let localVue;
beforeEach(() => {
tableItems = [];
localVue = createLocalVue();
vuetify = new Vuetify();
localVue.use(vuetify);
});
it('will test the first thing',() => {
const wrapper = mount(PermissionTable, {
localVue,
vuetify,
propsData: {
value: tableItems
}
});
expect(true).to.be(true);
});
it('will test the second thing',() => {
const wrapper = mount(PermissionTable, {
localVue,
vuetify,
propsData: {
value: tableItems
}
});
expect(true).to.be(true);
});
});
So I create a new Instance of localVue and vuetify for every test. and make localVue use vuetify. Now this brings me back to the error
[Vue warn]: Unknown custom element: - did you register the component correctly? For recursive components, make sure to provide the "name" option.
I also experimented with various alterations of injecting vuetify (instantiated) or Vuetify. using the global Vue.use etc. At the end I'm always left with one of those two behaviours.
Now the workouround seems to be to write each test in a single file which works but I think is really bad practice and I want to understand what exactly happens here.
I also created Issues for Vuetify and Vue-Test-Utils now as I can't imagine this is expected behaviour, as well as making a stripped down Version of my Component with a test as a Repo
rm-test-mount-fails-on-second-exec
To Reproduce:
Clone Repo
npm install
npm run test:unit
Versions:
Vue: 2.6.10
Vue-Test-Utils: 1.0.0-beta.29
Vuetify: 2.1.12
The bad news
Turns out this is indeed a bug in vue-test-utils at the moment. After I opened up issues I discovered another issue with a problem pretty
similar to mine and I'm pretty sure the root cause for this is the same as in my case.
Apperently this is due to a change that happend in the vue utils in v.beta.29
The issue can be found here #1130
The good News
There is a workaround to get your tests working again until this bug is resolved. You need to mount with the option sync: false so mounting in the top example would look like
const wrapper = mount(PermissionTable, {
localVue,
vuetify,
propsData: {
value: tableItems
},
sync: false
});
I still think this is a serious bug as identical tests should behave in the same way every time you run them no matter the settings. I will update this post as soon as there is news that this has been addressed.
The only thing that seems out of place in your first piece of code is that you are writing Vue.use(Vuetify) and also using an instance of Vuetify when doing mount.
I suggest that you keep the Vue.use(Vuetify) and mount your component like this:
const wrapper = mount(PermissionTable, {
localVue, // vuetify is removed from this object
propsData: {
value: tableItems
}
});
On a side note, unit tests generally should use shallowMount. I am not aware of your use case, but, if possible, please use it instead of mount

How to use external JavaScript objects in Vue.js methods

I'm trying to get Stripe working with my Vue.js 2 application. For PCI-DSS reasons, Stripe requires that their Javascript is always loaded from js.stripe.com. I've followed the instructions in:
How to add external JS scripts to VueJS Components
How to include a CDN to VueJS CLI without NPM or Webpack?
but I get a 'Stripe' is not defined error when I try to use the library. These solutions seemed to be aimed at merely getting a <script> tag into the output HTML (e.g. for analytics), not actually consuming the functions and objects in that script.
Here's what my component Javascript looks like:
<script>
export default {
name: "PaymentPage",
mounted() {
let stripeScript = document.createElement('script');
stripeScript.setAttribute('src', 'https://js.stripe.com/v3/');
document.head.appendChild(stripeScript);
let s = Stripe('pk_test_Fooo');
console.log(s);
}
}
</script>
I also tried adding the script tag to my public/index.html file instead, but I get the same outcome. This would probably be my preferred route, since Stripe encourages developers to import their script on all pages on the site.
<!DOCTYPE html>
<html lang="en">
<head>
// ...
<script src="https://js.stripe.com/v3/"></script>
</head>
How can I pull a script from an external CDN and use it within my component's Javascript?
I'm aware of some libraries to integrate Vue.js with Stripe (e.g. matfish2/vue-stripe and jofftiquez/vue-stripe-checkout), but the former doesn't import properly for me (I'm hitting issue #24) and the latter is built against the older Stripe API and the new version is still in beta.
You aren't giving the script time to load before checking if Stripe is there. What you need is something like this:
<script>
export default {
name: "PaymentPage",
mounted() {
let stripeScript = document.createElement('script');
stripeScript.setAttribute('src', 'https://js.stripe.com/v3/');
stripeScript.onload = () => {
let s = Stripe('pk_test_Fooo');
console.log(s);
};
document.head.appendChild(stripeScript);
}
}
</script>
Thanks to yuriy636's comment, I realised that errors were only from the linter, which presumably can't statically figure out what I'm up to.
I opted to put the script into index.html, then ensured I squashed linter errors with:
// eslint-disable-next-line no-undef
let s = Stripe('pk_test_Fooo');
In my case, I still had errors calling functions of the specific script. So it was required to specify the ¨window¨ scope. Also, if you need to access any Vue element inside the ¨onload¨function, you need a new variable for the ¨this¨ instance.
<script>
export default {
name: "PaymentPage",
mounted() {
let stripeScript = document.createElement('script');
// new variable for Vue elements.
let self = this;
stripeScript.onload = () => {
// call a script function using 'window' scope.
window.Stripe('pk_test_Fooo');
// call other Vue elements
self.otherVueMethod();
};
stripeScript.setAttribute('src', 'https://js.stripe.com/v3/');
document.head.appendChild(stripeScript);
}
}
I worked with this on Vue 2.6.
Just install the npm package npm install #stripe/stripe-js and use it like a regular import
import { loadStripe } from "#stripe/stripe-js";
export default {
async mounted() {
// init stripe
const stripe = await loadStripe('your_stripe_key_here');
this.stripe = stripe; // store the stripe instance
// access the stripe instance in your methods or where you want to use them
},
}
It's working as of 6th Jan 2022.

How to use an external non vue script in vue

I try to use an external script (https://libs.crefopay.de/3.0/secure-fields.js) which is not vue based
I added the script via -tags into index.html
But when I try to intsanciate an object, like in the excample of the script publisher.
let secureFieldsClientInstance =
new SecureFieldsClient('xxxxx',
this.custNo,
this.paymentRegisteredCallback,
this.initializationCompleteCallback,
configuration)
Vue says "'SecureFieldsClient' is not defined"
If I use this.
let secureFieldsClientInstance =
new this.SecureFieldsClient('xxxxx',
this.custNo,
this.paymentRegisteredCallback,
this.initializationCompleteCallback,
configuration)
secureFieldsClientInstance.registerPayment()
Vue says: Error in v-on handler: "TypeError: this.SecureFieldsClient is not a constructor"
My Code:
methods: {
startPayment () {
this.state = null
if (!this.selected) {
this.state = false
this.msg = 'Bitte Zahlungsweise auswählen.'
} else {
localStorage.payment = this.selected
let configuration = {
url: 'https://sandbox.crefopay.de/secureFields/',
placeholders: {
}
}
let secureFieldsClientInstance =
new SecureFieldsClient('xxxxx',
this.custNo,
this.paymentRegisteredCallback,
this.initializationCompleteCallback,
configuration)
secureFieldsClientInstance.registerPayment()
// this.$router.replace({ name: 'payment' })
}
}
}
Where is my mistake?
EDIT:
Updated the hole question
Here is a minimal Vue app for the context your provided, which works:
https://codepen.io/krukid/pen/voxqPj
Without additional details it's hard to say what your specific problem is, but most probably the library gets loaded after your method executes, so window.SecureFieldsClient is expectedly not yet defined. Or, there is some runtime error that crashes your script and prevents your method from executing. There could be some other more exotic issues, but lacking a broader context I can only speculate.
To ensure your library loads before running any code from it, you should attach an onload listener to your external script:
mounted () {
let crefPayApi = document.createElement('script')
crefPayApi.onload = () => this.startPayment()
crefPayApi.setAttribute('src', 'https://libs.crefopay.de/3.0/secure-fields.js')
document.head.appendChild(crefPayApi)
},
I found the solution.
the import was never the problem.
I had just to ignore VUEs/eslints complaining about the missing "this" via // eslint-disable-next-line and it works.
So external fuctions/opbjects should be called without "this" it seems.
let secureFieldsClientInstance =
new SecureFieldsClient('xxxxx',
this.custNo,
this.paymentRegisteredCallback,
this.initializationCompleteCallback,
configuration)
You could download the script and then use the import directive to load the script via webpack. You probably have something like import Vue from 'vue'; in your project. This just imports vue from your node modules.
It's the exact same thing for other external scripts, just use a relative path. When using Vue-CLI, you can do import i18n from './i18n';, where the src folder would contain a i18n.js
If you really want to use a CDN, you can add it like you normally would and then add it to the externals: https://webpack.js.org/configuration/externals/#externals to make it accessible from within webpack

How do you manually mock one of your own files in Jest?

I'm trying to mock an object (which I created) in Jest so I can provide default behaviour within the react component (so the real implementation isn't used)
This is my react component ChatApp (it's very straight forward)
'use strict';
var React, ChatApp, ChatPanel, i18n;
React = require('react');
ChatPanel = require('./chat_panel');
i18n = require('../support/i18n');
ChatApp = React.createClass({
render() {
return (
<div className="chat-app">
<h1>{i18n.t("app.title")}</h1>
<ChatPanel />
</div>
);
}
});
module.exports = ChatApp;
So I have a custom I18n dependency that does translations (I18n is something I've written that is a wrapper for node-polyglot).
So I want to do a basic test to see if the H1 has the correct word in it, but I don't want to set jest.dontMock() on my I18n object, because I don't want it to use the real object in the ChatApp test.
So following the basic instructions on the jest website, I created a mocks folder and created a mock file for i18n, which generates a mock from the original object and then overrides the t method and adds a method to allow me to set the return string for t.
This is the mock object
'use strict';
var i18nMock, _returnString;
i18nMock = jest.genMockFromModule('../scripts/support/i18n');
_returnString = "";
function __setReturnString(string) {
_returnString = string;
}
function t(key, options = null) {
return _returnString;
}
i18nMock.t.mockImplementation(t);
i18nMock.__setReturnString = __setReturnString;
module.exports = i18nMock;
Now in my ChatApp test I require the mock in a before each, like so:
'use strict';
var React, ChatApp, TestUtils, path;
path = '../../../scripts/components/';
jest.dontMock( path + 'chat_app');
React = require('react/addons');
ChatApp = require( path + 'chat_app');
TestUtils = React.addons.TestUtils;
describe('ChatApp', () => {
beforeEach(() => {
require('i18n').__setReturnString('Chat App');
});
var ChatAppElement = TestUtils.renderIntoDocument(<ChatApp />);
it('renders a title on the page', () => {
var title = TestUtils.findRenderedDOMComponentWithTag(ChatAppElement, 'h1');
expect(title.tagName).toEqual('H1');
expect(title.props.children).toEqual('Chat App');
});
});
If i console.log the i18n object within the test then I get the correct mocked object, the __setReturnString also gets triggered (as if I console.log in that message I see the log).
However, if I console.log the i18n object within the actual React component then it gets a Jest mock but it doesn't get my Jest mock, so the t method is an empty method that doesn't do anything, meaning the test fails.
Any ideas what I'm doing wrong?
Thanks a lot
I've had trouble getting the __mocks__ folder working as well. The way I got around it is by using the jest.setMock(); method.
In your case, you would jest.setMock('../../../scripts/i18n/', require('../__mocks__/i18n');
Obviously, I am not certain of the location of your mock and the location of the real library you're using, but the first parameter should use the path where your real module is stored and the second should use the path where your mock is stored.
This should force your module and all modules that yours require (including React) to use your manually mocked i18n module.
Jest does automatic mocking. Just i18n = require('../support/i18n') should be enough. That's why you usually have to call jest.dontMock in the first place.
You can find more information here: https://facebook.github.io/jest/docs/automatic-mocking.html
What mattykuzyk mentions in his answer did not work at all for me :(
However, what I found out seemed to be the problem for me was the setup of jest: I used moduleNameMapper in the beginning, and for some reason these are never mocked...
So for me the first step was to instead move my module name mapped folder to the moduleDirectories to get anything to work.
After that, I could simply add a __mocks__ file adjacent to the actual implementation (in my case utils/translation.js and utils/__mocks__/translation.js).
As my translations.js default exports a translation function, I also default exported my mock. The entire __mocks__/translations.js is super simply and looks like this:
export default jest.fn((key, unwrap = false) => (
unwrap && `${key}-unwrapped` || `${key}-wrapped`
))
Although I haven't tested it, adding a __setReturnString should be easy enough, for me it was sufficient to actually return my translation key. Hope this helps!

Categories

Resources