diff options
Diffstat (limited to 'spec/frontend/ci/reports/components')
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); + }); + }); +}); |