Commit 73ed71f5 authored by Kushal Pandya's avatar Kushal Pandya

Roadmap App Component

parent 84629f73
<script>
import _ from 'underscore';
import Flash from '~/flash';
import { s__ } from '~/locale';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import epicsListEmpty from './epics_list_empty.vue';
import roadmapShell from './roadmap_shell.vue';
export default {
components: {
loadingIcon,
epicsListEmpty,
roadmapShell,
},
props: {
store: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
emptyStateIllustrationPath: {
type: String,
required: true,
},
},
data() {
return {
isLoading: true,
isEpicsListEmpty: false,
hasError: false,
handleResizeThrottled: {},
};
},
computed: {
epics() {
return this.store.getEpics();
},
timeframe() {
return this.store.getTimeframe();
},
timeframeStart() {
return this.timeframe[0];
},
timeframeEnd() {
const last = this.timeframe.length - 1;
return this.timeframe[last];
},
currentGroupId() {
return this.store.getCurrentGroupId();
},
showRoadmap() {
return !this.hasError && !this.isLoading && !this.isEpicsListEmpty;
},
},
mounted() {
this.fetchEpics();
this.handleResizeThrottled = _.throttle(this.handleResize, 600);
window.addEventListener('resize', this.handleResizeThrottled, false);
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResizeThrottled, false);
},
methods: {
fetchEpics() {
this.hasError = false;
this.service.getEpics()
.then(res => res.data)
.then((epics) => {
this.isLoading = false;
if (epics.length) {
this.store.setEpics(epics);
} else {
this.isEpicsListEmpty = true;
}
})
.catch(() => {
this.isLoading = false;
this.hasError = true;
Flash(s__('GroupRoadmap|Something went wrong while fetching epics'));
});
},
/**
* Roadmap view works with absolute sizing and positioning
* of following child components of RoadmapShell;
*
* - RoadmapTimelineSection
* - TimelineTodayIndicator
* - EpicItemTimeline
*
* And hence when window is resized, any size attributes passed
* down to child components are no longer valid, so best approach
* to refresh entire app is to re-render it on resize, hence
* we toggle `isLoading` variable which is bound to `RoadmapShell`.
*/
handleResize() {
this.isLoading = true;
// We need to debounce the toggle to make sure loading animation
// shows up while app is being rerendered.
_.debounce(() => {
this.isLoading = false;
}, 200)();
},
},
};
</script>
<template>
<div class="roadmap-container">
<loading-icon
class="loading-animation prepend-top-20 append-bottom-20"
size="2"
v-if="isLoading"
:label="s__('GroupRoadmap|Loading roadmap')"
/>
<roadmap-shell
v-if="showRoadmap"
:epics="epics"
:timeframe="timeframe"
:current-group-id="currentGroupId"
/>
<epics-list-empty
v-if="isEpicsListEmpty"
:timeframe-start="timeframeStart"
:timeframe-end="timeframeEnd"
:empty-state-illustration-path="emptyStateIllustrationPath"
/>
</div>
</template>
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import appComponent from 'ee/roadmap/components/app.vue';
import RoadmapStore from 'ee/roadmap/store/roadmap_store';
import RoadmapService from 'ee/roadmap/service/roadmap_service';
import { mockTimeframe, mockGroupId, epicsPath, rawEpics, mockSvgPath } from '../mock_data';
import mountComponent from '../../helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(appComponent);
const timeframe = mockTimeframe;
const store = new RoadmapStore(mockGroupId, timeframe);
const service = new RoadmapService(epicsPath);
return mountComponent(Component, {
store,
service,
emptyStateIllustrationPath: mockSvgPath,
});
};
describe('AppComponent', () => {
let vm;
beforeEach(() => {
vm = createComponent();
});
afterEach(() => {
vm.$destroy();
});
describe('data', () => {
it('returns default data props', () => {
expect(vm.isLoading).toBe(true);
expect(vm.isEpicsListEmpty).toBe(false);
expect(vm.hasError).toBe(false);
expect(vm.handleResizeThrottled).toBeDefined();
});
});
describe('computed', () => {
describe('epics', () => {
it('returns array of epics', () => {
expect(Array.isArray(vm.epics)).toBe(true);
});
});
describe('timeframe', () => {
it('returns array of timeframe', () => {
expect(Array.isArray(vm.timeframe)).toBe(true);
});
});
describe('timeframeStart', () => {
it('returns first item of timeframe array', () => {
expect(vm.timeframeStart instanceof Date).toBe(true);
});
});
describe('timeframeEnd', () => {
it('returns last item of timeframe array', () => {
expect(vm.timeframeEnd instanceof Date).toBe(true);
});
});
describe('currentGroupId', () => {
it('returns current group Id', () => {
expect(vm.currentGroupId).toBe(mockGroupId);
});
});
describe('showRoadmap', () => {
it('returns true if `isLoading`, `isEpicsListEmpty` and `hasError` are all `false`', () => {
vm.isLoading = false;
vm.isEpicsListEmpty = false;
vm.hasError = false;
expect(vm.showRoadmap).toBe(true);
});
it('returns false if either of `isLoading`, `isEpicsListEmpty` and `hasError` is `true`', () => {
vm.isLoading = true;
vm.isEpicsListEmpty = false;
vm.hasError = false;
expect(vm.showRoadmap).toBe(false);
vm.isLoading = false;
vm.isEpicsListEmpty = true;
vm.hasError = false;
expect(vm.showRoadmap).toBe(false);
vm.isLoading = false;
vm.isEpicsListEmpty = false;
vm.hasError = true;
expect(vm.showRoadmap).toBe(false);
});
});
});
describe('methods', () => {
describe('fetchEpics', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
document.body.innerHTML += '<div class="flash-container"></div>';
});
afterEach(() => {
mock.restore();
document.querySelector('.flash-container').remove();
});
it('calls service.getEpics and sets response to the store on success', (done) => {
mock.onGet(vm.service.epicsPath).reply(200, rawEpics);
spyOn(vm.store, 'setEpics');
vm.fetchEpics();
expect(vm.hasError).toBe(false);
setTimeout(() => {
expect(vm.isLoading).toBe(false);
expect(vm.store.setEpics).toHaveBeenCalledWith(rawEpics);
done();
}, 0);
});
it('calls service.getEpics and sets `isEpicsListEmpty` to true if response is empty', (done) => {
mock.onGet(vm.service.epicsPath).reply(200, []);
spyOn(vm.store, 'setEpics');
vm.fetchEpics();
expect(vm.isEpicsListEmpty).toBe(false);
setTimeout(() => {
expect(vm.isEpicsListEmpty).toBe(true);
expect(vm.store.setEpics).not.toHaveBeenCalled();
done();
}, 0);
});
it('calls service.getEpics and sets `hasError` to true and shows flash message if request failed', (done) => {
mock.onGet(vm.service.epicsPath).reply(500, {});
vm.fetchEpics();
expect(vm.hasError).toBe(false);
setTimeout(() => {
expect(vm.hasError).toBe(true);
expect(document.querySelector('.flash-text').innerText.trim()).toBe('Something went wrong while fetching epics');
done();
}, 0);
});
});
});
describe('mounted', () => {
it('binds window resize event listener', () => {
spyOn(window, 'addEventListener');
const vmX = createComponent();
expect(vmX.handleResizeThrottled).toBeDefined();
expect(window.addEventListener).toHaveBeenCalledWith('resize', vmX.handleResizeThrottled, false);
vmX.$destroy();
});
});
describe('beforeDestroy', () => {
it('unbinds window resize event listener', () => {
spyOn(window, 'removeEventListener');
const vmX = createComponent();
vmX.$destroy();
expect(window.removeEventListener).toHaveBeenCalledWith('resize', vmX.handleResizeThrottled, false);
});
});
describe('template', () => {
it('renders roadmap container with class `roadmap-container`', () => {
expect(vm.$el.classList.contains('roadmap-container')).toBe(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