diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-27 21:09:41 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-27 21:09:41 +0300 |
commit | f569792df8a25caa1bed9c448c8c4c3f837f5164 (patch) | |
tree | 8c2ed7dae5ba132a97c0321a7649174e5832d637 /spec/frontend | |
parent | c2908ec6a0d7b62996cdb8da0350705bdad691bf (diff) |
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec/frontend')
-rw-r--r-- | spec/frontend/alert_management/components/alert_management_list_spec.js | 11 | ||||
-rw-r--r-- | spec/frontend/boards/boards_store_spec.js | 61 | ||||
-rw-r--r-- | spec/frontend/boards/issue_spec.js | 22 | ||||
-rw-r--r-- | spec/frontend/deploy_keys/components/action_btn_spec.js | 54 | ||||
-rw-r--r-- | spec/frontend/deploy_keys/components/app_spec.js | 142 | ||||
-rw-r--r-- | spec/frontend/deploy_keys/components/key_spec.js | 161 | ||||
-rw-r--r-- | spec/frontend/deploy_keys/components/keys_panel_spec.js | 63 | ||||
-rw-r--r-- | spec/frontend/dirty_submit/dirty_submit_collection_spec.js | 22 | ||||
-rw-r--r-- | spec/frontend/dirty_submit/dirty_submit_factory_spec.js | 18 | ||||
-rw-r--r-- | spec/frontend/dirty_submit/dirty_submit_form_spec.js | 97 | ||||
-rw-r--r-- | spec/frontend/dirty_submit/helper.js | 43 | ||||
-rw-r--r-- | spec/frontend/pipelines/mock_data.js | 423 | ||||
-rw-r--r-- | spec/frontend/pipelines/pipelines_spec.js | 659 | ||||
-rw-r--r-- | spec/frontend/users_select/utils_spec.js | 33 |
14 files changed, 1754 insertions, 55 deletions
diff --git a/spec/frontend/alert_management/components/alert_management_list_spec.js b/spec/frontend/alert_management/components/alert_management_list_spec.js index 9753300d035..c18c2ec0d53 100644 --- a/spec/frontend/alert_management/components/alert_management_list_spec.js +++ b/spec/frontend/alert_management/components/alert_management_list_spec.js @@ -12,7 +12,10 @@ describe('AlertManagementList', () => { function mountComponent({ stubs = {}, - props = { alertManagementEnabled: false }, + props = { + alertManagementEnabled: false, + userCanEnableAlertManagement: false, + }, data = {}, loading = false, } = {}) { @@ -62,7 +65,7 @@ describe('AlertManagementList', () => { it('loading state', () => { mountComponent({ stubs: { GlTable }, - props: { alertManagementEnabled: true }, + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, data: { alerts: null }, loading: true, }); @@ -73,7 +76,7 @@ describe('AlertManagementList', () => { it('error state', () => { mountComponent({ stubs: { GlTable }, - props: { alertManagementEnabled: true }, + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, data: { alerts: null, errored: true }, loading: false, }); @@ -86,7 +89,7 @@ describe('AlertManagementList', () => { it('empty state', () => { mountComponent({ stubs: { GlTable }, - props: { alertManagementEnabled: true }, + props: { alertManagementEnabled: true, userCanEnableAlertManagement: true }, data: { alerts: [], errored: false }, loading: false, }); diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js index 5c5315fd465..05a44138275 100644 --- a/spec/frontend/boards/boards_store_spec.js +++ b/spec/frontend/boards/boards_store_spec.js @@ -1040,5 +1040,66 @@ describe('boardsStore', () => { }); }); }); + + describe('updateIssue', () => { + let issue; + let patchSpy; + + beforeEach(() => { + issue = new ListIssue({ + title: 'Testing', + id: 1, + iid: 1, + confidential: false, + labels: [ + { + id: 1, + title: 'test', + color: 'red', + description: 'testing', + }, + ], + assignees: [ + { + id: 1, + name: 'name', + username: 'username', + avatar_url: 'http://avatar_url', + }, + ], + real_path: 'path/to/issue', + }); + + patchSpy = jest.fn().mockReturnValue([200, { labels: [] }]); + axiosMock.onPatch(`path/to/issue.json`).reply(({ data }) => patchSpy(JSON.parse(data))); + }); + + it('passes assignee ids when there are assignees', () => { + boardsStore.updateIssue(issue); + return boardsStore.updateIssue(issue).then(() => { + expect(patchSpy).toHaveBeenCalledWith({ + issue: { + milestone_id: null, + assignee_ids: [1], + label_ids: [1], + }, + }); + }); + }); + + it('passes assignee ids of [0] when there are no assignees', () => { + issue.removeAllAssignees(); + + return boardsStore.updateIssue(issue).then(() => { + expect(patchSpy).toHaveBeenCalledWith({ + issue: { + milestone_id: null, + assignee_ids: [0], + label_ids: [1], + }, + }); + }); + }); + }); }); }); diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js index ff72edaa695..412f20684f5 100644 --- a/spec/frontend/boards/issue_spec.js +++ b/spec/frontend/boards/issue_spec.js @@ -1,6 +1,5 @@ /* global ListIssue */ -import axios from '~/lib/utils/axios_utils'; import '~/boards/models/label'; import '~/boards/models/assignee'; import '~/boards/models/issue'; @@ -173,25 +172,12 @@ describe('Issue model', () => { }); describe('update', () => { - it('passes assignee ids when there are assignees', done => { - jest.spyOn(axios, 'patch').mockImplementation((url, data) => { - expect(data.issue.assignee_ids).toEqual([1]); - done(); - return Promise.resolve(); - }); - - issue.update('url'); - }); + it('passes update to boardsStore', () => { + jest.spyOn(boardsStore, 'updateIssue').mockImplementation(); - it('passes assignee ids of [0] when there are no assignees', done => { - jest.spyOn(axios, 'patch').mockImplementation((url, data) => { - expect(data.issue.assignee_ids).toEqual([0]); - done(); - return Promise.resolve(); - }); + issue.update(); - issue.removeAllAssignees(); - issue.update('url'); + expect(boardsStore.updateIssue).toHaveBeenCalledWith(issue); }); }); }); diff --git a/spec/frontend/deploy_keys/components/action_btn_spec.js b/spec/frontend/deploy_keys/components/action_btn_spec.js new file mode 100644 index 00000000000..b8211b02464 --- /dev/null +++ b/spec/frontend/deploy_keys/components/action_btn_spec.js @@ -0,0 +1,54 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlLoadingIcon } from '@gitlab/ui'; +import eventHub from '~/deploy_keys/eventhub'; +import actionBtn from '~/deploy_keys/components/action_btn.vue'; + +describe('Deploy keys action btn', () => { + const data = getJSONFixture('deploy_keys/keys.json'); + const deployKey = data.enabled_keys[0]; + let wrapper; + + const findLoadingIcon = () => wrapper.find(GlLoadingIcon); + + beforeEach(() => { + wrapper = shallowMount(actionBtn, { + propsData: { + deployKey, + type: 'enable', + }, + slots: { + default: 'Enable', + }, + }); + }); + + it('renders the default slot', () => { + expect(wrapper.text()).toBe('Enable'); + }); + + it('sends eventHub event with btn type', () => { + jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); + + wrapper.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(eventHub.$emit).toHaveBeenCalledWith('enable.key', deployKey, expect.anything()); + }); + }); + + it('shows loading spinner after click', () => { + wrapper.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(findLoadingIcon().exists()).toBe(true); + }); + }); + + it('disables button after click', () => { + wrapper.trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.attributes('disabled')).toBe('disabled'); + }); + }); +}); diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js new file mode 100644 index 00000000000..291502c9ed7 --- /dev/null +++ b/spec/frontend/deploy_keys/components/app_spec.js @@ -0,0 +1,142 @@ +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import { TEST_HOST } from 'spec/test_constants'; +import waitForPromises from 'helpers/wait_for_promises'; +import axios from '~/lib/utils/axios_utils'; +import eventHub from '~/deploy_keys/eventhub'; +import deployKeysApp from '~/deploy_keys/components/app.vue'; + +const TEST_ENDPOINT = `${TEST_HOST}/dummy/`; + +describe('Deploy keys app component', () => { + const data = getJSONFixture('deploy_keys/keys.json'); + let wrapper; + let mock; + + const mountComponent = () => { + wrapper = mount(deployKeysApp, { + propsData: { + endpoint: TEST_ENDPOINT, + projectId: '8', + }, + }); + + return waitForPromises(); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + mock.onGet(TEST_ENDPOINT).reply(200, data); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + const findLoadingIcon = () => wrapper.find('.gl-spinner'); + const findKeyPanels = () => wrapper.findAll('.deploy-keys .nav-links li'); + + it('renders loading icon while waiting for request', () => { + mock.onGet(TEST_ENDPOINT).reply(() => new Promise()); + + mountComponent(); + + return wrapper.vm.$nextTick().then(() => { + expect(findLoadingIcon().exists()).toBe(true); + }); + }); + + it('renders keys panels', () => { + return mountComponent().then(() => { + expect(findKeyPanels().length).toBe(3); + }); + }); + + it.each` + selector | label | count + ${'.js-deployKeys-tab-enabled_keys'} | ${'Enabled deploy keys'} | ${1} + ${'.js-deployKeys-tab-available_project_keys'} | ${'Privately accessible deploy keys'} | ${0} + ${'.js-deployKeys-tab-public_keys'} | ${'Publicly accessible deploy keys'} | ${1} + `('$selector title is $label with keys count equal to $count', ({ selector, label, count }) => { + return mountComponent().then(() => { + const element = wrapper.find(selector); + expect(element.exists()).toBe(true); + expect(element.text().trim()).toContain(label); + + expect( + element + .find('.badge') + .text() + .trim(), + ).toBe(count.toString()); + }); + }); + + it('does not render key panels when keys object is empty', () => { + mock.onGet(TEST_ENDPOINT).reply(200, []); + + return mountComponent().then(() => { + expect(findKeyPanels().length).toBe(0); + }); + }); + + it('re-fetches deploy keys when enabling a key', () => { + const key = data.public_keys[0]; + return mountComponent() + .then(() => { + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('enable.key', key); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); + }); + }); + + it('re-fetches deploy keys when disabling a key', () => { + const key = data.public_keys[0]; + return mountComponent() + .then(() => { + jest.spyOn(window, 'confirm').mockReturnValue(true); + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('disable.key', key); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); + }); + }); + + it('calls disableKey when removing a key', () => { + const key = data.public_keys[0]; + return mountComponent() + .then(() => { + jest.spyOn(window, 'confirm').mockReturnValue(true); + jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {}); + jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve()); + + eventHub.$emit('remove.key', key); + + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id); + expect(wrapper.vm.service.getKeys).toHaveBeenCalled(); + }); + }); + + it('hasKeys returns true when there are keys', () => { + return mountComponent().then(() => { + expect(wrapper.vm.hasKeys).toEqual(3); + }); + }); +}); diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js new file mode 100644 index 00000000000..7d942d969bb --- /dev/null +++ b/spec/frontend/deploy_keys/components/key_spec.js @@ -0,0 +1,161 @@ +import { mount } from '@vue/test-utils'; +import DeployKeysStore from '~/deploy_keys/store'; +import key from '~/deploy_keys/components/key.vue'; +import { getTimeago } from '~/lib/utils/datetime_utility'; + +describe('Deploy keys key', () => { + let wrapper; + let store; + + const data = getJSONFixture('deploy_keys/keys.json'); + + const findTextAndTrim = selector => + wrapper + .find(selector) + .text() + .trim(); + + const createComponent = propsData => { + wrapper = mount(key, { + propsData: { + store, + endpoint: 'https://test.host/dummy/endpoint', + ...propsData, + }, + }); + }; + + beforeEach(() => { + store = new DeployKeysStore(); + store.keys = data; + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('enabled key', () => { + const deployKey = data.enabled_keys[0]; + + it('renders the keys title', () => { + createComponent({ deployKey }); + + expect(findTextAndTrim('.title')).toContain('My title'); + }); + + it('renders human friendly formatted created date', () => { + createComponent({ deployKey }); + + expect(findTextAndTrim('.key-created-at')).toBe( + `${getTimeago().format(deployKey.created_at)}`, + ); + }); + + it('shows pencil button for editing', () => { + createComponent({ deployKey }); + + expect(wrapper.find('.btn .ic-pencil')).toExist(); + }); + + it('shows disable button when the project is not deletable', () => { + createComponent({ deployKey }); + + expect(wrapper.find('.btn .ic-cancel')).toExist(); + }); + + it('shows remove button when the project is deletable', () => { + createComponent({ + deployKey: { ...deployKey, destroyed_when_orphaned: true, almost_orphaned: true }, + }); + expect(wrapper.find('.btn .ic-remove')).toExist(); + }); + }); + + describe('deploy key labels', () => { + const deployKey = data.enabled_keys[0]; + const deployKeysProjects = [...deployKey.deploy_keys_projects]; + it('shows write access title when key has write access', () => { + deployKeysProjects[0] = { ...deployKeysProjects[0], can_push: true }; + createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } }); + + expect(wrapper.find('.deploy-project-label').attributes('data-original-title')).toBe( + 'Write access allowed', + ); + }); + + it('does not show write access title when key has write access', () => { + deployKeysProjects[0] = { ...deployKeysProjects[0], can_push: false }; + createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } }); + + expect(wrapper.find('.deploy-project-label').attributes('data-original-title')).toBe( + 'Read access only', + ); + }); + + it('shows expandable button if more than two projects', () => { + createComponent({ deployKey }); + const labels = wrapper.findAll('.deploy-project-label'); + + expect(labels.length).toBe(2); + expect(labels.at(1).text()).toContain('others'); + expect(labels.at(1).attributes('data-original-title')).toContain('Expand'); + }); + + it('expands all project labels after click', () => { + createComponent({ deployKey }); + const { length } = deployKey.deploy_keys_projects; + wrapper + .findAll('.deploy-project-label') + .at(1) + .trigger('click'); + + return wrapper.vm.$nextTick().then(() => { + const labels = wrapper.findAll('.deploy-project-label'); + + expect(labels.length).toBe(length); + expect(labels.at(1).text()).not.toContain(`+${length} others`); + expect(labels.at(1).attributes('data-original-title')).not.toContain('Expand'); + }); + }); + + it('shows two projects', () => { + createComponent({ + deployKey: { ...deployKey, deploy_keys_projects: [...deployKeysProjects].slice(0, 2) }, + }); + + const labels = wrapper.findAll('.deploy-project-label'); + + expect(labels.length).toBe(2); + expect(labels.at(1).text()).toContain(deployKey.deploy_keys_projects[1].project.full_name); + }); + }); + + describe('public keys', () => { + const deployKey = data.public_keys[0]; + + it('renders deploy keys without any enabled projects', () => { + createComponent({ deployKey: { ...deployKey, deploy_keys_projects: [] } }); + + expect(findTextAndTrim('.deploy-project-list')).toBe('None'); + }); + + it('shows enable button', () => { + createComponent({ deployKey }); + expect(findTextAndTrim('.btn')).toBe('Enable'); + }); + + it('shows pencil button for editing', () => { + createComponent({ deployKey }); + expect(wrapper.find('.btn .ic-pencil')).toExist(); + }); + + it('shows disable button when key is enabled', () => { + store.keys.enabled_keys.push(deployKey); + + createComponent({ deployKey }); + + expect(wrapper.find('.btn .ic-cancel')).toExist(); + }); + }); +}); diff --git a/spec/frontend/deploy_keys/components/keys_panel_spec.js b/spec/frontend/deploy_keys/components/keys_panel_spec.js new file mode 100644 index 00000000000..53c8ba073bc --- /dev/null +++ b/spec/frontend/deploy_keys/components/keys_panel_spec.js @@ -0,0 +1,63 @@ +import { mount } from '@vue/test-utils'; +import DeployKeysStore from '~/deploy_keys/store'; +import deployKeysPanel from '~/deploy_keys/components/keys_panel.vue'; + +describe('Deploy keys panel', () => { + const data = getJSONFixture('deploy_keys/keys.json'); + let wrapper; + + const findTableRowHeader = () => wrapper.find('.table-row-header'); + + const mountComponent = props => { + const store = new DeployKeysStore(); + store.keys = data; + wrapper = mount(deployKeysPanel, { + propsData: { + title: 'test', + keys: data.enabled_keys, + showHelpBox: true, + store, + endpoint: 'https://test.host/dummy/endpoint', + ...props, + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + it('renders list of keys', () => { + mountComponent(); + expect(wrapper.findAll('.deploy-key').length).toBe(wrapper.vm.keys.length); + }); + + it('renders table header', () => { + mountComponent(); + const tableHeader = findTableRowHeader(); + + expect(tableHeader).toExist(); + expect(tableHeader.text()).toContain('Deploy key'); + expect(tableHeader.text()).toContain('Project usage'); + expect(tableHeader.text()).toContain('Created'); + }); + + it('renders help box if keys are empty', () => { + mountComponent({ keys: [] }); + + expect(wrapper.find('.settings-message').exists()).toBe(true); + + expect( + wrapper + .find('.settings-message') + .text() + .trim(), + ).toBe('No deploy keys found. Create one with the form above.'); + }); + + it('renders no table header if keys are empty', () => { + mountComponent({ keys: [] }); + expect(findTableRowHeader().exists()).toBe(false); + }); +}); diff --git a/spec/frontend/dirty_submit/dirty_submit_collection_spec.js b/spec/frontend/dirty_submit/dirty_submit_collection_spec.js new file mode 100644 index 00000000000..170d581be23 --- /dev/null +++ b/spec/frontend/dirty_submit/dirty_submit_collection_spec.js @@ -0,0 +1,22 @@ +import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection'; +import { setInputValue, createForm } from './helper'; + +jest.mock('lodash/throttle', () => jest.fn(fn => fn)); + +describe('DirtySubmitCollection', () => { + const testElementsCollection = [createForm(), createForm()]; + const forms = testElementsCollection.map(testElements => testElements.form); + + new DirtySubmitCollection(forms); // eslint-disable-line no-new + + it.each(testElementsCollection)('disables submits until there are changes', testElements => { + const { input, submit } = testElements; + const originalValue = input.value; + + expect(submit.disabled).toBe(true); + setInputValue(input, `${originalValue} changes`); + expect(submit.disabled).toBe(false); + setInputValue(input, originalValue); + expect(submit.disabled).toBe(true); + }); +}); diff --git a/spec/frontend/dirty_submit/dirty_submit_factory_spec.js b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js new file mode 100644 index 00000000000..40843a68582 --- /dev/null +++ b/spec/frontend/dirty_submit/dirty_submit_factory_spec.js @@ -0,0 +1,18 @@ +import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory'; +import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; +import DirtySubmitCollection from '~/dirty_submit/dirty_submit_collection'; +import { createForm } from './helper'; + +describe('DirtySubmitCollection', () => { + it('returns a DirtySubmitForm instance for single form elements', () => { + const { form } = createForm(); + + expect(dirtySubmitFactory(form) instanceof DirtySubmitForm).toBe(true); + }); + + it('returns a DirtySubmitCollection instance for a collection of form elements', () => { + const forms = [createForm().form, createForm().form]; + + expect(dirtySubmitFactory(forms) instanceof DirtySubmitCollection).toBe(true); + }); +}); diff --git a/spec/frontend/dirty_submit/dirty_submit_form_spec.js b/spec/frontend/dirty_submit/dirty_submit_form_spec.js new file mode 100644 index 00000000000..d7f690df1f3 --- /dev/null +++ b/spec/frontend/dirty_submit/dirty_submit_form_spec.js @@ -0,0 +1,97 @@ +import { range as rge, throttle } from 'lodash'; +import DirtySubmitForm from '~/dirty_submit/dirty_submit_form'; +import { getInputValue, setInputValue, createForm } from './helper'; + +jest.mock('lodash/throttle', () => jest.fn(fn => fn)); +const lodash = jest.requireActual('lodash'); + +function expectToToggleDisableOnDirtyUpdate(submit, input) { + const originalValue = getInputValue(input); + + expect(submit.disabled).toBe(true); + + setInputValue(input, `${originalValue} changes`); + expect(submit.disabled).toBe(false); + setInputValue(input, originalValue); + expect(submit.disabled).toBe(true); +} + +describe('DirtySubmitForm', () => { + describe('submit button tests', () => { + it('disables submit until there are changes', () => { + const { form, input, submit } = createForm(); + + new DirtySubmitForm(form); // eslint-disable-line no-new + + expectToToggleDisableOnDirtyUpdate(submit, input); + }); + + it('disables submit until there are changes when initializing with a falsy value', () => { + const { form, input, submit } = createForm(); + input.value = ''; + + new DirtySubmitForm(form); // eslint-disable-line no-new + + expectToToggleDisableOnDirtyUpdate(submit, input); + }); + + it('disables submit until there are changes for radio inputs', () => { + const { form, input, submit } = createForm('radio'); + + new DirtySubmitForm(form); // eslint-disable-line no-new + + expectToToggleDisableOnDirtyUpdate(submit, input); + }); + + it('disables submit until there are changes for checkbox inputs', () => { + const { form, input, submit } = createForm('checkbox'); + + new DirtySubmitForm(form); // eslint-disable-line no-new + + expectToToggleDisableOnDirtyUpdate(submit, input); + }); + }); + + describe('throttling tests', () => { + beforeEach(() => { + throttle.mockImplementation(lodash.throttle); + jest.useFakeTimers(); + }); + + afterEach(() => { + throttle.mockReset(); + }); + + it('throttles updates when rapid changes are made to a single form element', () => { + const { form, input } = createForm(); + const updateDirtyInputSpy = jest.spyOn(new DirtySubmitForm(form), 'updateDirtyInput'); + + rge(10).forEach(i => { + setInputValue(input, `change ${i}`, false); + }); + + jest.runOnlyPendingTimers(); + + expect(updateDirtyInputSpy).toHaveBeenCalledTimes(1); + }); + + it('does not throttle updates when rapid changes are made to different form elements', () => { + const form = document.createElement('form'); + const range = rge(10); + range.forEach(i => { + form.innerHTML += `<input type="text" name="input-${i}" class="js-input-${i}"/>`; + }); + + const updateDirtyInputSpy = jest.spyOn(new DirtySubmitForm(form), 'updateDirtyInput'); + + range.forEach(i => { + const input = form.querySelector(`.js-input-${i}`); + setInputValue(input, `change`, false); + }); + + jest.runOnlyPendingTimers(); + + expect(updateDirtyInputSpy).toHaveBeenCalledTimes(range.length); + }); + }); +}); diff --git a/spec/frontend/dirty_submit/helper.js b/spec/frontend/dirty_submit/helper.js new file mode 100644 index 00000000000..c02512b7671 --- /dev/null +++ b/spec/frontend/dirty_submit/helper.js @@ -0,0 +1,43 @@ +function isCheckableType(type) { + return /^(radio|checkbox)$/.test(type); +} + +export function setInputValue(element, value) { + const { type } = element; + let eventType; + + if (isCheckableType(type)) { + element.checked = !element.checked; + eventType = 'change'; + } else { + element.value = value; + eventType = 'input'; + } + + element.dispatchEvent( + new Event(eventType, { + bubbles: true, + }), + ); +} + +export function getInputValue(input) { + return isCheckableType(input.type) ? input.checked : input.value; +} + +export function createForm(type = 'text') { + const form = document.createElement('form'); + form.innerHTML = ` + <input type="${type}" name="${type}" class="js-input"/> + <button type="submit" class="js-dirty-submit"></button> + `; + + const input = form.querySelector('.js-input'); + const submit = form.querySelector('.js-dirty-submit'); + + return { + form, + input, + submit, + }; +} diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js new file mode 100644 index 00000000000..f876987cd88 --- /dev/null +++ b/spec/frontend/pipelines/mock_data.js @@ -0,0 +1,423 @@ +export const pipelineWithStages = { + id: 20333396, + user: { + id: 128633, + name: 'Rémy Coutable', + username: 'rymai', + state: 'active', + avatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/rymai', + path: '/rymai', + }, + active: true, + coverage: '58.24', + source: 'push', + created_at: '2018-04-11T14:04:53.881Z', + updated_at: '2018-04-11T14:05:00.792Z', + path: '/gitlab-org/gitlab/pipelines/20333396', + flags: { + latest: true, + stuck: false, + auto_devops: false, + yaml_errors: false, + retryable: false, + cancelable: true, + failure_reason: false, + }, + details: { + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', + }, + duration: null, + finished_at: null, + stages: [ + { + name: 'build', + title: 'build: skipped', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#build', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_skipped-a2eee568a5bffdb494050c7b62dde241de9189280836288ac8923d369f16222d.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#build', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=build', + }, + { + name: 'prepare', + title: 'prepare: passed', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#prepare', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_success-26f59841becbef8c6fe414e9e74471d8bfd6a91b5855c19fe7f5923a40a7da47.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#prepare', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=prepare', + }, + { + name: 'test', + title: 'test: running', + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#test', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_running-2eb56be2871937954b2ba6d6f4ee9fdf7e5e1c146ac45f7be98119ccaca1aca9.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#test', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=test', + }, + { + name: 'post-test', + title: 'post-test: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#post-test', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#post-test', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-test', + }, + { + name: 'pages', + title: 'pages: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#pages', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#pages', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=pages', + }, + { + name: 'post-cleanup', + title: 'post-cleanup: created', + status: { + icon: 'status_created', + text: 'created', + label: 'created', + group: 'created', + has_details: true, + details_path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup', + favicon: + 'https://assets.gitlab-static.net/assets/ci_favicons/favicon_status_created-e997aa0b7db73165df8a9d6803932b18d7b7cc37d604d2d96e378fea2dba9c5f.ico', + }, + path: '/gitlab-org/gitlab/pipelines/20333396#post-cleanup', + dropdown_path: '/gitlab-org/gitlab/pipelines/20333396/stage.json?stage=post-cleanup', + }, + ], + artifacts: [ + { + name: 'gitlab:assets:compile', + expired: false, + expire_at: '2018-05-12T14:22:54.730Z', + path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411438/artifacts/browse', + }, + { + name: 'rspec-mysql 12 28', + expired: false, + expire_at: '2018-05-12T14:22:45.136Z', + path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411397/artifacts/browse', + }, + { + name: 'rspec-mysql 6 28', + expired: false, + expire_at: '2018-05-12T14:22:41.523Z', + path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411391/artifacts/browse', + }, + { + name: 'rspec-pg geo 0 1', + expired: false, + expire_at: '2018-05-12T14:22:13.287Z', + path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411353/artifacts/browse', + }, + { + name: 'rspec-mysql 0 28', + expired: false, + expire_at: '2018-05-12T14:22:06.834Z', + path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411385/artifacts/browse', + }, + { + name: 'spinach-mysql 0 2', + expired: false, + expire_at: '2018-05-12T14:21:51.409Z', + path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411423/artifacts/browse', + }, + { + name: 'karma', + expired: false, + expire_at: '2018-05-12T14:21:20.934Z', + path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411440/artifacts/browse', + }, + { + name: 'spinach-pg 0 2', + expired: false, + expire_at: '2018-05-12T14:20:01.028Z', + path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411419/artifacts/browse', + }, + { + name: 'spinach-pg 1 2', + expired: false, + expire_at: '2018-05-12T14:19:04.336Z', + path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411421/artifacts/browse', + }, + { + name: 'sast', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/download', + browse_path: '/gitlab-org/gitlab/-/jobs/62411442/artifacts/browse', + }, + { + name: 'code_quality', + expired: false, + expire_at: '2018-04-18T14:16:24.484Z', + path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411441/artifacts/browse', + }, + { + name: 'cache gems', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/download', + browse_path: '/gitlab-org/gitlab/-/jobs/62411447/artifacts/browse', + }, + { + name: 'dependency_scanning', + expired: null, + expire_at: null, + path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/download', + browse_path: '/gitlab-org/gitlab/-/jobs/62411443/artifacts/browse', + }, + { + name: 'compile-assets', + expired: false, + expire_at: '2018-04-18T14:12:07.638Z', + path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411334/artifacts/browse', + }, + { + name: 'setup-test-env', + expired: false, + expire_at: '2018-04-18T14:10:27.024Z', + path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411336/artifacts/browse', + }, + { + name: 'retrieve-tests-metadata', + expired: false, + expire_at: '2018-05-12T14:06:35.926Z', + path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/download', + keep_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/keep', + browse_path: '/gitlab-org/gitlab/-/jobs/62411333/artifacts/browse', + }, + ], + manual_actions: [ + { + name: 'package-and-qa', + path: '/gitlab-org/gitlab/-/jobs/62411330/play', + playable: true, + }, + { + name: 'review-docs-deploy', + path: '/gitlab-org/gitlab/-/jobs/62411332/play', + playable: true, + }, + ], + }, + ref: { + name: 'master', + path: '/gitlab-org/gitlab/commits/master', + tag: false, + branch: true, + }, + commit: { + id: 'e6a2885c503825792cb8a84a8731295e361bd059', + short_id: 'e6a2885c', + title: "Merge branch 'ce-to-ee-2018-04-11' into 'master'", + created_at: '2018-04-11T14:04:39.000Z', + parent_ids: [ + '5d9b5118f6055f72cff1a82b88133609912f2c1d', + '6fdc6ee76a8062fe41b1a33f7c503334a6ebdc02', + ], + message: + "Merge branch 'ce-to-ee-2018-04-11' into 'master'\n\nCE upstream - 2018-04-11 12:26 UTC\n\nSee merge request gitlab-org/gitlab-ee!5326", + author_name: 'Rémy Coutable', + author_email: 'remy@rymai.me', + authored_date: '2018-04-11T14:04:39.000Z', + committer_name: 'Rémy Coutable', + committer_email: 'remy@rymai.me', + committed_date: '2018-04-11T14:04:39.000Z', + author: { + id: 128633, + name: 'Rémy Coutable', + username: 'rymai', + state: 'active', + avatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + web_url: 'https://gitlab.com/rymai', + path: '/rymai', + }, + author_gravatar_url: + 'https://secure.gravatar.com/avatar/263da227929cc0035cb0eba512bcf81a?s=80\u0026d=identicon', + commit_url: + 'https://gitlab.com/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059', + commit_path: '/gitlab-org/gitlab/commit/e6a2885c503825792cb8a84a8731295e361bd059', + }, + cancel_path: '/gitlab-org/gitlab/pipelines/20333396/cancel', + triggered_by: null, + triggered: [], +}; + +export const stageReply = { + name: 'deploy', + title: 'deploy: running', + latest_statuses: [ + { + id: 928, + name: 'stop staging', + started: false, + build_path: '/twitter/flight/-/jobs/928', + cancel_path: '/twitter/flight/-/jobs/928/cancel', + playable: false, + created_at: '2018-04-04T20:02:02.728Z', + updated_at: '2018-04-04T20:02:02.766Z', + status: { + icon: 'status_pending', + text: 'pending', + label: 'pending', + group: 'pending', + tooltip: 'pending', + has_details: true, + details_path: '/twitter/flight/-/jobs/928', + favicon: + '/assets/ci_favicons/dev/favicon_status_pending-db32e1faf94b9f89530ac519790920d1f18ea8f6af6cd2e0a26cd6840cacf101.ico', + action: { + icon: 'cancel', + title: 'Cancel', + path: '/twitter/flight/-/jobs/928/cancel', + method: 'post', + }, + }, + }, + { + id: 926, + name: 'production', + started: false, + build_path: '/twitter/flight/-/jobs/926', + retry_path: '/twitter/flight/-/jobs/926/retry', + play_path: '/twitter/flight/-/jobs/926/play', + playable: true, + created_at: '2018-04-04T20:00:57.202Z', + updated_at: '2018-04-04T20:11:13.110Z', + status: { + icon: 'status_canceled', + text: 'canceled', + label: 'manual play action', + group: 'canceled', + tooltip: 'canceled', + has_details: true, + details_path: '/twitter/flight/-/jobs/926', + favicon: + '/assets/ci_favicons/dev/favicon_status_canceled-5491840b9b6feafba0bc599cbd49ee9580321dc809683856cf1b0d51532b1af6.ico', + action: { + icon: 'play', + title: 'Play', + path: '/twitter/flight/-/jobs/926/play', + method: 'post', + }, + }, + }, + { + id: 217, + name: 'staging', + started: '2018-03-07T08:41:46.234Z', + build_path: '/twitter/flight/-/jobs/217', + retry_path: '/twitter/flight/-/jobs/217/retry', + playable: false, + created_at: '2018-03-07T14:41:58.093Z', + updated_at: '2018-03-07T14:41:58.093Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/twitter/flight/-/jobs/217', + favicon: + '/assets/ci_favicons/dev/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.ico', + action: { + icon: 'retry', + title: 'Retry', + path: '/twitter/flight/-/jobs/217/retry', + method: 'post', + }, + }, + }, + ], + status: { + icon: 'status_running', + text: 'running', + label: 'running', + group: 'running', + tooltip: 'running', + has_details: true, + details_path: '/twitter/flight/pipelines/13#deploy', + favicon: + '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + }, + path: '/twitter/flight/pipelines/13#deploy', + dropdown_path: '/twitter/flight/pipelines/13/stage.json?stage=deploy', +}; diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js new file mode 100644 index 00000000000..40cd0ad9047 --- /dev/null +++ b/spec/frontend/pipelines/pipelines_spec.js @@ -0,0 +1,659 @@ +import { mount } from '@vue/test-utils'; +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import waitForPromises from 'helpers/wait_for_promises'; +import PipelinesComponent from '~/pipelines/components/pipelines.vue'; +import Store from '~/pipelines/stores/pipelines_store'; +import { pipelineWithStages, stageReply } from './mock_data'; + +describe('Pipelines', () => { + const jsonFixtureName = 'pipelines/pipelines.json'; + + preloadFixtures(jsonFixtureName); + + let pipelines; + let wrapper; + let mock; + + const paths = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + ciLintPath: '/ci/lint', + resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache', + newPipelinePath: '/twitter/flight/pipelines/new', + }; + + const noPermissions = { + endpoint: 'twitter/flight/pipelines.json', + autoDevopsPath: '/help/topics/autodevops/index.md', + helpPagePath: '/help/ci/quick_start/README', + emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg', + errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg', + noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg', + }; + + const defaultProps = { + hasGitlabCi: true, + canCreatePipeline: true, + ...paths, + }; + + const createComponent = (props = defaultProps, methods) => { + wrapper = mount(PipelinesComponent, { + propsData: { + store: new Store(), + ...props, + }, + methods: { + ...methods, + }, + }); + }; + + beforeEach(() => { + mock = new MockAdapter(axios); + pipelines = getJSONFixture(jsonFixtureName); + }); + + afterEach(() => { + wrapper.destroy(); + mock.restore(); + }); + + describe('With permission', () => { + describe('With pipelines in main tab', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + createComponent(); + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders Run Pipeline link', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toBe(paths.newPipelinePath); + }); + + it('renders CI Lint link', () => { + expect(wrapper.find('.js-ci-lint').attributes('href')).toBe(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(wrapper.find('.js-clear-cache').text()).toBe('Clear Runner Caches'); + }); + + it('renders pipelines table', () => { + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent(); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders Run Pipeline link', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath); + }); + + it('renders CI Lint link', () => { + expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath); + }); + + it('renders Clear Runner Cache button', () => { + expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches'); + }); + + it('renders tab empty state', () => { + expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + return waitForPromises(); + }); + + it('renders empty state', () => { + expect(wrapper.find('.js-empty-state h4').text()).toEqual('Build with confidence'); + + expect(wrapper.find('.js-get-started-pipelines').attributes('href')).toEqual( + paths.helpPagePath, + ); + }); + + it('does not render tabs nor buttons', () => { + expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy(); + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + }); + + describe('When API returns error', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(500, {}); + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('renders buttons', () => { + expect(wrapper.find('.js-run-pipeline').attributes('href')).toEqual(paths.newPipelinePath); + + expect(wrapper.find('.js-ci-lint').attributes('href')).toEqual(paths.ciLintPath); + expect(wrapper.find('.js-clear-cache').text()).toEqual('Clear Runner Caches'); + }); + + it('renders error state', () => { + expect(wrapper.find('.empty-state').text()).toContain( + 'There was an error fetching the pipelines.', + ); + }); + }); + }); + + describe('Without permission', () => { + describe('With pipelines in main tab', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders pipelines table', () => { + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + }); + + describe('Without pipelines on main tab with CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not render buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders tab empty state', () => { + expect(wrapper.find('.empty-state h4').text()).toEqual('There are currently no pipelines.'); + }); + }); + + describe('Without pipelines nor CI', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, { + pipelines: [], + count: { + all: 0, + pending: 0, + running: 0, + finished: 0, + }, + }); + + createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders empty state without button to set CI', () => { + expect(wrapper.find('.js-empty-state').text()).toEqual( + 'This project is not currently set up to run pipelines.', + ); + + expect(wrapper.find('.js-get-started-pipelines').exists()).toBeFalsy(); + }); + + it('does not render tabs or buttons', () => { + expect(wrapper.find('.js-pipelines-tab-all').exists()).toBeFalsy(); + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + }); + + describe('When API returns error', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(500, {}); + + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions }); + + return waitForPromises(); + }); + + it('renders tabs', () => { + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + }); + + it('does not renders buttons', () => { + expect(wrapper.find('.js-run-pipeline').exists()).toBeFalsy(); + expect(wrapper.find('.js-ci-lint').exists()).toBeFalsy(); + expect(wrapper.find('.js-clear-cache').exists()).toBeFalsy(); + }); + + it('renders error state', () => { + expect(wrapper.find('.empty-state').text()).toContain( + 'There was an error fetching the pipelines.', + ); + }); + }); + }); + + describe('successful request', () => { + describe('with pipelines', () => { + beforeEach(() => { + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + createComponent(); + return waitForPromises(); + }); + + it('should render table', () => { + expect(wrapper.find('.table-holder').exists()).toBe(true); + expect(wrapper.findAll('.gl-responsive-table-row')).toHaveLength( + pipelines.pipelines.length + 1, + ); + }); + + it('should render navigation tabs', () => { + expect(wrapper.find('.js-pipelines-tab-pending').text()).toContain('Pending'); + + expect(wrapper.find('.js-pipelines-tab-all').text()).toContain('All'); + + expect(wrapper.find('.js-pipelines-tab-running').text()).toContain('Running'); + + expect(wrapper.find('.js-pipelines-tab-finished').text()).toContain('Finished'); + + expect(wrapper.find('.js-pipelines-tab-branches').text()).toContain('Branches'); + + expect(wrapper.find('.js-pipelines-tab-tags').text()).toContain('Tags'); + }); + + it('should make an API request when using tabs', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + return waitForPromises().then(() => { + wrapper.find('.js-pipelines-tab-finished').trigger('click'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'finished', page: '1' }); + }); + }); + + describe('with pagination', () => { + it('should make an API request when using pagination', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + return waitForPromises() + .then(() => { + // Mock pagination + wrapper.vm.store.state.pageInfo = { + page: 1, + total: 10, + perPage: 2, + nextPage: 2, + totalPages: 5, + }; + + return wrapper.vm.$nextTick(); + }) + .then(() => { + wrapper.find('.next-page-item').trigger('click'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'all', page: '2' }); + }); + }); + }); + }); + }); + + describe('methods', () => { + beforeEach(() => { + jest.spyOn(window.history, 'pushState').mockImplementation(() => null); + }); + + describe('onChangeTab', () => { + it('should set page to 1', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + wrapper.vm.onChangeTab('running'); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: 'running', page: '1' }); + }); + }); + + describe('onChangePage', () => { + it('should update page and keep scope', () => { + const updateContentMock = jest.fn(() => {}); + createComponent( + { hasGitlabCi: true, canCreatePipeline: true, ...paths }, + { + updateContent: updateContentMock, + }, + ); + + wrapper.vm.onChangePage(4); + + expect(updateContentMock).toHaveBeenCalledWith({ scope: wrapper.vm.scope, page: '4' }); + }); + }); + }); + + describe('computed properties', () => { + beforeEach(() => { + createComponent(); + }); + + describe('tabs', () => { + it('returns default tabs', () => { + expect(wrapper.vm.tabs).toEqual([ + { name: 'All', scope: 'all', count: undefined, isActive: true }, + { name: 'Pending', scope: 'pending', count: undefined, isActive: false }, + { name: 'Running', scope: 'running', count: undefined, isActive: false }, + { name: 'Finished', scope: 'finished', count: undefined, isActive: false }, + { name: 'Branches', scope: 'branches', isActive: false }, + { name: 'Tags', scope: 'tags', isActive: false }, + ]); + }); + }); + + describe('emptyTabMessage', () => { + it('returns message with scope', () => { + wrapper.vm.scope = 'pending'; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pending pipelines.'); + }); + }); + + it('returns message without scope when scope is `all`', () => { + expect(wrapper.vm.emptyTabMessage).toEqual('There are currently no pipelines.'); + }); + }); + + describe('stateToRender', () => { + it('returns loading state when the app is loading', () => { + expect(wrapper.vm.stateToRender).toEqual('loading'); + }); + + it('returns error state when app has error', () => { + wrapper.vm.hasError = true; + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('error'); + }); + }); + + it('returns table list when app has pipelines', () => { + wrapper.vm.isLoading = false; + wrapper.vm.hasError = false; + wrapper.vm.state.pipelines = pipelines.pipelines; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('tableList'); + }); + }); + + it('returns empty tab when app does not have pipelines but project has pipelines', () => { + wrapper.vm.state.count.all = 10; + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyTab'); + }); + }); + + it('returns empty tab when project has CI', () => { + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyTab'); + }); + }); + + it('returns empty state when project does not have pipelines nor CI', () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + wrapper.vm.isLoading = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.stateToRender).toEqual('emptyState'); + }); + }); + }); + + describe('shouldRenderTabs', () => { + it('returns true when state is loading & has already made the first request', () => { + wrapper.vm.isLoading = true; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is tableList & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.state.pipelines = pipelines.pipelines; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is error & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.hasError = true; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns true when state is empty tab & has already made the first request', () => { + wrapper.vm.isLoading = false; + wrapper.vm.state.count.all = 10; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(true); + }); + }); + + it('returns false when has not made first request', () => { + wrapper.vm.hasMadeRequest = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(false); + }); + }); + + it('returns false when state is empty state', () => { + createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths }); + + wrapper.vm.isLoading = false; + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderTabs).toEqual(false); + }); + }); + }); + + describe('shouldRenderButtons', () => { + it('returns true when it has paths & has made the first request', () => { + wrapper.vm.hasMadeRequest = true; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderButtons).toEqual(true); + }); + }); + + it('returns false when it has not made the first request', () => { + wrapper.vm.hasMadeRequest = false; + + return wrapper.vm.$nextTick().then(() => { + expect(wrapper.vm.shouldRenderButtons).toEqual(false); + }); + }); + }); + }); + + describe('updates results when a staged is clicked', () => { + beforeEach(() => { + const copyPipeline = Object.assign({}, pipelineWithStages); + copyPipeline.id += 1; + mock + .onGet('twitter/flight/pipelines.json') + .reply( + 200, + { + pipelines: [pipelineWithStages], + count: { + all: 1, + finished: 1, + pending: 0, + running: 0, + }, + }, + { + 'POLL-INTERVAL': 100, + }, + ) + .onGet(pipelineWithStages.details.stages[0].dropdown_path) + .reply(200, stageReply); + + createComponent(); + }); + + describe('when a request is being made', () => { + it('stops polling, cancels the request, & restarts polling', () => { + const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + const cancelMock = jest.spyOn(wrapper.vm.service.cancelationSource, 'cancel'); + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + return waitForPromises() + .then(() => { + wrapper.vm.isMakingRequest = true; + wrapper.find('.js-builds-dropdown-button').trigger('click'); + }) + .then(() => { + expect(cancelMock).toHaveBeenCalled(); + expect(stopMock).toHaveBeenCalled(); + expect(restartMock).toHaveBeenCalled(); + }); + }); + }); + + describe('when no request is being made', () => { + it('stops polling & restarts polling', () => { + const stopMock = jest.spyOn(wrapper.vm.poll, 'stop'); + const restartMock = jest.spyOn(wrapper.vm.poll, 'restart'); + mock.onGet('twitter/flight/pipelines.json').reply(200, pipelines); + + return waitForPromises() + .then(() => { + wrapper.find('.js-builds-dropdown-button').trigger('click'); + expect(stopMock).toHaveBeenCalled(); + }) + .then(() => { + expect(restartMock).toHaveBeenCalled(); + }); + }); + }); + }); +}); diff --git a/spec/frontend/users_select/utils_spec.js b/spec/frontend/users_select/utils_spec.js deleted file mode 100644 index a09935d8a04..00000000000 --- a/spec/frontend/users_select/utils_spec.js +++ /dev/null @@ -1,33 +0,0 @@ -import $ from 'jquery'; -import { getAjaxUsersSelectOptions, getAjaxUsersSelectParams } from '~/users_select/utils'; - -const options = { - fooBar: 'baz', - activeUserId: 1, -}; - -describe('getAjaxUsersSelectOptions', () => { - it('returns options built from select data attributes', () => { - const $select = $('<select />', { 'data-foo-bar': 'baz', 'data-user-id': 1 }); - - expect( - getAjaxUsersSelectOptions($select, { fooBar: 'fooBar', activeUserId: 'user-id' }), - ).toEqual(options); - }); -}); - -describe('getAjaxUsersSelectParams', () => { - it('returns query parameters built from provided options', () => { - expect( - getAjaxUsersSelectParams(options, { - foo_bar: 'fooBar', - active_user_id: 'activeUserId', - non_existent_key: 'nonExistentKey', - }), - ).toEqual({ - foo_bar: 'baz', - active_user_id: 1, - non_existent_key: null, - }); - }); -}); |