import { GlAlert, GlButton } from '@gitlab/ui'; import { within } from '@testing-library/dom'; import { mount } from '@vue/test-utils'; import { nextTick } from 'vue'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import RecoveryCodes, { i18n, } from '~/authentication/two_factor_auth/components/recovery_codes.vue'; import { RECOVERY_CODE_DOWNLOAD_FILENAME, COPY_KEYBOARD_SHORTCUT, } from '~/authentication/two_factor_auth/constants'; import Tracking from '~/tracking'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { codes, codesFormattedString, codesDownloadHref, profileAccountPath } from '../mock_data'; describe('RecoveryCodes', () => { let wrapper; const createComponent = (options = {}) => { wrapper = extendedWrapper( mount(RecoveryCodes, { propsData: { codes, profileAccountPath, ...(options?.propsData || {}), }, ...options, }), ); }; const queryByText = (text, options) => within(wrapper.element).queryByText(text, options); const findAlert = () => wrapper.find(GlAlert); const findRecoveryCodes = () => wrapper.findByTestId('recovery-codes'); const findCopyButton = () => wrapper.find(ClipboardButton); const findButtonByText = (text) => wrapper.findAll(GlButton).wrappers.find((buttonWrapper) => buttonWrapper.text() === text); const findDownloadButton = () => findButtonByText('Download codes'); const findPrintButton = () => findButtonByText('Print codes'); const findProceedButton = () => findButtonByText('Proceed'); const manuallyCopyRecoveryCodes = () => wrapper.vm.$options.mousetrap.trigger(COPY_KEYBOARD_SHORTCUT); beforeEach(() => { jest.spyOn(Tracking, 'event'); createComponent(); }); it('renders title', () => { expect(queryByText(i18n.pageTitle)).toEqual(expect.any(HTMLElement)); }); it('renders alert', () => { expect(findAlert().exists()).toBe(true); expect(findAlert().text()).toBe(i18n.alertTitle); }); it('renders codes', () => { const recoveryCodes = findRecoveryCodes().text(); codes.forEach((code) => { expect(recoveryCodes).toContain(code); }); }); describe('"Proceed" button', () => { it('renders button as disabled', () => { const proceedButton = findProceedButton(); expect(proceedButton.exists()).toBe(true); expect(proceedButton.props('disabled')).toBe(true); expect(proceedButton.attributes()).toMatchObject({ title: i18n.proceedButton, href: profileAccountPath, }); }); it('fires Snowplow event', () => { expect(findProceedButton().attributes()).toMatchObject({ 'data-track-event': 'click_button', 'data-track-label': '2fa_recovery_codes_proceed_button', }); }); }); describe('"Copy codes" button', () => { it('renders button', () => { const copyButton = findCopyButton(); expect(copyButton.exists()).toBe(true); expect(copyButton.text()).toBe(i18n.copyButton); expect(copyButton.props()).toMatchObject({ title: i18n.copyButton, text: codesFormattedString, }); }); describe('when button is clicked', () => { beforeEach(async () => { findCopyButton().trigger('click'); await nextTick(); }); it('enables "Proceed" button', () => { expect(findProceedButton().props('disabled')).toBe(false); }); it('fires Snowplow event', () => { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { label: '2fa_recovery_codes_copy_button', }); }); }); }); describe('"Download codes" button', () => { it('renders button', () => { const downloadButton = findDownloadButton(); expect(downloadButton.exists()).toBe(true); expect(downloadButton.attributes()).toMatchObject({ title: i18n.downloadButton, download: RECOVERY_CODE_DOWNLOAD_FILENAME, href: codesDownloadHref, }); }); describe('when button is clicked', () => { beforeEach(async () => { const downloadButton = findDownloadButton(); // jsdom does not support navigating. // Since we are clicking an anchor tag there is no way to mock this // and we are forced to instead remove the `href` attribute. // More info: https://github.com/jsdom/jsdom/issues/2112#issuecomment-663672587 downloadButton.element.removeAttribute('href'); downloadButton.trigger('click'); await nextTick(); }); it('enables "Proceed" button', () => { expect(findProceedButton().props('disabled')).toBe(false); }); it('fires Snowplow event', () => { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { label: '2fa_recovery_codes_download_button', }); }); }); }); describe('"Print codes" button', () => { it('renders button', () => { const printButton = findPrintButton(); expect(printButton.exists()).toBe(true); expect(printButton.attributes()).toMatchObject({ title: i18n.printButton, }); }); describe('when button is clicked', () => { beforeEach(async () => { window.print = jest.fn(); findPrintButton().trigger('click'); await nextTick(); }); it('enables "Proceed" button and opens print dialog', () => { expect(findProceedButton().props('disabled')).toBe(false); expect(window.print).toHaveBeenCalled(); }); it('fires Snowplow event', () => { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'click_button', { label: '2fa_recovery_codes_print_button', }); }); }); }); describe('when codes are manually copied', () => { describe('when selected text is the recovery codes', () => { beforeEach(async () => { jest.spyOn(window, 'getSelection').mockImplementation(() => ({ toString: jest.fn(() => codesFormattedString), })); manuallyCopyRecoveryCodes(); await nextTick(); }); it('enables "Proceed" button', () => { expect(findProceedButton().props('disabled')).toBe(false); }); it('fires Snowplow event', () => { expect(Tracking.event).toHaveBeenCalledWith(undefined, 'copy_keyboard_shortcut', { label: '2fa_recovery_codes_manual_copy', }); }); }); describe('when selected text includes the recovery codes', () => { beforeEach(() => { jest.spyOn(window, 'getSelection').mockImplementation(() => ({ toString: jest.fn(() => `foo bar ${codesFormattedString}`), })); }); it('enables "Proceed" button', async () => { manuallyCopyRecoveryCodes(); await nextTick(); expect(findProceedButton().props('disabled')).toBe(false); }); }); describe('when selected text does not include the recovery codes', () => { beforeEach(() => { jest.spyOn(window, 'getSelection').mockImplementation(() => ({ toString: jest.fn(() => 'foo bar'), })); }); it('keeps "Proceed" button disabled', async () => { manuallyCopyRecoveryCodes(); await nextTick(); expect(findProceedButton().props('disabled')).toBe(true); }); }); }); });