I created a component to be used as a package. We are using third party code supplied to us, a config file (initOpinionLab.js) and a .min.js file (opinionlab.min.js). I am trying to write unit tests for my component. A module that index.js is dependent upon is a minified file located here ../vendor/opinionlab.min.js.
Since this is a component that is used as a node module. I created a __mocks__ file adjacent to the node_modules directory (https://jestjs.io/docs/en/manual-mocks.html) so that when my index.spec.js file looks for this, it will look to the mocks file. How do I mock this minified module if I don't know what it does or returns? I just made this export function up.
root of app/__mocks__ /opinionlab.min.js
export const aFunctionFromOpinionLab = jest.fn(() => Promise.resolve({}))
src/index.js
import '../vendor/opinionlab.min'
import '../assets/style.css'
import initOpinionLab from './initOpinionLab'
export default {
name: 'FeedbackLink',
props: {
linkText: {
type: String,
default: 'Feedback'
},
clientId: {
type: String,
default: null
},
flow: {
type: String,
default: null
},
srcCorrelationId: {
type: String,
default: null
}
},
mounted () {
console.log(this.clientId, this.flow, this.srcCorrelationId, 'this is from mounted line 26')
initOpinionLab({
clientId: this.clientId,
flow: this.flow,
srcCorrelationId: this.srcCorrelationId
})
},
methods: {
launchOpinionLab () {
window.OOo.inlineFeedbackShow()
}
},
template: '<a #click="launchOpinionLab" class="opinionlab-link">{{ linkText }}</a>'
}
src/index.spec.js
import FeedbackLink from '#src/index'
import { shallowMount } from '#vue/test-utils'
jest.mock('../vendor/opinionlab.min.js')
describe('FeedbackLink', () => {
const wrapper = shallowMount(FeedbackLink, {
propsData: {
linkText: 'Feedback',
clientId: 'abc12345',
flow: 'NEW_USER',
srcCorrelationId: 'xyz9876'
}
})
it('[positive] should render correct contents', () => {
expect(wrapper.html()).toMatchSnapshot()
})
})
So you are trying to write unit test for a 3rd party library? This kind of goes against the idea of unit testing your code since you at some point have to assume the other code has its own unit test already. I understand you mocking the 3rd party code so you can get yours to run, but a mock should not care about how the code works, and instead just provides the needed output so your actual code can run.
I would suggest stubbing the code or just returning a known testable value so you can at minimum unit test your code, and not worry about the inner workings of the opinionlab.min.js , as the value gained from trying to parse through a minified fiel will not be worth the time.
Related
I need to set up global head in Nuxt for my app, which some subpages will overwrite. Those global head needs to contain translated data.
I created seoHead.js file with code:
import Vue from "vue";
export const $t = (sign) => Vue.prototype.$nuxt.$options.i18n.t(sign);
export default {
title: $t("seoGlobal.title"),
meta: [
{ charset: "utf-8" },
{ name: "viewport", content: "width=device-width, initial-scale=1" },
{
hid: "description",
name: "description",
content: $t("seoGlobal.description"),
},
{
hid: "ogSiteName",
name: "og:site_name",
content: "Test Page",
},
{
hid: "ogTitle",
name: "og:title",
content: $t("seoGlobal.ogTitle"),
},
(...)
],
};
I import and use this data in my index.vue and other pages like this:
import seoHead from "~/constants/seoHead";
export default {
head() {
const metaI18n = this.$nuxtI18nSeo();
const currentPath = process.env.LP_URL + this.$router.currentRoute.fullPath;
return {
...seoHead,
meta: [
{
hid: "ogLocale",
name: "og:locale",
content: metaI18n.meta[0].content,
},
{
hid: "ogLocaleAlternate",
name: "og:locale:alternate",
content: metaI18n.meta[1].content,
},
{
hid: "ogUrl",
name: "og:url",
content: currentPath,
},
],
};
},
(...)
Unfortunately, I am facing Cannot read property '$options' of undefined error. It's strange for me, because I already used export const $t = (sign) => Vue.prototype.$nuxt.$options.i18n.t(sign); code in another js file. Anyone know why this error appears? You know the best way to translate global head options?
As discussed in the comments, there seems to be a timing issue with the Nuxt lifecycle and your component: at the time your component seoHead.js is imported, Nuxt has not yet injected its $nuxt object into Vue. So an easy workaround would be to delay the execution of your $t function (which accesses $nuxt):
Change your component to export a function which returns the object, instead of directly exporting the object:
export default function() {
return {
title: $t("seoGlobal.title"),
// ...
}
}
In index.vue, change your head function to call seoHead when spreading it:
return {
...seoHead(),
// ...
This way, the code which accesses $nuxt will be executed later -- not when seoHead is imported, but only when the head function is executed. At this time, the Nuxt lifecycle hopefully has finished its startup work and the required object is in place.
As I said, this is merely a workaround; if you would be calling head immediately in index.vue, the same error would appear. So unless you find out a proper way to integrate into the Nuxt lifecycle, I suggest to also put a safeguard into your translation function:
const $t = (sign) => Vue.prototype.$nuxt
? Vue.prototype.$nuxt.$options.i18n.t(sign)
: sign
This will return the i18n key if the required infrastructure is not yet in place. Not great, but better than an exception ;)
Alternatively you might be able to directly import your i18n functionality, without going through Nuxt at all; this way you wouldn't have any dependency on the infrastructure at all -- much better.
I think what you basically need here is a mixin.
export default {
title: $t("seoGlobal.title"),
meta: this.computedMeta,
computed:{
computedMeta(){
return [....] // this contains the array of objects in meta
}
}
methods:{
yourMethod(sign){
return this.$nuxt.$options.i18n.t(sign);
}
}
};
then just import it as a mixin in whatever file you need.
Consider the following code within gatsby-config.js:
module.exports = {
plugins: [
{
resolve: `gatsby-source-fetch`,
options: {
name: `brands`,
type: `brands`,
url: `${dynamicURL}`, // This is the part I need to be dynamic at run/build time.
method: `get`,
axiosConfig: {
headers: { Accept: "text/csv" },
},
saveTo: `${__dirname}/src/data/brands-summary.csv`,
createNodes: false,
},
},
],
}
As you can see above, the URL for the source plugin is something that I need to be dynamic. The reason for this is that the file URL will change every time it's updated in the CMS. I need to query the CMS for that field and get its CDN URL before passing to the plugin.
I tried adding the following to the top of gatsby-config.js but I'm getting errors.
const axios = require("axios")
let dynamicURL = ""
const getBrands = async () => {
return await axios({
method: "get",
url: "https://some-proxy-url-that-returns-json-with-the-csv-file-url",
})
}
;(async () => {
const brands = await getBrands()
dynamicURL = brands.data.summary.url
})()
I'm assuming this doesn't work because the config is not waiting for the request above to resolve and therefore, all we get is a blank URL.
Is there any better way to do this? I can't simply supply the source plugin with a fixed/known URL ahead of time.
Any help greatly appreciated. I'm normally a Vue.js guy but having to work with React/Gatsby and so I'm not entirely familiar with it.
I had similar requirement where I need to set siteId of gatsby-plugin-matomo dynamically by fetching data from async api. After searching a lot of documentation of gatsby build lifecycle, I found a solution.
Here is my approach -
gatsby-config.js
module.exports = {
siteMetadata: {
...
},
plugins: {
{
resolve: 'gatsby-plugin-matomo',
options: {
siteId: '',
matomoUrl: 'MATOMO_URL',
siteUrl: 'GATSBY_SITE_URL',
dev: true
}
}
}
};
Here siteId is blank because I need to put it dynamically.
gatsby-node.js
exports.onPreInit = async ({ actions, store }) => {
const { setPluginStatus } = actions;
const state = store.getState();
const plugin = state.flattenedPlugins.find(plugin => plugin.name === "gatsby-plugin-matomo");
if (plugin) {
const matomo_site_id = await fetchMatomoSiteId('API_ENDPOINT_URL');
plugin.pluginOptions = {...plugin.pluginOptions, ...{ siteId: matomo_site_id }};
setPluginStatus({ pluginOptions: plugin.pluginOptions }, plugin);
}
};
exports.createPages = async function createPages({ actions, graphql }) {
/* Create page code */
};
onPreInit is a gatsby lifecycle method which is executing just after plugin loaded from config. onPreInit lifecycle hook has some built in methods.
store is the redux store where gatsby is storing all required information for build process.
setPluginStatus is a redux action by which plugin data can be modified in redux store of gatsby.
Here the important thing is onPreInit lifecycle hook has to be called in async way.
Hope this helps someone in future.
Another approach that may work for you is using environment variables as you said, the URL is known so, you can add them in a .env file rather than a CSV.
By default, Gatsby uses .env.development for gatsby develop and a .env.production for gatsby build command. So you will need to create two files in the root of your project.
In your .env (both and .env.development and .env.production) just add:
DYNAMIC_URL: https://yourUrl.com
Since your gatsby-config.js is rendered in your Node server, you don't need to prefix them by GATSBY_ as the ones rendered in the client-side needs. So, in your gatsby-config.js:
module.exports = {
plugins: [
{
resolve: `gatsby-source-fetch`,
options: {
name: `brands`,
type: `brands`,
url: process.env.DYNAMIC_URL, // This is the part I need to be dynamic at run/build time.
method: `get`,
axiosConfig: {
headers: { Accept: "text/csv" },
},
saveTo: `${__dirname}/src/data/brands-summary.csv`,
createNodes: false,
},
},
],
It's important to avoid tracking those files in your Git repository since you don't want to expose this type of data.
I've some problem, in my project I need to add Sanitize.js on my project, I've copied to my own 3rd party folder ex vendor
to import it I'm using
import {san} from '../../vendor/Sanitize' //There's No error when compiling this one
but there's an error when I run the page, I'm trying to call the function from Sanitize.js as in readme saying to use it just do like this
var s = new san.Sanitize({
elements: ['a', 'span'],
attributes: {
a: ['href', 'title'],
span: ['class']
},
protocols: {
a: { href: ['http', 'https', 'mailto'] }
}
});
s.clean_node(p);
The Error is
san.Sanitize is not a function/ class constructor
Any idea why this is happening? or did I miss something? There's no Error in compiling process, the error only occurs when I try to run the web page,
Because Sanitize.js is not a module.
Maybe you can try the following solution:
Add export default Sanitize; in end of sanitize.js.
Use import Sanitize from "./sanitize"; to import it.
Remove the following code from sanitize.js.
if ( typeof define === "function" ) {
define( "sanitize", [], function () { return Sanitize; } );
}
I'm doing a react project using create-react-app and my current goal is to load an external js file (that is hosted in iis) and use their data.
I'm getting this file throught a script in index.html like:
<script type="text/javascript" src="http://localhost:5000/GetJsFile"></script>
Example of my js file:
var data = {
vars: {
id: ****,
user ******,
name: *******,
date: *******,
}
//...
}
My question is: how can I use/access the data from js file inside a component of react js?
In your utils.js page
you must change your code to this shape:
utils page
const data = {
vars: {
id: ****,
user ******,
name: *******,
date: *******,
}
//...
}
export default data;
and after that in other component that you want to use this data write this code:
import data from '../../../../utils';
...
console.log('data', data);
The problem with loading the JSON's data from the html file directly is that it will not be available for your react code to use.
Since you are loading it from an external source, you need to use something like fetch, axios or superagent to retrieve it.
You can either use async/await or promises.
async function loadJsonFromExternal() {
const dataFromJSON = await axios.get('http://localhost:5000/GetJson');
return dataFromJSON;
}
Say you have your component.js file with something like this:
import React, { useEffect } from 'react';
export default function CoolComponent (props) {
let myName = 'Enigma';
useEffect(() => {
loadJsonFromExternal()
.then((result) => { myName = result.name });
}, [])
return (
<div>My name is: {myName}</div>
)
}
That would be the approach to do.
Could you please tell me how to test componentDidMount function using enzyme.I am fetching data from the server in componentDidMount which work perfectly.Now I want to test this function.
here is my code
https://codesandbox.io/s/oq7kwzrnj5
componentDidMount(){
axios
.get('https://*******/getlist')
.then(res => {
this.setState({
items : res.data.data
})
})
.catch(err => console.log(err))
}
I try like this
it("check ajax call", () => {
const componentDidMountSpy = jest.spyOn(List.prototype, 'componentDidMount');
const wrapper = shallow(<List />);
});
see updated code
https://codesandbox.io/s/oq7kwzrnj5
it("check ajax call", () => {
jest.mock('axios', () => {
const exampleArticles:any = {
data :{
data:['A','B','c']
}
}
return {
get: jest.fn(() => Promise.resolve(exampleArticles)),
};
});
expect(axios.get).toHaveBeenCalled();
});
error
You look like you're almost there. Just add the expect():
expect(componentDidMountSpy).toHaveBeenCalled();
If you need to check if it was called multiple times, you can use toHaveBeenCalledTimes(count).
Also, be sure to mockRestore() the mock at the end to make it unmocked for other tests.
List.prototype.componentDidMount.restore();
To mock axios (or any node_modules package), create a folder named __mocks__ in the same directory as node_modules, like:
--- project root
|-- node_modules
|-- __mocks__
Inside of there, make a file named <package_name>.js (so axios.js).
Inside of there, you'll create your mocked version.
If you just need to mock .get(), it can be as simple as:
export default { get: jest.fn() }
Then in your code, near the top (after imports), add:
import axios from 'axios';
jest.mock('axios');
In your test, add a call to axios.get.mockImplementation() to specify what it'll return:
axios.get.mockImplementation(() => Promise.resolve({ data: { data: [1, 2, 3] } });
This will then make axios.get() return whatever you gave it (in this case, a Promise that resolves to that object).
You can then do whatever tests you need to do.
Finally, end the test with:
axios.get.mockReset();
to reset it to it's default mocked implementation.