feat(接口管理): 用例执行&用例详情删除&部分细节修改

This commit is contained in:
teukkk 2024-03-19 17:57:32 +08:00 committed by Craftsman
parent 7791b800a4
commit efa5387cf1
14 changed files with 490 additions and 167 deletions

View File

@ -53,6 +53,7 @@ import {
RecoverDefinitionUrl, RecoverDefinitionUrl,
RecoverOperationHistoryUrl, RecoverOperationHistoryUrl,
RecycleCasePageUrl, RecycleCasePageUrl,
RunCaseUrl,
SaveOperationHistoryUrl, SaveOperationHistoryUrl,
SortCaseUrl, SortCaseUrl,
SortDefinitionUrl, SortDefinitionUrl,
@ -409,6 +410,11 @@ export function toggleFollowCase(id: string | number) {
return MSR.get({ url: ToggleFollowCaseUrl, params: id }); return MSR.get({ url: ToggleFollowCaseUrl, params: id });
} }
// 用例执行,传请求详情执行
export function runCase(data: ExecuteRequestParams) {
return MSR.post({ url: RunCaseUrl, data });
}
/** /**
* *
*/ */

View File

@ -74,6 +74,7 @@ export const GetExecuteHistoryUrl = '/api/case/execute/page'; // 获取用的执
export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系 export const GetDependencyUrl = '/api/case/get-reference'; // 获取用例的依赖关系
export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系 export const GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系
export const ToggleFollowCaseUrl = '/api/case/follow'; // 接口定义-关注/取消关注 export const ToggleFollowCaseUrl = '/api/case/follow'; // 接口定义-关注/取消关注
export const RunCaseUrl = '/api/case/run'; // 执行接口用例
export const GetCaseReportByIdUrl = '/api/report/case/get/'; // 接口用例报告获取 export const GetCaseReportByIdUrl = '/api/report/case/get/'; // 接口用例报告获取
export const GetCaseReportDetailUrl = '/api/report/case/get/detail/'; // 接口用例报告获取 export const GetCaseReportDetailUrl = '/api/report/case/get/detail/'; // 接口用例报告获取

View File

@ -301,6 +301,7 @@ export interface ApiCaseDetail extends ExecuteRequestParams {
priority: string; priority: string;
num: number; num: number;
status: string; status: string;
protocol: string;
lastReportStatus: string; lastReportStatus: string;
lastReportId: string; lastReportId: string;
projectId: string; projectId: string;

View File

@ -587,6 +587,7 @@
request: RequestParam; // request: RequestParam; //
moduleTree?: ModuleTreeNode[]; // moduleTree?: ModuleTreeNode[]; //
isCase?: boolean; // isCase?: boolean; //
apiDetail?: RequestParam; //
detailLoading?: boolean; // detailLoading?: boolean; //
isDefinition?: boolean; // isDefinition?: boolean; //
hideResponseLayoutSwitch?: boolean; // hideResponseLayoutSwitch?: boolean; //
@ -677,10 +678,32 @@
label: t('apiTestDebug.setting'), label: t('apiTestDebug.setting'),
}, },
]; ];
const restNumApi = computed(
() =>
filterKeyValParams(props.apiDetail?.rest ?? props.apiDetail?.request.rest, defaultRequestParamsItem).validParams
.length
);
const queryNumApi = computed(
() =>
filterKeyValParams(props.apiDetail?.query ?? props.apiDetail?.request.query, defaultRequestParamsItem).validParams
.length
);
const bodyTabBadgeApi = computed(() =>
props.apiDetail?.request.body?.bodyType !== RequestBodyFormat.NONE ? '1' : ''
);
// tab // tab
const contentTabList = computed(() => { const contentTabList = computed(() => {
// HTTP tabs // HTTP tabs
if (isHttpProtocol.value) { if (isHttpProtocol.value) {
if (props.isCase) {
// BODY/QUERY/RESTtab
return httpContentTabList.filter(
(e) =>
!(!restNumApi.value && e.value === RequestComposition.REST) &&
!(!queryNumApi.value && e.value === RequestComposition.QUERY) &&
!(!bodyTabBadgeApi.value?.length && e.value === RequestComposition.BODY)
);
}
if (props.isDefinition) { if (props.isDefinition) {
// //
return requestVModel.value.mode === 'debug' return requestVModel.value.mode === 'debug'
@ -1214,6 +1237,14 @@
} else if (protocolOptions.value.length === 0) { } else if (protocolOptions.value.length === 0) {
await initProtocolList(); await initProtocolList();
} }
if (
props.isCase &&
requestVModel.value.protocol === 'HTTP' &&
(restNumApi.value || queryNumApi.value || bodyTabBadgeApi.value?.length)
) {
// BODY/QUERY/RESTtabtab
requestVModel.value.activeTab = contentTabList.value[1].value;
}
if (props.request.isExecute && !requestVModel.value.executeLoading) { if (props.request.isExecute && !requestVModel.value.executeLoading) {
// //
execute(isPriorityLocalExec.value ? 'localExec' : 'serverExec'); execute(isPriorityLocalExec.value ? 'localExec' : 'serverExec');

View File

@ -54,7 +54,7 @@
<caseTable <caseTable
:is-api="true" :is-api="true"
:active-module="props.activeModule" :active-module="props.activeModule"
:protocol="props.protocol" :protocol="activeApiTab.protocol"
:api-detail="activeApiTab" :api-detail="activeApiTab"
/> />
</a-tab-pane> </a-tab-pane>

View File

@ -234,6 +234,7 @@
</div> </div>
</div> </div>
</a-collapse-item> </a-collapse-item>
<a-spin :loading="previewDetail.executeLoading" class="w-full">
<a-collapse-item <a-collapse-item
v-if=" v-if="
previewDetail.responseDefinition && previewDetail.responseDefinition &&
@ -313,6 +314,7 @@
<MsFormTable :columns="responseHeaderColumns" :data="activeResponse?.headers || []" :selectable="false" /> <MsFormTable :columns="responseHeaderColumns" :data="activeResponse?.headers || []" :selectable="false" />
</div> </div>
</a-collapse-item> </a-collapse-item>
</a-spin>
</a-collapse> </a-collapse>
</template> </template>

View File

@ -2,14 +2,13 @@
<div class="h-full w-full overflow-hidden"> <div class="h-full w-full overflow-hidden">
<a-tabs v-model:active-key="activeKey" class="h-full px-[16px]" animation lazy-load> <a-tabs v-model:active-key="activeKey" class="h-full px-[16px]" animation lazy-load>
<template #extra> <template #extra>
<div v-show="!props.isDrawer" class="flex gap-[12px]"> <div class="flex gap-[12px]">
<a-button type="primary"> <environmentSelect v-if="props.isDrawer" ref="environmentSelectRef" />
{{ t('apiTestManagement.execute') }} <execute v-model:detail="caseDetail" :environment-id="environmentId as string" />
</a-button>
<a-dropdown position="br" :hide-on-select="false" @select="handleSelect"> <a-dropdown position="br" :hide-on-select="false" @select="handleSelect">
<a-button>{{ t('common.operation') }}</a-button> <a-button v-if="!props.isDrawer">{{ t('common.operation') }}</a-button>
<template #content> <template #content>
<a-doption value="edit"> <a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="edit">
<MsIcon type="icon-icon_edit_outlined" /> <MsIcon type="icon-icon_edit_outlined" />
{{ t('common.edit') }} {{ t('common.edit') }}
</a-doption> </a-doption>
@ -17,15 +16,19 @@
<MsIcon type="icon-icon_share1" /> <MsIcon type="icon-icon_share1" />
{{ t('common.share') }} {{ t('common.share') }}
</a-doption> </a-doption>
<a-doption value="fork"> <a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="fork">
<MsIcon <MsIcon
:type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'" :type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : ''}`" :class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : ''}`"
/> />
{{ t('common.fork') }} {{ t('common.fork') }}
</a-doption> </a-doption>
<a-divider margin="4px" /> <a-divider v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']" margin="4px" />
<a-doption class="error-6 text-[rgb(var(--danger-6))]"> <a-doption
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
value="delete"
class="error-6 text-[rgb(var(--danger-6))]"
>
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" /> <MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
{{ t('common.delete') }} {{ t('common.delete') }}
</a-doption> </a-doption>
@ -35,11 +38,6 @@
</template> </template>
<a-tab-pane key="detail" :title="t('apiTestManagement.detail')" class="px-[18px] py-[16px]"> <a-tab-pane key="detail" :title="t('apiTestManagement.detail')" class="px-[18px] py-[16px]">
<MsDetailCard :title="`【${caseDetail.num}】${caseDetail.name}`" :description="description" class="mb-[8px]"> <MsDetailCard :title="`【${caseDetail.num}】${caseDetail.name}`" :description="description" class="mb-[8px]">
<template #titleAppend>
<a-button v-show="props.isDrawer" type="primary" size="mini">
{{ t('apiTestManagement.execute') }}
</a-button>
</template>
<template #type="{ value }"> <template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag /> <apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template> </template>
@ -47,13 +45,13 @@
<caseLevel :case-level="value as CaseLevel" /> <caseLevel :case-level="value as CaseLevel" />
</template> </template>
</MsDetailCard> </MsDetailCard>
<detailTab :detail="caseDetail" :protocols="protocols" is-case /> <detailTab :detail="caseDetail" :protocols="protocols as ProtocolItem[]" is-case />
</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]">
<tab-case-dependency :source-id="caseDetail.id" /> <tab-case-dependency :source-id="caseDetail.id" />
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="executeHistory" :title="t('apiTestManagement.executeHistory')" class="px-[18px] py-[16px]"> <a-tab-pane key="executeHistory" :title="t('apiTestManagement.executeHistory')" class="px-[18px] py-[16px]">
<tab-case-execute-history :source-id="caseDetail.id" module-type="API_REPORT" :protocol="props.protocol" /> <tab-case-execute-history :source-id="caseDetail.id" module-type="API_REPORT" :protocol="caseDetail.protocol" />
</a-tab-pane> </a-tab-pane>
<!-- <a-tab-pane key="dependencies" :title="t('apiTestManagement.dependencies')" class="px-[18px] py-[16px]"> <!-- <a-tab-pane key="dependencies" :title="t('apiTestManagement.dependencies')" class="px-[18px] py-[16px]">
</a-tab-pane> --> </a-tab-pane> -->
@ -62,7 +60,7 @@
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
</div> </div>
<createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" :protocol="props.protocol" v-bind="$attrs" /> <createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" v-bind="$attrs" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -74,33 +72,41 @@
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue'; import MsDetailCard from '@/components/pure/ms-detail-card/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';
import environmentSelect from '../../environmentSelect.vue';
import detailTab from '../api/preview/detail.vue'; import detailTab from '../api/preview/detail.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue'; import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import execute from './execute.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import { RequestParam } from '@/views/api-test/components/requestComposition/index.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';
import { getProtocolList } from '@/api/modules/api-test/common'; import { deleteCase, toggleFollowCase } from '@/api/modules/api-test/management';
import { toggleFollowCase } from '@/api/modules/api-test/management'; import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { ProtocolItem } from '@/models/apiTest/common'; import { ProtocolItem } from '@/models/apiTest/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { RequestMethods } from '@/enums/apiEnum'; import { RequestMethods } from '@/enums/apiEnum';
const props = defineProps<{ const props = defineProps<{
isDrawer?: boolean; // isDrawer?: boolean; //
detail: RequestParam; detail: RequestParam;
protocol: string;
}>(); }>();
const emit = defineEmits(['updateFollow']); const emit = defineEmits<{
(e: 'updateFollow'): void;
(e: 'deleteCase', id: string): void;
}>();
const { copy, isSupported } = useClipboard(); const { copy, isSupported } = useClipboard();
const { t } = useI18n(); const { t } = useI18n();
const appStore = useAppStore(); const { openModal } = useModal();
const caseDetail = ref<RequestParam>(cloneDeep(props.detail)); // props.detailprops.detail
watchEffect(() => {
caseDetail.value = cloneDeep(props.detail); // props.detailprops.detail
});
const caseDetail = computed<RequestParam>(() => cloneDeep(props.detail)); // props.detailprops.detail
const activeKey = ref('detail'); const activeKey = ref('detail');
const description = computed(() => [ const description = computed(() => [
@ -126,20 +132,6 @@
}, },
]); ]);
const protocols = ref<ProtocolItem[]>([]);
async function initProtocolList() {
try {
protocols.value = await getProtocolList(appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
initProtocolList();
});
const followLoading = ref(false); const followLoading = ref(false);
async function follow() { async function follow() {
try { try {
@ -157,7 +149,7 @@
function share() { function share() {
if (isSupported) { if (isSupported) {
copy(`${window.location.href}&dId=${caseDetail.value.id}`); copy(`${window.location.href}&cId=${caseDetail.value.id}`);
Message.success(t('apiTestManagement.shareUrlCopied')); Message.success(t('apiTestManagement.shareUrlCopied'));
} else { } else {
Message.error(t('common.copyNotSupport')); Message.error(t('common.copyNotSupport'));
@ -169,6 +161,30 @@
createAndEditCaseDrawerRef.value?.open(caseDetail.value.apiDefinitionId, caseDetail.value, false); createAndEditCaseDrawerRef.value?.open(caseDetail.value.apiDefinitionId, caseDetail.value, 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);
emit('deleteCase', caseDetail.value.id as string);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
hideCancel: false,
});
}
function handleSelect(val: string | number | Record<string, any> | undefined) { function handleSelect(val: string | number | Record<string, any> | undefined) {
switch (val) { switch (val) {
case 'edit': case 'edit':
@ -180,15 +196,28 @@
case 'fork': case 'fork':
follow(); follow();
break; break;
case 'delete':
handleDelete();
break;
default: default:
break; break;
} }
} }
const protocols = inject<Ref<ProtocolItem[]>>('protocols');
const environmentSelectRef = ref<InstanceType<typeof environmentSelect>>();
const currentEnvConfigByDrawer = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig);
const currentEnvConfigByInject = inject<Ref<EnvConfig>>('currentEnvConfig');
const environmentId = computed(() =>
props.isDrawer ? currentEnvConfigByDrawer.value?.id : currentEnvConfigByInject?.value?.id
);
defineExpose({ defineExpose({
editCase, editCase,
share, share,
follow, follow,
handleDelete,
}); });
</script> </script>
@ -196,6 +225,9 @@
:deep(.arco-tabs-nav) { :deep(.arco-tabs-nav) {
border-bottom: 1px solid var(--color-text-n8); border-bottom: 1px solid var(--color-text-n8);
} }
:deep(.arco-tabs-nav-extra) {
line-height: 32px;
}
:deep(.arco-tabs-content) { :deep(.arco-tabs-content) {
@apply pt-0; @apply pt-0;
.arco-tabs-content-item { .arco-tabs-content-item {

View File

@ -7,9 +7,6 @@
:footer="false" :footer="false"
no-content-padding no-content-padding
> >
<template #headerLeft>
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
</template>
<template #tbutton> <template #tbutton>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<MsButton <MsButton
@ -39,7 +36,7 @@
{{ t('common.fork') }} {{ t('common.fork') }}
</MsButton> </MsButton>
<MsButton type="icon" status="secondary"> <MsButton type="icon" status="secondary">
<a-dropdown position="br"> <a-dropdown position="br" @select="handleSelect">
<div> <div>
<icon-more class="mr-[8px]" /> <icon-more class="mr-[8px]" />
<span> {{ t('common.more') }}</span> <span> {{ t('common.more') }}</span>
@ -47,6 +44,7 @@
<template #content> <template #content>
<a-doption <a-doption
v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']" v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']"
value="delete"
class="error-6 text-[rgb(var(--danger-6))]" class="error-6 text-[rgb(var(--danger-6))]"
> >
<MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" /> <MsIcon type="icon-icon_delete-trash_outlined" class="text-[rgb(var(--danger-6))]" />
@ -57,14 +55,7 @@
</MsButton> </MsButton>
</div> </div>
</template> </template>
<caseDetail <caseDetail ref="caseDerailRef" is-drawer :detail="props.detail" :api-detail="props.apiDetail" v-bind="$attrs" />
ref="caseDerailRef"
is-drawer
:detail="props.detail"
:protocol="props.protocol"
:api-detail="props.apiDetail"
v-bind="$attrs"
/>
</MsDrawer> </MsDrawer>
</template> </template>
@ -72,7 +63,6 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import environmentSelect from '../../environmentSelect.vue';
import caseDetail from './caseDetail.vue'; import caseDetail from './caseDetail.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
@ -81,7 +71,6 @@
const props = defineProps<{ const props = defineProps<{
detail: RequestParam; detail: RequestParam;
protocol: string;
apiDetail: RequestParam; apiDetail: RequestParam;
}>(); }>();
@ -91,6 +80,16 @@
required: true, required: true,
}); });
const caseDerailRef = ref<InstanceType<typeof caseDetail>>(); const caseDerailRef = ref<InstanceType<typeof caseDetail>>();
function handleSelect(val: string | number | Record<string, any> | undefined) {
switch (val) {
case 'delete':
caseDerailRef.value?.handleDelete();
break;
default:
break;
}
}
</script> </script>
<style scoped lang="less"> <style scoped lang="less">

View File

@ -43,6 +43,7 @@
</template> </template>
<template #caseLevel="{ record }"> <template #caseLevel="{ record }">
<a-select <a-select
v-if="hasAnyPermission(['PROJECT_API_DEFINITION_CASE:READ+UPDATE'])"
v-model:model-value="record.priority" v-model:model-value="record.priority"
:placeholder="t('common.pleaseSelect')" :placeholder="t('common.pleaseSelect')"
class="param-input w-full" class="param-input w-full"
@ -56,6 +57,7 @@
<caseLevel :case-level="item.text" /> <caseLevel :case-level="item.text" />
</a-option> </a-option>
</a-select> </a-select>
<span v-else class="text-[var(--color-text-2)]"> <caseLevel :case-level="record.priority" /></span>
</template> </template>
<template #caseLevelFilter="{ columnConfig }"> <template #caseLevelFilter="{ columnConfig }">
<a-trigger v-model:popup-visible="caseFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden"> <a-trigger v-model:popup-visible="caseFilterVisible" trigger="click" @popup-visible-change="handleFilterHidden">
@ -78,6 +80,7 @@
</template> </template>
<template #status="{ record }"> <template #status="{ record }">
<a-select <a-select
v-if="hasAnyPermission(['PROJECT_API_DEFINITION_CASE:READ+UPDATE'])"
v-model:model-value="record.status" v-model:model-value="record.status"
:placeholder="t('common.pleaseSelect')" :placeholder="t('common.pleaseSelect')"
class="param-input w-full" class="param-input w-full"
@ -91,6 +94,7 @@
<apiStatus :status="item" size="small" /> <apiStatus :status="item" size="small" />
</a-option> </a-option>
</a-select> </a-select>
<apiStatus v-else :status="record.status" size="small" />
</template> </template>
<template #statusFilter="{ columnConfig }"> <template #statusFilter="{ columnConfig }">
<a-trigger <a-trigger
@ -153,12 +157,26 @@
</a-tooltip> </a-tooltip>
</div> </div>
</template> </template>
<template #action="{ record }"> <template #operation="{ record }">
<MsButton type="text" class="!mr-0" @click="onExecute(record.id)"> <MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
type="text"
class="!mr-0"
@click="onExecute(record.id)"
>
{{ t('apiTestManagement.execute') }} {{ t('apiTestManagement.execute') }}
</MsButton> </MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider
<MsButton type="text" class="!mr-0" @click="copyCase(record)"> v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
direction="vertical"
:margin="8"
></a-divider>
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+ADD']"
type="text"
class="!mr-0"
@click="copyCase(record)"
>
{{ t('common.copy') }} {{ t('common.copy') }}
</MsButton> </MsButton>
<a-divider direction="vertical" :margin="8"></a-divider> <a-divider direction="vertical" :margin="8"></a-divider>
@ -239,17 +257,16 @@
</a-modal> </a-modal>
<createAndEditCaseDrawer <createAndEditCaseDrawer
ref="createAndEditCaseDrawerRef" ref="createAndEditCaseDrawerRef"
:protocol="props.protocol"
:api-detail="apiDetail" :api-detail="apiDetail"
@load-case="loadCaseListAndResetSelector()" @load-case="loadCaseListAndResetSelector()"
/> />
<caseDetailDrawer <caseDetailDrawer
v-model:visible="caseDetailDrawerVisible" v-model:visible="caseDetailDrawerVisible"
:detail="caseDetail as RequestParam" :detail="caseDetail as RequestParam"
:protocol="props.protocol"
:api-detail="apiDetail as RequestParam" :api-detail="apiDetail as RequestParam"
@update-follow="caseDetail.follow = !caseDetail.follow" @update-follow="caseDetail.follow = !caseDetail.follow"
@load-case="(id: string) => loadCase(id)" @load-case="(id: string) => loadCase(id)"
@delete-case="deleteCaseByDetail"
/> />
<a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480"> <a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
<template #title> <template #title>
@ -381,6 +398,7 @@
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useTableStore from '@/hooks/useTableStore'; import useTableStore from '@/hooks/useTableStore';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
import { ApiCaseDetail, Environment } from '@/models/apiTest/management'; import { ApiCaseDetail, Environment } from '@/models/apiTest/management';
import { DragSortParams } from '@/models/common'; import { DragSortParams } from '@/models/common';
@ -410,6 +428,13 @@
const keyword = ref(''); const keyword = ref('');
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree'); const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
const hasOperationPermission = computed(() =>
hasAnyPermission([
'PROJECT_API_DEFINITION_CASE:READ+DELETE',
'PROJECT_API_DEFINITION_CASE:READ+ADD',
'PROJECT_API_DEFINITION_CASE:READ+EXECUTE',
])
);
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'ID', title: 'ID',
@ -529,14 +554,13 @@
width: 180, width: 180,
}, },
{ {
title: 'common.operation', title: hasOperationPermission.value ? 'common.operation' : '',
slotName: 'action', slotName: 'operation',
dataIndex: 'operation', dataIndex: 'operation',
fixed: 'right', fixed: 'right',
width: 150, width: hasOperationPermission.value ? 150 : 50,
}, },
]; ];
await tableStore.initColumn(TableKeyEnum.API_TEST_MANAGEMENT_CASE, columns, 'drawer');
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getCasePage, { const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getCasePage, {
columns, columns,
scroll: { x: '100%' }, scroll: { x: '100%' },
@ -666,6 +690,7 @@
watch( watch(
() => props.protocol, () => props.protocol,
() => { () => {
if (props.isApi) return;
loadCaseListAndResetSelector(); loadCaseListAndResetSelector();
} }
); );
@ -980,16 +1005,15 @@
async function getCaseDetailInfo(id: string) { async function getCaseDetailInfo(id: string) {
try { try {
const res = await getCaseDetail(id); const res = await getCaseDetail(id);
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id ; let parseRequestBodyResult;
// if (res.protocol === 'HTTP') { // TODO: protocol if (res.protocol === 'HTTP') {
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
// } }
caseDetail.value = { caseDetail.value = {
...cloneDeep(defaultCaseParams as RequestParam), ...cloneDeep(defaultCaseParams as RequestParam),
...({ ...({
...res.request, ...res.request,
...res, ...res,
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response
url: res.path, url: res.path,
...parseRequestBodyResult, ...parseRequestBodyResult,
} as Partial<TabItem>), } as Partial<TabItem>),
@ -1004,11 +1028,22 @@
caseDetailDrawerVisible.value = true; caseDetailDrawerVisible.value = true;
} }
function deleteCaseByDetail() {
caseDetailDrawerVisible.value = false;
loadCaseList();
}
// api // api
async function loadCase(id: string) { function loadCase(id: string) {
getCaseDetailInfo(id); getCaseDetailInfo(id);
loadCaseList(); loadCaseList();
} }
defineExpose({
loadCaseList,
});
await tableStore.initColumn(TableKeyEnum.API_TEST_MANAGEMENT_CASE, columns, 'drawer');
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -4,6 +4,7 @@
:title="isEdit ? t('case.updateCase') : t('case.createCase')" :title="isEdit ? t('case.updateCase') : t('case.createCase')"
:width="894" :width="894"
no-content-padding no-content-padding
unmount-on-close
:ok-text="isEdit ? 'common.update' : 'common.create'" :ok-text="isEdit ? 'common.update' : 'common.create'"
:ok-loading="drawerLoading" :ok-loading="drawerLoading"
:save-continue-text="t('case.saveContinueText')" :save-continue-text="t('case.saveContinueText')"
@ -12,9 +13,6 @@
@continue="handleDrawerConfirm(true)" @continue="handleDrawerConfirm(true)"
@cancel="handleSaveCaseCancel" @cancel="handleSaveCaseCancel"
> >
<template #headerLeft>
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
</template>
<div class="flex h-full flex-col overflow-hidden"> <div class="flex h-full flex-col overflow-hidden">
<div class="px-[16px] pt-[16px]"> <div class="px-[16px] pt-[16px]">
<MsDetailCard <MsDetailCard
@ -36,9 +34,12 @@
:max-length="255" :max-length="255"
show-word-limit show-word-limit
/> />
<a-button type="primary"> <environmentSelect ref="environmentSelectRef" />
{{ t('apiTestManagement.execute') }} <execute
</a-button> v-model:detail="detailForm"
:environment-id="currentEnvConfig?.id as string"
:request="requestCompositionRef?.makeRequestParams"
/>
</div> </div>
</a-form-item> </a-form-item>
<div class="flex gap-[16px]"> <div class="flex gap-[16px]">
@ -74,6 +75,7 @@
ref="requestCompositionRef" ref="requestCompositionRef"
v-model:request="detailForm" v-model:request="detailForm"
:is-case="true" :is-case="true"
:api-detail="apiDetailInfo as RequestParam"
hide-response-layout-switch hide-response-layout-switch
:upload-temp-file-api="uploadTempFileCase" :upload-temp-file-api="uploadTempFileCase"
:file-save-as-source-id="detailForm.id" :file-save-as-source-id="detailForm.id"
@ -97,6 +99,7 @@
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';
import environmentSelect from '../../environmentSelect.vue'; import environmentSelect from '../../environmentSelect.vue';
import execute from './execute.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
import requestComposition, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import requestComposition, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
@ -161,21 +164,32 @@
const formRef = ref<FormInstance>(); const formRef = ref<FormInstance>();
const requestCompositionRef = ref<InstanceType<typeof requestComposition>>(); const requestCompositionRef = ref<InstanceType<typeof requestComposition>>();
const defaultCaseParams = inject<RequestParam>('defaultCaseParams'); const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const defaultDetail: RequestParam = { const defaultDetail = computed<RequestParam>(() => {
apiDefinitionId: apiDefinitionId.value, return {
...(defaultCaseParams as RequestParam), ...(defaultCaseParams as RequestParam),
apiDefinitionId: apiDefinitionId.value,
protocol: apiDetailInfo.value.protocol,
}; };
const detailForm = ref(cloneDeep(defaultDetail)); });
const detailForm = ref(cloneDeep(defaultDetail.value));
const isEdit = ref(false); const isEdit = ref(false);
function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) { async function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) {
apiDefinitionId.value = apiId; apiDefinitionId.value = apiId;
// apiapicaseapi // apiapicaseapi
if (props.apiDetail) { if (props.apiDetail) {
apiDetailInfo.value = props.apiDetail; apiDetailInfo.value = cloneDeep(props.apiDetail);
} else { } else {
getApiDetail(); await getApiDetail();
} }
//
detailForm.value = {
...cloneDeep(defaultDetail.value),
headers: apiDetailInfo.value.headers ?? apiDetailInfo.value.request.headers,
body: apiDetailInfo.value.body ?? apiDetailInfo.value.request.body,
rest: apiDetailInfo.value.rest ?? apiDetailInfo.value.request.rest,
query: apiDetailInfo.value.query ?? apiDetailInfo.value.request.query,
};
// //
if (isCopy) { if (isCopy) {
detailForm.value.name = `copy_${record?.name}`; detailForm.value.name = `copy_${record?.name}`;
@ -184,6 +198,7 @@
if (!isCopy && record?.id) { if (!isCopy && record?.id) {
isEdit.value = true; isEdit.value = true;
detailForm.value = cloneDeep(record as RequestParam); detailForm.value = cloneDeep(record as RequestParam);
detailForm.value.isNew = false;
} }
innerVisible.value = true; innerVisible.value = true;
} }
@ -193,7 +208,6 @@
isEdit.value = false; isEdit.value = false;
innerVisible.value = false; innerVisible.value = false;
formRef.value?.resetFields(); formRef.value?.resetFields();
detailForm.value = cloneDeep(defaultDetail);
} }
function handleDrawerConfirm(isContinue: boolean) { function handleDrawerConfirm(isContinue: boolean) {
@ -236,7 +250,7 @@
if (!isContinue) { if (!isContinue) {
handleSaveCaseCancel(); handleSaveCaseCancel();
} }
detailForm.value = cloneDeep(defaultDetail); detailForm.value = cloneDeep(defaultDetail.value);
drawerLoading.value = false; drawerLoading.value = false;
} }
}); });

View File

@ -0,0 +1,165 @@
<template>
<a-dropdown-button
v-if="!caseDetail.executeLoading"
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute"
>
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template v-if="hasLocalExec" #icon>
<icon-down />
</template>
<template v-if="hasLocalExec" #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else type="primary" @click="stopDebug">{{ t('common.stop') }}</a-button>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { cloneDeep } from 'lodash-es';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import { debugCase, runCase } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { getLocalConfig } from '@/api/modules/user/index';
import useAppStore from '@/store/modules/app';
import { getGenerateId } from '@/utils';
import { defaultResponse } from '@/views/api-test/components/config';
const props = defineProps<{
environmentId: string;
request?: (...args) => Record<string, any>;
}>();
const { t } = useI18n();
const appStore = useAppStore();
const caseDetail = defineModel<RequestParam>('detail', {
required: true,
});
const hasLocalExec = ref(false); // api
const isPriorityLocalExec = ref(false); //
const localExecuteUrl = ref('');
const reportId = ref('');
const websocket = ref<WebSocket>();
const temporaryResponseMap = {}; // websockettab
async function initLocalConfig() {
if (hasLocalExec.value) {
return;
}
try {
const res = await getLocalConfig(); // TODO:
const apiLocalExec = res.find((e) => e.type === 'API');
if (apiLocalExec) {
hasLocalExec.value = true;
isPriorityLocalExec.value = apiLocalExec.enable || false;
localExecuteUrl.value = apiLocalExec.userUrl || '';
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
/**
* 开启websocket监听接收执行结果
*/
function debugSocket(executeType?: 'localExec' | 'serverExec') {
websocket.value = getSocket(
reportId.value,
executeType === 'localExec' ? '/ws/debug' : '',
executeType === 'localExec' ? localExecuteUrl.value : ''
);
websocket.value.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
if (caseDetail.value.reportId === data.reportId) {
// tabtab
// TODO:
caseDetail.value.response = data.taskResult; //
caseDetail.value.executeLoading = false;
} else {
// tab
temporaryResponseMap[data.reportId] = data.taskResult;
}
} else if (data.msgType === 'EXEC_END') {
// websocket
websocket.value?.close();
caseDetail.value.executeLoading = false;
}
});
}
async function execute(executeType?: 'localExec' | 'serverExec') {
try {
caseDetail.value.executeLoading = true;
caseDetail.value.response = cloneDeep(defaultResponse);
const makeRequestParams = props.request && props.request(executeType); // reportIdreportId
reportId.value = getGenerateId();
caseDetail.value.reportId = reportId.value; // ID
let res;
const params = {
environmentId: props.environmentId as string,
frontendDebug: executeType === 'localExec',
reportId: reportId.value,
};
debugSocket(executeType); // websocket
if ((caseDetail.value.id as string).startsWith('c')) {
//
res = await debugCase({
request: makeRequestParams?.request,
linkFileIds: makeRequestParams?.linkFileIds,
uploadFileIds: makeRequestParams?.uploadFileIds,
id: `case-${Date.now()}`,
projectId: appStore.currentProjectId,
...params,
});
} else {
res = await runCase({
request: caseDetail.value.request,
id: caseDetail.value.id as string,
projectId: caseDetail.value.projectId,
linkFileIds: caseDetail.value.linkFileIds,
uploadFileIds: caseDetail.value.uploadFileIds,
...params,
});
}
if (executeType === 'localExec') {
await localExecuteApiDebug(localExecuteUrl.value, res); // TODO:
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
caseDetail.value.executeLoading = false;
}
}
function stopDebug() {
websocket.value?.close();
caseDetail.value.executeLoading = false;
}
onBeforeMount(() => {
initLocalConfig();
});
</script>
<style lang="less" scoped>
.exec-btn :deep(.arco-btn) {
color: white !important;
background-color: rgb(var(--primary-5)) !important;
.btn-base-primary-hover();
.btn-base-primary-active();
.btn-base-primary-disabled();
}
</style>

View File

@ -2,6 +2,7 @@
<div class="flex flex-1 flex-col overflow-hidden"> <div class="flex flex-1 flex-col overflow-hidden">
<div v-show="activeApiTab.id === 'all'" class="flex-1 overflow-hidden"> <div v-show="activeApiTab.id === 'all'" class="flex-1 overflow-hidden">
<caseTable <caseTable
ref="caseTableRef"
:is-api="false" :is-api="false"
:active-module="props.activeModule" :active-module="props.activeModule"
:protocol="props.protocol" :protocol="props.protocol"
@ -12,7 +13,7 @@
<caseDetail <caseDetail
:detail="activeApiTab" :detail="activeApiTab"
:module-tree="props.moduleTree" :module-tree="props.moduleTree"
:protocol="props.protocol" @delete-case="deleteCase"
@update-follow="activeApiTab.follow = !activeApiTab.follow" @update-follow="activeApiTab.follow = !activeApiTab.follow"
@load-case="(id: string) => openOrUpdateCaseTab(false, id)" @load-case="(id: string) => openOrUpdateCaseTab(false, id)"
/> />
@ -42,6 +43,9 @@
protocol: string; protocol: string;
moduleTree: ModuleTreeNode[]; // moduleTree: ModuleTreeNode[]; //
}>(); }>();
const emit = defineEmits<{
(e: 'deleteCase', id: string): void;
}>();
const apiTabs = defineModel<RequestParam[]>('apiTabs', { const apiTabs = defineModel<RequestParam[]>('apiTabs', {
required: true, required: true,
@ -58,16 +62,15 @@
try { try {
loading.value = true; loading.value = true;
const res = await getCaseDetail(id); const res = await getCaseDetail(id);
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id ; let parseRequestBodyResult;
// if (res.protocol === 'HTTP') { // TODO: protocol if (res.protocol === 'HTTP') {
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
// } }
const tabItemInfo = { const tabItemInfo = {
...cloneDeep(defaultCaseParams as RequestParam), ...cloneDeep(defaultCaseParams as RequestParam),
...({ ...({
...res.request, ...res.request,
...res, ...res,
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response
url: res.path, url: res.path,
...parseRequestBodyResult, ...parseRequestBodyResult,
} as Partial<TabItem>), } as Partial<TabItem>),
@ -92,7 +95,7 @@
} }
} }
async function openCaseTab(apiInfo: ApiCaseDetail) { async function openCaseTab(apiInfo: ApiCaseDetail | string) {
const isLoadedTabIndex = apiTabs.value.findIndex( const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id) (e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
); );
@ -103,4 +106,14 @@
} }
await openOrUpdateCaseTab(true, typeof apiInfo === 'string' ? apiInfo : apiInfo.id); await openOrUpdateCaseTab(true, typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
} }
const caseTableRef = ref<InstanceType<typeof caseTable>>();
function deleteCase(id: string) {
emit('deleteCase', id);
caseTableRef.value?.loadCaseList();
}
defineExpose({
openCaseTab,
});
</script> </script>

View File

@ -47,12 +47,14 @@
:module-tree="props.moduleTree" :module-tree="props.moduleTree"
/> />
<apiCase <apiCase
v-if="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.type === 'case'" v-show="(activeApiTab.id === 'all' && currentTab === 'case') || activeApiTab.type === 'case'"
ref="caseRef"
v-model:api-tabs="apiTabs" v-model:api-tabs="apiTabs"
v-model:active-api-tab="activeApiTab" v-model:active-api-tab="activeApiTab"
:active-module="props.activeModule" :active-module="props.activeModule"
:protocol="props.protocol" :protocol="props.protocol"
:module-tree="props.moduleTree" :module-tree="props.moduleTree"
@delete-case="(id) => handleDeleteApiFromModuleTree(id)"
/> />
</template> </template>
@ -67,11 +69,12 @@
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue'; import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
// import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue'; // import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
import { getEnvironment, getEnvList } from '@/api/modules/api-test/common'; import { getEnvironment, getEnvList, getProtocolList } from '@/api/modules/api-test/common';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import router from '@/router'; import router from '@/router';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
import { ProtocolItem } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common'; import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental'; import { EnvConfig } from '@/models/projectManagement/environmental';
import { import {
@ -104,6 +107,7 @@
]; ];
const apiRef = ref<InstanceType<typeof api>>(); const apiRef = ref<InstanceType<typeof api>>();
const caseRef = ref<InstanceType<typeof apiCase>>();
function newTab(apiInfo?: ModuleTreeNode | string, isCopy?: boolean, isExecute?: boolean) { function newTab(apiInfo?: ModuleTreeNode | string, isCopy?: boolean, isExecute?: boolean) {
if (apiInfo) { if (apiInfo) {
@ -113,6 +117,10 @@
} }
} }
function newCaseTab(id: string) {
caseRef.value?.openCaseTab(id);
}
const apiTabs = ref<RequestParam[]>([ const apiTabs = ref<RequestParam[]>([
{ {
id: 'all', id: 'all',
@ -310,16 +318,29 @@
} }
} }
const protocols = ref<ProtocolItem[]>([]);
async function initProtocolList() {
try {
protocols.value = await getProtocolList(appStore.currentOrgId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => { onBeforeMount(() => {
initEnvList(); initEnvList();
initProtocolList();
}); });
/** 向孙组件提供属性 */ /** 向孙组件提供属性 */
provide('currentEnvConfig', readonly(currentEnvConfig)); provide('currentEnvConfig', readonly(currentEnvConfig));
provide('defaultCaseParams', readonly(defaultCaseParams)); provide('defaultCaseParams', readonly(defaultCaseParams));
provide('protocols', readonly(protocols));
defineExpose({ defineExpose({
newTab, newTab,
newCaseTab,
refreshApiTable, refreshApiTable,
handleApiUpdateFromModuleTree, handleApiUpdateFromModuleTree,
handleDeleteApiFromModuleTree, handleDeleteApiFromModuleTree,

View File

@ -149,6 +149,9 @@
if (route.query.dId) { if (route.query.dId) {
// dId tab // dId tab
managementRef.value?.newTab(route.query.dId as string); managementRef.value?.newTab(route.query.dId as string);
} else if (route.query.cId) {
// cId tab
managementRef.value?.newCaseTab(route.query.cId as string);
} }
}); });