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, ScenarioAssociateExportUrl,
ScenarioBatchEditScheduleUrl, ScenarioBatchEditScheduleUrl,
ScenarioBatchExportLogUrl, ScenarioBatchExportLogUrl,
ScenarioCopyStepFilesUrl,
ScenarioExportLogUrl, ScenarioExportLogUrl,
ScenarioHistoryUrl, ScenarioHistoryUrl,
ScenarioPageUrl, ScenarioPageUrl,
@ -71,6 +72,7 @@ import {
ExecuteHistoryItem, ExecuteHistoryItem,
ExecutePageParams, ExecutePageParams,
type ExportScenarioParams, type ExportScenarioParams,
type GetScenarioUnSaveStepParams,
GetSystemRequestParams, GetSystemRequestParams,
type ImportScenarioParams, type ImportScenarioParams,
ImportSystemData, ImportSystemData,
@ -376,3 +378,11 @@ export function scenarioBatchEditSchedule(data: ApiScenarioBatchScheduleConfig)
export function getScenarioStatistics(data: string[]) { export function getScenarioStatistics(data: string[]) {
return MSR.post<ScenarioStatisticsItem[]>({ url: ScenarioStatisticsUrl, data }); 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 UpdateScenarioPriorityUrl = '/api/scenario/update-priority'; // 场景更新等级
export const UpdateScenarioStatusUrl = '/api/scenario/update-status'; // 场景更新状态 export const UpdateScenarioStatusUrl = '/api/scenario/update-status'; // 场景更新状态
export const ScenarioStatisticsUrl = '/api/scenario/statistics'; // 场景执行率统计 export const ScenarioStatisticsUrl = '/api/scenario/statistics'; // 场景执行率统计
export const ScenarioCopyStepFilesUrl = '/api/scenario/step/file/copy'; // 复制步骤时复制文件
// 场景导入导出相关 // 场景导入导出相关
export const ImportScenarioUrl = '/api/scenario/import'; // 导入场景 export const ImportScenarioUrl = '/api/scenario/import'; // 导入场景

View File

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

View File

@ -375,7 +375,12 @@
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common'; import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
import { getDefinitionDetail } from '@/api/modules/api-test/management'; 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 { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';
import { getGenerateId, parseQueryParams } from '@/utils'; import { getGenerateId, parseQueryParams } from '@/utils';
@ -403,6 +408,7 @@
RequestComposition, RequestComposition,
RequestMethods, RequestMethods,
ResponseComposition, ResponseComposition,
ScenarioStepRefType,
ScenarioStepType, ScenarioStepType,
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
@ -564,6 +570,7 @@
}; };
const requestVModel = ref<RequestParam>(defaultApiParams); const requestVModel = ref<RequestParam>(defaultApiParams);
const copyStepFileIdsMap = ref<Record<string, any>>({});
// //
const _stepType = computed(() => { const _stepType = computed(() => {
if (props.step) { if (props.step) {
@ -994,7 +1001,8 @@
requestVModel.value.body, requestVModel.value.body,
undefined, undefined,
props.fileParams?.uploadFileIds || requestVModel.value.uploadFileIds, // api requestVModel 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 = { requestParams = {
authConfig: requestVModel.value.authConfig, authConfig: requestVModel.value.authConfig,
@ -1208,7 +1216,20 @@
const res = await getDefinitionDetail(props.step?.resourceId || ''); const res = await getDefinitionDetail(props.step?.resourceId || '');
let parseRequestBodyResult; let parseRequestBodyResult;
if (res.protocol === 'HTTP') { 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 = { requestVModel.value = {
executeLoading: false, executeLoading: false,

View File

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

View File

@ -3,7 +3,7 @@ import { cloneDeep } from 'lodash-es';
import type { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types'; 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 { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app'; 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 type { Scenario, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import type { RequestParam } from '../common/customApiDrawer.vue';
import getStepType from '../common/stepType/utils'; import getStepType from '../common/stepType/utils';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils'; import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
@ -57,9 +58,28 @@ export default function useStepOperation({
try { try {
appStore.showLoading(); appStore.showLoading();
const res = await getScenarioStep(step.copyFromStepId || step.id); 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) { 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] = { stepDetails.value[step.id] = {
...res, ...res,
@ -68,9 +88,12 @@ export default function useStepOperation({
method: step.config.method || '', method: step.config.method || '',
...parseRequestBodyResult, ...parseRequestBodyResult,
}; };
if (!step.copyFromStepId && step.refType !== ScenarioStepRefType.COPY) {
// 复制的步骤文件都是新的,不需要记录,等详情抽屉关闭时会处理
scenario.value.stepFileParam[step.id] = { scenario.value.stepFileParam[step.id] = {
...parseRequestBodyResult, ...parseRequestBodyResult,
}; };
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);

View File

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