summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-04-20 18:38:24 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-04-20 18:38:24 +0000
commit983a0bba5d2a042c4a3bbb22432ec192c7501d82 (patch)
treeb153cd387c14ba23bd5a07514c7c01fddf6a78a0 /spec/frontend
parenta2bddee2cdb38673df0e004d5b32d9f77797de64 (diff)
downloadgitlab-ce-983a0bba5d2a042c4a3bbb22432ec192c7501d82.tar.gz
Add latest changes from gitlab-org/gitlab@12-10-stable-ee
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/blob/components/blob_edit_content_spec.js2
-rw-r--r--spec/frontend/ci_variable_list/components/ci_key_field_spec.js244
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js153
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js16
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js4
-rw-r--r--spec/frontend/diffs/components/diff_table_cell_spec.js15
-rw-r--r--spec/frontend/diffs/store/actions_spec.js1
-rw-r--r--spec/frontend/diffs/store/getters_versions_dropdowns_spec.js99
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js67
-rw-r--r--spec/frontend/fixtures/merge_requests_diffs.rb7
-rw-r--r--spec/frontend/helpers/dom_events_helper.js10
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js207
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js136
-rw-r--r--spec/frontend/jira_import/components/jira_import_progress_spec.js70
-rw-r--r--spec/frontend/jira_import/components/jira_import_setup_spec.js17
-rw-r--r--spec/frontend/jira_import/utils_spec.js27
-rw-r--r--spec/frontend/logs/mock_data.js76
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap169
-rw-r--r--spec/frontend/monitoring/components/charts/annotations_spec.js11
-rw-r--r--spec/frontend/monitoring/components/charts/options_spec.js29
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js55
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js132
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js3
-rw-r--r--spec/frontend/monitoring/components/panel_type_spec.js93
-rw-r--r--spec/frontend/monitoring/fixture_data.js49
-rw-r--r--spec/frontend/monitoring/init_utils.js57
-rw-r--r--spec/frontend/monitoring/mock_data.js348
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js75
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js90
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js7
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js31
-rw-r--r--spec/frontend/monitoring/store_utils.js34
-rw-r--r--spec/frontend/monitoring/utils_spec.js11
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js31
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js9
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js305
-rw-r--r--spec/frontend/pipelines/graph/job_group_dropdown_spec.js84
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js8
-rw-r--r--spec/frontend/pipelines/graph/job_name_component_spec.js36
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js24
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_column_spec.js38
-rw-r--r--spec/frontend/pipelines/graph/linked_pipelines_mock_data.js4084
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js261
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js136
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js49
-rw-r--r--spec/frontend/registry/explorer/stores/actions_spec.js29
-rw-r--r--spec/frontend/registry/explorer/stores/mutations_spec.js22
-rw-r--r--spec/frontend/repository/router_spec.js17
-rw-r--r--spec/frontend/sidebar/sidebar_assignees_spec.js74
-rw-r--r--spec/frontend/snippet/snippet_edit_spec.js45
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap1
-rw-r--r--spec/frontend/snippets/components/edit_spec.js279
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js10
-rw-r--r--spec/frontend/static_site_editor/components/invalid_content_message_spec.js23
-rw-r--r--spec/frontend/static_site_editor/components/publish_toolbar_spec.js4
-rw-r--r--spec/frontend/static_site_editor/components/saved_changes_message_spec.js28
-rw-r--r--spec/frontend/static_site_editor/components/static_site_editor_spec.js79
-rw-r--r--spec/frontend/static_site_editor/components/submit_changes_error_spec.js48
-rw-r--r--spec/frontend/static_site_editor/mock_data.js4
-rw-r--r--spec/frontend/static_site_editor/store/actions_spec.js19
-rw-r--r--spec/frontend/static_site_editor/store/mutations_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/awards_list_spec.js.snap287
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js213
-rw-r--r--spec/frontend/vue_shared/components/form/__snapshots__/title_spec.js.snap4
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js73
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 &lt;img src=x onerror=alert(document.domain)&gt;');
+ });
+ });
+});
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-&lt;img src=x onerror=alert(document.domain)&gt;',
+ );
+ });
+ });
+
+ 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 });