feat(接口测试): 断言&提取优化&部分 bug 修复

This commit is contained in:
baiqi 2024-05-09 18:23:18 +08:00 committed by 刘瑞斌
parent 58a069dc82
commit 3ed55b0e07
24 changed files with 293 additions and 113 deletions

View File

@ -1,5 +1,5 @@
<template> <template>
<a-spin class="!block" :loading="props.loading" :size="28"> <a-spin class="z-[100] !block" :loading="props.loading" :size="28">
<div <div
ref="fullRef" ref="fullRef"
:class="[ :class="[

View File

@ -310,6 +310,7 @@ export enum WhileConditionType {
CONDITION = 'CONDITION', CONDITION = 'CONDITION',
SCRIPT = 'SCRIPT', SCRIPT = 'SCRIPT',
} }
// 场景步骤多态名
export enum ScenarioStepPolymorphicName { export enum ScenarioStepPolymorphicName {
COMMON_SCRIPT = 'MsCommentScriptElement', COMMON_SCRIPT = 'MsCommentScriptElement',
IF_CONTROLLER = 'MsIfController', IF_CONTROLLER = 'MsIfController',
@ -317,7 +318,20 @@ export enum ScenarioStepPolymorphicName {
ONLY_ONCE = 'MsOnceOnlyController', ONLY_ONCE = 'MsOnceOnlyController',
TIME_CONTROLLER = 'MsConstantTimerController', TIME_CONTROLLER = 'MsConstantTimerController',
} }
// 场景设置-失败执行策略
export enum ScenarioFailureStrategy { export enum ScenarioFailureStrategy {
CONTINUE = 'CONTINUE', CONTINUE = 'CONTINUE',
STOP = 'STOP', STOP = 'STOP',
} }
// 接口响应-断言项类型
export enum FullResponseAssertionType {
DOCUMENT = 'DOCUMENT',
RESPONSE_CODE = 'RESPONSE_CODE',
RESPONSE_HEADER = 'RESPONSE_HEADER',
RESPONSE_TIME = 'RESPONSE_TIME',
SCRIPT = 'SCRIPT',
VARIABLE = 'VARIABLE',
JSON_PATH = 'JSON_PATH',
XPATH = 'XPATH',
REGEX = 'REGEX',
}

View File

@ -1,6 +1,7 @@
import { Language } from '@/components/pure/ms-code-editor/types'; import { Language } from '@/components/pure/ms-code-editor/types';
import { import {
type FullResponseAssertionType,
RequestAssertionCondition, RequestAssertionCondition,
RequestAuthType, RequestAuthType,
RequestBodyFormat, RequestBodyFormat,
@ -385,6 +386,24 @@ export interface ExecuteRequestParams {
frontendDebug?: boolean; // 是否本地调试,该模式下接口会返回执行参数,用来调用本地执行服务 frontendDebug?: boolean; // 是否本地调试,该模式下接口会返回执行参数,用来调用本地执行服务
apiDefinitionId?: string | number; // 接口用例执行和调试时需要传 apiDefinitionId?: string | number; // 接口用例执行和调试时需要传
} }
// 断言-断言列表表格子项
export interface ResponseAssertionTableItem {
actualValue: string;
assertionType: FullResponseAssertionType;
condition: RequestAssertionConditionType;
content: string;
expectedValue: string;
message: string;
name: string;
pass: boolean;
}
// 请求提取结果项
export interface ResponseExtractItem {
name: string;
value: string;
type: RequestExtractEnvType;
expression: string;
}
// 响应结果 // 响应结果
export interface ResponseResult { export interface ResponseResult {
body: string; body: string;
@ -402,7 +421,8 @@ export interface ResponseResult {
tcpHandshakeTime: number; tcpHandshakeTime: number;
transferStartTime: number; transferStartTime: number;
vars: string; vars: string;
assertions: any; extractResults: ResponseExtractItem[];
assertions: ResponseAssertionTableItem[];
imageUrl?: string; // 返回为图片时的图片地址 imageUrl?: string; // 返回为图片时的图片地址
} }

View File

@ -13,8 +13,6 @@ export interface OrgIdNameMap {
// 资源池配置信息对象 // 资源池配置信息对象
export interface TestResourceDTO { export interface TestResourceDTO {
loadTestImage: string; // 镜像
loadTestHeap: string; // Jmeter heap
nodesList: NodesListItem[]; // node资源 nodesList: NodesListItem[]; // node资源
ip: string; // k8s ip ip: string; // k8s ip
token: string; // k8s token token: string; // k8s token
@ -36,7 +34,6 @@ export interface ResourcePoolInfo {
type: string; // 资源池类型 type: string; // 资源池类型
enable: boolean; // 是否启用 enable: boolean; // 是否启用
apiTest: boolean; // 是否支持api测试 apiTest: boolean; // 是否支持api测试
loadTest: boolean; // 是否支持性能测试
uiTest: boolean; // 是否支持ui测试 uiTest: boolean; // 是否支持ui测试
serverUrl: string; // 资源池地址 serverUrl: string; // 资源池地址
allOrg: boolean; // 是否应用范围选择全部组织 allOrg: boolean; // 是否应用范围选择全部组织

View File

@ -517,7 +517,7 @@
ResponseBodyXPathAssertionFormat, ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
import { defaultKeyValueParamItem } from '@/views/api-test/components/config'; import { defaultKeyValueParamItem, extractTypeOptions } from '@/views/api-test/components/config';
const { openModal } = useModal(); const { openModal } = useModal();
@ -782,21 +782,12 @@ if (!result){
title: 'apiTestDebug.paramType', title: 'apiTestDebug.paramType',
dataIndex: 'variableType', dataIndex: 'variableType',
slotName: 'variableType', slotName: 'variableType',
typeOptions: [ typeOptions: extractTypeOptions.map((item) => {
// return {
// { label: t(item.label),
// label: t('apiTestDebug.globalParameter'), value: item.value,
// value: RequestExtractEnvType.GLOBAL, };
// }, }),
{
label: t('apiTestDebug.envParameter'),
value: RequestExtractEnvType.ENVIRONMENT,
},
{
label: t('apiTestDebug.tempParameter'),
value: RequestExtractEnvType.TEMPORARY,
},
],
width: 150, width: 150,
}, },
{ {

View File

@ -14,6 +14,7 @@ import {
} from '@/models/apiTest/common'; } from '@/models/apiTest/common';
import type { MockParams } from '@/models/apiTest/mock'; import type { MockParams } from '@/models/apiTest/mock';
import { import {
FullResponseAssertionType,
RequestAssertionCondition, RequestAssertionCondition,
RequestBodyFormat, RequestBodyFormat,
RequestCaseStatus, RequestCaseStatus,
@ -144,6 +145,7 @@ export const defaultResponse: RequestTaskResult = {
transferStartTime: 0, transferStartTime: 0,
sslHandshakeTime: 0, sslHandshakeTime: 0,
vars: '', vars: '',
extractResults: [],
assertions: [], assertions: [],
}, },
}, },
@ -239,6 +241,34 @@ export const regexDefaultParamItem = {
responseFormat: ResponseBodyXPathAssertionFormat.XML, responseFormat: ResponseBodyXPathAssertionFormat.XML,
moreSettingPopoverVisible: false, moreSettingPopoverVisible: false,
}; };
// 响应断言类型映射
export const responseAssertionTypeMap: Record<FullResponseAssertionType, string> = {
[FullResponseAssertionType.DOCUMENT]: 'apiTestManagement.document',
[FullResponseAssertionType.RESPONSE_CODE]: 'apiTestManagement.responseCode',
[FullResponseAssertionType.RESPONSE_HEADER]: 'apiTestManagement.responseHeader',
[FullResponseAssertionType.RESPONSE_TIME]: 'apiTestManagement.responseTime',
[FullResponseAssertionType.SCRIPT]: 'apiTestManagement.script',
[FullResponseAssertionType.VARIABLE]: 'apiTestManagement.variable',
[FullResponseAssertionType.JSON_PATH]: 'jsonPath',
[FullResponseAssertionType.XPATH]: 'xPath',
[FullResponseAssertionType.REGEX]: 'apiTestManagement.regex',
};
// 提取类型选项
export const extractTypeOptions = [
// 全局参数,暂时不上
// {
// label: t('apiTestDebug.globalParameter'),
// value: RequestExtractEnvType.GLOBAL,
// },
{
label: 'apiTestDebug.envParameter',
value: RequestExtractEnvType.ENVIRONMENT,
},
{
label: 'apiTestDebug.tempParameter',
value: RequestExtractEnvType.TEMPORARY,
},
];
// mock 匹配规则默认项 // mock 匹配规则默认项
export const defaultMatchRuleItem = { export const defaultMatchRuleItem = {
key: '', key: '',

View File

@ -10,8 +10,8 @@
</a-empty> </a-empty>
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col"> <div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
<div v-if="!props.isCase" class="px-[18px] pt-[8px]"> <div v-if="!props.isCase" class="px-[18px] pt-[8px]">
<div class="flex flex-wrap items-center justify-between gap-[12px]"> <div class="flex flex-wrap items-baseline justify-between gap-[12px]">
<div class="flex flex-1 items-center gap-[16px]"> <div class="flex flex-1 flex-wrap items-center gap-[16px]">
<a-select <a-select
v-if="requestVModel.isNew" v-if="requestVModel.isNew"
v-model:model-value="requestVModel.protocol" v-model:model-value="requestVModel.protocol"
@ -53,6 +53,9 @@
@change="handleUrlChange" @change="handleUrlChange"
/> />
</a-input-group> </a-input-group>
<div v-if="isUrlError" class="url-input-tip">
<span>{{ t('apiTestDebug.apiUrlRequired') }}</span>
</div>
</div> </div>
<div> <div>
<a-radio-group <a-radio-group
@ -184,9 +187,6 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="isUrlError" class="url-input-tip">
<span>{{ t('apiTestDebug.apiUrlRequired') }}</span>
</div>
<div :class="`${!props.isCase ? 'request-tab-and-response' : ''} mt-[8px] flex-1`"> <div :class="`${!props.isCase ? 'request-tab-and-response' : ''} mt-[8px] flex-1`">
<MsTab <MsTab
v-model:active-key="requestVModel.activeTab" v-model:active-key="requestVModel.activeTab"
@ -1622,11 +1622,13 @@
@apply leading-none; @apply leading-none;
} }
.url-input-tip { .url-input-tip {
margin-top: 2px 0 250px; @apply w-full;
margin-top: -14px;
padding-left: 226px;
font-size: 12px; font-size: 12px;
color: rgb(var(--danger-6)); color: rgb(var(--danger-6));
line-height: 16px; line-height: 16px;
@apply flex flex-col flex-nowrap items-center justify-start;
} }
.request-tab-and-response { .request-tab-and-response {
overflow-x: hidden; overflow-x: hidden;

View File

@ -3,7 +3,7 @@
v-if="props.requestResult?.responseResult?.responseCode" v-if="props.requestResult?.responseResult?.responseCode"
class="flex items-center justify-between gap-[24px] text-[14px]" class="flex items-center justify-between gap-[24px] text-[14px]"
> >
<a-tooltip :content="props.requestResult.fakeErrorCode"> <a-tooltip :content="props.requestResult.fakeErrorCode" :disabled="!props.requestResult.fakeErrorCode">
<executeStatus :status="finalStatus" size="small" class="ml-[4px]" /> <executeStatus :status="finalStatus" size="small" class="ml-[4px]" />
</a-tooltip> </a-tooltip>
<a-popover position="left" content-class="response-popover-content"> <a-popover position="left" content-class="response-popover-content">

View File

@ -12,16 +12,17 @@
/> />
<ResConsole v-else-if="activeTab === ResponseComposition.CONSOLE" :console="props.console?.trim()" /> <ResConsole v-else-if="activeTab === ResponseComposition.CONSOLE" :console="props.console?.trim()" />
<ResValueScript <ResValueScript
v-else-if=" v-else-if="activeTab === ResponseComposition.HEADER || activeTab === ResponseComposition.REAL_REQUEST"
activeTab === ResponseComposition.HEADER ||
activeTab === ResponseComposition.REAL_REQUEST ||
activeTab === ResponseComposition.EXTRACT
"
:active-tab="activeTab" :active-tab="activeTab"
:request-result="props.requestResult" :request-result="props.requestResult"
/> />
<ExtractTable
v-else-if="activeTab === ResponseComposition.EXTRACT"
:request-result="props.requestResult"
:scroll="{ x: '100%' }"
/>
<ResAssertion <ResAssertion
v-else-if="activeTab === 'ASSERTION'" v-else-if="activeTab === ResponseComposition.ASSERTION"
:request-result="props.requestResult" :request-result="props.requestResult"
:scroll="{ x: '100%' }" :scroll="{ x: '100%' }"
/> />
@ -58,6 +59,7 @@
import ResAssertion from './result/assertionTable.vue'; import ResAssertion from './result/assertionTable.vue';
import ResBody from './result/body.vue'; import ResBody from './result/body.vue';
import ResConsole from './result/console.vue'; import ResConsole from './result/console.vue';
import ExtractTable from './result/extractTable.vue';
import ResValueScript from './result/resValueScript.vue'; import ResValueScript from './result/resValueScript.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';

View File

@ -6,6 +6,15 @@
:selectable="false" :selectable="false"
:scroll="props.scroll" :scroll="props.scroll"
> >
<template #assertionItem="{ record }">
<div class="flex items-center gap-[4px]">
{{ t(responseAssertionTypeMap[(record as ResponseAssertionTableItem).assertionType]) }}
{{ record.name }}
</div>
</template>
<template #condition="{ record }">
{{ t(statusCodeOptions.find((item) => item.value === record.condition)?.label || '') }}
</template>
<template #status="{ record }"> <template #status="{ record }">
<MsTag :type="record.pass === true ? 'success' : 'danger'" theme="light"> <MsTag :type="record.pass === true ? 'success' : 'danger'" theme="light">
{{ record.pass === true ? t('common.success') : t('common.fail') }} {{ record.pass === true ? t('common.success') : t('common.fail') }}
@ -16,13 +25,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { statusCodeOptions } from '@/components/pure/ms-advance-filter/index';
import MsFormTable from '@/components/pure/ms-form-table/index.vue'; import MsFormTable from '@/components/pure/ms-form-table/index.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type'; import { MsTableColumn } from '@/components/pure/ms-table/type';
import MsTag from '@/components/pure/ms-tag/ms-tag.vue'; import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { RequestResult } from '@/models/apiTest/common'; import { RequestResult, ResponseAssertionTableItem } from '@/models/apiTest/common';
import { responseAssertionTypeMap } from '@/views/api-test/components/config';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps<{ const props = defineProps<{
@ -37,10 +49,29 @@
const columns: MsTableColumn = [ const columns: MsTableColumn = [
{ {
title: 'apiTestDebug.content', title: 'apiTestDebug.assertionItem',
dataIndex: 'content', dataIndex: 'assertionItem',
showTooltip: true, showTooltip: true,
width: 300, slotName: 'assertionItem',
width: 200,
},
{
title: 'apiTestDebug.actualValue',
dataIndex: 'actualValue',
showTooltip: true,
width: 200,
},
{
title: 'apiTestDebug.condition',
dataIndex: 'condition',
slotName: 'condition',
width: 120,
},
{
title: 'apiTestDebug.expectedValue',
dataIndex: 'expectedValue',
showTooltip: true,
width: 200,
}, },
{ {
title: 'apiTestDebug.status', title: 'apiTestDebug.status',

View File

@ -0,0 +1,69 @@
<template>
<a-scrollbar class="h-full overflow-y-auto">
<MsFormTable
:data="props.requestResult?.responseResult.extractResults"
:columns="columns"
:selectable="false"
:scroll="props.scroll"
>
<template #type="{ record }">
{{ t(extractTypeOptions.find((item) => item.value === record.type)?.label || '') }}
</template>
</MsFormTable>
</a-scrollbar>
</template>
<script setup lang="ts">
import MsFormTable from '@/components/pure/ms-form-table/index.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import { useI18n } from '@/hooks/useI18n';
import { RequestResult } from '@/models/apiTest/common';
import { extractTypeOptions } from '@/views/api-test/components/config';
const { t } = useI18n();
const props = defineProps<{
requestResult?: RequestResult;
scroll?: {
x?: number | string;
y?: number | string;
maxHeight?: number | string;
minWidth?: number | string;
};
}>();
const columns: MsTableColumn = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
showTooltip: true,
width: 200,
},
{
title: 'apiTestDebug.extractValue',
dataIndex: 'value',
showTooltip: true,
width: 200,
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
width: 120,
},
{
title: 'apiTestDebug.expression',
dataIndex: 'expression',
showTooltip: true,
width: 200,
},
];
</script>
<style lang="less" scoped>
.arco-scrollbar {
@apply h-full;
}
</style>

View File

@ -76,13 +76,13 @@
</template> </template>
</a-popover> </a-popover>
<a-popover position="left" content-class="response-popover-content"> <a-popover position="left" content-class="response-popover-content">
<div v-if="props.showType && props.showType !== 'CASE'" class="one-line-text max-w-[150px]">{{ <div v-if="props.showType && props.showType !== 'CASE'" class="one-line-text max-w-[150px]">
props.environmentName {{ props.environmentName }}
}}</div> </div>
<template #content> <template #content>
<div v-if="props.showType && props.showType !== 'CASE'" class="one-line-text">{{ <div v-if="props.showType && props.showType !== 'CASE'" class="one-line-text">
props.environmentName {{ props.environmentName }}
}}</div> </div>
</template> </template>
</a-popover> </a-popover>
</div> </div>

View File

@ -30,8 +30,6 @@
props.requestResult.headers props.requestResult.headers
}\nBody:\n${props.requestResult.body.trim()}` }\nBody:\n${props.requestResult.body.trim()}`
: ''; : '';
case ResponseComposition.EXTRACT:
return props.requestResult?.responseResult?.vars?.trim();
default: default:
return ''; return '';
} }

View File

@ -206,4 +206,8 @@ export default {
'Enter the column name and corresponding value in column storage. If you want to extract the first value of the name column, enter name_1', 'Enter the column name and corresponding value in column storage. If you want to extract the first value of the name column, enter name_1',
'apiTestDebug.responseRepeatMessage': 'The name is duplicated, please re-enter it.', 'apiTestDebug.responseRepeatMessage': 'The name is duplicated, please re-enter it.',
'apiTestDebug.saveAsApi': 'Save as Api', 'apiTestDebug.saveAsApi': 'Save as Api',
'apiTestDebug.assertionItem': 'Assertion item',
'apiTestDebug.actualValue': 'Actual value',
'apiTestDebug.condition': 'condition',
'apiTestDebug.expectedValue': 'Expected value',
}; };

View File

@ -192,4 +192,9 @@ export default {
'apiTestDebug.extractValueTitleTip': '输入按列存储中的列名和对应的数值如提取name列的第一个值则输入name_1', 'apiTestDebug.extractValueTitleTip': '输入按列存储中的列名和对应的数值如提取name列的第一个值则输入name_1',
'apiTestDebug.responseRepeatMessage': '名称重复,请重新输入', 'apiTestDebug.responseRepeatMessage': '名称重复,请重新输入',
'apiTestDebug.saveAsApi': '另存为接口', 'apiTestDebug.saveAsApi': '另存为接口',
'apiTestDebug.assertionItem': '断言项',
'apiTestDebug.actualValue': '返回值',
'apiTestDebug.condition': '匹配条件',
'apiTestDebug.expectedValue': '匹配值',
'apiTestDebug.extractValue': '提取值',
}; };

View File

@ -15,7 +15,7 @@
@close="handleCancel" @close="handleCancel"
> >
<template #tbutton> <template #tbutton>
<div v-if="mockDetail.id" class="right-operation-button-icon flex items-center gap-[4px]"> <div v-if="isReadOnly" class="right-operation-button-icon flex items-center gap-[4px]">
<MsButton <MsButton
v-permission="['PROJECT_API_DEFINITION_MOCK:READ+UPDATE']" v-permission="['PROJECT_API_DEFINITION_MOCK:READ+UPDATE']"
type="icon" type="icon"

View File

@ -154,6 +154,12 @@ export default {
'apiTestManagement.quoteSearchPlaceholder': 'Enter ID or name to search', 'apiTestManagement.quoteSearchPlaceholder': 'Enter ID or name to search',
'apiTestManagement.tableNoDataAndPlease': 'No data yet, please', 'apiTestManagement.tableNoDataAndPlease': 'No data yet, please',
'apiTestManagement.or': 'or', 'apiTestManagement.or': 'or',
'apiTestManagement.document': 'Document',
'apiTestManagement.responseHeader': 'Response header',
'apiTestManagement.responseTime': 'Response time',
'apiTestManagement.script': 'Script',
'apiTestManagement.variable': 'Variable',
'apiTestManagement.regex': 'Regular',
'case.execute.selectEnv': 'Environmental choice', 'case.execute.selectEnv': 'Environmental choice',
'case.execute.defaultEnv': 'Default environment', 'case.execute.defaultEnv': 'Default environment',
'case.execute.newEnv': 'New environment', 'case.execute.newEnv': 'New environment',

View File

@ -150,6 +150,12 @@ export default {
'apiTestManagement.getResponse': '获取响应内容', 'apiTestManagement.getResponse': '获取响应内容',
'apiTestManagement.tableNoDataAndPlease': '暂无数据,请', 'apiTestManagement.tableNoDataAndPlease': '暂无数据,请',
'apiTestManagement.or': '或', 'apiTestManagement.or': '或',
'apiTestManagement.document': '文档断言',
'apiTestManagement.responseHeader': '响应头',
'apiTestManagement.responseTime': '响应时间',
'apiTestManagement.script': '脚本',
'apiTestManagement.variable': '变量',
'apiTestManagement.regex': '正则表达式',
'case.execute.selectEnv': '环境选择', 'case.execute.selectEnv': '环境选择',
'case.execute.defaultEnv': '默认环境', 'case.execute.defaultEnv': '默认环境',
'case.execute.newEnv': '新环境', 'case.execute.newEnv': '新环境',

View File

@ -274,6 +274,7 @@
// //
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; // node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; //
node.isRefScenarioStep = node.parent.isRefScenarioStep; // node.isRefScenarioStep = node.parent.isRefScenarioStep; //
node.draggable = !node.parent.isQuoteScenarioStep; //
} }
return { return {
...node, ...node,

View File

@ -352,6 +352,7 @@
// //
child.isQuoteScenarioStep = child.parent.isQuoteScenarioStep; // child.isQuoteScenarioStep = child.parent.isQuoteScenarioStep; //
child.isRefScenarioStep = child.parent.isRefScenarioStep; // child.isRefScenarioStep = child.parent.isRefScenarioStep; //
child.draggable = !node.parent.isQuoteScenarioStep; //
} }
if (selectedKeys.value.includes(node.uniqueId) && !selectedKeys.value.includes(child.uniqueId)) { if (selectedKeys.value.includes(node.uniqueId) && !selectedKeys.value.includes(child.uniqueId)) {
// //

View File

@ -561,7 +561,8 @@
const loading = ref(false); const loading = ref(false);
const treeRef = ref<InstanceType<typeof MsTree>>(); const treeRef = ref<InstanceType<typeof MsTree>>();
const focusStepKey = ref<string | number>(''); // key const focusStepKey = ref<string | number>(''); // key
const activeStep = ref<ScenarioStepItem>(); // const activeStep = ref<ScenarioStepItem>(); //
const activeStepByCreate = ref<ScenarioStepItem | undefined>(); //
function setFocusNodeKey(id: string | number) { function setFocusNodeKey(id: string | number) {
focusStepKey.value = id || ''; focusStepKey.value = id || '';
@ -1151,11 +1152,13 @@
if (activeStep.value) { if (activeStep.value) {
return stepDetails.value[activeStep.value.id]; return stepDetails.value[activeStep.value.id];
} }
return undefined;
}); });
const currentStepFileParams = computed<ScenarioStepFileParams | undefined>(() => { const currentStepFileParams = computed<ScenarioStepFileParams | undefined>(() => {
if (activeStep.value) { if (activeStep.value) {
return scenario.value.stepFileParam[activeStep.value.id]; return scenario.value.stepFileParam[activeStep.value.id];
} }
return undefined;
}); });
function handleAddStepDone(newStep: ScenarioStepItem) { function handleAddStepDone(newStep: ScenarioStepItem) {
@ -1447,7 +1450,7 @@
step?: ScenarioStepItem, step?: ScenarioStepItem,
_activeCreateAction?: CreateStepAction _activeCreateAction?: CreateStepAction
) { ) {
activeStep.value = step; activeStepByCreate.value = step;
activeCreateAction.value = _activeCreateAction; activeCreateAction.value = _activeCreateAction;
switch (type) { switch (type) {
case ScenarioAddStepActionType.IMPORT_SYSTEM_API: case ScenarioAddStepActionType.IMPORT_SYSTEM_API:
@ -1473,16 +1476,16 @@
*/ */
function handleImportApiApply(type: 'copy' | 'quote', data: ImportData) { function handleImportApiApply(type: 'copy' | 'quote', data: ImportData) {
let sort = steps.value.length + 1; let sort = steps.value.length + 1;
if (activeStep.value && activeCreateAction.value) { if (activeStepByCreate.value && activeCreateAction.value) {
switch (activeCreateAction.value) { switch (activeCreateAction.value) {
case 'inside': case 'inside':
sort = activeStep.value.children ? activeStep.value.children.length : 0; sort = activeStepByCreate.value.children ? activeStepByCreate.value.children.length : 0;
break; break;
case 'before': case 'before':
sort = activeStep.value.sort; sort = activeStepByCreate.value.sort;
break; break;
case 'after': case 'after':
sort = activeStep.value.sort + 1; sort = activeStepByCreate.value.sort + 1;
break; break;
default: default:
break; break;
@ -1511,8 +1514,14 @@
appStore.currentProjectId appStore.currentProjectId
); );
const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps); const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps);
if (activeStep.value && activeCreateAction.value) { if (activeStepByCreate.value && activeCreateAction.value) {
handleCreateSteps(activeStep.value, insertSteps, steps.value, activeCreateAction.value, selectedKeys.value); handleCreateSteps(
activeStepByCreate.value,
insertSteps,
steps.value,
activeCreateAction.value,
selectedKeys.value
);
} else { } else {
steps.value = steps.value.concat(insertSteps); steps.value = steps.value.concat(insertSteps);
} }
@ -1533,7 +1542,7 @@
unLinkFileIds: request.unLinkFileIds, unLinkFileIds: request.unLinkFileIds,
}; };
emit('updateResource', request.uploadFileIds, request.linkFileIds); emit('updateResource', request.uploadFileIds, request.linkFileIds);
if (activeStep.value && activeCreateAction.value) { if (activeStepByCreate.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
stepType: ScenarioStepType.CUSTOM_REQUEST, stepType: ScenarioStepType.CUSTOM_REQUEST,
@ -1546,7 +1555,7 @@
uniqueId: request.stepId, uniqueId: request.stepId,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}, },
activeStep.value, activeStepByCreate.value,
steps.value, steps.value,
activeCreateAction.value, activeCreateAction.value,
selectedKeys.value selectedKeys.value
@ -1641,7 +1650,7 @@
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) { function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
const id = getGenerateId(); const id = getGenerateId();
stepDetails.value[id] = cloneDeep(scriptProcessor); stepDetails.value[id] = cloneDeep(scriptProcessor);
if (activeStep.value && activeCreateAction.value) { if (activeStepByCreate.value && activeCreateAction.value) {
handleCreateStep( handleCreateStep(
{ {
id, id,
@ -1651,7 +1660,7 @@
name, name,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}, },
activeStep.value, activeStepByCreate.value,
steps.value, steps.value,
activeCreateAction.value, activeCreateAction.value,
selectedKeys.value selectedKeys.value

View File

@ -386,6 +386,7 @@
// //
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; // node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; //
node.isRefScenarioStep = node.parent.isRefScenarioStep; // node.isRefScenarioStep = node.parent.isRefScenarioStep; //
node.draggable = !node.parent.isQuoteScenarioStep; //
} }
if (!node.isQuoteScenarioStep && !node.isRefScenarioStep) { if (!node.isQuoteScenarioStep && !node.isRefScenarioStep) {
// //
@ -410,6 +411,7 @@
// //
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; // node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; //
node.isRefScenarioStep = node.parent.isRefScenarioStep; // node.isRefScenarioStep = node.parent.isRefScenarioStep; //
node.draggable = !node.parent.isQuoteScenarioStep; //
} }
node.uniqueId = getGenerateId(); node.uniqueId = getGenerateId();
return node; return node;
@ -533,6 +535,7 @@
// //
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; // node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; //
node.isRefScenarioStep = node.parent.isRefScenarioStep; // node.isRefScenarioStep = node.parent.isRefScenarioStep; //
node.draggable = !node.parent.isQuoteScenarioStep; //
} }
node.uniqueId = getGenerateId(); node.uniqueId = getGenerateId();
return node; return node;

View File

@ -78,6 +78,22 @@
<a-option v-for="org of orgOptions" :key="org.id" :value="org.id">{{ org.name }}</a-option> <a-option v-for="org of orgOptions" :key="org.id" :value="org.id">{{ org.name }}</a-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<!-- <a-form-item
:label="t('system.resourcePool.use')"
field="use"
class="form-item"
:rules="[{ required: true, message: t('system.resourcePool.useRequired') }]"
asterisk-position="end"
>
<a-checkbox-group v-model:model-value="form.use" @change="() => setIsSave(false)">
<a-checkbox v-for="use of useList" :key="use.value" :value="use.value">{{ t(use.label) }}</a-checkbox>
</a-checkbox-group>
<MsFormItemSub
v-if="form.use.length === 3"
:text="t('system.resourcePool.allUseTip')"
:show-fill-icon="false"
/>
</a-form-item> -->
<!--TODO:暂无性能测试--> <!--TODO:暂无性能测试-->
<!-- <template v-if="isCheckedPerformance"> <!-- <template v-if="isCheckedPerformance">
<a-form-item :label="t('system.resourcePool.mirror')" field="testResourceDTO.loadTestImage" class="form-item"> <a-form-item :label="t('system.resourcePool.mirror')" field="testResourceDTO.loadTestImage" class="form-item">
@ -342,7 +358,7 @@
</template> </template>
</a-form> </a-form>
<template #footerLeft> <template #footerLeft>
<MsButton v-if="isCheckedPerformance && isShowK8SResources" @click="showJobDrawer = true"> <MsButton v-if="isShowK8SResources" @click="showJobDrawer = true">
{{ t('system.resourcePool.customJobTemplate') }} {{ t('system.resourcePool.customJobTemplate') }}
<a-tooltip :content="t('system.resourcePool.jobTemplateTip')" position="tl" mini> <a-tooltip :content="t('system.resourcePool.jobTemplateTip')" position="tl" mini>
<icon-question-circle class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-6))]" /> <icon-question-circle class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-6))]" />
@ -405,8 +421,6 @@
type: 'Node', type: 'Node',
addType: 'single', addType: 'single',
testResourceDTO: { testResourceDTO: {
loadTestImage: '',
loadTestHeap: '',
uiGrid: '', uiGrid: '',
girdConcurrentNumber: 1, girdConcurrentNumber: 1,
podThreads: 1, podThreads: 1,
@ -466,7 +480,7 @@
...res, ...res,
addType: 'single', addType: 'single',
orgType: res.allOrg ? 'allOrg' : 'set', orgType: res.allOrg ? 'allOrg' : 'set',
use: [res.loadTest ? 'performance' : '', res.apiTest ? 'API' : '', res.uiTest ? 'UI' : ''].filter((e) => e), use: [res.apiTest ? 'API' : '', res.uiTest ? 'UI' : ''].filter((e) => e),
testResourceDTO: { testResourceDTO: {
...testResourceReturnDTO, ...testResourceReturnDTO,
girdConcurrentNumber: girdConcurrentNumber || 1, girdConcurrentNumber: girdConcurrentNumber || 1,
@ -522,14 +536,12 @@
*/ */
// //
const isSpecifiedOrg = computed(() => form.value.orgType === 'set'); const isSpecifiedOrg = computed(() => form.value.orgType === 'set');
//
const isCheckedPerformance = computed(() => form.value.use.includes('performance'));
// UI // UI
const isCheckedUI = computed(() => form.value.use.includes('UI')); const isCheckedUI = computed(() => form.value.use.includes('UI'));
// //
const isCheckedAPI = computed(() => form.value.use.includes('API')); const isCheckedAPI = computed(() => form.value.use.includes('API'));
// //
const isShowTypeItem = computed(() => ['API', 'performance'].some((s) => form.value.use.includes(s))); const isShowTypeItem = computed(() => ['API'].some((s) => form.value.use.includes(s)));
// Node // Node
const isShowNodeResources = computed(() => form.value.type === 'Node' && isShowTypeItem.value); const isShowNodeResources = computed(() => form.value.type === 'Node' && isShowTypeItem.value);
// K8S // K8S
@ -567,6 +579,13 @@
rules: [{ required: true, message: t('system.resourcePool.portRequired') }], rules: [{ required: true, message: t('system.resourcePool.portRequired') }],
placeholder: 'system.resourcePool.portPlaceholder', placeholder: 'system.resourcePool.portPlaceholder',
}, },
{
filed: 'monitor',
type: 'input',
label: 'system.resourcePool.monitor',
rules: [{ required: true, message: t('system.resourcePool.monitorRequired') }],
placeholder: 'system.resourcePool.monitorPlaceholder',
},
{ {
filed: 'concurrentNumber', filed: 'concurrentNumber',
type: 'inputNumber', type: 'inputNumber',
@ -632,11 +651,12 @@
if (e.trim() !== '') { if (e.trim() !== '') {
// //
const line = e.split(','); const line = e.split(',');
if (line.every((s) => s.trim() !== '') && !Number.isNaN(Number(line[2]))) { if (line.every((s) => s.trim() !== '') && !Number.isNaN(Number(line[3]))) {
const item = { const item = {
ip: line[0], ip: line[0],
port: line[1], port: line[1],
concurrentNumber: Number(line[2]), monitor: line[2],
concurrentNumber: Number(line[3]),
}; };
if (i === 0) { if (i === 0) {
// concurrentNumber // concurrentNumber
@ -727,8 +747,6 @@
jobDefinition, // k8s job jobDefinition, // k8s job
deployName, // k8s api deployName, // k8s api
nodesList, nodesList,
loadTestImage,
loadTestHeap,
uiGrid, uiGrid,
girdConcurrentNumber, girdConcurrentNumber,
} = testResourceDTO; } = testResourceDTO;
@ -748,15 +766,6 @@
deployName: isCheckedAPI.value ? deployName : null, // deployName: isCheckedAPI.value ? deployName : null, //
} }
: {}; : {};
//
const performanceDTO = isCheckedPerformance.value
? {
loadTestImage,
loadTestHeap,
...nodeResourceDTO,
...k8sResourceDTO,
}
: {};
// //
const apiDTO = isCheckedAPI.value const apiDTO = isCheckedAPI.value
? { ? {
@ -772,15 +781,14 @@
} }
: {}; : {};
const jobDTO = isCheckedPerformance.value && isShowK8SResources ? { jobDefinition } : {}; const jobDTO = isShowK8SResources ? { jobDefinition } : {};
return { return {
...form.value, ...form.value,
type: isShowTypeItem.value ? form.value.type : 'Node', // Node type: isShowTypeItem.value ? form.value.type : 'Node', // Node
allOrg: form.value.orgType === 'allOrg', allOrg: form.value.orgType === 'allOrg',
apiTest: form.value.use.includes('API'), // api apiTest: form.value.use.includes('API'), // api
loadTest: form.value.use.includes('performance'), //
uiTest: form.value.use.includes('UI'), // ui uiTest: form.value.use.includes('UI'), // ui
testResourceDTO: { ...performanceDTO, ...apiDTO, ...uiDTO, ...jobDTO, orgIds: form.value.testResourceDTO.orgIds }, testResourceDTO: { ...apiDTO, ...uiDTO, ...jobDTO, orgIds: form.value.testResourceDTO.orgIds },
}; };
} }

View File

@ -76,7 +76,6 @@
/** /**
* @description 系统设置-资源池 * @description 系统设置-资源池
*/ */
import { onMounted, Ref, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
@ -192,6 +191,7 @@
Message.success(t('system.resourcePool.enablePoolSuccess')); Message.success(t('system.resourcePool.enablePoolSuccess'));
loadList(); loadList();
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console
console.log(error); console.log(error);
} finally { } finally {
loading.value = false; loading.value = false;
@ -298,7 +298,7 @@
activePool.value.apiTest ? t('system.resourcePool.useAPI') : '', activePool.value.apiTest ? t('system.resourcePool.useAPI') : '',
activePool.value.uiTest ? t('system.resourcePool.useUI') : '', activePool.value.uiTest ? t('system.resourcePool.useUI') : '',
]; ];
const { type, testResourceReturnDTO, loadTest, apiTest, uiTest } = activePool.value; const { type, testResourceReturnDTO, apiTest, uiTest } = activePool.value;
const { const {
ip, ip,
token, // k8s token token, // k8s token
@ -308,8 +308,6 @@
deployName, // k8s api deployName, // k8s api
girdConcurrentNumber, girdConcurrentNumber,
nodesList, nodesList,
loadTestImage,
loadTestHeap,
uiGrid, uiGrid,
} = testResourceReturnDTO; } = testResourceReturnDTO;
// Node // Node
@ -358,7 +356,7 @@
] ]
: []; : [];
const jobTemplate = const jobTemplate =
loadTest && type === 'Kubernetes' type === 'Kubernetes'
? [ ? [
{ {
label: t('system.resourcePool.jobTemplate'), label: t('system.resourcePool.jobTemplate'),
@ -370,21 +368,8 @@
}, },
] ]
: []; : [];
// //
const performanceDesc = loadTest const resourceDesc = apiTest ? [...nodeResourceDesc, ...k8sResourceDesc] : [];
? [
{
label: t('system.resourcePool.mirror'),
value: loadTestImage,
},
{
label: t('system.resourcePool.testHeap'),
value: loadTestHeap,
},
]
: [];
// /
const resourceDesc = apiTest || loadTest ? [...nodeResourceDesc, ...k8sResourceDesc] : [];
// ui // ui
const uiDesc = uiTest const uiDesc = uiTest
? [ ? [
@ -399,15 +384,14 @@
] ]
: []; : [];
const detailType = const detailType = apiTest
apiTest || loadTest ? [
? [ {
{ label: t('system.resourcePool.detailType'),
label: t('system.resourcePool.detailType'), value: activePool.value.type,
value: activePool.value.type, },
}, ]
] : [];
: [];
activePoolDesc.value = [ activePoolDesc.value = [
{ {
label: t('system.resourcePool.detailDesc'), label: t('system.resourcePool.detailDesc'),
@ -433,7 +417,6 @@
tagType: 'default', tagType: 'default',
isTag: true, isTag: true,
}, },
...performanceDesc,
...uiDesc, ...uiDesc,
...detailType, ...detailType,
...resourceDesc, ...resourceDesc,