Commit 8037cfdc authored by Scott Hampton's avatar Scott Hampton

Merge branch '341973-add-coverage-to-project-quality-summary' into 'master'

Add Test Runs and Test Coverage Data to Project Quality Summary

See merge request gitlab-org/gitlab!76485
parents 4a57a0d5 403d6405
......@@ -11,7 +11,7 @@ const apolloProvider = new VueApollo({
});
const mountPipelineChartsApp = (el) => {
const { projectPath, failedPipelinesLink } = el.dataset;
const { projectPath, failedPipelinesLink, coverageChartPath, defaultBranch } = el.dataset;
const shouldRenderDoraCharts = parseBoolean(el.dataset.shouldRenderDoraCharts);
const shouldRenderQualitySummary = parseBoolean(el.dataset.shouldRenderQualitySummary);
......@@ -28,6 +28,8 @@ const mountPipelineChartsApp = (el) => {
failedPipelinesLink,
shouldRenderDoraCharts,
shouldRenderQualitySummary,
coverageChartPath,
defaultBranch,
},
render: (createElement) => createElement(ProjectPipelinesCharts, {}),
});
......
......@@ -3,4 +3,6 @@
#js-project-pipelines-charts-app{ data: { project_path: @project.full_path,
should_render_dora_charts: should_render_dora_charts.to_s,
should_render_quality_summary: should_render_quality_summary.to_s,
failed_pipelines_link: project_pipelines_path(@project, page: '1', scope: 'all', status: 'failed') } }
failed_pipelines_link: project_pipelines_path(@project, page: '1', scope: 'all', status: 'failed'),
coverage_chart_path: charts_project_graph_path(@project, @project.default_branch),
default_branch: @project.default_branch } }
<script>
export default {};
import { GlSkeletonLoader, GlCard, GlLink, GlIcon, GlPopover } from '@gitlab/ui';
import { GlSingleStat } from '@gitlab/ui/dist/charts';
import createFlash from '~/flash';
import { s__ } from '~/locale';
import { percent, percentHundred } from '~/lib/utils/unit_format';
import { helpPagePath } from '~/helpers/help_page_helper';
import getProjectQuality from './graphql/queries/get_project_quality.query.graphql';
import { formatStat } from './utils';
export default {
components: {
GlSkeletonLoader,
GlCard,
GlLink,
GlIcon,
GlPopover,
GlSingleStat,
},
inject: {
projectPath: {
type: String,
default: '',
},
coverageChartPath: {
type: String,
default: '',
},
defaultBranch: {
type: String,
default: '',
},
},
data() {
return {
projectQuality: {},
};
},
apollo: {
projectQuality: {
query: getProjectQuality,
variables() {
return {
projectPath: this.projectPath,
defaultBranch: this.defaultBranch,
};
},
update(data) {
return data.project?.pipelines?.nodes[0];
},
error(error) {
createFlash({
message: this.$options.i18n.fetchError,
error,
});
},
},
},
computed: {
testSuccessPercentage() {
return formatStat(
this.projectQuality?.testReportSummary.total.success /
this.projectQuality?.testReportSummary.total.count,
percent,
);
},
testFailurePercentage() {
return formatStat(
this.projectQuality?.testReportSummary.total.failed /
this.projectQuality?.testReportSummary.total.count,
percent,
);
},
testSkippedPercentage() {
return formatStat(
this.projectQuality?.testReportSummary.total.skipped /
this.projectQuality?.testReportSummary.total.count,
percent,
);
},
coveragePercentage() {
return formatStat(this.projectQuality?.coverage, percentHundred);
},
pipelineTestReportPath() {
return `${this.projectQuality?.pipelinePath}/test_report`;
},
},
i18n: {
testRuns: {
title: s__('ProjectQualitySummary|Test runs'),
popoverBody: s__(
'ProjectQualitySummary|The percentage of tests that succeed, fail, or are skipped.',
),
learnMoreLink: s__('ProjectQualitySummary|Learn more about test reports'),
fullReportLink: s__('ProjectQualitySummary|See full report'),
successLabel: s__('ProjectQualitySummary|Success'),
failureLabel: s__('ProjectQualitySummary|Failure'),
skippedLabel: s__('ProjectQualitySummary|Skipped'),
},
coverage: {
title: s__('ProjectQualitySummary|Test coverage'),
popoverBody: s__(
'ProjectQualitySummary|Measure of how much of your code is covered by tests.',
),
learnMoreLink: s__('ProjectQualitySummary|Learn more about test coverage'),
fullReportLink: s__('ProjectQualitySummary|See project Code Coverage Statistics'),
coverageLabel: s__('ProjectQualitySummary|Coverage'),
},
subHeader: s__('ProjectQualitySummary|Latest pipeline results'),
fetchError: s__(
'ProjectQualitySummary|An error occurred while trying to fetch project quality statistics',
),
},
testRunsHelpPath: helpPagePath('ci/unit_test_reports'),
coverageHelpPath: helpPagePath('ci/pipelines/settings', {
anchor: 'add-test-coverage-results-to-a-merge-request',
}),
};
</script>
<template><div></div></template>
<template>
<div>
<gl-card class="gl-mt-6">
<template #header>
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline">
<h4 class="gl-m-2">{{ $options.i18n.testRuns.title }}</h4>
<gl-icon
id="test-runs-question-icon"
name="question-o"
class="gl-text-blue-600 gl-cursor-pointer gl-mx-2"
/>
<gl-popover
target="test-runs-question-icon"
:title="$options.i18n.testRuns.title"
placement="top"
container="viewport"
triggers="hover focus"
>
<p>{{ $options.i18n.testRuns.popoverBody }}</p>
<gl-link :href="$options.testRunsHelpPath" class="gl-font-sm" target="_blank">
{{ $options.i18n.testRuns.learnMoreLink }}
</gl-link>
</gl-popover>
<strong class="gl-text-gray-500 gl-mx-2">{{ $options.i18n.subHeader }}</strong>
<gl-link
:href="pipelineTestReportPath"
class="gl-flex-grow-1 gl-text-right gl-mx-2"
data-testid="test-runs-link"
>
{{ $options.i18n.testRuns.fullReportLink }}
</gl-link>
</div>
</template>
<template #default>
<gl-skeleton-loader v-if="$apollo.queries.projectQuality.loading" />
<div v-else class="row gl-ml-2">
<gl-single-stat
class="col-sm-6 col-md-4"
data-testid="test-runs-stat"
:title="$options.i18n.testRuns.successLabel"
:value="testSuccessPercentage"
variant="success"
meta-text="Passed"
meta-icon="status_success"
/>
<gl-single-stat
class="col-sm-6 col-md-4"
data-testid="test-runs-stat"
:title="$options.i18n.testRuns.failureLabel"
:value="testFailurePercentage"
variant="danger"
meta-text="Failed"
meta-icon="status_failed"
/>
<gl-single-stat
class="col-sm-6 col-md-4"
data-testid="test-runs-stat"
:title="$options.i18n.testRuns.skippedLabel"
:value="testSkippedPercentage"
variant="neutral"
meta-text="Skipped"
meta-icon="status_skipped"
/>
</div>
</template>
</gl-card>
<gl-card class="gl-mt-6">
<template #header>
<div class="gl-display-flex gl-justify-content-space-between gl-align-items-baseline">
<h4 class="gl-m-2">{{ $options.i18n.coverage.title }}</h4>
<gl-icon
id="coverage-question-icon"
name="question-o"
class="gl-text-blue-600 gl-cursor-pointer gl-mx-2"
/>
<gl-popover
target="coverage-question-icon"
:title="$options.i18n.coverage.title"
placement="top"
container="viewport"
triggers="hover focus"
>
<p>{{ $options.i18n.coverage.popoverBody }}</p>
<gl-link :href="$options.coverageHelpPath" class="gl-font-sm" target="_blank">
{{ $options.i18n.coverage.learnMoreLink }}
</gl-link>
</gl-popover>
<strong class="gl-text-gray-500 gl-mx-2">{{ $options.i18n.subHeader }}</strong>
<gl-link
:href="coverageChartPath"
class="gl-flex-grow-1 gl-text-right gl-mx-2"
data-testid="coverage-link"
>
{{ $options.i18n.coverage.fullReportLink }}
</gl-link>
</div>
</template>
<template #default>
<gl-skeleton-loader v-if="$apollo.queries.projectQuality.loading" />
<gl-single-stat
v-else
:title="$options.i18n.coverage.coverageLabel"
:value="coveragePercentage"
data-testid="coverage-stat"
/>
</template>
</gl-card>
</div>
</template>
query getProjectQuality($projectPath: ID!, $defaultBranch: String!) {
project(fullPath: $projectPath) {
id
pipelines(ref: $defaultBranch, scope: FINISHED, first: 1) {
nodes {
id
pipelinePath: path
coverage
testReportSummary {
total {
count
error
failed
skipped
success
suiteError
time
}
}
}
}
}
}
export const formatStat = (stat, formatter) => {
if (stat === null || typeof stat === 'undefined' || Number.isNaN(stat)) return '-';
return formatter(stat);
};
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Project Quality Summary (GraphQL fixtures)' do
describe GraphQL::Query, type: :request do
include ApiHelpers
include GraphqlHelpers
include JavaScriptFixturesHelpers
include TestReportsHelper
let_it_be(:project) { create(:project, :repository) }
let_it_be(:current_user) { create(:user) }
let_it_be(:pipeline) { create(:ci_pipeline, :with_test_reports, :with_report_results, project: project) }
let!(:coverage) { create(:ci_build, :success, pipeline: pipeline, coverage: 78) }
let!(:build) { create(:ci_build, pipeline: pipeline) }
let!(:report_result) { create(:ci_build_report_result, :with_junit_success, build: build) }
project_quality_summary_query_path = 'project_quality_summary/graphql/queries/get_project_quality.query.graphql'
it "graphql/#{project_quality_summary_query_path}.json" do
project.add_developer(current_user)
query = get_graphql_query_as_string(project_quality_summary_query_path, ee: true)
post_graphql(query, current_user: current_user, variables: { projectPath: project.full_path, defaultBranch: project.default_branch })
expect_graphql_errors_to_be_empty
end
end
end
import { GlSkeletonLoader } from '@gitlab/ui';
import { createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import mockProjectQualityResponse from 'test_fixtures/graphql/project_quality_summary/graphql/queries/get_project_quality.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import createFlash from '~/flash';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ProjectQualitySummary from 'ee/project_quality_summary/app.vue';
import getProjectQuality from 'ee/project_quality_summary/graphql/queries/get_project_quality.query.graphql';
jest.mock('~/flash');
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('Project quality summary app component', () => {
let wrapper;
const findTestRunsLink = () => wrapper.findByTestId('test-runs-link');
const findTestRunsStat = (index) => wrapper.findAllByTestId('test-runs-stat').at(index);
const findCoverageLink = () => wrapper.findByTestId('coverage-link');
const findCoverageStat = () => wrapper.findByTestId('coverage-stat');
const coverageChartPath = 'coverage/chart/path';
const { pipelinePath, coverage } = mockProjectQualityResponse.data.project.pipelines.nodes[0];
const createComponent = (
mockReturnValue = jest.fn().mockResolvedValue(mockProjectQualityResponse),
) => {
const apolloProvider = createMockApollo([[getProjectQuality, mockReturnValue]]);
wrapper = mountExtended(ProjectQualitySummary, {
localVue,
apolloProvider,
provide: {
projectPath: 'project-path',
coverageChartPath,
defaultBranch: 'main',
},
});
};
describe('when loading', () => {
beforeEach(() => {
createComponent(jest.fn().mockReturnValueOnce(new Promise(() => {})));
});
it('shows a loading state', () => {
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
describe('on error', () => {
beforeEach(() => {
createComponent(jest.fn().mockRejectedValueOnce(new Error('Error!')));
});
it('shows a flash message', () => {
expect(createFlash).toHaveBeenCalled();
});
});
describe('with data', () => {
beforeEach(() => {
createComponent();
});
describe('test runs card', () => {
it('shows a link to the full report', () => {
expect(findTestRunsLink().attributes('href')).toBe(`${pipelinePath}/test_report`);
});
it('shows the percentage of tests that passed', () => {
const passedStat = findTestRunsStat(0).text();
expect(passedStat).toContain('Passed');
expect(passedStat).toContain(' 50%');
});
it('shows the percentage of tests that failed', () => {
const failedStat = findTestRunsStat(1).text();
expect(failedStat).toContain('Failed');
expect(failedStat).toContain(' 0%');
});
it('shows the percentage of tests that were skipped', () => {
const skippedStat = findTestRunsStat(2).text();
expect(skippedStat).toContain('Skipped');
expect(skippedStat).toContain(' 0%');
});
});
describe('test coverage card', () => {
it('shows a link to coverage charts', () => {
expect(findCoverageLink().attributes('href')).toBe(coverageChartPath);
});
it('shows the coverage percentage', () => {
expect(findCoverageStat().text()).toContain(`${coverage}%`);
});
});
});
});
......@@ -27332,6 +27332,48 @@ msgstr ""
msgid "ProjectPage|Project ID: %{project_id}"
msgstr ""
msgid "ProjectQualitySummary|An error occurred while trying to fetch project quality statistics"
msgstr ""
msgid "ProjectQualitySummary|Coverage"
msgstr ""
msgid "ProjectQualitySummary|Failure"
msgstr ""
msgid "ProjectQualitySummary|Latest pipeline results"
msgstr ""
msgid "ProjectQualitySummary|Learn more about test coverage"
msgstr ""
msgid "ProjectQualitySummary|Learn more about test reports"
msgstr ""
msgid "ProjectQualitySummary|Measure of how much of your code is covered by tests."
msgstr ""
msgid "ProjectQualitySummary|See full report"
msgstr ""
msgid "ProjectQualitySummary|See project Code Coverage Statistics"
msgstr ""
msgid "ProjectQualitySummary|Skipped"
msgstr ""
msgid "ProjectQualitySummary|Success"
msgstr ""
msgid "ProjectQualitySummary|Test coverage"
msgstr ""
msgid "ProjectQualitySummary|Test runs"
msgstr ""
msgid "ProjectQualitySummary|The percentage of tests that succeed, fail, or are skipped."
msgstr ""
msgid "ProjectSelect| or group"
msgstr ""
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment