diff options
Diffstat (limited to 'spec/frontend/organizations')
7 files changed, 470 insertions, 0 deletions
diff --git a/spec/frontend/organizations/index/components/app_spec.js b/spec/frontend/organizations/index/components/app_spec.js new file mode 100644 index 00000000000..175b1e1c552 --- /dev/null +++ b/spec/frontend/organizations/index/components/app_spec.js @@ -0,0 +1,87 @@ +import { GlButton } from '@gitlab/ui'; +import Vue from 'vue'; +import VueApollo from 'vue-apollo'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import { 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 OrganizationsIndexApp from '~/organizations/index/components/app.vue'; +import OrganizationsView from '~/organizations/index/components/organizations_view.vue'; +import { MOCK_NEW_ORG_URL } from '../mock_data'; + +jest.mock('~/alert'); + +Vue.use(VueApollo); + +describe('OrganizationsIndexApp', () => { + let wrapper; + let mockApollo; + + const createComponent = (mockResolvers = resolvers) => { + mockApollo = createMockApollo([[organizationsQuery, mockResolvers]]); + + wrapper = shallowMountExtended(OrganizationsIndexApp, { + apolloProvider: mockApollo, + provide: { + newOrganizationUrl: MOCK_NEW_ORG_URL, + }, + }); + }; + + afterEach(() => { + mockApollo = null; + }); + + 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 } } }, + }); + 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 }) => { + beforeEach(async () => { + createComponent(mockResolver); + await waitForPromises(); + }); + + it(`does ${headerText ? '' : 'not '}render the header text`, () => { + expect(findOrganizationHeaderText().exists()).toBe(headerText); + }); + + it(`does ${newOrgLink ? '' : 'not '}render new organization button with correct link`, () => { + expect( + findNewOrganizationButton().exists() && findNewOrganizationButton().attributes('href'), + ).toBe(newOrgLink); + }); + + it(`renders the organizations view with ${loading} loading prop`, () => { + expect(findOrganizationsView().props('loading')).toBe(loading); + }); + + it(`renders the organizations view with ${ + orgsData ? 'correct' : 'empty' + } organizations array prop`, () => { + expect(findOrganizationsView().props('organizations')).toStrictEqual(orgsData); + }); + + it(`does ${error ? '' : 'not '}render an error message`, () => { + return error + ? expect(createAlert).toHaveBeenCalled() + : expect(createAlert).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/spec/frontend/organizations/index/components/organizations_list_item_spec.js b/spec/frontend/organizations/index/components/organizations_list_item_spec.js new file mode 100644 index 00000000000..b3bff5ed517 --- /dev/null +++ b/spec/frontend/organizations/index/components/organizations_list_item_spec.js @@ -0,0 +1,70 @@ +import { GlAvatarLabeled } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { getIdFromGraphQLId } from '~/graphql_shared/utils'; +import OrganizationsListItem from '~/organizations/index/components/organizations_list_item.vue'; +import { organizations } from '~/organizations/mock_data'; + +const MOCK_ORGANIZATION = organizations[0]; + +describe('OrganizationsListItem', () => { + let wrapper; + + const defaultProps = { + organization: MOCK_ORGANIZATION, + }; + + const createComponent = (props = {}) => { + wrapper = shallowMountExtended(OrganizationsListItem, { + propsData: { + ...defaultProps, + ...props, + }, + }); + }; + + const findGlAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled); + const findHTMLOrganizationDescription = () => + wrapper.findByTestId('organization-description-html'); + + describe('template', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders GlAvatarLabeled with correct data', () => { + expect(findGlAvatarLabeled().attributes()).toMatchObject({ + 'entity-id': getIdFromGraphQLId(MOCK_ORGANIZATION.id).toString(), + 'entity-name': MOCK_ORGANIZATION.name, + src: MOCK_ORGANIZATION.avatarUrl, + label: MOCK_ORGANIZATION.name, + labellink: MOCK_ORGANIZATION.webUrl, + }); + }); + }); + + describe('organization description', () => { + const descriptionHtml = '<p>Foo bar</p>'; + + describe('is a HTML description', () => { + beforeEach(() => { + createComponent({ organization: { ...MOCK_ORGANIZATION, descriptionHtml } }); + }); + + it('renders HTML description', () => { + expect(findHTMLOrganizationDescription().html()).toContain(descriptionHtml); + }); + }); + + describe('is not a HTML description', () => { + beforeEach(() => { + createComponent({ + organization: { ...MOCK_ORGANIZATION, descriptionHtml: null }, + }); + }); + + it('does not render HTML description', () => { + expect(findHTMLOrganizationDescription().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/organizations/index/components/organizations_list_spec.js b/spec/frontend/organizations/index/components/organizations_list_spec.js new file mode 100644 index 00000000000..0b59c212314 --- /dev/null +++ b/spec/frontend/organizations/index/components/organizations_list_spec.js @@ -0,0 +1,28 @@ +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'; + +describe('OrganizationsList', () => { + let wrapper; + + const createComponent = () => { + wrapper = shallowMount(OrganizationsList, { + propsData: { + organizations, + }, + }); + }; + + const findAllOrganizationsListItem = () => wrapper.findAllComponents(OrganizationsListItem); + + describe('template', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders a list item for each organization', () => { + expect(findAllOrganizationsListItem()).toHaveLength(organizations.length); + }); + }); +}); diff --git a/spec/frontend/organizations/index/components/organizations_view_spec.js b/spec/frontend/organizations/index/components/organizations_view_spec.js new file mode 100644 index 00000000000..85a1c11a2b1 --- /dev/null +++ b/spec/frontend/organizations/index/components/organizations_view_spec.js @@ -0,0 +1,57 @@ +import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import { organizations } from '~/organizations/mock_data'; +import OrganizationsView from '~/organizations/index/components/organizations_view.vue'; +import OrganizationsList from '~/organizations/index/components/organizations_list.vue'; +import { MOCK_NEW_ORG_URL, MOCK_ORG_EMPTY_STATE_SVG } from '../mock_data'; + +describe('OrganizationsView', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = shallowMount(OrganizationsView, { + propsData: { + ...props, + }, + provide: { + newOrganizationUrl: MOCK_NEW_ORG_URL, + organizationsEmptyStateSvgPath: MOCK_ORG_EMPTY_STATE_SVG, + }, + }); + }; + + const findGlLoading = () => wrapper.findComponent(GlLoadingIcon); + const findOrganizationsList = () => wrapper.findComponent(OrganizationsList); + const findGlEmptyState = () => wrapper.findComponent(GlEmptyState); + + describe.each` + description | loading | orgsData | emptyStateSvg | emptyStateUrl + ${'when loading'} | ${true} | ${[]} | ${false} | ${false} + ${'when not loading and has organizations'} | ${false} | ${organizations} | ${false} | ${false} + ${'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 }); + }); + + it(`does ${loading ? '' : 'not '}render loading icon`, () => { + expect(findGlLoading().exists()).toBe(loading); + }); + + it(`does ${orgsData.length ? '' : 'not '}render organizations list`, () => { + expect(findOrganizationsList().exists()).toBe(Boolean(orgsData.length)); + }); + + it(`does ${emptyStateSvg ? '' : 'not '}render empty state with SVG`, () => { + expect(findGlEmptyState().exists() && findGlEmptyState().attributes('svgpath')).toBe( + emptyStateSvg, + ); + }); + + it(`does ${emptyStateUrl ? '' : 'not '}render empty state with URL`, () => { + expect( + findGlEmptyState().exists() && findGlEmptyState().attributes('primarybuttonlink'), + ).toBe(emptyStateUrl); + }); + }); +}); diff --git a/spec/frontend/organizations/index/mock_data.js b/spec/frontend/organizations/index/mock_data.js new file mode 100644 index 00000000000..50b20b4f79c --- /dev/null +++ b/spec/frontend/organizations/index/mock_data.js @@ -0,0 +1,3 @@ +export const MOCK_NEW_ORG_URL = 'gitlab.com/organizations/new'; + +export const MOCK_ORG_EMPTY_STATE_SVG = 'illustrations/empty-state/empty-organizations-md.svg'; diff --git a/spec/frontend/organizations/new/components/app_spec.js b/spec/frontend/organizations/new/components/app_spec.js new file mode 100644 index 00000000000..06d30ad6b12 --- /dev/null +++ b/spec/frontend/organizations/new/components/app_spec.js @@ -0,0 +1,113 @@ +import VueApollo from 'vue-apollo'; +import Vue, { nextTick } from 'vue'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import App from '~/organizations/new/components/app.vue'; +import resolvers from '~/organizations/shared/graphql/resolvers'; +import NewEditForm from '~/organizations/shared/components/new_edit_form.vue'; +import { visitUrlWithAlerts } from '~/lib/utils/url_utility'; +import { createOrganizationResponse } from '~/organizations/mock_data'; +import { createAlert } from '~/alert'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; + +Vue.use(VueApollo); +jest.useFakeTimers(); + +jest.mock('~/lib/utils/url_utility'); +jest.mock('~/alert'); + +describe('OrganizationNewApp', () => { + let wrapper; + let mockApollo; + + const createComponent = ({ mockResolvers = resolvers } = {}) => { + mockApollo = createMockApollo([], mockResolvers); + + wrapper = shallowMountExtended(App, { apolloProvider: mockApollo }); + }; + + const findForm = () => wrapper.findComponent(NewEditForm); + const submitForm = async () => { + findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' }); + await nextTick(); + }; + + afterEach(() => { + mockApollo = null; + }); + + it('renders form', () => { + createComponent(); + + expect(findForm().exists()).toBe(true); + }); + + describe('when form is submitted', () => { + describe('when API is loading', () => { + beforeEach(async () => { + const mockResolvers = { + Mutation: { + createOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})), + }, + }; + + createComponent({ mockResolvers }); + + await submitForm(); + }); + + it('sets `NewEditForm` `loading` prop to `true`', () => { + expect(findForm().props('loading')).toBe(true); + }); + }); + + describe('when API request is successful', () => { + beforeEach(async () => { + createComponent(); + await submitForm(); + jest.runAllTimers(); + await waitForPromises(); + }); + + it('redirects user to organization path', () => { + expect(visitUrlWithAlerts).toHaveBeenCalledWith( + createOrganizationResponse.organization.path, + [ + { + id: 'organization-successfully-created', + title: 'Organization successfully created.', + message: 'You can now start using your new organization.', + variant: 'success', + }, + ], + ); + }); + }); + + describe('when API request is not successful', () => { + const error = new Error(); + + beforeEach(async () => { + const mockResolvers = { + Mutation: { + createOrganization: jest.fn().mockRejectedValueOnce(error), + }, + }; + + createComponent({ mockResolvers }); + await submitForm(); + jest.runAllTimers(); + await waitForPromises(); + }); + + it('displays error alert', () => { + expect(createAlert).toHaveBeenCalledWith({ + message: 'An error occurred creating an organization. Please try again.', + error, + captureError: true, + }); + }); + }); + }); +}); diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js new file mode 100644 index 00000000000..43c099fbb1c --- /dev/null +++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js @@ -0,0 +1,112 @@ +import { GlButton, GlInputGroupText, GlTruncate } from '@gitlab/ui'; + +import NewEditForm from '~/organizations/shared/components/new_edit_form.vue'; +import { mountExtended } from 'helpers/vue_test_utils_helper'; + +describe('NewEditForm', () => { + let wrapper; + + const defaultProvide = { + organizationsPath: '/-/organizations', + rootUrl: 'http://127.0.0.1:3000/', + }; + + const defaultPropsData = { + loading: false, + }; + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = mountExtended(NewEditForm, { + attachTo: document.body, + provide: defaultProvide, + propsData: { + ...defaultPropsData, + ...propsData, + }, + }); + }; + + const findNameField = () => wrapper.findByLabelText('Organization name'); + const findUrlField = () => wrapper.findByLabelText('Organization URL'); + const submitForm = async () => { + await wrapper.findByRole('button', { name: 'Create organization' }).trigger('click'); + }; + + it('renders `Organization name` field', () => { + createComponent(); + + expect(findNameField().exists()).toBe(true); + }); + + 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); + }); + + describe('when form is submitted without filling in required fields', () => { + beforeEach(async () => { + createComponent(); + await submitForm(); + }); + + it('shows error messages', () => { + expect(wrapper.findByText('Organization name is required.').exists()).toBe(true); + expect(wrapper.findByText('Organization URL is required.').exists()).toBe(true); + }); + }); + + describe('when form is submitted successfully', () => { + beforeEach(async () => { + createComponent(); + + await findNameField().setValue('Foo bar'); + await findUrlField().setValue('foo-bar'); + await submitForm(); + }); + + it('emits `submit` event with form values', () => { + expect(wrapper.emitted('submit')).toEqual([[{ name: 'Foo bar', path: 'foo-bar' }]]); + }); + }); + + describe('when `Organization URL` has not been manually set', () => { + beforeEach(async () => { + createComponent(); + + await findNameField().setValue('Foo bar'); + await submitForm(); + }); + + it('sets `Organization URL` when typing in `Organization name`', () => { + expect(findUrlField().element.value).toBe('foo-bar'); + }); + }); + + describe('when `Organization URL` has been manually set', () => { + beforeEach(async () => { + createComponent(); + + await findUrlField().setValue('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'); + }); + }); + + describe('when `loading` prop is `true`', () => { + beforeEach(() => { + createComponent({ propsData: { loading: true } }); + }); + + it('shows button with loading icon', () => { + expect(wrapper.findComponent(GlButton).props('loading')).toBe(true); + }); + }); +}); |