feat(接口场景): api、case 响应循环分页&脚本响应&步骤列表循环控制器响应结果优化

This commit is contained in:
baiqi 2024-04-01 20:55:36 +08:00 committed by Craftsman
parent bb04b9cdc5
commit 987df3fa75
13 changed files with 164 additions and 130 deletions

View File

@ -194,9 +194,11 @@ export interface TreeNode<T> {
* @param tree
* @param customNodeFn
* @param customChildrenKey key
* @param continueCondition
*/
export function traverseTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
continueCondition?: (node: TreeNode<T>) => boolean,
customNodeFn: (node: TreeNode<T>) => TreeNode<T> | null = (node) => node,
customChildrenKey = 'children'
) {
@ -209,7 +211,11 @@ export function traverseTree<T>(
customNodeFn(node);
}
if (node[customChildrenKey] && Array.isArray(node[customChildrenKey]) && node[customChildrenKey].length > 0) {
traverseTree(node[customChildrenKey], customNodeFn, customChildrenKey);
if (typeof continueCondition === 'function' && !continueCondition(node)) {
// 如果有继续递归的条件,则判断是否继续递归
break;
}
traverseTree(node[customChildrenKey], continueCondition, customNodeFn, customChildrenKey);
}
}
}

View File

@ -57,6 +57,7 @@
</a-radio-group>
</div>
</slot>
<slot name="titleRight"></slot>
<div
v-if="props.requestResult?.responseResult?.responseCode"
class="flex items-center justify-between gap-[24px] text-[14px]"

View File

@ -272,15 +272,20 @@
:is-priority-local-exec="isPriorityLocalExec"
:request-url="requestVModel.url"
:is-expanded="isVerticalExpanded"
:request-result="requestResult"
:console="requestResult?.console"
:request-result="currentResponse"
:console="currentResponse?.console"
:is-edit="false"
is-definition
hide-layout-switch
:loading="requestVModel.executeLoading || loading"
@change-expand="changeVerticalExpand"
@change-layout="handleActiveLayoutChange"
@execute="execute"
/>
>
<template #titleRight>
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
</template>
</response>
</template>
</MsSplitBox>
</div>
@ -299,6 +304,7 @@
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue';
import loopPagination from './loopPagination.vue';
import stepTypeVue from './stepType/stepType.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
@ -475,6 +481,7 @@
};
const requestVModel = ref<RequestParam>(defaultApiParams);
//
const _stepType = computed(() => {
if (props.step) {
return getStepType(props.step);
@ -484,30 +491,29 @@
isQuoteApi: false,
};
});
//
const title = computed(() => {
if (_stepType.value.isCopyApi || _stepType.value.isQuoteApi) {
return props.step?.name;
}
return t('apiScenario.customApi');
});
//
const showEnvPrefix = computed(
() =>
requestVModel.value.customizeRequestEnvEnable &&
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 currentLoop = ref(1);
const currentResponse = computed(() => {
if (props.step?.id) {
return props.stepResponses?.[props.step?.id]?.[currentLoop.value - 1];
}
});
const requestResult = computed(() => {
const length = props.stepResponses?.[requestVModel.value.stepId]
? props.stepResponses?.[requestVModel.value.stepId]?.length
: 0;
//
return props.stepResponses?.[requestVModel.value.stepId]?.[length];
const loopTotal = computed(() => (props.step?.id && props.stepResponses?.[props.step?.id]?.length) || 0);
// body
const responseResultBody = computed(() => {
return currentResponse.value?.responseResult.body;
});
watch(

View File

@ -219,15 +219,20 @@
:is-priority-local-exec="isPriorityLocalExec"
:request-url="requestVModel.url"
:is-expanded="isVerticalExpanded"
:request-result="requestResult"
:console="requestResult?.console"
:request-result="currentResponse"
:console="currentResponse?.console"
:is-edit="false"
is-definition
hide-layout-switch
:loading="requestVModel.executeLoading || loading"
@change-expand="changeVerticalExpand"
@change-layout="handleActiveLayoutChange"
@execute="execute"
/>
>
<template #titleRight>
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" class="!mb-0" />
</template>
</response>
</template>
</MsSplitBox>
</div>
@ -246,6 +251,7 @@
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue';
import loopPagination from './loopPagination.vue';
import stepType from './stepType/stepType.vue';
import auth from '@/views/api-test/components/requestComposition/auth.vue';
import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue';
@ -396,19 +402,16 @@
() =>
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 currentLoop = ref(1);
const currentResponse = computed(() => {
if (activeStep.value?.id) {
return props.stepResponses?.[activeStep.value?.id]?.[currentLoop.value - 1];
}
});
const requestResult = computed(() => {
const length = props.stepResponses?.[requestVModel.value.stepId]
? props.stepResponses?.[requestVModel.value.stepId]?.length
: 0;
//
return props.stepResponses?.[requestVModel.value.stepId]?.[length];
const loopTotal = computed(() => (activeStep.value?.id && props.stepResponses?.[activeStep.value?.id]?.length) || 0);
// body
const responseResultBody = computed(() => {
return currentResponse.value?.responseResult.body;
});
watch(

View File

@ -1,5 +1,10 @@
<template>
<MsTag v-if="status" :self-style="status.style" :size="props.size"> {{ status.text }}</MsTag>
<MsTag v-if="status" :self-style="status.style" :size="props.size">
<div class="flex items-center justify-between gap-[4px]">
<span>{{ status.text }}</span>
<span v-if="props.extraText">{{ props.extraText }}</span>
</div>
</MsTag>
</template>
<script setup lang="ts">
@ -12,6 +17,7 @@
const props = defineProps<{
status?: ScenarioExecuteStatus;
size?: Size;
extraText?: string;
}>();
const { t } = useI18n();

View File

@ -39,7 +39,8 @@
</script>
<style lang="less">
.arco-popover {
.arco-popover,
.arco-drawer {
.loop-pagination {
@apply justify-start;

View File

@ -4,7 +4,7 @@
content-class="scenario-step-response-popover"
@popup-visible-change="emit('visibleChange', $event, props.step)"
>
<executeStatus :status="lastExecuteStatus" size="small" class="ml-[4px]" />
<executeStatus :status="finalExecuteStatus" size="small" class="ml-[4px]" />
<template #content>
<div class="flex h-full flex-col">
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
@ -57,7 +57,7 @@
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(() => {
const finalExecuteStatus = computed(() => {
if (props.stepResponses[props.step.id] && props.stepResponses[props.step.id].length > 0) {
//
return props.stepResponses[props.step.id].some((report) => !report.isSuccessful)

View File

@ -21,9 +21,16 @@
size="small"
/>
</div>
<div class="mt-[10px] flex h-[calc(100%-40px)] gap-[8px]">
<div class="mt-[10px] flex flex-1 gap-[8px]">
<conditionContent v-if="visible" v-model:data="activeItem" :is-build-in="true" :is-format="true" />
</div>
<div v-if="currentResponse?.console" class="p-[8px]">
<div class="mb-[8px] font-medium text-[var(--color-text-1)]">{{ t('apiScenario.executionResult') }}</div>
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
<div class="h-[300px] bg-[var(--color-text-n9)] p-[12px]">
<pre class="response-header-pre">{{ currentResponse?.console }}</pre>
</div>
</div>
<template v-if="!props.detail" #footer>
<a-button type="secondary" @click="handleDrawerCancel">
{{ t('common.cancel') }}
@ -43,11 +50,12 @@
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import loopPagination from './loopPagination.vue';
// import stepTypeVue from './stepType/stepType.vue';
import { useI18n } from '@/hooks/useI18n';
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import { ExecuteConditionProcessor, RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { RequestConditionProcessor } from '@/enums/apiEnum';
@ -57,6 +65,7 @@
detail?: ExecuteConditionProcessor;
step?: ScenarioStepItem;
name?: string;
stepResponses?: Record<string | number, RequestResult[]>;
}>();
const emit = defineEmits<{
(e: 'add', name: string, scriptProcessor: ExecuteConditionProcessor): void;
@ -77,6 +86,13 @@
const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true });
const currentLoop = ref(1);
const currentResponse = computed(() => {
if (props.step?.id) {
return props.stepResponses?.[props.step?.id]?.[currentLoop.value - 1];
}
});
const loopTotal = computed(() => (props.step?.id && props.stepResponses?.[props.step?.id]?.length) || 0);
watch(
() => visible.value,
@ -118,4 +134,12 @@
}
</script>
<style lang="less" scoped></style>
<style lang="less" scoped>
.response-header-pre {
@apply h-full overflow-auto bg-white;
.ms-scroll-bar();
padding: 8px 12px;
border-radius: var(--border-radius-small);
}
</style>

View File

@ -100,106 +100,62 @@
function handleCreateActionSelect(val: ScenarioAddStepActionType) {
switch (val) {
case ScenarioAddStepActionType.LOOP_CONTROL:
if (step.value && props.createStepAction) {
handleCreateStep(
{
stepType: ScenarioStepType.LOOP_CONTROLLER,
name: t('apiScenario.loopControl'),
projectId: appStore.currentProjectId,
},
step.value,
steps.value,
props.createStepAction,
selectedKeys.value
);
} else {
steps.value.push(
buildInsertStepInfos(
const defaultLoopStep = buildInsertStepInfos(
[cloneDeep(defaultStepItemCommon)],
ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepRefType.DIRECT,
steps.value.length + 1,
appStore.currentProjectId
)[0]
);
)[0];
if (step.value && props.createStepAction) {
handleCreateStep(defaultLoopStep, step.value, steps.value, props.createStepAction, selectedKeys.value);
} else {
steps.value.push(defaultLoopStep);
}
emit('addDone');
break;
case ScenarioAddStepActionType.CONDITION_CONTROL:
if (step.value && props.createStepAction) {
handleCreateStep(
{
stepType: ScenarioStepType.IF_CONTROLLER,
name: t('apiScenario.conditionControl'),
projectId: appStore.currentProjectId,
},
step.value,
steps.value,
props.createStepAction,
selectedKeys.value
);
} else {
steps.value.push(
buildInsertStepInfos(
const defaultConditionStep = buildInsertStepInfos(
[cloneDeep(defaultStepItemCommon)],
ScenarioStepType.IF_CONTROLLER,
ScenarioStepRefType.DIRECT,
steps.value.length + 1,
appStore.currentProjectId
)[0]
);
)[0];
if (step.value && props.createStepAction) {
handleCreateStep(defaultConditionStep, step.value, steps.value, props.createStepAction, selectedKeys.value);
} else {
steps.value.push(defaultConditionStep);
}
emit('addDone');
break;
case ScenarioAddStepActionType.ONLY_ONCE_CONTROL:
if (step.value && props.createStepAction) {
handleCreateStep(
{
stepType: ScenarioStepType.ONCE_ONLY_CONTROLLER,
name: t('apiScenario.onlyOnceControl'),
projectId: appStore.currentProjectId,
},
step.value,
steps.value,
props.createStepAction,
selectedKeys.value
);
} else {
steps.value.push(
buildInsertStepInfos(
const defaultOnlyOnceStep = buildInsertStepInfos(
[cloneDeep(defaultStepItemCommon)],
ScenarioStepType.ONCE_ONLY_CONTROLLER,
ScenarioStepRefType.DIRECT,
steps.value.length + 1,
appStore.currentProjectId
)[0]
);
)[0];
if (step.value && props.createStepAction) {
handleCreateStep(defaultOnlyOnceStep, step.value, steps.value, props.createStepAction, selectedKeys.value);
} else {
steps.value.push(defaultOnlyOnceStep);
}
emit('addDone');
break;
case ScenarioAddStepActionType.WAIT_TIME:
if (step.value && props.createStepAction) {
handleCreateStep(
{
stepType: ScenarioStepType.CONSTANT_TIMER,
name: t('apiScenario.waitTime'),
projectId: appStore.currentProjectId,
},
step.value,
steps.value,
props.createStepAction,
selectedKeys.value
);
} else {
steps.value.push(
buildInsertStepInfos(
const defaultWaitTimeStep = buildInsertStepInfos(
[cloneDeep(defaultStepItemCommon)],
ScenarioStepType.CONSTANT_TIMER,
ScenarioStepRefType.DIRECT,
steps.value.length + 1,
appStore.currentProjectId
)[0]
);
)[0];
if (step.value && props.createStepAction) {
handleCreateStep(defaultWaitTimeStep, step.value, steps.value, props.createStepAction, selectedKeys.value);
} else {
steps.value.push(defaultWaitTimeStep);
}
emit('addDone');
break;

View File

@ -227,6 +227,7 @@
watchEffect(() => {
innerData.value = props.data;
console.log('watchEffect', props.data);
});
//

View File

@ -196,6 +196,7 @@
<executeStatus
v-else-if="step.executeStatus"
:status="getExecuteStatus(step)"
:extra-text="getExecuteStatusExtraText(step)"
size="small"
class="ml-[4px]"
/>
@ -255,6 +256,7 @@
:detail="currentStepDetail as unknown as ExecuteConditionProcessor"
:step="activeStep"
:name="activeStep?.name"
:step-responses="scenario.stepResponses"
@add="addScriptStep"
@save="saveScriptStep"
/>
@ -424,6 +426,7 @@
import {
ScenarioAddStepActionType,
ScenarioExecuteStatus,
ScenarioStepLoopTypeEnum,
ScenarioStepRefType,
ScenarioStepType,
} from '@/enums/apiEnum';
@ -481,6 +484,10 @@
focusStepKey.value = id || '';
}
function checkStepIsApi(step: ScenarioStepItem) {
return [ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(step.stepType);
}
function getExecuteStatus(step: ScenarioStepItem) {
if (scenario.value.stepResponses && scenario.value.stepResponses[step.id]) {
//
@ -491,6 +498,24 @@
return step.executeStatus;
}
function getExecuteStatusExtraText(step: ScenarioStepItem) {
if (
step.stepType === ScenarioStepType.LOOP_CONTROLLER &&
step.config.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT &&
step.config.msCountController &&
step.config.msCountController.loops > 0
) {
// /
const firstHasResultChild = step.children?.find((child) => {
return checkStepIsApi(child) || child.stepType === ScenarioStepType.SCRIPT;
});
return firstHasResultChild && scenario.value.stepResponses[firstHasResultChild.id]
? `${scenario.value.stepResponses[firstHasResultChild.id].length}/${step.config.msCountController.loops}`
: undefined;
}
return undefined;
}
function handleResponsePopoverVisibleChange(visible: boolean, step: ScenarioStepItem) {
if (visible) {
setFocusNodeKey(step.id);
@ -520,10 +545,6 @@
}
}
function checkStepIsApi(step: ScenarioStepItem) {
return [ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(step.stepType);
}
function checkStepShowMethod(step: ScenarioStepItem) {
return [
ScenarioStepType.API,
@ -1094,12 +1115,22 @@
realStep.reportId = getGenerateId();
const _stepDetails = {};
const stepFileParam = scenario.value.stepFileParam[realStep.id];
traverseTree(realStep, (step) => {
traverseTree(
realStep,
(step) => {
//
return step.enable;
},
(step) => {
if (step.enable) {
//
_stepDetails[step.id] = stepDetails.value[step.id];
step.executeStatus = ScenarioExecuteStatus.EXECUTING;
}
delete scenario.value.stepResponses[step.id]; //
return step;
});
}
);
realExecute(
{
steps: [realStep as ScenarioStepItem],

View File

@ -12,7 +12,6 @@ export default function updateStepStatus(
) {
for (let i = 0; i < steps.length; i++) {
const node = steps[i];
console.log('node', node.stepType, node.executeStatus);
if (
[
ScenarioStepType.LOOP_CONTROLLER,
@ -56,7 +55,6 @@ export default function updateStepStatus(
} else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) {
// 非逻辑控制器直接更改本身状态
if (stepResponses[node.id] && stepResponses[node.id].length > 0) {
console.log('stepResponses[node.id]', stepResponses[node.id]);
node.executeStatus = stepResponses[node.id].some((report) => !report.isSuccessful)
? ScenarioExecuteStatus.FAILED
: ScenarioExecuteStatus.SUCCESS;

View File

@ -64,6 +64,7 @@ export default {
'apiScenario.next': '下一次',
'apiScenario.sumLoop': '共{count}次循环',
'apiScenario.times': '次',
'apiScenario.executionResult': '执行结果',
// 批量操作文案
'api_scenario.batch_operation.success': '成功{opt}至 {name}',
'api_scenario.table.batchMoveConfirm': '{opt}{count}个场景至已选模块',