feat(接口场景): 执行响应优化&循环响应展示

This commit is contained in:
baiqi 2024-04-01 18:22:24 +08:00 committed by 刘瑞斌
parent 2441a882cc
commit e8f1ce98cc
17 changed files with 365 additions and 126 deletions

View File

@ -3,7 +3,7 @@ import { SelectProps } from '@arco-design/web-vue';
import { Size } from './types'; import { Size } from './types';
export const PAGE_ITEM_TYPES = ['page', 'more', 'previous', 'next'] as const; export const PAGE_ITEM_TYPES = ['page', 'more', 'previous', 'next', 'jumper'] as const;
export type PageItemType = (typeof PAGE_ITEM_TYPES)[number]; export type PageItemType = (typeof PAGE_ITEM_TYPES)[number];

View File

@ -15,10 +15,12 @@
@change="handleChange" @change="handleChange"
@enter="handleChange" @enter="handleChange"
/> />
<span v-if="$slots['jumper-append']" :class="`${prefixCls}-append`"><slot name="jumper-append" /></span> <span v-if="$slots['jumper-append']" :class="`${prefixCls}-append`">
<span :class="`${prefixCls}-total-page`" :style="{ 'min-width': totalPageWidth }">{{ <slot name="jumper-append" />
t('msPagination.page', { page: pages }) </span>
}}</span> <span :class="`${prefixCls}-total-page`" :style="{ 'min-width': totalPageWidth }">
{{ t('msPagination.page', { page: pages }) }}
</span>
</span> </span>
</template> </template>

View File

@ -389,7 +389,7 @@ export interface Scenario {
executeSuccessCount: number; // 执行成功数量 executeSuccessCount: number; // 执行成功数量
executeFailCount: number; // 执行失败数量 executeFailCount: number; // 执行失败数量
reportId?: string | number; // 场景报告 id reportId?: string | number; // 场景报告 id
stepResponses: Record<string | number, RequestResult>; // 步骤响应集合key 为步骤 idvalue 为步骤响应内容 stepResponses: Record<string | number, Array<RequestResult>>; // 步骤响应集合key 为步骤 idvalue 为步骤响应内容
isExecute?: boolean; // 是否从列表执行进去场景详情 isExecute?: boolean; // 是否从列表执行进去场景详情
isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤 isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤
} }

View File

@ -1,4 +1,4 @@
import { cloneDeep } from 'lodash-es'; import { cloneDeep, each } from 'lodash-es';
import JSEncrypt from 'jsencrypt'; import JSEncrypt from 'jsencrypt';
import { BatchActionQueryParams, MsTableColumnData } from '@/components/pure/ms-table/type'; import { BatchActionQueryParams, MsTableColumnData } from '@/components/pure/ms-table/type';
@ -194,6 +194,31 @@ export interface TreeNode<T> {
* @param tree * @param tree
* @param customNodeFn * @param customNodeFn
* @param customChildrenKey key * @param customChildrenKey key
*/
export function traverseTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
customNodeFn: (node: TreeNode<T>) => TreeNode<T> | null = (node) => node,
customChildrenKey = 'children'
) {
if (!Array.isArray(tree)) {
tree = [tree];
}
for (let i = 0; i < tree.length; i++) {
const node = tree[i];
if (typeof customNodeFn === 'function') {
customNodeFn(node);
}
if (node[customChildrenKey] && Array.isArray(node[customChildrenKey]) && node[customChildrenKey].length > 0) {
traverseTree(node[customChildrenKey], customNodeFn, customChildrenKey);
}
}
}
/**
*
* @param tree
* @param customNodeFn
* @param customChildrenKey key
* @param parent * @param parent
* @param parentPath * @param parentPath
* @param level * @param level

View File

@ -61,7 +61,9 @@
value: item.id, value: item.id,
})); }));
currentEnv.value = currentEnv.value.length ? currentEnv.value : res[0]?.id; currentEnv.value = currentEnv.value.length ? currentEnv.value : res[0]?.id;
await initEnvironment(); if (currentEnv.value) {
await initEnvironment();
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);

View File

@ -20,7 +20,6 @@
<MsEditableTab <MsEditableTab
v-model:active-tab="activeDebug" v-model:active-tab="activeDebug"
v-model:tabs="debugTabs" v-model:tabs="debugTabs"
:limit="10"
:readonly="!hasAnyPermission(['PROJECT_API_DEBUG:READ+ADD'])" :readonly="!hasAnyPermission(['PROJECT_API_DEBUG:READ+ADD'])"
at-least-one at-least-one
@add="addDebugTab" @add="addDebugTab"

View File

@ -12,7 +12,10 @@
> >
<template #title> <template #title>
<div class="flex max-w-[60%] items-center gap-[8px]"> <div class="flex max-w-[60%] items-center gap-[8px]">
<stepTypeVue v-if="props.step" :step="props.step" /> <stepTypeVue
v-if="props.step && [ScenarioStepType.API, ScenarioStepType.CUSTOM_REQUEST].includes(props.step?.stepType)"
:step="props.step"
/>
<a-tooltip :content="title" position="bottom"> <a-tooltip :content="title" position="bottom">
<div class="one-line-text"> <div class="one-line-text">
{{ title }} {{ title }}
@ -228,7 +231,7 @@
<postcondition <postcondition
v-else-if="requestVModel.activeTab === RequestComposition.POST_CONDITION" v-else-if="requestVModel.activeTab === RequestComposition.POST_CONDITION"
v-model:config="requestVModel.children[0].postProcessorConfig" v-model:config="requestVModel.children[0].postProcessorConfig"
:response="props.stepResponses?.[requestVModel.stepId]?.responseResult.body" :response="responseResultBody"
:layout="activeLayout" :layout="activeLayout"
:disabled="!isEditableApi || isQuoteScenarioStep" :disabled="!isEditableApi || isQuoteScenarioStep"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@ -238,7 +241,7 @@
<assertion <assertion
v-else-if="requestVModel.activeTab === RequestComposition.ASSERTION" v-else-if="requestVModel.activeTab === RequestComposition.ASSERTION"
v-model:params="requestVModel.children[0].assertionConfig.assertions" v-model:params="requestVModel.children[0].assertionConfig.assertions"
:response="props.stepResponses?.[requestVModel.stepId]?.responseResult.body" :response="responseResultBody"
is-definition is-definition
:disabled="!isEditableApi || isQuoteScenarioStep" :disabled="!isEditableApi || isQuoteScenarioStep"
:assertion-config="requestVModel.children[0].assertionConfig" :assertion-config="requestVModel.children[0].assertionConfig"
@ -269,8 +272,8 @@
:is-priority-local-exec="isPriorityLocalExec" :is-priority-local-exec="isPriorityLocalExec"
:request-url="requestVModel.url" :request-url="requestVModel.url"
:is-expanded="isVerticalExpanded" :is-expanded="isVerticalExpanded"
:request-result="props.stepResponses?.[requestVModel.stepId]" :request-result="requestResult"
:console="props.stepResponses?.[requestVModel.stepId]?.console" :console="requestResult?.console"
:is-edit="false" :is-edit="false"
is-definition is-definition
:loading="requestVModel.executeLoading || loading" :loading="requestVModel.executeLoading || loading"
@ -387,7 +390,7 @@
create: string; create: string;
update: string; update: string;
}; };
stepResponses?: Record<string | number, RequestResult>; stepResponses?: Record<string | number, RequestResult[]>;
fileParams?: ScenarioStepFileParams; fileParams?: ScenarioStepFileParams;
}>(); }>();
@ -485,13 +488,27 @@
if (_stepType.value.isCopyApi || _stepType.value.isQuoteApi) { if (_stepType.value.isCopyApi || _stepType.value.isQuoteApi) {
return props.step?.name; return props.step?.name;
} }
return props.step?.name || t('apiScenario.customApi'); return t('apiScenario.customApi');
}); });
const showEnvPrefix = computed( const showEnvPrefix = computed(
() => () =>
requestVModel.value.customizeRequestEnvEnable && requestVModel.value.customizeRequestEnvEnable &&
currentEnvConfig?.value.httpConfig.find((e) => e.type === 'NONE')?.url currentEnvConfig?.value.httpConfig.find((e) => e.type === 'NONE')?.url
); );
const responseResultBody = computed(() => {
const length = props.stepResponses?.[requestVModel.value.stepId]
? props.stepResponses?.[requestVModel.value.stepId]?.length
: 0;
//
return props.stepResponses?.[requestVModel.value.stepId]?.[length].responseResult.body;
});
const requestResult = computed(() => {
const length = props.stepResponses?.[requestVModel.value.stepId]
? props.stepResponses?.[requestVModel.value.stepId]?.length
: 0;
//
return props.stepResponses?.[requestVModel.value.stepId]?.[length];
});
watch( watch(
() => props.stepResponses, () => props.stepResponses,
@ -1086,6 +1103,7 @@
requestVModel.value = cloneDeep({ requestVModel.value = cloneDeep({
...defaultApiParams, ...defaultApiParams,
...props.request, ...props.request,
url: props.request.path, // path
activeTab: contentTabList.value[0].value, activeTab: contentTabList.value[0].value,
responseActiveTab: ResponseComposition.BODY, responseActiveTab: ResponseComposition.BODY,
isNew: false, isNew: false,

View File

@ -178,7 +178,7 @@
<postcondition <postcondition
v-else-if="requestVModel.activeTab === RequestComposition.POST_CONDITION" v-else-if="requestVModel.activeTab === RequestComposition.POST_CONDITION"
v-model:config="requestVModel.children[0].postProcessorConfig" v-model:config="requestVModel.children[0].postProcessorConfig"
:response="props.stepResponses?.[requestVModel.stepId]?.responseResult.body" :response="responseResultBody"
:layout="activeLayout" :layout="activeLayout"
:disabled="!isEditableApi" :disabled="!isEditableApi"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@ -188,7 +188,7 @@
<assertion <assertion
v-else-if="requestVModel.activeTab === RequestComposition.ASSERTION" v-else-if="requestVModel.activeTab === RequestComposition.ASSERTION"
v-model:params="requestVModel.children[0].assertionConfig.assertions" v-model:params="requestVModel.children[0].assertionConfig.assertions"
:response="props.stepResponses?.[requestVModel.stepId]?.responseResult.body" :response="responseResultBody"
is-definition is-definition
:disabled="!isEditableApi" :disabled="!isEditableApi"
:assertion-config="requestVModel.children[0].assertionConfig" :assertion-config="requestVModel.children[0].assertionConfig"
@ -219,8 +219,8 @@
:is-priority-local-exec="isPriorityLocalExec" :is-priority-local-exec="isPriorityLocalExec"
:request-url="requestVModel.url" :request-url="requestVModel.url"
:is-expanded="isVerticalExpanded" :is-expanded="isVerticalExpanded"
:request-result="props.stepResponses?.[requestVModel.stepId]" :request-result="requestResult"
:console="props.stepResponses?.[requestVModel.stepId]?.console" :console="requestResult?.console"
:is-edit="false" :is-edit="false"
is-definition is-definition
:loading="requestVModel.executeLoading || loading" :loading="requestVModel.executeLoading || loading"
@ -297,7 +297,7 @@
const props = defineProps<{ const props = defineProps<{
request?: RequestParam; // request?: RequestParam; //
stepResponses?: Record<string | number, RequestResult>; stepResponses?: Record<string | number, RequestResult[]>;
fileParams?: ScenarioStepFileParams; fileParams?: ScenarioStepFileParams;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
@ -396,6 +396,20 @@
() => () =>
activeStep.value?.stepType === ScenarioStepType.API_CASE && activeStep.value?.refType === ScenarioStepRefType.REF activeStep.value?.stepType === ScenarioStepType.API_CASE && activeStep.value?.refType === ScenarioStepRefType.REF
); );
const responseResultBody = computed(() => {
const length = props.stepResponses?.[requestVModel.value.stepId]
? props.stepResponses?.[requestVModel.value.stepId]?.length
: 0;
//
return props.stepResponses?.[requestVModel.value.stepId]?.[length].responseResult.body;
});
const requestResult = computed(() => {
const length = props.stepResponses?.[requestVModel.value.stepId]
? props.stepResponses?.[requestVModel.value.stepId]?.length
: 0;
//
return props.stepResponses?.[requestVModel.value.stepId]?.[length];
});
watch( watch(
() => props.stepResponses, () => props.stepResponses,

View File

@ -0,0 +1,95 @@
<template>
<MsPagination
v-if="props.loopTotal > 1"
v-model:current="currentLoop"
:page-size="1"
:total="props.loopTotal"
:show-jumper="props.loopTotal > 5"
show-total
size="mini"
class="loop-pagination"
>
<template #total="{ total }">
<div
class="rounded-[var(--border-radius-small)] bg-[var(--color-text-n8)] p-[2px_6px] leading-[20px] text-[var(--color-text-2)]"
>
{{ t('apiScenario.sumLoop', { count: total }) }}
</div>
</template>
<template #jumper-append>
{{ t('apiScenario.times') }}
</template>
</MsPagination>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import MsPagination from '@/components/pure/ms-pagination';
const props = defineProps<{
loopTotal: number;
}>();
const { t } = useI18n();
const currentLoop = defineModel<number>('currentLoop', {
required: true,
});
</script>
<style lang="less">
.arco-popover {
.loop-pagination {
@apply justify-start;
gap: 2px;
margin-bottom: 8px;
.ms-pagination-list {
gap: 2px;
padding: 2px;
border-radius: var(--border-radius-small);
background-color: var(--color-text-n8);
.ms-pagination-item {
padding: 0 6px;
min-width: 20px;
height: 20px;
border: none;
border-radius: var(--border-radius-mini);
color: var(--color-text-1);
background-color: white;
line-height: 20px;
}
.ms-pagination-item-previous {
margin-left: 0;
}
.ms-pagination-item-disabled {
cursor: not-allowed;
color: var(--color-text-4);
}
.ms-pagination-item-active {
border: 1px solid rgb(var(--primary-5)) !important;
color: rgb(var(--primary-5)) !important;
background-color: white !important;
}
}
.ms-pagination-jumper {
gap: 4px;
padding: 2px 6px;
border-radius: var(--border-radius-mini);
color: var(--color-text-2);
background-color: var(--color-text-n8);
.ms-pagination-jumper-input {
width: 48px;
background-color: white;
input {
height: 18px;
}
}
.ms-pagination-jumper-total-page {
@apply hidden;
}
}
}
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<a-popover
position="br"
content-class="scenario-step-response-popover"
@popup-visible-change="emit('visibleChange', $event, props.step)"
>
<executeStatus :status="lastExecuteStatus" size="small" class="ml-[4px]" />
<template #content>
<div class="flex h-full flex-col">
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
<div class="flex-1">
<responseResult
:active-tab="ResponseComposition.BODY"
:request-result="currentResponse"
:console="currentResponse?.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="props.step.name">
<div class="one-line-text">({{ props.step.name }})</div>
</a-tooltip>
</div>
</template>
</responseResult>
</div>
</div>
</template>
</a-popover>
</template>
<script lang="ts" setup>
import executeStatus from './executeStatus.vue';
import loopPagination from './loopPagination.vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ResponseComposition, ScenarioExecuteStatus } from '@/enums/apiEnum';
const responseResult = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/index.vue')
);
const props = defineProps<{
step: ScenarioStepItem;
stepResponses: Record<string | number, Array<RequestResult>>;
}>();
const emit = defineEmits(['visibleChange']);
const { t } = useI18n();
const currentLoop = ref(1);
const currentResponse = computed(() => props.stepResponses?.[props.step.id]?.[currentLoop.value - 1]);
const loopTotal = computed(() => props.stepResponses?.[props.step.id]?.length || 0);
const lastExecuteStatus = computed(() => {
if (props.stepResponses[props.step.id] && props.stepResponses[props.step.id].length > 0) {
//
return props.stepResponses[props.step.id].some((report) => !report.isSuccessful)
? ScenarioExecuteStatus.FAILED
: ScenarioExecuteStatus.SUCCESS;
}
return props.step.executeStatus;
});
</script>
<style lang="less">
.scenario-step-response-popover {
width: 540px;
height: 500px;
.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>

View File

@ -82,8 +82,16 @@
() => visible.value, () => visible.value,
(val) => { (val) => {
if (val) { if (val) {
scriptName.value = props.name || ''; scriptName.value = props.detail ? props.name || '' : '';
activeItem.value = cloneDeep(props.detail || defaultScript); activeItem.value = cloneDeep(
props.detail
? {
...props.detail,
processorType: RequestConditionProcessor.SCRIPT,
polymorphicName: 'MsScriptElement',
}
: defaultScript
);
} }
} }
); );

View File

@ -23,7 +23,7 @@ export default function useCreateActions() {
* @param steps * @param steps
* @param parent * @param parent
*/ */
function checkedIfNeed( function selectedIfNeed(
selectedKeys: (string | number)[], selectedKeys: (string | number)[],
steps: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[], steps: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
parent?: TreeNode<ScenarioStepItem> parent?: TreeNode<ScenarioStepItem>
@ -59,7 +59,7 @@ export default function useCreateActions() {
step.id, step.id,
newStep, newStep,
createStepAction, createStepAction,
(newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent), (newNode, parent) => selectedIfNeed(selectedKeys, [newNode], parent),
'id' 'id'
); );
} }
@ -174,13 +174,13 @@ export default function useCreateActions() {
undefined, undefined,
'id' 'id'
); );
checkedIfNeed(selectedKeys, readyInsertSteps, step); selectedIfNeed(selectedKeys, readyInsertSteps, step);
} }
return { return {
handleCreateStep, handleCreateStep,
buildInsertStepInfos, buildInsertStepInfos,
handleCreateSteps, handleCreateSteps,
checkedIfNeed, selectedIfNeed,
}; };
} }

View File

@ -1,13 +1,11 @@
<template> <template>
<div class="flex items-center gap-[4px]"> <div
<a-popover v-if="
v-if=" [ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.API_SCENARIO].includes(props.data.stepType)
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.API_SCENARIO].includes(props.data.stepType) "
" class="flex items-center gap-[4px]"
position="bl" >
content-class="detail-popover" <a-popover position="bl" content-class="detail-popover" arrow-class="hidden">
arrow-class="hidden"
>
<MsIcon type="icon-icon-draft" class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" /> <MsIcon type="icon-icon-draft" class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" />
<template #content> <template #content>
<div class="flex flex-col gap-[16px]"> <div class="flex flex-col gap-[16px]">

View File

@ -178,7 +178,7 @@
/> />
</template> </template>
<template #extraEnd="step"> <template #extraEnd="step">
<a-popover <responsePopover
v-if=" v-if="
![ ![
ScenarioStepType.LOOP_CONTROLLER, ScenarioStepType.LOOP_CONTROLLER,
@ -189,32 +189,16 @@
(getExecuteStatus(step) === ScenarioExecuteStatus.SUCCESS || (getExecuteStatus(step) === ScenarioExecuteStatus.SUCCESS ||
getExecuteStatus(step) === ScenarioExecuteStatus.FAILED) getExecuteStatus(step) === ScenarioExecuteStatus.FAILED)
" "
position="br" :step="step"
content-class="scenario-step-response-popover" :step-responses="scenario.stepResponses"
@popup-visible-change="handleResponsePopoverVisibleChange($event, step)" @visible-change="handleResponsePopoverVisibleChange"
> />
<executeStatus :status="getExecuteStatus(step)" size="small" /> <executeStatus
<template #content> v-else-if="step.executeStatus"
<responseResult :status="getExecuteStatus(step)"
:active-tab="ResponseComposition.BODY" size="small"
:request-result="scenario.stepResponses?.[step.id]" class="ml-[4px]"
:console="scenario.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>
<executeStatus v-else-if="step.executeStatus" :status="getExecuteStatus(step)" size="small" />
</template> </template>
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty> <template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
<div <div
@ -422,6 +406,7 @@
handleTreeDragDrop, handleTreeDragDrop,
insertNodes, insertNodes,
mapTree, mapTree,
traverseTree,
TreeNode, TreeNode,
} from '@/utils'; } from '@/utils';
@ -437,7 +422,6 @@
} from '@/models/apiTest/scenario'; } from '@/models/apiTest/scenario';
import { EnvConfig } from '@/models/projectManagement/environmental'; import { EnvConfig } from '@/models/projectManagement/environmental';
import { import {
ResponseComposition,
ScenarioAddStepActionType, ScenarioAddStepActionType,
ScenarioExecuteStatus, ScenarioExecuteStatus,
ScenarioStepRefType, ScenarioStepRefType,
@ -445,6 +429,7 @@
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
import type { RequestParam } from '../common/customApiDrawer.vue'; import type { RequestParam } from '../common/customApiDrawer.vue';
import updateStepStatus from '../utils';
import useCreateActions from './createAction/useCreateActions'; import useCreateActions from './createAction/useCreateActions';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils'; import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils'; import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
@ -456,9 +441,7 @@
const customCaseDrawer = defineAsyncComponent(() => import('../common/customCaseDrawer.vue')); const customCaseDrawer = defineAsyncComponent(() => import('../common/customCaseDrawer.vue'));
const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue')); const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue'));
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue')); const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
const responseResult = defineAsyncComponent( const responsePopover = defineAsyncComponent(() => import('../common/responsePopover.vue'));
() => import('@/views/api-test/components/requestComposition/response/index.vue')
);
const props = defineProps<{ const props = defineProps<{
stepKeyword: string; stepKeyword: string;
@ -500,9 +483,10 @@
function getExecuteStatus(step: ScenarioStepItem) { function getExecuteStatus(step: ScenarioStepItem) {
if (scenario.value.stepResponses && scenario.value.stepResponses[step.id]) { if (scenario.value.stepResponses && scenario.value.stepResponses[step.id]) {
return scenario.value.stepResponses[step.id].isSuccessful //
? ScenarioExecuteStatus.SUCCESS return scenario.value.stepResponses[step.id].some((report) => !report.isSuccessful)
: ScenarioExecuteStatus.FAILED; ? ScenarioExecuteStatus.FAILED
: ScenarioExecuteStatus.SUCCESS;
} }
return step.executeStatus; return step.executeStatus;
} }
@ -552,7 +536,7 @@
/** /**
* 增加步骤时判断父节点是否选中如果选中则需要把新节点也选中 * 增加步骤时判断父节点是否选中如果选中则需要把新节点也选中
*/ */
function checkedIfNeed(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) { function selectedIfNeed(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
if (parent && selectedKeys.value.includes(parent.id)) { if (parent && selectedKeys.value.includes(parent.id)) {
// //
selectedKeys.value.push(step.id); selectedKeys.value.push(step.id);
@ -803,7 +787,7 @@
id, id,
}, },
'after', 'after',
checkedIfNeed, selectedIfNeed,
'id' 'id'
); );
scenario.value.unSaved = true; scenario.value.unSaved = true;
@ -971,6 +955,15 @@
customApiDrawerVisible.value = true; customApiDrawerVisible.value = true;
} else if (step.stepType === ScenarioStepType.API_CASE) { } else if (step.stepType === ScenarioStepType.API_CASE) {
activeStep.value = step; activeStep.value = step;
if (
_stepType.isCopyCase &&
((stepDetails.value[step.id] === undefined && step.copyFromStepId) ||
(stepDetails.value[step.id] === undefined && !step.isNew))
) {
// case
//
await getStepDetail(step);
}
customCaseDrawerVisible.value = true; customCaseDrawerVisible.value = true;
} else if (step.stepType === ScenarioStepType.SCRIPT) { } else if (step.stepType === ScenarioStepType.SCRIPT) {
activeStep.value = step; activeStep.value = step;
@ -998,6 +991,7 @@
scenario.value.stepResponses[report.stepId] = temporaryStepReportMap[key]; scenario.value.stepResponses[report.stepId] = temporaryStepReportMap[key];
}); });
temporaryStepReportMap = {}; temporaryStepReportMap = {};
updateStepStatus(steps.value, scenario.value.stepResponses);
} }
} }
); );
@ -1017,22 +1011,25 @@
if (step.reportId === data.reportId) { if (step.reportId === data.reportId) {
// tabtab // tabtab
data.taskResult.requestResults.forEach((result) => { data.taskResult.requestResults.forEach((result) => {
scenario.value.stepResponses[result.stepId] = { if (scenario.value.stepResponses[result.stepId] === undefined) {
scenario.value.stepResponses[result.stepId] = [];
}
scenario.value.stepResponses[result.stepId].push({
...result, ...result,
console: data.taskResult.console, console: data.taskResult.console,
}; });
}); });
} else { } else {
// tab // tab
data.taskResult.requestResults.forEach((result) => { data.taskResult.requestResults.forEach((result) => {
if (step.reportId) { if (step.reportId) {
if (temporaryStepReportMap[step.reportId] === undefined) { if (temporaryStepReportMap[step.reportId] === undefined) {
temporaryStepReportMap[step.reportId] = {}; temporaryStepReportMap[step.reportId] = [];
} }
temporaryStepReportMap[step.reportId] = { temporaryStepReportMap[step.reportId].push({
...result, ...result,
console: data.taskResult.console, console: data.taskResult.console,
}; });
} }
}); });
} }
@ -1041,6 +1038,7 @@
websocketMap[reportId].close(); websocketMap[reportId].close();
if (step.reportId === data.reportId) { if (step.reportId === data.reportId) {
step.isExecuting = false; step.isExecuting = false;
updateStepStatus([step], scenario.value.stepResponses);
} }
} }
}); });
@ -1056,6 +1054,7 @@
const [currentStep] = executeParams.steps; const [currentStep] = executeParams.steps;
try { try {
currentStep.isExecuting = true; currentStep.isExecuting = true;
currentStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
debugSocket(currentStep, executeParams.reportId, executeType); // websocket debugSocket(currentStep, executeParams.reportId, executeType); // websocket
const res = await debugScenario({ const res = await debugScenario({
id: scenario.value.id || '', id: scenario.value.id || '',
@ -1093,21 +1092,18 @@
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.id, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.id, 'id');
if (realStep) { if (realStep) {
realStep.reportId = getGenerateId(); realStep.reportId = getGenerateId();
if ( const _stepDetails = {};
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(realStep.stepType)
) {
//
realStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
}
const stepDetail = stepDetails.value[realStep.id];
delete scenario.value.stepResponses[realStep.id]; //
const stepFileParam = scenario.value.stepFileParam[realStep.id]; const stepFileParam = scenario.value.stepFileParam[realStep.id];
traverseTree(realStep, (step) => {
_stepDetails[step.id] = stepDetails.value[step.id];
step.executeStatus = ScenarioExecuteStatus.EXECUTING;
delete scenario.value.stepResponses[step.id]; //
return step;
});
realExecute( realExecute(
{ {
steps: [realStep as ScenarioStepItem], steps: [realStep as ScenarioStepItem],
stepDetails: { stepDetails: _stepDetails,
[realStep.id]: stepDetail,
},
reportId: realStep.reportId, reportId: realStep.reportId,
uploadFileIds: stepFileParam?.uploadFileIds || [], uploadFileIds: stepFileParam?.uploadFileIds || [],
linkFileIds: stepFileParam?.linkFileIds || [], linkFileIds: stepFileParam?.linkFileIds || [],
@ -1286,7 +1282,10 @@
{ {
stepType: ScenarioStepType.CUSTOM_REQUEST, stepType: ScenarioStepType.CUSTOM_REQUEST,
name: t('apiScenario.customApi'), name: t('apiScenario.customApi'),
method: request.method, config: {
protocol: request.protocol,
method: request.method,
},
id: request.stepId, id: request.stepId,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}, },
@ -1355,6 +1354,8 @@
if (activeStep.value && activeCreateAction.value) { if (activeStep.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
id,
refType: ScenarioStepRefType.DIRECT,
stepType: ScenarioStepType.SCRIPT, stepType: ScenarioStepType.SCRIPT,
name, name,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
@ -1532,27 +1533,6 @@
color: rgb(var(--primary-5)); color: rgb(var(--primary-5));
background-color: rgb(var(--primary-1)); 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>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -8,10 +8,11 @@ import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
*/ */
export default function updateStepStatus( export default function updateStepStatus(
steps: ScenarioStepItem[], steps: ScenarioStepItem[],
stepResponses: Record<string | number, RequestResult> stepResponses: Record<string | number, RequestResult[]>
) { ) {
for (let i = 0; i < steps.length; i++) { for (let i = 0; i < steps.length; i++) {
const node = steps[i]; const node = steps[i];
console.log('node', node.stepType, node.executeStatus);
if ( if (
[ [
ScenarioStepType.LOOP_CONTROLLER, ScenarioStepType.LOOP_CONTROLLER,
@ -54,10 +55,11 @@ export default function updateStepStatus(
node.executeStatus = ScenarioExecuteStatus.SUCCESS; node.executeStatus = ScenarioExecuteStatus.SUCCESS;
} else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) { } else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) {
// 非逻辑控制器直接更改本身状态 // 非逻辑控制器直接更改本身状态
if (stepResponses[node.id]) { if (stepResponses[node.id] && stepResponses[node.id].length > 0) {
node.executeStatus = stepResponses[node.id].isSuccessful console.log('stepResponses[node.id]', stepResponses[node.id]);
? ScenarioExecuteStatus.SUCCESS node.executeStatus = stepResponses[node.id].some((report) => !report.isSuccessful)
: ScenarioExecuteStatus.FAILED; ? ScenarioExecuteStatus.FAILED
: ScenarioExecuteStatus.SUCCESS;
} else { } else {
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE; node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
} }

View File

@ -161,13 +161,6 @@
function setStepExecuteStatus() { function setStepExecuteStatus() {
updateStepStatus(activeScenarioTab.value.steps, activeScenarioTab.value.stepResponses); updateStepStatus(activeScenarioTab.value.steps, activeScenarioTab.value.stepResponses);
// activeScenarioTab.value.steps = mapTree<ScenarioStepItem>(activeScenarioTab.value.steps, (step) => {
// if (step.executeStatus === ScenarioExecuteStatus.EXECUTING) {
// //
// step.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
// }
// return step;
// });
} }
/** /**
@ -185,10 +178,13 @@
if (activeScenarioTab.value.reportId === data.reportId) { if (activeScenarioTab.value.reportId === data.reportId) {
// tabtab // tabtab
data.taskResult.requestResults.forEach((result) => { data.taskResult.requestResults.forEach((result) => {
activeScenarioTab.value.stepResponses[result.stepId] = { if (activeScenarioTab.value.stepResponses[result.stepId] === undefined) {
activeScenarioTab.value.stepResponses[result.stepId] = [];
}
activeScenarioTab.value.stepResponses[result.stepId].push({
...result, ...result,
console: data.taskResult.console, console: data.taskResult.console,
}; });
if (result.isSuccessful) { if (result.isSuccessful) {
activeScenarioTab.value.executeSuccessCount += 1; activeScenarioTab.value.executeSuccessCount += 1;
} else { } else {
@ -202,10 +198,13 @@
if (temporaryScenarioReportMap[activeScenarioTab.value.reportId] === undefined) { if (temporaryScenarioReportMap[activeScenarioTab.value.reportId] === undefined) {
temporaryScenarioReportMap[activeScenarioTab.value.reportId] = {}; temporaryScenarioReportMap[activeScenarioTab.value.reportId] = {};
} }
temporaryScenarioReportMap[activeScenarioTab.value.reportId][result.stepId] = { if (temporaryScenarioReportMap[activeScenarioTab.value.reportId][result.stepId]) {
temporaryScenarioReportMap[activeScenarioTab.value.reportId][result.stepId] = [];
}
temporaryScenarioReportMap[activeScenarioTab.value.reportId][result.stepId].push({
...result, ...result,
console: data.taskResult.console, console: data.taskResult.console,
}; });
} }
}); });
} }

View File

@ -60,6 +60,10 @@ export default {
'api_scenario.table.tableNoDataAndPlease': '暂无数据,请', 'api_scenario.table.tableNoDataAndPlease': '暂无数据,请',
'api_scenario.table.or': '或', 'api_scenario.table.or': '或',
'apiScenario.execute': '执行', 'apiScenario.execute': '执行',
'apiScenario.prev': '上一次',
'apiScenario.next': '下一次',
'apiScenario.sumLoop': '共{count}次循环',
'apiScenario.times': '次',
// 批量操作文案 // 批量操作文案
'api_scenario.batch_operation.success': '成功{opt}至 {name}', 'api_scenario.batch_operation.success': '成功{opt}至 {name}',
'api_scenario.table.batchMoveConfirm': '{opt}{count}个场景至已选模块', 'api_scenario.table.batchMoveConfirm': '{opt}{count}个场景至已选模块',