Backports security reports reusable components into CE code base
This commit is contained in:
parent
16b867d8ce
commit
cd1b20ae93
12 changed files with 883 additions and 0 deletions
|
@ -0,0 +1,48 @@
|
|||
<script>
|
||||
import $ from 'jquery';
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
import { inserted } from '~/feature_highlight/feature_highlight_helper';
|
||||
import { mouseenter, debouncedMouseleave, togglePopover } from '~/shared/popover';
|
||||
|
||||
export default {
|
||||
name: 'ReportsHelpPopover',
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
options: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const $el = $(this.$el);
|
||||
|
||||
$el
|
||||
.popover({
|
||||
html: true,
|
||||
trigger: 'focus',
|
||||
container: 'body',
|
||||
placement: 'top',
|
||||
template:
|
||||
'<div class="popover" role="tooltip"><div class="arrow"></div><p class="popover-header"></p><div class="popover-body"></div></div>',
|
||||
...this.options,
|
||||
})
|
||||
.on('mouseenter', mouseenter)
|
||||
.on('mouseleave', debouncedMouseleave(300))
|
||||
.on('inserted.bs.popover', inserted)
|
||||
.on('show.bs.popover', () => {
|
||||
window.addEventListener('scroll', togglePopover.bind($el, false), { once: true });
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-blank btn-transparent btn-help"
|
||||
tabindex="0"
|
||||
>
|
||||
<icon name="question" />
|
||||
</button>
|
||||
</template>
|
|
@ -0,0 +1,99 @@
|
|||
<script>
|
||||
import IssuesBlock from './report_issues.vue';
|
||||
|
||||
/**
|
||||
* Renders block of issues
|
||||
*/
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IssuesBlock,
|
||||
},
|
||||
props: {
|
||||
unresolvedIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
resolvedIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
neutralIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
allIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isFullReportVisible: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
unresolvedIssuesStatus() {
|
||||
return this.type === 'license' ? 'neutral' : 'failed';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
openFullReport() {
|
||||
this.isFullReportVisible = true;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="report-block-container">
|
||||
|
||||
<issues-block
|
||||
v-if="unresolvedIssues.length"
|
||||
:type="type"
|
||||
:status="unresolvedIssuesStatus"
|
||||
:issues="unresolvedIssues"
|
||||
class="js-mr-code-new-issues"
|
||||
/>
|
||||
|
||||
<issues-block
|
||||
v-if="isFullReportVisible"
|
||||
:type="type"
|
||||
:issues="allIssues"
|
||||
class="js-mr-code-all-issues"
|
||||
status="failed"
|
||||
/>
|
||||
|
||||
<issues-block
|
||||
v-if="neutralIssues.length"
|
||||
:type="type"
|
||||
:issues="neutralIssues"
|
||||
class="js-mr-code-non-issues"
|
||||
status="neutral"
|
||||
/>
|
||||
|
||||
<issues-block
|
||||
v-if="resolvedIssues.length"
|
||||
:type="type"
|
||||
:issues="resolvedIssues"
|
||||
class="js-mr-code-resolved-issues"
|
||||
status="success"
|
||||
/>
|
||||
|
||||
<button
|
||||
v-if="allIssues.length && !isFullReportVisible"
|
||||
type="button"
|
||||
class="btn-link btn-blank prepend-left-10 js-expand-full-list break-link"
|
||||
@click="openFullReport"
|
||||
>
|
||||
{{ s__("ciReport|Show complete code vulnerabilities report") }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,33 @@
|
|||
<script>
|
||||
import { mapActions } from 'vuex';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
// failed || success
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions(['openModal']),
|
||||
handleIssueClick() {
|
||||
const { issue, status, openModal } = this;
|
||||
openModal({ issue, status });
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<button
|
||||
type="button"
|
||||
class="btn-link btn-blank text-left break-link vulnerability-name-button"
|
||||
@click="handleIssueClick()"
|
||||
>
|
||||
{{ issue.title }}
|
||||
</button>
|
||||
</template>
|
|
@ -0,0 +1,72 @@
|
|||
<script>
|
||||
import Icon from '~/vue_shared/components/icon.vue';
|
||||
|
||||
export default {
|
||||
name: 'ReportIssues',
|
||||
components: {
|
||||
Icon,
|
||||
},
|
||||
props: {
|
||||
issues: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
// failed || success
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconName() {
|
||||
if (this.isStatusFailed) {
|
||||
return 'status_failed_borderless';
|
||||
} else if (this.isStatusSuccess) {
|
||||
return 'status_success_borderless';
|
||||
}
|
||||
|
||||
return 'status_created_borderless';
|
||||
},
|
||||
isStatusFailed() {
|
||||
return this.status === 'failed';
|
||||
},
|
||||
isStatusSuccess() {
|
||||
return this.status === 'success';
|
||||
},
|
||||
isStatusNeutral() {
|
||||
return this.status === 'neutral';
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<ul class="report-block-list">
|
||||
<li
|
||||
v-for="(issue, index) in issues"
|
||||
:class="{ 'is-dismissed': issue.isDismissed }"
|
||||
:key="index"
|
||||
class="report-block-list-issue"
|
||||
>
|
||||
<div
|
||||
:class="{
|
||||
failed: isStatusFailed,
|
||||
success: isStatusSuccess,
|
||||
neutral: isStatusNeutral,
|
||||
}"
|
||||
class="report-block-list-icon append-right-5"
|
||||
>
|
||||
<icon
|
||||
:name="iconName"
|
||||
:size="32"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,29 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'ReportIssueLink',
|
||||
props: {
|
||||
issue: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="report-block-list-issue-description-link">
|
||||
in
|
||||
|
||||
<a
|
||||
v-if="issue.urlPath"
|
||||
:href="issue.urlPath"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer nofollow"
|
||||
class="break-link"
|
||||
>
|
||||
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
|
||||
</a>
|
||||
<template v-else>
|
||||
{{ issue.path }}<template v-if="issue.line">:{{ issue.line }}</template>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,192 @@
|
|||
<script>
|
||||
import { __ } from '~/locale';
|
||||
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
|
||||
import IssuesList from './issues_list.vue';
|
||||
import Popover from './help_popover.vue';
|
||||
|
||||
const LOADING = 'LOADING';
|
||||
const ERROR = 'ERROR';
|
||||
const SUCCESS = 'SUCCESS';
|
||||
|
||||
export default {
|
||||
name: 'ReportSection',
|
||||
components: {
|
||||
IssuesList,
|
||||
StatusIcon,
|
||||
Popover,
|
||||
},
|
||||
props: {
|
||||
alwaysOpen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
status: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
loadingText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
errorText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
successText: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
unresolvedIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
resolvedIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
neutralIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
allIssues: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
infoText: {
|
||||
type: [String, Boolean],
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
hasIssues: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
popoverOptions: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
isCollapsed: true,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
collapseText() {
|
||||
return this.isCollapsed ? __('Expand') : __('Collapse');
|
||||
},
|
||||
isLoading() {
|
||||
return this.status === LOADING;
|
||||
},
|
||||
loadingFailed() {
|
||||
return this.status === ERROR;
|
||||
},
|
||||
isSuccess() {
|
||||
return this.status === SUCCESS;
|
||||
},
|
||||
isCollapsible() {
|
||||
return !this.alwaysOpen && this.hasIssues;
|
||||
},
|
||||
isExpanded() {
|
||||
return this.alwaysOpen || !this.isCollapsed;
|
||||
},
|
||||
statusIconName() {
|
||||
if (this.isLoading) {
|
||||
return 'loading';
|
||||
}
|
||||
if (this.loadingFailed || this.unresolvedIssues.length || this.neutralIssues.length) {
|
||||
return 'warning';
|
||||
}
|
||||
return 'success';
|
||||
},
|
||||
headerText() {
|
||||
if (this.isLoading) {
|
||||
return this.loadingText;
|
||||
}
|
||||
|
||||
if (this.isSuccess) {
|
||||
return this.successText;
|
||||
}
|
||||
|
||||
if (this.loadingFailed) {
|
||||
return this.errorText;
|
||||
}
|
||||
|
||||
return '';
|
||||
},
|
||||
hasPopover() {
|
||||
return Object.keys(this.popoverOptions).length > 0;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleCollapsed() {
|
||||
this.isCollapsed = !this.isCollapsed;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<section class="media-section">
|
||||
<div
|
||||
class="media"
|
||||
>
|
||||
<status-icon
|
||||
:status="statusIconName"
|
||||
/>
|
||||
<div
|
||||
class="media-body space-children d-flex"
|
||||
>
|
||||
<span
|
||||
class="js-code-text code-text"
|
||||
>
|
||||
{{ headerText }}
|
||||
|
||||
<popover
|
||||
v-if="hasPopover"
|
||||
:options="popoverOptions"
|
||||
class="prepend-left-5"
|
||||
/>
|
||||
</span>
|
||||
|
||||
<button
|
||||
v-if="isCollapsible"
|
||||
type="button"
|
||||
class="js-collapse-btn btn bt-default float-right btn-sm"
|
||||
@click="toggleCollapsed"
|
||||
>
|
||||
{{ collapseText }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="hasIssues"
|
||||
v-show="isExpanded"
|
||||
class="js-report-section-container"
|
||||
>
|
||||
<slot name="body">
|
||||
<issues-list
|
||||
:unresolved-issues="unresolvedIssues"
|
||||
:resolved-issues="resolvedIssues"
|
||||
:all-issues="allIssues"
|
||||
:type="type"
|
||||
/>
|
||||
</slot>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
|
@ -0,0 +1,66 @@
|
|||
<script>
|
||||
import CiIcon from '~/vue_shared/components/ci_icon.vue';
|
||||
import LoadingIcon from '~/vue_shared/components/loading_icon.vue';
|
||||
import Popover from './help_popover.vue';
|
||||
|
||||
/**
|
||||
* Renders the summary row for each report
|
||||
*
|
||||
* Used both in MR widget and Pipeline's view for:
|
||||
* - Unit tests reports
|
||||
* - Security reports
|
||||
*/
|
||||
|
||||
export default {
|
||||
name: 'ReportSummaryRow',
|
||||
components: {
|
||||
CiIcon,
|
||||
LoadingIcon,
|
||||
Popover,
|
||||
},
|
||||
props: {
|
||||
summary: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
statusIcon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
popoverOptions: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
iconStatus() {
|
||||
return {
|
||||
group: this.statusIcon,
|
||||
icon: `status_${this.statusIcon}`,
|
||||
};
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<template>
|
||||
<div class="report-block-list-issue report-block-list-issue-parent">
|
||||
<div class="report-block-list-icon append-right-10 prepend-left-5">
|
||||
<loading-icon
|
||||
v-if="statusIcon === 'loading'"
|
||||
css-class="report-block-list-loading-icon"
|
||||
/>
|
||||
<ci-icon
|
||||
v-else
|
||||
:status="iconStatus"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="report-block-list-issue-description">
|
||||
<div class="report-block-list-issue-description-text">
|
||||
{{ summary }}
|
||||
</div>
|
||||
|
||||
<popover :options="popoverOptions" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -0,0 +1,45 @@
|
|||
import Vue from 'vue';
|
||||
import Vuex from 'vuex';
|
||||
import component from '~/vue_shared/components/reports/modal_open_name.vue';
|
||||
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
|
||||
|
||||
describe('Modal open name', () => {
|
||||
const Component = Vue.extend(component);
|
||||
let vm;
|
||||
|
||||
const store = new Vuex.Store({
|
||||
actions: {
|
||||
openModal: () => {},
|
||||
},
|
||||
state: {},
|
||||
mutations: {},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponentWithStore(Component, {
|
||||
store,
|
||||
props: {
|
||||
issue: {
|
||||
title: 'Issue',
|
||||
},
|
||||
status: 'failed',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders the issue name', () => {
|
||||
expect(vm.$el.textContent.trim()).toEqual('Issue');
|
||||
});
|
||||
|
||||
it('calls openModal actions when button is clicked', () => {
|
||||
spyOn(vm, 'openModal');
|
||||
|
||||
vm.$el.click();
|
||||
|
||||
expect(vm.openModal).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,17 @@
|
|||
// import Vue from 'vue';
|
||||
// import reportIssues from '~/vue_shared/reports/components/report_issues.vue';
|
||||
|
||||
// describe('Report issues', () => {
|
||||
// let vm;
|
||||
// let ReportIssues;
|
||||
|
||||
// beforeEach(() => {
|
||||
// ReportIssues = Vue.extend(reportIssues);
|
||||
// });
|
||||
|
||||
// afterEach(() => {
|
||||
// vm.$destroy();
|
||||
// });
|
||||
|
||||
// // TODO
|
||||
// });
|
|
@ -0,0 +1,71 @@
|
|||
import Vue from 'vue';
|
||||
import component from '~/vue_shared/components/reports/report_link.vue';
|
||||
import mountComponent from '../../../helpers/vue_mount_component_helper';
|
||||
|
||||
describe('report link', () => {
|
||||
let vm;
|
||||
|
||||
const Component = Vue.extend(component);
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('With url', () => {
|
||||
it('renders link', () => {
|
||||
vm = mountComponent(Component, {
|
||||
issue: {
|
||||
path: 'Gemfile.lock',
|
||||
urlPath: '/Gemfile.lock',
|
||||
},
|
||||
});
|
||||
|
||||
expect(vm.$el.textContent.trim()).toContain('in');
|
||||
expect(vm.$el.querySelector('a').getAttribute('href')).toEqual('/Gemfile.lock');
|
||||
expect(vm.$el.querySelector('a').textContent.trim()).toEqual('Gemfile.lock');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Without url', () => {
|
||||
it('does not render link', () => {
|
||||
vm = mountComponent(Component, {
|
||||
issue: {
|
||||
path: 'Gemfile.lock',
|
||||
},
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('a')).toBeNull();
|
||||
expect(vm.$el.textContent.trim()).toContain('in');
|
||||
expect(vm.$el.textContent.trim()).toContain('Gemfile.lock');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with line', () => {
|
||||
it('renders line number', () => {
|
||||
vm = mountComponent(Component, {
|
||||
issue: {
|
||||
path: 'Gemfile.lock',
|
||||
urlPath:
|
||||
'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
|
||||
line: 22,
|
||||
},
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('a').textContent.trim()).toContain('Gemfile.lock:22');
|
||||
});
|
||||
});
|
||||
|
||||
describe('without line', () => {
|
||||
it('does not render line number', () => {
|
||||
vm = mountComponent(Component, {
|
||||
issue: {
|
||||
path: 'Gemfile.lock',
|
||||
urlPath:
|
||||
'https://groups.google.com/forum/#!topic/rubyonrails-security/335P1DcLG00',
|
||||
},
|
||||
});
|
||||
|
||||
expect(vm.$el.querySelector('a').textContent.trim()).not.toContain(':22');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,174 @@
|
|||
import Vue from 'vue';
|
||||
import reportSection from '~/vue_shared/components/reports/report_section.vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
|
||||
describe('Report section', () => {
|
||||
let vm;
|
||||
const ReportSection = Vue.extend(reportSection);
|
||||
|
||||
const resolvedIssues = [
|
||||
{
|
||||
name: 'Insecure Dependency',
|
||||
fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
|
||||
path: 'Gemfile.lock',
|
||||
line: 12,
|
||||
urlPath: 'foo/Gemfile.lock',
|
||||
},
|
||||
];
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
describe('computed', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(ReportSection, {
|
||||
type: 'codequality',
|
||||
status: 'SUCCESS',
|
||||
loadingText: 'Loading codeclimate report',
|
||||
errorText: 'foo',
|
||||
successText: 'Code quality improved on 1 point and degraded on 1 point',
|
||||
resolvedIssues,
|
||||
hasIssues: false,
|
||||
alwaysOpen: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('isCollapsible', () => {
|
||||
const testMatrix = [
|
||||
{ hasIssues: false, alwaysOpen: false, isCollapsible: false },
|
||||
{ hasIssues: false, alwaysOpen: true, isCollapsible: false },
|
||||
{ hasIssues: true, alwaysOpen: false, isCollapsible: true },
|
||||
{ hasIssues: true, alwaysOpen: true, isCollapsible: false },
|
||||
];
|
||||
|
||||
testMatrix.forEach(({ hasIssues, alwaysOpen, isCollapsible }) => {
|
||||
const issues = hasIssues ? 'has issues' : 'has no issues';
|
||||
const open = alwaysOpen ? 'is always open' : 'is not always open';
|
||||
|
||||
it(`is ${isCollapsible}, if the report ${issues} and ${open}`, done => {
|
||||
vm.hasIssues = hasIssues;
|
||||
vm.alwaysOpen = alwaysOpen;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.isCollapsible).toBe(isCollapsible);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isExpanded', () => {
|
||||
const testMatrix = [
|
||||
{ isCollapsed: false, alwaysOpen: false, isExpanded: true },
|
||||
{ isCollapsed: false, alwaysOpen: true, isExpanded: true },
|
||||
{ isCollapsed: true, alwaysOpen: false, isExpanded: false },
|
||||
{ isCollapsed: true, alwaysOpen: true, isExpanded: true },
|
||||
];
|
||||
|
||||
testMatrix.forEach(({ isCollapsed, alwaysOpen, isExpanded }) => {
|
||||
const issues = isCollapsed ? 'is collapsed' : 'is not collapsed';
|
||||
const open = alwaysOpen ? 'is always open' : 'is not always open';
|
||||
|
||||
it(`is ${isExpanded}, if the report ${issues} and ${open}`, done => {
|
||||
vm.isCollapsed = isCollapsed;
|
||||
vm.alwaysOpen = alwaysOpen;
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.isExpanded).toBe(isExpanded);
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when it is loading', () => {
|
||||
it('should render loading indicator', () => {
|
||||
vm = mountComponent(ReportSection, {
|
||||
type: 'codequality',
|
||||
status: 'LOADING',
|
||||
loadingText: 'Loading codeclimate report',
|
||||
errorText: 'foo',
|
||||
successText: 'Code quality improved on 1 point and degraded on 1 point',
|
||||
hasIssues: false,
|
||||
});
|
||||
expect(vm.$el.textContent.trim()).toEqual('Loading codeclimate report');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with success status', () => {
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(ReportSection, {
|
||||
type: 'codequality',
|
||||
status: 'SUCCESS',
|
||||
loadingText: 'Loading codeclimate report',
|
||||
errorText: 'foo',
|
||||
successText: 'Code quality improved on 1 point and degraded on 1 point',
|
||||
resolvedIssues,
|
||||
hasIssues: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render provided data', () => {
|
||||
expect(vm.$el.querySelector('.js-code-text').textContent.trim()).toEqual(
|
||||
'Code quality improved on 1 point and degraded on 1 point',
|
||||
);
|
||||
|
||||
expect(vm.$el.querySelectorAll('.js-mr-code-resolved-issues li').length).toEqual(
|
||||
resolvedIssues.length,
|
||||
);
|
||||
});
|
||||
|
||||
describe('toggleCollapsed', () => {
|
||||
const hiddenCss = { display: 'none' };
|
||||
|
||||
it('toggles issues', done => {
|
||||
vm.$el.querySelector('button').click();
|
||||
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
|
||||
expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Collapse');
|
||||
|
||||
vm.$el.querySelector('button').click();
|
||||
})
|
||||
.then(Vue.nextTick)
|
||||
.then(() => {
|
||||
expect(vm.$el.querySelector('.js-report-section-container')).toHaveCss(hiddenCss);
|
||||
expect(vm.$el.querySelector('button').textContent.trim()).toEqual('Expand');
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
|
||||
it('is always expanded, if always-open is set to true', done => {
|
||||
vm.alwaysOpen = true;
|
||||
Vue.nextTick()
|
||||
.then(() => {
|
||||
expect(vm.$el.querySelector('.js-report-section-container')).not.toHaveCss(hiddenCss);
|
||||
expect(vm.$el.querySelector('button')).toBeNull();
|
||||
})
|
||||
.then(done)
|
||||
.catch(done.fail);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with failed request', () => {
|
||||
it('should render error indicator', () => {
|
||||
vm = mountComponent(ReportSection, {
|
||||
type: 'codequality',
|
||||
status: 'ERROR',
|
||||
loadingText: 'Loading codeclimate report',
|
||||
errorText: 'Failed to load codeclimate report',
|
||||
successText: 'Code quality improved on 1 point and degraded on 1 point',
|
||||
hasIssues: false,
|
||||
});
|
||||
expect(vm.$el.textContent.trim()).toEqual('Failed to load codeclimate report');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,37 @@
|
|||
import Vue from 'vue';
|
||||
import component from '~/vue_shared/components/reports/summary_row.vue';
|
||||
import mountComponent from 'spec/helpers/vue_mount_component_helper';
|
||||
|
||||
describe('Summary row', () => {
|
||||
const Component = Vue.extend(component);
|
||||
let vm;
|
||||
|
||||
const props = {
|
||||
summary: 'SAST detected 1 new vulnerability and 1 fixed vulnerability',
|
||||
popoverOptions: {
|
||||
title: 'Static Application Security Testing (SAST)',
|
||||
content: '<a>Learn more about SAST</a>',
|
||||
},
|
||||
statusIcon: 'warning',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
vm = mountComponent(Component, props);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vm.$destroy();
|
||||
});
|
||||
|
||||
it('renders provided summary', () => {
|
||||
expect(
|
||||
vm.$el.querySelector('.report-block-list-issue-description-text').textContent.trim(),
|
||||
).toEqual(props.summary);
|
||||
});
|
||||
|
||||
it('renders provided icon', () => {
|
||||
expect(vm.$el.querySelector('.report-block-list-icon span').classList).toContain(
|
||||
'js-ci-status-icon-warning',
|
||||
);
|
||||
});
|
||||
});
|
Loading…
Reference in a new issue