feat(接口场景): 场景步骤批量初步调试&插件问题修复
This commit is contained in:
parent
a2c6ba1b88
commit
82474851b4
|
@ -322,6 +322,7 @@ export interface ScenarioStepItem {
|
|||
name: string;
|
||||
executeStatus?: ScenarioExecuteStatus;
|
||||
enable: boolean; // 是否启用
|
||||
copyFromStepId?: string; // 如果步骤是复制的,这个字段是复制的步骤id;如果复制的步骤也是复制的,并且没有加载过详情,则这个 id 是最原始的 被复制的步骤 id
|
||||
resourceId?: string; // 详情或者引用的类型才有
|
||||
resourceNum?: string; // 详情或者引用的类型才有
|
||||
stepType: ScenarioStepType;
|
||||
|
@ -368,7 +369,9 @@ export interface Scenario {
|
|||
executeTime?: string | number; // 执行时间
|
||||
executeSuccessCount?: number; // 执行成功数量
|
||||
executeFailCount?: number; // 执行失败数量
|
||||
reportId?: string;
|
||||
reportId?: string | number; // 场景报告 id
|
||||
stepReportId?: string | number; // 步骤报告 id(单个或批量调试)
|
||||
isExecute?: boolean; // 是否从列表执行进去场景详情
|
||||
}
|
||||
export interface ScenarioDetail extends Scenario {
|
||||
stepTotal: number;
|
||||
|
|
|
@ -488,7 +488,7 @@ export function handleTreeDragDrop<T>(
|
|||
* @param treeArr 目标树
|
||||
* @param targetKey 目标节点唯一值
|
||||
*/
|
||||
export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string, customKey = 'key'): void {
|
||||
export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string | number, customKey = 'key'): void {
|
||||
function deleteNodeInTree(tree: TreeNode<T>[]): void {
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
|
@ -505,6 +505,28 @@ export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string, customK
|
|||
deleteNodeInTree(treeArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除树形数组中的多个节点
|
||||
* @param treeArr 目标树
|
||||
* @param targetKeys 目标节点唯一值的数组
|
||||
*/
|
||||
export function deleteNodes<T>(treeArr: TreeNode<T>[], targetKeys: (string | number)[], customKey = 'key'): void {
|
||||
const targetKeysSet = new Set(targetKeys);
|
||||
function deleteNodesInTree(tree: TreeNode<T>[]): void {
|
||||
for (let i = tree.length - 1; i >= 0; i--) {
|
||||
const node = tree[i];
|
||||
if (targetKeysSet.has(node[customKey])) {
|
||||
tree.splice(i, 1); // 直接删除当前节点
|
||||
targetKeysSet.delete(node[customKey]); // 删除后从集合中移除
|
||||
} else if (Array.isArray(node.children)) {
|
||||
deleteNodesInTree(node.children); // 递归删除子节点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
deleteNodesInTree(treeArr);
|
||||
}
|
||||
|
||||
/**
|
||||
* 找出俩数组之间的差异项并返回
|
||||
* @param targetMap 目标项
|
||||
|
|
|
@ -826,7 +826,7 @@
|
|||
* 控制插件表单字段显示
|
||||
*/
|
||||
function controlPluginFormFields() {
|
||||
const allFields = fApi.value?.fields();
|
||||
const currentFormFields = fApi.value?.fields();
|
||||
let fields: string[] = [];
|
||||
if (props.isDefinition) {
|
||||
// 接口定义使用接口定义的字段集
|
||||
|
@ -836,7 +836,14 @@
|
|||
// 根据 apiDebugFields 字段集合展示需要的字段,隐藏其他字段
|
||||
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields || [];
|
||||
}
|
||||
fApi.value?.hidden(true, allFields?.filter((e) => !fields.includes(e)) || []);
|
||||
// 确保fields展示完整
|
||||
fApi.value?.hidden(false, fields);
|
||||
if (currentFormFields && currentFormFields.length < fields.length) {
|
||||
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
|
||||
} else {
|
||||
// 隐藏多余的字段
|
||||
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
|
@ -1087,8 +1094,10 @@
|
|||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
websocket.value?.close();
|
||||
requestVModel.value.executeLoading = false;
|
||||
requestVModel.value.isExecute = false;
|
||||
if (requestVModel.value.reportId === data.reportId) {
|
||||
requestVModel.value.executeLoading = false;
|
||||
requestVModel.value.isExecute = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -650,8 +650,8 @@
|
|||
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields || [];
|
||||
}
|
||||
// 确保fields展示完整
|
||||
fApi.value?.hidden(false, fields);
|
||||
if (currentFormFields && currentFormFields.length < fields.length) {
|
||||
fApi.value?.hidden(false, fields);
|
||||
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
|
||||
} else {
|
||||
// 隐藏多余的字段
|
||||
|
|
|
@ -60,13 +60,14 @@
|
|||
<a-select
|
||||
v-model:model-value="record.status"
|
||||
class="param-input w-full"
|
||||
size="mini"
|
||||
@change="() => handleStatusChange(record)"
|
||||
>
|
||||
<template #label>
|
||||
<apiStatus :status="record.status" />
|
||||
<apiStatus :status="record.status" size="small" />
|
||||
</template>
|
||||
<a-option v-for="item of Object.values(ApiScenarioStatus)" :key="item" :value="item">
|
||||
<apiStatus :status="item" />
|
||||
<apiStatus :status="item" size="small" />
|
||||
</a-option>
|
||||
</a-select>
|
||||
</template>
|
||||
|
@ -75,6 +76,7 @@
|
|||
v-model:model-value="record.priority"
|
||||
:placeholder="t('common.pleaseSelect')"
|
||||
class="param-input w-full"
|
||||
size="mini"
|
||||
@change="() => handlePriorityStatusChange(record)"
|
||||
>
|
||||
<template #label>
|
||||
|
@ -398,9 +400,8 @@
|
|||
sorter: true,
|
||||
},
|
||||
fixed: 'left',
|
||||
width: 126,
|
||||
width: 100,
|
||||
showTooltip: true,
|
||||
showInTable: true,
|
||||
columnSelectorDisabled: true,
|
||||
},
|
||||
{
|
||||
|
@ -412,7 +413,6 @@
|
|||
},
|
||||
width: 134,
|
||||
showTooltip: true,
|
||||
showInTable: true,
|
||||
columnSelectorDisabled: true,
|
||||
},
|
||||
{
|
||||
|
@ -420,7 +420,6 @@
|
|||
dataIndex: 'priority',
|
||||
slotName: 'priority',
|
||||
width: 100,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.status',
|
||||
|
@ -428,7 +427,6 @@
|
|||
slotName: 'status',
|
||||
titleSlotName: 'statusFilter',
|
||||
width: 140,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.runResult',
|
||||
|
@ -437,15 +435,12 @@
|
|||
slotName: 'lastReportStatus',
|
||||
showTooltip: true,
|
||||
width: 100,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.tags',
|
||||
dataIndex: 'tags',
|
||||
isTag: true,
|
||||
isStringTag: true,
|
||||
width: 456,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.scenarioEnv',
|
||||
|
@ -456,19 +451,16 @@
|
|||
title: 'apiScenario.table.columns.steps',
|
||||
dataIndex: 'stepTotal',
|
||||
width: 100,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.passRate',
|
||||
dataIndex: 'requestPassRate',
|
||||
width: 100,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.module',
|
||||
dataIndex: 'modulePath',
|
||||
width: 176,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.createTime',
|
||||
|
@ -478,7 +470,6 @@
|
|||
sorter: true,
|
||||
},
|
||||
width: 189,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.updateTime',
|
||||
|
@ -488,21 +479,16 @@
|
|||
sorter: true,
|
||||
},
|
||||
width: 189,
|
||||
showDrag: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.createUser',
|
||||
dataIndex: 'createUserName',
|
||||
titleSlotName: 'createUser',
|
||||
width: 109,
|
||||
showDrag: true,
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'apiScenario.table.columns.updateUser',
|
||||
dataIndex: 'updateUserName',
|
||||
titleSlotName: 'updateUser',
|
||||
width: 109,
|
||||
showDrag: true,
|
||||
showTooltip: true,
|
||||
},
|
||||
{
|
||||
title: 'common.operation',
|
||||
|
|
|
@ -118,15 +118,22 @@
|
|||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import stepTree from './stepTree.vue';
|
||||
|
||||
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
|
||||
import { debugScenario } from '@/api/modules/api-test/scenario';
|
||||
import { getSocket } from '@/api/modules/project-management/commonScript';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { deleteNodes, filterTree, getGenerateId } from '@/utils';
|
||||
import { countNodes } from '@/utils/tree';
|
||||
|
||||
import { Scenario } from '@/models/apiTest/scenario';
|
||||
import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
|
||||
const props = defineProps<{
|
||||
isNew?: boolean; // 是否新建
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const scenario = defineModel<Scenario>('scenario', {
|
||||
|
@ -209,12 +216,9 @@
|
|||
}
|
||||
}
|
||||
|
||||
function batchDebug() {
|
||||
console.log('批量调试');
|
||||
}
|
||||
|
||||
function batchDelete() {
|
||||
console.log('批量删除');
|
||||
deleteNodes(scenario.value.steps, checkedKeys.value, 'id');
|
||||
Message.success(t('common.deleteSuccess'));
|
||||
}
|
||||
|
||||
function checkReport() {
|
||||
|
@ -225,15 +229,95 @@
|
|||
console.log('刷新步骤信息');
|
||||
}
|
||||
|
||||
async function executeScenario() {
|
||||
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
const stepReportId = ref('');
|
||||
const websocket = ref<WebSocket>();
|
||||
const temporaryStepReportMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
|
||||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
*/
|
||||
function debugSocket(executeType?: 'localExec' | 'serverExec', localExecuteUrl?: string) {
|
||||
websocket.value = getSocket(
|
||||
stepReportId.value,
|
||||
executeType === 'localExec' ? '/ws/debug' : '',
|
||||
executeType === 'localExec' ? localExecuteUrl : ''
|
||||
);
|
||||
websocket.value.addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.msgType === 'EXEC_RESULT') {
|
||||
if (scenario.value.stepReportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab
|
||||
scenario.value.executeLoading = false;
|
||||
scenario.value.isExecute = false;
|
||||
} else {
|
||||
// 不是则需要把报告缓存起来,等切换到对应的tab再赋值
|
||||
temporaryStepReportMap[data.reportId] = data.taskResult;
|
||||
}
|
||||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
websocket.value?.close();
|
||||
if (scenario.value.reportId === data.reportId) {
|
||||
scenario.value.executeLoading = false;
|
||||
scenario.value.isExecute = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function realExecute(
|
||||
executeParams: ApiScenarioDebugRequest,
|
||||
executeType?: 'localExec' | 'serverExec',
|
||||
localExecuteUrl?: string
|
||||
) {
|
||||
try {
|
||||
scenario.value.executeLoading = true;
|
||||
stepReportId.value = getGenerateId();
|
||||
scenario.value.reportId = stepReportId.value; // 存储报告ID
|
||||
debugSocket(executeType, localExecuteUrl); // 开启websocket
|
||||
executeParams.environmentId = currentEnvConfig?.value.id || '';
|
||||
const res = await debugScenario(executeParams);
|
||||
if (executeType === 'localExec' && localExecuteUrl) {
|
||||
await localExecuteApiDebug(localExecuteUrl, res);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
scenario.value.executeLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function batchDebug() {
|
||||
const selectedKeysSet = new Set(checkedKeys.value);
|
||||
const waitTingDebugSteps = filterTree(
|
||||
scenario.value.steps,
|
||||
(node) => {
|
||||
if (selectedKeysSet.has(node.id)) {
|
||||
selectedKeysSet.delete(node.id);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
'id'
|
||||
);
|
||||
const waitingDebugStepDetails = {};
|
||||
Object.keys(scenario.value.stepDetails).forEach((key) => {
|
||||
if (selectedKeysSet.has(key)) {
|
||||
waitingDebugStepDetails[key] = scenario.value.stepDetails[key];
|
||||
}
|
||||
});
|
||||
realExecute({
|
||||
id: scenario.value.id || '',
|
||||
steps: waitTingDebugSteps,
|
||||
stepDetails: waitingDebugStepDetails,
|
||||
grouped: false,
|
||||
environmentId: currentEnvConfig?.value.id || '',
|
||||
uploadFileIds: scenario.value.uploadFileIds,
|
||||
linkFileIds: scenario.value.linkFileIds,
|
||||
projectId: appStore.currentProjectId,
|
||||
scenarioConfig: scenario.value.scenarioConfig,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
|
|
@ -104,7 +104,7 @@
|
|||
</div>
|
||||
<a-tooltip :content="step.name">
|
||||
<div class="step-name-container">
|
||||
<div class="one-line-text mr-[4px] max-w-[150px] font-medium text-[var(--color-text-1)]">
|
||||
<div class="one-line-text mr-[4px] max-w-[350px] font-medium text-[var(--color-text-1)]">
|
||||
{{ step.name }}
|
||||
</div>
|
||||
<MsIcon
|
||||
|
@ -442,20 +442,34 @@
|
|||
switch (item.eventTag) {
|
||||
case 'copy':
|
||||
const id = getGenerateId();
|
||||
const stepDetail = stepDetails.value[node.id];
|
||||
if (stepDetail) {
|
||||
// 如果复制的步骤还有详情数据,则也复制详情数据
|
||||
stepDetails.value[id] = cloneDeep(stepDetail);
|
||||
}
|
||||
insertNodes<ScenarioStepItem>(
|
||||
steps.value,
|
||||
node.id,
|
||||
{
|
||||
...cloneDeep(
|
||||
mapTree<ScenarioStepItem>(node, (childNode) => {
|
||||
const childId = getGenerateId();
|
||||
const childStepDetail = stepDetails.value[node.id];
|
||||
if (childStepDetail) {
|
||||
// 如果复制的步骤下子步骤还有详情数据,则也复制详情数据
|
||||
stepDetails.value[childId] = cloneDeep(childStepDetail);
|
||||
}
|
||||
return {
|
||||
...childNode,
|
||||
id: getGenerateId(), // TODO:引用类型额外需要一个复制来源 ID
|
||||
...cloneDeep(childNode),
|
||||
copyFromStepId: childNode.id,
|
||||
id: childId,
|
||||
};
|
||||
})[0]
|
||||
),
|
||||
name: `copy-${node.name}`,
|
||||
copyFromStepId: node.id,
|
||||
sort: node.sort + 1,
|
||||
isNew: false,
|
||||
id,
|
||||
},
|
||||
'after',
|
||||
|
@ -567,9 +581,10 @@
|
|||
async function getStepDetail(step: ScenarioStepItem) {
|
||||
try {
|
||||
appStore.showLoading();
|
||||
const res = await getScenarioStep(step.id);
|
||||
const res = await getScenarioStep(step.copyFromStepId || step.id);
|
||||
stepDetails.value[step.id] = {
|
||||
...res,
|
||||
stepId: step.id,
|
||||
protocol: step.config.protocol,
|
||||
method: step.config.method,
|
||||
};
|
||||
|
@ -597,8 +612,11 @@
|
|||
if (_stepType.isCopyApi || _stepType.isQuoteApi || step.stepType === ScenarioStepType.CUSTOM_REQUEST) {
|
||||
// 复制 api、引用 api、自定义 api打开抽屉
|
||||
activeStep.value = step;
|
||||
if (stepDetails.value[step.id] === undefined && !step.isNew) {
|
||||
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情
|
||||
if (
|
||||
(stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
|
||||
(stepDetails.value[step.id] === undefined && !step.isNew)
|
||||
) {
|
||||
// 查看场景详情时,详情映射中没有对应数据,初始化步骤详情(复制的步骤没有加载详情前就被复制,打开复制后的步骤就初始化被复制步骤的详情)
|
||||
await getStepDetail(step);
|
||||
}
|
||||
customApiDrawerVisible.value = true;
|
||||
|
@ -734,7 +752,6 @@
|
|||
projectId: appStore.currentProjectId,
|
||||
});
|
||||
}
|
||||
console.log(steps.value);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -274,7 +274,7 @@
|
|||
const currentEnvConfig = ref<EnvConfig>();
|
||||
const reportId = ref('');
|
||||
const websocket = ref<WebSocket>();
|
||||
const temporaryResponseMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
const temporaryScenarioReportMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
|
||||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
|
@ -291,14 +291,18 @@
|
|||
if (activeScenarioTab.value.reportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
activeScenarioTab.value.isExecute = false;
|
||||
} else {
|
||||
// 不是则需要把报告缓存起来,等切换到对应的tab再赋值
|
||||
temporaryResponseMap[activeScenarioTab.value.id][data.reportId] = data.taskResult;
|
||||
temporaryScenarioReportMap[data.reportId] = data.taskResult;
|
||||
}
|
||||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
websocket.value?.close();
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
if (activeScenarioTab.value.reportId === data.reportId) {
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
activeScenarioTab.value.isExecute = false;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -346,9 +350,10 @@
|
|||
const scenarioId = computed(() => activeScenarioTab.value.id);
|
||||
const scenarioExecuteLoading = computed(() => activeScenarioTab.value.executeLoading);
|
||||
// 为子孙组件提供属性
|
||||
provide('currentEnvConfig', readonly(currentEnvConfig));
|
||||
provide('scenarioId', scenarioId);
|
||||
provide('scenarioExecuteLoading', scenarioExecuteLoading);
|
||||
provide('temporaryResponseMap', temporaryResponseMap);
|
||||
provide('temporaryScenarioReportMap', readonly(temporaryScenarioReportMap));
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
Loading…
Reference in New Issue