diff options
Diffstat (limited to 'spec/frontend')
15 files changed, 903 insertions, 488 deletions
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js index 23b3b13d69c..9260718a94b 100644 --- a/spec/frontend/boards/components/board_content_spec.js +++ b/spec/frontend/boards/components/board_content_spec.js @@ -1,9 +1,11 @@ import { GlAlert } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import VueApollo from 'vue-apollo'; import Vue from 'vue'; import Draggable from 'vuedraggable'; import Vuex from 'vuex'; import eventHub from '~/boards/eventhub'; +import createMockApollo from 'helpers/mock_apollo_helper'; import { stubComponent } from 'helpers/stub_component'; import waitForPromises from 'helpers/wait_for_promises'; import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue'; @@ -11,9 +13,18 @@ import getters from 'ee_else_ce/boards/stores/getters'; import BoardColumn from '~/boards/components/board_column.vue'; import BoardContent from '~/boards/components/board_content.vue'; import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue'; +import updateBoardListMutation from '~/boards/graphql/board_list_update.mutation.graphql'; import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue'; -import { mockLists, mockListsById } from '../mock_data'; - +import { DraggableItemTypes } from 'ee_else_ce/boards/constants'; +import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql'; +import { + mockLists, + mockListsById, + updateBoardListResponse, + boardListsQueryResponse, +} from '../mock_data'; + +Vue.use(VueApollo); Vue.use(Vuex); const actions = { @@ -22,6 +33,9 @@ const actions = { describe('BoardContent', () => { let wrapper; + let mockApollo; + + const updateListHandler = jest.fn().mockResolvedValue(updateBoardListResponse); const defaultState = { isShowingEpicsSwimlanes: false, @@ -47,21 +61,32 @@ describe('BoardContent', () => { isIssueBoard = true, isEpicBoard = false, } = {}) => { + mockApollo = createMockApollo([[updateBoardListMutation, updateListHandler]]); + const listQueryVariables = { isProject: true }; + + mockApollo.clients.defaultClient.writeQuery({ + query: boardListsQuery, + variables: listQueryVariables, + data: boardListsQueryResponse.data, + }); + const store = createStore({ ...defaultState, ...state, }); wrapper = shallowMount(BoardContent, { + apolloProvider: mockApollo, propsData: { boardId: 'gid://gitlab/Board/1', filterParams: {}, isSwimlanesOn: false, boardListsApollo: mockListsById, - listQueryVariables: {}, + listQueryVariables, addColumnFormVisible: false, ...props, }, provide: { + boardType: 'project', canAdminList, issuableType, isIssueBoard, @@ -81,6 +106,7 @@ describe('BoardContent', () => { const findBoardColumns = () => wrapper.findAllComponents(BoardColumn); const findBoardAddNewColumn = () => wrapper.findComponent(BoardAddNewColumn); + const findDraggable = () => wrapper.findComponent(Draggable); describe('default', () => { beforeEach(() => { @@ -128,7 +154,7 @@ describe('BoardContent', () => { }); it('renders draggable component', () => { - expect(wrapper.findComponent(Draggable).exists()).toBe(true); + expect(findDraggable().exists()).toBe(true); }); }); @@ -138,7 +164,7 @@ describe('BoardContent', () => { }); it('does not render draggable component', () => { - expect(wrapper.findComponent(Draggable).exists()).toBe(false); + expect(findDraggable().exists()).toBe(false); }); }); @@ -164,6 +190,21 @@ describe('BoardContent', () => { expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists); }); + + it('reorders lists', async () => { + const movableListsOrder = [mockLists[0].id, mockLists[1].id]; + + findDraggable().vm.$emit('end', { + item: { dataset: { listId: mockLists[0].id, draggableItemType: DraggableItemTypes.list } }, + newIndex: 1, + to: { + children: movableListsOrder.map((listId) => ({ dataset: { listId } })), + }, + }); + await waitForPromises(); + + expect(updateListHandler).toHaveBeenCalled(); + }); }); describe('when "add column" form is visible', () => { diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js index 60f906d2157..68f665e004c 100644 --- a/spec/frontend/boards/mock_data.js +++ b/spec/frontend/boards/mock_data.js @@ -1023,6 +1023,7 @@ export const updateBoardListResponse = { data: { updateBoardList: { list: mockList, + errors: [], }, }, }; diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js index cc4a022c2df..89ce3a2e18c 100644 --- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js +++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js @@ -1,5 +1,6 @@ +import Vue from 'vue'; import { GlAlert, GlButton, GlLoadingIcon, GlSprintf } from '@gitlab/ui'; -import { shallowMount, createLocalVue } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import VueApollo from 'vue-apollo'; import createMockApollo from 'helpers/mock_apollo_helper'; import setWindowLocation from 'helpers/set_window_location_helper'; @@ -53,9 +54,6 @@ jest.mock('~/lib/utils/url_utility', () => ({ redirectTo: jest.fn(), })); -const localVue = createLocalVue(); -localVue.use(VueApollo); - const defaultProvide = { ciConfigPath: mockCiConfigPath, defaultBranch: mockDefaultBranch, @@ -74,24 +72,10 @@ describe('Pipeline editor app component', () => { let mockLatestCommitShaQuery; let mockPipelineQuery; - const createComponent = ({ - blobLoading = false, - options = {}, - provide = {}, - stubs = {}, - } = {}) => { + const createComponent = ({ options = {}, provide = {}, stubs = {} } = {}) => { wrapper = shallowMount(PipelineEditorApp, { provide: { ...defaultProvide, ...provide }, stubs, - mocks: { - $apollo: { - queries: { - initialCiFileContent: { - loading: blobLoading, - }, - }, - }, - }, ...options, }); }; @@ -101,6 +85,8 @@ describe('Pipeline editor app component', () => { stubs = {}, withUndefinedBranch = false, } = {}) => { + Vue.use(VueApollo); + const handlers = [ [getBlobContent, mockBlobContentData], [getCiConfigData, mockCiConfigData], @@ -137,7 +123,6 @@ describe('Pipeline editor app component', () => { }); const options = { - localVue, mocks: {}, apolloProvider: mockApollo, }; @@ -164,7 +149,7 @@ describe('Pipeline editor app component', () => { describe('loading state', () => { it('displays a loading icon if the blob query is loading', () => { - createComponent({ blobLoading: true }); + createComponentWithApollo(); expect(findLoadingIcon().exists()).toBe(true); expect(findEditorHome().exists()).toBe(false); @@ -246,10 +231,6 @@ describe('Pipeline editor app component', () => { describe('when file exists', () => { beforeEach(async () => { await createComponentWithApollo(); - - jest - .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling') - .mockImplementation(jest.fn()); }); it('shows pipeline editor home component', () => { @@ -268,8 +249,8 @@ describe('Pipeline editor app component', () => { }); }); - it('does not poll for the commit sha', () => { - expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0); + it('calls once and does not start poll for the commit sha', () => { + expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(1); }); }); @@ -281,10 +262,6 @@ describe('Pipeline editor app component', () => { PipelineEditorEmptyState, }, }); - - jest - .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling') - .mockImplementation(jest.fn()); }); it('shows an empty state and does not show editor home component', () => { @@ -293,8 +270,8 @@ describe('Pipeline editor app component', () => { expect(findEditorHome().exists()).toBe(false); }); - it('does not poll for the commit sha', () => { - expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(0); + it('calls once and does not start poll for the commit sha', () => { + expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(1); }); describe('because of a fetching error', () => { @@ -381,38 +358,27 @@ describe('Pipeline editor app component', () => { }); it('polls for commit sha while pipeline data is not yet available for current branch', async () => { - jest - .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling') - .mockImplementation(jest.fn()); - - // simulate a commit to the current branch findEditorHome().vm.$emit('updateCommitSha'); await waitForPromises(); - expect(wrapper.vm.$apollo.queries.commitSha.startPolling).toHaveBeenCalledTimes(1); + expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2); }); it('stops polling for commit sha when pipeline data is available for newly committed branch', async () => { - jest - .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling') - .mockImplementation(jest.fn()); - mockLatestCommitShaQuery.mockResolvedValue(mockCommitShaResults); - await wrapper.vm.$apollo.queries.commitSha.refetch(); + await waitForPromises(); + + await findEditorHome().vm.$emit('updateCommitSha'); - expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1); + expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2); }); it('stops polling for commit sha when pipeline data is available for current branch', async () => { - jest - .spyOn(wrapper.vm.$apollo.queries.commitSha, 'stopPolling') - .mockImplementation(jest.fn()); - mockLatestCommitShaQuery.mockResolvedValue(mockNewCommitShaResults); findEditorHome().vm.$emit('updateCommitSha'); await waitForPromises(); - expect(wrapper.vm.$apollo.queries.commitSha.stopPolling).toHaveBeenCalledTimes(1); + expect(mockLatestCommitShaQuery).toHaveBeenCalledTimes(2); }); }); @@ -497,15 +463,12 @@ describe('Pipeline editor app component', () => { it('refetches blob content', async () => { await createComponentWithApollo(); - jest - .spyOn(wrapper.vm.$apollo.queries.initialCiFileContent, 'refetch') - .mockImplementation(jest.fn()); - expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(0); + expect(mockBlobContentData).toHaveBeenCalledTimes(1); - await wrapper.vm.refetchContent(); + findEditorHome().vm.$emit('refetchContent'); - expect(wrapper.vm.$apollo.queries.initialCiFileContent.refetch).toHaveBeenCalledTimes(1); + expect(mockBlobContentData).toHaveBeenCalledTimes(2); }); it('hides start screen when refetch fetches CI file', async () => { @@ -516,7 +479,8 @@ describe('Pipeline editor app component', () => { expect(findEditorHome().exists()).toBe(false); mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse); - await wrapper.vm.$apollo.queries.initialCiFileContent.refetch(); + findEmptyState().vm.$emit('refetchContent'); + await waitForPromises(); expect(findEmptyState().exists()).toBe(false); expect(findEditorHome().exists()).toBe(true); @@ -573,10 +537,6 @@ describe('Pipeline editor app component', () => { mockGetTemplate.mockResolvedValue(mockCiTemplateQueryResponse); await createComponentWithApollo(); - - jest - .spyOn(wrapper.vm.$apollo.queries.commitSha, 'startPolling') - .mockImplementation(jest.fn()); }); it('skips empty state and shows editor home component', () => { diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js index 98f170d8f18..93be4d9d35e 100644 --- a/spec/frontend/ci/runner/components/runner_form_fields_spec.js +++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js @@ -21,6 +21,7 @@ describe('RunnerFormFields', () => { const findInput = (name) => wrapper.find(`input[name="${name}"]`); const expectRendersFields = () => { + expect(wrapper.text()).toContain(s__('Runners|Tags')); expect(wrapper.text()).toContain(s__('Runners|Details')); expect(wrapper.text()).toContain(s__('Runners|Configuration')); @@ -42,10 +43,11 @@ describe('RunnerFormFields', () => { }); it('renders a loading frame', () => { + expect(wrapper.text()).toContain(s__('Runners|Tags')); expect(wrapper.text()).toContain(s__('Runners|Details')); expect(wrapper.text()).toContain(s__('Runners|Configuration')); - expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(2); + expect(wrapper.findAllComponents(GlSkeletonLoader)).toHaveLength(3); expect(wrapper.findAll('input')).toHaveLength(0); }); @@ -101,23 +103,23 @@ describe('RunnerFormFields', () => { it('checks checkbox fields', async () => { createComponent({ value: { + runUntagged: false, paused: false, accessLevel: ACCESS_LEVEL_NOT_PROTECTED, - runUntagged: false, }, }); + findInput('run-untagged').setChecked(true); findInput('paused').setChecked(true); findInput('protected').setChecked(true); - findInput('run-untagged').setChecked(true); await nextTick(); expect(wrapper.emitted('input').at(-1)).toEqual([ { + runUntagged: true, paused: true, accessLevel: ACCESS_LEVEL_REF_PROTECTED, - runUntagged: true, }, ]); }); diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js index 9d521b0b8ca..22797433b58 100644 --- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js +++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js @@ -1,27 +1,46 @@ import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-pipeline-md.svg?url'; import FILTERED_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-search-md.svg?url'; import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; -import { s__ } from '~/locale'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue'; - -import { mockRegistrationToken, newRunnerPath } from 'jest/ci/runner/mock_data'; +import { + I18N_GET_STARTED, + I18N_RUNNERS_ARE_AGENTS, + I18N_CREATE_RUNNER_LINK, + I18N_STILL_USING_REGISTRATION_TOKENS, + I18N_CONTACT_ADMIN_TO_REGISTER, + I18N_FOLLOW_REGISTRATION_INSTRUCTIONS, + I18N_NO_RESULTS, + I18N_EDIT_YOUR_SEARCH, +} from '~/ci/runner/constants'; + +import { + mockRegistrationToken, + newRunnerPath as mockNewRunnerPath, +} from 'jest/ci/runner/mock_data'; import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue'; describe('RunnerListEmptyState', () => { let wrapper; + let glFeatures; const findEmptyState = () => wrapper.findComponent(GlEmptyState); + const findLinks = () => wrapper.findAllComponents(GlLink); const findLink = () => wrapper.findComponent(GlLink); const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal); - const createComponent = ({ props, mountFn = shallowMountExtended, ...options } = {}) => { + const expectTitleToBe = (title) => { + expect(findEmptyState().find('h1').text()).toBe(title); + }; + const expectDescriptionToBe = (sentences) => { + expect(findEmptyState().find('p').text()).toMatchInterpolatedText(sentences.join(' ')); + }; + + const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => { wrapper = mountFn(RunnerListEmptyState, { propsData: { - registrationToken: mockRegistrationToken, - newRunnerPath, ...props, }, directives: { @@ -30,109 +49,146 @@ describe('RunnerListEmptyState', () => { stubs: { GlEmptyState, GlSprintf, - GlLink, }, - ...options, + provide: { glFeatures }, }); }; - describe('when search is not filtered', () => { - const title = s__('Runners|Get started with runners'); + beforeEach(() => { + glFeatures = null; + }); - describe('when there is a registration token', () => { + describe('when search is not filtered', () => { + describe.each([ + { createRunnerWorkflowForAdmin: true }, + { createRunnerWorkflowForNamespace: true }, + ])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => { beforeEach(() => { - createComponent(); - }); - - it('renders an illustration', () => { - expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL); - }); - - it('displays "no results" text with instructions', () => { - const desc = s__( - 'Runners|Runners are the agents that run your CI/CD jobs. Follow the %{linkStart}installation and registration instructions%{linkEnd} to set up a runner.', - ); - - expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); + glFeatures = currentGlFeatures; }); - describe.each([ - { createRunnerWorkflowForAdmin: true }, - { createRunnerWorkflowForNamespace: true }, - ])('when %o', (glFeatures) => { - describe('when newRunnerPath is defined', () => { + describe.each` + newRunnerPath | registrationToken | expectedMessages + ${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]} + ${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]} + ${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]} + ${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]} + `( + 'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken', + ({ newRunnerPath, registrationToken, expectedMessages }) => { beforeEach(() => { createComponent({ - provide: { - glFeatures, + props: { + newRunnerPath, + registrationToken, }, }); }); - it('shows a link to the new runner page', () => { - expect(findLink().attributes('href')).toBe(newRunnerPath); + it('shows title', () => { + expectTitleToBe(I18N_GET_STARTED); }); - }); - describe('when newRunnerPath not defined', () => { - beforeEach(() => { - createComponent({ - props: { - newRunnerPath: null, - }, - provide: { - glFeatures, - }, - }); + it('renders an illustration', () => { + expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL); }); - it('opens a runner registration instructions modal with a link', () => { - const { value } = getBinding(findLink().element, 'gl-modal'); + it(`shows description: "${expectedMessages.join(' ')}"`, () => { + expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]); + }); + }, + ); - expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); + describe('with newRunnerPath and registration token', () => { + beforeEach(() => { + createComponent({ + props: { + registrationToken: mockRegistrationToken, + newRunnerPath: mockNewRunnerPath, + }, }); }); + + it('shows links to the new runner page and registration instructions', () => { + expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath); + + const { value } = getBinding(findLinks().at(1).element, 'gl-modal'); + expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); + }); }); - describe.each([ - { createRunnerWorkflowForAdmin: false }, - { createRunnerWorkflowForNamespace: false }, - ])('when %o', (glFeatures) => { + describe('with newRunnerPath and no registration token', () => { beforeEach(() => { createComponent({ - provide: { - glFeatures, + props: { + registrationToken: mockRegistrationToken, + newRunnerPath: null, }, }); }); it('opens a runner registration instructions modal with a link', () => { const { value } = getBinding(findLink().element, 'gl-modal'); - expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); }); }); - }); - describe('when there is no registration token', () => { - beforeEach(() => { - createComponent({ props: { registrationToken: null } }); - }); + describe('with no newRunnerPath nor registration token', () => { + beforeEach(() => { + createComponent({ + props: { + registrationToken: null, + newRunnerPath: null, + }, + }); + }); - it('renders an illustration', () => { - expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL); + it('has no link', () => { + expect(findLink().exists()).toBe(false); + }); }); + }); + + describe('when createRunnerWorkflow is disabled', () => { + describe('when there is a registration token', () => { + beforeEach(() => { + createComponent({ + props: { + registrationToken: mockRegistrationToken, + }, + }); + }); + + it('renders an illustration', () => { + expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL); + }); + + it('opens a runner registration instructions modal with a link', () => { + const { value } = getBinding(findLink().element, 'gl-modal'); + expect(findRunnerInstructionsModal().props('modalId')).toEqual(value); + }); - it('displays "no results" text', () => { - const desc = s__( - 'Runners|Runners are the agents that run your CI/CD jobs. To register new runners, please contact your administrator.', - ); + it('displays text with registration instructions', () => { + expectTitleToBe(I18N_GET_STARTED); - expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`); + expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]); + }); }); - it('has no registration instructions link', () => { - expect(findLink().exists()).toBe(false); + describe('when there is no registration token', () => { + beforeEach(() => { + createComponent({ props: { registrationToken: null } }); + }); + + it('displays "contact admin" text', () => { + expectTitleToBe(I18N_GET_STARTED); + + expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]); + }); + + it('has no registration instructions link', () => { + expect(findLink().exists()).toBe(false); + }); }); }); }); @@ -147,8 +203,9 @@ describe('RunnerListEmptyState', () => { }); it('displays "no filtered results" text', () => { - expect(findEmptyState().text()).toContain(s__('Runners|No results found')); - expect(findEmptyState().text()).toContain(s__('Runners|Edit your search and try again')); + expectTitleToBe(I18N_NO_RESULTS); + + expectDescriptionToBe([I18N_EDIT_YOUR_SEARCH]); }); }); }); diff --git a/spec/frontend/commit/components/refs_list_spec.js b/spec/frontend/commit/components/refs_list_spec.js index 594f8827d58..cc783dc3b58 100644 --- a/spec/frontend/commit/components/refs_list_spec.js +++ b/spec/frontend/commit/components/refs_list_spec.js @@ -61,7 +61,7 @@ describe('Commit references component', () => { it('renders links to refs', () => { const index = 0; const refBadge = findTippingRefs().at(index); - const refUrl = `${refsListPropsMock.urlPart}${refsListPropsMock.tippingRefs[index]}`; + const refUrl = `${refsListPropsMock.urlPart}${refsListPropsMock.tippingRefs[index]}?ref_type=${refsListPropsMock.refType}`; expect(refBadge.attributes('href')).toBe(refUrl); }); diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js index 9c8f9266986..2a618e08c50 100644 --- a/spec/frontend/commit/mock_data.js +++ b/spec/frontend/commit/mock_data.js @@ -289,4 +289,5 @@ export const refsListPropsMock = { tippingRefs: tippingBranchesMock, isLoading: false, urlPart: '/some/project/-/commits/', + refType: 'heads', }; diff --git a/spec/frontend/fixtures/pipeline_header.rb b/spec/frontend/fixtures/pipeline_header.rb index a4fba7e8675..d25bf12623f 100644 --- a/spec/frontend/fixtures/pipeline_header.rb +++ b/spec/frontend/fixtures/pipeline_header.rb @@ -51,6 +51,8 @@ RSpec.describe "GraphQL Pipeline Header", '(JavaScript fixtures)', type: :reques ) end + let_it_be(:build) { create(:ci_build, :running, pipeline: pipeline, ref: 'master') } + it "graphql/pipelines/pipeline_header_running.json" do query = get_graphql_query_as_string(query_path) @@ -59,4 +61,29 @@ RSpec.describe "GraphQL Pipeline Header", '(JavaScript fixtures)', type: :reques expect_graphql_errors_to_be_empty end end + + context 'with failed pipeline' do + let_it_be(:pipeline) do + create( + :ci_pipeline, + project: project, + sha: commit.id, + ref: 'master', + user: user, + status: :failed, + started_at: 1.hour.ago, + finished_at: Time.current + ) + end + + let_it_be(:build) { create(:ci_build, :canceled, pipeline: pipeline, ref: 'master') } + + it "graphql/pipelines/pipeline_header_failed.json" do + query = get_graphql_query_as_string(query_path) + + post_graphql(query, current_user: user, variables: { fullPath: project.full_path, iid: pipeline.iid }) + + expect_graphql_errors_to_be_empty + end + end end diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js index 5c36dbf9c9c..2b60684e60a 100644 --- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js @@ -1,22 +1,37 @@ -import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon } from '@gitlab/ui'; +import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon, GlModal } from '@gitlab/ui'; import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { stubComponent } from 'helpers/stub_component'; import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import Tracking from '~/tracking'; import { s__ } from '~/locale'; +import { createAlert } from '~/alert'; import { packageFiles as packageFilesMock, packageFilesQuery, + packageDestroyFilesMutation, + packageDestroyFilesMutationError, } from 'jest/packages_and_registries/package_registry/mock_data'; +import { + DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION, + DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT, + DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT, + DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + DELETE_PACKAGE_FILE_ERROR_MESSAGE, + DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, + DELETE_PACKAGE_FILES_ERROR_MESSAGE, +} from '~/packages_and_registries/package_registry/constants'; import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue'; import FileIcon from '~/vue_shared/components/file_icon.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import getPackageFiles from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql'; +import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql'; Vue.use(VueApollo); +jest.mock('~/alert'); describe('Package Files', () => { let wrapper; @@ -24,6 +39,7 @@ describe('Package Files', () => { const findAllRows = () => wrapper.findAllByTestId('file-row'); const findDeleteSelectedButton = () => wrapper.findByTestId('delete-selected'); + const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal'); const findFirstRow = () => extendedWrapper(findAllRows().at(0)); const findSecondRow = () => extendedWrapper(findAllRows().at(1)); const findPackageFilesAlert = () => wrapper.findComponent(GlAlert); @@ -41,27 +57,39 @@ describe('Package Files', () => { const files = packageFilesMock(); const [file] = files; + const showMock = jest.fn(); + const eventCategory = 'UI::NpmPackages'; + const createComponent = ({ packageId = '1', packageType = 'NPM', - isLoading = false, + projectPath = 'gitlab-test', canDelete = true, stubs, - resolver = jest.fn().mockResolvedValue(packageFilesQuery([file])), + resolver = jest.fn().mockResolvedValue(packageFilesQuery({ files: [file] })), + filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()), } = {}) => { - const requestHandlers = [[getPackageFiles, resolver]]; + const requestHandlers = [ + [getPackageFiles, resolver], + [destroyPackageFilesMutation, filesDeleteMutationResolver], + ]; apolloProvider = createMockApollo(requestHandlers); wrapper = mountExtended(PackageFiles, { apolloProvider, propsData: { canDelete, - isLoading, packageId, packageType, + projectPath, }, stubs: { GlTable: false, + GlModal: stubComponent(GlModal, { + methods: { + show: showMock, + }, + }), ...stubs, }, }); @@ -122,10 +150,16 @@ describe('Package Files', () => { expect(findFirstRowDownloadLink().attributes('href')).toBe(file.downloadPath); }); - it('emits "download-file" event on click', () => { + it('tracks "download-file" event on click', () => { + const eventSpy = jest.spyOn(Tracking, 'event'); + findFirstRowDownloadLink().vm.$emit('click'); - expect(wrapper.emitted('download-file')).toEqual([[]]); + expect(eventSpy).toHaveBeenCalledWith( + eventCategory, + DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION, + expect.any(Object), + ); }); }); @@ -179,12 +213,14 @@ describe('Package Files', () => { expect(findActionMenuDelete().exists()).toBe(true); }); - it('emits a delete event when clicked', async () => { + it('shows delete file confirmation modal', async () => { await findActionMenuDelete().trigger('click'); - const [[items]] = wrapper.emitted('delete-files'); - const [{ id }] = items; - expect(id).toBe(file.id); + expect(showMock).toHaveBeenCalledTimes(1); + + expect(findDeleteFilesModal().text()).toBe( + 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?', + ); }); }); }); @@ -213,21 +249,6 @@ describe('Package Files', () => { expect(findDeleteSelectedButton().props('disabled')).toBe(true); }); - it('delete selected button exists & is disabled when isLoading prop is true', async () => { - createComponent(); - await waitForPromises(); - const first = findAllRowCheckboxes().at(0); - - await first.setChecked(true); - - expect(findDeleteSelectedButton().props('disabled')).toBe(false); - - await wrapper.setProps({ isLoading: true }); - - expect(findDeleteSelectedButton().props('disabled')).toBe(true); - expect(findLoadingIcon().exists()).toBe(true); - }); - it('checkboxes to select file are visible', async () => { createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); await waitForPromises(); @@ -295,7 +316,7 @@ describe('Package Files', () => { }); }); - it('emits a delete event when selected', async () => { + it('shows delete modal with single file confirmation text when delete selected is clicked', async () => { createComponent(); await waitForPromises(); @@ -305,12 +326,14 @@ describe('Package Files', () => { await findDeleteSelectedButton().trigger('click'); - const [[items]] = wrapper.emitted('delete-files'); - const [{ id }] = items; - expect(id).toBe(file.id); + expect(showMock).toHaveBeenCalledTimes(1); + + expect(findDeleteFilesModal().text()).toBe( + 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?', + ); }); - it('emits delete event with both items when all are selected', async () => { + it('shows delete modal with multiple files confirmation text when delete selected is clicked', async () => { createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) }); await waitForPromises(); @@ -318,8 +341,63 @@ describe('Package Files', () => { await findDeleteSelectedButton().trigger('click'); - const [[items]] = wrapper.emitted('delete-files'); - expect(items).toHaveLength(2); + expect(showMock).toHaveBeenCalledTimes(1); + + expect(findDeleteFilesModal().text()).toMatchInterpolatedText( + 'You are about to delete 2 assets. This operation is irreversible.', + ); + }); + + describe('emits delete-all-files event', () => { + it('with right content for last file in package', async () => { + createComponent({ + resolver: jest.fn().mockResolvedValue( + packageFilesQuery({ + files: [file], + pageInfo: { + hasNextPage: false, + }, + }), + ), + }); + await waitForPromises(); + const first = findAllRowCheckboxes().at(0); + + await first.setChecked(true); + + await findDeleteSelectedButton().trigger('click'); + + expect(showMock).toHaveBeenCalledTimes(0); + + expect(wrapper.emitted('delete-all-files')).toHaveLength(1); + expect(wrapper.emitted('delete-all-files')[0]).toEqual([ + DELETE_LAST_PACKAGE_FILE_MODAL_CONTENT, + ]); + }); + + it('with right content for all files in package', async () => { + createComponent({ + resolver: jest.fn().mockResolvedValue( + packageFilesQuery({ + pageInfo: { + hasNextPage: false, + }, + }), + ), + }); + await waitForPromises(); + + await findCheckAllCheckbox().setChecked(true); + + await findDeleteSelectedButton().trigger('click'); + + expect(showMock).toHaveBeenCalledTimes(0); + + expect(wrapper.emitted('delete-all-files')).toHaveLength(1); + expect(wrapper.emitted('delete-all-files')[0]).toEqual([ + DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT, + ]); + }); }); }); @@ -343,6 +421,195 @@ describe('Package Files', () => { }); }); + describe('deleting a file', () => { + const doDeleteFile = async () => { + const first = findAllRowCheckboxes().at(0); + + await first.setChecked(true); + + await findDeleteSelectedButton().trigger('click'); + + findDeleteFilesModal().vm.$emit('primary'); + }; + + it('confirming on the modal sets the loading state', async () => { + createComponent(); + + await waitForPromises(); + + await doDeleteFile(); + + await nextTick(); + + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('confirming on the modal deletes the file and shows a success message', async () => { + const resolver = jest.fn().mockResolvedValue(packageFilesQuery({ files: [file] })); + const filesDeleteMutationResolver = jest + .fn() + .mockResolvedValue(packageDestroyFilesMutation()); + createComponent({ resolver, filesDeleteMutationResolver }); + + await waitForPromises(); + + await doDeleteFile(); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, + }), + ); + + expect(filesDeleteMutationResolver).toHaveBeenCalledWith({ + ids: [file.id], + projectPath: 'gitlab-test', + }); + + // we are re-fetching the package files, so we expect the resolver to have been called twice + expect(resolver).toHaveBeenCalledTimes(2); + expect(resolver).toHaveBeenCalledWith({ + id: '1', + first: 100, + }); + }); + + describe('errors', () => { + it('shows an error when the mutation request fails', async () => { + createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() }); + await waitForPromises(); + + await doDeleteFile(); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + }), + ); + }); + + it('shows an error when the mutation request returns an error payload', async () => { + createComponent({ + filesDeleteMutationResolver: jest + .fn() + .mockResolvedValue(packageDestroyFilesMutationError()), + }); + await waitForPromises(); + + await doDeleteFile(); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, + }), + ); + }); + }); + }); + + describe('deleting multiple files', () => { + const doDeleteFiles = async () => { + await findCheckAllCheckbox().setChecked(true); + + await findDeleteSelectedButton().trigger('click'); + + findDeleteFilesModal().vm.$emit('primary'); + }; + + it('confirming on the modal sets the loading state', async () => { + createComponent(); + + await waitForPromises(); + + await doDeleteFiles(); + + await nextTick(); + + expect(findLoadingIcon().exists()).toBe(true); + }); + + it('confirming on the modal deletes the file and shows a success message', async () => { + const resolver = jest.fn().mockResolvedValue(packageFilesQuery()); + const filesDeleteMutationResolver = jest + .fn() + .mockResolvedValue(packageDestroyFilesMutation()); + createComponent({ resolver, filesDeleteMutationResolver }); + + await waitForPromises(); + + await doDeleteFiles(); + + await waitForPromises(); + + expect(findLoadingIcon().exists()).toBe(false); + + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, + }), + ); + + expect(filesDeleteMutationResolver).toHaveBeenCalledWith({ + ids: files.map(({ id }) => id), + projectPath: 'gitlab-test', + }); + + // we are re-fetching the package files, so we expect the resolver to have been called twice + expect(resolver).toHaveBeenCalledTimes(2); + expect(resolver).toHaveBeenCalledWith({ + id: '1', + first: 100, + }); + }); + + describe('errors', () => { + it('shows an error when the mutation request fails', async () => { + const resolver = jest.fn().mockResolvedValue(packageFilesQuery()); + createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue(), resolver }); + await waitForPromises(); + + await doDeleteFiles(); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILES_ERROR_MESSAGE, + }), + ); + }); + + it('shows an error when the mutation request returns an error payload', async () => { + const resolver = jest.fn().mockResolvedValue(packageFilesQuery()); + createComponent({ + filesDeleteMutationResolver: jest + .fn() + .mockResolvedValue(packageDestroyFilesMutationError()), + resolver, + }); + await waitForPromises(); + + await doDeleteFiles(); + + await waitForPromises(); + + expect(createAlert).toHaveBeenCalledWith( + expect.objectContaining({ + message: DELETE_PACKAGE_FILES_ERROR_MESSAGE, + }), + ); + }); + }); + }); + describe('additional details', () => { describe('details toggle button', () => { it('exists', async () => { @@ -357,7 +624,9 @@ describe('Package Files', () => { noShaFile.fileSha256 = null; noShaFile.fileMd5 = null; noShaFile.fileSha1 = null; - createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([noShaFile])) }); + createComponent({ + resolver: jest.fn().mockResolvedValue(packageFilesQuery({ files: [noShaFile] })), + }); await waitForPromises(); expect(findFirstToggleDetailsButton().exists()).toBe(false); @@ -410,7 +679,9 @@ describe('Package Files', () => { const { ...missingMd5 } = file; missingMd5.fileMd5 = null; - createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery([missingMd5])) }); + createComponent({ + resolver: jest.fn().mockResolvedValue(packageFilesQuery({ files: [missingMd5] })), + }); await waitForPromises(); await showShaFiles(); diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js index fa6a69b1a1f..f1dab38a9e6 100644 --- a/spec/frontend/packages_and_registries/package_registry/mock_data.js +++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js @@ -254,9 +254,6 @@ export const packageDetailsQuery = ({ __typename: 'PipelineConnection', }, packageFiles: { - pageInfo: { - hasNextPage: true, - }, nodes: packageFiles().map(({ id, size }) => ({ id, size })), __typename: 'PackageFileConnection', }, @@ -285,11 +282,15 @@ export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({ }, }); -export const packageFilesQuery = (files = packageFiles()) => ({ +export const packageFilesQuery = ({ files = packageFiles(), pageInfo = {} } = {}) => ({ data: { package: { id: 'gid://gitlab/Packages::Package/111', packageFiles: { + pageInfo: { + hasNextPage: true, + ...pageInfo, + }, nodes: files, __typename: 'PackageFileConnection', }, diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js index 8b15dfd7d4a..0f91a7aeb50 100644 --- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js +++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js @@ -21,10 +21,7 @@ import { REQUEST_FORWARDING_HELP_PAGE_PATH, FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, PACKAGE_TYPE_COMPOSER, - DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, - DELETE_PACKAGE_FILE_ERROR_MESSAGE, - DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, - DELETE_PACKAGE_FILES_ERROR_MESSAGE, + DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT, PACKAGE_TYPE_NUGET, PACKAGE_TYPE_MAVEN, PACKAGE_TYPE_CONAN, @@ -32,7 +29,6 @@ import { PACKAGE_TYPE_NPM, } from '~/packages_and_registries/package_registry/constants'; -import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql//queries/get_package_versions.query.graphql'; import { @@ -41,9 +37,6 @@ import { packageVersions, dependencyLinks, emptyPackageDetailsQuery, - packageFiles, - packageDestroyFilesMutation, - packageDestroyFilesMutationError, defaultPackageGroupSettings, } from '../mock_data'; @@ -74,13 +67,9 @@ describe('PackagesApp', () => { function createComponent({ resolver = jest.fn().mockResolvedValue(packageDetailsQuery()), - filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()), routeId = '1', } = {}) { - const requestHandlers = [ - [getPackageDetails, resolver], - [destroyPackageFilesMutation, filesDeleteMutationResolver], - ]; + const requestHandlers = [[getPackageDetails, resolver]]; apolloProvider = createMockApollo(requestHandlers); wrapper = shallowMountExtended(PackagesApp, { @@ -117,8 +106,6 @@ describe('PackagesApp', () => { const findDeleteModal = () => wrapper.findByTestId('delete-modal'); const findDeleteButton = () => wrapper.findByTestId('delete-package'); const findPackageFiles = () => wrapper.findComponent(PackageFiles); - const findDeleteFileModal = () => wrapper.findByTestId('delete-file-modal'); - const findDeleteFilesModal = () => wrapper.findByTestId('delete-files-modal'); const findVersionsList = () => wrapper.findComponent(PackageVersionsList); const findVersionsCountBadge = () => wrapper.findByTestId('other-versions-badge'); const findNoVersionsMessage = () => wrapper.findByTestId('no-versions-message'); @@ -336,9 +323,9 @@ describe('PackagesApp', () => { expect(findPackageFiles().props()).toMatchObject({ canDelete: packageData().canDestroy, - isLoading: false, packageId: packageData().id, packageType: packageData().packageType, + projectPath: 'gitlab-test', }); }); @@ -356,250 +343,26 @@ describe('PackagesApp', () => { expect(findPackageFiles().exists()).toBe(false); }); - describe('deleting a file', () => { - const [fileToDelete] = packageFiles(); - - const doDeleteFile = () => { - findPackageFiles().vm.$emit('delete-files', [fileToDelete]); - - findDeleteFileModal().vm.$emit('primary'); - - return waitForPromises(); - }; - - it('opens delete file confirmation modal', async () => { - createComponent(); - - await waitForPromises(); - - findPackageFiles().vm.$emit('delete-files', [fileToDelete]); - - expect(showMock).toHaveBeenCalledTimes(1); - - await waitForPromises(); - - expect(findDeleteFileModal().text()).toBe( - 'You are about to delete foo-1.0.1.tgz. This is a destructive action that may render your package unusable. Are you sure?', - ); - }); - - it('when its the only file opens delete package confirmation modal', async () => { - const [packageFile] = packageFiles(); + describe('emits delete-all-files event', () => { + it('opens the delete package confirmation modal and shows confirmation text', async () => { const resolver = jest.fn().mockResolvedValue( packageDetailsQuery({ - extendPackage: { - packageFiles: { - pageInfo: { - hasNextPage: false, - }, - nodes: [packageFile], - __typename: 'PackageFileConnection', - }, - }, + extendPackage: {}, packageSettings: { ...defaultPackageGroupSettings, npmPackageRequestsForwarding: false, }, }), ); - - createComponent({ - resolver, - }); - - await waitForPromises(); - - findPackageFiles().vm.$emit('delete-files', [fileToDelete]); - - expect(showMock).toHaveBeenCalledTimes(1); - - await waitForPromises(); - - expect(findDeleteModal().text()).toBe( - 'Deleting the last package asset will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?', - ); - }); - - it('confirming on the modal sets the loading state', async () => { - createComponent(); - - await waitForPromises(); - - findPackageFiles().vm.$emit('delete-files', [fileToDelete]); - - findDeleteFileModal().vm.$emit('primary'); - - await nextTick(); - - expect(findPackageFiles().props('isLoading')).toEqual(true); - }); - - it('confirming on the modal deletes the file and shows a success message', async () => { - const resolver = jest.fn().mockResolvedValue(packageDetailsQuery()); - createComponent({ resolver }); - - await waitForPromises(); - - await doDeleteFile(); - - expect(createAlert).toHaveBeenCalledWith( - expect.objectContaining({ - message: DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, - }), - ); - // we are re-fetching the package details, so we expect the resolver to have been called twice - expect(resolver).toHaveBeenCalledTimes(2); - }); - - describe('errors', () => { - it('shows an error when the mutation request fails', async () => { - createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() }); - await waitForPromises(); - - await doDeleteFile(); - - expect(createAlert).toHaveBeenCalledWith( - expect.objectContaining({ - message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, - }), - ); - }); - - it('shows an error when the mutation request returns an error payload', async () => { - createComponent({ - filesDeleteMutationResolver: jest - .fn() - .mockResolvedValue(packageDestroyFilesMutationError()), - }); - await waitForPromises(); - - await doDeleteFile(); - - expect(createAlert).toHaveBeenCalledWith( - expect.objectContaining({ - message: DELETE_PACKAGE_FILE_ERROR_MESSAGE, - }), - ); - }); - }); - }); - - describe('deleting multiple files', () => { - const doDeleteFiles = () => { - findPackageFiles().vm.$emit('delete-files', packageFiles()); - - findDeleteFilesModal().vm.$emit('primary'); - - return waitForPromises(); - }; - - it('opens delete files confirmation modal', async () => { - createComponent(); - - await waitForPromises(); - - const showDeleteFilesSpy = jest.spyOn(wrapper.vm.$refs.deleteFilesModal, 'show'); - - findPackageFiles().vm.$emit('delete-files', packageFiles()); - - expect(showDeleteFilesSpy).toHaveBeenCalled(); - }); - - it('confirming on the modal sets the loading state', async () => { - createComponent(); - - await waitForPromises(); - - findPackageFiles().vm.$emit('delete-files', packageFiles()); - - findDeleteFilesModal().vm.$emit('primary'); - - await nextTick(); - - expect(findPackageFiles().props('isLoading')).toEqual(true); - }); - - it('confirming on the modal deletes the file and shows a success message', async () => { - const resolver = jest.fn().mockResolvedValue(packageDetailsQuery()); createComponent({ resolver }); await waitForPromises(); - await doDeleteFiles(); - - expect(resolver).toHaveBeenCalledTimes(2); - - expect(createAlert).toHaveBeenCalledWith( - expect.objectContaining({ - message: DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, - }), - ); - // we are re-fetching the package details, so we expect the resolver to have been called twice - expect(resolver).toHaveBeenCalledTimes(2); - }); - - describe('errors', () => { - it('shows an error when the mutation request fails', async () => { - createComponent({ filesDeleteMutationResolver: jest.fn().mockRejectedValue() }); - await waitForPromises(); - - await doDeleteFiles(); - - expect(createAlert).toHaveBeenCalledWith( - expect.objectContaining({ - message: DELETE_PACKAGE_FILES_ERROR_MESSAGE, - }), - ); - }); - - it('shows an error when the mutation request returns an error payload', async () => { - createComponent({ - filesDeleteMutationResolver: jest - .fn() - .mockResolvedValue(packageDestroyFilesMutationError()), - }); - await waitForPromises(); - - await doDeleteFiles(); - - expect(createAlert).toHaveBeenCalledWith( - expect.objectContaining({ - message: DELETE_PACKAGE_FILES_ERROR_MESSAGE, - }), - ); - }); - }); - }); - - describe('deleting all files', () => { - it('opens the delete package confirmation modal', async () => { - const resolver = jest.fn().mockResolvedValue( - packageDetailsQuery({ - extendPackage: { - packageFiles: { - pageInfo: { - hasNextPage: false, - }, - nodes: packageFiles(), - }, - }, - packageSettings: { - ...defaultPackageGroupSettings, - npmPackageRequestsForwarding: false, - }, - }), - ); - createComponent({ - resolver, - }); - - await waitForPromises(); - - findPackageFiles().vm.$emit('delete-files', packageFiles()); + findPackageFiles().vm.$emit('delete-all-files', DELETE_ALL_PACKAGE_FILES_MODAL_CONTENT); expect(showMock).toHaveBeenCalledTimes(1); - await waitForPromises(); + await nextTick(); expect(findDeleteModal().text()).toBe( 'Deleting all package assets will remove version 1.0.0 of @gitlab-org/package-15. Are you sure?', diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js index fd654eb6f10..8bbe0ef78c0 100644 --- a/spec/frontend/pipelines/mock_data.js +++ b/spec/frontend/pipelines/mock_data.js @@ -1,5 +1,6 @@ import pipelineHeaderSuccess from 'test_fixtures/graphql/pipelines/pipeline_header_success.json'; import pipelineHeaderRunning from 'test_fixtures/graphql/pipelines/pipeline_header_running.json'; +import pipelineHeaderFailed from 'test_fixtures/graphql/pipelines/pipeline_header_failed.json'; const PIPELINE_RUNNING = 'RUNNING'; const PIPELINE_CANCELED = 'CANCELED'; @@ -8,7 +9,31 @@ const PIPELINE_FAILED = 'FAILED'; const threeWeeksAgo = new Date(); threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21); -export { pipelineHeaderSuccess, pipelineHeaderRunning }; +export { pipelineHeaderSuccess, pipelineHeaderRunning, pipelineHeaderFailed }; + +export const pipelineRetryMutationResponseSuccess = { + data: { pipelineRetry: { errors: [] } }, +}; + +export const pipelineRetryMutationResponseFailed = { + data: { pipelineRetry: { errors: ['error'] } }, +}; + +export const pipelineCancelMutationResponseSuccess = { + data: { pipelineRetry: { errors: [] } }, +}; + +export const pipelineCancelMutationResponseFailed = { + data: { pipelineRetry: { errors: ['error'] } }, +}; + +export const pipelineDeleteMutationResponseSuccess = { + data: { pipelineRetry: { errors: [] } }, +}; + +export const pipelineDeleteMutationResponseFailed = { + data: { pipelineRetry: { errors: ['error'] } }, +}; export const mockPipelineHeader = { detailedStatus: {}, diff --git a/spec/frontend/pipelines/pipeline_details_header_spec.js b/spec/frontend/pipelines/pipeline_details_header_spec.js index 08ae35fe808..7141e10fb17 100644 --- a/spec/frontend/pipelines/pipeline_details_header_spec.js +++ b/spec/frontend/pipelines/pipeline_details_header_spec.js @@ -1,23 +1,59 @@ -import { GlBadge, GlLoadingIcon } from '@gitlab/ui'; -import Vue from 'vue'; +import { GlAlert, GlBadge, GlLoadingIcon, GlModal } from '@gitlab/ui'; +import Vue, { nextTick } from 'vue'; import VueApollo from 'vue-apollo'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import createMockApollo from 'helpers/mock_apollo_helper'; import waitForPromises from 'helpers/wait_for_promises'; import PipelineDetailsHeader from '~/pipelines/components/pipeline_details_header.vue'; +import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants'; import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue'; import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; +import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql'; +import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql'; +import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql'; import getPipelineDetailsQuery from '~/pipelines/graphql/queries/get_pipeline_header_data.query.graphql'; -import { pipelineHeaderSuccess, pipelineHeaderRunning } from './mock_data'; +import { + pipelineHeaderSuccess, + pipelineHeaderRunning, + pipelineHeaderFailed, + pipelineRetryMutationResponseSuccess, + pipelineCancelMutationResponseSuccess, + pipelineDeleteMutationResponseSuccess, + pipelineRetryMutationResponseFailed, + pipelineCancelMutationResponseFailed, + pipelineDeleteMutationResponseFailed, +} from './mock_data'; Vue.use(VueApollo); describe('Pipeline details header', () => { let wrapper; + let glModalDirective; const successHandler = jest.fn().mockResolvedValue(pipelineHeaderSuccess); const runningHandler = jest.fn().mockResolvedValue(pipelineHeaderRunning); + const failedHandler = jest.fn().mockResolvedValue(pipelineHeaderFailed); + const retryMutationHandlerSuccess = jest + .fn() + .mockResolvedValue(pipelineRetryMutationResponseSuccess); + const cancelMutationHandlerSuccess = jest + .fn() + .mockResolvedValue(pipelineCancelMutationResponseSuccess); + const deleteMutationHandlerSuccess = jest + .fn() + .mockResolvedValue(pipelineDeleteMutationResponseSuccess); + const retryMutationHandlerFailed = jest + .fn() + .mockResolvedValue(pipelineRetryMutationResponseFailed); + const cancelMutationHandlerFailed = jest + .fn() + .mockResolvedValue(pipelineCancelMutationResponseFailed); + const deleteMutationHandlerFailed = jest + .fn() + .mockResolvedValue(pipelineDeleteMutationResponseFailed); + + const findAlert = () => wrapper.findComponent(GlAlert); const findStatus = () => wrapper.findComponent(CiBadgeLink); const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findTimeAgo = () => wrapper.findComponent(TimeAgo); @@ -28,6 +64,10 @@ describe('Pipeline details header', () => { const findCommitLink = () => wrapper.findByTestId('commit-link'); const findPipelineRunningText = () => wrapper.findByTestId('pipeline-running-text').text(); const findPipelineRefText = () => wrapper.findByTestId('pipeline-ref-text').text(); + const findRetryButton = () => wrapper.findByTestId('retry-pipeline'); + const findCancelButton = () => wrapper.findByTestId('cancel-pipeline'); + const findDeleteButton = () => wrapper.findByTestId('delete-pipeline'); + const findDeleteModal = () => wrapper.findComponent(GlModal); const defaultHandlers = [[getPipelineDetailsQuery, successHandler]]; @@ -58,7 +98,7 @@ describe('Pipeline details header', () => { stuck: false, }, refText: - 'For merge request <a class="mr-iid" href="/root/ci-project/-/merge_requests/1">!1</a> to merge <a class="ref-name" href="/root/ci-project/-/commits/test">test</a>', + 'Related merge request <a class="mr-iid" href="/root/ci-project/-/merge_requests/1">!1</a> to merge <a class="ref-name" href="/root/ci-project/-/commits/test">test</a>', }; const createMockApolloProvider = (handlers) => { @@ -66,6 +106,8 @@ describe('Pipeline details header', () => { }; const createComponent = (handlers = defaultHandlers, props = defaultProps) => { + glModalDirective = jest.fn(); + wrapper = shallowMountExtended(PipelineDetailsHeader, { provide: { ...defaultProvideOptions, @@ -73,6 +115,13 @@ describe('Pipeline details header', () => { propsData: { ...props, }, + directives: { + glModal: { + bind(_, { value }) { + glModalDirective(value); + }, + }, + }, apolloProvider: createMockApolloProvider(handlers), }); }; @@ -125,7 +174,7 @@ describe('Pipeline details header', () => { }); it('displays ref text', () => { - expect(findPipelineRefText()).toBe('For merge request !1 to merge test'); + expect(findPipelineRefText()).toBe('Related merge request !1 to merge test'); }); }); @@ -164,4 +213,155 @@ describe('Pipeline details header', () => { expect(findPipelineRunningText()).toBe('In progress, queued for 3600 seconds'); }); }); + + describe('actions', () => { + describe('retry action', () => { + beforeEach(async () => { + createComponent([ + [getPipelineDetailsQuery, failedHandler], + [retryPipelineMutation, retryMutationHandlerSuccess], + ]); + + await waitForPromises(); + }); + + it('should call retryPipeline Mutation with pipeline id', () => { + findRetryButton().vm.$emit('click'); + + expect(retryMutationHandlerSuccess).toHaveBeenCalledWith({ + id: pipelineHeaderFailed.data.project.pipeline.id, + }); + expect(findAlert().exists()).toBe(false); + }); + + it('should render retry action tooltip', () => { + expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY); + }); + }); + + describe('retry action failed', () => { + beforeEach(async () => { + createComponent([ + [getPipelineDetailsQuery, failedHandler], + [retryPipelineMutation, retryMutationHandlerFailed], + ]); + + await waitForPromises(); + }); + + it('should display error message on failure', async () => { + findRetryButton().vm.$emit('click'); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + }); + + it('retry button loading state should reset on error', async () => { + findRetryButton().vm.$emit('click'); + + await nextTick(); + + expect(findRetryButton().props('loading')).toBe(true); + + await waitForPromises(); + + expect(findRetryButton().props('loading')).toBe(false); + }); + }); + + describe('cancel action', () => { + it('should call cancelPipeline Mutation with pipeline id', async () => { + createComponent([ + [getPipelineDetailsQuery, runningHandler], + [cancelPipelineMutation, cancelMutationHandlerSuccess], + ]); + + await waitForPromises(); + + findCancelButton().vm.$emit('click'); + + expect(cancelMutationHandlerSuccess).toHaveBeenCalledWith({ + id: pipelineHeaderRunning.data.project.pipeline.id, + }); + expect(findAlert().exists()).toBe(false); + }); + + it('should render cancel action tooltip', async () => { + createComponent([ + [getPipelineDetailsQuery, runningHandler], + [cancelPipelineMutation, cancelMutationHandlerSuccess], + ]); + + await waitForPromises(); + + expect(findCancelButton().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL); + }); + + it('should display error message on failure', async () => { + createComponent([ + [getPipelineDetailsQuery, runningHandler], + [cancelPipelineMutation, cancelMutationHandlerFailed], + ]); + + await waitForPromises(); + + findCancelButton().vm.$emit('click'); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + }); + }); + + describe('delete action', () => { + it('displays delete modal when clicking on delete and does not call the delete action', async () => { + createComponent([ + [getPipelineDetailsQuery, successHandler], + [deletePipelineMutation, deleteMutationHandlerSuccess], + ]); + + await waitForPromises(); + + findDeleteButton().vm.$emit('click'); + + const modalId = 'pipeline-delete-modal'; + + expect(findDeleteModal().props('modalId')).toBe(modalId); + expect(glModalDirective).toHaveBeenCalledWith(modalId); + expect(deleteMutationHandlerSuccess).not.toHaveBeenCalled(); + expect(findAlert().exists()).toBe(false); + }); + + it('should call deletePipeline Mutation with pipeline id when modal is submitted', async () => { + createComponent([ + [getPipelineDetailsQuery, successHandler], + [deletePipelineMutation, deleteMutationHandlerSuccess], + ]); + + await waitForPromises(); + + findDeleteModal().vm.$emit('primary'); + + expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({ + id: pipelineHeaderSuccess.data.project.pipeline.id, + }); + }); + + it('should display error message on failure', async () => { + createComponent([ + [getPipelineDetailsQuery, successHandler], + [deletePipelineMutation, deleteMutationHandlerFailed], + ]); + + await waitForPromises(); + + findDeleteModal().vm.$emit('primary'); + + await waitForPromises(); + + expect(findAlert().exists()).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js index e3c9983aa52..43336bbc748 100644 --- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js +++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js @@ -1,9 +1,11 @@ +import { nextTick } from 'vue'; import { GlAlert, GlDropdown, GlSprintf, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import { mockTracking, unmockTracking } from 'helpers/tracking_helper'; import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import waitForPromises from 'helpers/wait_for_promises'; +import { stubComponent } from 'helpers/stub_component'; import axios from '~/lib/utils/axios_utils'; import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status'; import PipelineMultiActions, { @@ -14,6 +16,7 @@ import { TRACKING_CATEGORIES } from '~/pipelines/constants'; describe('Pipeline Multi Actions Dropdown', () => { let wrapper; let mockAxios; + const focusInputMock = jest.fn(); const artifacts = [ { @@ -30,7 +33,7 @@ describe('Pipeline Multi Actions Dropdown', () => { const artifactsEndpoint = `endpoint/${artifactsEndpointPlaceholder}/artifacts.json`; const pipelineId = 108; - const createComponent = ({ mockData = {} } = {}) => { + const createComponent = () => { wrapper = extendedWrapper( shallowMount(PipelineMultiActions, { provide: { @@ -40,14 +43,12 @@ describe('Pipeline Multi Actions Dropdown', () => { propsData: { pipelineId, }, - data() { - return { - ...mockData, - }; - }, stubs: { GlSprintf, GlDropdown, + GlSearchBoxByType: stubComponent(GlSearchBoxByType, { + methods: { focusInput: focusInputMock }, + }), }, }), ); @@ -76,70 +77,91 @@ describe('Pipeline Multi Actions Dropdown', () => { }); describe('Artifacts', () => { - it('should fetch artifacts and show search box on dropdown click', async () => { - const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId); - mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts }); - createComponent(); - findDropdown().vm.$emit('show'); - await waitForPromises(); + const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId); - expect(mockAxios.history.get).toHaveLength(1); - expect(wrapper.vm.artifacts).toEqual(artifacts); - expect(findSearchBox().exists()).toBe(true); - }); + describe('while loading artifacts', () => { + beforeEach(() => { + mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts }); + }); - it('should focus the search box when opened with artifacts', () => { - createComponent({ mockData: { artifacts } }); - wrapper.vm.$refs.searchInput.focusInput = jest.fn(); + it('should render a loading spinner and no empty message', async () => { + createComponent(); - findDropdown().vm.$emit('shown'); + findDropdown().vm.$emit('show'); + await nextTick(); - expect(wrapper.vm.$refs.searchInput.focusInput).toHaveBeenCalled(); + expect(findLoadingIcon().exists()).toBe(true); + expect(findEmptyMessage().exists()).toBe(false); + }); }); - it('should render all the provided artifacts when search query is empty', () => { - const searchQuery = ''; - createComponent({ mockData: { searchQuery, artifacts } }); + describe('artifacts loaded successfully', () => { + describe('artifacts exist', () => { + beforeEach(async () => { + mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts }); - expect(findAllArtifactItems()).toHaveLength(artifacts.length); - expect(findEmptyMessage().exists()).toBe(false); - }); + createComponent(); - it('should render filtered artifacts when search query is not empty', () => { - const searchQuery = 'job-2'; - createComponent({ mockData: { searchQuery, artifacts } }); + findDropdown().vm.$emit('show'); + await waitForPromises(); + }); - expect(findAllArtifactItems()).toHaveLength(1); - expect(findEmptyMessage().exists()).toBe(false); - }); + it('should fetch artifacts and show search box on dropdown click', () => { + expect(mockAxios.history.get).toHaveLength(1); + expect(findSearchBox().exists()).toBe(true); + }); - it('should render the correct artifact name and path', () => { - createComponent({ mockData: { artifacts } }); + it('should focus the search box when opened with artifacts', () => { + findDropdown().vm.$emit('shown'); - expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path); - expect(findFirstArtifactItem().text()).toBe(artifacts[0].name); - }); + expect(focusInputMock).toHaveBeenCalled(); + }); - it('should render empty message and no search box when no artifacts are found', () => { - createComponent({ mockData: { artifacts: [] } }); + it('should render all the provided artifacts when search query is empty', () => { + findSearchBox().vm.$emit('input', ''); - expect(findEmptyMessage().exists()).toBe(true); - expect(findSearchBox().exists()).toBe(false); - }); + expect(findAllArtifactItems()).toHaveLength(artifacts.length); + expect(findEmptyMessage().exists()).toBe(false); + }); - describe('while loading artifacts', () => { - it('should render a loading spinner and no empty message', () => { - createComponent({ mockData: { isLoading: true, artifacts: [] } }); + it('should render filtered artifacts when search query is not empty', async () => { + findSearchBox().vm.$emit('input', 'job-2'); + await waitForPromises(); - expect(findLoadingIcon().exists()).toBe(true); - expect(findEmptyMessage().exists()).toBe(false); + expect(findAllArtifactItems()).toHaveLength(1); + expect(findEmptyMessage().exists()).toBe(false); + }); + + it('should render the correct artifact name and path', () => { + expect(findFirstArtifactItem().attributes('href')).toBe(artifacts[0].path); + expect(findFirstArtifactItem().text()).toBe(artifacts[0].name); + }); + }); + + describe('artifacts list is empty', () => { + beforeEach(() => { + mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts: [] }); + }); + + it('should render empty message and no search box when no artifacts are found', async () => { + createComponent(); + + findDropdown().vm.$emit('show'); + await waitForPromises(); + + expect(findEmptyMessage().exists()).toBe(true); + expect(findSearchBox().exists()).toBe(false); + expect(findLoadingIcon().exists()).toBe(false); + }); }); }); describe('with a failing request', () => { - it('should render an error message', async () => { - const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId); + beforeEach(() => { mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR); + }); + + it('should render an error message', async () => { createComponent(); findDropdown().vm.$emit('show'); await waitForPromises(); diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js index afb509b9fe6..8c860c9b06f 100644 --- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js +++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js @@ -1,4 +1,4 @@ -import { GlLink } from '@gitlab/ui'; +import { GlBadge } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue'; import CiIcon from '~/vue_shared/components/ci_icon.vue'; @@ -46,6 +46,13 @@ describe('CI Badge Link Component', () => { icon: 'status_pending', details_path: 'status/pending', }, + preparing: { + text: 'preparing', + label: 'preparing', + group: 'preparing', + icon: 'status_preparing', + details_path: 'status/preparing', + }, running: { text: 'running', label: 'running', @@ -53,6 +60,13 @@ describe('CI Badge Link Component', () => { icon: 'status_running', details_path: 'status/running', }, + scheduled: { + text: 'scheduled', + label: 'scheduled', + group: 'scheduled', + icon: 'status_scheduled', + details_path: 'status/scheduled', + }, skipped: { text: 'skipped', label: 'skipped', @@ -61,8 +75,8 @@ describe('CI Badge Link Component', () => { details_path: 'status/skipped', }, success_warining: { - text: 'passed', - label: 'passed', + text: 'warning', + label: 'passed with warnings', group: 'success-with-warnings', icon: 'status_warning', details_path: 'status/warning', @@ -77,6 +91,8 @@ describe('CI Badge Link Component', () => { }; const findIcon = () => wrapper.findComponent(CiIcon); + const findBadge = () => wrapper.findComponent(GlBadge); + const findBadgeText = () => wrapper.find('[data-testid="ci-badge-text"'); const createComponent = (propsData) => { wrapper = shallowMount(CiBadgeLink, { propsData }); @@ -87,22 +103,50 @@ describe('CI Badge Link Component', () => { expect(wrapper.attributes('href')).toBe(statuses[status].details_path); expect(wrapper.text()).toBe(statuses[status].text); - expect(wrapper.classes()).toContain('ci-status'); - expect(wrapper.classes()).toContain(`ci-${statuses[status].group}`); + expect(findBadge().props('size')).toBe('md'); expect(findIcon().exists()).toBe(true); }); + it.each` + status | textColor | variant + ${statuses.success} | ${'gl-text-green-700'} | ${'success'} + ${statuses.success_warining} | ${'gl-text-orange-700'} | ${'warning'} + ${statuses.failed} | ${'gl-text-red-700'} | ${'danger'} + ${statuses.running} | ${'gl-text-blue-700'} | ${'info'} + ${statuses.pending} | ${'gl-text-orange-700'} | ${'warning'} + ${statuses.preparing} | ${'gl-text-gray-600'} | ${'muted'} + ${statuses.canceled} | ${'gl-text-gray-700'} | ${'neutral'} + ${statuses.scheduled} | ${'gl-text-gray-600'} | ${'muted'} + ${statuses.skipped} | ${'gl-text-gray-600'} | ${'muted'} + ${statuses.manual} | ${'gl-text-gray-700'} | ${'neutral'} + ${statuses.created} | ${'gl-text-gray-600'} | ${'muted'} + `( + 'should contain correct badge class and variant for status: $status.text', + ({ status, textColor, variant }) => { + createComponent({ status }); + + expect(findBadgeText().classes()).toContain(textColor); + expect(findBadge().props('variant')).toBe(variant); + }, + ); + it('should not render label', () => { createComponent({ status: statuses.canceled, showText: false }); expect(wrapper.text()).toBe(''); }); - it('should emit ciStatusBadgeClick event', async () => { + it('should emit ciStatusBadgeClick event', () => { createComponent({ status: statuses.success }); - await wrapper.findComponent(GlLink).vm.$emit('click'); + findBadge().vm.$emit('click'); expect(wrapper.emitted('ciStatusBadgeClick')).toEqual([[]]); }); + + it('should render dynamic badge size', () => { + createComponent({ status: statuses.success, badgeSize: 'lg' }); + + expect(findBadge().props('size')).toBe('lg'); + }); }); |