Commit d693a0a3 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '11170-pipeline-dashboard-doesn-t-add-remove-project' into 'master'

Resolve "Pipeline Dashboard doesn't update after add/remove project"

Closes #11170

See merge request gitlab-org/gitlab-ee!11029
parents fd6e94ab 0f67f5f7
<script>
import _ from 'underscore';
import { mapState, mapActions } from 'vuex';
import { GlLoadingIcon, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
import { GlDashboardSkeleton, GlModal, GlModalDirective, GlButton } from '@gitlab/ui';
import ProjectSelector from '~/vue_shared/components/project_selector/project_selector.vue';
import DashboardProject from './project.vue';
......@@ -9,7 +9,7 @@ export default {
components: {
DashboardProject,
GlModal,
GlLoadingIcon,
GlDashboardSkeleton,
GlButton,
ProjectSelector,
},
......
......@@ -97,7 +97,7 @@ export default {
};
</script>
<template>
<div class="dashboard-card card border-0">
<div class="js-dashboard-project dashboard-card card border-0">
<project-header
:project="project"
:has-pipeline-failed="hasPipelineFailed"
......@@ -117,7 +117,7 @@ export default {
/>
</div>
<div class="col-10 col-sm-6 pr-0 pl-5 align-self-center align-middle ci-table">
<div class="col-10 col-sm-7 pr-0 pl-5 align-self-center align-middle ci-table">
<commit
:tag="commitRef.tag"
:commit-ref="commitRef"
......@@ -129,7 +129,7 @@ export default {
/>
</div>
<div class="col-sm-5 pl-0 text-right align-self-center d-none d-sm-block">
<div class="col-sm-4 pl-0 text-right align-self-center d-none d-sm-block">
<time-ago
v-if="shouldShowTimeAgo"
:time="finishedTime"
......
......@@ -61,14 +61,14 @@ export default {
</gl-link>
</div>
<div class="dropdown js-more-actions">
<gl-button
<button
v-gl-tooltip
class="js-more-actions-toggle d-flex align-items-center bg-transparent border-0 p-0 ml-2"
data-toggle="dropdown"
:title="__('More actions')"
>
<icon name="ellipsis_v" class="text-secondary" />
</gl-button>
</button>
<ul class="dropdown-menu dropdown-menu-right">
<li>
<gl-button class="btn btn-transparent js-remove-button" @click="onRemove">
......
......@@ -76,7 +76,7 @@ export const receiveAddProjectsToDashboardSuccess = ({ dispatch, state }, data)
}
if (added.length) {
dispatch('fetchProjects');
dispatch('forceProjectsRequest');
}
};
......@@ -135,7 +135,7 @@ export const removeProject = ({ dispatch }, removePath) => {
.catch(() => dispatch('receiveRemoveProjectError'));
};
export const receiveRemoveProjectSuccess = ({ dispatch }) => dispatch('fetchProjects');
export const receiveRemoveProjectSuccess = ({ dispatch }) => dispatch('forceProjectsRequest');
export const receiveRemoveProjectError = () => {
createFlash(__('Something went wrong, unable to remove project'));
......
---
title: Fix add/remove pipeline dashboard issue
merge_request: 11029
author:
type: fixed
import Vue from 'vue';
import store from 'ee/operations/store/index';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import Project from 'ee/operations/components/dashboard/project.vue';
import Dashboard from 'ee/operations/components/dashboard/dashboard.vue';
import DashboardProject from 'ee/operations/components/dashboard/project.vue';
import store from 'ee/operations/store';
import timeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import { trimText } from 'spec/helpers/vue_component_helper';
import { getChildInstances, clearState } from '../../helpers';
import { mockProjectData, mockText } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('dashboard component', () => {
const DashboardComponent = Vue.extend(Dashboard);
const DashboardProjectComponent = Vue.extend(DashboardProject);
const mount = () =>
new DashboardComponent({
const mockAddEndpoint = 'mock-addPath';
const mockListEndpoint = 'mock-listPath';
const DashboardComponent = localVue.extend(Dashboard);
let wrapper;
let mockAxios;
const mountComponent = () =>
mount(DashboardComponent, {
sync: false,
store,
localVue,
propsData: {
addPath: 'mock-addPath',
listPath: 'mock-listPath',
addPath: mockAddEndpoint,
listPath: mockListEndpoint,
emptyDashboardSvgPath: '/assets/illustrations/operations-dashboard_empty.svg',
emptyDashboardHelpPath: '/help/user/operations_dashboard/index.html',
},
methods: {
fetchProjects: () => {},
},
}).$mount();
let vm;
});
beforeEach(() => {
vm = mount();
mockAxios = new MockAdapter(axios);
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: mockProjectData(1) });
wrapper = mountComponent();
});
afterEach(() => {
vm.$destroy();
clearState(store);
wrapper.destroy();
mockAxios.restore();
});
it('renders dashboard title', () => {
expect(vm.$el.querySelector('.js-dashboard-title').innerText.trim()).toBe(
mockText.DASHBOARD_TITLE,
);
const dashboardTitle = wrapper.element.querySelector('.js-dashboard-title');
expect(dashboardTitle.innerText.trim()).toEqual(mockText.DASHBOARD_TITLE);
});
describe('add projects button', () => {
let button;
beforeEach(() => {
button = vm.$el.querySelector('.js-add-projects-button');
button = wrapper.element.querySelector('.js-add-projects-button');
});
it('renders add projects text', () => {
......@@ -53,7 +63,31 @@ describe('dashboard component', () => {
it('renders the projects modal', () => {
button.click();
expect(vm.$el.querySelector('.add-projects-modal')).toBeDefined();
expect(wrapper.element.querySelector('.add-projects-modal')).toBeDefined();
});
describe('when a project is added', () => {
it('immediately requests the project list again', done => {
mockAxios.reset();
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: mockProjectData(2) });
mockAxios.onPost(mockAddEndpoint).replyOnce(200, { added: [1], invalid: [] });
wrapper.vm
.$nextTick()
.then(() => {
wrapper.vm.projectClicked({ id: 1 });
})
.then(timeoutPromise)
.then(() => {
wrapper.vm.onOk();
})
.then(timeoutPromise)
.then(() => {
expect(store.state.projects.length).toEqual(2);
expect(wrapper.element.querySelectorAll('.js-dashboard-project').length).toEqual(2);
done();
})
.catch(done.fail);
});
});
});
......@@ -64,53 +98,77 @@ describe('dashboard component', () => {
beforeEach(() => {
store.state.projects = projects;
vm = mount();
wrapper = mountComponent();
});
it('includes a dashboard project component for each project', () => {
expect(getChildInstances(vm, DashboardProjectComponent).length).toBe(projectCount);
const projectComponents = wrapper.element.querySelectorAll('.js-dashboard-project');
expect(projectComponents.length).toBe(projectCount);
});
it('passes each project to the dashboard project component', () => {
const [oneProject] = projects;
const [projectComponent] = getChildInstances(vm, DashboardProjectComponent);
const projectComponent = wrapper.find(Project);
expect(projectComponent.project).toEqual(oneProject);
expect(projectComponent.props().project).toEqual(oneProject);
});
describe('when a project is removed', () => {
it('immediately requests the project list again', done => {
mockAxios.reset();
mockAxios.onDelete(projects[0].remove_path).reply(200);
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: [] });
wrapper.find('.js-remove-button').vm.$emit('click');
timeoutPromise()
.then(() => {
expect(store.state.projects.length).toEqual(0);
expect(wrapper.element.querySelectorAll('.js-dashboard-project').length).toEqual(0);
done();
})
.catch(done.fail);
});
});
});
describe('empty state', () => {
beforeEach(() => {
store.state.projects = [];
vm = mount();
mockAxios.reset();
mockAxios.onGet(mockListEndpoint).replyOnce(200, { projects: [] });
wrapper = mountComponent();
});
it('renders empty state svg after requesting projects with no results', () => {
const svgSrc = vm.$el.querySelector('.js-empty-state-svg').src;
const svgSrc = wrapper.element.querySelector('.js-empty-state-svg').src;
expect(svgSrc).toMatch(mockText.EMPTY_SVG_SOURCE);
});
it('renders title', () => {
expect(vm.$el.querySelector('.js-title').innerText.trim()).toBe(mockText.EMPTY_TITLE);
expect(wrapper.element.querySelector('.js-title').innerText.trim()).toBe(
mockText.EMPTY_TITLE,
);
});
it('renders sub-title', () => {
expect(trimText(vm.$el.querySelector('.js-sub-title').innerText)).toBe(
expect(trimText(wrapper.element.querySelector('.js-sub-title').innerText)).toBe(
mockText.EMPTY_SUBTITLE,
);
});
it('renders link to documentation', () => {
const link = vm.$el.querySelector('.js-documentation-link');
const link = wrapper.element.querySelector('.js-documentation-link');
expect(link.innerText.trim()).toBe('More information');
});
it('links to documentation', () => {
const link = vm.$el.querySelector('.js-documentation-link');
const link = wrapper.element.querySelector('.js-documentation-link');
expect(link.href).toMatch(vm.emptyDashboardHelpPath);
expect(link.href).toMatch(wrapper.props().emptyDashboardHelpPath);
});
});
});
......
......@@ -22,6 +22,7 @@ describe('actions', () => {
afterEach(() => {
clearState(store);
mockAxios.restore();
actions.clearProjectsEtagPoll();
});
describe('addProjectsToDashboard', () => {
......@@ -109,7 +110,7 @@ describe('actions', () => {
[],
[
{
type: 'fetchProjects',
type: 'forceProjectsRequest',
},
],
done,
......@@ -189,10 +190,6 @@ describe('actions', () => {
});
describe('fetchProjects', () => {
afterEach(() => {
actions.clearProjectsEtagPoll();
});
it('calls project list endpoint', done => {
store.state.projectEndpoints.list = mockListEndpoint;
mockAxios.onGet(mockListEndpoint).replyOnce(200);
......@@ -312,7 +309,7 @@ describe('actions', () => {
null,
null,
[],
[{ type: 'fetchProjects' }],
[{ type: 'forceProjectsRequest' }],
done,
);
});
......
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