diff options
Diffstat (limited to 'spec/frontend/registry/explorer/components')
17 files changed, 1098 insertions, 213 deletions
diff --git a/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap b/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap new file mode 100644 index 00000000000..aeb49f88770 --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/__snapshots__/tags_loader_spec.js.snap @@ -0,0 +1,63 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TagsLoader component has the correct markup 1`] = ` +<div> + <div + preserve-aspect-ratio="xMinYMax meet" + > + <rect + height="15" + rx="4" + width="15" + x="0" + y="12.5" + /> + + <rect + height="20" + rx="4" + width="250" + x="25" + y="10" + /> + + <circle + cx="290" + cy="20" + r="10" + /> + + <rect + height="20" + rx="4" + width="100" + x="315" + y="10" + /> + + <rect + height="20" + rx="4" + width="100" + x="500" + y="10" + /> + + <rect + height="20" + rx="4" + width="100" + x="630" + y="10" + /> + + <rect + height="40" + rx="4" + width="40" + x="960" + y="0" + /> + </div> +</div> +`; 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 new file mode 100644 index 00000000000..5d54986978b --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/delete_alert_spec.js @@ -0,0 +1,116 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; +import component from '~/registry/explorer/components/details_page/delete_alert.vue'; +import { + DELETE_TAG_SUCCESS_MESSAGE, + DELETE_TAG_ERROR_MESSAGE, + DELETE_TAGS_SUCCESS_MESSAGE, + DELETE_TAGS_ERROR_MESSAGE, + ADMIN_GARBAGE_COLLECTION_TIP, +} from '~/registry/explorer/constants'; + +describe('Delete alert', () => { + let wrapper; + + const findAlert = () => wrapper.find(GlAlert); + const findLink = () => wrapper.find(GlLink); + + const mountComponent = propsData => { + wrapper = shallowMount(component, { stubs: { GlSprintf }, propsData }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when deleteAlertType is null', () => { + it('does not show the alert', () => { + mountComponent(); + expect(findAlert().exists()).toBe(false); + }); + }); + + describe('when deleteAlertType is not null', () => { + describe('success states', () => { + describe.each` + deleteAlertType | message + ${'success_tag'} | ${DELETE_TAG_SUCCESS_MESSAGE} + ${'success_tags'} | ${DELETE_TAGS_SUCCESS_MESSAGE} + `('when deleteAlertType is $deleteAlertType', ({ deleteAlertType, message }) => { + it('alert exists', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + }); + + describe('when the user is an admin', () => { + beforeEach(() => { + mountComponent({ + deleteAlertType, + isAdmin: true, + garbageCollectionHelpPagePath: 'foo', + }); + }); + + it(`alert title is ${message}`, () => { + expect(findAlert().attributes('title')).toBe(message); + }); + + it('alert body contains admin tip', () => { + expect(findAlert().text()).toMatchInterpolatedText(ADMIN_GARBAGE_COLLECTION_TIP); + }); + + it('alert body contains link', () => { + const alertLink = findLink(); + expect(alertLink.exists()).toBe(true); + expect(alertLink.attributes('href')).toBe('foo'); + }); + }); + + describe('when the user is not an admin', () => { + it('alert exist and text is appropriate', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(message); + }); + }); + }); + }); + describe('error states', () => { + describe.each` + deleteAlertType | message + ${'danger_tag'} | ${DELETE_TAG_ERROR_MESSAGE} + ${'danger_tags'} | ${DELETE_TAGS_ERROR_MESSAGE} + `('when deleteAlertType is $deleteAlertType', ({ deleteAlertType, message }) => { + it('alert exists', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + }); + + describe('when the user is an admin', () => { + it('alert exist and text is appropriate', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(message); + }); + }); + + describe('when the user is not an admin', () => { + it('alert exist and text is appropriate', () => { + mountComponent({ deleteAlertType }); + expect(findAlert().exists()).toBe(true); + expect(findAlert().text()).toBe(message); + }); + }); + }); + }); + + describe('dismissing alert', () => { + it('GlAlert dismiss event triggers a change event', () => { + mountComponent({ deleteAlertType: 'success_tags' }); + findAlert().vm.$emit('dismiss'); + expect(wrapper.emitted('change')).toEqual([[null]]); + }); + }); + }); +}); 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 new file mode 100644 index 00000000000..c77f7a54d34 --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/delete_modal_spec.js @@ -0,0 +1,79 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlSprintf } from '@gitlab/ui'; +import component from '~/registry/explorer/components/details_page/delete_modal.vue'; +import { + REMOVE_TAG_CONFIRMATION_TEXT, + REMOVE_TAGS_CONFIRMATION_TEXT, +} from '~/registry/explorer/constants'; +import { GlModal } from '../../stubs'; + +describe('Delete Modal', () => { + let wrapper; + + const findModal = () => wrapper.find(GlModal); + const findDescription = () => wrapper.find('[data-testid="description"]'); + + const mountComponent = propsData => { + wrapper = shallowMount(component, { + propsData, + stubs: { + GlSprintf, + GlModal, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('contains a GlModal', () => { + mountComponent(); + expect(findModal().exists()).toBe(true); + }); + + describe('events', () => { + it.each` + glEvent | localEvent + ${'ok'} | ${'confirmDelete'} + ${'cancel'} | ${'cancelDelete'} + `('GlModal $glEvent emits $localEvent', ({ glEvent, localEvent }) => { + mountComponent(); + findModal().vm.$emit(glEvent); + expect(wrapper.emitted(localEvent)).toBeTruthy(); + }); + }); + + describe('methods', () => { + it('show calls gl-modal show', () => { + mountComponent(); + wrapper.vm.show(); + expect(GlModal.methods.show).toHaveBeenCalled(); + }); + }); + + 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 action', () => { + expect(wrapper.text()).toContain('Remove tag'); + }); + }); + + 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 action', () => { + 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 new file mode 100644 index 00000000000..cb31efa428f --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/details_header_spec.js @@ -0,0 +1,32 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlSprintf } from '@gitlab/ui'; +import component from '~/registry/explorer/components/details_page/details_header.vue'; +import { DETAILS_PAGE_TITLE } from '~/registry/explorer/constants'; + +describe('Details Header', () => { + let wrapper; + + const mountComponent = propsData => { + wrapper = shallowMount(component, { + propsData, + stubs: { + GlSprintf, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('has the correct title ', () => { + mountComponent(); + expect(wrapper.text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE); + }); + + it('shows imageName in the title', () => { + mountComponent({ imageName: 'foo' }); + expect(wrapper.text()).toContain('foo'); + }); +}); diff --git a/spec/frontend/registry/explorer/components/details_page/empty_tags_state.js b/spec/frontend/registry/explorer/components/details_page/empty_tags_state.js new file mode 100644 index 00000000000..da80c75a26a --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/empty_tags_state.js @@ -0,0 +1,43 @@ +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().exist()).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/tags_loader_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js new file mode 100644 index 00000000000..b27d3e2c042 --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/tags_loader_spec.js @@ -0,0 +1,49 @@ +import { shallowMount } from '@vue/test-utils'; +import component from '~/registry/explorer/components/details_page/tags_loader.vue'; +import { GlSkeletonLoader } from '../../stubs'; + +describe('TagsLoader component', () => { + let wrapper; + + const findGlSkeletonLoaders = () => wrapper.findAll(GlSkeletonLoader); + + const mountComponent = () => { + wrapper = shallowMount(component, { + stubs: { + GlSkeletonLoader, + }, + // set the repeat to 1 to avoid a long and verbose snapshot + loader: { + ...component.loader, + repeat: 1, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('produces the correct amount of loaders ', () => { + mountComponent(); + expect(findGlSkeletonLoaders().length).toBe(1); + }); + + it('has the correct props', () => { + mountComponent(); + expect( + findGlSkeletonLoaders() + .at(0) + .props(), + ).toMatchObject({ + width: component.loader.width, + height: component.loader.height, + }); + }); + + it('has the correct markup', () => { + mountComponent(); + expect(wrapper.element).toMatchSnapshot(); + }); +}); diff --git a/spec/frontend/registry/explorer/components/details_page/tags_table_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_table_spec.js new file mode 100644 index 00000000000..a60a362dcfe --- /dev/null +++ b/spec/frontend/registry/explorer/components/details_page/tags_table_spec.js @@ -0,0 +1,286 @@ +import { mount } from '@vue/test-utils'; +import stubChildren from 'helpers/stub_children'; +import component from '~/registry/explorer/components/details_page/tags_table.vue'; +import { tagsListResponse } from '../../mock_data'; + +describe('tags_table', () => { + let wrapper; + const tags = [...tagsListResponse.data]; + + const findMainCheckbox = () => wrapper.find('[data-testid="mainCheckbox"]'); + const findFirstRowItem = testid => wrapper.find(`[data-testid="${testid}"]`); + const findBulkDeleteButton = () => wrapper.find('[data-testid="bulkDeleteButton"]'); + const findAllDeleteButtons = () => wrapper.findAll('[data-testid="singleDeleteButton"]'); + const findAllCheckboxes = () => wrapper.findAll('[data-testid="rowCheckbox"]'); + const findCheckedCheckboxes = () => findAllCheckboxes().filter(c => c.attributes('checked')); + const findFirsTagColumn = () => wrapper.find('.js-tag-column'); + const findFirstTagNameText = () => wrapper.find('[data-testid="rowNameText"]'); + + const findLoaderSlot = () => wrapper.find('[data-testid="loaderSlot"]'); + const findEmptySlot = () => wrapper.find('[data-testid="emptySlot"]'); + + const mountComponent = (propsData = { tags, isDesktop: true }) => { + wrapper = mount(component, { + stubs: { + ...stubChildren(component), + GlTable: false, + }, + propsData, + slots: { + loader: '<div data-testid="loaderSlot"></div>', + empty: '<div data-testid="emptySlot"></div>', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it.each([ + 'rowCheckbox', + 'rowName', + 'rowShortRevision', + 'rowSize', + 'rowTime', + 'singleDeleteButton', + ])('%s exist in the table', element => { + mountComponent(); + + expect(findFirstRowItem(element).exists()).toBe(true); + }); + + describe('header checkbox', () => { + it('exists', () => { + mountComponent(); + expect(findMainCheckbox().exists()).toBe(true); + }); + + it('if selected selects all the rows', () => { + mountComponent(); + findMainCheckbox().vm.$emit('change'); + return wrapper.vm.$nextTick().then(() => { + expect(findMainCheckbox().attributes('checked')).toBeTruthy(); + expect(findCheckedCheckboxes()).toHaveLength(tags.length); + }); + }); + + it('if deselect deselects all the row', () => { + mountComponent(); + findMainCheckbox().vm.$emit('change'); + return wrapper.vm + .$nextTick() + .then(() => { + expect(findMainCheckbox().attributes('checked')).toBeTruthy(); + findMainCheckbox().vm.$emit('change'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findMainCheckbox().attributes('checked')).toBe(undefined); + expect(findCheckedCheckboxes()).toHaveLength(0); + }); + }); + }); + + describe('row checkbox', () => { + beforeEach(() => { + mountComponent(); + }); + + it('selecting and deselecting the checkbox works as intended', () => { + findFirstRowItem('rowCheckbox').vm.$emit('change'); + return wrapper.vm + .$nextTick() + .then(() => { + expect(wrapper.vm.selectedItems).toEqual([tags[0].name]); + expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBeTruthy(); + findFirstRowItem('rowCheckbox').vm.$emit('change'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(wrapper.vm.selectedItems.length).toBe(0); + expect(findFirstRowItem('rowCheckbox').attributes('checked')).toBe(undefined); + }); + }); + }); + + describe('header delete button', () => { + beforeEach(() => { + mountComponent(); + }); + + it('exists', () => { + expect(findBulkDeleteButton().exists()).toBe(true); + }); + + it('is disabled if no item is selected', () => { + expect(findBulkDeleteButton().attributes('disabled')).toBe('true'); + }); + + it('is enabled if at least one item is selected', () => { + expect(findBulkDeleteButton().attributes('disabled')).toBe('true'); + findFirstRowItem('rowCheckbox').vm.$emit('change'); + return wrapper.vm.$nextTick().then(() => { + expect(findBulkDeleteButton().attributes('disabled')).toBeFalsy(); + }); + }); + + describe('on click', () => { + it('when one item is selected', () => { + findFirstRowItem('rowCheckbox').vm.$emit('change'); + findBulkDeleteButton().vm.$emit('click'); + expect(wrapper.emitted('delete')).toEqual([[['centos6']]]); + }); + + it('when multiple items are selected', () => { + findMainCheckbox().vm.$emit('change'); + findBulkDeleteButton().vm.$emit('click'); + + expect(wrapper.emitted('delete')).toEqual([[tags.map(t => t.name)]]); + }); + }); + }); + + describe('row delete button', () => { + beforeEach(() => { + mountComponent(); + }); + + it('exists', () => { + expect( + findAllDeleteButtons() + .at(0) + .exists(), + ).toBe(true); + }); + + it('is disabled if the item has no destroy_path', () => { + expect( + findAllDeleteButtons() + .at(1) + .attributes('disabled'), + ).toBe('true'); + }); + + it('on click', () => { + findAllDeleteButtons() + .at(0) + .vm.$emit('click'); + + expect(wrapper.emitted('delete')).toEqual([[['centos6']]]); + }); + }); + + describe('name cell', () => { + it('tag column has a tooltip with the tag name', () => { + mountComponent(); + expect(findFirstTagNameText().attributes('title')).toBe(tagsListResponse.data[0].name); + }); + + describe('on desktop viewport', () => { + beforeEach(() => { + mountComponent(); + }); + + it('table header has class w-25', () => { + expect(findFirsTagColumn().classes()).toContain('w-25'); + }); + + it('tag column has the mw-m class', () => { + expect(findFirstRowItem('rowName').classes()).toContain('mw-m'); + }); + }); + + describe('on mobile viewport', () => { + beforeEach(() => { + mountComponent({ tags, isDesktop: false }); + }); + + it('table header does not have class w-25', () => { + expect(findFirsTagColumn().classes()).not.toContain('w-25'); + }); + + it('tag column has the gl-justify-content-end class', () => { + expect(findFirstRowItem('rowName').classes()).toContain('gl-justify-content-end'); + }); + }); + }); + + describe('last updated cell', () => { + let timeCell; + + beforeEach(() => { + mountComponent(); + timeCell = findFirstRowItem('rowTime'); + }); + + it('displays the time in string format', () => { + expect(timeCell.text()).toBe('2 years ago'); + }); + + it('has a tooltip timestamp', () => { + expect(timeCell.attributes('title')).toBe('Sep 19, 2017 1:45pm GMT+0000'); + }); + }); + + describe('empty state slot', () => { + describe('when the table is empty', () => { + beforeEach(() => { + mountComponent({ tags: [], isDesktop: true }); + }); + + it('does not show table rows', () => { + expect(findFirstTagNameText().exists()).toBe(false); + }); + + it('has the empty state slot', () => { + expect(findEmptySlot().exists()).toBe(true); + }); + }); + + describe('when the table is not empty', () => { + beforeEach(() => { + mountComponent({ tags, isDesktop: true }); + }); + + it('does show table rows', () => { + expect(findFirstTagNameText().exists()).toBe(true); + }); + + it('does not show the empty state', () => { + expect(findEmptySlot().exists()).toBe(false); + }); + }); + }); + + describe('loader slot', () => { + describe('when the data is loading', () => { + beforeEach(() => { + mountComponent({ isLoading: true, tags }); + }); + + it('show the loader', () => { + expect(findLoaderSlot().exists()).toBe(true); + }); + + it('does not show the table rows', () => { + expect(findFirstTagNameText().exists()).toBe(false); + }); + }); + + describe('when the data is not loading', () => { + beforeEach(() => { + mountComponent({ isLoading: false, tags }); + }); + + it('does not show the loader', () => { + expect(findLoaderSlot().exists()).toBe(false); + }); + + it('shows the table rows', () => { + expect(findFirstTagNameText().exists()).toBe(true); + }); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/components/image_list_spec.js b/spec/frontend/registry/explorer/components/image_list_spec.js deleted file mode 100644 index 12f0fbe0c87..00000000000 --- a/spec/frontend/registry/explorer/components/image_list_spec.js +++ /dev/null @@ -1,74 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import { GlPagination } from '@gitlab/ui'; -import Component from '~/registry/explorer/components/image_list.vue'; -import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; -import { RouterLink } from '../stubs'; -import { imagesListResponse, imagePagination } from '../mock_data'; - -describe('Image List', () => { - let wrapper; - - const firstElement = imagesListResponse.data[0]; - - const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]'); - const findRowItems = () => wrapper.findAll('[data-testid="rowItem"]'); - const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]'); - const findClipboardButton = () => wrapper.find(ClipboardButton); - const findPagination = () => wrapper.find(GlPagination); - - const mountComponent = () => { - wrapper = shallowMount(Component, { - stubs: { - RouterLink, - }, - propsData: { - images: imagesListResponse.data, - pagination: imagePagination, - }, - }); - }; - - beforeEach(() => { - mountComponent(); - }); - - it('contains one list element for each image', () => { - expect(findRowItems().length).toBe(imagesListResponse.data.length); - }); - - it('contains a link to the details page', () => { - const link = findDetailsLink(); - expect(link.html()).toContain(firstElement.path); - expect(link.props('to').name).toBe('details'); - }); - - it('contains a clipboard button', () => { - const button = findClipboardButton(); - expect(button.exists()).toBe(true); - expect(button.props('text')).toBe(firstElement.location); - expect(button.props('title')).toBe(firstElement.location); - }); - - it('should be possible to delete a repo', () => { - const deleteBtn = findDeleteBtn(); - expect(deleteBtn.exists()).toBe(true); - }); - - describe('pagination', () => { - it('exists', () => { - expect(findPagination().exists()).toBe(true); - }); - - it('is wired to the correct pagination props', () => { - const pagination = findPagination(); - expect(pagination.props('perPage')).toBe(imagePagination.perPage); - expect(pagination.props('totalItems')).toBe(imagePagination.total); - expect(pagination.props('value')).toBe(imagePagination.page); - }); - - it('emits a pageChange event when the page change', () => { - wrapper.setData({ currentPage: 2 }); - expect(wrapper.emitted('pageChange')).toEqual([[2]]); - }); - }); -}); diff --git a/spec/frontend/registry/explorer/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap index 3761369c944..3761369c944 100644 --- a/spec/frontend/registry/explorer/components/__snapshots__/group_empty_state_spec.js.snap +++ b/spec/frontend/registry/explorer/components/list_page/__snapshots__/group_empty_state_spec.js.snap diff --git a/spec/frontend/registry/explorer/components/__snapshots__/project_empty_state_spec.js.snap b/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap index 19767aefd1a..d8ec9c3ca4d 100644 --- a/spec/frontend/registry/explorer/components/__snapshots__/project_empty_state_spec.js.snap +++ b/spec/frontend/registry/explorer/components/list_page/__snapshots__/project_empty_state_spec.js.snap @@ -19,7 +19,7 @@ exports[`Registry Project Empty state to match the default snapshot 1`] = ` </p> <h5> - Quick Start + CLI Commands </h5> <p diff --git a/spec/frontend/registry/explorer/components/quickstart_dropdown_spec.js b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js index 0c3baefbc58..a556be12089 100644 --- a/spec/frontend/registry/explorer/components/quickstart_dropdown_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js @@ -3,7 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui'; import Tracking from '~/tracking'; import * as getters from '~/registry/explorer/stores/getters'; -import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue'; +import QuickstartDropdown from '~/registry/explorer/components/list_page/cli_commands.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { @@ -19,7 +19,7 @@ import { const localVue = createLocalVue(); localVue.use(Vuex); -describe('quickstart_dropdown', () => { +describe('cli_commands', () => { let wrapper; let store; diff --git a/spec/frontend/registry/explorer/components/group_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js index 1b4de534317..2f51e875672 100644 --- a/spec/frontend/registry/explorer/components/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 groupEmptyState from '~/registry/explorer/components/group_empty_state.vue'; +import { GlEmptyState } from '../../stubs'; +import groupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue'; 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 new file mode 100644 index 00000000000..78de35ae1dc --- /dev/null +++ b/spec/frontend/registry/explorer/components/list_page/image_list_row_spec.js @@ -0,0 +1,140 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlIcon, GlSprintf } from '@gitlab/ui'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import Component from '~/registry/explorer/components/list_page/image_list_row.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { + ROW_SCHEDULED_FOR_DELETION, + LIST_DELETE_BUTTON_DISABLED, +} from '~/registry/explorer/constants'; +import { RouterLink } from '../../stubs'; +import { imagesListResponse } from '../../mock_data'; + +describe('Image List Row', () => { + let wrapper; + const item = imagesListResponse.data[0]; + const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]'); + const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]'); + const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]'); + const findDeleteButtonWrapper = () => wrapper.find('[data-testid="deleteButtonWrapper"]'); + const findClipboardButton = () => wrapper.find(ClipboardButton); + + const mountComponent = props => { + wrapper = shallowMount(Component, { + stubs: { + RouterLink, + GlSprintf, + }, + propsData: { + item, + ...props, + }, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('main tooltip', () => { + it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => { + mountComponent(); + const tooltip = getBinding(wrapper.element, 'gl-tooltip'); + expect(tooltip).toBeDefined(); + expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION); + }); + + it('is disabled when item is being deleted', () => { + mountComponent({ item: { ...item, deleting: true } }); + const tooltip = getBinding(wrapper.element, 'gl-tooltip'); + expect(tooltip.value.disabled).toBe(false); + }); + }); + + describe('image title and path', () => { + it('contains a link to the details page', () => { + mountComponent(); + const link = findDetailsLink(); + expect(link.html()).toContain(item.path); + expect(link.props('to').name).toBe('details'); + }); + + it('contains a clipboard button', () => { + mountComponent(); + const button = findClipboardButton(); + expect(button.exists()).toBe(true); + expect(button.props('text')).toBe(item.location); + expect(button.props('title')).toBe(item.location); + }); + }); + + describe('delete button wrapper', () => { + it('has a tooltip', () => { + mountComponent(); + const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip'); + expect(tooltip).toBeDefined(); + expect(tooltip.value.title).toBe(LIST_DELETE_BUTTON_DISABLED); + }); + it('tooltip is enabled when destroy_path is falsy', () => { + mountComponent({ item: { ...item, destroy_path: null } }); + const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip'); + expect(tooltip.value.disabled).toBeFalsy(); + }); + }); + + describe('delete button', () => { + it('exists', () => { + mountComponent(); + expect(findDeleteBtn().exists()).toBe(true); + }); + + it('emits a delete event', () => { + mountComponent(); + findDeleteBtn().vm.$emit('click'); + expect(wrapper.emitted('delete')).toEqual([[item]]); + }); + + it.each` + destroy_path | deleting | state + ${null} | ${null} | ${'true'} + ${null} | ${true} | ${'true'} + ${'foo'} | ${true} | ${'true'} + ${'foo'} | ${false} | ${undefined} + `( + 'disabled is $state when destroy_path is $destroy_path and deleting is $deleting', + ({ destroy_path, deleting, state }) => { + mountComponent({ item: { ...item, destroy_path, deleting } }); + expect(findDeleteBtn().attributes('disabled')).toBe(state); + }, + ); + }); + + describe('tags count', () => { + it('exists', () => { + mountComponent(); + expect(findTagsCount().exists()).toBe(true); + }); + + it('contains a tag icon', () => { + mountComponent(); + const icon = findTagsCount().find(GlIcon); + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('tag'); + }); + + describe('tags count text', () => { + it('with one tag in the image', () => { + mountComponent({ item: { ...item, tags_count: 1 } }); + expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag'); + }); + it('with more than one tag in the image', () => { + mountComponent({ item: { ...item, tags_count: 3 } }); + expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags'); + }); + }); + }); +}); 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 new file mode 100644 index 00000000000..03ba6ad7f80 --- /dev/null +++ b/spec/frontend/registry/explorer/components/list_page/image_list_spec.js @@ -0,0 +1,62 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlPagination } from '@gitlab/ui'; +import Component from '~/registry/explorer/components/list_page/image_list.vue'; +import ImageListRow from '~/registry/explorer/components/list_page/image_list_row.vue'; + +import { imagesListResponse, imagePagination } from '../../mock_data'; + +describe('Image List', () => { + let wrapper; + + const findRow = () => wrapper.findAll(ImageListRow); + const findPagination = () => wrapper.find(GlPagination); + + const mountComponent = () => { + wrapper = shallowMount(Component, { + propsData: { + images: imagesListResponse.data, + pagination: imagePagination, + }, + }); + }; + + beforeEach(() => { + mountComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('list', () => { + it('contains one list element for each image', () => { + expect(findRow().length).toBe(imagesListResponse.data.length); + }); + + it('when delete event is emitted on the row it emits up a delete event', () => { + findRow() + .at(0) + .vm.$emit('delete', 'foo'); + expect(wrapper.emitted('delete')).toEqual([['foo']]); + }); + }); + + describe('pagination', () => { + it('exists', () => { + expect(findPagination().exists()).toBe(true); + }); + + it('is wired to the correct pagination props', () => { + const pagination = findPagination(); + expect(pagination.props('perPage')).toBe(imagePagination.perPage); + expect(pagination.props('totalItems')).toBe(imagePagination.total); + expect(pagination.props('value')).toBe(imagePagination.page); + }); + + it('emits a pageChange event when the page change', () => { + findPagination().vm.$emit(GlPagination.model.event, 2); + expect(wrapper.emitted('pageChange')).toEqual([[2]]); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/components/project_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js index 4b209646da9..73746c545cb 100644 --- a/spec/frontend/registry/explorer/components/project_empty_state_spec.js +++ b/spec/frontend/registry/explorer/components/list_page/project_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 projectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; +import { GlEmptyState } from '../../stubs'; +import projectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue'; import * as getters from '~/registry/explorer/stores/getters'; const localVue = createLocalVue(); 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 new file mode 100644 index 00000000000..7484fccbea7 --- /dev/null +++ b/spec/frontend/registry/explorer/components/list_page/registry_header_spec.js @@ -0,0 +1,221 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlSprintf, GlLink } from '@gitlab/ui'; +import Component from '~/registry/explorer/components/list_page/registry_header.vue'; +import { + CONTAINER_REGISTRY_TITLE, + LIST_INTRO_TEXT, + EXPIRATION_POLICY_DISABLED_MESSAGE, + EXPIRATION_POLICY_DISABLED_TEXT, + EXPIRATION_POLICY_WILL_RUN_IN, +} from '~/registry/explorer/constants'; + +jest.mock('~/lib/utils/datetime_utility', () => ({ + approximateDuration: jest.fn(), + calculateRemainingMilliseconds: jest.fn(), +})); + +describe('registry_header', () => { + let wrapper; + + const findHeader = () => wrapper.find('[data-testid="header"]'); + const findTitle = () => wrapper.find('[data-testid="title"]'); + const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]'); + const findInfoArea = () => wrapper.find('[data-testid="info-area"]'); + const findIntroText = () => wrapper.find('[data-testid="default-intro"]'); + const findSubHeader = () => wrapper.find('[data-testid="subheader"]'); + const findImagesCountSubHeader = () => wrapper.find('[data-testid="images-count"]'); + const findExpirationPolicySubHeader = () => wrapper.find('[data-testid="expiration-policy"]'); + const findDisabledExpirationPolicyMessage = () => + wrapper.find('[data-testid="expiration-disabled-message"]'); + + const mountComponent = (propsData, slots) => { + wrapper = shallowMount(Component, { + stubs: { + GlSprintf, + }, + propsData, + slots, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('header', () => { + it('exists', () => { + mountComponent(); + expect(findHeader().exists()).toBe(true); + }); + + it('contains the title of the page', () => { + mountComponent(); + const title = findTitle(); + expect(title.exists()).toBe(true); + expect(title.text()).toBe(CONTAINER_REGISTRY_TITLE); + }); + + it('has a commands slot', () => { + mountComponent(null, { commands: 'baz' }); + expect(findCommandsSlot().text()).toBe('baz'); + }); + }); + + describe('subheader', () => { + describe('when there are no images', () => { + it('is hidden ', () => { + mountComponent(); + expect(findSubHeader().exists()).toBe(false); + }); + }); + + describe('when there are images', () => { + it('is visible', () => { + mountComponent({ imagesCount: 1 }); + expect(findSubHeader().exists()).toBe(true); + }); + + describe('sub header parts', () => { + describe('images count', () => { + it('exists', () => { + mountComponent({ imagesCount: 1 }); + expect(findImagesCountSubHeader().exists()).toBe(true); + }); + + it('when there is one image', () => { + mountComponent({ imagesCount: 1 }); + expect(findImagesCountSubHeader().text()).toMatchInterpolatedText('1 Image repository'); + }); + + it('when there is more than one image', () => { + mountComponent({ imagesCount: 3 }); + expect(findImagesCountSubHeader().text()).toMatchInterpolatedText( + '3 Image repositories', + ); + }); + }); + + describe('expiration policy', () => { + it('when is disabled', () => { + mountComponent({ + expirationPolicy: { enabled: false }, + expirationPolicyHelpPagePath: 'foo', + imagesCount: 1, + }); + const text = findExpirationPolicySubHeader(); + expect(text.exists()).toBe(true); + expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_DISABLED_TEXT); + }); + + it('when is enabled', () => { + mountComponent({ + expirationPolicy: { enabled: true }, + expirationPolicyHelpPagePath: 'foo', + imagesCount: 1, + }); + const text = findExpirationPolicySubHeader(); + expect(text.exists()).toBe(true); + expect(text.text()).toMatchInterpolatedText(EXPIRATION_POLICY_WILL_RUN_IN); + }); + it('when the expiration policy is completely disabled', () => { + mountComponent({ + expirationPolicy: { enabled: true }, + expirationPolicyHelpPagePath: 'foo', + imagesCount: 1, + hideExpirationPolicyData: true, + }); + const text = findExpirationPolicySubHeader(); + expect(text.exists()).toBe(false); + }); + }); + }); + }); + }); + + describe('info area', () => { + it('exists', () => { + mountComponent(); + expect(findInfoArea().exists()).toBe(true); + }); + + describe('default message', () => { + beforeEach(() => { + mountComponent({ helpPagePath: 'bar' }); + }); + + it('exists', () => { + expect(findIntroText().exists()).toBe(true); + }); + + it('has the correct copy', () => { + expect(findIntroText().text()).toMatchInterpolatedText(LIST_INTRO_TEXT); + }); + + it('has the correct link', () => { + expect( + findIntroText() + .find(GlLink) + .attributes('href'), + ).toBe('bar'); + }); + }); + + describe('expiration policy info message', () => { + describe('when there are no images', () => { + it('is hidden', () => { + mountComponent(); + expect(findDisabledExpirationPolicyMessage().exists()).toBe(false); + }); + }); + + describe('when there are images', () => { + describe('when expiration policy is disabled', () => { + beforeEach(() => { + mountComponent({ + expirationPolicy: { enabled: false }, + expirationPolicyHelpPagePath: 'foo', + imagesCount: 1, + }); + }); + it('message exist', () => { + expect(findDisabledExpirationPolicyMessage().exists()).toBe(true); + }); + it('has the correct copy', () => { + expect(findDisabledExpirationPolicyMessage().text()).toMatchInterpolatedText( + EXPIRATION_POLICY_DISABLED_MESSAGE, + ); + }); + + it('has the correct link', () => { + expect( + findDisabledExpirationPolicyMessage() + .find(GlLink) + .attributes('href'), + ).toBe('foo'); + }); + }); + + describe('when expiration policy is enabled', () => { + it('message does not exist', () => { + mountComponent({ + expirationPolicy: { enabled: true }, + imagesCount: 1, + }); + expect(findDisabledExpirationPolicyMessage().exists()).toBe(false); + }); + }); + describe('when the expiration policy is completely disabled', () => { + it('message does not exist', () => { + mountComponent({ + expirationPolicy: { enabled: true }, + imagesCount: 1, + hideExpirationPolicyData: true, + }); + expect(findDisabledExpirationPolicyMessage().exists()).toBe(false); + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/components/project_policy_alert_spec.js b/spec/frontend/registry/explorer/components/project_policy_alert_spec.js deleted file mode 100644 index 89c37e55398..00000000000 --- a/spec/frontend/registry/explorer/components/project_policy_alert_spec.js +++ /dev/null @@ -1,132 +0,0 @@ -import Vuex from 'vuex'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui'; -import * as dateTimeUtils from '~/lib/utils/datetime_utility'; -import component from '~/registry/explorer/components/project_policy_alert.vue'; -import { - EXPIRATION_POLICY_ALERT_TITLE, - EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON, -} from '~/registry/explorer/constants'; - -const localVue = createLocalVue(); -localVue.use(Vuex); - -describe('Project Policy Alert', () => { - let wrapper; - let store; - - const defaultState = { - config: { - expirationPolicy: { - enabled: true, - }, - settingsPath: 'foo', - expirationPolicyHelpPagePath: 'bar', - }, - images: [], - isLoading: false, - }; - - const findAlert = () => wrapper.find(GlAlert); - const findLink = () => wrapper.find(GlLink); - - const createComponent = (state = defaultState) => { - store = new Vuex.Store({ - state, - }); - wrapper = shallowMount(component, { - localVue, - store, - stubs: { - GlSprintf, - }, - }); - }; - - const documentationExpectation = () => { - it('contain a documentation link', () => { - createComponent(); - expect(findLink().attributes('href')).toBe(defaultState.config.expirationPolicyHelpPagePath); - expect(findLink().text()).toBe('documentation'); - }); - }; - - beforeEach(() => { - jest.spyOn(dateTimeUtils, 'approximateDuration').mockReturnValue('1 day'); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('is hidden', () => { - it('when expiration policy does not exist', () => { - createComponent({ config: {} }); - expect(findAlert().exists()).toBe(false); - }); - - it('when expiration policy exist but is disabled', () => { - createComponent({ - ...defaultState, - config: { - expirationPolicy: { - enabled: false, - }, - }, - }); - expect(findAlert().exists()).toBe(false); - }); - }); - - describe('is visible', () => { - it('when expiration policy exists and is enabled', () => { - createComponent(); - expect(findAlert().exists()).toBe(true); - }); - }); - - describe('full info alert', () => { - beforeEach(() => { - createComponent({ ...defaultState, images: [1] }); - }); - - it('has a primary button', () => { - const alert = findAlert(); - expect(alert.props('primaryButtonText')).toBe(EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON); - expect(alert.props('primaryButtonLink')).toBe(defaultState.config.settingsPath); - }); - - it('has a title', () => { - const alert = findAlert(); - expect(alert.props('title')).toBe(EXPIRATION_POLICY_ALERT_TITLE); - }); - - it('has the full message', () => { - expect(findAlert().html()).toContain('<strong>1 day</strong>'); - }); - - documentationExpectation(); - }); - - describe('compact info alert', () => { - beforeEach(() => { - createComponent({ ...defaultState, images: [] }); - }); - - it('does not have a button', () => { - const alert = findAlert(); - expect(alert.props('primaryButtonText')).toBe(null); - }); - - it('does not have a title', () => { - const alert = findAlert(); - expect(alert.props('title')).toBe(null); - }); - - it('has the short message', () => { - expect(findAlert().html()).not.toContain('<strong>1 day</strong>'); - }); - - documentationExpectation(); - }); -}); |