In a Laravel + Vue3 + Inertia project which setup using Laravel Mix, how we can create front-end tests?
Especially, I have no idea how to handle Inertia's Share Data, usePage() and useForm methods?
The first error I'm facing is:
TypeError: Cannot read properties of undefined (reading 'someSharedData')
2 |
3 | export const handleSometing = (something) =>
> 4 | usePage().props.value.someSharedData
| ^
5 | ...
6 | )
After googling some useless hours and finding nothing to this exact problem, I've found this solution.
The key was in Jest Partial Mocking!
You can mock useForm, usePage, and then Shared Data using Jest Partial Mocking.
After setup the vue-test-util, I have created this test file and it was working like a charm.
In the below example, the i18n is mocked using the config object of the vue-test-utils.
The Inertia's methods are mocked by jest.mock().
import { config, shallowMount } from '#vue/test-utils'
import Dashboard from '#/Components/ExampleComponent'
config.global.mocks = {
$t: () => '',
}
jest.mock('#inertiajs/inertia-vue3', () => ({
__esModule: true,
...jest.requireActual('#inertiajs/inertia-vue3'), // Keep the rest of Inertia untouched!
useForm: () => ({
/** Return what you need **/
/** Don't forget to mock post, put, ... methods **/
}),
usePage: () => ({
props: {
value: {
someSharedData: 'something',
},
},
}),
}))
test('Render ExampleComponent without crash', () => {
const wrapper = shallowMount(ExampleComponent, {
props: {
otherPageProps: {}
}
})
expect(wrapper.text()).toContain('Hi! I am ExampleComponent.')
})
Related
I'm trying to make application lavevel + vue with server side render. I have found this manual and it works perfect. Bu there is a small problem. I need fetch data before page loading for SEO issues and I found official vue ssr manual for prefetch. But it does not work. I only see error in the console
entry-client.js:6952 [Vue warn]: Cannot find element: #app.
my entry-server.js
import {createApp} from './app'
export default context => {
return new Promise((resolve, reject) => {
const {app, router, store} = createApp();
router.push(context.url)
router.onReady(() => {
// This `rendered` hook is called when the app has finished rendering
context.rendered = () => {
context.state = store.state
}
resolve(app)
}, reject)
}).then(app => {
renderVueComponentToString(app, (err, res) => {
print(res);
});
})
.catch((err) => {
print(err);
})
}
Are there any idea how solve this problem?
Looks like Promise does not work
Laravel 5.7 and Vue 2.6.6
I have looked at the following tutorials https://hackernoon.com/unit-testing-redux-connected-components-692fa3c4441c https://airbnb.io/enzyme/docs/api/shallow.html and tried to create a shallow rendered test of a component but i have actions being triggered on render which collect data and help render the component. how can i mock this?
tests/jest/containers/homecontent.js
import configureStore from 'redux-mock-store'
import { shallow } from 'enzyme';
import { HomeContent } from '../../../app/containers/home';
const passMetaBack = meta => {
this.setState({
title: 'test',
description: 'test'
});
};
// create any initial state needed
const initialState = {};
// here it is possible to pass in any middleware if needed into //configureStore
const mockStore = configureStore();
describe('Login Component', () => {
let wrapper;
let store;
beforeEach(() => {
// our mock login function to replace the one provided by mapDispatchToProps
const mockLoginfn = jest.fn();
//creates the store with any initial state or middleware needed
store = mockStore(initialState)
wrapper = shallow(<HomeContent isGuest={false} isReady={true} priv={{}} passMetaBack={passMetaBack} fetchContents={mockLoginfn} />)
});
it('+++ render the DUMB component', () => {
expect(wrapper.length).toEqual(1)
});
});
The error i get is
FAIL tests/jest/containers/homecontent.test.js
Login Component
✕ +++ render the DUMB component (25ms)
● Login Component › +++ render the DUMB component
TypeError: Cannot read property 'then' of undefined
38 | if(this.props.isReady && this.props.priv != undefined){
39 | let self = this;
> 40 | this.props.fetchContents()
41 | .then(response => {
42 | let data = response.payload.data;
43 | if (data.header.error) {
at HomeContent.initData (app/containers/home.js:40:7)
at HomeContent.render (app/containers/home.js:71:12)
at ReactShallowRenderer._mountClassComponent (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:195:37)
at ReactShallowRenderer.render (node_modules/react-test-renderer/cjs/react-test-renderer-shallow.development.js:143:14)
at node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:287:35
at withSetStateAllowed (node_modules/enzyme-adapter-utils/build/Utils.js:103:16)
at Object.render (node_modules/enzyme-adapter-react-16/build/ReactSixteenAdapter.js:286:68)
at new ShallowWrapper (node_modules/enzyme/build/ShallowWrapper.js:119:22)
at shallow (node_modules/enzyme/build/shallow.js:19:10)
at Object.<anonymous> (tests/jest/containers/homecontent.test.js:24:19)
● Login Component › +++ render the DUMB component
TypeError: Cannot read property 'length' of undefined
26 |
27 | it('+++ render the DUMB component', () => {
> 28 | expect(wrapper.length).toEqual(1)
29 | });
30 | });
31 |
at Object.<anonymous> (tests/jest/containers/homecontent.test.js:28:24)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 2.218s
Ran all test suites matching /tests\/jest\/containers\/homecontent.test.js/i.
this.props.fetchContents() comes in from an action on the component
mockLoginfn is used as this.props.fetchContents in the component. fetchContents is a function that returns a promise, whereas mockLoginfn is a jest mock function that doesn't return anything.
So, you need to provide a mock implementation for the mockLoginfn so it behaves like a promise. For example (using the code snippet above):
const mockLoginfn = jest.fn();
mockLoginfn.mockImplementation(() => Promise.resolve({
payload: {
data: {
header: {
error: 'some error'
}
}
}
}));
I am writing unit testing for a vuejs 2 application that uses Vuex as a store. I have the following pattern in many of my components:
example component thing.vue:
<template>
<div>
{{ thing.label }}
</div>
</template>
<script>
export default {
name: 'thing',
data() { return { } },
computed: {
thing () {
return this.$store.state.thing;
}
}
}
</script>
Example Store State:
export const state = {
thing: { label: 'test' }
};
Example Unit for Thing.vue:
describe('thing ', () => {
const storeMock = new Vuex.Store( state: { thing: { label: 'test' } } );
it('should pull thing from store', () => {
const Constructor = Vue.extend(thing);
const component new Constructor({ store }).$mount();
expect(component.thing).toEqual({ label: 'test' });
});
});
Example Unit test for Store:
import store from './store';
describe('Vuex store ', () => {
it('should have a thing object', () => {
expect(store.state.thing).toEqual({ label: 'test' });
});
});
There is a huge problem with this pattern. When another developer refractors the store state, they will see the Store test fail, but because the thing unit test is based on a mocked version of the store that test with continue to pass, even though that component will never work. There isn't a good way to know a refactor invalidated a Mock.
So how do people unit test this type of dependence?
One way would be to cheat a little on the unit test and use the real store state, but then it isn't really a unit test. The other way is rely on integration testing to catch the mock - store mismatch, but that feels like it would be painful to debug why the unit tests pass but the integration tests are failing.
What we ended up doing is using the actual store. Because the store state is just an object we figured it was acceptable.
We also use the store getters, actions and mutations as templates for jasmine spyies.
// Vuex needs polyfill
import { polyfill } from 'es6-promise';
polyfill();
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
import test from 'app/components/test.vue';
import module from 'app/store/modules/module';
describe('Spec for Test.vue', () => {
var props;
var state;
var actions;
var mutations;
var getters;
var store;
beforeEach( () => {
jasmine.addMatchers(customMatchers);
props = { };
// Don't change the modules
state = Object.assign({}, module.state);
actions = Object.assign({}, module.actions);
mutations = Object.assign({}, module.mutations);
getters = Object.assign({}, module.getters);
// Add require global actions, mutations, and getters here...
actions.globalActionHere = 'anything'; // this turns into a spy
// Update State with required fields
state.defaults = { id: 1 } // default expected when the component loads
// Replace modules copies with mocks
actions = jasmine.createSpyObj('actions', actions);
mutations = jasmine.createSpyObj('mutations', mutations);
getters = jasmine.createSpyObj('getters', getters);
store = new Vuex.Store( { state: { module: state }, getters, actions, mutations } );
} );
it('should have a name of test', () => {
const Constructor = Vue.extend(thing);
const component new Constructor({ store, props }).$mount();
expect(component.$options.name).toBe('test');
});
});
Note the part
jasmine.createSpyObj('actions', actions);
Jasmine spies will use the module to create spyies for each of the methods, which is very useful.
I have a component called Login, and these selectors:
const selectLogin = () => (state) => state.get('login');
const selectUser = () => createSelector(
selectLogin(),
(loginState) => loginState.get('user')
);
Here's what state looks like for the "login" component:
login: {
user: {
id: 206
}
}
In another component, I want to select the "user" object.
At the top of my file, I have
import { createStructuredSelector } from 'reselect';
import {
selectLogin,
selectUser
} from 'containers/Login/selectors';
const mapStateToProps = createStructuredSelector({
login: selectLogin(),
user: selectUser(),
});
When I use "selectUser()", I get "loginState.get is not a function".
If I remove all references to "selectUser()", I can access this.props.login.user. That works for me, but I want to know why I can't select from within the "login" state. The examples use the same "substate" convention in the selector, and they work. Any ideas?
Is this another component in another route?
You have to manually inject reducers and sagas required for the page in each route.
In route.js, loadReducer and inject it to the page, something like this:
{
path: '/projects/add',
...
getComponent(nextState, cb) {
const importModules = Promise.all([
System.import('containers/Project/reducer'),
System.import('containers/Login/reducer')
...
]);
const renderRoute = loadModule(cb);
importModules.then(([projectReducer, loginReducer ...]) => {
injectReducer('projects', projectReducer.default);
injectReducer('login', projectReducer.default);
renderRoute(component);
});
importModules.catch(errorLoading);
},
A react component wrapped with an apollo-client query will automatically initiate a call to the server for data.
I would like to fire off a request for data only on a specific user input.
You can pass the skip option in the query options - but this means the refetch() function is not provided as a prop to the component; and it appears that the value of skip is not assessed dynamically on prop update.
My use is case is a map component. I only want data for markers to be loaded when the user presses a button, but not on initial component mount or location change.
A code sample below:
// GraphQL wrapping
Explore = graphql(RoutesWithinQuery, {
options: ({ displayedMapRegion }) => ({
variables: {
scope: 'WITHIN',
targetRegion: mapRegionToGeoRegionInputType(displayedMapRegion)
},
skip: ({ targetResource, searchIsAllowedForMapArea }) => {
const skip = Boolean(!searchIsAllowedForMapArea || targetResource != 'ROUTE');
return skip;
},
}),
props: ({ ownProps, data: { loading, viewer, refetch }}) => ({
routes: viewer && viewer.routes ? viewer.routes : [],
refetch,
loading
})
})(Explore);
To include an HoC based on a condition affected by a props change, you could use branch from recompose.
branch(
test: (props: Object) => boolean,
left: HigherOrderComponent,
right: ?HigherOrderComponent
): HigherOrderComponent
check: https://github.com/acdlite/recompose/blob/master/docs/API.md#branch
For this specific example, would look something like:
const enhance = compose(
branch(
// evaluate condition
({ targetResource, searchIsAllowedForMapArea }) =>
Boolean(!searchIsAllowedForMapArea || targetResource != 'ROUTE'),
// HoC if condition is true
graphql(RoutesWithinQuery, {
options: ({ displayedMapRegion }) => ({
variables: {
scope: 'WITHIN',
targetRegion: mapRegionToGeoRegionInputType(displayedMapRegion)
},
}),
props: ({ ownProps, data: { loading, viewer, refetch } }) => ({
routes: viewer && viewer.routes ? viewer.routes : [],
refetch,
loading
})
})
)
);
Explore = enhance(Explore);
I have a similar use case, I wanted to load the data only when the user clicked.
I've not tried the withQuery suggestion given by pencilcheck above. But I've seen the same suggestion elsewhere. I will try it, but in the meantime this is how I got it working based off a discussion on github:
./loadQuery.js
Note: I'm using the skip directive:
const LOAD = `
query Load($ids:[String], $skip: Boolean = false) {
things(ids: $ids) #skip(if: $skip) {
title
}
`
LoadMoreButtonWithQuery.js
Here I use the withState higher-order function to add in a flag and a flag setter to control skip:
import { graphql, compose } from 'react-apollo';
import { withState } from 'recompose';
import LoadMoreButton from './LoadMoreButton';
import LOAD from './loadQuery';
export default compose(
withState('isSkipRequest', 'setSkipRequest', true),
graphql(
gql(LOAD),
{
name: 'handleLoad',
options: ({ids, isSkipRequest}) => ({
variables: {
ids,
skip: isSkipRequest
},
})
}
),
)(Button);
./LoadMoreButton.js
Here I have to manually "flip" the flag added using withState:
export default props => (
<Button onClick={
() => {
props.setSkipRequest(false); // setter added by withState
props.handleLoad.refetch();
}
}>+</Button>
);
Frankly I'm a little unhappy with this, as it is introduces a new set of wiring (composed in by "withState"). Its also not battle tested - I just got it working and I came to StackOverflow to check for better solutions.