Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-12-20 16:37:47 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2021-12-20 16:37:47 +0300
commitaee0a117a889461ce8ced6fcf73207fe017f1d99 (patch)
tree891d9ef189227a8445d83f35c1b0fc99573f4380 /spec/frontend/admin
parent8d46af3258650d305f53b819eabf7ab18d22f59e (diff)
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'spec/frontend/admin')
-rw-r--r--spec/frontend/admin/analytics/devops_score/components/devops_score_spec.js10
-rw-r--r--spec/frontend/admin/deploy_keys/components/table_spec.js209
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js7
-rw-r--r--spec/frontend/admin/users/components/actions/actions_spec.js7
-rw-r--r--spec/frontend/admin/users/components/modals/__snapshots__/delete_user_modal_spec.js.snap80
-rw-r--r--spec/frontend/admin/users/components/modals/delete_user_modal_spec.js29
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js6
7 files changed, 320 insertions, 28 deletions
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: `
+ <div>
+ <slot name="modal-title"></slot>
+ <slot></slot>
+ <slot name="modal-footer"></slot>
+ </div>`,
+ }),
+ },
+ });
+ };
+
+ 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`]
</gl-button-stub>
</div>
`;
+
+exports[`User Operation confirmation modal when user's name has leading and trailing whitespace displays user's name without whitespace 1`] = `
+<div>
+ <p>
+ content
+ </p>
+
+ <user-deletion-obstacles-list-stub
+ obstacles="schedule1,policy1"
+ username="John Smith"
+ />
+
+ <p>
+ To confirm, type
+ <code
+ class="gl-white-space-pre-wrap"
+ >
+ John Smith
+ </code>
+ </p>
+
+ <form
+ action="delete-url"
+ method="post"
+ >
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ value="csrf"
+ />
+
+ <gl-form-input-stub
+ autocomplete="off"
+ autofocus=""
+ name="username"
+ type="text"
+ value=""
+ />
+ </form>
+ <gl-button-stub
+ buttontextclasses=""
+ category="primary"
+ icon=""
+ size="medium"
+ variant="default"
+ >
+ Cancel
+ </gl-button-stub>
+
+ <gl-button-stub
+ buttontextclasses=""
+ category="secondary"
+ disabled="true"
+ icon=""
+ size="medium"
+ variant="danger"
+ >
+
+ secondaryAction
+
+ </gl-button-stub>
+
+ <gl-button-stub
+ buttontextclasses=""
+ category="primary"
+ disabled="true"
+ icon=""
+ size="medium"
+ variant="danger"
+ >
+ action
+ </gl-button-stub>
+</div>
+`;
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,