Commit 8f372152 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch '_acet-related-mrs-widget-rewrite' into 'master'

Rewrite related MRs widget with Vue

Closes #58926 and #57662

See merge request gitlab-org/gitlab-ce!27027
parents 302dbfff 7650677d
......@@ -15,7 +15,6 @@ export default class Issue {
Issue.$btnNewBranch = $('#new-branch');
Issue.createMrDropdownWrap = document.querySelector('.create-mr-dropdown-wrap');
Issue.initMergeRequests();
if (document.querySelector('#related-branches')) {
Issue.initRelatedBranches();
}
......@@ -143,19 +142,6 @@ export default class Issue {
}
}
static initMergeRequests() {
var $container;
$container = $('#merge-requests');
return axios
.get($container.data('url'))
.then(({ data }) => {
if ('html' in data) {
$container.html(data.html);
}
})
.catch(() => flash('Failed to load referenced merge requests'));
}
static initRelatedBranches() {
var $container;
$container = $('#related-branches');
......
import Vue from 'vue';
import sanitize from 'sanitize-html';
import issuableApp from './components/app.vue';
import { parseIssuableData } from './utils/parse_data';
import '../vue_shared/vue_resource_interceptor';
export default function initIssueableApp() {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const props = JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"'));
return new Vue({
el: document.getElementById('js-issuable-app'),
components: {
......@@ -14,7 +11,7 @@ export default function initIssueableApp() {
},
render(createElement) {
return createElement('issuable-app', {
props,
props: parseIssuableData(),
});
},
});
......
import sanitize from 'sanitize-html';
export const parseIssuableData = () => {
try {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
return JSON.parse(sanitize(initialDataEl.textContent).replace(/"/g, '"'));
} catch (e) {
console.error(e); // eslint-disable-line no-console
return {};
}
};
export default {};
......@@ -4,9 +4,11 @@ import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ZenMode from '~/zen_mode';
import '~/notes/index';
import initIssueableApp from '~/issue_show';
import initRelatedMergeRequestsApp from '~/related_merge_requests';
export default function() {
initIssueableApp();
initRelatedMergeRequestsApp();
new Issue(); // eslint-disable-line no-new
new ShortcutsIssuable(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
......
<script>
import { mapState, mapActions } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { sprintf, n__, s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import { parseIssuableData } from '../../issue_show/utils/parse_data';
export default {
name: 'RelatedMergeRequests',
components: {
Icon,
GlLoadingIcon,
RelatedIssuableItem,
},
props: {
endpoint: {
type: String,
required: true,
},
projectNamespace: {
type: String,
required: true,
},
projectPath: {
type: String,
required: true,
},
},
computed: {
...mapState(['isFetchingMergeRequests', 'mergeRequests', 'totalCount']),
closingMergeRequestsText() {
if (!this.hasClosingMergeRequest) {
return '';
}
const mrText = n__(
'When this merge request is accepted',
'When these merge requests are accepted',
this.totalCount,
);
return sprintf(s__('%{mrText}, this issue will be closed automatically.'), { mrText });
},
},
mounted() {
this.setInitialState({ apiEndpoint: this.endpoint });
this.fetchMergeRequests();
},
created() {
this.hasClosingMergeRequest = parseIssuableData().hasClosingMergeRequest;
},
methods: {
...mapActions(['setInitialState', 'fetchMergeRequests']),
getAssignees(mr) {
if (mr.assignees) {
return mr.assignees;
}
return mr.assignee ? [mr.assignee] : [];
},
},
};
</script>
<template>
<div v-if="isFetchingMergeRequests || (!isFetchingMergeRequests && totalCount)">
<div id="merge-requests" class="card-slim mt-3">
<div class="card-header">
<div class="card-title mt-0 mb-0 h5 merge-requests-title">
<span class="mr-1">
{{ __('Related merge requests') }}
</span>
<div v-if="totalCount" class="d-inline-flex lh-100 align-middle">
<div class="mr-count-badge">
<div class="mr-count-badge-count">
<svg class="s16 mr-1 text-secondary">
<icon name="merge-request" class="mr-1 text-secondary" />
</svg>
<span class="js-items-count">{{ totalCount }}</span>
</div>
</div>
</div>
</div>
</div>
<div>
<div
v-if="isFetchingMergeRequests"
class="related-related-merge-requests-icon qa-related-merge-requests-loading-icon"
>
<gl-loading-icon label="Fetching related merge requests" class="py-2" />
</div>
<ul v-else class="content-list related-items-list">
<li v-for="mr in mergeRequests" :key="mr.id" class="list-item pt-0 pb-0">
<related-issuable-item
:id-key="mr.id"
:display-reference="mr.reference"
:title="mr.title"
:milestone="mr.milestone"
:assignees="getAssignees(mr)"
:created-at="mr.created_at"
:closed-at="mr.closed_at"
:merged-at="mr.merged_at"
:path="mr.web_url"
:state="mr.state"
:is-merge-request="true"
:pipeline-status="mr.head_pipeline && mr.head_pipeline.detailed_status"
path-id-separator="!"
/>
</li>
</ul>
</div>
</div>
<div
v-if="hasClosingMergeRequest && !isFetchingMergeRequests"
class="issue-closed-by-widget second-block"
>
{{ closingMergeRequestsText }}
</div>
</div>
</template>
import Vue from 'vue';
import RelatedMergeRequests from './components/related_merge_requests.vue';
import createStore from './store';
export default function initRelatedMergeRequests() {
const relatedMergeRequestsElement = document.querySelector('#js-related-merge-requests');
if (relatedMergeRequestsElement) {
const { endpoint, projectPath, projectNamespace } = relatedMergeRequestsElement.dataset;
// eslint-disable-next-line no-new
new Vue({
el: relatedMergeRequestsElement,
components: {
RelatedMergeRequests,
},
store: createStore(),
render: createElement =>
createElement('related-merge-requests', {
props: { endpoint, projectNamespace, projectPath },
}),
});
}
}
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import * as types from './mutation_types';
const REQUEST_PAGE_COUNT = 100;
export const setInitialState = ({ commit }, props) => {
commit(types.SET_INITIAL_STATE, props);
};
export const requestData = ({ commit }) => commit(types.REQUEST_DATA);
export const receiveDataSuccess = ({ commit }, data) => commit(types.RECEIVE_DATA_SUCCESS, data);
export const receiveDataError = ({ commit }) => commit(types.RECEIVE_DATA_ERROR);
export const fetchMergeRequests = ({ state, dispatch }) => {
dispatch('requestData');
return axios
.get(`${state.apiEndpoint}?per_page=${REQUEST_PAGE_COUNT}`)
.then(res => {
const { headers, data } = res;
const total = Number(normalizeHeaders(headers)['X-TOTAL']) || 0;
dispatch('receiveDataSuccess', { data, total });
})
.catch(() => {
dispatch('receiveDataError');
createFlash(s__('Something went wrong while fetching related merge requests.'));
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import Vue from 'vue';
import Vuex from 'vuex';
import createState from './state';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
state: createState(),
actions,
mutations,
});
export const SET_INITIAL_STATE = 'SET_INITIAL_STATE';
export const REQUEST_DATA = 'REQUEST_DATA';
export const RECEIVE_DATA_SUCCESS = 'RECEIVE_DATA_SUCCESS';
export const RECEIVE_DATA_ERROR = 'RECEIVE_DATA_ERROR';
import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, { apiEndpoint }) {
state.apiEndpoint = apiEndpoint;
},
[types.REQUEST_DATA](state) {
state.isFetchingMergeRequests = true;
},
[types.RECEIVE_DATA_SUCCESS](state, { data, total }) {
state.isFetchingMergeRequests = false;
state.mergeRequests = data;
state.totalCount = total;
},
[types.RECEIVE_DATA_ERROR](state) {
state.isFetchingMergeRequests = false;
state.hasErrorFetchingMergeRequests = true;
},
};
export default () => ({
apiEndpoint: '',
isFetchingMergeRequests: false,
hasErrorFetchingMergeRequests: false,
mergeRequests: [],
totalCount: 0,
});
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
import relatedIssuableMixin from '~/vue_shared/mixins/related_issuable_mixin';
import { sprintf } from '~/locale';
import IssueMilestone from '../../components/issue/issue_milestone.vue';
import IssueAssignees from '../../components/issue/issue_assignees.vue';
import relatedIssuableMixin from '../../mixins/related_issuable_mixin';
import CiIcon from '../ci_icon.vue';
export default {
name: 'IssueItem',
components: {
IssueMilestone,
IssueAssignees,
CiIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
......@@ -27,9 +29,9 @@ export default {
return sprintf(
'<span class="bold">%{state}</span> %{timeInWords}<br/><span class="text-tertiary">%{timestamp}</span>',
{
state: this.isOpen ? __('Opened') : __('Closed'),
timeInWords: this.isOpen ? this.createdAtInWords : this.closedAtInWords,
timestamp: this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp,
state: this.stateText,
timeInWords: this.stateTimeInWords,
timestamp: this.stateTimestamp,
},
);
},
......@@ -84,6 +86,11 @@ export default {
{{ pathIdSeparator }}{{ itemId }}
</div>
<div class="item-meta-child d-flex align-items-center">
<span v-if="hasPipeline" class="mr-ci-status pr-2">
<a :href="pipelineStatus.details_path">
<ci-icon v-gl-tooltip :status="pipelineStatus" :title="pipelineStatusTooltip" />
</a>
</span>
<issue-milestone
v-if="hasMilestone"
:milestone="milestone"
......
import _ from 'underscore';
import { sprintf, __ } from '~/locale';
import { formatDate } from '~/lib/utils/datetime_utility';
import tooltip from '~/vue_shared/directives/tooltip';
import icon from '~/vue_shared/components/icon.vue';
......@@ -58,6 +59,11 @@ const mixins = {
required: false,
default: '',
},
mergedAt: {
type: String,
required: false,
default: '',
},
milestone: {
type: Object,
required: false,
......@@ -83,6 +89,16 @@ const mixins = {
required: false,
default: false,
},
isMergeRequest: {
type: Boolean,
required: false,
default: false,
},
pipelineStatus: {
type: Object,
required: false,
default: () => ({}),
},
},
components: {
icon,
......@@ -95,12 +111,18 @@ const mixins = {
hasState() {
return this.state && this.state.length > 0;
},
hasPipeline() {
return this.isMergeRequest && this.pipelineStatus && Object.keys(this.pipelineStatus).length;
},
isOpen() {
return this.state === 'opened';
},
isClosed() {
return this.state === 'closed';
},
isMerged() {
return this.state === 'merged';
},
hasTitle() {
return this.title.length > 0;
},
......@@ -108,9 +130,17 @@ const mixins = {
return !_.isEmpty(this.milestone);
},
iconName() {
if (this.isMergeRequest && this.isMerged) {
return 'merge';
}
return this.isOpen ? 'issue-open-m' : 'issue-close';
},
iconClass() {
if (this.isMergeRequest && this.isClosed) {
return 'merge-request-status closed issue-token-state-icon-closed';
}
return this.isOpen ? 'issue-token-state-icon-open' : 'issue-token-state-icon-closed';
},
computedLinkElementType() {
......@@ -131,12 +161,44 @@ const mixins = {
createdAtTimestamp() {
return this.createdAt ? formatDate(new Date(this.createdAt)) : '';
},
mergedAtTimestamp() {
return this.mergedAt ? formatDate(new Date(this.mergedAt)) : '';
},
mergedAtInWords() {
return this.mergedAt ? this.timeFormated(this.mergedAt) : '';
},
closedAtInWords() {
return this.closedAt ? this.timeFormated(this.closedAt) : '';
},
closedAtTimestamp() {
return this.closedAt ? formatDate(new Date(this.closedAt)) : '';
},
stateText() {
if (this.isMerged) {
return __('Merged');
}
return this.isOpen ? __('Opened') : __('Closed');
},
stateTimeInWords() {
if (this.isMerged) {
return this.mergedAtInWords;
}
return this.isOpen ? this.createdAtInWords : this.closedAtInWords;
},
stateTimestamp() {
if (this.isMerged) {
return this.mergedAtTimestamp;
}
return this.isOpen ? this.createdAtTimestamp : this.closedAtTimestamp;
},
pipelineStatusTooltip() {
return this.hasPipeline
? sprintf(__('Pipeline: %{status}'), { status: this.pipelineStatus.label })
: '';
},
},
methods: {
onRemoveRequest() {
......
......@@ -277,6 +277,8 @@ module IssuablesHelper
initialTaskStatus: issuable.task_status
}
data[:hasClosingMergeRequest] = issuable.merge_requests_count != 0 if issuable.is_a?(Issue)
if parent.is_a?(Group)
data[:groupPath] = parent.path
else
......
.issue-closed-by-widget.second-block
- pluralized_mr_this = merge_request_count > 1 ? "these" : "this"
- pluralized_mr_is = merge_request_count > 1 ? "are" : "is"
When #{pluralized_mr_this} merge #{"request".pluralize(merge_request_count)} #{pluralized_mr_is} accepted, this issue will be closed automatically.
- if @merge_requests.any?
.card-slim.mt-3
.card-header
%h2.card-title.mt-0.mb-0.h5.merge-requests-title
%span.mr-1.bold
= _('Related merge requests')
.d-inline-flex.lh-100.align-middle
.mr-count-badge
.mr-count-badge-count
= sprite_icon('merge-request', size: 16, css_class: 'mr-1 text-secondary')
= @merge_requests.count
%ul.content-list.related-items-list
- has_any_head_pipeline = @merge_requests.any?(&:head_pipeline_id)
- @merge_requests.each do |merge_request|
- merge_request = merge_request.present(current_user: current_user)
%li.list-item.py-0.px-0
.item-body.issuable-info-container.py-lg-3.px-lg-3.pl-md-3
.item-contents
.item-title.d-flex.align-items-center.mr-title
= render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-none d-xl-block append-right-8' }
= link_to merge_request.title, merge_request_path(merge_request), { class: 'mr-title-link'}
.item-meta
= render partial: 'projects/issues/merge_requests_status', locals: { merge_request: merge_request, css_class: 'd-xl-none d-lg-block append-right-5' }
%span.d-flex.align-items-center.append-right-8.mr-item-path.item-path-id.mt-0
%span.path-id-text.bold.text-truncate{ data: { toggle: 'tooltip'}, title: merge_request.target_project.full_path }
= merge_request.target_project.full_path
= merge_request.to_reference
%span.mr-ci-status.flex-md-grow-1.justify-content-end.d-flex.ml-md-2
- if merge_request.can_read_pipeline?
= render 'ci/status/icon', status: merge_request.head_pipeline.detailed_status(current_user), tooltip_placement: 'bottom'
- elsif has_any_head_pipeline
= icon('blank fw')
- if @closed_by_merge_requests.present?
%p
= render partial: 'projects/issues/closed_by_box', locals: {merge_request_count: @merge_requests.count}
......@@ -77,12 +77,11 @@
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue-edited-ago js-issue-edited-ago')
#merge-requests{ data: { url: referenced_merge_requests_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript.
#js-related-merge-requests{ data: { endpoint: api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
// This element is filled in using JavaScript.
-# This element is filled in using JavaScript.
.content-block.emoji-block.emoji-block-sticky
.row
......
---
title: Rewrite related MRs widget with Vue
merge_request: 27027
author:
type: other
......@@ -141,6 +141,9 @@ msgstr ""
msgid "%{lock_path} is locked by GitLab User %{lock_user_id}"
msgstr ""
msgid "%{mrText}, this issue will be closed automatically."
msgstr ""
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
......@@ -5830,6 +5833,9 @@ msgstr ""
msgid "Pipeline triggers"
msgstr ""
msgid "Pipeline: %{status}"
msgstr ""
msgid "PipelineCharts|Failed:"
msgstr ""
......@@ -7510,6 +7516,9 @@ msgstr ""
msgid "Something went wrong while fetching comments. Please try again."
msgstr ""
msgid "Something went wrong while fetching related merge requests."
msgstr ""
msgid "Something went wrong while fetching the environments for this merge request. Please try again."
msgstr ""
......@@ -9238,6 +9247,11 @@ msgstr ""
msgid "When enabled, users cannot use GitLab until the terms have been accepted."
msgstr ""
msgid "When this merge request is accepted"
msgid_plural "When these merge requests are accepted"
msgstr[0] ""
msgstr[1] ""
msgid "When:"
msgstr ""
......
......@@ -70,7 +70,7 @@ describe "Internal references", :js do
page.within("#merge-requests ul") do
expect(page).to have_content(private_project_merge_request.title)
expect(page).to have_css(".merge-request-status")
expect(page).to have_css(".ic-issue-open-m")
end
expect(page).to have_content("mentioned in merge request #{private_project_merge_request.to_reference(public_project)}")
......
......@@ -65,3 +65,61 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
store_frontend_fixture(response, fixture_file_name)
end
end
describe API::Issues, '(JavaScript fixtures)', type: :request do
include ApiHelpers
include JavaScriptFixturesHelpers
def get_related_merge_requests(project_id, issue_iid, user = nil)
get api("/projects/#{project_id}/issues/#{issue_iid}/related_merge_requests", user)
end
def create_referencing_mr(user, project, issue)
attributes = {
author: user,
source_project: project,
target_project: project,
source_branch: "master",
target_branch: "test",
assignee: user,
description: "See #{issue.to_reference}"
}
create(:merge_request, attributes).tap do |merge_request|
create(:note, :system, project: issue.project, noteable: issue, author: user, note: merge_request.to_reference(full: true))
end
end
it 'issues/related_merge_requests.json' do |example|
user = create(:user)
project = create(:project, :public, creator_id: user.id, namespace: user.namespace)
issue_title = 'foo'
issue_description = 'closed'
milestone = create(:milestone, title: '1.0.0', project: project)
issue = create :issue,
author: user,
assignees: [user],
project: project,
milestone: milestone,
created_at: generate(:past_time),
updated_at: 1.hour.ago,
title: issue_title,
description: issue_description
project.add_reporter(user)
create_referencing_mr(user, project, issue)
create(:merge_request,
:simple,
author: user,
source_project: project,
target_project: project,
description: "Some description")
project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline))
get_related_merge_requests(project.id, issue.iid, user)
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
end
import { mount, createLocalVue } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
import RelatedMergeRequests from '~/related_merge_requests/components/related_merge_requests.vue';
import createStore from '~/related_merge_requests/store/index';
const FIXTURE_PATH = 'issues/related_merge_requests.json';
const API_ENDPOINT = '/api/v4/projects/2/issues/33/related_merge_requests';
const localVue = createLocalVue();
describe('RelatedMergeRequests', () => {
let wrapper;
let mock;
let mockData;
beforeEach(done => {
loadFixtures(FIXTURE_PATH);
mockData = getJSONFixture(FIXTURE_PATH);
mock = new MockAdapter(axios);
mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
wrapper = mount(RelatedMergeRequests, {
localVue,
sync: false,
store: createStore(),
propsData: {
endpoint: API_ENDPOINT,
projectNamespace: 'gitlab-org',
projectPath: 'gitlab-ce',
},
});
setTimeout(done);
});
afterEach(() => {
wrapper.destroy();
mock.restore();
});
describe('methods', () => {
describe('getAssignees', () => {
const assignees = [{ name: 'foo' }, { name: 'bar' }];
describe('when there is assignees array', () => {
it('should return assignees array', () => {
const mr = { assignees };
expect(wrapper.vm.getAssignees(mr)).toEqual(assignees);
});
});
it('should return an array with single assingee', () => {
const mr = { assignee: assignees[0] };
expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]);
});
it('should return empty array when assignee is not set', () => {
expect(wrapper.vm.getAssignees({})).toEqual([]);
expect(wrapper.vm.getAssignees({ assignee: null })).toEqual([]);
});
});
});
describe('template', () => {
it('should render related merge request items', () => {
expect(wrapper.find('.js-items-count').text()).toEqual('2');
expect(wrapper.findAll(RelatedIssuableItem).length).toEqual(2);
const props = wrapper
.findAll(RelatedIssuableItem)
.at(1)
.props();
const data = mockData[1];
expect(props.idKey).toEqual(data.id);
expect(props.pathIdSeparator).toEqual('!');
expect(props.pipelineStatus).toBe(data.head_pipeline.detailed_status);
expect(props.assignees).toEqual([data.assignee]);
expect(props.isMergeRequest).toBe(true);
expect(props.confidential).toEqual(false);
expect(props.title).toEqual(data.title);
expect(props.state).toEqual(data.state);
expect(props.createdAt).toEqual(data.created_at);
});
});
});
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import * as types from '~/related_merge_requests/store/mutation_types';
import actionsModule, * as actions from '~/related_merge_requests/store/actions';
import testAction from 'spec/helpers/vuex_action_helper';
describe('RelatedMergeRequest store actions', () => {
let state;
let flashSpy;
let mock;
beforeEach(() => {
state = {
apiEndpoint: '/api/related_merge_requests',
};
flashSpy = spyOnDependency(actionsModule, 'createFlash');
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('setInitialState', () => {
it('commits types.SET_INITIAL_STATE with given props', done => {
const props = { a: 1, b: 2 };
testAction(
actions.setInitialState,
props,
{},
[{ type: types.SET_INITIAL_STATE, payload: props }],
[],
done,
);
});
});
describe('requestData', () => {
it('commits types.REQUEST_DATA', done => {
testAction(actions.requestData, null, {}, [{ type: types.REQUEST_DATA }], [], done);
});
});
describe('receiveDataSuccess', () => {
it('commits types.RECEIVE_DATA_SUCCESS with data', done => {
const data = { a: 1, b: 2 };
testAction(
actions.receiveDataSuccess,
data,
{},
[{ type: types.RECEIVE_DATA_SUCCESS, payload: data }],
[],
done,
);
});
});
describe('receiveDataError', () => {
it('commits types.RECEIVE_DATA_ERROR', done => {
testAction(
actions.receiveDataError,
null,
{},
[{ type: types.RECEIVE_DATA_ERROR }],
[],
done,
);
});
});
describe('fetchMergeRequests', () => {
describe('for a successful request', () => {
it('should dispatch success action', done => {
const data = { a: 1 };
mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
testAction(
actions.fetchMergeRequests,
null,
state,
[],
[{ type: 'requestData' }, { type: 'receiveDataSuccess', payload: { data, total: 2 } }],
done,
);
});
});
describe('for a failing request', () => {
it('should dispatch error action', done => {
mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
testAction(
actions.fetchMergeRequests,
null,
state,
[],
[{ type: 'requestData' }, { type: 'receiveDataError' }],
() => {
expect(flashSpy).toHaveBeenCalledTimes(1);
expect(flashSpy).toHaveBeenCalledWith(jasmine.stringMatching('Something went wrong'));
done();
},
);
});
});
});
});
import mutations from '~/related_merge_requests/store/mutations';
import * as types from '~/related_merge_requests/store/mutation_types';
describe('RelatedMergeRequests Store Mutations', () => {
describe('SET_INITIAL_STATE', () => {
it('should set initial state according to given data', () => {
const apiEndpoint = '/api';
const state = {};
mutations[types.SET_INITIAL_STATE](state, { apiEndpoint });
expect(state.apiEndpoint).toEqual(apiEndpoint);
});
});
describe('REQUEST_DATA', () => {
it('should set loading flag', () => {
const state = {};
mutations[types.REQUEST_DATA](state);
expect(state.isFetchingMergeRequests).toEqual(true);
});
});
describe('RECEIVE_DATA_SUCCESS', () => {
it('should set loading flag and data', () => {
const state = {};
const mrs = [1, 2, 3];
mutations[types.RECEIVE_DATA_SUCCESS](state, { data: mrs, total: mrs.length });
expect(state.isFetchingMergeRequests).toEqual(false);
expect(state.mergeRequests).toEqual(mrs);
expect(state.totalCount).toEqual(mrs.length);
});
});
describe('RECEIVE_DATA_ERROR', () => {
it('should set loading and error flags', () => {
const state = {};
mutations[types.RECEIVE_DATA_ERROR](state);
expect(state.isFetchingMergeRequests).toEqual(false);
expect(state.hasErrorFetchingMergeRequests).toEqual(true);
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment