feat(接口测试): 用例详情页面布局调整&diff抽屉调整&编辑详情页面新增对比

This commit is contained in:
xinxin.wu 2024-08-02 18:31:26 +08:00 committed by Craftsman
parent c96bd87065
commit bc1d906ae6
13 changed files with 636 additions and 44 deletions

View File

@ -100,9 +100,7 @@
.ms-detail-card { .ms-detail-card {
@apply relative flex flex-col; @apply relative flex flex-col;
padding: 16px;
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
gap: 8px; gap: 8px;
.ms-detail-card-title { .ms-detail-card-title {
@apply flex items-center justify-between overflow-hidden; @apply flex items-center justify-between overflow-hidden;

View File

@ -3,6 +3,7 @@ export enum ApiTestRouteEnum {
API_TEST_DEBUG_MANAGEMENT = 'apiTestDebug', API_TEST_DEBUG_MANAGEMENT = 'apiTestDebug',
API_TEST_MANAGEMENT = 'apiTestManagement', API_TEST_MANAGEMENT = 'apiTestManagement',
API_TEST_MANAGEMENT_RECYCLE = 'apiTestManagementRecycle', API_TEST_MANAGEMENT_RECYCLE = 'apiTestManagementRecycle',
API_TEST_MANAGEMENT_CASE_DETAIL = 'apiTestManagementCaseDetail',
API_TEST_SCENARIO = 'apiTestScenario', API_TEST_SCENARIO = 'apiTestScenario',
API_TEST_SCENARIO_RECYCLE = 'apiTestScenarioRecycle', API_TEST_SCENARIO_RECYCLE = 'apiTestScenarioRecycle',
API_TEST_REPORT = 'apiTestReport', API_TEST_REPORT = 'apiTestReport',

View File

@ -64,6 +64,26 @@ const ApiTest: AppRouteRecordRaw = {
], ],
}, },
}, },
// 接口定义-API-用例-用例列表详情
{
path: 'caseDetail',
name: ApiTestRouteEnum.API_TEST_MANAGEMENT_CASE_DETAIL,
component: () => import('@/views/api-test/management/components/management/case/apiCaseDetail.vue'),
meta: {
locale: 'case.apiCaseDetail',
roles: ['PROJECT_API_DEFINITION:READ'],
breadcrumbs: [
{
name: ApiTestRouteEnum.API_TEST_MANAGEMENT,
locale: 'case.apiCaseList',
},
{
name: ApiTestRouteEnum.API_TEST_MANAGEMENT_CASE_DETAIL,
locale: 'case.apiCaseDetail',
},
],
},
},
{ {
path: 'scenario', path: 'scenario',
name: ApiTestRouteEnum.API_TEST_SCENARIO, name: ApiTestRouteEnum.API_TEST_SCENARIO,

View File

@ -1,6 +1,7 @@
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
import { EQUAL } from '@/components/pure/ms-advance-filter'; import { EQUAL } from '@/components/pure/ms-advance-filter';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -18,14 +19,18 @@ import type { MockParams } from '@/models/apiTest/mock';
import { import {
FullResponseAssertionType, FullResponseAssertionType,
RequestAssertionCondition, RequestAssertionCondition,
RequestAuthType,
RequestBodyFormat, RequestBodyFormat,
RequestCaseStatus, RequestCaseStatus,
RequestComposition,
RequestContentTypeEnum, RequestContentTypeEnum,
RequestDefinitionStatus,
RequestExtractEnvType, RequestExtractEnvType,
RequestExtractExpressionEnum, RequestExtractExpressionEnum,
RequestExtractExpressionRuleType, RequestExtractExpressionRuleType,
RequestExtractResultMatchingRule, RequestExtractResultMatchingRule,
RequestExtractScope, RequestExtractScope,
RequestMethods,
RequestParamsType, RequestParamsType,
ResponseBodyFormat, ResponseBodyFormat,
ResponseBodyXPathAssertionFormat, ResponseBodyXPathAssertionFormat,
@ -432,3 +437,74 @@ export const lastReportStatusListOptions = computed(() => {
}; };
}); });
}); });
// api下的创建用例弹窗也用到了defaultCaseParams
const initDefaultId = `case-${Date.now()}`;
export const defaultCaseParams: RequestParam = {
id: initDefaultId,
type: 'case',
moduleId: '',
protocol: 'HTTP',
tags: [],
description: '',
priority: 'P0',
status: RequestDefinitionStatus.PROCESSING,
url: '',
activeTab: RequestComposition.HEADER,
closable: true,
method: RequestMethods.GET,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
basicAuth: {
userName: '',
password: '',
},
digestAuth: {
userName: '',
password: '',
},
},
children: [
{
polymorphicName: 'MsCommonElement', // 协议多态名称写死MsCommonElement
assertionConfig: {
enableGlobal: true,
assertions: [],
},
postProcessorConfig: {
enableGlobal: true,
processors: [],
},
preProcessorConfig: {
enableGlobal: true,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true,
unSaved: false,
executeLoading: false,
preDependency: [], // 前置依赖
postDependency: [], // 后置依赖
errorMessageInfo: {},
};

View File

@ -2,14 +2,29 @@
<a-collapse v-model:active-key="activeDetailKey" :bordered="false"> <a-collapse v-model:active-key="activeDetailKey" :bordered="false">
<a-collapse-item key="request"> <a-collapse-item key="request">
<template #header> <template #header>
<div class="flex items-center gap-[4px]"> <div class="flex items-center justify-between">
<div v-if="activeDetailKey.includes('request')" class="down-icon"> <div class="flex items-center gap-[4px]">
<icon-down :size="10" class="block" /> <div class="font-medium">{{ t('apiTestManagement.requestParams') }}</div>
<div v-if="activeDetailKey.includes('request')" class="down-icon">
<icon-down :size="10" class="block" />
</div>
<div v-else class="h-[16px] w-[16px] !rounded-full p-[4px]">
<icon-right :size="10" class="block" />
</div>
</div> </div>
<div v-else class="h-[16px] w-[16px] !rounded-full p-[4px]"> <MsTag
<icon-right :size="10" class="block" /> v-if="props.detail.inconsistentWithApi"
</div> class="cursor-pointer"
<div class="font-medium">{{ t('apiTestManagement.requestParams') }}</div> type="warning"
theme="light"
:tooltip-disabled="true"
@click.stop="showDiffDrawer"
>
<template #icon>
<MsIcon type="icon-icon_warning_colorful" size="16" />
</template>
<span class="ml-[8px]"> {{ statusText }}</span>
</MsTag>
</div> </div>
</template> </template>
<div class="detail-collapse-item"> <div class="detail-collapse-item">
@ -258,13 +273,13 @@
<template #header> <template #header>
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<div class="font-medium">{{ t('apiTestManagement.responseContent') }}</div>
<div v-if="activeDetailKey.includes('response')" class="down-icon"> <div v-if="activeDetailKey.includes('response')" class="down-icon">
<icon-down :size="10" class="block" /> <icon-down :size="10" class="block" />
</div> </div>
<div v-else class="h-[16px] w-[16px] !rounded-full p-[4px]"> <div v-else class="h-[16px] w-[16px] !rounded-full p-[4px]">
<icon-right :size="10" class="block" /> <icon-right :size="10" class="block" />
</div> </div>
<div class="font-medium">{{ t('apiTestManagement.responseContent') }}</div>
</div> </div>
<responseCodeTimeSize v-if="props.isCase" :request-result="previewDetail.response?.requestResults[0]" /> <responseCodeTimeSize v-if="props.isCase" :request-result="previewDetail.response?.requestResults[0]" />
</div> </div>
@ -404,6 +419,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsJsonSchema from '@/components/pure/ms-json-schema/index.vue'; import MsJsonSchema from '@/components/pure/ms-json-schema/index.vue';
import { parseSchemaToJsonSchemaTableData } from '@/components/pure/ms-json-schema/utils'; import { parseSchemaToJsonSchemaTableData } from '@/components/pure/ms-json-schema/utils';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { ResponseItem } from '@/views/api-test/components/requestComposition/response/edit.vue'; import { ResponseItem } from '@/views/api-test/components/requestComposition/response/edit.vue';
import responseCodeTimeSize from '@/views/api-test/components/requestComposition/response/responseCodeTimeSize.vue'; import responseCodeTimeSize from '@/views/api-test/components/requestComposition/response/responseCodeTimeSize.vue';
import Result from '@/views/api-test/components/requestComposition/response/result.vue'; import Result from '@/views/api-test/components/requestComposition/response/result.vue';
@ -425,6 +441,7 @@
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'execute', val: 'localExec' | 'serverExec'): void; (e: 'execute', val: 'localExec' | 'serverExec'): void;
(e: 'showDiff'): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -864,11 +881,23 @@
activeResponse.value.body.jsonBody.enableJsonSchema = type === 'Schema'; activeResponse.value.body.jsonBody.enableJsonSchema = type === 'Schema';
} }
} }
const statusText = computed(() => {
if (props.detail.inconsistentWithApi) {
return t('case.definitionInconsistent');
}
// TODO
});
// diff
function showDiffDrawer() {
emit('showDiff');
}
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
.down-icon { .down-icon {
padding: 4px; padding: 3px;
width: 16px; width: 16px;
height: 16px; height: 16px;
border-radius: 50%; border-radius: 50%;
@ -886,9 +915,8 @@
.arco-collapse-item-header-title { .arco-collapse-item-header-title {
@apply block w-full; @apply block w-full;
padding: 8px 16px; padding: 8px 0;
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
background-color: var(--color-text-n9);
} }
} }
.detail-collapse-item { .detail-collapse-item {

View File

@ -0,0 +1,416 @@
<template>
<MsCard :header-min-width="1100" :min-width="150" auto-height hide-footer no-content-padding hide-divider hide-back>
<template #headerLeft>
<div class="flex items-center gap-4">
<caseLevel :case-level="caseDetail.priority as CaseLevel" />
<div class="one-line-text max-w-[300px] font-medium text-[var(--color-text-1)]">
{{ `[${caseDetail.num}] ${caseDetail.name}` }}
</div>
<a-tooltip :content="t(caseDetail.follow ? 'common.forked' : 'common.notForked')">
<MsIcon
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
:loading="followLoading"
:type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
class="cursor-pointer"
:size="16"
@click="follow"
/>
</a-tooltip>
<a-tooltip :content="t('report.detail.api.copyLink')">
<MsIcon type="icon-icon_share1" class="cursor-pointer text-[var(--color-text-4)]" :size="16" @click="share" />
</a-tooltip>
</div>
</template>
<template #headerRight>
<div class="flex gap-[12px]">
<MsEnvironmentSelect :env="environmentIdByDrawer" />
<executeButton
ref="executeRef"
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
:execute-loading="caseDetail.executeLoading"
@stop-debug="stopDebug"
@execute="handleExecute"
/>
<a-dropdown-button type="outline" @click="editCase">
{{ t('common.edit') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
value="delete"
class="error-6 text-[rgb(var(--danger-6))]"
@click="handleDelete"
>
<MsIcon type="icon-icon_delete-trash_outlined1" class="text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }}
</a-doption>
</template>
</a-dropdown-button>
</div>
</template>
<createAndEditCaseDrawer
ref="createAndEditCaseDrawerRef"
v-bind="$attrs"
@load-case="(id)=>getCaseDetailInfo(id as string)"
/>
<a-divider :margin="0"></a-divider>
<MsTab
v-model:active-key="activeKey"
:show-badge="false"
:content-tab-list="tabList"
no-content
class="relative mx-[16px] border-b"
/>
</MsCard>
<MsCard class="mt-[16px]" :special-height="174" simple>
<div v-if="activeKey === 'detail'">
<MsDetailCard :title="t('common.baseInfo')" :description="description" class="mb-[8px]">
<template #type="{ value }">
<apiMethodName v-if="value" :method="value as RequestMethods" tag-size="small" is-tag />
</template>
</MsDetailCard>
<detailTab
:detail="caseDetail as RequestParam"
:protocols="protocols as ProtocolItem[]"
:is-priority-local-exec="isPriorityLocalExec"
is-case
@execute="handleExecute"
@show-diff="showDiffDrawer"
/>
<DifferentDrawer
v-model:visible="showDifferentDrawer"
:active-api-case-id="activeApiCaseId"
:active-defined-id="activeDefinedId"
@close="closeDifferent"
/>
</div>
<tab-case-dependency v-else-if="activeKey === 'reference'" :source-id="caseDetail.id" />
<tab-case-execute-history
v-else-if="activeKey === 'executeHistory'"
ref="executeHistoryRef"
:source-id="caseDetail.id"
module-type="API_REPORT"
:protocol="caseDetail.protocol"
/>
<tab-case-change-history v-else :source-id="caseDetail.id" />
</MsCard>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsTab from '@/components/pure/ms-tab/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import MsEnvironmentSelect from '@/components/business/ms-environment-select/index.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import executeButton from '@/views/api-test/components/executeButton.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import detailTab from '@/views/api-test/management/components/management/api/preview/detail.vue';
import createAndEditCaseDrawer from '@/views/api-test/management/components/management/case/createAndEditCaseDrawer.vue';
import DifferentDrawer from '@/views/api-test/management/components/management/case/differentDrawer.vue';
import TabCaseChangeHistory from '@/views/api-test/management/components/management/case/tabContent/tabCaseChangeHistory.vue';
import TabCaseDependency from '@/views/api-test/management/components/management/case/tabContent/tabCaseDependency.vue';
import TabCaseExecuteHistory from '@/views/api-test/management/components/management/case/tabContent/tabCaseExecuteHistory.vue';
import { getProtocolList, localExecuteApiDebug, stopExecute, stopLocalExecute } from '@/api/modules/api-test/common';
import { debugCase, deleteCase, getCaseDetail, runCase, toggleFollowCase } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { getGenerateId } from '@/utils';
import { ProtocolItem } from '@/models/apiTest/common';
import { RequestMethods, ScenarioStepType } from '@/enums/apiEnum';
import { defaultCaseParams, defaultResponse } from '@/views/api-test/components/config';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
const { openModal } = useModal();
const appStore = useAppStore();
const { t } = useI18n();
const route = useRoute();
const router = useRouter();
const activeKey = ref('detail');
const { copy, isSupported } = useClipboard({ legacy: true });
const caseDetail = ref<RequestParam>(cloneDeep(defaultCaseParams));
const environmentIdByDrawer = ref('');
const followLoading = ref(false);
async function follow() {
try {
followLoading.value = true;
await toggleFollowCase(caseDetail.value.id);
Message.success(caseDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
caseDetail.value.follow = !caseDetail.value.follow;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
followLoading.value = false;
}
}
const executeRef = ref<InstanceType<typeof executeButton>>();
const reportId = ref('');
const executeCase = ref<boolean>(false);
const websocket = ref<WebSocket>();
async function stopDebug() {
try {
if (caseDetail.value.frontendDebug) {
await stopLocalExecute(executeRef.value?.localExecuteUrl || '', reportId.value, ScenarioStepType.API_CASE);
} else {
await stopExecute(reportId.value, ScenarioStepType.API_CASE);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
websocket.value?.close();
caseDetail.value.executeLoading = false;
executeCase.value = false;
}
const temporaryResponseMap: Record<string, any> = {}; // websockettab
const executeHistoryRef = ref<InstanceType<typeof TabCaseExecuteHistory>>();
// websocket
function debugSocket(executeType?: 'localExec' | 'serverExec') {
websocket.value = getSocket(
reportId.value,
executeType === 'localExec' ? '/ws/debug' : '',
executeType === 'localExec' ? executeRef.value?.localExecuteUrl : ''
);
websocket.value.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
if (caseDetail.value.reportId === data.reportId) {
// tabtab
caseDetail.value.response = data.taskResult; //
caseDetail.value.executeLoading = false;
executeCase.value = false;
} else {
// tab
temporaryResponseMap[data.reportId] = data.taskResult;
}
} else if (data.msgType === 'EXEC_END') {
// websocket
websocket.value?.close();
caseDetail.value.executeLoading = false;
executeCase.value = false;
}
});
}
//
async function handleExecute(executeType?: 'localExec' | 'serverExec') {
try {
caseDetail.value.executeLoading = true;
caseDetail.value.response = cloneDeep(defaultResponse);
reportId.value = getGenerateId();
caseDetail.value.reportId = reportId.value; // ID
let res;
const params = {
id: caseDetail.value.id as string,
environmentId: appStore.currentEnvConfig?.id || '',
frontendDebug: executeType === 'localExec',
reportId: reportId.value,
apiDefinitionId: caseDetail.value.apiDefinitionId,
request: caseDetail.value.request,
projectId: caseDetail.value.projectId,
linkFileIds: caseDetail.value.linkFileIds,
uploadFileIds: caseDetail.value.uploadFileIds,
};
debugSocket(executeType); // websocket
if (executeType === 'serverExec') {
//
res = await runCase(params);
} else {
res = await debugCase(params);
}
if (executeType === 'localExec') {
await localExecuteApiDebug(executeRef.value?.localExecuteUrl as string, res);
}
//
executeHistoryRef.value?.loadExecuteList();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
caseDetail.value.executeLoading = false;
executeCase.value = false;
}
}
const createAndEditCaseDrawerRef = ref<InstanceType<typeof createAndEditCaseDrawer>>();
function editCase() {
createAndEditCaseDrawerRef.value?.open(
caseDetail.value.apiDefinitionId,
caseDetail.value as unknown as RequestParam,
false
);
}
function handleDelete() {
openModal({
type: 'error',
title: t('apiTestManagement.deleteApiTipTitle', { name: caseDetail.value.name }),
content: t('case.deleteCaseTip'),
okText: t('common.confirmDelete'),
cancelText: t('common.cancel'),
okButtonProps: {
status: 'danger',
},
maskClosable: false,
onBeforeOk: async () => {
try {
await deleteCase(caseDetail.value.id as string);
// TODO
router.back();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
hideCancel: false,
});
}
provide('defaultCaseParams', readonly(defaultCaseParams));
function share() {
if (isSupported) {
const url = window.location.href;
const dIdParam = `&cId=${caseDetail.value.id}`;
const copyUrl = url.includes('cId') ? url.split('&cId')[0] : url;
copy(`${copyUrl}${dIdParam}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));
}
}
async function getCaseDetailInfo(id: string) {
try {
const res = await getCaseDetail(id);
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
}
caseDetail.value = {
...cloneDeep(defaultCaseParams as RequestParam),
...({
...res.request,
...res,
url: res.path,
...parseRequestBodyResult,
} as Partial<TabItem>),
};
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const description = computed(() => [
{
key: 'type',
locale: 'apiTestManagement.apiType',
value: caseDetail.value.method,
},
{
key: 'name',
locale: 'case.belongingApi',
value: `[${caseDetail.value.num}] ${caseDetail.value.name}`,
},
{
key: 'path',
locale: 'apiTestManagement.path',
value: caseDetail.value.url || caseDetail.value.path,
},
{
key: 'tags',
locale: 'common.tag',
value: caseDetail.value.tags,
},
]);
const tabList = ref([
{
value: 'detail',
label: t('case.detail'),
},
{
value: 'reference',
label: t('apiTestManagement.reference'),
},
{
value: 'executeHistory',
label: t('apiTestManagement.executeHistory'),
},
{
value: 'changeHistory',
label: t('apiTestManagement.changeHistory'),
},
]);
const protocols = ref<ProtocolItem[]>([]);
provide('protocols', readonly(protocols));
async function initProtocolList() {
try {
protocols.value = await getProtocolList(appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
// id
const activeDefinedId = ref<string>('');
// id
const activeApiCaseId = ref<string>('');
const showDifferentDrawer = ref<boolean>(false);
// diff
function showDiffDrawer() {
activeApiCaseId.value = caseDetail.value.id as string;
activeDefinedId.value = caseDetail.value.apiDefinitionId;
showDifferentDrawer.value = true;
}
function closeDifferent() {
showDifferentDrawer.value = false;
activeApiCaseId.value = '';
activeDefinedId.value = '';
}
const isPriorityLocalExec = computed(() => executeRef.value?.isPriorityLocalExec ?? false);
onBeforeMount(() => {
initProtocolList();
const caseId = route.query.id;
getCaseDetailInfo(caseId as string);
});
</script>
<style scoped lang="less">
:deep(.ms-detail-card-desc) {
gap: 16px;
flex-wrap: nowrap !important;
& > div:nth-of-type(n) {
width: auto;
max-width: 30%;
}
}
</style>

View File

@ -66,6 +66,13 @@
:is-priority-local-exec="isPriorityLocalExec" :is-priority-local-exec="isPriorityLocalExec"
is-case is-case
@execute="handleExecute" @execute="handleExecute"
@show-diff="showDiffDrawer"
/>
<DifferentDrawer
v-model:visible="showDifferentDrawer"
:active-api-case-id="activeApiCaseId"
:active-defined-id="activeDefinedId"
@close="closeDifferent"
/> />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]"> <a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]">
@ -104,6 +111,7 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import executeButton from '@/views/api-test/components/executeButton.vue'; import executeButton from '@/views/api-test/components/executeButton.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import DifferentDrawer from '@/views/api-test/management/components/management/case/differentDrawer.vue';
import TabCaseChangeHistory from '@/views/api-test/management/components/management/case/tabContent/tabCaseChangeHistory.vue'; import TabCaseChangeHistory from '@/views/api-test/management/components/management/case/tabContent/tabCaseChangeHistory.vue';
import TabCaseDependency from '@/views/api-test/management/components/management/case/tabContent/tabCaseDependency.vue'; import TabCaseDependency from '@/views/api-test/management/components/management/case/tabContent/tabCaseDependency.vue';
import TabCaseExecuteHistory from '@/views/api-test/management/components/management/case/tabContent/tabCaseExecuteHistory.vue'; import TabCaseExecuteHistory from '@/views/api-test/management/components/management/case/tabContent/tabCaseExecuteHistory.vue';
@ -315,6 +323,25 @@
executeCase.value = false; executeCase.value = false;
} }
// id
const activeDefinedId = ref<string>('');
// id
const activeApiCaseId = ref<string>('');
const showDifferentDrawer = ref<boolean>(false);
// diff
function showDiffDrawer() {
activeApiCaseId.value = caseDetail.value.id as string;
activeDefinedId.value = caseDetail.value.apiDefinitionId;
showDifferentDrawer.value = true;
}
function closeDifferent() {
showDifferentDrawer.value = false;
activeApiCaseId.value = '';
activeDefinedId.value = '';
}
watch( watch(
() => props.detail, () => props.detail,
() => { () => {

View File

@ -51,7 +51,6 @@
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)"> <MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">
{{ record.num }} {{ record.num }}
</MsButton> </MsButton>
<!-- TODO 后台缺少字段 等待联调 -->
<a-tooltip v-if="record.apiChange" class="ms-tooltip-white"> <a-tooltip v-if="record.apiChange" class="ms-tooltip-white">
<!-- 接口参数发生变更提示 --> <!-- 接口参数发生变更提示 -->
<MsIcon type="icon-icon_warning_colorful" size="16" /> <MsIcon type="icon-icon_warning_colorful" size="16" />
@ -278,7 +277,9 @@
ref="createAndEditCaseDrawerRef" ref="createAndEditCaseDrawerRef"
:api-detail="apiDetail" :api-detail="apiDetail"
@load-case="loadCaseListAndResetSelector()" @load-case="loadCaseListAndResetSelector()"
@show-diff="showDifferences"
/> />
<!-- TODO 之后要去掉 使用页面代替抽屉 -->
<caseDetailDrawer <caseDetailDrawer
v-model:visible="caseDetailDrawerVisible" v-model:visible="caseDetailDrawerVisible"
v-model:execute-case="caseExecute" v-model:execute-case="caseExecute"
@ -303,8 +304,6 @@
<!-- diff对比抽屉 --> <!-- diff对比抽屉 -->
<DifferentDrawer <DifferentDrawer
v-model:visible="showDifferentDrawer" v-model:visible="showDifferentDrawer"
:detail="caseDetail as RequestParam"
:api-detail="apiDetail as RequestParam"
:active-api-case-id="activeApiCaseId" :active-api-case-id="activeApiCaseId"
:active-defined-id="activeDefinedId" :active-defined-id="activeDefinedId"
@close="closeDifferent" @close="closeDifferent"
@ -312,6 +311,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { useRoute, useRouter } from 'vue-router';
import { FormInstance, Message } from '@arco-design/web-vue'; import { FormInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es'; import { cloneDeep } from 'lodash-es';
@ -357,6 +357,7 @@
import { DragSortParams } from '@/models/common'; import { DragSortParams } from '@/models/common';
import { RequestCaseStatus } from '@/enums/apiEnum'; import { RequestCaseStatus } from '@/enums/apiEnum';
import { ReportEnum, ReportStatus } from '@/enums/reportEnum'; import { ReportEnum, ReportStatus } from '@/enums/reportEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { TableKeyEnum } from '@/enums/tableEnum'; import { TableKeyEnum } from '@/enums/tableEnum';
import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum'; import { FilterRemoteMethodsEnum, FilterSlotNameEnum } from '@/enums/tableFilterEnum';
@ -382,6 +383,8 @@
const { t } = useI18n(); const { t } = useI18n();
const tableStore = useTableStore(); const tableStore = useTableStore();
const { openModal } = useModal(); const { openModal } = useModal();
const route = useRoute();
const router = useRouter();
const keyword = ref(''); const keyword = ref('');
@ -925,7 +928,13 @@
async function openCaseDetailDrawer(id: string) { async function openCaseDetailDrawer(id: string) {
await getCaseDetailInfo(id); await getCaseDetailInfo(id);
caseExecute.value = false; caseExecute.value = false;
caseDetailDrawerVisible.value = true; router.push({
name: ApiTestRouteEnum.API_TEST_MANAGEMENT_CASE_DETAIL,
query: {
...route.query,
id,
},
});
} }
async function openCaseDetailDrawerAndExecute(id: string) { async function openCaseDetailDrawerAndExecute(id: string) {
@ -972,7 +981,6 @@
activeDefinedId.value = record.apiDefinitionId; activeDefinedId.value = record.apiDefinitionId;
showDifferentDrawer.value = true; showDifferentDrawer.value = true;
} }
// TODO
function closeDifferent() { function closeDifferent() {
showDifferentDrawer.value = false; showDifferentDrawer.value = false;
activeApiCaseId.value = ''; activeApiCaseId.value = '';

View File

@ -69,7 +69,24 @@
<MsTagsInput v-model:model-value="detailForm.tags" :max-tag-count="1" /> <MsTagsInput v-model:model-value="detailForm.tags" :max-tag-count="1" />
</a-form-item> </a-form-item>
</a-form> </a-form>
<div class="px-[16px] font-medium">{{ t('apiTestManagement.requestParams') }}</div> <div class="flex items-center justify-between">
<div class="px-[16px] font-medium">{{ t('apiTestManagement.requestParams') }}</div>
<!-- 与定义不一致 TODO 等待联调 -->
<MsTag
v-if="detailForm.inconsistentWithApi"
class="cursor-pointer"
type="warning"
theme="light"
:tooltip-disabled="true"
@click="showDiffDrawer"
>
<template #icon>
<MsIcon type="icon-icon_warning_colorful" size="16" />
</template>
<span class="ml-[8px]"> {{ t('case.definitionInconsistent') }}</span>
</MsTag>
</div>
<requestComposition <requestComposition
ref="requestCompositionRef" ref="requestCompositionRef"
v-model:request="detailForm" v-model:request="detailForm"
@ -94,6 +111,7 @@
import MsDetailCard, { type Description } from '@/components/pure/ms-detail-card/index.vue'; import MsDetailCard, { type Description } from '@/components/pure/ms-detail-card/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue'; import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue'; import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types'; import type { CaseLevel } from '@/components/business/ms-case-associate/types';
@ -129,6 +147,7 @@
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'loadCase', id?: string): void; (e: 'loadCase', id?: string): void;
(e: 'showDiff', apiDetailInfo: ApiCaseDetail): void;
}>(); }>();
const { t } = useI18n(); const { t } = useI18n();
@ -384,6 +403,11 @@
detailForm.value.executeLoading = false; detailForm.value.executeLoading = false;
} }
//
function showDiffDrawer() {
emit('showDiff', detailForm.value as unknown as ApiCaseDetail);
}
defineExpose({ defineExpose({
open, open,
}); });

View File

@ -32,22 +32,15 @@
</a-checkbox-group> </a-checkbox-group>
<a-divider direction="vertical" :margin="0" class="!mr-[8px]"></a-divider> <a-divider direction="vertical" :margin="0" class="!mr-[8px]"></a-divider>
<a-switch v-model:model-value="form.ignoreUpdate" size="small" /> <a-switch v-model:model-value="form.ignoreUpdate" size="small" />
<a-select <div class="ml-[8px]">{{ t('case.ignoreAllChange') }}</div>
v-model="form.ignoreUpdateType"
class="ml-[8px] w-[160px]"
:placeholder="t('caseManagement.featureCase.PleaseSelect')"
:disabled="!form.ignoreUpdate"
@change="changeIgnoreType"
>
<a-option v-for="item of ignoreList" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-option>
</a-select>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
<a-switch v-model:model-value="form.deleteParams" size="small" /> <a-switch v-model:model-value="form.deleteParams" size="small" />
<div class="ml-[8px] font-normal text-[var(--color-text-1)]">{{ t('case.deleteNotCorrespondValue') }}</div> <div class="ml-[8px] font-normal text-[var(--color-text-1)]">{{ t('case.deleteNotCorrespondValue') }}</div>
<a-divider direction="vertical" :margin="0" class="!ml-[8px]"></a-divider> <a-divider direction="vertical" :margin="0" class="!ml-[8px]"></a-divider>
<a-button class="mx-[12px]" type="secondary" @click="cancel">{{ t('common.cancel') }}</a-button> <a-button class="mx-[12px]" type="secondary" @click="cancel">{{ t('common.cancel') }}</a-button>
<a-button class="mr-[12px]" type="outline">
{{ t('case.ignoreAllChange') }}
</a-button>
<a-button type="primary" :loading="syncLoading" :disabled="!form.checkType.length" @click="confirmBatchSync"> <a-button type="primary" :loading="syncLoading" :disabled="!form.checkType.length" @click="confirmBatchSync">
{{ t('case.apiSyncChange') }} {{ t('case.apiSyncChange') }}
</a-button> </a-button>
@ -157,17 +150,6 @@
const form = ref({ ...initForm }); const form = ref({ ...initForm });
const ignoreList = ref([
{
value: 'THIS_TIME',
label: t('case.ignoreThisChange'),
},
{
value: 'ALL',
label: t('case.ignoreAllChange'),
},
]);
// //
function changeIgnoreType() {} function changeIgnoreType() {}

View File

@ -222,9 +222,15 @@ export default {
'case.NoticeApiScenarioCreator': 'Notify the founder of citing the use case scenario', 'case.NoticeApiScenarioCreator': 'Notify the founder of citing the use case scenario',
'case.apiAndCaseDiff': 'Interface vs. use case differences', 'case.apiAndCaseDiff': 'Interface vs. use case differences',
'case.ignoreThisChange': 'Ignore this change', 'case.ignoreThisChange': 'Ignore this change',
'case.ignoreAllChange': 'Ignore all changes', 'case.ignoreAllChange': 'Ignore each time difference',
'case.diffAdd': 'Add', 'case.diffAdd': 'Add',
'case.notSetData': 'No data has been set', 'case.notSetData': 'No data has been set',
'case.definitionInconsistent': 'Inconsistent with the definition',
'case.haveIgnoredTheChange': 'Have ignored the change',
'case.eachHasBeenIgnored': 'Each change difference has been ignored',
'case.apiCaseList': 'List',
'case.apiCaseDetail': 'Use case details',
'case.belongingApi': 'Belonging interface',
'case.saveContinueText': 'Save and Continue Creating', 'case.saveContinueText': 'Save and Continue Creating',
'case.detail.changeHistoryTip': 'case.detail.changeHistoryTip':
"View and compare historical changes. According to the administrator's settings, historical data will be automatically deleted", "View and compare historical changes. According to the administrator's settings, historical data will be automatically deleted",

View File

@ -209,9 +209,15 @@ export default {
'case.NoticeApiScenarioCreator': '通知引用该用例的场景创建人', 'case.NoticeApiScenarioCreator': '通知引用该用例的场景创建人',
'case.apiAndCaseDiff': '接口与用例差异对比', 'case.apiAndCaseDiff': '接口与用例差异对比',
'case.ignoreThisChange': '忽略本次变更差异', 'case.ignoreThisChange': '忽略本次变更差异',
'case.ignoreAllChange': '忽略全部变更差异', 'case.ignoreAllChange': '忽略每次变更差异',
'case.diffAdd': '新增', 'case.diffAdd': '新增',
'case.notSetData': '暂未设置数据', 'case.notSetData': '暂未设置数据',
'case.definitionInconsistent': '与定义不一致',
'case.haveIgnoredTheChange': '已忽略本次变更差异',
'case.eachHasBeenIgnored': '已忽略每次变更差异',
'case.apiCaseList': '列表',
'case.apiCaseDetail': '用例详情',
'case.belongingApi': '所属接口',
'case.saveContinueText': '保存并继续创建', 'case.saveContinueText': '保存并继续创建',
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除', 'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
'case.detail.noReminders': '不再提醒', 'case.detail.noReminders': '不再提醒',

View File

@ -115,7 +115,7 @@
const apiCaseText = `▪ 本次测试包含${apiCaseDetail.caseTotal}条接口测试用例,执行了${apiCaseDetail.hasExecutedCase}条,未执行${apiCaseDetail.pending}条,执行率为${apiCaseDetail.apiExecutedRate},通过用例${apiCaseDetail.success}条,通过率为${apiCaseDetail.successRate}。共发现缺陷 ${props.detail.apiBugCount} 个。<br>`; const apiCaseText = `▪ 本次测试包含${apiCaseDetail.caseTotal}条接口测试用例,执行了${apiCaseDetail.hasExecutedCase}条,未执行${apiCaseDetail.pending}条,执行率为${apiCaseDetail.apiExecutedRate},通过用例${apiCaseDetail.success}条,通过率为${apiCaseDetail.successRate}。共发现缺陷 ${props.detail.apiBugCount} 个。<br>`;
const apiCaseDesc = apiCaseDetail.caseTotal ? `${apiCaseText}` : ``; const apiCaseDesc = apiCaseDetail.caseTotal ? `${apiCaseText}` : ``;
const scenarioCaseText = `▪ 本次测试包含${apiScenarioDetail.caseTotal}条场景测试用例,执行了${apiScenarioDetail.hasExecutedCase}条,未执行${apiScenarioDetail.pending}条,执行率为${apiScenarioDetail.apiExecutedRate}%,通过用例${apiScenarioDetail.success}条,通过率为${apiScenarioDetail.successRate}。共发现缺陷${props.detail.scenarioBugCount}`; const scenarioCaseText = `▪ 本次测试包含${apiScenarioDetail.caseTotal}条场景测试用例,执行了${apiScenarioDetail.hasExecutedCase}条,未执行${apiScenarioDetail.pending}条,执行率为${apiScenarioDetail.apiExecutedRate},通过用例${apiScenarioDetail.success}条,通过率为${apiScenarioDetail.successRate}。共发现缺陷${props.detail.scenarioBugCount}`;
const scenarioCaseDesc = apiScenarioDetail.caseTotal ? `${scenarioCaseText}` : ``; const scenarioCaseDesc = apiScenarioDetail.caseTotal ? `${scenarioCaseText}` : ``;
const isPass = Number(allSuccessRate) >= Number(props.detail.passThreshold); const isPass = Number(allSuccessRate) >= Number(props.detail.passThreshold);