feat(任务中心): 测试计划执行结果

This commit is contained in:
baiqi 2024-11-19 19:04:53 +08:00 committed by Craftsman
parent 81ff2a8f9b
commit 7956aa5b42
7 changed files with 463 additions and 22 deletions

View File

@ -209,4 +209,7 @@ export function getCollectScenarioPage(data: TableQueryParams) {
return MSR.post<CommonList<testPlanSetItem>>({ url: `${reportUrl.getCollectScenarioUrl}`, data });
}
export default {};
// 测试计划-获取执行结果
export function getTestPlanResult(id: string) {
return MSR.get({ url: `${reportUrl.GetTestPlanResultUrl}/${id}` });
}

View File

@ -86,3 +86,5 @@ export const getShareCollectFunctionalUrl = '/test-plan/report/share/detail/func
export const getShareCollectionApiUrl = '/test-plan/report/share/detail/api/collection/page';
// 测试计划-报告-详情-分享-报告-场景明细-测试点
export const getShareCollectScenarioUrl = '/test-plan/report/share/detail/scenario/collection/page';
// 任务中心-测试计划执行结果
export const GetTestPlanResultUrl = '/test-plan/report/get-result';

View File

@ -40,12 +40,14 @@
</MsButton>
</div>
</template>
<a-spin :loading="loading" class="block">
<CaseReportCom
v-if="!props.isScenario"
:detail-info="reportStepDetail"
:get-report-step-detail="props.getReportStepDetail"
/>
<ScenarioCom v-else :detail-info="reportStepDetail" :get-report-step-detail="props.getReportStepDetail" />
</a-spin>
</MsDrawer>
</template>
@ -128,8 +130,11 @@
const reportStepDetail = ref<ReportDetail>({
...initReportStepDetail,
});
const loading = ref(false);
async function getReportDetail() {
try {
loading.value = true;
if (props.reportDetail) {
reportStepDetail.value = await props.reportDetail(props.reportId, route.query.shareId as string | undefined);
return;
@ -142,6 +147,8 @@
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
@ -152,6 +159,9 @@
reportStepDetail.value = { ...initReportStepDetail };
await getReportDetail();
}
},
{
immediate: true,
}
);

View File

@ -70,7 +70,7 @@
{{ t('ms.taskCenter.rerun') }}
</MsButton>
<MsButton v-if="record.status === ExecuteStatusEnum.COMPLETED" @click="checkReport(record)">
{{ t('ms.taskCenter.checkReport') }}
{{ t('ms.taskCenter.executeResult') }}
</MsButton>
</template>
</ms-base-table>
@ -108,6 +108,11 @@
}"
:share-time="shareTime"
/>
<TestPlanExecuteResultDrawer
:id="activeDetailId"
v-model:visible="showTestPlanDetailDrawer"
:is-group="isTestPlanGroup"
/>
</template>
<script setup lang="ts">
@ -124,6 +129,7 @@
import execStatus from './execStatus.vue';
import executeRatePopper from './executeRatePopper.vue';
import executeResultStatus from './executeResultStatus.vue';
import TestPlanExecuteResultDrawer from './testPlanExecuteResultDrawer.vue';
import CaseReportDrawer from '@/views/api-test/report/component/caseReportDrawer.vue';
import ReportDetailDrawer from '@/views/api-test/report/component/reportDetailDrawer.vue';
@ -160,7 +166,6 @@
} from '@/api/modules/taskCenter/system';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app';
import { characterLimit } from '@/utils';
@ -168,7 +173,6 @@
import { TaskCenterTaskItem } from '@/models/taskCenter';
import { ReportEnum } from '@/enums/reportEnum';
import { TestPlanRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { ExecuteResultEnum, ExecuteStatusEnum, ExecuteTaskType } from '@/enums/taskCenter';
@ -185,7 +189,6 @@
const { t } = useI18n();
const { openModal } = useModal();
const { openNewPage } = useOpenNewPage();
const tableStore = useTableStore();
const appStore = useAppStore();
@ -657,6 +660,8 @@
const activeReportIndex = ref<number>(0);
const showDetailDrawer = ref<boolean>(false);
const showCaseDetailDrawer = ref<boolean>(false);
const showTestPlanDetailDrawer = ref<boolean>(false);
const isTestPlanGroup = ref(false);
function showReportDetail(record: TaskCenterTaskItem) {
activeDetailId.value = record.reportId;
@ -718,12 +723,9 @@
) {
showReportDetail(record);
} else if ([ExecuteTaskType.TEST_PLAN_GROUP, ExecuteTaskType.TEST_PLAN].includes(record.taskType)) {
openNewPage(TestPlanRouteEnum.TEST_PLAN_REPORT, {
orgId: record.organizationId,
pId: record.projectId,
id: record.reportId,
type: record.taskType === ExecuteTaskType.TEST_PLAN_GROUP ? 'GROUP' : '',
});
showTestPlanDetailDrawer.value = true;
activeDetailId.value = record.reportId;
isTestPlanGroup.value = record.taskType === ExecuteTaskType.TEST_PLAN_GROUP;
}
}

View File

@ -0,0 +1,417 @@
<template>
<MsDrawer v-model:visible="visible" :width="1200" :footer="false" no-content-padding>
<template #title>
<div class="flex flex-1 items-center gap-[8px] overflow-hidden">
<a-tag :color="executeResultMap[detail.resultStatus]?.color">
{{ t(executeResultMap[detail.resultStatus]?.label || '-') }}
</a-tag>
<div class="one-line-text flex-1">{{ detail.name }}</div>
</div>
<div class="flex justify-end">
<MsButton type="icon" status="secondary" class="!rounded-[var(--border-radius-small)]" @click="init">
<MsIcon type="icon-icon_reset_outlined" class="mr-[8px]" size="14" />
{{ t('common.refresh') }}
</MsButton>
</div>
</template>
<a-spin :loading="loading" class="block">
<div class="analysis-wrapper">
<SystemTrigger :is-preview="true">
<div class="analysis min-w-[330px]">
<ReportMetricsItem
v-for="analysisItem in reportAnalysisList"
:key="analysisItem.name"
:item-info="analysisItem"
/>
</div>
</SystemTrigger>
<SystemTrigger :is-preview="true">
<div class="analysis min-w-[330px]">
<ExecuteAnalysis :detail="detail" hide-title />
</div>
<template #content>
<div class="arco-table-filters-content px-[8px] py-[4px]">
{{ t('report.detail.systemInternalTooltip') }}
</div>
</template>
</SystemTrigger>
</div>
<div class="px-[24px]">
<a-select
v-if="testPlanGroups.length > 0"
v-model:model-value="activePlan"
:options="testPlanGroups"
class="w-[240px]"
@change="searchList"
></a-select>
<div class="flex items-center justify-between">
<MsTab
v-model:active-key="activeTable"
:content-tab-list="contentTabList"
:show-badge="false"
class="testPlan-execute-tab no-content"
@change="searchList"
/>
<a-input-search
v-model:model-value="keyword"
:placeholder="t('report.detail.caseDetailSearchPlaceholder')"
allow-clear
class="w-[240px]"
@search="searchList"
@press-enter="searchList"
@clear="searchList"
/>
</div>
<div class="mt-[8px]">
<MsBaseTable
v-bind="currentCaseTable.propsRes.value"
:row-class="getRowClass"
v-on="currentCaseTable.propsEvent.value"
>
<template #num="{ record }">
<MsButton type="text" @click="toDetail(record)">{{ record.num }}</MsButton>
</template>
<template #[FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL]="{ filterContent }">
<caseLevel :case-level="filterContent.value" />
</template>
<template #priority="{ record }">
<caseLevel :case-level="record.priority" />
</template>
<template #[FilterSlotNameEnum.API_TEST_CASE_API_LAST_EXECUTE_STATUS]="{ filterContent }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="filterContent.value" />
</template>
<template #lastExecResult="{ record }">
<ExecutionStatus
:module-type="ReportEnum.API_REPORT"
:status="record.executeResult"
:class="[!record.executeResult ? '' : 'cursor-pointer']"
@click="showReport(record)"
/>
</template>
</MsBaseTable>
</div>
</div>
</a-spin>
</MsDrawer>
<CaseAndScenarioReportDrawer
v-if="reportVisible"
v-model:visible="reportVisible"
:report-id="apiReportId"
do-not-show-share
:is-scenario="activeTable === 'scenario'"
:report-detail="activeTable === 'scenario' ? reportScenarioDetail : reportCaseDetail"
:get-report-step-detail="activeTable === 'scenario' ? reportStepDetail : reportCaseStepDetail"
/>
</template>
<script setup lang="ts">
import { SelectOptionData } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTab from '@/components/pure/ms-tab/index.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 caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import ExecuteAnalysis from '@/views/test-plan/report/detail/component/system-card/executeAnalysis.vue';
import ReportMetricsItem from '@/views/test-plan/report/detail/component/system-card/ReportMetricsItem.vue';
import SystemTrigger from '@/views/test-plan/report/detail/component/system-card/systemTrigger.vue';
import {
getApiPage,
getScenarioPage,
getTestPlanResult,
reportCaseDetail,
reportCaseStepDetail,
reportScenarioDetail,
reportStepDetail,
} from '@/api/modules/test-plan/report';
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { addCommasToNumber } from '@/utils';
import { ApiOrScenarioCaseItem } from '@/models/testPlan/report';
import { ReportMetricsItemModel } from '@/models/testPlan/testPlanReport';
import { ReportEnum } from '@/enums/reportEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { FilterSlotNameEnum } from '@/enums/tableFilterEnum';
import { casePriorityOptions, lastReportStatusListOptions } from '@/views/api-test/components/config';
import { executeResultMap } from '@/views/taskCenter/component/config';
const CaseAndScenarioReportDrawer = defineAsyncComponent(
() => import('@/views/api-test/components/caseAndScenarioReportDrawer.vue')
);
const props = defineProps<{
id: string;
isGroup: boolean;
}>();
const { openNewPage } = useOpenNewPage();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true });
const loading = ref(false);
const detail = ref<any>({});
const testPlanGroups = ref<SelectOptionData[]>([]);
const activePlan = ref('');
const reportAnalysisList = computed<ReportMetricsItemModel[]>(() => {
if (props.isGroup) {
return [
{
name: t('report.detail.testPlanTotal'),
value: detail.value.planCount,
unit: t('report.detail.number'),
icon: 'plan_total',
},
{
name: t('report.detail.testPlanCaseTotal'),
value: detail.value.caseTotal,
unit: t('report.detail.number'),
icon: 'case_total',
},
{
name: t('report.passRate'),
value: detail.value.passRate,
unit: '%',
icon: 'passRate',
},
{
name: t('report.detail.totalDefects'),
value: addCommasToNumber(detail.value.bugCount),
unit: t('report.detail.number'),
icon: 'bugTotal',
},
];
}
return [
{
name: t('report.detail.threshold'),
value: detail.value.passThreshold,
unit: '%',
icon: 'threshold',
},
{
name: t('report.passRate'),
value: detail.value.passRate,
unit: '%',
icon: 'passRate',
},
{
name: t('report.detail.performCompletion'),
value: detail.value.executeRate,
unit: '%',
icon: 'passRate',
},
{
name: t('report.detail.totalDefects'),
value: addCommasToNumber(detail.value.bugCount),
unit: t('report.detail.number'),
icon: 'bugTotal',
},
];
});
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
width: 100,
},
{
title: 'common.name',
dataIndex: 'name',
width: 150,
showTooltip: true,
},
{
title: 'report.detail.level',
dataIndex: 'priority',
slotName: 'priority',
filterConfig: {
options: casePriorityOptions,
filterSlotName: FilterSlotNameEnum.CASE_MANAGEMENT_CASE_LEVEL,
},
width: 150,
},
{
title: 'common.belongModule',
dataIndex: 'moduleName',
showTooltip: true,
width: 200,
},
{
title: 'testPlan.featureCase.executor',
dataIndex: 'executeUser',
showTooltip: true,
width: 150,
},
{
title: 'common.executionResult',
dataIndex: 'executeResult',
slotName: 'lastExecResult',
filterConfig: {
options: lastReportStatusListOptions.value,
filterSlotName: FilterSlotNameEnum.API_TEST_CASE_API_LAST_EXECUTE_STATUS,
emptyFilter: true,
},
width: 150,
},
];
const activeTable = ref<'case' | 'scenario'>('case');
const contentTabList = [
{ value: 'case', label: t('report.detail.apiCaseDetails') },
{ value: 'scenario', label: t('report.detail.scenarioCaseDetails') },
];
const keyword = ref('');
const useApiTable = useTable(getApiPage, {
scroll: { x: '100%' },
columns,
showSelectorAll: false,
heightUsed: 236,
showSetting: false,
paginationSize: 'mini',
});
const useScenarioTable = useTable(getScenarioPage, {
scroll: { x: '100%' },
columns,
showSelectorAll: false,
showSetting: false,
heightUsed: 236,
paginationSize: 'mini',
});
const currentCaseTable = computed(() => (activeTable.value === 'case' ? useApiTable : useScenarioTable));
//
const reportVisible = ref(false);
const apiReportId = ref<string>('');
const selectedReportId = ref<string>('');
function showReport(record: ApiOrScenarioCaseItem) {
if (!record.id) {
return;
}
if (!record.executeResult || record.executeResult === 'STOPPED') return;
reportVisible.value = true;
apiReportId.value = record.reportId;
selectedReportId.value = record.reportId;
}
function getRowClass(record: ApiOrScenarioCaseItem) {
return record.reportId === selectedReportId.value ? 'selected-row-class' : '';
}
function searchList() {
currentCaseTable.value.setLoadListParams({
keyword: keyword.value,
reportId: activePlan.value || detail.value.id,
});
currentCaseTable.value.loadList();
}
//
function toDetail(record: ApiOrScenarioCaseItem) {
if (activeTable.value === 'scenario') {
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, {
id: record.id,
pId: record.projectId,
});
} else {
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, {
cId: record.id,
pId: record.projectId,
});
}
}
async function init() {
try {
loading.value = true;
const res = await getTestPlanResult(props.id);
detail.value = res;
if (res.children?.length) {
testPlanGroups.value = res.children.map((item: any) => ({
value: item.id,
label: item.name,
}));
activePlan.value = res.children[0]?.id;
}
searchList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
watch(
() => visible.value,
(val) => {
if (props.id && val) {
init();
}
},
{ immediate: true }
);
</script>
<style lang="less">
.testPlan-execute-tab {
.arco-tabs-tab:first-child {
margin-left: 0;
}
}
</style>
<style lang="less" scoped>
:deep(.ms-description-item) {
@apply items-center;
margin-bottom: 8px;
font-size: 12px;
line-height: 16px;
}
.analysis-wrapper {
@apply mb-4 grid items-center gap-4;
padding: 16px 16px 16px 24px;
background-color: var(--color-bg-3);
grid-template-columns: repeat(2, 1fr);
.analysis {
padding: 16px;
border: 1px solid transparent;
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
@apply h-full rounded-xl bg-white;
.charts {
@apply absolute text-center;
top: 50%;
right: 0;
bottom: 0;
left: 50%;
z-index: 99;
width: 70px;
height: 42px;
transform: translateY(-50%) translateX(-50%);
}
}
}
.hover-analysis {
&:hover {
border: 1px solid rgb(var(--primary-5));
}
}
</style>

View File

@ -46,7 +46,10 @@
padding: 4px 8px;
border-radius: 4px;
background-color: var(--color-text-n9);
@apply mb-3 flex items-center justify-between;
@apply flex items-center justify-between;
&:not(:last-child) {
margin-bottom: 12px;
}
.report-analysis-item-icon {
@apply flex items-center;
}

View File

@ -1,5 +1,5 @@
<template>
<div class="mb-[16px] font-medium">{{ t('report.detail.executionAnalysis') }}</div>
<div v-if="!props.hideTitle" class="mb-[16px] font-medium">{{ t('report.detail.executionAnalysis') }}</div>
<SetReportChart
size="130px"
:legend-data="legendData"
@ -25,6 +25,7 @@
const props = defineProps<{
detail: PlanReportDetail;
hideTitle?: boolean;
animation?: boolean; //
}>();
@ -108,12 +109,15 @@
const getTotal = computed(() => {
const { executeCount } = props.detail;
if (executeCount) {
const { success, error, fakeError, pending, block } = executeCount;
return success + error + fakeError + pending + block;
}
return 0;
});
watchEffect(() => {
if (props.detail) {
if (Object.keys(props.detail).length > 0) {
initExecuteOptions();
}
});