feat(接口场景): 场景导入场景&部分问题修复

This commit is contained in:
baiqi 2024-03-27 18:14:37 +08:00 committed by Craftsman
parent 42d41df3f1
commit dee6bc7b32
13 changed files with 291 additions and 109 deletions

View File

@ -18,6 +18,7 @@ import {
GetModuleTreeUrl,
GetScenarioStepUrl,
GetScenarioUrl,
GetSystemRequestUrl,
GetTrashModuleCountUrl,
GetTrashModuleTreeUrl,
MoveModuleUrl,
@ -45,6 +46,7 @@ import {
ApiScenarioUpdateDTO,
ExecuteHistoryItem,
ExecutePageParams,
GetSystemRequestParams,
Scenario,
ScenarioDetail,
ScenarioHistoryItem,
@ -231,3 +233,8 @@ export function debugScenario(data: ApiScenarioDebugRequest) {
export function executeScenario(data: ApiScenarioDebugRequest) {
return MSR.post({ url: ExecuteScenarioUrl, data });
}
// 获取导入的系统请求数据
export function getSystemRequest(data: GetSystemRequestParams) {
return MSR.post<ApiScenarioTableItem[]>({ url: GetSystemRequestUrl, data });
}

View File

@ -15,6 +15,7 @@ export const ScenarioTransferFileUrl = '/api/scenario/transfer'; // 接口场景
export const ScenarioTransferModuleOptionsUrl = '/api/scenario/transfer/options'; // 接口场景临时文件转存目录
export const DebugScenarioUrl = '/api/scenario/debug'; // 接口场景调试(不保存报告)
export const ExecuteScenarioUrl = '/api/scenario/run'; // 接口场景执行(保存报告)
export const GetSystemRequestUrl = '/api/scenario/get/system-request'; // 获取导入的系统请求数据
export const BatchRecycleScenarioUrl = '/api/scenario/batch-operation/delete-gc'; // 批量删除接口场景
export const BatchMoveScenarioUrl = '/api/scenario/batch-operation/move'; // 批量移动接口场景
export const BatchCopyScenarioUrl = '/api/scenario/batch-operation/copy'; // 批量复制接口场景

View File

@ -407,3 +407,19 @@ export interface ApiScenarioUpdateDTO extends Partial<Scenario> {
deleteFileIds?: string[];
unLinkFileIds?: string[];
}
export interface GetSystemRequestTypeParams {
moduleIds?: (string | number)[];
selectedIds: (string | number)[];
unselectedIds: (string | number)[];
projectId: string;
protocol?: string;
versionId?: string;
}
export interface GetSystemRequestParams {
apiRequest?: GetSystemRequestTypeParams;
caseRequest?: GetSystemRequestTypeParams;
scenarioRequest?: GetSystemRequestTypeParams;
refType: ScenarioStepRefType.COPY | ScenarioStepRefType.REF;
}

View File

@ -235,7 +235,7 @@
if (matchesIterator) {
const matches = Array.from(matchesIterator);
try {
if (expressionForm.value.expressionMatchingRule === 'EXPRESSION') {
if (expressionForm.value.expressionMatchingRule === RequestExtractExpressionRuleType.EXPRESSION) {
//
matchResult.value = matches.map((e) => e[0]) || [];
} else {

View File

@ -380,6 +380,8 @@
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
width: 250,
showTooltip: true,
},
{
title: 'apiTestManagement.paramVal',

View File

@ -362,7 +362,7 @@
export type RequestParam = ExecuteApiRequestFullParams & {
response?: RequestTaskResult;
customizeRequestEnvEnable: boolean;
customizeRequestEnvEnable?: boolean;
} & RequestCustomAttr;
const props = defineProps<{

View File

@ -1,8 +1,6 @@
<template>
<MsDrawer
v-model:visible="visible"
unmount-on-close
:mask="false"
:width="900"
:footer="false"
show-full-screen
@ -88,17 +86,13 @@
import requestAndResponse from '@/views/api-test/components/requestAndResponse.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import {
debugCase,
getCaseDetail,
getTransferOptionsCase,
runCase,
transferFileCase,
uploadTempFileCase,
} from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { characterLimit, getGenerateId } from '@/utils';
import { characterLimit } from '@/utils';
import { RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
@ -121,6 +115,8 @@
const emit = defineEmits<{
(e: 'applyStep', request: RequestParam): void;
(e: 'deleteStep'): void;
(e: 'execute', request: RequestParam, executeType?: 'localExec' | 'serverExec'): void;
(e: 'stopDebug'): void;
}>();
const { t } = useI18n();
@ -208,11 +204,23 @@
() =>
activeStep.value?.stepType === ScenarioStepType.API_CASE && activeStep.value?.refType === ScenarioStepRefType.REF
);
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const stepName = ref(activeStep.value?.name);
watchEffect(() => {
stepName.value = activeStep.value?.name;
});
watch(
() => props.stepResponses,
(val) => {
if (val && val[requestVModel.value.stepId]) {
requestVModel.value.executeLoading = false;
}
},
{
deep: true,
}
);
const executeRef = ref<InstanceType<typeof executeButton>>();
const requestAndResponseRef = ref<InstanceType<typeof requestAndResponse>>();
@ -230,66 +238,31 @@
isShowEditStepNameInput.value = 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 (requestVModel.value.reportId === data.reportId) {
// tabtab
requestVModel.value.response = data.taskResult; //
requestVModel.value.executeLoading = false;
} else {
// tab
temporaryResponseMap[data.reportId] = data.taskResult;
}
} else if (data.msgType === 'EXEC_END') {
// websocket
websocket.value?.close();
requestVModel.value.executeLoading = false;
}
});
}
/**
* 执行调试
* @param val 执行类型
*/
async function handleExecute(executeType?: 'localExec' | 'serverExec') {
try {
requestVModel.value.executeLoading = true;
requestVModel.value.response = cloneDeep(defaultResponse);
const makeRequestParams = requestAndResponseRef.value?.makeRequestParams(executeType); // reportIdreportId
reportId.value = getGenerateId();
requestVModel.value.reportId = reportId.value; // ID
debugSocket(executeType); // websocket
let res;
const params = {
apiDefinitionId: requestVModel.value.apiDefinitionId,
...makeRequestParams,
reportId: reportId.value,
};
if (!(requestVModel.value.resourceId as string).startsWith('c') && executeType === 'serverExec') {
//
res = await runCase(params);
} else {
res = await debugCase(params);
}
if (executeType === 'localExec') {
await localExecuteApiDebug(executeRef.value?.localExecuteUrl ?? '', res);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
requestVModel.value.executeLoading = false;
requestVModel.value.executeLoading = true;
if (isHttpProtocol.value) {
emit('execute', requestAndResponseRef.value?.makeRequestParams(executeType), executeType);
} else {
//
// fApi.value?.validate(async (valid) => {
// if (valid === true) {
// emit('execute', requestAndResponseRef.value?.makeRequestParams(executeType), executeType);
// } else {
// requestVModel.value.activeTab = RequestComposition.PLUGIN;
// nextTick(() => {
// scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
// });
// }
// });
}
}
function stopDebug() {
websocket.value?.close();
requestVModel.value.executeLoading = false;
emit('stopDebug');
}
function handleClose() {

View File

@ -19,6 +19,7 @@
<div class="mb-[12px] flex items-center gap-[8px]">
<MsProjectSelect v-model:project="currentProject" @change="resetModule" />
<a-select
v-if="activeKey !== 'scenario'"
v-model:model-value="protocol"
:options="protocolOptions"
class="w-[90px]"
@ -70,11 +71,11 @@
</MsButton>
</div>
<div class="flex items-center gap-[12px]">
<a-button type="secondary" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :disabled="totalSelected === 0" @click="handleCopy">
<a-button type="secondary" :disabled="loading" @click="handleCancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :loading="loading" :disabled="totalSelected === 0" @click="handleCopy">
{{ t('common.copy') }}
</a-button>
<a-button type="primary" :disabled="totalSelected === 0" @click="handleQuote">
<a-button type="primary" :loading="loading" :disabled="totalSelected === 0" @click="handleQuote">
{{ t('common.quote') }}
</a-button>
</div>
@ -96,11 +97,13 @@
import apiTable from './table.vue';
import { getProtocolList } from '@/api/modules/api-test/common';
import { getSystemRequest } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import { ApiScenarioTableItem } from '@/models/apiTest/scenario';
import type { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import type { ApiScenarioTableItem } from '@/models/apiTest/scenario';
import { ScenarioStepRefType } from '@/enums/apiEnum';
export interface ImportData {
api: MsTableDataItem<ApiDefinitionDetail>[];
@ -121,6 +124,7 @@
});
const activeKey = ref<'api' | 'case' | 'scenario'>('api');
const loading = ref(false);
const selectedApis = ref<MsTableDataItem<ApiDefinitionDetail>[]>([]);
const selectedCases = ref<MsTableDataItem<ApiCaseDetail>[]>([]);
const selectedScenarios = ref<MsTableDataItem<ApiScenarioTableItem>[]>([]);
@ -167,7 +171,9 @@
const moduleIds = ref<(string | number)[]>([]);
function resetModule() {
moduleTreeRef.value?.init(activeKey.value);
nextTick(() => {
moduleTreeRef.value?.init(activeKey.value);
});
}
function handleModuleSelect(ids: (string | number)[], node: MsTreeNodeData) {
@ -187,28 +193,100 @@
visible.value = false;
}
function handleCopy() {
emit(
'copy',
cloneDeep({
api: selectedApis.value,
case: selectedCases.value,
scenario: selectedScenarios.value,
})
);
handleCancel();
async function getScenarioSteps(refType: ScenarioStepRefType.COPY | ScenarioStepRefType.REF) {
const scenarioMap: Record<string, MsTableDataItem<ApiScenarioTableItem>[]> = {};
selectedScenarios.value.forEach((e) => {
if (!scenarioMap[e.projectId]) {
scenarioMap[e.projectId] = [];
}
scenarioMap[e.projectId].push(e);
});
const scenarioRequestArr: any[] = [];
Object.keys(scenarioMap).forEach((projectId) => {
scenarioRequestArr.push(
getSystemRequest({
scenarioRequest: {
projectId,
unselectedIds: [],
selectedIds: scenarioMap[projectId].map((e) => e.id),
},
refType,
})
);
});
try {
loading.value = true;
const allRes = await Promise.all(scenarioRequestArr);
let fullScenarioArr: MsTableDataItem<ApiScenarioTableItem>[] = [];
allRes.forEach((res) => {
fullScenarioArr.push(...res);
});
if (refType === ScenarioStepRefType.COPY) {
fullScenarioArr = fullScenarioArr.map((e) => {
return {
...e,
name: `copy-${e.name}`,
copyFromStepId: e.id,
};
});
emit(
'copy',
cloneDeep({
api: selectedApis.value,
case: selectedCases.value,
scenario: fullScenarioArr,
})
);
handleCancel();
} else {
emit(
'quote',
cloneDeep({
api: selectedApis.value,
case: selectedCases.value,
scenario: fullScenarioArr,
})
);
handleCancel();
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
function handleQuote() {
emit(
'quote',
cloneDeep({
api: selectedApis.value,
case: selectedCases.value,
scenario: selectedScenarios.value,
})
);
handleCancel();
async function handleCopy() {
if (selectedScenarios.value.length > 0) {
await getScenarioSteps(ScenarioStepRefType.COPY);
} else {
emit(
'copy',
cloneDeep({
api: selectedApis.value,
case: selectedCases.value,
scenario: selectedScenarios.value,
})
);
handleCancel();
}
}
async function handleQuote() {
if (selectedScenarios.value.length > 0) {
await getScenarioSteps(ScenarioStepRefType.REF);
} else {
emit(
'quote',
cloneDeep({
api: selectedApis.value,
case: selectedCases.value,
scenario: selectedScenarios.value,
})
);
handleCancel();
}
}
onBeforeMount(() => {

View File

@ -191,7 +191,72 @@
//
const useCaseTable = useTable(getCasePage, tableConfig);
//
const useScenarioTable = useTable(getScenarioPage, tableConfig);
const useScenarioTable = useTable(getScenarioPage, {
...tableConfig,
columns: [
{
title: 'ID',
dataIndex: 'num',
slotName: 'num',
sortIndex: 1,
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
fixed: 'left',
width: 100,
showTooltip: true,
columnSelectorDisabled: true,
},
{
title: 'apiScenario.table.columns.name',
dataIndex: 'name',
sortable: {
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 134,
showTooltip: true,
columnSelectorDisabled: true,
},
{
title: 'apiScenario.table.columns.level',
dataIndex: 'priority',
slotName: 'priority',
width: 100,
},
{
title: 'apiScenario.table.columns.status',
dataIndex: 'status',
slotName: 'status',
titleSlotName: 'statusFilter',
width: 140,
},
{
title: 'apiScenario.table.columns.tags',
dataIndex: 'tags',
isTag: true,
isStringTag: true,
width: 240,
},
{
title: 'apiScenario.table.columns.scenarioEnv',
dataIndex: 'environmentName',
width: 159,
},
{
title: 'apiScenario.table.columns.steps',
dataIndex: 'stepTotal',
width: 100,
},
{
title: 'apiScenario.table.columns.module',
dataIndex: 'modulePath',
width: 120,
showTooltip: true,
},
],
});
const methodFilterVisible = ref(false);
const methodFilters = ref(Object.keys(RequestMethods));
@ -324,6 +389,7 @@
case 'scenario':
default:
routeName = ApiTestRouteEnum.API_TEST_SCENARIO;
query.sId = id;
break;
}
openNewPage(routeName, query);

View File

@ -133,8 +133,10 @@ export default function useCreateActions() {
...defaultStepItemCommon.config,
...config,
},
children: item.children || [],
stepType,
refType,
copyFromStepId: item.copyFromStepId,
...resourceField,
name: name || item.name,
sort: startOrder + index,

View File

@ -127,7 +127,7 @@
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import { deleteNodes, filterTree, getGenerateId } from '@/utils';
import { deleteNodes, filterTree, getGenerateId, mapTree } from '@/utils';
import { countNodes } from '@/utils/tree';
import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario';
@ -207,16 +207,22 @@
async function handleBeforeBatchToggle(done: (closed: boolean) => void) {
try {
let ids = checkedKeys.value;
const ids = new Set(checkedKeys.value);
if (batchToggleRange.value === 'top') {
ids = scenario.value.steps.map((item) => item.id);
scenario.value.steps = scenario.value.steps.map((item) => {
if (ids.has(item.id)) {
item.enable = isBatchEnable.value;
}
return item;
});
} else {
scenario.value.steps = mapTree(scenario.value.steps, (node) => {
if (ids.has(node.id)) {
node.enable = isBatchEnable.value;
}
return node;
});
}
console.log('ids', ids);
await new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, 1000);
});
done(true);
Message.success(isBatchEnable.value ? t('common.enableSuccess') : t('common.disableSuccess'));
} catch (error) {
@ -228,6 +234,10 @@
function batchDelete() {
deleteNodes(scenario.value.steps, checkedKeys.value, 'id');
Message.success(t('common.deleteSuccess'));
if (scenario.value.steps.length === 0) {
checkedAll.value = false;
indeterminate.value = false;
}
}
function checkReport() {

View File

@ -60,7 +60,7 @@
<div class="mr-[8px] flex items-center gap-[8px]">
<!-- 步骤启用/禁用 -->
<a-switch
:default-checked="step.enable"
v-model:model-value="step.enable"
size="small"
@click.stop="handleStepToggleEnable(step)"
></a-switch>
@ -242,6 +242,8 @@
:step-responses="scenario.stepResponses"
@apply-step="applyApiStep"
@delete-step="deleteCaseStep"
@stop-debug="handleStopExecute(activeStep)"
@execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)"
/>
<importApiDrawer
v-if="importApiDrawerVisible"
@ -519,6 +521,7 @@
case 'copy':
const id = getGenerateId();
const stepDetail = stepDetails.value[node.id];
const { isQuoteScenario } = getStepType(node as ScenarioStepItem);
if (stepDetail) {
//
stepDetails.value[id] = cloneDeep(stepDetail);
@ -531,13 +534,25 @@
mapTree<ScenarioStepItem>(node, (childNode) => {
const childId = getGenerateId();
const childStepDetail = stepDetails.value[node.id];
let childCopyFromStepId = childNode.id;
if (childStepDetail) {
//
stepDetails.value[childId] = cloneDeep(childStepDetail);
}
if (!isQuoteScenario) {
// id
if (childStepDetail || (childNode.isNew && childNode.stepRefType === ScenarioStepRefType.REF)) {
// id
// id
childCopyFromStepId = childNode.id;
} else if (childNode.isNew && childNode.stepRefType === ScenarioStepRefType.COPY) {
// id
childCopyFromStepId = childNode.copyFromStepId;
}
}
return {
...cloneDeep(childNode),
copyFromStepId: childNode.id,
copyFromStepId: childCopyFromStepId,
id: childId,
};
})[0]
@ -545,7 +560,7 @@
name: `copy-${node.name}`,
copyFromStepId: node.id,
sort: node.sort + 1,
isNew: false,
isNew: true,
id,
},
'after',
@ -819,7 +834,12 @@
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.id, 'id');
if (realStep) {
realStep.reportId = getGenerateId();
realStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
if (
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(realStep.stepType)
) {
//
realStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
}
const stepDetail = stepDetails.value[realStep.id];
delete scenario.value.stepResponses[realStep.id]; //
realExecute(

View File

@ -84,6 +84,7 @@
* @description 接口测试-接口场景主页
*/
import { useRoute } from 'vue-router';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import dayjs from 'dayjs';
@ -132,6 +133,7 @@
export type ScenarioParams = Scenario & TabItem;
const route = useRoute();
const appStore = useAppStore();
const { t } = useI18n();
@ -211,8 +213,6 @@
});
}
onBeforeMount(selectRecycleCount);
const createRef = ref<InstanceType<typeof create>>();
const saveLoading = ref(false);
@ -278,10 +278,10 @@
}
}
async function openScenarioTab(record: ApiScenarioTableItem, isCopy?: boolean) {
async function openScenarioTab(record: ApiScenarioTableItem | string, isCopy?: boolean) {
try {
appStore.showLoading();
const res = await getScenarioDetail(record.id);
const res = await getScenarioDetail(typeof record === 'string' ? record : record.id);
res.stepDetails = {};
if (!res.steps) {
res.steps = [];
@ -297,6 +297,13 @@
}
}
onBeforeMount(() => {
selectRecycleCount();
if (route.query.sId) {
openScenarioTab(route.query.sId as string);
}
});
const websocket = ref<WebSocket>();
const temporaryScenarioReportMap = {}; // websockettab