import { GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { stubComponent } from 'helpers/stub_component'; import CaptchaModal from '~/captcha/captcha_modal.vue'; import { initRecaptchaScript } from '~/captcha/init_recaptcha_script'; jest.mock('~/captcha/init_recaptcha_script'); describe('Captcha Modal', () => { let wrapper; let modal; let grecaptcha; const captchaSiteKey = 'abc123'; function createComponent({ props = {} } = {}) { wrapper = shallowMount(CaptchaModal, { propsData: { captchaSiteKey, ...props, }, stubs: { GlModal: stubComponent(GlModal), }, }); } beforeEach(() => { grecaptcha = { render: jest.fn(), }; initRecaptchaScript.mockResolvedValue(grecaptcha); }); afterEach(() => { wrapper.destroy(); wrapper = null; }); const findGlModal = () => { const glModal = wrapper.findComponent(GlModal); jest.spyOn(glModal.vm, 'show').mockImplementation(() => glModal.vm.$emit('shown')); jest .spyOn(glModal.vm, 'hide') .mockImplementation(() => glModal.vm.$emit('hide', { trigger: '' })); return glModal; }; const showModal = () => { wrapper.setProps({ needsCaptchaResponse: true }); }; beforeEach(() => { createComponent(); modal = findGlModal(); }); describe('rendering', () => { it('renders', () => { expect(modal.exists()).toBe(true); }); it('assigns the modal a unique ID', () => { const firstInstanceModalId = modal.props('modalId'); createComponent(); const secondInstanceModalId = findGlModal().props('modalId'); expect(firstInstanceModalId).not.toEqual(secondInstanceModalId); }); }); describe('functionality', () => { describe('when modal is shown', () => { describe('when initRecaptchaScript promise resolves successfully', () => { beforeEach(async () => { showModal(); await nextTick(); }); it('shows modal', async () => { expect(findGlModal().vm.show).toHaveBeenCalled(); }); it('renders window.grecaptcha', () => { expect(grecaptcha.render).toHaveBeenCalledWith(wrapper.vm.$refs.captcha, { sitekey: captchaSiteKey, callback: expect.any(Function), }); }); describe('then the user solves the captcha', () => { const captchaResponse = 'a captcha response'; beforeEach(() => { // simulate the grecaptcha library invoking the callback const { callback } = grecaptcha.render.mock.calls[0][1]; callback(captchaResponse); }); it('emits receivedCaptchaResponse exactly once with the captcha response', () => { expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[captchaResponse]]); }); it('hides modal with null trigger', async () => { // Assert that hide is called with zero args, so that we don't trigger the logic // for hiding the modal via cancel, esc, headerclose, etc, without a captcha response expect(modal.vm.hide).toHaveBeenCalledWith(); }); }); describe('then the user hides the modal without solving the captcha', () => { // Even though we don't explicitly check for these trigger values, these are the // currently supported ones which can be emitted. // See https://bootstrap-vue.org/docs/components/modal#prevent-closing describe.each` trigger | expected ${'cancel'} | ${[[null]]} ${'esc'} | ${[[null]]} ${'backdrop'} | ${[[null]]} ${'headerclose'} | ${[[null]]} `('using the $trigger trigger', ({ trigger, expected }) => { beforeEach(() => { const bvModalEvent = { trigger, }; modal.vm.$emit('hide', bvModalEvent); }); it(`emits receivedCaptchaResponse with ${JSON.stringify(expected)}`, () => { expect(wrapper.emitted('receivedCaptchaResponse')).toEqual(expected); }); }); }); }); describe('when initRecaptchaScript promise rejects', () => { const fakeError = {}; beforeEach(() => { initRecaptchaScript.mockImplementation(() => Promise.reject(fakeError)); jest.spyOn(console, 'error').mockImplementation(); showModal(); }); it('emits receivedCaptchaResponse exactly once with null', () => { expect(wrapper.emitted('receivedCaptchaResponse')).toEqual([[null]]); }); it('hides modal with null trigger', async () => { // Assert that hide is called with zero args, so that we don't trigger the logic // for hiding the modal via cancel, esc, headerclose, etc, without a captcha response expect(modal.vm.hide).toHaveBeenCalledWith(); }); it('calls console.error with a message and the exception', () => { // eslint-disable-next-line no-console expect(console.error).toHaveBeenCalledWith( expect.stringMatching(/exception.*captcha/), fakeError, ); }); }); }); }); });