diff --git a/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/constants.js b/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/constants.js index 54ab3d969962b3c950adcd8898362f6fe57ad27a..703a686da00e6b06e7b7ca44666da3843097f41c 100644 --- a/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/constants.js +++ b/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/constants.js @@ -8,7 +8,7 @@ const AT_LEAST_TWO_APPROVALS = 'AT_LEAST_TWO_APPROVALS'; export const FAIL_STATUS = 'FAIL'; export const NO_STANDARDS_ADHERENCES_FOUND = s__( - 'ComplianceReport|No projects with standards adherence checks found', + 'ComplianceStandardsAdherence|No projects with standards adherence checks found', ); export const STANDARDS_ADHERENCE_CHECK_LABELS = { @@ -33,6 +33,30 @@ export const STANDARDS_ADHERENCE_CHECK_DESCRIPTIONS = { ), }; +export const STANDARDS_ADHERENCE_CHECK_FAILURE_REASONS = { + [PREVENT_APPROVAL_BY_MERGE_REQUEST_AUTHOR]: s__( + 'ComplianceStandardsAdherence|No rule is configured to prevent author approved merge requests.', + ), + [PREVENT_APPROVAL_BY_MERGE_REQUEST_COMMITTERS]: s__( + 'ComplianceStandardsAdherence|No rule configured to prevent merge requests approved by committers.', + ), + [AT_LEAST_TWO_APPROVALS]: s__( + 'ComplianceStandardsAdherence|No rule is configured to require two approvals.', + ), +}; + +export const STANDARDS_ADHERENCE_CHECK_MR_FIX_TITLE = s__( + 'ComplianceStandardsAdherence|The following features help satisfy this requirement.', +); + +export const STANDARDS_ADHERENCE_CHECK_MR_FIX_FEATURES = [ + { + title: s__('ComplianceStandardsAdherence|Merge request approval rules'), + description: s__( + "ComplianceStandardsAdherence|Update approval settings in the project's merge request settings to satisfy this requirement.", + ), + }, +]; const GITLAB = 'GITLAB'; export const STANDARDS_ADHERENCE_STANARD_LABELS = { diff --git a/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/fix_suggestions_sidebar.vue b/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/fix_suggestions_sidebar.vue new file mode 100644 index 0000000000000000000000000000000000000000..398d9c2623d25c9a4441a3b420291885a766dc68 --- /dev/null +++ b/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/fix_suggestions_sidebar.vue @@ -0,0 +1,147 @@ +<script> +import { GlDrawer, GlIcon, GlBadge, GlButton, GlLink } from '@gitlab/ui'; +import { DRAWER_Z_INDEX } from '~/lib/utils/constants'; +import { getContentWrapperHeight } from '~/lib/utils/dom_utils'; +import { joinPaths } from '~/lib/utils/url_utility'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { + FAIL_STATUS, + STANDARDS_ADHERENCE_CHECK_DESCRIPTIONS, + STANDARDS_ADHERENCE_CHECK_FAILURE_REASONS, + STANDARDS_ADHERENCE_CHECK_MR_FIX_TITLE, + STANDARDS_ADHERENCE_CHECK_MR_FIX_FEATURES, + STANDARDS_ADHERENCE_CHECK_LABELS, +} from './constants'; + +export default { + name: 'FixSuggestionsSidebar', + components: { + GlDrawer, + GlIcon, + GlBadge, + GlButton, + GlLink, + }, + props: { + groupPath: { + type: String, + required: true, + }, + showDrawer: { + type: Boolean, + required: false, + default: false, + }, + adherence: { + type: Object, + required: true, + }, + }, + computed: { + getDrawerHeaderHeight() { + return getContentWrapperHeight(); + }, + project() { + return this.adherence.project; + }, + projectMRSettingsPath() { + return joinPaths(this.project.webUrl, '-', 'settings', 'merge_requests'); + }, + isFailedStatus() { + return this.adherence.status === FAIL_STATUS; + }, + adherenceCheckName() { + return STANDARDS_ADHERENCE_CHECK_LABELS[this.adherence.checkName]; + }, + adherenceCheckDescription() { + return STANDARDS_ADHERENCE_CHECK_DESCRIPTIONS[this.adherence.checkName]; + }, + adherenceCheckFailureReason() { + return STANDARDS_ADHERENCE_CHECK_FAILURE_REASONS[this.adherence.checkName]; + }, + }, + standardsAdherenceCheckMRFixTitle: STANDARDS_ADHERENCE_CHECK_MR_FIX_TITLE, + standardsAdherenceCheckMRFixes: STANDARDS_ADHERENCE_CHECK_MR_FIX_FEATURES, + projectMRSettingsDocsPath: helpPagePath('user/project/merge_requests/approvals/rules'), + DRAWER_Z_INDEX, +}; +</script> + +<template> + <gl-drawer + :open="showDrawer" + :header-height="getDrawerHeaderHeight" + :z-index="$options.DRAWER_Z_INDEX" + @close="$emit('close')" + > + <template #title> + <div> + <h2 class="gl-mt-0" data-testid="sidebar-title">{{ adherenceCheckName }}</h2> + <div> + <span v-if="isFailedStatus" class="gl-text-red-500 gl-font-weight-bold"> + <gl-icon name="status_failed" /> {{ __('Fail') }} + </span> + <span v-else class="gl-text-green-500 gl-font-weight-bold"> + <gl-icon name="status_success" /> {{ __('Success') }} + </span> + + <gl-link :href="project.webUrl"> {{ project.name }} </gl-link> + + <span v-for="framework in project.complianceFrameworks.nodes" :key="framework.id"> + <gl-badge size="sm" class="gl-mt-3"> {{ framework.name }}</gl-badge> + </span> + </div> + </div> + </template> + + <template #default> + <div> + <h4 data-testid="sidebar-requirement-title" class="gl-mt-0"> + {{ s__('ComplianceStandardsAdherence|Requirement') }} + </h4> + <span data-testid="sidebar-requirement-content">{{ adherenceCheckDescription }}</span> + </div> + + <div> + <h4 data-testid="sidebar-failure-title" class="gl-mt-0"> + {{ s__('ComplianceStandardsAdherence|Failure reason') }} + </h4> + <span data-testid="sidebar-failure-content">{{ adherenceCheckFailureReason }}</span> + </div> + + <div data-testid="sidebar-how-to-fix"> + <div> + <h4 class="gl-mt-0">{{ s__('ComplianceStandardsAdherence|How to fix') }}</h4> + </div> + <div class="gl-my-5"> + {{ $options.standardsAdherenceCheckMRFixTitle }} + </div> + <div + v-for="fix in $options.standardsAdherenceCheckMRFixes" + :key="fix.title" + class="gl-mb-4" + > + <div class="gl-my-2 gl-font-weight-bold">{{ fix.title }}</div> + <div class="gl-mb-4">{{ fix.description }}</div> + <gl-button + class="gl-my-3" + size="small" + category="secondary" + variant="confirm" + :href="projectMRSettingsPath" + data-testid="sidebar-mr-settings-button" + > + {{ __('Manage rules') }} + </gl-button> + <gl-button + size="small" + :href="$options.projectMRSettingsDocsPath" + data-testid="sidebar-mr-settings-learn-more-button" + > + {{ __('Learn more') }} + </gl-button> + </div> + </div> + </template> + </gl-drawer> +</template> diff --git a/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/standards_adherence_table.vue b/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/standards_adherence_table.vue index 1e7e776f6828d7d98e4166418973b5963c98d1a4..4ba6c372491c1ac0436ba54e571364ee2e573f21 100644 --- a/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/standards_adherence_table.vue +++ b/ee/app/assets/javascripts/compliance_dashboard/components/standards_adherence_report/standards_adherence_table.vue @@ -1,5 +1,5 @@ <script> -import { GlTable, GlIcon, GlLink, GlBadge } from '@gitlab/ui'; +import { GlTable, GlIcon, GlLink, GlBadge, GlLoadingIcon } from '@gitlab/ui'; import * as Sentry from '@sentry/browser'; import { formatDate } from '~/lib/utils/datetime_utility'; import getProjectComplianceStandardsAdherence from '../../graphql/compliance_standards_adherence.query.graphql'; @@ -10,7 +10,7 @@ import { STANDARDS_ADHERENCE_STANARD_LABELS, NO_STANDARDS_ADHERENCES_FOUND, } from './constants'; -// import FixSuggestionsSidebar from './fix_suggestions_sidebar.vue'; +import FixSuggestionsSidebar from './fix_suggestions_sidebar.vue'; export default { name: 'ComplianceStandardsAdherenceTable', @@ -19,7 +19,8 @@ export default { GlIcon, GlLink, GlBadge, - // FixSuggestionsSidebar, + GlLoadingIcon, + FixSuggestionsSidebar, }, props: { groupPath: { @@ -33,7 +34,8 @@ export default { adherences: { list: [], }, - showDrawer: false, + drawerId: null, + drawerAdherence: {}, }; }, apollo: { @@ -56,6 +58,14 @@ export default { }, }, }, + computed: { + isLoading() { + return this.$apollo.queries.adherences.loading; + }, + showDrawer() { + return this.drawerId !== null; + }, + }, methods: { adherenceCheckName(check) { return STANDARDS_ADHERENCE_CHECK_LABELS[check]; @@ -66,15 +76,27 @@ export default { adherenceStandardLabel(standard) { return STANDARDS_ADHERENCE_STANARD_LABELS[standard]; }, - closeDrawer() { - this.showDrawer = false; - }, formatDate(dateString) { return formatDate(dateString, 'mmm d, yyyy'); }, isFailedStatus(status) { return status === FAIL_STATUS; }, + toggleDrawer(item) { + if (this.drawerId === item.id) { + this.closeDrawer(); + } else { + this.openDrawer(item); + } + }, + openDrawer(item) { + this.drawerAdherence = item; + this.drawerId = item.id; + }, + closeDrawer() { + this.drawerAdherence = {}; + this.drawerId = null; + }, }, fields: [ { @@ -119,60 +141,57 @@ export default { </script> <template> - <gl-table - :fields="$options.fields" - :items="adherences.list" - :empty-text="$options.noStandardsAdherencesFound" - show-empty - > - <template #cell(status)="{ item: { status } }"> - <span v-if="isFailedStatus(status)" class="gl-text-red-500"> - <gl-icon name="status_failed" /> {{ __('Fail') }} - </span> - <span v-else class="gl-text-green-500"> - <gl-icon name="status_success" /> {{ __('Success') }} - </span> - </template> - - <template #cell(project)="{ item: { project } }"> - <div>{{ project.name }}</div> - <div v-for="framework in project.complianceFrameworks.nodes" :key="framework.id"> - <gl-badge size="sm" class="gl-mt-3"> {{ framework.name }}</gl-badge> - </div> - </template> - - <template #cell(checks)="{ item: { checkName } }"> - <div class="gl-font-weight-bold">{{ adherenceCheckName(checkName) }}</div> - <div class="gl-mt-2">{{ adherenceCheckDescription(checkName) }}</div> - </template> + <section> + <gl-table + :fields="$options.fields" + :items="adherences.list" + :busy="isLoading" + :empty-text="$options.noStandardsAdherencesFound" + show-empty + > + <template #table-busy> + <gl-loading-icon size="lg" color="dark" class="gl-my-5" /> + </template> + <template #cell(status)="{ item: { status } }"> + <span v-if="isFailedStatus(status)" class="gl-text-red-500"> + <gl-icon name="status_failed" /> {{ __('Fail') }} + </span> + <span v-else class="gl-text-green-500"> + <gl-icon name="status_success" /> {{ __('Success') }} + </span> + </template> - <template #cell(standard)="{ item: { standard } }"> - {{ adherenceStandardLabel(standard) }} - </template> + <template #cell(project)="{ item: { project } }"> + <div>{{ project.name }}</div> + <div v-for="framework in project.complianceFrameworks.nodes" :key="framework.id"> + <gl-badge size="sm" class="gl-mt-3"> {{ framework.name }}</gl-badge> + </div> + </template> - <template #cell(lastScanned)="{ item: { updatedAt } }"> - {{ formatDate(updatedAt) }} - </template> + <template #cell(checks)="{ item: { checkName } }"> + <div class="gl-font-weight-bold">{{ adherenceCheckName(checkName) }}</div> + <div class="gl-mt-2">{{ adherenceCheckDescription(checkName) }}</div> + </template> - <!-- Note: This template will be replaced with the template below --> - <!-- as this is part of https://gitlab.com/gitlab-org/gitlab/-/issues/413718 --> - <template #cell(fixSuggestions)="{}"> - <gl-link @click="showDrawer = true">{{ - s__('ComplianceStandardsAdherence|View details') - }}</gl-link> - </template> + <template #cell(standard)="{ item: { standard } }"> + {{ adherenceStandardLabel(standard) }} + </template> - <!-- <template #cell(fixSuggestions)="{ item: { status, project, checks, fixSuggestions } }">--> - <!-- <gl-link @click="showDrawer = true">{{ fixSuggestions }}</gl-link>--> + <template #cell(lastScanned)="{ item: { updatedAt } }"> + {{ formatDate(updatedAt) }} + </template> - <!-- <fix-suggestions-sidebar--> - <!-- :status="status"--> - <!-- :project="project"--> - <!-- :title="checks.name"--> - <!-- :description="checks.description"--> - <!-- :show-drawer="showDrawer"--> - <!-- @close="closeDrawer"--> - <!-- />--> - <!-- </template>--> - </gl-table> + <template #cell(fixSuggestions)="{ item }"> + <gl-link @click="toggleDrawer(item)">{{ + s__('ComplianceStandardsAdherence|View details') + }}</gl-link> + </template> + </gl-table> + <fix-suggestions-sidebar + :group-path="groupPath" + :show-drawer="showDrawer" + :adherence="drawerAdherence" + @close="closeDrawer" + /> + </section> </template> diff --git a/ee/app/assets/javascripts/compliance_dashboard/graphql/compliance_standards_adherence.query.graphql b/ee/app/assets/javascripts/compliance_dashboard/graphql/compliance_standards_adherence.query.graphql index 918bc69f76d1faa43a30721e7bb5caba21bb7925..edfd12f25012e25d24131c25383b0b6329636fc6 100644 --- a/ee/app/assets/javascripts/compliance_dashboard/graphql/compliance_standards_adherence.query.graphql +++ b/ee/app/assets/javascripts/compliance_dashboard/graphql/compliance_standards_adherence.query.graphql @@ -11,6 +11,7 @@ query getProjectComplianceStandardsAdherence($fullPath: ID!) { project { id name + webUrl complianceFrameworks { nodes { id diff --git a/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/fix_suggestions_sidebar_spec.js b/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/fix_suggestions_sidebar_spec.js new file mode 100644 index 0000000000000000000000000000000000000000..bd8bce32032366fdfcbc18b16feee16393ce0bc3 --- /dev/null +++ b/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/fix_suggestions_sidebar_spec.js @@ -0,0 +1,134 @@ +import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; +import FixSuggestionsSidebar from 'ee/compliance_dashboard/components/standards_adherence_report/fix_suggestions_sidebar.vue'; + +describe('FixSuggestionsSidebar component', () => { + let wrapper; + + const findRequirementSectionTitle = () => wrapper.findByTestId('sidebar-requirement-title'); + const findRequirementSectionContent = () => wrapper.findByTestId('sidebar-requirement-content'); + const findFailureSectionReasonTitle = () => wrapper.findByTestId('sidebar-failure-title'); + const findFailureSectionReasonContent = () => wrapper.findByTestId('sidebar-failure-content'); + const findHowToFixSection = () => wrapper.findByTestId('sidebar-how-to-fix'); + const findManageRulesBtn = () => wrapper.findByTestId('sidebar-mr-settings-button'); + const findLearnMoreBtn = () => wrapper.findByTestId('sidebar-mr-settings-learn-more-button'); + + const createComponent = ({ propsData = {} } = {}) => { + wrapper = shallowMountExtended(FixSuggestionsSidebar, { + propsData: { + showDrawer: true, + groupPath: 'example-group', + ...propsData, + }, + }); + }; + + describe('default behavior', () => { + beforeEach(() => { + createComponent({ + propsData: { + adherence: { + checkName: '', + status: 'PASS', + project: { + id: 'gid://gitlab/Project/21', + name: 'example project', + }, + }, + }, + }); + }); + + describe('for drawer body content', () => { + it('renders the `requirement` title', () => { + expect(findRequirementSectionTitle().text()).toBe('Requirement'); + }); + + it('renders the `failure reason` title', () => { + expect(findFailureSectionReasonTitle().text()).toBe('Failure reason'); + }); + + it('renders the `how to fix` title and description', () => { + expect(findHowToFixSection().text()).toContain('How to fix'); + expect(findHowToFixSection().text()).toContain( + 'The following features help satisfy this requirement', + ); + }); + }); + }); + + describe('content for each check type', () => { + beforeEach(() => { + createComponent({ + propsData: { + adherence: { + checkName: 'PREVENT_APPROVAL_BY_MERGE_REQUEST_AUTHOR', + status: 'PASS', + project: { + id: 'gid://gitlab/Project/21', + name: 'example project', + webUrl: 'example.com/groups/example-group/example-project', + }, + }, + }, + }); + }); + + describe('for checks related to MRs', () => { + describe('for the `how to fix` section', () => { + it('has the details', () => { + expect(findHowToFixSection().text()).toContain('Merge request approval rules'); + + expect(findHowToFixSection().text()).toContain( + "Update approval settings in the project's merge request settings to satisfy this requirement.", + ); + }); + + it('has the `manage rules` button', () => { + expect(findManageRulesBtn().text()).toBe('Manage rules'); + + expect(findManageRulesBtn().attributes('href')).toBe( + 'example.com/groups/example-group/example-project/-/settings/merge_requests', + ); + }); + + it('has the `learn more` button', () => { + expect(findLearnMoreBtn().text()).toBe('Learn more'); + + expect(findLearnMoreBtn().attributes('href')).toBe( + '/help/user/project/merge_requests/approvals/rules', + ); + }); + }); + + describe.each` + checkName | expectedRequirement | expectedFailureReason + ${'PREVENT_APPROVAL_BY_MERGE_REQUEST_AUTHOR'} | ${'Have a valid rule that prevents author approved merge requests'} | ${'No rule is configured to prevent author approved merge requests.'} + ${'PREVENT_APPROVAL_BY_MERGE_REQUEST_COMMITTERS'} | ${'Have a valid rule that prevents merge requests approved by committers'} | ${'No rule configured to prevent merge requests approved by committers.'} + ${'AT_LEAST_TWO_APPROVALS'} | ${'Have a valid rule that requires any merge request to have more than two approvals'} | ${'No rule is configured to require two approvals.'} + `('when check is $checkName', ({ checkName, expectedRequirement, expectedFailureReason }) => { + beforeEach(() => { + createComponent({ + propsData: { + adherence: { + checkName, + status: 'PASS', + project: { + id: 'gid://gitlab/Project/21', + name: 'example project', + }, + }, + }, + }); + }); + + it('renders the requirement', () => { + expect(findRequirementSectionContent().text()).toBe(expectedRequirement); + }); + + it('renders the failure reason', () => { + expect(findFailureSectionReasonContent().text()).toBe(expectedFailureReason); + }); + }); + }); + }); +}); diff --git a/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/report_spec.js b/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/report_spec.js index a7923d263bcc616f7c5b198796bb23840d878108..75c0aaa1c3bfaeb797093873b19ffc161b586d95 100644 --- a/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/report_spec.js +++ b/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/report_spec.js @@ -1,4 +1,4 @@ -import { mount } from '@vue/test-utils'; +import { shallowMount } from '@vue/test-utils'; import { GlAlert } from '@gitlab/ui'; import ComplianceStandardsAdherenceReport from 'ee/compliance_dashboard/components/standards_adherence_report/report.vue'; import ComplianceStandardsAdherenceTable from 'ee/compliance_dashboard/components/standards_adherence_report/standards_adherence_table.vue'; @@ -12,7 +12,7 @@ describe('ComplianceStandardsAdherenceReport component', () => { const findAdherencesTable = () => wrapper.findComponent(ComplianceStandardsAdherenceTable); const createComponent = () => { - wrapper = mount(ComplianceStandardsAdherenceReport, { + wrapper = shallowMount(ComplianceStandardsAdherenceReport, { propsData: { groupPath, }, diff --git a/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/standards_adherence_table_spec.js b/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/standards_adherence_table_spec.js index b75d5e1106dcd21d71d4ddb3728e740971715663..38c9c2691a0ee0aa6247df7770d2d75205f6b7bc 100644 --- a/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/standards_adherence_table_spec.js +++ b/ee/spec/frontend/compliance_dashboard/components/standards_adherence_report/standards_adherence_table_spec.js @@ -1,41 +1,52 @@ -import { GlAlert, GlTable } from '@gitlab/ui'; -import { mountExtended } from 'helpers/vue_test_utils_helper'; +import { GlAlert, GlLoadingIcon, GlTable, GlLink } from '@gitlab/ui'; +import VueApollo from 'vue-apollo'; +import Vue, { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import { extendedWrapper } from 'helpers/vue_test_utils_helper'; import ComplianceStandardsAdherenceTable from 'ee/compliance_dashboard/components/standards_adherence_report/standards_adherence_table.vue'; +import FixSuggestionsSidebar from 'ee/compliance_dashboard/components/standards_adherence_report/fix_suggestions_sidebar.vue'; +import waitForPromises from 'helpers/wait_for_promises'; +import createMockApollo from 'helpers/mock_apollo_helper'; +import getProjectComplianceStandardsAdherence from 'ee/compliance_dashboard/graphql/compliance_standards_adherence.query.graphql'; import { createComplianceAdherencesResponse } from '../../mock_data'; +Vue.use(VueApollo); + describe('ComplianceStandardsAdherenceTable component', () => { let wrapper; - const adherencesResponse = (checkName) => createComplianceAdherencesResponse({ checkName }); - const adherences = (checkName) => - adherencesResponse(checkName).data.group.projectComplianceStandardsAdherence.nodes; + const defaultAdherencesResponse = createComplianceAdherencesResponse(); + const mockGraphQlSuccess = jest.fn().mockResolvedValue(defaultAdherencesResponse); + const mockGraphQlLoading = jest.fn().mockResolvedValue(new Promise(() => {})); + const createMockApolloProvider = (resolverMock) => { + return createMockApollo([[getProjectComplianceStandardsAdherence, resolverMock]]); + }; const findErrorMessage = () => wrapper.findComponent(GlAlert); const findStandardsAdherenceTable = () => wrapper.findComponent(GlTable); + const findFixSuggestionSidebar = () => wrapper.findComponent(FixSuggestionsSidebar); + const findTableLoadingIcon = () => wrapper.findComponent(GlLoadingIcon); const findTableHeaders = () => findStandardsAdherenceTable().findAll('th'); const findTableRows = () => findStandardsAdherenceTable().findAll('tr'); const findFirstTableRow = () => findTableRows().at(1); const findFirstTableRowData = () => findFirstTableRow().findAll('td'); + const findViewDetails = () => wrapper.findComponent(GlLink); - const createComponent = ({ - propsData = {}, - data = {}, - checkName = 'AT_LEAST_TWO_APPROVALS', - } = {}) => { - wrapper = mountExtended(ComplianceStandardsAdherenceTable, { - propsData: { - groupPath: 'example-group', - ...propsData, - }, - data() { - return { - adherences: { - list: adherences(checkName), - }, - ...data, - }; - }, - }); + const openSidebar = async () => { + await findViewDetails().trigger('click'); + await nextTick(); + }; + + const createComponent = ({ propsData = {}, resolverMock = mockGraphQlLoading } = {}) => { + wrapper = extendedWrapper( + mount(ComplianceStandardsAdherenceTable, { + apolloProvider: createMockApolloProvider(resolverMock), + propsData: { + groupPath: 'example-group', + ...propsData, + }, + }), + ); }; describe('default behavior', () => { @@ -62,11 +73,19 @@ describe('ComplianceStandardsAdherenceTable component', () => { }); describe('when there are standards adherence checks available', () => { - describe('when check is `PREVENT_APPROVAL_BY_MERGE_REQUEST_AUTHOR`', () => { - beforeEach(() => { - createComponent({ checkName: 'PREVENT_APPROVAL_BY_MERGE_REQUEST_AUTHOR' }); - }); + beforeEach(() => { + createComponent({ resolverMock: mockGraphQlSuccess }); + return waitForPromises(); + }); + + it('does not render the table loading icon', () => { + expect(mockGraphQlSuccess).toHaveBeenCalledTimes(1); + + expect(findTableLoadingIcon().exists()).toBe(false); + }); + + describe('when check is `PREVENT_APPROVAL_BY_MERGE_REQUEST_AUTHOR`', () => { it('renders the table row properly', () => { const rowText = findFirstTableRowData().wrappers.map((e) => e.text()); @@ -83,7 +102,16 @@ describe('ComplianceStandardsAdherenceTable component', () => { describe('when check is `PREVENT_APPROVAL_BY_MERGE_REQUEST_COMMITTERS`', () => { beforeEach(() => { - createComponent({ checkName: 'PREVENT_APPROVAL_BY_MERGE_REQUEST_COMMITTERS' }); + const preventApprovalbyMRCommitersAdherencesResponse = createComplianceAdherencesResponse({ + checkName: 'PREVENT_APPROVAL_BY_MERGE_REQUEST_COMMITTERS', + }); + const mockResolver = jest + .fn() + .mockResolvedValue(preventApprovalbyMRCommitersAdherencesResponse); + + createComponent({ resolverMock: mockResolver }); + + return waitForPromises(); }); it('renders the table row properly', () => { @@ -102,7 +130,14 @@ describe('ComplianceStandardsAdherenceTable component', () => { describe('when check is `AT_LEAST_TWO_APPROVALS`', () => { beforeEach(() => { - createComponent(); + const atLeastTwoApprovalsAdherencesResponse = createComplianceAdherencesResponse({ + checkName: 'AT_LEAST_TWO_APPROVALS', + }); + const mockResolver = jest.fn().mockResolvedValue(atLeastTwoApprovalsAdherencesResponse); + + createComponent({ resolverMock: mockResolver }); + + return waitForPromises(); }); it('renders the table row properly', () => { @@ -122,7 +157,12 @@ describe('ComplianceStandardsAdherenceTable component', () => { describe('when there are no standards adherence checks available', () => { beforeEach(() => { - createComponent({ data: { adherences: { list: [] } } }); + const noAdherencesResponse = createComplianceAdherencesResponse({ count: 0 }); + const mockResolver = jest.fn().mockResolvedValue(noAdherencesResponse); + + createComponent({ resolverMock: mockResolver }); + + return waitForPromises(); }); it('renders the empty table message', () => { @@ -131,4 +171,36 @@ describe('ComplianceStandardsAdherenceTable component', () => { ); }); }); + + describe('fixSuggestionSidebar', () => { + beforeEach(() => { + createComponent({ resolverMock: mockGraphQlSuccess }); + + return waitForPromises(); + }); + + describe('closing the sidebar', () => { + it('has the correct props when closed', async () => { + await openSidebar(); + + await findFixSuggestionSidebar().vm.$emit('close'); + + expect(findFixSuggestionSidebar().props('groupPath')).toBe('example-group'); + expect(findFixSuggestionSidebar().props('showDrawer')).toBe(false); + expect(findFixSuggestionSidebar().props('adherence')).toStrictEqual({}); + }); + }); + + describe('opening the sidebar', () => { + it('has the correct props when opened', async () => { + await openSidebar(); + + expect(findFixSuggestionSidebar().props('groupPath')).toBe('example-group'); + expect(findFixSuggestionSidebar().props('showDrawer')).toBe(true); + expect(findFixSuggestionSidebar().props('adherence')).toStrictEqual( + wrapper.vm.adherences.list[0], + ); + }); + }); + }); }); diff --git a/ee/spec/frontend/compliance_dashboard/mock_data.js b/ee/spec/frontend/compliance_dashboard/mock_data.js index d1a3ef145470cb843bac34baabee10177ffc0617..f54569efdc5ec178c257bbfe69e6323dbb78686d 100644 --- a/ee/spec/frontend/compliance_dashboard/mock_data.js +++ b/ee/spec/frontend/compliance_dashboard/mock_data.js @@ -45,6 +45,7 @@ export const createComplianceAdherence = (id, checkName) => ({ project: { id: 'gid://gitlab/Project/1', name: 'Example Project', + webUrl: 'example.com/groups/example-group/example-project', complianceFrameworks: { nodes: { id: 'gid://gitlab/ComplianceManagement::Framework/1', @@ -52,6 +53,7 @@ export const createComplianceAdherence = (id, checkName) => ({ description: 'asds', color: '#0000ff', __typename: 'ComplianceFramework', + default: true, }, }, }, @@ -63,7 +65,10 @@ export const createComplianceAdherencesResponse = ({ } = {}) => ({ data: { group: { + id: 'gid://gitlab/Group/1', + __typename: 'Group', projectComplianceStandardsAdherence: { + __typename: 'ComplianceAdherenceConnection', nodes: Array(count) .fill(null) .map((_, id) => createComplianceAdherence(id, checkName)), diff --git a/locale/gitlab.pot b/locale/gitlab.pot index a4f8eaa4c250f9ef58bd02ee5164b4608d637d68..b23534c5ddb4eb308772a9ad0219cae531f742a3 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -12277,9 +12277,6 @@ msgstr "" msgid "ComplianceReport|No projects found that match filters" msgstr "" -msgid "ComplianceReport|No projects with standards adherence checks found" -msgstr "" - msgid "ComplianceReport|No violations found" msgstr "" @@ -12313,6 +12310,9 @@ msgstr "" msgid "ComplianceStandardsAdherence|At least two approvals" msgstr "" +msgid "ComplianceStandardsAdherence|Failure reason" +msgstr "" + msgid "ComplianceStandardsAdherence|Have a valid rule that prevents author approved merge requests" msgstr "" @@ -12322,12 +12322,39 @@ msgstr "" msgid "ComplianceStandardsAdherence|Have a valid rule that requires any merge request to have more than two approvals" msgstr "" +msgid "ComplianceStandardsAdherence|How to fix" +msgstr "" + +msgid "ComplianceStandardsAdherence|Merge request approval rules" +msgstr "" + +msgid "ComplianceStandardsAdherence|No projects with standards adherence checks found" +msgstr "" + +msgid "ComplianceStandardsAdherence|No rule configured to prevent merge requests approved by committers." +msgstr "" + +msgid "ComplianceStandardsAdherence|No rule is configured to prevent author approved merge requests." +msgstr "" + +msgid "ComplianceStandardsAdherence|No rule is configured to require two approvals." +msgstr "" + msgid "ComplianceStandardsAdherence|Prevent authors as approvers" msgstr "" msgid "ComplianceStandardsAdherence|Prevent committers as approvers" msgstr "" +msgid "ComplianceStandardsAdherence|Requirement" +msgstr "" + +msgid "ComplianceStandardsAdherence|The following features help satisfy this requirement." +msgstr "" + +msgid "ComplianceStandardsAdherence|Update approval settings in the project's merge request settings to satisfy this requirement." +msgstr "" + msgid "ComplianceStandardsAdherence|View details" msgstr "" @@ -28378,6 +28405,9 @@ msgstr "" msgid "Manage projects." msgstr "" +msgid "Manage rules" +msgstr "" + msgid "Manage two-factor authentication" msgstr ""