feat(接口场景): 场景步骤批量初步调试&插件问题修复

This commit is contained in:
baiqi 2024-03-25 20:55:18 +08:00 committed by Craftsman
parent a2c6ba1b88
commit 82474851b4
8 changed files with 173 additions and 47 deletions

View File

@ -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;

View File

@ -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

View File

@ -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;
}
}
});
}

View File

@ -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 {
//

View File

@ -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',

View File

@ -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 = {}; // websockettab
/**
* 开启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) {
// tabtab
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">

View File

@ -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);
}
/**

View File

@ -274,7 +274,7 @@
const currentEnvConfig = ref<EnvConfig>();
const reportId = ref('');
const websocket = ref<WebSocket>();
const temporaryResponseMap = {}; // websockettab
const temporaryScenarioReportMap = {}; // websockettab
/**
* 开启websocket监听接收执行结果
@ -291,14 +291,18 @@
if (activeScenarioTab.value.reportId === data.reportId) {
// tabtab
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">