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:
Diffstat (limited to 'spec/frontend/organizations')
-rw-r--r--spec/frontend/organizations/index/components/app_spec.js202
-rw-r--r--spec/frontend/organizations/index/components/organizations_list_spec.js68
-rw-r--r--spec/frontend/organizations/index/components/organizations_view_spec.js28
-rw-r--r--spec/frontend/organizations/settings/general/components/advanced_settings_spec.js25
-rw-r--r--spec/frontend/organizations/settings/general/components/app_spec.js7
-rw-r--r--spec/frontend/organizations/settings/general/components/change_url_spec.js191
-rw-r--r--spec/frontend/organizations/settings/general/components/organization_settings_spec.js108
-rw-r--r--spec/frontend/organizations/shared/components/new_edit_form_spec.js28
-rw-r--r--spec/frontend/organizations/shared/components/organization_url_field_spec.js66
-rw-r--r--spec/frontend/organizations/users/components/app_spec.js84
-rw-r--r--spec/frontend/organizations/users/components/users_view_spec.js68
-rw-r--r--spec/frontend/organizations/users/mock_data.js34
12 files changed, 798 insertions, 111 deletions
diff --git a/spec/frontend/organizations/index/components/app_spec.js b/spec/frontend/organizations/index/components/app_spec.js
index 175b1e1c552..670eb34bffd 100644
--- a/spec/frontend/organizations/index/components/app_spec.js
+++ b/spec/frontend/organizations/index/components/app_spec.js
@@ -5,9 +5,9 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import { organizations } from '~/organizations/mock_data';
-import resolvers from '~/organizations/shared/graphql/resolvers';
-import organizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import { DEFAULT_PER_PAGE } from '~/api';
+import { organizations as nodes, pageInfo, pageInfoEmpty } from '~/organizations/mock_data';
+import organizationsQuery from '~/organizations/shared/graphql/queries/organizations.query.graphql';
import OrganizationsIndexApp from '~/organizations/index/components/app.vue';
import OrganizationsView from '~/organizations/index/components/organizations_view.vue';
import { MOCK_NEW_ORG_URL } from '../mock_data';
@@ -20,8 +20,27 @@ describe('OrganizationsIndexApp', () => {
let wrapper;
let mockApollo;
- const createComponent = (mockResolvers = resolvers) => {
- mockApollo = createMockApollo([[organizationsQuery, mockResolvers]]);
+ const organizations = {
+ nodes,
+ pageInfo,
+ };
+
+ const organizationEmpty = {
+ nodes: [],
+ pageInfo: pageInfoEmpty,
+ };
+
+ const successHandler = jest.fn().mockResolvedValue({
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ organizations,
+ },
+ },
+ });
+
+ const createComponent = (handler = successHandler) => {
+ mockApollo = createMockApollo([[organizationsQuery, handler]]);
wrapper = shallowMountExtended(OrganizationsIndexApp, {
apolloProvider: mockApollo,
@@ -35,53 +54,168 @@ describe('OrganizationsIndexApp', () => {
mockApollo = null;
});
+ // Finders
const findOrganizationHeaderText = () => wrapper.findByText('Organizations');
const findNewOrganizationButton = () => wrapper.findComponent(GlButton);
const findOrganizationsView = () => wrapper.findComponent(OrganizationsView);
- const loadingResolver = jest.fn().mockReturnValue(new Promise(() => {}));
- const successfulResolver = (nodes) =>
- jest.fn().mockResolvedValue({
- data: { currentUser: { id: 1, organizations: { nodes } } },
+ // Assertions
+ const itRendersHeaderText = () => {
+ it('renders the header text', () => {
+ expect(findOrganizationHeaderText().exists()).toBe(true);
+ });
+ };
+
+ const itRendersNewOrganizationButton = () => {
+ it('render new organization button with correct link', () => {
+ expect(findNewOrganizationButton().attributes('href')).toBe(MOCK_NEW_ORG_URL);
+ });
+ };
+
+ const itDoesNotRenderErrorMessage = () => {
+ it('does not render an error message', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ };
+
+ const itDoesNotRenderHeaderText = () => {
+ it('does not render the header text', () => {
+ expect(findOrganizationHeaderText().exists()).toBe(false);
+ });
+ };
+
+ const itDoesNotRenderNewOrganizationButton = () => {
+ it('does not render new organization button', () => {
+ expect(findNewOrganizationButton().exists()).toBe(false);
+ });
+ };
+
+ describe('when API call is loading', () => {
+ beforeEach(() => {
+ createComponent(jest.fn().mockReturnValue(new Promise(() => {})));
+ });
+
+ itRendersHeaderText();
+ itRendersNewOrganizationButton();
+ itDoesNotRenderErrorMessage();
+
+ it('renders the organizations view with loading prop set to true', () => {
+ expect(findOrganizationsView().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API call is successful', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ itRendersHeaderText();
+ itRendersNewOrganizationButton();
+ itDoesNotRenderErrorMessage();
+
+ it('passes organizations to view component', () => {
+ expect(findOrganizationsView().props()).toMatchObject({
+ loading: false,
+ organizations,
+ });
});
- const errorResolver = jest.fn().mockRejectedValue('error');
+ });
- describe.each`
- description | mockResolver | headerText | newOrgLink | loading | orgsData | error
- ${'when API call is loading'} | ${loadingResolver} | ${true} | ${MOCK_NEW_ORG_URL} | ${true} | ${[]} | ${false}
- ${'when API returns successful with results'} | ${successfulResolver(organizations)} | ${true} | ${MOCK_NEW_ORG_URL} | ${false} | ${organizations} | ${false}
- ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${false} | ${false} | ${[]} | ${false}
- ${'when API returns error'} | ${errorResolver} | ${false} | ${false} | ${false} | ${[]} | ${true}
- `('$description', ({ mockResolver, headerText, newOrgLink, loading, orgsData, error }) => {
+ describe('when API call is successful and returns no organizations', () => {
beforeEach(async () => {
- createComponent(mockResolver);
+ createComponent(
+ jest.fn().mockResolvedValue({
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ organizations: organizationEmpty,
+ },
+ },
+ }),
+ );
await waitForPromises();
});
- it(`does ${headerText ? '' : 'not '}render the header text`, () => {
- expect(findOrganizationHeaderText().exists()).toBe(headerText);
+ itDoesNotRenderHeaderText();
+ itDoesNotRenderNewOrganizationButton();
+ itDoesNotRenderErrorMessage();
+
+ it('renders view component with correct organizations and loading props', () => {
+ expect(findOrganizationsView().props()).toMatchObject({
+ loading: false,
+ organizations: organizationEmpty,
+ });
});
+ });
+
+ describe('when API call is not successful', () => {
+ const error = new Error();
- it(`does ${newOrgLink ? '' : 'not '}render new organization button with correct link`, () => {
- expect(
- findNewOrganizationButton().exists() && findNewOrganizationButton().attributes('href'),
- ).toBe(newOrgLink);
+ beforeEach(async () => {
+ createComponent(jest.fn().mockRejectedValue(error));
+ await waitForPromises();
});
- it(`renders the organizations view with ${loading} loading prop`, () => {
- expect(findOrganizationsView().props('loading')).toBe(loading);
+ itDoesNotRenderHeaderText();
+ itDoesNotRenderNewOrganizationButton();
+
+ it('renders view component with correct organizations and loading props', () => {
+ expect(findOrganizationsView().props()).toMatchObject({
+ loading: false,
+ organizations: {},
+ });
});
- it(`renders the organizations view with ${
- orgsData ? 'correct' : 'empty'
- } organizations array prop`, () => {
- expect(findOrganizationsView().props('organizations')).toStrictEqual(orgsData);
+ it('renders error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message:
+ 'An error occurred loading user organizations. Please refresh the page to try again.',
+ error,
+ captureError: true,
+ });
});
+ });
+
+ describe('when view component emits `next` event', () => {
+ const endCursor = 'mockEndCursor';
+
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('calls GraphQL query with correct pageInfo variables', async () => {
+ findOrganizationsView().vm.$emit('next', endCursor);
+ await waitForPromises();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ first: DEFAULT_PER_PAGE,
+ after: endCursor,
+ last: null,
+ before: null,
+ });
+ });
+ });
+
+ describe('when view component emits `prev` event', () => {
+ const startCursor = 'mockStartCursor';
+
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('calls GraphQL query with correct pageInfo variables', async () => {
+ findOrganizationsView().vm.$emit('prev', startCursor);
+ await waitForPromises();
- it(`does ${error ? '' : 'not '}render an error message`, () => {
- return error
- ? expect(createAlert).toHaveBeenCalled()
- : expect(createAlert).not.toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledWith({
+ first: null,
+ after: null,
+ last: DEFAULT_PER_PAGE,
+ before: startCursor,
+ });
});
});
});
diff --git a/spec/frontend/organizations/index/components/organizations_list_spec.js b/spec/frontend/organizations/index/components/organizations_list_spec.js
index 0b59c212314..7d904ee802f 100644
--- a/spec/frontend/organizations/index/components/organizations_list_spec.js
+++ b/spec/frontend/organizations/index/components/organizations_list_spec.js
@@ -1,28 +1,84 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { omit } from 'lodash';
import { shallowMount } from '@vue/test-utils';
import OrganizationsList from '~/organizations/index/components/organizations_list.vue';
import OrganizationsListItem from '~/organizations/index/components/organizations_list_item.vue';
-import { organizations } from '~/organizations/mock_data';
+import { organizations as nodes, pageInfo, pageInfoOnePage } from '~/organizations/mock_data';
describe('OrganizationsList', () => {
let wrapper;
- const createComponent = () => {
+ const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMount(OrganizationsList, {
propsData: {
- organizations,
+ organizations: {
+ nodes,
+ pageInfo,
+ },
+ ...propsData,
},
});
};
const findAllOrganizationsListItem = () => wrapper.findAllComponents(OrganizationsListItem);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
describe('template', () => {
- beforeEach(() => {
+ it('renders a list item for each organization', () => {
createComponent();
+
+ expect(findAllOrganizationsListItem()).toHaveLength(nodes.length);
});
- it('renders a list item for each organization', () => {
- expect(findAllOrganizationsListItem()).toHaveLength(organizations.length);
+ describe('when there is one page of organizations', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ organizations: {
+ nodes,
+ pageInfo: pageInfoOnePage,
+ },
+ },
+ });
+ });
+
+ it('does not render pagination', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('when there are multiple pages of organizations', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders pagination', () => {
+ expect(findPagination().props()).toMatchObject(omit(pageInfo, '__typename'));
+ });
+
+ describe('when `GlKeysetPagination` emits `next` event', () => {
+ const endCursor = 'mockEndCursor';
+
+ beforeEach(() => {
+ findPagination().vm.$emit('next', endCursor);
+ });
+
+ it('emits `next` event', () => {
+ expect(wrapper.emitted('next')).toEqual([[endCursor]]);
+ });
+ });
+
+ describe('when `GlKeysetPagination` emits `prev` event', () => {
+ const startCursor = 'startEndCursor';
+
+ beforeEach(() => {
+ findPagination().vm.$emit('prev', startCursor);
+ });
+
+ it('emits `prev` event', () => {
+ expect(wrapper.emitted('prev')).toEqual([[startCursor]]);
+ });
+ });
});
});
});
diff --git a/spec/frontend/organizations/index/components/organizations_view_spec.js b/spec/frontend/organizations/index/components/organizations_view_spec.js
index 85a1c11a2b1..fe167a1418f 100644
--- a/spec/frontend/organizations/index/components/organizations_view_spec.js
+++ b/spec/frontend/organizations/index/components/organizations_view_spec.js
@@ -31,7 +31,7 @@ describe('OrganizationsView', () => {
${'when not loading and has no organizations'} | ${false} | ${[]} | ${MOCK_ORG_EMPTY_STATE_SVG} | ${MOCK_NEW_ORG_URL}
`('$description', ({ loading, orgsData, emptyStateSvg, emptyStateUrl }) => {
beforeEach(() => {
- createComponent({ loading, organizations: orgsData });
+ createComponent({ loading, organizations: { nodes: orgsData, pageInfo: {} } });
});
it(`does ${loading ? '' : 'not '}render loading icon`, () => {
@@ -54,4 +54,30 @@ describe('OrganizationsView', () => {
).toBe(emptyStateUrl);
});
});
+
+ describe('when `OrganizationsList` emits `next` event', () => {
+ const endCursor = 'mockEndCursor';
+
+ beforeEach(() => {
+ createComponent({ loading: false, organizations: { nodes: organizations, pageInfo: {} } });
+ findOrganizationsList().vm.$emit('next', endCursor);
+ });
+
+ it('emits `next` event', () => {
+ expect(wrapper.emitted('next')).toEqual([[endCursor]]);
+ });
+ });
+
+ describe('when `OrganizationsList` emits `prev` event', () => {
+ const startCursor = 'mockStartCursor';
+
+ beforeEach(() => {
+ createComponent({ loading: false, organizations: { nodes: organizations, pageInfo: {} } });
+ findOrganizationsList().vm.$emit('prev', startCursor);
+ });
+
+ it('emits `next` event', () => {
+ expect(wrapper.emitted('prev')).toEqual([[startCursor]]);
+ });
+ });
});
diff --git a/spec/frontend/organizations/settings/general/components/advanced_settings_spec.js b/spec/frontend/organizations/settings/general/components/advanced_settings_spec.js
new file mode 100644
index 00000000000..34793200b0d
--- /dev/null
+++ b/spec/frontend/organizations/settings/general/components/advanced_settings_spec.js
@@ -0,0 +1,25 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AdvancedSettings from '~/organizations/settings/general/components/advanced_settings.vue';
+import ChangeUrl from '~/organizations/settings/general/components/change_url.vue';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+
+describe('AdvancedSettings', () => {
+ let wrapper;
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AdvancedSettings);
+ };
+
+ const findSettingsBlock = () => wrapper.findComponent(SettingsBlock);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders settings block', () => {
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('renders `ChangeUrl` component', () => {
+ expect(findSettingsBlock().findComponent(ChangeUrl).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/organizations/settings/general/components/app_spec.js b/spec/frontend/organizations/settings/general/components/app_spec.js
index 6d75f8a9949..e954b927715 100644
--- a/spec/frontend/organizations/settings/general/components/app_spec.js
+++ b/spec/frontend/organizations/settings/general/components/app_spec.js
@@ -1,8 +1,9 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationSettings from '~/organizations/settings/general/components/organization_settings.vue';
+import AdvancedSettings from '~/organizations/settings/general/components/advanced_settings.vue';
import App from '~/organizations/settings/general/components/app.vue';
-describe('OrganizationSettings', () => {
+describe('OrganizationSettingsGeneralApp', () => {
let wrapper;
const createComponent = () => {
@@ -16,4 +17,8 @@ describe('OrganizationSettings', () => {
it('renders `Organization settings` section', () => {
expect(wrapper.findComponent(OrganizationSettings).exists()).toBe(true);
});
+
+ it('renders `Advanced` section', () => {
+ expect(wrapper.findComponent(AdvancedSettings).exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/organizations/settings/general/components/change_url_spec.js b/spec/frontend/organizations/settings/general/components/change_url_spec.js
new file mode 100644
index 00000000000..a4e3db0557c
--- /dev/null
+++ b/spec/frontend/organizations/settings/general/components/change_url_spec.js
@@ -0,0 +1,191 @@
+import { GlButton, GlForm } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ChangeUrl from '~/organizations/settings/general/components/change_url.vue';
+import organizationUpdateMutation from '~/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql';
+import {
+ organizationUpdateResponse,
+ organizationUpdateResponseWithErrors,
+} from '~/organizations/mock_data';
+import { createAlert } from '~/alert';
+import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrlWithAlerts: jest.fn(),
+}));
+
+Vue.use(VueApollo);
+
+describe('ChangeUrl', () => {
+ let wrapper;
+ let mockApollo;
+
+ const defaultProvide = {
+ organization: {
+ id: 1,
+ name: 'GitLab',
+ path: 'foo-bar',
+ },
+ organizationsPath: '/-/organizations',
+ rootUrl: 'http://127.0.0.1:3000/',
+ };
+
+ const successfulResponseHandler = jest.fn().mockResolvedValue(organizationUpdateResponse);
+
+ const createComponent = ({
+ handlers = [[organizationUpdateMutation, successfulResponseHandler]],
+ } = {}) => {
+ mockApollo = createMockApollo(handlers);
+
+ wrapper = mountExtended(ChangeUrl, {
+ attachTo: document.body,
+ provide: defaultProvide,
+ apolloProvider: mockApollo,
+ });
+ };
+
+ const findSubmitButton = () => wrapper.findComponent(GlButton);
+ const findOrganizationUrlField = () => wrapper.findByLabelText('Organization URL');
+ const submitForm = async () => {
+ await wrapper.findComponent(GlForm).trigger('submit');
+ await nextTick();
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ it('renders `Organization URL` field', () => {
+ createComponent();
+
+ expect(findOrganizationUrlField().exists()).toBe(true);
+ });
+
+ it('disables submit button until `Organization URL` field is changed', async () => {
+ createComponent();
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+
+ await findOrganizationUrlField().setValue('foo-bar-baz');
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+
+ describe('when form is submitted', () => {
+ it('requires `Organization URL` field', async () => {
+ createComponent();
+
+ await findOrganizationUrlField().setValue('');
+ await submitForm();
+
+ expect(wrapper.findByText('Organization URL is required.').exists()).toBe(true);
+ });
+
+ it('requires `Organization URL` field to be a minimum of two characters', async () => {
+ createComponent();
+
+ await findOrganizationUrlField().setValue('f');
+ await submitForm();
+
+ expect(
+ wrapper.findByText('Organization URL is too short (minimum is 2 characters).').exists(),
+ ).toBe(true);
+ });
+
+ describe('when API is loading', () => {
+ beforeEach(async () => {
+ createComponent({
+ handlers: [
+ [organizationUpdateMutation, jest.fn().mockReturnValueOnce(new Promise(() => {}))],
+ ],
+ });
+
+ await findOrganizationUrlField().setValue('foo-bar-baz');
+ await submitForm();
+ });
+
+ it('shows submit button as loading', () => {
+ expect(findSubmitButton().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(async () => {
+ createComponent();
+ await findOrganizationUrlField().setValue('foo-bar-baz');
+ await submitForm();
+ await waitForPromises();
+ });
+
+ it('calls mutation with correct variables and redirects user to new organization settings page with success alert', () => {
+ expect(successfulResponseHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/Organizations::Organization/1',
+ path: 'foo-bar-baz',
+ },
+ });
+ expect(visitUrlWithAlerts).toHaveBeenCalledWith(
+ `${organizationUpdateResponse.data.organizationUpdate.organization.webUrl}/settings/general`,
+ [
+ {
+ id: 'organization-url-successfully-changed',
+ message: 'Organization URL successfully changed.',
+ variant: 'info',
+ },
+ ],
+ );
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ describe('when there is a network error', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ createComponent({
+ handlers: [[organizationUpdateMutation, jest.fn().mockRejectedValue(error)]],
+ });
+ await findOrganizationUrlField().setValue('foo-bar-baz');
+ await submitForm();
+ await waitForPromises();
+ });
+
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred changing your organization URL. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
+ });
+
+ describe('when there are GraphQL errors', () => {
+ beforeEach(async () => {
+ createComponent({
+ handlers: [
+ [
+ organizationUpdateMutation,
+ jest.fn().mockResolvedValue(organizationUpdateResponseWithErrors),
+ ],
+ ],
+ });
+ await submitForm();
+ await waitForPromises();
+ });
+
+ it('displays form errors alert', () => {
+ expect(wrapper.findComponent(FormErrorsAlert).props('errors')).toEqual(
+ organizationUpdateResponseWithErrors.data.organizationUpdate.errors,
+ );
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/settings/general/components/organization_settings_spec.js b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
index 7645b41e3bd..d1c637331a8 100644
--- a/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
+++ b/spec/frontend/organizations/settings/general/components/organization_settings_spec.js
@@ -6,14 +6,26 @@ import OrganizationSettings from '~/organizations/settings/general/components/or
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
-import resolvers from '~/organizations/shared/graphql/resolvers';
-import { createAlert, VARIANT_INFO } from '~/alert';
+import organizationUpdateMutation from '~/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql';
+import {
+ organizationUpdateResponse,
+ organizationUpdateResponseWithErrors,
+} from '~/organizations/mock_data';
+import { createAlert } from '~/alert';
+import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
Vue.use(VueApollo);
-jest.useFakeTimers();
jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrlWithAlerts: jest.fn(),
+}));
+
+useMockLocationHelper();
describe('OrganizationSettings', () => {
let wrapper;
@@ -26,8 +38,12 @@ describe('OrganizationSettings', () => {
},
};
- const createComponent = ({ mockResolvers = resolvers } = {}) => {
- mockApollo = createMockApollo([], mockResolvers);
+ const successfulResponseHandler = jest.fn().mockResolvedValue(organizationUpdateResponse);
+
+ const createComponent = ({
+ handlers = [[organizationUpdateMutation, successfulResponseHandler]],
+ } = {}) => {
+ mockApollo = createMockApollo(handlers);
wrapper = shallowMountExtended(OrganizationSettings, {
provide: defaultProvide,
@@ -66,13 +82,11 @@ describe('OrganizationSettings', () => {
describe('when form is submitted', () => {
describe('when API is loading', () => {
beforeEach(async () => {
- const mockResolvers = {
- Mutation: {
- updateOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
- },
- };
-
- createComponent({ mockResolvers });
+ createComponent({
+ handlers: [
+ [organizationUpdateMutation, jest.fn().mockReturnValueOnce(new Promise(() => {}))],
+ ],
+ });
await submitForm();
});
@@ -86,39 +100,65 @@ describe('OrganizationSettings', () => {
beforeEach(async () => {
createComponent();
await submitForm();
- jest.runAllTimers();
await waitForPromises();
});
- it('displays info alert', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'Organization was successfully updated.',
- variant: VARIANT_INFO,
+ it('calls mutation with correct variables and displays info alert', () => {
+ expect(successfulResponseHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/Organizations::Organization/1',
+ name: 'Foo bar',
+ },
});
+ expect(visitUrlWithAlerts).toHaveBeenCalledWith(window.location.href, [
+ {
+ id: 'organization-successfully-updated',
+ message: 'Organization was successfully updated.',
+ variant: 'info',
+ },
+ ]);
});
});
describe('when API request is not successful', () => {
- const error = new Error();
-
- beforeEach(async () => {
- const mockResolvers = {
- Mutation: {
- updateOrganization: jest.fn().mockRejectedValueOnce(error),
- },
- };
+ describe('when there is a network error', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ createComponent({
+ handlers: [[organizationUpdateMutation, jest.fn().mockRejectedValue(error)]],
+ });
+ await submitForm();
+ await waitForPromises();
+ });
- createComponent({ mockResolvers });
- await submitForm();
- jest.runAllTimers();
- await waitForPromises();
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred updating your organization. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
});
- it('displays error alert', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: 'An error occurred updating your organization. Please try again.',
- error,
- captureError: true,
+ describe('when there are GraphQL errors', () => {
+ beforeEach(async () => {
+ createComponent({
+ handlers: [
+ [
+ organizationUpdateMutation,
+ jest.fn().mockResolvedValue(organizationUpdateResponseWithErrors),
+ ],
+ ],
+ });
+ await submitForm();
+ await waitForPromises();
+ });
+
+ it('displays form errors alert', () => {
+ expect(wrapper.findComponent(FormErrorsAlert).props('errors')).toEqual(
+ organizationUpdateResponseWithErrors.data.organizationUpdate.errors,
+ );
});
});
});
diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
index 93f022a3259..1fcfc20bf1a 100644
--- a/spec/frontend/organizations/shared/components/new_edit_form_spec.js
+++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
@@ -1,6 +1,8 @@
-import { GlButton, GlInputGroupText, GlTruncate } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
+import { nextTick } from 'vue';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import OrganizationUrlField from '~/organizations/shared/components/organization_url_field.vue';
import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '~/organizations/shared/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -29,7 +31,12 @@ describe('NewEditForm', () => {
const findNameField = () => wrapper.findByLabelText('Organization name');
const findIdField = () => wrapper.findByLabelText('Organization ID');
- const findUrlField = () => wrapper.findByLabelText('Organization URL');
+ const findUrlField = () => wrapper.findComponent(OrganizationUrlField);
+
+ const setUrlFieldValue = async (value) => {
+ findUrlField().vm.$emit('input', value);
+ await nextTick();
+ };
const submitForm = async () => {
await wrapper.findByRole('button', { name: 'Create organization' }).trigger('click');
};
@@ -43,20 +50,17 @@ describe('NewEditForm', () => {
it('renders `Organization URL` field', () => {
createComponent();
- expect(wrapper.findComponent(GlInputGroupText).findComponent(GlTruncate).props('text')).toBe(
- 'http://127.0.0.1:3000/-/organizations/',
- );
expect(findUrlField().exists()).toBe(true);
});
it('requires `Organization URL` field to be a minimum of two characters', async () => {
createComponent();
- await findUrlField().setValue('f');
+ await setUrlFieldValue('f');
await submitForm();
expect(
- wrapper.findByText('Organization URL must be a minimum of two characters.').exists(),
+ wrapper.findByText('Organization URL is too short (minimum is 2 characters).').exists(),
).toBe(true);
});
@@ -89,7 +93,7 @@ describe('NewEditForm', () => {
it('sets initial values for fields', () => {
expect(findNameField().element.value).toBe('Foo bar');
expect(findIdField().element.value).toBe('1');
- expect(findUrlField().element.value).toBe('foo-bar');
+ expect(findUrlField().props('value')).toBe('foo-bar');
});
});
@@ -116,7 +120,7 @@ describe('NewEditForm', () => {
createComponent();
await findNameField().setValue('Foo bar');
- await findUrlField().setValue('foo-bar');
+ await setUrlFieldValue('foo-bar');
await submitForm();
});
@@ -134,7 +138,7 @@ describe('NewEditForm', () => {
});
it('sets `Organization URL` when typing in `Organization name`', () => {
- expect(findUrlField().element.value).toBe('foo-bar');
+ expect(findUrlField().props('value')).toBe('foo-bar');
});
});
@@ -142,13 +146,13 @@ describe('NewEditForm', () => {
beforeEach(async () => {
createComponent();
- await findUrlField().setValue('foo-bar-baz');
+ await setUrlFieldValue('foo-bar-baz');
await findNameField().setValue('Foo bar');
await submitForm();
});
it('does not modify `Organization URL` when typing in `Organization name`', () => {
- expect(findUrlField().element.value).toBe('foo-bar-baz');
+ expect(findUrlField().props('value')).toBe('foo-bar-baz');
});
});
diff --git a/spec/frontend/organizations/shared/components/organization_url_field_spec.js b/spec/frontend/organizations/shared/components/organization_url_field_spec.js
new file mode 100644
index 00000000000..d854134e596
--- /dev/null
+++ b/spec/frontend/organizations/shared/components/organization_url_field_spec.js
@@ -0,0 +1,66 @@
+import { GlFormInputGroup, GlInputGroupText, GlTruncate, GlFormInput } from '@gitlab/ui';
+
+import OrganizedUrlField from '~/organizations/shared/components/organization_url_field.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('OrganizationUrlField', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ organizationsPath: '/-/organizations',
+ rootUrl: 'http://127.0.0.1:3000/',
+ };
+
+ const defaultPropsData = {
+ id: 'organization-url',
+ value: 'foo-bar',
+ validation: {
+ invalidFeedback: 'Invalid',
+ state: false,
+ },
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(OrganizedUrlField, {
+ attachTo: document.body,
+ provide: defaultProvide,
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ const findInputGroup = () => wrapper.findComponent(GlFormInputGroup);
+ const findInput = () => findInputGroup().findComponent(GlFormInput);
+
+ it('renders organization url field with correct props', () => {
+ createComponent();
+
+ expect(
+ findInputGroup().findComponent(GlInputGroupText).findComponent(GlTruncate).props('text'),
+ ).toBe('http://127.0.0.1:3000/-/organizations/');
+ expect(findInput().attributes('id')).toBe(defaultPropsData.id);
+ expect(findInput().vm.$attrs).toMatchObject({
+ value: defaultPropsData.value,
+ invalidFeedback: defaultPropsData.validation.invalidFeedback,
+ state: defaultPropsData.validation.state,
+ });
+ });
+
+ it('emits `input` event', () => {
+ createComponent();
+
+ findInput().vm.$emit('input', 'foo');
+
+ expect(wrapper.emitted('input')).toEqual([['foo']]);
+ });
+
+ it('emits `blur` event', () => {
+ createComponent();
+
+ findInput().vm.$emit('blur', true);
+
+ expect(wrapper.emitted('blur')).toEqual([[true]]);
+ });
+});
diff --git a/spec/frontend/organizations/users/components/app_spec.js b/spec/frontend/organizations/users/components/app_spec.js
index b30fd984099..30380bcf6a5 100644
--- a/spec/frontend/organizations/users/components/app_spec.js
+++ b/spec/frontend/organizations/users/components/app_spec.js
@@ -4,9 +4,16 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
+import { ORGANIZATION_USERS_PER_PAGE } from '~/organizations/constants';
import organizationUsersQuery from '~/organizations/users/graphql/organization_users.query.graphql';
import OrganizationsUsersApp from '~/organizations/users/components/app.vue';
-import { MOCK_ORGANIZATION_GID, MOCK_USERS } from '../mock_data';
+import OrganizationsUsersView from '~/organizations/users/components/users_view.vue';
+import {
+ MOCK_ORGANIZATION_GID,
+ MOCK_USERS,
+ MOCK_USERS_FORMATTED,
+ MOCK_PAGE_INFO,
+} from '../mock_data';
jest.mock('~/alert');
@@ -15,10 +22,11 @@ Vue.use(VueApollo);
const mockError = new Error();
const loadingResolver = jest.fn().mockReturnValue(new Promise(() => {}));
-const successfulResolver = (nodes) =>
- jest.fn().mockResolvedValue({
- data: { organization: { id: 1, organizationUsers: { nodes } } },
+const successfulResolver = (nodes, pageInfo = {}) => {
+ return jest.fn().mockResolvedValue({
+ data: { organization: { id: 1, organizationUsers: { nodes, pageInfo } } },
});
+};
const errorResolver = jest.fn().mockRejectedValueOnce(mockError);
describe('OrganizationsUsersApp', () => {
@@ -40,31 +48,31 @@ describe('OrganizationsUsersApp', () => {
mockApollo = null;
});
- const findOrganizationUsersLoading = () => wrapper.findByText('Loading');
- const findOrganizationUsers = () => wrapper.findByTestId('organization-users');
+ const findOrganizationUsersView = () => wrapper.findComponent(OrganizationsUsersView);
describe.each`
- description | mockResolver | loading | userData | error
- ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${false}
- ${'when API returns successful with results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS} | ${false}
- ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${false}
- ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${true}
- `('$description', ({ mockResolver, loading, userData, error }) => {
+ description | mockResolver | loading | userData | pageInfo | error
+ ${'when API call is loading'} | ${loadingResolver} | ${true} | ${[]} | ${{}} | ${false}
+ ${'when API returns successful with one page of results'} | ${successfulResolver(MOCK_USERS)} | ${false} | ${MOCK_USERS_FORMATTED} | ${{}} | ${false}
+ ${'when API returns successful with multiple pages of results'} | ${successfulResolver(MOCK_USERS, MOCK_PAGE_INFO)} | ${false} | ${MOCK_USERS_FORMATTED} | ${MOCK_PAGE_INFO} | ${false}
+ ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${[]} | ${{}} | ${false}
+ ${'when API returns error'} | ${errorResolver} | ${false} | ${[]} | ${{}} | ${true}
+ `('$description', ({ mockResolver, loading, userData, pageInfo, error }) => {
beforeEach(async () => {
createComponent(mockResolver);
await waitForPromises();
});
- it(`does ${
- loading ? '' : 'not '
- }render the organization users view with loading placeholder`, () => {
- expect(findOrganizationUsersLoading().exists()).toBe(loading);
+ it(`renders OrganizationUsersView with loading prop set to ${loading}`, () => {
+ expect(findOrganizationUsersView().props('loading')).toBe(loading);
});
- it(`renders the organization users view with ${
- userData.length ? 'correct' : 'empty'
- } users array raw data`, () => {
- expect(JSON.parse(findOrganizationUsers().text())).toStrictEqual(userData);
+ it('renders OrganizationUsersView with correct users prop', () => {
+ expect(findOrganizationUsersView().props('users')).toStrictEqual(userData);
+ });
+
+ it('renders OrganizationUsersView with correct pageInfo prop', () => {
+ expect(findOrganizationUsersView().props('pageInfo')).toStrictEqual(pageInfo);
});
it(`does ${error ? '' : 'not '}render an error message`, () => {
@@ -78,4 +86,40 @@ describe('OrganizationsUsersApp', () => {
: expect(createAlert).not.toHaveBeenCalled();
});
});
+
+ describe('Pagination', () => {
+ const mockResolver = successfulResolver(MOCK_USERS, MOCK_PAGE_INFO);
+
+ beforeEach(async () => {
+ createComponent(mockResolver);
+ await waitForPromises();
+ mockResolver.mockClear();
+ });
+
+ it('handleNextPage calls organizationUsersQuery with correct pagination data', async () => {
+ findOrganizationUsersView().vm.$emit('next');
+ await waitForPromises();
+
+ expect(mockResolver).toHaveBeenCalledWith({
+ id: MOCK_ORGANIZATION_GID,
+ before: '',
+ after: MOCK_PAGE_INFO.endCursor,
+ first: ORGANIZATION_USERS_PER_PAGE,
+ last: null,
+ });
+ });
+
+ it('handlePrevPage calls organizationUsersQuery with correct pagination data', async () => {
+ findOrganizationUsersView().vm.$emit('prev');
+ await waitForPromises();
+
+ expect(mockResolver).toHaveBeenCalledWith({
+ id: MOCK_ORGANIZATION_GID,
+ before: MOCK_PAGE_INFO.startCursor,
+ after: '',
+ first: ORGANIZATION_USERS_PER_PAGE,
+ last: null,
+ });
+ });
+ });
});
diff --git a/spec/frontend/organizations/users/components/users_view_spec.js b/spec/frontend/organizations/users/components/users_view_spec.js
new file mode 100644
index 00000000000..d665c60d425
--- /dev/null
+++ b/spec/frontend/organizations/users/components/users_view_spec.js
@@ -0,0 +1,68 @@
+import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import UsersView from '~/organizations/users/components/users_view.vue';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+import { MOCK_PATHS, MOCK_USERS_FORMATTED, MOCK_PAGE_INFO } from '../mock_data';
+
+describe('UsersView', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(UsersView, {
+ propsData: {
+ loading: false,
+ users: MOCK_USERS_FORMATTED,
+ pageInfo: MOCK_PAGE_INFO,
+ ...props,
+ },
+ provide: {
+ paths: MOCK_PATHS,
+ },
+ });
+ };
+
+ const findGlLoading = () => wrapper.findComponent(GlLoadingIcon);
+ const findUsersTable = () => wrapper.findComponent(UsersTable);
+ const findGlKeysetPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ describe.each`
+ description | loading | usersData
+ ${'when loading'} | ${true} | ${[]}
+ ${'when not loading and has users'} | ${false} | ${MOCK_USERS_FORMATTED}
+ ${'when not loading and has no users'} | ${false} | ${[]}
+ `('$description', ({ loading, usersData }) => {
+ beforeEach(() => {
+ createComponent({ loading, users: usersData });
+ });
+
+ it(`does ${loading ? '' : 'not '}render loading icon`, () => {
+ expect(findGlLoading().exists()).toBe(loading);
+ });
+
+ it(`does ${!loading ? '' : 'not '}render users table`, () => {
+ expect(findUsersTable().exists()).toBe(!loading);
+ });
+
+ it(`does ${!loading ? '' : 'not '}render pagination`, () => {
+ expect(findGlKeysetPagination().exists()).toBe(Boolean(!loading));
+ });
+ });
+
+ describe('Pagination', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('@next event forwards up to the parent component', () => {
+ findGlKeysetPagination().vm.$emit('next');
+
+ expect(wrapper.emitted('next')).toHaveLength(1);
+ });
+
+ it('@prev event forwards up to the parent component', () => {
+ findGlKeysetPagination().vm.$emit('prev');
+
+ expect(wrapper.emitted('prev')).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/organizations/users/mock_data.js b/spec/frontend/organizations/users/mock_data.js
index 4f159c70c2c..16b3ec3bbcb 100644
--- a/spec/frontend/organizations/users/mock_data.js
+++ b/spec/frontend/organizations/users/mock_data.js
@@ -1,15 +1,31 @@
+const createUser = (id) => {
+ return {
+ id: `gid://gitlab/User/${id}`,
+ username: `test_user_${id}`,
+ avatarUrl: `/path/test_user_${id}`,
+ name: `Test User ${id}`,
+ publicEmail: `test_user_${id}@gitlab.com`,
+ createdAt: Date.now(),
+ lastActivityOn: Date.now(),
+ };
+};
+
export const MOCK_ORGANIZATION_GID = 'gid://gitlab/Organizations::Organization/1';
+export const MOCK_PATHS = {
+ adminUser: '/admin/users/:id',
+};
+
export const MOCK_USERS = [
{
badges: [],
id: 'gid://gitlab/Organizations::OrganizationUser/3',
- user: { id: 'gid://gitlab/User/3' },
+ user: createUser(3),
},
{
badges: [],
id: 'gid://gitlab/Organizations::OrganizationUser/2',
- user: { id: 'gid://gitlab/User/2' },
+ user: createUser(2),
},
{
badges: [
@@ -17,6 +33,18 @@ export const MOCK_USERS = [
{ text: "It's you!", variant: 'muted' },
],
id: 'gid://gitlab/Organizations::OrganizationUser/1',
- user: { id: 'gid://gitlab/User/1' },
+ user: createUser(1),
},
];
+
+export const MOCK_USERS_FORMATTED = MOCK_USERS.map(({ badges, user }) => {
+ return { ...user, badges, email: user.publicEmail };
+});
+
+export const MOCK_PAGE_INFO = {
+ startCursor: 'aaaa',
+ endCursor: 'bbbb',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ __typename: 'PageInfo',
+};