diff options
author | Shinya Maeda <shinya@gitlab.com> | 2018-07-23 14:23:29 +0900 |
---|---|---|
committer | Shinya Maeda <shinya@gitlab.com> | 2018-07-23 14:23:29 +0900 |
commit | 41a439c75983a8bd200e0728b231487ff6e1699c (patch) | |
tree | b25df6509d4bb5ac2496eb132379109b38ddb43d | |
parent | bc959fffb1a1e8937078a1399dad2f698534ac84 (diff) | |
parent | 7105b37a558acf22a23125cddfefc517c040a0fb (diff) | |
download | gitlab-ce-41a439c75983a8bd200e0728b231487ff6e1699c.tar.gz |
Merge branch 'artifact-format-v2-with-parser' of gitlab.com:gitlab-org/gitlab-ce into artifact-format-v2-with-parser
19 files changed, 835 insertions, 2 deletions
diff --git a/app/assets/javascripts/reports/components/grouped_test_reports_app.vue b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue new file mode 100644 index 00000000000..175424e9d73 --- /dev/null +++ b/app/assets/javascripts/reports/components/grouped_test_reports_app.vue @@ -0,0 +1,115 @@ +<script> + import { mapActions, mapGetters } from 'vuex'; + import { s__ } from '~/locale'; + import ReportSection from '~/vue_shared/components/reports/report_section.vue'; + import SummaryRow from '~/vue_shared/components/reports/summary_row.vue'; + import IssuesList from '~/vue_shared/components/reports/issues_list.vue'; + import Modal from './modal.vue'; + import createStore from '../store'; + import { textBuilder, statusIcon } from '../store/utils'; + + export default { + name: 'GroupedTestReportsApp', + store: createStore(), + components: { + ReportSection, + SummaryRow, + IssuesList, + Modal, + }, + props: { + endpoint: { + type: String, + required: true, + }, + }, + computed: { + ...mapGetters([ + 'reports', + 'summaryStatus', + 'isLoading', + 'hasError', + 'summaryCounts', + 'modalTitle', + 'modalData', + 'isCreatingNewIssue', + ]), + + groupedSummaryText() { + if (this.isLoading) { + return s__('Reports|Test summary results are being parsed'); + } + + if (this) { + return s__('Reports|Test summary failed loading results'); + } + + const { failed, total, resolved } = this.summaryCounts; + + return textBuilder(s__('Reports|Test summary'), failed, resolved, total); + }, + }, + created() { + this.setEndpoint(this.endpoint); + + this.fetchReports(); + }, + methods: { + ...mapActions(['setEndpoint', 'fetchReports']), + reportText(report) { + // TODO_SAM: Check with backend if summary is always present. + // if it is we may be able to remove this check. + const summary = report.summary || {}; + return textBuilder(report.name, summary.failed, summary.resolved, summary.total); + }, + getReportIcon(report) { + return statusIcon(report.status); + }, + createIssue() { + // TODO_SAM + }, + }, + }; +</script> +<template> + <report-section + :status="summaryStatus" + :success-text="groupedSummaryText" + :loading-text="groupedSummaryText" + :error-text="groupedSummaryText" + :has-issues="reports.length > 0" + class="mr-widget-border-top grouped-security-reports" + > + <div + slot="body" + class="mr-widget-grouped-section report-block" + > + <template + v-for="(report, i) in reports" + > + <summary-row + :summary="reportText(report)" + :status-icon="getReportIcon(report)" + :key="`summary-row-${i}`" + + /> + <issues-list + :unresolved-issues="report.new_failures" + :resolved-issues="report.resolved_failures" + :all-issues="report.existing_failures" + :key="`issues-list-${i}`" + type="test" + class="report-block-group-list" + /> + </template> + + <modal + :title="modalTitle" + :modal-data="modalData" + :is-creating-new-issue="isCreatingNewIssue" + @createIssue="createIssue" + /> + </div> + </report-section> +</template> + diff --git a/app/assets/javascripts/reports/components/modal.vue b/app/assets/javascripts/reports/components/modal.vue new file mode 100644 index 00000000000..83edb6455a9 --- /dev/null +++ b/app/assets/javascripts/reports/components/modal.vue @@ -0,0 +1,98 @@ +<script> + import Modal from '~/vue_shared/components/gl_modal.vue'; + import LoadingButton from '~/vue_shared/components/loading_button.vue'; + import { fieldTypes } from '../constants'; + + export default { + components: { + Modal, + LoadingButton, + }, + props: { + title: { + type: String, + required: true, + }, + modalData: { + type: Object, + required: true, + }, + isCreatingNewIssue: { + type: Boolean, + required: true, + }, + }, + fieldTypes, + methods: { + handleCreateIssueClick() { + // TODO_SAM: Check with Shynia how to handle this. + // I believe we've agreed we'd do this in a new iteration + // we may need to hide the footer always. - We probably need to provide some params + this.$emit('createIssue'); + }, + }, + }; +</script> +<template> + <modal + id="modal-mrwidget-reports" + :header-title-text="title" + :class="{ 'modal-hide-footer': false }" + class="modal-security-report-dast" + > + <slot> + <div + v-for="(field, key, index) in modalData" + v-if="field.value" + :key="index" + class="row prepend-top-10 append-bottom-10" + > + <label class="col-sm-2 text-right font-weight-bold"> + {{ field.text }}: + </label> + + <div class="col-sm-10 text-secondary"> + <template v-if="field.type === $options.fieldTypes.codeBock"> + <pre> {{ field.value }} </pre> + </template> + + <template v-else-if="field.type === $options.fieldTypes.link"> + <a + :href="field.value" + target="_blank" + rel="noopener noreferrer" + > + {{ field.value }} + </a> + </template> + + <template v-else-if="field.type === $options.fieldTypes.miliseconds"> + {{ field.value }} ms + </template> + + <template v-else-if="field.type === $options.fieldTypes.text"> + {{ field.value }} + </template> + </div> + </div> + </slot> + <div slot="footer"> + <button + type="button" + class="btn btn-default" + data-dismiss="modal" + > + {{ __('Cancel' ) }} + </button> + + <loading-button + v-if="canCreateIssuePermission" + :loading="isCreatingNewIssue" + :disabled="isCreatingNewIssue" + :label="__('Create issue')" + container-class="js-create-issue-btn btn btn-success btn-inverted" + @click="handleCreateIssueClick" + /> + </div> + </modal> +</template> diff --git a/app/assets/javascripts/reports/components/test_issue_body.vue b/app/assets/javascripts/reports/components/test_issue_body.vue new file mode 100644 index 00000000000..0b01bee4557 --- /dev/null +++ b/app/assets/javascripts/reports/components/test_issue_body.vue @@ -0,0 +1,42 @@ +<script> + import { mapActions } from 'vuex'; + + export default { + props: { + issue: { + type: Object, + required: true, + }, + // failed || success + status: { + type: String, + required: true, + }, + }, + methods: { + ...mapActions(['openModal']), + handleIssueClick() { + const { issue, status, openModal } = this; + openModal({ issue, status }); + }, + }, + }; +</script> +<template> + <div class="report-block-list-issue-description prepend-top-5 append-bottom-5"> + <div class="report-block-list-issue-description-text"> + <!-- TODO_SAM: I've duplicated this from modal_open_name, + because it's expecting a title and we have name + Not sure if it's worth refactring now, or if it's ok + to leave it duplciated and fix it when we move the components around + --> + <button + type="button" + class="btn-link btn-blank text-left break-link vulnerability-name-button" + @click="handleIssueClick()" + > + {{ issue.name }} + </button> + </div> + </div> +</template> diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js new file mode 100644 index 00000000000..882617c20f1 --- /dev/null +++ b/app/assets/javascripts/reports/constants.js @@ -0,0 +1,10 @@ +export const fieldTypes = { + codeBock: 'codeBlock', + link: 'link', + miliseconds: 'miliseconds', + text: 'text', +}; + +export const LOADING = 'LOADING'; +export const ERROR = 'ERROR'; +export const SUCCESS = 'SUCCESS'; diff --git a/app/assets/javascripts/reports/store/actions.js b/app/assets/javascripts/reports/store/actions.js new file mode 100644 index 00000000000..c18885f23a4 --- /dev/null +++ b/app/assets/javascripts/reports/store/actions.js @@ -0,0 +1,92 @@ +import Visibility from 'visibilityjs'; +import $ from 'jquery'; +import axios from '../../lib/utils/axios_utils'; +import Poll from '../../lib/utils/poll'; +import flash from '../../flash'; +import { s__ } from '../../locale'; +import * as types from './mutation_types'; + +export const setEndpoint = ({ commit }, endpoint) => commit(types.SET_ENDPOINT, endpoint); + +export const requestReports = ({ commit }) => commit(types.REQUEST_REPORTS); + +let eTagPoll; + +export const clearEtagPoll = () => { + eTagPoll = null; +}; + +export const stopPolling = () => { + if (eTagPoll) eTagPoll.stop(); +}; + +export const restartPolling = () => { + if (eTagPoll) eTagPoll.restart(); +}; + +/** + * We need to poll the reports endpoint while they are being parsed in the Backend. + * This can take up to one minute. + * + * Poll.js will handle etag response. + * While http status code is 204, it means it's parsing, and we'll keep polling + * When http status code is 200, it means parsing is done, we can show the results & stop polling + * When http status code is 500, it means parsins went wrong and we stop polling + */ +export const fetchReports = ({ state, dispatch }) => { + dispatch('requestReports'); + + eTagPoll = new Poll({ + resource: { + getReports(endpoint) { + return axios.get(endpoint); + }, + }, + data: state.endpoint, + method: 'getReports', + successCallback: ({ data }) => dispatch('receiveReportsSuccess', data), + errorCallback: () => dispatch('receiveReportsError'), + }); + + if (!Visibility.hidden()) { + eTagPoll.makeRequest(); + } + + Visibility.change(() => { + if (!Visibility.hidden()) { + dispatch('restartPolling'); + } else { + dispatch('stopPolling'); + } + }); +}; + +export const receiveReportsSuccess = ({ commit }, response) => + commit(types.RECEIVE_REPORTS_SUCCESS, response); + +export const receiveReportsError = ({ commit }) => commit(types.RECEIVE_REPORTS_ERROR); + +export const openModal = ({ dispatch }, payload) => { + dispatch('setModalData', payload); + + $('#modal-mrwidget-reports').modal('show'); +}; + +export const setModalData = ({ commit }, payload) => commit(types.SET_ISSUE_MODAL_DATA, payload); + +export const createNewIssue = ({ state, dispatch }) => { + dispatch('requestCreateIssue'); + return axios.post(state.modal.endpoint) + .then(() => dispatch('receiveCreateIssueSuccess')) + .catch(() => dispatch('receiveCreateIssueError')); +}; + +export const requestCreateIssue = ({ commit }) => commit(types.REQUEST_CREATE_ISSUE); +export const receiveCreateIssueSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_ISSUE_SUCCESS); +export const receiveCreateIssueError = ({ commit }) => { + flash(s__('Report|There was an error creating the issue. Please try again.')); + commit(types.RECEIVE_CREATE_ISSUE_ERROR); +}; + +// prevent babel-plugin-rewire from generating an invalid default during karma tests +export default () => {}; diff --git a/app/assets/javascripts/reports/store/getters.js b/app/assets/javascripts/reports/store/getters.js new file mode 100644 index 00000000000..ddc4b3d3e76 --- /dev/null +++ b/app/assets/javascripts/reports/store/getters.js @@ -0,0 +1,21 @@ +import { LOADING, ERROR, SUCCESS } from '../constants'; + +export const reports = state => state.reports; +export const summaryCounts = state => state.summary; +export const isLoading = state => state.isLoading; +export const hasError = state => state.hasError; +export const modalTitle = state => state.modal.title || ''; +export const modalData = state => state.modal.data || {}; +export const isCreatingNewIssue = state => state.modal.isLoading; + +export const summaryStatus = state => { + if (state.isLoading) { + return LOADING; + } + + if (state.hasError) { + return ERROR; + } + + return SUCCESS; +}; diff --git a/app/assets/javascripts/reports/store/index.js b/app/assets/javascripts/reports/store/index.js new file mode 100644 index 00000000000..1eb2960910b --- /dev/null +++ b/app/assets/javascripts/reports/store/index.js @@ -0,0 +1,16 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import * as getters from './getters'; + +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +export default () => new Vuex.Store({ + actions, + mutations, + getters, + state: state(), +}); diff --git a/app/assets/javascripts/reports/store/mutation_types.js b/app/assets/javascripts/reports/store/mutation_types.js new file mode 100644 index 00000000000..6242c31b4e2 --- /dev/null +++ b/app/assets/javascripts/reports/store/mutation_types.js @@ -0,0 +1,9 @@ +export const SET_ENDPOINT = 'SET_ENDPOINT'; + +export const REQUEST_REPORTS = 'REQUEST_REPORTS'; +export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS'; +export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR'; +export const SET_ISSUE_MODAL_DATA = 'SET_ISSUE_MODAL_DATA'; +export const REQUEST_CREATE_ISSUE = 'REQUEST_CREATE_ISSUE'; +export const RECEIVE_CREATE_ISSUE_SUCCESS = 'RECEIVE_CREATE_ISSUE_SUCCESS'; +export const RECEIVE_CREATE_ISSUE_ERROR = 'RECEIVE_CREATE_ISSUE_ERROR'; diff --git a/app/assets/javascripts/reports/store/mutations.js b/app/assets/javascripts/reports/store/mutations.js new file mode 100644 index 00000000000..120ec177666 --- /dev/null +++ b/app/assets/javascripts/reports/store/mutations.js @@ -0,0 +1,47 @@ +/* eslint-disable no-param-reassign */ +import Vue from 'vue'; +import * as types from './mutation_types'; + +export default { + [types.SET_ENDPOINT](state, endpoint) { + state.endpoint = endpoint; + }, + [types.REQUEST_REPORTS](state) { + state.isLoading = true; + }, + [types.RECEIVE_REPORTS_SUCCESS](state, response) { + + state.isLoading = false; + + Vue.set(state.summary, 'total', response.summary.total); + Vue.set(state.summary, 'resolved', response.summary.resolved); + Vue.set(state.summary, 'failed', response.summary.failed); + + state.status = response.status; + state.reports = response.suites; + + }, + [types.RECEIVE_REPORTS_ERROR](state) { + state.isLoading = false; + state.hasError = true; + }, + [types.SET_ISSUE_MODAL_DATA](state, payload) { + Vue.set(state.modal, 'title', payload.issue.name); + Vue.set(state.modal, 'status', payload.status); + + Object.keys(payload.issue).forEach((key) => { + if (Object.prototype.hasOwnProperty.call(state.modal.data, key)) { + Vue.set(state.modal.data[key], 'value', payload.issue[key]); + } + }); + }, + [types.REQUEST_CREATE_ISSUE](state) { + Vue.set(state.modal, 'isLoading', true); + }, + [types.RECEIVE_CREATE_ISSUE_SUCCESS](state) { + Vue.set(state.modal, 'isLoading', false); + }, + [types.RECEIVE_CREATE_ISSUE_ERROR](state) { + Vue.set(state.modal, 'isLoading', false); + }, +}; diff --git a/app/assets/javascripts/reports/store/state.js b/app/assets/javascripts/reports/store/state.js new file mode 100644 index 00000000000..4254f839fd7 --- /dev/null +++ b/app/assets/javascripts/reports/store/state.js @@ -0,0 +1,65 @@ +import { s__ } from '~/locale'; +import { fieldTypes } from '../constants'; + +export default () => ({ + endpoint: null, + + isLoading: false, + hasError: false, + + status: null, + + summary: { + total: 0, + resolved: 0, + failed: 0, + }, + + /** + * Each report will have the following format: + * { + * name: {String}, + * summary: { + * total: {Number}, + * resolved: {Number}, + * failed: {Number}, + * }, + * new_failures: {Array.<Object>}, + * resolved_failures: {Array.<Object>}, + * existing_failures: {Array.<Object>}, + * } + */ + reports: [], + + modal: { + title: null, + + status: null, + + isCreatingNewIssue: false, + hasError: false, + + data: { + class: { + value: null, + text: s__('Reports|Class'), + type: fieldTypes.link, + }, + execution_time: { + value: null, + text: s__('Reports|Execution time'), + type: fieldTypes.miliseconds, + }, + failure: { + value: null, + text: s__('Reports|Failure'), + type: fieldTypes.codeBock, + }, + system_output: { + value: null, + text: s__('Reports|System output'), + type: fieldTypes.codeBock, + }, + }, + }, +}); diff --git a/app/assets/javascripts/reports/store/utils.js b/app/assets/javascripts/reports/store/utils.js new file mode 100644 index 00000000000..23162c595b7 --- /dev/null +++ b/app/assets/javascripts/reports/store/utils.js @@ -0,0 +1,44 @@ +import { sprintf, n__, s__ } from '~/locale'; + +export const textBuilder = (name = '', failed = 0, resolved = 0, total = 0) => { + if (failed === 0 && resolved === 0) { + // TODO_SAM: Check with dimitrie + return sprintf(s__('Reports|%{name} found no tests'), { name }); + } + + const text = [name]; + + if (failed > 0 && resolved === 0) { + text.push(n__('found %d failed test', 'found %d failed tests', failed)); + } + + if (failed > 0 && resolved > 0) { + text.push( + `${n__('found %d failed test', 'found %d failed tests', failed)} ${n__( + 'and %d fixed test', + 'and %d fixed tests', + resolved, + )}`, + ); + } + + if (failed === 0 && resolved > 0) { + text.push(n__('found %d fixed test', 'found %d fixed tests', resolved)); + } + + if (failed === 0 && resolved === 0) { + text.push(s__('found no tests')); + } + + text.push(sprintf('out of %{total} total', { total })); + + return text.join(' '); +}; + +export const statusIcon = (status) => { + if (status === 'failed') { + return 'warning'; + } + + return 'success'; +}; diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue index b5de3dd6d73..77b40076fd9 100644 --- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue +++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue @@ -36,6 +36,7 @@ import { notify, SourceBranchRemovalStatus, } from './dependencies'; +import GroupedTestReportsApp from '../reports/components/grouped_test_reports_app.vue'; import { setFaviconOverlay } from '../lib/utils/common_utils'; export default { @@ -68,6 +69,7 @@ export default { 'mr-widget-auto-merge-failed': AutoMergeFailed, 'mr-widget-rebase': RebaseState, SourceBranchRemovalStatus, + GroupedTestReportsApp, }, props: { mrData: { @@ -259,7 +261,12 @@ export default { :key="deployment.id" :deployment="deployment" /> + <div class="mr-section-container"> + <grouped-test-reports-app + v-if="mr.testResultsPath" + :endpoint="mr.testResultsPath" + /> <div class="mr-widget-section"> <component :is="componentName" diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js index e84c436905d..bb78f3aed26 100644 --- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js +++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js @@ -108,6 +108,8 @@ export default class MergeRequestStore { this.isPipelineBlocked = pipelineStatus ? pipelineStatus.group === 'manual' : false; this.ciStatusFaviconPath = pipelineStatus ? pipelineStatus.favicon : null; + this.testResultsPath = data.test_results_path; + this.setState(data); } diff --git a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue index ecffb02a3a0..47b2de8fcef 100644 --- a/app/assets/javascripts/vue_shared/components/reports/report_issues.vue +++ b/app/assets/javascripts/vue_shared/components/reports/report_issues.vue @@ -1,10 +1,12 @@ <script> import Icon from '~/vue_shared/components/icon.vue'; +import Issue from '~/reports/components/test_issue_body.vue'; export default { name: 'ReportIssues', components: { Icon, + Issue, }, props: { issues: { @@ -40,6 +42,11 @@ export default { isStatusNeutral() { return this.status === 'neutral'; }, + isTypeTest() { + // TODO: Remove this. It's needed because of the EE port of this. + // Ideally there would be no type here. + return this.type === 'test'; + }, }, }; </script> @@ -66,6 +73,12 @@ export default { /> </div> + <issue + v-if="isTypeTest" + :issue="issue" + :status="status" + /> + </li> </ul> </div> diff --git a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue b/app/assets/javascripts/vue_shared/components/reports/summary_row.vue index 997bad960e2..b85103d95f2 100644 --- a/app/assets/javascripts/vue_shared/components/reports/summary_row.vue +++ b/app/assets/javascripts/vue_shared/components/reports/summary_row.vue @@ -29,7 +29,8 @@ export default { }, popoverOptions: { type: Object, - required: true, + required: false, + default: null, }, }, computed: { @@ -60,7 +61,10 @@ export default { {{ summary }} </div> - <popover :options="popoverOptions" /> + <popover + v-if="popoverOptions" + :options="popoverOptions" + /> </div> </div> </template> diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss index c8349a4ef79..d61c7eb6a0e 100644 --- a/app/assets/stylesheets/pages/merge_requests.scss +++ b/app/assets/stylesheets/pages/merge_requests.scss @@ -15,6 +15,10 @@ } } +.mr-widget-border-top { + border-top: 1px solid $border-color; +} + .mr-widget-heading { position: relative; border: 1px solid $border-color; @@ -54,6 +58,14 @@ padding: 0; } + .grouped-security-reports { + padding: 0; + + > .media { + padding: $gl-padding; + } + } + form { margin-bottom: 0; diff --git a/changelogs/unreleased/45318-vuex-store.yml b/changelogs/unreleased/45318-vuex-store.yml new file mode 100644 index 00000000000..5ea89034bce --- /dev/null +++ b/changelogs/unreleased/45318-vuex-store.yml @@ -0,0 +1,5 @@ +--- +title: Adds Vuex store for reports section in MR widget +merge_request: 20709 +author: +type: added diff --git a/spec/javascripts/reports/store/actions_spec.js b/spec/javascripts/reports/store/actions_spec.js new file mode 100644 index 00000000000..fc2973de5bd --- /dev/null +++ b/spec/javascripts/reports/store/actions_spec.js @@ -0,0 +1,130 @@ +import MockAdapter from 'axios-mock-adapter'; +import axios from '~/lib/utils/axios_utils'; +import { + setEndpoint, + requestReports, + fetchReports, + stopPolling, + clearEtagPoll, + receiveReportsSuccess, + receiveReportsError, +} from '~/reports/store/actions'; +import state from '~/reports/store/state'; +import * as types from '~/reports/store/mutation_types'; +import testAction from 'spec/helpers/vuex_action_helper'; +import { TEST_HOST } from 'spec/test_constants'; + +describe('Reports Store Actions', () => { + let mockedState; + + beforeEach(() => { + mockedState = state(); + }); + + describe('setEndpoint', () => { + it('should commit SET_ENDPOINT mutation', done => { + testAction( + setEndpoint, + 'endpoint.json', + mockedState, + [{ type: types.SET_ENDPOINT, payload: 'endpoint.json' }], + [], + done, + ); + }); + }); + + describe('requestReports', () => { + it('should commit REQUEST_REPORTS mutation', done => { + testAction(requestReports, null, mockedState, [{ type: types.REQUEST_REPORTS }], [], done); + }); + }); + + describe('fetchReports', () => { + let mock; + + beforeEach(() => { + mockedState.endpoint = `${TEST_HOST}/endpoint.json`; + mock = new MockAdapter(axios); + }); + + afterEach(() => { + mock.restore(); + stopPolling(); + clearEtagPoll(); + }); + + describe('success', () => { + it('dispatches requestReports and receiveReportsSuccess ', done => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, { summary: {}, suites: [{ name: 'rspec' }] }); + + testAction( + fetchReports, + null, + mockedState, + [], + [ + { + type: 'requestReports', + }, + { + payload: { summary: {}, suites: [{ name: 'rspec' }] }, + type: 'receiveReportsSuccess', + }, + ], + done, + ); + }); + }); + + describe('error', () => { + beforeEach(() => { + mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500); + }); + + it('dispatches requestReports and receiveReportsError ', done => { + testAction( + fetchReports, + null, + mockedState, + [], + [ + { + type: 'requestReports', + }, + { + type: 'receiveReportsError', + }, + ], + done, + ); + }); + }); + }); + + describe('receiveReportsSuccess', () => { + it('should commit RECEIVE_REPORTS_SUCCESS mutation', done => { + testAction( + receiveReportsSuccess, + { summary: {} }, + mockedState, + [{ type: types.RECEIVE_REPORTS_SUCCESS, payload: { summary: {} } }], + [], + done, + ); + }); + }); + + describe('receiveReportsError', () => { + it('should commit RECEIVE_REPORTS_ERROR mutation', done => { + testAction( + receiveReportsError, + null, + mockedState, + [{ type: types.RECEIVE_REPORTS_ERROR }], + [], + done, + ); + }); + }); +}); diff --git a/spec/javascripts/reports/store/mutations_spec.js b/spec/javascripts/reports/store/mutations_spec.js new file mode 100644 index 00000000000..3e0b15438c3 --- /dev/null +++ b/spec/javascripts/reports/store/mutations_spec.js @@ -0,0 +1,101 @@ +import state from '~/reports/store/state'; +import mutations from '~/reports/store/mutations'; +import * as types from '~/reports/store/mutation_types'; + +describe('Reports Store Mutations', () => { + let stateCopy; + + beforeEach(() => { + stateCopy = state(); + }); + + describe('SET_ENDPOINT', () => { + it('should set endpoint', () => { + mutations[types.SET_ENDPOINT](stateCopy, 'endpoint.json'); + expect(stateCopy.endpoint).toEqual('endpoint.json'); + }); + }); + + describe('REQUEST_REPORTS', () => { + it('should set isLoading to true', () => { + mutations[types.REQUEST_REPORTS](stateCopy); + expect(stateCopy.isLoading).toEqual(true); + }); + }); + + describe('RECEIVE_REPORTS_SUCCESS', () => { + const mockedResponse = { + summary: { + total: 14, + resolved: 0, + failed: 7, + }, + suites: [ + { + name: 'build:linux', + summary: { + total: 2, + resolved: 0, + failed: 1, + }, + new_failures: [ + { + name: 'StringHelper#concatenate when a is git and b is lab returns summary', + execution_time: 0.0092435, + system_output: + 'Failure/Error: is_expected.to eq(\'gitlab\')', + }, + ], + resolved_failures: [ + { + name: 'StringHelper#concatenate when a is git and b is lab returns summary', + execution_time: 0.009235, + system_output: + 'Failure/Error: is_expected.to eq(\'gitlab\')', + }, + ], + existing_failures: [ + { + name: 'StringHelper#concatenate when a is git and b is lab returns summary', + execution_time: 1232.08, + system_output: + 'Failure/Error: is_expected.to eq(\'gitlab\')', + }, + ], + }, + ], + }; + + beforeEach(() => { + mutations[types.RECEIVE_REPORTS_SUCCESS](stateCopy, mockedResponse); + }); + + it('should reset isLoading', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set summary counts', () => { + expect(stateCopy.summary.total).toEqual(mockedResponse.summary.total); + expect(stateCopy.summary.resolved).toEqual(mockedResponse.summary.resolved); + expect(stateCopy.summary.failed).toEqual(mockedResponse.summary.failed); + }); + + it('should set reports', () => { + expect(stateCopy.reports).toEqual(mockedResponse.suites); + }); + }); + + describe('RECEIVE_REPORTS_ERROR', () => { + beforeEach(() => { + mutations[types.RECEIVE_REPORTS_ERROR](stateCopy); + }); + it('should reset isLoading', () => { + expect(stateCopy.isLoading).toEqual(false); + }); + + it('should set hasError to true', () => { + expect(stateCopy.hasError).toEqual(true); + }); + + }); +}); |