fix(接口管理): 执行结果抽屉&用例列表跳转执行&部分细节

This commit is contained in:
teukkk 2024-03-27 16:42:30 +08:00 committed by Craftsman
parent 3a5a33ad0d
commit ad6bc5be35
16 changed files with 449 additions and 249 deletions

View File

@ -0,0 +1,3 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.66602 0.666504L8.72002 0.668504L8.76532 0.67385C8.77665 0.675589 8.78827 0.677639 8.79983 0.679997C8.81451 0.68293 8.82897 0.686393 8.84324 0.690318C8.85366 0.693236 8.86392 0.696323 8.87411 0.699663C8.88721 0.703919 8.9004 0.708714 8.91339 0.713907L8.94816 0.728953C8.96194 0.735422 8.97554 0.742367 8.98887 0.749754C8.99694 0.75417 9.00509 0.758909 9.01315 0.763834C9.0302 0.774316 9.04677 0.785497 9.06279 0.79738L9.07736 0.808456C9.07912 0.809885 9.08106 0.811419 9.08298 0.812963L9.13742 0.861766L12.4708 4.1951L12.516 4.2465L12.5351 4.26963C12.547 4.28575 12.5582 4.30232 12.5686 4.3194L12.5829 4.3438C12.5902 4.35697 12.5971 4.37058 12.6036 4.38444L12.6185 4.41895C12.6238 4.43212 12.6286 4.44531 12.633 4.45868C12.6362 4.4686 12.6393 4.47886 12.6421 4.48917C12.6461 4.50355 12.6496 4.51801 12.6526 4.53264C12.6549 4.54425 12.6569 4.55587 12.6587 4.56753C12.6597 4.57435 12.6607 4.58153 12.6615 4.58876L12.6635 4.60902C12.6646 4.62099 12.6653 4.63299 12.6657 4.645L12.666 4.6665V7.99984H11.3327V5.33317H8.66602C8.32412 5.33317 8.04234 5.07581 8.00383 4.74425L7.99935 4.6665V1.99984H1.99935V13.9998H7.99935V15.3332H1.99935C1.26297 15.3332 0.666016 14.7362 0.666016 13.9998V1.99984C0.666016 1.26346 1.26297 0.666504 1.99935 0.666504H8.66602ZM12.5381 8.8665C13.7153 8.8665 14.5574 9.64206 14.666 10.637H13.2444C13.1629 10.2923 12.9727 10.0259 12.5291 10.0259H11.5783C11.0531 10.0259 10.7181 10.4098 10.7181 10.872L10.7271 13.3277C10.7271 13.7899 11.0622 14.1737 11.5874 14.1737H12.5381C12.9727 14.1737 13.1538 13.9231 13.2444 13.5862H14.6751C14.5664 14.5654 13.7153 15.3332 12.5472 15.3332H11.5783C10.3468 15.3332 9.34174 14.4636 9.34174 13.3982L9.33268 10.8093C9.33268 9.73607 10.3378 8.8665 11.5692 8.8665H12.5381ZM9.33268 2.94317V3.99984H10.3893L9.33268 2.94317Z" fill="#3370FF"/>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -1,7 +1,6 @@
<template>
<a-dropdown-button
v-if="!caseDetail?.executeLoading && !props.executeLoading"
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
v-if="hasLocalExec && !props.executeLoading"
class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute"
@ -16,32 +15,21 @@
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else type="primary" @click="stopDebug">{{ t('common.stop') }}</a-button>
<a-button v-else-if="!hasLocalExec && !props.executeLoading" type="primary" @click="() => execute('serverExec')">
{{ t('apiTestDebug.serverExec') }}
</a-button>
<a-button v-else type="primary" @click="emit('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 useAppStore from '@/store/modules/app';
import { getGenerateId } from '@/utils';
import { LocalConfig } from '@/models/user';
import { defaultResponse } from '@/views/api-test/components/config';
import { getLocalConfig } from '@/api/modules/user/index';
const props = defineProps<{
environmentId?: string;
request?: (...args) => Record<string, any>;
isCaseDetail?: boolean;
executeCase?: boolean;
executeLoading?: boolean;
isEmit?: boolean;
}>();
const emit = defineEmits<{
@ -50,117 +38,35 @@
}>();
const { t } = useI18n();
const appStore = useAppStore();
const caseDetail = defineModel<RequestParam>('detail', {
required: false,
});
const hasLocalExec = ref(false); // api
const isPriorityLocalExec = ref(false); //
const localExecuteUrl = ref('');
const apiLocalExec = inject<Ref<LocalConfig>>('apiLocalExec');
const isPriorityLocalExec = ref(apiLocalExec?.value?.enable || false); //
const localExecuteUrl = ref(apiLocalExec?.value?.userUrl || '');
const reportId = ref('');
const websocket = ref<WebSocket>();
const temporaryResponseMap = {}; // websockettab
/**
* 开启websocket监听接收执行结果
*/
function debugSocket(executeType?: 'localExec' | 'serverExec') {
websocket.value = getSocket(
reportId.value,
executeType === 'localExec' ? '/ws/debug' : '',
executeType === 'localExec' ? localExecuteUrl.value : ''
);
websocket.value.addEventListener('message', (event) => {
if (!caseDetail.value || props.isEmit) return;
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;
} 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') {
if (!caseDetail.value || props.isEmit) {
emit('execute', executeType, localExecuteUrl.value);
async function initLocalConfig() {
if (hasLocalExec.value) {
return;
}
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,
apiDefinitionId: caseDetail.value.apiDefinitionId,
};
debugSocket(executeType); // websocket
if (!(caseDetail.value.id as string).startsWith('c') && executeType === 'serverExec') {
//
res = await runCase({
request: props.isCaseDetail ? caseDetail.value.request : makeRequestParams?.request,
id: caseDetail.value.id as string,
projectId: caseDetail.value.projectId,
linkFileIds: caseDetail.value.linkFileIds,
uploadFileIds: caseDetail.value.uploadFileIds,
...params,
});
} else {
res = await debugCase({
request: props.isCaseDetail ? caseDetail.value.request : makeRequestParams?.request,
linkFileIds: makeRequestParams?.linkFileIds,
uploadFileIds: makeRequestParams?.uploadFileIds,
id: `case-${Date.now()}`,
projectId: appStore.currentProjectId,
...params,
});
}
if (executeType === 'localExec') {
await localExecuteApiDebug(localExecuteUrl.value, res);
const res = await getLocalConfig();
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);
caseDetail.value.executeLoading = false;
}
}
onBeforeMount(() => {
initLocalConfig();
});
function stopDebug() {
if (!caseDetail.value || props.isEmit) {
emit('stopDebug');
return;
async function execute(executeType?: 'localExec' | 'serverExec') {
emit('execute', executeType, localExecuteUrl.value);
}
websocket.value?.close();
caseDetail.value.executeLoading = false;
}
watch(
() => props.executeCase,
(val) => {
if (val === true) {
setTimeout(() => {
execute(isPriorityLocalExec.value ? 'localExec' : 'serverExec');
}, 300);
}
},
{ immediate: true }
);
defineExpose({
isPriorityLocalExec,

View File

@ -705,7 +705,7 @@
.length
);
const bodyTabBadgeApi = computed(() =>
props.apiDetail?.request.body?.bodyType !== RequestBodyFormat.NONE ? '1' : ''
props.apiDetail?.request?.body?.bodyType !== RequestBodyFormat.NONE ? '1' : ''
);
// tab
const contentTabList = computed(() => {
@ -1575,6 +1575,7 @@
}
onBeforeMount(() => {
if (props.isCase) return;
initLocalConfig();
});

View File

@ -11,7 +11,13 @@
/>
</div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
<a-tabs v-model:active-key="activeApiTab.definitionActiveKey" animation lazy-load class="ms-api-tab-nav">
<a-tabs
v-model:active-key="activeApiTab.definitionActiveKey"
animation
lazy-load
class="ms-api-tab-nav"
@change="changeDefinitionActiveKey"
>
<a-tab-pane
v-if="!activeApiTab.isNew"
key="preview"
@ -52,6 +58,7 @@
</a-tab-pane>
<a-tab-pane v-if="!activeApiTab.isNew" key="case" :title="t('apiTestManagement.case')" class="ms-api-tab-pane">
<caseTable
ref="caseTableRef"
:is-api="true"
:active-module="props.activeModule"
:protocol="activeApiTab.protocol"
@ -229,6 +236,7 @@
}
const apiTableRef = ref<InstanceType<typeof apiTable>>();
const caseTableRef = ref<InstanceType<typeof caseTable>>();
watch(
() => activeApiTab.value.id,
@ -299,6 +307,13 @@
apiTableRef.value?.loadApiList();
}
function changeDefinitionActiveKey(val: string | number) {
// case
if (val === 'case') {
caseTableRef.value?.loadCaseList();
}
}
defineExpose({
openApiTab,
addApiTab,

View File

@ -1,15 +1,15 @@
<template>
<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 @change="changeActiveKey">
<template #extra>
<div class="flex gap-[12px]">
<environmentSelect v-if="props.isDrawer" v-model:current-env="environmentIdByDrawer" />
<execute
<executeButton
ref="executeRef"
v-model:detail="caseDetail"
:execute-case="props.executeCase"
:environment-id="environmentId as string"
is-case-detail
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
:execute-loading="caseDetail.executeLoading"
@stop-debug="stopDebug"
@execute="handleExecute"
/>
<a-dropdown position="br" :hide-on-select="false" @select="handleSelect">
<a-button v-if="!props.isDrawer" type="outline">{{ t('common.operation') }}</a-button>
@ -42,7 +42,7 @@
</a-dropdown>
</div>
</template>
<a-tab-pane key="detail" :title="t('apiTestManagement.detail')" class="px-[18px] py-[16px]">
<a-tab-pane key="detail" :title="t('case.detail')" class="px-[18px] py-[16px]">
<MsDetailCard :title="`【${caseDetail.num}】${caseDetail.name}`" :description="description" class="mb-[8px]">
<template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
@ -56,14 +56,19 @@
:protocols="protocols as ProtocolItem[]"
:is-priority-local-exec="isPriorityLocalExec"
is-case
@execute="(val: 'localExec' | 'serverExec')=>executeRef?.execute(val)"
@execute="handleExecute"
/>
</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="caseDetail.protocol" />
<tab-case-execute-history
ref="executeHistoryRef"
: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> -->
@ -88,22 +93,26 @@
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import environmentSelect from '@/views/api-test/components/environmentSelect.vue';
import execute 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 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 { deleteCase, toggleFollowCase } from '@/api/modules/api-test/management';
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import { debugCase, deleteCase, runCase, toggleFollowCase } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import useModal from '@/hooks/useModal';
import { getGenerateId } from '@/utils';
import { ProtocolItem } from '@/models/apiTest/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { RequestMethods } from '@/enums/apiEnum';
import { defaultResponse } from '@/views/api-test/components/config';
const props = defineProps<{
isDrawer?: boolean; //
executeCase?: boolean;
detail: RequestParam;
}>();
const emit = defineEmits<{
@ -115,12 +124,9 @@
const { t } = useI18n();
const { openModal } = useModal();
const executeCase = defineModel<boolean>('executeCase', { default: false });
const caseDetail = ref<RequestParam>(cloneDeep(props.detail)); // props.detailprops.detail
const environmentIdByDrawer = ref(props.detail.environmentId);
watchEffect(() => {
caseDetail.value = cloneDeep(props.detail); // props.detailprops.detail
environmentIdByDrawer.value = props.detail.environmentId;
});
const activeKey = ref('detail');
@ -226,9 +232,105 @@
props.isDrawer ? environmentIdByDrawer.value : currentEnvConfigByInject?.value?.id
);
const executeRef = ref<InstanceType<typeof execute>>();
const executeHistoryRef = ref<InstanceType<typeof TabCaseExecuteHistory>>();
function changeActiveKey(val: string | number) {
if (val === 'executeHistory') {
executeHistoryRef.value?.loadExecuteList();
}
}
const executeRef = ref<InstanceType<typeof executeButton>>();
const isPriorityLocalExec = computed(() => executeRef.value?.isPriorityLocalExec ?? false);
const reportId = ref('');
const websocket = ref<WebSocket>();
const temporaryResponseMap = {}; // websockettab
// 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: environmentId.value,
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;
}
}
function stopDebug() {
websocket.value?.close();
caseDetail.value.executeLoading = false;
executeCase.value = false;
}
watch(
() => props.detail,
() => {
caseDetail.value = cloneDeep(props.detail); // props.detailprops.detail
environmentIdByDrawer.value = props.detail.environmentId;
},
{ immediate: true }
);
onMounted(async () => {
if (executeCase.value && !caseDetail.value.executeLoading) {
//
handleExecute(isPriorityLocalExec.value ? 'localExec' : 'serverExec');
}
});
defineExpose({
editCase,
share,

View File

@ -56,8 +56,8 @@
</template>
<caseDetail
ref="caseDerailRef"
v-model:execute-case="executeCase"
is-drawer
:execute-case="props.executeCase"
:detail="props.detail"
:api-detail="props.apiDetail"
v-bind="$attrs"
@ -78,7 +78,6 @@
const props = defineProps<{
detail: RequestParam;
apiDetail?: RequestParam;
executeCase?: boolean;
}>();
const { t } = useI18n();
@ -86,6 +85,7 @@
const innerVisible = defineModel<boolean>('visible', {
required: true,
});
const executeCase = defineModel<boolean>('executeCase', { default: false });
const caseDerailRef = ref<InstanceType<typeof caseDetail>>();
function handleSelect(val: string | number | Record<string, any> | undefined) {

View File

@ -0,0 +1,139 @@
<template>
<MsDrawer
v-model:visible="innerVisible"
:title="reportStepDetail.name"
:width="1200"
:footer="false"
unmount-on-close
no-content-padding
show-full-screen
>
<template #tbutton>
<MsButton type="icon" status="secondary" class="mr-4 !rounded-[var(--border-radius-small)]" @click="shareHandler">
<MsIcon type="icon-icon_share1" class="mr-2 font-[16px]" />
{{ t('common.share') }}
</MsButton>
</template>
<CaseReportCom :detail-info="reportStepDetail" />
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import CaseReportCom from '@/views/api-test/report/component/caseReportCom.vue';
import { getShareInfo, reportCaseDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import type { ReportDetail } from '@/models/apiTest/report';
import { RouteEnum } from '@/enums/routeEnum';
const props = defineProps<{
reportId: string;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const innerVisible = defineModel<boolean>('visible', {
required: true,
});
const reportStepDetail = ref<ReportDetail>({
id: '',
name: '', //
testPlanId: '',
createUser: '',
deleteTime: 0,
deleteUser: '',
deleted: false,
updateUser: '',
updateTime: 0,
startTime: 0, // /
endTime: 0, // /
requestDuration: 0, //
status: '', // /SUCCESS/ERROR
triggerMode: '', //
runMode: '', //
poolId: '', //
poolName: '', //
versionId: '',
integrated: false, //
projectId: '',
environmentId: '', // id
environmentName: '', //
errorCount: 0, //
fakeErrorCount: 0, //
pendingCount: 0, //
successCount: 0, //
assertionCount: 0, //
assertionSuccessCount: 0, //
requestErrorRate: '', //
requestPendingRate: '', //
requestFakeErrorRate: '', //
requestPassRate: '', //
assertionPassRate: '', //
scriptIdentifier: '', //
children: [], //
stepTotal: 0, //
console: '',
});
async function getReportCaseDetail() {
try {
reportStepDetail.value = await reportCaseDetail(props.reportId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
watch(
() => innerVisible.value,
async (val) => {
if (val) {
await getReportCaseDetail();
}
}
);
// share
const shareLink = ref<string>('');
async function shareHandler() {
try {
const res = await getShareInfo({
reportId: reportStepDetail.value.id,
projectId: appStore.currentProjectId,
});
const shareId = res.shareUrl;
const { origin } = window.location;
shareLink.value = `${origin}/#/${RouteEnum.SHARE}/${RouteEnum.SHARE_REPORT_CASE}${shareId}`;
if (navigator.clipboard) {
navigator.clipboard.writeText(shareLink.value).then(
() => {
Message.info(t('bugManagement.detail.shareTip'));
},
(e) => {
Message.error(e);
}
);
} else {
const input = document.createElement('input');
input.value = shareLink.value;
document.body.appendChild(input);
input.select();
document.execCommand('copy');
document.body.removeChild(input);
Message.info(t('bugManagement.detail.shareTip'));
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
</script>

View File

@ -148,7 +148,12 @@
</a-trigger>
</template>
<template #lastReportStatus="{ record }">
<ExecutionStatus :module-type="ReportEnum.API_REPORT" :status="record.lastReportStatus" />
<ExecutionStatus
:module-type="ReportEnum.API_REPORT"
:status="record.lastReportStatus"
:class="[!record.lastReportId ? '' : 'cursor-pointer']"
@click="showResult(record)"
/>
</template>
<template #passRateColumn>
<div class="flex items-center text-[var(--color-text-3)]">
@ -285,9 +290,9 @@
/>
<caseDetailDrawer
v-model:visible="caseDetailDrawerVisible"
v-model:execute-case="caseExecute"
:detail="caseDetail as RequestParam"
:api-detail="apiDetail as RequestParam"
:execute-case="caseExecute"
@update-follow="caseDetail.follow = !caseDetail.follow"
@load-case="(id: string) => loadCase(id)"
@delete-case="deleteCaseByDetail"
@ -300,6 +305,8 @@
:batch-run-func="batchExecuteCase"
@finished="loadCaseListAndResetSelector"
/>
<!-- 执行结果抽屉 -->
<caseReportDrawer v-model:visible="showExecuteResult" :report-id="activeReportId" />
</template>
<script setup lang="ts">
@ -316,6 +323,7 @@
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import caseDetailDrawer from './caseDetailDrawer.vue';
import caseReportDrawer from './caseReportDrawer.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import BatchRunModal from '@/views/api-test/components/batchRunModal.vue';
@ -367,7 +375,6 @@
const { openModal } = useModal();
const keyword = ref('');
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
const hasOperationPermission = computed(() =>
hasAnyPermission([
@ -712,9 +719,6 @@
Message.success(t('common.deleteSuccess'));
resetSelector();
loadCaseListAndResetSelector();
if (typeof refreshModuleTree === 'function') {
refreshModuleTree();
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
@ -932,6 +936,14 @@
loadCaseList();
}
const activeReportId = ref('');
const showExecuteResult = ref(false);
async function showResult(record: ApiCaseDetail) {
if (!record.lastReportId) return;
activeReportId.value = record.lastReportId;
showExecuteResult.value = true;
}
defineExpose({
loadCaseList,
});

View File

@ -34,12 +34,13 @@
:max-length="255"
show-word-limit
/>
<environmentSelect v-model:current-env="environmentId" />
<execute
<environmentSelect ref="environmentSelectRef" v-model:current-env="environmentId" />
<executeButton
ref="executeRef"
v-model:detail="detailForm"
:environment-id="currentEnvConfig?.id as string"
:request="requestCompositionRef?.makeRequestParams"
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
:execute-loading="detailForm.executeLoading"
@stop-debug="stopDebug"
@execute="handleExecute"
/>
</div>
</a-form-item>
@ -84,7 +85,7 @@
:file-save-as-api="transferFileCase"
:current-env-config="currentEnvConfig"
is-definition
@execute="(val: 'localExec' | 'serverExec')=>executeRef?.execute(val)"
@execute="handleExecute"
/>
</div>
</div>
@ -103,25 +104,30 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import environmentSelect from '@/views/api-test/components/environmentSelect.vue';
import execute from '@/views/api-test/components/executeButton.vue';
import executeButton from '@/views/api-test/components/executeButton.vue';
import requestComposition, { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import {
addCase,
debugCase,
getDefinitionDetail,
getTransferOptionsCase,
runCase,
transferFileCase,
updateCase,
uploadTempFileCase,
} from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { getGenerateId } from '@/utils';
import { AddApiCaseParams, ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { RequestDefinitionStatus, RequestMethods } from '@/enums/apiEnum';
import { casePriorityOptions } from '@/views/api-test/components/config';
import { casePriorityOptions, defaultResponse } from '@/views/api-test/components/config';
const props = defineProps<{
apiDetail?: RequestParam | ApiDefinitionDetail;
@ -175,7 +181,6 @@
});
const detailForm = ref(cloneDeep(defaultDetail.value));
const isEdit = ref(false);
const executeRef = ref<InstanceType<typeof execute>>();
async function open(apiId: string, record?: ApiCaseDetail | RequestParam, isCopy?: boolean) {
apiDefinitionId.value = apiId;
@ -276,6 +281,84 @@
}
});
}
const executeRef = ref<InstanceType<typeof executeButton>>();
const reportId = ref('');
const websocket = ref<WebSocket>();
const temporaryResponseMap = {}; // websockettab
// 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 (detailForm.value.reportId === data.reportId) {
// tabtab
detailForm.value.response = data.taskResult; //
detailForm.value.executeLoading = false;
} else {
// tab
temporaryResponseMap[data.reportId] = data.taskResult;
}
} else if (data.msgType === 'EXEC_END') {
// websocket
websocket.value?.close();
detailForm.value.executeLoading = false;
}
});
}
async function handleExecute(executeType?: 'localExec' | 'serverExec') {
try {
detailForm.value.executeLoading = true;
detailForm.value.response = cloneDeep(defaultResponse);
const makeRequestParams = requestCompositionRef.value?.makeRequestParams(executeType); // reportIdreportId
reportId.value = getGenerateId();
detailForm.value.reportId = reportId.value; // ID
let res;
const params = {
environmentId: environmentId.value as string,
frontendDebug: executeType === 'localExec',
reportId: reportId.value,
apiDefinitionId: detailForm.value.apiDefinitionId,
request: makeRequestParams?.request,
linkFileIds: makeRequestParams?.linkFileIds,
uploadFileIds: makeRequestParams?.uploadFileIds,
};
debugSocket(executeType); // websocket
if (!(detailForm.value.id as string).startsWith('c') && executeType === 'serverExec') {
//
res = await runCase({
id: detailForm.value.id as string,
projectId: detailForm.value.projectId,
...params,
});
} else {
res = await debugCase({
id: `case-${Date.now()}`,
projectId: appStore.currentProjectId,
...params,
});
}
if (executeType === 'localExec') {
await localExecuteApiDebug(executeRef.value?.localExecuteUrl as string, res);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
detailForm.value.executeLoading = false;
}
}
function stopDebug() {
websocket.value?.close();
detailForm.value.executeLoading = false;
}
const environmentSelectRef = ref<InstanceType<typeof environmentSelect>>();
const currentEnvConfigByDrawer = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig);
provide('currentEnvConfig', readonly(currentEnvConfigByDrawer));
defineExpose({
open,

View File

@ -13,8 +13,8 @@
</div>
<div v-if="activeApiTab.id !== 'all'" class="flex-1 overflow-hidden">
<caseDetail
v-model:execute-case="caseExecute"
:detail="activeApiTab"
:execute-case="caseExecute"
:module-tree="props.moduleTree"
@delete-case="deleteCase"
@update-follow="activeApiTab.follow = !activeApiTab.follow"

View File

@ -82,59 +82,42 @@
<template #status="{ record }">
<ExecutionStatus :status="record.status" :module-type="ReportEnum.API_REPORT" />
</template>
<template #operation="{ record }">
<template #operation="{ record, rowIndex }">
<a-tooltip :disabled="!record.deleted" :content="t('case.detail.report.delete')" position="top">
<MsButton :disabled="record.deleted" class="!mr-0" @click="showResult(record)"
<MsButton :disabled="record.deleted" class="!mr-0" @click="showResult(record, rowIndex)"
>{{ t('apiScenario.executeHistory.execution.operation') }}
</MsButton>
</a-tooltip>
</template>
</ms-base-table>
<a-modal
v-model:visible="showResponse"
class="ms-modal-response ms-modal-response-body"
title-align="start"
:footer="false"
>
<template #title> {{ t('caseManagement.featureCase.tableColumnExecutionResult') }} </template>
<response
v-show="showResponse"
:hide-layout-switch="true"
:is-expanded="true"
:is-http-protocol="props.protocol === 'HTTP'"
:is-priority-local-exec="false"
:active-tab="ResponseComposition.BODY"
:request-result="responseContent?.requestResults[0]"
:console="responseContent?.console"
:is-definition="true"
:is-response-model="true"
></response>
</a-modal>
</div>
<CaseReportDrawer
v-model:visible="showResponse"
:report-id="activeReportId"
:active-report-index="activeReportIndex"
:table-data="propsRes.data"
:page-change="propsEvent.pageChange"
:pagination="propsRes.msPagination!"
/>
</template>
<script setup lang="ts">
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import response from '@/views/api-test/components/requestComposition/response/index.vue';
import CaseReportDrawer from '@/views/api-test/report/component/caseReportDrawer.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import { getApiCaseExecuteHistory, getCaseReportDetail, getReportById } from '@/api/modules/api-test/management';
import { getApiCaseExecuteHistory } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ApiCaseReportDetail, RequestTaskResult } from '@/models/apiTest/common';
import { ApiCaseExecuteHistoryItem } from '@/models/apiTest/management';
import { ResponseComposition } from '@/enums/apiEnum';
import { ReportEnum, ReportStatus, TriggerModeLabel } from '@/enums/reportEnum';
import { defaultResponse } from '@/views/api-test/components/config';
const triggerModeListFilters = ref<string[]>(Object.keys(TriggerModeLabel));
const triggerModeFilterVisible = ref(false);
const statusFilterVisible = ref(false);
@ -145,8 +128,6 @@
const showResponse = ref(false);
const responseContent = ref<RequestTaskResult>();
const props = defineProps<{
sourceId: string | number;
moduleType: string;
@ -262,40 +243,21 @@
loadExecuteList();
}
function loadedReportDetail(detail: ApiCaseReportDetail[]) {
responseContent.value = cloneDeep(defaultResponse);
const apiCaseReportDetailElement = detail[0];
if (apiCaseReportDetailElement.id) {
responseContent.value.requestResults[0] = apiCaseReportDetailElement.content;
}
}
async function loadedReport(detail: Record<string, any>) {
if (detail.id) {
if (detail.children && detail.children.length > 0) {
try {
const caseReportDetail = await getCaseReportDetail(detail.id, detail.children[0].stepId);
loadedReportDetail(caseReportDetail);
} catch (e) {
console.error(e);
}
}
}
}
async function showResult(record: ApiCaseExecuteHistoryItem) {
try {
const activeReportIndex = ref<number>(0);
const activeReportId = ref('');
async function showResult(record: ApiCaseExecuteHistoryItem, rowIndex: number) {
activeReportId.value = record.id;
activeReportIndex.value = rowIndex;
showResponse.value = true;
const result = await getReportById(record.id);
await loadedReport(result);
} catch (error) {
console.error(error);
}
}
onBeforeMount(() => {
loadExecuteList();
});
defineExpose({
loadExecuteList,
});
</script>
<style lang="less" scoped>

View File

@ -10,10 +10,17 @@
>
<template #label="{ tab }">
<apiMethodName
v-if="tab.id !== 'all'"
v-if="tab.id !== 'all' && tab.type === 'api'"
:method="tab.protocol === 'HTTP' ? tab.method : tab.protocol"
class="mr-[4px]"
/>
<svg-icon
v-if="tab.id !== 'all' && tab.type === 'case'"
width="16px"
height="16px"
:name="'apiCase'"
class="mr-[4px]"
/>
<a-tooltip :content="tab.name || tab.label" :mouse-enter-delay="500">
<div class="one-line-text max-w-[144px]">
{{ tab.name || tab.label }}
@ -58,14 +65,12 @@
// import MockTable from '@/views/api-test/management/components/management/mock/mockTable.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import { getLocalConfig } from '@/api/modules/user/index';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ProtocolItem } from '@/models/apiTest/common';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { LocalConfig } from '@/models/user';
import {
RequestAuthType,
RequestComposition,
@ -277,30 +282,17 @@
}
}
const apiLocalExec = ref<Record<string, any> | LocalConfig | undefined>({});
async function initLocalConfig() {
try {
const res = await getLocalConfig();
apiLocalExec.value = res.find((e) => e.type === 'API');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
const environmentSelectRef = ref<InstanceType<typeof environmentSelect>>();
const currentEnvConfig = computed<EnvConfig | undefined>(() => environmentSelectRef.value?.currentEnvConfig);
onBeforeMount(() => {
initProtocolList();
initLocalConfig();
});
/** 向孙组件提供属性 */
provide('currentEnvConfig', readonly(currentEnvConfig));
provide('defaultCaseParams', readonly(defaultCaseParams));
provide('protocols', readonly(protocols));
provide('apiLocalExec', readonly(apiLocalExec));
defineExpose({
newTab,

View File

@ -173,6 +173,7 @@ export default {
'case.execute.reportName': 'Report name',
'case.execute.pool': 'Resource pool operation',
'case.allCase': 'All Case',
'case.detail': 'Case Detail',
'case.caseName': 'Case Name',
'case.caseLevel': 'Case Level',
'case.caseEnvironment': 'Case Environment',

View File

@ -167,6 +167,7 @@ export default {
'case.execute.reportName': '报告名称',
'case.execute.pool': '资源池运行',
'case.allCase': '全部CASE',
'case.detail': '用例详情',
'case.caseName': '用例名称',
'case.caseNameRequired': '用例名称不能为空',
'case.caseNamePlaceholder': '请输入用例名称',

View File

@ -54,8 +54,7 @@
<executeButton
ref="executeRef"
class="ml-[16px]"
is-emit
:detail="requestVModel"
:execute-loading="requestVModel.executeLoading"
@execute="handleExecute"
@stop-debug="stopDebug"
/>
@ -99,12 +98,10 @@
uploadTempFileCase,
} from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { getLocalConfig } from '@/api/modules/user/index';
import { characterLimit, getGenerateId } from '@/utils';
import { RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { LocalConfig } from '@/models/user';
import {
RequestAuthType,
RequestComposition,
@ -221,18 +218,6 @@
const requestAndResponseRef = ref<InstanceType<typeof requestAndResponse>>();
const isPriorityLocalExec = computed(() => executeRef.value?.isPriorityLocalExec ?? false);
const apiLocalExec = ref<Record<string, any> | LocalConfig | undefined>({});
async function initLocalConfig() {
try {
const res = await getLocalConfig();
apiLocalExec.value = res.find((e) => e.type === 'API');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
provide('apiLocalExec', readonly(apiLocalExec));
const isShowEditStepNameInput = ref(false);
const stepNameInputRef = ref<InputInstance>();
function showEditScriptNameInput() {
@ -369,7 +354,6 @@
// (request.requestrequest null)
initQuoteCaseDetail();
}
await initLocalConfig();
}
}
);

View File

@ -23,7 +23,6 @@
<executeButton
ref="executeButtonRef"
:execute-loading="activeScenarioTab.executeLoading"
is-emit
@execute="handleExecute"
@stop-debug="handleStopExecute"
/>