Testing VueJS fetch() using Apollo and Nuxt - javascript

I am trying to test the async fetch() function of my Vue Component. This method uses some of the props to build a query to use with our Apollo GQL instance. I'm having a hard time finding examples using a similar configuration that I am using.
Ultimately, I'd love to test the full query (building it and the return data) but if it is difficult to test the full return data, then I can settle with just testing the building process of the variables and hopefully the schema of the return data.
// ProductList.spec.js
import { shallowMount } from '#vue/test-utils'
import ProductListing from '~/components/blocks/ProductListing.vue'
import PRODUCT_LISTING_BLOCK_QUERY from '~/queries/productListingBlock.gql'
import '#testing-library/jest-dom'
const directives = {
interpolation: () => {}
}
const mocks = {
$md: {
render: value => value
}
}
describe('ProductListing', () => {
test('fetch from apollo', () => {
const testVariables = {
maxItems: 10,
matchSkus: 'ms239,mk332,as484',
matchTags: 'tag1,tag2,tag3',
matchCategories: '111,222,333,444'
}
const wrapper = shallowMount(ProductListing, {
propsData: testVariables,
stubs: ['ProductTileList'],
directives,
mocks
})
const client = this.$apollo.provider.clients.defaultClient // get an error on this line saying $apollo is undefined
const products = await client
.query({
PRODUCT_LISTING_BLOCK_QUERY,
testVariables,
context: {
clientName: 'powerchord'
}
})
.then(r => r.data.allProducts)
// const promise = wrapper.vm.fetch() // this doesn't seem to work/call my function
})
})
// ProductList.vue
<template lang="pug">
.mb-8(:id='elementId')
ProductTileList(:products="products")
</template>
<script lang="ts">
import Vue from 'vue'
import BaseBlockMixin from './BaseBlockMixin'
import query from '~/queries/productListingBlock.gql'
import { IObjectKeys } from '~/types/common-types'
import mixins from '~/utils/typed-mixins'
const Block = Vue.extend({
props: {
matchCategories: {
type: String,
default: ''
},
matchSkus: {
type: String,
default: ''
},
matchTags: {
type: String,
default: ''
},
maxItems: {
type: Number,
default: 20
}
},
data () {
return {
products: []
}
},
async fetch () {
const client = this.$apollo.provider.clients.defaultClient
const variables: IObjectKeys = {
limit: this.maxItems
}
if (this.matchSkus.length) {
variables.skus = this.matchSkus.split(',')
}
if (this.matchTags.length) {
variables.tags = this.matchTags.split(',')
}
if (this.matchCategories.length) {
variables.categories = this.matchCategories.split(',')
.map(c => parseInt(c))
.filter(c => !isNaN(c))
}
this.products = await client
.query({
query,
variables,
context: {
clientName: 'some-company'
}
})
.then((r: any) => r.data.allProducts)
}
})
export default mixins(Block).extend(BaseBlockMixin)
</script>

Related

How to load the store in vue when the app first loads

I am trying to load products from my pinia store when my Vue app first loads.
This is my app.js
import {createApp} from "vue";
import App from "./App.vue";
import router from "./Router/index";
import { createPinia } from "pinia";
createApp(App)
.use(router)
.use(createPinia())
.mount("#app")
And this is my store:
import { defineStore } from "pinia";
import axios from "axios";
const state = {
cart: [],
order: {},
customer: {},
loading: true,
error: null,
products: [],
};
export const useCartStore = defineStore("shopState", {
state: () => state,
actions: {
async getProducts() {
try {
this.loading = true;
const response = await axios.get("/api/product");
this.products = response.data.data;
this.loading = false;
} catch (error) {
this.error = error;
}
},
addToCart({ item }) {
const foundProductInCartIndex = this.cart.findIndex(
(cartItem) => item.slug === cartItem.slug
);
if (foundProductInCartIndex > -1) {
this.cart[foundProductInCartIndex].quantity += 1;
} else {
item.quantity = 1;
this.cart.push(item);
}
},
removeProductFromCart({ item }) {
this.cart.splice(this.cart.indexOf(item), 1);
},
clearCart() {
this.cart.length = 0;
},
clearCustomer() {
this.customer = {};
},
clearOrder() {
this.order = {};
},
updateCustomer(customer) {
this.customer = customer;
},
updateOrder(order) {
this.order = order;
},
getSingleProduct(slug) {
return this.products.find((product) => product.slug === slug);
},
},
getters: {
getCartQuantity() {
return this.cart.reduce(
(total, product) => total + product.quantity,
0
);
},
getOrderDetails() {
return this.order;
},
getCartContents() {
return this.cart;
},
getCustomer() {
return this.customer;
},
getCartTotal() {
return this.cart.reduce(
(total, product) => total + product.price * product.quantity,
0
);
},
},
persist: true,
});
I would like to call getProducts when the app loads. I am able to do this using Vue2 but not sure how to do this with the new composition API version of Vue. Please can someone advise how I can do this?
If you want to load the products once the app is loaded, you can use the onMounted() hook in the composition API.
https://vuejs.org/api/composition-api-lifecycle.html#onmounted
On the component where you want to load the products:
<script setup>
import { onMounted } from 'vue';
import { useCartStore } from '../stores/storeName.js';
const store = useCartStore()
onMounted(() => {
store.getProducts()
})
</script>
Note: I'm using <script setup> here. But if you're using the setup() hook you need to manually return the function
https://vuejs.org/api/composition-api-setup.html

custom hook testing: when testing, code that causes React state updates should be wrapped into act(...):

Following this tutorial https://www.richardkotze.com/coding/mocking-react-hooks-unit-testing-jest, but getting this error even though the test passes, why is this error occurring and is there something missing from the test? code copied here for convenience
PASS src/use-the-fetch.spec.js
● Console
console.error
Warning: An update to TestComponent inside a test was not wrapped in act(...).
When testing, code that causes React state updates should be wrapped into act(...):
act(() => {
/* fire events that update state */
});
/* assert on the output */
This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
at TestComponent (~/Documents/projects/my-app/node_modules/#testing-library/react-hooks/lib/helpers/createTestHarness.js:22:5)
at Suspense
at ErrorBoundary (~/Documents/projects/my-app/node_modules/react-error-boundary/dist/react-error-boundary.cjs.js:59:35)
5 | async function fetchData() {
6 | const data = await getStarWars(path); // being mocked
> 7 | setResult({ loading: false, data });
| ^
8 | }
9 | useEffect(() => {
10 | fetchData();
at console.error (node_modules/#testing-library/react-hooks/lib/core/console.js:19:7)
at printWarning (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:68:30)
at error (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:44:5)
at warnIfNotCurrentlyActingUpdatesInDEV (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:15034:9)
at setResult (node_modules/react-test-renderer/cjs/react-test-renderer.development.js:7129:9)
at fetchData (src/use-the-fetch.js:7:5)
use-the-fetch.spec.js
import { renderHook } from "#testing-library/react-hooks";
import { useTheFetch } from "./use-the-fetch";
import { getStarWars } from "./base-fetch";
jest.mock("./base-fetch");
describe("use the fetch", () => {
it("initial data state is loading and data empty", () => {
const { result } = renderHook(() => useTheFetch("people"));
expect(result.current).toStrictEqual({ loading: true, data: null });
});
it("data is fetched and loading is complete", async () => {
const fakeSWData = { result: [{ name: "Luke Skywalker" }] };
getStarWars.mockResolvedValue(fakeSWData);
const { result, waitForNextUpdate } = renderHook(() =>
useTheFetch("people")
);
await waitForNextUpdate();
expect(getStarWars).toBeCalledWith("people");
expect(result.current).toStrictEqual({
loading: false,
data: fakeSWData,
});
});
});
use-the-fetch.js
import { useState, useEffect } from "react";
import { getStarWars } from "./base-fetch";
export function useTheFetch(path) {
const [result, setResult] = useState({ loading: true, data: null });
async function fetchData() {
const data = await getStarWars(path); // being mocked
setResult({ loading: false, data });
}
useEffect(() => {
fetchData();
}, []);
return result;
}
base-fetch.js
const BASE_URL = "https://swapi.co/api/";
export async function baseFetch(url, options = {}) {
const response = await fetch(url, options);
return await response.json();
}
export const getStarWars = async (path) => baseFetch(BASE_URL + path);

getStaticPaths not creating paths

I have data folder that contains events.ts:
export const EventsData: Event[] = [
{
name: 'School-Uniform-Distribution',
images: ['/community/conferences/react-foo.png', "/community/conferences/react-foo.png"],
},
{
name: 'College-Uniform',
images: ['/community/conferences/react-foo.png', "/community/conferences/react-foo.png"],
},
];
type Event is:
export type Event = {
name: string;
images: string[];
};
I have the getStaticPath and getStaticProps methods in pages/our-contribution/[pid].tsx :
export const getStaticProps: GetStaticProps<Props> = async (context) => {
const event = EventsData;
console.log(event, "event")
return {
props: { event: event },
};
};
export async function getStaticPaths() {
// Get the paths we want to pre-render based on posts
const paths = EventsData.map(event => ({
params: {pid: event.name},
}));
console.log(paths, "paths")
// We'll pre-render only these paths at build time.
return {paths, fallback: false}
}
I get this error:
Can you help me, please ?
Update:
This is the error trace for one route:
pages/our-contribution/[pid].tsx:
import { useRouter } from 'next/router';
import { GetStaticProps } from 'next';
import { Event } from 'types/event';
import {EventsData} from 'data/events';
type Props = {
event: Event[];
};
const Event = ({event} : Props) => {
const router = useRouter()
const { pid } = router.query
return <p>Event: {event}</p>
}
export const getStaticProps: GetStaticProps<Props> = async (context) => {
const event = EventsData;
console.log(event, "event")
return {
props: { event: event },
};
};
export async function getStaticPaths() {
// Get the paths we want to pre-render based on posts
const paths = EventsData.map(event => ({
params: {pid: event.name},
}));
console.log(paths, "paths")
// We'll pre-render only these paths at build time.
return {paths, fallback: false}
}
export default Event
I think there are a couple of errors in the code.
One error that block your build is
const Event = ({event} : Props) => {
const router = useRouter()
const { pid } = router.query
return <p>Event: {event}</p>
}
You can't directly print an object or array in tsx, you should convert the object first into string if you are trying to debug it. Something like:
return <p>Event: {event.toString()}</p>
Then a i notice something strange in your variables name event props looks like a single event but instead you give an array of events i don't know if it is correct but maybe it should be like this:
type Props = {
event: Event;
};
or it should be named:
type Props = {
events: Event[];
};

Timeout - Async callback was not invoked within the 5000ms

I created this hook:
import { useQuery, gql } from '#apollo/client';
export const GET_DECIDER = gql`
query GetDecider($name: [String]!) {
deciders(names: $name) {
decision
name
value
}
}
`;
export const useDecider = (name) => {
const { loading, data } = useQuery(GET_DECIDER, { variables: { name } });
console.log('loading:', loading);
console.log('data:', data);
return { enabled: data?.deciders[0]?.decision, loading };
};
Im trying to test it with react testing library:
const getMock = (decision) => [
{
request: {
query: GET_DECIDER,
variables: { name: 'FAKE_DECIDER' },
},
result: {
data: {
deciders: [{ decision }],
},
},
},
];
const FakeComponent = () => {
const { enabled, loading } = useDecider('FAKE_DECIDER');
if (loading) return <div>loading</div>;
console.log('DEBUG-enabled:', enabled);
return <div>{enabled ? 'isEnabled' : 'isDisabled'}</div>;
};
// Test
import React from 'react';
import { render, screen, cleanup, act } from '#testing-library/react';
import '#testing-library/jest-dom';
import { MockedProvider } from '#apollo/client/testing';
import { useDecider, GET_DECIDER } from './useDecider';
describe('useDecider', () => {
afterEach(() => {
cleanup();
});
it('when no decider provided - should return false', async () => {
render(<MockedProvider mocks={getMock(false)}>
<FakeComponent />
</MockedProvider>
);
expect(screen.getByText('loading')).toBeTruthy();
act((ms) => new Promise((done) => setTimeout(done, ms)))
const result = screen.findByText('isDisabled');
expect(result).toBeInTheDocument();
});
});
I keep getting this error:
Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

Test redux actions that calls an API

What is the best way to test this function
export function receivingItems() {
return (dispatch, getState) => {
axios.get('/api/items')
.then(function(response) {
dispatch(receivedItems(response.data));
});
};
}
this is currently what I have
describe('Items Action Creator', () => {
it('should create a receiving items function', () => {
expect(receivingItems()).to.be.a.function;
});
});
From Redux “Writing Tests” recipe:
For async action creators using Redux Thunk or other middleware, it’s best to completely mock the Redux store for tests. You can still use applyMiddleware() with a mock store, as shown below (you can find the following code in redux-mock-store). You can also use nock to mock the HTTP requests.
function fetchTodosRequest() {
return {
type: FETCH_TODOS_REQUEST
}
}
function fetchTodosSuccess(body) {
return {
type: FETCH_TODOS_SUCCESS,
body
}
}
function fetchTodosFailure(ex) {
return {
type: FETCH_TODOS_FAILURE,
ex
}
}
export function fetchTodos() {
return dispatch => {
dispatch(fetchTodosRequest())
return fetch('http://example.com/todos')
.then(res => res.json())
.then(json => dispatch(fetchTodosSuccess(json.body)))
.catch(ex => dispatch(fetchTodosFailure(ex)))
}
}
can be tested like:
import expect from 'expect'
import { applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
import * as actions from '../../actions/counter'
import * as types from '../../constants/ActionTypes'
import nock from 'nock'
const middlewares = [ thunk ]
/**
* Creates a mock of Redux store with middleware.
*/
function mockStore(getState, expectedActions, done) {
if (!Array.isArray(expectedActions)) {
throw new Error('expectedActions should be an array of expected actions.')
}
if (typeof done !== 'undefined' && typeof done !== 'function') {
throw new Error('done should either be undefined or function.')
}
function mockStoreWithoutMiddleware() {
return {
getState() {
return typeof getState === 'function' ?
getState() :
getState
},
dispatch(action) {
const expectedAction = expectedActions.shift()
try {
expect(action).toEqual(expectedAction)
if (done && !expectedActions.length) {
done()
}
return action
} catch (e) {
done(e)
}
}
}
}
const mockStoreWithMiddleware = applyMiddleware(
...middlewares
)(mockStoreWithoutMiddleware)
return mockStoreWithMiddleware()
}
describe('async actions', () => {
afterEach(() => {
nock.cleanAll()
})
it('creates FETCH_TODOS_SUCCESS when fetching todos has been done', (done) => {
nock('http://example.com/')
.get('/todos')
.reply(200, { todos: ['do something'] })
const expectedActions = [
{ type: types.FETCH_TODOS_REQUEST },
{ type: types.FETCH_TODOS_SUCCESS, body: { todos: ['do something'] } }
]
const store = mockStore({ todos: [] }, expectedActions, done)
store.dispatch(actions.fetchTodos())
})
})
I would use a stub axios (for example by using mock-require) and write a test that actually calls receivingItems()(dispatch, getState) and makes sure dispatch is called with the correct data.
I solved this in a different way: injecting axios as a dependency of action. I prefer this approach over 'rewiring' dependencies.
So I used the same approach of testing redux-connected components. When I export actions I export two versions: one with (to be used for components) and one without (for testing) binding dependencies.
Here is how my actions.js file looks like:
import axios from 'axios'
export const loadDataRequest = () => {
return {
type: 'LOAD_DATA_REQUEST'
}
}
export const loadDataError = () => {
return {
type: 'LOAD_DATA_ERROR'
}
}
export const loadDataSuccess = (data) =>{
return {
type: 'LOAD_DATA_SUCCESS',
data
}
}
export const loadData = (axios) => {
return dispatch => {
dispatch(loadDataRequest())
axios
.get('http://httpbin.org/ip')
.then(({data})=> dispatch(loadDataSuccess(data)))
.catch(()=> dispatch(loadDataError()))
}
}
export default {
loadData: loadData.bind(null, axios)
}
Then testing with jest (actions.test.js):
import { loadData } from './actions'
describe('testing loadData', ()=>{
test('loadData with success', (done)=>{
const get = jest.fn()
const data = {
mydata: { test: 1 }
}
get.mockReturnValue(Promise.resolve({data}))
let callNumber = 0
const dispatch = jest.fn(params =>{
if (callNumber===0){
expect(params).toEqual({ type: 'LOAD_DATA_REQUEST' })
}
if (callNumber===1){
expect(params).toEqual({
type: 'LOAD_DATA_SUCCESS',
data: data
})
done()
}
callNumber++
})
const axiosMock = {
get
}
loadData(axiosMock)(dispatch)
})
})
When using the actions inside a component I import everything:
import Actions from './actions'
And to dispatch:
Actions.loadData() // this is the version with axios binded.

Categories

Resources