How to test graphql apollo component? How to get complete coverage inside compose()? - redux

I'm having issues getting complete coverage on my apollo components. istanbul is reporting the functions inside compose() are not getting called. These are Redux connect() functions and apollo graph() functions.
export default compose (
...
connect(mapStateToProps, mapDispatchToProps), // <-- functions not covered
graphql(builderQuery, {
options: (ownProps) => { // <-- function not covered
...
)(ComponentName);
I'm mounting using enzyme, trying to do something similar to the react-apollo example.
const mounted = shallow(
<MockedProvider mocks={[
{ request: { query, variables }, result: { data: response.data } }
]}>
<ConnectedComponentName />
</MockedProvider>
);
The only way I've been able to achieve 100% coverage is if I export all of the functions and call them directly.

testing composed graphql/redux containers
Try something like this for your setup:
// mocks.js
import configureMockStore from 'redux-mock-store'
import { ApolloClient } from 'react-apollo'
import { mockNetworkInterface } from 'apollo-test-utils'
export const mockApolloClient = new ApolloClient({
networkInterface: mockNetworkInterface(),
})
export const createMockStore = configureMockStore()
This will set you up to properly test your containers:
// container-test.js
import { mount } from 'enzyme'
import { createMockStore, mockApolloClient } from 'mocks'
beforeEach(() => {
store = createMockStore(initialState)
wrapper = mount(
<ApolloProvider client={mockApolloClient} store={store}>
<Container />
</ApolloProvider>
)
})
it('maps state & dispatch to props', () => {
const props = wrapper.find('SearchResults').props()
const expected = expect.arrayContaining([
// These props come from an HOC returning my grapqhql composition
'selectedListing',
'selectedPin',
'pathname',
'query',
'bbox',
'pageNumber',
'locationSlug',
'selectListing',
'updateCriteria',
'selectPin',
])
const actual = Object.keys(props)
expect(actual).toEqual(expected)
})
testing graphql options
Because the graphql fn has a signature like graphql(query, config), you can export your config for testing in isolation for more granular coverage.
import { config } from '../someQuery/config'
describe('config.options', () => {
const state = {
bbox: [],
locationSlug: 'foo',
priceRange: 'bar',
refinements: 'baz',
userSort: 'buzz',
}
const results = {
points: [
{ propertyName: 'Foo' },
{ propertyName: 'Bar' },
],
properties: [
{ propertyName: 'Foo' },
{ propertyName: 'Bar' },
],
}
it('maps input to variables', () => {
const { variables } = config.options(state)
const expected = { bbox: [], locationSlug: 'foo', priceRange: 'bar', refinements: 'baz', userSort: 'buzz' }
expect(variables).toEqual(expected)
})
it('returns props', () => {
const response = { data: { loading: false, geo: { results } } }
const props = config.props(response)
expect(props.results).toEqual(results.properties)
expect(props.spotlightPoints).toEqual(results.points)
})
})

Related

nextjs: TypeError: createServer is not a function

I am trying to follow this tutorial:
I am stuck at step 3, which is where the server is defined as follows:
import { createServer } from "#graphql-yoga/node";
import { join } from "path";
import { readFileSync } from "fs";
const typeDefs = readFileSync(join(process.cwd(), "schema.graphql"), {
encoding: "utf-8",
});
const resolvers = {
Query: {
cart: (_, { id }) => {
return {
id,
totalItems: 0,
};
},
},
};
const server = createServer({
cors: false,
endpoint: "/api",
logging: {
prettyLog: false,
},
schema: {
typeDefs,
resolvers,
},
});
export default server;
When I try to use that definition and start the local host, I get an error that says:
TypeError: (0 ,
graphql_yoga_node__WEBPACK_IMPORTED_MODULE_0_.createServer) is not a function at eval
Can anyone see if this tutorial is now out of date. I can see that I am using next v 13.1.1 and the tutorial uses v12. I've been having an awful time trying to find an explanation of how to use these packages, in their current formats. Is this one now out of date?
Can anyone see how to define a server for next v13?
There are some change between graph-yoga v2 and v3, you can look this tutorial to solve it.
For others that might be stuck, this might be a way to define the schema using graphql-yoga (now instead of graphql-yoga/node)
import { createSchema, createYoga } from 'graphql-yoga'
import { createServer } from 'node:http'
import { join } from "path";
import { readFileSync } from "fs";
const typeDefs = readFileSync(join(process.cwd(), "schema.graphql"), {
encoding: "utf-8",
});
const resolvers = {
Query: {
cart: (_, { id }) => {
return {
id,
totalItems: 0,
};
},
},
};
const yoga = createYoga({
cors: false,
endpoint: "/api",
logging: {
prettyLog: false,
},
schema: createSchema({
typeDefs,
resolvers,
}),
});
const server = createServer(yoga);
server.listen(3000, () => {
console.info('Server is running on http://localhost:3000/graphql')
});
export default server;

Store State Issues - Next Redux Wrapper vs Redux ToolKit

FYI: Everything is working fine and all the flows are working as expected but logs are confusing.
I am using Redux ToolKit for managing redux state & using Next Redux Wrapper for Server Side rendering and caching queries.
CartSlice.js
import { createSlice } from '#reduxjs/toolkit';
export const cartSlice = createSlice({
name: 'cart',
initialState: { data: [] },
reducers: {
addToCart: (state, action) => {
const itemInCart = state.data.find((item) => item.id === action.payload.id);
if (itemInCart) {
itemInCart.quantity += action.payload.quantity;
} else {
state.data.push({ ...action.payload });
}
}
},
});
export const cartReducer = cartSlice.reducer;
export const { addToCart } = cartSlice.actions;
ServiceApi.js
import { createApi, fetchBaseQuery } from '#reduxjs/toolkit/query/react'
import { HYDRATE } from 'next-redux-wrapper'
export const serviceApi = createApi({
reducerPath: 'serviceApi',
baseQuery: fetchBaseQuery({ baseUrl: '<base-url>' }),
extractRehydrationInfo(action, { reducerPath }) {
if (action.type === HYDRATE) {
return action.payload[reducerPath]
}
},
endpoints: (builder) => ({
getItemById: builder.query({ query: (id) => `id/${id}` }),
getItems: builder.query({ query: () => `/` }),
}),
})
export const { getRunningQueriesThunk } = serviceApi.util
export const { useGetItemByIdQuery, useGetItemsQuery } = serviceApi
export const { getItemById, getItems } = serviceApi.endpoints;
store.js
import { configureStore } from '#reduxjs/toolkit'
import { serviceApi } from './serviceApi'
import { createWrapper } from "next-redux-wrapper";
import { cartSlice } from "./cartSlice";
export const store = configureStore({
reducer: {
[cartSlice.name]: cartSlice.reducer,
[serviceApi.reducerPath]: serviceApi.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(serviceApi.middleware),
})
const makeStore = () => store
export const wrapper = createWrapper(makeStore, {debug: true});
Using getServerSideProps
export const getServerSideProps = wrapper.getServerSideProps(
(store) => async (context) => {
const { id } = context.query;
store.dispatch(serviceApi.endpoints.getItemById.initiate(parseInt(id)));
await Promise.all(store.dispatch(serviceApi.util.getRunningQueriesThunk()));
return {
props: {},
};
}
);
And using wrappedStore
const { store, props } = wrapper.useWrappedStore(pageProps);
On the UI flows everything is working as expected and i am able to add items in the cart and i can see the store state is getting updated by looking the UI.
But the logs from next redux wrapper is confusing:

can not call action in Pinia option API

I am using Vue3 option API and Pinia .
I want to call an action in Pinia option Api from component
component
import { mapActions } from "pinia";
import { useTableStore } from "../../../stores/table";
export default {
name: "LoggingForm",
data() {
return {
login: {
username: "",
password: "",
serverhost: "",
},
};
},
methods: {
submit(){
this.getData(this.login)
}
},
computed: {
...mapActions(useTableStore, ["getData"]),
},
};
and this is store/table.js
import { defineStore } from 'pinia'
import authService from "#/api/auth.js";
export const useTableStore = defineStore({
id: 'table',
state: () => ({
table: []
}),
getters: {
headers: (state) => state.table[0],
body: (state) => state.table.slice(1)
},
actions: {
async getData1(data) {
// do something
}
},
}
})
But I get this error
I can Use state and getters perfectly Just action don't work !
what's the problem ?
Here is what you need
https://pinia.vuejs.org/core-concepts/actions.html#without-setup
In short:
computed => mapGetters
methods => mapActions
You are using mapActions with computed so that will not work

Composition API - Axios request in setup()

I am experimenting with Vue3's Composition API in a Laravel/VueJS/InertiaJS stack.
A practice that I have used a lot in Vue2 with this stack is to have 1 route that returns the Vue page component (eg. Invoices.vue) and then in the created() callback, I would trigger an axios call to an additional endpoint to fetch the actual data.
I am now trying to replicate a similar approach in Vue3 with composition API like so
export default {
components: {Loader, PageBase},
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: getInvoices(),
selectedInvoices: [],
});
async function getInvoices() {
loading.value = true;
return await axios.get(props.fetch_url).then(response => {
return response.data.data;
}).finally(() => {
loading.value = false;
})
}
function handleSelectionChange(selection) {
state.selectedInvoices = selection;
}
return {
loading,
state,
handleSelectionChange,
}
}
}
This however keeps on giving me the propise, rather than the actual data that is returned.
Changing it like so does work:
export default {
components: {Loader, PageBase},
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: [],
selectedInvoices: [],
});
axios.get(props.fetch_url).then(response => {
state.invoices = response.data.data;
}).finally(() => {
loading.value = false;
})
function handleSelectionChange(selection) {
state.selectedInvoices = selection;
}
return {
loading,
state,
handleSelectionChange,
}
}
}
I want to use function though, so I can re-use it for filtering etc.
Very curious to read how others are doing this.
I have been googling about it a bit, but cant seem to find relevant docu.
All feedback is highly welcomed.
I tried this now with async setup() and await getInvoices() and <Suspense> but it never displayed any content.
So this is how I'd do it, except I wouldn't and I'd use vuex and vuex-orm to store the invoices and fetch the state from the store.
<template>
<div>loading:{{ loading }}</div>
<div>state:{{ state }}</div>
</template>
<script>
import {defineComponent, ref, reactive} from "vue";
import axios from "axios";
export default defineComponent({
name: 'HelloWorld',
props: {
fetch_url: {
required: true,
type: String,
}
},
setup(props) {
const loading = ref(false)
const state = reactive({
invoices: []
})
async function getInvoices() {
loading.value = true;
await axios.get(props.fetch_url).then(response => {
state.invoices = response.data;
}).finally(() => {
loading.value = false;
})
}
return {
getInvoices,
loading,
state,
}
},
async created() {
await this.getInvoices()
}
})
</script>
<style scoped>
</style>
This is of course similar to what you're doing in option 2.

Vue 3 compoition API computed function

Trying to switch my code to the new composition API that comes with Vue 3 but I cant get it to work.
export default {
props: {
classProp: {type: String},
error: {type: String},
},
setup(){
// move to here (this is not working)
computed(() => {
const classObject = () => {
return ['form__control', this.classProp,
{
'form__invalid': this.error
}
]
}
})
},
computed: {
classObject: function () {
return ['form__control', this.classProp,
{
'form__invalid': this.error
}
]
}
},
}
skip "computed" all together
you need to use "ref" or "reactive". these are modules:
<script>
import { ref } from 'vue'
setup(){
const whateverObject = ref({ prop: "whatever initial value" });
whateverObject.value.prop= "if you change something within setup you need to access it trough .value";
return { whateverObject } // expose it to the template by returning it
}
</script>
if you want to use classes you import them like in this example of my own:
import { APIBroker } from '~/helpers/APIbroker'
const api = new APIBroker({})
Now "api" can be used inside setup() or wherever

Resources