Commit 403d6405 authored by Miranda Fluharty's avatar Miranda Fluharty Committed by Scott Hampton

Add test data to project quality summary page

Add GraphQL query to get coverage and test report summary
for latest pipeline for the project's default branch
Add cards and stats to project quality summary component
Feed in project's default branch name via haml
Add tests and GraphQL fixture to generate test data
parent c827d97b
......@@ -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}%`);
});
});
});
});
......@@ -27338,6 +27338,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