feat(测试计划): 测试计划报告联调(不包含聚合报告报告明细)

This commit is contained in:
xinxin.wu 2024-06-14 22:56:19 +08:00 committed by Craftsman
parent 60a44a81d2
commit fed0ce3e10
34 changed files with 368 additions and 518 deletions

View File

@ -9,6 +9,7 @@ import {
ReportBugItem, ReportBugItem,
UpdateReportDetailParams, UpdateReportDetailParams,
} from '@/models/testPlan/report'; } from '@/models/testPlan/report';
import { PlanReportDetail } from '@/models/testPlan/testPlanReport';
// 报告列表 // 报告列表
export function reportList(data: TableQueryParams) { export function reportList(data: TableQueryParams) {
@ -100,5 +101,13 @@ export function getShareApiPage(data: TableQueryParams) {
export function getShareScenarioPage(data: TableQueryParams) { export function getShareScenarioPage(data: TableQueryParams) {
return MSR.post<CommonList<ApiOrScenarioCaseItem>>({ url: reportUrl.ReportShareScenarioUrl, data }); 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 {}; export default {};

View File

@ -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 ReportShareApiUrl = '/test-plan/report/share/detail/api/case/page';
// 测试计划-独立报告-场景用例-分享 // 测试计划-独立报告-场景用例-分享
export const ReportShareScenarioUrl = '/test-plan/report/share/detail/scenario/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';

View File

@ -244,6 +244,11 @@
watch( watch(
() => () => props.currentProject, () => () => props.currentProject,
() => { () => {
setPagination({
current: 1,
});
resetSelector();
resetFilterParams();
loadCaseList(); loadCaseList();
} }
); );
@ -252,6 +257,8 @@
() => props.activeModule, () => props.activeModule,
(val) => { (val) => {
if (val) { if (val) {
resetSelector();
resetFilterParams();
loadCaseList(); loadCaseList();
} }
} }

View File

@ -206,6 +206,11 @@
watch( watch(
() => [() => props.currentProject, () => props.protocols], () => [() => props.currentProject, () => props.protocols],
() => { () => {
setPagination({
current: 1,
});
resetSelector();
resetFilterParams();
loadApiList(); loadApiList();
} }
); );
@ -225,6 +230,8 @@
() => props.activeModule, () => props.activeModule,
(val) => { (val) => {
if (val) { if (val) {
resetSelector();
resetFilterParams();
loadApiList(); loadApiList();
} }
} }

View File

@ -283,6 +283,11 @@
() => props.currentProject, () => props.currentProject,
(val) => { (val) => {
if (val) { if (val) {
setPagination({
current: 1,
});
resetSelector();
resetFilterParams();
loadCaseList(); loadCaseList();
} }
}, },
@ -295,6 +300,8 @@
() => props.activeModule, () => props.activeModule,
(val) => { (val) => {
if (val) { if (val) {
resetSelector();
resetFilterParams();
loadCaseList(); loadCaseList();
} }
} }

View File

@ -243,6 +243,11 @@
() => props.currentProject, () => props.currentProject,
(val) => { (val) => {
if (val) { if (val) {
setPagination({
current: 1,
});
resetSelector();
resetFilterParams();
loadScenarioList(); loadScenarioList();
} }
}, },
@ -255,6 +260,8 @@
() => props.activeModule, () => props.activeModule,
(val) => { (val) => {
if (val) { if (val) {
resetSelector();
resetFilterParams();
loadScenarioList(); loadScenarioList();
} }
} }

View File

@ -45,7 +45,14 @@
</MsTag> --> </MsTag> -->
<slot name="right"></slot> <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" /> <MsIcon class="text-[16px] text-[var(color-text-4)]" :size="32" type="icon-icon_reset_outlined" />
</MsTag> </MsTag>
</div> </div>

View File

@ -82,6 +82,7 @@ export const defaultReportDetail: PlanReportDetail = {
functionalCount: cloneDeep(defaultCount), functionalCount: cloneDeep(defaultCount),
apiCaseCount: cloneDeep(defaultCount), apiCaseCount: cloneDeep(defaultCount),
apiScenarioCount: cloneDeep(defaultCount), apiScenarioCount: cloneDeep(defaultCount),
planCount: 0,
}; };
export const statusConfig: StatusListType[] = [ export const statusConfig: StatusListType[] = [

View File

@ -72,8 +72,6 @@ export enum TableKeyEnum {
TEST_PLAN_DETAIL_API_CASE = 'testPlanDetailApiCase', TEST_PLAN_DETAIL_API_CASE = 'testPlanDetailApiCase',
TEST_PLAN_DETAIL_API_SCENARIO = 'testPlanDetailApiScenario', TEST_PLAN_DETAIL_API_SCENARIO = 'testPlanDetailApiScenario',
TEST_PLAN_REPORT_TABLE = 'testPlanReportTable', 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_SYSTEM = 'taskCenterApiCaseSystem',
TASK_API_CASE_ORGANIZATION = 'taskCenterApiCaseOrganization', TASK_API_CASE_ORGANIZATION = 'taskCenterApiCaseOrganization',
TASK_API_CASE_PROJECT = 'taskCenterApiCaseProject', TASK_API_CASE_PROJECT = 'taskCenterApiCaseProject',

View File

@ -147,6 +147,7 @@ export interface FollowPlanParams {
export interface TestPlanBaseParams { export interface TestPlanBaseParams {
projectId?: string; projectId?: string;
testPlanId: string; testPlanId: string;
triggerMode?: string;
} }
export interface PlanDetailFeatureCaseItem { export interface PlanDetailFeatureCaseItem {

View File

@ -21,6 +21,7 @@ export interface PlanReportDetail {
functionalCount: countDetail; functionalCount: countDetail;
apiCaseCount: countDetail; // 接口场景用例分析-用例数 apiCaseCount: countDetail; // 接口场景用例分析-用例数
apiScenarioCount: countDetail; // 接口场景用例分析-用例数 apiScenarioCount: countDetail; // 接口场景用例分析-用例数
planCount: number;
} }
export type AnalysisType = 'FUNCTIONAL' | 'API' | 'SCENARIO'; export type AnalysisType = 'FUNCTIONAL' | 'API' | 'SCENARIO';

View File

@ -2,7 +2,7 @@
<MsDetailDrawer <MsDetailDrawer
ref="detailDrawerRef" ref="detailDrawerRef"
v-model:visible="showDrawerVisible" v-model:visible="showDrawerVisible"
:width="900" :width="850"
:footer="false" :footer="false"
:title="t('bugManagement.detail.title', { id: detailInfo?.num, name: detailInfo?.title })" :title="t('bugManagement.detail.title', { id: detailInfo?.num, name: detailInfo?.title })"
:tooltip-text="(detailInfo && detailInfo.title) || null" :tooltip-text="(detailInfo && detailInfo.title) || null"

View File

@ -2,7 +2,7 @@
<MsDetailDrawer <MsDetailDrawer
ref="detailDrawerRef" ref="detailDrawerRef"
v-model:visible="showDrawerVisible" v-model:visible="showDrawerVisible"
:width="960" :width="850"
:footer="false" :footer="false"
:mask="false" :mask="false"
:title="t('caseManagement.featureCase.caseDetailTitle', { id: detailInfo?.num, name: detailInfo?.name })" :title="t('caseManagement.featureCase.caseDetailTitle', { id: detailInfo?.num, name: detailInfo?.name })"

View File

@ -28,7 +28,7 @@
type="text" type="text"
class="one-line-text w-full" class="one-line-text w-full"
:class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']" :class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']"
@click="showDetail()" @click="showDetail(record.resourceId)"
>{{ record.resourceNum }} >{{ record.resourceNum }}
</div> </div>
</template> </template>
@ -37,7 +37,7 @@
v-if="!record.integrated" v-if="!record.integrated"
class="one-line-text max-w-[300px]" class="one-line-text max-w-[300px]"
:class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']" :class="[hasJumpPermission ? 'text-[rgb(var(--primary-5))]' : '']"
@click="showDetail()" @click="showDetail(record.resourceId)"
>{{ record.resourceName }} >{{ record.resourceName }}
</div> </div>
</template> </template>
@ -75,7 +75,7 @@
<MsButton <MsButton
class="!mr-0" class="!mr-0"
:disabled="record.historyDeleted || !hasAnyPermission(permissionsMap[props.group].report)" :disabled="record.historyDeleted || !hasAnyPermission(permissionsMap[props.group].report)"
@click="viewReport(record.id)" @click="viewReport(record.id, record.integrated)"
>{{ t('project.taskCenter.viewReport') }} >{{ t('project.taskCenter.viewReport') }}
</MsButton> </MsButton>
</a-tooltip> </a-tooltip>
@ -84,7 +84,7 @@
<MsButton <MsButton
class="!mr-0" class="!mr-0"
:disabled="record.historyDeleted || !hasAnyPermission(permissionsMap[props.group].report)" :disabled="record.historyDeleted || !hasAnyPermission(permissionsMap[props.group].report)"
@click="viewReport(record.id)" @click="viewReport(record.id, record.integrated)"
>{{ t('project.taskCenter.viewReport') }} >{{ t('project.taskCenter.viewReport') }}
</MsButton> </MsButton>
</div> </div>
@ -420,14 +420,17 @@
} }
} }
function viewReport(id: string) { function viewReport(id: string, type: boolean) {
openNewPage(RouteEnum.TEST_PLAN_REPORT_DETAIL, { openNewPage(RouteEnum.TEST_PLAN_REPORT_DETAIL, {
id, id,
type: type ? 'GROUP' : 'TEST_PLAN',
}); });
} }
function showDetail() { function showDetail(id: string) {
// TODO openNewPage(RouteEnum.TEST_PLAN_INDEX_DETAIL, {
id,
});
} }
function stop(record: any) { function stop(record: any) {

View File

@ -36,7 +36,7 @@
<div <div
type="text" type="text"
class="one-line-text flex w-full text-[rgb(var(--primary-5))]" 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) }} {{ characterLimit(record.name) }}
</div> </div>
@ -56,7 +56,7 @@
</template> </template>
<template #passRate="{ record }"> <template #passRate="{ record }">
<div class="text-[var(--color-text-1)]"> <div class="text-[var(--color-text-1)]">
{{ `${record.passRate}%` }} {{ `${record.passRate || '0.00'}%` }}
</div> </div>
</template> </template>
<!-- 执行状态筛选 --> <!-- 执行状态筛选 -->
@ -420,19 +420,17 @@
/** /**
* 报告详情 showReportDetail * 报告详情 showReportDetail
*/ */
function showReportDetail(id: string) { function showReportDetail(id: string, type: boolean) {
router.push({ router.push({
name: TestPlanRouteEnum.TEST_PLAN_REPORT_DETAIL, name: TestPlanRouteEnum.TEST_PLAN_REPORT_DETAIL,
query: { query: {
id, id,
type: type ? 'GROUP' : 'TEST_PLAN',
}, },
}); });
} }
onBeforeMount(() => { onBeforeMount(() => {
if (route.query.id) {
showReportDetail(route.query.id as string);
}
initData(); initData();
}); });
</script> </script>

View File

@ -6,14 +6,14 @@
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }"> <template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<caseLevel :case-level="filterContent.value" /> <caseLevel :case-level="filterContent.value" />
</template> </template>
<template #caseLevel="{ record }"> <template #priority="{ record }">
<CaseLevel :case-level="record.caseLevel" /> <caseLevel :case-level="record.priority" />
</template> </template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }"> <template #[FilterSlotNameEnum.CASE_MANAGEMENT_EXECUTE_RESULT]="{ filterContent }">
<ExecuteResult :execute-result="filterContent.key" /> <ExecuteResult :execute-result="filterContent.key" />
</template> </template>
<template #lastExecResult="{ record }"> <template #lastExecResult="{ record }">
<ExecuteResult :execute-result="record.lastExecResult" /> <ExecuteResult :execute-result="record.executeResult" />
<!-- TOTO 暂时不上 --> <!-- TOTO 暂时不上 -->
<!-- <MsIcon <!-- <MsIcon
v-show="record.lastExecResult !== LastExecuteResults.PENDING" v-show="record.lastExecResult !== LastExecuteResults.PENDING"
@ -37,15 +37,13 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type'; import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; 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 ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import CaseAndScenarioReportDrawer from '@/views/api-test/components/caseAndScenarioReportDrawer.vue'; import CaseAndScenarioReportDrawer from '@/views/api-test/components/caseAndScenarioReportDrawer.vue';
import { getApiPage, getScenarioPage, getShareApiPage, getShareScenarioPage } from '@/api/modules/test-plan/report'; import { getApiPage, getScenarioPage, getShareApiPage, getShareScenarioPage } from '@/api/modules/test-plan/report';
import { ApiOrScenarioCaseItem } from '@/models/testPlan/report'; import { ApiOrScenarioCaseItem } from '@/models/testPlan/report';
import { LastExecuteResults } from '@/enums/caseEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { casePriorityOptions } from '@/views/api-test/components/config'; import { casePriorityOptions } from '@/views/api-test/components/config';
@ -61,6 +59,7 @@
{ {
title: 'ID', title: 'ID',
dataIndex: 'num', dataIndex: 'num',
slotName: 'num',
sortIndex: 1, sortIndex: 1,
fixed: 'left', fixed: 'left',
width: 100, width: 100,
@ -75,7 +74,7 @@
{ {
title: 'report.detail.level', title: 'report.detail.level',
dataIndex: 'priority', dataIndex: 'priority',
slotName: 'caseLevel', slotName: 'priority',
filterConfig: { filterConfig: {
options: casePriorityOptions, options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL, filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
@ -85,7 +84,7 @@
}, },
{ {
title: 'common.executionResult', title: 'common.executionResult',
dataIndex: 'lastExecResult', dataIndex: 'executeResult',
slotName: 'lastExecResult', slotName: 'lastExecResult',
filterConfig: { filterConfig: {
valueKey: 'key', valueKey: 'key',
@ -103,14 +102,6 @@
width: 200, width: 200,
showDrag: true, showDrag: true,
}, },
{
title: 'case.tableColumnCreateUser',
dataIndex: 'createUserName',
showTooltip: true,
width: 130,
showDrag: true,
},
{ {
title: 'testPlan.featureCase.executor', title: 'testPlan.featureCase.executor',
dataIndex: 'executeUser', dataIndex: 'executeUser',
@ -128,23 +119,16 @@
]; ];
const reportApiList = () => { const reportApiList = () => {
if (props.activeTab === 'scenarioCase') { 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(), reportApiList(),
{ {
scroll: { x: '100%' }, scroll: { x: '100%' },
columns, columns,
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG,
showSelectorAll: false, showSelectorAll: false,
},
(record) => {
return {
...record,
lastExecResult: record.executeResult ?? LastExecuteResults.PENDING,
};
} }
); );
@ -172,6 +156,8 @@
watch( watch(
() => props.activeTab, () => props.activeTab,
() => { () => {
resetFilterParams();
resetPagination();
loadCaseList(); loadCaseList();
} }
); );

View File

@ -10,17 +10,12 @@
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report'; import { getReportBugList, getReportShareBugList } from '@/api/modules/test-plan/report';
import { useTableStore } from '@/store';
import { TableKeyEnum } from '@/enums/tableEnum';
const props = defineProps<{ const props = defineProps<{
reportId: string; reportId: string;
shareId?: string; shareId?: string;
}>(); }>();
const tableStore = useTableStore();
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'ID', title: 'ID',
@ -69,7 +64,6 @@
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportBugList(), { const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportBugList(), {
scroll: { x: '100%' }, scroll: { x: '100%' },
columns, columns,
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG,
showSelectorAll: false, showSelectorAll: false,
}); });
@ -83,6 +77,4 @@
loadCaseList(); loadCaseList();
} }
}); });
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_REPORT_DETAIL_BUG, columns, 'drawer');
</script> </script>

View File

@ -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>

View File

@ -22,9 +22,7 @@
import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue'; import ExecuteResult from '@/components/business/ms-case-associate/executeResult.vue';
import { getReportFeatureCaseList, getReportShareFeatureCaseList } from '@/api/modules/test-plan/report'; import { getReportFeatureCaseList, getReportShareFeatureCaseList } from '@/api/modules/test-plan/report';
import { useTableStore } from '@/store';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils'; import { executionResultMap } from '@/views/case-management/caseManagementFeature/components/utils';
@ -32,10 +30,9 @@
const props = defineProps<{ const props = defineProps<{
reportId: string; reportId: string;
shareId?: string; shareId?: string;
activeTab: string;
}>(); }>();
const tableStore = useTableStore();
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'ID', title: 'ID',
@ -104,7 +101,6 @@
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportFeatureCaseList(), { const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportFeatureCaseList(), {
scroll: { x: '100%' }, scroll: { x: '100%' },
columns, columns,
tableKey: TableKeyEnum.TEST_PLAN_REPORT_DETAIL_FEATURE_CASE,
heightUsed: 20, heightUsed: 20,
showSelectorAll: false, showSelectorAll: false,
}); });
@ -119,6 +115,4 @@
loadCaseList(); loadCaseList();
} }
}); });
await tableStore.initColumn(TableKeyEnum.TEST_PLAN_REPORT_DETAIL_FEATURE_CASE, columns, 'drawer');
</script> </script>

View File

@ -10,13 +10,7 @@
/> />
</div> </div>
<div class="analysis min-w-[410px]"> <div class="analysis min-w-[410px]">
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div> <ExecuteAnalysis :detail="detail" />
<SetReportChart
size="160px"
:legend-data="legendData"
:options="executeCharOptions"
:request-total="getIndicators(detail.caseTotal) || 0"
/>
</div> </div>
</div> </div>
@ -117,6 +111,8 @@
v-model:richText="richText" v-model:richText="richText"
:share-id="shareId" :share-id="shareId"
:show-button="showButton" :show-button="showButton"
:is-plan-group="false"
:detail="detail"
@update-summary="handleUpdateReportDetail" @update-summary="handleUpdateReportDetail"
@cancel="handleCancel" @cancel="handleCancel"
@handle-summary="handleSummary" @handle-summary="handleSummary"
@ -130,9 +126,14 @@
class="relative mb-[16px] border-b" class="relative mb-[16px] border-b"
/> />
<BugTable v-if="activeTab === 'bug'" :report-id="detail.id" :share-id="shareId" /> <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 <ApiAndScenarioTable
v-if="activeTab === 'apiCase'" v-if="['apiCase', 'scenarioCase'].includes(activeTab)"
:report-id="detail.id" :report-id="detail.id"
:share-id="shareId" :share-id="shareId"
:active-tab="activeTab" :active-tab="activeTab"
@ -151,10 +152,10 @@
import MsCard from '@/components/pure/ms-card/index.vue'; import MsCard from '@/components/pure/ms-card/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue'; import MsTab from '@/components/pure/ms-tab/index.vue';
import ReportMetricsItem from './ReportMetricsItem.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 SingleStatusProgress from '@/views/test-plan/report/component/singleStatusProgress.vue';
import ApiAndScenarioTable from '@/views/test-plan/report/detail/component/apiAndScenarioTable.vue'; import ApiAndScenarioTable from '@/views/test-plan/report/detail/component/apiAndScenarioTable.vue';
import BugTable from '@/views/test-plan/report/detail/component/bugTable.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 FeatureCaseTable from '@/views/test-plan/report/detail/component/featureCaseTable.vue';
import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue'; import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue';
import Summary from '@/views/test-plan/report/detail/component/summary.vue'; import Summary from '@/views/test-plan/report/detail/component/summary.vue';
@ -164,7 +165,6 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils'; import { addCommasToNumber } from '@/utils';
import type { LegendData } from '@/models/apiTest/report';
import type { import type {
countDetail, countDetail,
PlanReportDetail, PlanReportDetail,
@ -172,7 +172,7 @@
StatusListType, StatusListType,
} from '@/models/testPlan/testPlanReport'; } from '@/models/testPlan/testPlanReport';
import { getIndicators } from '@/views/api-test/report/utils'; import { getSummaryDetail } from '@/views/test-plan/report/utils';
const { t } = useI18n(); const { t } = useI18n();
@ -197,53 +197,6 @@
*/ */
const shareId = ref<string>(route.query.shareId as string); 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({ const functionCaseOptions = ref({
...commonConfig, ...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) { function getPassRateData(caseDetailCount: countDetail) {
const caseCountDetail = caseDetailCount || defaultCount; const caseCountDetail = caseDetailCount || defaultCount;
@ -338,7 +267,6 @@
// //
function initOptionsData() { function initOptionsData() {
initExecuteOptions();
const { functionalCount, apiCaseCount, apiScenarioCount } = detail.value; const { functionalCount, apiCaseCount, apiScenarioCount } = detail.value;
functionCaseOptions.value.series.data = getPassRateData(functionalCount); functionCaseOptions.value.series.data = getPassRateData(functionalCount);
apiCaseOptions.value.series.data = getPassRateData(apiCaseCount); 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 functionCasePassRate = computed(() => {
const apiCaseDetail = getSummaryDetail(detail.value.functionalCount || defaultCount); const apiCaseDetail = getSummaryDetail(detail.value.functionalCount || defaultCount);
return apiCaseDetail.successRate; 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() { function handleCancel() {
richText.value = { summary: detail.value.summary }; richText.value = { summary: detail.value.summary };
showButton.value = false; showButton.value = false;
} }
function handleSummary() { function handleSummary(content: string) {
richText.value.summary = summaryContent.value; richText.value.summary = content;
} }
</script> </script>

View File

@ -10,19 +10,15 @@
/> />
</div> </div>
<div class="analysis min-w-[410px]"> <div class="analysis min-w-[410px]">
<div class="block-title">{{ t('report.detail.executionAnalysis') }}</div> <ExecuteAnalysis :detail="detail" />
<SetReportChart
size="160px"
:legend-data="legendData"
:options="charOptions"
:request-total="getIndicators(detail.caseTotal) || 0"
/>
</div> </div>
</div> </div>
<Summary <Summary
v-model:richText="richText" v-model:richText="richText"
:share-id="shareId" :share-id="shareId"
:show-button="showButton" :show-button="showButton"
:is-plan-group="true"
:detail="detail"
@update-summary="handleUpdateReportDetail" @update-summary="handleUpdateReportDetail"
@cancel="handleCancel" @cancel="handleCancel"
@handle-summary="handleSummary" @handle-summary="handleSummary"
@ -57,7 +53,7 @@
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import MsCard from '@/components/pure/ms-card/index.vue'; 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 ReportDetailTable from '@/views/test-plan/report/detail/component/reportDetailTable.vue';
import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue'; import ReportHeader from '@/views/test-plan/report/detail/component/reportHeader.vue';
import ReportMetricsItem from '@/views/test-plan/report/detail/component/ReportMetricsItem.vue'; import ReportMetricsItem from '@/views/test-plan/report/detail/component/ReportMetricsItem.vue';
@ -68,11 +64,8 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { addCommasToNumber } from '@/utils'; import { addCommasToNumber } from '@/utils';
import type { LegendData } from '@/models/apiTest/report';
import type { PlanReportDetail, ReportMetricsItemModel } from '@/models/testPlan/testPlanReport'; import type { PlanReportDetail, ReportMetricsItemModel } from '@/models/testPlan/testPlanReport';
import { getIndicators } from '@/views/api-test/report/utils';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -87,90 +80,23 @@
const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) }); const detail = ref<PlanReportDetail>({ ...cloneDeep(defaultReportDetail) });
const shareId = ref<string>(route.query.shareId as string); 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[]>(() => [ const reportAnalysisList = computed<ReportMetricsItemModel[]>(() => [
{ {
name: t('report.detail.testPlanTotal'), name: t('report.detail.testPlanTotal'),
value: detail.value.passThreshold, value: addCommasToNumber(detail.value.planCount),
unit: t('report.detail.number'), unit: t('report.detail.number'),
icon: 'plan_total', icon: 'plan_total',
}, },
{ {
name: t('report.detail.testPlanCaseTotal'), name: t('report.detail.testPlanCaseTotal'),
value: detail.value.passRate, value: addCommasToNumber(detail.value.caseTotal),
unit: t('report.detail.number'), unit: t('report.detail.number'),
icon: 'case_total', icon: 'case_total',
}, },
{ {
name: t('report.passRate'), name: t('report.passRate'),
value: detail.value.executeRate, value: detail.value.passRate,
unit: '%', unit: '%',
icon: 'passRate', icon: 'passRate',
}, },
@ -182,9 +108,8 @@
}, },
]); ]);
const summaryContent = ref<string>('');
const showButton = ref(false); const showButton = ref(false);
const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({ const richText = ref<{ summary: string; richTextTmpFileIds?: string[] }>({
summary: '', summary: '',
}); });
@ -210,8 +135,8 @@
showButton.value = false; showButton.value = false;
} }
function handleSummary() { function handleSummary(content: string) {
richText.value.summary = summaryContent.value; richText.value.summary = content;
} }
const currentMode = ref<string>('drawer'); const currentMode = ref<string>('drawer');
@ -219,6 +144,13 @@
currentMode.value = value as string; currentMode.value = value as string;
}; };
watchEffect(() => {
if (props.detailInfo) {
detail.value = cloneDeep(props.detailInfo);
richText.value.summary = detail.value.summary;
}
});
onMounted(async () => { onMounted(async () => {
nextTick(() => { nextTick(() => {
const editorContent = document.querySelector('.editor-content'); const editorContent = document.querySelector('.editor-content');

View File

@ -1,12 +1,13 @@
<template> <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> </template>
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue'; import { ref } from 'vue';
import { useRoute } from 'vue-router';
import { cloneDeep } from 'lodash-es'; 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 PlanGroupDetail from '@/views/test-plan/report/detail/component/planGroupDetail.vue';
import { getReportDetail } from '@/api/modules/test-plan/report'; import { getReportDetail } from '@/api/modules/test-plan/report';
@ -14,16 +15,17 @@
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport'; import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
const route = useRoute(); const props = defineProps<{
const reportId = ref<string>(route.query.id as string); isGroup: boolean;
reportId: string;
}>();
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail)); const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
async function getDetail() { async function getDetail() {
try { try {
detail.value = await getReportDetail(reportId.value); detail.value = await getReportDetail(props.reportId);
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} }
} }
@ -33,4 +35,4 @@
}); });
</script> </script>
<style scoped></style> <style scoped lang="less"></style>

View File

@ -11,6 +11,12 @@
</a-tooltip> </a-tooltip>
</div> </div>
</template> </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 }"> <template #operation="{ record }">
<MsButton class="!mx-0" @click="openReport(record)">{{ t('report.detail.testPlanGroup.viewReport') }}</MsButton> <MsButton class="!mx-0" @click="openReport(record)">{{ t('report.detail.testPlanGroup.viewReport') }}</MsButton>
</template> </template>
@ -25,13 +31,17 @@
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import type { MsTableColumn } from '@/components/pure/ms-table/type'; import type { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; 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 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 { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
import { PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { PlanReportStatus } from '@/enums/reportEnum';
import { RouteEnum } from '@/enums/routeEnum'; import { RouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
const { openNewPage } = useOpenNewPage(); const { openNewPage } = useOpenNewPage();
@ -42,6 +52,16 @@
shareId?: string; shareId?: string;
currentMode: string; currentMode: string;
}>(); }>();
const statusResultOptions = computed(() => {
return Object.keys(PlanReportStatus).map((key) => {
return {
value: key,
label: PlanReportStatus[key].statusText,
};
});
});
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'testPlan.testPlanIndex.operation', title: 'testPlan.testPlanIndex.operation',
@ -60,35 +80,41 @@
}, },
{ {
title: 'report.detail.testPlanGroup.result', title: 'report.detail.testPlanGroup.result',
dataIndex: 'result', dataIndex: 'resultStatus',
slotName: 'resultStatus',
filterConfig: {
options: statusResultOptions.value,
filterSlotName: FilterSlotNameEnum.TEST_PLAN_STATUS_FILTER,
},
showTooltip: true, showTooltip: true,
width: 200, width: 200,
}, },
{ {
title: 'report.detail.threshold', title: 'report.detail.threshold',
dataIndex: 'threshold', dataIndex: 'passThreshold',
slotName: 'threshold', slotName: 'passThreshold',
width: 150, width: 150,
}, },
{ {
title: 'report.passRate', title: 'report.passRate',
dataIndex: 'executeUser', dataIndex: 'passRate',
slotName: 'passRate',
titleSlotName: 'passRateTitle', titleSlotName: 'passRateTitle',
showTooltip: true, showTooltip: true,
width: 150, width: 150,
}, },
{ {
title: 'report.detail.testPlanGroup.useCasesCount', title: 'report.detail.testPlanGroup.useCasesCount',
dataIndex: 'bugCount', dataIndex: 'caseTotal',
width: 100, width: 100,
}, },
]; ];
const reportBugList = () => { const reportDetailList = () => {
return !props.shareId ? getReportBugList : getReportShareBugList; return !props.shareId ? getReportDetailPage : getReportDetailSharePage;
}; };
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportBugList(), { const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(reportDetailList(), {
columns, columns,
heightUsed: 20, heightUsed: 20,
showSelectorAll: false, showSelectorAll: false,
@ -104,8 +130,10 @@
loadReportDetailList(); loadReportDetailList();
} }
}); });
const reportVisible = ref(false); const reportVisible = ref(false);
function openReport(record: any) {
function openReport(record: PlanReportDetail) {
if (props.currentMode === 'drawer') { if (props.currentMode === 'drawer') {
reportVisible.value = true; reportVisible.value = true;
} else { } else {

View File

@ -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>

View File

@ -43,17 +43,23 @@
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { hasAnyPermission } from '@/utils/permission'; import { hasAnyPermission } from '@/utils/permission';
import type { PlanReportDetail } from '@/models/testPlan/testPlanReport';
import { getSummaryDetail } from '@/views/test-plan/report/utils';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
richText: { summary: string; richTextTmpFileIds?: string[] }; richText: { summary: string; richTextTmpFileIds?: string[] };
shareId?: string; shareId?: string;
showButton: boolean; showButton: boolean;
isPlanGroup: boolean;
detail: PlanReportDetail;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'updateSummary'): void; (e: 'updateSummary'): void;
(e: 'cancel'): void; (e: 'cancel'): void;
(e: 'handleSummary'): void; (e: 'handleSummary', content: string): void;
}>(); }>();
const innerSummary = useVModel(props, 'richText', emit); const innerSummary = useVModel(props, 'richText', emit);
@ -73,8 +79,34 @@
return data; 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() { function handleSummary() {
emit('handleSummary'); emit('handleSummary', summaryContent.value);
} }
</script> </script>

View File

@ -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>

View File

@ -1,7 +1,6 @@
<template> <template>
<PlanDetail :detail-info="detail" @update-success="getDetail()" /> <PlanGroupDetail v-if="isGroup" :detail-info="detail" @update-success="getDetail()" />
<!-- TODO 待联调计划组报告 --> <PlanDetail v-else :detail-info="detail" @update-success="getDetail()" />
<!-- <PlanGroupDetail :detail-info="detail" @update-success="getDetail()" /> -->
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -22,6 +21,8 @@
const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail)); const detail = ref<PlanReportDetail>(cloneDeep(defaultReportDetail));
const isGroup = computed(() => route.query.type === 'GROUP');
async function getDetail() { async function getDetail() {
try { try {
detail.value = await getReportDetail(reportId.value); detail.value = await getReportDetail(reportId.value);
@ -33,7 +34,6 @@
onBeforeMount(() => { onBeforeMount(() => {
getDetail(); getDetail();
}); });
//
</script> </script>
<style scoped lang="less"></style> <style scoped lang="less"></style>

View 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 {};

View File

@ -37,7 +37,7 @@
field="targetId" field="targetId"
:label="t('testPlan.testPlanIndex.testPlanGroup')" :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"> <a-option v-for="item of groupList" :key="item.id" :value="item.id">
{{ item.name }} {{ item.name }}
</a-option> </a-option>

View File

@ -157,13 +157,31 @@
</span> </span>
</a-tooltip> </a-tooltip>
</template> </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 }"> <template #passRate="{ record }">
<div class="mr-[8px] w-[100px]"> <div class="mr-[8px] w-[100px]">
<StatusProgress :status-detail="defaultCountDetailMap[record.id]" height="5px" /> <StatusProgress :status-detail="defaultCountDetailMap[record.id]" height="5px" />
</div> </div>
<div class="text-[var(--color-text-1)]"> <div class="text-[var(--color-text-1)]">
{{ `${defaultCountDetailMap[record.id] ? defaultCountDetailMap[record.id].passRate : '-'}%` }} {{ `${defaultCountDetailMap[record.id]?.passRate ? defaultCountDetailMap[record.id].passRate : '-'}%` }}
</div> </div>
</template> </template>
<template #passRateTitleSlot="{ columnConfig }"> <template #passRateTitleSlot="{ columnConfig }">
@ -221,6 +239,8 @@
<template #operation="{ record }"> <template #operation="{ record }">
<div class="flex items-center"> <div class="flex items-center">
<!-- TODO 测试计划组手动生成报告 -->
<!-- <MsButton class="mr-2" @click="handleGenerateReport(record.id)">生成报告</MsButton> -->
<MsButton <MsButton
v-if=" v-if="
getFunctionalCount(record.id) > 0 && getFunctionalCount(record.id) > 0 &&
@ -313,7 +333,6 @@
:type="showType" :type="showType"
@save="handleMoveOrCopy" @save="handleMoveOrCopy"
/> />
<!-- TODO 待联调[编辑] 字段加到统计里边 -->
<ScheduledModal <ScheduledModal
v-model:visible="showScheduledTaskModal" v-model:visible="showScheduledTaskModal"
:type="planType" :type="planType"
@ -398,6 +417,7 @@
PassRateCountDetail, PassRateCountDetail,
TestPlanItem, TestPlanItem,
} from '@/models/testPlan/testPlan'; } from '@/models/testPlan/testPlan';
import { LastExecuteResults } from '@/enums/caseEnum';
import { TestPlanRouteEnum } from '@/enums/routeEnum'; import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum'; import { ColumnEditTypeEnum, TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
@ -530,7 +550,6 @@
{ {
title: 'testPlan.testPlanIndex.planStartToEndTime', title: 'testPlan.testPlanIndex.planStartToEndTime',
slotName: 'planStartToEndTime', slotName: 'planStartToEndTime',
dataIndex: 'planStartToEndTime',
showInTable: false, showInTable: false,
sortable: { sortable: {
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
@ -542,7 +561,6 @@
{ {
title: 'testPlan.testPlanIndex.actualStartToEndTime', title: 'testPlan.testPlanIndex.actualStartToEndTime',
slotName: 'actualStartToEndTime', slotName: 'actualStartToEndTime',
dataIndex: 'actualStartToEndTime',
showInTable: false, showInTable: false,
sortable: { sortable: {
sortDirections: ['ascend', 'descend'], sortDirections: ['ascend', 'descend'],
@ -763,7 +781,7 @@
showSetting: true, showSetting: true,
heightUsed: 236, heightUsed: 236,
paginationSize: 'mini', paginationSize: 'mini',
showSelectorAll: true, showSelectorAll: false,
draggable: { type: 'handle' }, draggable: { type: 'handle' },
draggableCondition: true, draggableCondition: true,
}); });

View File

@ -132,7 +132,7 @@
const initForm: CreateTask = { const initForm: CreateTask = {
resourceId: '', resourceId: '',
cron: '', cron: '0 0 0/1 * * ?',
enable: false, enable: false,
runConfig: { runMode: 'SERIAL' }, runConfig: { runMode: 'SERIAL' },
}; };

View File

@ -359,6 +359,7 @@
await generateReport({ await generateReport({
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
testPlanId: detail.value.id as string, testPlanId: detail.value.id as string,
triggerMode: 'MANUAL',
}); });
Message.success(t('testPlan.testPlanDetail.successfullyGenerated')); Message.success(t('testPlan.testPlanDetail.successfullyGenerated'));
} catch (error) { } catch (error) {

View File

@ -145,4 +145,5 @@ export default {
'testPlan.plan': 'Test plan', 'testPlan.plan': 'Test plan',
'testPlan.planTip': 'testPlan.planTip':
'1. Create a test set for business classification testing; 2. Select the test set associated use case', '1. Create a test set for business classification testing; 2. Select the test set associated use case',
'testPlan.planStartToEndTimeTip': '测试计划已超时',
}; };

View File

@ -133,4 +133,5 @@ export default {
'testPlan.testPlanGroup.closeScheduleTaskSuccess': '关闭定时任务成功', 'testPlan.testPlanGroup.closeScheduleTaskSuccess': '关闭定时任务成功',
'testPlan.plan': '测试规划', 'testPlan.plan': '测试规划',
'testPlan.planTip': '1.创建测试集进行业务分类测试2.选择测试集关联用例', 'testPlan.planTip': '1.创建测试集进行业务分类测试2.选择测试集关联用例',
'testPlan.planStartToEndTimeTip': '测试计划已超时',
}; };