Commit 6e82de21 authored by Phil Hughes's avatar Phil Hughes

Merge branch '31849-pipeline-real-time-header' into 'master'

Pipeline show view real time header section

See merge request !11797
parents 68112433 228b73d5
...@@ -118,7 +118,7 @@ export default Vue.component('pipelines-table', { ...@@ -118,7 +118,7 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeDestroyed() { beforeDestroy() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
......
...@@ -109,7 +109,7 @@ export default { ...@@ -109,7 +109,7 @@ export default {
eventHub.$on('postAction', this.postAction); eventHub.$on('postAction', this.postAction);
}, },
beforeDestroyed() { beforeDestroy() {
eventHub.$off('toggleFolder'); eventHub.$off('toggleFolder');
eventHub.$off('postAction'); eventHub.$off('postAction');
}, },
......
<script>
import ciHeader from '../../vue_shared/components/header_ci_component.vue';
import eventHub from '../event_hub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
name: 'PipelineHeaderSection',
props: {
pipeline: {
type: Object,
required: true,
},
isLoading: {
type: Boolean,
required: true,
},
},
components: {
ciHeader,
loadingIcon,
},
data() {
return {
actions: this.getActions(),
};
},
computed: {
status() {
return this.pipeline.details && this.pipeline.details.status;
},
shouldRenderContent() {
return !this.isLoading && Object.keys(this.pipeline).length;
},
},
methods: {
postAction(action) {
const index = this.actions.indexOf(action);
this.$set(this.actions[index], 'isLoading', true);
eventHub.$emit('headerPostAction', action);
},
getActions() {
const actions = [];
if (this.pipeline.retry_path) {
actions.push({
label: 'Retry',
path: this.pipeline.retry_path,
cssClass: 'js-retry-button btn btn-inverted-secondary',
type: 'button',
isLoading: false,
});
}
if (this.pipeline.cancel_path) {
actions.push({
label: 'Cancel running',
path: this.pipeline.cancel_path,
cssClass: 'js-btn-cancel-pipeline btn btn-danger',
type: 'button',
isLoading: false,
});
}
return actions;
},
},
watch: {
pipeline() {
this.actions = this.getActions();
},
},
};
</script>
<template>
<div class="pipeline-header-container">
<ci-header
v-if="shouldRenderContent"
:status="status"
item-name="Pipeline"
:item-id="pipeline.id"
:time="pipeline.created_at"
:user="pipeline.user"
:actions="actions"
@actionClicked="postAction"
/>
<loading-icon
v-else
size="2"/>
</div>
</template>
...@@ -33,7 +33,7 @@ export default { ...@@ -33,7 +33,7 @@ export default {
<user-avatar-link <user-avatar-link
v-if="user" v-if="user"
class="js-pipeline-url-user" class="js-pipeline-url-user"
:link-href="pipeline.user.web_url" :link-href="pipeline.user.path"
:img-src="pipeline.user.avatar_url" :img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name" :tooltip-text="pipeline.user.name"
/> />
......
/* global Flash */
import Vue from 'vue'; import Vue from 'vue';
import PipelinesMediator from './pipeline_details_mediatior'; import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue'; import pipelineGraph from './components/graph/graph_component.vue';
import pipelineHeader from './components/header_component.vue';
import eventHub from './event_hub';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset; const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
...@@ -9,7 +13,8 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -9,7 +13,8 @@ document.addEventListener('DOMContentLoaded', () => {
mediator.fetchPipeline(); mediator.fetchPipeline();
const pipelineGraphApp = new Vue({ // eslint-disable-next-line
new Vue({
el: '#js-pipeline-graph-vue', el: '#js-pipeline-graph-vue',
data() { data() {
return { return {
...@@ -29,5 +34,37 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -29,5 +34,37 @@ document.addEventListener('DOMContentLoaded', () => {
}, },
}); });
return pipelineGraphApp; // eslint-disable-next-line
new Vue({
el: '#js-pipeline-header-vue',
data() {
return {
mediator,
};
},
components: {
pipelineHeader,
},
created() {
eventHub.$on('headerPostAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('headerPostAction', this.postAction);
},
methods: {
postAction(action) {
this.mediator.service.postAction(action.path)
.then(() => this.mediator.refreshPipeline())
.catch(() => new Flash('An error occurred while making the request.'));
},
},
render(createElement) {
return createElement('pipeline-header', {
props: {
isLoading: this.mediator.state.isLoading,
pipeline: this.mediator.store.state.pipeline,
},
});
},
});
}); });
...@@ -26,6 +26,8 @@ export default class pipelinesMediator { ...@@ -26,6 +26,8 @@ export default class pipelinesMediator {
if (!Visibility.hidden()) { if (!Visibility.hidden()) {
this.state.isLoading = true; this.state.isLoading = true;
this.poll.makeRequest(); this.poll.makeRequest();
} else {
this.refreshPipeline();
} }
Visibility.change(() => { Visibility.change(() => {
...@@ -48,4 +50,10 @@ export default class pipelinesMediator { ...@@ -48,4 +50,10 @@ export default class pipelinesMediator {
this.state.isLoading = false; this.state.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.'); return new Flash('An error occurred while fetching the pipeline.');
} }
refreshPipeline() {
this.service.getPipeline()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
}
} }
...@@ -169,7 +169,7 @@ export default { ...@@ -169,7 +169,7 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeDestroyed() { beforeDestroy() {
eventHub.$off('refreshPipelines'); eventHub.$off('refreshPipelines');
}, },
......
...@@ -11,4 +11,9 @@ export default class PipelineService { ...@@ -11,4 +11,9 @@ export default class PipelineService {
getPipeline() { getPipeline() {
return this.pipeline.get(); return this.pipeline.get();
} }
// eslint-disable-next-line
postAction(endpoint) {
return Vue.http.post(`${endpoint}.json`);
}
} }
...@@ -33,8 +33,6 @@ export default class PipelinesService { ...@@ -33,8 +33,6 @@ export default class PipelinesService {
/** /**
* Post request for all pipelines actions. * Post request for all pipelines actions.
* Endpoint content type needs to be:
* `Content-Type:application/x-www-form-urlencoded`
* *
* @param {String} endpoint * @param {String} endpoint
* @return {Promise} * @return {Promise}
......
...@@ -91,7 +91,7 @@ export default { ...@@ -91,7 +91,7 @@ export default {
hasAuthor() { hasAuthor() {
return this.author && return this.author &&
this.author.avatar_url && this.author.avatar_url &&
this.author.web_url && this.author.path &&
this.author.username; this.author.username;
}, },
...@@ -140,7 +140,7 @@ export default { ...@@ -140,7 +140,7 @@ export default {
<user-avatar-link <user-avatar-link
v-if="hasAuthor" v-if="hasAuthor"
class="avatar-image-container" class="avatar-image-container"
:link-href="author.web_url" :link-href="author.path"
:img-src="author.avatar_url" :img-src="author.avatar_url"
:img-alt="userImageAltDescription" :img-alt="userImageAltDescription"
:tooltip-text="author.username" :tooltip-text="author.username"
......
<script> <script>
import ciIconBadge from './ci_badge_link.vue'; import ciIconBadge from './ci_badge_link.vue';
import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue'; import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip'; import tooltipMixin from '../mixins/tooltip';
import userAvatarLink from './user_avatar/user_avatar_link.vue'; import userAvatarImage from './user_avatar/user_avatar_image.vue';
/** /**
* Renders header component for job and pipeline page based on UI mockups * Renders header component for job and pipeline page based on UI mockups
...@@ -31,7 +32,8 @@ export default { ...@@ -31,7 +32,8 @@ export default {
}, },
user: { user: {
type: Object, type: Object,
required: true, required: false,
default: () => ({}),
}, },
actions: { actions: {
type: Array, type: Array,
...@@ -46,8 +48,9 @@ export default { ...@@ -46,8 +48,9 @@ export default {
components: { components: {
ciIconBadge, ciIconBadge,
loadingIcon,
timeagoTooltip, timeagoTooltip,
userAvatarLink, userAvatarImage,
}, },
computed: { computed: {
...@@ -58,13 +61,13 @@ export default { ...@@ -58,13 +61,13 @@ export default {
methods: { methods: {
onClickAction(action) { onClickAction(action) {
this.$emit('postAction', action); this.$emit('actionClicked', action);
}, },
}, },
}; };
</script> </script>
<template> <template>
<header class="page-content-header top-area"> <header class="page-content-header">
<section class="header-main-content"> <section class="header-main-content">
<ci-icon-badge :status="status" /> <ci-icon-badge :status="status" />
...@@ -79,21 +82,23 @@ export default { ...@@ -79,21 +82,23 @@ export default {
by by
<user-avatar-link <template v-if="user">
:link-href="user.web_url" <a
:img-src="user.avatar_url" :href="user.path"
:img-alt="userAvatarAltText" :title="user.email"
:tooltip-text="user.name" class="js-user-link commit-committer-link"
:img-size="24" ref="tooltip">
/>
<user-avatar-image
<a :img-src="user.avatar_url"
:href="user.web_url" :img-alt="userAvatarAltText"
:title="user.email" :tooltip-text="user.name"
class="js-user-link commit-committer-link" :img-size="24"
ref="tooltip"> />
{{user.name}}
</a> {{user.name}}
</a>
</template>
</section> </section>
<section <section
...@@ -111,11 +116,17 @@ export default { ...@@ -111,11 +116,17 @@ export default {
<button <button
v-else="action.type === 'button'" v-else="action.type === 'button'"
@click="onClickAction(action)" @click="onClickAction(action)"
:disabled="action.isLoading"
:class="action.cssClass" :class="action.cssClass"
type="button"> type="button">
{{action.label}} {{action.label}}
</button>
<i
v-show="action.isLoading"
class="fa fa-spin fa-spinner"
aria-hidden="true">
</i>
</button>
</template> </template>
</section> </section>
</header> </header>
......
...@@ -83,7 +83,7 @@ export default { ...@@ -83,7 +83,7 @@ export default {
} else { } else {
commitAuthorInformation = { commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url, avatar_url: this.pipeline.commit.author_gravatar_url,
web_url: `mailto:${this.pipeline.commit.author_email}`, path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name, username: this.pipeline.commit.author_name,
}; };
} }
......
...@@ -60,6 +60,12 @@ export default { ...@@ -60,6 +60,12 @@ export default {
avatarSizeClass() { avatarSizeClass() {
return `s${this.size}`; return `s${this.size}`;
}, },
// API response sends null when gravatar is disabled and
// we provide an empty string when we use it inside user avatar link.
// In both cases we should render the defaultAvatarUrl
imageSource() {
return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
},
}, },
}; };
</script> </script>
...@@ -68,7 +74,7 @@ export default { ...@@ -68,7 +74,7 @@ export default {
<img <img
class="avatar" class="avatar"
:class="[avatarSizeClass, cssClasses]" :class="[avatarSizeClass, cssClasses]"
:src="imgSrc" :src="imageSource"
:width="size" :width="size"
:height="size" :height="size"
:alt="imgAlt" :alt="imgAlt"
......
...@@ -984,3 +984,11 @@ ...@@ -984,3 +984,11 @@
width: 12px; width: 12px;
} }
} }
.pipeline-header-container {
min-height: 55px;
.text-center {
padding-top: 12px;
}
}
class UserEntity < API::Entities::UserBasic class UserEntity < API::Entities::UserBasic
include RequestAwareEntity
expose :path do |user|
user_path(user)
end
end end
.page-content-header #js-pipeline-header-vue.pipeline-header-container
.header-main-content
= render 'ci/status/badge', status: @pipeline.detailed_status(current_user), title: @pipeline.status_title
%strong Pipeline ##{@pipeline.id}
triggered #{time_ago_with_tooltip(@pipeline.created_at)}
- if @pipeline.user
by
= user_avatar(user: @pipeline.user, size: 24)
= user_link(@pipeline.user)
.header-action-buttons
- if can?(current_user, :update_pipeline, @pipeline.project)
- if @pipeline.retryable?
= link_to "Retry", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'js-retry-button btn btn-inverted-secondary', method: :post
- if @pipeline.cancelable?
= link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
- if @commit - if @commit
.commit-box .commit-box
......
---
title: Makes header information of pipeline show page realtine
merge_request:
author:
...@@ -76,7 +76,7 @@ describe 'Commits' do ...@@ -76,7 +76,7 @@ describe 'Commits' do
end end
end end
describe 'Commit builds' do describe 'Commit builds', :feature, :js do
before do before do
visit ci_status_path(pipeline) visit ci_status_path(pipeline)
end end
...@@ -85,7 +85,6 @@ describe 'Commits' do ...@@ -85,7 +85,6 @@ describe 'Commits' do
expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message expect(page).to have_content pipeline.git_commit_message
expect(page).to have_content pipeline.user.name expect(page).to have_content pipeline.user.name
expect(page).to have_content pipeline.created_at.strftime('%b %d, %Y')
end end
end end
...@@ -102,7 +101,7 @@ describe 'Commits' do ...@@ -102,7 +101,7 @@ describe 'Commits' do
end end
describe 'Cancel all builds' do describe 'Cancel all builds' do
it 'cancels commit' do it 'cancels commit', :js do
visit ci_status_path(pipeline) visit ci_status_path(pipeline)
click_on 'Cancel running' click_on 'Cancel running'
expect(page).to have_content 'canceled' expect(page).to have_content 'canceled'
...@@ -110,9 +109,9 @@ describe 'Commits' do ...@@ -110,9 +109,9 @@ describe 'Commits' do
end end
describe 'Cancel build' do describe 'Cancel build' do
it 'cancels build' do it 'cancels build', :js do
visit ci_status_path(pipeline) visit ci_status_path(pipeline)
find('a.btn[title="Cancel"]').click find('.js-btn-cancel-pipeline').click
expect(page).to have_content 'canceled' expect(page).to have_content 'canceled'
end end
end end
...@@ -152,17 +151,20 @@ describe 'Commits' do ...@@ -152,17 +151,20 @@ describe 'Commits' do
visit ci_status_path(pipeline) visit ci_status_path(pipeline)
end end
it do it 'Renders header', :feature, :js do
expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message expect(page).to have_content pipeline.git_commit_message
expect(page).to have_content pipeline.user.name expect(page).to have_content pipeline.user.name
expect(page).to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Cancel running')
expect(page).not_to have_link('Retry') expect(page).not_to have_link('Retry')
end end
it do
expect(page).to have_link('Download artifacts')
end
end end
context 'when accessing internal project with disallowed access' do context 'when accessing internal project with disallowed access', :feature, :js do
before do before do
project.update( project.update(
visibility_level: Gitlab::VisibilityLevel::INTERNAL, visibility_level: Gitlab::VisibilityLevel::INTERNAL,
...@@ -175,7 +177,7 @@ describe 'Commits' do ...@@ -175,7 +177,7 @@ describe 'Commits' do
expect(page).to have_content pipeline.sha[0..7] expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message expect(page).to have_content pipeline.git_commit_message
expect(page).to have_content pipeline.user.name expect(page).to have_content pipeline.user.name
expect(page).not_to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running') expect(page).not_to have_link('Cancel running')
expect(page).not_to have_link('Retry') expect(page).not_to have_link('Retry')
end end
......
...@@ -229,7 +229,6 @@ describe 'Pipeline', :feature, :js do ...@@ -229,7 +229,6 @@ describe 'Pipeline', :feature, :js do
before { find('.js-retry-button').trigger('click') } before { find('.js-retry-button').trigger('click') }
it { expect(page).not_to have_content('Retry') } it { expect(page).not_to have_content('Retry') }
it { expect(page).to have_selector('.retried') }
end end
end end
...@@ -240,7 +239,6 @@ describe 'Pipeline', :feature, :js do ...@@ -240,7 +239,6 @@ describe 'Pipeline', :feature, :js do
before { click_on 'Cancel running' } before { click_on 'Cancel running' }
it { expect(page).not_to have_content('Cancel running') } it { expect(page).not_to have_content('Cancel running') }
it { expect(page).to have_selector('.ci-canceled') }
end end
end end
......
import Vue from 'vue';
import headerComponent from '~/pipelines/components/header_component.vue';
import eventHub from '~/pipelines/event_hub';
describe('Pipeline details header', () => {
let HeaderComponent;
let vm;
let props;
beforeEach(() => {
HeaderComponent = Vue.extend(headerComponent);
props = {
pipeline: {
details: {
status: {
group: 'failed',
icon: 'ci-status-failed',
label: 'failed',
text: 'failed',
details_path: 'path',
},
},
id: 123,
created_at: '2017-05-08T14:57:39.781Z',
user: {
web_url: 'path',
name: 'Foo',
username: 'foobar',
email: 'foo@bar.com',
avatar_url: 'link',
},
retry_path: 'path',
},
isLoading: false,
};
vm = new HeaderComponent({ propsData: props }).$mount();
});
afterEach(() => {
vm.$destroy();
});
it('should render provided pipeline info', () => {
expect(
vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo');
});
describe('action buttons', () => {
it('should call postAction when button action is clicked', () => {
eventHub.$on('headerPostAction', (action) => {
expect(action.path).toEqual('path');
});
vm.$el.querySelector('button').click();
});
});
});
import Vue from 'vue';
import PipelineMediator from '~/pipelines/pipeline_details_mediatior';
describe('PipelineMdediator', () => {
let mediator;
beforeEach(() => {
mediator = new PipelineMediator({ endpoint: 'foo' });
});
it('should set defaults', () => {
expect(mediator.options).toEqual({ endpoint: 'foo' });
expect(mediator.state.isLoading).toEqual(false);
expect(mediator.store).toBeDefined();
expect(mediator.service).toBeDefined();
});
describe('request and store data', () => {
const interceptor = (request, next) => {
next(request.respondWith(JSON.stringify({ foo: 'bar' }), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(interceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor);
});
it('should store received data', (done) => {
mediator.fetchPipeline();
setTimeout(() => {
expect(mediator.store.state.pipeline).toEqual({ foo: 'bar' });
done();
});
});
});
});
import PipelineStore from '~/pipelines/stores/pipeline_store';
describe('Pipeline Store', () => {
let store;
beforeEach(() => {
store = new PipelineStore();
});
it('should set defaults', () => {
expect(store.state).toEqual({ pipeline: {} });
expect(store.state.pipeline).toEqual({});
});
describe('storePipeline', () => {
it('should store empty object if none is provided', () => {
store.storePipeline();
expect(store.state.pipeline).toEqual({});
});
it('should store received object', () => {
store.storePipeline({ foo: 'bar' });
expect(store.state.pipeline).toEqual({ foo: 'bar' });
});
});
});
...@@ -47,6 +47,7 @@ describe('Pipeline Url Component', () => { ...@@ -47,6 +47,7 @@ describe('Pipeline Url Component', () => {
web_url: '/', web_url: '/',
name: 'foo', name: 'foo',
avatar_url: '/', avatar_url: '/',
path: '/',
}, },
}, },
}; };
......
...@@ -24,6 +24,7 @@ describe('Commit component', () => { ...@@ -24,6 +24,7 @@ describe('Commit component', () => {
author: { author: {
avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1', web_url: 'https://gitlab.com/jschatz1',
path: '/jschatz1',
username: 'jschatz1', username: 'jschatz1',
}, },
}, },
...@@ -46,6 +47,7 @@ describe('Commit component', () => { ...@@ -46,6 +47,7 @@ describe('Commit component', () => {
author: { author: {
avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png', avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1', web_url: 'https://gitlab.com/jschatz1',
path: '/jschatz1',
username: 'jschatz1', username: 'jschatz1',
}, },
commitIconSvg: '<svg></svg>', commitIconSvg: '<svg></svg>',
...@@ -81,7 +83,7 @@ describe('Commit component', () => { ...@@ -81,7 +83,7 @@ describe('Commit component', () => {
it('should render a link to the author profile', () => { it('should render a link to the author profile', () => {
expect( expect(
component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'), component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
).toEqual(props.author.web_url); ).toEqual(props.author.path);
}); });
it('Should render the author avatar with title and alt attributes', () => { it('Should render the author avatar with title and alt attributes', () => {
......
...@@ -33,12 +33,14 @@ describe('Header CI Component', () => { ...@@ -33,12 +33,14 @@ describe('Header CI Component', () => {
path: 'path', path: 'path',
type: 'button', type: 'button',
cssClass: 'btn', cssClass: 'btn',
isLoading: false,
}, },
{ {
label: 'Go', label: 'Go',
path: 'path', path: 'path',
type: 'link', type: 'link',
cssClass: 'link', cssClass: 'link',
isLoading: false,
}, },
], ],
}; };
...@@ -79,4 +81,13 @@ describe('Header CI Component', () => { ...@@ -79,4 +81,13 @@ describe('Header CI Component', () => {
expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label); expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path); expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
}); });
it('should show loading icon', (done) => {
vm.actions[0].isLoading = true;
Vue.nextTick(() => {
expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toEqual('');
done();
});
});
}); });
...@@ -76,7 +76,7 @@ describe('Pipelines Table Row', () => { ...@@ -76,7 +76,7 @@ describe('Pipelines Table Row', () => {
it('should render user information', () => { it('should render user information', () => {
expect( expect(
component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'), component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'),
).toEqual(pipeline.user.web_url); ).toEqual(pipeline.user.path);
expect( expect(
component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'), component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'),
...@@ -120,7 +120,7 @@ describe('Pipelines Table Row', () => { ...@@ -120,7 +120,7 @@ describe('Pipelines Table Row', () => {
component = buildComponent(pipeline); component = buildComponent(pipeline);
const { commitAuthorLink, commitAuthorName } = findElements(); const { commitAuthorLink, commitAuthorName } = findElements();
expect(commitAuthorLink).toEqual(pipeline.commit.author.web_url); expect(commitAuthorLink).toEqual(pipeline.commit.author.path);
expect(commitAuthorName).toEqual(pipeline.commit.author.username); expect(commitAuthorName).toEqual(pipeline.commit.author.username);
}); });
......
require 'spec_helper' require 'spec_helper'
describe UserEntity do describe UserEntity do
include Gitlab::Routing
let(:entity) { described_class.new(user) } let(:entity) { described_class.new(user) }
let(:user) { create(:user) } let(:user) { create(:user) }
subject { entity.as_json } subject { entity.as_json }
...@@ -20,4 +22,8 @@ describe UserEntity do ...@@ -20,4 +22,8 @@ describe UserEntity do
it 'does not expose 2FA OTPs' do it 'does not expose 2FA OTPs' do
expect(subject).not_to include(/otp/) expect(subject).not_to include(/otp/)
end end
it 'exposes user path' do
expect(subject[:path]).to eq user_path(user)
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment