diff options
Diffstat (limited to 'spec/frontend/ci/catalog')
11 files changed, 413 insertions, 239 deletions
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js index 382f8e46203..330163e9f39 100644 --- a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js +++ b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js @@ -2,7 +2,6 @@ import Vue from 'vue'; import VueApollo from 'vue-apollo'; import { GlEmptyState, GlLoadingIcon } from '@gitlab/ui'; import { mountExtended } from 'helpers/vue_test_utils_helper'; -import { resolvers } from '~/ci/catalog/graphql/settings'; import CiResourceComponents from '~/ci/catalog/components/details/ci_resource_components.vue'; import getCiCatalogcomponentComponents from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql'; import createMockApollo from 'helpers/mock_apollo_helper'; @@ -17,15 +16,15 @@ describe('CiResourceComponents', () => { let wrapper; let mockComponentsResponse; - const components = mockComponents.data.ciCatalogResource.components.nodes; + const components = mockComponents.data.ciCatalogResource.latestVersion.components.nodes; - const resourceId = 'gid://gitlab/Ci::Catalog::Resource/1'; + const resourcePath = 'twitter/project-1'; - const defaultProps = { resourceId }; + const defaultProps = { resourcePath }; const createComponent = async () => { const handlers = [[getCiCatalogcomponentComponents, mockComponentsResponse]]; - const mockApollo = createMockApollo(handlers, resolvers); + const mockApollo = createMockApollo(handlers); wrapper = mountExtended(CiResourceComponents, { propsData: { @@ -113,10 +112,9 @@ describe('CiResourceComponents', () => { expect(findComponents()).toHaveLength(components.length); }); - it('renders the component name, description and snippet', () => { + it('renders the component name and snippet', () => { components.forEach((component) => { expect(wrapper.text()).toContain(component.name); - expect(wrapper.text()).toContain(component.description); expect(wrapper.text()).toContain(component.path); }); }); @@ -134,9 +132,9 @@ describe('CiResourceComponents', () => { it('renders the component parameter attributes', () => { const [firstComponent] = components; - firstComponent.inputs.nodes.forEach((input) => { + firstComponent.inputs.forEach((input) => { expect(findComponents().at(0).text()).toContain(input.name); - expect(findComponents().at(0).text()).toContain(input.defaultValue); + expect(findComponents().at(0).text()).toContain(input.default); expect(findComponents().at(0).text()).toContain('Yes'); }); }); diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js index 1f7dcf9d4e5..e4b6c1cd046 100644 --- a/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js +++ b/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js @@ -8,7 +8,7 @@ describe('CiResourceDetails', () => { let wrapper; const defaultProps = { - resourceId: 'gid://gitlab/Ci::Catalog::Resource/1', + resourcePath: 'twitter/project-1', }; const defaultProvide = { glFeatures: { ciCatalogComponentsTab: true }, diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js index c061332ba13..6af9daabea0 100644 --- a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js +++ b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js @@ -3,7 +3,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue'; -import CiIcon from '~/vue_shared/components/ci_icon.vue'; +import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue'; import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock'; describe('CiResourceHeader', () => { @@ -45,9 +45,9 @@ describe('CiResourceHeader', () => { expect(wrapper.html()).toContain(resource.description); }); - it('renders the namespace and project path', () => { - expect(wrapper.html()).toContain(resource.rootNamespace.fullPath); - expect(wrapper.html()).toContain(resource.rootNamespace.name); + it('renders the project path and name', () => { + expect(wrapper.html()).toContain(resource.webPath); + expect(wrapper.html()).toContain(resource.name); }); it('renders the avatar', () => { diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js index 0dadac236a8..ad76b47db57 100644 --- a/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js +++ b/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js @@ -23,12 +23,13 @@ describe('CiResourceReadme', () => { data: { ciCatalogResource: { id: resourceId, + webPath: 'twitter/project-1', readmeHtml, }, }, }; - const defaultProps = { resourceId }; + const defaultProps = { resourcePath: readmeMockData.data.ciCatalogResource.webPath }; const createComponent = ({ props = {} } = {}) => { const handlers = [[getCiCatalogResourceReadme, mockReadmeResponse]]; diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js index 2a5c24d0515..e9d2e68c1a3 100644 --- a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js +++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js @@ -1,6 +1,7 @@ import { GlBanner, GlButton } from '@gitlab/ui'; import { useLocalStorageSpy } from 'helpers/local_storage_helper'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue'; import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue'; import { CATALOG_FEEDBACK_DISMISSED_KEY } from '~/ci/catalog/constants'; @@ -16,9 +17,10 @@ describe('CatalogHeader', () => { }; const findBanner = () => wrapper.findComponent(GlBanner); + const findBetaBadge = () => wrapper.findComponent(BetaBadge); const findFeedbackButton = () => findBanner().findComponent(GlButton); const findTitle = () => wrapper.find('h1'); - const findDescription = () => wrapper.findByTestId('description'); + const findDescription = () => wrapper.findByTestId('page-description'); const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => { wrapper = shallowMountExtended(CatalogHeader, { @@ -33,6 +35,16 @@ describe('CatalogHeader', () => { }); }; + describe('Default view', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders a Beta Badge', () => { + expect(findBetaBadge().exists()).toBe(true); + }); + }); + describe('title and description', () => { describe('when there are no values provided', () => { beforeEach(() => { @@ -42,10 +54,11 @@ describe('CatalogHeader', () => { it('renders the default values', () => { expect(findTitle().text()).toBe('CI/CD Catalog'); expect(findDescription().text()).toBe( - 'Discover CI configuration resources for a seamless CI/CD experience.', + 'Discover CI/CD components that can improve your pipeline with additional functionality.', ); }); }); + describe('when custom values are provided', () => { beforeEach(() => { createComponent({ provide: customProvide }); @@ -57,6 +70,7 @@ describe('CatalogHeader', () => { }); }); }); + describe('Feedback banner', () => { describe('when user has never dismissed', () => { beforeEach(() => { diff --git a/spec/frontend/ci/catalog/components/list/catalog_search_spec.js b/spec/frontend/ci/catalog/components/list/catalog_search_spec.js new file mode 100644 index 00000000000..c6f8498f2fd --- /dev/null +++ b/spec/frontend/ci/catalog/components/list/catalog_search_spec.js @@ -0,0 +1,103 @@ +import { GlSearchBoxByClick, GlSorting, GlSortingItem } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import CatalogSearch from '~/ci/catalog/components/list/catalog_search.vue'; +import { SORT_ASC, SORT_DESC, SORT_OPTION_CREATED } from '~/ci/catalog/constants'; + +describe('CatalogSearch', () => { + let wrapper; + + const findSearchBar = () => wrapper.findComponent(GlSearchBoxByClick); + const findSorting = () => wrapper.findComponent(GlSorting); + const findAllSortingItems = () => wrapper.findAllComponents(GlSortingItem); + + const createComponent = () => { + wrapper = shallowMountExtended(CatalogSearch, {}); + }; + + beforeEach(() => { + createComponent(); + }); + + describe('default UI', () => { + it('renders the search bar', () => { + expect(findSearchBar().exists()).toBe(true); + }); + + it('renders the sorting options', () => { + expect(findSorting().exists()).toBe(true); + expect(findAllSortingItems()).toHaveLength(1); + }); + + it('renders the `Created at` option as the default', () => { + expect(findAllSortingItems().at(0).text()).toBe('Created at'); + }); + }); + + describe('search', () => { + it('passes down the search value to the search component', async () => { + const newSearchTerm = 'cat'; + + expect(findSearchBar().props().value).toBe(''); + + await findSearchBar().vm.$emit('input', newSearchTerm); + + expect(findSearchBar().props().value).toBe(newSearchTerm); + }); + + it('does not submit only when typing', async () => { + expect(wrapper.emitted('update-search-term')).toBeUndefined(); + + await findSearchBar().vm.$emit('input', 'new'); + + expect(wrapper.emitted('update-search-term')).toBeUndefined(); + }); + + describe('when submitting the search', () => { + const newSearchTerm = 'dog'; + + beforeEach(async () => { + await findSearchBar().vm.$emit('input', newSearchTerm); + await findSearchBar().vm.$emit('submit'); + }); + + it('emits the event up with the new payload', () => { + expect(wrapper.emitted('update-search-term')).toEqual([[newSearchTerm]]); + }); + }); + + describe('when clearing the search', () => { + beforeEach(async () => { + await findSearchBar().vm.$emit('input', 'new'); + await findSearchBar().vm.$emit('clear'); + }); + + it('emits an update event with an empty string payload', () => { + expect(wrapper.emitted('update-search-term')).toEqual([['']]); + }); + }); + }); + + describe('sort', () => { + describe('when changing sort order', () => { + it('changes the `isAscending` prop to the sorting component', async () => { + expect(findSorting().props().isAscending).toBe(false); + + await findSorting().vm.$emit('sortDirectionChange'); + + expect(findSorting().props().isAscending).toBe(true); + }); + + it('emits an `update-sorting` event with the new direction', async () => { + expect(wrapper.emitted('update-sorting')).toBeUndefined(); + + await findSorting().vm.$emit('sortDirectionChange'); + await findSorting().vm.$emit('sortDirectionChange'); + + expect(wrapper.emitted('update-sorting')).toEqual([ + [`${SORT_OPTION_CREATED}_${SORT_ASC}`], + [`${SORT_OPTION_CREATED}_${SORT_DESC}`], + ]); + }); + }); + }); +}); diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js index 3862195d8c7..d74b133f386 100644 --- a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js +++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js @@ -1,21 +1,22 @@ import Vue from 'vue'; import VueRouter from 'vue-router'; -import { GlAvatar, GlBadge, GlButton, GlSprintf } from '@gitlab/ui'; +import { GlAvatar, GlBadge, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import { cleanLeadingSeparator } from '~/lib/utils/url_utility'; import { createRouter } from '~/ci/catalog/router/index'; import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants'; import { catalogSinglePageResponse } from '../../mock'; Vue.use(VueRouter); -let router; -let routerPush; +const defaultEvent = { preventDefault: jest.fn, ctrlKey: false, metaKey: false }; describe('CiResourcesListItem', () => { let wrapper; + let routerPush; + const router = createRouter(); const resource = catalogSinglePageResponse.data.ciCatalogResources.nodes[0]; const release = { author: { name: 'author', webUrl: '/user/1' }, @@ -35,22 +36,19 @@ describe('CiResourcesListItem', () => { }, stubs: { GlSprintf, - RouterLink: true, - RouterView: true, }, }); }; const findAvatar = () => wrapper.findComponent(GlAvatar); const findBadge = () => wrapper.findComponent(GlBadge); - const findResourceName = () => wrapper.findComponent(GlButton); + const findResourceName = () => wrapper.findByTestId('ci-resource-link'); const findResourceDescription = () => wrapper.findByText(defaultProps.resource.description); const findUserLink = () => wrapper.findByTestId('user-link'); const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf); const findFavorites = () => wrapper.findByTestId('stats-favorites'); beforeEach(() => { - router = createRouter(); routerPush = jest.spyOn(router, 'push').mockImplementation(() => {}); }); @@ -70,8 +68,9 @@ describe('CiResourcesListItem', () => { }); }); - it('renders the resource name button', () => { + it('renders the resource name and link', () => { expect(findResourceName().exists()).toBe(true); + expect(findResourceName().attributes().href).toBe(defaultProps.resource.webPath); }); it('renders the resource version badge', () => { @@ -81,58 +80,69 @@ describe('CiResourcesListItem', () => { it('renders the resource description', () => { expect(findResourceDescription().exists()).toBe(true); }); + }); - describe('release time', () => { - describe('when there is no release data', () => { - beforeEach(() => { - createComponent({ props: { resource: { ...resource, latestVersion: null } } }); - }); + describe('release time', () => { + describe('when there is no release data', () => { + beforeEach(() => { + createComponent({ props: { resource: { ...resource, latestVersion: null } } }); + }); - it('does not render the release', () => { - expect(findTimeAgoMessage().exists()).toBe(false); - }); + it('does not render the release', () => { + expect(findTimeAgoMessage().exists()).toBe(false); + }); - it('renders the generic `unreleased` badge', () => { - expect(findBadge().exists()).toBe(true); - expect(findBadge().text()).toBe('Unreleased'); - }); + it('renders the generic `unreleased` badge', () => { + expect(findBadge().exists()).toBe(true); + expect(findBadge().text()).toBe('Unreleased'); }); + }); - describe('when there is release data', () => { - beforeEach(() => { - createComponent({ props: { resource: { ...resource, latestVersion: { ...release } } } }); - }); + describe('when there is release data', () => { + beforeEach(() => { + createComponent({ props: { resource: { ...resource, latestVersion: { ...release } } } }); + }); - it('renders the user link', () => { - expect(findUserLink().exists()).toBe(true); - expect(findUserLink().attributes('href')).toBe(release.author.webUrl); - }); + it('renders the user link', () => { + expect(findUserLink().exists()).toBe(true); + expect(findUserLink().attributes('href')).toBe(release.author.webUrl); + }); - it('renders the time since the resource was released', () => { - expect(findTimeAgoMessage().exists()).toBe(true); - }); + it('renders the time since the resource was released', () => { + expect(findTimeAgoMessage().exists()).toBe(true); + }); - it('renders the version badge', () => { - expect(findBadge().exists()).toBe(true); - expect(findBadge().text()).toBe(release.tagName); - }); + it('renders the version badge', () => { + expect(findBadge().exists()).toBe(true); + expect(findBadge().text()).toBe(release.tagName); }); }); }); describe('when clicking on an item title', () => { - beforeEach(async () => { - createComponent(); + describe('without holding down a modifier key', () => { + it('navigates to the details page in the same tab', async () => { + createComponent(); + await findResourceName().vm.$emit('click', defaultEvent); - await findResourceName().vm.$emit('click'); + expect(routerPush).toHaveBeenCalledWith({ + path: cleanLeadingSeparator(resource.webPath), + }); + }); }); - it('navigates to the details page', () => { - expect(routerPush).toHaveBeenCalledWith({ - name: CI_RESOURCE_DETAILS_PAGE_NAME, - params: { - id: getIdFromGraphQLId(resource.id), - }, + describe.each` + keyName + ${'ctrlKey'} + ${'metaKey'} + `('when $keyName is being held down', ({ keyName }) => { + beforeEach(async () => { + createComponent(); + await findResourceName().vm.$emit('click', { ...defaultEvent, [keyName]: true }); + }); + + it('does not call VueRouter push', () => { + expect(routerPush).not.toHaveBeenCalled(); }); }); }); @@ -141,43 +151,35 @@ describe('CiResourcesListItem', () => { beforeEach(async () => { createComponent(); - await findAvatar().vm.$emit('click'); + await findAvatar().vm.$emit('click', defaultEvent); }); it('navigates to the details page', () => { - expect(routerPush).toHaveBeenCalledWith({ - name: CI_RESOURCE_DETAILS_PAGE_NAME, - params: { - id: getIdFromGraphQLId(resource.id), - }, - }); + expect(routerPush).toHaveBeenCalledWith({ path: cleanLeadingSeparator(resource.webPath) }); }); }); describe('statistics', () => { describe('when there are no statistics', () => { - beforeEach(() => { + it('render favorites as 0', () => { createComponent({ props: { resource: { + ...resource, starCount: 0, }, }, }); - }); - it('render favorites as 0', () => { expect(findFavorites().exists()).toBe(true); expect(findFavorites().text()).toBe('0'); }); }); describe('where there are statistics', () => { - beforeEach(() => { + it('render favorites', () => { createComponent(); - }); - it('render favorites', () => { expect(findFavorites().exists()).toBe(true); expect(findFavorites().text()).toBe(String(defaultProps.resource.starCount)); }); diff --git a/spec/frontend/ci/catalog/components/list/empty_state_spec.js b/spec/frontend/ci/catalog/components/list/empty_state_spec.js index f589ad96a9d..5db0c61371d 100644 --- a/spec/frontend/ci/catalog/components/list/empty_state_spec.js +++ b/spec/frontend/ci/catalog/components/list/empty_state_spec.js @@ -1,27 +1,83 @@ -import { GlEmptyState } from '@gitlab/ui'; +import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import EmptyState from '~/ci/catalog/components/list/empty_state.vue'; +import { COMPONENTS_DOCS_URL } from '~/ci/catalog/constants'; describe('EmptyState', () => { let wrapper; const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findComponentsDocLink = () => wrapper.findComponent(GlLink); const createComponent = ({ props = {} } = {}) => { wrapper = shallowMountExtended(EmptyState, { propsData: { ...props, }, + stubs: { + GlEmptyState, + GlSprintf, + }, }); }; - describe('when mounted', () => { + describe('default', () => { beforeEach(() => { createComponent(); }); - it('renders the empty state', () => { - expect(findEmptyState().exists()).toBe(true); + it('renders the default empty state', () => { + const emptyState = findEmptyState(); + + expect(emptyState.exists()).toBe(true); + expect(emptyState.props().title).toBe('Get started with the CI/CD Catalog'); + expect(emptyState.props().description).toBe( + 'Create a pipeline component repository and make reusing pipeline configurations faster and easier.', + ); + }); + }); + + describe('when there is a search query', () => { + beforeEach(() => { + createComponent({ + props: { searchTerm: 'a' }, + }); + }); + + it('renders the search description', () => { + expect(findEmptyState().text()).toContain( + 'Edit your search and try again. Or learn to create a component repository.', + ); + }); + + it('renders the link to the components documentation', () => { + const docsLink = findComponentsDocLink(); + expect(docsLink.exists()).toBe(true); + expect(docsLink.attributes().href).toBe(COMPONENTS_DOCS_URL); + }); + + describe('and it is less than 3 characters', () => { + beforeEach(() => { + createComponent({ + props: { searchTerm: 'a' }, + }); + }); + + it('render the too few chars empty state title', () => { + expect(findEmptyState().props().title).toBe('Search must be at least 3 characters'); + }); + }); + + describe('and it has more than 3 characters', () => { + beforeEach(() => { + createComponent({ + props: { searchTerm: 'my component' }, + }); + }); + + it('renders the search empty state title', () => { + expect(findEmptyState().props().title).toBe('No result found'); + }); }); }); }); diff --git a/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js index 40f243ed891..015c6504fa5 100644 --- a/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js +++ b/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js @@ -5,7 +5,8 @@ import { GlEmptyState } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; -import { CI_CATALOG_RESOURCE_TYPE, cacheConfig } from '~/ci/catalog/graphql/settings'; +import { cacheConfig } from '~/ci/catalog/graphql/settings'; +import { cleanLeadingSeparator } from '~/lib/utils/url_utility'; import getCiCatalogResourceSharedData from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql'; import getCiCatalogResourceDetails from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql'; @@ -17,7 +18,6 @@ import CiResourceHeaderSkeletonLoader from '~/ci/catalog/components/details/ci_r import { createRouter } from '~/ci/catalog/router/index'; import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants'; -import { convertToGraphQLId } from '~/graphql_shared/utils'; import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock'; Vue.use(VueApollo); @@ -75,7 +75,7 @@ describe('CiResourceDetailsPage', () => { router = createRouter(); await router.push({ name: CI_RESOURCE_DETAILS_PAGE_NAME, - params: { id: defaultSharedData.id }, + params: { id: defaultSharedData.webPath }, }); }); @@ -178,7 +178,7 @@ describe('CiResourceDetailsPage', () => { it('passes expected props', () => { expect(findDetailsComponent().props()).toEqual({ - resourceId: convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, defaultAdditionalData.id), + resourcePath: cleanLeadingSeparator(defaultSharedData.webPath), }); }); }); diff --git a/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js index e18b418b155..e6fbd63f307 100644 --- a/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js +++ b/spec/frontend/ci/catalog/components/pages/ci_resources_page_spec.js @@ -7,10 +7,12 @@ import createMockApollo from 'helpers/mock_apollo_helper'; import { createAlert } from '~/alert'; import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue'; +import CatalogSearch from '~/ci/catalog/components/list/catalog_search.vue'; import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue'; import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue'; import EmptyState from '~/ci/catalog/components/list/empty_state.vue'; -import { cacheConfig } from '~/ci/catalog/graphql/settings'; +import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings'; +import typeDefs from '~/ci/catalog/graphql/typedefs.graphql'; import ciResourcesPage from '~/ci/catalog/components/pages/ci_resources_page.vue'; import getCatalogResources from '~/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql'; @@ -24,9 +26,11 @@ describe('CiResourcesPage', () => { let wrapper; let catalogResourcesResponse; + const defaultQueryVariables = { first: 20 }; + const createComponent = () => { const handlers = [[getCatalogResources, catalogResourcesResponse]]; - const mockApollo = createMockApollo(handlers, {}, cacheConfig); + const mockApollo = createMockApollo(handlers, resolvers, { cacheConfig, typeDefs }); wrapper = shallowMountExtended(ciResourcesPage, { apolloProvider: mockApollo, @@ -36,6 +40,7 @@ describe('CiResourcesPage', () => { }; const findCatalogHeader = () => wrapper.findComponent(CatalogHeader); + const findCatalogSearch = () => wrapper.findComponent(CatalogSearch); const findCiResourcesList = () => wrapper.findComponent(CiResourcesList); const findLoadingState = () => wrapper.findComponent(CatalogListSkeletonLoader); const findEmptyState = () => wrapper.findComponent(EmptyState); @@ -71,8 +76,14 @@ describe('CiResourcesPage', () => { }); it('renders the empty state', () => { - expect(findLoadingState().exists()).toBe(false); expect(findEmptyState().exists()).toBe(true); + }); + + it('renders the search', () => { + expect(findCatalogSearch().exists()).toBe(true); + }); + + it('does not render the list', () => { expect(findCiResourcesList().exists()).toBe(false); }); }); @@ -99,6 +110,10 @@ describe('CiResourcesPage', () => { totalCount: count, }); }); + + it('renders the search and sort', () => { + expect(findCatalogSearch().exists()).toBe(true); + }); }); }); @@ -121,11 +136,12 @@ describe('CiResourcesPage', () => { if (eventName === 'onNextPage') { expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + ...defaultQueryVariables, after: pageInfo.endCursor, - first: 20, }); } else { expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + ...defaultQueryVariables, before: pageInfo.startCursor, last: 20, first: null, @@ -134,8 +150,75 @@ describe('CiResourcesPage', () => { }); }); + describe('search and sort', () => { + describe('on initial load', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + }); + + it('calls the query without search or sort', () => { + expect(catalogResourcesResponse).toHaveBeenCalledTimes(1); + expect(catalogResourcesResponse.mock.calls[0][0]).toEqual({ + ...defaultQueryVariables, + }); + }); + }); + + describe('when sorting changes', () => { + const newSort = 'MOST_AWESOME_ASC'; + + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + await findCatalogSearch().vm.$emit('update-sorting', newSort); + }); + + it('passes it to the graphql query', () => { + expect(catalogResourcesResponse).toHaveBeenCalledTimes(2); + expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + ...defaultQueryVariables, + sortValue: newSort, + }); + }); + }); + + describe('when search component emits a new search term', () => { + const newSearch = 'sloths'; + + describe('and there are no results', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(emptyCatalogResponseBody); + await createComponent(); + await findCatalogSearch().vm.$emit('update-search-term', newSearch); + }); + + it('renders the empty state and passes down the search query', () => { + expect(findEmptyState().exists()).toBe(true); + expect(findEmptyState().props().searchTerm).toBe(newSearch); + }); + }); + + describe('and there are results', () => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + await findCatalogSearch().vm.$emit('update-search-term', newSearch); + }); + + it('passes it to the graphql query', () => { + expect(catalogResourcesResponse).toHaveBeenCalledTimes(2); + expect(catalogResourcesResponse.mock.calls[1][0]).toEqual({ + ...defaultQueryVariables, + searchTerm: newSearch, + }); + }); + }); + }); + }); + describe('pages count', () => { - describe('when the fetchMore call suceeds', () => { + describe('when the fetchMore call succeeds', () => { beforeEach(async () => { catalogResourcesResponse.mockResolvedValue(catalogResponseBody); @@ -157,6 +240,31 @@ describe('CiResourcesPage', () => { }); }); + describe.each` + event | payload + ${'update-search-term'} | ${'cat'} + ${'update-sorting'} | ${'CREATED_ASC'} + `('when $event event is emitted', ({ event, payload }) => { + beforeEach(async () => { + catalogResourcesResponse.mockResolvedValue(catalogResponseBody); + await createComponent(); + }); + + it('resets the page count', async () => { + expect(findCiResourcesList().props().currentPage).toBe(1); + + findCiResourcesList().vm.$emit('onNextPage'); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(2); + + await findCatalogSearch().vm.$emit(event, payload); + await waitForPromises(); + + expect(findCiResourcesList().props().currentPage).toBe(1); + }); + }); + describe('when the fetchMore call fails', () => { const errorMessage = 'there was an error'; diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js index 125f003224c..e370ac5054f 100644 --- a/spec/frontend/ci/catalog/mock.js +++ b/spec/frontend/ci/catalog/mock.js @@ -1,5 +1,3 @@ -import { componentsMockData } from '~/ci/catalog/constants'; - export const emptyCatalogResponseBody = { data: { ciCatalogResources: { @@ -39,12 +37,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-42', __typename: 'CiCatalogResource', }, @@ -55,12 +47,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-41', __typename: 'CiCatalogResource', }, @@ -71,12 +57,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-40', __typename: 'CiCatalogResource', }, @@ -87,12 +67,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-39', __typename: 'CiCatalogResource', }, @@ -103,12 +77,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-38', __typename: 'CiCatalogResource', }, @@ -119,12 +87,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-37', __typename: 'CiCatalogResource', }, @@ -135,12 +97,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-36', __typename: 'CiCatalogResource', }, @@ -151,12 +107,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-35', __typename: 'CiCatalogResource', }, @@ -167,12 +117,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-34', __typename: 'CiCatalogResource', }, @@ -183,12 +127,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-33', __typename: 'CiCatalogResource', }, @@ -199,12 +137,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-32', __typename: 'CiCatalogResource', }, @@ -215,12 +147,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-31', __typename: 'CiCatalogResource', }, @@ -231,12 +157,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-30', __typename: 'CiCatalogResource', }, @@ -247,12 +167,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-29', __typename: 'CiCatalogResource', }, @@ -263,12 +177,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-28', __typename: 'CiCatalogResource', }, @@ -279,12 +187,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-27', __typename: 'CiCatalogResource', }, @@ -295,12 +197,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-26', __typename: 'CiCatalogResource', }, @@ -311,12 +207,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-25', __typename: 'CiCatalogResource', }, @@ -327,12 +217,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-24', __typename: 'CiCatalogResource', }, @@ -343,12 +227,6 @@ export const catalogResponseBody = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-23', __typename: 'CiCatalogResource', }, @@ -379,12 +257,6 @@ export const catalogSinglePageResponse = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-45', __typename: 'CiCatalogResource', }, @@ -395,12 +267,6 @@ export const catalogSinglePageResponse = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-44', __typename: 'CiCatalogResource', }, @@ -411,12 +277,6 @@ export const catalogSinglePageResponse = { description: 'A simple component', starCount: 0, latestVersion: null, - rootNamespace: { - id: 'gid://gitlab/Group/185', - fullPath: 'frontend-fixtures', - name: 'frontend-fixtures', - __typename: 'Namespace', - }, webPath: '/frontend-fixtures/project-43', __typename: 'CiCatalogResource', }, @@ -434,7 +294,6 @@ export const catalogSharedDataMock = { icon: null, description: 'This is the description of the repo', name: 'Ruby', - rootNamespace: { id: 1, fullPath: '/group/project', name: 'my-dumb-project' }, starCount: 1, latestVersion: { __typename: 'Release', @@ -444,7 +303,7 @@ export const catalogSharedDataMock = { releasedAt: Date.now(), author: { id: 1, webUrl: 'profile/1', name: 'username' }, }, - webPath: 'path/to/project', + webPath: '/path/to/project', }, }, }; @@ -454,6 +313,7 @@ export const catalogAdditionalDetailsMock = { ciCatalogResource: { __typename: 'CiCatalogResource', id: `gid://gitlab/CiCatalogResource/1`, + webPath: '/twitter/project', openIssuesCount: 4, openMergeRequestsCount: 10, readmeHtml: '<h1>Hello world</h1>', @@ -502,12 +362,6 @@ const generateResourcesNodes = (count = 20, startId = 0) => { description: `This is a component that does a bunch of stuff and is really just a number: ${i}`, icon: 'my-icon', name: `My component #${i}`, - rootNamespace: { - id: 1, - __typename: 'Namespace', - name: 'namespaceName', - path: 'namespacePath', - }, starCount: 10, latestVersion: { __typename: 'Release', @@ -526,13 +380,47 @@ const generateResourcesNodes = (count = 20, startId = 0) => { export const mockCatalogResourceItem = generateResourcesNodes(1)[0]; +const componentsMockData = { + __typename: 'CiComponentConnection', + nodes: [ + { + id: 'gid://gitlab/Ci::Component/1', + name: 'Ruby gal', + description: 'This is a pretty amazing component that does EVERYTHING ruby.', + path: 'gitlab.com/gitlab-org/ruby-gal@~latest', + inputs: [{ name: 'version', default: '1.0.0', required: true }], + }, + { + id: 'gid://gitlab/Ci::Component/2', + name: 'Javascript madness', + description: 'Adds some spice to your life.', + path: 'gitlab.com/gitlab-org/javascript-madness@~latest', + inputs: [ + { name: 'isFun', default: 'true', required: true }, + { name: 'RandomNumber', default: '10', required: false }, + ], + }, + { + id: 'gid://gitlab/Ci::Component/3', + name: 'Go go go', + description: 'When you write Go, you gotta go go go.', + path: 'gitlab.com/gitlab-org/go-go-go@~latest', + inputs: [{ name: 'version', default: '1.0.0', required: true }], + }, + ], +}; + export const mockComponents = { data: { ciCatalogResource: { __typename: 'CiCatalogResource', id: `gid://gitlab/CiCatalogResource/1`, - components: { - ...componentsMockData, + webPath: '/twitter/project-1', + latestVersion: { + id: 'gid://gitlab/Version/1', + components: { + ...componentsMockData, + }, }, }, }, @@ -543,7 +431,11 @@ export const mockComponentsEmpty = { ciCatalogResource: { __typename: 'CiCatalogResource', id: `gid://gitlab/CiCatalogResource/1`, - components: [], + webPath: '/twitter/project-1', + latestVersion: { + id: 'gid://gitlab/Version/1', + components: [], + }, }, }, }; |