diff options
Diffstat (limited to 'spec/frontend/ci/catalog/components/list')
5 files changed, 476 insertions, 0 deletions
diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js new file mode 100644 index 00000000000..912fd9e1a93 --- /dev/null +++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js @@ -0,0 +1,86 @@ +import { GlBanner, GlButton } from '@gitlab/ui'; +import { useLocalStorageSpy } from 'helpers/local_storage_helper'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue'; +import { CATALOG_FEEDBACK_DISMISSED_KEY } from '~/ci/catalog/constants'; + +describe('CatalogHeader', () => { + useLocalStorageSpy(); + + let wrapper; + + const defaultProps = {}; + const defaultProvide = { + pageTitle: 'Catalog page', + pageDescription: 'This is a nice catalog page', + }; + + const findBanner = () => wrapper.findComponent(GlBanner); + const findFeedbackButton = () => findBanner().findComponent(GlButton); + const findTitle = () => wrapper.findByText(defaultProvide.pageTitle); + const findDescription = () => wrapper.findByText(defaultProvide.pageDescription); + + const createComponent = ({ props = {}, stubs = {} } = {}) => { + wrapper = shallowMountExtended(CatalogHeader, { + propsData: { + ...defaultProps, + ...props, + }, + provide: defaultProvide, + stubs: { + ...stubs, + }, + }); + }; + + it('renders the Catalog title and description', () => { + createComponent(); + + expect(findTitle().exists()).toBe(true); + expect(findDescription().exists()).toBe(true); + }); + + describe('Feedback banner', () => { + describe('when user has never dismissed', () => { + beforeEach(() => { + createComponent({ stubs: { GlBanner } }); + }); + + it('is visible', () => { + expect(findBanner().exists()).toBe(true); + }); + + it('has link to feedback issue', () => { + expect(findFeedbackButton().attributes().href).toBe( + 'https://gitlab.com/gitlab-org/gitlab/-/issues/407556', + ); + }); + }); + + describe('when user dismisses it', () => { + beforeEach(() => { + createComponent(); + }); + + it('sets the local storage and removes the banner', async () => { + expect(findBanner().exists()).toBe(true); + + await findBanner().vm.$emit('close'); + + expect(localStorage.setItem).toHaveBeenCalledWith(CATALOG_FEEDBACK_DISMISSED_KEY, 'true'); + expect(findBanner().exists()).toBe(false); + }); + }); + + describe('when user has dismissed it before', () => { + beforeEach(() => { + localStorage.setItem(CATALOG_FEEDBACK_DISMISSED_KEY, 'true'); + createComponent(); + }); + + it('does not show the banner', () => { + expect(findBanner().exists()).toBe(false); + }); + }); + }); +}); diff --git a/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js b/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js new file mode 100644 index 00000000000..d21fd56eb2e --- /dev/null +++ b/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js @@ -0,0 +1,22 @@ +import { shallowMount } from '@vue/test-utils'; +import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue'; + +describe('CatalogListSkeletonLoader', () => { + let wrapper; + + const findSkeletonLoader = () => wrapper.findComponent(CatalogListSkeletonLoader); + + const createComponent = () => { + wrapper = shallowMount(CatalogListSkeletonLoader, {}); + }; + + describe('when mounted', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders skeleton item', () => { + expect(findSkeletonLoader().exists()).toBe(true); + }); + }); +}); 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 new file mode 100644 index 00000000000..7f446064366 --- /dev/null +++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js @@ -0,0 +1,198 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import { GlAvatar, GlBadge, GlButton, GlSprintf } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +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; + +describe('CiResourcesListItem', () => { + let wrapper; + + const resource = catalogSinglePageResponse.data.ciCatalogResources.nodes[0]; + const release = { + author: { name: 'author', webUrl: '/user/1' }, + releasedAt: Date.now(), + tagName: '1.0.0', + }; + const defaultProps = { + resource, + }; + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMountExtended(CiResourcesListItem, { + router, + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlSprintf, + RouterLink: true, + RouterView: true, + }, + }); + }; + + const findAvatar = () => wrapper.findComponent(GlAvatar); + const findBadge = () => wrapper.findComponent(GlBadge); + const findResourceName = () => wrapper.findComponent(GlButton); + const findResourceDescription = () => wrapper.findByText(defaultProps.resource.description); + const findUserLink = () => wrapper.findByTestId('user-link'); + const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf); + const findFavorites = () => wrapper.findByTestId('stats-favorites'); + const findForks = () => wrapper.findByTestId('stats-forks'); + + beforeEach(() => { + router = createRouter(); + routerPush = jest.spyOn(router, 'push').mockImplementation(() => {}); + }); + + describe('when mounted', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the resource avatar and passes the right props', () => { + const { icon, id, name } = defaultProps.resource; + + expect(findAvatar().exists()).toBe(true); + expect(findAvatar().props()).toMatchObject({ + entityId: getIdFromGraphQLId(id), + entityName: name, + src: icon, + }); + }); + + it('renders the resource name button', () => { + expect(findResourceName().exists()).toBe(true); + }); + + it('renders the resource version badge', () => { + expect(findBadge().exists()).toBe(true); + }); + + 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 } } }); + }); + + 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'); + }); + }); + + 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 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); + }); + }); + }); + }); + + describe('when clicking on an item title', () => { + beforeEach(async () => { + createComponent(); + + await findResourceName().vm.$emit('click'); + }); + + it('navigates to the details page', () => { + expect(routerPush).toHaveBeenCalledWith({ + name: CI_RESOURCE_DETAILS_PAGE_NAME, + params: { + id: getIdFromGraphQLId(resource.id), + }, + }); + }); + }); + + describe('when clicking on an item avatar', () => { + beforeEach(async () => { + createComponent(); + + await findAvatar().vm.$emit('click'); + }); + + it('navigates to the details page', () => { + expect(routerPush).toHaveBeenCalledWith({ + name: CI_RESOURCE_DETAILS_PAGE_NAME, + params: { + id: getIdFromGraphQLId(resource.id), + }, + }); + }); + }); + + describe('statistics', () => { + describe('when there are no statistics', () => { + beforeEach(() => { + createComponent({ + props: { + resource: { + forksCount: 0, + starCount: 0, + }, + }, + }); + }); + + it('render favorites as 0', () => { + expect(findFavorites().exists()).toBe(true); + expect(findFavorites().text()).toBe('0'); + }); + + it('render forks as 0', () => { + expect(findForks().exists()).toBe(true); + expect(findForks().text()).toBe('0'); + }); + }); + + describe('where there are statistics', () => { + beforeEach(() => { + createComponent(); + }); + + it('render favorites', () => { + expect(findFavorites().exists()).toBe(true); + expect(findFavorites().text()).toBe(String(defaultProps.resource.starCount)); + }); + + it('render forks', () => { + expect(findForks().exists()).toBe(true); + expect(findForks().text()).toBe(String(defaultProps.resource.forksCount)); + }); + }); + }); +}); diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js new file mode 100644 index 00000000000..aca20a83979 --- /dev/null +++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js @@ -0,0 +1,143 @@ +import { GlKeysetPagination } from '@gitlab/ui'; + +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue'; +import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue'; +import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings'; +import { catalogResponseBody, catalogSinglePageResponse } from '../../mock'; + +describe('CiResourcesList', () => { + let wrapper; + + const createComponent = ({ props = {} } = {}) => { + const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources; + + const defaultProps = { + currentPage: 1, + resources: nodes, + pageInfo, + totalCount: count, + }; + + wrapper = shallowMountExtended(CiResourcesList, { + propsData: { + ...defaultProps, + ...props, + }, + stubs: { + GlKeysetPagination, + }, + }); + }; + + const findPageCount = () => wrapper.findByTestId('pageCount'); + const findResourcesListItems = () => wrapper.findAllComponents(CiResourcesListItem); + const findPrevBtn = () => wrapper.findByTestId('prevButton'); + const findNextBtn = () => wrapper.findByTestId('nextButton'); + + describe('contains only one page', () => { + const { nodes, pageInfo, count } = catalogSinglePageResponse.data.ciCatalogResources; + + beforeEach(async () => { + await createComponent({ + props: { currentPage: 1, resources: nodes, pageInfo, totalCount: count }, + }); + }); + + it('shows the right number of items', () => { + expect(findResourcesListItems()).toHaveLength(nodes.length); + }); + + it('hides the keyset control for previous page', () => { + expect(findPrevBtn().exists()).toBe(false); + }); + + it('hides the keyset control for next page', () => { + expect(findNextBtn().exists()).toBe(false); + }); + + it('shows the correct count of current page', () => { + expect(findPageCount().text()).toContain('1 of 1'); + }); + }); + + describe.each` + hasPreviousPage | hasNextPage | pageText | expectedTotal | currentPage + ${false} | ${true} | ${'1 of 3'} | ${ciCatalogResourcesItemsCount} | ${1} + ${true} | ${true} | ${'2 of 3'} | ${ciCatalogResourcesItemsCount} | ${2} + ${true} | ${false} | ${'3 of 3'} | ${ciCatalogResourcesItemsCount} | ${3} + `( + 'when on page $pageText', + ({ currentPage, expectedTotal, pageText, hasPreviousPage, hasNextPage }) => { + const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources; + + const previousPageState = hasPreviousPage ? 'enabled' : 'disabled'; + const nextPageState = hasNextPage ? 'enabled' : 'disabled'; + + beforeEach(async () => { + await createComponent({ + props: { + currentPage, + resources: nodes, + pageInfo: { ...pageInfo, hasPreviousPage, hasNextPage }, + totalCount: count, + }, + }); + }); + + it('shows the right number of items', () => { + expect(findResourcesListItems()).toHaveLength(expectedTotal); + }); + + it(`shows the keyset control for previous page as ${previousPageState}`, () => { + const disableAttr = findPrevBtn().attributes('disabled'); + + if (previousPageState === 'disabled') { + expect(disableAttr).toBeDefined(); + } else { + expect(disableAttr).toBeUndefined(); + } + }); + + it(`shows the keyset control for next page as ${nextPageState}`, () => { + const disableAttr = findNextBtn().attributes('disabled'); + + if (nextPageState === 'disabled') { + expect(disableAttr).toBeDefined(); + } else { + expect(disableAttr).toBeUndefined(); + } + }); + + it('shows the correct count of current page', () => { + expect(findPageCount().text()).toContain(pageText); + }); + }, + ); + + describe('when there is an error getting the page count', () => { + beforeEach(() => { + createComponent({ props: { totalCount: 0 } }); + }); + + it('hides the page count', () => { + expect(findPageCount().exists()).toBe(false); + }); + }); + + describe('emitted events', () => { + beforeEach(() => { + createComponent(); + }); + + it.each` + btnText | elFinder | eventName + ${'previous'} | ${findPrevBtn} | ${'onPrevPage'} + ${'next'} | ${findNextBtn} | ${'onNextPage'} + `('emits $eventName when clicking on the $btnText button', async ({ elFinder, eventName }) => { + await elFinder().vm.$emit('click'); + + expect(wrapper.emitted(eventName)).toHaveLength(1); + }); + }); +}); diff --git a/spec/frontend/ci/catalog/components/list/empty_state_spec.js b/spec/frontend/ci/catalog/components/list/empty_state_spec.js new file mode 100644 index 00000000000..f589ad96a9d --- /dev/null +++ b/spec/frontend/ci/catalog/components/list/empty_state_spec.js @@ -0,0 +1,27 @@ +import { GlEmptyState } from '@gitlab/ui'; +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import EmptyState from '~/ci/catalog/components/list/empty_state.vue'; + +describe('EmptyState', () => { + let wrapper; + + const findEmptyState = () => wrapper.findComponent(GlEmptyState); + + const createComponent = ({ props = {} } = {}) => { + wrapper = shallowMountExtended(EmptyState, { + propsData: { + ...props, + }, + }); + }; + + describe('when mounted', () => { + beforeEach(() => { + createComponent(); + }); + + it('renders the empty state', () => { + expect(findEmptyState().exists()).toBe(true); + }); + }); +}); |