diff options
Diffstat (limited to 'spec/frontend/pipeline_new')
3 files changed, 261 insertions, 82 deletions
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js index 51bb0ecee9c..7ec5818010a 100644 --- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js +++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js @@ -1,4 +1,4 @@ -import { GlDropdown, GlDropdownItem, GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui'; +import { GlForm, GlSprintf, GlLoadingIcon } from '@gitlab/ui'; import { mount, shallowMount } from '@vue/test-utils'; import MockAdapter from 'axios-mock-adapter'; import waitForPromises from 'helpers/wait_for_promises'; @@ -6,34 +6,26 @@ import axios from '~/lib/utils/axios_utils'; import httpStatusCodes from '~/lib/utils/http_status'; import { redirectTo } from '~/lib/utils/url_utility'; import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue'; -import { - mockBranches, - mockTags, - mockParams, - mockPostParams, - mockProjectId, - mockError, -} from '../mock_data'; +import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue'; +import { mockQueryParams, mockPostParams, mockProjectId, mockError, mockRefs } from '../mock_data'; jest.mock('~/lib/utils/url_utility', () => ({ redirectTo: jest.fn(), })); +const projectRefsEndpoint = '/root/project/refs'; const pipelinesPath = '/root/project/-/pipelines'; const configVariablesPath = '/root/project/-/pipelines/config_variables'; -const postResponse = { id: 1 }; +const newPipelinePostResponse = { id: 1 }; +const defaultBranch = 'master'; describe('Pipeline New Form', () => { let wrapper; let mock; - - const dummySubmitEvent = { - preventDefault() {}, - }; + let dummySubmitEvent; const findForm = () => wrapper.find(GlForm); - const findDropdown = () => wrapper.find(GlDropdown); - const findDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findRefsDropdown = () => wrapper.findComponent(RefsDropdown); const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]'); const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]'); const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]'); @@ -44,33 +36,42 @@ describe('Pipeline New Form', () => { const findWarningAlertSummary = () => findWarningAlert().find(GlSprintf); const findWarnings = () => wrapper.findAll('[data-testid="run-pipeline-warning"]'); const findLoadingIcon = () => wrapper.find(GlLoadingIcon); - const getExpectedPostParams = () => JSON.parse(mock.history.post[0].data); - const changeRef = (i) => findDropdownItems().at(i).vm.$emit('click'); + const getFormPostParams = () => JSON.parse(mock.history.post[0].data); + + const selectBranch = (branch) => { + // Select a branch in the dropdown + findRefsDropdown().vm.$emit('input', { + shortName: branch, + fullName: `refs/heads/${branch}`, + }); + }; - const createComponent = (term = '', props = {}, method = shallowMount) => { + const createComponent = (props = {}, method = shallowMount) => { wrapper = method(PipelineNewForm, { + provide: { + projectRefsEndpoint, + }, propsData: { projectId: mockProjectId, pipelinesPath, configVariablesPath, - branches: mockBranches, - tags: mockTags, - defaultBranch: 'master', + defaultBranch, + refParam: defaultBranch, settingsLink: '', maxWarnings: 25, ...props, }, - data() { - return { - searchTerm: term, - }; - }, }); }; beforeEach(() => { mock = new MockAdapter(axios); mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {}); + mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs); + + dummySubmitEvent = { + preventDefault: jest.fn(), + }; }); afterEach(() => { @@ -80,38 +81,17 @@ describe('Pipeline New Form', () => { mock.restore(); }); - describe('Dropdown with branches and tags', () => { - beforeEach(() => { - mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse); - }); - - it('displays dropdown with all branches and tags', () => { - const refLength = mockBranches.length + mockTags.length; - - createComponent(); - - expect(findDropdownItems()).toHaveLength(refLength); - }); - - it('when user enters search term the list is filtered', () => { - createComponent('master'); - - expect(findDropdownItems()).toHaveLength(1); - expect(findDropdownItems().at(0).text()).toBe('master'); - }); - }); - describe('Form', () => { beforeEach(async () => { - createComponent('', mockParams, mount); + createComponent(mockQueryParams, mount); - mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse); + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse); await waitForPromises(); }); it('displays the correct values for the provided query params', async () => { - expect(findDropdown().props('text')).toBe('tag-1'); + expect(findRefsDropdown().props('value')).toEqual({ shortName: 'tag-1' }); expect(findVariableRows()).toHaveLength(3); }); @@ -152,11 +132,19 @@ describe('Pipeline New Form', () => { describe('Pipeline creation', () => { beforeEach(async () => { - mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, postResponse); + mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse); await waitForPromises(); }); + it('does not submit the native HTML form', async () => { + createComponent(); + + findForm().vm.$emit('submit', dummySubmitEvent); + + expect(dummySubmitEvent.preventDefault).toHaveBeenCalled(); + }); + it('disables the submit button immediately after submitting', async () => { createComponent(); @@ -171,19 +159,15 @@ describe('Pipeline New Form', () => { it('creates pipeline with full ref and variables', async () => { createComponent(); - changeRef(0); - findForm().vm.$emit('submit', dummySubmitEvent); - await waitForPromises(); - expect(getExpectedPostParams().ref).toEqual(wrapper.vm.$data.refValue.fullName); - expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`); + expect(getFormPostParams().ref).toEqual(`refs/heads/${defaultBranch}`); + expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); }); - it('creates a pipeline with short ref and variables', async () => { - // query params are used - createComponent('', mockParams); + it('creates a pipeline with short ref and variables from the query params', async () => { + createComponent(mockQueryParams); await waitForPromises(); @@ -191,19 +175,19 @@ describe('Pipeline New Form', () => { await waitForPromises(); - expect(getExpectedPostParams()).toEqual(mockPostParams); - expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`); + expect(getFormPostParams()).toEqual(mockPostParams); + expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${newPipelinePostResponse.id}`); }); }); describe('When the ref has been changed', () => { beforeEach(async () => { - createComponent('', {}, mount); + createComponent({}, mount); await waitForPromises(); }); it('variables persist between ref changes', async () => { - changeRef(0); // change to master + selectBranch('master'); await waitForPromises(); @@ -213,7 +197,7 @@ describe('Pipeline New Form', () => { await wrapper.vm.$nextTick(); - changeRef(1); // change to branch-1 + selectBranch('branch-1'); await waitForPromises(); @@ -223,14 +207,14 @@ describe('Pipeline New Form', () => { await wrapper.vm.$nextTick(); - changeRef(0); // change back to master + selectBranch('master'); await waitForPromises(); expect(findKeyInputs().at(0).element.value).toBe('build_var'); expect(findVariableRows().length).toBe(2); - changeRef(1); // change back to branch-1 + selectBranch('branch-1'); await waitForPromises(); @@ -248,7 +232,7 @@ describe('Pipeline New Form', () => { const mockYmlDesc = 'A var from yml.'; it('loading icon is shown when content is requested and hidden when received', async () => { - createComponent('', mockParams, mount); + createComponent(mockQueryParams, mount); mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { [mockYmlKey]: { @@ -265,7 +249,7 @@ describe('Pipeline New Form', () => { }); it('multi-line strings are added to the value field without removing line breaks', async () => { - createComponent('', mockParams, mount); + createComponent(mockQueryParams, mount); mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { [mockYmlKey]: { @@ -281,7 +265,7 @@ describe('Pipeline New Form', () => { describe('with description', () => { beforeEach(async () => { - createComponent('', mockParams, mount); + createComponent(mockQueryParams, mount); mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { [mockYmlKey]: { @@ -323,7 +307,7 @@ describe('Pipeline New Form', () => { describe('without description', () => { beforeEach(async () => { - createComponent('', mockParams, mount); + createComponent(mockQueryParams, mount); mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, { [mockYmlKey]: { @@ -346,6 +330,21 @@ describe('Pipeline New Form', () => { createComponent(); }); + describe('when the refs cannot be loaded', () => { + beforeEach(() => { + mock + .onGet(projectRefsEndpoint, { params: { search: '' } }) + .reply(httpStatusCodes.INTERNAL_SERVER_ERROR); + + findRefsDropdown().vm.$emit('loadingError'); + }); + + it('shows both an error alert', () => { + expect(findErrorAlert().exists()).toBe(true); + expect(findWarningAlert().exists()).toBe(false); + }); + }); + describe('when the error response can be handled', () => { beforeEach(async () => { mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError); diff --git a/spec/frontend/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/pipeline_new/components/refs_dropdown_spec.js new file mode 100644 index 00000000000..8dafbf230f9 --- /dev/null +++ b/spec/frontend/pipeline_new/components/refs_dropdown_spec.js @@ -0,0 +1,182 @@ +import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; +import httpStatusCodes from '~/lib/utils/http_status'; + +import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue'; + +import { mockRefs, mockFilteredRefs } from '../mock_data'; + +const projectRefsEndpoint = '/root/project/refs'; +const refShortName = 'master'; +const refFullName = 'refs/heads/master'; + +jest.mock('~/flash'); + +describe('Pipeline New Form', () => { + let wrapper; + let mock; + + const findDropdown = () => wrapper.find(GlDropdown); + const findRefsDropdownItems = () => wrapper.findAll(GlDropdownItem); + const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType); + + const createComponent = (props = {}, mountFn = shallowMount) => { + wrapper = mountFn(RefsDropdown, { + provide: { + projectRefsEndpoint, + }, + propsData: { + value: { + shortName: refShortName, + fullName: refFullName, + }, + ...props, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(httpStatusCodes.OK, mockRefs); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + mock.restore(); + }); + + beforeEach(() => { + createComponent(); + }); + + it('displays empty dropdown initially', async () => { + await findDropdown().vm.$emit('show'); + + expect(findRefsDropdownItems()).toHaveLength(0); + }); + + it('does not make requests immediately', async () => { + expect(mock.history.get).toHaveLength(0); + }); + + describe('when user opens dropdown', () => { + beforeEach(async () => { + await findDropdown().vm.$emit('show'); + await waitForPromises(); + }); + + it('requests unfiltered tags and branches', async () => { + expect(mock.history.get).toHaveLength(1); + expect(mock.history.get[0].url).toBe(projectRefsEndpoint); + expect(mock.history.get[0].params).toEqual({ search: '' }); + }); + + it('displays dropdown with branches and tags', async () => { + const refLength = mockRefs.Tags.length + mockRefs.Branches.length; + + expect(findRefsDropdownItems()).toHaveLength(refLength); + }); + + it('displays the names of refs', () => { + // Branches + expect(findRefsDropdownItems().at(0).text()).toBe(mockRefs.Branches[0]); + + // Tags (appear after branches) + const firstTag = mockRefs.Branches.length; + expect(findRefsDropdownItems().at(firstTag).text()).toBe(mockRefs.Tags[0]); + }); + + it('when user shows dropdown a second time, only one request is done', () => { + expect(mock.history.get).toHaveLength(1); + }); + + describe('when user selects a value', () => { + const selectedIndex = 1; + + beforeEach(async () => { + await findRefsDropdownItems().at(selectedIndex).vm.$emit('click'); + }); + + it('component emits @input', () => { + const inputs = wrapper.emitted('input'); + + expect(inputs).toHaveLength(1); + expect(inputs[0]).toEqual([{ shortName: 'branch-1', fullName: 'refs/heads/branch-1' }]); + }); + }); + + describe('when user types searches for a tag', () => { + const mockSearchTerm = 'my-search'; + + beforeEach(async () => { + mock + .onGet(projectRefsEndpoint, { params: { search: mockSearchTerm } }) + .reply(httpStatusCodes.OK, mockFilteredRefs); + + await findSearchBox().vm.$emit('input', mockSearchTerm); + await waitForPromises(); + }); + + it('requests filtered tags and branches', async () => { + expect(mock.history.get).toHaveLength(2); + expect(mock.history.get[1].params).toEqual({ + search: mockSearchTerm, + }); + }); + + it('displays dropdown with branches and tags', async () => { + const filteredRefLength = mockFilteredRefs.Tags.length + mockFilteredRefs.Branches.length; + + expect(findRefsDropdownItems()).toHaveLength(filteredRefLength); + }); + }); + }); + + describe('when user has selected a value', () => { + const selectedIndex = 1; + const mockShortName = mockRefs.Branches[selectedIndex]; + const mockFullName = `refs/heads/${mockShortName}`; + + beforeEach(async () => { + mock + .onGet(projectRefsEndpoint, { + params: { ref: mockFullName }, + }) + .reply(httpStatusCodes.OK, mockRefs); + + createComponent({ + value: { + shortName: mockShortName, + fullName: mockFullName, + }, + }); + await findDropdown().vm.$emit('show'); + await waitForPromises(); + }); + + it('branch is checked', () => { + expect(findRefsDropdownItems().at(selectedIndex).props('isChecked')).toBe(true); + }); + }); + + describe('when server returns an error', () => { + beforeEach(async () => { + mock + .onGet(projectRefsEndpoint, { params: { search: '' } }) + .reply(httpStatusCodes.INTERNAL_SERVER_ERROR); + + await findDropdown().vm.$emit('show'); + await waitForPromises(); + }); + + it('loading error event is emitted', () => { + expect(wrapper.emitted('loadingError')).toHaveLength(1); + expect(wrapper.emitted('loadingError')[0]).toEqual([expect.any(Error)]); + }); + }); +}); diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js index feb24ec602d..4fb58cb8e62 100644 --- a/spec/frontend/pipeline_new/mock_data.js +++ b/spec/frontend/pipeline_new/mock_data.js @@ -1,16 +1,14 @@ -export const mockBranches = [ - { shortName: 'master', fullName: 'refs/heads/master' }, - { shortName: 'branch-1', fullName: 'refs/heads/branch-1' }, - { shortName: 'branch-2', fullName: 'refs/heads/branch-2' }, -]; +export const mockRefs = { + Branches: ['master', 'branch-1', 'branch-2'], + Tags: ['1.0.0', '1.1.0', '1.2.0'], +}; -export const mockTags = [ - { shortName: '1.0.0', fullName: 'refs/tags/1.0.0' }, - { shortName: '1.1.0', fullName: 'refs/tags/1.1.0' }, - { shortName: '1.2.0', fullName: 'refs/tags/1.2.0' }, -]; +export const mockFilteredRefs = { + Branches: ['branch-1'], + Tags: ['1.0.0', '1.1.0'], +}; -export const mockParams = { +export const mockQueryParams = { refParam: 'tag-1', variableParams: { test_var: 'test_var_val', |