diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-16 03:15:50 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-16 03:15:50 +0300 |
commit | e04431d29efaf17dda9dfbfbd0c5001693b25ee4 (patch) | |
tree | f114abad1f4882ef6c9c702e8de3a84334809031 /spec/frontend | |
parent | 1c898dc5c10bbedf94386d917259153d73608495 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
18 files changed, 497 insertions, 110 deletions
diff --git a/spec/frontend/__helpers__/emoji.js b/spec/frontend/__helpers__/emoji.js index a64135601ae..014a7854024 100644 --- a/spec/frontend/__helpers__/emoji.js +++ b/spec/frontend/__helpers__/emoji.js @@ -1,8 +1,7 @@ -import MockAdapter from 'axios-mock-adapter'; import { initEmojiMap, EMOJI_VERSION } from '~/emoji'; -import axios from '~/lib/utils/axios_utils'; +import { CACHE_VERSION_KEY, CACHE_KEY } from '~/emoji/constants'; -export const emojiFixtureMap = { +export const validEmoji = { atom: { moji: '⚛', description: 'atom symbol', @@ -49,11 +48,39 @@ export const emojiFixtureMap = { unicodeVersion: '5.1', description: 'white medium star', }, + gay_pride_flag: { + moji: '🏳️🌈', + unicodeVersion: '7.0', + description: 'because it contains a zero width joiner', + }, + family_mmb: { + moji: '👨👨👦', + unicodeVersion: '6.0', + description: 'because it contains multiple zero width joiners', + }, +}; + +export const invalidEmoji = { xss: { moji: '<img src=x onerror=prompt(1)>', unicodeVersion: '5.1', description: 'xss', }, + non_moji: { + moji: 'I am not an emoji...', + unicodeVersion: '9.0', + description: '...and should be filtered out', + }, + multiple_moji: { + moji: '🍂🏭', + unicodeVersion: '9.0', + description: 'Multiple separate emoji that are not joined by a zero width joiner', + }, +}; + +export const emojiFixtureMap = { + ...validEmoji, + ...invalidEmoji, }; export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => { @@ -63,11 +90,14 @@ export const mockEmojiData = Object.keys(emojiFixtureMap).reduce((acc, k) => { return acc; }, {}); -export async function initEmojiMock(mockData = mockEmojiData) { - const mock = new MockAdapter(axios); - mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, JSON.stringify(mockData)); +export function clearEmojiMock() { + localStorage.clear(); + initEmojiMap.promise = null; +} +export async function initEmojiMock(mockData = mockEmojiData) { + clearEmojiMock(); + localStorage.setItem(CACHE_VERSION_KEY, EMOJI_VERSION); + localStorage.setItem(CACHE_KEY, JSON.stringify(mockData)); await initEmojiMap(); - - return mock; } diff --git a/spec/frontend/access_tokens/components/token_spec.js b/spec/frontend/access_tokens/components/token_spec.js new file mode 100644 index 00000000000..1af21aaa8cd --- /dev/null +++ b/spec/frontend/access_tokens/components/token_spec.js @@ -0,0 +1,65 @@ +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +import Token from '~/access_tokens/components/token.vue'; +import InputCopyToggleVisibility from '~/vue_shared/components/form/input_copy_toggle_visibility.vue'; + +describe('Token', () => { + let wrapper; + + const defaultPropsData = { + token: 'az4a2l5f8ssa0zvdfbhidbzlx', + inputId: 'feed_token', + inputLabel: 'Feed token', + copyButtonTitle: 'Copy feed token', + }; + + const defaultSlots = { + title: 'Feed token title', + description: 'Feed token description', + 'input-description': 'Feed token input description', + }; + + const createComponent = () => { + wrapper = mountExtended(Token, { propsData: defaultPropsData, slots: defaultSlots }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders title slot', () => { + createComponent(); + + expect(wrapper.findByText(defaultSlots.title, { selector: 'h4' }).exists()).toBe(true); + }); + + it('renders description slot', () => { + createComponent(); + + expect(wrapper.findByText(defaultSlots.description).exists()).toBe(true); + }); + + it('renders input description slot', () => { + createComponent(); + + expect(wrapper.findByText(defaultSlots['input-description']).exists()).toBe(true); + }); + + it('correctly passes props to `InputCopyToggleVisibility` component', () => { + createComponent(); + + const inputCopyToggleVisibilityComponent = wrapper.findComponent(InputCopyToggleVisibility); + + expect(inputCopyToggleVisibilityComponent.props()).toMatchObject({ + formInputGroupProps: { + id: defaultPropsData.inputId, + }, + value: defaultPropsData.token, + copyButtonTitle: defaultPropsData.copyButtonTitle, + }); + expect(inputCopyToggleVisibilityComponent.attributes()).toMatchObject({ + label: defaultPropsData.inputLabel, + 'label-for': defaultPropsData.inputId, + }); + }); +}); diff --git a/spec/frontend/access_tokens/components/tokens_app_spec.js b/spec/frontend/access_tokens/components/tokens_app_spec.js new file mode 100644 index 00000000000..d7acfbb47eb --- /dev/null +++ b/spec/frontend/access_tokens/components/tokens_app_spec.js @@ -0,0 +1,148 @@ +import { merge } from 'lodash'; + +import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; + +import TokensApp from '~/access_tokens/components/tokens_app.vue'; +import { FEED_TOKEN, INCOMING_EMAIL_TOKEN, STATIC_OBJECT_TOKEN } from '~/access_tokens/constants'; + +describe('TokensApp', () => { + let wrapper; + + const defaultProvide = { + tokenTypes: { + [FEED_TOKEN]: { + enabled: true, + token: 'DUKu345VD73Py7zz3z89', + resetPath: '/-/profile/reset_feed_token', + }, + [INCOMING_EMAIL_TOKEN]: { + enabled: true, + token: 'az4a2l5f8ssa0zvdfbhidbzlx', + resetPath: '/-/profile/reset_incoming_email_token', + }, + [STATIC_OBJECT_TOKEN]: { + enabled: true, + token: 'QHXwGHYioHTgxQnAcyZ-', + resetPath: '/-/profile/reset_static_object_token', + }, + }, + }; + + const createComponent = (options = {}) => { + wrapper = mountExtended(TokensApp, merge({}, { provide: defaultProvide }, options)); + }; + + const expectTokenRendered = ({ + testId, + expectedLabel, + expectedDescription, + expectedInputDescription, + expectedResetPath, + expectedResetConfirmMessage, + expectedProps, + }) => { + const container = extendedWrapper(wrapper.findByTestId(testId)); + + expect(container.findByText(expectedLabel, { selector: 'h4' }).exists()).toBe(true); + expect(container.findByText(expectedDescription).exists()).toBe(true); + expect(container.findByText(expectedInputDescription, { exact: false }).exists()).toBe(true); + expect(container.findByText('reset this token').attributes()).toMatchObject({ + 'data-confirm': expectedResetConfirmMessage, + 'data-method': 'put', + href: expectedResetPath, + }); + expect(container.props()).toMatchObject(expectedProps); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders all enabled tokens', () => { + createComponent(); + + expectTokenRendered({ + testId: TokensApp.htmlAttributes[FEED_TOKEN].containerTestId, + expectedLabel: TokensApp.i18n[FEED_TOKEN].label, + expectedDescription: TokensApp.i18n[FEED_TOKEN].description, + expectedInputDescription: + 'Keep this token secret. Anyone who has it can read activity and issue RSS feeds or your calendar feed as if they were you.', + expectedResetPath: defaultProvide.tokenTypes[FEED_TOKEN].resetPath, + expectedResetConfirmMessage: TokensApp.i18n[FEED_TOKEN].resetConfirmMessage, + expectedProps: { + token: defaultProvide.tokenTypes[FEED_TOKEN].token, + inputId: TokensApp.htmlAttributes[FEED_TOKEN].inputId, + inputLabel: TokensApp.i18n[FEED_TOKEN].label, + copyButtonTitle: TokensApp.i18n[FEED_TOKEN].copyButtonTitle, + }, + }); + + expectTokenRendered({ + testId: TokensApp.htmlAttributes[INCOMING_EMAIL_TOKEN].containerTestId, + expectedLabel: TokensApp.i18n[INCOMING_EMAIL_TOKEN].label, + expectedDescription: TokensApp.i18n[INCOMING_EMAIL_TOKEN].description, + expectedInputDescription: + 'Keep this token secret. Anyone who has it can create issues as if they were you.', + expectedResetPath: defaultProvide.tokenTypes[INCOMING_EMAIL_TOKEN].resetPath, + expectedResetConfirmMessage: TokensApp.i18n[INCOMING_EMAIL_TOKEN].resetConfirmMessage, + expectedProps: { + token: defaultProvide.tokenTypes[INCOMING_EMAIL_TOKEN].token, + inputId: TokensApp.htmlAttributes[INCOMING_EMAIL_TOKEN].inputId, + inputLabel: TokensApp.i18n[INCOMING_EMAIL_TOKEN].label, + copyButtonTitle: TokensApp.i18n[INCOMING_EMAIL_TOKEN].copyButtonTitle, + }, + }); + + expectTokenRendered({ + testId: TokensApp.htmlAttributes[STATIC_OBJECT_TOKEN].containerTestId, + expectedLabel: TokensApp.i18n[STATIC_OBJECT_TOKEN].label, + expectedDescription: TokensApp.i18n[STATIC_OBJECT_TOKEN].description, + expectedInputDescription: + 'Keep this token secret. Anyone who has it can access repository static objects as if they were you.', + expectedResetPath: defaultProvide.tokenTypes[STATIC_OBJECT_TOKEN].resetPath, + expectedResetConfirmMessage: TokensApp.i18n[STATIC_OBJECT_TOKEN].resetConfirmMessage, + expectedProps: { + token: defaultProvide.tokenTypes[STATIC_OBJECT_TOKEN].token, + inputId: TokensApp.htmlAttributes[STATIC_OBJECT_TOKEN].inputId, + inputLabel: TokensApp.i18n[STATIC_OBJECT_TOKEN].label, + copyButtonTitle: TokensApp.i18n[STATIC_OBJECT_TOKEN].copyButtonTitle, + }, + }); + }); + + it("doesn't render disabled tokens", () => { + createComponent({ + provide: { + tokenTypes: { + [FEED_TOKEN]: { + enabled: false, + }, + }, + }, + }); + + expect( + wrapper.findByTestId(TokensApp.htmlAttributes[FEED_TOKEN].containerTestId).exists(), + ).toBe(false); + }); + + describe('when there are tokens missing an `i18n` definition', () => { + it('renders without errors', () => { + createComponent({ + provide: { + tokenTypes: { + fooBar: { + enabled: true, + token: 'rewjoa58dfm54jfkdlsdf', + resetPath: '/-/profile/foo_bar', + }, + }, + }, + }); + + expect( + wrapper.findByTestId(TokensApp.htmlAttributes[FEED_TOKEN].containerTestId).exists(), + ).toBe(true); + }); + }); +}); diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap index f4d3fd97fd8..ec5b6a5597b 100644 --- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap +++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap @@ -12,6 +12,7 @@ exports[`Alert integration settings form default state should match the default <gl-form-group-stub class="gl-pl-0" labeldescription="" + optionaltext="(optional)" > <gl-form-checkbox-stub checked="true" @@ -28,6 +29,7 @@ exports[`Alert integration settings form default state should match the default label-for="alert-integration-settings-issue-template" label-size="sm" labeldescription="" + optionaltext="(optional)" > <label class="gl-display-inline-flex" @@ -83,6 +85,7 @@ exports[`Alert integration settings form default state should match the default <gl-form-group-stub class="gl-pl-0 gl-mb-5" labeldescription="" + optionaltext="(optional)" > <gl-form-checkbox-stub> <span> @@ -94,6 +97,7 @@ exports[`Alert integration settings form default state should match the default <gl-form-group-stub class="gl-pl-0 gl-mb-5" labeldescription="" + optionaltext="(optional)" > <gl-form-checkbox-stub checked="true" diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js index 09270174674..c4002ec11f3 100644 --- a/spec/frontend/awards_handler_spec.js +++ b/spec/frontend/awards_handler_spec.js @@ -1,15 +1,12 @@ -import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; import Cookies from 'js-cookie'; +import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame'; import loadAwardsHandler from '~/awards_handler'; -import { EMOJI_VERSION } from '~/emoji'; -import axios from '~/lib/utils/axios_utils'; window.gl = window.gl || {}; window.gon = window.gon || {}; -let mock; let awardsHandler = null; const urlRoot = gon.relative_url_root; @@ -76,8 +73,7 @@ describe('AwardsHandler', () => { }; beforeEach(async () => { - mock = new MockAdapter(axios); - mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData); + await initEmojiMock(emojiData); loadFixtures('snippets/show.html'); @@ -89,7 +85,7 @@ describe('AwardsHandler', () => { // restore original url root value gon.relative_url_root = urlRoot; - mock.restore(); + clearEmojiMock(); // Undo what we did to the shared <body> $('body').removeAttr('data-page'); diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js index d23a0a84997..0f4e2e08dbd 100644 --- a/spec/frontend/behaviors/gl_emoji_spec.js +++ b/spec/frontend/behaviors/gl_emoji_spec.js @@ -1,15 +1,13 @@ -import MockAdapter from 'axios-mock-adapter'; +import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; import waitForPromises from 'helpers/wait_for_promises'; import installGlEmojiElement from '~/behaviors/gl_emoji'; -import { initEmojiMap, EMOJI_VERSION } from '~/emoji'; +import { EMOJI_VERSION } from '~/emoji'; import * as EmojiUnicodeSupport from '~/emoji/support'; -import axios from '~/lib/utils/axios_utils'; jest.mock('~/emoji/support'); describe('gl_emoji', () => { - let mock; const emojiData = { grey_question: { c: 'symbols', @@ -38,15 +36,12 @@ describe('gl_emoji', () => { return div.firstElementChild; } - beforeEach(() => { - mock = new MockAdapter(axios); - mock.onGet(`/-/emojis/${EMOJI_VERSION}/emojis.json`).reply(200, emojiData); - - return initEmojiMap().catch(() => {}); + beforeEach(async () => { + await initEmojiMock(emojiData); }); afterEach(() => { - mock.restore(); + clearEmojiMock(); document.body.innerHTML = ''; }); diff --git a/spec/frontend/crm/new_contact_form_spec.js b/spec/frontend/crm/contact_form_spec.js index 1497c348676..b2753ad8cf5 100644 --- a/spec/frontend/crm/new_contact_form_spec.js +++ b/spec/frontend/crm/contact_form_spec.js @@ -4,41 +4,49 @@ import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import NewContactForm from '~/crm/components/new_contact_form.vue'; +import ContactForm from '~/crm/components/contact_form.vue'; import createContactMutation from '~/crm/components/queries/create_contact.mutation.graphql'; +import updateContactMutation from '~/crm/components/queries/update_contact.mutation.graphql'; import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql'; import { createContactMutationErrorResponse, createContactMutationResponse, getGroupContactsQueryResponse, + updateContactMutationErrorResponse, + updateContactMutationResponse, } from './mock_data'; -describe('Customer relations contacts root app', () => { +describe('Customer relations contact form component', () => { Vue.use(VueApollo); let wrapper; let fakeApollo; + let mutation; let queryHandler; - const findCreateNewContactButton = () => wrapper.findByTestId('create-new-contact-button'); + const findSaveContactButton = () => wrapper.findByTestId('save-contact-button'); const findCancelButton = () => wrapper.findByTestId('cancel-button'); const findForm = () => wrapper.find('form'); const findError = () => wrapper.findComponent(GlAlert); - const mountComponent = ({ mountFunction = shallowMountExtended } = {}) => { - fakeApollo = createMockApollo([[createContactMutation, queryHandler]]); + const mountComponent = ({ mountFunction = shallowMountExtended, editForm = false } = {}) => { + fakeApollo = createMockApollo([[mutation, queryHandler]]); fakeApollo.clients.defaultClient.cache.writeQuery({ query: getGroupContactsQuery, variables: { groupFullPath: 'flightjs' }, data: getGroupContactsQueryResponse.data, }); - wrapper = mountFunction(NewContactForm, { + const propsData = { drawerOpen: true }; + if (editForm) + propsData.contact = { firstName: 'First', lastName: 'Last', email: 'email@example.com' }; + wrapper = mountFunction(ContactForm, { provide: { groupId: 26, groupFullPath: 'flightjs' }, apolloProvider: fakeApollo, - propsData: { drawerOpen: true }, + propsData, }); }; beforeEach(() => { + mutation = createContactMutation; queryHandler = jest.fn().mockResolvedValue(createContactMutationResponse); }); @@ -47,14 +55,14 @@ describe('Customer relations contacts root app', () => { fakeApollo = null; }); - describe('Create new contact button', () => { - it('should be disabled by default', () => { + describe('Save contact button', () => { + it('should be disabled when required fields are empty', () => { mountComponent(); - expect(findCreateNewContactButton().attributes('disabled')).toBeTruthy(); + expect(findSaveContactButton().props('disabled')).toBe(true); }); - it('should not be disabled when first, last and email have values', async () => { + it('should not be disabled when required fields have values', async () => { mountComponent(); wrapper.find('#contact-first-name').vm.$emit('input', 'A'); @@ -62,7 +70,7 @@ describe('Customer relations contacts root app', () => { wrapper.find('#contact-email').vm.$emit('input', 'C'); await waitForPromises(); - expect(findCreateNewContactButton().attributes('disabled')).toBeFalsy(); + expect(findSaveContactButton().props('disabled')).toBe(false); }); }); @@ -74,7 +82,7 @@ describe('Customer relations contacts root app', () => { expect(wrapper.emitted().close).toBeTruthy(); }); - describe('when query is successful', () => { + describe('when create mutation is successful', () => { it("should emit 'close'", async () => { mountComponent(); @@ -85,7 +93,7 @@ describe('Customer relations contacts root app', () => { }); }); - describe('when query fails', () => { + describe('when create mutation fails', () => { it('should show error on reject', async () => { queryHandler = jest.fn().mockRejectedValue('ERROR'); mountComponent(); @@ -107,4 +115,43 @@ describe('Customer relations contacts root app', () => { expect(findError().text()).toBe('Phone is invalid.'); }); }); + + describe('when update mutation is successful', () => { + it("should emit 'close'", async () => { + mutation = updateContactMutation; + queryHandler = jest.fn().mockResolvedValue(updateContactMutationResponse); + mountComponent({ editForm: true }); + + findForm().trigger('submit'); + await waitForPromises(); + + expect(wrapper.emitted().close).toBeTruthy(); + }); + }); + + describe('when update mutation fails', () => { + beforeEach(() => { + mutation = updateContactMutation; + }); + + it('should show error on reject', async () => { + queryHandler = jest.fn().mockRejectedValue('ERROR'); + mountComponent({ editForm: true }); + findForm().trigger('submit'); + await waitForPromises(); + + expect(findError().exists()).toBe(true); + }); + + it('should show error on error response', async () => { + queryHandler = jest.fn().mockResolvedValue(updateContactMutationErrorResponse); + mountComponent({ editForm: true }); + + findForm().trigger('submit'); + await waitForPromises(); + + expect(findError().exists()).toBe(true); + expect(findError().text()).toBe('Email is invalid.'); + }); + }); }); diff --git a/spec/frontend/crm/contacts_root_spec.js b/spec/frontend/crm/contacts_root_spec.js index 18bd2d7c45b..b30349305a3 100644 --- a/spec/frontend/crm/contacts_root_spec.js +++ b/spec/frontend/crm/contacts_root_spec.js @@ -6,8 +6,10 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import ContactsRoot from '~/crm/components/contacts_root.vue'; -import NewContactForm from '~/crm/components/new_contact_form.vue'; +import ContactForm from '~/crm/components/contact_form.vue'; import getGroupContactsQuery from '~/crm/components/queries/get_group_contacts.query.graphql'; +import { NEW_ROUTE_NAME, EDIT_ROUTE_NAME } from '~/crm/constants'; +import routes from '~/crm/routes'; import { getGroupContactsQueryResponse } from './mock_data'; describe('Customer relations contacts root app', () => { @@ -21,7 +23,8 @@ describe('Customer relations contacts root app', () => { const findRowByName = (rowName) => wrapper.findAllByRole('row', { name: rowName }); const findIssuesLinks = () => wrapper.findAllByTestId('issues-link'); const findNewContactButton = () => wrapper.findByTestId('new-contact-button'); - const findNewContactForm = () => wrapper.findComponent(NewContactForm); + const findEditContactButton = () => wrapper.findByTestId('edit-contact-button'); + const findContactForm = () => wrapper.findComponent(ContactForm); const findError = () => wrapper.findComponent(GlAlert); const successQueryHandler = jest.fn().mockResolvedValue(getGroupContactsQueryResponse); @@ -49,7 +52,7 @@ describe('Customer relations contacts root app', () => { router = new VueRouter({ base: basePath, mode: 'history', - routes: [], + routes, }); }); @@ -79,12 +82,12 @@ describe('Customer relations contacts root app', () => { }); }); - describe('new contact form', () => { + describe('contact form', () => { it('should not exist by default', async () => { mountComponent(); await waitForPromises(); - expect(findNewContactForm().exists()).toBe(false); + expect(findContactForm().exists()).toBe(false); }); it('should exist when user clicks new contact button', async () => { @@ -93,25 +96,54 @@ describe('Customer relations contacts root app', () => { findNewContactButton().vm.$emit('click'); await waitForPromises(); - expect(findNewContactForm().exists()).toBe(true); + expect(findContactForm().exists()).toBe(true); }); - it('should exist when user navigates directly to /new', async () => { - router.replace({ path: '/new' }); + it('should exist when user navigates directly to `new` route', async () => { + router.replace({ name: NEW_ROUTE_NAME }); mountComponent(); await waitForPromises(); - expect(findNewContactForm().exists()).toBe(true); + expect(findContactForm().exists()).toBe(true); }); - it('should not exist when form emits close', async () => { - router.replace({ path: '/new' }); + it('should exist when user clicks edit contact button', async () => { + mountComponent({ mountFunction: mountExtended }); + await waitForPromises(); + + findEditContactButton().vm.$emit('click'); + await waitForPromises(); + + expect(findContactForm().exists()).toBe(true); + }); + + it('should exist when user navigates directly to `edit` route', async () => { + router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } }); mountComponent(); + await waitForPromises(); + + expect(findContactForm().exists()).toBe(true); + }); + + it('should not exist when new form emits close', async () => { + router.replace({ name: NEW_ROUTE_NAME }); + mountComponent(); + + findContactForm().vm.$emit('close'); + await waitForPromises(); + + expect(findContactForm().exists()).toBe(false); + }); + + it('should not exist when edit form emits close', async () => { + router.replace({ name: EDIT_ROUTE_NAME, params: { id: 16 } }); + mountComponent(); + await waitForPromises(); - findNewContactForm().vm.$emit('close'); + findContactForm().vm.$emit('close'); await waitForPromises(); - expect(findNewContactForm().exists()).toBe(false); + expect(findContactForm().exists()).toBe(false); }); }); diff --git a/spec/frontend/crm/mock_data.js b/spec/frontend/crm/mock_data.js index e784ac3764d..3abbc488081 100644 --- a/spec/frontend/crm/mock_data.js +++ b/spec/frontend/crm/mock_data.js @@ -106,3 +106,31 @@ export const createContactMutationErrorResponse = { }, }, }; + +export const updateContactMutationResponse = { + data: { + customerRelationsContactUpdate: { + __typeName: 'CustomerRelationsContactCreatePayload', + contact: { + __typename: 'CustomerRelationsContact', + id: 'gid://gitlab/CustomerRelations::Contact/1', + firstName: 'First', + lastName: 'Last', + email: 'email@example.com', + phone: null, + description: null, + organization: null, + }, + errors: [], + }, + }, +}; + +export const updateContactMutationErrorResponse = { + data: { + customerRelationsContactUpdate: { + contact: null, + errors: ['Email is invalid.'], + }, + }, +}; diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js index 9652c513671..cc037586496 100644 --- a/spec/frontend/emoji/index_spec.js +++ b/spec/frontend/emoji/index_spec.js @@ -1,6 +1,21 @@ -import { emojiFixtureMap, mockEmojiData, initEmojiMock } from 'helpers/emoji'; +import { + emojiFixtureMap, + mockEmojiData, + initEmojiMock, + validEmoji, + invalidEmoji, + clearEmojiMock, +} from 'helpers/emoji'; import { trimText } from 'helpers/text_helper'; -import { glEmojiTag, searchEmoji, getEmojiInfo, sortEmoji } from '~/emoji'; +import { + glEmojiTag, + searchEmoji, + getEmojiInfo, + sortEmoji, + initEmojiMap, + getAllEmoji, +} from '~/emoji'; + import isEmojiUnicodeSupported, { isFlagEmoji, isRainbowFlagEmoji, @@ -9,7 +24,6 @@ import isEmojiUnicodeSupported, { isHorceRacingSkinToneComboEmoji, isPersonZwjEmoji, } from '~/emoji/support/is_emoji_unicode_supported'; -import { sanitize } from '~/lib/dompurify'; const emptySupportMap = { personZwj: false, @@ -31,14 +45,55 @@ const emptySupportMap = { }; describe('emoji', () => { - let mock; - beforeEach(async () => { - mock = await initEmojiMock(); + await initEmojiMock(); }); afterEach(() => { - mock.restore(); + clearEmojiMock(); + }); + + describe('initEmojiMap', () => { + it('should contain valid emoji', async () => { + await initEmojiMap(); + + const allEmoji = Object.keys(getAllEmoji()); + Object.keys(validEmoji).forEach((key) => { + expect(allEmoji.includes(key)).toBe(true); + }); + }); + + it('should not contain invalid emoji', async () => { + await initEmojiMap(); + + const allEmoji = Object.keys(getAllEmoji()); + Object.keys(invalidEmoji).forEach((key) => { + expect(allEmoji.includes(key)).toBe(false); + }); + }); + + it('fixes broken pride emoji', async () => { + clearEmojiMock(); + await initEmojiMock({ + gay_pride_flag: { + c: 'flags', + // Without a zero-width joiner + e: '🏳🌈', + name: 'gay_pride_flag', + u: '6.0', + }, + }); + + expect(getAllEmoji()).toEqual({ + gay_pride_flag: { + c: 'flags', + // With a zero-width joiner + e: '🏳️🌈', + name: 'gay_pride_flag', + u: '6.0', + }, + }); + }); }); describe('glEmojiTag', () => { @@ -378,32 +433,14 @@ describe('emoji', () => { }); describe('searchEmoji', () => { - const emojiFixture = Object.keys(mockEmojiData).reduce((acc, k) => { - const { name, e, u, d } = mockEmojiData[k]; - acc[k] = { name, e: sanitize(e), u, d }; - - return acc; - }, {}); - it.each([undefined, null, ''])("should return all emoji when the input is '%s'", (input) => { const search = searchEmoji(input); - const expected = [ - 'atom', - 'bomb', - 'construction_worker_tone5', - 'five', - 'grey_question', - 'black_heart', - 'heart', - 'custard', - 'star', - 'xss', - ].map((name) => { + const expected = Object.keys(validEmoji).map((name) => { return { - emoji: emojiFixture[name], + emoji: mockEmojiData[name], field: 'd', - fieldValue: emojiFixture[name].d, + fieldValue: mockEmojiData[name].d, score: 0, }; }); @@ -453,7 +490,7 @@ describe('emoji', () => { const { field, score, fieldValue, name } = item; return { - emoji: emojiFixture[name], + emoji: mockEmojiData[name], field, fieldValue, score, @@ -564,9 +601,9 @@ describe('emoji', () => { const { field, score, name } = item; return { - emoji: emojiFixture[name], + emoji: mockEmojiData[name], field, - fieldValue: emojiFixture[name][field], + fieldValue: mockEmojiData[name][field], score, }; }); @@ -622,13 +659,4 @@ describe('emoji', () => { expect(sortEmoji(scoredItems)).toEqual(expected); }); }); - - describe('sanitize emojis', () => { - it('should return sanitized emoji', () => { - expect(getEmojiInfo('xss')).toEqual({ - ...mockEmojiData.xss, - e: '<img src="x">', - }); - }); - }); }); diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js index 631e3307f7f..1ab3286fe4c 100644 --- a/spec/frontend/gfm_auto_complete_spec.js +++ b/spec/frontend/gfm_auto_complete_spec.js @@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import $ from 'jquery'; import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json'; import GfmAutoComplete, { membersBeforeSave, highlighter } from 'ee_else_ce/gfm_auto_complete'; -import { initEmojiMock } from 'helpers/emoji'; +import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; import '~/lib/utils/jquery_at_who'; import { TEST_HOST } from 'helpers/test_constants'; import waitForPromises from 'helpers/wait_for_promises'; @@ -803,8 +803,6 @@ describe('GfmAutoComplete', () => { }); describe('emoji', () => { - let mock; - const mockItem = { 'atwho-at': ':', emoji: { @@ -818,14 +816,14 @@ describe('GfmAutoComplete', () => { }; beforeEach(async () => { - mock = await initEmojiMock(); + await initEmojiMock(); await new GfmAutoComplete({}).loadEmojiData({ atwho() {}, trigger() {} }, ':'); if (!GfmAutoComplete.glEmojiTag) throw new Error('emoji not loaded'); }); afterEach(() => { - mock.restore(); + clearEmojiMock(); }); describe('Emoji.templateFunction', () => { diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap index 33e2c0db5e5..9447e7daba8 100644 --- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap +++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap @@ -47,6 +47,7 @@ exports[`grafana integration component default state to match the default snapsh label="Enable authentication" label-for="grafana-integration-enabled" labeldescription="" + optionaltext="(optional)" > <gl-form-checkbox-stub id="grafana-integration-enabled" @@ -62,6 +63,7 @@ exports[`grafana integration component default state to match the default snapsh label="Grafana URL" label-for="grafana-url" labeldescription="" + optionaltext="(optional)" > <gl-form-input-stub id="grafana-url" @@ -74,6 +76,7 @@ exports[`grafana integration component default state to match the default snapsh label="API token" label-for="grafana-token" labeldescription="" + optionaltext="(optional)" > <gl-form-input-stub id="grafana-token" diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap index 174a42c5dc2..feee14c9c40 100644 --- a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap +++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap @@ -14,6 +14,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] = <gl-form-group-stub class="col-8 col-md-9 gl-p-0" labeldescription="" + optionaltext="(optional)" > <gl-toggle-stub id="active" @@ -28,6 +29,7 @@ exports[`Alert integration settings form should match the default snapshot 1`] = label="Webhook URL" label-for="url" labeldescription="" + optionaltext="(optional)" > <gl-form-input-group-stub data-testid="webhook-url" diff --git a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap index 1a874c3dcd6..c968c28c811 100644 --- a/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap +++ b/spec/frontend/self_monitor/components/__snapshots__/self_monitor_form_spec.js.snap @@ -52,6 +52,7 @@ exports[`self monitor component When the self monitor project has not been creat <gl-form-group-stub labeldescription="" + optionaltext="(optional)" > <gl-toggle-stub label="Self monitoring" diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js index 7577c65132b..d7261784edc 100644 --- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js +++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js @@ -1,6 +1,6 @@ import { GlModal, GlFormCheckbox } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { initEmojiMock } from 'helpers/emoji'; +import { initEmojiMock, clearEmojiMock } from 'helpers/emoji'; import * as UserApi from '~/api/user_api'; import EmojiPicker from '~/emoji/components/picker.vue'; import createFlash from '~/flash'; @@ -12,7 +12,6 @@ jest.mock('~/flash'); describe('SetStatusModalWrapper', () => { let wrapper; - let mockEmoji; const $toast = { show: jest.fn(), }; @@ -63,12 +62,12 @@ describe('SetStatusModalWrapper', () => { afterEach(() => { wrapper.destroy(); - mockEmoji.restore(); + clearEmojiMock(); }); describe('with minimum props', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent(); return initModal(); }); @@ -112,7 +111,7 @@ describe('SetStatusModalWrapper', () => { describe('improvedEmojiPicker is true', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent({}, true); return initModal(); }); @@ -126,7 +125,7 @@ describe('SetStatusModalWrapper', () => { describe('with no currentMessage set', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent({ currentMessage: '' }); return initModal(); }); @@ -146,7 +145,7 @@ describe('SetStatusModalWrapper', () => { describe('with no currentEmoji set', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent({ currentEmoji: '' }); return initModal(); }); @@ -161,7 +160,7 @@ describe('SetStatusModalWrapper', () => { describe('with no currentMessage set', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent({ currentEmoji: '', currentMessage: '' }); return initModal(); }); @@ -174,7 +173,7 @@ describe('SetStatusModalWrapper', () => { describe('with currentClearStatusAfter set', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent({ currentClearStatusAfter: '2021-01-01 00:00:00 UTC' }); return initModal(); }); @@ -190,7 +189,7 @@ describe('SetStatusModalWrapper', () => { describe('update status', () => { describe('succeeds', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent(); await initModal(); @@ -246,7 +245,7 @@ describe('SetStatusModalWrapper', () => { describe('success message', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent({ currentEmoji: '', currentMessage: '' }); jest.spyOn(UserApi, 'updateUserStatus').mockResolvedValue(); return initModal({ mockOnUpdateSuccess: false }); @@ -262,7 +261,7 @@ describe('SetStatusModalWrapper', () => { describe('with errors', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent(); await initModal(); @@ -279,7 +278,7 @@ describe('SetStatusModalWrapper', () => { describe('error message', () => { beforeEach(async () => { - mockEmoji = await initEmojiMock(); + await initEmojiMock(); wrapper = createComponent({ currentEmoji: '', currentMessage: '' }); jest.spyOn(UserApi, 'updateUserStatus').mockRejectedValue(); return initModal({ mockOnUpdateFailure: false }); diff --git a/spec/frontend/shortcuts_spec.js b/spec/frontend/shortcuts_spec.js index 455db325066..49148123a1c 100644 --- a/spec/frontend/shortcuts_spec.js +++ b/spec/frontend/shortcuts_spec.js @@ -25,6 +25,7 @@ describe('Shortcuts', () => { jest.spyOn(document.querySelector('.js-new-note-form .js-md-preview-button'), 'focus'); jest.spyOn(document.querySelector('.edit-note .js-md-preview-button'), 'focus'); + jest.spyOn(document.querySelector('#search'), 'focus'); new Shortcuts(); // eslint-disable-line no-new }); @@ -111,4 +112,12 @@ describe('Shortcuts', () => { }); }); }); + + describe('focusSearch', () => { + it('focuses the search bar', () => { + Shortcuts.focusSearch(createEvent('KeyboardEvent')); + + expect(document.querySelector('#search').focus).toHaveBeenCalled(); + }); + }); }); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap index 5df69ffb5f8..f4ebc5c3e3f 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap @@ -23,6 +23,7 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] = class="gl-mb-0" id="visibility-level-setting" labeldescription="" + optionaltext="(optional)" > <gl-form-radio-group-stub checked="private" diff --git a/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap b/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap index ff1dad2de68..58ad1f681bc 100644 --- a/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap +++ b/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap @@ -5,6 +5,7 @@ exports[`Title edit field matches the snapshot 1`] = ` label="Title" label-for="title-field-edit" labeldescription="" + optionaltext="(optional)" > <gl-form-input-stub /> </gl-form-group-stub> |