Merge branch 'ide-jobs-list-components' into 'master'
Show CI jobs in IDE Closes #44604 See merge request gitlab-org/gitlab-ce!19106
This commit is contained in:
commit
5b9e4d986a
44 changed files with 1577 additions and 553 deletions
|
@ -24,8 +24,6 @@ const Api = {
|
|||
commitPipelinesPath: '/:project_id/commit/:sha/pipelines',
|
||||
branchSinglePath: '/api/:version/projects/:id/repository/branches/:branch',
|
||||
createBranchPath: '/api/:version/projects/:id/repository/branches',
|
||||
pipelinesPath: '/api/:version/projects/:id/pipelines',
|
||||
pipelineJobsPath: '/api/:version/projects/:id/pipelines/:pipeline_id/jobs',
|
||||
|
||||
group(groupId, callback) {
|
||||
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
|
||||
|
@ -238,20 +236,6 @@ const Api = {
|
|||
});
|
||||
},
|
||||
|
||||
pipelines(projectPath, params = {}) {
|
||||
const url = Api.buildUrl(this.pipelinesPath).replace(':id', encodeURIComponent(projectPath));
|
||||
|
||||
return axios.get(url, { params });
|
||||
},
|
||||
|
||||
pipelineJobs(projectPath, pipelineId, params = {}) {
|
||||
const url = Api.buildUrl(this.pipelineJobsPath)
|
||||
.replace(':id', encodeURIComponent(projectPath))
|
||||
.replace(':pipeline_id', pipelineId);
|
||||
|
||||
return axios.get(url, { params });
|
||||
},
|
||||
|
||||
buildUrl(url) {
|
||||
let urlRoot = '';
|
||||
if (gon.relative_url_root != null) {
|
||||
|
|
|
@ -6,6 +6,7 @@ import RepoTabs from './repo_tabs.vue';
|
|||
import IdeStatusBar from './ide_status_bar.vue';
|
||||
import RepoEditor from './repo_editor.vue';
|
||||
import FindFile from './file_finder/index.vue';
|
||||
import RightPane from './panes/right.vue';
|
||||
|
||||
const originalStopCallback = Mousetrap.stopCallback;
|
||||
|
||||
|
@ -16,6 +17,7 @@ export default {
|
|||
IdeStatusBar,
|
||||
RepoEditor,
|
||||
FindFile,
|
||||
RightPane,
|
||||
},
|
||||
computed: {
|
||||
...mapState([
|
||||
|
@ -25,6 +27,7 @@ export default {
|
|||
'currentMergeRequestId',
|
||||
'fileFindVisible',
|
||||
'emptyStateSvgPath',
|
||||
'currentProjectId',
|
||||
]),
|
||||
...mapGetters(['activeFile', 'hasChanges']),
|
||||
},
|
||||
|
@ -122,6 +125,9 @@ export default {
|
|||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<right-pane
|
||||
v-if="currentProjectId"
|
||||
/>
|
||||
</div>
|
||||
<ide-status-bar :file="activeFile"/>
|
||||
</article>
|
||||
|
|
|
@ -31,6 +31,7 @@ export default {
|
|||
computed: {
|
||||
...mapState(['currentBranchId', 'currentProjectId']),
|
||||
...mapGetters(['currentProject', 'lastCommit']),
|
||||
...mapState('pipelines', ['latestPipeline']),
|
||||
},
|
||||
watch: {
|
||||
lastCommit() {
|
||||
|
@ -51,14 +52,14 @@ export default {
|
|||
}
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['pipelinePoll', 'stopPipelinePolling']),
|
||||
...mapActions('pipelines', ['fetchLatestPipeline', 'stopPipelinePolling']),
|
||||
startTimer() {
|
||||
this.intervalId = setInterval(() => {
|
||||
this.commitAgeUpdate();
|
||||
}, 1000);
|
||||
},
|
||||
initPipelinePolling() {
|
||||
this.pipelinePoll();
|
||||
this.fetchLatestPipeline();
|
||||
this.isPollingInitialized = true;
|
||||
},
|
||||
commitAgeUpdate() {
|
||||
|
@ -81,18 +82,18 @@ export default {
|
|||
>
|
||||
<span
|
||||
class="ide-status-pipeline"
|
||||
v-if="lastCommit.pipeline && lastCommit.pipeline.details"
|
||||
v-if="latestPipeline && latestPipeline.details"
|
||||
>
|
||||
<ci-icon
|
||||
:status="lastCommit.pipeline.details.status"
|
||||
:status="latestPipeline.details.status"
|
||||
v-tooltip
|
||||
:title="lastCommit.pipeline.details.status.text"
|
||||
:title="latestPipeline.details.status.text"
|
||||
/>
|
||||
Pipeline
|
||||
<a
|
||||
class="monospace"
|
||||
:href="lastCommit.pipeline.details.status.details_path">#{{ lastCommit.pipeline.id }}</a>
|
||||
{{ lastCommit.pipeline.details.status.text }}
|
||||
:href="latestPipeline.details.status.details_path">#{{ latestPipeline.id }}</a>
|
||||
{{ latestPipeline.details.status.text }}
|
||||
for
|
||||
</span>
|
||||
|
||||
|
|
46
app/assets/javascripts/ide/components/jobs/item.vue
Normal file
46
app/assets/javascripts/ide/components/jobs/item.vue
Normal file
|
@ -0,0 +1,46 @@
|
|||
<script>
|
||||
import Icon from '../../../vue_shared/components/icon.vue';
|
||||
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
CiIcon,
|
||||
},
|
||||
props: {
|
||||
job: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
jobId() {
|
||||
return `#${this.job.id}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-job-item">
|
||||
<ci-icon
|
||||
:status="job.status"
|
||||
:borderless="true"
|
||||
:size="24"
|
||||
/>
|
||||
<span class="prepend-left-8">
|
||||
{{ job.name }}
|
||||
<a
|
||||
:href="job.path"
|
||||
target="_blank"
|
||||
class="ide-external-link"
|
||||
>
|
||||
{{ jobId }}
|
||||
<icon
|
||||
name="external-link"
|
||||
:size="12"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
44
app/assets/javascripts/ide/components/jobs/list.vue
Normal file
44
app/assets/javascripts/ide/components/jobs/list.vue
Normal file
|
@ -0,0 +1,44 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
|
||||
import Stage from './stage.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingIcon,
|
||||
Stage,
|
||||
},
|
||||
props: {
|
||||
stages: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions('pipelines', ['fetchJobs', 'toggleStageCollapsed']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<loading-icon
|
||||
v-if="loading && !stages.length"
|
||||
class="prepend-top-default"
|
||||
size="2"
|
||||
/>
|
||||
<template v-else>
|
||||
<stage
|
||||
v-for="stage in stages"
|
||||
:key="stage.id"
|
||||
:stage="stage"
|
||||
@fetch="fetchJobs"
|
||||
@toggleCollapsed="toggleStageCollapsed"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
108
app/assets/javascripts/ide/components/jobs/stage.vue
Normal file
108
app/assets/javascripts/ide/components/jobs/stage.vue
Normal file
|
@ -0,0 +1,108 @@
|
|||
<script>
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
import Icon from '../../../vue_shared/components/icon.vue';
|
||||
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
|
||||
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
|
||||
import Item from './item.vue';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
CiIcon,
|
||||
LoadingIcon,
|
||||
Item,
|
||||
},
|
||||
props: {
|
||||
stage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTooltip: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
collapseIcon() {
|
||||
return this.stage.isCollapsed ? 'angle-left' : 'angle-down';
|
||||
},
|
||||
showLoadingIcon() {
|
||||
return this.stage.isLoading && !this.stage.jobs.length;
|
||||
},
|
||||
jobsCount() {
|
||||
return this.stage.jobs.length;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const { stageTitle } = this.$refs;
|
||||
|
||||
this.showTooltip = stageTitle.scrollWidth > stageTitle.offsetWidth;
|
||||
|
||||
this.$emit('fetch', this.stage);
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.$emit('toggleCollapsed', this.stage.id);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="ide-stage card prepend-top-default"
|
||||
>
|
||||
<div
|
||||
class="card-header"
|
||||
:class="{
|
||||
'border-bottom-0': stage.isCollapsed
|
||||
}"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
<ci-icon
|
||||
:status="stage.status"
|
||||
:size="24"
|
||||
/>
|
||||
<strong
|
||||
v-tooltip="showTooltip"
|
||||
:title="showTooltip ? stage.name : null"
|
||||
data-container="body"
|
||||
class="prepend-left-8 ide-stage-title"
|
||||
ref="stageTitle"
|
||||
>
|
||||
{{ stage.name }}
|
||||
</strong>
|
||||
<div
|
||||
v-if="!stage.isLoading || stage.jobs.length"
|
||||
class="append-right-8 prepend-left-4"
|
||||
>
|
||||
<span class="badge badge-pill">
|
||||
{{ jobsCount }}
|
||||
</span>
|
||||
</div>
|
||||
<icon
|
||||
:name="collapseIcon"
|
||||
css-classes="ide-stage-collapse-icon"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="card-body"
|
||||
v-show="!stage.isCollapsed"
|
||||
>
|
||||
<loading-icon
|
||||
v-if="showLoadingIcon"
|
||||
/>
|
||||
<template v-else>
|
||||
<item
|
||||
v-for="job in stage.jobs"
|
||||
:key="job.id"
|
||||
:job="job"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
65
app/assets/javascripts/ide/components/panes/right.vue
Normal file
65
app/assets/javascripts/ide/components/panes/right.vue
Normal file
|
@ -0,0 +1,65 @@
|
|||
<script>
|
||||
import { mapActions, mapState } from 'vuex';
|
||||
import tooltip from '../../../vue_shared/directives/tooltip';
|
||||
import Icon from '../../../vue_shared/components/icon.vue';
|
||||
import { rightSidebarViews } from '../../constants';
|
||||
import PipelinesList from '../pipelines/list.vue';
|
||||
|
||||
export default {
|
||||
directives: {
|
||||
tooltip,
|
||||
},
|
||||
components: {
|
||||
Icon,
|
||||
PipelinesList,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['rightPane']),
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setRightPane']),
|
||||
clickTab(e, view) {
|
||||
e.target.blur();
|
||||
|
||||
this.setRightPane(view);
|
||||
},
|
||||
},
|
||||
rightSidebarViews,
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="multi-file-commit-panel ide-right-sidebar"
|
||||
>
|
||||
<div
|
||||
class="multi-file-commit-panel-inner"
|
||||
v-if="rightPane"
|
||||
>
|
||||
<component :is="rightPane" />
|
||||
</div>
|
||||
<nav class="ide-activity-bar">
|
||||
<ul class="list-unstyled">
|
||||
<li>
|
||||
<button
|
||||
v-tooltip
|
||||
data-container="body"
|
||||
data-placement="left"
|
||||
:title="__('Pipelines')"
|
||||
class="ide-sidebar-link is-right"
|
||||
:class="{
|
||||
active: rightPane === $options.rightSidebarViews.pipelines
|
||||
}"
|
||||
type="button"
|
||||
@click="clickTab($event, $options.rightSidebarViews.pipelines)"
|
||||
>
|
||||
<icon
|
||||
:size="16"
|
||||
name="pipeline"
|
||||
/>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
146
app/assets/javascripts/ide/components/pipelines/list.vue
Normal file
146
app/assets/javascripts/ide/components/pipelines/list.vue
Normal file
|
@ -0,0 +1,146 @@
|
|||
<script>
|
||||
import { mapActions, mapGetters, mapState } from 'vuex';
|
||||
import _ from 'underscore';
|
||||
import { sprintf, __ } from '../../../locale';
|
||||
import LoadingIcon from '../../../vue_shared/components/loading_icon.vue';
|
||||
import Icon from '../../../vue_shared/components/icon.vue';
|
||||
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
|
||||
import Tabs from '../../../vue_shared/components/tabs/tabs';
|
||||
import Tab from '../../../vue_shared/components/tabs/tab.vue';
|
||||
import EmptyState from '../../../pipelines/components/empty_state.vue';
|
||||
import JobsList from '../jobs/list.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingIcon,
|
||||
Icon,
|
||||
CiIcon,
|
||||
Tabs,
|
||||
Tab,
|
||||
JobsList,
|
||||
EmptyState,
|
||||
},
|
||||
computed: {
|
||||
...mapState(['pipelinesEmptyStateSvgPath', 'links']),
|
||||
...mapGetters(['currentProject']),
|
||||
...mapGetters('pipelines', ['jobsCount', 'failedJobsCount', 'failedStages', 'pipelineFailed']),
|
||||
...mapState('pipelines', ['isLoadingPipeline', 'latestPipeline', 'stages', 'isLoadingJobs']),
|
||||
ciLintText() {
|
||||
return sprintf(
|
||||
__('You can also test your .gitlab-ci.yml in the %{linkStart}Lint%{linkEnd}'),
|
||||
{
|
||||
linkStart: `<a href="${_.escape(this.currentProject.web_url)}/-/ci/lint">`,
|
||||
linkEnd: '</a>',
|
||||
},
|
||||
false,
|
||||
);
|
||||
},
|
||||
showLoadingIcon() {
|
||||
return this.isLoadingPipeline && this.latestPipeline === null;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fetchLatestPipeline();
|
||||
},
|
||||
methods: {
|
||||
...mapActions('pipelines', ['fetchLatestPipeline']),
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="ide-pipeline">
|
||||
<loading-icon
|
||||
v-if="showLoadingIcon"
|
||||
class="prepend-top-default"
|
||||
size="2"
|
||||
/>
|
||||
<template v-else-if="latestPipeline !== null">
|
||||
<header
|
||||
v-if="latestPipeline"
|
||||
class="ide-tree-header ide-pipeline-header"
|
||||
>
|
||||
<ci-icon
|
||||
:status="latestPipeline.details.status"
|
||||
:size="24"
|
||||
/>
|
||||
<span class="prepend-left-8">
|
||||
<strong>
|
||||
{{ __('Pipeline') }}
|
||||
</strong>
|
||||
<a
|
||||
:href="latestPipeline.path"
|
||||
target="_blank"
|
||||
class="ide-external-link"
|
||||
>
|
||||
#{{ latestPipeline.id }}
|
||||
<icon
|
||||
name="external-link"
|
||||
:size="12"
|
||||
/>
|
||||
</a>
|
||||
</span>
|
||||
</header>
|
||||
<empty-state
|
||||
v-if="latestPipeline === false"
|
||||
:help-page-path="links.ciHelpPagePath"
|
||||
:empty-state-svg-path="pipelinesEmptyStateSvgPath"
|
||||
:can-set-ci="true"
|
||||
/>
|
||||
<div
|
||||
v-else-if="latestPipeline.yamlError"
|
||||
class="bs-callout bs-callout-danger"
|
||||
>
|
||||
<p class="append-bottom-0">
|
||||
{{ __('Found errors in your .gitlab-ci.yml:') }}
|
||||
</p>
|
||||
<p class="append-bottom-0">
|
||||
{{ latestPipeline.yamlError }}
|
||||
</p>
|
||||
<p
|
||||
class="append-bottom-0"
|
||||
v-html="ciLintText"
|
||||
></p>
|
||||
</div>
|
||||
<tabs
|
||||
v-else
|
||||
class="ide-pipeline-list"
|
||||
>
|
||||
<tab
|
||||
:active="!pipelineFailed"
|
||||
>
|
||||
<template slot="title">
|
||||
{{ __('Jobs') }}
|
||||
<span
|
||||
v-if="jobsCount"
|
||||
class="badge badge-pill"
|
||||
>
|
||||
{{ jobsCount }}
|
||||
</span>
|
||||
</template>
|
||||
<jobs-list
|
||||
:loading="isLoadingJobs"
|
||||
:stages="stages"
|
||||
/>
|
||||
</tab>
|
||||
<tab
|
||||
:active="pipelineFailed"
|
||||
>
|
||||
<template slot="title">
|
||||
{{ __('Failed Jobs') }}
|
||||
<span
|
||||
v-if="failedJobsCount"
|
||||
class="badge badge-pill"
|
||||
>
|
||||
{{ failedJobsCount }}
|
||||
</span>
|
||||
</template>
|
||||
<jobs-list
|
||||
:loading="isLoadingJobs"
|
||||
:stages="failedStages"
|
||||
/>
|
||||
</tab>
|
||||
</tabs>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -20,3 +20,7 @@ export const viewerTypes = {
|
|||
edit: 'editor',
|
||||
diff: 'diff',
|
||||
};
|
||||
|
||||
export const rightSidebarViews = {
|
||||
pipelines: 'pipelines-list',
|
||||
};
|
||||
|
|
|
@ -63,7 +63,7 @@ router.beforeEach((to, from, next) => {
|
|||
.then(() => {
|
||||
const fullProjectId = `${to.params.namespace}/${to.params.project}`;
|
||||
|
||||
const baseSplit = to.params[0].split('/-/');
|
||||
const baseSplit = (to.params[0] && to.params[0].split('/-/')) || [''];
|
||||
const branchId = baseSplit[0].slice(-1) === '/' ? baseSplit[0].slice(0, -1) : baseSplit[0];
|
||||
|
||||
if (branchId) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import Vue from 'vue';
|
||||
import { mapActions } from 'vuex';
|
||||
import Translate from '~/vue_shared/translate';
|
||||
import ide from './components/ide.vue';
|
||||
import store from './stores';
|
||||
|
@ -17,11 +18,18 @@ export function initIde(el) {
|
|||
ide,
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch('setEmptyStateSvgs', {
|
||||
this.setEmptyStateSvgs({
|
||||
emptyStateSvgPath: el.dataset.emptyStateSvgPath,
|
||||
noChangesStateSvgPath: el.dataset.noChangesStateSvgPath,
|
||||
committedStateSvgPath: el.dataset.committedStateSvgPath,
|
||||
pipelinesEmptyStateSvgPath: el.dataset.pipelinesEmptyStateSvgPath,
|
||||
});
|
||||
this.setLinks({
|
||||
ciHelpPagePath: el.dataset.ciHelpPagePath,
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['setEmptyStateSvgs', 'setLinks']),
|
||||
},
|
||||
render(createElement) {
|
||||
return createElement('ide');
|
||||
|
|
|
@ -169,6 +169,12 @@ export const burstUnusedSeal = ({ state, commit }) => {
|
|||
}
|
||||
};
|
||||
|
||||
export const setRightPane = ({ commit }, view) => {
|
||||
commit(types.SET_RIGHT_PANE, view);
|
||||
};
|
||||
|
||||
export const setLinks = ({ commit }, links) => commit(types.SET_LINKS, links);
|
||||
|
||||
export * from './actions/tree';
|
||||
export * from './actions/file';
|
||||
export * from './actions/project';
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import flash from '~/flash';
|
||||
import { __ } from '~/locale';
|
||||
import service from '../../services';
|
||||
import * as types from '../mutation_types';
|
||||
import Poll from '../../../lib/utils/poll';
|
||||
|
||||
let eTagPoll;
|
||||
|
||||
export const getProjectData = ({ commit, state }, { namespace, projectId, force = false } = {}) =>
|
||||
new Promise((resolve, reject) => {
|
||||
|
@ -85,61 +81,3 @@ export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {})
|
|||
.catch(() => {
|
||||
flash(__('Error loading last commit.'), 'alert', document, null, false, true);
|
||||
});
|
||||
|
||||
export const pollSuccessCallBack = ({ commit, state }, { data }) => {
|
||||
if (data.pipelines && data.pipelines.length) {
|
||||
const lastCommitHash =
|
||||
state.projects[state.currentProjectId].branches[state.currentBranchId].commit.id;
|
||||
const lastCommitPipeline = data.pipelines.find(
|
||||
pipeline => pipeline.commit.id === lastCommitHash,
|
||||
);
|
||||
commit(types.SET_LAST_COMMIT_PIPELINE, {
|
||||
projectId: state.currentProjectId,
|
||||
branchId: state.currentBranchId,
|
||||
pipeline: lastCommitPipeline || {},
|
||||
});
|
||||
}
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const pipelinePoll = ({ getters, dispatch }) => {
|
||||
eTagPoll = new Poll({
|
||||
resource: service,
|
||||
method: 'lastCommitPipelines',
|
||||
data: {
|
||||
getters,
|
||||
},
|
||||
successCallback: ({ data }) => dispatch('pollSuccessCallBack', { data }),
|
||||
errorCallback: () => {
|
||||
flash(
|
||||
__('Something went wrong while fetching the latest pipeline status.'),
|
||||
'alert',
|
||||
document,
|
||||
null,
|
||||
false,
|
||||
true,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
eTagPoll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
eTagPoll.restart();
|
||||
} else {
|
||||
eTagPoll.stop();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const stopPipelinePolling = () => {
|
||||
eTagPoll.stop();
|
||||
};
|
||||
|
||||
export const restartPipelinePolling = () => {
|
||||
eTagPoll.restart();
|
||||
};
|
||||
|
|
|
@ -9,13 +9,16 @@ import pipelines from './modules/pipelines';
|
|||
|
||||
Vue.use(Vuex);
|
||||
|
||||
export default new Vuex.Store({
|
||||
state: state(),
|
||||
actions,
|
||||
mutations,
|
||||
getters,
|
||||
modules: {
|
||||
commit: commitModule,
|
||||
pipelines,
|
||||
},
|
||||
});
|
||||
export const createStore = () =>
|
||||
new Vuex.Store({
|
||||
state: state(),
|
||||
actions,
|
||||
mutations,
|
||||
getters,
|
||||
modules: {
|
||||
commit: commitModule,
|
||||
pipelines,
|
||||
},
|
||||
});
|
||||
|
||||
export default createStore();
|
||||
|
|
|
@ -1,49 +1,80 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import axios from 'axios';
|
||||
import { __ } from '../../../../locale';
|
||||
import Api from '../../../../api';
|
||||
import flash from '../../../../flash';
|
||||
import Poll from '../../../../lib/utils/poll';
|
||||
import service from '../../../services';
|
||||
import * as types from './mutation_types';
|
||||
|
||||
let eTagPoll;
|
||||
|
||||
export const clearEtagPoll = () => {
|
||||
eTagPoll = null;
|
||||
};
|
||||
export const stopPipelinePolling = () => eTagPoll && eTagPoll.stop();
|
||||
export const restartPipelinePolling = () => eTagPoll && eTagPoll.restart();
|
||||
|
||||
export const requestLatestPipeline = ({ commit }) => commit(types.REQUEST_LATEST_PIPELINE);
|
||||
export const receiveLatestPipelineError = ({ commit }) => {
|
||||
export const receiveLatestPipelineError = ({ commit, dispatch }) => {
|
||||
flash(__('There was an error loading latest pipeline'));
|
||||
commit(types.RECEIVE_LASTEST_PIPELINE_ERROR);
|
||||
dispatch('stopPipelinePolling');
|
||||
};
|
||||
export const receiveLatestPipelineSuccess = ({ commit }, pipeline) =>
|
||||
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, pipeline);
|
||||
export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipelines }) => {
|
||||
let lastCommitPipeline = false;
|
||||
|
||||
if (pipelines && pipelines.length) {
|
||||
const lastCommitHash = rootGetters.lastCommit && rootGetters.lastCommit.id;
|
||||
lastCommitPipeline = pipelines.find(pipeline => pipeline.commit.id === lastCommitHash);
|
||||
}
|
||||
|
||||
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline);
|
||||
};
|
||||
|
||||
export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
|
||||
if (eTagPoll) return;
|
||||
|
||||
export const fetchLatestPipeline = ({ dispatch, rootState }, sha) => {
|
||||
dispatch('requestLatestPipeline');
|
||||
|
||||
return Api.pipelines(rootState.currentProjectId, { sha, per_page: '1' })
|
||||
.then(({ data }) => {
|
||||
dispatch('receiveLatestPipelineSuccess', data.pop());
|
||||
})
|
||||
.catch(() => dispatch('receiveLatestPipelineError'));
|
||||
eTagPoll = new Poll({
|
||||
resource: service,
|
||||
method: 'lastCommitPipelines',
|
||||
data: { getters: rootGetters },
|
||||
successCallback: ({ data }) => dispatch('receiveLatestPipelineSuccess', data),
|
||||
errorCallback: () => dispatch('receiveLatestPipelineError'),
|
||||
});
|
||||
|
||||
if (!Visibility.hidden()) {
|
||||
eTagPoll.makeRequest();
|
||||
}
|
||||
|
||||
Visibility.change(() => {
|
||||
if (!Visibility.hidden()) {
|
||||
eTagPoll.restart();
|
||||
} else {
|
||||
eTagPoll.stop();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export const requestJobs = ({ commit }) => commit(types.REQUEST_JOBS);
|
||||
export const receiveJobsError = ({ commit }) => {
|
||||
export const requestJobs = ({ commit }, id) => commit(types.REQUEST_JOBS, id);
|
||||
export const receiveJobsError = ({ commit }, id) => {
|
||||
flash(__('There was an error loading jobs'));
|
||||
commit(types.RECEIVE_JOBS_ERROR);
|
||||
commit(types.RECEIVE_JOBS_ERROR, id);
|
||||
};
|
||||
export const receiveJobsSuccess = ({ commit }, data) => commit(types.RECEIVE_JOBS_SUCCESS, data);
|
||||
export const receiveJobsSuccess = ({ commit }, { id, data }) =>
|
||||
commit(types.RECEIVE_JOBS_SUCCESS, { id, data });
|
||||
|
||||
export const fetchJobs = ({ dispatch, state, rootState }, page = '1') => {
|
||||
dispatch('requestJobs');
|
||||
export const fetchJobs = ({ dispatch }, stage) => {
|
||||
dispatch('requestJobs', stage.id);
|
||||
|
||||
Api.pipelineJobs(rootState.currentProjectId, state.latestPipeline.id, {
|
||||
page,
|
||||
})
|
||||
.then(({ data, headers }) => {
|
||||
const nextPage = headers && headers['x-next-page'];
|
||||
|
||||
dispatch('receiveJobsSuccess', data);
|
||||
|
||||
if (nextPage) {
|
||||
dispatch('fetchJobs', nextPage);
|
||||
}
|
||||
})
|
||||
.catch(() => dispatch('receiveJobsError'));
|
||||
axios
|
||||
.get(stage.dropdownPath)
|
||||
.then(({ data }) => dispatch('receiveJobsSuccess', { id: stage.id, data }))
|
||||
.catch(() => dispatch('receiveJobsError', stage.id));
|
||||
};
|
||||
|
||||
export const toggleStageCollapsed = ({ commit }, stageId) =>
|
||||
commit(types.TOGGLE_STAGE_COLLAPSE, stageId);
|
||||
|
||||
export default () => {};
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const states = {
|
||||
failed: 'failed',
|
||||
};
|
|
@ -1,7 +1,22 @@
|
|||
import { states } from './constants';
|
||||
|
||||
export const hasLatestPipeline = state => !state.isLoadingPipeline && !!state.latestPipeline;
|
||||
|
||||
export const failedJobs = state =>
|
||||
export const pipelineFailed = state =>
|
||||
state.latestPipeline && state.latestPipeline.details.status.text === states.failed;
|
||||
|
||||
export const failedStages = state =>
|
||||
state.stages.filter(stage => stage.status.text.toLowerCase() === states.failed).map(stage => ({
|
||||
...stage,
|
||||
jobs: stage.jobs.filter(job => job.status.text.toLowerCase() === states.failed),
|
||||
}));
|
||||
|
||||
export const failedJobsCount = state =>
|
||||
state.stages.reduce(
|
||||
(acc, stage) => acc.concat(stage.jobs.filter(job => job.status === 'failed')),
|
||||
[],
|
||||
(acc, stage) => acc + stage.jobs.filter(j => j.status.text === states.failed).length,
|
||||
0,
|
||||
);
|
||||
|
||||
export const jobsCount = state => state.stages.reduce((acc, stage) => acc + stage.jobs.length, 0);
|
||||
|
||||
export default () => {};
|
||||
|
|
|
@ -5,3 +5,5 @@ export const RECEIVE_LASTEST_PIPELINE_SUCCESS = 'RECEIVE_LASTEST_PIPELINE_SUCCES
|
|||
export const REQUEST_JOBS = 'REQUEST_JOBS';
|
||||
export const RECEIVE_JOBS_ERROR = 'RECEIVE_JOBS_ERROR';
|
||||
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
|
||||
|
||||
export const TOGGLE_STAGE_COLLAPSE = 'TOGGLE_STAGE_COLLAPSE';
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
import * as types from './mutation_types';
|
||||
import { normalizeJob } from './utils';
|
||||
|
||||
export default {
|
||||
[types.REQUEST_LATEST_PIPELINE](state) {
|
||||
|
@ -14,40 +15,52 @@ export default {
|
|||
if (pipeline) {
|
||||
state.latestPipeline = {
|
||||
id: pipeline.id,
|
||||
status: pipeline.status,
|
||||
path: pipeline.path,
|
||||
commit: pipeline.commit,
|
||||
details: {
|
||||
status: pipeline.details.status,
|
||||
},
|
||||
yamlError: pipeline.yaml_errors,
|
||||
};
|
||||
state.stages = pipeline.details.stages.map((stage, i) => {
|
||||
const foundStage = state.stages.find(s => s.id === i);
|
||||
return {
|
||||
id: i,
|
||||
dropdownPath: stage.dropdown_path,
|
||||
name: stage.name,
|
||||
status: stage.status,
|
||||
isCollapsed: foundStage ? foundStage.isCollapsed : false,
|
||||
isLoading: foundStage ? foundStage.isLoading : false,
|
||||
jobs: foundStage ? foundStage.jobs : [],
|
||||
};
|
||||
});
|
||||
} else {
|
||||
state.latestPipeline = false;
|
||||
}
|
||||
},
|
||||
[types.REQUEST_JOBS](state) {
|
||||
state.isLoadingJobs = true;
|
||||
[types.REQUEST_JOBS](state, id) {
|
||||
state.stages = state.stages.map(stage => ({
|
||||
...stage,
|
||||
isLoading: stage.id === id ? true : stage.isLoading,
|
||||
}));
|
||||
},
|
||||
[types.RECEIVE_JOBS_ERROR](state) {
|
||||
state.isLoadingJobs = false;
|
||||
[types.RECEIVE_JOBS_ERROR](state, id) {
|
||||
state.stages = state.stages.map(stage => ({
|
||||
...stage,
|
||||
isLoading: stage.id === id ? false : stage.isLoading,
|
||||
}));
|
||||
},
|
||||
[types.RECEIVE_JOBS_SUCCESS](state, jobs) {
|
||||
state.isLoadingJobs = false;
|
||||
|
||||
state.stages = jobs.reduce((acc, job) => {
|
||||
let stage = acc.find(s => s.title === job.stage);
|
||||
|
||||
if (!stage) {
|
||||
stage = {
|
||||
title: job.stage,
|
||||
jobs: [],
|
||||
};
|
||||
|
||||
acc.push(stage);
|
||||
}
|
||||
|
||||
stage.jobs = stage.jobs.concat({
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
status: job.status,
|
||||
stage: job.stage,
|
||||
duration: job.duration,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, state.stages);
|
||||
[types.RECEIVE_JOBS_SUCCESS](state, { id, data }) {
|
||||
state.stages = state.stages.map(stage => ({
|
||||
...stage,
|
||||
isLoading: stage.id === id ? false : stage.isLoading,
|
||||
jobs: stage.id === id ? data.latest_statuses.map(normalizeJob) : stage.jobs,
|
||||
}));
|
||||
},
|
||||
[types.TOGGLE_STAGE_COLLAPSE](state, id) {
|
||||
state.stages = state.stages.map(stage => ({
|
||||
...stage,
|
||||
isCollapsed: stage.id === id ? !stage.isCollapsed : stage.isCollapsed,
|
||||
}));
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
export default () => ({
|
||||
isLoadingPipeline: false,
|
||||
isLoadingPipeline: true,
|
||||
isLoadingJobs: false,
|
||||
latestPipeline: null,
|
||||
stages: [],
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// eslint-disable-next-line import/prefer-default-export
|
||||
export const normalizeJob = job => ({
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
status: job.status,
|
||||
path: job.build_path,
|
||||
});
|
|
@ -6,6 +6,7 @@ export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
|
|||
export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
|
||||
export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
|
||||
export const SET_EMPTY_STATE_SVGS = 'SET_EMPTY_STATE_SVGS';
|
||||
export const SET_LINKS = 'SET_LINKS';
|
||||
|
||||
// Project Mutation Types
|
||||
export const SET_PROJECT = 'SET_PROJECT';
|
||||
|
@ -23,7 +24,6 @@ export const SET_BRANCH = 'SET_BRANCH';
|
|||
export const SET_BRANCH_COMMIT = 'SET_BRANCH_COMMIT';
|
||||
export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
|
||||
export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
|
||||
export const SET_LAST_COMMIT_PIPELINE = 'SET_LAST_COMMIT_PIPELINE';
|
||||
|
||||
// Tree mutation types
|
||||
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
|
||||
|
@ -66,3 +66,5 @@ export const UPDATE_ACTIVITY_BAR_VIEW = 'UPDATE_ACTIVITY_BAR_VIEW';
|
|||
export const UPDATE_TEMP_FLAG = 'UPDATE_TEMP_FLAG';
|
||||
export const TOGGLE_FILE_FINDER = 'TOGGLE_FILE_FINDER';
|
||||
export const BURST_UNUSED_SEAL = 'BURST_UNUSED_SEAL';
|
||||
|
||||
export const SET_RIGHT_PANE = 'SET_RIGHT_PANE';
|
||||
|
|
|
@ -114,12 +114,13 @@ export default {
|
|||
},
|
||||
[types.SET_EMPTY_STATE_SVGS](
|
||||
state,
|
||||
{ emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath },
|
||||
{ emptyStateSvgPath, noChangesStateSvgPath, committedStateSvgPath, pipelinesEmptyStateSvgPath },
|
||||
) {
|
||||
Object.assign(state, {
|
||||
emptyStateSvgPath,
|
||||
noChangesStateSvgPath,
|
||||
committedStateSvgPath,
|
||||
pipelinesEmptyStateSvgPath,
|
||||
});
|
||||
},
|
||||
[types.TOGGLE_FILE_FINDER](state, fileFindVisible) {
|
||||
|
@ -148,6 +149,14 @@ export default {
|
|||
unusedSeal: false,
|
||||
});
|
||||
},
|
||||
[types.SET_RIGHT_PANE](state, view) {
|
||||
Object.assign(state, {
|
||||
rightPane: state.rightPane === view ? null : view,
|
||||
});
|
||||
},
|
||||
[types.SET_LINKS](state, links) {
|
||||
Object.assign(state, { links });
|
||||
},
|
||||
...projectMutations,
|
||||
...mergeRequestMutation,
|
||||
...fileMutations,
|
||||
|
|
|
@ -14,10 +14,6 @@ export default {
|
|||
treeId: `${projectPath}/${branchName}`,
|
||||
active: true,
|
||||
workingReference: '',
|
||||
commit: {
|
||||
...branch.commit,
|
||||
pipeline: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -32,9 +28,4 @@ export default {
|
|||
commit,
|
||||
});
|
||||
},
|
||||
[types.SET_LAST_COMMIT_PIPELINE](state, { projectId, branchId, pipeline }) {
|
||||
Object.assign(state.projects[projectId].branches[branchId].commit, {
|
||||
pipeline,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -23,4 +23,6 @@ export default () => ({
|
|||
currentActivityView: activityBarViews.edit,
|
||||
unusedSeal: true,
|
||||
fileFindVisible: false,
|
||||
rightPane: null,
|
||||
links: {},
|
||||
});
|
||||
|
|
|
@ -22,6 +22,8 @@ import Icon from '../../vue_shared/components/icon.vue';
|
|||
* - Jobs show view header
|
||||
* - Jobs show view sidebar
|
||||
*/
|
||||
const validSizes = [8, 12, 16, 18, 24, 32, 48, 72];
|
||||
|
||||
export default {
|
||||
components: {
|
||||
Icon,
|
||||
|
@ -31,17 +33,36 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
size: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 16,
|
||||
validator(value) {
|
||||
return validSizes.includes(value);
|
||||
},
|
||||
},
|
||||
borderless: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
cssClass() {
|
||||
const status = this.status.group;
|
||||
return `ci-status-icon ci-status-icon-${status} js-ci-status-icon-${status}`;
|
||||
},
|
||||
icon() {
|
||||
return this.borderless ? `${this.status.icon}_borderless` : this.status.icon;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<span :class="cssClass">
|
||||
<icon :name="status.icon" />
|
||||
<icon
|
||||
:name="icon"
|
||||
:size="size"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
|
42
app/assets/javascripts/vue_shared/components/tabs/tab.vue
Normal file
42
app/assets/javascripts/vue_shared/components/tabs/tab.vue
Normal file
|
@ -0,0 +1,42 @@
|
|||
<script>
|
||||
export default {
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
active: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// props can't be updated, so we map it to data where we can
|
||||
localActive: this.active,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
active() {
|
||||
this.localActive = this.active;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.isTab = true;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="tab-pane"
|
||||
:class="{
|
||||
active: localActive
|
||||
}"
|
||||
role="tabpanel"
|
||||
>
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
64
app/assets/javascripts/vue_shared/components/tabs/tabs.js
Normal file
64
app/assets/javascripts/vue_shared/components/tabs/tabs.js
Normal file
|
@ -0,0 +1,64 @@
|
|||
export default {
|
||||
data() {
|
||||
return {
|
||||
currentIndex: 0,
|
||||
tabs: [],
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.updateTabs();
|
||||
},
|
||||
methods: {
|
||||
updateTabs() {
|
||||
this.tabs = this.$children.filter(child => child.isTab);
|
||||
this.currentIndex = this.tabs.findIndex(tab => tab.localActive);
|
||||
},
|
||||
setTab(index) {
|
||||
this.tabs[this.currentIndex].localActive = false;
|
||||
this.tabs[index].localActive = true;
|
||||
|
||||
this.currentIndex = index;
|
||||
},
|
||||
},
|
||||
render(h) {
|
||||
const navItems = this.tabs.map((tab, i) =>
|
||||
h(
|
||||
'li',
|
||||
{
|
||||
key: i,
|
||||
},
|
||||
[
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: tab.localActive ? 'active' : null,
|
||||
attrs: {
|
||||
href: '#',
|
||||
},
|
||||
on: {
|
||||
click: () => this.setTab(i),
|
||||
},
|
||||
},
|
||||
tab.$slots.title || tab.title,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
const nav = h(
|
||||
'ul',
|
||||
{
|
||||
class: 'nav-links tab-links',
|
||||
},
|
||||
[navItems],
|
||||
);
|
||||
const content = h(
|
||||
'div',
|
||||
{
|
||||
class: ['tab-content'],
|
||||
},
|
||||
[this.$slots.default],
|
||||
);
|
||||
|
||||
return h('div', {}, [[nav], content]);
|
||||
},
|
||||
};
|
|
@ -192,6 +192,10 @@
|
|||
&.active {
|
||||
color: $color-700;
|
||||
box-shadow: inset 3px 0 $color-700;
|
||||
|
||||
&.is-right {
|
||||
box-shadow: inset -3px 0 $color-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -909,6 +909,16 @@
|
|||
width: 1px;
|
||||
background: $white-light;
|
||||
}
|
||||
|
||||
&.is-right {
|
||||
padding-right: $gl-padding;
|
||||
padding-left: $gl-padding + 1px;
|
||||
|
||||
&::after {
|
||||
right: auto;
|
||||
left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1121,3 +1131,112 @@
|
|||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-external-link {
|
||||
svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
svg {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ide-right-sidebar {
|
||||
width: auto;
|
||||
min-width: 60px;
|
||||
|
||||
.ide-activity-bar {
|
||||
border-left: 1px solid $white-dark;
|
||||
}
|
||||
|
||||
.multi-file-commit-panel-inner {
|
||||
width: 350px;
|
||||
padding: $grid-size $gl-padding;
|
||||
background-color: $white-light;
|
||||
border-left: 1px solid $white-dark;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-pipeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
|
||||
.empty-state {
|
||||
margin-top: auto;
|
||||
margin-bottom: auto;
|
||||
|
||||
p {
|
||||
margin: $grid-size 0;
|
||||
text-align: center;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.btn,
|
||||
h4 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ide-pipeline-list {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.ide-pipeline-header {
|
||||
min-height: 50px;
|
||||
padding-left: $gl-padding;
|
||||
padding-right: $gl-padding;
|
||||
|
||||
.ci-status-icon {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-job-item {
|
||||
display: flex;
|
||||
padding: 16px;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $border-color;
|
||||
}
|
||||
|
||||
.ci-status-icon {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
height: 20px;
|
||||
margin-top: -2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-stage {
|
||||
.card-header {
|
||||
display: flex;
|
||||
cursor: pointer;
|
||||
|
||||
.ci-status-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ide-stage-collapse-icon {
|
||||
margin: auto 0 auto auto;
|
||||
}
|
||||
|
||||
.ide-stage-title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
|
||||
#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg'),
|
||||
"no-changes-state-svg-path" => image_path('illustrations/multi-editor_no_changes_empty.svg'),
|
||||
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg') } }
|
||||
"committed-state-svg-path" => image_path('illustrations/multi-editor_all_changes_committed_empty.svg'),
|
||||
"pipelines-empty-state-svg-path": image_path('illustrations/pipelines_empty.svg'),
|
||||
"ci-help-page-path" => help_page_path('ci/quick_start/README'), } }
|
||||
.text-center
|
||||
= icon('spinner spin 2x')
|
||||
%h2.clgray= _('Loading the GitLab IDE...')
|
||||
|
|
29
spec/javascripts/ide/components/jobs/item_spec.js
Normal file
29
spec/javascripts/ide/components/jobs/item_spec.js
Normal file
|
@ -0,0 +1,29 @@
|
|||
import Vue from 'vue';
|
||||
import JobItem from '~/ide/components/jobs/item.vue';
|
||||
import mountComponent from '../../../helpers/vue_mount_component_helper';
|
||||
import { jobs } from '../../mock_data';
|
||||
|
||||
describe('IDE jobs item', () => {
|
||||
const Component = Vue.extend(JobItem);
|
||||
const job = jobs[0];
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component, {
|
||||
job,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders job details', () => {
|
||||
expect(vm.$el.textContent).toContain(job.name);
|
||||
expect(vm.$el.textContent).toContain(`#${job.id}`);
|
||||
});
|
||||
|
||||
it('renders CI icon', () => {
|
||||
expect(vm.$el.querySelector('.ic-status_passed_borderless')).not.toBe(null);
|
||||
});
|
||||
});
|
67
spec/javascripts/ide/components/jobs/list_spec.js
Normal file
67
spec/javascripts/ide/components/jobs/list_spec.js
Normal file
|
@ -0,0 +1,67 @@
|
|||
import Vue from 'vue';
|
||||
import StageList from '~/ide/components/jobs/list.vue';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
|
||||
import { stages, jobs } from '../../mock_data';
|
||||
|
||||
describe('IDE stages list', () => {
|
||||
const Component = Vue.extend(StageList);
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
const store = createStore();
|
||||
|
||||
vm = createComponentWithStore(Component, store, {
|
||||
stages: stages.map((mappedState, i) => ({
|
||||
...mappedState,
|
||||
id: i,
|
||||
dropdownPath: mappedState.dropdown_path,
|
||||
jobs: [...jobs],
|
||||
isLoading: false,
|
||||
isCollapsed: false,
|
||||
})),
|
||||
loading: false,
|
||||
});
|
||||
|
||||
spyOn(vm, 'fetchJobs');
|
||||
spyOn(vm, 'toggleStageCollapsed');
|
||||
|
||||
vm.$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders list of stages', () => {
|
||||
expect(vm.$el.querySelectorAll('.card').length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders loading icon when no stages & is loading', done => {
|
||||
vm.stages = [];
|
||||
vm.loading = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls toggleStageCollapsed when clicking stage header', done => {
|
||||
vm.$el.querySelector('.card-header').click();
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.toggleStageCollapsed).toHaveBeenCalledWith(0);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('calls fetchJobs when stage is mounted', () => {
|
||||
expect(vm.fetchJobs.calls.count()).toBe(stages.length);
|
||||
|
||||
expect(vm.fetchJobs.calls.argsFor(0)).toEqual([vm.stages[0]]);
|
||||
expect(vm.fetchJobs.calls.argsFor(1)).toEqual([vm.stages[1]]);
|
||||
});
|
||||
});
|
95
spec/javascripts/ide/components/jobs/stage_spec.js
Normal file
95
spec/javascripts/ide/components/jobs/stage_spec.js
Normal file
|
@ -0,0 +1,95 @@
|
|||
import Vue from 'vue';
|
||||
import Stage from '~/ide/components/jobs/stage.vue';
|
||||
import { stages, jobs } from '../../mock_data';
|
||||
|
||||
describe('IDE pipeline stage', () => {
|
||||
const Component = Vue.extend(Stage);
|
||||
let vm;
|
||||
let stage;
|
||||
|
||||
beforeEach(() => {
|
||||
stage = {
|
||||
...stages[0],
|
||||
id: 0,
|
||||
dropdownPath: stages[0].dropdown_path,
|
||||
jobs: [...jobs],
|
||||
isLoading: false,
|
||||
isCollapsed: false,
|
||||
};
|
||||
|
||||
vm = new Component({
|
||||
propsData: { stage },
|
||||
});
|
||||
|
||||
spyOn(vm, '$emit');
|
||||
|
||||
vm.$mount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('emits fetch event when mounted', () => {
|
||||
expect(vm.$emit).toHaveBeenCalledWith('fetch', vm.stage);
|
||||
});
|
||||
|
||||
it('renders stages details', () => {
|
||||
expect(vm.$el.textContent).toContain(vm.stage.name);
|
||||
});
|
||||
|
||||
it('renders CI icon', () => {
|
||||
expect(vm.$el.querySelector('.ic-status_failed')).not.toBe(null);
|
||||
});
|
||||
|
||||
describe('collapsed', () => {
|
||||
it('emits event when clicking header', done => {
|
||||
vm.$el.querySelector('.card-header').click();
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed', vm.stage.id);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles collapse status when collapsed', done => {
|
||||
vm.stage.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.card-body').style.display).toBe('none');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets border bottom class when collapsed', done => {
|
||||
vm.stage.isCollapsed = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.card-header').classList).toContain('border-bottom-0');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('renders jobs count', () => {
|
||||
expect(vm.$el.querySelector('.badge').textContent).toContain('4');
|
||||
});
|
||||
|
||||
it('renders loading icon when no jobs and isLoading is true', done => {
|
||||
vm.stage.isLoading = true;
|
||||
vm.stage.jobs = [];
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('renders list of jobs', () => {
|
||||
expect(vm.$el.querySelectorAll('.ide-job-item').length).toBe(4);
|
||||
});
|
||||
});
|
117
spec/javascripts/ide/components/pipelines/list_spec.js
Normal file
117
spec/javascripts/ide/components/pipelines/list_spec.js
Normal file
|
@ -0,0 +1,117 @@
|
|||
import Vue from 'vue';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { createStore } from '~/ide/stores';
|
||||
import List from '~/ide/components/pipelines/list.vue';
|
||||
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
|
||||
import { pipelines, projectData, stages, jobs } from '../../mock_data';
|
||||
|
||||
describe('IDE pipelines list', () => {
|
||||
const Component = Vue.extend(List);
|
||||
let vm;
|
||||
let mock;
|
||||
|
||||
beforeEach(done => {
|
||||
const store = createStore();
|
||||
|
||||
mock = new MockAdapter(axios);
|
||||
|
||||
store.state.currentProjectId = 'abc/def';
|
||||
store.state.currentBranchId = 'master';
|
||||
store.state.projects['abc/def'] = {
|
||||
...projectData,
|
||||
path_with_namespace: 'abc/def',
|
||||
branches: {
|
||||
master: { commit: { id: '123' } },
|
||||
},
|
||||
};
|
||||
store.state.links = { ciHelpPagePath: gl.TEST_HOST };
|
||||
store.state.pipelinesEmptyStateSvgPath = gl.TEST_HOST;
|
||||
store.state.pipelines.stages = stages.map((mappedState, i) => ({
|
||||
...mappedState,
|
||||
id: i,
|
||||
dropdownPath: mappedState.dropdown_path,
|
||||
jobs: [...jobs],
|
||||
isLoading: false,
|
||||
isCollapsed: false,
|
||||
}));
|
||||
|
||||
mock
|
||||
.onGet('/abc/def/commit/123/pipelines')
|
||||
.replyOnce(200, { pipelines: [...pipelines] }, { 'poll-interval': '-1' });
|
||||
|
||||
vm = createComponentWithStore(Component, store).$mount();
|
||||
|
||||
setTimeout(done);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$store.dispatch('pipelines/stopPipelinePolling');
|
||||
vm.$store.dispatch('pipelines/clearEtagPoll');
|
||||
|
||||
vm.$destroy();
|
||||
mock.restore();
|
||||
});
|
||||
|
||||
it('renders pipeline data', () => {
|
||||
expect(vm.$el.textContent).toContain('#1');
|
||||
});
|
||||
|
||||
it('renders CI icon', () => {
|
||||
expect(vm.$el.querySelector('.ci-status-icon-failed')).not.toBe(null);
|
||||
});
|
||||
|
||||
it('renders list of jobs', () => {
|
||||
expect(vm.$el.querySelectorAll('.tab-pane:first-child .ide-job-item').length).toBe(
|
||||
jobs.length * stages.length,
|
||||
);
|
||||
});
|
||||
|
||||
it('renders list of failed jobs on failed jobs tab', done => {
|
||||
vm.$el.querySelectorAll('.tab-links a')[1].click();
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelectorAll('.tab-pane.active .ide-job-item').length).toBe(2);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('YAML error', () => {
|
||||
it('renders YAML error', done => {
|
||||
vm.$store.state.pipelines.latestPipeline.yamlError = 'test yaml error';
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.textContent).toContain('Found errors in your .gitlab-ci.yml:');
|
||||
expect(vm.$el.textContent).toContain('test yaml error');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('empty state', () => {
|
||||
it('renders pipelines empty state', done => {
|
||||
vm.$store.state.pipelines.latestPipeline = false;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.empty-state')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loading state', () => {
|
||||
it('renders loading state when there is no latest pipeline', done => {
|
||||
vm.$store.state.pipelines.latestPipeline = null;
|
||||
vm.$store.state.pipelines.isLoadingPipeline = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.querySelector('.loading-container')).not.toBe(null);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,11 +1,13 @@
|
|||
import { decorateData } from '~/ide/stores/utils';
|
||||
import state from '~/ide/stores/state';
|
||||
import commitState from '~/ide/stores/modules/commit/state';
|
||||
import pipelinesState from '~/ide/stores/modules/pipelines/state';
|
||||
|
||||
export const resetStore = store => {
|
||||
const newState = {
|
||||
...state(),
|
||||
commit: commitState(),
|
||||
pipelines: pipelinesState(),
|
||||
};
|
||||
store.replaceState(newState);
|
||||
};
|
||||
|
|
|
@ -19,13 +19,48 @@ export const pipelines = [
|
|||
id: 1,
|
||||
ref: 'master',
|
||||
sha: '123',
|
||||
status: 'failed',
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_failed',
|
||||
group: 'failed',
|
||||
text: 'Failed',
|
||||
},
|
||||
},
|
||||
commit: { id: '123' },
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
ref: 'master',
|
||||
sha: '213',
|
||||
status: 'success',
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_failed',
|
||||
group: 'failed',
|
||||
text: 'Failed',
|
||||
},
|
||||
},
|
||||
commit: { id: '213' },
|
||||
},
|
||||
];
|
||||
|
||||
export const stages = [
|
||||
{
|
||||
dropdown_path: `${gl.TEST_HOST}/testing`,
|
||||
name: 'build',
|
||||
status: {
|
||||
icon: 'status_failed',
|
||||
group: 'failed',
|
||||
text: 'failed',
|
||||
},
|
||||
},
|
||||
{
|
||||
dropdown_path: 'testing',
|
||||
name: 'test',
|
||||
status: {
|
||||
icon: 'status_failed',
|
||||
group: 'failed',
|
||||
text: 'failed',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
@ -33,28 +68,44 @@ export const jobs = [
|
|||
{
|
||||
id: 1,
|
||||
name: 'test',
|
||||
status: 'failed',
|
||||
path: 'testing',
|
||||
status: {
|
||||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
stage: 'test',
|
||||
duration: 1,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'test 2',
|
||||
status: 'failed',
|
||||
path: 'testing2',
|
||||
status: {
|
||||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
stage: 'test',
|
||||
duration: 1,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
name: 'test 3',
|
||||
status: 'failed',
|
||||
path: 'testing3',
|
||||
status: {
|
||||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
stage: 'test',
|
||||
duration: 1,
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'test 3',
|
||||
status: 'failed',
|
||||
name: 'test 4',
|
||||
path: 'testing4',
|
||||
status: {
|
||||
icon: 'status_failed',
|
||||
text: 'failed',
|
||||
},
|
||||
stage: 'build',
|
||||
duration: 1,
|
||||
},
|
||||
|
@ -68,14 +119,16 @@ export const fullPipelinesResponse = {
|
|||
pipelines: [
|
||||
{
|
||||
id: '51',
|
||||
path: 'test',
|
||||
commit: {
|
||||
id: 'xxxxxxxxxxxxxxxxxxxx',
|
||||
id: '123',
|
||||
},
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_failed',
|
||||
text: 'failed',
|
||||
},
|
||||
stages: [...stages],
|
||||
},
|
||||
},
|
||||
{
|
||||
|
@ -88,6 +141,7 @@ export const fullPipelinesResponse = {
|
|||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
stages: [...stages],
|
||||
},
|
||||
},
|
||||
],
|
||||
|
|
|
@ -1,31 +1,10 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { refreshLastCommitData, pollSuccessCallBack } from '~/ide/stores/actions';
|
||||
import { refreshLastCommitData } from '~/ide/stores/actions';
|
||||
import store from '~/ide/stores';
|
||||
import service from '~/ide/services';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import { fullPipelinesResponse } from '../../mock_data';
|
||||
import { resetStore } from '../../helpers';
|
||||
import testAction from '../../../helpers/vuex_action_helper';
|
||||
|
||||
describe('IDE store project actions', () => {
|
||||
const setProjectState = () => {
|
||||
store.state.currentProjectId = 'abc/def';
|
||||
store.state.currentBranchId = 'master';
|
||||
store.state.projects['abc/def'] = {
|
||||
id: 4,
|
||||
path_with_namespace: 'abc/def',
|
||||
branches: {
|
||||
master: {
|
||||
commit: {
|
||||
id: 'abc123def456ghi789jkl',
|
||||
title: 'example',
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
store.state.projects['abc/def'] = {};
|
||||
});
|
||||
|
@ -101,92 +80,4 @@ describe('IDE store project actions', () => {
|
|||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pipelinePoll', () => {
|
||||
let mock;
|
||||
|
||||
beforeEach(() => {
|
||||
setProjectState();
|
||||
jasmine.clock().install();
|
||||
mock = new MockAdapter(axios);
|
||||
mock
|
||||
.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
|
||||
.reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
mock.restore();
|
||||
store.dispatch('stopPipelinePolling');
|
||||
});
|
||||
|
||||
it('calls service periodically', done => {
|
||||
spyOn(axios, 'get').and.callThrough();
|
||||
spyOn(Visibility, 'hidden').and.returnValue(false);
|
||||
|
||||
store
|
||||
.dispatch('pipelinePoll')
|
||||
.then(() => {
|
||||
jasmine.clock().tick(1000);
|
||||
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
expect(axios.get.calls.count()).toBe(1);
|
||||
})
|
||||
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
||||
.then(() => {
|
||||
jasmine.clock().tick(10000);
|
||||
expect(axios.get.calls.count()).toBe(2);
|
||||
})
|
||||
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
||||
.then(() => {
|
||||
jasmine.clock().tick(10000);
|
||||
expect(axios.get.calls.count()).toBe(3);
|
||||
})
|
||||
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
||||
.then(() => {
|
||||
jasmine.clock().tick(10000);
|
||||
expect(axios.get.calls.count()).toBe(4);
|
||||
})
|
||||
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pollSuccessCallBack', () => {
|
||||
beforeEach(() => {
|
||||
setProjectState();
|
||||
});
|
||||
|
||||
it('commits correct pipeline', done => {
|
||||
testAction(
|
||||
pollSuccessCallBack,
|
||||
fullPipelinesResponse,
|
||||
store.state,
|
||||
[
|
||||
{
|
||||
type: 'SET_LAST_COMMIT_PIPELINE',
|
||||
payload: {
|
||||
projectId: 'abc/def',
|
||||
branchId: 'master',
|
||||
pipeline: {
|
||||
id: '50',
|
||||
commit: {
|
||||
id: 'abc123def456ghi789jkl',
|
||||
},
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
], // mutations
|
||||
[], // action
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import Visibility from 'visibilityjs';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import axios from '~/lib/utils/axios_utils';
|
||||
import actions, {
|
||||
|
@ -5,10 +6,13 @@ import actions, {
|
|||
receiveLatestPipelineError,
|
||||
receiveLatestPipelineSuccess,
|
||||
fetchLatestPipeline,
|
||||
stopPipelinePolling,
|
||||
clearEtagPoll,
|
||||
requestJobs,
|
||||
receiveJobsError,
|
||||
receiveJobsSuccess,
|
||||
fetchJobs,
|
||||
toggleStageCollapsed,
|
||||
} from '~/ide/stores/modules/pipelines/actions';
|
||||
import state from '~/ide/stores/modules/pipelines/state';
|
||||
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
|
||||
|
@ -51,7 +55,7 @@ describe('IDE pipelines actions', () => {
|
|||
null,
|
||||
mockedState,
|
||||
[{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
|
||||
[],
|
||||
[{ type: 'stopPipelinePolling' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
@ -59,91 +63,128 @@ describe('IDE pipelines actions', () => {
|
|||
it('creates flash message', () => {
|
||||
const flashSpy = spyOnDependency(actions, 'flash');
|
||||
|
||||
receiveLatestPipelineError({ commit() {} });
|
||||
receiveLatestPipelineError({ commit() {}, dispatch() {} });
|
||||
|
||||
expect(flashSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveLatestPipelineSuccess', () => {
|
||||
it('commits pipeline', done => {
|
||||
testAction(
|
||||
receiveLatestPipelineSuccess,
|
||||
const rootGetters = {
|
||||
lastCommit: { id: '123' },
|
||||
};
|
||||
let commit;
|
||||
|
||||
beforeEach(() => {
|
||||
commit = jasmine.createSpy('commit');
|
||||
});
|
||||
|
||||
it('commits pipeline', () => {
|
||||
receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines });
|
||||
|
||||
expect(commit.calls.argsFor(0)).toEqual([
|
||||
types.RECEIVE_LASTEST_PIPELINE_SUCCESS,
|
||||
pipelines[0],
|
||||
mockedState,
|
||||
[{ type: types.RECEIVE_LASTEST_PIPELINE_SUCCESS, payload: pipelines[0] }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
]);
|
||||
});
|
||||
|
||||
it('commits false when there are no pipelines', () => {
|
||||
receiveLatestPipelineSuccess({ rootGetters, commit }, { pipelines: [] });
|
||||
|
||||
expect(commit.calls.argsFor(0)).toEqual([types.RECEIVE_LASTEST_PIPELINE_SUCCESS, false]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchLatestPipeline', () => {
|
||||
beforeEach(() => {
|
||||
jasmine.clock().install();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jasmine.clock().uninstall();
|
||||
stopPipelinePolling();
|
||||
clearEtagPoll();
|
||||
});
|
||||
|
||||
describe('success', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(200, pipelines);
|
||||
mock
|
||||
.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
|
||||
.reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
|
||||
});
|
||||
|
||||
it('dispatches request', done => {
|
||||
testAction(
|
||||
fetchLatestPipeline,
|
||||
'123',
|
||||
mockedState,
|
||||
[],
|
||||
[{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineSuccess' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
spyOn(axios, 'get').and.callThrough();
|
||||
spyOn(Visibility, 'hidden').and.returnValue(false);
|
||||
|
||||
it('dispatches success with latest pipeline', done => {
|
||||
testAction(
|
||||
fetchLatestPipeline,
|
||||
'123',
|
||||
mockedState,
|
||||
[],
|
||||
[
|
||||
{ type: 'requestLatestPipeline' },
|
||||
{ type: 'receiveLatestPipelineSuccess', payload: pipelines[0] },
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
const dispatch = jasmine.createSpy('dispatch');
|
||||
const rootGetters = {
|
||||
lastCommit: { id: 'abc123def456ghi789jkl' },
|
||||
currentProject: { path_with_namespace: 'abc/def' },
|
||||
};
|
||||
|
||||
it('calls axios with correct params', () => {
|
||||
const apiSpy = spyOn(axios, 'get').and.callThrough();
|
||||
fetchLatestPipeline({ dispatch, rootGetters });
|
||||
|
||||
fetchLatestPipeline({ dispatch() {}, rootState: state }, '123');
|
||||
expect(dispatch.calls.argsFor(0)).toEqual(['requestLatestPipeline']);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalledWith(jasmine.anything(), {
|
||||
params: {
|
||||
sha: '123',
|
||||
per_page: '1',
|
||||
},
|
||||
});
|
||||
jasmine.clock().tick(1000);
|
||||
|
||||
new Promise(resolve => requestAnimationFrame(resolve))
|
||||
.then(() => {
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
expect(axios.get.calls.count()).toBe(1);
|
||||
|
||||
expect(dispatch.calls.argsFor(1)).toEqual([
|
||||
'receiveLatestPipelineSuccess',
|
||||
jasmine.anything(),
|
||||
]);
|
||||
|
||||
jasmine.clock().tick(10000);
|
||||
})
|
||||
.then(() => new Promise(resolve => requestAnimationFrame(resolve)))
|
||||
.then(() => {
|
||||
expect(axios.get).toHaveBeenCalled();
|
||||
expect(axios.get.calls.count()).toBe(2);
|
||||
|
||||
expect(dispatch.calls.argsFor(2)).toEqual([
|
||||
'receiveLatestPipelineSuccess',
|
||||
jasmine.anything(),
|
||||
]);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
|
||||
mock.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines').reply(500);
|
||||
});
|
||||
|
||||
it('dispatches error', done => {
|
||||
testAction(
|
||||
fetchLatestPipeline,
|
||||
'123',
|
||||
mockedState,
|
||||
[],
|
||||
[{ type: 'requestLatestPipeline' }, { type: 'receiveLatestPipelineError' }],
|
||||
done,
|
||||
);
|
||||
const dispatch = jasmine.createSpy('dispatch');
|
||||
const rootGetters = {
|
||||
lastCommit: { id: 'abc123def456ghi789jkl' },
|
||||
currentProject: { path_with_namespace: 'abc/def' },
|
||||
};
|
||||
|
||||
fetchLatestPipeline({ dispatch, rootGetters });
|
||||
|
||||
jasmine.clock().tick(1500);
|
||||
|
||||
new Promise(resolve => requestAnimationFrame(resolve))
|
||||
.then(() => {
|
||||
expect(dispatch.calls.argsFor(1)).toEqual(['receiveLatestPipelineError']);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('requestJobs', () => {
|
||||
it('commits request', done => {
|
||||
testAction(requestJobs, null, mockedState, [{ type: types.REQUEST_JOBS }], [], done);
|
||||
testAction(requestJobs, 1, mockedState, [{ type: types.REQUEST_JOBS, payload: 1 }], [], done);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -151,9 +192,9 @@ describe('IDE pipelines actions', () => {
|
|||
it('commits error', done => {
|
||||
testAction(
|
||||
receiveJobsError,
|
||||
null,
|
||||
1,
|
||||
mockedState,
|
||||
[{ type: types.RECEIVE_JOBS_ERROR }],
|
||||
[{ type: types.RECEIVE_JOBS_ERROR, payload: 1 }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
|
@ -162,19 +203,19 @@ describe('IDE pipelines actions', () => {
|
|||
it('creates flash message', () => {
|
||||
const flashSpy = spyOnDependency(actions, 'flash');
|
||||
|
||||
receiveJobsError({ commit() {} });
|
||||
receiveJobsError({ commit() {} }, 1);
|
||||
|
||||
expect(flashSpy).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('receiveJobsSuccess', () => {
|
||||
it('commits jobs', done => {
|
||||
it('commits data', done => {
|
||||
testAction(
|
||||
receiveJobsSuccess,
|
||||
jobs,
|
||||
{ id: 1, data: jobs },
|
||||
mockedState,
|
||||
[{ type: types.RECEIVE_JOBS_SUCCESS, payload: jobs }],
|
||||
[{ type: types.RECEIVE_JOBS_SUCCESS, payload: { id: 1, data: jobs } }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
|
@ -182,108 +223,62 @@ describe('IDE pipelines actions', () => {
|
|||
});
|
||||
|
||||
describe('fetchJobs', () => {
|
||||
let page = '';
|
||||
|
||||
beforeEach(() => {
|
||||
mockedState.latestPipeline = pipelines[0];
|
||||
});
|
||||
const stage = {
|
||||
id: 1,
|
||||
dropdownPath: `${gl.TEST_HOST}/jobs`,
|
||||
};
|
||||
|
||||
describe('success', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines\/(.*)\/jobs/).replyOnce(() => [
|
||||
200,
|
||||
jobs,
|
||||
{
|
||||
'x-next-page': page,
|
||||
},
|
||||
]);
|
||||
mock.onGet(stage.dropdownPath).replyOnce(200, jobs);
|
||||
});
|
||||
|
||||
it('dispatches request', done => {
|
||||
testAction(
|
||||
fetchJobs,
|
||||
null,
|
||||
mockedState,
|
||||
[],
|
||||
[{ type: 'requestJobs' }, { type: 'receiveJobsSuccess' }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches success with latest pipeline', done => {
|
||||
testAction(
|
||||
fetchJobs,
|
||||
null,
|
||||
mockedState,
|
||||
[],
|
||||
[{ type: 'requestJobs' }, { type: 'receiveJobsSuccess', payload: jobs }],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('dispatches twice for both pages', done => {
|
||||
page = '2';
|
||||
|
||||
testAction(
|
||||
fetchJobs,
|
||||
null,
|
||||
stage,
|
||||
mockedState,
|
||||
[],
|
||||
[
|
||||
{ type: 'requestJobs' },
|
||||
{ type: 'receiveJobsSuccess', payload: jobs },
|
||||
{ type: 'fetchJobs', payload: '2' },
|
||||
{ type: 'requestJobs' },
|
||||
{ type: 'receiveJobsSuccess', payload: jobs },
|
||||
{ type: 'requestJobs', payload: stage.id },
|
||||
{ type: 'receiveJobsSuccess', payload: { id: stage.id, data: jobs } },
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
|
||||
it('calls axios with correct URL', () => {
|
||||
const apiSpy = spyOn(axios, 'get').and.callThrough();
|
||||
|
||||
fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
|
||||
|
||||
expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
|
||||
params: { page: '1' },
|
||||
});
|
||||
});
|
||||
|
||||
it('calls axios with page next page', () => {
|
||||
const apiSpy = spyOn(axios, 'get').and.callThrough();
|
||||
|
||||
fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState });
|
||||
|
||||
expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
|
||||
params: { page: '1' },
|
||||
});
|
||||
|
||||
page = '2';
|
||||
|
||||
fetchJobs({ dispatch() {}, state: mockedState, rootState: mockedState }, page);
|
||||
|
||||
expect(apiSpy).toHaveBeenCalledWith('/api/v4/projects/test%2Fproject/pipelines/1/jobs', {
|
||||
params: { page: '2' },
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('error', () => {
|
||||
beforeEach(() => {
|
||||
mock.onGet(/\/api\/v4\/projects\/(.*)\/pipelines(.*)/).replyOnce(500);
|
||||
mock.onGet(stage.dropdownPath).replyOnce(500);
|
||||
});
|
||||
|
||||
it('dispatches error', done => {
|
||||
testAction(
|
||||
fetchJobs,
|
||||
null,
|
||||
stage,
|
||||
mockedState,
|
||||
[],
|
||||
[{ type: 'requestJobs' }, { type: 'receiveJobsError' }],
|
||||
[
|
||||
{ type: 'requestJobs', payload: stage.id },
|
||||
{ type: 'receiveJobsError', payload: stage.id },
|
||||
],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleStageCollapsed', () => {
|
||||
it('commits collapse', done => {
|
||||
testAction(
|
||||
toggleStageCollapsed,
|
||||
1,
|
||||
mockedState,
|
||||
[{ type: types.TOGGLE_STAGE_COLLAPSE, payload: 1 }],
|
||||
[],
|
||||
done,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,35 +37,4 @@ describe('IDE pipeline getters', () => {
|
|||
expect(getters.hasLatestPipeline(mockedState)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('failedJobs', () => {
|
||||
it('returns array of failed jobs', () => {
|
||||
mockedState.stages = [
|
||||
{
|
||||
title: 'test',
|
||||
jobs: [{ id: 1, status: 'failed' }, { id: 2, status: 'success' }],
|
||||
},
|
||||
{
|
||||
title: 'build',
|
||||
jobs: [{ id: 3, status: 'failed' }, { id: 4, status: 'failed' }],
|
||||
},
|
||||
];
|
||||
|
||||
expect(getters.failedJobs(mockedState).length).toBe(3);
|
||||
expect(getters.failedJobs(mockedState)).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
status: jasmine.anything(),
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
status: jasmine.anything(),
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
status: jasmine.anything(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import mutations from '~/ide/stores/modules/pipelines/mutations';
|
||||
import state from '~/ide/stores/modules/pipelines/state';
|
||||
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
|
||||
import { pipelines, jobs } from '../../../mock_data';
|
||||
import { fullPipelinesResponse, stages, jobs } from '../../../mock_data';
|
||||
|
||||
describe('IDE pipelines mutations', () => {
|
||||
let mockedState;
|
||||
|
@ -28,93 +28,147 @@ describe('IDE pipelines mutations', () => {
|
|||
|
||||
describe(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, () => {
|
||||
it('sets loading to false on success', () => {
|
||||
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
|
||||
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
|
||||
mockedState,
|
||||
fullPipelinesResponse.data.pipelines[0],
|
||||
);
|
||||
|
||||
expect(mockedState.isLoadingPipeline).toBe(false);
|
||||
});
|
||||
|
||||
it('sets latestPipeline', () => {
|
||||
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, pipelines[0]);
|
||||
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
|
||||
mockedState,
|
||||
fullPipelinesResponse.data.pipelines[0],
|
||||
);
|
||||
|
||||
expect(mockedState.latestPipeline).toEqual({
|
||||
id: pipelines[0].id,
|
||||
status: pipelines[0].status,
|
||||
id: '51',
|
||||
path: 'test',
|
||||
commit: { id: '123' },
|
||||
details: { status: jasmine.any(Object) },
|
||||
yamlError: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('does not set latest pipeline if pipeline is null', () => {
|
||||
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](mockedState, null);
|
||||
|
||||
expect(mockedState.latestPipeline).toEqual(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.REQUEST_JOBS, () => {
|
||||
it('sets jobs loading to true', () => {
|
||||
mutations[types.REQUEST_JOBS](mockedState);
|
||||
|
||||
expect(mockedState.isLoadingJobs).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RECEIVE_JOBS_ERROR, () => {
|
||||
it('sets jobs loading to false', () => {
|
||||
mutations[types.RECEIVE_JOBS_ERROR](mockedState);
|
||||
|
||||
expect(mockedState.isLoadingJobs).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RECEIVE_JOBS_SUCCESS, () => {
|
||||
it('sets jobs loading to false on success', () => {
|
||||
mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
|
||||
|
||||
expect(mockedState.isLoadingJobs).toBe(false);
|
||||
expect(mockedState.latestPipeline).toEqual(false);
|
||||
});
|
||||
|
||||
it('sets stages', () => {
|
||||
mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
|
||||
mutations[types.RECEIVE_LASTEST_PIPELINE_SUCCESS](
|
||||
mockedState,
|
||||
fullPipelinesResponse.data.pipelines[0],
|
||||
);
|
||||
|
||||
expect(mockedState.stages.length).toBe(2);
|
||||
expect(mockedState.stages).toEqual([
|
||||
{
|
||||
title: 'test',
|
||||
jobs: jasmine.anything(),
|
||||
id: 0,
|
||||
dropdownPath: stages[0].dropdown_path,
|
||||
name: stages[0].name,
|
||||
status: stages[0].status,
|
||||
isCollapsed: false,
|
||||
isLoading: false,
|
||||
jobs: [],
|
||||
},
|
||||
{
|
||||
title: 'build',
|
||||
jobs: jasmine.anything(),
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('sets jobs in stages', () => {
|
||||
mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, jobs);
|
||||
|
||||
expect(mockedState.stages[0].jobs.length).toBe(3);
|
||||
expect(mockedState.stages[1].jobs.length).toBe(1);
|
||||
expect(mockedState.stages).toEqual([
|
||||
{
|
||||
title: jasmine.anything(),
|
||||
jobs: jobs.filter(job => job.stage === 'test').map(job => ({
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
status: job.status,
|
||||
stage: job.stage,
|
||||
duration: job.duration,
|
||||
})),
|
||||
},
|
||||
{
|
||||
title: jasmine.anything(),
|
||||
jobs: jobs.filter(job => job.stage === 'build').map(job => ({
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
status: job.status,
|
||||
stage: job.stage,
|
||||
duration: job.duration,
|
||||
})),
|
||||
id: 1,
|
||||
dropdownPath: stages[1].dropdown_path,
|
||||
name: stages[1].name,
|
||||
status: stages[1].status,
|
||||
isCollapsed: false,
|
||||
isLoading: false,
|
||||
jobs: [],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.REQUEST_JOBS, () => {
|
||||
beforeEach(() => {
|
||||
mockedState.stages = stages.map((stage, i) => ({
|
||||
...stage,
|
||||
id: i,
|
||||
}));
|
||||
});
|
||||
|
||||
it('sets isLoading on stage', () => {
|
||||
mutations[types.REQUEST_JOBS](mockedState, mockedState.stages[0].id);
|
||||
|
||||
expect(mockedState.stages[0].isLoading).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RECEIVE_JOBS_ERROR, () => {
|
||||
beforeEach(() => {
|
||||
mockedState.stages = stages.map((stage, i) => ({
|
||||
...stage,
|
||||
id: i,
|
||||
}));
|
||||
});
|
||||
|
||||
it('sets isLoading on stage after error', () => {
|
||||
mutations[types.RECEIVE_JOBS_ERROR](mockedState, mockedState.stages[0].id);
|
||||
|
||||
expect(mockedState.stages[0].isLoading).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.RECEIVE_JOBS_SUCCESS, () => {
|
||||
let data;
|
||||
|
||||
beforeEach(() => {
|
||||
mockedState.stages = stages.map((stage, i) => ({
|
||||
...stage,
|
||||
id: i,
|
||||
}));
|
||||
|
||||
data = {
|
||||
latest_statuses: [...jobs],
|
||||
};
|
||||
});
|
||||
|
||||
it('updates loading', () => {
|
||||
mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data });
|
||||
|
||||
expect(mockedState.stages[0].isLoading).toBe(false);
|
||||
});
|
||||
|
||||
it('sets jobs on stage', () => {
|
||||
mutations[types.RECEIVE_JOBS_SUCCESS](mockedState, { id: mockedState.stages[0].id, data });
|
||||
|
||||
expect(mockedState.stages[0].jobs.length).toBe(jobs.length);
|
||||
expect(mockedState.stages[0].jobs).toEqual(
|
||||
jobs.map(job => ({
|
||||
id: job.id,
|
||||
name: job.name,
|
||||
status: job.status,
|
||||
path: job.build_path,
|
||||
})),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe(types.TOGGLE_STAGE_COLLAPSE, () => {
|
||||
beforeEach(() => {
|
||||
mockedState.stages = stages.map((stage, i) => ({
|
||||
...stage,
|
||||
id: i,
|
||||
isCollapsed: false,
|
||||
}));
|
||||
});
|
||||
|
||||
it('toggles collapsed state', () => {
|
||||
mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id);
|
||||
|
||||
expect(mockedState.stages[0].isCollapsed).toBe(true);
|
||||
|
||||
mutations[types.TOGGLE_STAGE_COLLAPSE](mockedState, mockedState.stages[0].id);
|
||||
|
||||
expect(mockedState.stages[0].isCollapsed).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -37,40 +37,4 @@ describe('Multi-file store branch mutations', () => {
|
|||
expect(localState.projects.Example.branches.master.commit.title).toBe('Example commit');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SET_LAST_COMMIT_PIPELINE', () => {
|
||||
it('sets the pipeline for the last commit on current project', () => {
|
||||
localState.projects = {
|
||||
Example: {
|
||||
branches: {
|
||||
master: {
|
||||
commit: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
mutations.SET_LAST_COMMIT_PIPELINE(localState, {
|
||||
projectId: 'Example',
|
||||
branchId: 'master',
|
||||
pipeline: {
|
||||
id: '50',
|
||||
details: {
|
||||
status: {
|
||||
icon: 'status_passed',
|
||||
text: 'passed',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(localState.projects.Example.branches.master.commit.pipeline.id).toBe('50');
|
||||
expect(localState.projects.Example.branches.master.commit.pipeline.details.status.text).toBe(
|
||||
'passed',
|
||||
);
|
||||
expect(localState.projects.Example.branches.master.commit.pipeline.details.status.icon).toBe(
|
||||
'status_passed',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
32
spec/javascripts/vue_shared/components/tabs/tab_spec.js
Normal file
32
spec/javascripts/vue_shared/components/tabs/tab_spec.js
Normal file
|
@ -0,0 +1,32 @@
|
|||
import Vue from 'vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
import Tab from '~/vue_shared/components/tabs/tab.vue';
|
||||
|
||||
describe('Tab component', () => {
|
||||
const Component = Vue.extend(Tab);
|
||||
let vm;
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component);
|
||||
});
|
||||
|
||||
it('sets localActive to equal active', done => {
|
||||
vm.active = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.localActive).toBe(true);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('sets active class', done => {
|
||||
vm.active = true;
|
||||
|
||||
vm.$nextTick(() => {
|
||||
expect(vm.$el.classList).toContain('active');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
68
spec/javascripts/vue_shared/components/tabs/tabs_spec.js
Normal file
68
spec/javascripts/vue_shared/components/tabs/tabs_spec.js
Normal file
|
@ -0,0 +1,68 @@
|
|||
import Vue from 'vue';
|
||||
import Tabs from '~/vue_shared/components/tabs/tabs';
|
||||
import Tab from '~/vue_shared/components/tabs/tab.vue';
|
||||
|
||||
describe('Tabs component', () => {
|
||||
let vm;
|
||||
|
||||
beforeEach(done => {
|
||||
vm = new Vue({
|
||||
components: {
|
||||
Tabs,
|
||||
Tab,
|
||||
},
|
||||
template: `
|
||||
<div>
|
||||
<tabs>
|
||||
<tab title="Testing" active>
|
||||
First tab
|
||||
</tab>
|
||||
<tab>
|
||||
<template slot="title">Test slot</template>
|
||||
Second tab
|
||||
</tab>
|
||||
</tabs>
|
||||
</div>
|
||||
`,
|
||||
}).$mount();
|
||||
|
||||
setTimeout(done);
|
||||
});
|
||||
|
||||
describe('tab links', () => {
|
||||
it('renders links for tabs', () => {
|
||||
expect(vm.$el.querySelectorAll('a').length).toBe(2);
|
||||
});
|
||||
|
||||
it('renders link titles from props', () => {
|
||||
expect(vm.$el.querySelector('a').textContent).toContain('Testing');
|
||||
});
|
||||
|
||||
it('renders link titles from slot', () => {
|
||||
expect(vm.$el.querySelectorAll('a')[1].textContent).toContain('Test slot');
|
||||
});
|
||||
|
||||
it('renders active class', () => {
|
||||
expect(vm.$el.querySelector('a').classList).toContain('active');
|
||||
});
|
||||
|
||||
it('updates active class on click', done => {
|
||||
vm.$el.querySelectorAll('a')[1].click();
|
||||
|
||||
setTimeout(() => {
|
||||
expect(vm.$el.querySelector('a').classList).not.toContain('active');
|
||||
expect(vm.$el.querySelectorAll('a')[1].classList).toContain('active');
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('content', () => {
|
||||
it('renders content panes', () => {
|
||||
expect(vm.$el.querySelectorAll('.tab-pane').length).toBe(2);
|
||||
expect(vm.$el.querySelectorAll('.tab-pane')[0].textContent).toContain('First tab');
|
||||
expect(vm.$el.querySelectorAll('.tab-pane')[1].textContent).toContain('Second tab');
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue