diff options
author | Robert Speicher <rspeicher@gmail.com> | 2021-01-20 22:34:23 +0300 |
---|---|---|
committer | Robert Speicher <rspeicher@gmail.com> | 2021-01-20 22:34:23 +0300 |
commit | 6438df3a1e0fb944485cebf07976160184697d72 (patch) | |
tree | 00b09bfd170e77ae9391b1a2f5a93ef6839f2597 /spec/frontend/registry | |
parent | 42bcd54d971da7ef2854b896a7b34f4ef8601067 (diff) |
Add latest changes from gitlab-org/gitlab@13-8-stable-eev13.8.0-rc42
Diffstat (limited to 'spec/frontend/registry')
25 files changed, 381 insertions, 203 deletions
diff --git a/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap index feae2f629b7..4be4fce1abf 100644 --- a/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap +++ b/spec/frontend/registry/explorer/components/__snapshots__/registry_breadcrumb_spec.js.snap @@ -1,28 +1,71 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`Registry Breadcrumb when is rootRoute renders 1`] = ` -<ul> - <li - class="foo bar" +exports[`Registry Breadcrumb when is not rootRoute renders 1`] = ` +<div + class="gl-breadcrumbs" +> + <ol + class="breadcrumb gl-breadcrumb-list" > - baz - </li> - <li - class="foo bar" + + <li + class="breadcrumb-item gl-breadcrumb-item" + > + <a + class="" + href="/" + target="_self" + /> + </li> + + <span + class="gl-breadcrumb-separator" + data-testid="separator" + > + <svg + aria-hidden="true" + class="gl-icon s8" + data-testid="angle-right-icon" + > + <use + href="#angle-right" + /> + </svg> + </span> + <li + class="breadcrumb-item gl-breadcrumb-item" + > + <a + class="" + href="#" + target="_self" + /> + </li> + + <!----> + </ol> +</div> +`; + +exports[`Registry Breadcrumb when is rootRoute renders 1`] = ` +<div + class="gl-breadcrumbs" +> + <ol + class="breadcrumb gl-breadcrumb-list" > - foo - </li> - - <!----> - - <li> - <a - class="foo" + + <li + class="breadcrumb-item gl-breadcrumb-item" > - <a> - - </a> - </a> - </li> -</ul> + <a + class="" + href="/" + target="_self" + /> + </li> + + <!----> + </ol> +</div> `; diff --git a/spec/frontend/registry/explorer/components/delete_button_spec.js b/spec/frontend/registry/explorer/components/delete_button_spec.js index a79ca77a464..cd43e97009b 100644 --- a/spec/frontend/registry/explorer/components/delete_button_spec.js +++ b/spec/frontend/registry/explorer/components/delete_button_spec.js @@ -13,7 +13,7 @@ describe('delete_button', () => { const findButton = () => wrapper.find(GlButton); - const mountComponent = props => { + const mountComponent = (props) => { wrapper = shallowMount(component, { propsData: { ...defaultProps, 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 5d54986978b..6a7fbbe367a 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 @@ -15,7 +15,7 @@ describe('Delete alert', () => { const findAlert = () => wrapper.find(GlAlert); const findLink = () => wrapper.find(GlLink); - const mountComponent = propsData => { + const mountComponent = (propsData) => { wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData }); }; 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 c77f7a54d34..636e0a285a6 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 @@ -13,7 +13,7 @@ describe('Delete Modal', () => { const findModal = () => wrapper.find(GlModal); const findDescription = () => wrapper.find('[data-testid="description"]'); - const mountComponent = propsData => { + const mountComponent = (propsData) => { wrapper = shallowMount(component, { propsData, stubs: { 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 f642c66832b..337235e3de5 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 @@ -3,7 +3,18 @@ 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 } from '~/registry/explorer/constants'; +import { + DETAILS_PAGE_TITLE, + UNSCHEDULED_STATUS, + SCHEDULED_STATUS, + ONGOING_STATUS, + UNFINISHED_STATUS, + CLEANUP_DISABLED_TEXT, + CLEANUP_DISABLED_TOOLTIP, + CLEANUP_SCHEDULED_TOOLTIP, + CLEANUP_ONGOING_TOOLTIP, + CLEANUP_UNFINISHED_TOOLTIP, +} from '~/registry/explorer/constants'; describe('Details Header', () => { let wrapper; @@ -11,15 +22,22 @@ describe('Details Header', () => { const defaultImage = { name: 'foo', updatedAt: '2020-11-03T13:29:21Z', + tagsCount: 10, 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 = () => wrapper.find('[data-testid="updated-and-visibility"]'); + const findLastUpdatedAndVisibility = () => findByTestId('updated-and-visibility'); + const findTagsCount = () => findByTestId('tags-count'); + const findCleanup = () => findByTestId('cleanup'); const waitForMetadataItems = async () => { // Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available @@ -54,25 +72,96 @@ describe('Details Header', () => { expect(wrapper.text()).toContain('foo'); }); - it('has a metadata item with last updated text', async () => { - mountComponent(); - await waitForMetadataItems(); + describe('metadata items', () => { + describe('tags count', () => { + it('when there is more than one tag has the correct text', async () => { + mountComponent(); + await waitForMetadataItems(); - expect(findLastUpdatedAndVisibility().props('text')).toBe('Last updated 1 month ago'); - }); + expect(findTagsCount().props('text')).toBe('10 tags'); + }); + + it('when there is one tag has the correct text', async () => { + mountComponent({ ...defaultImage, tagsCount: 1 }); + 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('visibility icon', () => { - it('shows an eye when the project is public', async () => { - mountComponent(); - await waitForMetadataItems(); + describe('cleanup metadata item', () => { + it('has the correct icon', async () => { + mountComponent(); + await waitForMetadataItems(); - expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye'); + 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({ + ...defaultImage, + expirationPolicyCleanupStatus: status, + project: { + containerExpirationPolicy: { enabled: true, nextRunAt: '2021-01-03T14:29:21Z' }, + }, + }); + await waitForMetadataItems(); + + expect(findCleanup().props()).toMatchObject({ + text, + textTooltip: tooltip, + }); + }, + ); }); - it('shows an eye slashed when the project is not public', async () => { - mountComponent({ ...defaultImage, project: { visibility: 'private' } }); - await waitForMetadataItems(); - expect(findLastUpdatedAndVisibility().props('icon')).toBe('eye-slash'); + 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({ ...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 94944643e8b..c2efc71c159 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 @@ -312,11 +312,7 @@ describe('tags list row', () => { }); it(`is ${clipboard} that clipboard button exist`, () => { - expect( - finderFunction() - .find(ClipboardButton) - .exists(), - ).toBe(clipboard); + expect(finderFunction().find(ClipboardButton).exists()).toBe(clipboard); }); }); }); 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 035b59731c9..413795a7a57 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 @@ -8,7 +8,7 @@ import { tagsMock } from '../../mock_data'; describe('Tags List', () => { let wrapper; const tags = [...tagsMock]; - const readOnlyTags = tags.map(t => ({ ...t, canDelete: false })); + const readOnlyTags = tags.map((t) => ({ ...t, canDelete: false })); const findTagsListRow = () => wrapper.findAll(TagsListRow); const findDeleteButton = () => wrapper.find(GlButton); @@ -78,18 +78,14 @@ describe('Tags List', () => { it('is enabled when at least one item is selected', async () => { mountComponent(); - findTagsListRow() - .at(0) - .vm.$emit('select'); + findTagsListRow().at(0).vm.$emit('select'); await wrapper.vm.$nextTick(); expect(findDeleteButton().attributes('disabled')).toBe(undefined); }); it('click event emits a deleted event with selected items', () => { mountComponent(); - findTagsListRow() - .at(0) - .vm.$emit('select'); + findTagsListRow().at(0).vm.$emit('select'); findDeleteButton().vm.$emit('click'); expect(wrapper.emitted('delete')).toEqual([[{ 'beta-24753': true }]]); @@ -116,22 +112,14 @@ describe('Tags List', () => { describe('events', () => { it('select event update the selected items', async () => { mountComponent(); - findTagsListRow() - .at(0) - .vm.$emit('select'); + findTagsListRow().at(0).vm.$emit('select'); await wrapper.vm.$nextTick(); - expect( - findTagsListRow() - .at(0) - .attributes('selected'), - ).toBe('true'); + expect(findTagsListRow().at(0).attributes('selected')).toBe('true'); }); it('delete event emit a delete event', () => { mountComponent(); - findTagsListRow() - .at(0) - .vm.$emit('delete'); + findTagsListRow().at(0).vm.$emit('delete'); expect(wrapper.emitted('delete')).toEqual([[{ 'beta-24753': true }]]); }); }); diff --git a/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js index b27d3e2c042..40d84d9d4a5 100644 --- a/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js +++ b/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js @@ -32,11 +32,7 @@ describe('TagsLoader component', () => { it('has the correct props', () => { mountComponent(); - expect( - findGlSkeletonLoaders() - .at(0) - .props(), - ).toMatchObject({ + expect(findGlSkeletonLoaders().at(0).props()).toMatchObject({ width: component.loader.width, height: component.loader.height, }); diff --git a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap index bab6b25cc15..46b07b4c2d6 100644 --- a/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap +++ b/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap @@ -56,7 +56,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` </p> <gl-form-input-group-stub - class="gl-mb-4 " + class="gl-mb-4" predefinedoptions="[object Object]" value="" > 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 b9839d92f1d..a06c4795b2e 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,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlIcon, GlSprintf } from '@gitlab/ui'; +import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; @@ -23,12 +23,13 @@ describe('Image List Row', () => { const [item] = imagesListResponse; const findDetailsLink = () => wrapper.find('[data-testid="details-link"]'); - const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]'); + const findTagsCount = () => wrapper.find('[data-testid="tags-count"]'); const findDeleteBtn = () => wrapper.find(DeleteButton); const findClipboardButton = () => wrapper.find(ClipboardButton); const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]'); + const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); - const mountComponent = props => { + const mountComponent = (props) => { wrapper = shallowMount(Component, { stubs: { RouterLink, @@ -164,6 +165,20 @@ describe('Image List Row', () => { expect(icon.props('name')).toBe('tag'); }); + describe('loading state', () => { + it('shows a loader when metadataLoading is true', () => { + mountComponent({ metadataLoading: true }); + + expect(findSkeletonLoader().exists()).toBe(true); + }); + + it('hides the tags count while loading', () => { + mountComponent({ metadataLoading: true }); + + expect(findTagsCount().exists()).toBe(false); + }); + }); + describe('tags count text', () => { it('with one tag in the image', () => { mountComponent({ item: { ...item, tagsCount: 1 } }); 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 54befc9973a..61c362f4d78 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 @@ -11,11 +11,12 @@ describe('Image List', () => { const findRow = () => wrapper.findAll(ImageListRow); const findPagination = () => wrapper.find(GlKeysetPagination); - const mountComponent = (pageInfo = defaultPageInfo) => { + const mountComponent = (props) => { wrapper = shallowMount(Component, { propsData: { images: imagesListResponse, - pageInfo, + pageInfo: defaultPageInfo, + ...props, }, }); }; @@ -35,11 +36,14 @@ describe('Image List', () => { it('when delete event is emitted on the row it emits up a delete event', () => { mountComponent(); - findRow() - .at(0) - .vm.$emit('delete', 'foo'); + findRow().at(0).vm.$emit('delete', 'foo'); expect(wrapper.emitted('delete')).toEqual([['foo']]); }); + + it('passes down the metadataLoading prop', () => { + mountComponent({ metadataLoading: true }); + expect(findRow().at(0).props('metadataLoading')).toBe(true); + }); }); describe('pagination', () => { @@ -57,7 +61,7 @@ describe('Image List', () => { `( 'when hasNextPage is $hasNextPage and hasPreviousPage is $hasPreviousPage: is $isVisible that the component is visible', ({ hasNextPage, hasPreviousPage, isVisible }) => { - mountComponent({ hasNextPage, hasPreviousPage }); + mountComponent({ pageInfo: { ...defaultPageInfo, hasNextPage, hasPreviousPage } }); expect(findPagination().exists()).toBe(isVisible); expect(findPagination().props('hasPreviousPage')).toBe(hasPreviousPage); @@ -66,7 +70,7 @@ describe('Image List', () => { ); it('emits "prev-page" when the user clicks the back page button', () => { - mountComponent({ hasPreviousPage: true }); + mountComponent(); findPagination().vm.$emit('prev'); @@ -74,7 +78,7 @@ describe('Image List', () => { }); it('emits "next-page" when the user clicks the forward page button', () => { - mountComponent({ hasNextPage: true }); + mountComponent(); findPagination().vm.$emit('next'); 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 3c997093d46..58439c185e3 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 @@ -41,9 +41,12 @@ describe('registry_header', () => { describe('header', () => { it('has a title', () => { - mountComponent(); + mountComponent({ metadataLoading: true }); - expect(findTitleArea().props('title')).toBe(CONTAINER_REGISTRY_TITLE); + expect(findTitleArea().props()).toMatchObject({ + title: CONTAINER_REGISTRY_TITLE, + metadataLoading: true, + }); }); it('has a commands slot', () => { diff --git a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js b/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js index fb0b98ba004..487f33594c1 100644 --- a/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js +++ b/spec/frontend/registry/explorer/components/registry_breadcrumb_spec.js @@ -1,4 +1,4 @@ -import { shallowMount } from '@vue/test-utils'; +import { mount } from '@vue/test-utils'; import component from '~/registry/explorer/components/registry_breadcrumb.vue'; @@ -6,45 +6,13 @@ describe('Registry Breadcrumb', () => { let wrapper; const nameGenerator = jest.fn(); - const crumb = { - className: 'foo bar', - tagName: 'div', - innerHTML: 'baz', - querySelector: jest.fn(), - children: [ - { - tagName: 'a', - className: 'foo', - }, - ], - }; - - const querySelectorReturnValue = { - classList: ['js-divider'], - tagName: 'svg', - innerHTML: 'foo', - }; - - const crumbs = [crumb, { ...crumb, innerHTML: 'foo' }, { ...crumb, className: 'baz' }]; - const routes = [ - { name: 'foo', meta: { nameGenerator, root: true } }, - { name: 'baz', meta: { nameGenerator } }, + { name: 'list', path: '/', meta: { nameGenerator, root: true } }, + { name: 'details', path: '/:id', meta: { nameGenerator } }, ]; - const findDivider = () => wrapper.find('.js-divider'); - const findRootRoute = () => wrapper.find({ ref: 'rootRouteLink' }); - const findChildRoute = () => wrapper.find({ ref: 'childRouteLink' }); - const findLastCrumb = () => wrapper.find({ ref: 'lastCrumb' }); - - const mountComponent = $route => { - wrapper = shallowMount(component, { - propsData: { - crumbs, - }, - stubs: { - 'router-link': { name: 'router-link', template: '<a><slot></slot></a>', props: ['to'] }, - }, + const mountComponent = ($route) => { + wrapper = mount(component, { mocks: { $route, $router: { @@ -58,7 +26,6 @@ describe('Registry Breadcrumb', () => { beforeEach(() => { nameGenerator.mockClear(); - crumb.querySelector = jest.fn(); }); afterEach(() => { @@ -75,8 +42,11 @@ describe('Registry Breadcrumb', () => { expect(wrapper.element).toMatchSnapshot(); }); - it('contains a router-link for the child route', () => { - expect(findChildRoute().exists()).toBe(true); + it('contains only a single router-link to list', () => { + const links = wrapper.findAll('a'); + + expect(links).toHaveLength(1); + expect(links.at(0).attributes('href')).toBe('/'); }); it('the link text is calculated by nameGenerator', () => { @@ -86,52 +56,23 @@ describe('Registry Breadcrumb', () => { describe('when is not rootRoute', () => { beforeEach(() => { - crumb.querySelector.mockReturnValue(querySelectorReturnValue); mountComponent(routes[1]); }); - it('renders a divider', () => { - expect(findDivider().exists()).toBe(true); + it('renders', () => { + expect(wrapper.element).toMatchSnapshot(); }); - it('contains a router-link for the root route', () => { - expect(findRootRoute().exists()).toBe(true); - }); + it('contains two router-links to list and details', () => { + const links = wrapper.findAll('a'); - it('contains a router-link for the child route', () => { - expect(findChildRoute().exists()).toBe(true); + expect(links).toHaveLength(2); + expect(links.at(0).attributes('href')).toBe('/'); + expect(links.at(1).attributes('href')).toBe('#'); }); it('the link text is calculated by nameGenerator', () => { expect(nameGenerator).toHaveBeenCalledTimes(2); }); }); - - describe('last crumb', () => { - const lastChildren = crumb.children[0]; - beforeEach(() => { - nameGenerator.mockReturnValue('foo'); - mountComponent(routes[0]); - }); - - it('has the same tag as the last children of the crumbs', () => { - expect(findLastCrumb().element.tagName).toBe(lastChildren.tagName.toUpperCase()); - }); - - it('has the same classes as the last children of the crumbs', () => { - expect( - findLastCrumb() - .classes() - .join(' '), - ).toEqual(lastChildren.className); - }); - - it('has a link to the current route', () => { - expect(findChildRoute().props('to')).toEqual({ to: routes[0].name }); - }); - - it('the link has the correct text', () => { - expect(findChildRoute().text()).toEqual('foo'); - }); - }); }); diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js index 72a9bff8a47..b0fc009872c 100644 --- a/spec/frontend/registry/explorer/mock_data.js +++ b/spec/frontend/registry/explorer/mock_data.js @@ -8,7 +8,6 @@ export const imagesListResponse = [ location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-12009', canDelete: true, createdAt: '2020-11-03T13:29:21Z', - tagsCount: 18, expirationPolicyStartedAt: null, }, { @@ -20,7 +19,6 @@ export const imagesListResponse = [ location: '0.0.0.0:5000/gitlab-org/gitlab-test/rails-20572', canDelete: true, createdAt: '2020-09-21T06:57:43Z', - tagsCount: 1, expirationPolicyStartedAt: null, }, ]; @@ -117,8 +115,13 @@ export const containerRepositoryMock = { updatedAt: '2020-11-03T13:29:21Z', tagsCount: 13, expirationPolicyStartedAt: null, + expirationPolicyCleanupStatus: 'UNSCHEDULED', project: { visibility: 'public', + containerExpirationPolicy: { + enabled: false, + nextRunAt: '2020-11-27T08:59:27Z', + }, __typename: 'Project', }, }; @@ -158,7 +161,7 @@ export const tagsMock = [ }, ]; -export const graphQLImageDetailsMock = override => ({ +export const graphQLImageDetailsMock = (override) => ({ data: { containerRepository: { ...containerRepositoryMock, @@ -209,3 +212,26 @@ export const dockerCommands = { dockerPushCommand: 'barbar', dockerLoginCommand: 'bazbaz', }; + +export const graphQLProjectImageRepositoriesDetailsMock = { + data: { + project: { + containerRepositories: { + nodes: [ + { + id: 'gid://gitlab/ContainerRepository/26', + tagsCount: 4, + __typename: 'ContainerRepository', + }, + { + id: 'gid://gitlab/ContainerRepository/11', + tagsCount: 1, + __typename: 'ContainerRepository', + }, + ], + __typename: 'ContainerRepositoryConnection', + }, + __typename: 'Project', + }, + }, +}; diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js index d307dfe590c..1746a6a63b6 100644 --- a/spec/frontend/registry/explorer/pages/details_spec.js +++ b/spec/frontend/registry/explorer/pages/details_spec.js @@ -1,7 +1,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import { GlKeysetPagination } from '@gitlab/ui'; import VueApollo from 'vue-apollo'; -import createMockApollo from 'jest/helpers/mock_apollo_helper'; +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'; @@ -15,6 +15,8 @@ import EmptyTagsState from '~/registry/explorer/components/details_page/empty_ta import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql'; import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql'; +import { UNFINISHED_STATUS } from '~/registry/explorer/constants/index'; + import { graphQLImageDetailsMock, graphQLImageDetailsEmptyTagsMock, @@ -46,7 +48,7 @@ describe('Details Page', () => { updateName: jest.fn(), }; - const cleanTags = tagsMock.map(t => { + const cleanTags = tagsMock.map((t) => { const result = { ...t }; // eslint-disable-next-line no-underscore-dangle delete result.__typename; @@ -58,7 +60,7 @@ describe('Details Page', () => { await wrapper.vm.$nextTick(); }; - const tagsArrayToSelectedTags = tags => + const tagsArrayToSelectedTags = (tags) => tags.reduce((acc, c) => { acc[c.name] = true; return acc; @@ -334,7 +336,7 @@ describe('Details Page', () => { findDeleteModal().vm.$emit('confirmDelete'); expect(mutationResolver).toHaveBeenCalledWith( - expect.objectContaining({ tagNames: tagsMock.map(t => t.name) }), + expect.objectContaining({ tagNames: tagsMock.map((t) => t.name) }), ); }); }); @@ -353,10 +355,13 @@ describe('Details Page', () => { mountComponent(); await waitForApolloRequestRender(); - expect(findDetailsHeader().props('image')).toMatchObject({ - name: containerRepositoryMock.name, - project: { - visibility: containerRepositoryMock.project.visibility, + expect(findDetailsHeader().props()).toMatchObject({ + metadataLoading: false, + image: { + name: containerRepositoryMock.name, + project: { + visibility: containerRepositoryMock.project.visibility, + }, }, }); }); @@ -398,13 +403,13 @@ describe('Details Page', () => { cleanupPoliciesHelpPagePath: 'bar', }; - describe('when expiration_policy_started is not null', () => { + describe(`when expirationPolicyCleanupStatus is ${UNFINISHED_STATUS}`, () => { let resolver; beforeEach(() => { resolver = jest.fn().mockResolvedValue( graphQLImageDetailsMock({ - expirationPolicyStartedAt: Date.now().toString(), + expirationPolicyCleanupStatus: UNFINISHED_STATUS, }), ); }); @@ -439,7 +444,7 @@ describe('Details Page', () => { }); }); - describe('when expiration_policy_started is null', () => { + describe(`when expirationPolicyCleanupStatus is not ${UNFINISHED_STATUS}`, () => { it('the component is hidden', async () => { mountComponent(); await waitForApolloRequestRender(); diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js index 7d32a667011..c4556934934 100644 --- a/spec/frontend/registry/explorer/pages/list_spec.js +++ b/spec/frontend/registry/explorer/pages/list_spec.js @@ -1,8 +1,9 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui'; -import createMockApollo from 'jest/helpers/mock_apollo_helper'; +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 CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue'; @@ -19,8 +20,7 @@ import { SEARCH_PLACEHOLDER_TEXT, } from '~/registry/explorer/constants'; -import getProjectContainerRepositoriesQuery from '~/registry/explorer/graphql/queries/get_project_container_repositories.query.graphql'; -import getGroupContainerRepositoriesQuery from '~/registry/explorer/graphql/queries/get_group_container_repositories.query.graphql'; +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 { @@ -31,6 +31,8 @@ import { graphQLEmptyImageListMock, graphQLEmptyGroupImageListMock, pageInfo, + graphQLProjectImageRepositoriesDetailsMock, + dockerCommands, } from '../mock_data'; import { GlModal, GlEmptyState } from '../stubs'; import { $toast } from '../../shared/mocks'; @@ -58,6 +60,7 @@ describe('List Page', () => { const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]'); const waitForApolloRequestRender = async () => { + jest.runOnlyPendingTimers(); await waitForPromises(); await wrapper.vm.$nextTick(); }; @@ -65,15 +68,15 @@ describe('List Page', () => { const mountComponent = ({ mocks, resolver = jest.fn().mockResolvedValue(graphQLImageListMock), - groupResolver = jest.fn().mockResolvedValue(graphQLImageListMock), + detailsResolver = jest.fn().mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock), mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock), - config = {}, + config = { isGroupPage: false }, } = {}) => { localVue.use(VueApollo); const requestHandlers = [ - [getProjectContainerRepositoriesQuery, resolver], - [getGroupContainerRepositoriesQuery, groupResolver], + [getContainerRepositoriesQuery, resolver], + [getContainerRepositoriesDetails, detailsResolver], [deleteContainerRepositoryMutation, mutationResolver], ]; @@ -99,6 +102,7 @@ describe('List Page', () => { provide() { return { config, + ...dockerCommands, }; }, }); @@ -116,6 +120,7 @@ describe('List Page', () => { expect(findRegistryHeader().exists()).toBe(true); expect(findRegistryHeader().props()).toMatchObject({ imagesCount: 2, + metadataLoading: false, }); }); @@ -124,6 +129,7 @@ describe('List Page', () => { characterError: true, containersErrorImage: 'foo', helpPagePath: 'bar', + isGroupPage: false, }; it('should show an empty state', () => { @@ -170,6 +176,12 @@ describe('List Page', () => { expect(findCliCommands().exists()).toBe(false); }); + + it('title has the metadataLoading props set to true', () => { + mountComponent(); + + expect(findRegistryHeader().props('metadataLoading')).toBe(true); + }); }); describe('list is empty', () => { @@ -192,15 +204,16 @@ describe('List Page', () => { expect(findProjectEmptyState().exists()).toBe(true); }); }); + describe('group page', () => { - const groupResolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock); + const resolver = jest.fn().mockResolvedValue(graphQLEmptyGroupImageListMock); const config = { isGroupPage: true, }; it('group empty state is visible', async () => { - mountComponent({ groupResolver, config }); + mountComponent({ resolver, config }); await waitForApolloRequestRender(); @@ -208,7 +221,7 @@ describe('List Page', () => { }); it('cli commands is not visible', async () => { - mountComponent({ groupResolver, config }); + mountComponent({ resolver, config }); await waitForApolloRequestRender(); @@ -216,7 +229,7 @@ describe('List Page', () => { }); it('list header is not visible', async () => { - mountComponent({ groupResolver, config }); + mountComponent({ resolver, config }); await waitForApolloRequestRender(); @@ -253,6 +266,39 @@ describe('List Page', () => { expect(header.text()).toBe(IMAGE_REPOSITORY_LIST_LABEL); }); + describe('additional metadata', () => { + it('is called on component load', async () => { + const detailsResolver = jest + .fn() + .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); + mountComponent({ detailsResolver }); + + jest.runOnlyPendingTimers(); + await waitForPromises(); + + expect(detailsResolver).toHaveBeenCalled(); + }); + + it('does not block the list ui to show', async () => { + const detailsResolver = jest.fn().mockRejectedValue(); + mountComponent({ detailsResolver }); + + await waitForApolloRequestRender(); + + expect(findImageList().exists()).toBe(true); + }); + + it('loading state is passed to list component', async () => { + // this is a promise that never resolves, to trick apollo to think that this request is still loading + const detailsResolver = jest.fn().mockImplementation(() => new Promise(() => {})); + + mountComponent({ detailsResolver }); + await waitForApolloRequestRender(); + + expect(findImageList().props('metadataLoading')).toBe(true); + }); + }); + describe('delete image', () => { const deleteImage = async () => { await wrapper.vm.$nextTick(); @@ -274,7 +320,7 @@ describe('List Page', () => { const updatedImage = findImageList() .props('images') - .find(i => i.id === deletedContainerRepository.id); + .find((i) => i.id === deletedContainerRepository.id); expect(updatedImage.status).toBe(deletedContainerRepository.status); }); @@ -350,9 +396,15 @@ describe('List Page', () => { it('when search result is empty displays an empty search message', async () => { const resolver = jest.fn().mockResolvedValue(graphQLImageListMock); - mountComponent({ resolver }); + const detailsResolver = jest + .fn() + .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); + mountComponent({ resolver, detailsResolver }); + + await waitForApolloRequestRender(); resolver.mockResolvedValue(graphQLEmptyImageListMock); + detailsResolver.mockResolvedValue(graphQLEmptyImageListMock); await doSearch(); @@ -363,28 +415,42 @@ describe('List Page', () => { describe('pagination', () => { it('prev-page event triggers a fetchMore request', async () => { const resolver = jest.fn().mockResolvedValue(graphQLImageListMock); - mountComponent({ resolver }); + const detailsResolver = jest + .fn() + .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); + mountComponent({ resolver, detailsResolver }); await waitForApolloRequestRender(); findImageList().vm.$emit('prev-page'); + await wrapper.vm.$nextTick(); expect(resolver).toHaveBeenCalledWith( - expect.objectContaining({ first: null, before: pageInfo.startCursor }), + expect.objectContaining({ before: pageInfo.startCursor }), + ); + expect(detailsResolver).toHaveBeenCalledWith( + expect.objectContaining({ before: pageInfo.startCursor }), ); }); it('next-page event triggers a fetchMore request', async () => { const resolver = jest.fn().mockResolvedValue(graphQLImageListMock); - mountComponent({ resolver }); + const detailsResolver = jest + .fn() + .mockResolvedValue(graphQLProjectImageRepositoriesDetailsMock); + mountComponent({ resolver, detailsResolver }); await waitForApolloRequestRender(); findImageList().vm.$emit('next-page'); + await wrapper.vm.$nextTick(); expect(resolver).toHaveBeenCalledWith( expect.objectContaining({ after: pageInfo.endCursor }), ); + expect(detailsResolver).toHaveBeenCalledWith( + expect.objectContaining({ after: pageInfo.endCursor }), + ); }); }); }); @@ -411,7 +477,7 @@ describe('List Page', () => { mountComponent(); }); - const testTrackingCall = action => { + const testTrackingCall = (action) => { expect(Tracking.event).toHaveBeenCalledWith(undefined, action, { label: 'registry_repository_delete', }); 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 index d7f89ce070e..7a52b4a5d0f 100644 --- a/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap +++ b/spec/frontend/registry/settings/components/__snapshots__/settings_form_spec.js.snap @@ -32,7 +32,7 @@ exports[`Settings Form Keep N matches snapshot 1`] = ` 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}More information%{linkEnd}" + description="Tags with names that match this regex pattern are kept. %{linkStart}View regex examples.%{linkEnd}" error="" label="Keep tags matching:" name="keep-regex" @@ -54,7 +54,7 @@ exports[`Settings Form OlderThan matches snapshot 1`] = ` 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}More information%{linkEnd}" + description="Tags with names that match this regex pattern are removed. %{linkStart}View regex examples.%{linkEnd}" error="" label="Remove tags matching:" name="remove-regex" diff --git a/spec/frontend/registry/settings/components/expiration_dropdown_spec.js b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js index e0cac317ad6..f777f7ec9de 100644 --- a/spec/frontend/registry/settings/components/expiration_dropdown_spec.js +++ b/spec/frontend/registry/settings/components/expiration_dropdown_spec.js @@ -8,14 +8,17 @@ describe('ExpirationDropdown', () => { const defaultProps = { name: 'foo', label: 'label-bar', - formOptions: [{ key: 'foo', label: 'bar' }, { key: 'baz', label: 'zab' }], + 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 => { + const mountComponent = (props) => { wrapper = shallowMount(component, { stubs: { GlFormGroup, diff --git a/spec/frontend/registry/settings/components/expiration_input_spec.js b/spec/frontend/registry/settings/components/expiration_input_spec.js index 849f85aa265..383158067dc 100644 --- a/spec/frontend/registry/settings/components/expiration_input_spec.js +++ b/spec/frontend/registry/settings/components/expiration_input_spec.js @@ -22,7 +22,7 @@ describe('ExpirationInput', () => { const findDescription = () => wrapper.find('[data-testid="description"]'); const findDescriptionLink = () => wrapper.find(GlLink); - const mountComponent = props => { + const mountComponent = (props) => { wrapper = shallowMount(component, { stubs: { GlSprintf, 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 c594b1f449d..d74ee2d1c18 100644 --- a/spec/frontend/registry/settings/components/expiration_run_text_spec.js +++ b/spec/frontend/registry/settings/components/expiration_run_text_spec.js @@ -11,7 +11,7 @@ describe('ExpirationToggle', () => { const findInput = () => wrapper.find(GlFormInput); const findFormGroup = () => wrapper.find(GlFormGroup); - const mountComponent = propsData => { + const mountComponent = (propsData) => { wrapper = shallowMount(component, { stubs: { GlFormGroup, diff --git a/spec/frontend/registry/settings/components/expiration_toggle_spec.js b/spec/frontend/registry/settings/components/expiration_toggle_spec.js index 99ff7a7f77a..ce016e852ee 100644 --- a/spec/frontend/registry/settings/components/expiration_toggle_spec.js +++ b/spec/frontend/registry/settings/components/expiration_toggle_spec.js @@ -13,7 +13,7 @@ describe('ExpirationToggle', () => { const findToggle = () => wrapper.find(GlToggle); const findDescription = () => wrapper.find('[data-testid="description"]'); - const mountComponent = propsData => { + const mountComponent = (propsData) => { wrapper = shallowMount(component, { stubs: { GlFormGroup, 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 c31c7bdf99b..32d4f16221d 100644 --- a/spec/frontend/registry/settings/components/registry_settings_app_spec.js +++ b/spec/frontend/registry/settings/components/registry_settings_app_spec.js @@ -1,7 +1,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; -import createMockApollo from 'jest/helpers/mock_apollo_helper'; +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'; @@ -59,7 +59,7 @@ describe('Registry Settings App', () => { apolloProvider: fakeApollo, }); - return requestHandlers.map(request => request[1]); + return requestHandlers.map((request) => request[1]); }; afterEach(() => { diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js index b89269c0ae4..626f4fcc9f5 100644 --- a/spec/frontend/registry/settings/components/settings_form_spec.js +++ b/spec/frontend/registry/settings/components/settings_form_spec.js @@ -1,6 +1,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; -import createMockApollo from 'jest/helpers/mock_apollo_helper'; +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'; @@ -103,7 +103,7 @@ describe('Settings Form', () => { }, }); - return requestHandlers.map(resolvers => resolvers[1]); + return requestHandlers.map((resolvers) => resolvers[1]); }; beforeEach(() => { diff --git a/spec/frontend/registry/settings/mock_data.js b/spec/frontend/registry/settings/mock_data.js index 7cc645fcf55..9778f409010 100644 --- a/spec/frontend/registry/settings/mock_data.js +++ b/spec/frontend/registry/settings/mock_data.js @@ -8,7 +8,7 @@ export const containerExpirationPolicyData = () => ({ nextRunAt: '2020-11-19T07:37:03.941Z', }); -export const expirationPolicyPayload = override => ({ +export const expirationPolicyPayload = (override) => ({ data: { project: { containerExpirationPolicy: { diff --git a/spec/frontend/registry/settings/utils_spec.js b/spec/frontend/registry/settings/utils_spec.js index f92d51db307..7bc627908af 100644 --- a/spec/frontend/registry/settings/utils_spec.js +++ b/spec/frontend/registry/settings/utils_spec.js @@ -11,7 +11,10 @@ describe('Utils', () => { [{ variable: 1 }, { variable: 2 }], olderThanTranslationGenerator, ); - expect(result).toEqual([{ variable: 1, label: '1 day' }, { variable: 2, label: '2 days' }]); + expect(result).toEqual([ + { variable: 1, label: '1 day' }, + { variable: 2, label: '2 days' }, + ]); }); }); |