diff options
Diffstat (limited to 'spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js')
-rw-r--r-- | spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js | 242 |
1 files changed, 242 insertions, 0 deletions
diff --git a/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js new file mode 100644 index 00000000000..025e605b920 --- /dev/null +++ b/spec/frontend/authentication/two_factor_auth/components/recovery_codes_spec.js @@ -0,0 +1,242 @@ +import { mount } from '@vue/test-utils'; +import { GlAlert, GlButton } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import { within } from '@testing-library/dom'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; +import Tracking from '~/tracking'; +import RecoveryCodes, { + i18n, +} from '~/authentication/two_factor_auth/components/recovery_codes.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { + RECOVERY_CODE_DOWNLOAD_FILENAME, + COPY_KEYBOARD_SHORTCUT, +} from '~/authentication/two_factor_auth/constants'; +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); + }); + }); + }); +}); |