diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-20 18:38:24 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2020-04-20 18:38:24 +0000 |
commit | 983a0bba5d2a042c4a3bbb22432ec192c7501d82 (patch) | |
tree | b153cd387c14ba23bd5a07514c7c01fddf6a78a0 /spec/frontend | |
parent | a2bddee2cdb38673df0e004d5b32d9f77797de64 (diff) | |
download | gitlab-ce-983a0bba5d2a042c4a3bbb22432ec192c7501d82.tar.gz |
Add latest changes from gitlab-org/gitlab@12-10-stable-ee
Diffstat (limited to 'spec/frontend')
67 files changed, 7346 insertions, 1362 deletions
diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js index 189d2629efa..971ef72521d 100644 --- a/spec/frontend/blob/components/blob_edit_content_spec.js +++ b/spec/frontend/blob/components/blob_edit_content_spec.js @@ -80,7 +80,7 @@ describe('Blob Header Editing', () => { getValue: jest.fn().mockReturnValue(value), }; - editorEl.trigger('focusout'); + editorEl.trigger('keyup'); return nextTick().then(() => { expect(wrapper.emitted().input[0]).toEqual([value]); diff --git a/spec/frontend/ci_variable_list/components/ci_key_field_spec.js b/spec/frontend/ci_variable_list/components/ci_key_field_spec.js new file mode 100644 index 00000000000..bcc29f22dd1 --- /dev/null +++ b/spec/frontend/ci_variable_list/components/ci_key_field_spec.js @@ -0,0 +1,244 @@ +import { mount } from '@vue/test-utils'; +import { GlButton, GlFormInput } from '@gitlab/ui'; +import { AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION } from '~/ci_variable_list/constants'; +import CiKeyField from '~/ci_variable_list/components/ci_key_field.vue'; + +import { + awsTokens, + awsTokenList, +} from '~/ci_variable_list/components/ci_variable_autocomplete_tokens'; + +const doTimes = (num, fn) => { + for (let i = 0; i < num; i += 1) { + fn(); + } +}; + +describe('Ci Key field', () => { + let wrapper; + + const createComponent = () => { + wrapper = mount({ + data() { + return { + inputVal: '', + tokens: awsTokenList, + }; + }, + components: { CiKeyField }, + template: ` + <div> + <ci-key-field + v-model="inputVal" + :token-list="tokens" + /> + </div> + `, + }); + }; + + const findDropdown = () => wrapper.find('#ci-variable-dropdown'); + const findDropdownOptions = () => wrapper.findAll(GlButton).wrappers.map(item => item.text()); + const findInput = () => wrapper.find(GlFormInput); + const findInputValue = () => findInput().element.value; + const setInput = val => findInput().setValue(val); + const clickDown = () => findInput().trigger('keydown.down'); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('match and filter functionality', () => { + beforeEach(() => { + createComponent(); + }); + + it('is closed when the input is empty', () => { + expect(findInput().isVisible()).toBe(true); + expect(findInputValue()).toBe(''); + expect(findDropdown().isVisible()).toBe(false); + }); + + it('is open when the input text matches a token', () => { + setInput('AWS'); + return wrapper.vm.$nextTick().then(() => { + expect(findDropdown().isVisible()).toBe(true); + }); + }); + + it('shows partial matches at string start', () => { + setInput('AWS'); + return wrapper.vm.$nextTick().then(() => { + expect(findDropdown().isVisible()).toBe(true); + expect(findDropdownOptions()).toEqual(awsTokenList); + }); + }); + + it('shows partial matches mid-string', () => { + setInput('D'); + return wrapper.vm.$nextTick().then(() => { + expect(findDropdown().isVisible()).toBe(true); + expect(findDropdownOptions()).toEqual([ + awsTokens[AWS_ACCESS_KEY_ID].name, + awsTokens[AWS_DEFAULT_REGION].name, + ]); + }); + }); + + it('is closed when the text does not match', () => { + setInput('elephant'); + return wrapper.vm.$nextTick().then(() => { + expect(findDropdown().isVisible()).toBe(false); + }); + }); + }); + + describe('keyboard navigation in dropdown', () => { + beforeEach(() => { + createComponent(); + }); + + describe('on down arrow + enter', () => { + it('selects the next item in the list and closes the dropdown', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.down'); + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[0]); + }); + }); + + it('loops to the top when it reaches the bottom', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + doTimes(findDropdownOptions().length + 1, clickDown); + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[0]); + }); + }); + }); + + describe('on up arrow + enter', () => { + it('selects the previous item in the list and closes the dropdown', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + doTimes(3, clickDown); + findInput().trigger('keydown.up'); + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[1]); + }); + }); + + it('loops to the bottom when it reaches the top', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.down'); + findInput().trigger('keydown.up'); + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[awsTokenList.length - 1]); + }); + }); + }); + + describe('on enter with no item highlighted', () => { + it('does not select any item and closes the dropdown', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.enter'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe('AWS'); + }); + }); + }); + + describe('on click', () => { + it('selects the clicked item regardless of arrow highlight', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + wrapper.find(GlButton).trigger('click'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(awsTokenList[0]); + }); + }); + }); + + describe('on tab', () => { + it('selects entered text, closes dropdown', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.tab'); + doTimes(2, clickDown); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe('AWS'); + expect(findDropdown().isVisible()).toBe(false); + }); + }); + }); + + describe('on esc', () => { + describe('when dropdown is open', () => { + it('closes dropdown and does not select anything', () => { + setInput('AWS'); + return wrapper.vm + .$nextTick() + .then(() => { + findInput().trigger('keydown.esc'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe('AWS'); + expect(findDropdown().isVisible()).toBe(false); + }); + }); + }); + + describe('when dropdown is closed', () => { + it('clears the input field', () => { + setInput('elephant'); + return wrapper.vm + .$nextTick() + .then(() => { + expect(findDropdown().isVisible()).toBe(false); + findInput().trigger('keydown.esc'); + return wrapper.vm.$nextTick(); + }) + .then(() => { + expect(findInputValue()).toBe(''); + }); + }); + }); + }); + }); +}); diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js index 70edd36669b..7b8d69df35e 100644 --- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js +++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js @@ -1,7 +1,10 @@ import Vuex from 'vuex'; -import { createLocalVue, shallowMount } from '@vue/test-utils'; +import { createLocalVue, shallowMount, mount } from '@vue/test-utils'; import { GlDeprecatedButton } from '@gitlab/ui'; +import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants'; import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue'; +import CiKeyField from '~/ci_variable_list/components/ci_key_field.vue'; +import { awsTokens } from '~/ci_variable_list/components/ci_variable_autocomplete_tokens'; import createStore from '~/ci_variable_list/store'; import mockData from '../services/mock_data'; import ModalStub from '../stubs'; @@ -13,14 +16,17 @@ describe('Ci variable modal', () => { let wrapper; let store; - const createComponent = () => { + const createComponent = (method, options = {}) => { store = createStore(); - wrapper = shallowMount(CiVariableModal, { + wrapper = method(CiVariableModal, { + attachToDocument: true, + provide: { glFeatures: { ciKeyAutocomplete: true } }, stubs: { GlModal: ModalStub, }, localVue, store, + ...options, }); }; @@ -34,22 +40,46 @@ describe('Ci variable modal', () => { .findAll(GlDeprecatedButton) .at(1); - beforeEach(() => { - createComponent(); - jest.spyOn(store, 'dispatch').mockImplementation(); - }); - afterEach(() => { wrapper.destroy(); }); - it('button is disabled when no key/value pair are present', () => { - expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + describe('Feature flag', () => { + describe('when off', () => { + beforeEach(() => { + createComponent(shallowMount, { provide: { glFeatures: { ciKeyAutocomplete: false } } }); + }); + + it('does not render the autocomplete dropdown', () => { + expect(wrapper.contains(CiKeyField)).toBe(false); + }); + }); + + describe('when on', () => { + beforeEach(() => { + createComponent(shallowMount); + }); + it('renders the autocomplete dropdown', () => { + expect(wrapper.find(CiKeyField).exists()).toBe(true); + }); + }); + }); + + describe('Basic interactions', () => { + beforeEach(() => { + createComponent(shallowMount); + }); + + it('button is disabled when no key/value pair are present', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + }); }); describe('Adding a new variable', () => { beforeEach(() => { const [variable] = mockData.mockVariables; + createComponent(shallowMount); + jest.spyOn(store, 'dispatch').mockImplementation(); store.state.variable = variable; }); @@ -71,6 +101,8 @@ describe('Ci variable modal', () => { describe('Editing a variable', () => { beforeEach(() => { const [variable] = mockData.mockVariables; + createComponent(shallowMount); + jest.spyOn(store, 'dispatch').mockImplementation(); store.state.variableBeingEdited = variable; }); @@ -96,4 +128,105 @@ describe('Ci variable modal', () => { expect(store.dispatch).toHaveBeenCalledWith('deleteVariable', mockData.mockVariables[0]); }); }); + + describe('Validations', () => { + const maskError = 'This variable can not be masked.'; + + describe('when the key state is invalid', () => { + beforeEach(() => { + const [variable] = mockData.mockVariables; + const invalidKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhy', + secret_value: 'AKIAIOSFODNN7EXAMPLEjdhy', + }; + createComponent(mount); + store.state.variable = invalidKeyVariable; + }); + + it('disables the submit button', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage; + expect(findModal().text()).toContain(errorText); + }); + }); + + describe('when the mask state is invalid', () => { + beforeEach(() => { + const [variable] = mockData.mockVariables; + const invalidMaskVariable = { + ...variable, + key: 'qs', + value: 'd:;', + secret_value: 'd:;', + masked: true, + }; + createComponent(mount); + store.state.variable = invalidMaskVariable; + }); + + it('disables the submit button', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + expect(findModal().text()).toContain(maskError); + }); + }); + + describe('when the mask and key states are invalid', () => { + beforeEach(() => { + const [variable] = mockData.mockVariables; + const invalidMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLEjdhyd:;', + secret_value: 'AKIAIOSFODNN7EXAMPLEjdhyd:;', + masked: true, + }; + createComponent(mount); + store.state.variable = invalidMaskandKeyVariable; + }); + + it('disables the submit button', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy(); + }); + + it('shows the correct error text', () => { + const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage; + expect(findModal().text()).toContain(maskError); + expect(findModal().text()).toContain(errorText); + }); + }); + + describe('when both states are valid', () => { + beforeEach(() => { + const [variable] = mockData.mockVariables; + const validMaskandKeyVariable = { + ...variable, + key: AWS_ACCESS_KEY_ID, + value: 'AKIAIOSFODNN7EXAMPLE', + secret_value: 'AKIAIOSFODNN7EXAMPLE', + masked: true, + }; + createComponent(mount); + store.state.variable = validMaskandKeyVariable; + store.state.maskableRegex = /^[a-zA-Z0-9_+=/@:-]{8,}$/; + }); + + it('does not disable the submit button', () => { + expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy(); + }); + + it('shows no error text', () => { + const errorText = awsTokens[AWS_ACCESS_KEY_ID].invalidMessage; + expect(findModal().text()).not.toContain(maskError); + expect(findModal().text()).not.toContain(errorText); + }); + }); + }); }); diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js index 8632c5c4e26..b27cd2c80fd 100644 --- a/spec/frontend/clusters/services/application_state_machine_spec.js +++ b/spec/frontend/clusters/services/application_state_machine_spec.js @@ -161,4 +161,20 @@ describe('applicationStateMachine', () => { }); }); }); + + describe('current state is undefined', () => { + it('returns the current state without having any effects', () => { + const currentAppState = {}; + expect(transitionApplicationState(currentAppState, INSTALLABLE)).toEqual(currentAppState); + }); + }); + + describe('with event is undefined', () => { + it('returns the current state without having any effects', () => { + const currentAppState = { + status: NO_STATUS, + }; + expect(transitionApplicationState(currentAppState, undefined)).toEqual(currentAppState); + }); + }); }); diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js index 517d050eb54..6bb3a0dcf21 100644 --- a/spec/frontend/diffs/components/commit_item_spec.js +++ b/spec/frontend/diffs/components/commit_item_spec.js @@ -59,9 +59,7 @@ describe('diffs/components/commit_item', () => { expect(titleElement.text()).toBe(commit.title_html); }); - // https://gitlab.com/gitlab-org/gitlab/-/issues/209776 - // eslint-disable-next-line jest/no-disabled-tests - it.skip('renders commit description', () => { + it('renders commit description', () => { const descElement = getDescElement(); const descExpandElement = getDescExpandElement(); diff --git a/spec/frontend/diffs/components/diff_table_cell_spec.js b/spec/frontend/diffs/components/diff_table_cell_spec.js index 1af0746f3bd..e871d86d901 100644 --- a/spec/frontend/diffs/components/diff_table_cell_spec.js +++ b/spec/frontend/diffs/components/diff_table_cell_spec.js @@ -85,15 +85,18 @@ describe('DiffTableCell', () => { describe('comment button', () => { it.each` - showCommentButton | userData | query | expectation - ${true} | ${TEST_USER} | ${'diff_head=false'} | ${true} - ${true} | ${TEST_USER} | ${'diff_head=true'} | ${false} - ${false} | ${TEST_USER} | ${'bogus'} | ${false} - ${true} | ${null} | ${''} | ${false} + showCommentButton | userData | query | mergeRefHeadComments | expectation + ${true} | ${TEST_USER} | ${'diff_head=false'} | ${false} | ${true} + ${true} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${true} + ${true} | ${TEST_USER} | ${'diff_head=true'} | ${false} | ${false} + ${false} | ${TEST_USER} | ${'diff_head=true'} | ${true} | ${false} + ${false} | ${TEST_USER} | ${'bogus'} | ${true} | ${false} + ${true} | ${null} | ${''} | ${true} | ${false} `( 'exists is $expectation - with showCommentButton ($showCommentButton) userData ($userData) query ($query)', - ({ showCommentButton, userData, query, expectation }) => { + ({ showCommentButton, userData, query, mergeRefHeadComments, expectation }) => { store.state.notes.userData = userData; + gon.features = { mergeRefHeadComments }; setWindowLocation({ href: `${TEST_HOST}?${query}` }); createComponent({ showCommentButton }); diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js index 8a1c3e56e5a..ceccce6312f 100644 --- a/spec/frontend/diffs/store/actions_spec.js +++ b/spec/frontend/diffs/store/actions_spec.js @@ -466,6 +466,7 @@ describe('DiffsStoreActions', () => { old_path: 'file2', line_code: 'ABC_1_1', position_type: 'text', + line_range: null, }, }, hash: 'ABC_123', diff --git a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js index 3e5ba66d5e4..0343ef75732 100644 --- a/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js +++ b/spec/frontend/diffs/store/getters_versions_dropdowns_spec.js @@ -1,6 +1,9 @@ import * as getters from '~/diffs/store/getters'; import state from '~/diffs/store/modules/diff_state'; -import { DIFF_COMPARE_BASE_VERSION_INDEX } from '~/diffs/constants'; +import { + DIFF_COMPARE_BASE_VERSION_INDEX, + DIFF_COMPARE_HEAD_VERSION_INDEX, +} from '~/diffs/constants'; import diffsMockData from '../mock_data/merge_request_diffs'; describe('Compare diff version dropdowns', () => { @@ -37,47 +40,93 @@ describe('Compare diff version dropdowns', () => { describe('diffCompareDropdownTargetVersions', () => { // diffCompareDropdownTargetVersions slices the array at the first position - // and appends a "base" version which is why we use diffsMockData[1] below - // This is to display "base" at the end of the target dropdown - const expectedFirstVersion = { - ...diffsMockData[1], - href: expect.any(String), - versionName: expect.any(String), + // and appends a "base" and "head" version at the end of the list so that + // "base" and "head" appear at the bottom of the dropdown + // this is also why we use diffsMockData[1] for the "first" version + + let expectedFirstVersion; + let expectedBaseVersion; + let expectedHeadVersion; + const originalLocation = window.location; + + const setupTest = includeDiffHeadParam => { + const diffHeadParam = includeDiffHeadParam ? '?diff_head=true' : ''; + + Object.defineProperty(window, 'location', { + writable: true, + value: { href: `https://example.gitlab.com${diffHeadParam}` }, + }); + + expectedFirstVersion = { + ...diffsMockData[1], + href: expect.any(String), + versionName: expect.any(String), + selected: false, + }; + + expectedBaseVersion = { + versionName: 'baseVersion', + version_index: DIFF_COMPARE_BASE_VERSION_INDEX, + href: 'basePath', + isBase: true, + selected: false, + }; + + expectedHeadVersion = { + versionName: 'baseVersion', + version_index: DIFF_COMPARE_HEAD_VERSION_INDEX, + href: 'headPath', + isHead: true, + selected: false, + }; }; - const expectedBaseVersion = { - versionName: 'baseVersion', - version_index: DIFF_COMPARE_BASE_VERSION_INDEX, - href: 'basePath', - isBase: true, + const assertVersions = targetVersions => { + // base and head should be the last two versions in that order + const targetBaseVersion = targetVersions[targetVersions.length - 2]; + const targetHeadVersion = targetVersions[targetVersions.length - 1]; + expect(targetVersions[0]).toEqual(expectedFirstVersion); + expect(targetBaseVersion).toEqual(expectedBaseVersion); + expect(targetHeadVersion).toEqual(expectedHeadVersion); }; + afterEach(() => { + window.location = originalLocation; + }); + it('base version selected', () => { - expectedFirstVersion.selected = false; + setupTest(); expectedBaseVersion.selected = true; - const targetVersions = getters.diffCompareDropdownTargetVersions(localState, { - selectedTargetIndex: DIFF_COMPARE_BASE_VERSION_INDEX, - }); + const targetVersions = getters.diffCompareDropdownTargetVersions(localState, getters); + assertVersions(targetVersions); + }); - const lastVersion = targetVersions[targetVersions.length - 1]; - expect(targetVersions[0]).toEqual(expectedFirstVersion); - expect(lastVersion).toEqual(expectedBaseVersion); + it('head version selected', () => { + setupTest(true); + + expectedHeadVersion.selected = true; + + const targetVersions = getters.diffCompareDropdownTargetVersions(localState, getters); + assertVersions(targetVersions); }); it('first version selected', () => { - expectedFirstVersion.selected = true; - expectedBaseVersion.selected = false; + // NOTE: It should not be possible to have both "diff_head=true" and + // have anything other than the head version selected, but the user could + // manually add "?diff_head=true" to the url. In this instance we still + // want the actual selected version to display as "selected" + // Passing in "true" here asserts that first version is still selected + // even if "diff_head" is present in the url + setupTest(true); + expectedFirstVersion.selected = true; localState.startVersion = expectedFirstVersion; const targetVersions = getters.diffCompareDropdownTargetVersions(localState, { selectedTargetIndex: expectedFirstVersion.version_index, }); - - const lastVersion = targetVersions[targetVersions.length - 1]; - expect(targetVersions[0]).toEqual(expectedFirstVersion); - expect(lastVersion).toEqual(expectedBaseVersion); + assertVersions(targetVersions); }); }); diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js index c44feaf4b63..858ab5be167 100644 --- a/spec/frontend/diffs/store/mutations_spec.js +++ b/spec/frontend/diffs/store/mutations_spec.js @@ -615,6 +615,73 @@ describe('DiffsStoreMutations', () => { expect(state.diffFiles[0].highlighted_diff_lines[0].discussions.length).toEqual(1); expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toEqual(1); }); + + it('should add discussions by line_codes and positions attributes', () => { + const diffPosition = { + base_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + head_sha: 'b921914f9a834ac47e6fd9420f78db0f83559130', + new_line: null, + new_path: '500-lines-4.txt', + old_line: 5, + old_path: '500-lines-4.txt', + start_sha: 'ed13df29948c41ba367caa757ab3ec4892509910', + }; + + const state = { + latestDiff: true, + diffFiles: [ + { + file_hash: 'ABC', + parallel_diff_lines: [ + { + left: { + line_code: 'ABC_1', + discussions: [], + }, + right: { + line_code: 'ABC_1', + discussions: [], + }, + }, + ], + highlighted_diff_lines: [ + { + line_code: 'ABC_1', + discussions: [], + }, + ], + }, + ], + }; + const discussion = { + id: 1, + line_code: 'ABC_2', + line_codes: ['ABC_1'], + diff_discussion: true, + resolvable: true, + original_position: {}, + position: {}, + positions: [diffPosition], + diff_file: { + file_hash: state.diffFiles[0].file_hash, + }, + }; + + const diffPositionByLineCode = { + ABC_1: diffPosition, + }; + + mutations[types.SET_LINE_DISCUSSIONS_FOR_FILE](state, { + discussion, + diffPositionByLineCode, + }); + + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions).toHaveLength(1); + expect(state.diffFiles[0].parallel_diff_lines[0].left.discussions[0].id).toBe(1); + + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions).toHaveLength(1); + expect(state.diffFiles[0].highlighted_diff_lines[0].discussions[0].id).toBe(1); + }); }); describe('REMOVE_LINE_DISCUSSIONS', () => { diff --git a/spec/frontend/fixtures/merge_requests_diffs.rb b/spec/frontend/fixtures/merge_requests_diffs.rb index 7997ee79a01..76bb8567a64 100644 --- a/spec/frontend/fixtures/merge_requests_diffs.rb +++ b/spec/frontend/fixtures/merge_requests_diffs.rb @@ -10,7 +10,6 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type let(:project) { create(:project, :repository, namespace: namespace, path: 'merge-requests-project') } let(:merge_request) { create(:merge_request, :with_diffs, source_project: project, target_project: project, description: '- [ ] Task List Item') } let(:path) { "files/ruby/popen.rb" } - let(:selected_commit) { merge_request.all_commits[0] } let(:position) do build(:text_diff_position, :added, file: path, @@ -34,11 +33,11 @@ describe Projects::MergeRequests::DiffsController, '(JavaScript fixtures)', type end it 'merge_request_diffs/with_commit.json' do - # Create a user that matches the selected commit author + # Create a user that matches the project.commit author # This is so that the "author" information will be populated - create(:user, email: selected_commit.author_email, name: selected_commit.author_name) + create(:user, email: project.commit.author_email, name: project.commit.author_name) - render_merge_request(merge_request, commit_id: selected_commit.sha) + render_merge_request(merge_request, commit_id: project.commit.sha) end it 'merge_request_diffs/inline_changes_tab_with_comments.json' do diff --git a/spec/frontend/helpers/dom_events_helper.js b/spec/frontend/helpers/dom_events_helper.js new file mode 100644 index 00000000000..b66c12daf4f --- /dev/null +++ b/spec/frontend/helpers/dom_events_helper.js @@ -0,0 +1,10 @@ +export const triggerDOMEvent = type => { + window.document.dispatchEvent( + new Event(type, { + bubbles: true, + cancelable: true, + }), + ); +}; + +export default () => {}; diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js index fb3ffe1ede3..ce32559d5c9 100644 --- a/spec/frontend/jira_import/components/jira_import_app_spec.js +++ b/spec/frontend/jira_import/components/jira_import_app_spec.js @@ -1,38 +1,213 @@ +import { GlAlert, GlLoadingIcon } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; +import Vue from 'vue'; import JiraImportApp from '~/jira_import/components/jira_import_app.vue'; +import JiraImportForm from '~/jira_import/components/jira_import_form.vue'; +import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue'; import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue'; +import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql'; +import { IMPORT_STATE } from '~/jira_import/utils'; + +const mountComponent = ({ + isJiraConfigured = true, + errorMessage = '', + showAlert = true, + status = IMPORT_STATE.NONE, + loading = false, + mutate = jest.fn(() => Promise.resolve()), +} = {}) => + shallowMount(JiraImportApp, { + propsData: { + isJiraConfigured, + inProgressIllustration: 'in-progress-illustration.svg', + issuesPath: 'gitlab-org/gitlab-test/-/issues', + jiraProjects: [ + ['My Jira Project', 'MJP'], + ['My Second Jira Project', 'MSJP'], + ['Migrate to GitLab', 'MTG'], + ], + projectPath: 'gitlab-org/gitlab-test', + setupIllustration: 'setup-illustration.svg', + }, + data() { + return { + errorMessage, + showAlert, + jiraImportDetails: { + status, + import: { + jiraProjectKey: 'MTG', + scheduledAt: '2020-04-08T12:17:25+00:00', + scheduledBy: { + name: 'Jane Doe', + }, + }, + }, + }; + }, + mocks: { + $apollo: { + loading, + mutate, + }, + }, + }); describe('JiraImportApp', () => { let wrapper; + const getFormComponent = () => wrapper.find(JiraImportForm); + + const getProgressComponent = () => wrapper.find(JiraImportProgress); + + const getSetupComponent = () => wrapper.find(JiraImportSetup); + + const getAlert = () => wrapper.find(GlAlert); + + const getLoadingIcon = () => wrapper.find(GlLoadingIcon); + afterEach(() => { wrapper.destroy(); wrapper = null; }); - describe('set up Jira integration page', () => { + describe('when Jira integration is not configured', () => { + beforeEach(() => { + wrapper = mountComponent({ isJiraConfigured: false }); + }); + + it('shows the "Set up Jira integration" screen', () => { + expect(getSetupComponent().exists()).toBe(true); + }); + + it('does not show loading icon', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + it('does not show the "Import in progress" screen', () => { + expect(getProgressComponent().exists()).toBe(false); + }); + + it('does not show the "Import Jira project" form', () => { + expect(getFormComponent().exists()).toBe(false); + }); + }); + + describe('when Jira integration is configured but data is being fetched', () => { + beforeEach(() => { + wrapper = mountComponent({ loading: true }); + }); + + it('does not show the "Set up Jira integration" screen', () => { + expect(getSetupComponent().exists()).toBe(false); + }); + + it('shows loading icon', () => { + expect(getLoadingIcon().exists()).toBe(true); + }); + + it('does not show the "Import in progress" screen', () => { + expect(getProgressComponent().exists()).toBe(false); + }); + + it('does not show the "Import Jira project" form', () => { + expect(getFormComponent().exists()).toBe(false); + }); + }); + + describe('when Jira integration is configured but import is in progress', () => { + beforeEach(() => { + wrapper = mountComponent({ status: IMPORT_STATE.SCHEDULED }); + }); + + it('does not show the "Set up Jira integration" screen', () => { + expect(getSetupComponent().exists()).toBe(false); + }); + + it('does not show loading icon', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + it('shows the "Import in progress" screen', () => { + expect(getProgressComponent().exists()).toBe(true); + }); + + it('does not show the "Import Jira project" form', () => { + expect(getFormComponent().exists()).toBe(false); + }); + }); + + describe('when Jira integration is configured and there is no import in progress', () => { beforeEach(() => { - wrapper = shallowMount(JiraImportApp, { - propsData: { - isJiraConfigured: true, - projectPath: 'gitlab-org/gitlab-test', - setupIllustration: 'illustration.svg', + wrapper = mountComponent(); + }); + + it('does not show the "Set up Jira integration" screen', () => { + expect(getSetupComponent().exists()).toBe(false); + }); + + it('does not show loading icon', () => { + expect(getLoadingIcon().exists()).toBe(false); + }); + + it('does not show the Import in progress" screen', () => { + expect(getProgressComponent().exists()).toBe(false); + }); + + it('shows the "Import Jira project" form', () => { + expect(getFormComponent().exists()).toBe(true); + }); + }); + + describe('initiating a Jira import', () => { + it('calls the mutation with the expected arguments', () => { + const mutate = jest.fn(() => Promise.resolve()); + + wrapper = mountComponent({ mutate }); + + const mutationArguments = { + mutation: initiateJiraImportMutation, + variables: { + input: { + jiraProjectKey: 'MTG', + projectPath: 'gitlab-org/gitlab-test', + }, }, - }); + }; + + getFormComponent().vm.$emit('initiateJiraImport', 'MTG'); + + expect(mutate).toHaveBeenCalledWith(expect.objectContaining(mutationArguments)); }); - it('is shown when Jira integration is not configured', () => { - wrapper.setProps({ - isJiraConfigured: false, - }); + it('shows alert message with error message on error', () => { + const mutate = jest.fn(() => Promise.reject()); + + wrapper = mountComponent({ mutate }); + + getFormComponent().vm.$emit('initiateJiraImport', 'MTG'); + + // One tick doesn't update the dom to the desired state so we have two ticks here + return Vue.nextTick() + .then(Vue.nextTick) + .then(() => { + expect(getAlert().text()).toBe('There was an error importing the Jira project.'); + }); + }); + }); - return wrapper.vm.$nextTick(() => { - expect(wrapper.find(JiraImportSetup).exists()).toBe(true); - }); + it('can dismiss alert message', () => { + wrapper = mountComponent({ + errorMessage: 'There was an error importing the Jira project.', + showAlert: true, }); - it('is not shown when Jira integration is configured', () => { - expect(wrapper.find(JiraImportSetup).exists()).toBe(false); + expect(getAlert().exists()).toBe(true); + + getAlert().vm.$emit('dismiss'); + + return Vue.nextTick().then(() => { + expect(getAlert().exists()).toBe(false); }); }); }); diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js index 315ccccd991..0987eb11693 100644 --- a/spec/frontend/jira_import/components/jira_import_form_spec.js +++ b/spec/frontend/jira_import/components/jira_import_form_spec.js @@ -1,62 +1,126 @@ -import { GlAvatar, GlNewButton, GlFormSelect, GlLabel } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; +import { GlAvatar, GlButton, GlFormSelect, GlLabel } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; import JiraImportForm from '~/jira_import/components/jira_import_form.vue'; +const mountComponent = ({ mountType } = {}) => { + const mountFunction = mountType === 'mount' ? mount : shallowMount; + + return mountFunction(JiraImportForm, { + propsData: { + issuesPath: 'gitlab-org/gitlab-test/-/issues', + jiraProjects: [ + { + text: 'My Jira Project', + value: 'MJP', + }, + { + text: 'My Second Jira Project', + value: 'MSJP', + }, + { + text: 'Migrate to GitLab', + value: 'MTG', + }, + ], + }, + }); +}; + describe('JiraImportForm', () => { let wrapper; - beforeEach(() => { - wrapper = shallowMount(JiraImportForm); - }); + const getCancelButton = () => wrapper.findAll(GlButton).at(1); afterEach(() => { wrapper.destroy(); wrapper = null; }); - it('shows a dropdown to choose the Jira project to import from', () => { - expect(wrapper.find(GlFormSelect).exists()).toBe(true); - }); + describe('select dropdown', () => { + it('is shown', () => { + wrapper = mountComponent(); - it('shows a label which will be applied to imported Jira projects', () => { - expect(wrapper.find(GlLabel).attributes('title')).toBe('jira-import::KEY-1'); - }); + expect(wrapper.find(GlFormSelect).exists()).toBe(true); + }); - it('shows information to the user', () => { - expect(wrapper.find('p').text()).toBe( - "For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:", - ); - }); + it('contains a list of Jira projects to select from', () => { + wrapper = mountComponent({ mountType: 'mount' }); - it('shows jira.issue.summary for the Title', () => { - expect(wrapper.find('[id="jira-project-title"]').text()).toBe('jira.issue.summary'); + const optionItems = ['My Jira Project', 'My Second Jira Project', 'Migrate to GitLab']; + + wrapper + .find(GlFormSelect) + .findAll('option') + .wrappers.forEach((optionEl, index) => { + expect(optionEl.text()).toBe(optionItems[index]); + }); + }); }); - it('shows an avatar for the Reporter', () => { - expect(wrapper.find(GlAvatar).exists()).toBe(true); + describe('form information', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('shows a label which will be applied to imported Jira projects', () => { + expect(wrapper.find(GlLabel).attributes('title')).toBe('jira-import::KEY-1'); + }); + + it('shows information to the user', () => { + expect(wrapper.find('p').text()).toBe( + "For each Jira issue successfully imported, we'll create a new GitLab issue with the following data:", + ); + }); + + it('shows jira.issue.summary for the Title', () => { + expect(wrapper.find('[id="jira-project-title"]').text()).toBe('jira.issue.summary'); + }); + + it('shows an avatar for the Reporter', () => { + expect(wrapper.find(GlAvatar).exists()).toBe(true); + }); + + it('shows jira.issue.description.content for the Description', () => { + expect(wrapper.find('[id="jira-project-description"]').text()).toBe( + 'jira.issue.description.content', + ); + }); }); - it('shows jira.issue.description.content for the Description', () => { - expect(wrapper.find('[id="jira-project-description"]').text()).toBe( - 'jira.issue.description.content', - ); + describe('Next button', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('is shown', () => { + expect(wrapper.find(GlButton).text()).toBe('Next'); + }); }); - it('shows a Next button', () => { - const nextButton = wrapper - .findAll(GlNewButton) - .at(0) - .text(); + describe('Cancel button', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('is shown', () => { + expect(getCancelButton().text()).toBe('Cancel'); + }); - expect(nextButton).toBe('Next'); + it('links to the Issues page', () => { + expect(getCancelButton().attributes('href')).toBe('gitlab-org/gitlab-test/-/issues'); + }); }); - it('shows a Cancel button', () => { - const cancelButton = wrapper - .findAll(GlNewButton) - .at(1) - .text(); + it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => { + const selectedOption = 'MTG'; + + wrapper = mountComponent(); + wrapper.setData({ + selectedOption, + }); + + wrapper.find('form').trigger('submit'); - expect(cancelButton).toBe('Cancel'); + expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([selectedOption]); }); }); diff --git a/spec/frontend/jira_import/components/jira_import_progress_spec.js b/spec/frontend/jira_import/components/jira_import_progress_spec.js new file mode 100644 index 00000000000..9a6fc3b5925 --- /dev/null +++ b/spec/frontend/jira_import/components/jira_import_progress_spec.js @@ -0,0 +1,70 @@ +import { GlEmptyState } from '@gitlab/ui'; +import { mount, shallowMount } from '@vue/test-utils'; +import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue'; + +describe('JiraImportProgress', () => { + let wrapper; + + const getGlEmptyStateAttribute = attribute => wrapper.find(GlEmptyState).attributes(attribute); + + const getParagraphText = () => wrapper.find('p').text(); + + const mountComponent = ({ mountType = 'shallowMount' } = {}) => { + const mountFunction = mountType === 'shallowMount' ? shallowMount : mount; + return mountFunction(JiraImportProgress, { + propsData: { + illustration: 'illustration.svg', + importInitiator: 'Jane Doe', + importProject: 'JIRAPROJECT', + importTime: '2020-04-08T12:17:25+00:00', + issuesPath: 'gitlab-org/gitlab-test/-/issues', + }, + }); + }; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('empty state', () => { + beforeEach(() => { + wrapper = mountComponent(); + }); + + it('contains illustration', () => { + expect(getGlEmptyStateAttribute('svgpath')).toBe('illustration.svg'); + }); + + it('contains a title', () => { + const title = 'Import in progress'; + expect(getGlEmptyStateAttribute('title')).toBe(title); + }); + + it('contains button text', () => { + expect(getGlEmptyStateAttribute('primarybuttontext')).toBe('View issues'); + }); + + it('contains button url', () => { + expect(getGlEmptyStateAttribute('primarybuttonlink')).toBe('gitlab-org/gitlab-test/-/issues'); + }); + }); + + describe('description', () => { + beforeEach(() => { + wrapper = mountComponent({ mountType: 'mount' }); + }); + + it('shows who initiated the import', () => { + expect(getParagraphText()).toContain('Import started by: Jane Doe'); + }); + + it('shows the time of import', () => { + expect(getParagraphText()).toContain('Time of import: Apr 8, 2020 12:17pm GMT+0000'); + }); + + it('shows the project key of the import', () => { + expect(getParagraphText()).toContain('Jira project: JIRAPROJECT'); + }); + }); +}); diff --git a/spec/frontend/jira_import/components/jira_import_setup_spec.js b/spec/frontend/jira_import/components/jira_import_setup_spec.js index 27366bd7e8a..834c14b512e 100644 --- a/spec/frontend/jira_import/components/jira_import_setup_spec.js +++ b/spec/frontend/jira_import/components/jira_import_setup_spec.js @@ -1,9 +1,12 @@ +import { GlEmptyState } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue'; describe('JiraImportSetup', () => { let wrapper; + const getGlEmptyStateAttribute = attribute => wrapper.find(GlEmptyState).attributes(attribute); + beforeEach(() => { wrapper = shallowMount(JiraImportSetup, { propsData: { @@ -17,12 +20,16 @@ describe('JiraImportSetup', () => { wrapper = null; }); - it('displays a message to the user', () => { - const message = 'You will first need to set up Jira Integration to use this feature.'; - expect(wrapper.find('p').text()).toBe(message); + it('contains illustration', () => { + expect(getGlEmptyStateAttribute('svgpath')).toBe('illustration.svg'); + }); + + it('contains a description', () => { + const description = 'You will first need to set up Jira Integration to use this feature.'; + expect(getGlEmptyStateAttribute('description')).toBe(description); }); - it('contains button to set up Jira integration', () => { - expect(wrapper.find('a').text()).toBe('Set up Jira Integration'); + it('contains button text', () => { + expect(getGlEmptyStateAttribute('primarybuttontext')).toBe('Set up Jira Integration'); }); }); diff --git a/spec/frontend/jira_import/utils_spec.js b/spec/frontend/jira_import/utils_spec.js new file mode 100644 index 00000000000..a14db104229 --- /dev/null +++ b/spec/frontend/jira_import/utils_spec.js @@ -0,0 +1,27 @@ +import { IMPORT_STATE, isInProgress } from '~/jira_import/utils'; + +describe('isInProgress', () => { + it('returns true when state is IMPORT_STATE.SCHEDULED', () => { + expect(isInProgress(IMPORT_STATE.SCHEDULED)).toBe(true); + }); + + it('returns true when state is IMPORT_STATE.STARTED', () => { + expect(isInProgress(IMPORT_STATE.STARTED)).toBe(true); + }); + + it('returns false when state is IMPORT_STATE.FAILED', () => { + expect(isInProgress(IMPORT_STATE.FAILED)).toBe(false); + }); + + it('returns false when state is IMPORT_STATE.FINISHED', () => { + expect(isInProgress(IMPORT_STATE.FINISHED)).toBe(false); + }); + + it('returns false when state is IMPORT_STATE.NONE', () => { + expect(isInProgress(IMPORT_STATE.NONE)).toBe(false); + }); + + it('returns false when state is undefined', () => { + expect(isInProgress()).toBe(false); + }); +}); diff --git a/spec/frontend/logs/mock_data.js b/spec/frontend/logs/mock_data.js index 537582cff5a..14c8f7a2ba2 100644 --- a/spec/frontend/logs/mock_data.js +++ b/spec/frontend/logs/mock_data.js @@ -34,91 +34,31 @@ export const mockPods = [ export const mockLogsResult = [ { timestamp: '2019-12-13T13:43:18.2760123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13', + message: 'log line 1', pod: 'foo', }, { timestamp: '2019-12-13T13:43:18.2760123Z', - message: '- -> /', + message: 'log line A', pod: 'bar', }, { timestamp: '2019-12-13T13:43:26.8420123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13', + message: 'log line 2', pod: 'foo', }, { timestamp: '2019-12-13T13:43:26.8420123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:28.3710123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:28.3710123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:36.8860123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:36.8860123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:38.4000123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:38.4000123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:46.8420123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:46.8430123Z', - message: '- -> /', - pod: 'bar', - }, - { - timestamp: '2019-12-13T13:43:48.3240123Z', - message: '10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13', - pod: 'foo', - }, - { - timestamp: '2019-12-13T13:43:48.3250123Z', - message: '- -> /', + message: 'log line B', pod: 'bar', }, ]; export const mockTrace = [ - 'Dec 13 13:43:18.276Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:48 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:18.276Z | bar | - -> /', - 'Dec 13 13:43:26.842Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:57 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:26.842Z | bar | - -> /', - 'Dec 13 13:43:28.371Z | foo | 10.36.0.1 - - [16/Oct/2019:06:29:58 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:28.371Z | bar | - -> /', - 'Dec 13 13:43:36.886Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:07 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:36.886Z | bar | - -> /', - 'Dec 13 13:43:38.400Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:08 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:38.400Z | bar | - -> /', - 'Dec 13 13:43:46.842Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:17 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:46.843Z | bar | - -> /', - 'Dec 13 13:43:48.324Z | foo | 10.36.0.1 - - [16/Oct/2019:06:30:18 UTC] "GET / HTTP/1.1" 200 13', - 'Dec 13 13:43:48.325Z | bar | - -> /', + 'Dec 13 13:43:18.276 | foo | log line 1', + 'Dec 13 13:43:18.276 | bar | log line A', + 'Dec 13 13:43:26.842 | foo | log line 2', + 'Dec 13 13:43:26.842 | bar | log line B', ]; export const mockResponse = { diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap index d968b042ff1..1906ad7c6ed 100644 --- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap +++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap @@ -6,101 +6,106 @@ exports[`Dashboard template matches the default snapshot 1`] = ` data-qa-selector="prometheus_graphs" > <div - class="prometheus-graphs-header gl-p-3 pb-0 border-bottom bg-gray-light" + class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light" > <div - class="row" + class="mb-2 pr-2 d-flex d-sm-block" > - <gl-form-group-stub - class="col-sm-12 col-md-6 col-lg-2" - label="Dashboard" - label-for="monitor-dashboards-dropdown" - label-size="sm" - > - <dashboards-dropdown-stub - class="mb-0 d-flex" - data-qa-selector="dashboards_filter_dropdown" - defaultbranch="master" - id="monitor-dashboards-dropdown" - selecteddashboard="[object Object]" - toggle-class="dropdown-menu-toggle" - /> - </gl-form-group-stub> - - <gl-form-group-stub - class="col-sm-6 col-md-6 col-lg-2" - label="Environment" - label-for="monitor-environments-dropdown" - label-size="sm" + <dashboards-dropdown-stub + class="flex-grow-1" + data-qa-selector="dashboards_filter_dropdown" + defaultbranch="master" + id="monitor-dashboards-dropdown" + selecteddashboard="[object Object]" + toggle-class="dropdown-menu-toggle" + /> + </div> + + <div + class="mb-2 pr-2 d-flex d-sm-block" + > + <gl-dropdown-stub + class="flex-grow-1" + data-qa-selector="environments_dropdown" + id="monitor-environments-dropdown" + menu-class="monitor-environment-dropdown-menu" + text="production" + toggle-class="dropdown-menu-toggle" > - <gl-dropdown-stub - class="mb-0 d-flex" - data-qa-selector="environments_dropdown" - id="monitor-environments-dropdown" - menu-class="monitor-environment-dropdown-menu" - text="production" - toggle-class="dropdown-menu-toggle" + <div + class="d-flex flex-column overflow-hidden" > + <gl-dropdown-header-stub + class="monitor-environment-dropdown-header text-center" + > + + Environment + + </gl-dropdown-header-stub> + + <gl-dropdown-divider-stub /> + + <gl-search-box-by-type-stub + class="m-2" + clearbuttontitle="Clear" + value="" + /> + + <div + class="flex-fill overflow-auto" + /> + <div - class="d-flex flex-column overflow-hidden" + class="text-secondary no-matches-message" > - <gl-dropdown-header-stub - class="monitor-environment-dropdown-header text-center" - > - Environment - </gl-dropdown-header-stub> - - <gl-dropdown-divider-stub /> - - <gl-search-box-by-type-stub - class="m-2" - clearbuttontitle="Clear" - value="" - /> - - <div - class="flex-fill overflow-auto" - /> - - <div - class="text-secondary no-matches-message" - > - - No matching results - </div> + No matching results + </div> - </gl-dropdown-stub> - </gl-form-group-stub> - - <gl-form-group-stub - class="col-sm-auto col-md-auto col-lg-auto" + </div> + </gl-dropdown-stub> + </div> + + <div + class="mb-2 pr-2 d-flex d-sm-block" + > + <date-time-picker-stub + class="flex-grow-1 show-last-dropdown" + customenabled="true" data-qa-selector="show_last_dropdown" - label="Show last" - label-for="monitor-time-window-dropdown" - label-size="sm" + options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" + value="[object Object]" + /> + </div> + + <div + class="mb-2 pr-2 d-flex d-sm-block" + > + <gl-deprecated-button-stub + class="flex-grow-1" + size="md" + title="Refresh dashboard" + variant="default" > - <date-time-picker-stub - customenabled="true" - options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]" - value="[object Object]" + <icon-stub + name="retry" + size="16" /> - </gl-form-group-stub> + </gl-deprecated-button-stub> + </div> + + <div + class="flex-grow-1" + /> + + <div + class="d-sm-flex" + > + <!----> - <gl-form-group-stub - class="col-sm-2 col-md-2 col-lg-1 refresh-dashboard-button" - > - <gl-deprecated-button-stub - size="md" - title="Refresh dashboard" - variant="default" - > - <icon-stub - name="retry" - size="16" - /> - </gl-deprecated-button-stub> - </gl-form-group-stub> + <!----> + + <!----> <!----> </div> diff --git a/spec/frontend/monitoring/components/charts/annotations_spec.js b/spec/frontend/monitoring/components/charts/annotations_spec.js index 69bf1fe4ced..fc90175d307 100644 --- a/spec/frontend/monitoring/components/charts/annotations_spec.js +++ b/spec/frontend/monitoring/components/charts/annotations_spec.js @@ -54,6 +54,7 @@ describe('annotations spec', () => { yAxisIndex: 1, data: expect.any(Array), markLine: expect.any(Object), + markPoint: expect.any(Object), }), ); @@ -61,11 +62,12 @@ describe('annotations spec', () => { expect(annotation).toEqual(expect.any(Object)); }); - expect(annotations.data).toHaveLength(annotationsData.length); + expect(annotations.data).toHaveLength(0); expect(annotations.markLine.data).toHaveLength(annotationsData.length); + expect(annotations.markPoint.data).toHaveLength(annotationsData.length); }); - it('when deploments and annotations data is passed', () => { + it('when deployments and annotations data is passed', () => { const annotations = generateAnnotationsSeries({ deployments: deploymentData, annotations: annotationsData, @@ -77,6 +79,7 @@ describe('annotations spec', () => { yAxisIndex: 1, data: expect.any(Array), markLine: expect.any(Object), + markPoint: expect.any(Object), }), ); @@ -84,7 +87,9 @@ describe('annotations spec', () => { expect(annotation).toEqual(expect.any(Object)); }); - expect(annotations.data).toHaveLength(deploymentData.length + annotationsData.length); + expect(annotations.data).toHaveLength(deploymentData.length); + expect(annotations.markLine.data).toHaveLength(annotationsData.length); + expect(annotations.markPoint.data).toHaveLength(annotationsData.length); }); }); }); diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js index d219a6627bf..1c8fdc01e3e 100644 --- a/spec/frontend/monitoring/components/charts/options_spec.js +++ b/spec/frontend/monitoring/components/charts/options_spec.js @@ -31,7 +31,32 @@ describe('options spec', () => { }); }); - it('formatter options', () => { + it('formatter options defaults to engineering notation', () => { + const options = getYAxisOptions(); + + expect(options.axisLabel.formatter).toEqual(expect.any(Function)); + expect(options.axisLabel.formatter(3002.1)).toBe('3k'); + }); + + it('formatter options allows for precision to be set explicitly', () => { + const options = getYAxisOptions({ + precision: 4, + }); + + expect(options.axisLabel.formatter).toEqual(expect.any(Function)); + expect(options.axisLabel.formatter(5002.1)).toBe('5.0021k'); + }); + + it('formatter options allows for overrides in milliseconds', () => { + const options = getYAxisOptions({ + format: SUPPORTED_FORMATS.milliseconds, + }); + + expect(options.axisLabel.formatter).toEqual(expect.any(Function)); + expect(options.axisLabel.formatter(1.1234)).toBe('1.12ms'); + }); + + it('formatter options allows for overrides in bytes', () => { const options = getYAxisOptions({ format: SUPPORTED_FORMATS.bytes, }); @@ -46,7 +71,7 @@ describe('options spec', () => { const formatter = getTooltipFormatter(); expect(formatter).toEqual(expect.any(Function)); - expect(formatter(1)).toBe('1.000'); + expect(formatter(0.11111)).toBe('111.1m'); }); it('defined format', () => { diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 870e47edde0..5ac716b0c63 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -1,6 +1,7 @@ import { mount } from '@vue/test-utils'; import { setTestTimeout } from 'helpers/timeout'; import { GlLink } from '@gitlab/ui'; +import { TEST_HOST } from 'jest/helpers/test_constants'; import { GlAreaChart, GlLineChart, @@ -12,23 +13,16 @@ import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper'; import { createStore } from '~/monitoring/stores'; import TimeSeries from '~/monitoring/components/charts/time_series.vue'; import * as types from '~/monitoring/stores/mutation_types'; +import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data'; import { - deploymentData, - mockedQueryResultFixture, + metricsDashboardPayload, metricsDashboardViewModel, - mockProjectDir, - mockHost, -} from '../../mock_data'; + metricResultStatus, +} from '../../fixture_data'; import * as iconUtils from '~/lib/utils/icon_utils'; -import { getJSONFixture } from '../../../helpers/fixtures'; const mockSvgPathContent = 'mockSvgPathContent'; -const metricsDashboardFixture = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); -const metricsDashboardPayload = metricsDashboardFixture.dashboard; - jest.mock('lodash/throttle', () => // this throttle mock executes immediately jest.fn(func => { @@ -51,7 +45,7 @@ describe('Time series component', () => { graphData: { ...graphData, type }, deploymentData: store.state.monitoringDashboard.deploymentData, annotations: store.state.monitoringDashboard.annotations, - projectPath: `${mockHost}${mockProjectDir}`, + projectPath: `${TEST_HOST}${mockProjectDir}`, }, store, stubs: { @@ -74,7 +68,7 @@ describe('Time series component', () => { store.commit( `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultFixture, + metricResultStatus, ); // dashboard is a dynamically generated fixture and stored at environment_metrics_dashboard.json [mockGraphData] = store.state.monitoringDashboard.dashboard.panelGroups[1].panels; @@ -284,6 +278,33 @@ describe('Time series component', () => { }); }); + describe('formatAnnotationsTooltipText', () => { + const annotationsMetadata = { + name: 'annotations', + xAxis: annotationsData[0].from, + yAxis: 0, + tooltipData: { + title: '2020/02/19 10:01:41', + content: annotationsData[0].description, + }, + }; + + const mockMarkPoint = { + componentType: 'markPoint', + name: 'annotations', + value: undefined, + data: annotationsMetadata, + }; + + it('formats tooltip title and sets tooltip content', () => { + const formattedTooltipData = timeSeriesChart.vm.formatAnnotationsTooltipText( + mockMarkPoint, + ); + expect(formattedTooltipData.title).toBe('19 Feb 2020, 10:01AM'); + expect(formattedTooltipData.content).toBe(annotationsMetadata.tooltipData.content); + }); + }); + describe('setSvg', () => { const mockSvgName = 'mockSvgName'; @@ -386,6 +407,8 @@ describe('Time series component', () => { series: [ { name: mockSeriesName, + type: 'line', + data: [], }, ], }, @@ -448,8 +471,8 @@ describe('Time series component', () => { deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter; }); - it('formats and rounds to 2 decimal places', () => { - expect(dataFormatter(0.88888)).toBe('0.89'); + it('formats by default to precision notation', () => { + expect(dataFormatter(0.88888)).toBe('889m'); }); it('deployment formatter is set as is required to display a tooltip', () => { @@ -606,7 +629,7 @@ describe('Time series component', () => { store = createStore(); const graphData = cloneDeep(metricsDashboardViewModel.panelGroups[0].panels[3]); graphData.metrics.forEach(metric => - Object.assign(metric, { result: mockedQueryResultFixture.result }), + Object.assign(metric, { result: metricResultStatus.result }), ); timeSeriesChart = makeTimeSeriesChart(graphData, 'area-chart'); diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js index f0b510a01f4..8b6ee9b3bf6 100644 --- a/spec/frontend/monitoring/components/dashboard_spec.js +++ b/spec/frontend/monitoring/components/dashboard_spec.js @@ -1,34 +1,23 @@ -import { shallowMount, createLocalVue, mount } from '@vue/test-utils'; -import { GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import Tracking from '~/tracking'; +import { GlModal, GlDropdownItem, GlDeprecatedButton } from '@gitlab/ui'; import VueDraggable from 'vuedraggable'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import statusCodes from '~/lib/utils/http_status'; import { metricStates } from '~/monitoring/constants'; import Dashboard from '~/monitoring/components/dashboard.vue'; -import { getJSONFixture } from '../../../../spec/frontend/helpers/fixtures'; import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue'; +import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue'; import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue'; import GroupEmptyState from '~/monitoring/components/group_empty_state.vue'; import PanelType from 'ee_else_ce/monitoring/components/panel_type.vue'; import { createStore } from '~/monitoring/stores'; import * as types from '~/monitoring/stores/mutation_types'; -import { setupComponentStore, propsData } from '../init_utils'; -import { - metricsDashboardViewModel, - environmentData, - dashboardGitResponse, - mockedQueryResultFixture, -} from '../mock_data'; - -const localVue = createLocalVue(); -const expectedPanelCount = 4; - -const metricsDashboardFixture = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); -const metricsDashboardPayload = metricsDashboardFixture.dashboard; +import { setupStoreWithDashboard, setMetricResult, setupStoreWithData } from '../store_utils'; +import { environmentData, dashboardGitResponse, propsData } from '../mock_data'; +import { metricsDashboardViewModel, metricsDashboardPanelCount } from '../fixture_data'; describe('Dashboard', () => { let store; @@ -43,7 +32,6 @@ describe('Dashboard', () => { const createShallowWrapper = (props = {}, options = {}) => { wrapper = shallowMount(Dashboard, { - localVue, propsData: { ...propsData, ...props }, methods: { fetchData: jest.fn(), @@ -55,7 +43,6 @@ describe('Dashboard', () => { const createMountedWrapper = (props = {}, options = {}) => { wrapper = mount(Dashboard, { - localVue, propsData: { ...propsData, ...props }, methods: { fetchData: jest.fn(), @@ -144,7 +131,7 @@ describe('Dashboard', () => { { stubs: ['graph-group', 'panel-type'] }, ); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick().then(() => { expect(wrapper.vm.showEmptyState).toEqual(false); @@ -172,7 +159,7 @@ describe('Dashboard', () => { beforeEach(() => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick(); }); @@ -201,14 +188,7 @@ describe('Dashboard', () => { it('hides the environments dropdown list when there is no environments', () => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, - metricsDashboardPayload, - ); - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultFixture, - ); + setupStoreWithDashboard(wrapper.vm.$store); return wrapper.vm.$nextTick().then(() => { expect(findAllEnvironmentsDropdownItems()).toHaveLength(0); @@ -218,7 +198,7 @@ describe('Dashboard', () => { it('renders the datetimepicker dropdown', () => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick().then(() => { expect(wrapper.find(DateTimePicker).exists()).toBe(true); @@ -228,7 +208,7 @@ describe('Dashboard', () => { it('renders the refresh dashboard button', () => { createMountedWrapper({ hasMetrics: true }, { stubs: ['graph-group', 'panel-type'] }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick().then(() => { const refreshBtn = wrapper.findAll({ ref: 'refreshDashboardBtn' }); @@ -241,7 +221,11 @@ describe('Dashboard', () => { describe('when one of the metrics is missing', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); - setupComponentStore(wrapper); + + const { $store } = wrapper.vm; + + setupStoreWithDashboard($store); + setMetricResult({ $store, result: [], panel: 2 }); return wrapper.vm.$nextTick(); }); @@ -273,7 +257,7 @@ describe('Dashboard', () => { }, ); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick(); }); @@ -348,14 +332,14 @@ describe('Dashboard', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick(); }); it('wraps vuedraggable', () => { expect(findDraggablePanels().exists()).toBe(true); - expect(findDraggablePanels().length).toEqual(expectedPanelCount); + expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount); }); it('is disabled by default', () => { @@ -411,11 +395,11 @@ describe('Dashboard', () => { it('shows a remove button, which removes a panel', () => { expect(findFirstDraggableRemoveButton().isEmpty()).toBe(false); - expect(findDraggablePanels().length).toEqual(expectedPanelCount); + expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount); findFirstDraggableRemoveButton().trigger('click'); return wrapper.vm.$nextTick(() => { - expect(findDraggablePanels().length).toEqual(expectedPanelCount - 1); + expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount - 1); }); }); @@ -534,7 +518,7 @@ describe('Dashboard', () => { beforeEach(() => { createShallowWrapper({ hasMetrics: true, currentDashboard }); - setupComponentStore(wrapper); + setupStoreWithData(wrapper.vm.$store); return wrapper.vm.$nextTick(); }); @@ -564,4 +548,74 @@ describe('Dashboard', () => { }); }); }); + + describe('add custom metrics', () => { + const findAddMetricButton = () => wrapper.vm.$refs.addMetricBtn; + describe('when not available', () => { + beforeEach(() => { + createShallowWrapper({ + hasMetrics: true, + customMetricsPath: '/endpoint', + }); + }); + it('does not render add button on the dashboard', () => { + expect(findAddMetricButton()).toBeUndefined(); + }); + }); + + describe('when available', () => { + let origPage; + beforeEach(done => { + jest.spyOn(Tracking, 'event').mockReturnValue(); + createShallowWrapper({ + hasMetrics: true, + customMetricsPath: '/endpoint', + customMetricsAvailable: true, + }); + setupStoreWithData(wrapper.vm.$store); + + origPage = document.body.dataset.page; + document.body.dataset.page = 'projects:environments:metrics'; + + wrapper.vm.$nextTick(done); + }); + afterEach(() => { + document.body.dataset.page = origPage; + }); + + it('renders add button on the dashboard', () => { + expect(findAddMetricButton()).toBeDefined(); + }); + + it('uses modal for custom metrics form', () => { + expect(wrapper.find(GlModal).exists()).toBe(true); + expect(wrapper.find(GlModal).attributes().modalid).toBe('add-metric'); + }); + it('adding new metric is tracked', done => { + const submitButton = wrapper.vm.$refs.submitCustomMetricsFormBtn; + wrapper.setData({ + formIsValid: true, + }); + wrapper.vm.$nextTick(() => { + submitButton.$el.click(); + wrapper.vm.$nextTick(() => { + expect(Tracking.event).toHaveBeenCalledWith( + document.body.dataset.page, + 'click_button', + { + label: 'add_new_metric', + property: 'modal', + value: undefined, + }, + ); + done(); + }); + }); + }); + + it('renders custom metrics form fields', () => { + expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true); + }); + }); + }); }); diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js index 38523ab82bc..d1790df4189 100644 --- a/spec/frontend/monitoring/components/dashboard_template_spec.js +++ b/spec/frontend/monitoring/components/dashboard_template_spec.js @@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Dashboard from '~/monitoring/components/dashboard.vue'; import { createStore } from '~/monitoring/stores'; -import { propsData } from '../init_utils'; +import { propsData } from '../mock_data'; jest.mock('~/lib/utils/url_utility'); diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js index ebfa09874fa..65e9d036d1a 100644 --- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js +++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js @@ -9,12 +9,11 @@ import { updateHistory, } from '~/lib/utils/url_utility'; import axios from '~/lib/utils/axios_utils'; -import { mockProjectDir } from '../mock_data'; +import { mockProjectDir, propsData } from '../mock_data'; import Dashboard from '~/monitoring/components/dashboard.vue'; import { createStore } from '~/monitoring/stores'; import { defaultTimeRange } from '~/vue_shared/constants'; -import { propsData } from '../init_utils'; jest.mock('~/flash'); jest.mock('~/lib/utils/url_utility'); diff --git a/spec/frontend/monitoring/components/panel_type_spec.js b/spec/frontend/monitoring/components/panel_type_spec.js index 02511ac46ea..819b5235284 100644 --- a/spec/frontend/monitoring/components/panel_type_spec.js +++ b/spec/frontend/monitoring/components/panel_type_spec.js @@ -10,17 +10,17 @@ import TimeSeriesChart from '~/monitoring/components/charts/time_series.vue'; import AnomalyChart from '~/monitoring/components/charts/anomaly.vue'; import { anomalyMockGraphData, - graphDataPrometheusQueryRange, mockLogsHref, mockLogsPath, mockNamespace, mockNamespacedData, mockTimeRange, -} from 'jest/monitoring/mock_data'; +} from '../mock_data'; + +import { graphData, graphDataEmpty } from '../fixture_data'; import { createStore, monitoringDashboard } from '~/monitoring/stores'; import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group'; -global.IS_EE = true; global.URL.createObjectURL = jest.fn(); const mocks = { @@ -39,10 +39,13 @@ describe('Panel Type component', () => { const findCopyLink = () => wrapper.find({ ref: 'copyChartLink' }); const findTimeChart = () => wrapper.find({ ref: 'timeChart' }); + const findTitle = () => wrapper.find({ ref: 'graphTitle' }); + const findContextualMenu = () => wrapper.find({ ref: 'contextualMenu' }); const createWrapper = props => { wrapper = shallowMount(PanelType, { propsData: { + graphData, ...props, }, store, @@ -64,14 +67,9 @@ describe('Panel Type component', () => { }); describe('When no graphData is available', () => { - let glEmptyChart; - // Deep clone object before modifying - const graphDataNoResult = JSON.parse(JSON.stringify(graphDataPrometheusQueryRange)); - graphDataNoResult.metrics[0].result = []; - beforeEach(() => { createWrapper({ - graphData: graphDataNoResult, + graphData: graphDataEmpty, }); }); @@ -80,12 +78,8 @@ describe('Panel Type component', () => { }); describe('Empty Chart component', () => { - beforeEach(() => { - glEmptyChart = wrapper.find(EmptyChart); - }); - it('renders the chart title', () => { - expect(wrapper.find({ ref: 'graphTitle' }).text()).toBe(graphDataNoResult.title); + expect(findTitle().text()).toBe(graphDataEmpty.title); }); it('renders the no download csv link', () => { @@ -93,26 +87,19 @@ describe('Panel Type component', () => { }); it('does not contain graph widgets', () => { - expect(wrapper.find('.js-graph-widgets').exists()).toBe(false); + expect(findContextualMenu().exists()).toBe(false); }); it('is a Vue instance', () => { - expect(glEmptyChart.isVueInstance()).toBe(true); - }); - - it('it receives a graph title', () => { - const props = glEmptyChart.props(); - - expect(props.graphTitle).toBe(wrapper.vm.graphData.title); + expect(wrapper.find(EmptyChart).exists()).toBe(true); + expect(wrapper.find(EmptyChart).isVueInstance()).toBe(true); }); }); }); describe('when graph data is available', () => { beforeEach(() => { - createWrapper({ - graphData: graphDataPrometheusQueryRange, - }); + createWrapper(); }); afterEach(() => { @@ -120,11 +107,11 @@ describe('Panel Type component', () => { }); it('renders the chart title', () => { - expect(wrapper.find({ ref: 'graphTitle' }).text()).toBe(graphDataPrometheusQueryRange.title); + expect(findTitle().text()).toBe(graphData.title); }); it('contains graph widgets', () => { - expect(wrapper.find('.js-graph-widgets').exists()).toBe(true); + expect(findContextualMenu().exists()).toBe(true); expect(wrapper.find({ ref: 'downloadCsvLink' }).exists()).toBe(true); }); @@ -177,11 +164,7 @@ describe('Panel Type component', () => { const findEditCustomMetricLink = () => wrapper.find({ ref: 'editMetricLink' }); beforeEach(() => { - createWrapper({ - graphData: { - ...graphDataPrometheusQueryRange, - }, - }); + createWrapper(); return wrapper.vm.$nextTick(); }); @@ -193,10 +176,10 @@ describe('Panel Type component', () => { it('is present when the panel contains an edit_path property', () => { wrapper.setProps({ graphData: { - ...graphDataPrometheusQueryRange, + ...graphData, metrics: [ { - ...graphDataPrometheusQueryRange.metrics[0], + ...graphData.metrics[0], edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit', }, ], @@ -205,23 +188,6 @@ describe('Panel Type component', () => { return wrapper.vm.$nextTick(() => { expect(findEditCustomMetricLink().exists()).toBe(true); - }); - }); - - it('shows an "Edit metric" link for a panel with a single metric', () => { - wrapper.setProps({ - graphData: { - ...graphDataPrometheusQueryRange, - metrics: [ - { - ...graphDataPrometheusQueryRange.metrics[0], - edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit', - }, - ], - }, - }); - - return wrapper.vm.$nextTick(() => { expect(findEditCustomMetricLink().text()).toBe('Edit metric'); }); }); @@ -229,14 +195,14 @@ describe('Panel Type component', () => { it('shows an "Edit metrics" link for a panel with multiple metrics', () => { wrapper.setProps({ graphData: { - ...graphDataPrometheusQueryRange, + ...graphData, metrics: [ { - ...graphDataPrometheusQueryRange.metrics[0], + ...graphData.metrics[0], edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit', }, { - ...graphDataPrometheusQueryRange.metrics[0], + ...graphData.metrics[0], edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit', }, ], @@ -253,9 +219,7 @@ describe('Panel Type component', () => { const findViewLogsLink = () => wrapper.find({ ref: 'viewLogsLink' }); beforeEach(() => { - createWrapper({ - graphData: graphDataPrometheusQueryRange, - }); + createWrapper(); return wrapper.vm.$nextTick(); }); @@ -327,7 +291,6 @@ describe('Panel Type component', () => { beforeEach(() => { createWrapper({ clipboardText, - graphData: graphDataPrometheusQueryRange, }); }); @@ -353,11 +316,13 @@ describe('Panel Type component', () => { describe('when downloading metrics data as CSV', () => { beforeEach(() => { - graphDataPrometheusQueryRange.y_label = 'metric'; wrapper = shallowMount(PanelType, { propsData: { clipboardText: exampleText, - graphData: graphDataPrometheusQueryRange, + graphData: { + y_label: 'metric', + ...graphData, + }, }, store, }); @@ -370,12 +335,12 @@ describe('Panel Type component', () => { describe('csvText', () => { it('converts metrics data from json to csv', () => { - const header = `timestamp,${graphDataPrometheusQueryRange.y_label}`; - const data = graphDataPrometheusQueryRange.metrics[0].result[0].values; + const header = `timestamp,${graphData.y_label}`; + const data = graphData.metrics[0].result[0].values; const firstRow = `${data[0][0]},${data[0][1]}`; const secondRow = `${data[1][0]},${data[1][1]}`; - expect(wrapper.vm.csvText).toBe(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`); + expect(wrapper.vm.csvText).toMatch(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`); }); }); @@ -402,7 +367,7 @@ describe('Panel Type component', () => { wrapper = shallowMount(PanelType, { propsData: { - graphData: graphDataPrometheusQueryRange, + graphData, namespace: mockNamespace, }, store, diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js new file mode 100644 index 00000000000..b7b72a15992 --- /dev/null +++ b/spec/frontend/monitoring/fixture_data.js @@ -0,0 +1,49 @@ +import { mapToDashboardViewModel } from '~/monitoring/stores/utils'; +import { metricStates } from '~/monitoring/constants'; + +import { metricsResult } from './mock_data'; + +// Use globally available `getJSONFixture` so this file can be imported by both karma and jest specs +export const metricsDashboardResponse = getJSONFixture( + 'metrics_dashboard/environment_metrics_dashboard.json', +); +export const metricsDashboardPayload = metricsDashboardResponse.dashboard; +export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload); + +export const metricsDashboardPanelCount = 22; +export const metricResultStatus = { + // First metric in fixture `metrics_dashboard/environment_metrics_dashboard.json` + metricId: 'NO_DB_response_metrics_nginx_ingress_throughput_status_code', + result: metricsResult, +}; +export const metricResultPods = { + // Second metric in fixture `metrics_dashboard/environment_metrics_dashboard.json` + metricId: 'NO_DB_response_metrics_nginx_ingress_latency_pod_average', + result: metricsResult, +}; +export const metricResultEmpty = { + metricId: 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code', + result: [], +}; + +// Graph data + +const firstPanel = metricsDashboardViewModel.panelGroups[0].panels[0]; + +export const graphData = { + ...firstPanel, + metrics: firstPanel.metrics.map(metric => ({ + ...metric, + result: metricsResult, + state: metricStates.OK, + })), +}; + +export const graphDataEmpty = { + ...firstPanel, + metrics: firstPanel.metrics.map(metric => ({ + ...metric, + result: [], + state: metricStates.NO_DATA, + })), +}; diff --git a/spec/frontend/monitoring/init_utils.js b/spec/frontend/monitoring/init_utils.js deleted file mode 100644 index 55b6199fdfc..00000000000 --- a/spec/frontend/monitoring/init_utils.js +++ /dev/null @@ -1,57 +0,0 @@ -import * as types from '~/monitoring/stores/mutation_types'; -import { - metricsDashboardPayload, - mockedEmptyResult, - mockedQueryResultPayload, - mockedQueryResultPayloadCoresTotal, - mockApiEndpoint, - environmentData, -} from './mock_data'; - -export const propsData = { - hasMetrics: false, - documentationPath: '/path/to/docs', - settingsPath: '/path/to/settings', - clustersPath: '/path/to/clusters', - tagsPath: '/path/to/tags', - projectPath: '/path/to/project', - logsPath: '/path/to/logs', - defaultBranch: 'master', - metricsEndpoint: mockApiEndpoint, - deploymentsEndpoint: null, - emptyGettingStartedSvgPath: '/path/to/getting-started.svg', - emptyLoadingSvgPath: '/path/to/loading.svg', - emptyNoDataSvgPath: '/path/to/no-data.svg', - emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg', - emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', - currentEnvironmentName: 'production', - customMetricsAvailable: false, - customMetricsPath: '', - validateQueryPath: '', -}; - -export const setupComponentStore = wrapper => { - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, - metricsDashboardPayload, - ); - - // Load 3 panels to the dashboard, one with an empty result - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedEmptyResult, - ); - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultPayload, - ); - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, - mockedQueryResultPayloadCoresTotal, - ); - - wrapper.vm.$store.commit( - `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, - environmentData, - ); -}; diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js index 84dd0b70e71..56236918c68 100644 --- a/spec/frontend/monitoring/mock_data.js +++ b/spec/frontend/monitoring/mock_data.js @@ -1,13 +1,47 @@ -import { mapToDashboardViewModel } from '~/monitoring/stores/utils'; - // This import path needs to be relative for now because this mock data is used in // Karma specs too, where the helpers/test_constants alias can not be resolved import { TEST_HOST } from '../helpers/test_constants'; -export const mockHost = 'http://test.host'; export const mockProjectDir = '/frontend-fixtures/environments-project'; export const mockApiEndpoint = `${TEST_HOST}/monitoring/mock`; +export const propsData = { + hasMetrics: false, + documentationPath: '/path/to/docs', + settingsPath: '/path/to/settings', + clustersPath: '/path/to/clusters', + tagsPath: '/path/to/tags', + projectPath: '/path/to/project', + logsPath: '/path/to/logs', + defaultBranch: 'master', + metricsEndpoint: mockApiEndpoint, + deploymentsEndpoint: null, + emptyGettingStartedSvgPath: '/path/to/getting-started.svg', + emptyLoadingSvgPath: '/path/to/loading.svg', + emptyNoDataSvgPath: '/path/to/no-data.svg', + emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg', + emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg', + currentEnvironmentName: 'production', + customMetricsAvailable: false, + customMetricsPath: '', + validateQueryPath: '', +}; + +const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({ + default: false, + display_name: `Custom Dashboard ${idx}`, + can_edit: true, + system_dashboard: false, + project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`, + path: `.gitlab/dashboards/dashboard_${idx}.yml`, +})); + +export const mockDashboardsErrorResponse = { + all_dashboards: customDashboardsData, + message: "Each 'panel_group' must define an array :panels", + status: 'error', +}; + export const anomalyDeploymentData = [ { id: 111, @@ -213,130 +247,27 @@ export const deploymentData = [ export const annotationsData = [ { id: 'gid://gitlab/Metrics::Dashboard::Annotation/1', - starting_at: '2020-04-01T12:51:58.373Z', - ending_at: null, + startingAt: '2020-04-12 12:51:53 UTC', + endingAt: null, panelId: null, description: 'This is a test annotation', }, { id: 'gid://gitlab/Metrics::Dashboard::Annotation/2', description: 'test annotation 2', - starting_at: '2020-04-02T12:51:58.373Z', - ending_at: null, + startingAt: '2020-04-13 12:51:53 UTC', + endingAt: null, panelId: null, }, { id: 'gid://gitlab/Metrics::Dashboard::Annotation/3', description: 'test annotation 3', - starting_at: '2020-04-04T12:51:58.373Z', - ending_at: null, + startingAt: '2020-04-16 12:51:53 UTC', + endingAt: null, panelId: null, }, ]; -export const metricsNewGroupsAPIResponse = [ - { - group: 'System metrics (Kubernetes)', - priority: 5, - panels: [ - { - title: 'Memory Usage (Pod average)', - type: 'area-chart', - y_label: 'Memory Used per Pod', - weight: 2, - metrics: [ - { - id: 'system_metrics_kubernetes_container_memory_average', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024', - label: 'Pod average', - unit: 'MB', - metric_id: 17, - prometheus_endpoint_path: - '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=avg%28sum%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+by+%28job%29%29+without+%28job%29+%2F+count%28avg%28container_memory_usage_bytes%7Bcontainer_name%21%3D%22POD%22%2Cpod_name%3D~%22%5E%25%7Bci_environment_slug%7D-%28%5B%5Ec%5D.%2A%7Cc%28%5B%5Ea%5D%7Ca%28%5B%5En%5D%7Cn%28%5B%5Ea%5D%7Ca%28%5B%5Er%5D%7Cr%5B%5Ey%5D%29%29%29%29.%2A%7C%29-%28.%2A%29%22%2Cnamespace%3D%22%25%7Bkube_namespace%7D%22%7D%29+without+%28job%29%29+%2F1024%2F1024', - appearance: { - line: { - width: 2, - }, - }, - }, - ], - }, - ], - }, -]; - -const metricsResult = [ - { - metric: {}, - values: [ - [1563272065.589, '10.396484375'], - [1563272125.589, '10.333984375'], - [1563272185.589, '10.333984375'], - [1563272245.589, '10.333984375'], - [1563272305.589, '10.333984375'], - [1563272365.589, '10.333984375'], - [1563272425.589, '10.38671875'], - [1563272485.589, '10.333984375'], - [1563272545.589, '10.333984375'], - [1563272605.589, '10.333984375'], - [1563272665.589, '10.333984375'], - [1563272725.589, '10.333984375'], - [1563272785.589, '10.396484375'], - [1563272845.589, '10.333984375'], - [1563272905.589, '10.333984375'], - [1563272965.589, '10.3984375'], - [1563273025.589, '10.337890625'], - [1563273085.589, '10.34765625'], - [1563273145.589, '10.337890625'], - [1563273205.589, '10.337890625'], - [1563273265.589, '10.337890625'], - [1563273325.589, '10.337890625'], - [1563273385.589, '10.337890625'], - [1563273445.589, '10.337890625'], - [1563273505.589, '10.337890625'], - [1563273565.589, '10.337890625'], - [1563273625.589, '10.337890625'], - [1563273685.589, '10.337890625'], - [1563273745.589, '10.337890625'], - [1563273805.589, '10.337890625'], - [1563273865.589, '10.390625'], - [1563273925.589, '10.390625'], - ], - }, -]; - -export const mockedEmptyResult = { - metricId: '1_response_metrics_nginx_ingress_throughput_status_code', - result: [], -}; - -export const mockedEmptyThroughputResult = { - metricId: 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code', - result: [], -}; - -export const mockedQueryResultPayload = { - metricId: '12_system_metrics_kubernetes_container_memory_total', - result: metricsResult, -}; - -export const mockedQueryResultPayloadCoresTotal = { - metricId: '13_system_metrics_kubernetes_container_cores_total', - result: metricsResult, -}; - -export const mockedQueryResultFixture = { - // First metric in fixture `metrics_dashboard/environment_metrics_dashboard.json` - metricId: 'NO_DB_response_metrics_nginx_ingress_throughput_status_code', - result: metricsResult, -}; - -export const mockedQueryResultFixtureStatusCode = { - metricId: 'NO_DB_response_metrics_nginx_ingress_latency_pod_average', - result: metricsResult, -}; - const extraEnvironmentData = new Array(15).fill(null).map((_, idx) => ({ id: `gid://gitlab/Environments/${150 + idx}`, name: `no-deployment/noop-branch-${idx}`, @@ -384,158 +315,6 @@ export const environmentData = [ }, ].concat(extraEnvironmentData); -export const metricsDashboardPayload = { - dashboard: 'Environment metrics', - priority: 1, - panel_groups: [ - { - group: 'System metrics (Kubernetes)', - priority: 5, - panels: [ - { - title: 'Memory Usage (Total)', - type: 'area-chart', - y_label: 'Total Memory Used', - weight: 4, - y_axis: { - format: 'megabytes', - }, - metrics: [ - { - id: 'system_metrics_kubernetes_container_memory_total', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1000/1000', - label: 'Total', - unit: 'MB', - metric_id: 12, - prometheus_endpoint_path: 'http://test', - }, - ], - }, - { - title: 'Core Usage (Total)', - type: 'area-chart', - y_label: 'Total Cores', - weight: 3, - metrics: [ - { - id: 'system_metrics_kubernetes_container_cores_total', - query_range: - 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)', - label: 'Total', - unit: 'cores', - metric_id: 13, - }, - ], - }, - { - title: 'Memory Usage (Pod average)', - type: 'line-chart', - y_label: 'Memory Used per Pod', - weight: 2, - metrics: [ - { - id: 'system_metrics_kubernetes_container_memory_average', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024', - label: 'Pod average', - unit: 'MB', - metric_id: 14, - }, - ], - }, - { - title: 'memories', - type: 'area-chart', - y_label: 'memories', - metrics: [ - { - id: 'metric_of_ages_1000', - label: 'memory_1000', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 20, - }, - { - id: 'metric_of_ages_1001', - label: 'memory_1000', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 21, - }, - { - id: 'metric_of_ages_1002', - label: 'memory_1000', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 22, - }, - { - id: 'metric_of_ages_1003', - label: 'memory_1000', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 23, - }, - { - id: 'metric_of_ages_1004', - label: 'memory_1004', - unit: 'count', - prometheus_endpoint_path: '/root', - metric_id: 24, - }, - ], - }, - ], - }, - { - group: 'Response metrics (NGINX Ingress VTS)', - priority: 10, - panels: [ - { - metrics: [ - { - id: 'response_metrics_nginx_ingress_throughput_status_code', - label: 'Status Code', - metric_id: 1, - prometheus_endpoint_path: - '/root/autodevops-deploy/environments/32/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29', - query_range: - 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[2m])) by (status_code)', - unit: 'req / sec', - }, - ], - title: 'Throughput', - type: 'area-chart', - weight: 1, - y_label: 'Requests / Sec', - }, - ], - }, - ], -}; - -/** - * Mock of response of metrics_dashboard.json - */ -export const metricsDashboardResponse = { - all_dashboards: [], - dashboard: metricsDashboardPayload, - metrics_data: {}, - status: 'success', -}; - -export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload); - -const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({ - default: false, - display_name: `Custom Dashboard ${idx}`, - can_edit: true, - system_dashboard: false, - project_blob_path: `${mockProjectDir}/blob/master/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`, - path: `.gitlab/dashboards/dashboard_${idx}.yml`, -})); - export const dashboardGitResponse = [ { default: true, @@ -548,11 +327,19 @@ export const dashboardGitResponse = [ ...customDashboardsData, ]; -export const mockDashboardsErrorResponse = { - all_dashboards: customDashboardsData, - message: "Each 'panel_group' must define an array :panels", - status: 'error', -}; +// Metrics mocks + +export const metricsResult = [ + { + metric: {}, + values: [ + [1563272065.589, '10.396484375'], + [1563272125.589, '10.333984375'], + [1563272185.589, '10.333984375'], + [1563272245.589, '10.333984375'], + ], + }, +]; export const graphDataPrometheusQuery = { title: 'Super Chart A2', @@ -578,29 +365,6 @@ export const graphDataPrometheusQuery = { ], }; -export const graphDataPrometheusQueryRange = { - title: 'Super Chart A1', - type: 'area-chart', - weight: 2, - metrics: [ - { - metricId: '2_metric_a', - query_range: - 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024', - unit: 'MB', - label: 'Total Consumption', - prometheus_endpoint_path: - '/root/kubernetes-gke-project/environments/35/prometheus/api/v1/query?query=max%28go_memstats_alloc_bytes%7Bjob%3D%22prometheus%22%7D%29+by+%28job%29+%2F1024%2F1024', - result: [ - { - metric: {}, - values: [[1495700554.925, '8.0390625'], [1495700614.925, '8.0390625']], - }, - ], - }, - ], -}; - export const graphDataPrometheusQueryRangeMultiTrack = { title: 'Super Chart A3', type: 'heatmap', diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js index c34a5afceb0..f312aa1fd34 100644 --- a/spec/frontend/monitoring/store/actions_spec.js +++ b/spec/frontend/monitoring/store/actions_spec.js @@ -23,7 +23,11 @@ import { setGettingStartedEmptyState, duplicateSystemDashboard, } from '~/monitoring/stores/actions'; -import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils'; +import { + gqClient, + parseEnvironmentsResponse, + parseAnnotationsResponse, +} from '~/monitoring/stores/utils'; import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql'; import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql'; import storeState from '~/monitoring/stores/state'; @@ -31,11 +35,14 @@ import { deploymentData, environmentData, annotationsData, - metricsDashboardResponse, - metricsDashboardViewModel, dashboardGitResponse, mockDashboardsErrorResponse, } from '../mock_data'; +import { + metricsDashboardResponse, + metricsDashboardViewModel, + metricsDashboardPanelCount, +} from '../fixture_data'; jest.mock('~/flash'); @@ -221,6 +228,10 @@ describe('Monitoring store actions', () => { describe('fetchAnnotations', () => { const { state } = store; + state.timeRange = { + start: '2020-04-15T12:54:32.137Z', + end: '2020-08-15T12:54:32.137Z', + }; state.projectPath = 'gitlab-org/gitlab-test'; state.currentEnvironmentName = 'production'; state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml'; @@ -236,17 +247,25 @@ describe('Monitoring store actions', () => { variables: { projectPath: state.projectPath, environmentName: state.currentEnvironmentName, - dashboardId: state.currentDashboard, + dashboardPath: state.currentDashboard, + startingFrom: state.timeRange.start, }, }; + const parsedResponse = parseAnnotationsResponse(annotationsData); mockMutate.mockResolvedValue({ data: { project: { - environment: { - metricDashboard: { - annotations: annotationsData, - }, + environments: { + nodes: [ + { + metricsDashboard: { + annotations: { + nodes: parsedResponse, + }, + }, + }, + ], }, }, }, @@ -257,10 +276,7 @@ describe('Monitoring store actions', () => { null, state, [], - [ - { type: 'requestAnnotations' }, - { type: 'receiveAnnotationsSuccess', payload: annotationsData }, - ], + [{ type: 'receiveAnnotationsSuccess', payload: parsedResponse }], () => { expect(mockMutate).toHaveBeenCalledWith(mutationVariables); }, @@ -274,7 +290,8 @@ describe('Monitoring store actions', () => { variables: { projectPath: state.projectPath, environmentName: state.currentEnvironmentName, - dashboardId: state.currentDashboard, + dashboardPath: state.currentDashboard, + startingFrom: state.timeRange.start, }, }; @@ -285,7 +302,7 @@ describe('Monitoring store actions', () => { null, state, [], - [{ type: 'requestAnnotations' }, { type: 'receiveAnnotationsFailure' }], + [{ type: 'receiveAnnotationsFailure' }], () => { expect(mockMutate).toHaveBeenCalledWith(mutationVariables); }, @@ -553,7 +570,7 @@ describe('Monitoring store actions', () => { fetchDashboardData({ state, commit, dispatch }) .then(() => { - expect(dispatch).toHaveBeenCalledTimes(10); // one per metric plus 1 for deployments + expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 1); // plus 1 for deployments expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData'); expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', { metric, @@ -581,11 +598,13 @@ describe('Monitoring store actions', () => { let metric; let state; let data; + let prometheusEndpointPath; beforeEach(() => { state = storeState(); - [metric] = metricsDashboardResponse.dashboard.panel_groups[0].panels[0].metrics; - metric = convertObjectPropsToCamelCase(metric, { deep: true }); + [metric] = metricsDashboardViewModel.panelGroups[0].panels[0].metrics; + + prometheusEndpointPath = metric.prometheusEndpointPath; data = { metricId: metric.metricId, @@ -594,7 +613,7 @@ describe('Monitoring store actions', () => { }); it('commits result', done => { - mock.onGet('http://test').reply(200, { data }); // One attempt + mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt testAction( fetchPrometheusMetric, @@ -631,7 +650,7 @@ describe('Monitoring store actions', () => { }; it('uses calculated step', done => { - mock.onGet('http://test').reply(200, { data }); // One attempt + mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt testAction( fetchPrometheusMetric, @@ -673,7 +692,7 @@ describe('Monitoring store actions', () => { }; it('uses metric step', done => { - mock.onGet('http://test').reply(200, { data }); // One attempt + mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt testAction( fetchPrometheusMetric, @@ -705,10 +724,10 @@ describe('Monitoring store actions', () => { it('commits result, when waiting for results', done => { // Mock multiple attempts while the cache is filling up - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').reply(200, { data }); // 4th attempt + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).reply(200, { data }); // 4th attempt testAction( fetchPrometheusMetric, @@ -739,10 +758,10 @@ describe('Monitoring store actions', () => { it('commits failure, when waiting for results and getting a server error', done => { // Mock multiple attempts while the cache is filling up and fails - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').replyOnce(statusCodes.NO_CONTENT); - mock.onGet('http://test').reply(500); // 4th attempt + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT); + mock.onGet(prometheusEndpointPath).reply(500); // 4th attempt const error = new Error('Request failed with status code 500'); diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js index 40341d32cf5..f040876b832 100644 --- a/spec/frontend/monitoring/store/getters_spec.js +++ b/spec/frontend/monitoring/store/getters_spec.js @@ -3,18 +3,13 @@ import * as getters from '~/monitoring/stores/getters'; import mutations from '~/monitoring/stores/mutations'; import * as types from '~/monitoring/stores/mutation_types'; import { metricStates } from '~/monitoring/constants'; +import { environmentData, metricsResult } from '../mock_data'; import { - environmentData, - mockedEmptyThroughputResult, - mockedQueryResultFixture, - mockedQueryResultFixtureStatusCode, -} from '../mock_data'; -import { getJSONFixture } from '../../helpers/fixtures'; - -const metricsDashboardFixture = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); -const metricsDashboardPayload = metricsDashboardFixture.dashboard; + metricsDashboardPayload, + metricResultStatus, + metricResultPods, + metricResultEmpty, +} from '../fixture_data'; describe('Monitoring store Getters', () => { describe('getMetricStates', () => { @@ -22,6 +17,21 @@ describe('Monitoring store Getters', () => { let state; let getMetricStates; + const setMetricSuccess = ({ result = metricsResult, group = 0, panel = 0, metric = 0 }) => { + const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric]; + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, { + metricId, + result, + }); + }; + + const setMetricFailure = ({ group = 0, panel = 0, metric = 0 }) => { + const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric]; + mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { + metricId, + }); + }; + beforeEach(() => { setupState = (initState = {}) => { state = initState; @@ -61,31 +71,30 @@ describe('Monitoring store Getters', () => { it('on an empty metric with no result, returns NO_DATA', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyThroughputResult); + setMetricSuccess({ result: [], group: 2 }); expect(getMetricStates()).toEqual([metricStates.NO_DATA]); }); it('on a metric with a result, returns OK', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); + setMetricSuccess({ group: 1 }); expect(getMetricStates()).toEqual([metricStates.OK]); }); it('on a metric with an error, returns an error', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[0].panels[0].metrics[0].metricId, - }); + setMetricFailure({}); expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]); }); it('on multiple metrics with results, returns OK', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); + + setMetricSuccess({ group: 1 }); + setMetricSuccess({ group: 1, panel: 1 }); expect(getMetricStates()).toEqual([metricStates.OK]); @@ -96,15 +105,8 @@ describe('Monitoring store Getters', () => { it('on multiple metrics errors', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[0].panels[0].metrics[0].metricId, - }); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[0].panels[0].metrics[0].metricId, - }); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[1].panels[0].metrics[0].metricId, - }); + setMetricFailure({}); + setMetricFailure({ group: 1 }); // Entire dashboard fails expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]); @@ -116,14 +118,11 @@ describe('Monitoring store Getters', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); // An success in 1 group - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); + setMetricSuccess({ group: 1 }); + // An error in 2 groups - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[1].panels[1].metrics[0].metricId, - }); - mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[2].panels[0].metrics[0].metricId, - }); + setMetricFailure({ group: 1, panel: 1 }); + setMetricFailure({ group: 2, panel: 0 }); expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]); expect(getMetricStates(groups[1].key)).toEqual([ @@ -182,38 +181,35 @@ describe('Monitoring store Getters', () => { it('an empty metric, returns empty', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedEmptyThroughputResult); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultEmpty); expect(metricsWithData()).toEqual([]); }); it('a metric with results, it returns a metric', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus); - expect(metricsWithData()).toEqual([mockedQueryResultFixture.metricId]); + expect(metricsWithData()).toEqual([metricResultStatus.metricId]); }); it('multiple metrics with results, it return multiple metrics', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultPods); - expect(metricsWithData()).toEqual([ - mockedQueryResultFixture.metricId, - mockedQueryResultFixtureStatusCode.metricId, - ]); + expect(metricsWithData()).toEqual([metricResultStatus.metricId, metricResultPods.metricId]); }); it('multiple metrics with results, it returns metrics filtered by group', () => { mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); - mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus); + mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultPods); // First group has metrics expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([ - mockedQueryResultFixture.metricId, - mockedQueryResultFixtureStatusCode.metricId, + metricResultStatus.metricId, + metricResultPods.metricId, ]); // Second group has no metrics diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index 34d224e13b0..1452e9bc491 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -6,12 +6,7 @@ import state from '~/monitoring/stores/state'; import { metricStates } from '~/monitoring/constants'; import { deploymentData, dashboardGitResponse } from '../mock_data'; -import { getJSONFixture } from '../../helpers/fixtures'; - -const metricsDashboardFixture = getJSONFixture( - 'metrics_dashboard/environment_metrics_dashboard.json', -); -const metricsDashboardPayload = metricsDashboardFixture.dashboard; +import { metricsDashboardPayload } from '../fixture_data'; describe('Monitoring mutations', () => { let stateCopy; diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js index f46409e8e32..7ee2a16b4bd 100644 --- a/spec/frontend/monitoring/store/utils_spec.js +++ b/spec/frontend/monitoring/store/utils_spec.js @@ -2,9 +2,11 @@ import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; import { uniqMetricsId, parseEnvironmentsResponse, + parseAnnotationsResponse, removeLeadingSlash, mapToDashboardViewModel, } from '~/monitoring/stores/utils'; +import { annotationsData } from '../mock_data'; import { NOT_IN_DB_PREFIX } from '~/monitoring/constants'; const projectPath = 'gitlab-org/gitlab-test'; @@ -56,7 +58,7 @@ describe('mapToDashboardViewModel', () => { y_label: 'Y Label A', yAxis: { name: 'Y Label A', - format: 'number', + format: 'engineering', precision: 2, }, metrics: [], @@ -138,7 +140,7 @@ describe('mapToDashboardViewModel', () => { y_label: '', yAxis: { name: '', - format: SUPPORTED_FORMATS.number, + format: SUPPORTED_FORMATS.engineering, precision: 2, }, metrics: [], @@ -159,7 +161,7 @@ describe('mapToDashboardViewModel', () => { }, yAxis: { name: '', - format: SUPPORTED_FORMATS.number, + format: SUPPORTED_FORMATS.engineering, precision: 2, }, metrics: [], @@ -219,7 +221,7 @@ describe('mapToDashboardViewModel', () => { }, }); - expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.number); + expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.engineering); }); // This property allows single_stat panels to render percentile values @@ -376,6 +378,27 @@ describe('parseEnvironmentsResponse', () => { }); }); +describe('parseAnnotationsResponse', () => { + const parsedAnnotationResponse = [ + { + description: 'This is a test annotation', + endingAt: null, + id: 'gid://gitlab/Metrics::Dashboard::Annotation/1', + panelId: null, + startingAt: new Date('2020-04-12T12:51:53.000Z'), + }, + ]; + it.each` + case | input | expected + ${'Returns empty array for null input'} | ${null} | ${[]} + ${'Returns empty array for undefined input'} | ${undefined} | ${[]} + ${'Returns empty array for empty input'} | ${[]} | ${[]} + ${'Returns parsed responses for annotations data'} | ${[annotationsData[0]]} | ${parsedAnnotationResponse} + `('$case', ({ input, expected }) => { + expect(parseAnnotationsResponse(input)).toEqual(expected); + }); +}); + describe('removeLeadingSlash', () => { [ { input: null, output: '' }, diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js new file mode 100644 index 00000000000..d764a79ccc3 --- /dev/null +++ b/spec/frontend/monitoring/store_utils.js @@ -0,0 +1,34 @@ +import * as types from '~/monitoring/stores/mutation_types'; +import { metricsResult, environmentData } from './mock_data'; +import { metricsDashboardPayload } from './fixture_data'; + +export const setMetricResult = ({ $store, result, group = 0, panel = 0, metric = 0 }) => { + const { dashboard } = $store.state.monitoringDashboard; + const { metricId } = dashboard.panelGroups[group].panels[panel].metrics[metric]; + + $store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, { + metricId, + result, + }); +}; + +const setEnvironmentData = $store => { + $store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData); +}; + +export const setupStoreWithDashboard = $store => { + $store.commit( + `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, + metricsDashboardPayload, + ); +}; + +export const setupStoreWithData = $store => { + setupStoreWithDashboard($store); + + setMetricResult({ $store, result: [], panel: 0 }); + setMetricResult({ $store, result: metricsResult, panel: 1 }); + setMetricResult({ $store, result: metricsResult, panel: 2 }); + + setEnvironmentData($store); +}; diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js index 262b8b985cc..0bb1b987b2e 100644 --- a/spec/frontend/monitoring/utils_spec.js +++ b/spec/frontend/monitoring/utils_spec.js @@ -1,17 +1,17 @@ import * as monitoringUtils from '~/monitoring/utils'; import { queryToObject, mergeUrlParams, removeParams } from '~/lib/utils/url_utility'; +import { TEST_HOST } from 'jest/helpers/test_constants'; import { - mockHost, mockProjectDir, graphDataPrometheusQuery, - graphDataPrometheusQueryRange, anomalyMockGraphData, barMockData, } from './mock_data'; +import { graphData } from './fixture_data'; jest.mock('~/lib/utils/url_utility'); -const mockPath = `${mockHost}${mockProjectDir}/-/environments/29/metrics`; +const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`; const generatedLink = 'http://chart.link.com'; @@ -101,10 +101,7 @@ describe('monitoring/utils', () => { * the validator will look for the `values` key instead of `value` */ it('validates data with the query_range format', () => { - const validGraphData = monitoringUtils.graphDataValidatorForValues( - false, - graphDataPrometheusQueryRange, - ); + const validGraphData = monitoringUtils.graphDataValidatorForValues(false, graphData); expect(validGraphData).toBe(true); }); diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js index d3932ca09ff..9c292fa0f2b 100644 --- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js +++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js @@ -55,7 +55,12 @@ describe('Settings Panel', () => { currentSettings: { ...defaultProps.currentSettings, ...currentSettings }, }; - return mountFn(settingsPanel, { propsData }); + return mountFn(settingsPanel, { + propsData, + provide: { + glFeatures: { metricsDashboardVisibilitySwitchingAvailable: true }, + }, + }); }; const overrideCurrentSettings = (currentSettingsProps, extraProps = {}) => { @@ -471,4 +476,28 @@ describe('Settings Panel', () => { }); }); }); + + describe('Metrics dashboard', () => { + it('should show the metrics dashboard access toggle', () => { + return wrapper.vm.$nextTick(() => { + expect(wrapper.find({ ref: 'metrics-visibility-settings' }).exists()).toBe(true); + }); + }); + + it('should set the visibility level description based upon the selected visibility level', () => { + wrapper + .find('[name="project[project_feature_attributes][metrics_dashboard_access_level]"]') + .setValue(visibilityOptions.PUBLIC); + + expect(wrapper.vm.metricsAccessLevel).toBe(visibilityOptions.PUBLIC); + }); + + it('should contain help text', () => { + wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE }); + + expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toEqual( + 'With Metrics Dashboard you can visualize this project performance metrics', + ); + }); + }); }); diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js index 43da6388efa..3c5938cfa1f 100644 --- a/spec/frontend/pipelines/graph/action_component_spec.js +++ b/spec/frontend/pipelines/graph/action_component_spec.js @@ -7,6 +7,7 @@ import ActionComponent from '~/pipelines/components/graph/action_component.vue'; describe('pipeline graph action component', () => { let wrapper; let mock; + const findButton = () => wrapper.find('button'); beforeEach(() => { mock = new MockAdapter(axios); @@ -44,15 +45,15 @@ describe('pipeline graph action component', () => { }); it('should render an svg', () => { - expect(wrapper.find('.ci-action-icon-wrapper')).toBeDefined(); - expect(wrapper.find('svg')).toBeDefined(); + expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true); + expect(wrapper.find('svg').exists()).toBe(true); }); describe('on click', () => { it('emits `pipelineActionRequestComplete` after a successful request', done => { jest.spyOn(wrapper.vm, '$emit'); - wrapper.find('button').trigger('click'); + findButton().trigger('click'); waitForPromises() .then(() => { @@ -63,7 +64,7 @@ describe('pipeline graph action component', () => { }); it('renders a loading icon while waiting for request', done => { - wrapper.find('button').trigger('click'); + findButton().trigger('click'); wrapper.vm.$nextTick(() => { expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true); diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js new file mode 100644 index 00000000000..a9b06eab3fa --- /dev/null +++ b/spec/frontend/pipelines/graph/graph_component_spec.js @@ -0,0 +1,305 @@ +import Vue from 'vue'; +import { mount } from '@vue/test-utils'; +import PipelineStore from '~/pipelines/stores/pipeline_store'; +import graphComponent from '~/pipelines/components/graph/graph_component.vue'; +import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; +import linkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; +import graphJSON from './mock_data'; +import linkedPipelineJSON from './linked_pipelines_mock_data'; +import PipelinesMediator from '~/pipelines/pipeline_details_mediator'; + +describe('graph component', () => { + const store = new PipelineStore(); + store.storePipeline(linkedPipelineJSON); + const mediator = new PipelinesMediator({ endpoint: '' }); + + let wrapper; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + describe('while is loading', () => { + it('should render a loading icon', () => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: true, + pipeline: {}, + mediator, + }, + }); + + expect(wrapper.find('.gl-spinner').exists()).toBe(true); + }); + }); + + describe('with data', () => { + it('should render the graph', () => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: graphJSON, + mediator, + }, + }); + + expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true); + + expect(wrapper.find(stageColumnComponent).classes()).toContain('no-margin'); + + expect( + wrapper + .findAll(stageColumnComponent) + .at(1) + .classes(), + ).toContain('left-margin'); + + expect(wrapper.find('.stage-column:nth-child(2) .build:nth-child(1)').classes()).toContain( + 'left-connector', + ); + + expect(wrapper.find('.loading-icon').exists()).toBe(false); + + expect(wrapper.find('.stage-column-list').exists()).toBe(true); + }); + }); + + describe('when linked pipelines are present', () => { + beforeEach(() => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: store.state.pipeline, + mediator, + }, + }); + }); + + describe('rendered output', () => { + it('should include the pipelines graph', () => { + expect(wrapper.find('.js-pipeline-graph').exists()).toBe(true); + }); + + it('should not include the loading icon', () => { + expect(wrapper.find('.fa-spinner').exists()).toBe(false); + }); + + it('should include the stage column list', () => { + expect(wrapper.find(stageColumnComponent).exists()).toBe(true); + }); + + it('should include the no-margin class on the first child if there is only one job', () => { + const firstStageColumnElement = wrapper.find(stageColumnComponent); + + expect(firstStageColumnElement.classes()).toContain('no-margin'); + }); + + it('should include the has-only-one-job class on the first child', () => { + const firstStageColumnElement = wrapper.find('.stage-column-list .stage-column'); + + expect(firstStageColumnElement.classes()).toContain('has-only-one-job'); + }); + + it('should include the left-margin class on the second child', () => { + const firstStageColumnElement = wrapper.find('.stage-column-list .stage-column:last-child'); + + expect(firstStageColumnElement.classes()).toContain('left-margin'); + }); + + it('should include the js-has-linked-pipelines flag', () => { + expect(wrapper.find('.js-has-linked-pipelines').exists()).toBe(true); + }); + }); + + describe('computeds and methods', () => { + describe('capitalizeStageName', () => { + it('it capitalizes the stage name', () => { + expect( + wrapper + .findAll('.stage-column .stage-name') + .at(1) + .text(), + ).toBe('Prebuild'); + }); + }); + + describe('stageConnectorClass', () => { + it('it returns left-margin when there is a triggerer', () => { + expect( + wrapper + .findAll(stageColumnComponent) + .at(1) + .classes(), + ).toContain('left-margin'); + }); + }); + }); + + describe('linked pipelines components', () => { + beforeEach(() => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: store.state.pipeline, + mediator, + }, + }); + }); + + it('should render an upstream pipelines column at first position', () => { + expect(wrapper.find(linkedPipelinesColumn).exists()).toBe(true); + expect(wrapper.find('.stage-column .stage-name').text()).toBe('Upstream'); + }); + + it('should render a downstream pipelines column at last position', () => { + const stageColumnNames = wrapper.findAll('.stage-column .stage-name'); + + expect(wrapper.find(linkedPipelinesColumn).exists()).toBe(true); + expect(stageColumnNames.at(stageColumnNames.length - 1).text()).toBe('Downstream'); + }); + + describe('triggered by', () => { + describe('on click', () => { + it('should emit `onClickTriggeredBy` when triggered by linked pipeline is clicked', () => { + const btnWrapper = wrapper.find('.linked-pipeline-content'); + + btnWrapper.trigger('click'); + + btnWrapper.vm.$nextTick(() => { + expect(wrapper.emitted().onClickTriggeredBy).toEqual([ + store.state.pipeline.triggered_by, + ]); + }); + }); + }); + + describe('with expanded pipeline', () => { + it('should render expanded pipeline', done => { + // expand the pipeline + store.state.pipeline.triggered_by[0].isExpanded = true; + + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: store.state.pipeline, + mediator, + }, + }); + + Vue.nextTick() + .then(() => { + expect(wrapper.find('.js-upstream-pipeline-12').exists()).toBe(true); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + + describe('triggered', () => { + describe('on click', () => { + it('should emit `onClickTriggered`', () => { + // We have to mock this method since we do both style change and + // emit and event, not mocking returns an error. + wrapper.setMethods({ + handleClickedDownstream: jest.fn(() => + wrapper.vm.$emit('onClickTriggered', ...store.state.pipeline.triggered), + ), + }); + + const btnWrappers = wrapper.findAll('.linked-pipeline-content'); + const downstreamBtnWrapper = btnWrappers.at(btnWrappers.length - 1); + + downstreamBtnWrapper.trigger('click'); + + downstreamBtnWrapper.vm.$nextTick(() => { + expect(wrapper.emitted().onClickTriggered).toEqual([store.state.pipeline.triggered]); + }); + }); + }); + + describe('with expanded pipeline', () => { + it('should render expanded pipeline', done => { + // expand the pipeline + store.state.pipeline.triggered[0].isExpanded = true; + + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: store.state.pipeline, + mediator, + }, + }); + + Vue.nextTick() + .then(() => { + expect(wrapper.find('.js-downstream-pipeline-34993051')).not.toBeNull(); + }) + .then(done) + .catch(done.fail); + }); + }); + }); + }); + }); + + describe('when linked pipelines are not present', () => { + beforeEach(() => { + const pipeline = Object.assign(linkedPipelineJSON, { triggered: null, triggered_by: null }); + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline, + mediator, + }, + }); + }); + + describe('rendered output', () => { + it('should include the first column with a no margin', () => { + const firstColumn = wrapper.find('.stage-column'); + + expect(firstColumn.classes()).toContain('no-margin'); + }); + + it('should not render a linked pipelines column', () => { + expect(wrapper.find('.linked-pipelines-column').exists()).toBe(false); + }); + }); + + describe('stageConnectorClass', () => { + it('it returns no-margin when no triggerer and there is one job', () => { + expect(wrapper.find(stageColumnComponent).classes()).toContain('no-margin'); + }); + + it('it returns left-margin when no triggerer and not the first stage', () => { + expect( + wrapper + .findAll(stageColumnComponent) + .at(1) + .classes(), + ).toContain('left-margin'); + }); + }); + }); + + describe('capitalizeStageName', () => { + it('capitalizes and escapes stage name', () => { + wrapper = mount(graphComponent, { + propsData: { + isLoading: false, + pipeline: graphJSON, + mediator, + }, + }); + + expect( + wrapper + .find('.stage-column:nth-child(2) .stage-name') + .text() + .trim(), + ).toEqual('Deploy <img src=x onerror=alert(document.domain)>'); + }); + }); +}); diff --git a/spec/frontend/pipelines/graph/job_group_dropdown_spec.js b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js new file mode 100644 index 00000000000..b323e1d8a06 --- /dev/null +++ b/spec/frontend/pipelines/graph/job_group_dropdown_spec.js @@ -0,0 +1,84 @@ +import { shallowMount } from '@vue/test-utils'; +import JobGroupDropdown from '~/pipelines/components/graph/job_group_dropdown.vue'; + +describe('job group dropdown component', () => { + const group = { + jobs: [ + { + id: 4256, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + tooltip: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4256', + has_details: true, + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4256/retry', + method: 'post', + }, + }, + }, + { + id: 4299, + name: 'test', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + tooltip: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4299', + has_details: true, + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4299/retry', + method: 'post', + }, + }, + }, + ], + name: 'rspec:linux', + size: 2, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + tooltip: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4256', + has_details: true, + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4256/retry', + method: 'post', + }, + }, + }; + + let wrapper; + const findButton = () => wrapper.find('button'); + + afterEach(() => { + wrapper.destroy(); + }); + + beforeEach(() => { + wrapper = shallowMount(JobGroupDropdown, { propsData: { group } }); + }); + + it('renders button with group name and size', () => { + expect(findButton().text()).toContain(group.name); + expect(findButton().text()).toContain(group.size); + }); + + it('renders dropdown with jobs', () => { + expect(wrapper.findAll('.scrollable-menu>ul>li').length).toBe(group.jobs.length); + }); +}); diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js index 0c64d5c9fa8..da777466e3e 100644 --- a/spec/frontend/pipelines/graph/job_item_spec.js +++ b/spec/frontend/pipelines/graph/job_item_spec.js @@ -47,7 +47,7 @@ describe('pipeline graph job item', () => { expect(link.attributes('title')).toEqual(`${mockJob.name} - ${mockJob.status.label}`); - expect(wrapper.find('.js-status-icon-success')).toBeDefined(); + expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(trimText(wrapper.find('.ci-status-text').text())).toBe(mockJob.name); @@ -73,7 +73,7 @@ describe('pipeline graph job item', () => { }, }); - expect(wrapper.find('.js-status-icon-success')).toBeDefined(); + expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); expect(wrapper.find('a').exists()).toBe(false); expect(trimText(wrapper.find('.ci-status-text').text())).toEqual(mockJob.name); @@ -84,8 +84,8 @@ describe('pipeline graph job item', () => { it('it should render the action icon', () => { createWrapper({ job: mockJob }); - expect(wrapper.find('a.ci-action-icon-container')).toBeDefined(); - expect(wrapper.find('i.ci-action-icon-wrapper')).toBeDefined(); + expect(wrapper.find('.ci-action-icon-container').exists()).toBe(true); + expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true); }); }); diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js new file mode 100644 index 00000000000..3574b66403e --- /dev/null +++ b/spec/frontend/pipelines/graph/job_name_component_spec.js @@ -0,0 +1,36 @@ +import { mount } from '@vue/test-utils'; +import ciIcon from '~/vue_shared/components/ci_icon.vue'; + +import jobNameComponent from '~/pipelines/components/graph/job_name_component.vue'; + +describe('job name component', () => { + let wrapper; + + const propsData = { + name: 'foo', + status: { + icon: 'status_success', + group: 'success', + }, + }; + + beforeEach(() => { + wrapper = mount(jobNameComponent, { + propsData, + }); + }); + + it('should render the provided name', () => { + expect( + wrapper + .find('.ci-status-text') + .text() + .trim(), + ).toBe(propsData.name); + }); + + it('should render an icon with the provided status', () => { + expect(wrapper.find(ciIcon).exists()).toBe(true); + expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js index 7f49b21100d..cf78aa3ef71 100644 --- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js +++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js @@ -1,12 +1,17 @@ import { mount } from '@vue/test-utils'; import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue'; +import CiStatus from '~/vue_shared/components/ci_icon.vue'; import mockData from './linked_pipelines_mock_data'; const mockPipeline = mockData.triggered[0]; +const validTriggeredPipelineId = mockPipeline.project.id; +const invalidTriggeredPipelineId = mockPipeline.project.id + 5; + describe('Linked pipeline', () => { let wrapper; + const findButton = () => wrapper.find('button'); const createWrapper = propsData => { wrapper = mount(LinkedPipelineComponent, { @@ -21,7 +26,7 @@ describe('Linked pipeline', () => { describe('rendered output', () => { const props = { pipeline: mockPipeline, - projectId: 20, + projectId: invalidTriggeredPipelineId, columnTitle: 'Downstream', }; @@ -44,14 +49,13 @@ describe('Linked pipeline', () => { }); it('should render an svg within the status container', () => { - const pipelineStatusElement = wrapper.find('.js-linked-pipeline-status'); + const pipelineStatusElement = wrapper.find(CiStatus); expect(pipelineStatusElement.find('svg').exists()).toBe(true); }); it('should render the pipeline status icon svg', () => { - expect(wrapper.find('.js-ci-status-icon-running').exists()).toBe(true); - expect(wrapper.find('.js-ci-status-icon-running').html()).toContain('<svg'); + expect(wrapper.find('.ci-status-icon-failed svg').exists()).toBe(true); }); it('should have a ci-status child component', () => { @@ -88,7 +92,7 @@ describe('Linked pipeline', () => { describe('parent/child', () => { const downstreamProps = { pipeline: mockPipeline, - projectId: 19, + projectId: validTriggeredPipelineId, columnTitle: 'Downstream', }; @@ -116,7 +120,7 @@ describe('Linked pipeline', () => { describe('when isLoading is true', () => { const props = { pipeline: { ...mockPipeline, isLoading: true }, - projectId: 19, + projectId: invalidTriggeredPipelineId, columnTitle: 'Downstream', }; @@ -132,7 +136,7 @@ describe('Linked pipeline', () => { describe('on click', () => { const props = { pipeline: mockPipeline, - projectId: 19, + projectId: validTriggeredPipelineId, columnTitle: 'Downstream', }; @@ -142,18 +146,18 @@ describe('Linked pipeline', () => { it('emits `pipelineClicked` event', () => { jest.spyOn(wrapper.vm, '$emit'); - wrapper.find('button').trigger('click'); + findButton().trigger('click'); expect(wrapper.emitted().pipelineClicked).toBeTruthy(); }); it('should emit `bv::hide::tooltip` to close the tooltip', () => { jest.spyOn(wrapper.vm.$root, '$emit'); - wrapper.find('button').trigger('click'); + findButton().trigger('click'); expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([ 'bv::hide::tooltip', - 'js-linked-pipeline-132', + 'js-linked-pipeline-34993051', ]); }); }); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js new file mode 100644 index 00000000000..82eaa553d0c --- /dev/null +++ b/spec/frontend/pipelines/graph/linked_pipelines_column_spec.js @@ -0,0 +1,38 @@ +import { shallowMount } from '@vue/test-utils'; +import LinkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines_column.vue'; +import LinkedPipeline from '~/pipelines/components/graph/linked_pipeline.vue'; +import mockData from './linked_pipelines_mock_data'; + +describe('Linked Pipelines Column', () => { + const propsData = { + columnTitle: 'Upstream', + linkedPipelines: mockData.triggered, + graphPosition: 'right', + projectId: 19, + }; + let wrapper; + + beforeEach(() => { + wrapper = shallowMount(LinkedPipelinesColumn, { propsData }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the pipeline orientation', () => { + const titleElement = wrapper.find('.linked-pipelines-column-title'); + + expect(titleElement.text()).toBe(propsData.columnTitle); + }); + + it('renders the correct number of linked pipelines', () => { + const linkedPipelineElements = wrapper.findAll(LinkedPipeline); + + expect(linkedPipelineElements.length).toBe(propsData.linkedPipelines.length); + }); + + it('renders cross project triangle when column is upstream', () => { + expect(wrapper.find('.cross-project-triangle').exists()).toBe(true); + }); +}); diff --git a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js index c9a94b3101f..3e9c0814403 100644 --- a/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js +++ b/spec/frontend/pipelines/graph/linked_pipelines_mock_data.js @@ -1,411 +1,3779 @@ export default { - project: { - id: 19, + id: 23211253, + user: { + id: 3585, + name: 'Achilleas Pipinellis', + username: 'axil', + state: 'active', + avatar_url: 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png', + web_url: 'https://gitlab.com/axil', + status_tooltip_html: + '\u003cspan class="user-status-emoji has-tooltip" title="I like pizza" data-html="true" data-placement="top"\u003e\u003cgl-emoji title="slice of pizza" data-name="pizza" data-unicode-version="6.0"\u003e🍕\u003c/gl-emoji\u003e\u003c/span\u003e', + path: '/axil', }, - triggered_by: { - id: 129, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/129', - project: { - name: 'GitLabCE', - }, - details: { - status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', - has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/129', - favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', - }, - }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: '7-5-stable', - path: '/gitlab-org/gitlab-foss/commits/7-5-stable', - tag: false, - branch: true, - }, - commit: { - id: '23433d4d8b20d7e45c103d0b6048faad38a130ab', - short_id: '23433d4d', - title: 'Version 7.5.0.rc1', - created_at: '2014-11-17T15:44:14.000+01:00', - parent_ids: ['30ac909f30f58d319b42ed1537664483894b18cd'], - message: 'Version 7.5.0.rc1\n', - author_name: 'Jacob Vosmaer', - author_email: 'contact@jacobvosmaer.nl', - authored_date: '2014-11-17T15:44:14.000+01:00', - committer_name: 'Jacob Vosmaer', - committer_email: 'contact@jacobvosmaer.nl', - committed_date: '2014-11-17T15:44:14.000+01:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/e66d11c0eedf8c07b3b18fca46599807?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab', - commit_path: '/gitlab-org/gitlab-foss/commit/23433d4d8b20d7e45c103d0b6048faad38a130ab', - }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/129/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/129/cancel', - created_at: '2017-05-24T14:46:20.090Z', - updated_at: '2017-05-24T14:46:29.906Z', + active: false, + coverage: null, + source: 'push', + created_at: '2018-06-05T11:31:30.452Z', + updated_at: '2018-10-31T16:35:31.305Z', + path: '/gitlab-org/gitlab-runner/pipelines/23211253', + flags: { + latest: false, + stuck: false, + auto_devops: false, + merge_request: false, + yaml_errors: false, + retryable: false, + cancelable: false, + failure_reason: false, }, - triggered: [ - { - id: 132, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/132', - project: { - name: 'GitLabCE', - id: 19, - }, - details: { + details: { + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/gitlab-org/gitlab-runner/pipelines/23211253', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + duration: 53, + finished_at: '2018-10-31T16:35:31.299Z', + stages: [ + { + name: 'prebuild', + title: 'prebuild: passed', + groups: [ + { + name: 'review-docs-deploy', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'manual play action', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/gitlab-org/gitlab-runner/-/jobs/72469032', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 72469032, + name: 'review-docs-deploy', + started: '2018-10-31T16:34:58.778Z', + archived: false, + build_path: '/gitlab-org/gitlab-runner/-/jobs/72469032', + retry_path: '/gitlab-org/gitlab-runner/-/jobs/72469032/retry', + play_path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play', + playable: true, + scheduled: false, + created_at: '2018-06-05T11:31:30.495Z', + updated_at: '2018-10-31T16:35:31.251Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'manual play action', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/gitlab-org/gitlab-runner/-/jobs/72469032', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + ], status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/132', + details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#prebuild', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', }, + path: '/gitlab-org/gitlab-runner/pipelines/23211253#prebuild', + dropdown_path: '/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=prebuild', }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - short_id: 'b9d58c4c', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-03T12:50:33.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-03T12:50:33.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/132/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/132/cancel', - created_at: '2017-05-24T14:46:24.644Z', - updated_at: '2017-05-24T14:48:55.226Z', - }, - { - id: 133, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/133', - project: { - name: 'GitLabCE', - }, - details: { + { + name: 'test', + title: 'test: passed', + groups: [ + { + name: 'docs check links', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/gitlab-org/gitlab-runner/-/jobs/72469033', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/gitlab-org/gitlab-runner/-/jobs/72469033/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 72469033, + name: 'docs check links', + started: '2018-06-05T11:31:33.240Z', + archived: false, + build_path: '/gitlab-org/gitlab-runner/-/jobs/72469033', + retry_path: '/gitlab-org/gitlab-runner/-/jobs/72469033/retry', + playable: false, + scheduled: false, + created_at: '2018-06-05T11:31:30.627Z', + updated_at: '2018-06-05T11:31:54.363Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/gitlab-org/gitlab-runner/-/jobs/72469033', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/gitlab-org/gitlab-runner/-/jobs/72469033/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + ], status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/133', + details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#test', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + 'https://gitlab.com/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', }, + path: '/gitlab-org/gitlab-runner/pipelines/23211253#test', + dropdown_path: '/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=test', }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b', - short_id: 'b6bd4856', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-02T20:39:29.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-02T20:39:29.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/133/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/133/cancel', - created_at: '2017-05-24T14:46:24.648Z', - updated_at: '2017-05-24T14:48:59.673Z', - }, - { - id: 130, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/130', - project: { - name: 'GitLabCE', - }, - details: { + { + name: 'cleanup', + title: 'cleanup: skipped', + groups: [ + { + name: 'review-docs-cleanup', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual stop action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-org/gitlab-runner/-/jobs/72469034', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'stop', + title: 'Stop', + path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play', + method: 'post', + button_title: 'Stop this environment', + }, + }, + jobs: [ + { + id: 72469034, + name: 'review-docs-cleanup', + started: null, + archived: false, + build_path: '/gitlab-org/gitlab-runner/-/jobs/72469034', + play_path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play', + playable: true, + scheduled: false, + created_at: '2018-06-05T11:31:30.760Z', + updated_at: '2018-06-05T11:31:56.037Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual stop action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-org/gitlab-runner/-/jobs/72469034', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'stop', + title: 'Stop', + path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play', + method: 'post', + button_title: 'Stop this environment', + }, + }, + }, + ], + }, + ], status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/130', + details_path: '/gitlab-org/gitlab-runner/pipelines/23211253#cleanup', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', }, + path: '/gitlab-org/gitlab-runner/pipelines/23211253#cleanup', + dropdown_path: '/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup', }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, + ], + artifacts: [], + manual_actions: [ + { + name: 'review-docs-cleanup', + path: '/gitlab-org/gitlab-runner/-/jobs/72469034/play', + playable: true, + scheduled: false, }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, + { + name: 'review-docs-deploy', + path: '/gitlab-org/gitlab-runner/-/jobs/72469032/play', + playable: true, + scheduled: false, }, - commit: { - id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f', - short_id: '6d7ced4a', - title: 'Whitespace fixes to patch', - created_at: '2013-10-08T13:53:22.000-05:00', - parent_ids: ['1875141a963a4238bda29011d8f7105839485253'], - message: 'Whitespace fixes to patch\n', - author_name: 'Dale Hamel', - author_email: 'dale.hamel@srvthe.net', - authored_date: '2013-10-08T13:53:22.000-05:00', - committer_name: 'Dale Hamel', - committer_email: 'dale.hamel@invenia.ca', - committed_date: '2013-10-08T13:53:22.000-05:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', - commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', + ], + scheduled_actions: [], + }, + ref: { + name: 'docs/add-development-guide-to-readme', + path: '/gitlab-org/gitlab-runner/commits/docs/add-development-guide-to-readme', + tag: false, + branch: true, + merge_request: false, + }, + commit: { + id: '8083eb0a920572214d0dccedd7981f05d535ad46', + short_id: '8083eb0a', + title: 'Add link to development guide in readme', + created_at: '2018-06-05T11:30:48.000Z', + parent_ids: ['1d7cf79b5a1a2121b9474ac20d61c1b8f621289d'], + message: + 'Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n', + author_name: 'Achilleas Pipinellis', + author_email: 'axil@gitlab.com', + authored_date: '2018-06-05T11:30:48.000Z', + committer_name: 'Achilleas Pipinellis', + committer_email: 'axil@gitlab.com', + committed_date: '2018-06-05T11:30:48.000Z', + author: { + id: 3585, + name: 'Achilleas Pipinellis', + username: 'axil', + state: 'active', + avatar_url: 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png', + web_url: 'https://gitlab.com/axil', + status_tooltip_html: null, + path: '/axil', + }, + author_gravatar_url: + 'https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon', + commit_url: + 'https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46', + commit_path: '/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46', + }, + project: { id: 20 }, + triggered_by: { + id: 12, + user: { + id: 376774, + name: 'Alessio Caiazza', + username: 'nolith', + state: 'active', + avatar_url: 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png', + web_url: 'https://gitlab.com/nolith', + status_tooltip_html: null, + path: '/nolith', + }, + active: false, + coverage: null, + source: 'pipeline', + path: '/gitlab-com/gitlab-docs/pipelines/34993051', + details: { + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/130/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/130/cancel', - created_at: '2017-05-24T14:46:24.630Z', - updated_at: '2017-05-24T14:49:45.091Z', + duration: 118, + finished_at: '2018-10-31T16:41:40.615Z', + stages: [ + { + name: 'build-images', + title: 'build-images: skipped', + groups: [ + { + name: 'image:bootstrap', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 11421321982853, + name: 'image:bootstrap', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.704Z', + updated_at: '2018-10-31T16:35:24.118Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + { + name: 'image:builder-onbuild', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 1149822131854, + name: 'image:builder-onbuild', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.728Z', + updated_at: '2018-10-31T16:35:24.070Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + { + name: 'image:nginx-onbuild', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 11498285523424, + name: 'image:nginx-onbuild', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.753Z', + updated_at: '2018-10-31T16:35:24.033Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images', + }, + { + name: 'build', + title: 'build: failed', + groups: [ + { + name: 'compile_dev', + size: 1, + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed - (script failure)', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/528/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 1149846949786, + name: 'compile_dev', + started: '2018-10-31T16:39:41.598Z', + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + retry_path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:39:41.138Z', + updated_at: '2018-10-31T16:41:40.072Z', + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed - (script failure)', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/528/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + recoverable: false, + }, + ], + }, + ], + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#build', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build', + }, + { + name: 'deploy', + title: 'deploy: skipped', + groups: [ + { + name: 'review', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 11498282342357, + name: 'review', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:35:23.805Z', + updated_at: '2018-10-31T16:41:40.569Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + { + name: 'review_stop', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 114982858, + name: 'review_stop', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:35:23.840Z', + updated_at: '2018-10-31T16:41:40.480Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + ], + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy', + }, + ], + artifacts: [], + manual_actions: [ + { + name: 'image:bootstrap', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + playable: true, + scheduled: false, + }, + { + name: 'image:builder-onbuild', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + playable: true, + scheduled: false, + }, + { + name: 'image:nginx-onbuild', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + playable: true, + scheduled: false, + }, + { + name: 'review_stop', + path: '/gitlab-com/gitlab-docs/-/jobs/114982858/play', + playable: false, + scheduled: false, + }, + ], + scheduled_actions: [], }, - { - id: 131, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/132', - project: { - name: 'GitLabCE', + project: { + id: 20, + name: 'Test', + full_path: '/gitlab-com/gitlab-docs', + full_name: 'GitLab.com / GitLab Docs', + }, + triggered_by: { + id: 349932310342451, + user: { + id: 376774, + name: 'Alessio Caiazza', + username: 'nolith', + state: 'active', + avatar_url: + 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png', + web_url: 'https://gitlab.com/nolith', + status_tooltip_html: null, + path: '/nolith', }, + active: false, + coverage: null, + source: 'pipeline', + path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/132', + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', }, + duration: 118, + finished_at: '2018-10-31T16:41:40.615Z', + stages: [ + { + name: 'build-images', + title: 'build-images: skipped', + groups: [ + { + name: 'image:bootstrap', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 11421321982853, + name: 'image:bootstrap', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.704Z', + updated_at: '2018-10-31T16:35:24.118Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + { + name: 'image:builder-onbuild', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 1149822131854, + name: 'image:builder-onbuild', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.728Z', + updated_at: '2018-10-31T16:35:24.070Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + { + name: 'image:nginx-onbuild', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 11498285523424, + name: 'image:nginx-onbuild', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.753Z', + updated_at: '2018-10-31T16:35:24.033Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images', + dropdown_path: + '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images', + }, + { + name: 'build', + title: 'build: failed', + groups: [ + { + name: 'compile_dev', + size: 1, + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed - (script failure)', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 1149846949786, + name: 'compile_dev', + started: '2018-10-31T16:39:41.598Z', + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + retry_path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:39:41.138Z', + updated_at: '2018-10-31T16:41:40.072Z', + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed - (script failure)', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + recoverable: false, + }, + ], + }, + ], + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#build', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build', + }, + { + name: 'deploy', + title: 'deploy: skipped', + groups: [ + { + name: 'review', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 11498282342357, + name: 'review', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:35:23.805Z', + updated_at: '2018-10-31T16:41:40.569Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + { + name: 'review_stop', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 114982858, + name: 'review_stop', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:35:23.840Z', + updated_at: '2018-10-31T16:41:40.480Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + ], + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy', + }, + ], + artifacts: [], + manual_actions: [ + { + name: 'image:bootstrap', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + playable: true, + scheduled: false, + }, + { + name: 'image:builder-onbuild', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + playable: true, + scheduled: false, + }, + { + name: 'image:nginx-onbuild', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + playable: true, + scheduled: false, + }, + { + name: 'review_stop', + path: '/gitlab-com/gitlab-docs/-/jobs/114982858/play', + playable: false, + scheduled: false, + }, + ], + scheduled_actions: [], }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - short_id: 'b9d58c4c', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-03T12:50:33.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n\nchangelog updated to include ssh key retrieval feature update\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-03T12:50:33.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', - commit_path: '/gitlab-org/gitlab-foss/commit/b9d58c4cecd06be74c3cc32ccfb522b31544ab2e', + project: { + id: 20, + name: 'GitLab Docs', + full_path: '/gitlab-com/gitlab-docs', + full_name: 'GitLab.com / GitLab Docs', }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/132/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/132/cancel', - created_at: '2017-05-24T14:46:24.644Z', - updated_at: '2017-05-24T14:48:55.226Z', }, + triggered: [], + }, + triggered: [ { - id: 134, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/133', - project: { - name: 'GitLabCE', + id: 34993051, + user: { + id: 376774, + name: 'Alessio Caiazza', + username: 'nolith', + state: 'active', + avatar_url: + 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png', + web_url: 'https://gitlab.com/nolith', + status_tooltip_html: null, + path: '/nolith', }, + active: false, + coverage: null, + source: 'pipeline', + path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/133', + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', }, + duration: 118, + finished_at: '2018-10-31T16:41:40.615Z', + stages: [ + { + name: 'build-images', + title: 'build-images: skipped', + groups: [ + { + name: 'image:bootstrap', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 114982853, + name: 'image:bootstrap', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.704Z', + updated_at: '2018-10-31T16:35:24.118Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + { + name: 'image:builder-onbuild', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 114982854, + name: 'image:builder-onbuild', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.728Z', + updated_at: '2018-10-31T16:35:24.070Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + { + name: 'image:nginx-onbuild', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 114982855, + name: 'image:nginx-onbuild', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.753Z', + updated_at: '2018-10-31T16:35:24.033Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images', + dropdown_path: + '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images', + }, + { + name: 'build', + title: 'build: failed', + groups: [ + { + name: 'compile_dev', + size: 1, + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed - (script failure)', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/528/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 114984694, + name: 'compile_dev', + started: '2018-10-31T16:39:41.598Z', + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + retry_path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:39:41.138Z', + updated_at: '2018-10-31T16:41:40.072Z', + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed - (script failure)', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/528/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + recoverable: false, + }, + ], + }, + ], + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#build', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build', + }, + { + name: 'deploy', + title: 'deploy: skipped', + groups: [ + { + name: 'review', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 114982857, + name: 'review', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:35:23.805Z', + updated_at: '2018-10-31T16:41:40.569Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + { + name: 'review_stop', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 114982858, + name: 'review_stop', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:35:23.840Z', + updated_at: '2018-10-31T16:41:40.480Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + ], + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy', + }, + ], + artifacts: [], + manual_actions: [ + { + name: 'image:bootstrap', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + playable: true, + scheduled: false, + }, + { + name: 'image:builder-onbuild', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + playable: true, + scheduled: false, + }, + { + name: 'image:nginx-onbuild', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + playable: true, + scheduled: false, + }, + { + name: 'review_stop', + path: '/gitlab-com/gitlab-docs/-/jobs/114982858/play', + playable: false, + scheduled: false, + }, + ], + scheduled_actions: [], }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: 'b6bd4856a33df3d144be66c4ed1f1396009bb08b', - short_id: 'b6bd4856', - title: 'getting user keys publically through http without any authentication, the github…', - created_at: '2013-10-02T20:39:29.000+05:30', - parent_ids: ['e219cf7246c6a0495e4507deaffeba11e79f13b8'], - message: - 'getting user keys publically through http without any authentication, the github way. E.g: http://github.com/devaroop.keys\n', - author_name: 'devaroop', - author_email: 'devaroop123@yahoo.co.in', - authored_date: '2013-10-02T20:39:29.000+05:30', - committer_name: 'devaroop', - committer_email: 'devaroop123@yahoo.co.in', - committed_date: '2013-10-02T20:39:29.000+05:30', - author_gravatar_url: - 'http://www.gravatar.com/avatar/35df4b155ec66a3127d53459941cf8a2?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', - commit_path: '/gitlab-org/gitlab-foss/commit/b6bd4856a33df3d144be66c4ed1f1396009bb08b', + project: { + id: 20, + name: 'GitLab Docs', + full_path: '/gitlab-com/gitlab-docs', + full_name: 'GitLab.com / GitLab Docs', }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/133/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/133/cancel', - created_at: '2017-05-24T14:46:24.648Z', - updated_at: '2017-05-24T14:48:59.673Z', }, { - id: 135, - active: true, - path: '/gitlab-org/gitlab-foss/pipelines/130', - project: { - name: 'GitLabCE', + id: 34993052, + user: { + id: 376774, + name: 'Alessio Caiazza', + username: 'nolith', + state: 'active', + avatar_url: + 'https://assets.gitlab-static.net/uploads/-/system/user/avatar/376774/avatar.png', + web_url: 'https://gitlab.com/nolith', + status_tooltip_html: null, + path: '/nolith', }, + active: false, + coverage: null, + source: 'pipeline', + path: '/gitlab-com/gitlab-docs/pipelines/34993051', details: { status: { - icon: 'status_running', - text: 'running', - label: 'running', - group: 'running', + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', has_details: true, - details_path: '/gitlab-org/gitlab-foss/pipelines/130', + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051', + illustration: null, favicon: - '/assets/ci_favicons/dev/favicon_status_running-c3ad2fc53ea6079c174e5b6c1351ff349e99ec3af5a5622fb77b0fe53ea279c1.ico', + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', }, + duration: 118, + finished_at: '2018-10-31T16:41:40.615Z', + stages: [ + { + name: 'build-images', + title: 'build-images: skipped', + groups: [ + { + name: 'image:bootstrap', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 114982853, + name: 'image:bootstrap', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.704Z', + updated_at: '2018-10-31T16:35:24.118Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982853', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + { + name: 'image:builder-onbuild', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 114982854, + name: 'image:builder-onbuild', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.728Z', + updated_at: '2018-10-31T16:35:24.070Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982854', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + { + name: 'image:nginx-onbuild', + size: 1, + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 1224982855, + name: 'image:nginx-onbuild', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + play_path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + playable: true, + scheduled: false, + created_at: '2018-10-31T16:35:23.753Z', + updated_at: '2018-10-31T16:35:24.033Z', + status: { + icon: 'status_manual', + text: 'manual', + label: 'manual play action', + group: 'manual', + tooltip: 'manual action', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982855', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_manual-829a0804612cef47d9efc1618dba38325483657c847dba0546c3b9f0295bb36c.png', + action: { + icon: 'play', + title: 'Play', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#build-images', + dropdown_path: + '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build-images', + }, + { + name: 'build', + title: 'build: failed', + groups: [ + { + name: 'compile_dev', + size: 1, + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed - (script failure)', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 1123984694, + name: 'compile_dev', + started: '2018-10-31T16:39:41.598Z', + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + retry_path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:39:41.138Z', + updated_at: '2018-10-31T16:41:40.072Z', + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed - (script failure)', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114984694', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/gitlab-com/gitlab-docs/-/jobs/114984694/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + recoverable: false, + }, + ], + }, + ], + status: { + icon: 'status_failed', + text: 'failed', + label: 'failed', + group: 'failed', + tooltip: 'failed', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#build', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#build', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=build', + }, + { + name: 'deploy', + title: 'deploy: skipped', + groups: [ + { + name: 'review', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 1143232982857, + name: 'review', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:35:23.805Z', + updated_at: '2018-10-31T16:41:40.569Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982857', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + { + name: 'review_stop', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 114921313182858, + name: 'review_stop', + started: null, + archived: false, + build_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + playable: false, + scheduled: false, + created_at: '2018-10-31T16:35:23.840Z', + updated_at: '2018-10-31T16:41:40.480Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/-/jobs/114982858', + illustration: { + image: + 'https://assets.gitlab-static.net/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + ], + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy', + illustration: null, + favicon: + 'https://gitlab.com/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + path: '/gitlab-com/gitlab-docs/pipelines/34993051#deploy', + dropdown_path: '/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy', + }, + ], + artifacts: [], + manual_actions: [ + { + name: 'image:bootstrap', + path: '/gitlab-com/gitlab-docs/-/jobs/114982853/play', + playable: true, + scheduled: false, + }, + { + name: 'image:builder-onbuild', + path: '/gitlab-com/gitlab-docs/-/jobs/114982854/play', + playable: true, + scheduled: false, + }, + { + name: 'image:nginx-onbuild', + path: '/gitlab-com/gitlab-docs/-/jobs/114982855/play', + playable: true, + scheduled: false, + }, + { + name: 'review_stop', + path: '/gitlab-com/gitlab-docs/-/jobs/114982858/play', + playable: false, + scheduled: false, + }, + ], + scheduled_actions: [], }, - flags: { - latest: false, - triggered: false, - stuck: false, - yaml_errors: false, - retryable: true, - cancelable: true, - }, - ref: { - name: 'crowd', - path: '/gitlab-org/gitlab-foss/commits/crowd', - tag: false, - branch: true, - }, - commit: { - id: '6d7ced4a2311eeff037c5575cca1868a6d3f586f', - short_id: '6d7ced4a', - title: 'Whitespace fixes to patch', - created_at: '2013-10-08T13:53:22.000-05:00', - parent_ids: ['1875141a963a4238bda29011d8f7105839485253'], - message: 'Whitespace fixes to patch\n', - author_name: 'Dale Hamel', - author_email: 'dale.hamel@srvthe.net', - authored_date: '2013-10-08T13:53:22.000-05:00', - committer_name: 'Dale Hamel', - committer_email: 'dale.hamel@invenia.ca', - committed_date: '2013-10-08T13:53:22.000-05:00', - author_gravatar_url: - 'http://www.gravatar.com/avatar/cd08930e69fa5ad1a669206e7bafe476?s=80&d=identicon', - commit_url: - 'http://localhost:3000/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', - commit_path: '/gitlab-org/gitlab-foss/commit/6d7ced4a2311eeff037c5575cca1868a6d3f586f', + project: { + id: 20, + name: 'GitLab Docs', + full_path: '/gitlab-com/gitlab-docs', + full_name: 'GitLab.com / GitLab Docs', }, - retry_path: '/gitlab-org/gitlab-foss/pipelines/130/retry', - cancel_path: '/gitlab-org/gitlab-foss/pipelines/130/cancel', - created_at: '2017-05-24T14:46:24.630Z', - updated_at: '2017-05-24T14:49:45.091Z', + triggered: [ + { + id: 26, + user: null, + active: false, + coverage: null, + source: 'push', + created_at: '2019-01-06T17:48:37.599Z', + updated_at: '2019-01-06T17:48:38.371Z', + path: '/h5bp/html5-boilerplate/pipelines/26', + flags: { + latest: true, + stuck: false, + auto_devops: false, + merge_request: false, + yaml_errors: false, + retryable: true, + cancelable: false, + failure_reason: false, + }, + details: { + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/pipelines/26', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + duration: null, + finished_at: '2019-01-06T17:48:38.370Z', + stages: [ + { + name: 'build', + title: 'build: passed', + groups: [ + { + name: 'build:linux', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/526', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/526/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 526, + name: 'build:linux', + started: '2019-01-06T08:48:20.236Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/526', + retry_path: '/h5bp/html5-boilerplate/-/jobs/526/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:37.806Z', + updated_at: '2019-01-06T17:48:37.806Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/526', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/526/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'build:osx', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/527', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/527/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 527, + name: 'build:osx', + started: '2019-01-06T07:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/527', + retry_path: '/h5bp/html5-boilerplate/-/jobs/527/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:37.846Z', + updated_at: '2019-01-06T17:48:37.846Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/527', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/527/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/pipelines/26#build', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/h5bp/html5-boilerplate/pipelines/26#build', + dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=build', + }, + { + name: 'test', + title: 'test: passed with warnings', + groups: [ + { + name: 'jenkins', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: null, + group: 'success', + tooltip: null, + has_details: false, + details_path: null, + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + jobs: [ + { + id: 546, + name: 'jenkins', + started: '2019-01-06T11:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/546', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.359Z', + updated_at: '2019-01-06T17:48:38.359Z', + status: { + icon: 'status_success', + text: 'passed', + label: null, + group: 'success', + tooltip: null, + has_details: false, + details_path: null, + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + }, + ], + }, + { + name: 'rspec:linux', + size: 3, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: false, + details_path: null, + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + jobs: [ + { + id: 528, + name: 'rspec:linux 0 3', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/528', + retry_path: '/h5bp/html5-boilerplate/-/jobs/528/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:37.885Z', + updated_at: '2019-01-06T17:48:37.885Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/528', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/528/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + { + id: 529, + name: 'rspec:linux 1 3', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/529', + retry_path: '/h5bp/html5-boilerplate/-/jobs/529/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:37.907Z', + updated_at: '2019-01-06T17:48:37.907Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/529', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/529/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + { + id: 530, + name: 'rspec:linux 2 3', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/530', + retry_path: '/h5bp/html5-boilerplate/-/jobs/530/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:37.927Z', + updated_at: '2019-01-06T17:48:37.927Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/530', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/530/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'rspec:osx', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/535', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/535/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 535, + name: 'rspec:osx', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/535', + retry_path: '/h5bp/html5-boilerplate/-/jobs/535/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.018Z', + updated_at: '2019-01-06T17:48:38.018Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/535', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/535/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'rspec:windows', + size: 3, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: false, + details_path: null, + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + jobs: [ + { + id: 531, + name: 'rspec:windows 0 3', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/531', + retry_path: '/h5bp/html5-boilerplate/-/jobs/531/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:37.944Z', + updated_at: '2019-01-06T17:48:37.944Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/531', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/531/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + { + id: 532, + name: 'rspec:windows 1 3', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/532', + retry_path: '/h5bp/html5-boilerplate/-/jobs/532/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:37.962Z', + updated_at: '2019-01-06T17:48:37.962Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/532', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/532/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + { + id: 534, + name: 'rspec:windows 2 3', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/534', + retry_path: '/h5bp/html5-boilerplate/-/jobs/534/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:37.999Z', + updated_at: '2019-01-06T17:48:37.999Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/534', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/534/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'spinach:linux', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/536', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/536/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 536, + name: 'spinach:linux', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/536', + retry_path: '/h5bp/html5-boilerplate/-/jobs/536/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.050Z', + updated_at: '2019-01-06T17:48:38.050Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/536', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/536/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'spinach:osx', + size: 1, + status: { + icon: 'status_warning', + text: 'failed', + label: 'failed (allowed to fail)', + group: 'failed-with-warnings', + tooltip: 'failed - (unknown failure) (allowed to fail)', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/537', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/537/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 537, + name: 'spinach:osx', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/537', + retry_path: '/h5bp/html5-boilerplate/-/jobs/537/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.069Z', + updated_at: '2019-01-06T17:48:38.069Z', + status: { + icon: 'status_warning', + text: 'failed', + label: 'failed (allowed to fail)', + group: 'failed-with-warnings', + tooltip: 'failed - (unknown failure) (allowed to fail)', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/537', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_failed-41304d7f7e3828808b0c26771f0309e55296819a9beea3ea9fbf6689d9857c12.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/537/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + callout_message: 'There is an unknown failure, please try again', + recoverable: true, + }, + ], + }, + ], + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/pipelines/26#test', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/h5bp/html5-boilerplate/pipelines/26#test', + dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=test', + }, + { + name: 'security', + title: 'security: passed', + groups: [ + { + name: 'container_scanning', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/541', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/541/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 541, + name: 'container_scanning', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/541', + retry_path: '/h5bp/html5-boilerplate/-/jobs/541/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.186Z', + updated_at: '2019-01-06T17:48:38.186Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/541', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/541/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'dast', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/538', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/538/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 538, + name: 'dast', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/538', + retry_path: '/h5bp/html5-boilerplate/-/jobs/538/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.087Z', + updated_at: '2019-01-06T17:48:38.087Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/538', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/538/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'dependency_scanning', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/540', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/540/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 540, + name: 'dependency_scanning', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/540', + retry_path: '/h5bp/html5-boilerplate/-/jobs/540/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.153Z', + updated_at: '2019-01-06T17:48:38.153Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/540', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/540/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'sast', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/539', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/539/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 539, + name: 'sast', + started: '2019-01-06T09:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/539', + retry_path: '/h5bp/html5-boilerplate/-/jobs/539/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.121Z', + updated_at: '2019-01-06T17:48:38.121Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/539', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/539/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/pipelines/26#security', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/h5bp/html5-boilerplate/pipelines/26#security', + dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=security', + }, + { + name: 'deploy', + title: 'deploy: passed', + groups: [ + { + name: 'production', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/544', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 544, + name: 'production', + started: null, + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/544', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.313Z', + updated_at: '2019-01-06T17:48:38.313Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/544', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + { + name: 'staging', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/542', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/542/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + jobs: [ + { + id: 542, + name: 'staging', + started: '2019-01-06T11:48:20.237Z', + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/542', + retry_path: '/h5bp/html5-boilerplate/-/jobs/542/retry', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.219Z', + updated_at: '2019-01-06T17:48:38.219Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/542', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job does not have a trace.', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/h5bp/html5-boilerplate/-/jobs/542/retry', + method: 'post', + button_title: 'Retry this job', + }, + }, + }, + ], + }, + { + name: 'stop staging', + size: 1, + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/543', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + jobs: [ + { + id: 543, + name: 'stop staging', + started: null, + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/543', + playable: false, + scheduled: false, + created_at: '2019-01-06T17:48:38.283Z', + updated_at: '2019-01-06T17:48:38.283Z', + status: { + icon: 'status_skipped', + text: 'skipped', + label: 'skipped', + group: 'skipped', + tooltip: 'skipped', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/543', + illustration: { + image: + '/assets/illustrations/skipped-job_empty-8b877955fbf175e42ae65b6cb95346e15282c6fc5b682756c329af3a0055225e.svg', + size: 'svg-430', + title: 'This job has been skipped', + }, + favicon: + '/assets/ci_favicons/favicon_status_skipped-0b9c5e543588945e8c4ca57786bbf2d0c56631959c9f853300392d0315be829b.png', + }, + }, + ], + }, + ], + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/pipelines/26#deploy', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/h5bp/html5-boilerplate/pipelines/26#deploy', + dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=deploy', + }, + { + name: 'notify', + title: 'notify: passed', + groups: [ + { + name: 'slack', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'manual play action', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/545', + illustration: { + image: + '/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'play', + title: 'Play', + path: '/h5bp/html5-boilerplate/-/jobs/545/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + jobs: [ + { + id: 545, + name: 'slack', + started: null, + archived: false, + build_path: '/h5bp/html5-boilerplate/-/jobs/545', + retry_path: '/h5bp/html5-boilerplate/-/jobs/545/retry', + play_path: '/h5bp/html5-boilerplate/-/jobs/545/play', + playable: true, + scheduled: false, + created_at: '2019-01-06T17:48:38.341Z', + updated_at: '2019-01-06T17:48:38.341Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'manual play action', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/-/jobs/545', + illustration: { + image: + '/assets/illustrations/manual_action-2b4ca0d1bcfd92aebf33d484e36cbf7a102d007f76b5a0cfea636033a629d601.svg', + size: 'svg-394', + title: 'This job requires a manual action', + content: + 'This job depends on a user to trigger its process. Often they are used to deploy code to production environments', + }, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + action: { + icon: 'play', + title: 'Play', + path: '/h5bp/html5-boilerplate/-/jobs/545/play', + method: 'post', + button_title: 'Trigger this manual action', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + tooltip: 'passed', + has_details: true, + details_path: '/h5bp/html5-boilerplate/pipelines/26#notify', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + path: '/h5bp/html5-boilerplate/pipelines/26#notify', + dropdown_path: '/h5bp/html5-boilerplate/pipelines/26/stage.json?stage=notify', + }, + ], + artifacts: [ + { + name: 'build:linux', + expired: null, + expire_at: null, + path: '/h5bp/html5-boilerplate/-/jobs/526/artifacts/download', + browse_path: '/h5bp/html5-boilerplate/-/jobs/526/artifacts/browse', + }, + { + name: 'build:osx', + expired: null, + expire_at: null, + path: '/h5bp/html5-boilerplate/-/jobs/527/artifacts/download', + browse_path: '/h5bp/html5-boilerplate/-/jobs/527/artifacts/browse', + }, + ], + manual_actions: [ + { + name: 'stop staging', + path: '/h5bp/html5-boilerplate/-/jobs/543/play', + playable: false, + scheduled: false, + }, + { + name: 'production', + path: '/h5bp/html5-boilerplate/-/jobs/544/play', + playable: false, + scheduled: false, + }, + { + name: 'slack', + path: '/h5bp/html5-boilerplate/-/jobs/545/play', + playable: true, + scheduled: false, + }, + ], + scheduled_actions: [], + }, + ref: { + name: 'master', + path: '/h5bp/html5-boilerplate/commits/master', + tag: false, + branch: true, + merge_request: false, + }, + commit: { + id: 'bad98c453eab56d20057f3929989251d45cd1a8b', + short_id: 'bad98c45', + title: 'remove instances of shrink-to-fit=no (#2103)', + created_at: '2018-12-17T20:52:18.000Z', + parent_ids: ['49130f6cfe9ff1f749015d735649a2bc6f66cf3a'], + message: + 'remove instances of shrink-to-fit=no (#2103)\n\ncloses #2102\r\n\r\nPer my findings, the need for it as a default was rectified with the release of iOS 9.3, where the viewport no longer shrunk to accommodate overflow, as was introduced in iOS 9.', + author_name: "Scott O'Hara", + author_email: 'scottaohara@users.noreply.github.com', + authored_date: '2018-12-17T20:52:18.000Z', + committer_name: 'Rob Larsen', + committer_email: 'rob@drunkenfist.com', + committed_date: '2018-12-17T20:52:18.000Z', + author: null, + author_gravatar_url: + 'https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80\u0026d=identicon', + commit_url: + 'http://localhost:3001/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b', + commit_path: '/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b', + }, + retry_path: '/h5bp/html5-boilerplate/pipelines/26/retry', + triggered_by: { + id: 4, + user: null, + active: false, + coverage: null, + source: 'push', + path: '/gitlab-org/gitlab-test/pipelines/4', + details: { + status: { + icon: 'status_warning', + text: 'passed', + label: 'passed with warnings', + group: 'success-with-warnings', + tooltip: 'passed', + has_details: true, + details_path: '/gitlab-org/gitlab-test/pipelines/4', + illustration: null, + favicon: + '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png', + }, + }, + project: { + id: 1, + name: 'Gitlab Test', + full_path: '/gitlab-org/gitlab-test', + full_name: 'Gitlab Org / Gitlab Test', + }, + }, + triggered: [], + project: { + id: 20, + name: 'GitLab Docs', + full_path: '/gitlab-com/gitlab-docs', + full_name: 'GitLab.com / GitLab Docs', + }, + }, + ], }, ], }; diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js new file mode 100644 index 00000000000..a4a5d78f906 --- /dev/null +++ b/spec/frontend/pipelines/graph/mock_data.js @@ -0,0 +1,261 @@ +export default { + id: 123, + user: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + }, + active: false, + coverage: null, + path: '/root/ci-mock/pipelines/123', + details: { + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + }, + duration: 9, + finished_at: '2017-04-19T14:30:27.542Z', + stages: [ + { + name: 'test', + title: 'test: passed', + groups: [ + { + name: 'test', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4153', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4153/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4153, + name: 'test', + build_path: '/root/ci-mock/builds/4153', + retry_path: '/root/ci-mock/builds/4153/retry', + playable: false, + created_at: '2017-04-13T09:25:18.959Z', + updated_at: '2017-04-13T09:25:23.118Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4153', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4153/retry', + method: 'post', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123#test', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + }, + path: '/root/ci-mock/pipelines/123#test', + dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=test', + }, + { + name: 'deploy <img src=x onerror=alert(document.domain)>', + title: 'deploy: passed', + groups: [ + { + name: 'deploy to production', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4166', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4166/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4166, + name: 'deploy to production', + build_path: '/root/ci-mock/builds/4166', + retry_path: '/root/ci-mock/builds/4166/retry', + playable: false, + created_at: '2017-04-19T14:29:46.463Z', + updated_at: '2017-04-19T14:30:27.498Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4166', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4166/retry', + method: 'post', + }, + }, + }, + ], + }, + { + name: 'deploy to staging', + size: 1, + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4159', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4159/retry', + method: 'post', + }, + }, + jobs: [ + { + id: 4159, + name: 'deploy to staging', + build_path: '/root/ci-mock/builds/4159', + retry_path: '/root/ci-mock/builds/4159/retry', + playable: false, + created_at: '2017-04-18T16:32:08.420Z', + updated_at: '2017-04-18T16:32:12.631Z', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/builds/4159', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4159/retry', + method: 'post', + }, + }, + }, + ], + }, + ], + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + has_details: true, + details_path: '/root/ci-mock/pipelines/123#deploy', + favicon: + '/assets/ci_favicons/favicon_status_success-308b4fc054cdd1b68d0865e6cfb7b02e92e3472f201507418f8eddb74ac11a59.png', + }, + path: '/root/ci-mock/pipelines/123#deploy', + dropdown_path: '/root/ci-mock/pipelines/123/stage.json?stage=deploy', + }, + ], + artifacts: [], + manual_actions: [ + { + name: 'deploy to production', + path: '/root/ci-mock/builds/4166/play', + playable: false, + }, + ], + }, + flags: { + latest: true, + triggered: false, + stuck: false, + yaml_errors: false, + retryable: false, + cancelable: false, + }, + ref: { + name: 'master', + path: '/root/ci-mock/tree/master', + tag: false, + branch: true, + }, + commit: { + id: '798e5f902592192afaba73f4668ae30e56eae492', + short_id: '798e5f90', + title: "Merge branch 'new-branch' into 'master'\r", + created_at: '2017-04-13T10:25:17.000+01:00', + parent_ids: [ + '54d483b1ed156fbbf618886ddf7ab023e24f8738', + 'c8e2d38a6c538822e81c57022a6e3a0cfedebbcc', + ], + message: + "Merge branch 'new-branch' into 'master'\r\n\r\nAdd new file\r\n\r\nSee merge request !1", + author_name: 'Root', + author_email: 'admin@example.com', + authored_date: '2017-04-13T10:25:17.000+01:00', + committer_name: 'Root', + committer_email: 'admin@example.com', + committed_date: '2017-04-13T10:25:17.000+01:00', + author: { + name: 'Root', + username: 'root', + id: 1, + state: 'active', + avatar_url: null, + web_url: 'http://localhost:3000/root', + }, + author_gravatar_url: null, + commit_url: + 'http://localhost:3000/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492', + commit_path: '/root/ci-mock/commit/798e5f902592192afaba73f4668ae30e56eae492', + }, + created_at: '2017-04-13T09:25:18.881Z', + updated_at: '2017-04-19T14:30:27.561Z', +}; diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js new file mode 100644 index 00000000000..88e56eee1d6 --- /dev/null +++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js @@ -0,0 +1,136 @@ +import { shallowMount } from '@vue/test-utils'; + +import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue'; + +describe('stage column component', () => { + const mockJob = { + id: 4250, + name: 'test', + status: { + icon: 'status_success', + text: 'passed', + label: 'passed', + group: 'success', + details_path: '/root/ci-mock/builds/4250', + action: { + icon: 'retry', + title: 'Retry', + path: '/root/ci-mock/builds/4250/retry', + method: 'post', + }, + }, + }; + + let wrapper; + + beforeEach(() => { + const mockGroups = []; + for (let i = 0; i < 3; i += 1) { + const mockedJob = Object.assign({}, mockJob); + mockedJob.id += i; + mockGroups.push(mockedJob); + } + + wrapper = shallowMount(stageColumnComponent, { + propsData: { + title: 'foo', + groups: mockGroups, + hasTriggeredBy: false, + }, + }); + }); + + it('should render provided title', () => { + expect( + wrapper + .find('.stage-name') + .text() + .trim(), + ).toBe('foo'); + }); + + it('should render the provided groups', () => { + expect(wrapper.findAll('.builds-container > ul > li').length).toBe( + wrapper.props('groups').length, + ); + }); + + describe('jobId', () => { + it('escapes job name', () => { + wrapper = shallowMount(stageColumnComponent, { + propsData: { + groups: [ + { + id: 4259, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + label: 'success', + tooltip: '<img src=x onerror=alert(document.domain)>', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + }, + }); + + expect(wrapper.find('.builds-container li').attributes('id')).toBe( + 'ci-badge-<img src=x onerror=alert(document.domain)>', + ); + }); + }); + + describe('with action', () => { + it('renders action button', () => { + wrapper = shallowMount(stageColumnComponent, { + propsData: { + groups: [ + { + id: 4259, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + label: 'success', + tooltip: '<img src=x onerror=alert(document.domain)>', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + action: { + icon: 'play', + title: 'Play all', + path: 'action', + }, + }, + }); + + expect(wrapper.find('.js-stage-action').exists()).toBe(true); + }); + }); + + describe('without action', () => { + it('does not render action button', () => { + wrapper = shallowMount(stageColumnComponent, { + propsData: { + groups: [ + { + id: 4259, + name: '<img src=x onerror=alert(document.domain)>', + status: { + icon: 'status_success', + label: 'success', + tooltip: '<img src=x onerror=alert(document.domain)>', + }, + }, + ], + title: 'test', + hasTriggeredBy: false, + }, + }); + + expect(wrapper.find('.js-stage-action').exists()).toBe(false); + }); + }); +}); diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js index 3e46a29f776..f69b849521d 100644 --- a/spec/frontend/registry/explorer/pages/list_spec.js +++ b/spec/frontend/registry/explorer/pages/list_spec.js @@ -1,11 +1,12 @@ import VueRouter from 'vue-router'; import { shallowMount, createLocalVue } from '@vue/test-utils'; -import { GlPagination, GlSkeletonLoader, GlSprintf } from '@gitlab/ui'; +import { GlPagination, GlSkeletonLoader, GlSprintf, GlAlert } from '@gitlab/ui'; import Tracking from '~/tracking'; import component from '~/registry/explorer/pages/list.vue'; import QuickstartDropdown from '~/registry/explorer/components/quickstart_dropdown.vue'; import GroupEmptyState from '~/registry/explorer/components/group_empty_state.vue'; import ProjectEmptyState from '~/registry/explorer/components/project_empty_state.vue'; +import ProjectPolicyAlert from '~/registry/explorer/components/project_policy_alert.vue'; import store from '~/registry/explorer/stores/'; import { SET_MAIN_LOADING } from '~/registry/explorer/stores/mutation_types/'; import { @@ -35,6 +36,8 @@ describe('List Page', () => { const findQuickStartDropdown = () => wrapper.find(QuickstartDropdown); const findProjectEmptyState = () => wrapper.find(ProjectEmptyState); const findGroupEmptyState = () => wrapper.find(GroupEmptyState); + const findProjectPolicyAlert = () => wrapper.find(ProjectPolicyAlert); + const findDeleteAlert = () => wrapper.find(GlAlert); beforeEach(() => { wrapper = shallowMount(component, { @@ -57,6 +60,18 @@ describe('List Page', () => { wrapper.destroy(); }); + describe('Expiration policy notification', () => { + it('shows up on project page', () => { + expect(findProjectPolicyAlert().exists()).toBe(true); + }); + it('does show up on group page', () => { + store.dispatch('setInitialState', { isGroupPage: true }); + return wrapper.vm.$nextTick().then(() => { + expect(findProjectPolicyAlert().exists()).toBe(false); + }); + }); + }); + describe('connection error', () => { const config = { characterError: true, @@ -179,32 +194,38 @@ describe('List Page', () => { it('should call deleteItem when confirming deletion', () => { dispatchSpy.mockResolvedValue(); - const itemToDelete = wrapper.vm.images[0]; - wrapper.setData({ itemToDelete }); + findDeleteBtn().vm.$emit('click'); + expect(wrapper.vm.itemToDelete).not.toEqual({}); findDeleteModal().vm.$emit('ok'); expect(store.dispatch).toHaveBeenCalledWith( 'requestDeleteImage', - itemToDelete.destroy_path, + wrapper.vm.itemToDelete, ); }); - it('should show a success toast when delete request is successful', () => { + it('should show a success alert when delete request is successful', () => { dispatchSpy.mockResolvedValue(); + findDeleteBtn().vm.$emit('click'); + expect(wrapper.vm.itemToDelete).not.toEqual({}); return wrapper.vm.handleDeleteImage().then(() => { - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_IMAGE_SUCCESS_MESSAGE, { - type: 'success', - }); - expect(wrapper.vm.itemToDelete).toEqual({}); + const alert = findDeleteAlert(); + expect(alert.exists()).toBe(true); + expect(alert.text().replace(/\s\s+/gm, ' ')).toBe( + DELETE_IMAGE_SUCCESS_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path), + ); }); }); - it('should show a error toast when delete request fails', () => { + it('should show an error alert when delete request fails', () => { dispatchSpy.mockRejectedValue(); + findDeleteBtn().vm.$emit('click'); + expect(wrapper.vm.itemToDelete).not.toEqual({}); return wrapper.vm.handleDeleteImage().then(() => { - expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(DELETE_IMAGE_ERROR_MESSAGE, { - type: 'error', - }); - expect(wrapper.vm.itemToDelete).toEqual({}); + const alert = findDeleteAlert(); + expect(alert.exists()).toBe(true); + expect(alert.text().replace(/\s\s+/gm, ' ')).toBe( + DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path), + ); }); }); }); diff --git a/spec/frontend/registry/explorer/stores/actions_spec.js b/spec/frontend/registry/explorer/stores/actions_spec.js index b39c79dd1ab..58f61a0e8c2 100644 --- a/spec/frontend/registry/explorer/stores/actions_spec.js +++ b/spec/frontend/registry/explorer/stores/actions_spec.js @@ -279,39 +279,32 @@ describe('Actions RegistryExplorer Store', () => { }); describe('request delete single image', () => { - const deletePath = 'delete/path'; + const image = { + destroy_path: 'delete/path', + }; + it('successfully performs the delete request', done => { - mock.onDelete(deletePath).replyOnce(200); + mock.onDelete(image.destroy_path).replyOnce(200); testAction( actions.requestDeleteImage, - deletePath, - { - pagination: {}, - }, + image, + {}, [ { type: types.SET_MAIN_LOADING, payload: true }, + { type: types.UPDATE_IMAGE, payload: { ...image, deleting: true } }, { type: types.SET_MAIN_LOADING, payload: false }, ], - [ - { - type: 'setShowGarbageCollectionTip', - payload: true, - }, - { - type: 'requestImagesList', - payload: { pagination: {} }, - }, - ], + [], done, ); }); it('should turn off loading on error', done => { - mock.onDelete(deletePath).replyOnce(400); + mock.onDelete(image.destroy_path).replyOnce(400); testAction( actions.requestDeleteImage, - deletePath, + image, {}, [ { type: types.SET_MAIN_LOADING, payload: true }, diff --git a/spec/frontend/registry/explorer/stores/mutations_spec.js b/spec/frontend/registry/explorer/stores/mutations_spec.js index 029fd23f7ce..43b2ba84218 100644 --- a/spec/frontend/registry/explorer/stores/mutations_spec.js +++ b/spec/frontend/registry/explorer/stores/mutations_spec.js @@ -28,14 +28,32 @@ describe('Mutations Registry Explorer Store', () => { describe('SET_IMAGES_LIST_SUCCESS', () => { it('should set the images list', () => { - const images = [1, 2, 3]; - const expectedState = { ...mockState, images }; + const images = [{ name: 'foo' }, { name: 'bar' }]; + const defaultStatus = { deleting: false, failedDelete: false }; + const expectedState = { + ...mockState, + images: [{ name: 'foo', ...defaultStatus }, { name: 'bar', ...defaultStatus }], + }; mutations[types.SET_IMAGES_LIST_SUCCESS](mockState, images); expect(mockState).toEqual(expectedState); }); }); + describe('UPDATE_IMAGE', () => { + it('should update an image', () => { + mockState.images = [{ id: 1, name: 'foo' }, { id: 2, name: 'bar' }]; + const payload = { id: 1, name: 'baz' }; + const expectedState = { + ...mockState, + images: [payload, { id: 2, name: 'bar' }], + }; + mutations[types.UPDATE_IMAGE](mockState, payload); + + expect(mockState).toEqual(expectedState); + }); + }); + describe('SET_TAGS_LIST_SUCCESS', () => { it('should set the tags list', () => { const tags = [1, 2, 3]; diff --git a/spec/frontend/repository/router_spec.js b/spec/frontend/repository/router_spec.js index 6944b23558a..8f3ac53c37a 100644 --- a/spec/frontend/repository/router_spec.js +++ b/spec/frontend/repository/router_spec.js @@ -4,15 +4,14 @@ import createRouter from '~/repository/router'; describe('Repository router spec', () => { it.each` - path | branch | component | componentName - ${'/'} | ${'master'} | ${IndexPage} | ${'IndexPage'} - ${'/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/master'} | ${'master'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/master/app/assets'} | ${'master'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/feature/test-%23/app/assets'} | ${'feature/test-#'} | ${TreePage} | ${'TreePage'} - ${'/-/tree/123/app/assets'} | ${'master'} | ${null} | ${'null'} - `('sets component as $componentName for path "$path"', ({ path, component, branch }) => { - const router = createRouter('', branch); + path | component | componentName + ${'/'} | ${IndexPage} | ${'IndexPage'} + ${'/tree/master'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/master'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/master/app/assets'} | ${TreePage} | ${'TreePage'} + ${'/-/tree/123/app/assets'} | ${null} | ${'null'} + `('sets component as $componentName for path "$path"', ({ path, component }) => { + const router = createRouter('', 'master'); const componentsForRoute = router.getMatchedComponents(path); diff --git a/spec/frontend/sidebar/sidebar_assignees_spec.js b/spec/frontend/sidebar/sidebar_assignees_spec.js new file mode 100644 index 00000000000..c1876066a21 --- /dev/null +++ b/spec/frontend/sidebar/sidebar_assignees_spec.js @@ -0,0 +1,74 @@ +import { shallowMount } from '@vue/test-utils'; +import AxiosMockAdapter from 'axios-mock-adapter'; +import axios from 'axios'; +import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.vue'; +import Assigness from '~/sidebar/components/assignees/assignees.vue'; +import SidebarMediator from '~/sidebar/sidebar_mediator'; +import SidebarService from '~/sidebar/services/sidebar_service'; +import SidebarStore from '~/sidebar/stores/sidebar_store'; +import Mock from './mock_data'; + +describe('sidebar assignees', () => { + let wrapper; + let mediator; + let axiosMock; + + const createComponent = () => { + wrapper = shallowMount(SidebarAssignees, { + propsData: { + mediator, + field: '', + }, + // Attaching to document is required because this component emits something from the parent element :/ + attachToDocument: true, + }); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + mediator = new SidebarMediator(Mock.mediator); + + jest.spyOn(mediator, 'saveAssignees'); + jest.spyOn(mediator, 'assignYourself'); + + createComponent(); + }); + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + + SidebarService.singleton = null; + SidebarStore.singleton = null; + SidebarMediator.singleton = null; + axiosMock.restore(); + }); + + it('calls the mediator when saves the assignees', () => { + expect(mediator.saveAssignees).not.toHaveBeenCalled(); + + wrapper.vm.saveAssignees(); + + expect(mediator.saveAssignees).toHaveBeenCalled(); + }); + + it('calls the mediator when "assignSelf" method is called', () => { + expect(mediator.assignYourself).not.toHaveBeenCalled(); + expect(mediator.store.assignees.length).toBe(0); + + wrapper.vm.assignSelf(); + + expect(mediator.assignYourself).toHaveBeenCalled(); + expect(mediator.store.assignees.length).toBe(1); + }); + + it('hides assignees until fetched', () => { + expect(wrapper.find(Assigness).exists()).toBe(false); + + wrapper.vm.store.isFetching.assignees = false; + + return wrapper.vm.$nextTick(() => { + expect(wrapper.find(Assigness).exists()).toBe(true); + }); + }); +}); diff --git a/spec/frontend/snippet/snippet_edit_spec.js b/spec/frontend/snippet/snippet_edit_spec.js new file mode 100644 index 00000000000..cfe5062c86b --- /dev/null +++ b/spec/frontend/snippet/snippet_edit_spec.js @@ -0,0 +1,45 @@ +import '~/snippet/snippet_edit'; +import { SnippetEditInit } from '~/snippets'; +import initSnippet from '~/snippet/snippet_bundle'; + +import { triggerDOMEvent } from 'jest/helpers/dom_events_helper'; + +jest.mock('~/snippet/snippet_bundle'); +jest.mock('~/snippets'); + +describe('Snippet edit form initialization', () => { + const setFF = flag => { + gon.features = { snippetsEditVue: flag }; + }; + let features; + + beforeEach(() => { + features = gon.features; + setFixtures('<div class="snippet-form"></div>'); + }); + + afterEach(() => { + gon.features = features; + }); + + it.each` + name | flag | isVue + ${'Regular'} | ${false} | ${false} + ${'Vue'} | ${true} | ${true} + `('correctly initializes $name Snippet Edit form', ({ flag, isVue }) => { + initSnippet.mockClear(); + SnippetEditInit.mockClear(); + + setFF(flag); + + triggerDOMEvent('DOMContentLoaded'); + + if (isVue) { + expect(initSnippet).not.toHaveBeenCalled(); + expect(SnippetEditInit).toHaveBeenCalled(); + } else { + expect(initSnippet).toHaveBeenCalled(); + expect(SnippetEditInit).not.toHaveBeenCalled(); + } + }); +}); diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap index 3c3f9764f64..334ceaa064f 100644 --- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap +++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap @@ -39,7 +39,6 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] = qa-description-textarea" data-supports-quick-actions="false" dir="auto" - id="snippet-description" placeholder="Write a comment or drag your files here…" /> </markdown-field-stub> diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js new file mode 100644 index 00000000000..21a4ccf5a74 --- /dev/null +++ b/spec/frontend/snippets/components/edit_spec.js @@ -0,0 +1,279 @@ +import { shallowMount } from '@vue/test-utils'; +import axios from '~/lib/utils/axios_utils'; + +import { GlLoadingIcon } from '@gitlab/ui'; +import { joinPaths, redirectTo } from '~/lib/utils/url_utility'; + +import SnippetEditApp from '~/snippets/components/edit.vue'; +import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue'; +import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue'; +import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue'; +import TitleField from '~/vue_shared/components/form/title.vue'; +import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue'; + +import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql'; +import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql'; + +import AxiosMockAdapter from 'axios-mock-adapter'; +import waitForPromises from 'helpers/wait_for_promises'; +import { ApolloMutation } from 'vue-apollo'; + +jest.mock('~/lib/utils/url_utility', () => ({ + getBaseURL: jest.fn().mockReturnValue('foo/'), + redirectTo: jest.fn().mockName('redirectTo'), + joinPaths: jest + .fn() + .mockName('joinPaths') + .mockReturnValue('contentApiURL'), +})); + +let flashSpy; + +const contentMock = 'Foo Bar'; +const rawPathMock = '/foo/bar'; +const rawProjectPathMock = '/project/path'; +const newlyEditedSnippetUrl = 'http://foo.bar'; +const apiError = { message: 'Ufff' }; + +const defaultProps = { + snippetGid: 'gid://gitlab/PersonalSnippet/42', + markdownPreviewPath: 'http://preview.foo.bar', + markdownDocsPath: 'http://docs.foo.bar', +}; + +describe('Snippet Edit app', () => { + let wrapper; + let axiosMock; + + const resolveMutate = jest.fn().mockResolvedValue({ + data: { + updateSnippet: { + errors: [], + snippet: { + webUrl: newlyEditedSnippetUrl, + }, + }, + }, + }); + + const rejectMutation = jest.fn().mockRejectedValue(apiError); + + const mutationTypes = { + RESOLVE: resolveMutate, + REJECT: rejectMutation, + }; + + function createComponent({ + props = defaultProps, + data = {}, + loading = false, + mutationRes = mutationTypes.RESOLVE, + } = {}) { + const $apollo = { + queries: { + snippet: { + loading, + }, + }, + mutate: mutationRes, + }; + + wrapper = shallowMount(SnippetEditApp, { + mocks: { $apollo }, + stubs: { + FormFooterActions, + ApolloMutation, + }, + propsData: { + ...props, + }, + data() { + return data; + }, + }); + + flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure'); + } + + afterEach(() => { + wrapper.destroy(); + }); + + const findSubmitButton = () => wrapper.find('[type=submit]'); + + describe('rendering', () => { + it('renders loader while the query is in flight', () => { + createComponent({ loading: true }); + expect(wrapper.find(GlLoadingIcon).exists()).toBe(true); + }); + + it('renders all required components', () => { + createComponent(); + + expect(wrapper.contains(TitleField)).toBe(true); + expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true); + expect(wrapper.contains(SnippetBlobEdit)).toBe(true); + expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true); + expect(wrapper.contains(FormFooterActions)).toBe(true); + }); + + it('does not fail if there is no snippet yet (new snippet creation)', () => { + const snippetGid = ''; + createComponent({ + props: { + ...defaultProps, + snippetGid, + }, + }); + + expect(wrapper.props('snippetGid')).toBe(snippetGid); + }); + + it.each` + title | content | expectation + ${''} | ${''} | ${true} + ${'foo'} | ${''} | ${true} + ${''} | ${'foo'} | ${true} + ${'foo'} | ${'bar'} | ${false} + `( + 'disables submit button unless both title and content are present', + ({ title, content, expectation }) => { + createComponent({ + data: { + snippet: { title }, + content, + }, + }); + const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled')); + expect(isBtnDisabled).toBe(expectation); + }, + ); + }); + + describe('functionality', () => { + describe('handling of the data from GraphQL response', () => { + const snippet = { + blob: { + rawPath: rawPathMock, + }, + }; + const getResSchema = newSnippet => { + return { + data: { + snippets: { + edges: newSnippet ? [] : [snippet], + }, + }, + }; + }; + + const bootstrapForExistingSnippet = resp => { + createComponent({ + data: { + snippet, + }, + }); + + if (resp === 500) { + axiosMock.onGet('contentApiURL').reply(500); + } else { + axiosMock.onGet('contentApiURL').reply(200, contentMock); + } + wrapper.vm.onSnippetFetch(getResSchema()); + }; + + const bootstrapForNewSnippet = () => { + createComponent(); + wrapper.vm.onSnippetFetch(getResSchema(true)); + }; + + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + + it('fetches blob content with the additional query', () => { + bootstrapForExistingSnippet(); + + return waitForPromises().then(() => { + expect(joinPaths).toHaveBeenCalledWith('foo/', rawPathMock); + expect(wrapper.vm.newSnippet).toBe(false); + expect(wrapper.vm.content).toBe(contentMock); + }); + }); + + it('flashes the error message if fetching content fails', () => { + bootstrapForExistingSnippet(500); + + return waitForPromises().then(() => { + expect(flashSpy).toHaveBeenCalled(); + expect(wrapper.vm.content).toBe(''); + }); + }); + + it('does not fetch content for new snippet', () => { + bootstrapForNewSnippet(); + + return waitForPromises().then(() => { + // we keep using waitForPromises to make sure we do not run failed test + expect(wrapper.vm.newSnippet).toBe(true); + expect(wrapper.vm.content).toBe(''); + expect(joinPaths).not.toHaveBeenCalled(); + expect(wrapper.vm.snippet).toEqual(wrapper.vm.$options.newSnippetSchema); + }); + }); + }); + + describe('form submission handling', () => { + it.each` + newSnippet | projectPath | mutation | mutationName + ${true} | ${rawProjectPathMock} | ${CreateSnippetMutation} | ${'CreateSnippetMutation with projectPath'} + ${true} | ${''} | ${CreateSnippetMutation} | ${'CreateSnippetMutation without projectPath'} + ${false} | ${rawProjectPathMock} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation with projectPath'} + ${false} | ${''} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation without projectPath'} + `('should submit $mutationName correctly', ({ newSnippet, projectPath, mutation }) => { + createComponent({ + data: { + newSnippet, + }, + props: { + ...defaultProps, + projectPath, + }, + }); + + const mutationPayload = { + mutation, + variables: { + input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object), + }, + }; + + wrapper.vm.handleFormSubmit(); + expect(resolveMutate).toHaveBeenCalledWith(mutationPayload); + }); + + it('redirects to snippet view on successful mutation', () => { + createComponent(); + wrapper.vm.handleFormSubmit(); + return waitForPromises().then(() => { + expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl); + }); + }); + + it('flashes an error if mutation failed', () => { + createComponent({ + mutationRes: mutationTypes.REJECT, + }); + wrapper.vm.handleFormSubmit(); + return waitForPromises().then(() => { + expect(redirectTo).not.toHaveBeenCalled(); + expect(flashSpy).toHaveBeenCalledWith(apiError); + }); + }); + }); + }); +}); diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js index 1b67c08e5a4..16a66c70d6a 100644 --- a/spec/frontend/snippets/components/snippet_header_spec.js +++ b/spec/frontend/snippets/components/snippet_header_spec.js @@ -1,7 +1,7 @@ import SnippetHeader from '~/snippets/components/snippet_header.vue'; import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql'; import { ApolloMutation } from 'vue-apollo'; -import { GlNewButton, GlModal } from '@gitlab/ui'; +import { GlButton, GlModal } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; describe('Snippet header component', () => { @@ -89,7 +89,7 @@ describe('Snippet header component', () => { updateSnippet: false, }, }); - expect(wrapper.findAll(GlNewButton).length).toEqual(0); + expect(wrapper.findAll(GlButton).length).toEqual(0); createComponent({ permissions: { @@ -97,7 +97,7 @@ describe('Snippet header component', () => { updateSnippet: false, }, }); - expect(wrapper.findAll(GlNewButton).length).toEqual(1); + expect(wrapper.findAll(GlButton).length).toEqual(1); createComponent({ permissions: { @@ -105,7 +105,7 @@ describe('Snippet header component', () => { updateSnippet: true, }, }); - expect(wrapper.findAll(GlNewButton).length).toEqual(2); + expect(wrapper.findAll(GlButton).length).toEqual(2); createComponent({ permissions: { @@ -117,7 +117,7 @@ describe('Snippet header component', () => { canCreateSnippet: true, }); return wrapper.vm.$nextTick().then(() => { - expect(wrapper.findAll(GlNewButton).length).toEqual(3); + expect(wrapper.findAll(GlButton).length).toEqual(3); }); }); diff --git a/spec/frontend/static_site_editor/components/invalid_content_message_spec.js b/spec/frontend/static_site_editor/components/invalid_content_message_spec.js new file mode 100644 index 00000000000..7e699e9451c --- /dev/null +++ b/spec/frontend/static_site_editor/components/invalid_content_message_spec.js @@ -0,0 +1,23 @@ +import { shallowMount } from '@vue/test-utils'; + +import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; + +describe('~/static_site_editor/components/invalid_content_message.vue', () => { + let wrapper; + const findDocumentationButton = () => wrapper.find({ ref: 'documentationButton' }); + const documentationUrl = + 'https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman'; + + beforeEach(() => { + wrapper = shallowMount(InvalidContentMessage); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders the configuration button link', () => { + expect(findDocumentationButton().exists()).toBe(true); + expect(findDocumentationButton().attributes('href')).toBe(documentationUrl); + }); +}); diff --git a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js index f00fc38430f..82eb12d4c4d 100644 --- a/spec/frontend/static_site_editor/components/publish_toolbar_spec.js +++ b/spec/frontend/static_site_editor/components/publish_toolbar_spec.js @@ -1,5 +1,5 @@ import { shallowMount } from '@vue/test-utils'; -import { GlNewButton, GlLoadingIcon } from '@gitlab/ui'; +import { GlButton, GlLoadingIcon } from '@gitlab/ui'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; @@ -18,7 +18,7 @@ describe('Static Site Editor Toolbar', () => { }; const findReturnUrlLink = () => wrapper.find({ ref: 'returnUrlLink' }); - const findSaveChangesButton = () => wrapper.find(GlNewButton); + const findSaveChangesButton = () => wrapper.find(GlButton); const findLoadingIndicator = () => wrapper.find(GlLoadingIcon); beforeEach(() => { diff --git a/spec/frontend/static_site_editor/components/saved_changes_message_spec.js b/spec/frontend/static_site_editor/components/saved_changes_message_spec.js index 76ac7de5c32..659e9be59d2 100644 --- a/spec/frontend/static_site_editor/components/saved_changes_message_spec.js +++ b/spec/frontend/static_site_editor/components/saved_changes_message_spec.js @@ -1,22 +1,17 @@ import { shallowMount } from '@vue/test-utils'; + import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue'; +import { returnUrl, savedContentMeta } from '../mock_data'; + describe('~/static_site_editor/components/saved_changes_message.vue', () => { let wrapper; + const { branch, commit, mergeRequest } = savedContentMeta; const props = { - branch: { - label: '123-the-branch', - url: 'https://gitlab.com/gitlab-org/gitlab/-/tree/123-the-branch', - }, - commit: { - label: 'a123', - url: 'https://gitlab.com/gitlab-org/gitlab/-/commit/a123', - }, - mergeRequest: { - label: '123', - url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/123', - }, - returnUrl: 'https://www.the-static-site.com/post', + branch, + commit, + mergeRequest, + returnUrl, }; const findReturnToSiteButton = () => wrapper.find({ ref: 'returnToSiteButton' }); const findMergeRequestButton = () => wrapper.find({ ref: 'mergeRequestButton' }); @@ -51,11 +46,14 @@ describe('~/static_site_editor/components/saved_changes_message.vue', () => { ${'branch'} | ${findBranchLink} | ${props.branch} ${'commit'} | ${findCommitLink} | ${props.commit} ${'merge request'} | ${findMergeRequestLink} | ${props.mergeRequest} - `('renders $desc link', ({ findEl, prop }) => { + `('renders $desc link', ({ desc, findEl, prop }) => { const el = findEl(); expect(el.exists()).toBe(true); - expect(el.attributes('href')).toBe(prop.url); expect(el.text()).toBe(prop.label); + + if (desc !== 'branch') { + expect(el.attributes('href')).toBe(prop.url); + } }); }); diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js index d427df9bd4b..5d4e3758557 100644 --- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js +++ b/spec/frontend/static_site_editor/components/static_site_editor_spec.js @@ -1,6 +1,5 @@ import Vuex from 'vuex'; import { shallowMount, createLocalVue } from '@vue/test-utils'; - import { GlSkeletonLoader } from '@gitlab/ui'; import createState from '~/static_site_editor/store/state'; @@ -8,9 +7,18 @@ import createState from '~/static_site_editor/store/state'; import StaticSiteEditor from '~/static_site_editor/components/static_site_editor.vue'; import EditArea from '~/static_site_editor/components/edit_area.vue'; import EditHeader from '~/static_site_editor/components/edit_header.vue'; +import InvalidContentMessage from '~/static_site_editor/components/invalid_content_message.vue'; import PublishToolbar from '~/static_site_editor/components/publish_toolbar.vue'; +import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue'; +import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue'; -import { sourceContent, sourceContentTitle } from '../mock_data'; +import { + returnUrl, + sourceContent, + sourceContentTitle, + savedContentMeta, + submitChangesError, +} from '../mock_data'; const localVue = createLocalVue(); @@ -22,14 +30,19 @@ describe('StaticSiteEditor', () => { let loadContentActionMock; let setContentActionMock; let submitChangesActionMock; + let dismissSubmitChangesErrorActionMock; const buildStore = ({ initialState, getters } = {}) => { loadContentActionMock = jest.fn(); setContentActionMock = jest.fn(); submitChangesActionMock = jest.fn(); + dismissSubmitChangesErrorActionMock = jest.fn(); store = new Vuex.Store({ - state: createState(initialState), + state: createState({ + isSupportedContent: true, + ...initialState, + }), getters: { contentChanged: () => false, ...getters, @@ -38,6 +51,7 @@ describe('StaticSiteEditor', () => { loadContent: loadContentActionMock, setContent: setContentActionMock, submitChanges: submitChangesActionMock, + dismissSubmitChangesError: dismissSubmitChangesErrorActionMock, }, }); }; @@ -62,8 +76,11 @@ describe('StaticSiteEditor', () => { const findEditArea = () => wrapper.find(EditArea); const findEditHeader = () => wrapper.find(EditHeader); + const findInvalidContentMessage = () => wrapper.find(InvalidContentMessage); const findPublishToolbar = () => wrapper.find(PublishToolbar); const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader); + const findSubmitChangesError = () => wrapper.find(SubmitChangesError); + const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage); beforeEach(() => { buildStore(); @@ -74,6 +91,17 @@ describe('StaticSiteEditor', () => { wrapper.destroy(); }); + it('renders the saved changes message when changes are submitted successfully', () => { + buildStore({ initialState: { returnUrl, savedContentMeta } }); + buildWrapper(); + + expect(findSavedChangesMessage().exists()).toBe(true); + expect(findSavedChangesMessage().props()).toEqual({ + returnUrl, + ...savedContentMeta, + }); + }); + describe('when content is not loaded', () => { it('does not render edit area', () => { expect(findEditArea().exists()).toBe(false); @@ -86,6 +114,10 @@ describe('StaticSiteEditor', () => { it('does not render toolbar', () => { expect(findPublishToolbar().exists()).toBe(false); }); + + it('does not render saved changes message', () => { + expect(findSavedChangesMessage().exists()).toBe(false); + }); }); describe('when content is loaded', () => { @@ -140,6 +172,13 @@ describe('StaticSiteEditor', () => { expect(findSkeletonLoader().exists()).toBe(true); }); + it('does not display submit changes error when an error does not exist', () => { + buildContentLoadedStore(); + buildWrapper(); + + expect(findSubmitChangesError().exists()).toBe(false); + }); + it('sets toolbar as saving when saving changes', () => { buildContentLoadedStore({ initialState: { @@ -151,6 +190,40 @@ describe('StaticSiteEditor', () => { expect(findPublishToolbar().props('savingChanges')).toBe(true); }); + it('displays invalid content message when content is not supported', () => { + buildStore({ initialState: { isSupportedContent: false } }); + buildWrapper(); + + expect(findInvalidContentMessage().exists()).toBe(true); + }); + + describe('when submitting changes fail', () => { + beforeEach(() => { + buildContentLoadedStore({ + initialState: { + submitChangesError, + }, + }); + buildWrapper(); + }); + + it('displays submit changes error message', () => { + expect(findSubmitChangesError().exists()).toBe(true); + }); + + it('dispatches submitChanges action when error message emits retry event', () => { + findSubmitChangesError().vm.$emit('retry'); + + expect(submitChangesActionMock).toHaveBeenCalled(); + }); + + it('dispatches dismissSubmitChangesError action when error message emits dismiss event', () => { + findSubmitChangesError().vm.$emit('dismiss'); + + expect(dismissSubmitChangesErrorActionMock).toHaveBeenCalled(); + }); + }); + it('dispatches load content action', () => { expect(loadContentActionMock).toHaveBeenCalled(); }); diff --git a/spec/frontend/static_site_editor/components/submit_changes_error_spec.js b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js new file mode 100644 index 00000000000..7af3014b338 --- /dev/null +++ b/spec/frontend/static_site_editor/components/submit_changes_error_spec.js @@ -0,0 +1,48 @@ +import { shallowMount } from '@vue/test-utils'; +import { GlButton, GlAlert } from '@gitlab/ui'; + +import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue'; + +import { submitChangesError as error } from '../mock_data'; + +describe('Submit Changes Error', () => { + let wrapper; + + const buildWrapper = (propsData = {}) => { + wrapper = shallowMount(SubmitChangesError, { + propsData: { + ...propsData, + }, + stubs: { + GlAlert, + }, + }); + }; + + const findRetryButton = () => wrapper.find(GlButton); + const findAlert = () => wrapper.find(GlAlert); + + beforeEach(() => { + buildWrapper({ error }); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('renders error message', () => { + expect(findAlert().text()).toContain(error); + }); + + it('emits dismiss event when alert emits dismiss event', () => { + findAlert().vm.$emit('dismiss'); + + expect(wrapper.emitted('dismiss')).toHaveLength(1); + }); + + it('emits retry event when retry button is clicked', () => { + findRetryButton().vm.$emit('click'); + + expect(wrapper.emitted('retry')).toHaveLength(1); + }); +}); diff --git a/spec/frontend/static_site_editor/mock_data.js b/spec/frontend/static_site_editor/mock_data.js index 345ae0ce6f6..962047e6dd2 100644 --- a/spec/frontend/static_site_editor/mock_data.js +++ b/spec/frontend/static_site_editor/mock_data.js @@ -21,10 +21,10 @@ export const sourcePath = 'foobar.md.html'; export const savedContentMeta = { branch: { label: 'foobar', - url: 'foobar/-/tree/foorbar', + url: 'foobar/-/tree/foobar', }, commit: { - label: 'c1461b08 ', + label: 'c1461b08', url: 'foobar/-/c1461b08', }, mergeRequest: { diff --git a/spec/frontend/static_site_editor/store/actions_spec.js b/spec/frontend/static_site_editor/store/actions_spec.js index a9c039517b7..6b0b77f59b7 100644 --- a/spec/frontend/static_site_editor/store/actions_spec.js +++ b/spec/frontend/static_site_editor/store/actions_spec.js @@ -124,24 +124,29 @@ describe('Static Site Editor Store actions', () => { }); describe('on error', () => { + const error = new Error(submitChangesError); const expectedMutations = [ { type: mutationTypes.SUBMIT_CHANGES }, - { type: mutationTypes.SUBMIT_CHANGES_ERROR }, + { type: mutationTypes.SUBMIT_CHANGES_ERROR, payload: error.message }, ]; beforeEach(() => { - submitContentChanges.mockRejectedValueOnce(new Error(submitChangesError)); + submitContentChanges.mockRejectedValueOnce(error); }); it('dispatches receiveContentError', () => { testAction(actions.submitChanges, null, state, expectedMutations); }); + }); + }); - it('displays flash communicating error', () => { - return testAction(actions.submitChanges, null, state, expectedMutations).then(() => { - expect(createFlash).toHaveBeenCalledWith(submitChangesError); - }); - }); + describe('dismissSubmitChangesError', () => { + it('commits dismissSubmitChangesError', () => { + testAction(actions.dismissSubmitChangesError, null, state, [ + { + type: mutationTypes.DISMISS_SUBMIT_CHANGES_ERROR, + }, + ]); }); }); }); diff --git a/spec/frontend/static_site_editor/store/mutations_spec.js b/spec/frontend/static_site_editor/store/mutations_spec.js index 0b213c11a04..2441f317d90 100644 --- a/spec/frontend/static_site_editor/store/mutations_spec.js +++ b/spec/frontend/static_site_editor/store/mutations_spec.js @@ -5,6 +5,7 @@ import { sourceContentTitle as title, sourceContent as content, savedContentMeta, + submitChangesError, } from '../mock_data'; describe('Static Site Editor Store mutations', () => { @@ -16,19 +17,21 @@ describe('Static Site Editor Store mutations', () => { }); it.each` - mutation | stateProperty | payload | expectedValue - ${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content} - ${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content} - ${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false} - ${types.SET_CONTENT} | ${'content'} | ${content} | ${content} - ${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true} - ${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta} - ${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${false} - ${types.SUBMIT_CHANGES_ERROR} | ${'isSavingChanges'} | ${undefined} | ${false} + mutation | stateProperty | payload | expectedValue + ${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content} + ${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content} + ${types.RECEIVE_CONTENT_ERROR} | ${'isLoadingContent'} | ${undefined} | ${false} + ${types.SET_CONTENT} | ${'content'} | ${content} | ${content} + ${types.SUBMIT_CHANGES} | ${'isSavingChanges'} | ${undefined} | ${true} + ${types.SUBMIT_CHANGES_SUCCESS} | ${'savedContentMeta'} | ${savedContentMeta} | ${savedContentMeta} + ${types.SUBMIT_CHANGES_SUCCESS} | ${'isSavingChanges'} | ${savedContentMeta} | ${false} + ${types.SUBMIT_CHANGES_ERROR} | ${'isSavingChanges'} | ${undefined} | ${false} + ${types.SUBMIT_CHANGES_ERROR} | ${'submitChangesError'} | ${submitChangesError} | ${submitChangesError} + ${types.DISMISS_SUBMIT_CHANGES_ERROR} | ${'submitChangesError'} | ${undefined} | ${''} `( '$mutation sets $stateProperty to $expectedValue', ({ mutation, stateProperty, payload, expectedValue }) => { diff --git a/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap new file mode 100644 index 00000000000..df4b30f1cb8 --- /dev/null +++ b/spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap @@ -0,0 +1,287 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`vue_shared/components/awards_list default matches snapshot 1`] = ` +<div + class="awards js-awards-block" +> + <button + class="btn award-control" + data-boundary="viewport" + data-original-title="Ada, Leonardo, and Marie" + data-testid="award-button" + title="" + type="button" + > + <span + data-testid="award-html" + > + + + <gl-emoji + data-fallback-src="/assets/emoji/thumbsup-59ec2457ab33e8897261d01a495f6cf5c668d0004807dc541c3b1be5294b1e61.png" + data-name="thumbsup" + data-unicode-version="6.0" + title="thumbs up sign" + > + + 👍 + + </gl-emoji> + + + </span> + + <span + class="award-control-text js-counter" + > + 3 + </span> + </button> + <button + class="btn award-control active" + data-boundary="viewport" + data-original-title="You, Ada, and Marie" + data-testid="award-button" + title="" + type="button" + > + <span + data-testid="award-html" + > + + + <gl-emoji + data-fallback-src="/assets/emoji/thumbsdown-5954334e2dae5357312b3d629f10a496c728029e02216f8c8b887f9b51561c61.png" + data-name="thumbsdown" + data-unicode-version="6.0" + title="thumbs down sign" + > + + 👎 + + </gl-emoji> + + + </span> + + <span + class="award-control-text js-counter" + > + 3 + </span> + </button> + <button + class="btn award-control" + data-boundary="viewport" + data-original-title="Ada and Jane" + data-testid="award-button" + title="" + type="button" + > + <span + data-testid="award-html" + > + + + <gl-emoji + data-fallback-src="/assets/emoji/smile-14905c372d5bf7719bd727c9efae31a03291acec79801652a23710c6848c5d14.png" + data-name="smile" + data-unicode-version="6.0" + title="smiling face with open mouth and smiling eyes" + > + + 😄 + + </gl-emoji> + + + </span> + + <span + class="award-control-text js-counter" + > + 2 + </span> + </button> + <button + class="btn award-control active" + data-boundary="viewport" + data-original-title="You, Ada, Jane, and Leonardo" + data-testid="award-button" + title="" + type="button" + > + <span + data-testid="award-html" + > + + + <gl-emoji + data-fallback-src="/assets/emoji/ok_hand-d63002dce3cc3655b67b8765b7c28d370edba0e3758b2329b60e0e61c4d8e78d.png" + data-name="ok_hand" + data-unicode-version="6.0" + title="ok hand sign" + > + + 👌 + + </gl-emoji> + + + </span> + + <span + class="award-control-text js-counter" + > + 4 + </span> + </button> + <button + class="btn award-control active" + data-boundary="viewport" + data-original-title="You" + data-testid="award-button" + title="" + type="button" + > + <span + data-testid="award-html" + > + + + <gl-emoji + data-fallback-src="/assets/emoji/cactus-2c5c4c35f26c7046fdc002b337e0d939729b33a26980e675950f9934c91e40fd.png" + data-name="cactus" + data-unicode-version="6.0" + title="cactus" + > + + 🌵 + + </gl-emoji> + + + </span> + + <span + class="award-control-text js-counter" + > + 1 + </span> + </button> + <button + class="btn award-control" + data-boundary="viewport" + data-original-title="Marie" + data-testid="award-button" + title="" + type="button" + > + <span + data-testid="award-html" + > + + + <gl-emoji + data-fallback-src="/assets/emoji/a-bddbb39e8a1d35d42b7c08e7d47f63988cb4d8614b79f74e70b9c67c221896cc.png" + data-name="a" + data-unicode-version="6.0" + title="negative squared latin capital letter a" + > + + 🅰 + + </gl-emoji> + + + </span> + + <span + class="award-control-text js-counter" + > + 1 + </span> + </button> + <button + class="btn award-control active" + data-boundary="viewport" + data-original-title="You" + data-testid="award-button" + title="" + type="button" + > + <span + data-testid="award-html" + > + + + <gl-emoji + data-fallback-src="/assets/emoji/b-722f9db9442e7c0fc0d0ac0f5291fbf47c6a0ac4d8abd42e97957da705fb82bf.png" + data-name="b" + data-unicode-version="6.0" + title="negative squared latin capital letter b" + > + + 🅱 + + </gl-emoji> + + + </span> + + <span + class="award-control-text js-counter" + > + 1 + </span> + </button> + + <div + class="award-menu-holder" + > + <button + aria-label="Add reaction" + class="award-control btn js-add-award js-test-add-button-class" + data-boundary="viewport" + data-original-title="Add reaction" + title="" + type="button" + > + <span + class="award-control-icon award-control-icon-neutral" + > + <gl-icon-stub + aria-hidden="true" + name="slight-smile" + size="16" + /> + </span> + + <span + class="award-control-icon award-control-icon-positive" + > + <gl-icon-stub + aria-hidden="true" + name="smiley" + size="16" + /> + </span> + + <span + class="award-control-icon award-control-icon-super-positive" + > + <gl-icon-stub + aria-hidden="true" + name="smiley" + size="16" + /> + </span> + + <i + aria-hidden="true" + class="fa fa-spinner fa-spin award-control-icon award-control-icon-loading" + /> + </button> + </div> +</div> +`; diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap index d837c793784..4cd03a690e9 100644 --- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap +++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap @@ -42,7 +42,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` <b-input-group-append-stub tag="div" > - <gl-new-button-stub + <gl-button-stub category="tertiary" data-clipboard-text="ssh://foo.bar" icon="" @@ -55,7 +55,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` size="16" title="Copy URL" /> - </gl-new-button-stub> + </gl-button-stub> </b-input-group-append-stub> </b-input-group-stub> </div> @@ -92,7 +92,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` <b-input-group-append-stub tag="div" > - <gl-new-button-stub + <gl-button-stub category="tertiary" data-clipboard-text="http://foo.bar" icon="" @@ -105,7 +105,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = ` size="16" title="Copy URL" /> - </gl-new-button-stub> + </gl-button-stub> </b-input-group-append-stub> </b-input-group-stub> </div> diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js new file mode 100644 index 00000000000..bb3e60ab9e2 --- /dev/null +++ b/spec/frontend/vue_shared/components/awards_list_spec.js @@ -0,0 +1,213 @@ +import { shallowMount } from '@vue/test-utils'; +import AwardsList from '~/vue_shared/components/awards_list.vue'; + +const createUser = (id, name) => ({ id, name }); +const createAward = (name, user) => ({ name, user }); + +const USERS = { + root: createUser(1, 'Root'), + ada: createUser(2, 'Ada'), + marie: createUser(3, 'Marie'), + jane: createUser(4, 'Jane'), + leonardo: createUser(5, 'Leonardo'), +}; + +const EMOJI_SMILE = 'smile'; +const EMOJI_OK = 'ok_hand'; +const EMOJI_THUMBSUP = 'thumbsup'; +const EMOJI_THUMBSDOWN = 'thumbsdown'; +const EMOJI_A = 'a'; +const EMOJI_B = 'b'; +const EMOJI_CACTUS = 'cactus'; +const EMOJI_100 = '100'; + +const TEST_AWARDS = [ + createAward(EMOJI_SMILE, USERS.ada), + createAward(EMOJI_OK, USERS.ada), + createAward(EMOJI_THUMBSUP, USERS.ada), + createAward(EMOJI_THUMBSDOWN, USERS.ada), + createAward(EMOJI_SMILE, USERS.jane), + createAward(EMOJI_OK, USERS.jane), + createAward(EMOJI_OK, USERS.leonardo), + createAward(EMOJI_THUMBSUP, USERS.leonardo), + createAward(EMOJI_THUMBSUP, USERS.marie), + createAward(EMOJI_THUMBSDOWN, USERS.marie), + createAward(EMOJI_THUMBSDOWN, USERS.root), + createAward(EMOJI_OK, USERS.root), + // Test that emoji list preserves order of occurrence, not alphabetical order + createAward(EMOJI_CACTUS, USERS.root), + createAward(EMOJI_A, USERS.marie), + createAward(EMOJI_B, USERS.root), +]; +const TEST_ADD_BUTTON_CLASS = 'js-test-add-button-class'; + +describe('vue_shared/components/awards_list', () => { + let wrapper; + + afterEach(() => { + wrapper.destroy(); + wrapper = null; + }); + + const createComponent = (props = {}) => { + if (wrapper) { + throw new Error('There should only be one wrapper created per test'); + } + + wrapper = shallowMount(AwardsList, { propsData: props }); + }; + const matchingEmojiTag = name => expect.stringMatching(`gl-emoji data-name="${name}"`); + const findAwardButtons = () => wrapper.findAll('[data-testid="award-button"'); + const findAwardsData = () => + findAwardButtons().wrappers.map(x => { + return { + classes: x.classes(), + title: x.attributes('data-original-title'), + html: x.find('[data-testid="award-html"]').element.innerHTML, + count: Number(x.find('.js-counter').text()), + }; + }); + const findAddAwardButton = () => wrapper.find('.js-add-award'); + + describe('default', () => { + beforeEach(() => { + createComponent({ + awards: TEST_AWARDS, + canAwardEmoji: true, + currentUserId: USERS.root.id, + addButtonClass: TEST_ADD_BUTTON_CLASS, + }); + }); + + it('matches snapshot', () => { + expect(wrapper.element).toMatchSnapshot(); + }); + + it('shows awards in correct order', () => { + expect(findAwardsData()).toEqual([ + { + classes: ['btn', 'award-control'], + count: 3, + html: matchingEmojiTag(EMOJI_THUMBSUP), + title: 'Ada, Leonardo, and Marie', + }, + { + classes: ['btn', 'award-control', 'active'], + count: 3, + html: matchingEmojiTag(EMOJI_THUMBSDOWN), + title: 'You, Ada, and Marie', + }, + { + classes: ['btn', 'award-control'], + count: 2, + html: matchingEmojiTag(EMOJI_SMILE), + title: 'Ada and Jane', + }, + { + classes: ['btn', 'award-control', 'active'], + count: 4, + html: matchingEmojiTag(EMOJI_OK), + title: 'You, Ada, Jane, and Leonardo', + }, + { + classes: ['btn', 'award-control', 'active'], + count: 1, + html: matchingEmojiTag(EMOJI_CACTUS), + title: 'You', + }, + { + classes: ['btn', 'award-control'], + count: 1, + html: matchingEmojiTag(EMOJI_A), + title: 'Marie', + }, + { + classes: ['btn', 'award-control', 'active'], + count: 1, + html: matchingEmojiTag(EMOJI_B), + title: 'You', + }, + ]); + }); + + it('with award clicked, it emits award', () => { + expect(wrapper.emitted().award).toBeUndefined(); + + findAwardButtons() + .at(2) + .trigger('click'); + + expect(wrapper.emitted().award).toEqual([[EMOJI_SMILE]]); + }); + + it('shows add award button', () => { + const btn = findAddAwardButton(); + + expect(btn.exists()).toBe(true); + expect(btn.classes(TEST_ADD_BUTTON_CLASS)).toBe(true); + }); + }); + + describe('with numeric award', () => { + beforeEach(() => { + createComponent({ + awards: [createAward(EMOJI_100, USERS.ada)], + canAwardEmoji: true, + currentUserId: USERS.root.id, + }); + }); + + it('when clicked, it emits award as number', () => { + expect(wrapper.emitted().award).toBeUndefined(); + + findAwardButtons() + .at(0) + .trigger('click'); + + expect(wrapper.emitted().award).toEqual([[Number(EMOJI_100)]]); + }); + }); + + describe('with no awards', () => { + beforeEach(() => { + createComponent({ + awards: [], + canAwardEmoji: true, + }); + }); + + it('has no award buttons', () => { + expect(findAwardButtons().length).toBe(0); + }); + }); + + describe('when cannot award emoji', () => { + beforeEach(() => { + createComponent({ + awards: [createAward(EMOJI_CACTUS, USERS.root.id)], + canAwardEmoji: false, + currentUserId: USERS.marie.id, + }); + }); + + it('does not have add button', () => { + expect(findAddAwardButton().exists()).toBe(false); + }); + }); + + describe('with no user', () => { + beforeEach(() => { + createComponent({ + awards: TEST_AWARDS, + canAwardEmoji: false, + }); + }); + + it('disables award buttons', () => { + const buttons = findAwardButtons(); + + expect(buttons.length).toBe(7); + expect(buttons.wrappers.every(x => x.classes('disabled'))).toBe(true); + }); + }); +}); diff --git a/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap b/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap index 980e9b517db..e5035614196 100644 --- a/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap +++ b/spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap @@ -5,8 +5,6 @@ exports[`Title edit field matches the snapshot 1`] = ` label="Title" label-for="title-field-edit" > - <gl-form-input-stub - id="title-field-edit" - /> + <gl-form-input-stub /> </gl-form-group-stub> `; diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js index a2e2d2447d5..2c7fce714f0 100644 --- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js +++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js @@ -10,8 +10,7 @@ const DEFAULT_PROPS = { name: 'Administrator', location: 'Vienna', bio: null, - organization: null, - jobTitle: null, + workInformation: null, status: null, }, }; @@ -59,8 +58,7 @@ describe('User Popover Component', () => { username: null, location: null, bio: null, - organization: null, - jobTitle: null, + workInformation: null, status: null, }, }, @@ -93,7 +91,7 @@ describe('User Popover Component', () => { const findWorkInformation = () => wrapper.find({ ref: 'workInformation' }); const findBio = () => wrapper.find({ ref: 'bio' }); - it('should show only bio if organization and job title are not available', () => { + it('should show only bio if work information is not available', () => { const user = { ...DEFAULT_PROPS.user, bio: 'My super interesting bio' }; createWrapper({ user }); @@ -102,27 +100,10 @@ describe('User Popover Component', () => { expect(findWorkInformation().exists()).toBe(false); }); - it('should show only organization if job title is not available', () => { - const user = { ...DEFAULT_PROPS.user, organization: 'GitLab' }; - - createWrapper({ user }); - - expect(findWorkInformation().text()).toBe('GitLab'); - }); - - it('should show only job title if organization is not available', () => { - const user = { ...DEFAULT_PROPS.user, jobTitle: 'Frontend Engineer' }; - - createWrapper({ user }); - - expect(findWorkInformation().text()).toBe('Frontend Engineer'); - }); - - it('should show organization and job title if they are both available', () => { + it('should show work information when it is available', () => { const user = { ...DEFAULT_PROPS.user, - organization: 'GitLab', - jobTitle: 'Frontend Engineer', + workInformation: 'Frontend Engineer at GitLab', }; createWrapper({ user }); @@ -130,17 +111,17 @@ describe('User Popover Component', () => { expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab'); }); - it('should display bio and job info in separate lines', () => { + it('should display bio and work information in separate lines', () => { const user = { ...DEFAULT_PROPS.user, bio: 'My super interesting bio', - organization: 'GitLab', + workInformation: 'Frontend Engineer at GitLab', }; createWrapper({ user }); expect(findBio().text()).toBe('My super interesting bio'); - expect(findWorkInformation().text()).toBe('GitLab'); + expect(findWorkInformation().text()).toBe('Frontend Engineer at GitLab'); }); it('should not encode special characters in bio', () => { @@ -154,40 +135,6 @@ describe('User Popover Component', () => { expect(findBio().text()).toBe('I like <html> & CSS'); }); - it('should not encode special characters in organization', () => { - const user = { - ...DEFAULT_PROPS.user, - organization: 'Me & my <funky> Company', - }; - - createWrapper({ user }); - - expect(findWorkInformation().text()).toBe('Me & my <funky> Company'); - }); - - it('should not encode special characters in job title', () => { - const user = { - ...DEFAULT_PROPS.user, - jobTitle: 'Manager & Team Lead', - }; - - createWrapper({ user }); - - expect(findWorkInformation().text()).toBe('Manager & Team Lead'); - }); - - it('should not encode special characters when both job title and organization are set', () => { - const user = { - ...DEFAULT_PROPS.user, - jobTitle: 'Manager & Team Lead', - organization: 'Me & my <funky> Company', - }; - - createWrapper({ user }); - - expect(findWorkInformation().text()).toBe('Manager & Team Lead at Me & my <funky> Company'); - }); - it('shows icon for bio', () => { const user = { ...DEFAULT_PROPS.user, @@ -201,10 +148,10 @@ describe('User Popover Component', () => { ); }); - it('shows icon for organization', () => { + it('shows icon for work information', () => { const user = { ...DEFAULT_PROPS.user, - organization: 'GitLab', + workInformation: 'GitLab', }; createWrapper({ user }); |