feat(接口场景): 场景配置&场景拖拽&场景启用禁用

This commit is contained in:
baiqi 2024-03-27 22:44:33 +08:00 committed by Craftsman
parent 008f930a98
commit ce9811329d
15 changed files with 1052 additions and 133 deletions

View File

@ -468,7 +468,6 @@
}
}
}
.arco-radio-checked:not(.arco-radio-disabled) {
.arco-radio-icon {
@apply !bg-white;
@ -596,7 +595,6 @@
.ms-container {
height: calc(100vh - 84px);
}
.ms-form {
.arco-form-item {
margin-bottom: 16px;
@ -616,18 +614,19 @@
height: 14px;
}
}
margin-right: 24px;
}
.arco-radio-group-button {
padding: 1px;
background-color: var(--color-text-n8);
.arco-radio-button {
@apply bg-transparent;
@apply bg-transparent;
margin: 1px;
}
.arco-radio-checked {
@apply bg-white;
@apply bg-white;
color: rgb(var(--primary-5));
}
@ -643,7 +642,8 @@
}
.arco-radio-checked:not(.arco-radio-disabled) {
.arco-radio-icon {
@apply !bg-white;
@apply !bg-white;
width: 16px;
height: 16px;
line-height: 16px;
@ -658,11 +658,8 @@
}
}
}
}
/** 气泡弹窗 **/
.arco-popover-title {
font-size: 14px;

View File

@ -312,6 +312,12 @@ export default function useTableProps<T>(
}
};
// 重置筛选
const clearSelector = () => {
propsRes.value.selectorStatus = SelectAllEnum.NONE; // 重置选择器状态
resetSelector();
};
// 获取当前表格的选中项数量
const getSelectedCount = () => {
const { selectorStatus, msPagination, excludeKeys, selectedKeys } = propsRes.value;
@ -472,6 +478,7 @@ export default function useTableProps<T>(
setAdvanceFilter,
resetPagination,
getSelectedCount,
clearSelector,
resetSelector,
getTableQueryParams,
setTableSelected,

View File

@ -255,7 +255,7 @@ export enum ScenarioStepType {
LOOP_CONTROLLER = 'LOOP_CONTROLLER', // 循环控制器
API = 'API', // 接口定义
CUSTOM_REQUEST = 'CUSTOM_REQUEST', // 自定义请求
API_SCENARIO = ' API_SCENARIO', // 场景
API_SCENARIO = 'API_SCENARIO', // 场景
IF_CONTROLLER = 'IF_CONTROLLER', // 条件控制器
ONCE_ONLY_CONTROLLER = 'ONCE_ONLY_CONTROLLER', // 一次控制器
CONSTANT_TIMER = 'CONSTANT_TIMER', // 等待控制器

View File

@ -298,8 +298,20 @@ export interface LoopStepDetail extends StepDetailsCommon {
msCountController: CountController;
whileController: WhileController;
}
export interface ScenarioStepConfig {
useCurrentScenarioParam: boolean; // 是否优先使用当前场景参数
useBothScenarioParam: boolean; // 是否当前场景参数和源场景参数都应用(勾选非空值时为 true
enableScenarioEnv: boolean; // 是否应用源场景环境
}
export type ScenarioStepDetail = Partial<
CustomApiStepDetail & ConditionStepDetail & LoopStepDetail & { protocol: string; method: RequestMethods }
CustomApiStepDetail &
ConditionStepDetail &
LoopStepDetail &
ScenarioStepConfig & {
protocol: string;
method: RequestMethods;
isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用
}
>;
export interface ScenarioStepItem {
id: string | number;

View File

@ -260,7 +260,6 @@
:is-expanded="isVerticalExpanded"
:request-result="props.stepResponses?.[requestVModel.stepId]"
:console="props.stepResponses?.[requestVModel.stepId]?.console"
:show-empty="false"
:is-edit="false"
is-definition
:loading="requestVModel.executeLoading || loading"
@ -362,6 +361,7 @@
export type RequestParam = ExecuteApiRequestFullParams & {
response?: RequestTaskResult;
customizeRequest?: boolean;
customizeRequestEnvEnable?: boolean;
} & RequestCustomAttr;
@ -395,11 +395,12 @@
const visible = defineModel<boolean>('visible', { required: true });
const loading = defineModel<boolean>('detailLoading', { default: false });
const defaultDebugParams: RequestParam = {
const defaultApiParams: RequestParam = {
name: '',
type: 'api',
stepId: '',
resourceId: '',
customizeRequest: true,
customizeRequestEnvEnable: false,
protocol: 'HTTP',
url: '',
@ -455,7 +456,7 @@
executeLoading: false,
};
const requestVModel = ref<RequestParam>(defaultDebugParams);
const requestVModel = ref<RequestParam>(defaultApiParams);
const _stepType = computed(() => {
if (props.step) {
return getStepType(props.step);
@ -832,7 +833,9 @@
if (val) {
verticalSplitBoxRef.value?.expand(0.6);
} else {
verticalSplitBoxRef.value?.collapse(1);
verticalSplitBoxRef.value?.collapse(
splitContainerRef.value ? `${splitContainerRef.value.clientHeight - 42}px` : 0
);
}
}
@ -843,7 +846,8 @@
if (val) {
changeVerticalExpand(true);
} else {
changeVerticalExpand(false);
isVerticalExpanded.value = false;
verticalSplitBoxRef.value?.collapse(1);
}
});
}
@ -931,6 +935,7 @@
protocol: requestVModel.value.protocol,
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
name: requestVModel.value.name,
customizeRequest: props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.request,
customizeRequestEnvEnable: requestVModel.value.customizeRequestEnvEnable,
children: [
{
@ -1066,7 +1071,7 @@
if (props.request) {
// api api
requestVModel.value = cloneDeep({
...defaultDebugParams,
...defaultApiParams,
...props.request,
isNew: false,
});
@ -1082,7 +1087,7 @@
} else {
//
requestVModel.value = cloneDeep({
...defaultDebugParams,
...defaultApiParams,
stepId: getGenerateId(),
});
}

View File

@ -40,73 +40,303 @@
</div>
</div>
</template>
<div class="flex items-center p-[16px]">
<a-input
v-model:model-value="requestVModel.name"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
allow-clear
:max-length="255"
:show-word-limit="!isQuote"
:disabled="isQuote"
/>
<executeButton
ref="executeRef"
class="ml-[16px]"
:execute-loading="requestVModel.executeLoading"
@execute="handleExecute"
@stop-debug="stopDebug"
/>
<a-empty
v-if="pluginError && !isHttpProtocol"
:description="t('apiTestDebug.noPlugin')"
class="h-[200px] items-center justify-center"
>
<template #image>
<MsIcon type="icon-icon_plugin_outlined" size="48" />
</template>
</a-empty>
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
<div class="px-[18px] pt-[8px]">
<div class="flex flex-wrap items-center justify-between gap-[12px]">
<div class="flex flex-1 items-center gap-[16px]">
<a-select
v-if="requestVModel.isNew"
v-model:model-value="requestVModel.protocol"
:options="protocolOptions"
:loading="protocolLoading"
:disabled="_stepType.isQuoteCase"
class="w-[90px]"
@change="(val) => handleActiveDebugProtocolChange(val as string)"
/>
<div v-else class="flex items-center gap-[4px]">
<apiMethodName
:method="(requestVModel.protocol as RequestMethods)"
tag-background-color="rgb(var(--link-7))"
tag-text-color="white"
is-tag
class="flex items-center"
/>
<a-tooltip v-if="!isHttpProtocol" :content="requestVModel.name" :mouse-enter-delay="500">
<div class="one-line-text max-w-[350px]"> {{ requestVModel.name }}</div>
</a-tooltip>
</div>
<a-input-group v-if="isHttpProtocol" class="flex-1">
<apiMethodSelect
v-model:model-value="requestVModel.method"
class="w-[140px]"
:disabled="_stepType.isQuoteCase"
@change="handleActiveDebugChange"
/>
<a-input
v-model:model-value="requestVModel.url"
:max-length="255"
:placeholder="t('apiTestDebug.urlPlaceholder')"
allow-clear
class="hover:z-10"
:style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''"
:disabled="_stepType.isQuoteCase"
@input="() => (isUrlError = false)"
@change="handleUrlChange"
/>
</a-input-group>
</div>
<div>
<a-dropdown-button
v-if="hasLocalExec"
:disabled="requestVModel.executeLoading || (isHttpProtocol && !requestVModel.url)"
class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@select="execute"
>
{{ isPriorityLocalExec ? t('apiTestDebug.localExec') : t('apiTestDebug.serverExec') }}
<template #icon>
<icon-down />
</template>
<template #content>
<a-doption :value="isPriorityLocalExec ? 'serverExec' : 'localExec'">
{{ isPriorityLocalExec ? t('apiTestDebug.serverExec') : t('apiTestDebug.localExec') }}
</a-doption>
</template>
</a-dropdown-button>
<a-button v-else-if="!requestVModel.executeLoading" type="primary" @click="() => execute('serverExec')">
{{ t('apiTestDebug.serverExec') }}
</a-button>
<a-button v-else type="primary" class="mr-[12px]" @click="stopDebug">{{ t('common.stop') }}</a-button>
</div>
</div>
<a-input
v-if="activeStep?.stepType && (_stepType.isCopyCase || _stepType.isQuoteCase) && isHttpProtocol"
v-model:model-value="requestVModel.name"
:max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
:disabled="!isEditableApi"
allow-clear
class="mt-[8px]"
/>
</div>
<div class="px-[16px]">
<MsTab
v-model:active-key="requestVModel.activeTab"
:content-tab-list="contentTabList"
:get-text-func="getTabBadge"
class="no-content relative mt-[8px] border-b"
/>
</div>
<div ref="splitContainerRef" class="h-[calc(100%-87px)]">
<MsSplitBox
ref="verticalSplitBoxRef"
v-model:size="splitBoxSize"
:max="!showResponse ? 1 : 0.98"
min="10px"
:direction="activeLayout"
second-container-class="!overflow-y-hidden"
:class="!showResponse ? 'hidden-second' : 'show-second'"
@expand-change="handleVerticalExpandChange"
>
<template #first>
<a-spin class="block h-full w-full" :loading="requestVModel.executeLoading || loading">
<div
:class="`flex h-full min-w-[800px] flex-col p-[16px] ${
activeLayout === 'horizontal' ? ' pr-[16px]' : ''
}`"
>
<div class="tab-pane-container">
<a-spin
v-show="requestVModel.activeTab === RequestComposition.PLUGIN"
:loading="pluginLoading"
class="min-h-[100px] w-full"
>
<MsFormCreate
v-model:api="fApi"
:rule="currentPluginScript"
:option="currentPluginOptions"
@change="
() => {
if (isInitPluginForm) {
handlePluginFormChange();
}
}
"
/>
</a-spin>
<httpHeader
v-if="requestVModel.activeTab === RequestComposition.HEADER"
v-model:params="requestVModel.headers"
:disabled-except-param="!isEditableApi"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<httpBody
v-else-if="requestVModel.activeTab === RequestComposition.BODY"
v-model:params="requestVModel.body"
:layout="activeLayout"
:disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight"
:upload-temp-file-api="uploadTempFileCase"
:file-save-as-source-id="scenarioId"
:file-save-as-api="transferFileCase"
:file-module-options-api="getTransferOptionsCase"
@change="handleActiveDebugChange"
/>
<httpQuery
v-else-if="requestVModel.activeTab === RequestComposition.QUERY"
v-model:params="requestVModel.query"
:layout="activeLayout"
:disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<httpRest
v-else-if="requestVModel.activeTab === RequestComposition.REST"
v-model:params="requestVModel.rest"
:layout="activeLayout"
:disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<precondition
v-else-if="requestVModel.activeTab === RequestComposition.PRECONDITION"
v-model:config="requestVModel.children[0].preProcessorConfig"
is-definition
:disabled="!isEditableApi"
@change="handleActiveDebugChange"
/>
<postcondition
v-else-if="requestVModel.activeTab === RequestComposition.POST_CONDITION"
v-model:config="requestVModel.children[0].postProcessorConfig"
:response="props.stepResponses?.[requestVModel.stepId].responseResult.body"
:layout="activeLayout"
:disabled="!isEditableApi"
:second-box-height="secondBoxHeight"
is-definition
@change="handleActiveDebugChange"
/>
<assertion
v-else-if="requestVModel.activeTab === RequestComposition.ASSERTION"
v-model:params="requestVModel.children[0].assertionConfig.assertions"
:response="props.stepResponses?.[requestVModel.stepId].responseResult.body"
is-definition
:disabled="!isEditableApi"
:assertion-config="requestVModel.children[0].assertionConfig"
/>
<auth
v-else-if="requestVModel.activeTab === RequestComposition.AUTH"
v-model:params="requestVModel.authConfig"
:disabled="!isEditableApi"
@change="handleActiveDebugChange"
/>
<setting
v-else-if="requestVModel.activeTab === RequestComposition.SETTING"
v-model:params="requestVModel.otherConfig"
:disabled="!isEditableApi"
@change="handleActiveDebugChange"
/>
</div>
</div>
</a-spin>
</template>
<template #second>
<response
v-if="visible"
v-show="showResponse"
v-model:active-layout="activeLayout"
v-model:active-tab="requestVModel.responseActiveTab"
:is-http-protocol="isHttpProtocol"
:is-priority-local-exec="isPriorityLocalExec"
:request-url="requestVModel.url"
:is-expanded="isVerticalExpanded"
:request-result="props.stepResponses?.[requestVModel.stepId]"
:console="props.stepResponses?.[requestVModel.stepId]?.console"
:is-edit="false"
is-definition
:loading="requestVModel.executeLoading || loading"
@change-expand="changeVerticalExpand"
@change-layout="handleActiveLayoutChange"
@execute="execute"
/>
</template>
</MsSplitBox>
</div>
</div>
<requestAndResponse
ref="requestAndResponseRef"
:detail-loading="loading"
:disabled-except-param="isQuote"
:disabled-param-value="isQuote"
:request="requestVModel"
:is-priority-local-exec="isPriorityLocalExec"
:file-save-as-source-id="requestVModel.resourceId"
:file-module-options-api="getTransferOptionsCase"
:file-save-as-api="transferFileCase"
:upload-temp-file="uploadTempFileCase"
is-show-common-content-tab-key
@execute="handleExecute"
/>
</MsDrawer>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { InputInstance } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import { InputInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep, debounce } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
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 stepType from './stepType/stepType.vue';
import executeButton from '@/views/api-test/components/executeButton.vue';
import requestAndResponse from '@/views/api-test/components/requestAndResponse.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import auth from '@/views/api-test/components/requestComposition/auth.vue';
import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue';
import precondition from '@/views/api-test/components/requestComposition/precondition.vue';
import response from '@/views/api-test/components/requestComposition/response/index.vue';
import setting from '@/views/api-test/components/requestComposition/setting.vue';
import { RequestParam } from '@/views/api-test/scenario/components/common/customApiDrawer.vue';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
import {
getCaseDetail,
getTransferOptionsCase,
transferFileCase,
uploadTempFileCase,
} from '@/api/modules/api-test/management';
import { characterLimit } from '@/utils';
import { useAppStore } from '@/store';
import { characterLimit, parseQueryParams } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import { RequestResult } from '@/models/apiTest/common';
import { ExecuteConditionConfig, PluginConfig, RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import {
RequestAuthType,
RequestBodyFormat,
RequestComposition,
RequestConditionProcessor,
RequestMethods,
ResponseComposition,
ScenarioStepRefType,
ScenarioStepType,
} from '@/enums/apiEnum';
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
import getStepType from './stepType/utils';
import {
defaultBodyParams,
defaultBodyParamsItem,
defaultHeaderParamsItem,
defaultKeyValueParamItem,
defaultRequestParamsItem,
defaultResponse,
} from '@/views/api-test/components/config';
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
import { Api } from '@form-create/arco-design';
// Http
const httpHeader = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/header.vue'));
const httpBody = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/body.vue'));
const httpQuery = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/query.vue'));
const httpRest = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/rest.vue'));
const props = defineProps<{
request?: RequestParam; //
@ -119,34 +349,34 @@
(e: 'stopDebug'): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true });
const activeStep = defineModel<ScenarioStepItem>('activeStep', {
required: false,
});
//
const scenarioId = inject<string | number>('scenarioId');
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
const defaultCaseParams: RequestParam = {
id: `case-${Date.now()}`,
const defaultApiParams: RequestParam = {
name: '',
type: 'api',
stepId: '',
resourceId: '',
type: 'case',
moduleId: 'root',
customizeRequestEnvEnable: false,
protocol: 'HTTP',
tags: [],
description: '',
priority: 'P0',
url: '',
activeTab: RequestComposition.HEADER,
closable: true,
method: RequestMethods.GET,
unSaved: false,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
@ -184,17 +414,21 @@
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
responseActiveTab: ResponseComposition.BODY,
isNew: true,
unSaved: false,
executeLoading: false,
preDependency: [], //
postDependency: [], //
};
const requestVModel = ref<RequestParam>(props.request || cloneDeep(defaultCaseParams));
const requestVModel = ref<RequestParam>(defaultApiParams);
const _stepType = computed(() => {
if (activeStep.value) {
return getStepType(activeStep.value);
}
return {
isCopyCase: false,
isQuoteCase: false,
};
});
const isCopyCase = computed(
() =>
activeStep.value?.stepType === ScenarioStepType.API_CASE && activeStep.value?.refType === ScenarioStepRefType.COPY
@ -204,12 +438,7 @@
() =>
activeStep.value?.stepType === ScenarioStepType.API_CASE && activeStep.value?.refType === ScenarioStepRefType.REF
);
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const stepName = ref(activeStep.value?.name);
watchEffect(() => {
stepName.value = activeStep.value?.name;
});
watch(
() => props.stepResponses,
(val) => {
@ -222,9 +451,18 @@
}
);
const executeRef = ref<InstanceType<typeof executeButton>>();
const requestAndResponseRef = ref<InstanceType<typeof requestAndResponse>>();
const isPriorityLocalExec = computed(() => executeRef.value?.isPriorityLocalExec ?? false);
// api props.request
const isEditableApi = computed(() => _stepType.value.isCopyCase);
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const isInitPluginForm = ref(false);
const loading = ref(false);
function handleActiveDebugChange() {
if (!loading.value || (!isHttpProtocol.value && isInitPluginForm.value)) {
// change
requestVModel.value.unSaved = true;
}
}
const isShowEditStepNameInput = ref(false);
const stepNameInputRef = ref<InputInstance>();
@ -238,26 +476,468 @@
isShowEditStepNameInput.value = false;
}
// tabKey
const commonContentTabKey = [
RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION,
RequestComposition.ASSERTION,
];
// tab
const pluginContentTab = [
{
value: RequestComposition.PLUGIN,
label: t('apiTestDebug.pluginData'),
},
];
// Http tab
const httpContentTabList = [
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
},
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
},
{
value: RequestComposition.QUERY,
label: RequestComposition.QUERY,
},
{
value: RequestComposition.REST,
label: RequestComposition.REST,
},
{
value: RequestComposition.PRECONDITION,
label: t('apiTestDebug.prefix'),
},
{
value: RequestComposition.POST_CONDITION,
label: t('apiTestDebug.post'),
},
{
value: RequestComposition.ASSERTION,
label: t('apiTestDebug.assertion'),
},
{
value: RequestComposition.AUTH,
label: t('apiTestDebug.auth'),
},
{
value: RequestComposition.SETTING,
label: t('apiTestDebug.setting'),
},
];
const headerNum = computed(
() => filterKeyValParams(requestVModel.value?.headers ?? [], defaultHeaderParamsItem).validParams?.length
);
const restNum = computed(
() => filterKeyValParams(requestVModel.value?.rest ?? [], defaultRequestParamsItem).validParams?.length
);
const queryNum = computed(
() => filterKeyValParams(requestVModel.value?.query ?? [], defaultRequestParamsItem).validParams?.length
);
const preProcessorNum = computed(() => requestVModel.value.children[0].preProcessorConfig.processors.length);
const postProcessorNum = computed(() => requestVModel.value.children[0].postProcessorConfig.processors.length);
const assertionsNum = computed(() => requestVModel.value.children[0].assertionConfig.assertions.length);
// tab
const contentTabList = computed(() => {
// HTTP tabs
if (isHttpProtocol.value) {
// APIqueryresttab
if (!isEditableApi.value) {
return httpContentTabList.filter(
(item) =>
!(!restNum.value && item.value === RequestComposition.REST) &&
!(!queryNum.value && item.value === RequestComposition.QUERY) &&
!(!headerNum.value && item.value === RequestComposition.HEADER) &&
!(!preProcessorNum.value && item.value === RequestComposition.PRECONDITION) &&
!(!postProcessorNum.value && item.value === RequestComposition.POST_CONDITION) &&
!(!assertionsNum.value && item.value === RequestComposition.ASSERTION)
);
}
return httpContentTabList;
}
if (!isEditableApi.value) {
return [
...pluginContentTab,
...httpContentTabList
.filter((e) => commonContentTabKey.includes(e.value))
.filter(
(item) =>
!(!preProcessorNum.value && item.value === RequestComposition.PRECONDITION) &&
!(!postProcessorNum.value && item.value === RequestComposition.POST_CONDITION) &&
!(!assertionsNum.value && item.value === RequestComposition.ASSERTION)
),
];
}
return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
});
/**
* 获取 tab 的参数数量徽标
*/
function getTabBadge(tabKey: RequestComposition) {
switch (tabKey) {
case RequestComposition.HEADER:
return `${headerNum.value > 0 ? headerNum.value : ''}`;
case RequestComposition.BODY:
return requestVModel.value.body?.bodyType !== RequestBodyFormat.NONE ? '1' : '';
case RequestComposition.QUERY:
return `${queryNum.value > 0 ? queryNum.value : ''}`;
case RequestComposition.REST:
return `${restNum.value > 0 ? restNum.value : ''}`;
case RequestComposition.PRECONDITION:
return `${preProcessorNum.value > 99 ? '99+' : preProcessorNum.value || ''}`;
case RequestComposition.POST_CONDITION:
return `${postProcessorNum.value > 99 ? '99+' : postProcessorNum.value || ''}`;
case RequestComposition.ASSERTION:
return `${assertionsNum.value > 99 ? '99+' : assertionsNum.value || ''}`;
case RequestComposition.AUTH:
return requestVModel.value.authConfig.authType !== RequestAuthType.NONE ? '1' : '';
default:
return '';
}
}
const protocolLoading = ref(false);
const protocolOptions = ref<SelectOptionData[]>([]);
async function initProtocolList() {
try {
protocolLoading.value = true;
const res = await getProtocolList(appStore.currentOrgId);
protocolOptions.value = res.map((e) => ({
label: e.protocol,
value: e.protocol,
polymorphicName: e.polymorphicName,
pluginId: e.pluginId,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
protocolLoading.value = false;
}
}
const hasLocalExec = ref(false); // api
const pluginScriptMap = ref<Record<string, PluginConfig>>({}); //
const temporaryPluginFormMap: Record<string, any> = {}; // API
const pluginLoading = ref(false);
const fApi = ref<Api>();
const currentPluginOptions = computed<Record<string, any>>(
() => pluginScriptMap.value[requestVModel.value.protocol]?.options || {}
);
const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[requestVModel.value.protocol]?.script || []
);
//
const handlePluginFormChange = debounce(() => {
if (isEditableApi.value) {
//
temporaryPluginFormMap[requestVModel.value.stepId] = fApi.value?.formData();
}
handleActiveDebugChange();
}, 300);
/**
* 控制插件表单字段显示
*/
function controlPluginFormFields() {
const currentFormFields = fApi.value?.fields();
let fields: string[] = [];
if (requestVModel.value.customizeRequestEnvEnable) {
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDefinitionFields || [];
} else {
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields || [];
}
// fields
fApi.value?.hidden(false, fields);
if (currentFormFields && currentFormFields.length < fields.length) {
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
} else {
//
fApi.value?.hidden(true, currentFormFields?.filter((e) => !fields.includes(e)) || []);
}
return fields;
}
/**
* 设置插件表单数据
*/
function setPluginFormData() {
const tempForm = temporaryPluginFormMap[requestVModel.value.stepId];
if (tempForm || !requestVModel.value.isNew) {
//
const formData = isEditableApi.value ? tempForm || requestVModel.value : requestVModel.value;
nextTick(() => {
if (fApi.value) {
fApi.value.nextRefresh(() => {
const form = {};
controlPluginFormFields().forEach((key) => {
form[key] = formData[key];
});
fApi.value?.setValue(cloneDeep(form));
setTimeout(() => {
// 300ms handlePluginFormChange
isInitPluginForm.value = true;
}, 300);
});
}
});
} else {
nextTick(() => {
controlPluginFormFields();
fApi.value?.clearValidateState();
fApi.value?.resetFields();
isInitPluginForm.value = true;
});
}
}
const pluginError = ref(false);
async function initPluginScript() {
const pluginId = protocolOptions.value.find((e) => e.value === requestVModel.value.protocol)?.pluginId;
if (!pluginId) {
Message.warning(t('apiTestDebug.noPluginTip'));
pluginError.value = true;
return;
}
pluginError.value = false;
isInitPluginForm.value = false;
if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) {
setPluginFormData();
//
return;
}
try {
pluginLoading.value = true;
const res = await getPluginScript(pluginId);
pluginScriptMap.value[requestVModel.value.protocol] = res;
setPluginFormData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
pluginLoading.value = false;
}
}
/**
* 处理协议切换 HTTP 协议切换到插件协议时需要初始化插件表单
*/
function handleActiveDebugProtocolChange(val: string) {
if (val !== 'HTTP') {
requestVModel.value.activeTab = RequestComposition.PLUGIN;
initPluginScript();
} else {
requestVModel.value.activeTab = RequestComposition.HEADER;
if (!Object.values(RequestMethods).includes(requestVModel.value.method)) {
// HTTP GET
requestVModel.value.method = RequestMethods.GET;
}
}
handleActiveDebugChange();
}
/**
* 处理url输入框变化解析成参数表格
*/
function handleUrlChange(val: string) {
const params = parseQueryParams(val.trim());
if (params.length > 0) {
requestVModel.value.query = [
...params.map((e, i) => ({
id: (new Date().getTime() + i).toString(),
...defaultRequestParamsItem,
...e,
})),
cloneDeep(defaultRequestParamsItem),
];
requestVModel.value.activeTab = RequestComposition.QUERY;
[requestVModel.value.url] = val.split('?');
}
handleActiveDebugChange();
}
const showResponse = computed(
() => isHttpProtocol.value || requestVModel.value.response?.requestResults[0]?.responseResult.responseCode
);
const splitBoxSize = ref<string | number>(!showResponse.value ? 1 : 0.6);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const splitContainerRef = ref<HTMLElement>();
const secondBoxHeight = ref(0);
watch(
() => splitBoxSize.value,
debounce((val) => {
// 300ms
if (splitContainerRef.value) {
if (typeof val === 'string' && val.includes('px')) {
val = Number(val.split('px')[0]);
secondBoxHeight.value = splitContainerRef.value.clientHeight - val;
} else {
secondBoxHeight.value = splitContainerRef.value.clientHeight * (1 - val);
}
}
}, 300),
{
immediate: true,
}
);
const verticalSplitBoxRef = ref<InstanceType<typeof MsSplitBox>>();
const isVerticalExpanded = ref(true);
function handleVerticalExpandChange(val: boolean) {
isVerticalExpanded.value = val;
}
function changeVerticalExpand(val: boolean) {
isVerticalExpanded.value = val;
if (val) {
verticalSplitBoxRef.value?.expand(0.6);
} else {
verticalSplitBoxRef.value?.collapse(
splitContainerRef.value ? `${splitContainerRef.value.clientHeight - 42}px` : 0
);
}
}
watch(
() => showResponse.value,
(val) => {
nextTick(() => {
if (val) {
changeVerticalExpand(true);
} else {
isVerticalExpanded.value = false;
verticalSplitBoxRef.value?.collapse(1);
}
});
}
);
function handleActiveLayoutChange() {
isVerticalExpanded.value = true;
splitBoxSize.value = 0.6;
verticalSplitBoxRef.value?.expand(0.6);
}
function filterConditionsSqlValidParams(condition: ExecuteConditionConfig) {
const conditionCopy = cloneDeep(condition);
conditionCopy.processors = conditionCopy.processors.map((processor) => {
if (processor.processorType === RequestConditionProcessor.SQL) {
processor.extractParams = filterKeyValParams(
processor.extractParams || [],
defaultKeyValueParamItem
).validParams;
}
return processor;
});
return conditionCopy;
}
/**
* 生成请求参数
* @param executeType 执行类型执行时传入
*/
function makeRequestParams(executeType?: 'localExec' | 'serverExec') {
const isExecute = executeType === 'localExec' || executeType === 'serverExec';
const polymorphicName = protocolOptions.value.find(
(e) => e.value === requestVModel.value.protocol
)?.polymorphicName; //
let parseRequestBodyResult;
let requestParams;
if (isHttpProtocol.value) {
const { formDataBody, wwwFormBody } = requestVModel.value.body;
const realFormDataBodyValues = filterKeyValParams(
formDataBody.formValues,
defaultBodyParamsItem,
isExecute
).validParams;
const realWwwFormBodyValues = filterKeyValParams(
wwwFormBody.formValues,
defaultBodyParamsItem,
isExecute
).validParams;
parseRequestBodyResult = parseRequestBodyFiles(
requestVModel.value.body,
requestVModel.value.uploadFileIds, //
requestVModel.value.linkFileIds //
);
requestParams = {
authConfig: requestVModel.value.authConfig,
body: {
...requestVModel.value.body,
formDataBody: {
formValues: realFormDataBodyValues,
},
wwwFormBody: {
formValues: realWwwFormBodyValues,
},
},
headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem, isExecute).validParams,
otherConfig: requestVModel.value.otherConfig,
path: requestVModel.value.url || requestVModel.value.path,
query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem, isExecute).validParams,
rest: filterKeyValParams(requestVModel.value.rest, defaultRequestParamsItem, isExecute).validParams,
url: requestVModel.value.url,
polymorphicName,
};
} else {
requestParams = {
...fApi.value?.formData(),
polymorphicName,
};
}
return {
...requestParams,
resourceId: requestVModel.value.resourceId,
stepId: requestVModel.value.stepId,
activeTab: requestVModel.value.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
responseActiveTab: ResponseComposition.BODY,
protocol: requestVModel.value.protocol,
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
name: requestVModel.value.name,
customizeRequestEnvEnable: requestVModel.value.customizeRequestEnvEnable,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: requestVModel.value.children[0].assertionConfig,
postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
},
],
executeLoading: isExecute,
...parseRequestBodyResult,
};
}
/**
* 执行调试
* @param val 执行类型
*/
async function handleExecute(executeType?: 'localExec' | 'serverExec') {
async function execute(executeType?: 'localExec' | 'serverExec') {
requestVModel.value.executeLoading = true;
if (isHttpProtocol.value) {
emit('execute', requestAndResponseRef.value?.makeRequestParams(executeType), executeType);
emit('execute', makeRequestParams(executeType), executeType);
} else {
//
// fApi.value?.validate(async (valid) => {
// if (valid === true) {
// emit('execute', requestAndResponseRef.value?.makeRequestParams(executeType), executeType);
// } else {
// requestVModel.value.activeTab = RequestComposition.PLUGIN;
// nextTick(() => {
// scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
// });
// }
// });
fApi.value?.validate(async (valid) => {
if (valid === true) {
emit('execute', makeRequestParams(executeType), executeType);
} else {
requestVModel.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
});
}
}
@ -266,16 +946,20 @@
}
function handleClose() {
if (!isQuote.value) {
emit('applyStep', { ...requestVModel.value, ...requestAndResponseRef.value?.makeRequestParams() });
// applyStep
if (!requestVModel.value.isNew) {
emit('applyStep', cloneDeep(makeRequestParams()));
}
}
const isUrlError = ref(false);
// const showAddDependencyDrawer = ref(false);
// const addDependencyMode = ref<'pre' | 'post'>('pre');
function handleDelete() {
emit('deleteStep');
}
const loading = ref(false);
async function initQuoteCaseDetail() {
try {
loading.value = true;
@ -297,10 +981,10 @@
url: res.path,
name: res.name, // requestnamenull
resourceId: res.id,
stepId: activeStep.value?.id || '',
...parseRequestBodyResult,
};
nextTick(() => {
requestAndResponseRef.value?.setActiveTabByFirst();
// loading
loading.value = false;
});
@ -315,14 +999,15 @@
() => visible.value,
async (val) => {
if (val) {
requestVModel.value = {
...cloneDeep(defaultCaseParams),
if (protocolOptions.value.length === 0) {
await initProtocolList();
}
// api api
requestVModel.value = cloneDeep({
...defaultApiParams,
...props.request,
response: {
requestResults: [props.stepResponses?.[props.request?.stepId] || defaultResponse.requestResults[0]],
console: props.stepResponses?.[props.request?.stepId]?.console || '',
},
};
isNew: false,
});
if (isQuote.value || isCopyNeedInit.value) {
// (request.requestrequest null)
initQuoteCaseDetail();

View File

@ -100,6 +100,7 @@
import { getSystemRequest } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { getGenerateId, mapTree } from '@/utils';
import type { ApiCaseDetail, ApiDefinitionDetail } from '@/models/apiTest/management';
import type { ApiScenarioTableItem } from '@/models/apiTest/scenario';
@ -193,8 +194,13 @@
visible.value = false;
}
/**
* 获取复制或引用的步骤数据
* @param refType 复制或引用
*/
async function getScenarioSteps(refType: ScenarioStepRefType.COPY | ScenarioStepRefType.REF) {
const scenarioMap: Record<string, MsTableDataItem<ApiScenarioTableItem>[]> = {};
// id
selectedScenarios.value.forEach((e) => {
if (!scenarioMap[e.projectId]) {
scenarioMap[e.projectId] = [];
@ -203,6 +209,7 @@
});
const scenarioRequestArr: any[] = [];
Object.keys(scenarioMap).forEach((projectId) => {
//
scenarioRequestArr.push(
getSystemRequest({
scenarioRequest: {
@ -225,8 +232,15 @@
fullScenarioArr = fullScenarioArr.map((e) => {
return {
...e,
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => {
return {
...node,
copyFromStepId: node.id,
id: getGenerateId(),
};
}),
name: `copy-${e.name}`,
copyFromStepId: e.id,
copyFromStepId: e.resourceId,
};
});
emit(
@ -239,6 +253,21 @@
);
handleCancel();
} else {
fullScenarioArr = fullScenarioArr.map((e) => {
return {
...e,
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => {
return {
...node,
copyFromStepId: node.id,
config: {
isRefScenarioStep: true, //
},
id: getGenerateId(),
};
}),
};
});
emit(
'quote',
cloneDeep({

View File

@ -287,13 +287,18 @@
if (tableSelectedKeys.value.includes(key)) {
//
tableSelectedData.value = tableSelectedData.value.filter((e) => e.id !== key);
}
if (selectedData) {
} else if (selectedData) {
tableSelectedData.value.push(selectedData);
}
emit('select', tableSelectedData.value);
}
function clearSelector() {
tableSelectedData.value = [];
currentTable.value.clearSelector();
emit('select', []);
}
/**
* 表格全选事件处理
*/
@ -310,10 +315,13 @@
//
useApiTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
useApiTable.propsEvent.value.selectAllChange = handleSelectAllChange;
useApiTable.propsEvent.value.clearSelector = clearSelector;
useCaseTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
useCaseTable.propsEvent.value.selectAllChange = handleSelectAllChange;
useCaseTable.propsEvent.value.clearSelector = clearSelector;
useScenarioTable.propsEvent.value.rowSelectChange = handleRowSelectChange;
useScenarioTable.propsEvent.value.selectAllChange = handleSelectAllChange;
useScenarioTable.propsEvent.value.clearSelector = clearSelector;
function loadPage(ids?: (string | number)[]) {
nextTick(() => {

View File

@ -1,4 +1,4 @@
import { Scenario } from '@/models/apiTest/scenario';
import { Scenario, ScenarioStepConfig } from '@/models/apiTest/scenario';
import {
ApiScenarioStatus,
RequestAssertionCondition,
@ -51,6 +51,13 @@ export const defaultTimeController = {
delay: 0, // 等待时间
};
// 场景配置
export const defaultScenarioStepConfig: ScenarioStepConfig = {
enableScenarioEnv: false,
useBothScenarioParam: false,
useCurrentScenarioParam: true,
};
export const defaultStepItemCommon = {
checked: false,
expanded: false,
@ -62,6 +69,7 @@ export const defaultStepItemCommon = {
id: '',
name: '',
enable: true,
isRefScenarioStep: false,
},
createActionsVisible: false,
responsePopoverVisible: false,

View File

@ -9,6 +9,7 @@ import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import {
defaultConditionController,
defaultLoopController,
defaultScenarioStepConfig,
defaultStepItemCommon,
defaultTimeController,
} from '../../config';
@ -109,11 +110,13 @@ export default function useCreateActions() {
config = cloneDeep(defaultConditionController);
} else if (stepType === ScenarioStepType.CONSTANT_TIMER) {
config = cloneDeep(defaultTimeController);
} else if (stepType === ScenarioStepType.API_SCENARIO) {
config = cloneDeep(defaultScenarioStepConfig);
}
if (item.id) {
if (item.id || item.resourceId) {
// 引用复制接口、用例、场景时的源资源信息
resourceField = {
resourceId: item.id,
resourceId: item.id || item.resourceId,
resourceNum: item.num,
resourceName: item.name,
};
@ -132,6 +135,7 @@ export default function useCreateActions() {
config: {
...defaultStepItemCommon.config,
...config,
isRefScenarioStep: item.config?.isRefScenarioStep || false,
},
children: item.children || [],
stepType,

View File

@ -217,7 +217,8 @@
});
} else {
scenario.value.steps = mapTree(scenario.value.steps, (node) => {
if (ids.has(node.id)) {
if (ids.has(node.id) && node.config.isRefScenarioStep !== true) {
//
node.enable = isBatchEnable.value;
}
return node;

View File

@ -58,8 +58,9 @@
</div>
</a-tooltip>
<div class="mr-[8px] flex items-center gap-[8px]">
<!-- 步骤启用/禁用 -->
<!-- 步骤启用/禁用完全引用的场景下的子孙步骤不可禁用 -->
<a-switch
v-show="step.config.isRefScenarioStep !== true"
v-model:model-value="step.enable"
size="small"
@click.stop="handleStepToggleEnable(step)"
@ -241,7 +242,7 @@
:request="currentStepDetail"
:step-responses="scenario.stepResponses"
@apply-step="applyApiStep"
@delete-step="deleteCaseStep"
@delete-step="deleteCaseStep(activeStep)"
@stop-debug="handleStopExecute(activeStep)"
@execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)"
/>
@ -286,6 +287,85 @@
</template>
</MsCodeEditor>
</a-modal>
<a-modal
v-model:visible="showScenarioConfig"
:title="t('apiScenario.scenarioConfig')"
:ok-text="t('common.confirm')"
class="ms-modal-form"
body-class="!overflow-hidden !p-0"
:width="680"
title-align="start"
@ok="applyQuickInput"
>
<a-form :model="scenarioConfigForm" layout="vertical" class="ms-form">
<a-form-item>
<template #label>
<div class="flex items-center gap-[4px]">
{{ t('apiScenario.quoteMode') }}
<a-tooltip position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
<template #content>
<div>{{ t('apiScenario.fullQuoteTip') }}</div>
<div>{{ t('apiScenario.stepQuoteTip') }}</div>
</template>
</a-tooltip>
</div>
</template>
<a-radio-group v-model:model-value="scenarioConfigForm.refType">
<a-radio :value="ScenarioStepRefType.REF">{{ t('apiScenario.fullQuote') }}</a-radio>
<a-radio :value="ScenarioStepRefType.PARTIAL_REF">{{ t('apiScenario.stepQuote') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item :label="t('apiScenario.runRule')">
<a-radio-group v-model:model-value="scenarioConfigForm.useCurrentScenarioParam" type="button">
<a-radio :value="true">{{ t('apiScenario.currentScenario') }}</a-radio>
<a-radio :value="false">{{ t('apiScenario.sourceScenario') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="" class="hidden-item">
<a-radio-group v-model:model-value="scenarioConfigForm.useBothScenarioParam">
<a-radio :value="false">{{ t('apiScenario.empty') }}</a-radio>
<a-radio :value="true">{{ t('apiScenario.sourceScenarioEnv') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item label="" class="hidden-item !mb-0">
<div class="flex items-center gap-[8px]">
<a-checkbox v-model:model-value="scenarioConfigForm.enableScenarioEnv" class="ml-[6px]"></a-checkbox>
<div class="flex items-center gap-[4px]">
{{ t('apiScenario.sourceScenarioEnv') }}
<a-tooltip :content="t('apiScenario.sourceScenarioEnvTip')" position="right">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</div>
</a-form-item>
</a-form>
<template #footer>
<div class="flex items-center justify-between">
<div class="flex items-center">
<div class="text-[var(--color-text-4)]">
{{ t('apiScenario.valuePriority') }}
</div>
<div v-if="scenarioConfigForm.useCurrentScenarioParam" class="text-[var(--color-text-1)]">
{{ t('apiScenario.currentScenarioAndNull') }}
</div>
<div v-else class="text-[var(--color-text-1)]">
{{ t('apiScenario.sourceScenarioAndNull') }}
</div>
</div>
<div class="flex items-center gap-[12px]">
<a-button type="secondary" @click="cancelScenarioConfig">{{ t('common.cancel') }}</a-button>
<a-button type="primary" @click="saveScenarioConfig">{{ t('common.confirm') }}</a-button>
</div>
</div>
</template>
</a-modal>
</div>
</template>
@ -327,7 +407,13 @@
} from '@/utils';
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import { ApiScenarioDebugRequest, CreateStepAction, Scenario, ScenarioStepItem } from '@/models/apiTest/scenario';
import {
ApiScenarioDebugRequest,
CreateStepAction,
Scenario,
ScenarioStepConfig,
ScenarioStepItem,
} from '@/models/apiTest/scenario';
import { EnvConfig } from '@/models/projectManagement/environmental';
import {
ResponseComposition,
@ -380,6 +466,7 @@
const loading = ref(false);
const treeRef = ref<InstanceType<typeof MsTree>>();
const focusStepKey = ref<string | number>(''); // key
const activeStep = ref<ScenarioStepItem>(); //
function setFocusNodeKey(id: string | number) {
focusStepKey.value = id || '';
@ -516,6 +603,53 @@
return stepMoreActions;
}
const scenarioConfigForm = ref<
ScenarioStepConfig & {
refType: ScenarioStepRefType;
}
>({
refType: ScenarioStepRefType.REF,
enableScenarioEnv: false,
useBothScenarioParam: false,
useCurrentScenarioParam: true,
});
const showScenarioConfig = ref(false);
function cancelScenarioConfig() {
showScenarioConfig.value = false;
scenarioConfigForm.value = {
refType: ScenarioStepRefType.REF,
enableScenarioEnv: false,
useBothScenarioParam: false,
useCurrentScenarioParam: true,
};
}
function saveScenarioConfig() {
if (activeStep.value) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, activeStep.value.id, 'id');
if (realStep) {
realStep.refType = scenarioConfigForm.value.refType;
realStep.config = {
...realStep.config,
...scenarioConfigForm.value,
};
realStep.children = mapTree<ScenarioStepItem>(realStep.children || [], (child) => {
//
if (scenarioConfigForm.value.refType === ScenarioStepRefType.REF) {
child.config.isRefScenarioStep = true;
child.enable = true;
} else {
child.config.isRefScenarioStep = false;
}
return child;
});
Message.success(t('apiScenario.setSuccess'));
cancelScenarioConfig();
}
}
}
function handleStepMoreActionSelect(item: ActionsItem, node: MsTreeNodeData) {
switch (item.eventTag) {
case 'copy':
@ -569,7 +703,12 @@
);
break;
case 'config':
console.log('config', node);
activeStep.value = node as ScenarioStepItem;
scenarioConfigForm.value = {
refType: node.refType,
...node.config,
};
showScenarioConfig.value = true;
break;
case 'delete':
deleteNode(steps.value, node.id, 'id');
@ -659,7 +798,6 @@
const customCaseDrawerVisible = ref(false);
const customApiDrawerVisible = ref(false);
const scriptOperationDrawerVisible = ref(false);
const activeStep = ref<ScenarioStepItem>(); //
const activeCreateAction = ref<CreateStepAction>(); //
const currentStepDetail = computed<any>(() => {
// TODO:
@ -1008,7 +1146,11 @@
*/
function addCustomApiStep(request: RequestParam) {
request.isNew = false;
stepDetails.value[request.stepId] = request;
stepDetails.value[request.stepId] = {
...request,
customizeRequest: true,
customizeRequestEnvEnable: request.customizeRequestEnvEnable,
};
emit('updateResource', request.uploadFileIds, request.linkFileIds);
if (activeStep.value && activeCreateAction.value) {
handleCreateStep(
@ -1028,8 +1170,6 @@
steps.value.push({
...cloneDeep(defaultStepItemCommon),
config: {
customizeRequest: true,
customizeRequestEnvEnable: request.customizeRequestEnvEnable,
protocol: request.protocol,
method: request.method,
},
@ -1058,11 +1198,10 @@
/**
* 删除
*/
function deleteCaseStep() {
if (activeStep.value) {
function deleteCaseStep(step?: ScenarioStepItem) {
if (step) {
customCaseDrawerVisible.value = false;
steps.value = steps.value.filter((item) => item.id !== activeStep.value?.id);
delete stepDetails.value[activeStep.value?.id];
deleteNode(steps.value, step.id, 'id');
activeStep.value = undefined;
}
}
@ -1103,11 +1242,16 @@
* @param dropNode 释放节点
*/
function isAllowDropInside(dropNode: MsTreeNodeData) {
return [
ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.IF_CONTROLLER,
ScenarioStepType.ONCE_ONLY_CONTROLLER,
].includes(dropNode.stepType);
return (
//
[
ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.IF_CONTROLLER,
ScenarioStepType.ONCE_ONLY_CONTROLLER,
].includes(dropNode.stepType) ||
//
(dropNode.stepType === ScenarioStepType.API_SCENARIO && dropNode.refType === ScenarioStepRefType.COPY)
);
}
/**
@ -1359,4 +1503,10 @@
@apply !visible !w-auto;
}
}
.ms-form {
:deep(.arco-form-item-wrapper-col),
:deep(.arco-form-item-content) {
min-height: auto;
}
}
</style>

View File

@ -119,10 +119,11 @@
ApiScenarioGetModuleParams,
ApiScenarioTableItem,
Scenario,
ScenarioStepItem,
} from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { defaultScenario } from './components/config';

View File

@ -139,6 +139,21 @@ export default {
'apiScenario.running': '执行中',
'apiScenario.unExecute': '未执行',
'apiScenario.response': '响应内容',
'apiScenario.quoteMode': '引用模式',
'apiScenario.fullQuote': '完全引用',
'apiScenario.stepQuote': '步骤引用',
'apiScenario.runRule': '运行取值规则设置',
'apiScenario.currentScenario': '当前场景参数',
'apiScenario.sourceScenario': '源场景参数',
'apiScenario.empty': '空值',
'apiScenario.sourceScenarioEnv': '源场景环境',
'apiScenario.valuePriority': '取值优先级:',
'apiScenario.currentScenarioAndNull': '步骤参数>场景参数>环境参数>空值',
'apiScenario.sourceScenarioAndNull': '源场景参数>源场景环境>步骤参数>当前场景参数>当前环境参数',
'apiScenario.fullQuoteTip': '完全引用:跟随源步骤内容及步骤状态变化,步骤状态不可调整',
'apiScenario.stepQuoteTip': '步骤引用:仅跟随源步骤内容变化,步骤状态可调整',
'apiScenario.sourceScenarioEnvTip': '运行环境,含环境参数',
'apiScenario.setSuccess': '设置成功',
// 执行历史
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
'apiScenario.executeHistory.num': '序号',

View File

@ -167,10 +167,7 @@
allow-clear
/>
</a-form-item>
<a-form-item
:label="t('system.config.email.from')"
field="from" asterisk-position="end"
:rules="[emailRule]">
<a-form-item :label="t('system.config.email.from')" field="from" asterisk-position="end" :rules="[emailRule]">
<a-input
v-model:model-value="emailConfigForm.from"
:max-length="255"