From aee0a117a889461ce8ced6fcf73207fe017f1d99 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Mon, 20 Dec 2021 13:37:47 +0000 Subject: Add latest changes from gitlab-org/gitlab@14-6-stable-ee --- .../devops_score/components/devops_score_spec.js | 10 +- .../admin/deploy_keys/components/table_spec.js | 209 ++++++++++++++++++++- .../admin/statistics_panel/components/app_spec.js | 7 +- .../admin/users/components/actions/actions_spec.js | 7 +- .../__snapshots__/delete_user_modal_spec.js.snap | 80 ++++++++ .../components/modals/delete_user_modal_spec.js | 29 ++- .../admin/users/components/users_table_spec.js | 6 +- 7 files changed, 320 insertions(+), 28 deletions(-) (limited to 'spec/frontend/admin') diff --git a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js index 824eb033671..14f94e671a4 100644 --- a/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js +++ b/spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js @@ -1,4 +1,4 @@ -import { GlTable, GlBadge, GlEmptyState } from '@gitlab/ui'; +import { GlTableLite, GlBadge, GlEmptyState } from '@gitlab/ui'; import { GlSingleStat } from '@gitlab/ui/dist/charts'; import { mount } from '@vue/test-utils'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; @@ -20,7 +20,7 @@ describe('DevopsScore', () => { ); }; - const findTable = () => wrapper.findComponent(GlTable); + const findTable = () => wrapper.findComponent(GlTableLite); const findEmptyState = () => wrapper.findComponent(GlEmptyState); const findCol = (testId) => findTable().find(`[data-testid="${testId}"]`); const findUsageCol = () => findCol('usageCol'); @@ -44,7 +44,7 @@ describe('DevopsScore', () => { }); it('displays the correct message', () => { - expect(findEmptyState().text()).toBe( + expect(findEmptyState().text().replace(/\s+/g, ' ')).toBe( 'Data is still calculating... It may be several days before you see feature usage data. See example DevOps Score page in our documentation.', ); }); @@ -124,11 +124,11 @@ describe('DevopsScore', () => { describe('table columns', () => { describe('Your usage', () => { - it('displays the corrrect value', () => { + it('displays the correct value', () => { expect(findUsageCol().text()).toContain('3.2'); }); - it('displays the corrrect badge', () => { + it('displays the correct badge', () => { const badge = findUsageCol().find(GlBadge); expect(badge.exists()).toBe(true); diff --git a/spec/frontend/admin/deploy_keys/components/table_spec.js b/spec/frontend/admin/deploy_keys/components/table_spec.js index 3b3be488043..49bda7100fb 100644 --- a/spec/frontend/admin/deploy_keys/components/table_spec.js +++ b/spec/frontend/admin/deploy_keys/components/table_spec.js @@ -1,8 +1,19 @@ import { merge } from 'lodash'; -import { GlTable, GlButton } from '@gitlab/ui'; +import { GlLoadingIcon, GlEmptyState, GlPagination, GlModal } from '@gitlab/ui'; +import { nextTick } from 'vue'; +import responseBody from 'test_fixtures/api/deploy_keys/index.json'; import { mountExtended } from 'helpers/vue_test_utils_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { stubComponent } from 'helpers/stub_component'; import DeployKeysTable from '~/admin/deploy_keys/components/table.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import Api, { DEFAULT_PER_PAGE } from '~/api'; +import createFlash from '~/flash'; + +jest.mock('~/api'); +jest.mock('~/flash'); +jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' })); describe('DeployKeysTable', () => { let wrapper; @@ -14,9 +25,60 @@ describe('DeployKeysTable', () => { emptyStateSvgPath: '/assets/illustrations/empty-state/empty-deploy-keys.svg', }; + const deployKey = responseBody[0]; + const deployKey2 = responseBody[1]; + const createComponent = (provide = {}) => { wrapper = mountExtended(DeployKeysTable, { provide: merge({}, defaultProvide, provide), + stubs: { + GlModal: stubComponent(GlModal, { + template: ` +
+ + + +
`, + }), + }, + }); + }; + + const findEditButton = (index) => + wrapper.findAllByLabelText(DeployKeysTable.i18n.edit, { selector: 'a' }).at(index); + const findRemoveButton = (index) => + wrapper.findAllByLabelText(DeployKeysTable.i18n.delete, { selector: 'button' }).at(index); + const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); + const findTimeAgoTooltip = (index) => wrapper.findAllComponents(TimeAgoTooltip).at(index); + const findPagination = () => wrapper.findComponent(GlPagination); + + const expectDeployKeyIsRendered = (expectedDeployKey, expectedRowIndex) => { + const editButton = findEditButton(expectedRowIndex); + const timeAgoTooltip = findTimeAgoTooltip(expectedRowIndex); + + expect(wrapper.findByText(expectedDeployKey.title).exists()).toBe(true); + expect(wrapper.findByText(expectedDeployKey.fingerprint, { selector: 'code' }).exists()).toBe( + true, + ); + expect(timeAgoTooltip.exists()).toBe(true); + expect(timeAgoTooltip.props('time')).toBe(expectedDeployKey.created_at); + expect(editButton.exists()).toBe(true); + expect(editButton.attributes('href')).toBe(`/admin/deploy_keys/${expectedDeployKey.id}/edit`); + expect(findRemoveButton(expectedRowIndex).exists()).toBe(true); + }; + + const itRendersTheEmptyState = () => { + it('renders empty state', () => { + const emptyState = wrapper.findComponent(GlEmptyState); + + expect(emptyState.exists()).toBe(true); + expect(emptyState.props()).toMatchObject({ + svgPath: defaultProvide.emptyStateSvgPath, + title: DeployKeysTable.i18n.emptyStateTitle, + description: DeployKeysTable.i18n.emptyStateDescription, + primaryButtonText: DeployKeysTable.i18n.newDeployKeyButtonText, + primaryButtonLink: defaultProvide.createPath, + }); }); }; @@ -30,18 +92,149 @@ describe('DeployKeysTable', () => { expect(wrapper.findByText(DeployKeysTable.i18n.pageTitle).exists()).toBe(true); }); - it('renders table', () => { + it('renders `New deploy key` button', () => { createComponent(); - expect(wrapper.findComponent(GlTable).exists()).toBe(true); + const newDeployKeyButton = wrapper.findByTestId('new-deploy-key-button'); + + expect(newDeployKeyButton.exists()).toBe(true); + expect(newDeployKeyButton.attributes('href')).toBe(defaultProvide.createPath); + }); + + describe('when `/deploy_keys` API request is pending', () => { + beforeEach(() => { + Api.deployKeys.mockImplementation(() => new Promise(() => {})); + }); + + it('shows loading icon', async () => { + createComponent(); + + await nextTick(); + + expect(findLoadingIcon().exists()).toBe(true); + }); }); - it('renders `New deploy key` button', () => { - createComponent(); + describe('when `/deploy_keys` API request is successful', () => { + describe('when there are deploy keys', () => { + beforeEach(() => { + Api.deployKeys.mockResolvedValue({ + data: responseBody, + headers: { 'x-total': `${responseBody.length}` }, + }); - const newDeployKeyButton = wrapper.findComponent(GlButton); + createComponent(); + }); - expect(newDeployKeyButton.text()).toBe(DeployKeysTable.i18n.newDeployKeyButtonText); - expect(newDeployKeyButton.attributes('href')).toBe(defaultProvide.createPath); + it('renders deploy keys in table', () => { + expectDeployKeyIsRendered(deployKey, 0); + expectDeployKeyIsRendered(deployKey2, 1); + }); + + describe('when delete button is clicked', () => { + it('asks user to confirm', async () => { + await findRemoveButton(0).trigger('click'); + + const modal = wrapper.findComponent(GlModal); + const form = modal.find('form'); + const submitSpy = jest.spyOn(form.element, 'submit'); + + expect(modal.props('visible')).toBe(true); + expect(form.attributes('action')).toBe(`/admin/deploy_keys/${deployKey.id}`); + expect(form.find('input[name="_method"]').attributes('value')).toBe('delete'); + expect(form.find('input[name="authenticity_token"]').attributes('value')).toBe( + 'mock-csrf-token', + ); + + modal.vm.$emit('primary'); + + expect(submitSpy).toHaveBeenCalled(); + }); + }); + }); + + describe('pagination', () => { + beforeEach(() => { + Api.deployKeys.mockResolvedValueOnce({ + data: [deployKey], + headers: { 'x-total': '2' }, + }); + + createComponent(); + }); + + it('renders pagination', () => { + const pagination = findPagination(); + expect(pagination.exists()).toBe(true); + expect(pagination.props()).toMatchObject({ + value: 1, + perPage: DEFAULT_PER_PAGE, + totalItems: responseBody.length, + nextText: DeployKeysTable.i18n.pagination.next, + prevText: DeployKeysTable.i18n.pagination.prev, + align: 'center', + }); + }); + + describe('when pagination is changed', () => { + it('calls API with `page` parameter', async () => { + const pagination = findPagination(); + expectDeployKeyIsRendered(deployKey, 0); + + Api.deployKeys.mockResolvedValue({ + data: [deployKey2], + headers: { 'x-total': '2' }, + }); + + pagination.vm.$emit('input', 2); + + await nextTick(); + + expect(findLoadingIcon().exists()).toBe(true); + expect(pagination.exists()).toBe(false); + + await waitForPromises(); + + expect(Api.deployKeys).toHaveBeenCalledWith({ + page: 2, + public: true, + }); + expectDeployKeyIsRendered(deployKey2, 0); + }); + }); + }); + + describe('when there are no deploy keys', () => { + beforeEach(() => { + Api.deployKeys.mockResolvedValue({ + data: [], + headers: { 'x-total': '0' }, + }); + + createComponent(); + }); + + itRendersTheEmptyState(); + }); + }); + + describe('when `deploy_keys` API request is unsuccessful', () => { + const error = new Error('Network Error'); + + beforeEach(() => { + Api.deployKeys.mockRejectedValue(error); + + createComponent(); + }); + + itRendersTheEmptyState(); + + it('displays flash', () => { + expect(createFlash).toHaveBeenCalledWith({ + message: DeployKeysTable.i18n.apiErrorMessage, + captureError: true, + error, + }); + }); }); }); diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js index 9c424491d04..3cfb6feeb86 100644 --- a/spec/frontend/admin/statistics_panel/components/app_spec.js +++ b/spec/frontend/admin/statistics_panel/components/app_spec.js @@ -1,6 +1,7 @@ import { GlLoadingIcon } from '@gitlab/ui'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import AxiosMockAdapter from 'axios-mock-adapter'; +import Vue from 'vue'; import Vuex from 'vuex'; import StatisticsPanelApp from '~/admin/statistics_panel/components/app.vue'; import statisticsLabels from '~/admin/statistics_panel/constants'; @@ -9,8 +10,7 @@ import axios from '~/lib/utils/axios_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import mockStatistics from '../mock_data'; -const localVue = createLocalVue(); -localVue.use(Vuex); +Vue.use(Vuex); describe('Admin statistics app', () => { let wrapper; @@ -19,7 +19,6 @@ describe('Admin statistics app', () => { const createComponent = () => { wrapper = shallowMount(StatisticsPanelApp, { - localVue, store, }); }; diff --git a/spec/frontend/admin/users/components/actions/actions_spec.js b/spec/frontend/admin/users/components/actions/actions_spec.js index 67dcf5c6149..fa485e73999 100644 --- a/spec/frontend/admin/users/components/actions/actions_spec.js +++ b/spec/frontend/admin/users/components/actions/actions_spec.js @@ -1,7 +1,7 @@ import { GlDropdownItem } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { kebabCase } from 'lodash'; import { nextTick } from 'vue'; +import { kebabCase } from 'lodash'; import Actions from '~/admin/users/components/actions'; import SharedDeleteAction from '~/admin/users/components/actions/shared/shared_delete_action.vue'; import { capitalizeFirstCharacter } from '~/lib/utils/text_utility'; @@ -39,9 +39,6 @@ describe('Action components', () => { }); await nextTick(); - - expect(wrapper.attributes('data-path')).toBe('/test'); - expect(wrapper.attributes('data-modal-attributes')).toContain('John Doe'); expect(findDropdownItem().exists()).toBe(true); }); }); @@ -66,7 +63,6 @@ describe('Action components', () => { }); await nextTick(); - const sharedAction = wrapper.find(SharedDeleteAction); expect(sharedAction.attributes('data-block-user-url')).toBe(paths.block); @@ -76,6 +72,7 @@ describe('Action components', () => { expect(sharedAction.attributes('data-user-deletion-obstacles')).toBe( JSON.stringify(userDeletionObstacles), ); + expect(findDropdownItem().exists()).toBe(true); }, ); diff --git a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap index 472158a9b10..7a17ef2cc6c 100644 --- a/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap +++ b/spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap @@ -78,3 +78,83 @@ exports[`User Operation confirmation modal renders modal with form included 1`] `; + +exports[`User Operation confirmation modal when user's name has leading and trailing whitespace displays user's name without whitespace 1`] = ` +
+

+ content +

+ + + +

+ To confirm, type + + John Smith + +

+ +
+ + + + + + + + Cancel + + + + + secondaryAction + + + + + action + +
+`; diff --git a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js index 82307c9e3b3..025ae825e0d 100644 --- a/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js +++ b/spec/frontend/admin/users/components/modals/delete_user_modal_spec.js @@ -1,4 +1,4 @@ -import { GlButton, GlFormInput } from '@gitlab/ui'; +import { GlButton, GlFormInput, GlSprintf } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import DeleteUserModal from '~/admin/users/components/modals/delete_user_modal.vue'; import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue'; @@ -35,7 +35,7 @@ describe('User Operation confirmation modal', () => { const badUsername = 'bad_username'; const userDeletionObstacles = '["schedule1", "policy1"]'; - const createComponent = (props = {}) => { + const createComponent = (props = {}, stubs = {}) => { wrapper = shallowMount(DeleteUserModal, { propsData: { username, @@ -51,6 +51,7 @@ describe('User Operation confirmation modal', () => { }, stubs: { GlModal: ModalStub, + ...stubs, }, }); }; @@ -150,6 +151,30 @@ describe('User Operation confirmation modal', () => { }); }); + describe("when user's name has leading and trailing whitespace", () => { + beforeEach(() => { + createComponent( + { + username: ' John Smith ', + }, + { GlSprintf }, + ); + }); + + it("displays user's name without whitespace", () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it("shows enabled buttons when user's name is entered without whitespace", async () => { + setUsername('John Smith'); + + await wrapper.vm.$nextTick(); + + expect(findPrimaryButton().attributes('disabled')).toBeUndefined(); + expect(findSecondaryButton().attributes('disabled')).toBeUndefined(); + }); + }); + describe('Related user-deletion-obstacles list', () => { it('does NOT render the list when user has no related obstacles', () => { createComponent({ userDeletionObstacles: '[]' }); diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js index 708c9e1979e..9ff5961c7ec 100644 --- a/spec/frontend/admin/users/components/users_table_spec.js +++ b/spec/frontend/admin/users/components/users_table_spec.js @@ -1,5 +1,5 @@ import { GlTable, GlSkeletonLoader } from '@gitlab/ui'; -import { createLocalVue } from '@vue/test-utils'; +import Vue from 'vue'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -16,8 +16,7 @@ import { users, paths, createGroupCountResponse } from '../mock_data'; jest.mock('~/flash'); -const localVue = createLocalVue(); -localVue.use(VueApollo); +Vue.use(VueApollo); describe('AdminUsersTable component', () => { let wrapper; @@ -48,7 +47,6 @@ describe('AdminUsersTable component', () => { const initComponent = (props = {}, resolverMock = fetchGroupCountsResponse) => { wrapper = mountExtended(AdminUsersTable, { - localVue, apolloProvider: createMockApolloProvider(resolverMock), propsData: { users, -- cgit v1.2.3