diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-24 15:09:32 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-07-24 15:09:32 +0300 |
commit | f296f23500b4b3758670ae0c5ce2e1779f533e8b (patch) | |
tree | 717151cb9e81d489b4ecf880988ea10d77b7224f /spec/frontend | |
parent | fd7c75bf603f4f2f1a4a4e63ef5cbc1a51cc0a15 (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
7 files changed, 298 insertions, 40 deletions
diff --git a/spec/frontend/groups/service/archived_projects_service_spec.js b/spec/frontend/groups/service/archived_projects_service_spec.js index 3aec9d57ee1..8e9dfb0f971 100644 --- a/spec/frontend/groups/service/archived_projects_service_spec.js +++ b/spec/frontend/groups/service/archived_projects_service_spec.js @@ -18,11 +18,9 @@ describe('ArchivedProjectsService', () => { const query = 'git'; const sort = 'created_asc'; - beforeEach(() => { + it('returns promise the resolves with formatted project', async () => { Api.groupProjects.mockResolvedValueOnce({ data: projects, headers }); - }); - it('returns promise the resolves with formatted project', async () => { await expect(service.getGroups(undefined, page, query, sort)).resolves.toEqual({ data: projects.map((project) => { return { @@ -47,7 +45,7 @@ describe('ArchivedProjectsService', () => { number_users_with_delimiter: 0, star_count: project.star_count, updated_at: project.updated_at, - marked_for_deletion: project.marked_for_deletion_at !== null, + marked_for_deletion: false, last_activity_at: project.last_activity_at, }; }), @@ -63,6 +61,35 @@ describe('ArchivedProjectsService', () => { }); describe.each` + markedForDeletionAt | expected + ${null} | ${false} + ${undefined} | ${false} + ${'2023-07-21'} | ${true} + `( + 'when `marked_for_deletion_at` is $markedForDeletionAt', + ({ markedForDeletionAt, expected }) => { + it(`sets marked_for_deletion to ${expected}`, async () => { + Api.groupProjects.mockResolvedValueOnce({ + data: projects.map((project) => ({ + ...project, + marked_for_deletion_at: markedForDeletionAt, + })), + headers, + }); + + await expect(service.getGroups(undefined, page, query, sort)).resolves.toMatchObject({ + data: projects.map(() => { + return { + marked_for_deletion: expected, + }; + }), + headers, + }); + }); + }, + ); + + describe.each` sortArgument | expectedOrderByParameter | expectedSortParameter ${'name_asc'} | ${'name'} | ${'asc'} ${'name_desc'} | ${'name'} | ${'desc'} @@ -75,6 +102,8 @@ describe('ArchivedProjectsService', () => { 'when the sort argument is $sortArgument', ({ sortArgument, expectedSortParameter, expectedOrderByParameter }) => { it(`calls the API with sort parameter set to ${expectedSortParameter} and order_by parameter set to ${expectedOrderByParameter}`, () => { + Api.groupProjects.mockResolvedValueOnce({ data: projects, headers }); + service.getGroups(undefined, page, query, sortArgument); expect(Api.groupProjects).toHaveBeenCalledWith(groupId, query, { diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js index 4c13ec555c2..87bee6afd62 100644 --- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js +++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js @@ -1,4 +1,10 @@ -import { GlDropdown, GlIcon, GlDropdownItem } from '@gitlab/ui'; +import { + GlDisclosureDropdown, + GlDisclosureDropdownItem, + GlButtonGroup, + GlButton, + GlIcon, +} from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue'; @@ -13,6 +19,11 @@ describe('import actions cell', () => { isInvalid: false, ...props, }, + stubs: { + GlButtonGroup, + GlDisclosureDropdown, + GlDisclosureDropdownItem, + }, }); }; @@ -22,9 +33,9 @@ describe('import actions cell', () => { }); it('renders import dropdown', () => { - const dropdown = wrapper.findComponent(GlDropdown); - expect(dropdown.exists()).toBe(true); - expect(dropdown.props('text')).toBe('Import with projects'); + const button = wrapper.findComponent(GlButton); + expect(button.exists()).toBe(true); + expect(button.text()).toBe('Import with projects'); }); it('does not render icon with a hint', () => { @@ -38,9 +49,9 @@ describe('import actions cell', () => { }); it('renders re-import dropdown', () => { - const dropdown = wrapper.findComponent(GlDropdown); - expect(dropdown.exists()).toBe(true); - expect(dropdown.props('text')).toBe('Re-import with projects'); + const button = wrapper.findComponent(GlButton); + expect(button.exists()).toBe(true); + expect(button.text()).toBe('Re-import with projects'); }); it('renders icon with a hint', () => { @@ -55,22 +66,22 @@ describe('import actions cell', () => { it('does not render import dropdown when group is not available for import', () => { createComponent({ isAvailableForImport: false }); - const dropdown = wrapper.findComponent(GlDropdown); + const dropdown = wrapper.findComponent(GlDisclosureDropdown); expect(dropdown.exists()).toBe(false); }); it('renders import dropdown as disabled when group is invalid', () => { createComponent({ isInvalid: true, isAvailableForImport: true }); - const dropdown = wrapper.findComponent(GlDropdown); + const dropdown = wrapper.findComponent(GlDisclosureDropdown); expect(dropdown.props().disabled).toBe(true); }); it('emits import-group event when import button is clicked', () => { createComponent({ isAvailableForImport: true }); - const dropdown = wrapper.findComponent(GlDropdown); - dropdown.vm.$emit('click'); + const button = wrapper.findComponent(GlButton); + button.vm.$emit('click'); expect(wrapper.emitted('import-group')).toHaveLength(1); }); @@ -87,23 +98,24 @@ describe('import actions cell', () => { }); it('render import dropdown', () => { - const dropdown = wrapper.findComponent(GlDropdown); - expect(dropdown.props('text')).toBe(`${expectedAction} with projects`); - expect(dropdown.findComponent(GlDropdownItem).text()).toBe( + const button = wrapper.findComponent(GlButton); + const dropdown = wrapper.findComponent(GlDisclosureDropdown); + expect(button.element).toHaveText(`${expectedAction} with projects`); + expect(dropdown.findComponent(GlDisclosureDropdownItem).text()).toBe( `${expectedAction} without projects`, ); }); it('request migrate projects by default', () => { - const dropdown = wrapper.findComponent(GlDropdown); - dropdown.vm.$emit('click'); + const button = wrapper.findComponent(GlButton); + button.vm.$emit('click'); expect(wrapper.emitted('import-group')[0]).toStrictEqual([{ migrateProjects: true }]); }); it('request not to migrate projects via dropdown option', () => { - const dropdown = wrapper.findComponent(GlDropdown); - dropdown.findComponent(GlDropdownItem).vm.$emit('click'); + const dropdown = wrapper.findComponent(GlDisclosureDropdown); + dropdown.findComponent(GlDisclosureDropdownItem).vm.$emit('action'); expect(wrapper.emitted('import-group')[0]).toStrictEqual([{ migrateProjects: false }]); }); diff --git a/spec/frontend/sessions/new/components/email_verification_spec.js b/spec/frontend/sessions/new/components/email_verification_spec.js new file mode 100644 index 00000000000..8ff139e8475 --- /dev/null +++ b/spec/frontend/sessions/new/components/email_verification_spec.js @@ -0,0 +1,205 @@ +import { GlForm, GlFormInput } from '@gitlab/ui'; +import axios from 'axios'; +import MockAdapter from 'axios-mock-adapter'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { s__ } from '~/locale'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; +import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status'; +import EmailVerification from '~/sessions/new/components/email_verification.vue'; +import { visitUrl } from '~/lib/utils/url_utility'; +import { + I18N_EMAIL_EMPTY_CODE, + I18N_EMAIL_INVALID_CODE, + I18N_GENERIC_ERROR, + I18N_RESEND_LINK, + I18N_EMAIL_RESEND_SUCCESS, +} from '~/sessions/new/constants'; + +jest.mock('~/alert'); +jest.mock('~/lib/utils/url_utility', () => ({ + ...jest.requireActual('~/lib/utils/url_utility'), + visitUrl: jest.fn(), +})); + +describe('EmailVerification', () => { + let wrapper; + let axiosMock; + + const defaultPropsData = { + obfuscatedEmail: 'al**@g*****.com', + verifyPath: '/users/sign_in', + resendPath: '/users/resend_verification_code', + }; + + const createComponent = () => { + wrapper = mountExtended(EmailVerification, { + propsData: defaultPropsData, + }); + }; + + const findForm = () => wrapper.findComponent(GlForm); + const findCodeInput = () => wrapper.findComponent(GlFormInput); + const findSubmitButton = () => wrapper.find('[type="submit"]'); + const findResendLink = () => wrapper.findByText(I18N_RESEND_LINK); + const enterCode = (code) => findCodeInput().setValue(code); + const submitForm = () => findForm().trigger('submit'); + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + createComponent(); + }); + + afterEach(() => { + createAlert.mockClear(); + axiosMock.restore(); + }); + + describe('rendering the form', () => { + it('contains the obfuscated email address', () => { + expect(wrapper.text()).toContain(defaultPropsData.obfuscatedEmail); + }); + }); + + describe('verifying the code', () => { + describe('when successfully verifying the code', () => { + const redirectPath = 'root'; + + beforeEach(async () => { + enterCode('123456'); + + axiosMock + .onPost(defaultPropsData.verifyPath) + .reply(HTTP_STATUS_OK, { status: 'success', redirect_path: redirectPath }); + + await submitForm(); + await axios.waitForAll(); + }); + + it('redirects to the returned redirect path', () => { + expect(visitUrl).toHaveBeenCalledWith(redirectPath); + }); + }); + + describe('error messages', () => { + it.each` + scenario | code | submit | codeValid | errorShown | message + ${'shows no error messages before submitting the form'} | ${''} | ${false} | ${false} | ${false} | ${null} + ${'shows no error messages before submitting the form'} | ${'xxx'} | ${false} | ${false} | ${false} | ${null} + ${'shows no error messages before submitting the form'} | ${'123456'} | ${false} | ${true} | ${false} | ${null} + ${'shows empty code error message when submitting the form'} | ${''} | ${true} | ${false} | ${true} | ${I18N_EMAIL_EMPTY_CODE} + ${'shows invalid error message when submitting the form'} | ${'xxx'} | ${true} | ${false} | ${true} | ${I18N_EMAIL_INVALID_CODE} + ${'shows incorrect code error message returned from the server'} | ${'123456'} | ${true} | ${true} | ${true} | ${s__('IdentityVerification|The code is incorrect. Enter it again, or send a new code.')} + `(`$scenario with code $code`, async ({ code, submit, codeValid, errorShown, message }) => { + enterCode(code); + + if (submit && codeValid) { + axiosMock + .onPost(defaultPropsData.verifyPath) + .replyOnce(HTTP_STATUS_OK, { status: 'failure', message }); + } + + if (submit) { + await submitForm(); + await axios.waitForAll(); + } + + expect(findCodeInput().classes('is-invalid')).toBe(errorShown); + expect(findSubmitButton().props('disabled')).toBe(errorShown); + if (message) expect(wrapper.text()).toContain(message); + }); + + it('keeps showing error messages for invalid codes after submitting the form', async () => { + const serverErrorMessage = 'error message'; + + enterCode('123456'); + + axiosMock + .onPost(defaultPropsData.verifyPath) + .replyOnce(HTTP_STATUS_OK, { status: 'failure', message: serverErrorMessage }); + + await submitForm(); + await axios.waitForAll(); + + expect(wrapper.text()).toContain(serverErrorMessage); + + await enterCode(''); + expect(wrapper.text()).toContain(I18N_EMAIL_EMPTY_CODE); + + await enterCode('xxx'); + expect(wrapper.text()).toContain(I18N_EMAIL_INVALID_CODE); + }); + + it('captures the error and shows an alert message when the request failed', async () => { + enterCode('123456'); + + axiosMock.onPost(defaultPropsData.verifyPath).replyOnce(HTTP_STATUS_OK, null); + + await submitForm(); + await axios.waitForAll(); + + expect(createAlert).toHaveBeenCalledWith({ + message: I18N_GENERIC_ERROR, + captureError: true, + error: expect.any(Error), + }); + }); + + it('captures the error and shows an alert message when the request undefined', async () => { + enterCode('123456'); + + axiosMock.onPost(defaultPropsData.verifyPath).reply(HTTP_STATUS_OK, { status: undefined }); + + await submitForm(); + await axios.waitForAll(); + + expect(createAlert).toHaveBeenCalledWith({ + message: I18N_GENERIC_ERROR, + captureError: true, + error: undefined, + }); + }); + }); + }); + + describe('resending the code', () => { + const failedMessage = 'Failure sending the code'; + const successAlertObject = { + message: I18N_EMAIL_RESEND_SUCCESS, + variant: VARIANT_SUCCESS, + }; + const failedAlertObject = { + message: failedMessage, + }; + const undefinedAlertObject = { + captureError: true, + error: undefined, + message: I18N_GENERIC_ERROR, + }; + const genericAlertObject = { + message: I18N_GENERIC_ERROR, + captureError: true, + error: expect.any(Error), + }; + + it.each` + scenario | statusCode | response | alertObject + ${'the code was successfully resend'} | ${HTTP_STATUS_OK} | ${{ status: 'success' }} | ${successAlertObject} + ${'there was a problem resending the code'} | ${HTTP_STATUS_OK} | ${{ status: 'failure', message: failedMessage }} | ${failedAlertObject} + ${'when the request is undefined'} | ${HTTP_STATUS_OK} | ${{ status: undefined }} | ${undefinedAlertObject} + ${'when the request failed'} | ${HTTP_STATUS_NOT_FOUND} | ${null} | ${genericAlertObject} + `(`shows an alert message when $scenario`, async ({ statusCode, response, alertObject }) => { + enterCode('xxx'); + + await submitForm(); + + axiosMock.onPost(defaultPropsData.resendPath).replyOnce(statusCode, response); + + findResendLink().trigger('click'); + + await axios.waitForAll(); + + expect(createAlert).toHaveBeenCalledWith(alertObject); + expect(findCodeInput().element.value).toBe(''); + }); + }); +}); diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js index e983519d9fc..03f509a3fa3 100644 --- a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js +++ b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js @@ -1,8 +1,13 @@ import { mount } from '@vue/test-utils'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; import IssuableCreateRoot from '~/vue_shared/issuable/create/components/issuable_create_root.vue'; import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue'; +Vue.use(VueApollo); + const createComponent = ({ descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown', descriptionHelpPath = '/help/user/markdown', @@ -16,6 +21,7 @@ const createComponent = ({ labelsFetchPath, labelsManagePath, }, + apolloProvider: createMockApollo(), slots: { title: ` <h1 class="js-create-title">New Issuable</h1> diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js index ae2fd5ebffa..338dc80b43e 100644 --- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js +++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js @@ -2,8 +2,9 @@ import { GlFormInput } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue'; -import MarkdownField from '~/vue_shared/components/markdown/field.vue'; +import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue'; import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue'; +import { __ } from '~/locale'; const createComponent = ({ descriptionPreviewPath = '/gitlab-org/gitlab-shell/preview_markdown', @@ -24,7 +25,7 @@ const createComponent = ({ `, }, stubs: { - MarkdownField, + MarkdownEditor, }, }); }; @@ -71,18 +72,20 @@ describe('IssuableForm', () => { expect(descriptionFieldEl.exists()).toBe(true); expect(descriptionFieldEl.find('label').text()).toBe('Description'); - expect(descriptionFieldEl.findComponent(MarkdownField).exists()).toBe(true); - expect(descriptionFieldEl.findComponent(MarkdownField).props()).toMatchObject({ - markdownPreviewPath: wrapper.vm.descriptionPreviewPath, + expect(descriptionFieldEl.findComponent(MarkdownEditor).exists()).toBe(true); + expect(descriptionFieldEl.findComponent(MarkdownEditor).props()).toMatchObject({ + renderMarkdownPath: wrapper.vm.descriptionPreviewPath, markdownDocsPath: wrapper.vm.descriptionHelpPath, - addSpacingClasses: false, - showSuggestPopover: true, - textareaValue: '', + value: '', + formFieldProps: { + ariaLabel: __('Description'), + class: 'rspec-issuable-form-description', + placeholder: __('Write a comment or drag your files here…'), + dataQaSelector: 'issuable_form_description_field', + id: 'issuable-description', + name: 'issuable-description', + }, }); - expect(descriptionFieldEl.find('textarea').exists()).toBe(true); - expect(descriptionFieldEl.find('textarea').attributes('placeholder')).toBe( - 'Write a comment or drag your files here…', - ); }); it('renders labels select field', () => { diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js index dd46505bd65..e24cfe27616 100644 --- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js +++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js @@ -108,8 +108,8 @@ describe('WorkItemLinks', () => { describe('add link form', () => { it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => { await createComponent(); - findToggleFormDropdown().vm.$emit('click'); - findToggleAddFormButton().vm.$emit('click'); + findToggleFormDropdown().vm.$emit('action'); + findToggleAddFormButton().vm.$emit('action'); await nextTick(); expect(findAddLinksForm().exists()).toBe(true); @@ -123,8 +123,8 @@ describe('WorkItemLinks', () => { it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => { await createComponent(); - findToggleFormDropdown().vm.$emit('click'); - findToggleCreateFormButton().vm.$emit('click'); + findToggleFormDropdown().vm.$emit('action'); + findToggleCreateFormButton().vm.$emit('action'); await nextTick(); expect(findAddLinksForm().exists()).toBe(true); @@ -195,8 +195,8 @@ describe('WorkItemLinks', () => { .fn() .mockResolvedValue(getIssueDetailsResponse({ confidential: true })), }); - findToggleFormDropdown().vm.$emit('click'); - findToggleAddFormButton().vm.$emit('click'); + findToggleFormDropdown().vm.$emit('action'); + findToggleAddFormButton().vm.$emit('action'); await nextTick(); expect(findAddLinksForm().props('parentConfidential')).toBe(true); diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js index d7e5c02ffbe..0c5ce179acc 100644 --- a/spec/frontend/work_items/mock_data.js +++ b/spec/frontend/work_items/mock_data.js @@ -584,6 +584,7 @@ export const workItemResponseFactory = ({ __typename: 'WorkItemWidgetProgress', type: 'PROGRESS', progress: 0, + updatedAt: new Date(), } : { type: 'MOCK TYPE' }, milestoneWidgetPresent @@ -1145,6 +1146,7 @@ export const workItemObjectiveMetadataWidgets = { type: 'PROGRESS', __typename: 'WorkItemWidgetProgress', progress: 10, + updatedAt: new Date(), }, }; @@ -1213,6 +1215,7 @@ export const workItemObjectiveNoMetadata = { __typename: 'WorkItemWidgetProgress', type: 'PROGRESS', progress: null, + updatedAt: null, }, { __typename: 'WorkItemWidgetMilestone', |