Jest - How to test GoogleMapsApiLoader - vue.js

I am using the google map api loader for get the google map config details.
Ref: https://v2.vuejs.org/v2/cookbook/practical-use-of-scoped-slots.html#1-Create-a-component-that-initializes-our-map
I am trying to cover unit test case for this scenario.
googleMap.vue
async mounted() {
const googleMapApi = await GoogleMapsApiLoader({
apiKey: this.apiKey
})
this.google = googleMapApi
this.initializeMap()
},
googleMap.spec.js
jest.mock('google-maps-api-loader', () => {
return { ... }
});
require('google-maps-api-loader');
const wrapper = shallowMount(GoogleMap, {
propsData: {
mapConfig: {},
apiClient: 'apiclient-test-id',
apiChannel: 'apichannel-test-id'
},
mocks: {
mocksData
}
});
describe('googlemap.vue', () => {
it('should all the elements rendered', async() => {
wrapper = mountGoogleMap();
});
});
Those given line was not covering.. I am struggling to write unit test case for this file

Related

How to test complex async reducers with Jest

I have such kinds of reducers that use fetch API as its base ultimately:
export const fetchRelatedFamilies = () => {
return (dispatch, getState) => {
if (isEmpty(getState().relatedFamiliesById)) {
dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
new HttpRequestHelper('/api/related_families',
(responseJson) => {
dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
},
e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }),
).get()
}
}
}
Code for HttpRequestHelper is here: https://github.com/broadinstitute/seqr/blob/master/ui/shared/utils/httpRequestHelper.js
Here is how I am trying to test it (but its not working):
import configureStore from 'redux-mock-store'
import fetchMock from 'fetch-mock'
import thunk from 'redux-thunk'
import { cloneDeep } from 'lodash'
import { fetchRelatedFamilies, REQUEST_RELATED_FAMILIES_BY_ID, RECEIVE_RELATED_FAMILIES_BY_ID } from 'redux/rootReducer'
import { STATE1 } from '/shared/components/panel/fixtures.js'
describe('fetchRelatedFamilies', () => {
const middlewares = [thunk]
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
store.dispatch(fetchRelatedFamilies())
// need to mimick wait for async actions to be dispatched
//await new Promise((r) => setTimeout(r, 200));
expect(store.getActions()).toEqual(expectedActions)
}
afterEach(() => {
fetchMock.reset()
fetchMock.restore()
})
it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', () => {
const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
fetchMock
.getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })
STATE1.relatedFamiliesById = {}
const expectedActions = [
{ type: REQUEST_RELATED_FAMILIES_BY_ID },
{ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
]
testActionsDispatch(STATE1, expectedActions)
})
})
I don't see { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById } in the resulting store actions, so I tried to use the trick: await new Promise((r) => setTimeout(r, 200)); in hope that it's the issue with async fetch but what it causes is that test will pass no matter what expected actions are as if the code that is following await is completely being ignored. I can't use store.dispatch(fetchRelatedFamilies()).then(... probably because Promise is not returned, and I am getting then access of undefined error. I tried to use waitFor from the library: https://testing-library.com/docs/guide-disappearance/ but I am having really big troubles installing the library itself due to the nature of the project itself and its version, so I need to avoid it still somehow.
So, the only question that I have is how I can make the action dispatched inside the async reducer to appear, in this case - { type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }.
The problem with the current code is that although you are awaiting for 200ms in your testActionsDispatch helper method (so that the mocked promise is resolved), you are not awaiting in the test code for that promise of 200ms to resolve.
In order to do that you have to declare your test as async and await for the execution of the testActionsDispatch code:
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
store.dispatch(fetchRelatedFamilies())
// need to mimick wait for async actions to be dispatched
await new Promise((r) => setTimeout(r, 200));
expect(store.getActions()).toEqual(expectedActions)
}
// Note that the test is declared as async
it('Dispatches correct actions when data - relatedFamiliesById - is absent in state', async () => {
const relatedFamiliesById = cloneDeep(STATE1.relatedFamiliesById)
fetchMock
.getOnce('/api/related_families', { body: relatedFamiliesById, headers: { 'content-type': 'application/json' } })
STATE1.relatedFamiliesById = {}
const expectedActions = [
{ type: REQUEST_RELATED_FAMILIES_BY_ID },
{ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: relatedFamiliesById }
]
// Await the execution of the helper code
await testActionsDispatch(STATE1, expectedActions)
})
Now that should work, but we are adding a delay of 200ms in every test that uses this testActionsDispatch helper. That can end up adding a lot of time when you launch your test and ultimately at a logical level is not really ensuring that the promise resolves.
A better approach is to return the promise in your reducer so we can wait for it to resolve directly in the test (I'm assuming the get method from HttpRequestHelper returns the promise created by fetch and returning it):
export const fetchRelatedFamilies = () => {
return (dispatch, getState) => {
if (isEmpty(getState().relatedFamiliesById)) {
dispatch({ type: REQUEST_RELATED_FAMILIES_BY_ID })
return new HttpRequestHelper('/api/related_families',
(responseJson) => {
dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, updates: responseJson.relatedFamiliesById })
},
e => dispatch({ type: RECEIVE_RELATED_FAMILIES_BY_ID, error: e.message, updates: {} }),
).get()
}
}
}
Then, in your helper you can simply await for this returned promise to resolve:
const testActionsDispatch = async (currstate, expectedActions) => {
const store = configureStore(middlewares)(currstate)
// Await for the promise instead of awaiting a random amount of time.
await store.dispatch(fetchRelatedFamilies())
expect(store.getActions()).toEqual(expectedActions)
}

Vue test method function which changes data

I just want to test a particular function that changes the data of the Vue component and doesn't return anything
data() {
return {
collections: [],
inlineSubtitle: '',
loading: false,
}
},
async loadcsv(element) {
const reader = new FileReader()
const file = element[0].file
this.toggleLoading()
reader.onload = async (e) => {
try {
const results = e.target.result
let resultSplit = results.split('\n')
let table = resultSplit.join('\n')
const rows = await d3.csvParse(table)
await Promise.all(
rows.map(async (row) => {
if (!(this.getSerial(row).length == 12)) {
if (!this.getName(row)) {
throw `Please enter Serial number`
}
if (!row.country) {
throw `Country for ${this.getName(
row,
)} is empty.`
}
//Adding row to collections
}
} catch (err) {
this.toggleLoading()
this.collections = []
this.inlineNotification('error', 'Unable to process CSV', `${err}.`)
}
I want to check in the unit test if the length of the collection array increase upon the right csv input or if there's an error if the notification fails
describe('ModalAddCollectionCSV', () => {
it('Test load csv function', async () => {
const localVue = createLocalVue()
const wrapper = mount(ModalAddCollectionCSV, {
localVue,
propsData: {
visible: true,
},
})
const fileDict = [{ file: new Blob([wrongDateFormat], { type: 'text/csv;charset=utf-8;' }) }]
await wrapper.vm.loadcsv(fileDict)
expect(wrapper.vm.inlineSubtitle).toContain('Error')
})
})

vue unit test get data of component

I am testing if a function works,
The function doesn't return anything but infact changes the data in the component
This is the test I am trying to write
describe('ModalAddCollectionCSV', () => {
it('Test load csv function', () => {
const localVue = createLocalVue()
const wrapper = shallowMount(AddCSV, {
localVue,
propsData: {
visible: true,
},
})
var fileDict = [{ file: new Blob([wrongDateFormat]) }]
wrapper.vm.loadcsv(fileDict)
expect(wrapper.html()).toContain('error')
})
})
I am expecting some changes to the data of the component
data() {
return {
collections: [],
inlineVisibility: false,
inlineTitle: '',
inlineSubtitle: '',
inlineKind: 'info',
loading: false,
}
},
This is the function I am testing in unit test, as you see it doesn't return anything just changes the data at the end of it
async loadcsv(element) {
const reader = new FileReader()
const file = element[0].file
this.toggleLoading()
reader.onload = async (e) => {
try {
//Normalising headers
const results = e.target.result
let resultSplit = results.split('\n')
const header = resultSplit[0]
.toLowerCase()
.replace(/[^a-zA-Z,]/g, '')
.trim()
resultSplit[0] = header
let table = resultSplit.join('\n')
const rows = await d3.csvParse(table)
await Promise.all(
rows.map(async (row) => {
if (!(this.getSerial(row).length == 9)) {
if (!this.getName(row)) {
throw `Please enter Person of interest`
}
if (!row.country) {
throw `Country for ${this.getName(
row,
)} is empty`
}
}
)
this.inlineVisibility = false
this.$emit('rowEmit', {
collections: this.row,
})
this.toggleLoading()
this.collections = []
this.$refs.modal.hide()
} catch (err) {
this.collections = []
this.inlineNotification('error', 'Unable to process CSV', `${err}.`)
}
}
reader.readAsText(file)
},
Is there a way to check emit action or error action?
I tried wrapper.text()
Wrapper.html()
but no difference

How to refactor my test to use Jest's Manual Mock feature?

I am able to get a basic test working with Jest, but when I try to refactor it to use Jest's manual mocks features, the test no longer works.
Any ideas what I could be doing wrong?
Thank you for your time 🙏
error message:
TypeError: _backendService.default.post is not a function
16 |
17 | return $axios
> 18 | .post(`${RESOURCE_PATH}/batch_upload/`, formData, {
| ^
19 | headers: {
20 | "Content-Type": "multipart/form-data",
21 | },
in tests/.../actions.spec.js:
//import $axios from "#/services/backend-service"; // could not get manual mock to work
import actions from "#/store/modules/transactions/actions";
//jest.mock("#/services/backend-service"); // could not get manual mock to work
// this bit of code works
jest.mock("#/services/backend-service", () => {
return {
post: jest.fn().mockResolvedValue(),
};
});
// this bit of code works:end
describe("store/modules/transactions/actions", () => {
it("uploads transactions succeeds", async() => {
const state = {
commit: jest.fn(),
};
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'}
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToSucceeded');
});
});
in src/.../__mocks__/backend-service.js:
const mock = jest.fn().mockImplementation(() => {
return {
post: jest.fn().mockResolvedValue(),
};
});
export default mock;
in src/.../backend-service.js:
import axios from "axios";
const API_BASE_URL =
`${process.env["VUE_APP_BACKEND_SCHEME"]}` +
`://` +
`${process.env["VUE_APP_BACKEND_HOST"]}` +
`:` +
`${process.env["VUE_APP_BACKEND_PORT"]}` +
`/` +
`${process.env["VUE_APP_BACKEND_PATH_PREFIX"]}`;
const $axios = axios.create({
baseURL: API_BASE_URL,
headers: {
"Content-Type": "application/vnd.api+json",
},
});
export default $axios;
in src/.../actions.js:
import $axios from "#/services/backend-service";
const RESOURCE_NAME = "transaction";
const RESOURCE_PATH = `${RESOURCE_NAME}s`;
export const actions = {
uploadTransactions(state, payload) {
let formData = new FormData();
formData.append("file", payload["file"]);
return $axios
.post(`${RESOURCE_PATH}/batch_upload/`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
state.commit("changeUploadStatusToSucceeded");
})
.catch(function (error) {
if (error.response) {
state.commit("changeUploadStatusToFailed");
}
});
},
};
export default actions;
I've tried looking at examples from these resources, but nothing worked for me:
mocking Axios Interceptors: Mocking axios with Jest throws error “Cannot read property 'interceptors' of undefined”
overriding mock implmentations:
Mock.mockImplementation() not working
How do I change the mock implementation of a function in a mock module in Jest
How to change mock implementation on a per single test basis [Jestjs]
Jest Mock Documentation: https://jestjs.io/docs/mock-function-api#mockfnmockimplementationfn
Jest Manual Mock documentation:
https://jestjs.io/docs/es6-class-mocks
https://jestjs.io/docs/manual-mocks#examples
using 3rd party libraries: https://vhudyma-blog.eu/3-ways-to-mock-axios-in-jest/
simple actions test example: https://lmiller1990.github.io/vue-testing-handbook/vuex-actions.html#creating-the-action
outdated actions test example:
https://vuex.vuejs.org/guide/testing.html#testing-actions
In case it helps others, I ended up just using spies and did not need to use manual mocks.
These were references that helped me figure it out:
https://silvenon.com/blog/mocking-with-jest/functions
https://silvenon.com/blog/mocking-with-jest/modules/
How to mock jest.spyOn for a specific axios call
And here's the example code that ended up working for me:
in tests/.../actions.spec.js:
import $axios from "#/services/backend-service";
import actions from "#/store/modules/transactions/actions";
describe("store/modules/transactions/actions", () => {
let state;
let postSpy;
beforeEach(() => {
state = {
commit: jest.fn(),
};
postSpy = jest.spyOn($axios, 'post')
});
it("uploads transactions succeeds", async() => {
postSpy.mockImplementation(() => {
return Promise.resolve();
});
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'},
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToSucceeded');
});
it("uploads transactions fails", async() => {
postSpy.mockImplementation(() => {
return Promise.reject({
response: true,
});
});
await actions.uploadTransactions(
state,
{'file': 'arbitrary filename'},
)
expect(state.commit).toHaveBeenCalledWith('changeUploadStatusToFailed');
});
});
in src/.../actions.js:
import $axios from "#/services/backend-service";
const RESOURCE_NAME = "transaction";
const RESOURCE_PATH = `${RESOURCE_NAME}s`;
export const actions = {
uploadTransactions(state, payload) {
let formData = new FormData();
formData.append("account_id", 1); // change to get dynamically when ready
formData.append("file", payload["file"]);
//$axios.interceptors.request.use(function (config) {
// state.commit("changeUploadStatusToUploading");
// return config;
//});
return $axios
.post(`${RESOURCE_PATH}/batch_upload/`, formData, {
headers: {
"Content-Type": "multipart/form-data",
},
})
.then((response) => {
console.log(response);
state.commit("changeUploadStatusToSucceeded");
})
.catch(function (error) {
if (error.response) {
state.commit("changeUploadStatusToFailed");
}
});
},
};
export default actions;

Restarting express server in esbuild

I am trying to create a simple express server with esbuild. These are my code
import express from "express";
const app = express();
const port = 3000;
const stopServer = {
stop: () => {},
};
export const createServer = async () => {
app.get("/", async (req, res) => {
res.json({
first: "Hello",
});
});
const server = app.listen(port, () => {
console.log(`Listening on port: ${port}`);
});
stopServer.stop = () => {
server.close();
};
};
export const stop = () => {
stopServer.stop();
stopServer.stop = () => {};
};
esbuild.config.js
const esbuild = require("esbuild");
const path = require("path");
const restartPlugin = () => {
return {
name: "restart-express",
setup(build) {
build.onEnd(async (res) => {
const { stop, createServer } = await import("../dist/server.js");
stop();
createServer();
});
},
};
};
const run = async () => {
await esbuild.build({
entryPoints: [path.resolve(__dirname, "../src/server.ts")],
outdir: path.resolve(__dirname, "../dist"),
platform: "node",
sourcemap: true,
format: "cjs",
watch: {
onRebuild: async (err, res) => {
if (err) {
console.error(err);
} else {
console.log("There is some change");
}
},
},
plugins: [restartPlugin()],
});
};
run();
Reference for plugin : https://github.com/evanw/esbuild/issues/1258#issuecomment-834676530
If you were to run this application It i will work initially but when you change the code, the server wont get updated even if you refresh the page.
I am not really sure where I am making mistake, Any help please
The problem is that node cache the import("..dist/server.js"), as a result it will never return new module. To solve this problem we will write a function
const purgeAppRequireCache = (buildPath) => {
for (let key in require.cache) {
if (key.startsWith(buildPath)) {
delete require.cache[key];
}
}
};
Which will remove the cache from the node. We can also use this function in this manner. Which solves my problem
const esbuild = require("esbuild");
const path = require("path");
const startPlugin = () => {
return {
name: "startPlugin",
setup(build) {
build.onEnd((res) => {
const serverPath = path.resolve(__dirname, "../dist/server.js");
const { stop } = require("../dist/server.js");
stop();
purgeAppRequireCache(serverPath);
purgeAppRequireCache(path.resolve(__dirname, "../src"));
const { listen } = require("../dist/server");
listen();
});
},
};
};
const run = async () => {
await esbuild.build({
entryPoints: [path.resolve(__dirname, "../src/server.tsx")],
outdir: path.resolve(__dirname, "../dist"),
platform: "node",
sourcemap: true,
format: "cjs",
watch: true,
bundle: true,
plugins: [startPlugin()],
});
};
run();
const purgeAppRequireCache = (buildPath) => {
for (let key in require.cache) {
if (key.startsWith(buildPath)) {
delete require.cache[key];
}
}
};
If you not reload runtime, the global's object and sub require(xxx) maby have same error.
You can use kill and fork cluster when change you code, it's same fast like require(xxx), there have example codes: https://github.com/ymzuiku/bike/blob/master/lib/index.js
If you need see kill and fork cluster example, here's a same feature package, also use esbuild, but it use fs.watch: https://www.npmjs.com/package/bike
Hope there could help you :)
#es-exec/esbuild-plugin-serve or #es-exec/esbuild-plugin-start are two alternative esbuild plugins that you can try. They run your bundles or any command line script for you after building your project (supports watch mode for rebuilding on file changes).
The documentation can be found at the following:
#es-exec/esbuild-plugin-serve
#es-exec/esbuild-plugin-start
Disclaimer: I am the author of these packages.