feat(接口测试): 场景复制步骤&复制场景文件

This commit is contained in:
baiqi 2024-12-05 16:26:10 +08:00 committed by Craftsman
parent e83985a2f5
commit 0608840cb0
8 changed files with 234 additions and 108 deletions

View File

@ -36,6 +36,7 @@ import {
ScenarioAssociateExportUrl,
ScenarioBatchEditScheduleUrl,
ScenarioBatchExportLogUrl,
ScenarioCopyStepFilesUrl,
ScenarioExportLogUrl,
ScenarioHistoryUrl,
ScenarioPageUrl,
@ -71,6 +72,7 @@ import {
ExecuteHistoryItem,
ExecutePageParams,
type ExportScenarioParams,
type GetScenarioUnSaveStepParams,
GetSystemRequestParams,
type ImportScenarioParams,
ImportSystemData,
@ -376,3 +378,11 @@ export function scenarioBatchEditSchedule(data: ApiScenarioBatchScheduleConfig)
export function getScenarioStatistics(data: string[]) {
return MSR.post<ScenarioStatisticsItem[]>({ url: ScenarioStatisticsUrl, data });
}
// 复制步骤时复制文件
export function scenarioCopyStepFiles(data: GetScenarioUnSaveStepParams) {
return MSR.post<Record<string, any>>({
url: ScenarioCopyStepFilesUrl,
data,
});
}

View File

@ -29,6 +29,7 @@ export const BatchRunScenarioUrl = '/api/scenario/batch-operation/run'; // 批
export const UpdateScenarioPriorityUrl = '/api/scenario/update-priority'; // 场景更新等级
export const UpdateScenarioStatusUrl = '/api/scenario/update-status'; // 场景更新状态
export const ScenarioStatisticsUrl = '/api/scenario/statistics'; // 场景执行率统计
export const ScenarioCopyStepFilesUrl = '/api/scenario/step/file/copy'; // 复制步骤时复制文件
// 场景导入导出相关
export const ImportScenarioUrl = '/api/scenario/import'; // 导入场景

View File

@ -571,3 +571,13 @@ export interface ScenarioStatisticsItem {
id: string;
execPassRate: string;
}
// 场景未保存步骤请求参数
export interface GetScenarioUnSaveStepParams {
copyFromStepId?: string;
resourceId?: string;
stepType?: string;
refType: string;
isTempFile: boolean; // 复制未保存的步骤时 true
fileIds?: string[]; // 未保存的步骤文件 id复制未加载/修改过详情的步骤时无需传
}

View File

@ -31,12 +31,17 @@ export interface ParseResult {
/**
* /Mock body
* @param body body
* @param response
* @param saveUploadFileIds id
* @param saveLinkFileIds id
* @param newFileMap id
*/
export function parseRequestBodyFiles(
body: ExecuteBody | MockBody,
response?: ResponseDefinition[],
saveUploadFileIds?: string[],
saveLinkFileIds?: string[]
saveLinkFileIds?: string[],
newFileMap?: Record<string, string>
): ParseResult {
const { binaryBody } = body;
const uploadFileIds = new Set<string>(); // 存储本地上传的文件 id 集合
@ -45,49 +50,58 @@ export function parseRequestBodyFiles(
const tempSaveLinkFileIds = new Set<string>(); // 临时存储 body 内已保存的关联文件 id 集合,用于对比 saveLinkFileIds 以判断有哪些文件被取消关联
// 获取上传文件和关联文件
const formValues =
((body as ExecuteBody).formDataBody?.formValues || (body as MockBody).formDataBody?.matchRules || []).filter(
(e) => e
) || [];
(body as ExecuteBody).formDataBody?.formValues || (body as MockBody).formDataBody?.matchRules || [] || [];
for (let i = 0; i < formValues.length; i++) {
const item = formValues[i];
if (item) {
if (item.paramType === RequestParamsType.FILE) {
if (item.files) {
for (let j = 0; j < item.files.length; j++) {
const file = item.files[j];
let { fileId } = file;
if (newFileMap && newFileMap[fileId]) {
fileId = newFileMap[fileId];
file.fileId = fileId;
}
if (file.local) {
// 本地上传的文件
if (saveUploadFileIds) {
// 如果有已保存的上传文件id集合
if (saveUploadFileIds.includes(file.fileId)) {
if (saveUploadFileIds.includes(fileId)) {
// 当前文件是已保存的文件,存入 tempSaveUploadFileIds
tempSaveUploadFileIds.add(file.fileId);
tempSaveUploadFileIds.add(fileId);
} else {
// 当前文件不是已保存的文件,存入 uploadFileIds
uploadFileIds.add(file.fileId);
uploadFileIds.add(fileId);
}
} else {
// 没有已保存的文件id集合直接存入 uploadFileIds
uploadFileIds.add(file.fileId);
uploadFileIds.add(fileId);
}
} else if (saveLinkFileIds) {
// 如果有已保存的关联文件id集合
if (saveLinkFileIds.includes(file.fileId)) {
if (saveLinkFileIds.includes(fileId)) {
// 当前文件是已保存的文件,存入
tempSaveLinkFileIds.add(file.fileId);
tempSaveLinkFileIds.add(fileId);
} else {
// 当前文件不是已保存的文件,存入 uploadFileIds
linkFileIds.add(file.fileId);
linkFileIds.add(fileId);
}
} else {
// 关联的文件
linkFileIds.add(file.fileId);
linkFileIds.add(fileId);
}
}
}
}
}
}
if (binaryBody && binaryBody.file) {
const { fileId } = binaryBody.file;
let { fileId } = binaryBody.file;
if (newFileMap && newFileMap[fileId]) {
fileId = newFileMap[fileId];
binaryBody.file.fileId = fileId;
}
if (binaryBody.file?.local) {
if (saveUploadFileIds) {
// 如果有已保存的上传文件id集合
@ -119,7 +133,11 @@ export function parseRequestBodyFiles(
if (response) {
response.forEach((res) => {
if (res.body.binaryBody && res.body.binaryBody.file) {
const { fileId } = res.body.binaryBody.file;
let { fileId } = res.body.binaryBody.file;
if (newFileMap && newFileMap[fileId]) {
fileId = newFileMap[fileId];
res.body.binaryBody.file.fileId = fileId;
}
if (res.body.binaryBody.file?.local) {
if (saveUploadFileIds) {
// 如果有已保存的上传文件id集合

View File

@ -375,7 +375,12 @@
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
import { getDefinitionDetail } from '@/api/modules/api-test/management';
import { getTransferOptions, stepTransferFile, uploadTempFile } from '@/api/modules/api-test/scenario';
import {
getTransferOptions,
scenarioCopyStepFiles,
stepTransferFile,
uploadTempFile,
} from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { getGenerateId, parseQueryParams } from '@/utils';
@ -403,6 +408,7 @@
RequestComposition,
RequestMethods,
ResponseComposition,
ScenarioStepRefType,
ScenarioStepType,
} from '@/enums/apiEnum';
@ -564,6 +570,7 @@
};
const requestVModel = ref<RequestParam>(defaultApiParams);
const copyStepFileIdsMap = ref<Record<string, any>>({});
//
const _stepType = computed(() => {
if (props.step) {
@ -994,7 +1001,8 @@
requestVModel.value.body,
undefined,
props.fileParams?.uploadFileIds || requestVModel.value.uploadFileIds, // api requestVModel
props.fileParams?.linkFileIds || requestVModel.value.linkFileIds // api requestVModel
props.fileParams?.linkFileIds || requestVModel.value.linkFileIds, // api requestVModel
copyStepFileIdsMap.value
);
requestParams = {
authConfig: requestVModel.value.authConfig,
@ -1208,7 +1216,20 @@
const res = await getDefinitionDetail(props.step?.resourceId || '');
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body, res.response); // id
if ((props.step?.copyFromStepId || props.step?.refType === ScenarioStepRefType.COPY) && props.step?.isNew) {
//
copyStepFileIdsMap.value = await scenarioCopyStepFiles({
copyFromStepId: props.step?.copyFromStepId,
resourceId: props.step?.resourceId,
stepType: props.step?.stepType,
refType: props.step?.refType,
isTempFile: false, // true
fileIds: Object.values(parseRequestBodyFiles(res.request.body, [], [], [])).flat(),
});
parseRequestBodyFiles(res.body, [], [], [], copyStepFileIdsMap.value);
} else {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body, [], [], [], copyStepFileIdsMap.value); // id
}
}
requestVModel.value = {
executeLoading: false,

View File

@ -482,6 +482,7 @@
import saveAsApiModal from '@/views/api-test/components/saveAsApiModal.vue';
import { addCase, getDefinitionDetail } from '@/api/modules/api-test/management';
import { scenarioCopyStepFiles } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
@ -514,6 +515,7 @@
import useStepNodeEdit from './useStepNodeEdit';
import useStepOperation from './useStepOperation';
import { casePriorityOptions, caseStatusOptions } from '@/views/api-test/components/config';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
@ -919,23 +921,50 @@
* 复制步骤
* @param node 复制的节点
*/
function copyStep(node: MsTreeNodeData) {
async function copyStep(node: MsTreeNodeData) {
loading.value = true;
try {
const id = getGenerateId();
const stepDetail = stepDetails.value[node.id];
const stepFileParam = scenario.value.stepFileParam[node.id];
const { isQuoteScenario } = getStepType(node as ScenarioStepItem);
let { copyFromStepId } = node;
if (stepDetail || node.isNew !== true || !node.copyFromStepId) {
// id
// id
copyFromStepId = node.id;
}
let parseRequestBodyResult: Record<string, any> = {
uploadFileIds: [],
linkFileIds: [],
deleteFileIds: [], // id
unLinkFileIds: [], // id
};
let newFileRes;
if (node.config.protocol === 'HTTP' && (stepDetail as RequestParam)?.body) {
if (node.copyFromStepId || node.refType === ScenarioStepRefType.COPY) {
//
newFileRes = await scenarioCopyStepFiles({
copyFromStepId,
resourceId: node.resourceId,
stepType: node.stepType,
refType: node.refType,
isTempFile: !!stepDetail, // true
fileIds: Object.values(parseRequestBodyFiles((stepDetail as RequestParam).body, [], [], [])).flat(),
});
parseRequestBodyFiles((stepDetail as RequestParam).body, [], [], [], newFileRes);
} else {
parseRequestBodyResult = parseRequestBodyFiles((stepDetail as RequestParam).body, [], [], [], newFileRes); // id
}
}
if (stepDetail) {
//
stepDetails.value[id] = cloneDeep({
...stepDetail,
stepId: id,
uniqueId: id,
...parseRequestBodyResult,
});
}
if (stepFileParam) {
//
scenario.value.stepFileParam[id] = cloneDeep(stepFileParam);
}
insertNodes<ScenarioStepItem>(
steps.value,
node.uniqueId,
@ -944,16 +973,11 @@
mapTree<ScenarioStepItem>(node, (childNode) => {
const childId = getGenerateId();
const childStepDetail = stepDetails.value[childNode.id];
const childStepFileParam = scenario.value.stepFileParam[childNode.id];
let childCopyFromStepId = childNode.id;
if (childStepDetail) {
//
stepDetails.value[childId] = cloneDeep(childStepDetail);
}
if (childStepFileParam) {
//
scenario.value.stepFileParam[childNode.id] = cloneDeep(childStepFileParam);
}
if (!isQuoteScenario) {
// id
if (childStepDetail || (childNode.isNew && childNode.stepRefType === ScenarioStepRefType.REF)) {
@ -975,7 +999,7 @@
})[0]
),
name: `copy_${node.name}`.substring(0, 255),
copyFromStepId: stepDetail || node.isNew !== true ? node.id : node.copyFromStepId,
copyFromStepId,
sort: node.sort + 1,
isNew: true,
id,
@ -986,6 +1010,12 @@
'uniqueId'
);
scenario.value.unSaved = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
loading.value = false;
}
}
async function handleStepMoreActionSelect(item: ActionsItem, node: MsTreeNodeData) {
@ -1189,6 +1219,14 @@
appStore.currentProjectId
);
const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps);
insertSteps.forEach((step) => {
scenario.value.stepFileParam[step.id] = {
linkFileIds: [],
uploadFileIds: [],
deleteFileIds: [],
unLinkFileIds: [],
};
});
if (activeStepByCreate.value && activeCreateAction.value) {
handleCreateSteps(
activeStepByCreate.value,

View File

@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es';
import type { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import { getScenarioStep } from '@/api/modules/api-test/scenario';
import { getScenarioStep, scenarioCopyStepFiles } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
@ -12,6 +12,7 @@ import { deleteNode, findNodeByKey, handleTreeDragDrop, mapTree } from '@/utils'
import type { Scenario, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import type { RequestParam } from '../common/customApiDrawer.vue';
import getStepType from '../common/stepType/utils';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
@ -57,9 +58,28 @@ export default function useStepOperation({
try {
appStore.showLoading();
const res = await getScenarioStep(step.copyFromStepId || step.id);
let parseRequestBodyResult;
let parseRequestBodyResult: Record<string, any> = {
uploadFileIds: [],
linkFileIds: [],
deleteFileIds: [], // 存储对比已保存的文件后,需要删除的文件 id 集合
unLinkFileIds: [], // 存储对比已保存的文件后,需要取消关联的文件 id 集合
};
let newFileRes;
if (step.config.protocol === 'HTTP' && res.body) {
parseRequestBodyResult = parseRequestBodyFiles(res.body); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
if ((step.copyFromStepId || step.refType === ScenarioStepRefType.COPY) && step.isNew) {
// 复制的步骤需要复制文件
newFileRes = await scenarioCopyStepFiles({
copyFromStepId: step.copyFromStepId,
resourceId: step.resourceId,
stepType: step.stepType,
refType: step.refType,
isTempFile: false, // 复制未保存的步骤时 true
fileIds: Object.values(parseRequestBodyFiles((res as RequestParam).body, [], [], [])).flat(),
});
parseRequestBodyFiles(res.body, [], [], [], newFileRes);
} else {
parseRequestBodyResult = parseRequestBodyFiles(res.body, [], [], [], newFileRes); // 解析请求体中的文件,将详情中的文件 id 集合收集,更新时以判断文件是否删除以及是否新上传的文件
}
}
stepDetails.value[step.id] = {
...res,
@ -68,9 +88,12 @@ export default function useStepOperation({
method: step.config.method || '',
...parseRequestBodyResult,
};
if (!step.copyFromStepId && step.refType !== ScenarioStepRefType.COPY) {
// 复制的步骤文件都是新的,不需要记录,等详情抽屉关闭时会处理
scenario.value.stepFileParam[step.id] = {
...parseRequestBodyResult,
};
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);

View File

@ -153,7 +153,7 @@
import router from '@/router';
import useAppStore from '@/store/modules/app';
import useCacheStore from '@/store/modules/cache/cache';
import { filterTree, getGenerateId, mapTree } from '@/utils';
import { filterTree, getGenerateId, mapTree, traverseTree } from '@/utils';
import { hasAnyPermission } from '@/utils/permission';
import { RequestResult } from '@/models/apiTest/common';
@ -425,6 +425,7 @@
if (isCopy) {
// copyFromStepId
copySteps = mapTree(defaultScenarioInfo.steps, (node) => {
node.isNew = true;
node.copyFromStepId = node.id;
if (
node.parent &&
@ -677,6 +678,10 @@
}),
});
}
traverseTree(activeScenarioTab.value.steps, (node) => {
node.isNew = false;
});
activeScenarioTab.value.stepFileParam = {};
refreshTree(tempTableQueryParams.value);
Message.success(activeScenarioTab.value.isNew ? t('common.createSuccess') : t('common.saveSuccess'));
activeScenarioTab.value.unSaved = false;