diff options
Diffstat (limited to 'spec/frontend/packages_and_registries/shared/components')
9 files changed, 706 insertions, 0 deletions
diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap new file mode 100644 index 00000000000..5f243799bae --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/publish_method_spec.js.snap @@ -0,0 +1,43 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`publish_method renders 1`] = ` +<div + class="gl-display-flex gl-align-items-center" +> + <gl-icon-stub + class="gl-mr-2" + name="git-merge" + size="16" + /> + + <span + class="gl-mr-2" + data-testid="pipeline-ref" + > + branch-name + </span> + + <gl-icon-stub + class="gl-mr-2" + name="commit" + size="16" + /> + + <gl-link-stub + class="gl-mr-2" + data-testid="pipeline-sha" + href="../commit/sha-baz" + > + sha-baz + </gl-link-stub> + + <clipboard-button-stub + category="tertiary" + size="small" + text="sha-baz" + title="Copy commit SHA" + tooltipplacement="top" + variant="default" + /> +</div> +`; diff --git a/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap new file mode 100644 index 00000000000..ceae8eebaef --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/__snapshots__/registry_breadcrumb_spec.js.snap @@ -0,0 +1,96 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Registry Breadcrumb when is not rootRoute renders 1`] = ` +<nav + aria-label="Breadcrumb" + class="gl-breadcrumbs" +> + + <ol + class="breadcrumb gl-breadcrumb-list" + > + <li + class="breadcrumb-item gl-breadcrumb-item" + > + <a + class="" + href="/" + target="_self" + > + <span> + + </span> + + <span + class="gl-breadcrumb-separator" + data-testid="separator" + > + <span + class="gl-mx-n5" + > + <svg + aria-hidden="true" + class="gl-icon s8" + data-testid="angle-right-icon" + role="img" + > + <use + href="#angle-right" + /> + </svg> + </span> + </span> + </a> + </li> + + <!----> + <li + class="breadcrumb-item gl-breadcrumb-item" + > + <a + class="" + href="#" + target="_self" + > + <span> + + </span> + + <!----> + </a> + </li> + + <!----> + </ol> +</nav> +`; + +exports[`Registry Breadcrumb when is rootRoute renders 1`] = ` +<nav + aria-label="Breadcrumb" + class="gl-breadcrumbs" +> + + <ol + class="breadcrumb gl-breadcrumb-list" + > + <li + class="breadcrumb-item gl-breadcrumb-item" + > + <a + class="" + href="/" + target="_self" + > + <span> + + </span> + + <!----> + </a> + </li> + + <!----> + </ol> +</nav> +`; diff --git a/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js new file mode 100644 index 00000000000..d6d1970cb12 --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/package_icon_and_name_spec.js @@ -0,0 +1,32 @@ +import { GlIcon } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import PackageIconAndName from '~/packages_and_registries/shared/components/package_icon_and_name.vue'; + +describe('PackageIconAndName', () => { + let wrapper; + + const findIcon = () => wrapper.find(GlIcon); + + const mountComponent = () => { + wrapper = shallowMount(PackageIconAndName, { + slots: { + default: 'test', + }, + }); + }; + + it('has an icon', () => { + mountComponent(); + + const icon = findIcon(); + + expect(icon.exists()).toBe(true); + expect(icon.props('name')).toBe('package'); + }); + + it('renders the slot content', () => { + mountComponent(); + + expect(wrapper.text()).toBe('test'); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/components/package_path_spec.js b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js new file mode 100644 index 00000000000..93425d4f399 --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/package_path_spec.js @@ -0,0 +1,104 @@ +import { shallowMount } from '@vue/test-utils'; +import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; +import PackagePath from '~/packages_and_registries/shared/components/package_path.vue'; + +describe('PackagePath', () => { + let wrapper; + + const mountComponent = (propsData = { path: 'foo' }) => { + wrapper = shallowMount(PackagePath, { + propsData, + directives: { + GlTooltip: createMockDirective(), + }, + }); + }; + + const BASE_ICON = 'base-icon'; + const ROOT_LINK = 'root-link'; + const ROOT_CHEVRON = 'root-chevron'; + const ELLIPSIS_ICON = 'ellipsis-icon'; + const ELLIPSIS_CHEVRON = 'ellipsis-chevron'; + const LEAF_LINK = 'leaf-link'; + + const findItem = (name) => wrapper.find(`[data-testid="${name}"]`); + const findTooltip = (w) => getBinding(w.element, 'gl-tooltip'); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe.each` + path | rootUrl | shouldExist | shouldNotExist + ${'foo/bar'} | ${'/foo/bar'} | ${[]} | ${[ROOT_CHEVRON, ELLIPSIS_ICON, ELLIPSIS_CHEVRON, LEAF_LINK]} + ${'foo/bar/baz'} | ${'/foo/bar'} | ${[ROOT_CHEVRON, LEAF_LINK]} | ${[ELLIPSIS_ICON, ELLIPSIS_CHEVRON]} + ${'foo/bar/baz/baz2'} | ${'/foo/bar'} | ${[ROOT_CHEVRON, LEAF_LINK, ELLIPSIS_ICON, ELLIPSIS_CHEVRON]} | ${[]} + ${'foo/bar/baz/baz2/bar2'} | ${'/foo/bar'} | ${[ROOT_CHEVRON, LEAF_LINK, ELLIPSIS_ICON, ELLIPSIS_CHEVRON]} | ${[]} + `('given path $path', ({ path, shouldExist, shouldNotExist, rootUrl }) => { + const pathPieces = path.split('/').slice(1); + const hasTooltip = shouldExist.includes(ELLIPSIS_ICON); + + describe('not disabled component', () => { + beforeEach(() => { + mountComponent({ path }); + }); + + it('should have a base icon', () => { + expect(findItem(BASE_ICON).exists()).toBe(true); + }); + + it('should have a root link', () => { + const root = findItem(ROOT_LINK); + expect(root.exists()).toBe(true); + expect(root.attributes('href')).toBe(rootUrl); + }); + + if (hasTooltip) { + it('should have a tooltip', () => { + const tooltip = findTooltip(findItem(ELLIPSIS_ICON)); + expect(tooltip).toBeDefined(); + expect(tooltip.value).toMatchObject({ + title: path, + }); + }); + } + + if (shouldExist.length) { + it.each(shouldExist)(`should have %s`, (element) => { + expect(findItem(element).exists()).toBe(true); + }); + } + + if (shouldNotExist.length) { + it.each(shouldNotExist)(`should not have %s`, (element) => { + expect(findItem(element).exists()).toBe(false); + }); + } + + if (shouldExist.includes(LEAF_LINK)) { + it('the last link should be the last piece of the path', () => { + const leaf = findItem(LEAF_LINK); + expect(leaf.attributes('href')).toBe(`/${path}`); + expect(leaf.text()).toBe(pathPieces[pathPieces.length - 1]); + }); + } + }); + + describe('disabled component', () => { + beforeEach(() => { + mountComponent({ path, disabled: true }); + }); + + it('root link is disabled', () => { + expect(findItem(ROOT_LINK).attributes('disabled')).toBe('true'); + }); + + if (shouldExist.includes(LEAF_LINK)) { + it('the last link is disabled', () => { + expect(findItem(LEAF_LINK).attributes('disabled')).toBe('true'); + }); + } + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js b/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js new file mode 100644 index 00000000000..33e96c0775e --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/package_tags_spec.js @@ -0,0 +1,107 @@ +import { mount } from '@vue/test-utils'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; +import { mockTags } from 'jest/packages_and_registries/infrastructure_registry/components/mock_data'; + +describe('PackageTags', () => { + let wrapper; + + function createComponent(tags = [], props = {}) { + const propsData = { + tags, + ...props, + }; + + wrapper = mount(PackageTags, { + propsData, + }); + } + + const tagLabel = () => wrapper.find('[data-testid="tagLabel"]'); + const tagBadges = () => wrapper.findAll('[data-testid="tagBadge"]'); + const moreBadge = () => wrapper.find('[data-testid="moreBadge"]'); + + afterEach(() => { + if (wrapper) wrapper.destroy(); + }); + + describe('tag label', () => { + it('shows the tag label by default', () => { + createComponent(); + + expect(tagLabel().exists()).toBe(true); + }); + + it('hides when hideLabel prop is set to true', () => { + createComponent(mockTags, { hideLabel: true }); + + expect(tagLabel().exists()).toBe(false); + }); + }); + + it('renders the correct number of tags', () => { + createComponent(mockTags.slice(0, 2)); + + expect(tagBadges()).toHaveLength(2); + expect(moreBadge().exists()).toBe(false); + }); + + it('does not render more than the configured tagDisplayLimit', () => { + createComponent(mockTags); + + expect(tagBadges()).toHaveLength(2); + }); + + it('renders the more tags badge if there are more than the configured limit', () => { + createComponent(mockTags); + + expect(tagBadges()).toHaveLength(2); + expect(moreBadge().exists()).toBe(true); + expect(moreBadge().text()).toContain('2'); + }); + + it('renders the configured tagDisplayLimit when set in props', () => { + createComponent(mockTags, { tagDisplayLimit: 1 }); + + expect(tagBadges()).toHaveLength(1); + expect(moreBadge().exists()).toBe(true); + expect(moreBadge().text()).toContain('3'); + }); + + describe('tagBadgeStyle', () => { + const defaultStyle = ['badge', 'badge-info', 'gl-display-none']; + + it('shows tag badge when there is only one', () => { + createComponent([mockTags[0]]); + + const expectedStyle = [...defaultStyle, 'gl-display-flex', 'gl-ml-3']; + + expect(tagBadges().at(0).classes()).toEqual(expect.arrayContaining(expectedStyle)); + }); + + it('shows tag badge for medium or heigher resolutions', () => { + createComponent(mockTags); + + const expectedStyle = [...defaultStyle, 'd-md-flex']; + + expect(tagBadges().at(1).classes()).toEqual(expect.arrayContaining(expectedStyle)); + }); + + it('correctly prepends left and appends right when there is more than one tag', () => { + createComponent(mockTags, { + tagDisplayLimit: 4, + }); + + const expectedStyleWithoutAppend = [...defaultStyle, 'd-md-flex']; + const expectedStyleWithAppend = [...expectedStyleWithoutAppend, 'gl-mr-2']; + + const allBadges = tagBadges(); + + expect(allBadges.at(0).classes()).toEqual( + expect.arrayContaining([...expectedStyleWithAppend, 'gl-ml-3']), + ); + expect(allBadges.at(1).classes()).toEqual(expect.arrayContaining(expectedStyleWithAppend)); + expect(allBadges.at(2).classes()).toEqual(expect.arrayContaining(expectedStyleWithAppend)); + expect(allBadges.at(3).classes()).toEqual(expect.arrayContaining(expectedStyleWithoutAppend)); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js new file mode 100644 index 00000000000..0005162e0bb --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/packages_list_loader_spec.js @@ -0,0 +1,51 @@ +import { mount } from '@vue/test-utils'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; + +describe('PackagesListLoader', () => { + let wrapper; + + const createComponent = (props = {}) => { + wrapper = mount(PackagesListLoader, { + propsData: { + ...props, + }, + }); + }; + + const findDesktopShapes = () => wrapper.find('[data-testid="desktop-loader"]'); + const findMobileShapes = () => wrapper.find('[data-testid="mobile-loader"]'); + + beforeEach(createComponent); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('desktop loader', () => { + it('produces the right loader', () => { + expect(findDesktopShapes().findAll('rect[width="1000"]')).toHaveLength(20); + }); + + it('has the correct classes', () => { + expect(findDesktopShapes().classes()).toEqual([ + 'gl-display-none', + 'gl-sm-display-flex', + 'gl-flex-direction-column', + ]); + }); + }); + + describe('mobile loader', () => { + it('produces the right loader', () => { + expect(findMobileShapes().findAll('rect[height="170"]')).toHaveLength(5); + }); + + it('has the correct classes', () => { + expect(findMobileShapes().classes()).toEqual([ + 'gl-flex-direction-column', + 'gl-sm-display-none', + ]); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js new file mode 100644 index 00000000000..bd492a5ae8f --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/persisted_search_spec.js @@ -0,0 +1,145 @@ +import { nextTick } from 'vue'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; +import component from '~/packages_and_registries/shared/components/persisted_search.vue'; +import UrlSync from '~/vue_shared/components/url_sync.vue'; +import { useMockLocationHelper } from 'helpers/mock_window_location_helper'; +import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils'; + +jest.mock('~/packages_and_registries/shared/utils'); + +useMockLocationHelper(); + +describe('Persisted Search', () => { + let wrapper; + + const defaultQueryParamsMock = { + filters: ['foo'], + sorting: { sort: 'desc', orderBy: 'test' }, + }; + + const defaultProps = { + sortableFields: [ + { orderBy: 'test', label: 'test' }, + { orderBy: 'foo', label: 'foo' }, + ], + defaultOrder: 'test', + defaultSort: 'asc', + }; + + const findRegistrySearch = () => wrapper.findComponent(RegistrySearch); + const findUrlSync = () => wrapper.findComponent(UrlSync); + + const mountComponent = (propsData = defaultProps) => { + wrapper = shallowMountExtended(component, { + propsData, + stubs: { + UrlSync, + }, + }); + }; + + beforeEach(() => { + extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('has a registry search component', async () => { + mountComponent(); + + await nextTick(); + + expect(findRegistrySearch().exists()).toBe(true); + }); + + it('registry search is mounted after mount', async () => { + mountComponent(); + + expect(findRegistrySearch().exists()).toBe(false); + }); + + it('has a UrlSync component', () => { + mountComponent(); + + expect(findUrlSync().exists()).toBe(true); + }); + + it('on sorting:changed emits update event and update internal sort', async () => { + const payload = { sort: 'desc', orderBy: 'test' }; + + mountComponent(); + + await nextTick(); + + findRegistrySearch().vm.$emit('sorting:changed', payload); + + await nextTick(); + + expect(findRegistrySearch().props('sorting')).toMatchObject(payload); + + // there is always a first call on mounted that emits up default values + expect(wrapper.emitted('update')[1]).toEqual([ + { + filters: ['foo'], + sort: 'TEST_DESC', + }, + ]); + }); + + it('on filter:changed updates the filters', async () => { + const payload = ['foo']; + + mountComponent(); + + await nextTick(); + + findRegistrySearch().vm.$emit('filter:changed', payload); + + await nextTick(); + + expect(findRegistrySearch().props('filter')).toEqual(['foo']); + }); + + it('on filter:submit emits update event', async () => { + mountComponent(); + + await nextTick(); + + findRegistrySearch().vm.$emit('filter:submit'); + + expect(wrapper.emitted('update')[1]).toEqual([ + { + filters: ['foo'], + sort: 'TEST_DESC', + }, + ]); + }); + + it('on query:changed calls updateQuery from UrlSync', async () => { + jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {}); + + mountComponent(); + + await nextTick(); + + findRegistrySearch().vm.$emit('query:changed'); + + expect(UrlSync.methods.updateQuery).toHaveBeenCalled(); + }); + + it('sets the component sorting and filtering based on the querystring', async () => { + mountComponent(); + + await nextTick(); + + expect(getQueryParams).toHaveBeenCalled(); + + expect(findRegistrySearch().props()).toMatchObject({ + filter: defaultQueryParamsMock.filters, + sorting: defaultQueryParamsMock.sorting, + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js new file mode 100644 index 00000000000..fa8f8f7641a --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/publish_method_spec.js @@ -0,0 +1,50 @@ +import { shallowMount } from '@vue/test-utils'; +import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; +import { packageList } from 'jest/packages_and_registries/infrastructure_registry/components/mock_data'; + +describe('publish_method', () => { + let wrapper; + + const [packageWithoutPipeline, packageWithPipeline] = packageList; + + const findPipelineRef = () => wrapper.find('[data-testid="pipeline-ref"]'); + const findPipelineSha = () => wrapper.find('[data-testid="pipeline-sha"]'); + const findManualPublish = () => wrapper.find('[data-testid="manually-published"]'); + + const mountComponent = (packageEntity = {}, isGroup = false) => { + wrapper = shallowMount(PublishMethod, { + propsData: { + packageEntity, + isGroup, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders', () => { + mountComponent(packageWithPipeline); + expect(wrapper.element).toMatchSnapshot(); + }); + + describe('pipeline information', () => { + it('displays branch and commit when pipeline info exists', () => { + mountComponent(packageWithPipeline); + + expect(findPipelineRef().exists()).toBe(true); + expect(findPipelineSha().exists()).toBe(true); + }); + + it('does not show any pipeline details when no information exists', () => { + mountComponent(packageWithoutPipeline); + + expect(findPipelineRef().exists()).toBe(false); + expect(findPipelineSha().exists()).toBe(false); + expect(findManualPublish().exists()).toBe(true); + expect(findManualPublish().text()).toBe('Manually Published'); + }); + }); +}); diff --git a/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js new file mode 100644 index 00000000000..6dfe116c285 --- /dev/null +++ b/spec/frontend/packages_and_registries/shared/components/registry_breadcrumb_spec.js @@ -0,0 +1,78 @@ +import { mount } from '@vue/test-utils'; + +import component from '~/packages_and_registries/shared/components/registry_breadcrumb.vue'; + +describe('Registry Breadcrumb', () => { + let wrapper; + const nameGenerator = jest.fn(); + + const routes = [ + { name: 'list', path: '/', meta: { nameGenerator, root: true } }, + { name: 'details', path: '/:id', meta: { nameGenerator } }, + ]; + + const mountComponent = ($route) => { + wrapper = mount(component, { + mocks: { + $route, + $router: { + options: { + routes, + }, + }, + }, + }); + }; + + beforeEach(() => { + nameGenerator.mockClear(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('when is rootRoute', () => { + beforeEach(() => { + mountComponent(routes[0]); + }); + + it('renders', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + 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', () => { + expect(nameGenerator).toHaveBeenCalledTimes(1); + }); + }); + + describe('when is not rootRoute', () => { + beforeEach(() => { + mountComponent(routes[1]); + }); + + it('renders', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('contains two router-links to list and details', () => { + const links = wrapper.findAll('a'); + + 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); + }); + }); +}); |