Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend/ci/reports/components')
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap26
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap37
-rw-r--r--spec/frontend/ci/reports/components/grouped_issues_list_spec.js87
-rw-r--r--spec/frontend/ci/reports/components/issue_status_icon_spec.js29
-rw-r--r--spec/frontend/ci/reports/components/report_item_spec.js34
-rw-r--r--spec/frontend/ci/reports/components/report_link_spec.js56
-rw-r--r--spec/frontend/ci/reports/components/report_section_spec.js285
-rw-r--r--spec/frontend/ci/reports/components/summary_row_spec.js68
8 files changed, 622 insertions, 0 deletions
diff --git a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
new file mode 100644
index 00000000000..311a67a3e31
--- /dev/null
+++ b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = `
+Object {
+ "length": 4,
+ "remain": 20,
+ "rtag": "div",
+ "size": 32,
+ "wclass": "report-block-list",
+ "wtag": "ul",
+}
+`;
+
+exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
+Object {
+ "component": "CodequalityIssueBody",
+ "iconComponent": "IssueStatusIcon",
+ "isNew": false,
+ "issue": Object {
+ "name": "foo",
+ },
+ "showReportSectionStatusIcon": false,
+ "status": "none",
+ "statusIconSize": 24,
+}
+`;
diff --git a/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
new file mode 100644
index 00000000000..b5a4cb42463
--- /dev/null
+++ b/spec/frontend/ci/reports/components/__snapshots__/issue_status_icon_spec.js.snap
@@ -0,0 +1,37 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`IssueStatusIcon renders "failed" state correctly 1`] = `
+<div
+ class="report-block-list-icon failed"
+>
+ <gl-icon-stub
+ data-qa-selector="status_failed_icon"
+ name="status_failed_borderless"
+ size="24"
+ />
+</div>
+`;
+
+exports[`IssueStatusIcon renders "neutral" state correctly 1`] = `
+<div
+ class="report-block-list-icon neutral"
+>
+ <gl-icon-stub
+ data-qa-selector="status_neutral_icon"
+ name="dash"
+ size="24"
+ />
+</div>
+`;
+
+exports[`IssueStatusIcon renders "success" state correctly 1`] = `
+<div
+ class="report-block-list-icon success"
+>
+ <gl-icon-stub
+ data-qa-selector="status_success_icon"
+ name="status_success_borderless"
+ size="24"
+ />
+</div>
+`;
diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
new file mode 100644
index 00000000000..3e4adfc7794
--- /dev/null
+++ b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
@@ -0,0 +1,87 @@
+import { shallowMount } from '@vue/test-utils';
+import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+
+describe('Grouped Issues List', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
+ wrapper = shallowMount(GroupedIssuesList, {
+ propsData,
+ stubs,
+ });
+ };
+
+ const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a smart virtual list with the correct props', () => {
+ createComponent({
+ propsData: {
+ resolvedIssues: [{ name: 'foo' }],
+ unresolvedIssues: [{ name: 'bar' }],
+ },
+ stubs: {
+ SmartVirtualList,
+ },
+ });
+
+ expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot();
+ });
+
+ describe('without data', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', (issueName) => {
+ expect(findHeading(issueName).exists()).toBe(false);
+ });
+
+ it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => {
+ expect(wrapper.findComponent(ReportItem).exists()).toBe(false);
+ });
+ });
+
+ describe('with data', () => {
+ it.each`
+ givenIssues | givenHeading | groupName
+ ${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'}
+ ${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'}
+ `('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => {
+ createComponent({
+ propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading },
+ });
+
+ expect(findHeading(groupName).text()).toBe(givenHeading);
+ });
+
+ it.each(['resolved', 'unresolved'])('renders all %s issues', (issueName) => {
+ const issues = [{ name: 'foo' }, { name: 'bar' }];
+
+ createComponent({
+ propsData: { [`${issueName}Issues`]: issues },
+ });
+
+ expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length);
+ });
+
+ it('renders a report item with the correct props', () => {
+ createComponent({
+ propsData: {
+ resolvedIssues: [{ name: 'foo' }],
+ component: 'CodequalityIssueBody',
+ },
+ stubs: {
+ ReportItem,
+ },
+ });
+
+ expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/ci/reports/components/issue_status_icon_spec.js b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
new file mode 100644
index 00000000000..fb13d4407e2
--- /dev/null
+++ b/spec/frontend/ci/reports/components/issue_status_icon_spec.js
@@ -0,0 +1,29 @@
+import { shallowMount } from '@vue/test-utils';
+import ReportItem from '~/ci/reports/components/issue_status_icon.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/ci/reports/constants';
+
+describe('IssueStatusIcon', () => {
+ let wrapper;
+
+ const createComponent = ({ status }) => {
+ wrapper = shallowMount(ReportItem, {
+ propsData: {
+ status,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each([STATUS_SUCCESS, STATUS_NEUTRAL, STATUS_FAILED])(
+ 'renders "%s" state correctly',
+ (status) => {
+ createComponent({ status });
+
+ expect(wrapper.element).toMatchSnapshot();
+ },
+ );
+});
diff --git a/spec/frontend/ci/reports/components/report_item_spec.js b/spec/frontend/ci/reports/components/report_item_spec.js
new file mode 100644
index 00000000000..d835d549531
--- /dev/null
+++ b/spec/frontend/ci/reports/components/report_item_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import { componentNames } from '~/ci/reports/components/issue_body';
+import IssueStatusIcon from '~/ci/reports/components/issue_status_icon.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import { STATUS_SUCCESS } from '~/ci/reports/constants';
+
+describe('ReportItem', () => {
+ describe('showReportSectionStatusIcon', () => {
+ it('does not render CI Status Icon when showReportSectionStatusIcon is false', () => {
+ const wrapper = shallowMount(ReportItem, {
+ propsData: {
+ issue: { foo: 'bar' },
+ component: componentNames.CodequalityIssueBody,
+ status: STATUS_SUCCESS,
+ showReportSectionStatusIcon: false,
+ },
+ });
+
+ expect(wrapper.findComponent(IssueStatusIcon).exists()).toBe(false);
+ });
+
+ it('shows status icon when unspecified', () => {
+ const wrapper = shallowMount(ReportItem, {
+ propsData: {
+ issue: { foo: 'bar' },
+ component: componentNames.CodequalityIssueBody,
+ status: STATUS_SUCCESS,
+ },
+ });
+
+ expect(wrapper.findComponent(IssueStatusIcon).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ci/reports/components/report_link_spec.js b/spec/frontend/ci/reports/components/report_link_spec.js
new file mode 100644
index 00000000000..ba541ba0303
--- /dev/null
+++ b/spec/frontend/ci/reports/components/report_link_spec.js
@@ -0,0 +1,56 @@
+import { shallowMount } from '@vue/test-utils';
+import ReportLink from '~/ci/reports/components/report_link.vue';
+
+describe('app/assets/javascripts/ci/reports/components/report_link.vue', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const defaultProps = {
+ issue: {},
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ReportLink, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ describe('When an issue prop has a $urlPath property', () => {
+ it('render a link that will take the user to the $urlPath', () => {
+ createComponent({ issue: { path: 'Gemfile.lock', urlPath: '/Gemfile.lock' } });
+
+ expect(wrapper.text()).toContain('in');
+ expect(wrapper.find('a').attributes('href')).toBe('/Gemfile.lock');
+ expect(wrapper.find('a').text()).toContain('Gemfile.lock');
+ });
+ });
+
+ describe('When an issue prop has no $urlPath property', () => {
+ it('does not render link', () => {
+ createComponent({ issue: { path: 'Gemfile.lock' } });
+
+ expect(wrapper.find('a').exists()).toBe(false);
+ expect(wrapper.text()).toContain('in');
+ expect(wrapper.text()).toContain('Gemfile.lock');
+ });
+ });
+
+ describe('When an issue prop has a $line property', () => {
+ it('render a line number', () => {
+ createComponent({ issue: { path: 'Gemfile.lock', urlPath: '/Gemfile.lock', line: 22 } });
+
+ expect(wrapper.find('a').text()).toContain('Gemfile.lock:22');
+ });
+ });
+
+ describe('When an issue prop does not have a $line property', () => {
+ it('does not render a line number', () => {
+ createComponent({ issue: { urlPath: '/Gemfile.lock' } });
+
+ expect(wrapper.find('a').text()).not.toContain(':22');
+ });
+ });
+});
diff --git a/spec/frontend/ci/reports/components/report_section_spec.js b/spec/frontend/ci/reports/components/report_section_spec.js
new file mode 100644
index 00000000000..f032b210184
--- /dev/null
+++ b/spec/frontend/ci/reports/components/report_section_spec.js
@@ -0,0 +1,285 @@
+import { GlButton } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import ReportSection from '~/ci/reports/components/report_section.vue';
+
+describe('ReportSection component', () => {
+ let wrapper;
+
+ const findExpandButton = () => wrapper.findComponent(GlButton);
+ const findPopover = () => wrapper.findComponent(HelpPopover);
+ const findReportSection = () => wrapper.find('.js-report-section-container');
+ const expectExpandButtonOpen = () =>
+ expect(findExpandButton().props('icon')).toBe('chevron-lg-up');
+ const expectExpandButtonClosed = () =>
+ expect(findExpandButton().props('icon')).toBe('chevron-lg-down');
+
+ const resolvedIssues = [
+ {
+ name: 'Insecure Dependency',
+ fingerprint: 'ca2e59451e98ae60ba2f54e3857c50e5',
+ path: 'Gemfile.lock',
+ line: 12,
+ urlPath: 'foo/Gemfile.lock',
+ },
+ ];
+
+ const defaultProps = {
+ component: '',
+ status: 'SUCCESS',
+ loadingText: 'Loading Code Quality report',
+ errorText: 'foo',
+ successText: 'Code quality improved on 1 point and degraded on 1 point',
+ resolvedIssues,
+ hasIssues: false,
+ alwaysOpen: false,
+ };
+
+ const createComponent = ({ props = {}, data = {}, slots = {} } = {}) => {
+ wrapper = mountExtended(ReportSection, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ data() {
+ return data;
+ },
+ slots,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ 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}`, () => {
+ createComponent({ props: { hasIssues, alwaysOpen } });
+
+ expect(wrapper.vm.isCollapsible).toBe(isCollapsible);
+ });
+ });
+ });
+
+ 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}`, () => {
+ createComponent({ props: { alwaysOpen }, data: { isCollapsed } });
+
+ expect(wrapper.vm.isExpanded).toBe(isExpanded);
+ });
+ });
+ });
+ });
+
+ describe('when it is loading', () => {
+ it('should render loading indicator', () => {
+ createComponent({
+ props: {
+ component: '',
+ status: 'LOADING',
+ loadingText: 'Loading Code Quality report',
+ errorText: 'foo',
+ successText: 'Code quality improved on 1 point and degraded on 1 point',
+ hasIssues: false,
+ },
+ });
+
+ expect(wrapper.text()).toBe('Loading Code Quality report');
+ });
+ });
+
+ describe('with success status', () => {
+ it('should render provided data', () => {
+ createComponent({ props: { hasIssues: true } });
+
+ expect(wrapper.find('.js-code-text').text()).toBe(
+ 'Code quality improved on 1 point and degraded on 1 point',
+ );
+ expect(wrapper.findAllComponents(ReportItem)).toHaveLength(resolvedIssues.length);
+ });
+
+ describe('toggleCollapsed', () => {
+ it('toggles issues', async () => {
+ createComponent({ props: { hasIssues: true } });
+
+ await findExpandButton().trigger('click');
+
+ expect(findReportSection().isVisible()).toBe(true);
+ expectExpandButtonOpen();
+
+ await findExpandButton().trigger('click');
+
+ expect(findReportSection().isVisible()).toBe(false);
+ expectExpandButtonClosed();
+ });
+
+ it('is always expanded, if always-open is set to true', () => {
+ createComponent({ props: { hasIssues: true, alwaysOpen: true } });
+
+ expect(findReportSection().isVisible()).toBe(true);
+ expect(findExpandButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('snowplow events', () => {
+ it('does emit an event on issue toggle if the shouldEmitToggleEvent prop does exist', () => {
+ createComponent({ props: { hasIssues: true, shouldEmitToggleEvent: true } });
+
+ expect(wrapper.emitted('toggleEvent')).toBeUndefined();
+
+ findExpandButton().trigger('click');
+
+ expect(wrapper.emitted('toggleEvent')).toEqual([[]]);
+ });
+
+ it('does not emit an event on issue toggle if the shouldEmitToggleEvent prop does not exist', () => {
+ createComponent({ props: { hasIssues: true } });
+
+ expect(wrapper.emitted('toggleEvent')).toBeUndefined();
+
+ findExpandButton().trigger('click');
+
+ expect(wrapper.emitted('toggleEvent')).toBeUndefined();
+ });
+
+ it('does not emit an event if always-open is set to true', () => {
+ createComponent({
+ props: { alwaysOpen: true, hasIssues: true, shouldEmitToggleEvent: true },
+ });
+
+ expect(wrapper.emitted('toggleEvent')).toBeUndefined();
+ });
+ });
+
+ describe('with failed request', () => {
+ it('should render error indicator', () => {
+ createComponent({
+ props: {
+ component: '',
+ status: 'ERROR',
+ loadingText: 'Loading Code Quality report',
+ errorText: 'Failed to load Code Quality report',
+ successText: 'Code quality improved on 1 point and degraded on 1 point',
+ hasIssues: false,
+ },
+ });
+
+ expect(wrapper.text()).toBe('Failed to load Code Quality report');
+ });
+ });
+
+ describe('with action buttons passed to the slot', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ status: 'SUCCESS',
+ successText: 'success',
+ hasIssues: true,
+ },
+ slots: {
+ 'action-buttons': ['Action!'],
+ },
+ });
+ });
+
+ it('should render the passed button', () => {
+ expect(wrapper.text()).toContain('Action!');
+ });
+
+ it('should still render the expand/collapse button', () => {
+ expectExpandButtonClosed();
+ });
+ });
+
+ describe('Success and Error slots', () => {
+ const createComponentWithSlots = (status) => {
+ createComponent({
+ props: {
+ status,
+ hasIssues: true,
+ },
+ slots: {
+ success: ['This is a success'],
+ loading: ['This is loading'],
+ error: ['This is an error'],
+ },
+ });
+ };
+
+ it('only renders success slot when status is "SUCCESS"', () => {
+ createComponentWithSlots('SUCCESS');
+
+ expect(wrapper.text()).toContain('This is a success');
+ expect(wrapper.text()).not.toContain('This is an error');
+ expect(wrapper.text()).not.toContain('This is loading');
+ });
+
+ it('only renders error slot when status is "ERROR"', () => {
+ createComponentWithSlots('ERROR');
+
+ expect(wrapper.text()).toContain('This is an error');
+ expect(wrapper.text()).not.toContain('This is a success');
+ expect(wrapper.text()).not.toContain('This is loading');
+ });
+
+ it('only renders loading slot when status is "LOADING"', () => {
+ createComponentWithSlots('LOADING');
+
+ expect(wrapper.text()).toContain('This is loading');
+ expect(wrapper.text()).not.toContain('This is an error');
+ expect(wrapper.text()).not.toContain('This is a success');
+ });
+ });
+
+ describe('help popover', () => {
+ describe('when popover options are defined', () => {
+ const options = {
+ title: 'foo',
+ content: 'bar',
+ };
+
+ beforeEach(() => {
+ createComponent({ props: { popoverOptions: options } });
+ });
+
+ it('popover is shown with options', () => {
+ expect(findPopover().props('options')).toEqual(options);
+ });
+ });
+
+ describe('when popover options are not defined', () => {
+ beforeEach(() => {
+ createComponent({ props: { popoverOptions: {} } });
+ });
+
+ it('popover is not shown', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js
new file mode 100644
index 00000000000..fb2ae5371d5
--- /dev/null
+++ b/spec/frontend/ci/reports/components/summary_row_spec.js
@@ -0,0 +1,68 @@
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+import SummaryRow from '~/ci/reports/components/summary_row.vue';
+
+describe('Summary row', () => {
+ let wrapper;
+
+ const summary = 'SAST detected 1 new vulnerability and 1 fixed vulnerability';
+ const popoverOptions = {
+ title: 'Static Application Security Testing (SAST)',
+ content: '<a>Learn more about SAST</a>',
+ };
+ const statusIcon = 'warning';
+
+ const createComponent = ({ props = {}, slots = {} } = {}) => {
+ wrapper = extendedWrapper(
+ mount(SummaryRow, {
+ propsData: {
+ summary,
+ popoverOptions,
+ statusIcon,
+ ...props,
+ },
+ slots,
+ }),
+ );
+ };
+
+ const findSummary = () => wrapper.findByTestId('summary-row-description');
+ const findStatusIcon = () => wrapper.findByTestId('summary-row-icon');
+ const findHelpPopover = () => wrapper.findComponent(HelpPopover);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders provided summary', () => {
+ createComponent();
+ expect(findSummary().text()).toContain(summary);
+ });
+
+ it('renders provided icon', () => {
+ createComponent();
+ expect(findStatusIcon().classes()).toContain('js-ci-status-icon-warning');
+ });
+
+ it('renders help popover if popoverOptions are provided', () => {
+ createComponent();
+ expect(findHelpPopover().props('options')).toEqual(popoverOptions);
+ });
+
+ it('does not render help popover if popoverOptions are not provided', () => {
+ createComponent({ props: { popoverOptions: null } });
+ expect(findHelpPopover().exists()).toBe(false);
+ });
+
+ describe('summary slot', () => {
+ it('replaces the summary prop', () => {
+ const summarySlotContent = 'Summary slot content';
+ createComponent({ slots: { summary: summarySlotContent } });
+
+ expect(wrapper.text()).not.toContain(summary);
+ expect(findSummary().text()).toContain(summarySlotContent);
+ });
+ });
+});