Commit 702c4a12 authored by Andrew Fontaine's avatar Andrew Fontaine

Merge branch '282440-mlunoe-improve-graphql-local-resolver-tests' into 'master'

Test(DevOps Adoption): `groups` local resolver

See merge request gitlab-org/gitlab!47793
parents 581efbce 251bfd72
...@@ -833,7 +833,7 @@ If your application contains `@client` queries, most probably you will have an A ...@@ -833,7 +833,7 @@ If your application contains `@client` queries, most probably you will have an A
```javascript ```javascript
import createMockApollo from 'jest/helpers/mock_apollo_helper'; import createMockApollo from 'jest/helpers/mock_apollo_helper';
... ...
fakeApollo = createMockApollo(requestHandlers, {}); mockApollo = createMockApollo(requestHandlers, resolvers);
``` ```
Sometimes we want to test a `result` hook of the local query. In order to have it triggered, we need to populate a cache with correct data to be fetched with this query: Sometimes we want to test a `result` hook of the local query. In order to have it triggered, we need to populate a cache with correct data to be fetched with this query:
...@@ -849,14 +849,14 @@ query fetchLocalUser { ...@@ -849,14 +849,14 @@ query fetchLocalUser {
```javascript ```javascript
import fetchLocalUserQuery from '~/design_management/graphql/queries/fetch_local_user.query.graphql'; import fetchLocalUserQuery from '~/design_management/graphql/queries/fetch_local_user.query.graphql';
function createComponentWithApollo() { function createMockApolloProvider() {
const requestHandlers = [ const requestHandlers = [
[getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)], [getDesignListQuery, jest.fn().mockResolvedValue(designListQueryResponse)],
[permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)], [permissionsQuery, jest.fn().mockResolvedValue(permissionsQueryResponse)],
]; ];
fakeApollo = createMockApollo(requestHandlers, {}); mockApollo = createMockApollo(requestHandlers, {});
fakeApollo.clients.defaultClient.cache.writeQuery({ mockApollo.clients.defaultClient.cache.writeQuery({
query: fetchLocalUserQuery, query: fetchLocalUserQuery,
data: { data: {
fetchLocalUser: { fetchLocalUser: {
...@@ -864,15 +864,107 @@ function createComponentWithApollo() { ...@@ -864,15 +864,107 @@ function createComponentWithApollo() {
name: 'Test', name: 'Test',
}, },
}, },
}) });
wrapper = shallowMount(Index, { return mockApollo;
}
function createComponent(options = {}) {
const { mockApollo } = options;
return shallowMount(Index, {
localVue, localVue,
apolloProvider: fakeApollo, apolloProvider: mockApollo,
}); });
} }
``` ```
Sometimes it is necessary to control what the local resolver returns and inspect how it is called by the component. This can be done by mocking your local resolver:
```javascript
import fetchLocalUserQuery from '~/design_management/graphql/queries/fetch_local_user.query.graphql';
function createMockApolloProvider(options = {}) {
const { fetchLocalUserSpy } = options;
mockApollo = createMockApollo([], {
Query: {
fetchLocalUser: fetchLocalUserSpy,
},
});
// Necessary for local resolvers to be activated
mockApollo.clients.defaultClient.cache.writeQuery({
query: fetchLocalUserQuery,
data: {},
});
return mockApollo;
}
```
In the test you can then control what the spy is supposed to do and inspect the component after the request have returned:
```javascript
describe('My Index test with `createMockApollo`', () => {
let wrapper;
let fetchLocalUserSpy;
afterEach(() => {
wrapper.destroy();
wrapper = null;
fetchLocalUserSpy = null;
});
describe('when loading', () => {
beforeEach(() => {
const mockApollo = createMockApolloProvider();
wrapper = createComponent({ mockApollo });
});
it('displays the loader', () => {
// Assess that the loader is present
});
});
describe('with data', () => {
beforeEach(async () => {
fetchLocalUserSpy = jest.fn().mockResolvedValue(localUserQueryResponse);
const mockApollo = createMockApolloProvider(fetchLocalUserSpy);
wrapper = createComponent({ mockApollo });
await waitForPromises();
});
it('should fetch data once', () => {
expect(fetchLocalUserSpy).toHaveBeenCalledTimes(1);
});
it('displays data', () => {
// Assess that data is present
});
});
describe('with error', () => {
const error = 'Error!';
beforeEach(async () => {
fetchLocalUserSpy = jest.fn().mockRejectedValueOnce(error);
const mockApollo = createMockApolloProvider(fetchLocalUserSpy);
wrapper = createComponent({ mockApollo });
await waitForPromises();
});
it('should fetch data once', () => {
expect(fetchLocalUserSpy).toHaveBeenCalledTimes(1);
});
it('displays the error', () => {
// Assess that the error is displayed
});
});
});
```
## Handling errors ## Handling errors
GitLab's GraphQL mutations currently have two distinct error modes: [Top-level](#top-level-errors) and [errors-as-data](#errors-as-data). GitLab's GraphQL mutations currently have two distinct error modes: [Top-level](#top-level-errors) and [errors-as-data](#errors-as-data).
......
...@@ -3,7 +3,7 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; ...@@ -3,7 +3,7 @@ import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '~/sentry/wrapper'; import * as Sentry from '~/sentry/wrapper';
import getGroupsQuery from '../graphql/queries/get_groups.query.graphql'; import getGroupsQuery from '../graphql/queries/get_groups.query.graphql';
import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue'; import DevopsAdoptionEmptyState from './devops_adoption_empty_state.vue';
import { DEVOPS_ADOPTION_STRINGS } from '../constants'; import { DEVOPS_ADOPTION_STRINGS, MAX_REQUEST_COUNT } from '../constants';
export default { export default {
name: 'DevopsAdoptionApp', name: 'DevopsAdoptionApp',
...@@ -17,6 +17,7 @@ export default { ...@@ -17,6 +17,7 @@ export default {
}, },
data() { data() {
return { return {
requestCount: MAX_REQUEST_COUNT,
loadingError: false, loadingError: false,
}; };
}, },
...@@ -25,7 +26,9 @@ export default { ...@@ -25,7 +26,9 @@ export default {
query: getGroupsQuery, query: getGroupsQuery,
loadingKey: 'loading', loadingKey: 'loading',
result() { result() {
if (this.groups?.pageInfo?.nextPage) { this.requestCount -= 1;
if (this.requestCount > 0 && this.groups?.pageInfo?.nextPage) {
this.fetchNextPage(); this.fetchNextPage();
} }
}, },
...@@ -55,7 +58,8 @@ export default { ...@@ -55,7 +58,8 @@ export default {
}, },
updateQuery: (previousResult, { fetchMoreResult }) => { updateQuery: (previousResult, { fetchMoreResult }) => {
const { nodes, ...rest } = fetchMoreResult.groups; const { nodes, ...rest } = fetchMoreResult.groups;
const previousNodes = previousResult.groups.nodes; const { nodes: previousNodes } = previousResult.groups;
return { groups: { ...rest, nodes: [...previousNodes, ...nodes] } }; return { groups: { ...rest, nodes: [...previousNodes, ...nodes] } };
}, },
}) })
...@@ -65,9 +69,9 @@ export default { ...@@ -65,9 +69,9 @@ export default {
}; };
</script> </script>
<template> <template>
<gl-loading-icon v-if="isLoading" size="md" class="gl-my-5" /> <gl-alert v-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">
<gl-alert v-else-if="loadingError" variant="danger" :dismissible="false" class="gl-mt-3">
{{ $options.i18n.groupsError }} {{ $options.i18n.groupsError }}
</gl-alert> </gl-alert>
<gl-loading-icon v-else-if="isLoading" size="md" class="gl-my-5" />
<devops-adoption-empty-state v-else-if="isEmpty" /> <devops-adoption-empty-state v-else-if="isEmpty" />
</template> </template>
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export const MAX_REQUEST_COUNT = 10;
export const DEVOPS_ADOPTION_STRINGS = { export const DEVOPS_ADOPTION_STRINGS = {
app: { app: {
groupsError: s__('DevopsAdoption|There was an error fetching Groups'), groupsError: s__('DevopsAdoption|There was an error fetching Groups'),
......
...@@ -17,6 +17,12 @@ export const groupNodes = [ ...@@ -17,6 +17,12 @@ export const groupNodes = [
}, },
]; ];
export const nextGroupNode = {
__typename: 'Group',
full_name: 'Baz',
id: 'baz',
};
export const groupPageInfo = { export const groupPageInfo = {
nextPage: 2, nextPage: 2,
}; };
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment