feat(接口场景): 场景步骤批量调试&响应
This commit is contained in:
parent
0d716882d9
commit
4e0a3fd33b
|
@ -244,6 +244,7 @@ export enum ScenarioDetailComposition {
|
|||
// 场景执行状态
|
||||
export enum ScenarioExecuteStatus {
|
||||
SUCCESS = 'SUCCESS',
|
||||
EXECUTING = 'EXECUTING',
|
||||
FAILED = 'FAILED',
|
||||
STOP = 'STOP',
|
||||
}
|
||||
|
|
|
@ -408,6 +408,8 @@ export interface RequestResult {
|
|||
url: string;
|
||||
method: string;
|
||||
responseResult: ResponseResult;
|
||||
isSuccessful?: boolean;
|
||||
console?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
export interface RequestTaskResult {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
ExecuteApiRequestFullParams,
|
||||
ExecuteAssertionItem,
|
||||
ExecuteConditionConfig,
|
||||
RequestResult,
|
||||
ResponseDefinition,
|
||||
} from './common';
|
||||
|
||||
|
@ -320,7 +321,6 @@ export interface ScenarioStepItem {
|
|||
id: string | number;
|
||||
sort: number;
|
||||
name: string;
|
||||
executeStatus?: ScenarioExecuteStatus;
|
||||
enable: boolean; // 是否启用
|
||||
copyFromStepId?: string; // 如果步骤是复制的,这个字段是复制的步骤id;如果复制的步骤也是复制的,并且没有加载过详情,则这个 id 是最原始的 被复制的步骤 id
|
||||
resourceId?: string; // 详情或者引用的类型才有
|
||||
|
@ -337,9 +337,11 @@ export interface ScenarioStepItem {
|
|||
checked?: boolean; // 是否选中
|
||||
expanded?: boolean; // 是否展开
|
||||
createActionsVisible?: boolean; // 是否展示创建步骤下拉
|
||||
responsePopoverVisible?: boolean; // 是否展示步骤响应 popover
|
||||
parent?: ScenarioStepItem; // 父级节点,第一层的父级节点为undefined
|
||||
resourceName?: string; // 引用复制接口、用例、场景时的源资源名称
|
||||
method?: RequestMethods;
|
||||
executeStatus?: ScenarioExecuteStatus;
|
||||
}
|
||||
// 场景
|
||||
export interface Scenario {
|
||||
|
@ -367,11 +369,12 @@ export interface Scenario {
|
|||
unSaved: boolean;
|
||||
executeLoading: boolean; // 执行loading
|
||||
executeTime?: string | number; // 执行时间
|
||||
executeSuccessCount?: number; // 执行成功数量
|
||||
executeFailCount?: number; // 执行失败数量
|
||||
executeSuccessCount: number; // 执行成功数量
|
||||
executeFailCount: number; // 执行失败数量
|
||||
reportId?: string | number; // 场景报告 id
|
||||
stepReportId?: string | number; // 步骤报告 id(单个或批量调试)
|
||||
stepResponses: Record<string | number, RequestResult>; // 步骤响应集合,key 为步骤 id,value 为步骤响应内容
|
||||
isExecute?: boolean; // 是否从列表执行进去场景详情
|
||||
isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤
|
||||
}
|
||||
export interface ScenarioDetail extends Scenario {
|
||||
stepTotal: number;
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { TreeNodeData } from '@arco-design/web-vue';
|
||||
|
||||
import { RequestMethods } from '@/enums/apiEnum';
|
||||
|
||||
// 请求返回结构
|
||||
|
@ -61,7 +63,7 @@ export interface AddModuleParams {
|
|||
parentId: string;
|
||||
}
|
||||
// 模块树节点
|
||||
export interface ModuleTreeNode {
|
||||
export interface ModuleTreeNode extends TreeNodeData {
|
||||
id: string;
|
||||
name: string;
|
||||
type: 'MODULE' | 'API';
|
||||
|
|
|
@ -249,7 +249,7 @@ export function mapTree<T>(
|
|||
*/
|
||||
export function filterTree<T>(
|
||||
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
filterFn: (node: TreeNode<T>) => boolean,
|
||||
filterFn: (node: T) => boolean,
|
||||
customChildrenKey = 'children'
|
||||
): T[] {
|
||||
if (!Array.isArray(tree)) {
|
||||
|
@ -257,10 +257,10 @@ export function filterTree<T>(
|
|||
}
|
||||
const filteredTree: T[] = [];
|
||||
for (let i = 0; i < tree.length; i++) {
|
||||
const node = tree[i];
|
||||
const node: T = tree[i];
|
||||
// 如果节点满足过滤条件,则保留该节点,并递归过滤子节点
|
||||
if (filterFn(node)) {
|
||||
const newNode: T = { ...node };
|
||||
const newNode: T = cloneDeep(node);
|
||||
if (node[customChildrenKey] && node[customChildrenKey].length > 0) {
|
||||
// 递归过滤子节点,并将过滤后的子节点添加到当前节点中
|
||||
newNode[customChildrenKey] = filterTree(node[customChildrenKey], filterFn, customChildrenKey);
|
||||
|
|
|
@ -110,7 +110,8 @@
|
|||
:is-priority-local-exec="props.isPriorityLocalExec"
|
||||
:request-url="requestVModel.url"
|
||||
:is-expanded="true"
|
||||
:request-task-result="requestVModel.response"
|
||||
:request-result="requestVModel.response?.requestResults[0]"
|
||||
:console="requestVModel.response?.console"
|
||||
:is-edit="false"
|
||||
hide-layout-switch
|
||||
:upload-temp-file-api="props.uploadTempFileApi"
|
||||
|
|
|
@ -276,7 +276,8 @@
|
|||
:request-url="requestVModel.url"
|
||||
:is-expanded="isVerticalExpanded"
|
||||
:hide-layout-switch="props.hideResponseLayoutSwitch"
|
||||
:request-task-result="requestVModel.response"
|
||||
:request-result="requestVModel.response?.requestResults[0]"
|
||||
:console="requestVModel.response?.console"
|
||||
:is-edit="props.isDefinition && isHttpProtocol && !props.isCase"
|
||||
:upload-temp-file-api="props.uploadTempFileApi"
|
||||
:loading="requestVModel.executeLoading || loading"
|
||||
|
@ -1039,7 +1040,7 @@
|
|||
const saveModalFormRef = ref<FormInstance>();
|
||||
const saveLoading = ref(false);
|
||||
const selectTree = computed(() =>
|
||||
filterTree(cloneDeep(props.moduleTree), (e) => {
|
||||
filterTree(cloneDeep(props.moduleTree || []), (e) => {
|
||||
e.draggable = false;
|
||||
return e.type === 'MODULE';
|
||||
})
|
||||
|
@ -1227,6 +1228,7 @@
|
|||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
websocket.value?.close();
|
||||
requestVModel.value.executeLoading = false;
|
||||
}
|
||||
} else {
|
||||
|
@ -1244,6 +1246,7 @@
|
|||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
websocket.value?.close();
|
||||
requestVModel.value.executeLoading = false;
|
||||
}
|
||||
} else {
|
||||
|
|
|
@ -1,73 +1,75 @@
|
|||
<template>
|
||||
<div class="response flex h-full min-w-[300px] flex-col">
|
||||
<div :class="['response-head', props.isExpanded ? '' : 'border-t']">
|
||||
<div class="flex items-center justify-between">
|
||||
<template v-if="props.activeLayout === 'vertical'">
|
||||
<MsButton
|
||||
v-if="props.isExpanded"
|
||||
type="icon"
|
||||
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
|
||||
<slot name="titleLeft">
|
||||
<div class="flex items-center justify-between">
|
||||
<template v-if="props.activeLayout === 'vertical'">
|
||||
<MsButton
|
||||
v-if="props.isExpanded"
|
||||
type="icon"
|
||||
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
|
||||
size="small"
|
||||
@click="emit('changeExpand', false)"
|
||||
>
|
||||
<icon-down :size="8" />
|
||||
</MsButton>
|
||||
<MsButton
|
||||
v-else
|
||||
type="icon"
|
||||
status="secondary"
|
||||
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
|
||||
size="small"
|
||||
@click="emit('changeExpand', true)"
|
||||
>
|
||||
<icon-right :size="8" />
|
||||
</MsButton>
|
||||
</template>
|
||||
<div
|
||||
v-if="props.isEdit && props.requestResult?.responseResult?.responseCode"
|
||||
class="ml-[4px] flex items-center"
|
||||
>
|
||||
<MsButton
|
||||
type="text"
|
||||
:class="['font-medium', activeResponseType === 'content' ? '' : '!text-[var(--color-text-n4)]', '!mr-0']"
|
||||
@click="() => setActiveResponse('content')"
|
||||
>
|
||||
{{ t('apiTestDebug.responseContent') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="4"></a-divider>
|
||||
<MsButton
|
||||
type="text"
|
||||
:class="['font-medium', activeResponseType === 'result' ? '' : '!text-[var(--color-text-n4)]']"
|
||||
@click="() => setActiveResponse('result')"
|
||||
>
|
||||
{{ t('apiTestManagement.executeResult') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
<div v-else class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
|
||||
<a-radio-group
|
||||
v-if="!props.hideLayoutSwitch"
|
||||
v-model:model-value="innerLayout"
|
||||
type="button"
|
||||
size="small"
|
||||
@click="emit('changeExpand', false)"
|
||||
@change="(val) => emit('changeLayout', val as Direction)"
|
||||
>
|
||||
<icon-down :size="8" />
|
||||
</MsButton>
|
||||
<MsButton
|
||||
v-else
|
||||
type="icon"
|
||||
status="secondary"
|
||||
class="!mr-0 !rounded-full bg-[rgb(var(--primary-1))]"
|
||||
size="small"
|
||||
@click="emit('changeExpand', true)"
|
||||
>
|
||||
<icon-right :size="8" />
|
||||
</MsButton>
|
||||
</template>
|
||||
<div
|
||||
v-if="props.isEdit && props.requestTaskResult?.requestResults[0]?.responseResult?.responseCode"
|
||||
class="ml-[4px] flex items-center"
|
||||
>
|
||||
<MsButton
|
||||
type="text"
|
||||
:class="['font-medium', activeResponseType === 'content' ? '' : '!text-[var(--color-text-n4)]', '!mr-0']"
|
||||
@click="() => setActiveResponse('content')"
|
||||
>
|
||||
{{ t('apiTestDebug.responseContent') }}
|
||||
</MsButton>
|
||||
<a-divider direction="vertical" :margin="4"></a-divider>
|
||||
<MsButton
|
||||
type="text"
|
||||
:class="['font-medium', activeResponseType === 'result' ? '' : '!text-[var(--color-text-n4)]']"
|
||||
@click="() => setActiveResponse('result')"
|
||||
>
|
||||
{{ t('apiTestManagement.executeResult') }}
|
||||
</MsButton>
|
||||
<a-radio value="vertical">{{ t('apiTestDebug.vertical') }}</a-radio>
|
||||
<a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<div v-else class="ml-[4px] mr-[24px] font-medium">{{ t('apiTestDebug.responseContent') }}</div>
|
||||
<a-radio-group
|
||||
v-if="!props.hideLayoutSwitch"
|
||||
v-model:model-value="innerLayout"
|
||||
type="button"
|
||||
size="small"
|
||||
@change="(val) => emit('changeLayout', val as Direction)"
|
||||
>
|
||||
<a-radio value="vertical">{{ t('apiTestDebug.vertical') }}</a-radio>
|
||||
<a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio>
|
||||
</a-radio-group>
|
||||
</div>
|
||||
</slot>
|
||||
<div
|
||||
v-if="props.requestTaskResult?.requestResults[0]?.responseResult?.responseCode"
|
||||
class="flex items-center justify-between gap-[24px]"
|
||||
v-if="props.requestResult?.responseResult?.responseCode"
|
||||
class="flex items-center justify-between gap-[24px] text-[14px]"
|
||||
>
|
||||
<a-popover position="left" content-class="response-popover-content">
|
||||
<div class="one-line-text max-w-[200px]" :style="{ color: statusCodeColor }">
|
||||
{{ props.requestTaskResult.requestResults[0].responseResult.responseCode }}
|
||||
{{ props.requestResult.responseResult.responseCode }}
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="flex items-center gap-[8px] text-[14px]">
|
||||
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
|
||||
<div :style="{ color: statusCodeColor }">
|
||||
{{ props.requestTaskResult.requestResults[0].responseResult.responseCode }}
|
||||
{{ props.requestResult.responseResult.responseCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -84,13 +86,13 @@
|
|||
</a-popover>
|
||||
<a-popover position="left" content-class="response-popover-content">
|
||||
<div class="one-line-text text-[rgb(var(--success-7))]">
|
||||
{{ props.requestTaskResult.requestResults[0].responseResult.responseSize }} bytes
|
||||
{{ props.requestResult.responseResult.responseSize }} bytes
|
||||
</div>
|
||||
<template #content>
|
||||
<div class="flex items-center gap-[8px] text-[14px]">
|
||||
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div>
|
||||
<div class="one-line-text text-[rgb(var(--success-7))]">
|
||||
{{ props.requestTaskResult.requestResults[0].responseResult.responseSize }} bytes
|
||||
{{ props.requestResult.responseResult.responseSize }} bytes
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -128,12 +130,13 @@
|
|||
<result
|
||||
v-else-if="!props.isEdit || (props.isEdit && activeResponseType === 'result')"
|
||||
v-model:active-tab="innerActiveTab"
|
||||
:request-result="props.requestTaskResult?.requestResults[0]"
|
||||
:console="props.requestTaskResult?.console"
|
||||
:request-result="props.requestResult"
|
||||
:console="props.console"
|
||||
:is-http-protocol="props.isHttpProtocol"
|
||||
:is-priority-local-exec="props.isPriorityLocalExec"
|
||||
:request-url="props.requestUrl"
|
||||
:is-definition="props.isDefinition"
|
||||
:show-empty="props.showEmpty"
|
||||
@execute="emit('execute', props.isPriorityLocalExec ? 'localExec' : 'serverExec')"
|
||||
/>
|
||||
</a-spin>
|
||||
|
@ -149,29 +152,33 @@
|
|||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { RequestTaskResult } from '@/models/apiTest/common';
|
||||
import { RequestResult } from '@/models/apiTest/common';
|
||||
import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
activeTab: ResponseComposition;
|
||||
isExpanded: boolean;
|
||||
isPriorityLocalExec: boolean;
|
||||
isExpanded?: boolean;
|
||||
isPriorityLocalExec?: boolean;
|
||||
requestUrl?: string;
|
||||
isHttpProtocol: boolean;
|
||||
isHttpProtocol?: boolean;
|
||||
activeLayout?: Direction;
|
||||
responseDefinition?: ResponseItem[];
|
||||
requestTaskResult?: RequestTaskResult;
|
||||
requestResult?: RequestResult;
|
||||
console?: string;
|
||||
hideLayoutSwitch?: boolean; // 隐藏布局切换
|
||||
loading?: boolean;
|
||||
isEdit?: boolean; // 是否可编辑
|
||||
uploadTempFileApi?: (...args) => Promise<any>; // 上传临时文件接口
|
||||
isDefinition?: boolean;
|
||||
isResponseModel?: boolean;
|
||||
showEmpty?: boolean;
|
||||
}>(),
|
||||
{
|
||||
isExpanded: true,
|
||||
activeLayout: 'vertical',
|
||||
hideLayoutSwitch: false,
|
||||
showEmpty: true,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits<{
|
||||
|
@ -194,7 +201,7 @@
|
|||
});
|
||||
// 响应时间信息
|
||||
const timingInfo = computed(() => {
|
||||
if (props.requestTaskResult) {
|
||||
if (props.requestResult) {
|
||||
const {
|
||||
dnsLookupTime,
|
||||
downloadTime,
|
||||
|
@ -204,7 +211,7 @@
|
|||
sslHandshakeTime,
|
||||
tcpHandshakeTime,
|
||||
transferStartTime,
|
||||
} = props.requestTaskResult.requestResults[0].responseResult;
|
||||
} = props.requestResult.responseResult;
|
||||
return {
|
||||
dnsLookupTime,
|
||||
tcpHandshakeTime,
|
||||
|
@ -220,8 +227,8 @@
|
|||
});
|
||||
// 响应状态码对应颜色
|
||||
const statusCodeColor = computed(() => {
|
||||
if (props.requestTaskResult) {
|
||||
const code = props.requestTaskResult.requestResults[0].responseResult.responseCode;
|
||||
if (props.requestResult) {
|
||||
const code = props.requestResult.responseResult.responseCode;
|
||||
if (code >= 200 && code < 300) {
|
||||
return 'rgb(var(--success-7)';
|
||||
}
|
||||
|
@ -295,9 +302,9 @@
|
|||
}
|
||||
|
||||
watch(
|
||||
() => props.requestTaskResult,
|
||||
(task) => {
|
||||
if (task?.requestResults[0]?.responseResult?.responseCode) {
|
||||
() => props.requestResult,
|
||||
(requestResult) => {
|
||||
if (requestResult?.responseResult?.responseCode) {
|
||||
setActiveResponse('result');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
</div>
|
||||
</div>
|
||||
<a-empty
|
||||
v-if="props.showEmpty"
|
||||
v-show="!props.requestResult?.responseResult.responseCode"
|
||||
class="flex h-[150px] items-center gap-[16px] p-[16px]"
|
||||
>
|
||||
|
@ -55,14 +56,20 @@
|
|||
import { RequestResult } from '@/models/apiTest/common';
|
||||
import { ResponseComposition } from '@/enums/apiEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
requestResult?: RequestResult;
|
||||
console?: string;
|
||||
isPriorityLocalExec: boolean;
|
||||
requestUrl?: string;
|
||||
isHttpProtocol: boolean;
|
||||
isDefinition?: boolean;
|
||||
}>();
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
requestResult?: RequestResult;
|
||||
console?: string;
|
||||
isPriorityLocalExec: boolean;
|
||||
requestUrl?: string;
|
||||
isHttpProtocol?: boolean;
|
||||
isDefinition?: boolean;
|
||||
showEmpty?: boolean;
|
||||
}>(),
|
||||
{
|
||||
showEmpty: true,
|
||||
}
|
||||
);
|
||||
const emit = defineEmits(['execute']);
|
||||
|
||||
const { t } = useI18n();
|
||||
|
|
|
@ -104,7 +104,8 @@
|
|||
:is-http-protocol="props.protocol === 'HTTP'"
|
||||
:is-priority-local-exec="false"
|
||||
:active-tab="ResponseComposition.BODY"
|
||||
:request-task-result="responseContent"
|
||||
:request-result="responseContent?.requestResults[0]"
|
||||
:console="responseContent?.console"
|
||||
:is-definition="true"
|
||||
:is-response-model="true"
|
||||
></response>
|
||||
|
|
|
@ -260,14 +260,14 @@
|
|||
:is-priority-local-exec="isPriorityLocalExec"
|
||||
:request-url="requestVModel.url"
|
||||
:is-expanded="isVerticalExpanded"
|
||||
:request-task-result="requestVModel.response"
|
||||
:request-result="props.stepResponses?.[requestVModel.stepId]"
|
||||
:console="props.stepResponses?.[requestVModel.stepId].console"
|
||||
:show-empty="false"
|
||||
:is-edit="false"
|
||||
:upload-temp-file-api="uploadTempFile"
|
||||
is-definition
|
||||
:loading="requestVModel.executeLoading || loading"
|
||||
:is-definition="false"
|
||||
@change-expand="changeVerticalExpand"
|
||||
@change-layout="handleActiveLayoutChange"
|
||||
@change="handleActiveDebugChange"
|
||||
@execute="execute"
|
||||
/>
|
||||
</template>
|
||||
|
@ -311,6 +311,7 @@
|
|||
ExecuteConditionConfig,
|
||||
ExecuteRequestParams,
|
||||
PluginConfig,
|
||||
RequestResult,
|
||||
RequestTaskResult,
|
||||
} from '@/models/apiTest/common';
|
||||
import { ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
|
@ -383,6 +384,7 @@
|
|||
create: string;
|
||||
update: string;
|
||||
};
|
||||
stepResponses?: Record<string | number, RequestResult>;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{
|
||||
|
|
|
@ -102,6 +102,7 @@
|
|||
import { getLocalConfig } from '@/api/modules/user/index';
|
||||
import { characterLimit, getGenerateId } from '@/utils';
|
||||
|
||||
import { RequestResult } from '@/models/apiTest/common';
|
||||
import { ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import { LocalConfig } from '@/models/user';
|
||||
import {
|
||||
|
@ -118,6 +119,7 @@
|
|||
|
||||
const props = defineProps<{
|
||||
request?: RequestParam; // 请求参数集合
|
||||
stepResponses?: Record<string | number, RequestResult>;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'applyStep', request: RequestParam): void;
|
||||
|
@ -355,7 +357,14 @@
|
|||
() => visible.value,
|
||||
async (val) => {
|
||||
if (val) {
|
||||
requestVModel.value = { ...cloneDeep(defaultCaseParams), ...props.request };
|
||||
requestVModel.value = {
|
||||
...cloneDeep(defaultCaseParams),
|
||||
...props.request,
|
||||
response: {
|
||||
requestResults: [props.stepResponses?.[props.request?.stepId] || defaultResponse.requestResults[0]],
|
||||
console: props.stepResponses?.[props.request?.stepId].console || '',
|
||||
},
|
||||
};
|
||||
if (isQuote.value || isCopyNeedInit.value) {
|
||||
// 引用时,需要初始化引用的详情;复制只在第一次初始化的时候需要加载后台数据(request.request是复制请求时列表参数字段request会为 null,以此判断释放第一次初始化)
|
||||
initQuoteCaseDetail();
|
||||
|
|
|
@ -22,10 +22,15 @@
|
|||
color: 'rgb(var(--link-6))',
|
||||
text: 'common.stop',
|
||||
},
|
||||
[ScenarioExecuteStatus.EXECUTING]: {
|
||||
bgColor: 'rgb(var(--link-2))',
|
||||
color: 'rgb(var(--link-6))',
|
||||
text: 'apiScenario.running',
|
||||
},
|
||||
[ScenarioExecuteStatus.FAILED]: {
|
||||
bgColor: 'rgb(var(--danger-2))',
|
||||
color: 'rgb(var(--danger-6))',
|
||||
text: 'common.failed',
|
||||
text: 'common.fail',
|
||||
},
|
||||
[ScenarioExecuteStatus.SUCCESS]: {
|
||||
bgColor: 'rgb(var(--success-2))',
|
||||
|
|
|
@ -63,6 +63,7 @@ export const defaultStepItemCommon = {
|
|||
enable: true,
|
||||
},
|
||||
createActionsVisible: false,
|
||||
responsePopoverVisible: false,
|
||||
};
|
||||
|
||||
export const defaultScenario: Scenario = {
|
||||
|
@ -112,6 +113,8 @@ export const defaultScenario: Scenario = {
|
|||
isNew: true,
|
||||
unSaved: false,
|
||||
executeLoading: false, // 执行loading
|
||||
isDebug: false,
|
||||
stepResponses: {},
|
||||
};
|
||||
|
||||
export const conditionOptions = [
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
v-show="scenario.steps.length > 0"
|
||||
v-model:model-value="checkedAll"
|
||||
:indeterminate="indeterminate"
|
||||
:disabled="scenarioExecuteLoading"
|
||||
@change="handleChangeAll"
|
||||
/>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
|
@ -28,16 +29,16 @@
|
|||
</a-button>
|
||||
</a-tooltip>
|
||||
<template v-if="checkedAll || indeterminate">
|
||||
<a-button type="outline" size="mini" @click="batchEnable">
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchEnable">
|
||||
{{ t('common.batchEnable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDisable">
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDisable">
|
||||
{{ t('common.batchDisable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDebug">
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDebug">
|
||||
{{ t('common.batchDebug') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" @click="batchDelete">
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDelete">
|
||||
{{ t('common.batchDelete') }}
|
||||
</a-button>
|
||||
</template>
|
||||
|
@ -57,7 +58,9 @@
|
|||
<div class="text-[var(--color-text-1)]">{{ t('common.fail') }}</div>
|
||||
<div class="text-[rgb(var(--success-6))]">{{ scenario.executeFailCount }}</div>
|
||||
</div>
|
||||
<MsButton type="text" @click="checkReport">{{ t('apiScenario.checkReport') }}</MsButton>
|
||||
<MsButton v-if="scenario.isDebug === false" type="text" @click="checkReport">
|
||||
{{ t('apiScenario.checkReport') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!checkedAll && !indeterminate" class="action-group ml-auto">
|
||||
|
@ -87,6 +90,8 @@
|
|||
v-model:stepKeyword="keyword"
|
||||
:expand-all="isExpandAll"
|
||||
:step-details="scenario.stepDetails"
|
||||
:step-responses="scenario.stepResponses"
|
||||
@update-resource="handleUpdateResource"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -118,27 +123,26 @@
|
|||
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 { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import { ScenarioExecuteStatus } from '@/enums/apiEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
isNew?: boolean; // 是否新建
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'batchDebug', data: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId'>): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const scenario = defineModel<Scenario>('scenario', {
|
||||
required: true,
|
||||
});
|
||||
const scenarioExecuteLoading = inject<Ref<boolean>>('scenarioExecuteLoading');
|
||||
|
||||
const checkedAll = ref(false); // 是否全选
|
||||
const indeterminate = ref(false); // 是否半选
|
||||
|
@ -229,95 +233,46 @@
|
|||
console.log('刷新步骤信息');
|
||||
}
|
||||
|
||||
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);
|
||||
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;
|
||||
scenario.value.executeLoading = true;
|
||||
const checkedKeysSet = new Set(checkedKeys.value);
|
||||
const waitTingDebugSteps = filterTree(scenario.value.steps, (node) => {
|
||||
if (checkedKeysSet.has(node.id)) {
|
||||
if (!node.enable) {
|
||||
// 如果步骤未开启,则删除已选 id,方便下面waitingDebugStepDetails详情判断是否携带
|
||||
checkedKeysSet.delete(node.id);
|
||||
} else {
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
'id'
|
||||
);
|
||||
return !!node.enable;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
const waitingDebugStepDetails = {};
|
||||
Object.keys(scenario.value.stepDetails).forEach((key) => {
|
||||
if (selectedKeysSet.has(key)) {
|
||||
if (checkedKeysSet.has(key)) {
|
||||
waitingDebugStepDetails[key] = scenario.value.stepDetails[key];
|
||||
}
|
||||
});
|
||||
realExecute({
|
||||
id: scenario.value.id || '',
|
||||
emit('batchDebug', {
|
||||
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,
|
||||
reportId: getGenerateId(),
|
||||
});
|
||||
}
|
||||
|
||||
function handleUpdateResource(uploadFileIds, linkFileIds) {
|
||||
const uploadFileIdsSet = new Set(scenario.value.uploadFileIds);
|
||||
const linkFileIdsSet = new Set(scenario.value.linkFileIds);
|
||||
uploadFileIds.forEach((id) => {
|
||||
uploadFileIdsSet.add(id);
|
||||
});
|
||||
linkFileIds.forEach((id) => {
|
||||
linkFileIdsSet.add(id);
|
||||
});
|
||||
// scenario.value.uploadFileIds = Array.from(uploadFileIdsSet);
|
||||
// scenario.value.linkFileIds = Array.from(linkFileIdsSet);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less">
|
||||
|
|
|
@ -168,7 +168,33 @@
|
|||
/>
|
||||
</template>
|
||||
<template #extraEnd="step">
|
||||
<executeStatus v-if="step.executeStatus" :status="step.executeStatus" size="small" />
|
||||
<a-popover
|
||||
v-if="step.executeStatus"
|
||||
position="br"
|
||||
content-class="scenario-step-response-popover"
|
||||
@popup-visible-change="handleResponsePopoverVisibleChange($event, step)"
|
||||
>
|
||||
<executeStatus :status="getExecuteStatus(step) || step.executeStatus" size="small" />
|
||||
<template #content>
|
||||
<responseResult
|
||||
:active-tab="ResponseComposition.BODY"
|
||||
:request-result="props.stepResponses?.[step.id]"
|
||||
:console="props.stepResponses?.[step.id].console"
|
||||
:show-empty="false"
|
||||
:is-edit="false"
|
||||
is-definition
|
||||
>
|
||||
<template #titleLeft>
|
||||
<div class="flex items-center text-[14px]">
|
||||
<div class="font-medium text-[var(--color-text-1)]">{{ t('apiScenario.response') }}</div>
|
||||
<a-tooltip :content="step.name">
|
||||
<div class="one-line-text">({{ step.name }})</div>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
</responseResult>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
|
||||
<div
|
||||
|
@ -192,6 +218,7 @@
|
|||
:env-detail-item="{ id: 'demp-id-112233', projectId: '123456', name: 'demo环境' }"
|
||||
:request="currentStepDetail"
|
||||
:step="activeStep"
|
||||
:step-responses="props.stepResponses"
|
||||
@add-step="addCustomApiStep"
|
||||
@apply-step="applyApiStep"
|
||||
/>
|
||||
|
@ -199,6 +226,7 @@
|
|||
v-model:visible="customCaseDrawerVisible"
|
||||
:active-step="activeStep"
|
||||
:request="currentStepDetail"
|
||||
:step-responses="props.stepResponses"
|
||||
@apply-step="applyApiStep"
|
||||
@delete-step="deleteCaseStep"
|
||||
/>
|
||||
|
@ -266,6 +294,7 @@
|
|||
import waitTimeContent from './stepNodeComposition/waitTimeContent.vue';
|
||||
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
|
||||
import { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
|
||||
import responseResult from '@/views/api-test/components/requestComposition/response/index.vue';
|
||||
|
||||
import { getScenarioStep } from '@/api/modules/api-test/scenario';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
@ -280,9 +309,15 @@
|
|||
TreeNode,
|
||||
} from '@/utils';
|
||||
|
||||
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
|
||||
import { ExecuteConditionProcessor, RequestResult } from '@/models/apiTest/common';
|
||||
import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import { ScenarioAddStepActionType, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
||||
import {
|
||||
ResponseComposition,
|
||||
ScenarioAddStepActionType,
|
||||
ScenarioExecuteStatus,
|
||||
ScenarioStepRefType,
|
||||
ScenarioStepType,
|
||||
} from '@/enums/apiEnum';
|
||||
|
||||
import type { RequestParam } from '../common/customApiDrawer.vue';
|
||||
import useCreateActions from './createAction/useCreateActions';
|
||||
|
@ -299,6 +334,10 @@
|
|||
const props = defineProps<{
|
||||
stepKeyword: string;
|
||||
expandAll?: boolean;
|
||||
stepResponses?: Record<string | number, RequestResult>;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateResource', uploadFileIds: string[], linkFileIds: string[]): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
@ -314,11 +353,31 @@
|
|||
const stepDetails = defineModel<Record<string, any>>('stepDetails', {
|
||||
required: true,
|
||||
});
|
||||
const scenarioExecuteLoading = inject<Ref<boolean>>('scenarioExecuteLoading');
|
||||
|
||||
const selectedKeys = ref<(string | number)[]>([]); // 没啥用,目前用来展示选中样式
|
||||
const loading = ref(false);
|
||||
const treeRef = ref<InstanceType<typeof MsTree>>();
|
||||
const focusStepKey = ref<string>(''); // 聚焦的key
|
||||
const focusStepKey = ref<string | number>(''); // 聚焦的key
|
||||
|
||||
function setFocusNodeKey(id: string | number) {
|
||||
focusStepKey.value = id || '';
|
||||
}
|
||||
|
||||
function getExecuteStatus(step: ScenarioStepItem) {
|
||||
if (props.stepResponses && props.stepResponses[step.id]) {
|
||||
return props.stepResponses[step.id].isSuccessful ? ScenarioExecuteStatus.SUCCESS : ScenarioExecuteStatus.FAILED;
|
||||
}
|
||||
return step.executeStatus;
|
||||
}
|
||||
|
||||
function handleResponsePopoverVisibleChange(visible: boolean, step: ScenarioStepItem) {
|
||||
if (visible) {
|
||||
setFocusNodeKey(step.id);
|
||||
} else {
|
||||
setFocusNodeKey('');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据步骤类型获取步骤内容组件
|
||||
|
@ -342,10 +401,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
function setFocusNodeKey(id: string) {
|
||||
focusStepKey.value = id || '';
|
||||
}
|
||||
|
||||
function checkStepIsApi(step: ScenarioStepItem) {
|
||||
return [ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(step.stepType);
|
||||
}
|
||||
|
@ -630,6 +685,9 @@
|
|||
}
|
||||
|
||||
function executeStep(node: MsTreeNodeData) {
|
||||
if (scenarioExecuteLoading?.value) {
|
||||
return;
|
||||
}
|
||||
console.log('执行步骤', node);
|
||||
}
|
||||
|
||||
|
@ -721,6 +779,7 @@
|
|||
function addCustomApiStep(request: RequestParam) {
|
||||
request.isNew = false;
|
||||
stepDetails.value[request.stepId] = request;
|
||||
emit('updateResource', request.uploadFileIds, request.linkFileIds);
|
||||
if (activeStep.value && activeCreateAction.value) {
|
||||
handleCreateStep(
|
||||
{
|
||||
|
@ -761,6 +820,7 @@
|
|||
if (activeStep.value) {
|
||||
request.isNew = false;
|
||||
stepDetails.value[activeStep.value?.id] = request;
|
||||
emit('updateResource', request.uploadFileIds, request.linkFileIds);
|
||||
activeStep.value = undefined;
|
||||
}
|
||||
}
|
||||
|
@ -947,6 +1007,27 @@
|
|||
color: rgb(var(--primary-5));
|
||||
background-color: rgb(var(--primary-1));
|
||||
}
|
||||
.scenario-step-response-popover {
|
||||
width: 500px;
|
||||
height: 450px;
|
||||
.arco-popover-content {
|
||||
@apply h-full;
|
||||
.response {
|
||||
.response-head {
|
||||
background-color: var(--color-text-n9);
|
||||
}
|
||||
|
||||
border: 1px solid var(--color-text-n8);
|
||||
border-radius: var(--border-radius-small);
|
||||
.arco-spin {
|
||||
padding: 0;
|
||||
.response-container {
|
||||
padding: 0 16px 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -7,7 +7,12 @@
|
|||
:title="t('apiScenario.step')"
|
||||
class="scenario-create-tab-pane"
|
||||
>
|
||||
<step v-if="activeKey === ScenarioCreateComposition.STEP" v-model:scenario="scenario" is-new />
|
||||
<step
|
||||
v-if="activeKey === ScenarioCreateComposition.STEP"
|
||||
v-model:scenario="scenario"
|
||||
is-new
|
||||
@batch-debug="emit('batchDebug', $event)"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioCreateComposition.PARAMS"
|
||||
|
@ -171,7 +176,7 @@
|
|||
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
|
||||
import { Scenario } from '@/models/apiTest/scenario';
|
||||
import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { ApiScenarioStatus, ScenarioCreateComposition } from '@/enums/apiEnum';
|
||||
|
||||
|
@ -187,6 +192,9 @@
|
|||
const props = defineProps<{
|
||||
moduleTree: ModuleTreeNode[]; // 模块树
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'batchDebug', data: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId'>): void;
|
||||
}>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
|
|
@ -48,7 +48,11 @@
|
|||
:title="t('apiScenario.step')"
|
||||
class="scenario-detail-tab-pane"
|
||||
>
|
||||
<step v-if="activeKey === ScenarioDetailComposition.STEP" v-model:scenario="scenario" />
|
||||
<step
|
||||
v-if="activeKey === ScenarioDetailComposition.STEP"
|
||||
v-model:scenario="scenario"
|
||||
@batch-debug="emit('batchDebug', $event)"
|
||||
/>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane
|
||||
:key="ScenarioDetailComposition.PARAMS"
|
||||
|
@ -133,7 +137,7 @@
|
|||
import step from '../components/step/index.vue';
|
||||
import apiStatus from '@/views/api-test/components/apiStatus.vue';
|
||||
|
||||
import { Scenario, ScenarioDetail } from '@/models/apiTest/scenario';
|
||||
import { ApiScenarioDebugRequest, Scenario, ScenarioDetail } from '@/models/apiTest/scenario';
|
||||
import { ScenarioDetailComposition } from '@/enums/apiEnum';
|
||||
|
||||
// 组成部分异步导入
|
||||
|
@ -146,7 +150,10 @@
|
|||
// const quote = defineAsyncComponent(() => import('../components/quote.vue'));
|
||||
const setting = defineAsyncComponent(() => import('../components/setting.vue'));
|
||||
|
||||
const emit = defineEmits(['updateFollow']);
|
||||
const emit = defineEmits<{
|
||||
(e: 'batchDebug', data: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId'>): void;
|
||||
(e: 'updateFollow'): void;
|
||||
}>();
|
||||
|
||||
const { copy, isSupported } = useClipboard();
|
||||
const { t } = useI18n();
|
||||
|
|
|
@ -66,10 +66,15 @@
|
|||
</MsSplitBox>
|
||||
</div>
|
||||
<div v-else-if="activeScenarioTab.isNew" class="pageWrap">
|
||||
<create ref="createRef" v-model:scenario="activeScenarioTab" :module-tree="folderTree"></create>
|
||||
<create
|
||||
ref="createRef"
|
||||
v-model:scenario="activeScenarioTab"
|
||||
:module-tree="folderTree"
|
||||
@batch-debug="realExecute"
|
||||
></create>
|
||||
</div>
|
||||
<div v-else class="pageWrap">
|
||||
<detail v-model:scenario="activeScenarioTab"></detail>
|
||||
<detail v-model:scenario="activeScenarioTab" @batch-debug="realExecute"></detail>
|
||||
</div>
|
||||
</MsCard>
|
||||
</template>
|
||||
|
@ -81,6 +86,7 @@
|
|||
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import MsCard from '@/components/pure/ms-card/index.vue';
|
||||
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
|
||||
|
@ -104,7 +110,7 @@
|
|||
import { useI18n } from '@/hooks/useI18n';
|
||||
import router from '@/router';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
import { getGenerateId } from '@/utils';
|
||||
import { filterTree, getGenerateId } from '@/utils';
|
||||
|
||||
import {
|
||||
ApiScenarioDebugRequest,
|
||||
|
@ -114,6 +120,7 @@
|
|||
} from '@/models/apiTest/scenario';
|
||||
import { ModuleTreeNode } from '@/models/common';
|
||||
import { EnvConfig } from '@/models/projectManagement/environmental';
|
||||
import { ScenarioExecuteStatus } from '@/enums/apiEnum';
|
||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
import { defaultScenario } from './components/config';
|
||||
|
@ -124,6 +131,7 @@
|
|||
|
||||
export type ScenarioParams = Scenario & TabItem;
|
||||
|
||||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const scenarioTabs = ref<ScenarioParams[]>([
|
||||
|
@ -142,6 +150,7 @@
|
|||
id: isCopy ? getGenerateId() : defaultScenarioInfo.id || '',
|
||||
label: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name,
|
||||
isNew: false,
|
||||
stepResponses: {},
|
||||
});
|
||||
} else {
|
||||
scenarioTabs.value.push({
|
||||
|
@ -149,6 +158,7 @@
|
|||
id: getGenerateId(),
|
||||
label: `${t('apiScenario.createScenario')}${scenarioTabs.value.length}`,
|
||||
moduleId: 'root',
|
||||
projectId: appStore.currentProjectId,
|
||||
priority: 'P0',
|
||||
});
|
||||
}
|
||||
|
@ -166,7 +176,6 @@
|
|||
const getActiveClass = (type: string) => {
|
||||
return activeFolder.value === type ? 'folder-text case-active' : 'folder-text';
|
||||
};
|
||||
const appStore = useAppStore();
|
||||
const recycleModulesCount = ref(0);
|
||||
|
||||
const scenarioModuleTreeRef = ref<InstanceType<typeof scenarioModuleTree>>();
|
||||
|
@ -272,16 +281,15 @@
|
|||
}
|
||||
|
||||
const currentEnvConfig = ref<EnvConfig>();
|
||||
const reportId = ref('');
|
||||
const websocket = ref<WebSocket>();
|
||||
const temporaryScenarioReportMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
|
||||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
*/
|
||||
function debugSocket(executeType?: 'localExec' | 'serverExec', localExecuteUrl?: string) {
|
||||
function debugSocket(reportId?: string | number, executeType?: 'localExec' | 'serverExec', localExecuteUrl?: string) {
|
||||
websocket.value = getSocket(
|
||||
reportId.value,
|
||||
reportId || '',
|
||||
executeType === 'localExec' ? '/ws/debug' : '',
|
||||
executeType === 'localExec' ? localExecuteUrl : ''
|
||||
);
|
||||
|
@ -289,12 +297,31 @@
|
|||
const data = JSON.parse(event.data);
|
||||
if (data.msgType === 'EXEC_RESULT') {
|
||||
if (activeScenarioTab.value.reportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
activeScenarioTab.value.isExecute = false;
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||
data.taskResult.requestResults.forEach((result) => {
|
||||
activeScenarioTab.value.stepResponses[result.stepId] = {
|
||||
...result,
|
||||
console: data.taskResult.console,
|
||||
};
|
||||
if (result.isSuccessful) {
|
||||
activeScenarioTab.value.executeSuccessCount += 1;
|
||||
} else {
|
||||
activeScenarioTab.value.executeFailCount += 1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 不是则需要把报告缓存起来,等切换到对应的tab再赋值
|
||||
temporaryScenarioReportMap[data.reportId] = data.taskResult;
|
||||
data.taskResult.requestResults.forEach((result) => {
|
||||
if (activeScenarioTab.value.reportId) {
|
||||
if (temporaryScenarioReportMap[activeScenarioTab.value.reportId] === undefined) {
|
||||
temporaryScenarioReportMap[activeScenarioTab.value.reportId] = {};
|
||||
}
|
||||
temporaryScenarioReportMap[activeScenarioTab.value.reportId][result.stepId] = {
|
||||
...result,
|
||||
console: data.taskResult.console,
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
|
@ -308,34 +335,55 @@
|
|||
}
|
||||
|
||||
async function realExecute(
|
||||
executeParams: ApiScenarioDebugRequest,
|
||||
executeParams: Pick<ApiScenarioDebugRequest, 'steps' | 'stepDetails' | 'reportId'>,
|
||||
executeType?: 'localExec' | 'serverExec',
|
||||
localExecuteUrl?: string
|
||||
) {
|
||||
try {
|
||||
activeScenarioTab.value.executeLoading = true;
|
||||
reportId.value = getGenerateId();
|
||||
activeScenarioTab.value.reportId = reportId.value; // 存储报告ID
|
||||
debugSocket(executeType, localExecuteUrl); // 开启websocket
|
||||
executeParams.environmentId = currentEnvConfig.value?.id || '';
|
||||
const res = await debugScenario(executeParams);
|
||||
debugSocket(executeParams.reportId, executeType, localExecuteUrl); // 开启websocket
|
||||
// 重置执行结果
|
||||
activeScenarioTab.value.executeTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
activeScenarioTab.value.executeSuccessCount = 0;
|
||||
activeScenarioTab.value.executeFailCount = 0;
|
||||
activeScenarioTab.value.stepResponses = {};
|
||||
activeScenarioTab.value.reportId = executeParams.reportId; // 存储报告ID
|
||||
activeScenarioTab.value.isDebug = true;
|
||||
const res = await debugScenario({
|
||||
id: activeScenarioTab.value.id,
|
||||
grouped: false,
|
||||
environmentId: currentEnvConfig.value?.id || '',
|
||||
projectId: appStore.currentProjectId,
|
||||
scenarioConfig: activeScenarioTab.value.scenarioConfig,
|
||||
uploadFileIds: activeScenarioTab.value.uploadFileIds,
|
||||
linkFileIds: activeScenarioTab.value.linkFileIds,
|
||||
...executeParams,
|
||||
});
|
||||
if (executeType === 'localExec' && localExecuteUrl) {
|
||||
await localExecuteApiDebug(localExecuteUrl, res);
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
websocket.value?.close();
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleExecute(executeType?: 'localExec' | 'serverExec', localExecuteUrl?: string) {
|
||||
const environmentId = currentEnvConfig.value?.id || '';
|
||||
const waitingDebugStepDetails = {};
|
||||
const waitTingDebugSteps = filterTree(activeScenarioTab.value.steps, (node) => {
|
||||
if (node.enable) {
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
waitingDebugStepDetails[node.id] = activeScenarioTab.value.stepDetails[node.id];
|
||||
}
|
||||
return !!node.enable;
|
||||
});
|
||||
realExecute(
|
||||
{
|
||||
grouped: false,
|
||||
environmentId,
|
||||
...activeScenarioTab.value,
|
||||
steps: waitTingDebugSteps,
|
||||
stepDetails: waitingDebugStepDetails,
|
||||
reportId: getGenerateId(),
|
||||
},
|
||||
executeType,
|
||||
localExecuteUrl
|
||||
|
@ -347,13 +395,36 @@
|
|||
activeScenarioTab.value.executeLoading = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => activeScenarioTab.value.id,
|
||||
(val) => {
|
||||
if (val !== 'all' && activeScenarioTab.value.reportId && !activeScenarioTab.value.executeLoading) {
|
||||
// 当前查看的 tab 非全部场景 tab 页,且当前场景有报告ID,且不是正在执行中,则读取缓存报告
|
||||
const cacheReport = temporaryScenarioReportMap[activeScenarioTab.value.reportId];
|
||||
if (cacheReport) {
|
||||
// 如果有缓存的报告未读取,则直接赋值
|
||||
Object.keys(cacheReport).forEach((stepId) => {
|
||||
const result = cacheReport[stepId];
|
||||
activeScenarioTab.value.stepResponses[stepId] = result;
|
||||
if (result.isSuccessful) {
|
||||
activeScenarioTab.value.executeSuccessCount += 1;
|
||||
} else {
|
||||
activeScenarioTab.value.executeFailCount += 1;
|
||||
}
|
||||
});
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
delete temporaryScenarioReportMap[activeScenarioTab.value.reportId]; // 取完释放缓存
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const scenarioId = computed(() => activeScenarioTab.value.id);
|
||||
const scenarioExecuteLoading = computed(() => activeScenarioTab.value.executeLoading);
|
||||
// 为子孙组件提供属性
|
||||
provide('currentEnvConfig', readonly(currentEnvConfig));
|
||||
provide('scenarioId', scenarioId);
|
||||
provide('scenarioExecuteLoading', scenarioExecuteLoading);
|
||||
provide('temporaryScenarioReportMap', readonly(temporaryScenarioReportMap));
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
|
|
|
@ -136,6 +136,8 @@ export default {
|
|||
'apiScenario.allStep': '所有子步骤',
|
||||
'apiScenario.saveAsApi': '保存为新接口',
|
||||
'apiScenario.scenarioLevel': '场景等级',
|
||||
'apiScenario.running': '执行中',
|
||||
'apiScenario.response': '响应内容',
|
||||
// 执行历史
|
||||
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
|
||||
'apiScenario.executeHistory.num': '序号',
|
||||
|
|
Loading…
Reference in New Issue