mirror of
https://gitee.com/fit2cloud-feizhiyun/MeterSphere.git
synced 2024-11-30 11:08:38 +08:00
feat(测试计划): 测试计划报告联调(不包含聚合报告报告明细)
This commit is contained in:
parent
60a44a81d2
commit
fed0ce3e10
@ -9,6 +9,7 @@ import {
|
||||
ReportBugItem,
|
||||
UpdateReportDetailParams,
|
||||
} from '@/models/testPlan/report';
|
||||
import { PlanReportDetail } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
// 报告列表
|
||||
export function reportList(data: TableQueryParams) {
|
||||
@ -100,5 +101,13 @@ export function getShareApiPage(data: TableQueryParams) {
|
||||
export function getShareScenarioPage(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<ApiOrScenarioCaseItem>>({ url: reportUrl.ReportShareScenarioUrl, data });
|
||||
}
|
||||
// 测试计划-聚合报告-报告明细
|
||||
export function getReportDetailPage(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<PlanReportDetail>>({ url: reportUrl.ReportDetailPageUrl, data });
|
||||
}
|
||||
// 测试计划-聚合报告-报告明细-分享
|
||||
export function getReportDetailSharePage(data: TableQueryParams) {
|
||||
return MSR.post<CommonList<PlanReportDetail>>({ url: reportUrl.ReportDetailSharePageUrl, data });
|
||||
}
|
||||
|
||||
export default {};
|
||||
|
@ -36,3 +36,7 @@ export const ReportIndependentScenarioUrl = '/test-plan/report/detail/scenario/c
|
||||
export const ReportShareApiUrl = '/test-plan/report/share/detail/api/case/page';
|
||||
// 测试计划-独立报告-场景用例-分享
|
||||
export const ReportShareScenarioUrl = '/test-plan/report/share/detail/scenario/case/page';
|
||||
// 测试计划-聚合报告-报告明细
|
||||
export const ReportDetailPageUrl = '/test-plan/report/detail/plan/report/page';
|
||||
// 测试计划-聚合报告-报告明细-分享
|
||||
export const ReportDetailSharePageUrl = '/test-plan/report/share/detail/plan/report/page';
|
||||
|
@ -244,6 +244,11 @@
|
||||
watch(
|
||||
() => () => props.currentProject,
|
||||
() => {
|
||||
setPagination({
|
||||
current: 1,
|
||||
});
|
||||
resetSelector();
|
||||
resetFilterParams();
|
||||
loadCaseList();
|
||||
}
|
||||
);
|
||||
@ -252,6 +257,8 @@
|
||||
() => props.activeModule,
|
||||
(val) => {
|
||||
if (val) {
|
||||
resetSelector();
|
||||
resetFilterParams();
|
||||
loadCaseList();
|
||||
}
|
||||
}
|
||||
|
@ -206,6 +206,11 @@
|
||||
watch(
|
||||
() => [() => props.currentProject, () => props.protocols],
|
||||
() => {
|
||||
setPagination({
|
||||
current: 1,
|
||||
});
|
||||
resetSelector();
|
||||
resetFilterParams();
|
||||
loadApiList();
|
||||
}
|
||||
);
|
||||
@ -225,6 +230,8 @@
|
||||
() => props.activeModule,
|
||||
(val) => {
|
||||
if (val) {
|
||||
resetSelector();
|
||||
resetFilterParams();
|
||||
loadApiList();
|
||||
}
|
||||
}
|
||||
|
@ -283,6 +283,11 @@
|
||||
() => props.currentProject,
|
||||
(val) => {
|
||||
if (val) {
|
||||
setPagination({
|
||||
current: 1,
|
||||
});
|
||||
resetSelector();
|
||||
resetFilterParams();
|
||||
loadCaseList();
|
||||
}
|
||||
},
|
||||
@ -295,6 +300,8 @@
|
||||
() => props.activeModule,
|
||||
(val) => {
|
||||
if (val) {
|
||||
resetSelector();
|
||||
resetFilterParams();
|
||||
loadCaseList();
|
||||
}
|
||||
}
|
||||
|
@ -243,6 +243,11 @@
|
||||
() => props.currentProject,
|
||||
(val) => {
|
||||
if (val) {
|
||||
setPagination({
|
||||
current: 1,
|
||||
});
|
||||
resetSelector();
|
||||
resetFilterParams();
|
||||
loadScenarioList();
|
||||
}
|
||||
},
|
||||
@ -255,6 +260,8 @@
|
||||
() => props.activeModule,
|
||||
(val) => {
|
||||
if (val) {
|
||||
resetSelector();
|
||||
resetFilterParams();
|
||||
loadScenarioList();
|
||||
}
|
||||
}
|
||||
|
@ -45,7 +45,14 @@
|
||||
</MsTag> -->
|
||||
|
||||
<slot name="right"></slot>
|
||||
<MsTag no-margin size="large" class="cursor-pointer" theme="outline" @click="handleRefresh">
|
||||
<MsTag
|
||||
no-margin
|
||||
size="large"
|
||||
:tooltip-disabled="true"
|
||||
class="cursor-pointer"
|
||||
theme="outline"
|
||||
@click="handleRefresh"
|
||||
>
|
||||
<MsIcon class="text-[16px] text-[var(color-text-4)]" :size="32" type="icon-icon_reset_outlined" />
|
||||
</MsTag>
|
||||
</div>
|
||||
|
@ -82,6 +82,7 @@ export const defaultReportDetail: PlanReportDetail = {
|
||||
functionalCount: cloneDeep(defaultCount),
|
||||
apiCaseCount: cloneDeep(defaultCount),
|
||||
apiScenarioCount: cloneDeep(defaultCount),
|
||||
planCount: 0,
|
||||
};
|
||||
|
||||
export const statusConfig: StatusListType[] = [
|
||||
|
@ -72,8 +72,6 @@ export enum TableKeyEnum {
|
||||
TEST_PLAN_DETAIL_API_CASE = 'testPlanDetailApiCase',
|
||||
TEST_PLAN_DETAIL_API_SCENARIO = 'testPlanDetailApiScenario',
|
||||
TEST_PLAN_REPORT_TABLE = 'testPlanReportTable',
|
||||
TEST_PLAN_REPORT_DETAIL_BUG = 'testPlanReportDetailBug',
|
||||
TEST_PLAN_REPORT_DETAIL_FEATURE_CASE = 'testPlanReportDetailFeatureCase',
|
||||
TASK_API_CASE_SYSTEM = 'taskCenterApiCaseSystem',
|
||||
TASK_API_CASE_ORGANIZATION = 'taskCenterApiCaseOrganization',
|
||||
TASK_API_CASE_PROJECT = 'taskCenterApiCaseProject',
|
||||
|
@ -147,6 +147,7 @@ export interface FollowPlanParams {
|
||||
export interface TestPlanBaseParams {
|
||||
projectId?: string;
|
||||
testPlanId: string;
|
||||
triggerMode?: string;
|
||||
}
|
||||
|
||||
export interface PlanDetailFeatureCaseItem {
|
||||
|
@ -21,6 +21,7 @@ export interface PlanReportDetail {
|
||||
functionalCount: countDetail;
|
||||
apiCaseCount: countDetail; // 接口场景用例分析-用例数
|
||||
apiScenarioCount: countDetail; // 接口场景用例分析-用例数
|
||||
planCount: number;
|
||||
}
|
||||
|
||||
export type AnalysisType = 'FUNCTIONAL' | 'API' | 'SCENARIO';
|
||||
|
@ -2,7 +2,7 @@
|
||||
<MsDetailDrawer
|
||||
ref="detailDrawerRef"
|
||||
v-model:visible="showDrawerVisible"
|
||||
:width="900"
|
||||
:width="850"
|
||||
:footer="false"
|
||||
:title="t('bugManagement.detail.title', { id: detailInfo?.num, name: detailInfo?.title })"
|
||||
:tooltip-text="(detailInfo && detailInfo.title) || null"
|
||||
|
@ -2,7 +2,7 @@
|
||||
<MsDetailDrawer
|
||||
ref="detailDrawerRef"
|
||||
v-model:visible="showDrawerVisible"
|
||||
:width="960"
|
||||
:width="850"
|
||||
:footer="false"
|
||||
:mask="false"
|
||||
:title="t('caseManagement.featureCase.caseDetailTitle', { id: detailInfo?.num, name: detailInfo?.name })"
|
||||
|
@ -28,7 +28,7 @@
|
||||
type="text"
|
||||
class="one-line-text w-full"
|
||||
:class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']"
|
||||
@click="showDetail()"
|
||||
@click="showDetail(record.resourceId)"
|
||||
>{{ record.resourceNum }}
|
||||
</div>
|
||||
</template>
|
||||
@ -37,7 +37,7 @@
|
||||
v-if="!record.integrated"
|
||||
class="one-line-text max-w-[300px]"
|
||||
:class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']"
|
||||
@click="showDetail()"
|
||||
@click="showDetail(record.resourceId)"
|
||||
>{{ record.resourceName }}
|
||||
</div>
|
||||
</template>
|
||||
@ -75,7 +75,7 @@
|
||||
<MsButton
|
||||
class="!mr-0"
|
||||
:disabled="record.historyDeleted || !hasAnyPermission(permissionsMap[props.group].report)"
|
||||
@click="viewReport(record.id)"
|
||||
@click="viewReport(record.id, record.integrated)"
|
||||
>{{ t('project.taskCenter.viewReport') }}
|
||||
</MsButton>
|
||||
</a-tooltip>
|
||||
@ -84,7 +84,7 @@
|
||||
<MsButton
|
||||
class="!mr-0"
|
||||
:disabled="record.historyDeleted || !hasAnyPermission(permissionsMap[props.group].report)"
|
||||
@click="viewReport(record.id)"
|
||||
@click="viewReport(record.id, record.integrated)"
|
||||
>{{ t('project.taskCenter.viewReport') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
@ -420,14 +420,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
function viewReport(id: string) {
|
||||
function viewReport(id: string, type: boolean) {
|
||||
openNewPage(RouteEnum.TEST_PLAN_REPORT_DETAIL, {
|
||||
id,
|
||||
type: type ? 'GROUP' : 'TEST_PLAN',
|
||||
});
|
||||
}
|
||||
|
||||
function showDetail() {
|
||||
// TODO
|
||||
function showDetail(id: string) {
|
||||
openNewPage(RouteEnum.TEST_PLAN_INDEX_DETAIL, {
|
||||
id,
|
||||
});
|
||||
}
|
||||
|
||||
function stop(record: any) {
|
||||
|
@ -36,7 +36,7 @@
|
||||
<div
|
||||
type="text"
|
||||
class="one-line-text flex w-full text-[rgb(var(--primary-5))]"
|
||||
@click="showReportDetail(record.id)"
|
||||
@click="showReportDetail(record.id, record.integrated)"
|
||||
>
|
||||
{{ characterLimit(record.name) }}
|
||||
</div>
|
||||
@ -56,7 +56,7 @@
|
||||
</template>
|
||||
<template #passRate="{ record }">
|
||||
<div class="text-[var(--color-text-1)]">
|
||||
{{ `${record.passRate}%` }}
|
||||
{{ `${record.passRate || '0.00'}%` }}
|
||||
</div>
|
||||
</template>
|
||||
<!-- 执行状态筛选 -->
|
||||
@ -420,19 +420,17 @@
|
||||
/**
|
||||
* 报告详情 showReportDetail
|
||||
*/
|
||||
function showReportDetail(id: string) {
|
||||
function showReportDetail(id: string, type: boolean) {
|
||||
router.push({
|
||||
name: TestPlanRouteEnum.TEST_PLAN_REPORT_DETAIL,
|
||||
query: {
|
||||
id,
|
||||
type: type ? 'GROUP' : 'TEST_PLAN',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (route.query.id) {
|
||||
showReportDetail(route.query.id as string);
|
||||
}
|
||||
initData();
|
||||
});
|
||||
</script>
|
||||
|
@ -6,14 +6,14 @@
|
||||
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
|
||||
<caseLevel :case-level="filterContent.value" />
|
||||
</template>
|
||||
<template #caseLevel="{ record }">
|
||||
<CaseLevel :case-level="record.caseLevel" />
|
||||
<template #priority="{ record }">
|
||||
<caseLevel :case-level="record.priority" />
|
||||
</template>
|
||||
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
|
||||
<ExecuteResult :execute-result="filterContent.key" />
|
||||
</template>
|
||||
<template #lastExecResult="{ record }">
|
||||
<ExecuteResult :execute-result="record.lastExecResult" />
|
||||
<ExecuteResult :execute-result="record.executeResult" />
|
||||
<!-- TOTO 暂时不上 -->
|
||||
<!-- <MsIcon
|
||||
v-show="record.lastExecResult !== LastExecuteResults.PENDING"
|
||||
@ -37,15 +37,13 @@
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
import CaseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
|
||||
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
|
||||
import CaseAndScenarioReportDrawer from '@/views/api-test/components/caseAndScenarioReportDrawer.vue';
|
||||
|
||||
import { getApiPage, getScenarioPage, getShareApiPage, getShareScenarioPage } from '@/api/modules/test-plan/report';
|
||||
|
||||
import { ApiOrScenarioCaseItem } from '@/models/testPlan/report';
|
||||
import { LastExecuteResults } from '@/enums/caseEnum';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||
|
||||
import { casePriorityOptions } from '@/views/api-test/components/config';
|
||||
@ -61,6 +59,7 @@
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'num',
|
||||
slotName: 'num',
|
||||
sortIndex: 1,
|
||||
fixed: 'left',
|
||||
width: 100,
|
||||
@ -75,7 +74,7 @@
|
||||
{
|
||||
title: 'report.detail.level',
|
||||
dataIndex: 'priority',
|
||||
slotName: 'caseLevel',
|
||||
slotName: 'priority',
|
||||
filterConfig: {
|
||||
options: casePriorityOptions,
|
||||
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
|
||||
@ -85,7 +84,7 @@
|
||||
},
|
||||
{
|
||||
title: 'common.executionResult',
|
||||
dataIndex: 'lastExecResult',
|
||||
dataIndex: 'executeResult',
|
||||
slotName: 'lastExecResult',
|
||||
filterConfig: {
|
||||
valueKey: 'key',
|
||||
@ -103,14 +102,6 @@
|
||||
width: 200,
|
||||
showDrag: true,
|
||||
},
|
||||
|
||||
{
|
||||
title: 'case.tableColumnCreateUser',
|
||||
dataIndex: 'createUserName',
|
||||
showTooltip: true,
|
||||
width: 130,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'testPlan.featureCase.executor',
|
||||
dataIndex: 'executeUser',
|
||||
@ -128,23 +119,16 @@
|
||||
];
|
||||
const reportApiList = () => {
|
||||
if (props.activeTab === 'scenarioCase') {
|
||||
return !props.shareId ? getShareScenarioPage : getScenarioPage;
|
||||
return !props.shareId ? getScenarioPage : getShareScenarioPage;
|
||||
}
|
||||
return !props.shareId ? getShareApiPage : getApiPage;
|
||||
return !props.shareId ? getApiPage : getShareApiPage;
|
||||
};
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetFilterParams, resetPagination } = useTable(
|
||||
reportApiList(),
|
||||
{
|
||||
scroll: { x: '100%' },
|
||||
columns,
|
||||
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG,
|
||||
showSelectorAll: false,
|
||||
},
|
||||
(record) => {
|
||||
return {
|
||||
...record,
|
||||
lastExecResult: record.executeResult ?? LastExecuteResults.PENDING,
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
@ -172,6 +156,8 @@
|
||||
watch(
|
||||
() => props.activeTab,
|
||||
() => {
|
||||
resetFilterParams();
|
||||
resetPagination();
|
||||
loadCaseList();
|
||||
}
|
||||
);
|
||||
|
@ -10,17 +10,12 @@
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
|
||||
import { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report';
|
||||
import { useTableStore } from '@/store';
|
||||
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
reportId: string;
|
||||
shareId?: string;
|
||||
}>();
|
||||
|
||||
const tableStore = useTableStore();
|
||||
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
@ -69,7 +64,6 @@
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportBugList(), {
|
||||
scroll: { x: '100%' },
|
||||
columns,
|
||||
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG,
|
||||
showSelectorAll: false,
|
||||
});
|
||||
|
||||
@ -83,6 +77,4 @@
|
||||
loadCaseList();
|
||||
}
|
||||
});
|
||||
|
||||
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG, columns, 'drawer');
|
||||
</script>
|
||||
|
@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div>
|
||||
<SetReportChart
|
||||
size="160px"
|
||||
:legend-data="legendData"
|
||||
:options="executeCharOptions"
|
||||
:request-total="getIndicators(detail.caseTotal) || 0"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
|
||||
import { commonConfig, seriesConfig, statusConfig } from '@/config/testPlan';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import type { PlanReportDetail, StatusListType } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
import { getIndicators } from '@/views/api-test/report/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
detail: PlanReportDetail;
|
||||
}>();
|
||||
|
||||
const legendData = ref<LegendData[]>([]);
|
||||
|
||||
// 执行分析
|
||||
const executeCharOptions = ref({
|
||||
...commonConfig,
|
||||
series: {
|
||||
...seriesConfig,
|
||||
data: [
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.success'),
|
||||
itemStyle: {
|
||||
color: '#00C261',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.fakeError'),
|
||||
itemStyle: {
|
||||
color: '#FFC14E',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.fail'),
|
||||
itemStyle: {
|
||||
color: '#ED0303',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.unExecute'),
|
||||
itemStyle: {
|
||||
color: '#D4D4D8',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.block'),
|
||||
itemStyle: {
|
||||
color: '#B379C8',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
function initExecuteOptions() {
|
||||
executeCharOptions.value.series.data = statusConfig.map((item: StatusListType) => {
|
||||
return {
|
||||
value: props.detail.executeCount[item.value] || 0,
|
||||
name: t(item.label),
|
||||
itemStyle: {
|
||||
color: item.color,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff',
|
||||
},
|
||||
};
|
||||
});
|
||||
legendData.value = statusConfig.map((item: StatusListType) => {
|
||||
const rate = (props.detail.executeCount[item.value] / props.detail.caseTotal) * 100;
|
||||
return {
|
||||
...item,
|
||||
label: t(item.label),
|
||||
count: props.detail.executeCount[item.value] || 0,
|
||||
rote: `${Number.isNaN(rate) ? 0 : rate.toFixed(2)}%`,
|
||||
};
|
||||
}) as unknown as LegendData[];
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.detail) {
|
||||
initExecuteOptions();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -22,9 +22,7 @@
|
||||
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
|
||||
|
||||
import { getReportFeatureCaseList, getReportShareFeatureCaseList } from '@/api/modules/test-plan/report';
|
||||
import { useTableStore } from '@/store';
|
||||
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||
|
||||
import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils';
|
||||
@ -32,10 +30,9 @@
|
||||
const props = defineProps<{
|
||||
reportId: string;
|
||||
shareId?: string;
|
||||
activeTab: string;
|
||||
}>();
|
||||
|
||||
const tableStore = useTableStore();
|
||||
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
@ -104,7 +101,6 @@
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportFeatureCaseList(), {
|
||||
scroll: { x: '100%' },
|
||||
columns,
|
||||
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_FEATURE_CASE,
|
||||
heightUsed: 20,
|
||||
showSelectorAll: false,
|
||||
});
|
||||
@ -119,6 +115,4 @@
|
||||
loadCaseList();
|
||||
}
|
||||
});
|
||||
|
||||
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_REPORT_DETAIL_FEATURE_CASE, columns, 'drawer');
|
||||
</script>
|
||||
|
@ -10,13 +10,7 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="analysis min-w-[410px]">
|
||||
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div>
|
||||
<SetReportChart
|
||||
size="160px"
|
||||
:legend-data="legendData"
|
||||
:options="executeCharOptions"
|
||||
:request-total="getIndicators(detail.caseTotal) || 0"
|
||||
/>
|
||||
<ExecuteAnalysis :detail="detail" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -117,6 +111,8 @@
|
||||
v-model:richText="richText"
|
||||
:share-id="shareId"
|
||||
:show-button="showButton"
|
||||
:is-plan-group="false"
|
||||
:detail="detail"
|
||||
@update-summary="handleUpdateReportDetail"
|
||||
@cancel="handleCancel"
|
||||
@handle-summary="handleSummary"
|
||||
@ -130,9 +126,14 @@
|
||||
class="relative mb-[16px] border-b"
|
||||
/>
|
||||
<BugTable v-if="activeTab === 'bug'" :report-id="detail.id" :share-id="shareId" />
|
||||
<FeatureCaseTable v-if="activeTab === 'featureCase'" :report-id="detail.id" :share-id="shareId" />
|
||||
<FeatureCaseTable
|
||||
v-if="activeTab === 'featureCase'"
|
||||
:active-tab="activeTab"
|
||||
:report-id="detail.id"
|
||||
:share-id="shareId"
|
||||
/>
|
||||
<ApiAndScenarioTable
|
||||
v-if="activeTab === 'apiCase'"
|
||||
v-if="['apiCase', 'scenarioCase'].includes(activeTab)"
|
||||
:report-id="detail.id"
|
||||
:share-id="shareId"
|
||||
:active-tab="activeTab"
|
||||
@ -151,10 +152,10 @@
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsTab from '@/components/pure/ms-tab/index.vue';
|
||||
import ReportMetricsItem from './ReportMetricsItem.vue';
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
import SingleStatusProgress from '@/views/test-plan/report/component/singleStatusProgress.vue';
|
||||
import ApiAndScenarioTable from '@/views/test-plan/report/detail/component/apiAndScenarioTable.vue';
|
||||
import BugTable from '@/views/test-plan/report/detail/component/bugTable.vue';
|
||||
import ExecuteAnalysis from '@/views/test-plan/report/detail/component/executeAnalysis.vue';
|
||||
import FeatureCaseTable from '@/views/test-plan/report/detail/component/featureCaseTable.vue';
|
||||
import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue';
|
||||
import Summary from '@/views/test-plan/report/detail/component/summary.vue';
|
||||
@ -164,7 +165,6 @@
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import type {
|
||||
countDetail,
|
||||
PlanReportDetail,
|
||||
@ -172,7 +172,7 @@
|
||||
StatusListType,
|
||||
} from '@/models/testPlan/testPlanReport';
|
||||
|
||||
import { getIndicators } from '@/views/api-test/report/utils';
|
||||
import { getSummaryDetail } from '@/views/test-plan/report/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
@ -197,53 +197,6 @@
|
||||
*/
|
||||
const shareId = ref<string>(route.query.shareId as string);
|
||||
|
||||
const legendData = ref<LegendData[]>([]);
|
||||
|
||||
// 执行分析
|
||||
const executeCharOptions = ref({
|
||||
...commonConfig,
|
||||
series: {
|
||||
...seriesConfig,
|
||||
data: [
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.success'),
|
||||
itemStyle: {
|
||||
color: '#00C261',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.fakeError'),
|
||||
itemStyle: {
|
||||
color: '#FFC14E',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.fail'),
|
||||
itemStyle: {
|
||||
color: '#ED0303',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.unExecute'),
|
||||
itemStyle: {
|
||||
color: '#D4D4D8',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.block'),
|
||||
itemStyle: {
|
||||
color: '#B379C8',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
// 功能用例分析
|
||||
const functionCaseOptions = ref({
|
||||
...commonConfig,
|
||||
@ -293,30 +246,6 @@
|
||||
},
|
||||
});
|
||||
|
||||
// 初始化执行分析
|
||||
function initExecuteOptions() {
|
||||
executeCharOptions.value.series.data = statusConfig.map((item: StatusListType) => {
|
||||
return {
|
||||
value: detail.value.executeCount[item.value] || 0,
|
||||
name: t(item.label),
|
||||
itemStyle: {
|
||||
color: item.color,
|
||||
borderWidth: 2,
|
||||
borderColor: '#ffffff',
|
||||
},
|
||||
};
|
||||
});
|
||||
legendData.value = statusConfig.map((item: StatusListType) => {
|
||||
const rate = (detail.value.executeCount[item.value] / detail.value.caseTotal) * 100;
|
||||
return {
|
||||
...item,
|
||||
label: t(item.label),
|
||||
count: detail.value.executeCount[item.value] || 0,
|
||||
rote: `${Number.isNaN(rate) ? 0 : rate.toFixed(2)}%`,
|
||||
};
|
||||
}) as unknown as LegendData[];
|
||||
}
|
||||
|
||||
// 获取通过率
|
||||
function getPassRateData(caseDetailCount: countDetail) {
|
||||
const caseCountDetail = caseDetailCount || defaultCount;
|
||||
@ -338,7 +267,6 @@
|
||||
|
||||
// 初始化图表
|
||||
function initOptionsData() {
|
||||
initExecuteOptions();
|
||||
const { functionalCount, apiCaseCount, apiScenarioCount } = detail.value;
|
||||
functionCaseOptions.value.series.data = getPassRateData(functionalCount);
|
||||
apiCaseOptions.value.series.data = getPassRateData(apiCaseCount);
|
||||
@ -388,38 +316,6 @@
|
||||
},
|
||||
]);
|
||||
|
||||
function getSummaryDetail(detailCount: countDetail) {
|
||||
if (detailCount) {
|
||||
const { success, error, fakeError, pending, block } = detailCount;
|
||||
// 已执行用例
|
||||
const hasExecutedCase = success + error + fakeError + block;
|
||||
// 用例总数
|
||||
const caseTotal = hasExecutedCase + pending;
|
||||
// 执行率
|
||||
const executedCount = (hasExecutedCase / caseTotal) * 100;
|
||||
const apiExecutedRate = `${Number.isNaN(executedCount) ? 0 : executedCount.toFixed(2)}%`;
|
||||
// 通过率
|
||||
const successCount = (success / caseTotal) * 100;
|
||||
const successRate = `${Number.isNaN(successCount) ? 0 : successCount.toFixed(2)}%`;
|
||||
return {
|
||||
hasExecutedCase,
|
||||
caseTotal,
|
||||
apiExecutedRate,
|
||||
successRate,
|
||||
pending,
|
||||
success,
|
||||
};
|
||||
}
|
||||
return {
|
||||
hasExecutedCase: 0,
|
||||
caseTotal: 0,
|
||||
apiExecutedRate: 0,
|
||||
successRate: 0,
|
||||
pending: 0,
|
||||
success: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const functionCasePassRate = computed(() => {
|
||||
const apiCaseDetail = getSummaryDetail(detail.value.functionalCount || defaultCount);
|
||||
return apiCaseDetail.successRate;
|
||||
@ -472,38 +368,13 @@
|
||||
});
|
||||
});
|
||||
|
||||
const summaryContent = computed(() => {
|
||||
const { functionalCount, apiCaseCount, apiScenarioCount } = detail.value;
|
||||
const functionalCaseDetail = getSummaryDetail(functionalCount);
|
||||
const apiCaseDetail = getSummaryDetail(apiCaseCount);
|
||||
const apiScenarioDetail = getSummaryDetail(apiScenarioCount);
|
||||
const allCaseTotal = functionalCaseDetail.caseTotal + apiCaseDetail.caseTotal + apiScenarioDetail.caseTotal;
|
||||
const allHasExecutedCase =
|
||||
functionalCaseDetail.hasExecutedCase + apiCaseDetail.hasExecutedCase + apiScenarioDetail.hasExecutedCase;
|
||||
const allPendingCase = functionalCaseDetail.pending + apiCaseDetail.pending + apiScenarioDetail.pending;
|
||||
const allSuccessCase = functionalCaseDetail.success + apiCaseDetail.success + apiScenarioDetail.success;
|
||||
|
||||
const allExecutedCount = (allHasExecutedCase / allCaseTotal) * 100;
|
||||
const allExecutedRate = `${Number.isNaN(allExecutedCount) ? 0 : allExecutedCount.toFixed(2)}%`;
|
||||
|
||||
// 通过率
|
||||
const allSuccessCount = (allSuccessCase / allCaseTotal) * 100;
|
||||
const allSuccessRate = `${Number.isNaN(allSuccessCount) ? 0 : allSuccessCount.toFixed(2)}%`;
|
||||
// 接口用例通过率
|
||||
return `<p style=""><span color="" fontsize="">本次完成 ${detail.value.name}的功能测试,接口测试;共 ${allCaseTotal}条 用例,已执行 ${allHasExecutedCase} 条,未执行 ${allPendingCase} 条,执行率为 ${allExecutedRate}%,通过用例 ${allSuccessCase} 条,通过率为 ${allSuccessRate},达到/未达到通过阈值(通过阈值为${detail.value.passThreshold}%),xxx计划满足/不满足发布要求。<br>
|
||||
(1)本次测试包含${functionalCaseDetail.caseTotal}条功能测试用例,执行了${functionalCaseDetail.hasExecutedCase}条,未执行${functionalCaseDetail.pending}条,执行率为${functionalCaseDetail.apiExecutedRate},通过用例${functionalCaseDetail.success}条,通过率为${functionalCaseDetail.successRate}。共发现缺陷0个。<br>
|
||||
(2)本次测试包含${apiCaseDetail.caseTotal}条接口测试用例,执行了${apiCaseDetail.hasExecutedCase}条,未执行${apiCaseDetail.pending}条,执行率为${apiCaseDetail.apiExecutedRate},通过用例${apiCaseDetail.success}条,通过率为${apiCaseDetail.successRate}。共发现缺陷0个。<br>
|
||||
(3)本次测试包含${apiScenarioDetail.caseTotal}条场景测试用例,执行了${apiScenarioDetail.hasExecutedCase}条,未执行${apiScenarioDetail.pending}条,执行率为${apiScenarioDetail.apiExecutedRate}%,通过用例${apiScenarioDetail.success}条,通过率为${apiScenarioDetail.successRate}。共发现缺陷0个</span></p>
|
||||
`;
|
||||
});
|
||||
|
||||
function handleCancel() {
|
||||
richText.value = { summary: detail.value.summary };
|
||||
showButton.value = false;
|
||||
}
|
||||
|
||||
function handleSummary() {
|
||||
richText.value.summary = summaryContent.value;
|
||||
function handleSummary(content: string) {
|
||||
richText.value.summary = content;
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -10,19 +10,15 @@
|
||||
/>
|
||||
</div>
|
||||
<div class="analysis min-w-[410px]">
|
||||
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div>
|
||||
<SetReportChart
|
||||
size="160px"
|
||||
:legend-data="legendData"
|
||||
:options="charOptions"
|
||||
:request-total="getIndicators(detail.caseTotal) || 0"
|
||||
/>
|
||||
<ExecuteAnalysis :detail="detail" />
|
||||
</div>
|
||||
</div>
|
||||
<Summary
|
||||
v-model:richText="richText"
|
||||
:share-id="shareId"
|
||||
:show-button="showButton"
|
||||
:is-plan-group="true"
|
||||
:detail="detail"
|
||||
@update-summary="handleUpdateReportDetail"
|
||||
@cancel="handleCancel"
|
||||
@handle-summary="handleSummary"
|
||||
@ -57,7 +53,7 @@
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import SetReportChart from '@/views/api-test/report/component/case/setReportChart.vue';
|
||||
import ExecuteAnalysis from '@/views/test-plan/report/detail/component/executeAnalysis.vue';
|
||||
import ReportDetailTable from '@/views/test-plan/report/detail/component/reportDetailTable.vue';
|
||||
import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue';
|
||||
import ReportMetricsItem from '@/views/test-plan/report/detail/component/ReportMetricsItem.vue';
|
||||
@ -68,11 +64,8 @@
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { addCommasToNumber } from '@/utils';
|
||||
|
||||
import type { LegendData } from '@/models/apiTest/report';
|
||||
import type { PlanReportDetail, ReportMetricsItemModel } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
import { getIndicators } from '@/views/api-test/report/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const route = useRoute();
|
||||
@ -87,90 +80,23 @@
|
||||
|
||||
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
|
||||
const shareId = ref<string>(route.query.shareId as string);
|
||||
const charOptions = ref({
|
||||
tooltip: {
|
||||
show: false,
|
||||
trigger: 'item',
|
||||
},
|
||||
legend: {
|
||||
show: false,
|
||||
},
|
||||
series: {
|
||||
name: '',
|
||||
type: 'pie',
|
||||
radius: ['62%', '80%'],
|
||||
center: ['50%', '50%'],
|
||||
avoidLabelOverlap: false,
|
||||
label: {
|
||||
show: false,
|
||||
position: 'center',
|
||||
},
|
||||
emphasis: {
|
||||
label: {
|
||||
show: false,
|
||||
fontSize: 40,
|
||||
fontWeight: 'bold',
|
||||
},
|
||||
},
|
||||
labelLine: {
|
||||
show: false,
|
||||
},
|
||||
data: [
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.success'),
|
||||
itemStyle: {
|
||||
color: '#00C261',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.fakeError'),
|
||||
itemStyle: {
|
||||
color: '#FFC14E',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.fail'),
|
||||
itemStyle: {
|
||||
color: '#ED0303',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.unExecute'),
|
||||
itemStyle: {
|
||||
color: '#D4D4D8',
|
||||
},
|
||||
},
|
||||
{
|
||||
value: 0,
|
||||
name: t('common.block'),
|
||||
itemStyle: {
|
||||
color: '#B379C8',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
const legendData = ref<LegendData[]>([]);
|
||||
|
||||
const reportAnalysisList = computed<ReportMetricsItemModel[]>(() => [
|
||||
{
|
||||
name: t('report.detail.testPlanTotal'),
|
||||
value: detail.value.passThreshold,
|
||||
value: addCommasToNumber(detail.value.planCount),
|
||||
unit: t('report.detail.number'),
|
||||
icon: 'plan_total',
|
||||
},
|
||||
{
|
||||
name: t('report.detail.testPlanCaseTotal'),
|
||||
value: detail.value.passRate,
|
||||
value: addCommasToNumber(detail.value.caseTotal),
|
||||
unit: t('report.detail.number'),
|
||||
icon: 'case_total',
|
||||
},
|
||||
{
|
||||
name: t('report.passRate'),
|
||||
value: detail.value.executeRate,
|
||||
value: detail.value.passRate,
|
||||
unit: '%',
|
||||
icon: 'passRate',
|
||||
},
|
||||
@ -182,9 +108,8 @@
|
||||
},
|
||||
]);
|
||||
|
||||
const summaryContent = ref<string>('');
|
||||
|
||||
const showButton = ref(false);
|
||||
|
||||
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
|
||||
summary: '',
|
||||
});
|
||||
@ -210,8 +135,8 @@
|
||||
showButton.value = false;
|
||||
}
|
||||
|
||||
function handleSummary() {
|
||||
richText.value.summary = summaryContent.value;
|
||||
function handleSummary(content: string) {
|
||||
richText.value.summary = content;
|
||||
}
|
||||
|
||||
const currentMode = ref<string>('drawer');
|
||||
@ -219,6 +144,13 @@
|
||||
currentMode.value = value as string;
|
||||
};
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.detailInfo) {
|
||||
detail.value = cloneDeep(props.detailInfo);
|
||||
richText.value.summary = detail.value.summary;
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(async () => {
|
||||
nextTick(() => {
|
||||
const editorContent = document.querySelector('.editor-content');
|
||||
|
@ -1,12 +1,13 @@
|
||||
<template>
|
||||
<PlanGroupDetail :detail-info="detail" />
|
||||
<PlanGroupDetail v-if="props.isGroup" :detail-info="detail" @update-success="getDetail()" />
|
||||
<PlanDetail v-else :detail-info="detail" @update-success="getDetail()" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRoute } from 'vue-router';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
|
||||
import PlanGroupDetail from '@/views/test-plan/report/detail/component/planGroupDetail.vue';
|
||||
|
||||
import { getReportDetail } from '@/api/modules/test-plan/report';
|
||||
@ -14,16 +15,17 @@
|
||||
|
||||
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
const route = useRoute();
|
||||
const reportId = ref<string>(route.query.id as string);
|
||||
const props = defineProps<{
|
||||
isGroup: boolean;
|
||||
reportId: string;
|
||||
}>();
|
||||
|
||||
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
|
||||
|
||||
async function getDetail() {
|
||||
try {
|
||||
detail.value = await getReportDetail(reportId.value);
|
||||
detail.value = await getReportDetail(props.reportId);
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
@ -33,4 +35,4 @@
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped lang="less"></style>
|
@ -11,6 +11,12 @@
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template #resultStatus="{ record }">
|
||||
<ExecutionStatus v-if="record.resultStatus !== '-'" :status="record.resultStatus" />
|
||||
</template>
|
||||
<template #[FilterSlotNameEnum.TEST_PLAN_STATUS_FILTER]="{ filterContent }">
|
||||
<ExecutionStatus :status="filterContent.value" />
|
||||
</template>
|
||||
<template #operation="{ record }">
|
||||
<MsButton class="!mx-0" @click="openReport(record)">{{ t('report.detail.testPlanGroup.viewReport') }}</MsButton>
|
||||
</template>
|
||||
@ -25,13 +31,17 @@
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
import ExecutionStatus from '@/views/test-plan/report/component/reportStatus.vue';
|
||||
import ReportDrawer from '@/views/test-plan/testPlan/detail/reportDrawer.vue';
|
||||
|
||||
import { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report';
|
||||
import { getReportDetailPage, getReportDetailSharePage } from '@/api/modules/test-plan/report';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
|
||||
import { PlanReportDetail } from '@/models/testPlan/testPlanReport';
|
||||
import { PlanReportStatus } from '@/enums/reportEnum';
|
||||
import { RouteEnum } from '@/enums/routeEnum';
|
||||
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||
|
||||
const { openNewPage } = useOpenNewPage();
|
||||
|
||||
@ -42,6 +52,16 @@
|
||||
shareId?: string;
|
||||
currentMode: string;
|
||||
}>();
|
||||
|
||||
const statusResultOptions = computed(() => {
|
||||
return Object.keys(PlanReportStatus).map((key) => {
|
||||
return {
|
||||
value: key,
|
||||
label: PlanReportStatus[key].statusText,
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'testPlan.testPlanIndex.operation',
|
||||
@ -60,35 +80,41 @@
|
||||
},
|
||||
{
|
||||
title: 'report.detail.testPlanGroup.result',
|
||||
dataIndex: 'result',
|
||||
dataIndex: 'resultStatus',
|
||||
slotName: 'resultStatus',
|
||||
filterConfig: {
|
||||
options: statusResultOptions.value,
|
||||
filterSlotName: FilterSlotNameEnum.TEST_PLAN_STATUS_FILTER,
|
||||
},
|
||||
showTooltip: true,
|
||||
width: 200,
|
||||
},
|
||||
{
|
||||
title: 'report.detail.threshold',
|
||||
dataIndex: 'threshold',
|
||||
slotName: 'threshold',
|
||||
dataIndex: 'passThreshold',
|
||||
slotName: 'passThreshold',
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'report.passRate',
|
||||
dataIndex: 'executeUser',
|
||||
dataIndex: 'passRate',
|
||||
slotName: 'passRate',
|
||||
titleSlotName: 'passRateTitle',
|
||||
showTooltip: true,
|
||||
width: 150,
|
||||
},
|
||||
{
|
||||
title: 'report.detail.testPlanGroup.useCasesCount',
|
||||
dataIndex: 'bugCount',
|
||||
dataIndex: 'caseTotal',
|
||||
width: 100,
|
||||
},
|
||||
];
|
||||
|
||||
const reportBugList = () => {
|
||||
return !props.shareId ? getReportBugList : getReportShareBugList;
|
||||
const reportDetailList = () => {
|
||||
return !props.shareId ? getReportDetailPage : getReportDetailSharePage;
|
||||
};
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportBugList(), {
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportDetailList(), {
|
||||
columns,
|
||||
heightUsed: 20,
|
||||
showSelectorAll: false,
|
||||
@ -104,8 +130,10 @@
|
||||
loadReportDetailList();
|
||||
}
|
||||
});
|
||||
|
||||
const reportVisible = ref(false);
|
||||
function openReport(record: any) {
|
||||
|
||||
function openReport(record: PlanReportDetail) {
|
||||
if (props.currentMode === 'drawer') {
|
||||
reportVisible.value = true;
|
||||
} else {
|
||||
|
@ -1,153 +0,0 @@
|
||||
<template>
|
||||
<MsBaseTable v-bind="propsRes" v-on="propsEvent">
|
||||
<template #num="{ record }">
|
||||
<MsButton type="text">{{ record.num }}</MsButton>
|
||||
</template>
|
||||
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
|
||||
<CaseLevel :case-level="filterContent.value" />
|
||||
</template>
|
||||
<template #caseLevel="{ record }">
|
||||
<CaseLevel :case-level="record.caseLevel" />
|
||||
</template>
|
||||
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
|
||||
<ExecuteResult :execute-result="filterContent.key" />
|
||||
</template>
|
||||
<template #lastExecResult="{ record }">
|
||||
<ExecuteResult :execute-result="record.lastExecResult" />
|
||||
<MsIcon
|
||||
v-show="record.lastExecResult !== LastExecuteResults.PENDING"
|
||||
type="icon-icon_take-action_outlined"
|
||||
class="ml-[8px] cursor-pointer text-[rgb(var(--primary-5))]"
|
||||
size="16"
|
||||
@click="showReport(record)"
|
||||
/>
|
||||
</template>
|
||||
</MsBaseTable>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onBeforeMount } from 'vue';
|
||||
|
||||
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
|
||||
import type { MsTableColumn } from '@/components/pure/ms-table/type';
|
||||
import useTable from '@/components/pure/ms-table/useTable';
|
||||
|
||||
import { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report';
|
||||
import { getPlanDetailApiCaseList } from '@/api/modules/test-plan/testPlan';
|
||||
import { useTableStore } from '@/store';
|
||||
|
||||
import type { PlanDetailApiScenarioItem } from '@/models/testPlan/testPlan';
|
||||
import { LastExecuteResults } from '@/enums/caseEnum';
|
||||
import { TableKeyEnum } from '@/enums/tableEnum';
|
||||
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||
|
||||
import { casePriorityOptions } from '@/views/api-test/components/config';
|
||||
import { executionResultMap, getCaseLevels } from '@/views/case-management/caseManagementFeature/components/utils';
|
||||
|
||||
const props = defineProps<{
|
||||
reportId: string;
|
||||
shareId?: string;
|
||||
}>();
|
||||
|
||||
const tableStore = useTableStore();
|
||||
|
||||
const columns: MsTableColumn = [
|
||||
{
|
||||
title: 'ID',
|
||||
dataIndex: 'num',
|
||||
sortIndex: 1,
|
||||
fixed: 'left',
|
||||
width: 100,
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'common.name',
|
||||
dataIndex: 'name',
|
||||
width: 150,
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'report.detail.level',
|
||||
dataIndex: 'caseLevel',
|
||||
slotName: 'caseLevel',
|
||||
filterConfig: {
|
||||
options: casePriorityOptions,
|
||||
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
|
||||
},
|
||||
width: 150,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'common.executionResult',
|
||||
dataIndex: 'lastExecResult',
|
||||
slotName: 'lastExecResult',
|
||||
filterConfig: {
|
||||
valueKey: 'key',
|
||||
labelKey: 'statusText',
|
||||
options: Object.values(executionResultMap),
|
||||
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT,
|
||||
},
|
||||
width: 150,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'common.belongModule',
|
||||
dataIndex: 'moduleId',
|
||||
showTooltip: true,
|
||||
width: 200,
|
||||
showDrag: true,
|
||||
},
|
||||
|
||||
{
|
||||
title: 'case.tableColumnCreateUser',
|
||||
dataIndex: 'createUserName',
|
||||
showTooltip: true,
|
||||
width: 130,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'testPlan.featureCase.executor',
|
||||
dataIndex: 'executeUserName',
|
||||
showTooltip: true,
|
||||
width: 130,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'testPlan.featureCase.bugCount',
|
||||
dataIndex: 'bugCount',
|
||||
slotName: 'bugCount',
|
||||
width: 100,
|
||||
showDrag: true,
|
||||
},
|
||||
];
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(getPlanDetailApiCaseList, {
|
||||
scroll: { x: '100%' },
|
||||
columns,
|
||||
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG,
|
||||
showSelectorAll: false,
|
||||
});
|
||||
|
||||
async function loadCaseList() {
|
||||
setLoadListParams({ reportId: props.reportId, shareId: props.shareId ?? undefined });
|
||||
loadList();
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.reportId) {
|
||||
loadCaseList();
|
||||
}
|
||||
});
|
||||
|
||||
// 显示执行报告
|
||||
const reportVisible = ref(false);
|
||||
|
||||
const apiReportId = ref('');
|
||||
|
||||
function showReport(record: PlanDetailApiScenarioItem) {
|
||||
reportVisible.value = true;
|
||||
apiReportId.value = record.lastExecReportId;
|
||||
}
|
||||
|
||||
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG, columns, 'drawer');
|
||||
</script>
|
@ -43,17 +43,23 @@
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import { hasAnyPermission } from '@/utils/permission';
|
||||
|
||||
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
import { getSummaryDetail } from '@/views/test-plan/report/utils';
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
richText: { summary: string; richTextTmpFileIds?: string[] };
|
||||
shareId?: string;
|
||||
showButton: boolean;
|
||||
isPlanGroup: boolean;
|
||||
detail: PlanReportDetail;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateSummary'): void;
|
||||
(e: 'cancel'): void;
|
||||
(e: 'handleSummary'): void;
|
||||
(e: 'handleSummary', content: string): void;
|
||||
}>();
|
||||
|
||||
const innerSummary = useVModel(props, 'richText', emit);
|
||||
@ -73,8 +79,34 @@
|
||||
return data;
|
||||
}
|
||||
|
||||
const summaryContent = computed(() => {
|
||||
const { functionalCount, apiCaseCount, apiScenarioCount } = props.detail;
|
||||
const functionalCaseDetail = getSummaryDetail(functionalCount);
|
||||
const apiCaseDetail = getSummaryDetail(apiCaseCount);
|
||||
const apiScenarioDetail = getSummaryDetail(apiScenarioCount);
|
||||
const allCaseTotal = functionalCaseDetail.caseTotal + apiCaseDetail.caseTotal + apiScenarioDetail.caseTotal;
|
||||
const allHasExecutedCase =
|
||||
functionalCaseDetail.hasExecutedCase + apiCaseDetail.hasExecutedCase + apiScenarioDetail.hasExecutedCase;
|
||||
const allSuccessCase = functionalCaseDetail.success + apiCaseDetail.success + apiScenarioDetail.success;
|
||||
|
||||
// 通过率
|
||||
const allSuccessCount = (allSuccessCase / allCaseTotal) * 100;
|
||||
const allSuccessRate = `${Number.isNaN(allSuccessCount) ? 0 : allSuccessCount.toFixed(2)}%`;
|
||||
// TODO 待联调
|
||||
if (props.isPlanGroup) {
|
||||
return `<p style=""><span color="" fontsize=""> <strong>${props.detail.name}</strong>包含 ${props.detail.planCount}个子计划。
|
||||
其中 X 个子计划通过, X 个子计划不通过。</span></p>`;
|
||||
}
|
||||
// 接口用例通过率
|
||||
return `<p style=""><span color="" fontsize=""> <strong>${props.detail.name}</strong> 包含功能测试、接口用例、场景用例, 共 ${allCaseTotal}条用例,已执行 ${allHasExecutedCase} 条,通过用例 ${allSuccessCase} 条,通过率为 ${allSuccessRate},达到/未达到通过阈值(通过阈值为${props.detail.passThreshold}%),<strong>${props.detail.name}</strong> 计划满足/不满足发布要求。<br>
|
||||
(1)本次测试包含${functionalCaseDetail.caseTotal}条功能测试用例,执行了${functionalCaseDetail.hasExecutedCase}条,未执行${functionalCaseDetail.pending}条,执行率为${functionalCaseDetail.apiExecutedRate},通过用例${functionalCaseDetail.success}条,通过率为${functionalCaseDetail.successRate}。共发现缺陷0个。<br>
|
||||
(2)本次测试包含${apiCaseDetail.caseTotal}条接口测试用例,执行了${apiCaseDetail.hasExecutedCase}条,未执行${apiCaseDetail.pending}条,执行率为${apiCaseDetail.apiExecutedRate},通过用例${apiCaseDetail.success}条,通过率为${apiCaseDetail.successRate}。共发现缺陷 ${props.detail.bugCount} 个。<br>
|
||||
(3)本次测试包含${apiScenarioDetail.caseTotal}条场景测试用例,执行了${apiScenarioDetail.hasExecutedCase}条,未执行${apiScenarioDetail.pending}条,执行率为${apiScenarioDetail.apiExecutedRate}%,通过用例${apiScenarioDetail.success}条,通过率为${apiScenarioDetail.successRate}。共发现缺陷0个</span></p>
|
||||
`;
|
||||
});
|
||||
|
||||
function handleSummary() {
|
||||
emit('handleSummary');
|
||||
emit('handleSummary', summaryContent.value);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -1,47 +0,0 @@
|
||||
<template>
|
||||
<PlanDetail :detail-info="detail" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import PlanDetail from '@/views/test-plan/report/detail/component/planDetail.vue';
|
||||
|
||||
import { getReportDetail, planGetShareHref } from '@/api/modules/test-plan/report';
|
||||
import { defaultReportDetail } from '@/config/testPlan';
|
||||
import { NOT_FOUND_RESOURCE } from '@/router/constants';
|
||||
|
||||
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const reportId = ref<string>(route.query.id as string);
|
||||
|
||||
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
|
||||
|
||||
async function getShareDetail() {
|
||||
try {
|
||||
const hrefShareDetail = await planGetShareHref(route.query.shareId as string);
|
||||
reportId.value = hrefShareDetail.reportId;
|
||||
if (hrefShareDetail.deleted) {
|
||||
router.push({
|
||||
name: NOT_FOUND_RESOURCE,
|
||||
});
|
||||
return;
|
||||
}
|
||||
detail.value = await getReportDetail(reportId.value, route.query.shareId as string);
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
watchEffect(() => {
|
||||
if (route.query.shareId) {
|
||||
getShareDetail();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped></style>
|
@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<PlanDetail :detail-info="detail" @update-success="getDetail()" />
|
||||
<!-- TODO 待联调计划组报告 -->
|
||||
<!-- <PlanGroupDetail :detail-info="detail" @update-success="getDetail()" /> -->
|
||||
<PlanGroupDetail v-if="isGroup" :detail-info="detail" @update-success="getDetail()" />
|
||||
<PlanDetail v-else :detail-info="detail" @update-success="getDetail()" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
@ -22,6 +21,8 @@
|
||||
|
||||
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
|
||||
|
||||
const isGroup = computed(() => route.query.type === 'GROUP');
|
||||
|
||||
async function getDetail() {
|
||||
try {
|
||||
detail.value = await getReportDetail(reportId.value);
|
||||
@ -33,7 +34,6 @@
|
||||
onBeforeMount(() => {
|
||||
getDetail();
|
||||
});
|
||||
// 测试代码结束
|
||||
</script>
|
||||
|
||||
<style scoped lang="less"></style>
|
||||
|
35
frontend/src/views/test-plan/report/utils.ts
Normal file
35
frontend/src/views/test-plan/report/utils.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import type { countDetail } from '@/models/testPlan/testPlanReport';
|
||||
|
||||
export function getSummaryDetail(detailCount: countDetail) {
|
||||
if (detailCount) {
|
||||
const { success, error, fakeError, pending, block } = detailCount;
|
||||
// 已执行用例
|
||||
const hasExecutedCase = success + error + fakeError + block;
|
||||
// 用例总数
|
||||
const caseTotal = hasExecutedCase + pending;
|
||||
// 执行率
|
||||
const executedCount = (hasExecutedCase / caseTotal) * 100;
|
||||
const apiExecutedRate = `${Number.isNaN(executedCount) ? 0 : executedCount.toFixed(2)}%`;
|
||||
// 通过率
|
||||
const successCount = (success / caseTotal) * 100;
|
||||
const successRate = `${Number.isNaN(successCount) ? 0 : successCount.toFixed(2)}%`;
|
||||
return {
|
||||
hasExecutedCase,
|
||||
caseTotal,
|
||||
apiExecutedRate,
|
||||
successRate,
|
||||
pending,
|
||||
success,
|
||||
};
|
||||
}
|
||||
return {
|
||||
hasExecutedCase: 0,
|
||||
caseTotal: 0,
|
||||
apiExecutedRate: 0,
|
||||
successRate: 0,
|
||||
pending: 0,
|
||||
success: 0,
|
||||
};
|
||||
}
|
||||
|
||||
export default {};
|
@ -37,7 +37,7 @@
|
||||
field="targetId"
|
||||
:label="t('testPlan.testPlanIndex.testPlanGroup')"
|
||||
>
|
||||
<a-select v-model="form.targetId" :placeholder="t('common.pleaseSelect')">
|
||||
<a-select v-model="form.targetId" :placeholder="t('common.pleaseSelect')" allow-search>
|
||||
<a-option v-for="item of groupList" :key="item.id" :value="item.id">
|
||||
{{ item.name }}
|
||||
</a-option>
|
||||
|
@ -157,13 +157,31 @@
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<template #planStartToEndTime="{ record }">
|
||||
<div>
|
||||
{{ record.plannedStartTime ? dayjs(record.plannedStartTime) : '-' }} {{ t('common.to') }}
|
||||
<a-tooltip
|
||||
class="ms-tooltip-red"
|
||||
:content="t('testPlan.planStartToEndTimeTip')"
|
||||
:disabled="record.execStatus !== LastExecuteResults.ERROR"
|
||||
>
|
||||
<span :class="[`${record.execStatus === LastExecuteResults.ERROR ? 'text-[rgb(var(--danger-6))' : ''}`]">
|
||||
{{ record?.plannedEndTime ? dayjs(record.plannedEndTime) : '-' }}
|
||||
</span>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
<template #actualStartToEndTime="{ record }">
|
||||
{{ record?.actualStartTime ? dayjs(record.actualStartTime) : '-' }}{{ t('common.to')
|
||||
}}{{ record.actualEndTime ? dayjs(record.actualEndTime) : '-' }}
|
||||
</template>
|
||||
|
||||
<template #passRate="{ record }">
|
||||
<div class="mr-[8px] w-[100px]">
|
||||
<StatusProgress :status-detail="defaultCountDetailMap[record.id]" height="5px" />
|
||||
</div>
|
||||
<div class="text-[var(--color-text-1)]">
|
||||
{{ `${defaultCountDetailMap[record.id] ? defaultCountDetailMap[record.id].passRate : '-'}%` }}
|
||||
{{ `${defaultCountDetailMap[record.id]?.passRate ? defaultCountDetailMap[record.id].passRate : '-'}%` }}
|
||||
</div>
|
||||
</template>
|
||||
<template #passRateTitleSlot="{ columnConfig }">
|
||||
@ -221,6 +239,8 @@
|
||||
|
||||
<template #operation="{ record }">
|
||||
<div class="flex items-center">
|
||||
<!-- TODO 测试计划组手动生成报告 -->
|
||||
<!-- <MsButton class="mr-2" @click="handleGenerateReport(record.id)">生成报告</MsButton> -->
|
||||
<MsButton
|
||||
v-if="
|
||||
getFunctionalCount(record.id) > 0 &&
|
||||
@ -313,7 +333,6 @@
|
||||
:type="showType"
|
||||
@save="handleMoveOrCopy"
|
||||
/>
|
||||
<!-- TODO 待联调[编辑] 字段加到统计里边 -->
|
||||
<ScheduledModal
|
||||
v-model:visible="showScheduledTaskModal"
|
||||
:type="planType"
|
||||
@ -398,6 +417,7 @@
|
||||
PassRateCountDetail,
|
||||
TestPlanItem,
|
||||
} from '@/models/testPlan/testPlan';
|
||||
import { LastExecuteResults } from '@/enums/caseEnum';
|
||||
import { TestPlanRouteEnum } from '@/enums/routeEnum';
|
||||
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
|
||||
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
|
||||
@ -530,7 +550,6 @@
|
||||
{
|
||||
title: 'testPlan.testPlanIndex.planStartToEndTime',
|
||||
slotName: 'planStartToEndTime',
|
||||
dataIndex: 'planStartToEndTime',
|
||||
showInTable: false,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
@ -542,7 +561,6 @@
|
||||
{
|
||||
title: 'testPlan.testPlanIndex.actualStartToEndTime',
|
||||
slotName: 'actualStartToEndTime',
|
||||
dataIndex: 'actualStartToEndTime',
|
||||
showInTable: false,
|
||||
sortable: {
|
||||
sortDirections: ['ascend', 'descend'],
|
||||
@ -763,7 +781,7 @@
|
||||
showSetting: true,
|
||||
heightUsed: 236,
|
||||
paginationSize: 'mini',
|
||||
showSelectorAll: true,
|
||||
showSelectorAll: false,
|
||||
draggable: { type: 'handle' },
|
||||
draggableCondition: true,
|
||||
});
|
||||
|
@ -132,7 +132,7 @@
|
||||
|
||||
const initForm: CreateTask = {
|
||||
resourceId: '',
|
||||
cron: '',
|
||||
cron: '0 0 0/1 * * ?',
|
||||
enable: false,
|
||||
runConfig: { runMode: 'SERIAL' },
|
||||
};
|
||||
|
@ -359,6 +359,7 @@
|
||||
await generateReport({
|
||||
projectId: appStore.currentProjectId,
|
||||
testPlanId: detail.value.id as string,
|
||||
triggerMode: 'MANUAL',
|
||||
});
|
||||
Message.success(t('testPlan.testPlanDetail.successfullyGenerated'));
|
||||
} catch (error) {
|
||||
|
@ -145,4 +145,5 @@ export default {
|
||||
'testPlan.plan': 'Test plan',
|
||||
'testPlan.planTip':
|
||||
'1. Create a test set for business classification testing; 2. Select the test set associated use case',
|
||||
'testPlan.planStartToEndTimeTip': '测试计划已超时',
|
||||
};
|
||||
|
@ -133,4 +133,5 @@ export default {
|
||||
'testPlan.testPlanGroup.closeScheduleTaskSuccess': '关闭定时任务成功',
|
||||
'testPlan.plan': '测试规划',
|
||||
'testPlan.planTip': '1.创建测试集进行业务分类测试;2.选择测试集关联用例',
|
||||
'testPlan.planStartToEndTimeTip': '测试计划已超时',
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user