feat(接口场景): 场景执行优化

This commit is contained in:
baiqi 2024-03-27 12:53:10 +08:00 committed by Craftsman
parent 348b926c6f
commit d44b7d232e
18 changed files with 187 additions and 38 deletions

View File

@ -1,7 +1,7 @@
@font-face {
font-family: iconfont; /* Project id 3462279 */
src: url('iconfont.woff2?t=1709537452442') format('woff2'), url('iconfont.woff?t=1709537452442') format('woff'),
url('iconfont.ttf?t=1709537452442') format('truetype'), url('iconfont.svg?t=1709537452442#iconfont') format('svg');
src: url('iconfont.woff2?t=1711511079663') format('woff2'), url('iconfont.woff?t=1711511079663') format('woff'),
url('iconfont.ttf?t=1711511079663') format('truetype'), url('iconfont.svg?t=1711511079663#iconfont') format('svg');
}
.iconfont {
font-size: 16px;
@ -10,6 +10,18 @@
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-icon_stop::before {
content: '\e7a8';
}
.icon-icon_env1::before {
content: '\e7a7';
}
.icon-icon_case::before {
content: '\e7a6';
}
.icon-icon_env::before {
content: '\e7a5';
}
.icon-icon_expand_interface::before {
content: '\e7a3';
}

File diff suppressed because one or more lines are too long

View File

@ -5,6 +5,34 @@
"css_prefix_text": "icon-",
"description": "DE、MS项目icon管理",
"glyphs": [
{
"icon_id": "39710057",
"name": "icon_stop",
"font_class": "icon_stop",
"unicode": "e7a8",
"unicode_decimal": 59304
},
{
"icon_id": "39697414",
"name": "icon_env",
"font_class": "icon_env1",
"unicode": "e7a7",
"unicode_decimal": 59303
},
{
"icon_id": "39697284",
"name": "icon_case",
"font_class": "icon_case",
"unicode": "e7a6",
"unicode_decimal": 59302
},
{
"icon_id": "39582483",
"name": "icon_env",
"font_class": "icon_env",
"unicode": "e7a5",
"unicode_decimal": 59301
},
{
"icon_id": "39388088",
"name": "icon_expand_interface",

View File

@ -14,6 +14,14 @@
/>
<missing-glyph />
<glyph glyph-name="icon_stop" unicode="&#59304;" d="M512 853.333333c259.2 0 469.333333-210.133333 469.333333-469.333333s-210.133333-469.333333-469.333333-469.333333S42.666667 124.8 42.666667 384 252.8 853.333333 512 853.333333z m0-85.333333a384 384 0 1 1 0-768 384 384 0 0 1 0 768zM405.333333 554.666667a42.666667 42.666667 0 0 0 42.666667-42.666667v-256a42.666667 42.666667 0 0 0-85.333333 0V512a42.666667 42.666667 0 0 0 42.666666 42.666667z m213.333334 0a42.666667 42.666667 0 0 0 42.666666-42.666667v-256a42.666667 42.666667 0 0 0-85.333333 0V512a42.666667 42.666667 0 0 0 42.666667 42.666667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_env1" unicode="&#59303;" d="M399.701333 810.666667a42.666667 42.666667 0 0 0 32.085334-14.506667L531.328 682.666667h362.410667c45.738667 0 83.968-34.346667 87.338666-78.933334L981.333333 597.333333v-554.666666c0-47.616-39.68-85.333333-87.594666-85.333334H130.261333C82.346667-42.666667 42.666667-4.949333 42.666667 42.666667V725.333333c0 47.616 39.68 85.333333 87.594666 85.333334h269.44z m-19.328-85.333334H130.261333C128.554667 725.333333 128 724.821333 128 725.333333v-682.666666c0 0.512 0.512 0 2.261333 0h763.477334c1.706667 0 2.261333 0.512 2.261333 0V597.333333c0-0.512-0.512 0-2.261333 0H512a42.666667 42.666667 0 0 0-32.085333 14.506667L380.373333 725.333333z m8.362667-279.808v-58.666666H246.186667v-43.690667h132.181333v-56.064H246.186667v-54.186667h146.602666V170.666667H161.109333v274.858666H388.693333z m187.861333-71.253333c21.12 0 37.632-6.272 49.578667-18.816 11.946667-12.586667 17.92-32 17.92-58.24V170.666667h-76.672v109.482666c0 12.501333-2.346667 21.333333-6.954667 26.538667-4.608 5.205333-11.093333 7.808-19.498666 7.808a27.733333 27.733333 0 0 1-22.485334-10.496c-5.76-7.04-8.661333-19.584-8.661333-37.717333V170.666667h-76.288v199.125333h71.04v-32.426667c10.666667 13.226667 21.376 22.698667 32.256 28.416 10.88 5.674667 24.149333 8.533333 39.765333 8.533334z m159.573334-4.48l38.613333-126.208 39.936 126.208h77.056L807.253333 170.666667h-67.669333l-82.901333 199.125333h79.530666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_case" unicode="&#59302;" d="M554.666667 853.333333l3.456-0.128 2.901333-0.341333a46.208 46.208 0 0 0 9.472-2.56l2.218667-0.981333a43.434667 43.434667 0 0 0 7.338666-4.352l0.938667-0.725334 0.341333-0.298666 3.498667-3.114667 213.333333-213.333333 2.901334-3.285334 1.237333-1.493333c0.768-1.024 1.450667-2.090667 2.133333-3.157333l0.896-1.578667c0.469333-0.853333 0.938667-1.706667 1.322667-2.56l0.981333-2.261333a42.154667 42.154667 0 0 0 2.730667-10.837334l0.128-1.28 0.128-2.346666L810.666667 597.333333v-213.333333h-85.333334V554.666667h-170.666666a42.666667 42.666667 0 0 0-42.368 37.674666L512 597.333333V768H128v-768h384v-85.333333H128a85.333333 85.333333 0 0 0-85.333333 85.333333V768a85.333333 85.333333 0 0 0 85.333333 85.333333h426.666667z m247.808-524.8c75.349333 0 129.237333-49.621333 136.192-113.322666h-90.965334c-5.248 22.058667-17.408 39.125333-45.781333 39.125333h-60.885333c-33.578667 0-55.04-24.576-55.04-54.186667l0.597333-157.141333c0-29.568 21.418667-54.144 55.04-54.144h60.842667c27.818667 0 39.424 16.042667 45.226666 37.589333h91.562667c-6.954667-62.634667-61.44-111.786667-136.192-111.786666h-62.037333c-78.805333 0-143.104 55.637333-143.104 123.818666L597.333333 204.202667c0 68.693333 64.341333 124.330667 143.146667 124.330666zM597.333333 707.626667V640h67.626667L597.333333 707.626667z" horiz-adv-x="1024" />
<glyph glyph-name="icon_env" unicode="&#59301;" d="M647.168 810.666667a106.666667 106.666667 0 0 0 75.434667-31.232l82.304-82.346667 102.528-102.485333A106.666667 106.666667 0 0 0 938.666667 519.168V64a106.666667 106.666667 0 0 0-106.666667-106.666667h-640A106.666667 106.666667 0 0 0 85.333333 64v640A106.666667 106.666667 0 0 0 192 810.666667h455.168z m0-85.333334H192a21.333333 21.333333 0 0 1-21.333333-21.333333v-640a21.333333 21.333333 0 0 1 21.333333-21.333333h640a21.333333 21.333333 0 0 1 21.333333 21.333333V519.168a21.333333 21.333333 0 0 1-6.229333 15.104l-184.832 184.832A21.333333 21.333333 0 0 1 647.168 725.333333z m-247.765333-256v-45.568h-116.48v-33.877333h108.074666v-43.52H282.88v-42.069333h119.893333V256H213.333333v213.333333h186.069334z m153.6-55.296c17.28 0 30.762667-4.864 40.533333-14.634666 9.770667-9.728 14.634667-24.789333 14.634667-45.184V256h-62.677334v84.992c0 9.685333-1.877333 16.554667-5.674666 20.565333a20.821333 20.821333 0 0 1-15.957334 6.058667 23.168 23.168 0 0 1-18.346666-8.149333c-4.693333-5.418667-7.082667-15.189333-7.082667-29.269334V256H436.053333v154.538667h58.069334v-25.173334c8.704 10.282667 17.493333 17.621333 26.368 22.058667a72.533333 72.533333 0 0 0 32.512 6.613333z m130.432-3.498666l31.573333-97.92 32.64 97.92H810.666667L741.546667 256h-55.338667l-67.754667 154.538667h64.981334z" horiz-adv-x="1024" />
<glyph glyph-name="icon_expand_interface" unicode="&#59299;" d="M827.733333 332.8a42.666667 42.666667 0 0 0 51.2-68.266667l-341.333333-256a42.666667 42.666667 0 0 0-51.2 0l-341.333333 256a42.666667 42.666667 0 1 0 51.2 68.266667l315.733333-236.8 315.733333 236.8zM853.333333 725.333333a42.666667 42.666667 0 0 0 0-85.333333H170.666667a42.666667 42.666667 0 1 0 0 85.333333h682.666666z m0-170.666666a42.666667 42.666667 0 0 0 0-85.333334H170.666667a42.666667 42.666667 0 1 0 0 85.333334h682.666666z" horiz-adv-x="1024" />
<glyph glyph-name="icon_collapse_interface" unicode="&#59300;" d="M196.266667 392.533333a42.666667 42.666667 0 0 0-51.2 68.266667l341.333333 256a42.666667 42.666667 0 0 0 51.2 0l341.333333-256a42.666667 42.666667 0 1 0-51.2-68.266667L512 629.333333 196.266667 392.533333zM170.666667 0a42.666667 42.666667 0 0 0 0 85.333333h682.666666a42.666667 42.666667 0 1 0 0-85.333333H170.666667z m0 170.666667a42.666667 42.666667 0 0 0 0 85.333333h682.666666a42.666667 42.666667 0 1 0 0-85.333333H170.666667z" horiz-adv-x="1024" />

Before

Width:  |  Height:  |  Size: 437 KiB

After

Width:  |  Height:  |  Size: 442 KiB

View File

@ -272,6 +272,7 @@ export interface ForEachController {
}
export interface CountController {
loops: number; // 循环次数
loopTime: number; // 循环间隔时间
}
export interface WhileScript {
scriptValue: string; // 脚本值
@ -400,7 +401,7 @@ export interface ApiScenarioDebugRequest {
environmentId: string;
scenarioConfig: ScenarioConfig;
stepDetails: Record<string, ScenarioStepDetail>;
reportId?: string | number;
reportId: string | number;
steps: ScenarioStepItem[];
projectId: string;
uploadFileIds: string[];

View File

@ -494,6 +494,10 @@ export function deleteNode<T>(treeArr: TreeNode<T>[], targetKey: string | number
const node = tree[i];
if (node[customKey] === targetKey) {
tree.splice(i, 1); // 直接删除当前节点
// 重新调整剩余子节点的 sort 序号
for (let j = i; j < tree.length; j++) {
tree[j].sort = j + 1;
}
return;
}
if (Array.isArray(node.children)) {
@ -518,6 +522,10 @@ export function deleteNodes<T>(treeArr: TreeNode<T>[], targetKeys: (string | num
if (targetKeysSet.has(node[customKey])) {
tree.splice(i, 1); // 直接删除当前节点
targetKeysSet.delete(node[customKey]); // 删除后从集合中移除
// 重新调整剩余子节点的 sort 序号
for (let j = i; j < tree.length; j++) {
tree[j].sort = j + 1;
}
} else if (Array.isArray(node.children)) {
deleteNodesInTree(node.children); // 递归删除子节点
}

View File

@ -73,23 +73,33 @@
"
>
<a-dropdown-button
v-if="!requestVModel.executeLoading"
v-if="hasLocalExec"
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute"
>
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template v-if="hasLocalExec" #icon>
<template #icon>
<icon-down />
</template>
<template v-if="hasLocalExec" #content>
<template #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
<a-button
v-else-if="!requestVModel.executeLoading"
class="mr-[12px]"
type="primary"
@click="() => execute('serverExec')"
>
{{ t('apiTestDebug.serverExec') }}
</a-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">
{{ t('common.stop') }}
</a-button>
</template>
<!-- 接口定义-且有保存或更新权限 -->
<template

View File

@ -474,7 +474,7 @@
watch(
() => props.stepResponses,
(val) => {
if (val) {
if (val && val[requestVModel.value.stepId]) {
requestVModel.value.executeLoading = false;
}
},

View File

@ -17,6 +17,7 @@ export const defaultLoopController = {
},
msCountController: {
loops: 0, // 循环次数
loopTime: 0, // 循环间隔时间
},
whileController: {
conditionType: WhileConditionType.CONDITION, // 条件类型
@ -65,6 +66,7 @@ export const defaultStepItemCommon = {
createActionsVisible: false,
responsePopoverVisible: false,
isExecuting: false,
executeStatus: undefined,
};
export const defaultScenario: Scenario = {

View File

@ -1,6 +1,6 @@
<template>
<div>
<div class="mb-[16px] flex items-center justify-end">
<div class="mb-[8px] flex items-center justify-end">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiScenario.executeHistory.searchPlaceholder')"
@ -119,7 +119,7 @@
dataIndex: 'id',
slotName: 'num',
fixed: 'left',
width: 100,
width: 150,
},
{
title: 'apiScenario.executeHistory.execution.triggerMode',
@ -131,7 +131,7 @@
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
width: 100,
},
{
title: 'apiScenario.executeHistory.execution.status',
@ -142,14 +142,13 @@
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 150,
width: 100,
},
{
title: 'apiScenario.executeHistory.execution.operator',
dataIndex: 'createUser',
slotName: 'operationUser',
dataIndex: 'operationUser',
showTooltip: true,
width: 150,
width: 100,
},
{
title: 'apiScenario.executeHistory.execution.operatorTime',
@ -159,7 +158,7 @@
sortDirections: ['ascend', 'descend'],
sorter: true,
},
width: 200,
width: 180,
},
{
title: 'common.operation',
@ -168,7 +167,7 @@
fixed: 'right',
showInTable: true,
showDrag: false,
width: 150,
width: 100,
},
];
@ -179,7 +178,7 @@
scroll: { x: '100%' },
showSetting: false,
selectable: false,
heightUsed: 374,
heightUsed: 398,
},
(item) => ({
...item,

View File

@ -131,7 +131,7 @@
import { countNodes } from '@/utils/tree';
import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario';
import { ScenarioExecuteStatus } from '@/enums/apiEnum';
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
const props = defineProps<{
@ -248,7 +248,10 @@
if (!node.enable) {
// id便waitingDebugStepDetails
checkedKeysSet.delete(node.id);
} else {
} else if (
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(node.stepType)
) {
//
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
}
return !!node.enable;

View File

@ -142,6 +142,7 @@
:disabled="!innerData.forEachController.loopTime"
>
<a-input-number
v-if="innerData.loopType === ScenarioStepLoopTypeEnum.FOREACH"
v-model:model-value="innerData.forEachController.loopTime"
size="mini"
:step="1"
@ -156,6 +157,22 @@
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.space') }}:</div>
</template>
</a-input-number>
<a-input-number
v-else-if="innerData.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT"
v-model:model-value="innerData.msCountController.loopTime"
size="mini"
:step="1"
:min="0"
:precision="0"
hide-button
class="w-[110px] px-[8px]"
model-event="input"
@blur="handleInputChange"
>
<template #prefix>
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.space') }}:</div>
</template>
</a-input-number>
</a-tooltip>
</div>
</template>

View File

@ -19,7 +19,7 @@
</div>
</template>
</a-popover> -->
<MsTag
<!-- <MsTag
v-if="props.data.projectId !== appStore.currentProjectId"
theme="outline"
size="small"
@ -30,7 +30,7 @@
}"
>
{{ t('apiScenario.crossProject') }}
</MsTag>
</MsTag> -->
</div>
</template>

View File

@ -66,11 +66,19 @@
></a-switch>
<!-- 步骤执行 -->
<MsIcon
v-show="!step.isExecuting"
type="icon-icon_play-round_filled"
:size="18"
class="cursor-pointer text-[rgb(var(--link-6))]"
@click.stop="executeStep(step)"
/>
<MsIcon
v-show="step.isExecuting"
type="icon-icon_stop"
:size="20"
class="cursor-pointer text-[rgb(var(--link-6))]"
@click.stop="handleStopExecute(step)"
/>
</div>
<!-- 步骤类型 -->
<stepType :step="step" />
@ -169,9 +177,10 @@
</template>
<template #extraEnd="step">
<a-popover
v-if="step.executeStatus"
v-if="step.executeStatus && checkStepIsApi(step)"
position="br"
content-class="scenario-step-response-popover"
:disabled="![ScenarioExecuteStatus.SUCCESS, ScenarioExecuteStatus.FAILED].includes(step.executeStatus)"
@popup-visible-change="handleResponsePopoverVisibleChange($event, step)"
>
<executeStatus :status="getExecuteStatus(step) || step.executeStatus" size="small" />
@ -195,6 +204,11 @@
</responseResult>
</template>
</a-popover>
<executeStatus
v-else-if="step.executeStatus"
:status="getExecuteStatus(step) || step.executeStatus"
size="small"
/>
</template>
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
<div
@ -220,7 +234,7 @@
:step-responses="scenario.stepResponses"
@add-step="addCustomApiStep"
@apply-step="applyApiStep"
@stop-debug="handleStopExecute"
@stop-debug="handleStopExecute(activeStep)"
@execute="handleApiExecute"
/>
<customCaseDrawer
@ -693,7 +707,7 @@
}
}
const websocket = ref<WebSocket>();
const websocketMap: Record<string | number, WebSocket> = {};
let temporaryStepReportMap = {}; // websockettab
watch(
@ -715,16 +729,16 @@
*/
function debugSocket(
step: ScenarioStepItem,
reportId?: string | number,
reportId: string | number,
executeType?: 'localExec' | 'serverExec',
localExecuteUrl?: string
) {
websocket.value = getSocket(
websocketMap[reportId] = getSocket(
reportId || '',
executeType === 'localExec' ? '/ws/debug' : '',
executeType === 'localExec' ? localExecuteUrl : ''
);
websocket.value.addEventListener('message', (event) => {
websocketMap[reportId].addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
if (step.reportId === data.reportId) {
@ -751,7 +765,7 @@
}
} else if (data.msgType === 'EXEC_END') {
// websocket
websocket.value?.close();
websocketMap[reportId].close();
if (step.reportId === data.reportId) {
step.isExecuting = false;
}
@ -786,7 +800,7 @@
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
websocket.value?.close();
websocketMap[executeParams.reportId].close();
currentStep.isExecuting = false;
}
}
@ -843,14 +857,43 @@
},
executeType
);
} else {
//
const reportId = getGenerateId();
request.executeLoading = true;
activeStep.value = {
id: request.stepId,
name: t('apiScenario.customApi'),
stepType: ScenarioStepType.CUSTOM_REQUEST,
refType: ScenarioStepRefType.DIRECT,
sort: 1,
enable: true,
isNew: true,
config: {},
projectId: appStore.currentProjectId,
isExecuting: false,
reportId,
};
realExecute(
{
steps: [activeStep.value],
stepDetails: {
[request.stepId]: request,
},
reportId,
uploadFileIds: request.uploadFileIds || [],
linkFileIds: request.linkFileIds || [],
},
executeType
);
}
}
function handleStopExecute() {
websocket.value?.close();
if (activeStep.value) {
activeStep.value.isExecuting = false;
activeStep.value.executeStatus = undefined;
function handleStopExecute(step?: ScenarioStepItem) {
if (step?.reportId) {
websocketMap[step.reportId].close();
step.isExecuting = false;
step.executeStatus = undefined;
}
}

View File

@ -112,7 +112,7 @@
import { useI18n } from '@/hooks/useI18n';
import router from '@/router';
import useAppStore from '@/store/modules/app';
import { filterTree, getGenerateId } from '@/utils';
import { filterTree, getGenerateId, mapTree } from '@/utils';
import {
ApiScenarioDebugRequest,
@ -122,7 +122,7 @@
} from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { ScenarioExecuteStatus } from '@/enums/apiEnum';
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { defaultScenario } from './components/config';
@ -367,6 +367,12 @@
uploadFileIds: activeScenarioTab.value.uploadFileIds,
linkFileIds: activeScenarioTab.value.linkFileIds,
...executeParams,
steps: mapTree(executeParams.steps, (node) => {
return {
...node,
parent: null, // axios
};
}),
});
} else {
res = await debugScenario({
@ -379,6 +385,12 @@
linkFileIds: activeScenarioTab.value.linkFileIds,
frontendDebug: executeType === 'localExec',
...executeParams,
steps: mapTree(executeParams.steps, (node) => {
return {
...node,
parent: null, // axios
};
}),
});
}
if (executeType === 'localExec' && localExecuteUrl) {
@ -398,6 +410,12 @@
if (node.enable) {
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
waitingDebugStepDetails[node.id] = activeScenarioTab.value.stepDetails[node.id];
if (
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(node.stepType)
) {
//
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
}
}
return !!node.enable;
});