diff options
Diffstat (limited to 'spec/frontend/registry')
17 files changed, 392 insertions, 1503 deletions
diff --git a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js index b50ed87a563..632f506f4ae 100644 --- a/spec/frontend/registry/explorer/components/details_page/details_header_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js @@ -1,7 +1,10 @@ import { GlButton, GlIcon } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; import { useFakeDate } from 'helpers/fake_date'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import waitForPromises from 'helpers/wait_for_promises'; import component from '~/registry/explorer/components/details_page/details_header.vue'; import { UNSCHEDULED_STATUS, @@ -16,15 +19,18 @@ import { ROOT_IMAGE_TEXT, ROOT_IMAGE_TOOLTIP, } from '~/registry/explorer/constants'; +import getContainerRepositoryTagCountQuery from '~/registry/explorer/graphql/queries/get_container_repository_tags_count.query.graphql'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { imageTagsCountMock } from '../../mock_data'; describe('Details Header', () => { let wrapper; + let apolloProvider; + let localVue; const defaultImage = { name: 'foo', updatedAt: '2020-11-03T13:29:21Z', - tagsCount: 10, canDelete: true, project: { visibility: 'public', @@ -51,12 +57,31 @@ describe('Details Header', () => { await wrapper.vm.$nextTick(); }; - const mountComponent = (propsData = { image: defaultImage }) => { + const mountComponent = ({ + propsData = { image: defaultImage }, + resolver = jest.fn().mockResolvedValue(imageTagsCountMock()), + $apollo = undefined, + } = {}) => { + const mocks = {}; + + if ($apollo) { + mocks.$apollo = $apollo; + } else { + localVue = createLocalVue(); + localVue.use(VueApollo); + + const requestHandlers = [[getContainerRepositoryTagCountQuery, resolver]]; + apolloProvider = createMockApollo(requestHandlers); + } + wrapper = shallowMount(component, { + localVue, + apolloProvider, propsData, directives: { GlTooltip: createMockDirective(), }, + mocks, stubs: { TitleArea, }, @@ -64,41 +89,48 @@ describe('Details Header', () => { }; afterEach(() => { + // if we want to mix createMockApollo and manual mocks we need to reset everything wrapper.destroy(); + apolloProvider = undefined; + localVue = undefined; wrapper = null; }); + describe('image name', () => { describe('missing image name', () => { - it('root image ', () => { - mountComponent({ image: { ...defaultImage, name: '' } }); + beforeEach(() => { + mountComponent({ propsData: { image: { ...defaultImage, name: '' } } }); + + return waitForPromises(); + }); + it('root image ', () => { expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT); }); it('has an icon', () => { - mountComponent({ image: { ...defaultImage, name: '' } }); - expect(findInfoIcon().exists()).toBe(true); expect(findInfoIcon().props('name')).toBe('information-o'); }); it('has a tooltip', () => { - mountComponent({ image: { ...defaultImage, name: '' } }); - const tooltip = getBinding(findInfoIcon().element, 'gl-tooltip'); expect(tooltip.value).toBe(ROOT_IMAGE_TOOLTIP); }); }); describe('with image name present', () => { - it('shows image.name ', () => { + beforeEach(() => { mountComponent(); + + return waitForPromises(); + }); + + it('shows image.name ', () => { expect(findTitle().text()).toContain('foo'); }); it('has no icon', () => { - mountComponent(); - expect(findInfoIcon().exists()).toBe(false); }); }); @@ -111,16 +143,10 @@ describe('Details Header', () => { expect(findDeleteButton().exists()).toBe(true); }); - it('is hidden while loading', () => { - mountComponent({ image: defaultImage, metadataLoading: true }); - - expect(findDeleteButton().exists()).toBe(false); - }); - it('has the correct text', () => { mountComponent(); - expect(findDeleteButton().text()).toBe('Delete'); + expect(findDeleteButton().text()).toBe('Delete image repository'); }); it('has the correct props', () => { @@ -149,7 +175,7 @@ describe('Details Header', () => { `( 'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled', ({ canDelete, disabled, isDisabled }) => { - mountComponent({ image: { ...defaultImage, canDelete }, disabled }); + mountComponent({ propsData: { image: { ...defaultImage, canDelete }, disabled } }); expect(findDeleteButton().props('disabled')).toBe(isDisabled); }, @@ -158,15 +184,32 @@ describe('Details Header', () => { describe('metadata items', () => { describe('tags count', () => { + it('displays "-- tags" while loading', async () => { + // here we are forced to mock apollo because `waitForMetadataItems` waits + // for two ticks, de facto allowing the promise to resolve, so there is + // no way to catch the component as both rendered and in loading state + mountComponent({ $apollo: { queries: { containerRepository: { loading: true } } } }); + + await waitForMetadataItems(); + + expect(findTagsCount().props('text')).toBe('-- tags'); + }); + it('when there is more than one tag has the correct text', async () => { mountComponent(); + + await waitForPromises(); await waitForMetadataItems(); - expect(findTagsCount().props('text')).toBe('10 tags'); + expect(findTagsCount().props('text')).toBe('13 tags'); }); it('when there is one tag has the correct text', async () => { - mountComponent({ image: { ...defaultImage, tagsCount: 1 } }); + mountComponent({ + resolver: jest.fn().mockResolvedValue(imageTagsCountMock({ tagsCount: 1 })), + }); + + await waitForPromises(); await waitForMetadataItems(); expect(findTagsCount().props('text')).toBe('1 tag'); @@ -208,11 +251,13 @@ describe('Details Header', () => { 'when the status is $status the text is $text and the tooltip is $tooltip', async ({ status, text, tooltip }) => { mountComponent({ - image: { - ...defaultImage, - expirationPolicyCleanupStatus: status, - project: { - containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' }, + propsData: { + image: { + ...defaultImage, + expirationPolicyCleanupStatus: status, + project: { + containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' }, + }, }, }, }); @@ -242,7 +287,9 @@ describe('Details Header', () => { expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye'); }); it('shows an eye slashed when the project is not public', async () => { - mountComponent({ image: { ...defaultImage, project: { visibility: 'private' } } }); + mountComponent({ + propsData: { image: { ...defaultImage, project: { visibility: 'private' } } }, + }); await waitForMetadataItems(); expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash'); diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js index 8b70f84c1bd..dc9063bde2c 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js @@ -1,5 +1,6 @@ import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import { nextTick } from 'vue'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import DeleteButton from '~/registry/explorer/components/delete_button.vue'; @@ -72,8 +73,15 @@ describe('tags list row', () => { expect(findCheckbox().exists()).toBe(false); }); - it('is disabled when the digest is missing', () => { - mountComponent({ tag: { ...tag, digest: null } }); + it.each` + digest | disabled + ${'foo'} | ${true} + ${null} | ${false} + ${null} | ${true} + ${'foo'} | ${true} + `('is disabled when the digest $digest and disabled is $disabled', ({ digest, disabled }) => { + mountComponent({ tag: { ...tag, digest }, disabled }); + expect(findCheckbox().attributes('disabled')).toBe('true'); }); @@ -141,6 +149,12 @@ describe('tags list row', () => { title: tag.location, }); }); + + it('is disabled when the component is disabled', () => { + mountComponent({ ...defaultProps, disabled: true }); + + expect(findClipboardButton().attributes('disabled')).toBe('true'); + }); }); describe('warning icon', () => { @@ -266,15 +280,19 @@ describe('tags list row', () => { }); it.each` - canDelete | digest - ${true} | ${null} - ${false} | ${'foo'} - ${false} | ${null} - `('is disabled when canDelete is $canDelete and digest is $digest', ({ canDelete, digest }) => { - mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest } }); - - expect(findDeleteButton().attributes('disabled')).toBe('true'); - }); + canDelete | digest | disabled + ${true} | ${null} | ${true} + ${false} | ${'foo'} | ${true} + ${false} | ${null} | ${true} + ${true} | ${'foo'} | ${true} + `( + 'is disabled when canDelete is $canDelete and digest is $digest and disabled is $disabled', + ({ canDelete, digest, disabled }) => { + mountComponent({ ...defaultProps, tag: { ...tag, canDelete, digest }, disabled }); + + expect(findDeleteButton().attributes('disabled')).toBe('true'); + }, + ); it('delete event emits delete', () => { mountComponent(); @@ -287,13 +305,10 @@ describe('tags list row', () => { describe('details rows', () => { describe('when the tag has a digest', () => { - beforeEach(() => { + it('has 3 details rows', async () => { mountComponent(); + await nextTick(); - return wrapper.vm.$nextTick(); - }); - - it('has 3 details rows', () => { expect(findDetailsRows().length).toBe(3); }); @@ -303,17 +318,37 @@ describe('tags list row', () => { ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true} ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true} `('$name details row', ({ finderFunction, text, icon, clipboard }) => { - it(`has ${text} as text`, () => { + it(`has ${text} as text`, async () => { + mountComponent(); + await nextTick(); + expect(finderFunction().text()).toMatchInterpolatedText(text); }); - it(`has the ${icon} icon`, () => { + it(`has the ${icon} icon`, async () => { + mountComponent(); + await nextTick(); + expect(finderFunction().props('icon')).toBe(icon); }); - it(`is ${clipboard} that clipboard button exist`, () => { - expect(finderFunction().find(ClipboardButton).exists()).toBe(clipboard); - }); + if (clipboard) { + it(`clipboard button exist`, async () => { + mountComponent(); + await nextTick(); + + expect(finderFunction().find(ClipboardButton).exists()).toBe(clipboard); + }); + + it('is disabled when the component is disabled', async () => { + mountComponent({ ...defaultProps, disabled: true }); + await nextTick(); + + expect(finderFunction().findComponent(ClipboardButton).attributes('disabled')).toBe( + 'true', + ); + }); + } }); }); @@ -321,7 +356,7 @@ describe('tags list row', () => { it('hides the details rows', async () => { mountComponent({ tag: { ...tag, digest: null } }); - await wrapper.vm.$nextTick(); + await nextTick(); expect(findDetailsRows().length).toBe(0); }); }); diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js index dc6760a17bd..51934cd074d 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_list_spec.js @@ -1,22 +1,55 @@ -import { GlButton } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlButton, GlKeysetPagination } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { nextTick } from 'vue'; +import VueApollo from 'vue-apollo'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import waitForPromises from 'helpers/wait_for_promises'; +import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue'; import component from '~/registry/explorer/components/details_page/tags_list.vue'; import TagsListRow from '~/registry/explorer/components/details_page/tags_list_row.vue'; +import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue'; import { TAGS_LIST_TITLE, REMOVE_TAGS_BUTTON_TITLE } from '~/registry/explorer/constants/index'; -import { tagsMock } from '../../mock_data'; +import getContainerRepositoryTagsQuery from '~/registry/explorer/graphql/queries/get_container_repository_tags.query.graphql'; +import { tagsMock, imageTagsMock, tagsPageInfo } from '../../mock_data'; + +const localVue = createLocalVue(); describe('Tags List', () => { let wrapper; + let apolloProvider; const tags = [...tagsMock]; const readOnlyTags = tags.map((t) => ({ ...t, canDelete: false })); const findTagsListRow = () => wrapper.findAll(TagsListRow); const findDeleteButton = () => wrapper.find(GlButton); const findListTitle = () => wrapper.find('[data-testid="list-title"]'); + const findPagination = () => wrapper.find(GlKeysetPagination); + const findEmptyState = () => wrapper.find(EmptyTagsState); + const findTagsLoader = () => wrapper.find(TagsLoader); + + const waitForApolloRequestRender = async () => { + await waitForPromises(); + await nextTick(); + }; + + const mountComponent = ({ + propsData = { isMobile: false, id: 1 }, + resolver = jest.fn().mockResolvedValue(imageTagsMock()), + } = {}) => { + localVue.use(VueApollo); + + const requestHandlers = [[getContainerRepositoryTagsQuery, resolver]]; - const mountComponent = (propsData = { tags, isMobile: false }) => { + apolloProvider = createMockApollo(requestHandlers); wrapper = shallowMount(component, { + localVue, + apolloProvider, propsData, + provide() { + return { + config: {}, + }; + }, }); }; @@ -26,15 +59,19 @@ describe('Tags List', () => { }); describe('List title', () => { - it('exists', () => { + it('exists', async () => { mountComponent(); + await waitForApolloRequestRender(); + expect(findListTitle().exists()).toBe(true); }); - it('has the correct text', () => { + it('has the correct text', async () => { mountComponent(); + await waitForApolloRequestRender(); + expect(findListTitle().text()).toBe(TAGS_LIST_TITLE); }); }); @@ -48,21 +85,29 @@ describe('Tags List', () => { ${readOnlyTags} | ${true} | ${false} `( 'is $isVisible that delete button exists when tags is $inputTags and isMobile is $isMobile', - ({ inputTags, isMobile, isVisible }) => { - mountComponent({ tags: inputTags, isMobile }); + async ({ inputTags, isMobile, isVisible }) => { + mountComponent({ + propsData: { tags: inputTags, isMobile, id: 1 }, + resolver: jest.fn().mockResolvedValue(imageTagsMock(inputTags)), + }); + + await waitForApolloRequestRender(); expect(findDeleteButton().exists()).toBe(isVisible); }, ); - it('has the correct text', () => { + it('has the correct text', async () => { mountComponent(); + await waitForApolloRequestRender(); + expect(findDeleteButton().text()).toBe(REMOVE_TAGS_BUTTON_TITLE); }); - it('has the correct props', () => { + it('has the correct props', async () => { mountComponent(); + await waitForApolloRequestRender(); expect(findDeleteButton().attributes()).toMatchObject({ category: 'secondary', @@ -79,35 +124,44 @@ describe('Tags List', () => { `( 'is $buttonDisabled that the button is disabled when the component disabled state is $disabled and is $doSelect that the user selected a tag', async ({ disabled, buttonDisabled, doSelect }) => { - mountComponent({ tags, disabled, isMobile: false }); + mountComponent({ propsData: { tags, disabled, isMobile: false, id: 1 } }); + + await waitForApolloRequestRender(); if (doSelect) { findTagsListRow().at(0).vm.$emit('select'); - await wrapper.vm.$nextTick(); + await nextTick(); } expect(findDeleteButton().attributes('disabled')).toBe(buttonDisabled); }, ); - it('click event emits a deleted event with selected items', () => { + it('click event emits a deleted event with selected items', async () => { mountComponent(); - findTagsListRow().at(0).vm.$emit('select'); + await waitForApolloRequestRender(); + + findTagsListRow().at(0).vm.$emit('select'); findDeleteButton().vm.$emit('click'); - expect(wrapper.emitted('delete')).toEqual([[{ 'beta-24753': true }]]); + + expect(wrapper.emitted('delete')[0][0][0].name).toBe(tags[0].name); }); }); describe('list rows', () => { - it('one row exist for each tag', () => { + it('one row exist for each tag', async () => { mountComponent(); + await waitForApolloRequestRender(); + expect(findTagsListRow()).toHaveLength(tags.length); }); - it('the correct props are bound to it', () => { - mountComponent({ tags, disabled: true }); + it('the correct props are bound to it', async () => { + mountComponent({ propsData: { disabled: true, id: 1 } }); + + await waitForApolloRequestRender(); const rows = findTagsListRow(); @@ -120,16 +174,138 @@ describe('Tags List', () => { describe('events', () => { it('select event update the selected items', async () => { mountComponent(); + + await waitForApolloRequestRender(); + findTagsListRow().at(0).vm.$emit('select'); - await wrapper.vm.$nextTick(); + + await nextTick(); + expect(findTagsListRow().at(0).attributes('selected')).toBe('true'); }); - it('delete event emit a delete event', () => { + it('delete event emit a delete event', async () => { mountComponent(); + + await waitForApolloRequestRender(); + findTagsListRow().at(0).vm.$emit('delete'); - expect(wrapper.emitted('delete')).toEqual([[{ 'beta-24753': true }]]); + expect(wrapper.emitted('delete')[0][0][0].name).toBe(tags[0].name); + }); + }); + }); + + describe('when the list of tags is empty', () => { + const resolver = jest.fn().mockResolvedValue(imageTagsMock([])); + + it('has the empty state', async () => { + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + + expect(findEmptyState().exists()).toBe(true); + }); + + it('does not show the loader', async () => { + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + + expect(findTagsLoader().exists()).toBe(false); + }); + + it('does not show the list', async () => { + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + + expect(findTagsListRow().exists()).toBe(false); + expect(findListTitle().exists()).toBe(false); + }); + }); + + describe('pagination', () => { + it('exists', async () => { + mountComponent(); + + await waitForApolloRequestRender(); + + expect(findPagination().exists()).toBe(true); + }); + + it('is hidden when loading', () => { + mountComponent(); + + expect(findPagination().exists()).toBe(false); + }); + + it('is hidden when there are no more pages', async () => { + mountComponent({ resolver: jest.fn().mockResolvedValue(imageTagsMock([])) }); + + await waitForApolloRequestRender(); + + expect(findPagination().exists()).toBe(false); + }); + + it('is wired to the correct pagination props', async () => { + mountComponent(); + + await waitForApolloRequestRender(); + + expect(findPagination().props()).toMatchObject({ + hasNextPage: tagsPageInfo.hasNextPage, + hasPreviousPage: tagsPageInfo.hasPreviousPage, }); }); + + it('fetch next page when user clicks next', async () => { + const resolver = jest.fn().mockResolvedValue(imageTagsMock()); + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + + findPagination().vm.$emit('next'); + + expect(resolver).toHaveBeenCalledWith( + expect.objectContaining({ after: tagsPageInfo.endCursor }), + ); + }); + + it('fetch previous page when user clicks prev', async () => { + const resolver = jest.fn().mockResolvedValue(imageTagsMock()); + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + + findPagination().vm.$emit('prev'); + + expect(resolver).toHaveBeenCalledWith( + expect.objectContaining({ first: null, before: tagsPageInfo.startCursor }), + ); + }); + }); + + describe('loading state', () => { + it.each` + isImageLoading | queryExecuting | loadingVisible + ${true} | ${true} | ${true} + ${true} | ${false} | ${true} + ${false} | ${true} | ${true} + ${false} | ${false} | ${false} + `( + 'when the isImageLoading is $isImageLoading, and is $queryExecuting that the query is still executing is $loadingVisible that the loader is shown', + async ({ isImageLoading, queryExecuting, loadingVisible }) => { + mountComponent({ propsData: { isImageLoading, isMobile: false, id: 1 } }); + + if (!queryExecuting) { + await waitForApolloRequestRender(); + } + + expect(findTagsLoader().exists()).toBe(loadingVisible); + expect(findTagsListRow().exists()).toBe(!loadingVisible); + expect(findListTitle().exists()).toBe(!loadingVisible); + expect(findPagination().exists()).toBe(!loadingVisible); + }, + ); }); }); diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js index 6c897b983f7..323d7b177e7 100644 --- a/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js @@ -25,10 +25,11 @@ describe('Image List Row', () => { const findDetailsLink = () => wrapper.find('[data-testid="details-link"]'); const findTagsCount = () => wrapper.find('[data-testid="tags-count"]'); - const findDeleteBtn = () => wrapper.find(DeleteButton); - const findClipboardButton = () => wrapper.find(ClipboardButton); + const findDeleteBtn = () => wrapper.findComponent(DeleteButton); + const findClipboardButton = () => wrapper.findComponent(ClipboardButton); const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]'); - const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); + const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); + const findListItemComponent = () => wrapper.findComponent(ListItem); const mountComponent = (props) => { wrapper = shallowMount(Component, { @@ -52,20 +53,28 @@ describe('Image List Row', () => { wrapper = null; }); - describe('main tooltip', () => { - it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => { - mountComponent(); + describe('list item component', () => { + describe('tooltip', () => { + it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => { + mountComponent(); + + const tooltip = getBinding(wrapper.element, 'gl-tooltip'); + expect(tooltip).toBeDefined(); + expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION); + }); - const tooltip = getBinding(wrapper.element, 'gl-tooltip'); - expect(tooltip).toBeDefined(); - expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION); + it('is disabled when item is being deleted', () => { + mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } }); + + const tooltip = getBinding(wrapper.element, 'gl-tooltip'); + expect(tooltip.value.disabled).toBe(false); + }); }); - it('is disabled when item is being deleted', () => { + it('is disabled when the item is in deleting status', () => { mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } }); - const tooltip = getBinding(wrapper.element, 'gl-tooltip'); - expect(tooltip.value.disabled).toBe(false); + expect(findListItemComponent().props('disabled')).toBe(true); }); }); @@ -118,6 +127,20 @@ describe('Image List Row', () => { }, ); }); + + describe('when the item is deleting', () => { + beforeEach(() => { + mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } }); + }); + + it('the router link is disabled', () => { + // we check the event prop as is the only workaround to disable a router link + expect(findDetailsLink().props('event')).toBe(''); + }); + it('the clipboard button is disabled', () => { + expect(findClipboardButton().attributes('disabled')).toBe('true'); + }); + }); }); describe('delete button', () => { diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js index f4453912db4..fe258dcd4e8 100644 --- a/spec/frontend/registry/explorer/mock_data.js +++ b/spec/frontend/registry/explorer/mock_data.js @@ -113,7 +113,6 @@ export const containerRepositoryMock = { canDelete: true, createdAt: '2020-11-03T13:29:21Z', updatedAt: '2020-11-03T13:29:21Z', - tagsCount: 13, expirationPolicyStartedAt: null, expirationPolicyCleanupStatus: 'UNSCHEDULED', project: { @@ -161,6 +160,30 @@ export const tagsMock = [ }, ]; +export const imageTagsMock = (nodes = tagsMock) => ({ + data: { + containerRepository: { + id: containerRepositoryMock.id, + tags: { + nodes, + pageInfo: { ...tagsPageInfo }, + __typename: 'ContainerRepositoryTagConnection', + }, + __typename: 'ContainerRepositoryDetails', + }, + }, +}); + +export const imageTagsCountMock = (override) => ({ + data: { + containerRepository: { + id: containerRepositoryMock.id, + tagsCount: 13, + ...override, + }, + }, +}); + export const graphQLImageDetailsMock = (override) => ({ data: { containerRepository: { diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index 76baf4f72c9..022f6e71fe6 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -28,12 +28,10 @@ import Tracking from '~/tracking'; import { graphQLImageDetailsMock, - graphQLImageDetailsEmptyTagsMock, graphQLDeleteImageRepositoryTagsMock, containerRepositoryMock, graphQLEmptyImageDetailsMock, tagsMock, - tagsPageInfo, } from '../mock_data'; import { DeleteModal } from '../stubs'; @@ -72,12 +70,6 @@ describe('Details Page', () => { await wrapper.vm.$nextTick(); }; - const tagsArrayToSelectedTags = (tags) => - tags.reduce((acc, c) => { - acc[c.name] = true; - return acc; - }, {}); - const mountComponent = ({ resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()), mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock), @@ -138,12 +130,6 @@ describe('Details Page', () => { expect(findTagsList().exists()).toBe(false); }); - - it('does not show pagination', () => { - mountComponent(); - - expect(findPagination().exists()).toBe(false); - }); }); describe('when the image does not exist', () => { @@ -167,34 +153,6 @@ describe('Details Page', () => { }); }); - describe('when the list of tags is empty', () => { - const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock); - - it('has the empty state', async () => { - mountComponent({ resolver }); - - await waitForApolloRequestRender(); - - expect(findEmptyState().exists()).toBe(true); - }); - - it('does not show the loader', async () => { - mountComponent({ resolver }); - - await waitForApolloRequestRender(); - - expect(findTagsLoader().exists()).toBe(false); - }); - - it('does not show the list', async () => { - mountComponent({ resolver }); - - await waitForApolloRequestRender(); - - expect(findTagsList().exists()).toBe(false); - }); - }); - describe('list', () => { it('exists', async () => { mountComponent(); @@ -211,7 +169,6 @@ describe('Details Page', () => { expect(findTagsList().props()).toMatchObject({ isMobile: false, - tags: cleanTags, }); }); @@ -224,7 +181,7 @@ describe('Details Page', () => { await waitForApolloRequestRender(); [tagToBeDeleted] = cleanTags; - findTagsList().vm.$emit('delete', { [tagToBeDeleted.name]: true }); + findTagsList().vm.$emit('delete', [tagToBeDeleted]); }); it('open the modal', async () => { @@ -244,7 +201,7 @@ describe('Details Page', () => { await waitForApolloRequestRender(); - findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(cleanTags)); + findTagsList().vm.$emit('delete', cleanTags); }); it('open the modal', () => { @@ -260,61 +217,6 @@ describe('Details Page', () => { }); }); - describe('pagination', () => { - it('exists', async () => { - mountComponent(); - - await waitForApolloRequestRender(); - - expect(findPagination().exists()).toBe(true); - }); - - it('is hidden when there are no more pages', async () => { - mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock) }); - - await waitForApolloRequestRender(); - - expect(findPagination().exists()).toBe(false); - }); - - it('is wired to the correct pagination props', async () => { - mountComponent(); - - await waitForApolloRequestRender(); - - expect(findPagination().props()).toMatchObject({ - hasNextPage: tagsPageInfo.hasNextPage, - hasPreviousPage: tagsPageInfo.hasPreviousPage, - }); - }); - - it('fetch next page when user clicks next', async () => { - const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()); - mountComponent({ resolver }); - - await waitForApolloRequestRender(); - - findPagination().vm.$emit('next'); - - expect(resolver).toHaveBeenCalledWith( - expect.objectContaining({ after: tagsPageInfo.endCursor }), - ); - }); - - it('fetch previous page when user clicks prev', async () => { - const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()); - mountComponent({ resolver }); - - await waitForApolloRequestRender(); - - findPagination().vm.$emit('prev'); - - expect(resolver).toHaveBeenCalledWith( - expect.objectContaining({ first: null, before: tagsPageInfo.startCursor }), - ); - }); - }); - describe('modal', () => { it('exists', async () => { mountComponent(); @@ -349,7 +251,7 @@ describe('Details Page', () => { }); describe('when one item is selected to be deleted', () => { it('calls apollo mutation with the right parameters', async () => { - findTagsList().vm.$emit('delete', { [cleanTags[0].name]: true }); + findTagsList().vm.$emit('delete', [cleanTags[0]]); await wrapper.vm.$nextTick(); @@ -363,7 +265,7 @@ describe('Details Page', () => { describe('when more than one item is selected to be deleted', () => { it('calls apollo mutation with the right parameters', async () => { - findTagsList().vm.$emit('delete', { ...tagsArrayToSelectedTags(tagsMock) }); + findTagsList().vm.$emit('delete', tagsMock); await wrapper.vm.$nextTick(); @@ -390,7 +292,6 @@ describe('Details Page', () => { await waitForApolloRequestRender(); expect(findDetailsHeader().props()).toMatchObject({ - metadataLoading: false, image: { name: containerRepositoryMock.name, project: { diff --git a/spec/frontend/registry/settings/__snapshots__/utils_spec.js.snap b/spec/frontend/registry/settings/__snapshots__/utils_spec.js.snap deleted file mode 100644 index 7062773b46b..00000000000 --- a/spec/frontend/registry/settings/__snapshots__/utils_spec.js.snap +++ /dev/null @@ -1,101 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Utils formOptionsGenerator returns an object containing cadence 1`] = ` -Array [ - Object { - "default": true, - "key": "EVERY_DAY", - "label": "Every day", - }, - Object { - "default": false, - "key": "EVERY_WEEK", - "label": "Every week", - }, - Object { - "default": false, - "key": "EVERY_TWO_WEEKS", - "label": "Every two weeks", - }, - Object { - "default": false, - "key": "EVERY_MONTH", - "label": "Every month", - }, - Object { - "default": false, - "key": "EVERY_THREE_MONTHS", - "label": "Every three months", - }, -] -`; - -exports[`Utils formOptionsGenerator returns an object containing keepN 1`] = ` -Array [ - Object { - "default": false, - "key": "ONE_TAG", - "label": "1 tag per image name", - "variable": 1, - }, - Object { - "default": false, - "key": "FIVE_TAGS", - "label": "5 tags per image name", - "variable": 5, - }, - Object { - "default": true, - "key": "TEN_TAGS", - "label": "10 tags per image name", - "variable": 10, - }, - Object { - "default": false, - "key": "TWENTY_FIVE_TAGS", - "label": "25 tags per image name", - "variable": 25, - }, - Object { - "default": false, - "key": "FIFTY_TAGS", - "label": "50 tags per image name", - "variable": 50, - }, - Object { - "default": false, - "key": "ONE_HUNDRED_TAGS", - "label": "100 tags per image name", - "variable": 100, - }, -] -`; - -exports[`Utils formOptionsGenerator returns an object containing olderThan 1`] = ` -Array [ - Object { - "default": false, - "key": "SEVEN_DAYS", - "label": "7 days", - "variable": 7, - }, - Object { - "default": false, - "key": "FOURTEEN_DAYS", - "label": "14 days", - "variable": 14, - }, - Object { - "default": false, - "key": "THIRTY_DAYS", - "label": "30 days", - "variable": 30, - }, - Object { - "default": true, - "key": "NINETY_DAYS", - "label": "90 days", - "variable": 90, - }, -] -`; diff --git a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap deleted file mode 100644 index 7a52b4a5d0f..00000000000 --- a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap +++ /dev/null @@ -1,64 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`Settings Form Cadence matches snapshot 1`] = ` -<expiration-dropdown-stub - class="gl-mr-7 gl-mb-0!" - data-testid="cadence-dropdown" - formoptions="[object Object],[object Object],[object Object],[object Object],[object Object]" - label="Run cleanup:" - name="cadence" - value="EVERY_DAY" -/> -`; - -exports[`Settings Form Enable matches snapshot 1`] = ` -<expiration-toggle-stub - class="gl-mb-0!" - data-testid="enable-toggle" - value="true" -/> -`; - -exports[`Settings Form Keep N matches snapshot 1`] = ` -<expiration-dropdown-stub - data-testid="keep-n-dropdown" - formoptions="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" - label="Keep the most recent:" - name="keep-n" - value="TEN_TAGS" -/> -`; - -exports[`Settings Form Keep Regex matches snapshot 1`] = ` -<expiration-input-stub - data-testid="keep-regex-input" - description="Tags with names that match this regex pattern are kept. %{linkStart}View regex examples.%{linkEnd}" - error="" - label="Keep tags matching:" - name="keep-regex" - placeholder="" - value="sss" -/> -`; - -exports[`Settings Form OlderThan matches snapshot 1`] = ` -<expiration-dropdown-stub - data-testid="older-than-dropdown" - formoptions="[object Object],[object Object],[object Object],[object Object]" - label="Remove tags older than:" - name="older-than" - value="FOURTEEN_DAYS" -/> -`; - -exports[`Settings Form Remove regex matches snapshot 1`] = ` -<expiration-input-stub - data-testid="remove-regex-input" - description="Tags with names that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}" - error="" - label="Remove tags matching:" - name="remove-regex" - placeholder=".*" - value="asdasdssssdfdf" -/> -`; diff --git a/spec/frontend/registry/settings/components/expiration_dropdown_spec.js b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js deleted file mode 100644 index f777f7ec9de..00000000000 --- a/spec/frontend/registry/settings/components/expiration_dropdown_spec.js +++ /dev/null @@ -1,86 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlFormGroup, GlFormSelect } from 'jest/registry/shared/stubs'; -import component from '~/registry/settings/components/expiration_dropdown.vue'; - -describe('ExpirationDropdown', () => { - let wrapper; - - const defaultProps = { - name: 'foo', - label: 'label-bar', - formOptions: [ - { key: 'foo', label: 'bar' }, - { key: 'baz', label: 'zab' }, - ], - }; - - const findFormSelect = () => wrapper.find(GlFormSelect); - const findFormGroup = () => wrapper.find(GlFormGroup); - const findOptions = () => wrapper.findAll('[data-testid="option"]'); - - const mountComponent = (props) => { - wrapper = shallowMount(component, { - stubs: { - GlFormGroup, - GlFormSelect, - }, - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('structure', () => { - it('has a form-select component', () => { - mountComponent(); - expect(findFormSelect().exists()).toBe(true); - }); - - it('has the correct options', () => { - mountComponent(); - - expect(findOptions()).toHaveLength(defaultProps.formOptions.length); - }); - }); - - describe('model', () => { - it('assign the right props to the form-select component', () => { - const value = 'foobar'; - const disabled = true; - - mountComponent({ value, disabled }); - - expect(findFormSelect().props()).toMatchObject({ - value, - disabled, - }); - expect(findFormSelect().attributes('id')).toBe(defaultProps.name); - }); - - it('assign the right props to the form-group component', () => { - mountComponent(); - - expect(findFormGroup().attributes()).toMatchObject({ - id: `${defaultProps.name}-form-group`, - 'label-for': defaultProps.name, - label: defaultProps.label, - }); - }); - - it('emits input event when form-select emits input', () => { - const emittedValue = 'barfoo'; - - mountComponent(); - - findFormSelect().vm.$emit('input', emittedValue); - - expect(wrapper.emitted('input')).toEqual([[emittedValue]]); - }); - }); -}); diff --git a/spec/frontend/registry/settings/components/expiration_input_spec.js b/spec/frontend/registry/settings/components/expiration_input_spec.js deleted file mode 100644 index b91599a2789..00000000000 --- a/spec/frontend/registry/settings/components/expiration_input_spec.js +++ /dev/null @@ -1,169 +0,0 @@ -import { GlSprintf, GlFormInput, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { GlFormGroup } from 'jest/registry/shared/stubs'; -import component from '~/registry/settings/components/expiration_input.vue'; -import { NAME_REGEX_LENGTH } from '~/registry/settings/constants'; - -describe('ExpirationInput', () => { - let wrapper; - - const defaultProps = { - name: 'foo', - label: 'label-bar', - placeholder: 'placeholder-baz', - description: '%{linkStart}description-foo%{linkEnd}', - }; - - const tagsRegexHelpPagePath = 'fooPath'; - - const findInput = () => wrapper.find(GlFormInput); - const findFormGroup = () => wrapper.find(GlFormGroup); - const findLabel = () => wrapper.find('[data-testid="label"]'); - const findDescription = () => wrapper.find('[data-testid="description"]'); - const findDescriptionLink = () => wrapper.find(GlLink); - - const mountComponent = (props) => { - wrapper = shallowMount(component, { - stubs: { - GlSprintf, - GlFormGroup, - }, - provide: { - tagsRegexHelpPagePath, - }, - propsData: { - ...defaultProps, - ...props, - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('structure', () => { - it('has a label', () => { - mountComponent(); - - expect(findLabel().text()).toBe(defaultProps.label); - }); - - it('has a textarea component', () => { - mountComponent(); - - expect(findInput().exists()).toBe(true); - }); - - it('has a description', () => { - mountComponent(); - - expect(findDescription().text()).toMatchInterpolatedText(defaultProps.description); - }); - - it('has a description link', () => { - mountComponent(); - - const link = findDescriptionLink(); - expect(link.exists()).toBe(true); - expect(link.attributes('href')).toBe(tagsRegexHelpPagePath); - }); - }); - - describe('model', () => { - it('assigns the right props to the textarea component', () => { - const value = 'foobar'; - const disabled = true; - - mountComponent({ value, disabled }); - - expect(findInput().attributes()).toMatchObject({ - id: defaultProps.name, - value, - placeholder: defaultProps.placeholder, - disabled: `${disabled}`, - trim: '', - }); - }); - - it('emits input event when textarea emits input', () => { - const emittedValue = 'barfoo'; - - mountComponent(); - - findInput().vm.$emit('input', emittedValue); - expect(wrapper.emitted('input')).toEqual([[emittedValue]]); - }); - }); - - describe('regex textarea validation', () => { - const invalidString = new Array(NAME_REGEX_LENGTH + 2).join(','); - - describe('when error contains an error message', () => { - const errorMessage = 'something went wrong'; - - it('shows the error message on the relevant field', () => { - mountComponent({ error: errorMessage }); - - expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage); - }); - - it('gives precedence to API errors compared to local ones', () => { - mountComponent({ - error: errorMessage, - value: invalidString, - }); - - expect(findFormGroup().attributes('invalid-feedback')).toBe(errorMessage); - }); - }); - - describe('when error is empty', () => { - describe('if the user did not type', () => { - it('validation is not emitted', () => { - mountComponent(); - - expect(wrapper.emitted('validation')).toBeUndefined(); - }); - - it('no error message is shown', () => { - mountComponent(); - - expect(findFormGroup().props('state')).toBe(true); - expect(findFormGroup().attributes('invalid-feedback')).toBe(''); - }); - }); - - describe('when the user typed something', () => { - describe(`when name regex is longer than ${NAME_REGEX_LENGTH}`, () => { - beforeEach(() => { - // since the component has no state we both emit the event and set the prop - mountComponent({ value: invalidString }); - - findInput().vm.$emit('input', invalidString); - }); - - it('textAreaValidation state is false', () => { - expect(findFormGroup().props('state')).toBe(false); - expect(findInput().attributes('state')).toBeUndefined(); - }); - - it('emits the @validation event with false payload', () => { - expect(wrapper.emitted('validation')).toEqual([[false]]); - }); - }); - - it(`when user input is less than ${NAME_REGEX_LENGTH} state is "true"`, () => { - mountComponent(); - - findInput().vm.$emit('input', 'foo'); - - expect(findFormGroup().props('state')).toBe(true); - expect(findInput().attributes('state')).toBe('true'); - expect(wrapper.emitted('validation')).toEqual([[true]]); - }); - }); - }); - }); -}); diff --git a/spec/frontend/registry/settings/components/expiration_run_text_spec.js b/spec/frontend/registry/settings/components/expiration_run_text_spec.js deleted file mode 100644 index 753bb10ad08..00000000000 --- a/spec/frontend/registry/settings/components/expiration_run_text_spec.js +++ /dev/null @@ -1,62 +0,0 @@ -import { GlFormInput } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { GlFormGroup } from 'jest/registry/shared/stubs'; -import component from '~/registry/settings/components/expiration_run_text.vue'; -import { NEXT_CLEANUP_LABEL, NOT_SCHEDULED_POLICY_TEXT } from '~/registry/settings/constants'; - -describe('ExpirationToggle', () => { - let wrapper; - const value = 'foo'; - - const findInput = () => wrapper.find(GlFormInput); - const findFormGroup = () => wrapper.find(GlFormGroup); - - const mountComponent = (propsData) => { - wrapper = shallowMount(component, { - stubs: { - GlFormGroup, - }, - propsData, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('structure', () => { - it('has an input component', () => { - mountComponent(); - - expect(findInput().exists()).toBe(true); - }); - }); - - describe('model', () => { - it('assigns the right props to the form-group component', () => { - mountComponent(); - - expect(findFormGroup().attributes()).toMatchObject({ - label: NEXT_CLEANUP_LABEL, - }); - }); - }); - - describe('formattedValue', () => { - it.each` - valueProp | enabled | expected - ${value} | ${true} | ${value} - ${value} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT} - ${undefined} | ${false} | ${NOT_SCHEDULED_POLICY_TEXT} - ${undefined} | ${true} | ${NOT_SCHEDULED_POLICY_TEXT} - `( - 'when value is $valueProp and enabled is $enabled the input value is $expected', - ({ valueProp, enabled, expected }) => { - mountComponent({ value: valueProp, enabled }); - - expect(findInput().attributes('value')).toBe(expected); - }, - ); - }); -}); diff --git a/spec/frontend/registry/settings/components/expiration_toggle_spec.js b/spec/frontend/registry/settings/components/expiration_toggle_spec.js deleted file mode 100644 index 7598f6adc89..00000000000 --- a/spec/frontend/registry/settings/components/expiration_toggle_spec.js +++ /dev/null @@ -1,77 +0,0 @@ -import { GlToggle, GlSprintf } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import { GlFormGroup } from 'jest/registry/shared/stubs'; -import component from '~/registry/settings/components/expiration_toggle.vue'; -import { - ENABLED_TOGGLE_DESCRIPTION, - DISABLED_TOGGLE_DESCRIPTION, -} from '~/registry/settings/constants'; - -describe('ExpirationToggle', () => { - let wrapper; - - const findToggle = () => wrapper.find(GlToggle); - const findDescription = () => wrapper.find('[data-testid="description"]'); - - const mountComponent = (propsData) => { - wrapper = shallowMount(component, { - stubs: { - GlFormGroup, - GlSprintf, - }, - propsData, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('structure', () => { - it('has a toggle component', () => { - mountComponent(); - - expect(findToggle().props('label')).toBe(component.i18n.toggleLabel); - }); - - it('has a description', () => { - mountComponent(); - - expect(findDescription().exists()).toBe(true); - }); - }); - - describe('model', () => { - it('assigns the right props to the toggle component', () => { - mountComponent({ value: true, disabled: true }); - - expect(findToggle().props()).toMatchObject({ - value: true, - disabled: true, - }); - }); - - it('emits input event when toggle is updated', () => { - mountComponent(); - - findToggle().vm.$emit('change', false); - - expect(wrapper.emitted('input')).toEqual([[false]]); - }); - }); - - describe('toggle description', () => { - it('says enabled when the toggle is on', () => { - mountComponent({ value: true }); - - expect(findDescription().text()).toMatchInterpolatedText(ENABLED_TOGGLE_DESCRIPTION); - }); - - it('says disabled when the toggle is off', () => { - mountComponent({ value: false }); - - expect(findDescription().text()).toMatchInterpolatedText(DISABLED_TOGGLE_DESCRIPTION); - }); - }); -}); diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js deleted file mode 100644 index fd53efa884f..00000000000 --- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js +++ /dev/null @@ -1,164 +0,0 @@ -import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import component from '~/registry/settings/components/registry_settings_app.vue'; -import SettingsForm from '~/registry/settings/components/settings_form.vue'; -import { - FETCH_SETTINGS_ERROR_MESSAGE, - UNAVAILABLE_FEATURE_INTRO_TEXT, - UNAVAILABLE_USER_FEATURE_TEXT, -} from '~/registry/settings/constants'; -import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql'; - -import { - expirationPolicyPayload, - emptyExpirationPolicyPayload, - containerExpirationPolicyData, -} from '../mock_data'; - -const localVue = createLocalVue(); - -describe('Registry Settings App', () => { - let wrapper; - let fakeApollo; - - const defaultProvidedValues = { - projectPath: 'path', - isAdmin: false, - adminSettingsPath: 'settingsPath', - enableHistoricEntries: false, - }; - - const findSettingsComponent = () => wrapper.find(SettingsForm); - const findAlert = () => wrapper.find(GlAlert); - - const mountComponent = (provide = defaultProvidedValues, config) => { - wrapper = shallowMount(component, { - stubs: { - GlSprintf, - }, - mocks: { - $toast: { - show: jest.fn(), - }, - }, - provide, - ...config, - }); - }; - - const mountComponentWithApollo = ({ provide = defaultProvidedValues, resolver } = {}) => { - localVue.use(VueApollo); - - const requestHandlers = [[expirationPolicyQuery, resolver]]; - - fakeApollo = createMockApollo(requestHandlers); - mountComponent(provide, { - localVue, - apolloProvider: fakeApollo, - }); - - return requestHandlers.map((request) => request[1]); - }; - - afterEach(() => { - wrapper.destroy(); - }); - - describe('isEdited status', () => { - it.each` - description | apiResponse | workingCopy | result - ${'empty response and no changes from user'} | ${emptyExpirationPolicyPayload()} | ${{}} | ${false} - ${'empty response and changes from user'} | ${emptyExpirationPolicyPayload()} | ${{ enabled: true }} | ${true} - ${'response and no changes'} | ${expirationPolicyPayload()} | ${containerExpirationPolicyData()} | ${false} - ${'response and changes'} | ${expirationPolicyPayload()} | ${{ ...containerExpirationPolicyData(), nameRegex: '12345' }} | ${true} - ${'response and empty'} | ${expirationPolicyPayload()} | ${{}} | ${true} - `('$description', async ({ apiResponse, workingCopy, result }) => { - const requests = mountComponentWithApollo({ - provide: { ...defaultProvidedValues, enableHistoricEntries: true }, - resolver: jest.fn().mockResolvedValue(apiResponse), - }); - await Promise.all(requests); - - findSettingsComponent().vm.$emit('input', workingCopy); - - await wrapper.vm.$nextTick(); - - expect(findSettingsComponent().props('isEdited')).toBe(result); - }); - }); - - it('renders the setting form', async () => { - const requests = mountComponentWithApollo({ - resolver: jest.fn().mockResolvedValue(expirationPolicyPayload()), - }); - await Promise.all(requests); - - expect(findSettingsComponent().exists()).toBe(true); - }); - - describe('the form is disabled', () => { - it('the form is hidden', () => { - mountComponent(); - - expect(findSettingsComponent().exists()).toBe(false); - }); - - it('shows an alert', () => { - mountComponent(); - - const text = findAlert().text(); - expect(text).toContain(UNAVAILABLE_FEATURE_INTRO_TEXT); - expect(text).toContain(UNAVAILABLE_USER_FEATURE_TEXT); - }); - - describe('an admin is visiting the page', () => { - it('shows the admin part of the alert message', () => { - mountComponent({ ...defaultProvidedValues, isAdmin: true }); - - const sprintf = findAlert().find(GlSprintf); - expect(sprintf.text()).toBe('administration settings'); - expect(sprintf.find(GlLink).attributes('href')).toBe( - defaultProvidedValues.adminSettingsPath, - ); - }); - }); - }); - - describe('fetchSettingsError', () => { - beforeEach(() => { - const requests = mountComponentWithApollo({ - resolver: jest.fn().mockRejectedValue(new Error('GraphQL error')), - }); - return Promise.all(requests); - }); - - it('the form is hidden', () => { - expect(findSettingsComponent().exists()).toBe(false); - }); - - it('shows an alert', () => { - expect(findAlert().html()).toContain(FETCH_SETTINGS_ERROR_MESSAGE); - }); - }); - - describe('empty API response', () => { - it.each` - enableHistoricEntries | isShown - ${true} | ${true} - ${false} | ${false} - `('is $isShown that the form is shown', async ({ enableHistoricEntries, isShown }) => { - const requests = mountComponentWithApollo({ - provide: { - ...defaultProvidedValues, - enableHistoricEntries, - }, - resolver: jest.fn().mockResolvedValue(emptyExpirationPolicyPayload()), - }); - await Promise.all(requests); - - expect(findSettingsComponent().exists()).toBe(isShown); - }); - }); -}); diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js deleted file mode 100644 index ad94da6ca66..00000000000 --- a/spec/frontend/registry/settings/components/settings_form_spec.js +++ /dev/null @@ -1,460 +0,0 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import VueApollo from 'vue-apollo'; -import createMockApollo from 'helpers/mock_apollo_helper'; -import waitForPromises from 'helpers/wait_for_promises'; -import component from '~/registry/settings/components/settings_form.vue'; -import { - UPDATE_SETTINGS_ERROR_MESSAGE, - UPDATE_SETTINGS_SUCCESS_MESSAGE, -} from '~/registry/settings/constants'; -import updateContainerExpirationPolicyMutation from '~/registry/settings/graphql/mutations/update_container_expiration_policy.mutation.graphql'; -import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql'; -import Tracking from '~/tracking'; -import { GlCard, GlLoadingIcon } from '../../shared/stubs'; -import { expirationPolicyPayload, expirationPolicyMutationPayload } from '../mock_data'; - -const localVue = createLocalVue(); - -describe('Settings Form', () => { - let wrapper; - let fakeApollo; - - const defaultProvidedValues = { - projectPath: 'path', - }; - - const { - data: { - project: { containerExpirationPolicy }, - }, - } = expirationPolicyPayload(); - - const defaultProps = { - value: { ...containerExpirationPolicy }, - }; - - const trackingPayload = { - label: 'docker_container_retention_and_expiration_policies', - }; - - const findForm = () => wrapper.find({ ref: 'form-element' }); - - const findCancelButton = () => wrapper.find('[data-testid="cancel-button"'); - const findSaveButton = () => wrapper.find('[data-testid="save-button"'); - const findEnableToggle = () => wrapper.find('[data-testid="enable-toggle"]'); - const findCadenceDropdown = () => wrapper.find('[data-testid="cadence-dropdown"]'); - const findKeepNDropdown = () => wrapper.find('[data-testid="keep-n-dropdown"]'); - const findKeepRegexInput = () => wrapper.find('[data-testid="keep-regex-input"]'); - const findOlderThanDropdown = () => wrapper.find('[data-testid="older-than-dropdown"]'); - const findRemoveRegexInput = () => wrapper.find('[data-testid="remove-regex-input"]'); - - const mountComponent = ({ - props = defaultProps, - data, - config, - provide = defaultProvidedValues, - mocks, - } = {}) => { - wrapper = shallowMount(component, { - stubs: { - GlCard, - GlLoadingIcon, - }, - propsData: { ...props }, - provide, - data() { - return { - ...data, - }; - }, - mocks: { - $toast: { - show: jest.fn(), - }, - ...mocks, - }, - ...config, - }); - }; - - const mountComponentWithApollo = ({ - provide = defaultProvidedValues, - mutationResolver, - queryPayload = expirationPolicyPayload(), - } = {}) => { - localVue.use(VueApollo); - - const requestHandlers = [ - [updateContainerExpirationPolicyMutation, mutationResolver], - [expirationPolicyQuery, jest.fn().mockResolvedValue(queryPayload)], - ]; - - fakeApollo = createMockApollo(requestHandlers); - - // This component does not do the query directly, but we need a proper cache to update - fakeApollo.defaultClient.cache.writeQuery({ - query: expirationPolicyQuery, - variables: { - projectPath: provide.projectPath, - }, - ...queryPayload, - }); - - // we keep in sync what prop we pass to the component with the cache - const { - data: { - project: { containerExpirationPolicy: value }, - }, - } = queryPayload; - - mountComponent({ - provide, - props: { - ...defaultProps, - value, - }, - config: { - localVue, - apolloProvider: fakeApollo, - }, - }); - }; - - beforeEach(() => { - jest.spyOn(Tracking, 'event'); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe.each` - model | finder | fieldName | type | defaultValue - ${'enabled'} | ${findEnableToggle} | ${'Enable'} | ${'toggle'} | ${false} - ${'cadence'} | ${findCadenceDropdown} | ${'Cadence'} | ${'dropdown'} | ${'EVERY_DAY'} - ${'keepN'} | ${findKeepNDropdown} | ${'Keep N'} | ${'dropdown'} | ${'TEN_TAGS'} - ${'nameRegexKeep'} | ${findKeepRegexInput} | ${'Keep Regex'} | ${'textarea'} | ${''} - ${'olderThan'} | ${findOlderThanDropdown} | ${'OlderThan'} | ${'dropdown'} | ${'NINETY_DAYS'} - ${'nameRegex'} | ${findRemoveRegexInput} | ${'Remove regex'} | ${'textarea'} | ${''} - `('$fieldName', ({ model, finder, type, defaultValue }) => { - it('matches snapshot', () => { - mountComponent(); - - expect(finder().element).toMatchSnapshot(); - }); - - it('input event triggers a model update', () => { - mountComponent(); - - finder().vm.$emit('input', 'foo'); - expect(wrapper.emitted('input')[0][0]).toMatchObject({ - [model]: 'foo', - }); - }); - - it('shows the default option when none are selected', () => { - mountComponent({ props: { value: {} } }); - expect(finder().props('value')).toEqual(defaultValue); - }); - - if (type !== 'toggle') { - it.each` - isLoading | mutationLoading | enabledValue - ${false} | ${false} | ${false} - ${true} | ${false} | ${false} - ${true} | ${true} | ${true} - ${false} | ${true} | ${true} - ${false} | ${false} | ${false} - `( - 'is disabled when is loading is $isLoading, mutationLoading is $mutationLoading and enabled is $enabledValue', - ({ isLoading, mutationLoading, enabledValue }) => { - mountComponent({ - props: { isLoading, value: { enabled: enabledValue } }, - data: { mutationLoading }, - }); - expect(finder().props('disabled')).toEqual(true); - }, - ); - } else { - it.each` - isLoading | mutationLoading - ${true} | ${false} - ${true} | ${true} - ${false} | ${true} - `( - 'is disabled when is loading is $isLoading and mutationLoading is $mutationLoading', - ({ isLoading, mutationLoading }) => { - mountComponent({ - props: { isLoading, value: {} }, - data: { mutationLoading }, - }); - expect(finder().props('disabled')).toEqual(true); - }, - ); - } - - if (type === 'textarea') { - it('input event updates the api error property', async () => { - const apiErrors = { [model]: 'bar' }; - mountComponent({ data: { apiErrors } }); - - finder().vm.$emit('input', 'foo'); - expect(finder().props('error')).toEqual('bar'); - - await wrapper.vm.$nextTick(); - - expect(finder().props('error')).toEqual(''); - }); - - it('validation event updates buttons disabled state', async () => { - mountComponent(); - - expect(findSaveButton().props('disabled')).toBe(false); - - finder().vm.$emit('validation', false); - - await wrapper.vm.$nextTick(); - - expect(findSaveButton().props('disabled')).toBe(true); - }); - } - - if (type === 'dropdown') { - it('has the correct formOptions', () => { - mountComponent(); - expect(finder().props('formOptions')).toEqual(wrapper.vm.$options.formOptions[model]); - }); - } - }); - - describe('form', () => { - describe('form reset event', () => { - it('calls the appropriate function', () => { - mountComponent(); - - findForm().trigger('reset'); - - expect(wrapper.emitted('reset')).toEqual([[]]); - }); - - it('tracks the reset event', () => { - mountComponent(); - - findForm().trigger('reset'); - - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'reset_form', trackingPayload); - }); - - it('resets the errors objects', async () => { - mountComponent({ - data: { apiErrors: { nameRegex: 'bar' }, localErrors: { nameRegexKeep: false } }, - }); - - findForm().trigger('reset'); - - await wrapper.vm.$nextTick(); - - expect(findKeepRegexInput().props('error')).toBe(''); - expect(findRemoveRegexInput().props('error')).toBe(''); - expect(findSaveButton().props('disabled')).toBe(false); - }); - }); - - describe('form submit event ', () => { - it('save has type submit', () => { - mountComponent(); - - expect(findSaveButton().attributes('type')).toBe('submit'); - }); - - it('dispatches the correct apollo mutation', () => { - const mutationResolver = jest.fn().mockResolvedValue(expirationPolicyMutationPayload()); - mountComponentWithApollo({ - mutationResolver, - }); - - findForm().trigger('submit'); - - expect(mutationResolver).toHaveBeenCalled(); - }); - - it('saves the default values when a value is missing did not change the default options', async () => { - const mutationResolver = jest.fn().mockResolvedValue(expirationPolicyMutationPayload()); - mountComponentWithApollo({ - mutationResolver, - queryPayload: expirationPolicyPayload({ keepN: null, cadence: null, olderThan: null }), - }); - - await waitForPromises(); - - findForm().trigger('submit'); - - expect(mutationResolver).toHaveBeenCalledWith({ - input: { - cadence: 'EVERY_DAY', - enabled: true, - keepN: 'TEN_TAGS', - nameRegex: 'asdasdssssdfdf', - nameRegexKeep: 'sss', - olderThan: 'NINETY_DAYS', - projectPath: 'path', - }, - }); - }); - - it('tracks the submit event', () => { - mountComponentWithApollo({ - mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), - }); - - findForm().trigger('submit'); - - expect(Tracking.event).toHaveBeenCalledWith(undefined, 'submit_form', trackingPayload); - }); - - it('show a success toast when submit succeed', async () => { - mountComponentWithApollo({ - mutationResolver: jest.fn().mockResolvedValue(expirationPolicyMutationPayload()), - }); - - findForm().trigger('submit'); - await waitForPromises(); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE, { - type: 'success', - }); - }); - - describe('when submit fails', () => { - describe('user recoverable errors', () => { - it('when there is an error is shown in a toast', async () => { - mountComponentWithApollo({ - mutationResolver: jest - .fn() - .mockResolvedValue(expirationPolicyMutationPayload({ errors: ['foo'] })), - }); - - findForm().trigger('submit'); - await waitForPromises(); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('foo', { - type: 'error', - }); - }); - }); - - describe('global errors', () => { - it('shows an error', async () => { - mountComponentWithApollo({ - mutationResolver: jest.fn().mockRejectedValue(expirationPolicyMutationPayload()), - }); - - findForm().trigger('submit'); - await waitForPromises(); - await wrapper.vm.$nextTick(); - - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE, { - type: 'error', - }); - }); - - it('parses the error messages', async () => { - const mutate = jest.fn().mockRejectedValue({ - graphQLErrors: [ - { - extensions: { - problems: [{ path: ['nameRegexKeep'], message: 'baz' }], - }, - }, - ], - }); - mountComponent({ mocks: { $apollo: { mutate } } }); - - findForm().trigger('submit'); - await waitForPromises(); - await wrapper.vm.$nextTick(); - - expect(findKeepRegexInput().props('error')).toEqual('baz'); - }); - }); - }); - }); - }); - - describe('form actions', () => { - describe('cancel button', () => { - it('has type reset', () => { - mountComponent(); - - expect(findCancelButton().attributes('type')).toBe('reset'); - }); - - it.each` - isLoading | isEdited | mutationLoading - ${true} | ${true} | ${true} - ${false} | ${true} | ${true} - ${false} | ${false} | ${true} - ${true} | ${false} | ${false} - ${false} | ${false} | ${false} - `( - 'when isLoading is $isLoading, isEdited is $isEdited and mutationLoading is $mutationLoading is disabled', - ({ isEdited, isLoading, mutationLoading }) => { - mountComponent({ - props: { ...defaultProps, isEdited, isLoading }, - data: { mutationLoading }, - }); - - expect(findCancelButton().props('disabled')).toBe(true); - }, - ); - }); - - describe('submit button', () => { - it('has type submit', () => { - mountComponent(); - - expect(findSaveButton().attributes('type')).toBe('submit'); - }); - - it.each` - isLoading | localErrors | mutationLoading - ${true} | ${{}} | ${true} - ${true} | ${{}} | ${false} - ${false} | ${{}} | ${true} - ${false} | ${{ foo: false }} | ${true} - ${true} | ${{ foo: false }} | ${false} - ${false} | ${{ foo: false }} | ${false} - `( - 'when isLoading is $isLoading, localErrors is $localErrors and mutationLoading is $mutationLoading is disabled', - ({ localErrors, isLoading, mutationLoading }) => { - mountComponent({ - props: { ...defaultProps, isLoading }, - data: { mutationLoading, localErrors }, - }); - - expect(findSaveButton().props('disabled')).toBe(true); - }, - ); - - it.each` - isLoading | mutationLoading | showLoading - ${true} | ${true} | ${true} - ${true} | ${false} | ${true} - ${false} | ${true} | ${true} - ${false} | ${false} | ${false} - `( - 'when isLoading is $isLoading and mutationLoading is $mutationLoading is $showLoading that the loading icon is shown', - ({ isLoading, mutationLoading, showLoading }) => { - mountComponent({ - props: { ...defaultProps, isLoading }, - data: { mutationLoading }, - }); - - expect(findSaveButton().props('loading')).toBe(showLoading); - }, - ); - }); - }); -}); diff --git a/spec/frontend/registry/settings/graphql/cache_updated_spec.js b/spec/frontend/registry/settings/graphql/cache_updated_spec.js deleted file mode 100644 index 73655b6917b..00000000000 --- a/spec/frontend/registry/settings/graphql/cache_updated_spec.js +++ /dev/null @@ -1,56 +0,0 @@ -import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql'; -import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update'; - -describe('Registry settings cache update', () => { - let client; - - const payload = { - data: { - updateContainerExpirationPolicy: { - containerExpirationPolicy: { - enabled: true, - }, - }, - }, - }; - - const cacheMock = { - project: { - containerExpirationPolicy: { - enabled: false, - }, - }, - }; - - const queryAndVariables = { - query: expirationPolicyQuery, - variables: { projectPath: 'foo' }, - }; - - beforeEach(() => { - client = { - readQuery: jest.fn().mockReturnValue(cacheMock), - writeQuery: jest.fn(), - }; - }); - describe('Registry settings cache update', () => { - it('calls readQuery', () => { - updateContainerExpirationPolicy('foo')(client, payload); - expect(client.readQuery).toHaveBeenCalledWith(queryAndVariables); - }); - - it('writes the correct result in the cache', () => { - updateContainerExpirationPolicy('foo')(client, payload); - expect(client.writeQuery).toHaveBeenCalledWith({ - ...queryAndVariables, - data: { - project: { - containerExpirationPolicy: { - enabled: true, - }, - }, - }, - }); - }); - }); -}); diff --git a/spec/frontend/registry/settings/mock_data.js b/spec/frontend/registry/settings/mock_data.js deleted file mode 100644 index 9778f409010..00000000000 --- a/spec/frontend/registry/settings/mock_data.js +++ /dev/null @@ -1,40 +0,0 @@ -export const containerExpirationPolicyData = () => ({ - cadence: 'EVERY_DAY', - enabled: true, - keepN: 'TEN_TAGS', - nameRegex: 'asdasdssssdfdf', - nameRegexKeep: 'sss', - olderThan: 'FOURTEEN_DAYS', - nextRunAt: '2020-11-19T07:37:03.941Z', -}); - -export const expirationPolicyPayload = (override) => ({ - data: { - project: { - containerExpirationPolicy: { - ...containerExpirationPolicyData(), - ...override, - }, - }, - }, -}); - -export const emptyExpirationPolicyPayload = () => ({ - data: { - project: { - containerExpirationPolicy: {}, - }, - }, -}); - -export const expirationPolicyMutationPayload = ({ override, errors = [] } = {}) => ({ - data: { - updateContainerExpirationPolicy: { - containerExpirationPolicy: { - ...containerExpirationPolicyData(), - ...override, - }, - errors, - }, - }, -}); diff --git a/spec/frontend/registry/settings/utils_spec.js b/spec/frontend/registry/settings/utils_spec.js deleted file mode 100644 index 7bc627908af..00000000000 --- a/spec/frontend/registry/settings/utils_spec.js +++ /dev/null @@ -1,37 +0,0 @@ -import { - formOptionsGenerator, - optionLabelGenerator, - olderThanTranslationGenerator, -} from '~/registry/settings/utils'; - -describe('Utils', () => { - describe('optionLabelGenerator', () => { - it('returns an array with a set label', () => { - const result = optionLabelGenerator( - [{ variable: 1 }, { variable: 2 }], - olderThanTranslationGenerator, - ); - expect(result).toEqual([ - { variable: 1, label: '1 day' }, - { variable: 2, label: '2 days' }, - ]); - }); - }); - - describe('formOptionsGenerator', () => { - it('returns an object containing olderThan', () => { - expect(formOptionsGenerator().olderThan).toBeDefined(); - expect(formOptionsGenerator().olderThan).toMatchSnapshot(); - }); - - it('returns an object containing cadence', () => { - expect(formOptionsGenerator().cadence).toBeDefined(); - expect(formOptionsGenerator().cadence).toMatchSnapshot(); - }); - - it('returns an object containing keepN', () => { - expect(formOptionsGenerator().keepN).toBeDefined(); - expect(formOptionsGenerator().keepN).toMatchSnapshot(); - }); - }); -}); |