diff options
Diffstat (limited to 'spec/frontend/registry')
26 files changed, 644 insertions, 205 deletions
diff --git a/spec/frontend/registry/explorer/components/delete_button_spec.js b/spec/frontend/registry/explorer/components/delete_button_spec.js index cd43e97009b..a557d9afacc 100644 --- a/spec/frontend/registry/explorer/components/delete_button_spec.js +++ b/spec/frontend/registry/explorer/components/delete_button_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import component from '~/registry/explorer/components/delete_button.vue'; diff --git a/spec/frontend/registry/explorer/components/delete_image_spec.js b/spec/frontend/registry/explorer/components/delete_image_spec.js new file mode 100644 index 00000000000..9a0d070e42b --- /dev/null +++ b/spec/frontend/registry/explorer/components/delete_image_spec.js @@ -0,0 +1,152 @@ +import { shallowMount } from '@vue/test-utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import component from '~/registry/explorer/components/delete_image.vue'; +import { GRAPHQL_PAGE_SIZE } from '~/registry/explorer/constants/index'; +import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql'; +import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; + +describe('Delete Image', () => { + let wrapper; + const id = '1'; + const storeMock = { + readQuery: jest.fn().mockReturnValue({ + containerRepository: { + status: 'foo', + }, + }), + writeQuery: jest.fn(), + }; + + const updatePayload = { + data: { + destroyContainerRepository: { + containerRepository: { + status: 'baz', + }, + }, + }, + }; + + const findButton = () => wrapper.find('button'); + + const mountComponent = ({ + propsData = { id }, + mutate = jest.fn().mockResolvedValue({}), + } = {}) => { + wrapper = shallowMount(component, { + propsData, + mocks: { + $apollo: { + mutate, + }, + }, + scopedSlots: { + default: '<button @click="props.doDelete">test</button>', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('executes apollo mutate on doDelete', () => { + const mutate = jest.fn().mockResolvedValue({}); + mountComponent({ mutate }); + + wrapper.vm.doDelete(); + + expect(mutate).toHaveBeenCalledWith({ + mutation: deleteContainerRepositoryMutation, + variables: { + id, + }, + update: undefined, + }); + }); + + it('on success emits the correct events', async () => { + const mutate = jest.fn().mockResolvedValue({}); + mountComponent({ mutate }); + + wrapper.vm.doDelete(); + + await waitForPromises(); + + expect(wrapper.emitted('start')).toEqual([[]]); + expect(wrapper.emitted('success')).toEqual([[]]); + expect(wrapper.emitted('end')).toEqual([[]]); + }); + + it('when a payload contains an error emits an error event', async () => { + const mutate = jest + .fn() + .mockResolvedValue({ data: { destroyContainerRepository: { errors: ['foo'] } } }); + + mountComponent({ mutate }); + wrapper.vm.doDelete(); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[['foo']]]); + }); + + it('when the api call errors emits an error event', async () => { + const mutate = jest.fn().mockRejectedValue('error'); + + mountComponent({ mutate }); + wrapper.vm.doDelete(); + + await waitForPromises(); + + expect(wrapper.emitted('error')).toEqual([[['error']]]); + }); + + it('uses the update function, when the prop is set to true', () => { + const mutate = jest.fn().mockResolvedValue({}); + + mountComponent({ mutate, propsData: { id, useUpdateFn: true } }); + wrapper.vm.doDelete(); + + expect(mutate).toHaveBeenCalledWith({ + mutation: deleteContainerRepositoryMutation, + variables: { + id, + }, + update: wrapper.vm.updateImageStatus, + }); + }); + + it('updateImage status reads and write to the cache', () => { + mountComponent(); + + const variables = { + id, + first: GRAPHQL_PAGE_SIZE, + }; + + wrapper.vm.updateImageStatus(storeMock, updatePayload); + + expect(storeMock.readQuery).toHaveBeenCalledWith({ + query: getContainerRepositoryDetailsQuery, + variables, + }); + expect(storeMock.writeQuery).toHaveBeenCalledWith({ + query: getContainerRepositoryDetailsQuery, + variables, + data: { + containerRepository: { + status: updatePayload.data.destroyContainerRepository.containerRepository.status, + }, + }, + }); + }); + + it('binds the doDelete function to the default scoped slot', () => { + const mutate = jest.fn().mockResolvedValue({}); + mountComponent({ mutate }); + findButton().trigger('click'); + expect(mutate).toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js index 6a7fbbe367a..c2a2a4e06ea 100644 --- a/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import component from '~/registry/explorer/components/details_page/delete_alert.vue'; import { DELETE_TAG_SUCCESS_MESSAGE, diff --git a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js index 636e0a285a6..8fe659694ba 100644 --- a/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js @@ -1,9 +1,11 @@ -import { shallowMount } from '@vue/test-utils'; import { GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import component from '~/registry/explorer/components/details_page/delete_modal.vue'; import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT, + DELETE_IMAGE_CONFIRMATION_TITLE, + DELETE_IMAGE_CONFIRMATION_TEXT, } from '~/registry/explorer/constants'; import { GlModal } from '../../stubs'; @@ -35,13 +37,13 @@ describe('Delete Modal', () => { describe('events', () => { it.each` - glEvent | localEvent - ${'ok'} | ${'confirmDelete'} - ${'cancel'} | ${'cancelDelete'} + glEvent | localEvent + ${'primary'} | ${'confirmDelete'} + ${'cancel'} | ${'cancelDelete'} `('GlModal $glEvent emits $localEvent', ({ glEvent, localEvent }) => { mountComponent(); findModal().vm.$emit(glEvent); - expect(wrapper.emitted(localEvent)).toBeTruthy(); + expect(wrapper.emitted(localEvent)).toEqual([[]]); }); }); @@ -53,27 +55,51 @@ describe('Delete Modal', () => { }); }); - describe('itemsToBeDeleted contains one element', () => { - beforeEach(() => { - mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] }); - }); - it(`has the correct description`, () => { - expect(findDescription().text()).toBe(REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo')); + describe('when we are deleting images', () => { + it('has the correct title', () => { + mountComponent({ deleteImage: true }); + + expect(wrapper.text()).toContain(DELETE_IMAGE_CONFIRMATION_TITLE); }); - it('has the correct action', () => { - expect(wrapper.text()).toContain('Remove tag'); + + it('has the correct description', () => { + mountComponent({ deleteImage: true }); + + expect(wrapper.text()).toContain(DELETE_IMAGE_CONFIRMATION_TEXT); }); }); - describe('itemsToBeDeleted contains more than element', () => { - beforeEach(() => { - mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] }); - }); - it(`has the correct description`, () => { - expect(findDescription().text()).toBe(REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2')); + describe('when we are deleting tags', () => { + describe('itemsToBeDeleted contains one element', () => { + beforeEach(() => { + mountComponent({ itemsToBeDeleted: [{ path: 'foo' }] }); + }); + + it(`has the correct description`, () => { + expect(findDescription().text()).toBe( + REMOVE_TAG_CONFIRMATION_TEXT.replace('%{item}', 'foo'), + ); + }); + + it('has the correct title', () => { + expect(wrapper.text()).toContain('Remove tag'); + }); }); - it('has the correct action', () => { - expect(wrapper.text()).toContain('Remove tags'); + + describe('itemsToBeDeleted contains more than element', () => { + beforeEach(() => { + mountComponent({ itemsToBeDeleted: [{ path: 'foo' }, { path: 'bar' }] }); + }); + + it(`has the correct description`, () => { + expect(findDescription().text()).toBe( + REMOVE_TAGS_CONFIRMATION_TEXT.replace('%{item}', '2'), + ); + }); + + it('has the correct title', () => { + expect(wrapper.text()).toContain('Remove tags'); + }); }); }); }); 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 337235e3de5..3fa3a2ae1de 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,6 @@ +import { GlSprintf, GlButton } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; -import { GlSprintf } from '@gitlab/ui'; import { useFakeDate } from 'helpers/fake_date'; -import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import component from '~/registry/explorer/components/details_page/details_header.vue'; import { DETAILS_PAGE_TITLE, @@ -15,6 +14,7 @@ import { CLEANUP_ONGOING_TOOLTIP, CLEANUP_UNFINISHED_TOOLTIP, } from '~/registry/explorer/constants'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; describe('Details Header', () => { let wrapper; @@ -23,6 +23,7 @@ describe('Details Header', () => { name: 'foo', updatedAt: '2020-11-03T13:29:21Z', tagsCount: 10, + canDelete: true, project: { visibility: 'public', containerExpirationPolicy: { @@ -36,8 +37,10 @@ describe('Details Header', () => { const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`); const findLastUpdatedAndVisibility = () => findByTestId('updated-and-visibility'); + const findTitle = () => findByTestId('title'); const findTagsCount = () => findByTestId('tags-count'); const findCleanup = () => findByTestId('cleanup'); + const findDeleteButton = () => wrapper.find(GlButton); const waitForMetadataItems = async () => { // Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available @@ -45,11 +48,9 @@ describe('Details Header', () => { await wrapper.vm.$nextTick(); }; - const mountComponent = (image = defaultImage) => { + const mountComponent = (propsData = { image: defaultImage }) => { wrapper = shallowMount(component, { - propsData: { - image, - }, + propsData, stubs: { GlSprintf, TitleArea, @@ -63,13 +64,65 @@ describe('Details Header', () => { }); it('has the correct title ', () => { - mountComponent({ ...defaultImage, name: '' }); - expect(wrapper.text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE); + mountComponent({ image: { ...defaultImage, name: '' } }); + expect(findTitle().text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE); }); it('shows imageName in the title', () => { mountComponent(); - expect(wrapper.text()).toContain('foo'); + expect(findTitle().text()).toContain('foo'); + }); + + describe('delete button', () => { + it('exists', () => { + mountComponent(); + + 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'); + }); + + it('has the correct props', () => { + mountComponent(); + + expect(findDeleteButton().props()).toMatchObject({ + variant: 'danger', + disabled: false, + }); + }); + + it('emits the correct event', () => { + mountComponent(); + + findDeleteButton().vm.$emit('click'); + + expect(wrapper.emitted('delete')).toEqual([[]]); + }); + + it.each` + canDelete | disabled | isDisabled + ${true} | ${false} | ${false} + ${true} | ${true} | ${true} + ${false} | ${false} | ${true} + ${false} | ${true} | ${true} + `( + 'when canDelete is $canDelete and disabled is $disabled is $isDisabled that the button is disabled', + ({ canDelete, disabled, isDisabled }) => { + mountComponent({ image: { ...defaultImage, canDelete }, disabled }); + + expect(findDeleteButton().props('disabled')).toBe(isDisabled); + }, + ); }); describe('metadata items', () => { @@ -82,7 +135,7 @@ describe('Details Header', () => { }); it('when there is one tag has the correct text', async () => { - mountComponent({ ...defaultImage, tagsCount: 1 }); + mountComponent({ image: { ...defaultImage, tagsCount: 1 } }); await waitForMetadataItems(); expect(findTagsCount().props('text')).toBe('1 tag'); @@ -124,10 +177,12 @@ describe('Details Header', () => { 'when the status is $status the text is $text and the tooltip is $tooltip', async ({ status, text, tooltip }) => { mountComponent({ - ...defaultImage, - expirationPolicyCleanupStatus: status, - project: { - containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' }, + image: { + ...defaultImage, + expirationPolicyCleanupStatus: status, + project: { + containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' }, + }, }, }); await waitForMetadataItems(); @@ -156,7 +211,7 @@ describe('Details Header', () => { expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye'); }); it('shows an eye slashed when the project is not public', async () => { - mountComponent({ ...defaultImage, project: { visibility: 'private' } }); + mountComponent({ image: { ...defaultImage, project: { visibility: 'private' } } }); await waitForMetadataItems(); expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash'); diff --git a/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js b/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js new file mode 100644 index 00000000000..14b15945631 --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js @@ -0,0 +1,54 @@ +import { GlEmptyState } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import component from '~/registry/explorer/components/details_page/empty_state.vue'; +import { + NO_TAGS_TITLE, + NO_TAGS_MESSAGE, + MISSING_OR_DELETED_IMAGE_TITLE, + MISSING_OR_DELETED_IMAGE_MESSAGE, +} from '~/registry/explorer/constants'; + +describe('EmptyTagsState component', () => { + let wrapper; + + const findEmptyState = () => wrapper.find(GlEmptyState); + + const mountComponent = (propsData) => { + wrapper = shallowMount(component, { + stubs: { + GlEmptyState, + }, + propsData, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('contains gl-empty-state', () => { + mountComponent(); + expect(findEmptyState().exists()).toBe(true); + }); + + it.each` + isEmptyImage | title | description + ${false} | ${NO_TAGS_TITLE} | ${NO_TAGS_MESSAGE} + ${true} | ${MISSING_OR_DELETED_IMAGE_TITLE} | ${MISSING_OR_DELETED_IMAGE_MESSAGE} + `( + 'when isEmptyImage is $isEmptyImage has the correct props', + ({ isEmptyImage, title, description }) => { + mountComponent({ + noContainersImage: 'foo', + isEmptyImage, + }); + + expect(findEmptyState().props()).toMatchObject({ + title, + description, + svgPath: 'foo', + }); + }, + ); +}); diff --git a/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js b/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js deleted file mode 100644 index 09afd9d2d84..00000000000 --- a/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js +++ /dev/null @@ -1,43 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlEmptyState } from '@gitlab/ui'; -import component from '~/registry/explorer/components/details_page/empty_tags_state.vue'; -import { - EMPTY_IMAGE_REPOSITORY_TITLE, - EMPTY_IMAGE_REPOSITORY_MESSAGE, -} from '~/registry/explorer/constants'; - -describe('EmptyTagsState component', () => { - let wrapper; - - const findEmptyState = () => wrapper.find(GlEmptyState); - - const mountComponent = () => { - wrapper = shallowMount(component, { - stubs: { - GlEmptyState, - }, - propsData: { - noContainersImage: 'foo', - }, - }); - }; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('contains gl-empty-state', () => { - mountComponent(); - expect(findEmptyState().exists()).toBe(true); - }); - - it('has the correct props', () => { - mountComponent(); - expect(findEmptyState().props()).toMatchObject({ - title: EMPTY_IMAGE_REPOSITORY_TITLE, - description: EMPTY_IMAGE_REPOSITORY_MESSAGE, - svgPath: 'foo', - }); - }); -}); diff --git a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js index 17821d8be31..af8a23e412c 100644 --- a/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/partial_cleanup_alert_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; import { GlAlert, GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import component from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue'; import { DELETE_ALERT_TITLE, DELETE_ALERT_LINK_TEXT } from '~/registry/explorer/constants'; diff --git a/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js b/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js new file mode 100644 index 00000000000..b079883cefd --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/status_alert_spec.js @@ -0,0 +1,57 @@ +import { GlLink, GlSprintf, GlAlert } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import component from '~/registry/explorer/components/details_page/status_alert.vue'; +import { + DELETE_SCHEDULED, + DELETE_FAILED, + PACKAGE_DELETE_HELP_PAGE_PATH, + SCHEDULED_FOR_DELETION_STATUS_TITLE, + SCHEDULED_FOR_DELETION_STATUS_MESSAGE, + FAILED_DELETION_STATUS_TITLE, + FAILED_DELETION_STATUS_MESSAGE, +} from '~/registry/explorer/constants'; + +describe('Status Alert', () => { + let wrapper; + + const findLink = () => wrapper.find(GlLink); + const findAlert = () => wrapper.find(GlAlert); + const findMessage = () => wrapper.find('[data-testid="message"]'); + + const mountComponent = (propsData) => { + wrapper = shallowMount(component, { + propsData, + stubs: { + GlSprintf, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it.each` + status | title | variant | message | link + ${DELETE_SCHEDULED} | ${SCHEDULED_FOR_DELETION_STATUS_TITLE} | ${'info'} | ${SCHEDULED_FOR_DELETION_STATUS_MESSAGE} | ${PACKAGE_DELETE_HELP_PAGE_PATH} + ${DELETE_FAILED} | ${FAILED_DELETION_STATUS_TITLE} | ${'warning'} | ${FAILED_DELETION_STATUS_MESSAGE} | ${''} + `( + `when the status is $status, title is $title, variant is $variant, message is $message and the link is $link`, + ({ status, title, variant, message, link }) => { + mountComponent({ status }); + + expect(findMessage().text()).toMatchInterpolatedText(message); + expect(findAlert().props()).toMatchObject({ + title, + variant, + }); + if (link) { + expect(findLink().attributes()).toMatchObject({ + target: '_blank', + href: link, + }); + } + }, + ); +}); 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 c2efc71c159..8b70f84c1bd 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,12 +1,9 @@ -import { shallowMount } from '@vue/test-utils'; import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; -import component from '~/registry/explorer/components/details_page/tags_list_row.vue'; import DeleteButton from '~/registry/explorer/components/delete_button.vue'; -import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; +import component from '~/registry/explorer/components/details_page/tags_list_row.vue'; import { REMOVE_TAG_BUTTON_TITLE, REMOVE_TAG_BUTTON_DISABLE_TOOLTIP, @@ -14,6 +11,9 @@ import { NOT_AVAILABLE_TEXT, NOT_AVAILABLE_SIZE, } from '~/registry/explorer/constants/index'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import DetailsRow from '~/vue_shared/components/registry/details_row.vue'; +import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { tagsMock } from '../../mock_data'; import { ListItem } from '../../stubs'; 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 413795a7a57..dc6760a17bd 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,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; import { GlButton } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import component from '~/registry/explorer/components/details_page/tags_list.vue'; import TagsListRow from '~/registry/explorer/components/details_page/tags_list_row.vue'; import { TAGS_LIST_TITLE, REMOVE_TAGS_BUTTON_TITLE } from '~/registry/explorer/constants/index'; @@ -70,18 +70,25 @@ describe('Tags List', () => { }); }); - it('is disabled when no item is selected', () => { - mountComponent(); + it.each` + disabled | doSelect | buttonDisabled + ${true} | ${false} | ${'true'} + ${true} | ${true} | ${'true'} + ${false} | ${false} | ${'true'} + ${false} | ${true} | ${undefined} + `( + '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 }); - expect(findDeleteButton().attributes('disabled')).toBe('true'); - }); + if (doSelect) { + findTagsListRow().at(0).vm.$emit('select'); + await wrapper.vm.$nextTick(); + } - it('is enabled when at least one item is selected', async () => { - mountComponent(); - findTagsListRow().at(0).vm.$emit('select'); - await wrapper.vm.$nextTick(); - expect(findDeleteButton().attributes('disabled')).toBe(undefined); - }); + expect(findDeleteButton().attributes('disabled')).toBe(buttonDisabled); + }, + ); it('click event emits a deleted event with selected items', () => { mountComponent(); @@ -100,12 +107,13 @@ describe('Tags List', () => { }); it('the correct props are bound to it', () => { - mountComponent(); + mountComponent({ tags, disabled: true }); const rows = findTagsListRow(); expect(rows.at(0).attributes()).toMatchObject({ first: 'true', + disabled: 'true', }); }); diff --git a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js index 74b9ea5fd96..8ca8fca65ed 100644 --- a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js @@ -1,9 +1,7 @@ -import Vuex from 'vuex'; -import { mount, createLocalVue } from '@vue/test-utils'; import { GlDropdown } from '@gitlab/ui'; -import Tracking from '~/tracking'; +import { mount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; import QuickstartDropdown from '~/registry/explorer/components/list_page/cli_commands.vue'; -import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; import { QUICK_START, LOGIN_COMMAND_LABEL, @@ -13,6 +11,8 @@ import { PUSH_COMMAND_LABEL, COPY_PUSH_TITLE, } from '~/registry/explorer/constants'; +import Tracking from '~/tracking'; +import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; import { dockerCommands } from '../../mock_data'; diff --git a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js index 1ba2036dc34..989a60625e2 100644 --- a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js @@ -1,8 +1,8 @@ -import Vuex from 'vuex'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlSprintf } from '@gitlab/ui'; -import { GlEmptyState } from '../../stubs'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; import groupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue'; +import { GlEmptyState } from '../../stubs'; const localVue = createLocalVue(); localVue.use(Vuex); 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 a06c4795b2e..d6ee871341b 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 @@ -1,11 +1,9 @@ -import { shallowMount } from '@vue/test-utils'; import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import Component from '~/registry/explorer/components/list_page/image_list_row.vue'; -import ListItem from '~/vue_shared/components/registry/list_item.vue'; import DeleteButton from '~/registry/explorer/components/delete_button.vue'; +import Component from '~/registry/explorer/components/list_page/image_list_row.vue'; import { ROW_SCHEDULED_FOR_DELETION, LIST_DELETE_BUTTON_DISABLED, @@ -15,8 +13,10 @@ import { IMAGE_DELETE_SCHEDULED_STATUS, IMAGE_FAILED_DELETED_STATUS, } from '~/registry/explorer/constants'; -import { RouterLink } from '../../stubs'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; import { imagesListResponse } from '../../mock_data'; +import { RouterLink } from '../../stubs'; describe('Image List Row', () => { let wrapper; diff --git a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js b/spec/frontend/registry/explorer/components/list_page/image_list_spec.js index 61c362f4d78..d7dd825ca3e 100644 --- a/spec/frontend/registry/explorer/components/list_page/image_list_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/image_list_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; import { GlKeysetPagination } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import Component from '~/registry/explorer/components/list_page/image_list.vue'; import ImageListRow from '~/registry/explorer/components/list_page/image_list_row.vue'; diff --git a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js index 3a27cf1923c..111aa45f231 100644 --- a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js @@ -1,9 +1,9 @@ -import Vuex from 'vuex'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlSprintf } from '@gitlab/ui'; -import { GlEmptyState } from '../../stubs'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import Vuex from 'vuex'; import projectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue'; import { dockerCommands } from '../../mock_data'; +import { GlEmptyState } from '../../stubs'; const localVue = createLocalVue(); localVue.use(Vuex); diff --git a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js index 58439c185e3..07256d2bbf5 100644 --- a/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js @@ -1,13 +1,13 @@ -import { shallowMount } from '@vue/test-utils'; import { GlSprintf } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; import Component from '~/registry/explorer/components/list_page/registry_header.vue'; -import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import { CONTAINER_REGISTRY_TITLE, LIST_INTRO_TEXT, EXPIRATION_POLICY_DISABLED_MESSAGE, EXPIRATION_POLICY_DISABLED_TEXT, } from '~/registry/explorer/constants'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; jest.mock('~/lib/utils/datetime_utility', () => ({ approximateDuration: jest.fn(), diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js index b0fc009872c..f4453912db4 100644 --- a/spec/frontend/registry/explorer/mock_data.js +++ b/spec/frontend/registry/explorer/mock_data.js @@ -235,3 +235,9 @@ export const graphQLProjectImageRepositoriesDetailsMock = { }, }, }; + +export const graphQLEmptyImageDetailsMock = { + data: { + containerRepository: null, + }, +}; diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index 1746a6a63b6..65c58bf9874 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -1,27 +1,35 @@ -import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlKeysetPagination } from '@gitlab/ui'; +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 Tracking from '~/tracking'; -import component from '~/registry/explorer/pages/details.vue'; +import axios from '~/lib/utils/axios_utils'; +import DeleteImage from '~/registry/explorer/components/delete_image.vue'; import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue'; -import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue'; import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue'; -import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue'; +import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue'; +import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue'; +import StatusAlert from '~/registry/explorer/components/details_page/status_alert.vue'; import TagsList from '~/registry/explorer/components/details_page/tags_list.vue'; -import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue'; +import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue'; -import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; +import { + UNFINISHED_STATUS, + DELETE_SCHEDULED, + ALERT_DANGER_IMAGE, +} from '~/registry/explorer/constants'; import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql'; +import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; -import { UNFINISHED_STATUS } from '~/registry/explorer/constants/index'; +import component from '~/registry/explorer/pages/details.vue'; +import Tracking from '~/tracking'; import { graphQLImageDetailsMock, graphQLImageDetailsEmptyTagsMock, graphQLDeleteImageRepositoryTagsMock, containerRepositoryMock, + graphQLEmptyImageDetailsMock, tagsMock, tagsPageInfo, } from '../mock_data'; @@ -39,8 +47,10 @@ describe('Details Page', () => { const findTagsList = () => wrapper.find(TagsList); const findDeleteAlert = () => wrapper.find(DeleteAlert); const findDetailsHeader = () => wrapper.find(DetailsHeader); - const findEmptyTagsState = () => wrapper.find(EmptyTagsState); + const findEmptyState = () => wrapper.find(EmptyTagsState); const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert); + const findStatusAlert = () => wrapper.find(StatusAlert); + const findDeleteImage = () => wrapper.find(DeleteImage); const routeId = 1; @@ -86,6 +96,7 @@ describe('Details Page', () => { apolloProvider, stubs: { DeleteModal, + DeleteImage, }, mocks: { $route: { @@ -133,6 +144,27 @@ describe('Details Page', () => { }); }); + describe('when the image does not exist', () => { + it('does not show the default ui', async () => { + mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) }); + + await waitForApolloRequestRender(); + + expect(findTagsLoader().exists()).toBe(false); + expect(findDetailsHeader().exists()).toBe(false); + expect(findTagsList().exists()).toBe(false); + expect(findPagination().exists()).toBe(false); + }); + + it('shows an empty state message', async () => { + mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) }); + + await waitForApolloRequestRender(); + + expect(findEmptyState().exists()).toBe(true); + }); + }); + describe('when the list of tags is empty', () => { const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock); @@ -141,7 +173,7 @@ describe('Details Page', () => { await waitForApolloRequestRender(); - expect(findEmptyTagsState().exists()).toBe(true); + expect(findEmptyState().exists()).toBe(true); }); it('does not show the loader', async () => { @@ -401,6 +433,9 @@ describe('Details Page', () => { const config = { runCleanupPoliciesHelpPagePath: 'foo', cleanupPoliciesHelpPagePath: 'bar', + userCalloutsPath: 'call_out_path', + userCalloutId: 'call_out_id', + showUnfinishedTagCleanupCallout: true, }; describe(`when expirationPolicyCleanupStatus is ${UNFINISHED_STATUS}`, () => { @@ -413,8 +448,9 @@ describe('Details Page', () => { }), ); }); + it('exists', async () => { - mountComponent({ resolver }); + mountComponent({ resolver, config }); await waitForApolloRequestRender(); @@ -426,11 +462,16 @@ describe('Details Page', () => { await waitForApolloRequestRender(); - expect(findPartialCleanupAlert().props()).toEqual({ ...config }); + expect(findPartialCleanupAlert().props()).toEqual({ + runCleanupPoliciesHelpPagePath: config.runCleanupPoliciesHelpPagePath, + cleanupPoliciesHelpPagePath: config.cleanupPoliciesHelpPagePath, + }); }); it('dismiss hides the component', async () => { - mountComponent({ resolver }); + jest.spyOn(axios, 'post').mockReturnValue(); + + mountComponent({ resolver, config }); await waitForApolloRequestRender(); @@ -440,13 +481,25 @@ describe('Details Page', () => { await wrapper.vm.$nextTick(); + expect(axios.post).toHaveBeenCalledWith(config.userCalloutsPath, { + feature_name: config.userCalloutId, + }); + expect(findPartialCleanupAlert().exists()).toBe(false); + }); + + it('is hidden if the callout is dismissed', async () => { + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + expect(findPartialCleanupAlert().exists()).toBe(false); }); }); describe(`when expirationPolicyCleanupStatus is not ${UNFINISHED_STATUS}`, () => { it('the component is hidden', async () => { - mountComponent(); + mountComponent({ config }); + await waitForApolloRequestRender(); expect(findPartialCleanupAlert().exists()).toBe(false); @@ -463,4 +516,83 @@ describe('Details Page', () => { expect(breadCrumbState.updateName).toHaveBeenCalledWith(containerRepositoryMock.name); }); }); + + describe('when the image has a status different from null', () => { + const resolver = jest + .fn() + .mockResolvedValue(graphQLImageDetailsMock({ status: DELETE_SCHEDULED })); + it('disables all the actions', async () => { + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + + expect(findDetailsHeader().props('disabled')).toBe(true); + expect(findTagsList().props('disabled')).toBe(true); + }); + + it('shows a status alert', async () => { + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + + expect(findStatusAlert().exists()).toBe(true); + expect(findStatusAlert().props()).toMatchObject({ + status: DELETE_SCHEDULED, + }); + }); + }); + + describe('delete the image', () => { + const mountComponentAndDeleteImage = async () => { + mountComponent(); + + await waitForApolloRequestRender(); + findDetailsHeader().vm.$emit('delete'); + + await wrapper.vm.$nextTick(); + }; + + it('on delete event it deletes the image', async () => { + await mountComponentAndDeleteImage(); + + findDeleteModal().vm.$emit('confirmDelete'); + + expect(findDeleteImage().emitted('start')).toEqual([[]]); + }); + + it('binds the correct props to the modal', async () => { + await mountComponentAndDeleteImage(); + + expect(findDeleteModal().props()).toMatchObject({ + itemsToBeDeleted: [{ path: 'gitlab-org/gitlab-test/rails-12009' }], + deleteImage: true, + }); + }); + + it('binds correctly to delete-image start and end events', async () => { + mountComponent(); + + findDeleteImage().vm.$emit('start'); + + await wrapper.vm.$nextTick(); + + expect(findTagsLoader().exists()).toBe(true); + + findDeleteImage().vm.$emit('end'); + + await wrapper.vm.$nextTick(); + + expect(findTagsLoader().exists()).toBe(false); + }); + + it('binds correctly to delete-image error event', async () => { + mountComponent(); + + findDeleteImage().vm.$emit('error'); + + await wrapper.vm.$nextTick(); + + expect(findDeleteAlert().props('deleteAlertType')).toBe(ALERT_DANGER_IMAGE); + }); + }); }); diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js index c4556934934..f7f207cc183 100644 --- a/spec/frontend/registry/explorer/pages/list_spec.js +++ b/spec/frontend/registry/explorer/pages/list_spec.js @@ -1,33 +1,32 @@ +import { GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; -import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; -import Tracking from '~/tracking'; -import component from '~/registry/explorer/pages/list.vue'; +import DeleteImage from '~/registry/explorer/components/delete_image.vue'; import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue'; import GroupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue'; +import ImageList from '~/registry/explorer/components/list_page/image_list.vue'; import ProjectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue'; import RegistryHeader from '~/registry/explorer/components/list_page/registry_header.vue'; -import ImageList from '~/registry/explorer/components/list_page/image_list.vue'; -import TitleArea from '~/vue_shared/components/registry/title_area.vue'; - import { DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE, - IMAGE_REPOSITORY_LIST_LABEL, - SEARCH_PLACEHOLDER_TEXT, + SORT_FIELDS, } from '~/registry/explorer/constants'; - -import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql'; import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql'; +import getContainerRepositoriesDetails from '~/registry/explorer/graphql/queries/get_container_repositories_details.query.graphql'; +import component from '~/registry/explorer/pages/list.vue'; +import Tracking from '~/tracking'; +import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { $toast } from '../../shared/mocks'; import { graphQLImageListMock, graphQLImageDeleteMock, deletedContainerRepository, - graphQLImageDeleteMockError, graphQLEmptyImageListMock, graphQLEmptyGroupImageListMock, pageInfo, @@ -35,7 +34,6 @@ import { dockerCommands, } from '../mock_data'; import { GlModal, GlEmptyState } from '../stubs'; -import { $toast } from '../../shared/mocks'; const localVue = createLocalVue(); @@ -55,9 +53,9 @@ describe('List Page', () => { const findDeleteAlert = () => wrapper.find(GlAlert); const findImageList = () => wrapper.find(ImageList); - const findListHeader = () => wrapper.find('[data-testid="listHeader"]'); - const findSearchBox = () => wrapper.find(GlSearchBoxByClick); + const findRegistrySearch = () => wrapper.find(RegistrySearch); const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]'); + const findDeleteImage = () => wrapper.find(DeleteImage); const waitForApolloRequestRender = async () => { jest.runOnlyPendingTimers(); @@ -91,6 +89,7 @@ describe('List Page', () => { GlSprintf, RegistryHeader, TitleArea, + DeleteImage, }, mocks: { $toast, @@ -227,14 +226,6 @@ describe('List Page', () => { expect(findCliCommands().exists()).toBe(false); }); - - it('list header is not visible', async () => { - mountComponent({ resolver, config }); - - await waitForApolloRequestRender(); - - expect(findListHeader().exists()).toBe(false); - }); }); }); @@ -256,16 +247,6 @@ describe('List Page', () => { expect(findImageList().exists()).toBe(true); }); - it('list header is visible', async () => { - mountComponent(); - - await waitForApolloRequestRender(); - - const header = findListHeader(); - expect(header.exists()).toBe(true); - expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL); - }); - describe('additional metadata', () => { it('is called on component load', async () => { const detailsResolver = jest @@ -300,23 +281,22 @@ describe('List Page', () => { }); describe('delete image', () => { - const deleteImage = async () => { - await wrapper.vm.$nextTick(); + const selectImageForDeletion = async () => { + await waitForApolloRequestRender(); findImageList().vm.$emit('delete', deletedContainerRepository); - findDeleteModal().vm.$emit('ok'); - - await waitForApolloRequestRender(); }; it('should call deleteItem when confirming deletion', async () => { const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock); mountComponent({ mutationResolver }); - await deleteImage(); + await selectImageForDeletion(); + + findDeleteModal().vm.$emit('primary'); + await waitForApolloRequestRender(); expect(wrapper.vm.itemToDelete).toEqual(deletedContainerRepository); - expect(mutationResolver).toHaveBeenCalledWith({ id: deletedContainerRepository.id }); const updatedImage = findImageList() .props('images') @@ -326,10 +306,12 @@ describe('List Page', () => { }); it('should show a success alert when delete request is successful', async () => { - const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock); - mountComponent({ mutationResolver }); + mountComponent(); + + await selectImageForDeletion(); - await deleteImage(); + findDeleteImage().vm.$emit('success'); + await wrapper.vm.$nextTick(); const alert = findDeleteAlert(); expect(alert.exists()).toBe(true); @@ -340,23 +322,12 @@ describe('List Page', () => { describe('when delete request fails it shows an alert', () => { it('user recoverable error', async () => { - const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMockError); - mountComponent({ mutationResolver }); - - await deleteImage(); - - const alert = findDeleteAlert(); - expect(alert.exists()).toBe(true); - expect(alert.text().replace(/\s\s+/gm, ' ')).toBe( - DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path), - ); - }); + mountComponent(); - it('network error', async () => { - const mutationResolver = jest.fn().mockRejectedValue(); - mountComponent({ mutationResolver }); + await selectImageForDeletion(); - await deleteImage(); + findDeleteImage().vm.$emit('error'); + await wrapper.vm.$nextTick(); const alert = findDeleteAlert(); expect(alert.exists()).toBe(true); @@ -368,10 +339,15 @@ describe('List Page', () => { }); }); - describe('search', () => { + describe('search and sorting', () => { const doSearch = async () => { await waitForApolloRequestRender(); - findSearchBox().vm.$emit('submit', 'centos6'); + findRegistrySearch().vm.$emit('filter:changed', [ + { type: 'filtered-search-term', value: { data: 'centos6' } }, + ]); + + findRegistrySearch().vm.$emit('filter:submit'); + await wrapper.vm.$nextTick(); }; @@ -380,9 +356,26 @@ describe('List Page', () => { await waitForApolloRequestRender(); - const searchBox = findSearchBox(); - expect(searchBox.exists()).toBe(true); - expect(searchBox.attributes('placeholder')).toBe(SEARCH_PLACEHOLDER_TEXT); + const registrySearch = findRegistrySearch(); + expect(registrySearch.exists()).toBe(true); + expect(registrySearch.props()).toMatchObject({ + filter: [], + sorting: { orderBy: 'UPDATED', sort: 'desc' }, + sortableFields: SORT_FIELDS, + tokens: [], + }); + }); + + it('performs sorting', async () => { + const resolver = jest.fn().mockResolvedValue(graphQLImageListMock); + mountComponent({ resolver }); + + await waitForApolloRequestRender(); + + findRegistrySearch().vm.$emit('sorting:changed', { sort: 'asc' }); + await wrapper.vm.$nextTick(); + + expect(resolver).toHaveBeenCalledWith(expect.objectContaining({ sort: 'UPDATED_DESC' })); }); it('performs a search', async () => { @@ -499,9 +492,8 @@ describe('List Page', () => { testTrackingCall('cancel_delete'); }); - it('send an event when confirm is clicked on modal', () => { - const deleteModal = findDeleteModal(); - deleteModal.vm.$emit('ok'); + it('send an event when the deletion starts', () => { + findDeleteImage().vm.$emit('start'); testTrackingCall('confirm_delete'); }); }); diff --git a/spec/frontend/registry/settings/components/expiration_input_spec.js b/spec/frontend/registry/settings/components/expiration_input_spec.js index 383158067dc..b91599a2789 100644 --- a/spec/frontend/registry/settings/components/expiration_input_spec.js +++ b/spec/frontend/registry/settings/components/expiration_input_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; 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'; diff --git a/spec/frontend/registry/settings/components/expiration_run_text_spec.js b/spec/frontend/registry/settings/components/expiration_run_text_spec.js index d74ee2d1c18..753bb10ad08 100644 --- a/spec/frontend/registry/settings/components/expiration_run_text_spec.js +++ b/spec/frontend/registry/settings/components/expiration_run_text_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; 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'; diff --git a/spec/frontend/registry/settings/components/expiration_toggle_spec.js b/spec/frontend/registry/settings/components/expiration_toggle_spec.js index ce016e852ee..961bdfdf2c5 100644 --- a/spec/frontend/registry/settings/components/expiration_toggle_spec.js +++ b/spec/frontend/registry/settings/components/expiration_toggle_spec.js @@ -1,5 +1,5 @@ -import { shallowMount } from '@vue/test-utils'; 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 { diff --git a/spec/frontend/registry/settings/components/registry_settings_app_spec.js b/spec/frontend/registry/settings/components/registry_settings_app_spec.js index 32d4f16221d..fd53efa884f 100644 --- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js @@ -1,15 +1,15 @@ +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; -import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; import createMockApollo from 'helpers/mock_apollo_helper'; import component from '~/registry/settings/components/registry_settings_app.vue'; -import expirationPolicyQuery from '~/registry/settings/graphql/queries/get_expiration_policy.query.graphql'; 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, diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js index 626f4fcc9f5..7527910ad59 100644 --- a/spec/frontend/registry/settings/components/settings_form_spec.js +++ b/spec/frontend/registry/settings/components/settings_form_spec.js @@ -2,14 +2,14 @@ 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 Tracking from '~/tracking'; import component from '~/registry/settings/components/settings_form.vue'; -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 { 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'; diff --git a/spec/frontend/registry/settings/graphql/cache_updated_spec.js b/spec/frontend/registry/settings/graphql/cache_updated_spec.js index d88a5576f26..73655b6917b 100644 --- a/spec/frontend/registry/settings/graphql/cache_updated_spec.js +++ b/spec/frontend/registry/settings/graphql/cache_updated_spec.js @@ -1,5 +1,5 @@ -import { updateContainerExpirationPolicy } from '~/registry/settings/graphql/utils/cache_update'; 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; |