Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/boards/components/board_content_spec.js51
-rw-r--r--spec/frontend/boards/mock_data.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js82
-rw-r--r--spec/frontend/ci/runner/components/runner_form_fields_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js197
-rw-r--r--spec/frontend/commit/components/refs_list_spec.js2
-rw-r--r--spec/frontend/commit/mock_data.js1
-rw-r--r--spec/frontend/fixtures/pipeline_header.rb27
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js341
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js9
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js253
-rw-r--r--spec/frontend/pipelines/mock_data.js27
-rw-r--r--spec/frontend/pipelines/pipeline_details_header_spec.js210
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js122
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js58
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');
+ });
});