diff options
Diffstat (limited to 'spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js')
-rw-r--r-- | spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js | 304 |
1 files changed, 304 insertions, 0 deletions
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js new file mode 100644 index 00000000000..f06300efa29 --- /dev/null +++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/details_header_spec.js @@ -0,0 +1,304 @@ +import { GlDropdownItem, GlIcon } from '@gitlab/ui'; +import { shallowMount, createLocalVue } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; +import { GlDropdown } from 'jest/packages_and_registries/container_registry/explorer/stubs'; +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 '~/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue'; +import { + UNSCHEDULED_STATUS, + SCHEDULED_STATUS, + ONGOING_STATUS, + UNFINISHED_STATUS, + CLEANUP_DISABLED_TEXT, + CLEANUP_DISABLED_TOOLTIP, + CLEANUP_SCHEDULED_TOOLTIP, + CLEANUP_ONGOING_TOOLTIP, + CLEANUP_UNFINISHED_TOOLTIP, + ROOT_IMAGE_TEXT, + ROOT_IMAGE_TOOLTIP, +} from '~/packages_and_registries/container_registry/explorer/constants'; +import getContainerRepositoryTagCountQuery from '~/packages_and_registries/container_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', + canDelete: true, + project: { + visibility: 'public', + containerExpirationPolicy: { + enabled: false, + }, + }, + }; + + // set the date to Dec 4, 2020 + useFakeDate(2020, 11, 4); + 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.findComponent(GlDropdownItem); + const findInfoIcon = () => wrapper.findComponent(GlIcon); + + const waitForMetadataItems = async () => { + // Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available + await wrapper.vm.$nextTick(); + await wrapper.vm.$nextTick(); + }; + + 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, + GlDropdown, + GlDropdownItem, + }, + }); + }; + + 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', () => { + beforeEach(() => { + mountComponent({ propsData: { image: { ...defaultImage, name: '' } } }); + + return waitForPromises(); + }); + + it('root image ', () => { + expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT); + }); + + it('has an icon', () => { + expect(findInfoIcon().exists()).toBe(true); + expect(findInfoIcon().props('name')).toBe('information-o'); + }); + + it('has a tooltip', () => { + const tooltip = getBinding(findInfoIcon().element, 'gl-tooltip'); + expect(tooltip.value).toBe(ROOT_IMAGE_TOOLTIP); + }); + }); + + describe('with image name present', () => { + beforeEach(() => { + mountComponent(); + + return waitForPromises(); + }); + + it('shows image.name ', () => { + expect(findTitle().text()).toContain('foo'); + }); + + it('has no icon', () => { + expect(findInfoIcon().exists()).toBe(false); + }); + }); + }); + + describe('delete button', () => { + it('exists', () => { + mountComponent(); + + expect(findDeleteButton().exists()).toBe(true); + }); + + it('has the correct text', () => { + mountComponent(); + + expect(findDeleteButton().text()).toBe('Delete image repository'); + }); + + it('has the correct props', () => { + mountComponent(); + + expect(findDeleteButton().attributes()).toMatchObject( + expect.objectContaining({ + variant: 'danger', + }), + ); + }); + + it('emits the correct event', () => { + mountComponent(); + + findDeleteButton().vm.$emit('click'); + + expect(wrapper.emitted('delete')).toEqual([[]]); + }); + + it.each` + canDelete | disabled | isDisabled + ${true} | ${false} | ${undefined} + ${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({ propsData: { image: { ...defaultImage, canDelete }, disabled } }); + + expect(findDeleteButton().attributes('disabled')).toBe(isDisabled); + }, + ); + }); + + 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('13 tags'); + }); + + it('when there is one tag has the correct text', async () => { + mountComponent({ + resolver: jest.fn().mockResolvedValue(imageTagsCountMock({ tagsCount: 1 })), + }); + + await waitForPromises(); + await waitForMetadataItems(); + + expect(findTagsCount().props('text')).toBe('1 tag'); + }); + + it('has the correct icon', async () => { + mountComponent(); + await waitForMetadataItems(); + + expect(findTagsCount().props('icon')).toBe('tag'); + }); + }); + + describe('cleanup metadata item', () => { + it('has the correct icon', async () => { + mountComponent(); + await waitForMetadataItems(); + + expect(findCleanup().props('icon')).toBe('expire'); + }); + + it('when the expiration policy is disabled', async () => { + mountComponent(); + await waitForMetadataItems(); + + expect(findCleanup().props()).toMatchObject({ + text: CLEANUP_DISABLED_TEXT, + textTooltip: CLEANUP_DISABLED_TOOLTIP, + }); + }); + + it.each` + status | text | tooltip + ${UNSCHEDULED_STATUS} | ${'Cleanup will run in 1 month'} | ${''} + ${SCHEDULED_STATUS} | ${'Cleanup pending'} | ${CLEANUP_SCHEDULED_TOOLTIP} + ${ONGOING_STATUS} | ${'Cleanup in progress'} | ${CLEANUP_ONGOING_TOOLTIP} + ${UNFINISHED_STATUS} | ${'Cleanup incomplete'} | ${CLEANUP_UNFINISHED_TOOLTIP} + `( + 'when the status is $status the text is $text and the tooltip is $tooltip', + async ({ status, text, tooltip }) => { + mountComponent({ + propsData: { + image: { + ...defaultImage, + expirationPolicyCleanupStatus: status, + project: { + containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' }, + }, + }, + }, + }); + await waitForMetadataItems(); + + expect(findCleanup().props()).toMatchObject({ + text, + textTooltip: tooltip, + }); + }, + ); + }); + + describe('visibility and updated at ', () => { + it('has last updated text', async () => { + mountComponent(); + await waitForMetadataItems(); + + expect(findLastUpdatedAndVisibility().props('text')).toBe('Last updated 1 month ago'); + }); + + describe('visibility icon', () => { + it('shows an eye when the project is public', async () => { + mountComponent(); + await waitForMetadataItems(); + + expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye'); + }); + it('shows an eye slashed when the project is not public', async () => { + mountComponent({ + propsData: { image: { ...defaultImage, project: { visibility: 'private' } } }, + }); + await waitForMetadataItems(); + + expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash'); + }); + }); + }); + }); +}); |