From 8c7f4e9d5f36cff46365a7f8c4b9c21578c1e781 Mon Sep 17 00:00:00 2001 From: GitLab Bot Date: Thu, 18 Jun 2020 11:18:50 +0000 Subject: Add latest changes from gitlab-org/gitlab@13-1-stable-ee --- .../frontend/releases/components/app_index_spec.js | 150 +++++++++++++++++++++ .../releases/components/asset_links_form_spec.js | 34 ++++- .../components/release_block_assets_spec.js | 137 +++++++++++++++++++ 3 files changed, 320 insertions(+), 1 deletion(-) create mode 100644 spec/frontend/releases/components/app_index_spec.js create mode 100644 spec/frontend/releases/components/release_block_assets_spec.js (limited to 'spec/frontend/releases/components') diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js new file mode 100644 index 00000000000..91beb5b1418 --- /dev/null +++ b/spec/frontend/releases/components/app_index_spec.js @@ -0,0 +1,150 @@ +import { range as rge } from 'lodash'; +import Vue from 'vue'; +import { mountComponentWithStore } from 'helpers/vue_mount_component_helper'; +import app from '~/releases/components/app_index.vue'; +import createStore from '~/releases/stores'; +import listModule from '~/releases/stores/modules/list'; +import api from '~/api'; +import { resetStore } from '../stores/modules/list/helpers'; +import { + pageInfoHeadersWithoutPagination, + pageInfoHeadersWithPagination, + release2 as release, + releases, +} from '../mock_data'; +import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; +import waitForPromises from 'helpers/wait_for_promises'; + +describe('Releases App ', () => { + const Component = Vue.extend(app); + let store; + let vm; + let releasesPagination; + + const props = { + projectId: 'gitlab-ce', + documentationPath: 'help/releases', + illustrationPath: 'illustration/path', + }; + + beforeEach(() => { + store = createStore({ modules: { list: listModule } }); + releasesPagination = rge(21).map(index => ({ + ...convertObjectPropsToCamelCase(release, { deep: true }), + tagName: `${index}.00`, + })); + }); + + afterEach(() => { + resetStore(store); + vm.$destroy(); + }); + + describe('while loading', () => { + beforeEach(() => { + jest + .spyOn(api, 'releases') + // Need to defer the return value here to the next stack, + // otherwise the loading state disappears before our test even starts. + .mockImplementation(() => waitForPromises().then(() => ({ data: [], headers: {} }))); + vm = mountComponentWithStore(Component, { props, store }); + }); + + it('renders loading icon', () => { + expect(vm.$el.querySelector('.js-loading')).not.toBeNull(); + expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); + expect(vm.$el.querySelector('.js-success-state')).toBeNull(); + expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); + + return waitForPromises(); + }); + }); + + describe('with successful request', () => { + beforeEach(() => { + jest + .spyOn(api, 'releases') + .mockResolvedValue({ data: releases, headers: pageInfoHeadersWithoutPagination }); + vm = mountComponentWithStore(Component, { props, store }); + }); + + it('renders success state', () => { + expect(vm.$el.querySelector('.js-loading')).toBeNull(); + expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); + expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); + expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); + }); + }); + + describe('with successful request and pagination', () => { + beforeEach(() => { + jest + .spyOn(api, 'releases') + .mockResolvedValue({ data: releasesPagination, headers: pageInfoHeadersWithPagination }); + vm = mountComponentWithStore(Component, { props, store }); + }); + + it('renders success state', () => { + expect(vm.$el.querySelector('.js-loading')).toBeNull(); + expect(vm.$el.querySelector('.js-empty-state')).toBeNull(); + expect(vm.$el.querySelector('.js-success-state')).not.toBeNull(); + expect(vm.$el.querySelector('.gl-pagination')).not.toBeNull(); + }); + }); + + describe('with empty request', () => { + beforeEach(() => { + jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} }); + vm = mountComponentWithStore(Component, { props, store }); + }); + + it('renders empty state', () => { + expect(vm.$el.querySelector('.js-loading')).toBeNull(); + expect(vm.$el.querySelector('.js-empty-state')).not.toBeNull(); + expect(vm.$el.querySelector('.js-success-state')).toBeNull(); + expect(vm.$el.querySelector('.gl-pagination')).toBeNull(); + }); + }); + + describe('"New release" button', () => { + const findNewReleaseButton = () => vm.$el.querySelector('.js-new-release-btn'); + + beforeEach(() => { + jest.spyOn(api, 'releases').mockResolvedValue({ data: [], headers: {} }); + }); + + const factory = additionalProps => { + vm = mountComponentWithStore(Component, { + props: { + ...props, + ...additionalProps, + }, + store, + }); + }; + + describe('when the user is allowed to create a new Release', () => { + const newReleasePath = 'path/to/new/release'; + + beforeEach(() => { + factory({ newReleasePath }); + }); + + it('renders the "New release" button', () => { + expect(findNewReleaseButton()).not.toBeNull(); + }); + + it('renders the "New release" button with the correct href', () => { + expect(findNewReleaseButton().getAttribute('href')).toBe(newReleasePath); + }); + }); + + describe('when the user is not allowed to create a new Release', () => { + beforeEach(() => factory()); + + it('does not render the "New release" button', () => { + expect(findNewReleaseButton()).toBeNull(); + }); + }); + }); +}); diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js index 44542868cfe..e1f8592270e 100644 --- a/spec/frontend/releases/components/asset_links_form_spec.js +++ b/spec/frontend/releases/components/asset_links_form_spec.js @@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils'; import AssetLinksForm from '~/releases/components/asset_links_form.vue'; import { release as originalRelease } from '../mock_data'; import * as commonUtils from '~/lib/utils/common_utils'; +import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants'; const localVue = createLocalVue(); localVue.use(Vuex); @@ -24,6 +25,7 @@ describe('Release edit component', () => { addEmptyAssetLink: jest.fn(), updateAssetLinkUrl: jest.fn(), updateAssetLinkName: jest.fn(), + updateAssetLinkType: jest.fn(), removeAssetLink: jest.fn().mockImplementation((_context, linkId) => { state.release.assets.links = state.release.assets.links.filter(l => l.id !== linkId); }), @@ -51,6 +53,11 @@ describe('Release edit component', () => { wrapper = mount(AssetLinksForm, { localVue, store, + provide: { + glFeatures: { + releaseAssetLinkType: true, + }, + }, }); }; @@ -103,7 +110,7 @@ describe('Release edit component', () => { ); }); - it('calls the "updateAssetLinName" store method when text is entered into the "Link title" input field', () => { + it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => { const linkIdToUpdate = release.assets.links[0].id; const newName = 'updated name'; @@ -121,6 +128,31 @@ describe('Release edit component', () => { undefined, ); }); + + it('calls the "updateAssetLinkType" store method when an option is selected from the "Type" dropdown', () => { + const linkIdToUpdate = release.assets.links[0].id; + const newType = ASSET_LINK_TYPE.RUNBOOK; + + expect(actions.updateAssetLinkType).not.toHaveBeenCalled(); + + wrapper.find({ ref: 'typeSelect' }).vm.$emit('change', newType); + + expect(actions.updateAssetLinkType).toHaveBeenCalledTimes(1); + expect(actions.updateAssetLinkType).toHaveBeenCalledWith( + expect.anything(), + { + linkIdToUpdate, + newType, + }, + undefined, + ); + }); + + it('selects the default asset type if no type was provided by the backend', () => { + const selected = wrapper.find({ ref: 'typeSelect' }).element.value; + + expect(selected).toBe(DEFAULT_ASSET_LINK_TYPE); + }); }); describe('validation', () => { diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js new file mode 100644 index 00000000000..44b190b0d19 --- /dev/null +++ b/spec/frontend/releases/components/release_block_assets_spec.js @@ -0,0 +1,137 @@ +import { mount } from '@vue/test-utils'; +import { GlCollapse } from '@gitlab/ui'; +import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue'; +import { ASSET_LINK_TYPE } from '~/releases/constants'; +import { trimText } from 'helpers/text_helper'; +import { assets } from '../mock_data'; + +describe('Release block assets', () => { + let wrapper; + let defaultProps; + + // A map of types to the expected section heading text + const sections = { + [ASSET_LINK_TYPE.IMAGE]: 'Images', + [ASSET_LINK_TYPE.PACKAGE]: 'Packages', + [ASSET_LINK_TYPE.RUNBOOK]: 'Runbooks', + [ASSET_LINK_TYPE.OTHER]: 'Other', + }; + + const createComponent = (propsData = defaultProps) => { + wrapper = mount(ReleaseBlockAssets, { + provide: { + glFeatures: { releaseAssetLinkType: true }, + }, + propsData, + }); + }; + + const findSectionHeading = type => + wrapper.findAll('h5').filter(h5 => h5.text() === sections[type]); + + beforeEach(() => { + defaultProps = { assets }; + }); + + describe('with default props', () => { + beforeEach(() => createComponent()); + + const findAccordionButton = () => wrapper.find('[data-testid="accordion-button"]'); + + it('renders an "Assets" accordion with the asset count', () => { + const accordionButton = findAccordionButton(); + + expect(accordionButton.exists()).toBe(true); + expect(trimText(accordionButton.text())).toBe('Assets 5'); + }); + + it('renders the accordion as expanded by default', () => { + const accordion = wrapper.find(GlCollapse); + + expect(accordion.exists()).toBe(true); + expect(accordion.isVisible()).toBe(true); + }); + + it('renders sources with the expected text and URL', () => { + defaultProps.assets.sources.forEach(s => { + const sourceLink = wrapper.find(`li>a[href="${s.url}"]`); + + expect(sourceLink.exists()).toBe(true); + expect(sourceLink.text()).toBe(`Source code (${s.format})`); + }); + }); + + it('renders a heading for each assets type (except sources)', () => { + Object.keys(sections).forEach(type => { + const sectionHeadings = findSectionHeading(type); + + expect(sectionHeadings).toHaveLength(1); + }); + }); + + it('renders asset links with the expected text and URL', () => { + defaultProps.assets.links.forEach(l => { + const sourceLink = wrapper.find(`li>a[href="${l.directAssetUrl}"]`); + + expect(sourceLink.exists()).toBe(true); + expect(sourceLink.text()).toBe(l.name); + }); + }); + }); + + describe("when a release doesn't have a link with a certain asset type", () => { + const typeToExclude = ASSET_LINK_TYPE.IMAGE; + + beforeEach(() => { + defaultProps.assets.links = defaultProps.assets.links.filter( + l => l.linkType !== typeToExclude, + ); + createComponent(defaultProps); + }); + + it('does not render a section heading if there are no links of that type', () => { + const sectionHeadings = findSectionHeading(typeToExclude); + + expect(sectionHeadings).toHaveLength(0); + }); + }); + + describe('external vs internal links', () => { + const containsExternalSourceIndicator = () => + wrapper.contains('[data-testid="external-link-indicator"]'); + + describe('when a link is external', () => { + beforeEach(() => { + defaultProps.assets.sources = []; + defaultProps.assets.links = [ + { + ...defaultProps.assets.links[0], + external: true, + }, + ]; + createComponent(defaultProps); + }); + + it('renders the link with an "external source" indicator', () => { + expect(containsExternalSourceIndicator()).toBe(true); + }); + }); + + describe('when a link is internal', () => { + beforeEach(() => { + defaultProps.assets.sources = []; + defaultProps.assets.links = [ + { + ...defaultProps.assets.links[0], + external: false, + }, + ]; + createComponent(defaultProps); + }); + + it('renders the link without the "external source" indicator', () => { + expect(containsExternalSourceIndicator()).toBe(false); + }); + }); + }); +}); -- cgit v1.2.3