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,
RecoverOperationHistoryUrl,
RecycleCasePageUrl,
RunCaseUrl,
SaveOperationHistoryUrl,
SortCaseUrl,
SortDefinitionUrl,
@ -409,6 +410,11 @@ export function toggleFollowCase(id: string | number) {
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 GetChangeHistoryUrl = '/api/case/operation-history/page'; // 获取用例的依赖关系
export const ToggleFollowCaseUrl = '/api/case/follow'; // 接口定义-关注/取消关注
export const RunCaseUrl = '/api/case/run'; // 执行接口用例
export const GetCaseReportByIdUrl = '/api/report/case/get/'; // 接口用例报告获取
export const GetCaseReportDetailUrl = '/api/report/case/get/detail/'; // 接口用例报告获取

View File

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

View File

@ -587,6 +587,7 @@
request: RequestParam; //
moduleTree?: ModuleTreeNode[]; //
isCase?: boolean; //
apiDetail?: RequestParam; //
detailLoading?: boolean; //
isDefinition?: boolean; //
hideResponseLayoutSwitch?: boolean; //
@ -677,10 +678,32 @@
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
const contentTabList = computed(() => {
// HTTP tabs
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) {
//
return requestVModel.value.mode === 'debug'
@ -1214,6 +1237,14 @@
} else if (protocolOptions.value.length === 0) {
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) {
//
execute(isPriorityLocalExec.value ? 'localExec' : 'serverExec');

View File

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

View File

@ -234,85 +234,87 @@
</div>
</div>
</a-collapse-item>
<a-collapse-item
v-if="
previewDetail.responseDefinition &&
previewDetail.responseDefinition.length > 0 &&
props.detail.protocol === 'HTTP'
"
key="response"
>
<template #header>
<div class="flex items-center gap-[4px]">
<div v-if="activeDetailKey.includes('response')" 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 class="font-medium">{{ t('apiTestManagement.responseContent') }}</div>
</div>
</template>
<MsEditableTab
v-model:active-tab="activeResponse"
:tabs="previewDetail.responseDefinition?.map((e) => ({ ...e, closable: false })) || []"
hide-more-action
readonly
class="my-[8px]"
<a-spin :loading="previewDetail.executeLoading" class="w-full">
<a-collapse-item
v-if="
previewDetail.responseDefinition &&
previewDetail.responseDefinition.length > 0 &&
props.detail.protocol === 'HTTP'
"
key="response"
>
<template #label="{ tab }">
<div class="response-tab">
<div v-if="tab.defaultFlag" class="response-tab-default-icon"></div>
{{ t(tab.label || tab.name) }}({{ tab.statusCode }})
<template #header>
<div class="flex items-center gap-[4px]">
<div v-if="activeDetailKey.includes('response')" 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 class="font-medium">{{ t('apiTestManagement.responseContent') }}</div>
</div>
</template>
</MsEditableTab>
<div class="detail-item !pt-0">
<div class="detail-item-title">
<div class="detail-item-title-text">
{{ `${t('apiTestDebug.responseBody')}-${activeResponse?.body.bodyType}` }}
</div>
</div>
<MsFormTable
v-if="activeResponse?.body.bodyType === ResponseBodyFormat.BINARY"
:columns="responseBodyColumns"
:data="responseBodyTableData"
:selectable="false"
/>
<MsCodeEditor
v-else
:model-value="responseCode"
class="flex-1"
theme="vs"
height="200px"
:language="responseCodeLanguage"
:show-full-screen="false"
:show-theme-change="false"
read-only
<MsEditableTab
v-model:active-tab="activeResponse"
:tabs="previewDetail.responseDefinition?.map((e) => ({ ...e, closable: false })) || []"
hide-more-action
readonly
class="my-[8px]"
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(responseCode || '')"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
<template #label="{ tab }">
<div class="response-tab">
<div v-if="tab.defaultFlag" class="response-tab-default-icon"></div>
{{ t(tab.label || tab.name) }}({{ tab.statusCode }})
</div>
</template>
</MsCodeEditor>
</div>
<div v-if="activeResponse?.headers && activeResponse?.headers.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">
{{ t('apiTestDebug.responseHeader') }}
</MsEditableTab>
<div class="detail-item !pt-0">
<div class="detail-item-title">
<div class="detail-item-title-text">
{{ `${t('apiTestDebug.responseBody')}-${activeResponse?.body.bodyType}` }}
</div>
</div>
<MsFormTable
v-if="activeResponse?.body.bodyType === ResponseBodyFormat.BINARY"
:columns="responseBodyColumns"
:data="responseBodyTableData"
:selectable="false"
/>
<MsCodeEditor
v-else
:model-value="responseCode"
class="flex-1"
theme="vs"
height="200px"
:language="responseCodeLanguage"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(responseCode || '')"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
</div>
<MsFormTable :columns="responseHeaderColumns" :data="activeResponse?.headers || []" :selectable="false" />
</div>
</a-collapse-item>
<div v-if="activeResponse?.headers && activeResponse?.headers.length > 0" class="detail-item">
<div class="detail-item-title">
<div class="detail-item-title-text">
{{ t('apiTestDebug.responseHeader') }}
</div>
</div>
<MsFormTable :columns="responseHeaderColumns" :data="activeResponse?.headers || []" :selectable="false" />
</div>
</a-collapse-item>
</a-spin>
</a-collapse>
</template>

View File

@ -2,14 +2,13 @@
<div class="h-full w-full overflow-hidden">
<a-tabs v-model:active-key="activeKey" class="h-full px-[16px]" animation lazy-load>
<template #extra>
<div v-show="!props.isDrawer" class="flex gap-[12px]">
<a-button type="primary">
{{ t('apiTestManagement.execute') }}
</a-button>
<div class="flex gap-[12px]">
<environmentSelect v-if="props.isDrawer" ref="environmentSelectRef" />
<execute v-model:detail="caseDetail" :environment-id="environmentId as string" />
<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>
<a-doption value="edit">
<a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="edit">
<MsIcon type="icon-icon_edit_outlined" />
{{ t('common.edit') }}
</a-doption>
@ -17,15 +16,19 @@
<MsIcon type="icon-icon_share1" />
{{ t('common.share') }}
</a-doption>
<a-doption value="fork">
<a-doption v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" value="fork">
<MsIcon
:type="caseDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${caseDetail.follow ? 'text-[rgb(var(--warning-6))]' : ''}`"
/>
{{ t('common.fork') }}
</a-doption>
<a-divider margin="4px" />
<a-doption class="error-6 text-[rgb(var(--danger-6))]">
<a-divider v-permission="['PROJECT_API_DEFINITION_CASE:READ+DELETE']" margin="4px" />
<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))]" />
{{ t('common.delete') }}
</a-doption>
@ -35,11 +38,6 @@
</template>
<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]">
<template #titleAppend>
<a-button v-show="props.isDrawer" type="primary" size="mini">
{{ t('apiTestManagement.execute') }}
</a-button>
</template>
<template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
</template>
@ -47,13 +45,13 @@
<caseLevel :case-level="value as CaseLevel" />
</template>
</MsDetailCard>
<detailTab :detail="caseDetail" :protocols="protocols" is-case />
<detailTab :detail="caseDetail" :protocols="protocols as ProtocolItem[]" is-case />
</a-tab-pane>
<a-tab-pane key="reference" :title="t('apiTestManagement.reference')" class="px-[18px] py-[16px]">
<tab-case-dependency :source-id="caseDetail.id" />
</a-tab-pane>
<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 key="dependencies" :title="t('apiTestManagement.dependencies')" class="px-[18px] py-[16px]">
</a-tab-pane> -->
@ -62,7 +60,7 @@
</a-tab-pane>
</a-tabs>
</div>
<createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" :protocol="props.protocol" v-bind="$attrs" />
<createAndEditCaseDrawer ref="createAndEditCaseDrawerRef" v-bind="$attrs" />
</template>
<script setup lang="ts">
@ -74,33 +72,41 @@
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import environmentSelect from '../../environmentSelect.vue';
import detailTab from '../api/preview/detail.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import execute from './execute.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.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 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 } from '@/api/modules/api-test/common';
import { toggleFollowCase } from '@/api/modules/api-test/management';
import useAppStore from '@/store/modules/app';
import { deleteCase, toggleFollowCase } from '@/api/modules/api-test/management';
import useModal from '@/hooks/useModal';
import { ProtocolItem } from '@/models/apiTest/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { RequestMethods } from '@/enums/apiEnum';
const props = defineProps<{
isDrawer?: boolean; //
detail: RequestParam;
protocol: string;
}>();
const emit = defineEmits(['updateFollow']);
const emit = defineEmits<{
(e: 'updateFollow'): void;
(e: 'deleteCase', id: string): void;
}>();
const { copy, isSupported } = useClipboard();
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 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);
async function follow() {
try {
@ -157,7 +149,7 @@
function share() {
if (isSupported) {
copy(`${window.location.href}&dId=${caseDetail.value.id}`);
copy(`${window.location.href}&cId=${caseDetail.value.id}`);
Message.success(t('apiTestManagement.shareUrlCopied'));
} else {
Message.error(t('common.copyNotSupport'));
@ -169,6 +161,30 @@
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) {
switch (val) {
case 'edit':
@ -180,15 +196,28 @@
case 'fork':
follow();
break;
case 'delete':
handleDelete();
break;
default:
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({
editCase,
share,
follow,
handleDelete,
});
</script>
@ -196,6 +225,9 @@
:deep(.arco-tabs-nav) {
border-bottom: 1px solid var(--color-text-n8);
}
:deep(.arco-tabs-nav-extra) {
line-height: 32px;
}
:deep(.arco-tabs-content) {
@apply pt-0;
.arco-tabs-content-item {

View File

@ -7,9 +7,6 @@
:footer="false"
no-content-padding
>
<template #headerLeft>
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
</template>
<template #tbutton>
<div class="flex items-center gap-[4px]">
<MsButton
@ -39,7 +36,7 @@
{{ t('common.fork') }}
</MsButton>
<MsButton type="icon" status="secondary">
<a-dropdown position="br">
<a-dropdown position="br" @select="handleSelect">
<div>
<icon-more class="mr-[8px]" />
<span> {{ t('common.more') }}</span>
@ -47,6 +44,7 @@
<template #content>
<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))]" />
@ -57,14 +55,7 @@
</MsButton>
</div>
</template>
<caseDetail
ref="caseDerailRef"
is-drawer
:detail="props.detail"
:protocol="props.protocol"
:api-detail="props.apiDetail"
v-bind="$attrs"
/>
<caseDetail ref="caseDerailRef" is-drawer :detail="props.detail" :api-detail="props.apiDetail" v-bind="$attrs" />
</MsDrawer>
</template>
@ -72,7 +63,6 @@
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 environmentSelect from '../../environmentSelect.vue';
import caseDetail from './caseDetail.vue';
import { useI18n } from '@/hooks/useI18n';
@ -81,7 +71,6 @@
const props = defineProps<{
detail: RequestParam;
protocol: string;
apiDetail: RequestParam;
}>();
@ -91,6 +80,16 @@
required: true,
});
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>
<style scoped lang="less">

View File

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

View File

@ -4,6 +4,7 @@
:title="isEdit ? t('case.updateCase') : t('case.createCase')"
:width="894"
no-content-padding
unmount-on-close
:ok-text="isEdit ? 'common.update' : 'common.create'"
:ok-loading="drawerLoading"
:save-continue-text="t('case.saveContinueText')"
@ -12,9 +13,6 @@
@continue="handleDrawerConfirm(true)"
@cancel="handleSaveCaseCancel"
>
<template #headerLeft>
<environmentSelect ref="environmentSelectRef" class="ml-[16px]" />
</template>
<div class="flex h-full flex-col overflow-hidden">
<div class="px-[16px] pt-[16px]">
<MsDetailCard
@ -36,9 +34,12 @@
:max-length="255"
show-word-limit
/>
<a-button type="primary">
{{ t('apiTestManagement.execute') }}
</a-button>
<environmentSelect ref="environmentSelectRef" />
<execute
v-model:detail="detailForm"
:environment-id="currentEnvConfig?.id as string"
:request="requestCompositionRef?.makeRequestParams"
/>
</div>
</a-form-item>
<div class="flex gap-[16px]">
@ -74,6 +75,7 @@
ref="requestCompositionRef"
v-model:request="detailForm"
:is-case="true"
:api-detail="apiDetailInfo as RequestParam"
hide-response-layout-switch
:upload-temp-file-api="uploadTempFileCase"
:file-save-as-source-id="detailForm.id"
@ -97,6 +99,7 @@
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import environmentSelect from '../../environmentSelect.vue';
import execute from './execute.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import requestComposition, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
@ -161,21 +164,32 @@
const formRef = ref<FormInstance>();
const requestCompositionRef = ref<InstanceType<typeof requestComposition>>();
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const defaultDetail: RequestParam = {
apiDefinitionId: apiDefinitionId.value,
...(defaultCaseParams as RequestParam),
};
const detailForm = ref(cloneDeep(defaultDetail));
const defaultDetail = computed<RequestParam>(() => {
return {
...(defaultCaseParams as RequestParam),
apiDefinitionId: apiDefinitionId.value,
protocol: apiDetailInfo.value.protocol,
};
});
const detailForm = ref(cloneDeep(defaultDetail.value));
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;
// apiapicaseapi
if (props.apiDetail) {
apiDetailInfo.value = props.apiDetail;
apiDetailInfo.value = cloneDeep(props.apiDetail);
} 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) {
detailForm.value.name = `copy_${record?.name}`;
@ -184,6 +198,7 @@
if (!isCopy && record?.id) {
isEdit.value = true;
detailForm.value = cloneDeep(record as RequestParam);
detailForm.value.isNew = false;
}
innerVisible.value = true;
}
@ -193,7 +208,6 @@
isEdit.value = false;
innerVisible.value = false;
formRef.value?.resetFields();
detailForm.value = cloneDeep(defaultDetail);
}
function handleDrawerConfirm(isContinue: boolean) {
@ -236,7 +250,7 @@
if (!isContinue) {
handleSaveCaseCancel();
}
detailForm.value = cloneDeep(defaultDetail);
detailForm.value = cloneDeep(defaultDetail.value);
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 v-show="activeApiTab.id === 'all'" class="flex-1 overflow-hidden">
<caseTable
ref="caseTableRef"
:is-api="false"
:active-module="props.activeModule"
:protocol="props.protocol"
@ -12,7 +13,7 @@
<caseDetail
:detail="activeApiTab"
:module-tree="props.moduleTree"
:protocol="props.protocol"
@delete-case="deleteCase"
@update-follow="activeApiTab.follow = !activeApiTab.follow"
@load-case="(id: string) => openOrUpdateCaseTab(false, id)"
/>
@ -42,6 +43,9 @@
protocol: string;
moduleTree: ModuleTreeNode[]; //
}>();
const emit = defineEmits<{
(e: 'deleteCase', id: string): void;
}>();
const apiTabs = defineModel<RequestParam[]>('apiTabs', {
required: true,
@ -58,16 +62,15 @@
try {
loading.value = true;
const res = await getCaseDetail(id);
const parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id ;
// if (res.protocol === 'HTTP') { // TODO: protocol
// parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
// }
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
}
const tabItemInfo = {
...cloneDeep(defaultCaseParams as RequestParam),
...({
...res.request,
...res,
// responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })), // TODO: response
url: res.path,
...parseRequestBodyResult,
} as Partial<TabItem>),
@ -92,7 +95,7 @@
}
}
async function openCaseTab(apiInfo: ApiCaseDetail) {
async function openCaseTab(apiInfo: ApiCaseDetail | string) {
const isLoadedTabIndex = apiTabs.value.findIndex(
(e) => e.id === (typeof apiInfo === 'string' ? apiInfo : apiInfo.id)
);
@ -103,4 +106,14 @@
}
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>

View File

@ -47,12 +47,14 @@
:module-tree="props.moduleTree"
/>
<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:active-api-tab="activeApiTab"
:active-module="props.activeModule"
:protocol="props.protocol"
:module-tree="props.moduleTree"
@delete-case="(id) => handleDeleteApiFromModuleTree(id)"
/>
</template>
@ -67,11 +69,12 @@
import { RequestParam } from '@/views/api-test/components/requestComposition/index.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 router from '@/router';
import useAppStore from '@/store/modules/app';
import { ProtocolItem } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import {
@ -104,6 +107,7 @@
];
const apiRef = ref<InstanceType<typeof api>>();
const caseRef = ref<InstanceType<typeof apiCase>>();
function newTab(apiInfo?: ModuleTreeNode | string, isCopy?: boolean, isExecute?: boolean) {
if (apiInfo) {
@ -113,6 +117,10 @@
}
}
function newCaseTab(id: string) {
caseRef.value?.openCaseTab(id);
}
const apiTabs = ref<RequestParam[]>([
{
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(() => {
initEnvList();
initProtocolList();
});
/** 向孙组件提供属性 */
provide('currentEnvConfig', readonly(currentEnvConfig));
provide('defaultCaseParams', readonly(defaultCaseParams));
provide('protocols', readonly(protocols));
defineExpose({
newTab,
newCaseTab,
refreshApiTable,
handleApiUpdateFromModuleTree,
handleDeleteApiFromModuleTree,

View File

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