diff options
Diffstat (limited to 'spec/frontend/vue_shared/components/user_callout_dismisser_spec.js')
-rw-r--r-- | spec/frontend/vue_shared/components/user_callout_dismisser_spec.js | 306 |
1 files changed, 306 insertions, 0 deletions
diff --git a/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js new file mode 100644 index 00000000000..70dec42ab32 --- /dev/null +++ b/spec/frontend/vue_shared/components/user_callout_dismisser_spec.js @@ -0,0 +1,306 @@ +import { mount } from '@vue/test-utils'; +import { merge } from 'lodash'; +import Vue, { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql'; +import getUserCalloutsQuery from '~/graphql_shared/queries/get_user_callouts.query.graphql'; +import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue'; +import { + anonUserCalloutsResponse, + userCalloutMutationResponse, + userCalloutsResponse, +} from './user_callout_dismisser_mock_data'; + +Vue.use(VueApollo); + +const initialSlotProps = (changes = {}) => ({ + dismiss: expect.any(Function), + isAnonUser: false, + isDismissed: false, + isLoadingQuery: true, + isLoadingMutation: false, + mutationError: null, + queryError: null, + shouldShowCallout: false, + ...changes, +}); + +describe('UserCalloutDismisser', () => { + let wrapper; + + const MOCK_FEATURE_NAME = 'mock_feature_name'; + + // Query handlers + const successHandlerFactory = (dismissedCallouts = []) => async () => + userCalloutsResponse(dismissedCallouts); + const anonUserHandler = async () => anonUserCalloutsResponse(); + const errorHandler = () => Promise.reject(new Error('query error')); + const pendingHandler = () => new Promise(() => {}); + + // Mutation handlers + const mutationSuccessHandlerSpy = jest.fn(async (variables) => + userCalloutMutationResponse(variables), + ); + const mutationErrorHandlerSpy = jest.fn(async (variables) => + userCalloutMutationResponse(variables, ['mutation error']), + ); + + const defaultScopedSlotSpy = jest.fn(); + + const callDismissSlotProp = () => defaultScopedSlotSpy.mock.calls[0][0].dismiss(); + + const createComponent = ({ queryHandler, mutationHandler, ...options }) => { + wrapper = mount( + UserCalloutDismisser, + merge( + { + propsData: { + featureName: MOCK_FEATURE_NAME, + }, + scopedSlots: { + default: defaultScopedSlotSpy, + }, + apolloProvider: createMockApollo([ + [getUserCalloutsQuery, queryHandler], + [dismissUserCalloutMutation, mutationHandler], + ]), + }, + options, + ), + ); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + describe('when loading', () => { + beforeEach(() => { + createComponent({ + queryHandler: pendingHandler, + }); + }); + + it('passes expected slot props to child', () => { + expect(defaultScopedSlotSpy).lastCalledWith(initialSlotProps()); + }); + }); + + describe('when loaded and dismissed', () => { + beforeEach(() => { + createComponent({ + queryHandler: successHandlerFactory([MOCK_FEATURE_NAME]), + }); + + return waitForPromises(); + }); + + it('passes expected slot props to child', () => { + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isDismissed: true, + isLoadingQuery: false, + }), + ); + }); + }); + + describe('when loaded and not dismissed', () => { + beforeEach(() => { + createComponent({ + queryHandler: successHandlerFactory(), + }); + + return waitForPromises(); + }); + + it('passes expected slot props to child', () => { + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isLoadingQuery: false, + shouldShowCallout: true, + }), + ); + }); + }); + + describe('when loaded with errors', () => { + beforeEach(() => { + createComponent({ + queryHandler: errorHandler, + }); + + return waitForPromises(); + }); + + it('passes expected slot props to child', () => { + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isLoadingQuery: false, + queryError: expect.any(Error), + }), + ); + }); + }); + + describe('when loaded and the user is anonymous', () => { + beforeEach(() => { + createComponent({ + queryHandler: anonUserHandler, + }); + + return waitForPromises(); + }); + + it('passes expected slot props to child', () => { + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isAnonUser: true, + isLoadingQuery: false, + }), + ); + }); + }); + + describe('when skipQuery is true', () => { + let queryHandler; + beforeEach(() => { + queryHandler = jest.fn(); + + createComponent({ + queryHandler, + propsData: { + skipQuery: true, + }, + }); + }); + + it('does not run the query', async () => { + expect(queryHandler).not.toHaveBeenCalled(); + + await waitForPromises(); + + expect(queryHandler).not.toHaveBeenCalled(); + }); + + it('passes expected slot props to child', () => { + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isLoadingQuery: false, + shouldShowCallout: true, + }), + ); + }); + }); + + describe('dismissing', () => { + describe('given it succeeds', () => { + beforeEach(() => { + createComponent({ + queryHandler: successHandlerFactory(), + mutationHandler: mutationSuccessHandlerSpy, + }); + + return waitForPromises(); + }); + + it('dismissing calls mutation', () => { + expect(mutationSuccessHandlerSpy).not.toHaveBeenCalled(); + + callDismissSlotProp(); + + expect(mutationSuccessHandlerSpy).toHaveBeenCalledWith({ + input: { featureName: MOCK_FEATURE_NAME }, + }); + }); + + it('passes expected slot props to child', async () => { + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isLoadingQuery: false, + shouldShowCallout: true, + }), + ); + + callDismissSlotProp(); + + // Wait for Vue re-render due to prop change + await nextTick(); + + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isDismissed: true, + isLoadingMutation: true, + isLoadingQuery: false, + }), + ); + + // Wait for mutation to resolve + await waitForPromises(); + + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isDismissed: true, + isLoadingQuery: false, + }), + ); + }); + }); + + describe('given it fails', () => { + beforeEach(() => { + createComponent({ + queryHandler: successHandlerFactory(), + mutationHandler: mutationErrorHandlerSpy, + }); + + return waitForPromises(); + }); + + it('calls mutation', () => { + expect(mutationErrorHandlerSpy).not.toHaveBeenCalled(); + + callDismissSlotProp(); + + expect(mutationErrorHandlerSpy).toHaveBeenCalledWith({ + input: { featureName: MOCK_FEATURE_NAME }, + }); + }); + + it('passes expected slot props to child', async () => { + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isLoadingQuery: false, + shouldShowCallout: true, + }), + ); + + callDismissSlotProp(); + + // Wait for Vue re-render due to prop change + await nextTick(); + + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isDismissed: true, + isLoadingMutation: true, + isLoadingQuery: false, + }), + ); + + // Wait for mutation to resolve + await waitForPromises(); + + expect(defaultScopedSlotSpy).lastCalledWith( + initialSlotProps({ + isDismissed: true, + isLoadingQuery: false, + mutationError: ['mutation error'], + }), + ); + }); + }); + }); +}); |