Commit 5f68a904 authored by Phil Hughes's avatar Phil Hughes

Merge branch '7750-epic-app-refactor' into 'master'

Refactored Epic app shell

Closes #7750

See merge request gitlab-org/gitlab-ee!8059
parents 96f84698 d9bb01ad
<script>
import EpicHeader from './epic_header.vue';
import EpicBody from './epic_body.vue';
export default {
components: {
EpicHeader,
EpicBody,
},
};
</script>
<template>
<div class="epic-page-container">
<epic-header/>
<epic-body/>
</div>
</template>
<script>
import { mapState } from 'vuex';
import IssuableBody from '~/issue_show/components/app.vue';
import RelatedIssues from 'ee/related_issues/components/related_issues_root.vue';
export default {
components: {
IssuableBody,
RelatedIssues,
},
computed: {
...mapState([
'endpoint',
'updateEndpoint',
'issueLinksEndpoint',
'groupPath',
'markdownPreviewPath',
'markdownDocsPath',
'canUpdate',
'canDestroy',
'canAdmin',
'initialTitleHtml',
'initialTitleText',
'initialDescriptionHtml',
'initialDescriptionText',
]),
},
};
</script>
<template>
<div class="issuable-details content-block">
<div class="detail-page-description">
<issuable-body
:endpoint="endpoint"
:update-endpoint="updateEndpoint"
:project-path="groupPath"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="markdownDocsPath"
:can-update="canUpdate"
:can-destroy="canDestroy"
:show-delete-button="canDestroy"
:initial-title-html="initialTitleHtml"
:initial-title-text="initialTitleText"
:initial-description-html="initialDescriptionHtml"
:initial-description-text="initialDescriptionText"
:show-inline-edit-button="true"
:enable-autocomplete="true"
project-namespace=""
issuable-ref=""
issuable-type="epic"
/>
</div>
<related-issues
:endpoint="issueLinksEndpoint"
:can-admin="canAdmin"
:can-reorder="canAdmin"
:allow-auto-complete="false"
title="Issues"
/>
</div>
</template>
<script>
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
import tooltip from '~/vue_shared/directives/tooltip';
import Icon from '~/vue_shared/components/icon.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import epicUtils from '../utils/epic_utils';
import { statusType } from '../constants';
export default {
directives: {
tooltip,
},
components: {
Icon,
LoadingButton,
UserAvatarLink,
TimeagoTooltip,
},
computed: {
...mapState([
'epicDeleteInProgress',
'epicStatusChangeInProgress',
'author',
'created',
'canUpdate',
]),
...mapGetters(['isEpicOpen']),
statusIcon() {
return this.isEpicOpen ? 'issue-open-m' : 'mobile-issue-close';
},
statusText() {
return this.isEpicOpen ? __('Open') : __('Closed');
},
actionButtonClass() {
return `btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button ${
this.isEpicOpen ? 'btn-close' : 'btn-open'
}`;
},
actionButtonText() {
return this.isEpicOpen ? __('Close epic') : __('Reopen epic');
},
},
mounted() {
/**
* This event is triggered from Notes app
* when user clicks on `Close` button below
* comment form.
*
* When event is triggered, we want to reflect Epic status change
* across the UI so we directly call `requestEpicStatusChangeSuccess` action
* to update store state.
*/
epicUtils.bindDocumentEvent('issuable_vue_app:change', (e, isClosed) => {
const isEpicOpen = e.detail ? !e.detail.isClosed : !isClosed;
this.requestEpicStatusChangeSuccess({
state: isEpicOpen ? statusType.open : statusType.close,
});
});
},
methods: {
...mapActions(['requestEpicStatusChangeSuccess', 'toggleEpicStatus']),
},
};
</script>
<template>
<div class="detail-page-header">
<div class="detail-page-header-body">
<div
:class="{ 'status-box-open': isEpicOpen, 'status-box-issue-closed': !isEpicOpen }"
class="issuable-status-box status-box"
>
<icon
:name="statusIcon"
css-classes="d-block d-sm-none"
/>
<span class="d-none d-sm-block">{{ statusText }}</span>
</div>
<div class="issuable-meta">
{{ __('Opened') }}
<timeago-tooltip :time="created" />
{{ __('by') }}
<strong>
<user-avatar-link
:link-href="author.url"
:img-src="author.src"
:img-size="24"
:tooltip-text="author.username"
:username="author.name"
img-css-classes="avatar-inline"
/>
</strong>
</div>
</div>
<div
v-if="canUpdate"
class="detail-page-header-actions js-issuable-actions"
>
<loading-button
:label="actionButtonText"
:loading="epicStatusChangeInProgress"
:container-class="actionButtonClass"
@click="toggleEpicStatus(isEpicOpen)"
/>
</div>
</div>
</template>
export const statusType = {
open: 'opened',
close: 'closed',
};
export const statusEvent = {
close: 'close',
reopen: 'reopen',
};
import Vue from 'vue';
import { mapActions } from 'vuex';
import Cookies from 'js-cookie';
import bp from '~/breakpoints';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createStore from './store';
import EpicApp from './components/epic_app.vue';
export default () => {
const el = document.getElementById('epic-app-root');
const epicMeta = convertObjectPropsToCamelCase(JSON.parse(el.dataset.meta), { deep: true });
const epicData = JSON.parse(el.dataset.initial);
const store = createStore();
// Collapse the sidebar on mobile screens by default
const bpBreakpoint = bp.getBreakpointSize();
if (bpBreakpoint === 'xs' || bpBreakpoint === 'sm') {
Cookies.set('collapsed_gutter', true);
}
return new Vue({
el,
store,
components: { EpicApp },
created() {
this.setEpicMeta(epicMeta);
this.setEpicData(epicData);
},
methods: {
...mapActions(['setEpicMeta', 'setEpicData']),
},
render: createElement => createElement('epic-app'),
});
};
import flash from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import epicUtils from '../utils/epic_utils';
import { statusType, statusEvent } from '../constants';
import * as types from './mutation_types';
export const setEpicMeta = ({ commit }, meta) => commit(types.SET_EPIC_META, meta);
export const setEpicData = ({ commit }, data) => commit(types.SET_EPIC_DATA, data);
export const requestEpicStatusChange = ({ commit }) => commit(types.REQUEST_EPIC_STATUS_CHANGE);
export const requestEpicStatusChangeSuccess = ({ commit }, data) =>
commit(types.REQUEST_EPIC_STATUS_CHANGE_SUCCESS, data);
export const requestEpicStatusChangeFailure = ({ commit }) => {
commit(types.REQUEST_EPIC_STATUS_CHANGE_FAILURE);
flash(__('Unable to update this epic at this time.'));
};
export const triggerIssuableEvent = (_, { isEpicOpen }) => {
// Ensure that status change is reflected across the page.
// As `Close`/`Reopen` button is also present under
// comment form (part of Notes app) We've wrapped
// call to `$(document).trigger` within `triggerDocumentEvent`
// for ease of testing
epicUtils.triggerDocumentEvent('issuable_vue_app:change', isEpicOpen);
epicUtils.triggerDocumentEvent('issuable:change', isEpicOpen);
};
export const toggleEpicStatus = ({ state, dispatch }, isEpicOpen) => {
dispatch('requestEpicStatusChange');
const statusEventType = isEpicOpen ? statusEvent.close : statusEvent.reopen;
const queryParam = `epic[state_event]=${statusEventType}`;
axios
.put(`${state.endpoint}.json?${encodeURI(queryParam)}`)
.then(({ data }) => {
dispatch('requestEpicStatusChangeSuccess', data);
dispatch('triggerIssuableEvent', { isEpicOpen: data.state === statusType.close });
})
.catch(() => {
dispatch('requestEpicStatusChangeFailure');
dispatch('triggerIssuableEvent', { isEpicOpen: !isEpicOpen });
});
};
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
import { statusType } from '../constants';
export const isEpicOpen = state => state.state === statusType.open;
// prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {};
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);
const createStore = () =>
new Vuex.Store({
actions,
getters,
mutations,
state,
});
export default createStore;
export const SET_EPIC_META = 'SET_EPIC_META';
export const SET_EPIC_DATA = 'SET_EPIC_DATA';
export const REQUEST_EPIC_STATUS_CHANGE = 'REQUEST_EPIC_STATUS_CHANGE';
export const REQUEST_EPIC_STATUS_CHANGE_SUCCESS = 'REQUEST_EPIC_STATUS_CHANGE_SUCCESS';
export const REQUEST_EPIC_STATUS_CHANGE_FAILURE = 'REQUEST_EPIC_STATUS_CHANGE_FAILURE';
export const TRIGGER_ISSUABLE_EVENTS = 'TRIGGER_ISSUABLE_EVENTS';
import * as types from './mutation_types';
export default {
[types.SET_EPIC_META](state, meta) {
Object.assign(state, { ...meta });
},
[types.SET_EPIC_DATA](state, data) {
Object.assign(state, { ...data });
},
[types.REQUEST_EPIC_STATUS_CHANGE](state) {
state.epicStatusChangeInProgress = true;
},
[types.REQUEST_EPIC_STATUS_CHANGE_SUCCESS](state, data) {
state.state = data.state;
state.epicStatusChangeInProgress = false;
},
[types.REQUEST_EPIC_STATUS_CHANGE_FAILURE](state) {
state.epicStatusChangeInProgress = false;
},
};
export default {
// API Paths to Send/Receive Data
endpoint: '',
updateEndpoint: '',
issueLinksEndpoint: '',
groupPath: '',
markdownPreviewPath: '',
labelsPath: '',
todoPath: '',
todoDeletePath: '',
toggleSubscriptionPath: '',
// URLs to use with links
epicsWebUrl: '',
labelsWebUrl: '',
markdownDocsPath: '',
// Flags
canUpdate: false,
canDestroy: false,
canAdmin: false,
// Epic Information
epicId: 0,
state: '',
created: '',
author: null,
initialTitleHtml: '',
initialTitleText: '',
initialDescriptionHtml: '',
initialDescriptionText: '',
todoExists: false,
startDateSourcingMilestoneTitle: '',
startDateIsFixed: false,
startDateFixed: '',
startDateFromMilestones: '',
startDate: '',
dueDateSourcingMilestoneTitle: '',
dueDateIsFixed: '',
dueDateFixed: '',
dueDateFromMilestones: '',
dueDate: '',
labels: [],
participants: [],
subscribed: false,
// UI status flags
epicStatusChangeInProgress: false,
epicDeleteInProgress: false,
};
import $ from 'jquery';
const triggerDocumentEvent = (eventName, eventParam) => {
$(document).trigger(eventName, eventParam);
};
const bindDocumentEvent = (eventName, callback) => {
$(document).on(eventName, callback);
};
// This is for mocking methods from this
// file within tests using `spyOnDependency`
// which requires first param to always
// be default export of dependency as per
// https://gitlab.com/help/development/testing_guide/frontend_testing.md#stubbing-and-mocking
const epicUtils = {
triggerDocumentEvent,
bindDocumentEvent,
};
export default epicUtils;
import ZenMode from '~/zen_mode';
import Cookies from 'js-cookie';
import initEpicShow from 'ee/epics/epic_show/epic_show_bundle';
import ShortcutsEpic from 'ee/behaviors/shortcuts/shortcuts_epic';
import initEpicApp from 'ee/epic/epic_bundle';
import '~/notes/index';
document.addEventListener('DOMContentLoaded', () => {
new ZenMode(); // eslint-disable-line no-new
initEpicShow();
new ShortcutsEpic(); // eslint-disable-line no-new
if (Cookies.get('load_new_epic_app') === 'true') {
initEpicApp();
} else {
initEpicShow();
new ShortcutsEpic(); // eslint-disable-line no-new
}
});
module EpicsHelper
def epic_show_app_data(epic, opts)
author = epic.author
group = epic.group
todo = issuable_todo(epic)
epic_meta = {
epic_id: epic.id,
created: epic.created_at,
author: {
name: author.name,
url: user_path(author),
username: "@#{author.username}",
src: opts[:author_icon]
},
author: epic_author(epic, opts),
todo_exists: todo.present?,
todo_path: group_todos_path(group),
start_date: epic.start_date,
......@@ -34,7 +28,12 @@ module EpicsHelper
due_date: epic.due_date_sourcing_milestone&.due_date
},
end_date: epic.end_date,
state: epic.state
state: epic.state,
namespace: group.path,
labels_path: group_labels_path(group, format: :json, only_group_labels: true, include_ancestor_groups: true),
toggle_subscription_path: toggle_subscription_group_epic_path(group, epic),
labels_web_url: group_labels_path(group),
epics_web_url: group_epics_path(group)
}
epic_meta[:todo_delete_path] = dashboard_todo_path(todo) if todo.present?
......@@ -44,6 +43,8 @@ module EpicsHelper
participants: participants,
subscribed: epic.subscribed?(current_user))
# TODO: Remove from `namespace` to epics_web_url
# from below as it is already included in `epic_meta`
{
initial: initial.to_json,
meta: epic_meta.to_json,
......@@ -55,6 +56,15 @@ module EpicsHelper
}
end
def epic_author(epic, opts)
{
name: epic.author.name,
url: user_path(epic.author),
username: "@#{epic.author.username}",
src: opts[:author_icon]
}
end
def epic_endpoint_query_params(opts)
opts[:data] ||= {}
opts[:data][:endpoint_query_params] = {
......
......@@ -11,7 +11,10 @@
- page_card_attributes @epic.card_attributes
#epic-show-app{ data: epic_show_app_data(@epic, author_icon: avatar_icon_for_user(@epic.author), initial: issuable_initial_data(@epic)) }
- if cookies[:load_new_epic_app] == 'true'
#epic-app-root{ data: epic_show_app_data(@epic, author_icon: avatar_icon_for_user(@epic.author), initial: issuable_initial_data(@epic)) }
- else
#epic-show-app{ data: epic_show_app_data(@epic, author_icon: avatar_icon_for_user(@epic.author), initial: issuable_initial_data(@epic)) }
.content-block.emoji-block
.row
......
......@@ -30,10 +30,12 @@ describe EpicsHelper do
expected_keys = %i(initial meta namespace labels_path toggle_subscription_path labels_web_url epics_web_url)
expect(data.keys).to match_array(expected_keys)
expect(meta_data.keys).to match_array(%w[
created author epic_id todo_exists todo_path state
start_date start_date_fixed start_date_is_fixed start_date_from_milestones start_date_sourcing_milestone_title
end_date due_date due_date_fixed due_date_is_fixed due_date_from_milestones due_date_sourcing_milestone_title
start_date_sourcing_milestone_dates due_date_sourcing_milestone_dates
epic_id created author todo_exists todo_path start_date
start_date_is_fixed start_date_fixed start_date_from_milestones
start_date_sourcing_milestone_title start_date_sourcing_milestone_dates
due_date due_date_is_fixed due_date_fixed due_date_from_milestones due_date_sourcing_milestone_title
due_date_sourcing_milestone_dates end_date state namespace labels_path toggle_subscription_path
labels_web_url epics_web_url
])
expect(meta_data['author']).to eq({
'name' => user.name,
......@@ -74,15 +76,21 @@ describe EpicsHelper do
meta_data = JSON.parse(data[:meta])
expect(meta_data.keys).to match_array(%w[
created author epic_id todo_exists todo_path state
start_date start_date_fixed start_date_is_fixed start_date_from_milestones start_date_sourcing_milestone_title
end_date due_date due_date_fixed due_date_is_fixed due_date_from_milestones due_date_sourcing_milestone_title
start_date_sourcing_milestone_dates due_date_sourcing_milestone_dates
epic_id created author todo_exists todo_path start_date
start_date_is_fixed start_date_fixed start_date_from_milestones
start_date_sourcing_milestone_title start_date_sourcing_milestone_dates
due_date due_date_is_fixed due_date_fixed due_date_from_milestones due_date_sourcing_milestone_title
due_date_sourcing_milestone_dates end_date state namespace labels_path toggle_subscription_path
labels_web_url epics_web_url
])
expect(meta_data['start_date']).to eq('2000-01-01')
expect(meta_data['start_date_sourcing_milestone_title']).to eq(milestone1.title)
expect(meta_data['start_date_sourcing_milestone_dates']['start_date']).to eq(milestone1.start_date.to_s)
expect(meta_data['start_date_sourcing_milestone_dates']['due_date']).to eq(milestone1.due_date.to_s)
expect(meta_data['due_date']).to eq('2000-01-02')
expect(meta_data['due_date_sourcing_milestone_title']).to eq(milestone2.title)
expect(meta_data['due_date_sourcing_milestone_dates']['start_date']).to eq(milestone2.start_date.to_s)
expect(meta_data['due_date_sourcing_milestone_dates']['due_date']).to eq(milestone2.due_date.to_s)
end
end
end
......
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import EpicApp from 'ee/epic/components/epic_app.vue';
import createStore from 'ee/epic/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import issueShowData from 'spec/issue_show/mock_data';
import { TEST_HOST } from 'spec/test_constants';
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('EpicAppComponent', () => {
let vm;
let mock;
beforeEach(done => {
mock = new MockAdapter(axios);
mock.onGet(`${TEST_HOST}/realtime_changes`).reply(200, issueShowData.initialRequest);
const Component = Vue.extend(EpicApp);
const store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, {
store,
});
setTimeout(done);
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('template', () => {
it('renders component container element with class `epic-page-container`', () => {
expect(vm.$el.classList.contains('epic-page-container')).toBe(true);
});
});
});
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import EpicBody from 'ee/epic/components/epic_body.vue';
import createStore from 'ee/epic/store';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import issueShowData from 'spec/issue_show/mock_data';
import { TEST_HOST } from 'spec/test_constants';
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('EpicBodyComponent', () => {
let vm;
let mock;
beforeEach(done => {
mock = new MockAdapter(axios);
mock.onGet(`${TEST_HOST}/realtime_changes`).reply(200, issueShowData.initialRequest);
const Component = Vue.extend(EpicBody);
const store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, {
store,
});
setTimeout(done);
});
afterEach(() => {
mock.restore();
vm.$destroy();
});
describe('template', () => {
it('renders component container element with classes `issuable-details` & `content-block`', () => {
expect(vm.$el.classList.contains('issuable-details')).toBe(true);
expect(vm.$el.classList.contains('content-block')).toBe(true);
});
it('renders epic body container element with class `detail-page-description`', () => {
expect(vm.$el.querySelector('.detail-page-description')).not.toBeNull();
});
it('renders epic body elements', () => {
expect(vm.$el.querySelector('.title-container')).not.toBeNull();
expect(vm.$el.querySelector('.description')).not.toBeNull();
});
it('renders related issues list elements', () => {
expect(vm.$el.querySelector('.related-issues-block')).not.toBeNull();
expect(vm.$el.querySelector('.js-related-issues-header-issue-count')).not.toBeNull();
expect(vm.$el.querySelector('.related-issues-token-body')).not.toBeNull();
expect(vm.$el.querySelector('.issuable-list')).not.toBeNull();
});
});
});
import Vue from 'vue';
import EpicHeader from 'ee/epic/components/epic_header.vue';
import createStore from 'ee/epic/store';
import { statusType } from 'ee/epic/constants';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('EpicBodyComponent', () => {
let vm;
let store;
beforeEach(done => {
const Component = Vue.extend(EpicHeader);
store = createStore();
store.dispatch('setEpicMeta', mockEpicMeta);
store.dispatch('setEpicData', mockEpicData);
vm = mountComponentWithStore(Component, {
store,
});
setTimeout(done);
});
afterEach(() => {
vm.$destroy();
});
describe('computed', () => {
describe('statusIcon', () => {
it('returns string `issue-open-m` when `isEpicOpen` is true', () => {
vm.$store.state.state = statusType.open;
expect(vm.statusIcon).toBe('issue-open-m');
});
it('returns string `mobile-issue-close` when `isEpicOpen` is false', () => {
vm.$store.state.state = statusType.close;
expect(vm.statusIcon).toBe('mobile-issue-close');
});
});
describe('statusText', () => {
it('returns string `Open` when `isEpicOpen` is true', () => {
vm.$store.state.state = statusType.open;
expect(vm.statusText).toBe('Open');
});
it('returns string `Closed` when `isEpicOpen` is false', () => {
vm.$store.state.state = statusType.close;
expect(vm.statusText).toBe('Closed');
});
});
describe('actionButtonClass', () => {
it('returns default button classes along with `btn-close` when `isEpicOpen` is true', () => {
vm.$store.state.state = statusType.open;
expect(vm.actionButtonClass).toBe(
'btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button btn-close',
);
});
it('returns default button classes along with `btn-open` when `isEpicOpen` is false', () => {
vm.$store.state.state = statusType.close;
expect(vm.actionButtonClass).toBe(
'btn btn-grouped js-btn-epic-action qa-close-reopen-epic-button btn-open',
);
});
});
describe('actionButtonText', () => {
it('returns string `Close epic` when `isEpicOpen` is true', () => {
vm.$store.state.state = statusType.open;
expect(vm.actionButtonText).toBe('Close epic');
});
it('returns string `Reopen epic` when `isEpicOpen` is false', () => {
vm.$store.state.state = statusType.close;
expect(vm.actionButtonText).toBe('Reopen epic');
});
});
});
describe('template', () => {
it('renders component container element with class `detail-page-header`', () => {
expect(vm.$el.classList.contains('detail-page-header')).toBe(true);
expect(vm.$el.querySelector('.detail-page-header-body')).not.toBeNull();
});
it('renders epic status icon and text elements', () => {
const statusEl = vm.$el.querySelector('.issuable-status-box');
expect(statusEl).not.toBeNull();
expect(
statusEl.querySelector('svg.ic-issue-open-m use').getAttribute('xlink:href'),
).toContain('issue-open-m');
expect(statusEl.querySelector('span').innerText.trim()).toBe('Open');
});
it('renders epic author details element', () => {
const metaEl = vm.$el.querySelector('.issuable-meta');
expect(metaEl).not.toBeNull();
expect(metaEl.querySelector('strong a.user-avatar-link')).not.toBeNull();
});
it('renders action buttons element', () => {
const actionsEl = vm.$el.querySelector('.js-issuable-actions');
expect(actionsEl).not.toBeNull();
expect(actionsEl.querySelector('.js-btn-epic-action')).not.toBeNull();
expect(actionsEl.querySelector('.js-loading-button-label').innerText.trim()).toBe(
'Close epic',
);
});
});
});
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { TEST_HOST } from 'spec/test_constants';
const metaFixture = getJSONFixture('epic/mock_meta.json');
const meta = JSON.parse(metaFixture.meta);
const initial = JSON.parse(metaFixture.initial);
export const mockEpicMeta = convertObjectPropsToCamelCase(meta, {
deep: true,
});
export const mockEpicData = convertObjectPropsToCamelCase(
Object.assign({}, getJSONFixture('epic/mock_data.json'), initial, { endpoint: TEST_HOST }),
{ deep: true },
);
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import defaultState from 'ee/epic/store/state';
import * as actions from 'ee/epic/store/actions';
import epicUtils from 'ee/epic/utils/epic_utils';
import { statusType } from 'ee/epic/constants';
import axios from '~/lib/utils/axios_utils';
import testAction from 'spec/helpers/vuex_action_helper';
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('Epic Store Actions', () => {
let state;
beforeEach(() => {
state = Object.assign({}, defaultState);
});
describe('setEpicMeta', () => {
it('should set received Epic meta', done => {
testAction(
actions.setEpicMeta,
mockEpicMeta,
{},
[{ type: 'SET_EPIC_META', payload: mockEpicMeta }],
[],
done,
);
});
});
describe('setEpicData', () => {
it('should set received Epic data', done => {
testAction(
actions.setEpicData,
mockEpicData,
{},
[{ type: 'SET_EPIC_DATA', payload: mockEpicData }],
[],
done,
);
});
});
describe('requestEpicStatusChange', () => {
it('should set status change flag', done => {
testAction(
actions.requestEpicStatusChange,
{},
state,
[{ type: 'REQUEST_EPIC_STATUS_CHANGE' }],
[],
done,
);
});
});
describe('requestEpicStatusChangeSuccess', () => {
it('should set epic state type', done => {
testAction(
actions.requestEpicStatusChangeSuccess,
{ state: statusType.close },
state,
[{ type: 'REQUEST_EPIC_STATUS_CHANGE_SUCCESS', payload: { state: statusType.close } }],
[],
done,
);
});
});
describe('requestEpicStatusChangeFailure', () => {
beforeEach(() => {
setFixtures('<div class="flash-container"></div>');
});
it('should set status change flag', done => {
testAction(
actions.requestEpicStatusChangeFailure,
{},
state,
[{ type: 'REQUEST_EPIC_STATUS_CHANGE_FAILURE' }],
[],
done,
);
});
it('should show flash error', done => {
actions.requestEpicStatusChangeFailure({ commit: () => {} });
Vue.nextTick()
.then(() => {
expect(document.querySelector('.flash-container .flash-text').innerText.trim()).toBe(
'Unable to update this epic at this time.',
);
})
.then(done)
.catch(done.fail);
});
});
describe('triggerIssuableEvent', () => {
it('Calls `triggerDocumentEvent` with events `issuable_vue_app:change`, `issuable:change` and passes `isEpicOpen` as param', () => {
spyOn(epicUtils, 'triggerDocumentEvent').and.returnValue(false);
const data = { isEpicOpen: true };
actions.triggerIssuableEvent({}, data);
expect(epicUtils.triggerDocumentEvent).toHaveBeenCalledWith(
'issuable_vue_app:change',
data.isEpicOpen,
);
expect(epicUtils.triggerDocumentEvent).toHaveBeenCalledWith(
'issuable:change',
data.isEpicOpen,
);
});
});
describe('toggleEpicStatus', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('success', () => {
it('dispatches requestEpicStatusChange and requestEpicStatusChangeSuccess when request is complete', done => {
mock.onPut(/(.*)/).replyOnce(200, {
state: statusType.close,
});
testAction(
actions.toggleEpicStatus,
null,
state,
[],
[
{
type: 'requestEpicStatusChange',
},
{
type: 'requestEpicStatusChangeSuccess',
payload: { state: statusType.close },
},
{
type: 'triggerIssuableEvent',
payload: { isEpicOpen: true },
},
],
done,
);
});
});
describe('failure', () => {
it('dispatches requestEpicStatusChange and requestEpicStatusChangeFailure when request fails', done => {
mock.onPut(/(.*)/).replyOnce(500, {});
testAction(
actions.toggleEpicStatus,
null,
state,
[],
[
{
type: 'requestEpicStatusChange',
},
{
type: 'requestEpicStatusChangeFailure',
},
{
type: 'triggerIssuableEvent',
payload: { isEpicOpen: true },
},
],
done,
);
});
});
});
});
import * as getters from 'ee/epic/store/getters';
import { statusType } from 'ee/epic/constants';
describe('Epic Store Getters', () => {
describe('isEpicOpen', () => {
it('returns `true` when Epic `state` is `opened`', () => {
const epicState = {
state: statusType.open,
};
expect(getters.isEpicOpen(epicState)).toBe(true);
});
it('returns `false` when Epic `state` is `closed`', () => {
const epicState = {
state: statusType.closed,
};
expect(getters.isEpicOpen(epicState)).toBe(false);
});
});
});
import mutations from 'ee/epic/store/mutations';
import * as types from 'ee/epic/store/mutation_types';
import { mockEpicMeta, mockEpicData } from '../mock_data';
describe('Epic Store Mutations', () => {
describe('SET_EPIC_META', () => {
it('Should add Epic meta to state', () => {
const state = {};
mutations[types.SET_EPIC_META](state, mockEpicMeta);
expect(state).toEqual(mockEpicMeta);
});
});
describe('SET_EPIC_DATA', () => {
it('Should add Epic data to state', () => {
const state = {};
mutations[types.SET_EPIC_DATA](state, mockEpicData);
expect(state).toEqual(mockEpicData);
});
});
describe('REQUEST_EPIC_STATUS_CHANGE', () => {
it('Should set `epicStatusChangeInProgress` flag on state as `true`', () => {
const state = {};
mutations[types.REQUEST_EPIC_STATUS_CHANGE](state);
expect(state.epicStatusChangeInProgress).toBe(true);
});
});
describe('REQUEST_EPIC_STATUS_CHANGE_SUCCESS', () => {
it('Should set `epicStatusChangeInProgress` flag on state as `false` and update Epic `state`', () => {
const state = {
state: 'opened',
};
mutations[types.REQUEST_EPIC_STATUS_CHANGE_SUCCESS](state, { state: 'closed' });
expect(state.epicStatusChangeInProgress).toBe(false);
expect(state.state).toBe('closed');
});
});
describe('REQUEST_EPIC_STATUS_CHANGE_FAILURE', () => {
it('Should set `epicStatusChangeInProgress` flag on state as `false`', () => {
const state = {};
mutations[types.REQUEST_EPIC_STATUS_CHANGE_FAILURE](state);
expect(state.epicStatusChangeInProgress).toBe(false);
});
});
});
# frozen_string_literal: true
require 'spec_helper'
describe 'Epics (JavaScript fixtures)' do
include ApplicationHelper
include JavaScriptFixturesHelpers
let(:user) { create(:user) }
let(:group) { create(:group, name: 'frontend-fixtures-group' )}
let(:label) { create(:group_label, group: group, title: 'bug') }
let(:public_project) { create(:project, :public, group: group) }
let(:milestone1) { create(:milestone, group: group, title: 'Decade A', start_date: '2010-01-01', due_date: '2019-12-31')}
let(:milestone2) { create(:milestone, group: group, title: 'Decade B', start_date: '2020-01-01', due_date: '2029-12-31')}
let(:issue1) { create(:issue, project: public_project, milestone: milestone1)}
let(:issue2) { create(:issue, project: public_project, milestone: milestone2)}
let(:markdown) do
<<-MARKDOWN.strip_heredoc
This is an Epic description.
This is a task list:
- [ ] Incomplete entry 1
MARKDOWN
end
let(:epic) { create(:epic, group: group, title: 'This is a sample epic', description: markdown, start_date_fixed: '2018-06-01', due_date_fixed: '2018-08-01') }
let!(:epic_issues) do
[
create(:epic_issue, epic: epic, issue: issue1, relative_position: 1),
create(:epic_issue, epic: epic, issue: issue2, relative_position: 2)
]
end
before(:all) do
clean_frontend_fixtures('epic/')
end
describe EpicsHelper, '(JavaScript fixtures)', type: :helper do
before do
allow(helper).to receive(:current_user).and_return(user)
end
it 'epic/mock_meta.json' do |example|
result = helper.epic_show_app_data(epic, initial: {}, author_icon: 'icon_path')
store_frontend_fixture(result.to_json, example.description)
end
end
describe IssuablesHelper, '(JavaScript fixtures)', type: :helper do
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:can?).and_return(true)
end
it 'epic/mock_data.json' do |example|
@group = epic.group
result = helper.issuable_initial_data(epic)
store_frontend_fixture(result.to_json, example.description)
end
end
end
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