feat(接口测试):接口调试参数对齐&更新接口

This commit is contained in:
BAIQI 2024-02-10 19:20:12 +08:00 committed by Craftsman
parent a294a486a3
commit 8e2d37021d
23 changed files with 462 additions and 337 deletions

View File

@ -37,7 +37,7 @@
"dependencies": {
"@7polo/kity": "2.0.8",
"@7polo/kityminder-core": "1.4.53",
"@arco-design/web-vue": "^2.54.3",
"@arco-design/web-vue": "^2.54.4",
"@arco-themes/vue-ms-theme-default": "^0.0.30",
"@form-create/arco-design": "^3.1.23",
"@halo-dev/richtext-editor": "0.0.0-alpha.33",
@ -48,7 +48,7 @@
"@tiptap/vue-3": "^2.1.13",
"@types/color": "^3.0.4",
"@types/node": "^20.11.16",
"@vueuse/core": "^10.4.1",
"@vueuse/core": "^10.7.2",
"@xmldom/xmldom": "^0.8.10",
"ace-builds": "^1.24.2",
"ahooks-vue": "^0.15.1",

View File

@ -446,13 +446,13 @@ export const mockFunctions: MockParamItem[] = [
inputGroup: [
{
type: 'number',
value: NaN,
value: undefined,
label: 'start',
placeholder: 'ms.paramsInput.substrStartPlaceholder',
},
{
type: 'number',
value: NaN,
value: undefined,
label: 'end',
placeholder: 'ms.paramsInput.substrEndPlaceholder',
},
@ -896,6 +896,7 @@ export const JMeterAllVars = [
];
// 同名函数但参数不同,需要特殊处理
export const sameFuncNameVars = [
'@county(true)',
'@character(pool)',
"@character('lower')",
"@character('upper')",
@ -905,4 +906,4 @@ export const sameFuncNameVars = [
'@integer(1,100)',
];
// 带形参的函数集合,指的是函数入参为形参,如果用户未填写实参则不需要填充到入参框中
export const formalParameterVars = ['@character(pool)', '@idCard(birth)'];
export const formalParameterVars = ['@character(pool)', '@idCard(birth)', '@natural(1,100)', '@integer(1,100)'];

View File

@ -80,6 +80,7 @@
v-model:model-value="paramForm.func"
:options="paramFuncOptions"
:placeholder="t('ms.paramsInput.commonSelectPlaceholder')"
allow-clear
@change="(val) => handleParamFuncChange(val as string)"
>
<template #label="{ data }">
@ -228,8 +229,8 @@
</template>
<script setup lang="ts">
import { useEventListener, useVModel } from '@vueuse/core';
import { cloneDeep } from 'lodash-es';
import { useEventListener, useStorage, useVModel } from '@vueuse/core';
import { cloneDeep, includes } from 'lodash-es';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsCascader from '@/components/business/ms-cascader/index.vue';
@ -264,7 +265,7 @@
const autoCompleteParams = ref<MockParamItem[]>([]);
const isFocusAutoComplete = ref(false);
const popoverVisible = ref(false);
const lastTenParams = ref<MockParamItem[]>(JSON.parse(localStorage.getItem('ms-lastTenParams') || '[]')); // 使 10
const lastTenParams = useStorage('ms-lastTenParams', [] as MockParamItem[]); // 使 10
/**
* 搜索变量
@ -306,7 +307,8 @@
const lastParamsItem = lastTenParams.value.find((e) => e.value === val);
if (index > -1 && lastParamsItem) {
//
lastTenParams.value.splice(index, 1, lastParamsItem);
lastTenParams.value.splice(index, 1);
lastTenParams.value.unshift(lastParamsItem);
} else {
//
const mockParamItem = mockAllParams.find((e) => e.value === val);
@ -318,7 +320,6 @@
}
}
}
localStorage.setItem('ms-lastTenParams', JSON.stringify(lastTenParams.value));
}
function selectAutoComplete(val: string) {
@ -450,12 +451,13 @@
const variableParams = variableMatch[2]?.split(',').map((param) => param.trim());
const formalParameterVar = formalParameterVars.find((e) => e.includes(variableName)); //
if (formalParameterVar && variableParams.length > 0) {
// 使 ()
handleParamTypeChange(formalParameterVar);
} else if (sameFuncNameVars.includes(valueArr[0])) {
if (sameFuncNameVars.includes(valueArr[0])) {
//
//
handleParamTypeChange(valueArr[0]);
} else if (formalParameterVar && variableParams && variableParams.length > 0) {
// 使 ()
handleParamTypeChange(formalParameterVar);
} else {
handleParamTypeChange(`@${variableName}`); //
}
@ -516,8 +518,13 @@
const paramVal = [paramForm.value.param1, paramForm.value.param2, paramForm.value.param3, paramForm.value.param4]
.filter((e) => e !== '')
.join(',');
// ()
resultStr = paramVal !== '' ? paramForm.value.type.replace(testReg, `(${paramVal})`) : paramForm.value.type;
if (!paramForm.value.type.includes('(')) {
//
resultStr = `${paramForm.value.type}(${paramVal || ''})`;
} else {
// ()
resultStr = paramVal !== '' ? paramForm.value.type.replace(testReg, `(${paramVal})`) : paramForm.value.type;
}
} else {
resultStr = paramForm.value.type;
}

View File

@ -30,6 +30,7 @@
v-else-if="group.type === 'number'"
v-model:model-value="innerForm[`${paramKey}${index + 1}`]"
:placeholder="t(group.placeholder || '')"
model-event="input"
/>
<a-input
v-else-if="group.type === 'inputAppendSelect'"

View File

@ -38,8 +38,10 @@ export interface ResponseTiming {
}
// key-value参数信息
export interface KeyValueParam {
id: string; // id用于前端渲染后台无此字段
key: string;
value: string;
[key: string]: any; // 用于前端渲染时填充的自定义信息,后台无此字段
}
// 接口请求-带开启关闭的参数集合信息
export interface EnableKeyValueParam extends KeyValueParam {
@ -49,8 +51,8 @@ export interface EnableKeyValueParam extends KeyValueParam {
// 接口请求公共参数集合信息
export interface ExecuteRequestCommonParam extends EnableKeyValueParam {
encode: boolean; // 是否编码
maxLength: number;
minLength: number;
maxLength?: number;
minLength?: number;
paramType: RequestParamsType; // 参数类型
required: boolean;
description: string;
@ -148,6 +150,7 @@ export interface ScriptCommonConfig {
enableCommonScript: boolean; // 是否启用公共脚本
script: string; // 脚本内容
scriptId: string; // 脚本id
scriptName: string; // 脚本名称
scriptLanguage: RequestConditionScriptLanguageType; // 脚本语言
params: KeyValueParam[]; // 公共脚本参数
}
@ -169,14 +172,16 @@ export interface ResponseVariableAssertion {
}
// 执行请求-前后置条件处理器
export interface ExecuteConditionProcessorCommon {
id: number; // 处理器ID前端列表渲染需要后台无此字段
enable: boolean; // 是否启用
name: string; // 请求名称
name?: string; // 条件处理器名称
processorType: RequestConditionProcessor;
}
// 执行请求-前后置条件-脚本处理器
export type ScriptProcessor = ScriptCommonConfig;
// 执行请求-前后置条件-SQL脚本处理器
export interface SQLProcessor {
description: string; // 描述
dataSourceId: string; // 数据源ID
environmentId: string; // 环境ID
queryTimeout: number; // 超时时间
@ -219,9 +224,9 @@ export interface ExtractProcessor {
}
// 执行请求-前后置条件配置
export type ExecuteConditionProcessor = ExecuteConditionProcessorCommon &
(ScriptProcessor | SQLProcessor | TimeWaitingProcessor | ExtractProcessor);
Partial<ScriptProcessor & SQLProcessor & TimeWaitingProcessor & ExtractProcessor>;
export interface ExecuteConditionConfig {
enableGlobal: boolean; // 是否启用全局前置 默认为 true
enableGlobal?: boolean; // 是否启用全局前/后置 默认为 true
processors: ExecuteConditionProcessor[];
}
// 执行请求-断言配置子项
@ -248,7 +253,7 @@ export interface ExecuteCommonChild {
export interface ExecuteAuthConfig {
authType: RequestAuthType;
password: string;
username: string;
userName: string;
}
// 执行请求- body 配置-文本格式的 body
export interface ExecuteValueBody {

View File

@ -447,8 +447,8 @@ export function decodeStringToCharset(str: string, charset = 'UTF-8') {
interface ParsedCurlOptions {
url?: string;
queryParameters?: { name: string; value: string }[];
headers?: { name: string; value: string }[];
queryParameters?: { key: string; value: string }[];
headers?: { key: string; value: string }[];
}
/**
* curl
@ -467,8 +467,8 @@ export function parseCurlScript(curlScript: string): ParsedCurlOptions {
const queryMatch = curlScript.match(/\?(.*?)'/);
if (queryMatch) {
const queryParams = queryMatch[1].split('&').map((param) => {
const [name, value] = param.split('=');
return { name, value };
const [key, value] = param.split('=');
return { key, value };
});
options.queryParameters = queryParams;
}
@ -478,10 +478,10 @@ export function parseCurlScript(curlScript: string): ParsedCurlOptions {
if (headersMatch) {
const headers = headersMatch.map((header) => {
const [, value] = header.match(/-H\s+'([^']+)'/) || [];
const [name, rawValue] = value.split(':');
const trimmedName = name.trim();
const [key, rawValue] = value.split(':');
const trimmedName = key.trim();
const trimmedValue = rawValue ? rawValue.trim() : '';
return { name: trimmedName, value: trimmedValue };
return { key: trimmedName, value: trimmedValue };
});
// 过滤常用的 HTTP header
@ -500,12 +500,12 @@ export function parseCurlScript(curlScript: string): ParsedCurlOptions {
'sec-fetch-mode',
'sec-fetch-site',
'user-agent',
'Connection',
'Host',
'Accept-Encoding',
'X-Requested-With',
'connection',
'host',
'accept-encoding',
'x-requested-with',
];
options.headers = headers.filter((header) => !commonHeaders.includes(header.name.toLowerCase()));
options.headers = headers.filter((header) => !commonHeaders.includes(header.key.toLowerCase()));
}
return options;

View File

@ -1,19 +1,19 @@
<template>
<div class="condition-content">
<!-- 脚本操作 -->
<template v-if="condition.type === RequestConditionProcessor.SCRIPT">
<a-radio-group v-model:model-value="condition.scriptType" size="small" class="mb-[16px]">
<a-radio value="manual">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio value="quote">{{ t('apiTestDebug.quote') }}</a-radio>
<template v-if="condition.processorType === RequestConditionProcessor.SCRIPT">
<a-radio-group v-model:model-value="condition.enableCommonScript" class="mb-[16px]">
<a-radio :value="false">{{ t('apiTestDebug.manual') }}</a-radio>
<a-radio :value="true">{{ t('apiTestDebug.quote') }}</a-radio>
</a-radio-group>
<div
v-if="condition.scriptType === 'manual'"
v-if="!condition.enableCommonScript"
class="relative rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
>
<div v-if="isShowEditScriptNameInput" class="absolute left-[12px] z-10 w-[calc(100%-24px)]">
<a-input
ref="scriptNameInputRef"
v-model:model-value="condition.name"
v-model:model-value="condition.scriptName"
:placeholder="t('apiTestDebug.preconditionScriptNamePlaceholder')"
:max-length="255"
size="small"
@ -23,10 +23,10 @@
</div>
<div class="flex items-center justify-between">
<div class="flex items-center">
<a-tooltip :content="condition.name">
<a-tooltip :content="condition.scriptName">
<div class="script-name-container">
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
{{ condition.name }}
{{ condition.scriptName }}
</div>
<MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" />
</div>
@ -88,7 +88,7 @@
<div v-else class="flex h-[calc(100%-47px)] flex-col">
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{ condition.quoteScript.name || '-' }}
{{ condition.scriptName || '-' }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium">
@ -116,7 +116,7 @@
</MsBaseTable>
<div v-show="commonScriptShowType === 'scriptContent'" class="h-[calc(100%-76px)]">
<MsCodeEditor
v-model:model-value="condition.quoteScript.script"
v-model:model-value="condition.script"
theme="MS-text"
height="100%"
:show-full-screen="false"
@ -128,18 +128,18 @@
</div>
</template>
<!-- SQL操作 -->
<template v-else-if="condition.type === RequestConditionProcessor.SQL">
<template v-else-if="condition.processorType === RequestConditionProcessor.SQL">
<div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div>
<a-input
v-model:model-value="condition.desc"
v-model:model-value="condition.description"
:placeholder="t('apiTestDebug.commonPlaceholder')"
:max-length="255"
/>
</div>
<div class="mb-[16px] flex w-full items-center bg-[var(--color-text-n9)] p-[12px]">
<div class="text-[var(--color-text-2)]">
{{ condition.sqlSource.name || '-' }}
{{ condition.scriptName || '-' }}
</div>
<a-divider margin="8px" direction="vertical" />
<MsButton type="text" class="font-medium" @click="quoteSqlSourceDrawerVisible = true">
@ -149,7 +149,7 @@
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.sqlScript') }}</div>
<div class="mb-[16px] h-[300px]">
<MsCodeEditor
v-model:model-value="condition.sqlSource.script"
v-model:model-value="condition.script"
theme="vs"
height="276px"
:language="LanguageEnum.SQL"
@ -173,47 +173,43 @@
</template>
</a-tooltip>
</div>
<a-radio-group v-model:model-value="condition.sqlSource.storageType" size="small" type="button" class="w-fit">
<a-radio value="column">{{ t('apiTestDebug.storageByCol') }}</a-radio>
<a-radio value="result">{{ t('apiTestDebug.storageByResult') }}</a-radio>
</a-radio-group>
</div>
<div v-if="condition.sqlSource.storageType === 'column'" class="mb-[16px]">
<div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByCol') }}</div>
<a-input
v-model:model-value="condition.sqlSource.storageByCol"
v-model:model-value="condition.variableNames"
:max-length="255"
:placeholder="t('apiTestDebug.storageByColPlaceholder', { a: '{id_1}', b: '{username_1}' })"
/>
</div>
<div v-else class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByResult') }}</div>
<a-input
v-model:model-value="condition.sqlSource.storageByResult"
:max-length="255"
:placeholder="t('apiTestDebug.storageByResultPlaceholder', { a: '${result}' })"
/>
</div>
<div v-if="condition.sqlSource.storageType === 'column'" class="sql-table-container">
<div class="sql-table-container">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.extractParameter') }}</div>
<paramTable
v-model:params="condition.sqlSource.params"
v-model:params="condition.variables"
:columns="sqlSourceColumns"
:selectable="false"
@change="handleSqlSourceParamTableChange"
/>
</div>
<div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.storageByResult') }}</div>
<a-input
v-model:model-value="condition.resultVariable"
:max-length="255"
:placeholder="t('apiTestDebug.storageByResultPlaceholder', { a: '${result}' })"
/>
</div>
</template>
<!-- 等待时间 -->
<div v-else-if="condition.type === RequestConditionProcessor.TIME_WAITING">
<div v-else-if="condition.processorType === RequestConditionProcessor.TIME_WAITING">
<div class="mb-[8px] flex items-center">
{{ t('apiTestDebug.waitTime') }}
<div class="text-[var(--color-text-4)]">(ms)</div>
</div>
<a-input-number v-model:model-value="condition.time" mode="button" :step="100" :min="0" class="w-[160px]" />
<a-input-number v-model:model-value="condition.delay" mode="button" :step="100" :min="0" class="w-[160px]" />
</div>
<!-- 提取参数 -->
<div v-else-if="condition.type === RequestConditionProcessor.EXTRACT">
<div v-else-if="condition.processorType === RequestConditionProcessor.EXTRACT">
<paramTable
ref="extractParamsTableRef"
v-model:params="condition.extractParams"
@ -320,11 +316,12 @@
import { useI18n } from '@/hooks/useI18n';
import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
import { ExecuteConditionProcessor, JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
import {
RequestConditionProcessor,
RequestExtractEnvType,
RequestExtractExpressionEnum,
RequestExtractExpressionRuleType,
RequestExtractResultMatchingRule,
RequestExtractScope,
ResponseBodyXPathAssertionFormat,
@ -333,14 +330,14 @@
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
const props = defineProps<{
data: Record<string, any>;
data: ExecuteConditionProcessor;
response?: string; //
heightUsed?: number;
}>();
const emit = defineEmits<{
(e: 'update:data', data: Record<string, any>): void;
(e: 'update:data', data: ExecuteConditionProcessor): void;
(e: 'copy'): void;
(e: 'delete', id: string): void;
(e: 'delete', id: number): void;
(e: 'change'): void;
}>();
@ -358,18 +355,7 @@
});
}
const scriptEx = ref(`2023-12-04 11:19:28 INFO 9026fd6a 1-1 Thread started: 9026fd6a 1-1
2023-12-04 11:19:28 ERROR 9026fd6a 1-1 Problem in JSR223 script JSR223Sampler, message: {}
In file: inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' Encountered "import" at line 2, column 1.
in inline evaluation of: prev.getResponseCode() import java.net.URI; import org.apache.http.client.method . . . '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException '' at line number 2
javax.script.ScriptException
org.apache.http.client.method . . . '' at line number 2
`);
const scriptEx = ref('');
const { copy, isSupported } = useClipboard();
function copyScriptEx() {
@ -382,7 +368,7 @@ org.apache.http.client.method . . . '' at line number 2
}
function clearScript() {
condition.value.script = '';
condition.value.enable = false;
}
/**
@ -403,7 +389,8 @@ org.apache.http.client.method . . . '' at line number 2
const columns: MsTableColumn = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'key',
dataIndex: 'key',
showTooltip: true,
},
{
@ -413,7 +400,8 @@ org.apache.http.client.method . . . '' at line number 2
},
{
title: 'apiTestDebug.desc',
dataIndex: 'desc',
dataIndex: 'description',
slotName: 'description',
showTooltip: true,
},
];
@ -425,26 +413,26 @@ org.apache.http.client.method . . . '' at line number 2
{
id: new Date().getTime(),
required: false,
name: 'asdasd',
key: 'asdasd',
type: 'string',
value: '',
desc: '',
description: '',
},
{
id: new Date().getTime(),
required: true,
name: '23d23d',
key: '23d23d',
type: 'string',
value: '',
desc: '',
description: '',
},
] as any;
const sqlSourceColumns: ParamTableColumn[] = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
dataIndex: 'key',
slotName: 'key',
},
{
title: 'apiTestDebug.paramValue',
@ -459,13 +447,14 @@ org.apache.http.client.method . . . '' at line number 2
},
];
const quoteSqlSourceDrawerVisible = ref(false);
function handleQuoteSqlSourceApply(sqlSource: any) {
condition.value.sqlSource = sqlSource;
function handleQuoteSqlSourceApply(sqlSource: Record<string, any>) {
condition.value.script = sqlSource.script;
condition.value.dataSourceId = sqlSource.id;
emit('change');
}
function handleSqlSourceParamTableChange(resultArr: any[], isInit?: boolean) {
condition.value.sqlSource.params = [...resultArr];
condition.value.variables = [...resultArr];
if (!isInit) {
emit('change');
}
@ -474,26 +463,26 @@ org.apache.http.client.method . . . '' at line number 2
const extractParamsColumns: ParamTableColumn[] = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
dataIndex: 'variableName',
slotName: 'key',
width: 150,
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
dataIndex: 'variableType',
slotName: 'variableType',
typeOptions: [
{
label: t('apiTestDebug.globalParameter'),
value: 'global',
value: RequestExtractEnvType.GLOBAL,
},
{
label: t('apiTestDebug.envParameter'),
value: 'env',
value: RequestExtractEnvType.ENVIRONMENT,
},
{
label: t('apiTestDebug.tempParameter'),
value: 'temp',
value: RequestExtractEnvType.TEMPORARY,
},
],
width: 130,
@ -505,15 +494,15 @@ org.apache.http.client.method . . . '' at line number 2
typeOptions: [
{
label: t('apiTestDebug.regular'),
value: 'regular',
value: RequestExtractExpressionEnum.REGEX,
},
{
label: 'JSONPath',
value: 'JSONPath',
value: RequestExtractExpressionEnum.JSON_PATH,
},
{
label: 'XPath',
value: 'XPath',
value: RequestExtractExpressionEnum.X_PATH,
},
],
width: 120,
@ -525,35 +514,35 @@ org.apache.http.client.method . . . '' at line number 2
typeOptions: [
{
label: 'Body',
value: 'body',
value: RequestExtractScope.BODY,
},
{
label: 'Body (unescaped)',
value: 'body_unescaped',
value: RequestExtractScope.UNESCAPED_BODY,
},
{
label: 'Body as a Document',
value: 'body_document',
value: RequestExtractScope.BODY_AS_DOCUMENT,
},
{
label: 'URL',
value: 'url',
value: RequestExtractScope.URL,
},
{
label: 'Request Headers',
value: 'request_headers',
value: RequestExtractScope.REQUEST_HEADERS,
},
{
label: 'Response Headers',
value: 'response_headers',
value: RequestExtractScope.RESPONSE_HEADERS,
},
{
label: 'Response Code',
value: 'response_code',
value: RequestExtractScope.RESPONSE_CODE,
},
{
label: 'Response Message',
value: 'response_message',
value: RequestExtractScope.RESPONSE_MESSAGE,
},
],
width: 190,
@ -598,7 +587,7 @@ org.apache.http.client.method . . . '' at line number 2
extractScope: RequestExtractScope.BODY,
expression: '',
extractType: RequestExtractExpressionEnum.REGEX,
regexpMatchRule: 'expression',
expressionMatchingRule: RequestExtractExpressionRuleType.EXPRESSION,
resultMatchingRule: RequestExtractResultMatchingRule.RANDOM,
resultMatchingRuleNum: 1,
responseFormat: ResponseBodyXPathAssertionFormat.XML,
@ -632,13 +621,13 @@ org.apache.http.client.method . . . '' at line number 2
* 提取参数表格-应用更多设置
*/
function applyMoreSetting(record: ExpressionConfig) {
condition.value.extractParams = condition.value.extractParams.map((e) => {
condition.value.extractParams = condition.value.extractParams?.map((e) => {
if (e.id === activeRecord.value.id) {
record.moreSettingPopoverVisible = false;
return {
...activeRecord.value,
moreSettingPopoverVisible: false,
};
} as any; // TOOD:
}
return e;
});
@ -649,7 +638,7 @@ org.apache.http.client.method . . . '' at line number 2
* 提取参数表格-保存快速提取的配置
*/
function handleFastExtractionApply(config: RegexExtract | JSONPathExtract | XPathExtract) {
condition.value.extractParams = condition.value.extractParams.map((e) => {
condition.value.extractParams = condition.value.extractParams?.map((e) => {
if (e.id === activeRecord.value.id) {
return {
...e,

View File

@ -17,7 +17,7 @@
<slot name="titleRight"></slot>
</div>
</div>
<div v-show="data.length > 0" class="flex h-[calc(100%-40px)] gap-[8px]">
<div v-if="data.length > 0" class="flex h-[calc(100%-40px)] gap-[8px]">
<div class="h-full w-[20%] min-w-[220px]">
<conditionList
v-model:list="data"
@ -46,27 +46,27 @@
import { conditionTypeNameMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import { ConditionType } from '@/models/apiTest/debug';
import { RequestConditionProcessor } from '@/enums/apiEnum';
import { ConditionType, ExecuteConditionProcessor } from '@/models/apiTest/debug';
import { RequestConditionProcessor, RequestConditionScriptLanguage } from '@/enums/apiEnum';
const props = defineProps<{
list: Array<Record<string, any>>;
list: ExecuteConditionProcessor[];
conditionTypes: Array<ConditionType>;
addText: string;
heightUsed?: number;
response?: string; //
}>();
const emit = defineEmits<{
(e: 'update:list', list: Array<Record<string, any>>): void;
(e: 'update:list', list: ExecuteConditionProcessor[]): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
const data = useVModel(props, 'list', emit);
const activeItem = ref<Record<string, any>>({});
const activeItem = ref<ExecuteConditionProcessor>(data.value[0]);
function handleListActiveChange(item: Record<string, any>) {
function handleListActiveChange(item: ExecuteConditionProcessor) {
activeItem.value = item;
}
@ -78,8 +78,8 @@
...activeItem.value,
id: new Date().getTime(),
};
data.value.push(copyItem);
activeItem.value = copyItem;
data.value.push(copyItem as ExecuteConditionProcessor);
activeItem.value = copyItem as ExecuteConditionProcessor;
emit('change');
}
@ -117,45 +117,44 @@ org.apache.http.client.method . . . '' at line number 2
case RequestConditionProcessor.SCRIPT:
data.value.push({
id,
type: RequestConditionProcessor.SCRIPT,
name: t('apiTestDebug.preconditionScriptName'),
scriptType: 'manual',
processorType: RequestConditionProcessor.SCRIPT,
scriptName: t('apiTestDebug.preconditionScriptName'),
enableCommonScript: false,
enable: true,
script: '',
quoteScript: {
name: '',
script: scriptEx,
},
});
break;
case RequestConditionProcessor.SQL:
data.value.push({
id,
type: RequestConditionProcessor.SQL,
desc: '',
enable: true,
sqlSource: {
name: '',
script: scriptEx,
storageType: 'column',
params: [],
},
script: scriptEx.value,
scriptId: '',
scriptLanguage: RequestConditionScriptLanguage.BEANSHELL,
params: [],
});
break;
// case RequestConditionProcessor.SQL:
// data.value.push({
// id,
// enableCommonScript: false,
// desc: '',
// enable: true,
// sqlSource: {
// scriptName: '',
// script: scriptEx,
// storageType: 'column',
// params: [],
// },
// });
// break;
case RequestConditionProcessor.TIME_WAITING:
data.value.push({
id,
type: RequestConditionProcessor.TIME_WAITING,
processorType: RequestConditionProcessor.TIME_WAITING,
enable: true,
time: 1000,
delay: 1000,
});
break;
case RequestConditionProcessor.EXTRACT:
data.value.push({
id,
type: RequestConditionProcessor.EXTRACT,
processorType: RequestConditionProcessor.EXTRACT,
enable: true,
extractParams: [],
extractors: [],
});
break;
default:

View File

@ -43,13 +43,15 @@
import { conditionTypeNameMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import { ExecuteConditionProcessor } from '@/models/apiTest/debug';
const props = defineProps<{
list: Array<Record<string, any>>;
list: ExecuteConditionProcessor[];
activeId?: string | number;
}>();
const emit = defineEmits<{
(e: 'update:list', list: Array<Record<string, any>>): void;
(e: 'activeChange', item: Record<string, any>): void;
(e: 'update:list', list: ExecuteConditionProcessor[]): void;
(e: 'activeChange', item: ExecuteConditionProcessor): void;
(e: 'change'): void;
}>();
@ -58,7 +60,7 @@
//
const focusItemKey = ref<any>('');
//
const activeItem = ref<Record<string, any>>({});
const activeItem = ref<ExecuteConditionProcessor>({} as ExecuteConditionProcessor);
const itemMoreActions: ActionsItem[] = [
{
label: 'common.copy',
@ -81,7 +83,7 @@
}
);
function handleItemClick(item: Record<string, any>) {
function handleItemClick(item: ExecuteConditionProcessor) {
activeItem.value = item;
emit('activeChange', item);
}
@ -90,7 +92,7 @@
* 复制列表项
* @param item 列表项
*/
function copyListItem(item: Record<string, any>) {
function copyListItem(item: ExecuteConditionProcessor) {
const copyItem = {
...item,
id: new Date().getTime(),
@ -104,7 +106,7 @@
* 删除列表项
* @param item 列表项
*/
function deleteListItem(item: Record<string, any>) {
function deleteListItem(item: ExecuteConditionProcessor) {
data.value = data.value.filter((precondition) => precondition.id !== item.id);
if (activeItem.value.id === item.id) {
[activeItem.value] = data.value;
@ -117,7 +119,7 @@
* @param event
* @param item
*/
function handleMoreActionSelect(event: ActionsItem, item: Record<string, any>) {
function handleMoreActionSelect(event: ActionsItem, item: ExecuteConditionProcessor) {
if (event.eventTag === 'copy') {
copyListItem(item);
} else if (event.eventTag === 'delete') {

View File

@ -6,7 +6,7 @@
disabled-width-drag
@confirm="emit('apply', expressionForm)"
>
<div v-if="expressionForm.expressionType === 'regular'" class="h-[400px]">
<div v-if="expressionForm.extractType === RequestExtractExpressionEnum.REGEX" class="h-[400px]">
<MsCodeEditor
:model-value="props.response"
theme="vs"
@ -18,15 +18,15 @@
read-only
/>
</div>
<div v-else-if="expressionForm.expressionType === 'JSONPath'" class="code-container">
<div v-else-if="expressionForm.extractType === RequestExtractExpressionEnum.JSON_PATH" class="code-container">
<MsJsonPathPicker :data="props.response || ''" class="bg-white" @pick="handlePathPick" />
</div>
<div v-else-if="expressionForm.expressionType === 'XPath'" class="code-container">
<div v-else-if="expressionForm.extractType === RequestExtractExpressionEnum.X_PATH" class="code-container">
<MsXPathPicker :xml-string="props.response || ''" class="bg-white" @pick="handlePathPick" />
</div>
<a-form ref="expressionFormRef" :model="expressionForm" layout="vertical" class="mt-[16px]">
<a-form-item
v-if="expressionForm.expressionType === 'regular'"
v-if="expressionForm.extractType === RequestExtractExpressionEnum.REGEX"
field="expression"
:label="t('apiTestDebug.regularExpression')"
:rules="[{ required: true, message: t('apiTestDebug.regularExpressionRequired') }]"
@ -45,7 +45,7 @@
</div>
</a-form-item>
<a-form-item
v-else-if="expressionForm.expressionType === 'JSONPath'"
v-else-if="expressionForm.extractType === RequestExtractExpressionEnum.JSON_PATH"
field="expression"
label="JSONPath"
:rules="[{ required: true, message: t('apiTestDebug.JSONPathRequired') }]"
@ -101,13 +101,15 @@
</a-tooltip>
</div>
<a-radio-group
v-if="expressionForm.expressionType === 'regular'"
v-model:model-value="expressionForm.regexpMatchRule"
v-if="expressionForm.extractType === RequestExtractExpressionEnum.REGEX"
v-model:model-value="expressionForm.expressionMatchingRule"
type="button"
size="small"
>
<a-radio value="expression">{{ t('apiTestDebug.matchExpression') }}</a-radio>
<a-radio value="group">{{ t('apiTestDebug.matchGroup') }}</a-radio>
<a-radio :value="RequestExtractExpressionRuleType.EXPRESSION">
{{ t('apiTestDebug.matchExpression') }}
</a-radio>
<a-radio :value="RequestExtractExpressionRuleType.GROUP">{{ t('apiTestDebug.matchGroup') }}</a-radio>
</a-radio-group>
</div>
<div class="match-result">
@ -151,7 +153,8 @@
import { useI18n } from '@/hooks/useI18n';
import { matchXMLWithXPath } from '@/utils/xpath';
import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
import type { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
import { RequestExtractExpressionEnum, RequestExtractExpressionRuleType } from '@/enums/apiEnum';
const props = defineProps<{
visible: boolean;
@ -190,8 +193,8 @@
* 测试表达式
*/
function testExpression() {
switch (props.config.expressionType) {
case 'XPath':
switch (props.config.extractType) {
case RequestExtractExpressionEnum.X_PATH:
const nodes = matchXMLWithXPath(props.response || '', expressionForm.value.expression);
if (nodes) {
//
@ -211,7 +214,7 @@
matchResult.value = [];
}
break;
case 'JSONPath':
case RequestExtractExpressionEnum.JSON_PATH:
try {
matchResult.value = JSONPath({
json: props.response ? JSON.parse(props.response) : '',
@ -221,7 +224,7 @@
matchResult.value = JSONPath({ json: props.response || '', path: expressionForm.value.expression });
}
break;
case 'regular':
case RequestExtractExpressionEnum.REGEX:
default:
// /g
const matchesIterator = props.response?.matchAll(
@ -230,7 +233,7 @@
if (matchesIterator) {
const matches = Array.from(matchesIterator);
try {
if (expressionForm.value.regexpMatchRule === 'expression') {
if (expressionForm.value.expressionMatchingRule === 'expression') {
//
matchResult.value = matches.map((e) => e[0]) || [];
} else {

View File

@ -1,11 +1,11 @@
<template>
<div>
<div v-if="expressionForm.expressionType === 'regular' && props.isPopover" class="mb-[16px]">
<div v-if="expressionForm.extractType === RequestExtractExpressionEnum.REGEX && props.isPopover" class="mb-[16px]">
<div class="mb-[8px] text-[14px] text-[var(--color-text-1)]">
{{ t('apiTestDebug.expressionMatchRule') }}
</div>
<a-radio-group v-model:model-value="expressionForm.regexpMatchRule" size="small">
<a-radio value="expression">
<a-radio-group v-model:model-value="expressionForm.expressionMatchingRule" size="small">
<a-radio :value="RequestExtractExpressionRuleType.EXPRESSION.toLowerCase()">
<div class="flex items-center">
{{ t('apiTestDebug.matchExpression') }}
<a-tooltip :content="t('apiTestDebug.matchExpressionTip')" :content-style="{ maxWidth: '500px' }">
@ -16,7 +16,7 @@
</a-tooltip>
</div>
</a-radio>
<a-radio value="group">
<a-radio :value="RequestExtractExpressionRuleType.GROUP.toLowerCase()">
<div class="flex items-center">
{{ t('apiTestDebug.matchGroup') }}
<a-tooltip :content="t('apiTestDebug.matchGroupTip')" :content-style="{ maxWidth: '500px' }">
@ -33,8 +33,8 @@
<div class="mb-[8px] text-[14px] text-[var(--color-text-1)]">
{{ t('apiTestDebug.resultMatchRule') }}
</div>
<a-radio-group v-model:model-value="expressionForm.resultMatchRule" size="small">
<a-radio value="random">
<a-radio-group v-model:model-value="expressionForm.resultMatchingRule" size="small">
<a-radio :value="RequestExtractResultMatchingRule.RANDOM.toLowerCase()">
<div class="flex items-center">
{{ t('apiTestDebug.randomMatch') }}
<a-tooltip :content="t('apiTestDebug.randomMatchTip')" :content-style="{ maxWidth: '400px' }">
@ -45,7 +45,7 @@
</a-tooltip>
</div>
</a-radio>
<a-radio value="specify">
<a-radio :value="RequestExtractResultMatchingRule.SPECIFIC.toLowerCase()">
<div class="flex items-center">
{{ t('apiTestDebug.specifyMatch') }}
<a-tooltip :content="t('apiTestDebug.specifyMatchTip')" :content-style="{ maxWidth: '400px' }">
@ -56,7 +56,7 @@
</a-tooltip>
</div>
</a-radio>
<a-radio value="all">
<a-radio :value="RequestExtractResultMatchingRule.ALL.toLowerCase()">
<div class="flex items-center">
{{ t('apiTestDebug.allMatch') }}
<a-tooltip :content="t('apiTestDebug.allMatchTip')" :content-style="{ maxWidth: '400px' }">
@ -69,7 +69,7 @@
</a-radio>
</a-radio-group>
</div>
<div v-if="expressionForm.resultMatchRule === 'specify'" class="mb-[16px]">
<div v-if="expressionForm.resultMatchingRule === RequestExtractResultMatchingRule.SPECIFIC" class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">
{{ t('apiTestDebug.specifyMatchResult') }}
</div>
@ -79,13 +79,13 @@
{{ t('apiTestDebug.unit') }}
</div>
</div>
<div v-if="expressionForm.expressionType === 'XPath'" class="mb-[16px]">
<div v-if="expressionForm.extractType === RequestExtractExpressionEnum.X_PATH" class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">
{{ t('apiTestDebug.contentType') }}
</div>
<a-radio-group v-model:model-value="expressionForm.xmlMatchContentType" size="small">
<a-radio value="xml"> XML </a-radio>
<a-radio value="html"> HTML </a-radio>
<a-radio-group v-model:model-value="expressionForm.responseFormat" size="small">
<a-radio :value="ResponseBodyXPathAssertionFormat.XML"> {{ ResponseBodyXPathAssertionFormat.XML }} </a-radio>
<a-radio :value="ResponseBodyXPathAssertionFormat.HTML"> {{ ResponseBodyXPathAssertionFormat.HTML }} </a-radio>
</a-radio-group>
</div>
</div>
@ -97,6 +97,12 @@
import { useI18n } from '@/hooks/useI18n';
import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
import {
RequestExtractExpressionEnum,
RequestExtractExpressionRuleType,
RequestExtractResultMatchingRule,
ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum';
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;

View File

@ -29,7 +29,11 @@
</template>
<!-- 表格列 slot -->
<template #key="{ record, columnConfig }">
<a-popover position="tl" :disabled="!record.key || record.key.trim() === ''" class="ms-params-input-popover">
<a-popover
position="tl"
:disabled="!record[columnConfig.dataIndex as string] || record[columnConfig.dataIndex as string].trim() === ''"
class="ms-params-input-popover"
>
<template #content>
<div class="param-popover-title">
{{ t('apiTestDebug.paramName') }}
@ -47,7 +51,7 @@
/>
</a-popover>
</template>
<template #type="{ record, columnConfig }">
<template #paramType="{ record, columnConfig }">
<a-tooltip
v-if="columnConfig.hasRequired"
:content="t(record.required ? 'apiTestDebug.paramRequired' : 'apiTestDebug.paramNotRequired')"
@ -64,7 +68,7 @@
</MsButton>
</a-tooltip>
<a-select
v-model:model-value="record.type"
v-model:model-value="record.paramType"
:options="columnConfig.typeOptions || []"
class="param-input w-full"
@change="(val) => handleTypeChange(val, record)"
@ -78,9 +82,17 @@
@change="(val) => handleExpressionTypeChange(val)"
/>
</template>
<template #range="{ record, columnConfig }">
<template #variableType="{ record, columnConfig }">
<a-select
v-model:model-value="record.range"
v-model:model-value="record.variableType"
:options="columnConfig.typeOptions || []"
class="param-input w-[110px]"
@change="(val) => handleVariableTypeChange(val)"
/>
</template>
<template #extractScope="{ record, columnConfig }">
<a-select
v-model:model-value="record.extractScope"
:options="columnConfig.typeOptions || []"
class="param-input w-[180px]"
@change="(val) => handleRangeChange(val)"
@ -123,19 +135,19 @@
<template #lengthRange="{ record }">
<div class="flex items-center justify-between">
<a-input-number
v-model:model-value="record.min"
v-model:model-value="record.minLength"
:placeholder="t('apiTestDebug.paramMin')"
:min="0"
class="param-input param-input-number"
@change="(val) => addTableLine(val || '', 'min')"
@change="(val) => addTableLine(val, 'minLength')"
/>
<div class="mx-[4px]"></div>
<a-input-number
v-model:model-value="record.max"
v-model:model-value="record.maxLength"
:placeholder="t('apiTestDebug.paramMax')"
:min="0"
class="param-input"
@change="(val) => addTableLine(val || '', 'max')"
@change="(val) => addTableLine(val, 'maxLength')"
/>
</div>
</template>
@ -314,23 +326,6 @@
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
import { SelectAllEnum, TableKeyEnum } from '@/enums/tableEnum';
interface Param {
id: number;
required: boolean;
key: string;
type: string;
value: string;
min: number | undefined;
max: number | undefined;
contentType: RequestContentTypeEnum;
description: string;
encode: boolean;
tag: string[];
enable: boolean;
mustContain: boolean;
[key: string]: any;
}
export type ParamTableColumn = MsTableColumnData & {
isNormal?: boolean; // value MsParamsInput
hasRequired?: boolean; // type required
@ -343,8 +338,8 @@
const props = withDefaults(
defineProps<{
params: any[];
defaultParamItem?: Partial<Param>; //
params?: any[];
defaultParamItem?: Record<string, any>; //
columns: ParamTableColumn[];
scroll?: {
x?: number | string;
@ -363,6 +358,7 @@
response?: string; //
}>(),
{
params: () => [],
selectable: true,
showSetting: false,
tableKey: undefined,
@ -370,10 +366,10 @@
defaultParamItem: () => ({
required: false,
key: '',
type: RequestParamsType.STRING,
paramType: RequestParamsType.STRING,
value: '',
min: undefined,
max: undefined,
minLength: undefined,
maxLength: undefined,
contentType: RequestContentTypeEnum.TEXT,
tag: [],
description: '',
@ -570,9 +566,9 @@
function handleTypeChange(
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[],
record: Partial<Param>
record: Record<string, any>
) {
addTableLine(val as string, 'type');
addTableLine(val as string, 'paramType');
// Content-Type
if (record.contentType) {
if (val === 'file') {
@ -591,10 +587,16 @@
addTableLine(val as string, 'expressionType');
}
function handleVariableTypeChange(
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
addTableLine(val as string, 'variableType');
}
function handleRangeChange(
val: string | number | boolean | Record<string, any> | (string | number | boolean | Record<string, any>)[]
) {
addTableLine(val as string, 'range');
addTableLine(val as string, 'extractScope');
}
/**

View File

@ -9,7 +9,7 @@
<a-form v-if="authForm.authType !== 'NONE'" ref="authFormRef" :model="authForm" layout="vertical">
<a-form-item :label="t('apiTestDebug.username')">
<a-input
v-model:model-value="authForm.username"
v-model:model-value="authForm.userName"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
:max-length="255"
@ -58,7 +58,7 @@
function authTypeChange(val: string | number | boolean) {
if (val === 'none') {
authForm.value.username = '';
authForm.value.userName = '';
authForm.value.password = '';
}
}

View File

@ -4,11 +4,18 @@
v-model:model-value="innerParams.bodyType"
type="button"
size="small"
@change="(val) => changeBodyFormat(val as string)"
@change="(val) => changeBodyFormat(val as RequestBodyFormat)"
>
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">{{ requestBodyTypeMap[item] }}</a-radio>
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">
{{ requestBodyTypeMap[item] }}
</a-radio>
</a-radio-group>
<batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" />
<batchAddKeyVal
v-if="showParamTable"
:params="currentTableParams"
:default-param-item="defaultParamItem"
@apply="handleBatchParamApply"
/>
</div>
<div
v-if="innerParams.bodyType === RequestBodyFormat.NONE"
@ -24,6 +31,7 @@
:height-used="heightUsed"
:show-setting="true"
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_DATA"
:default-param-item="defaultParamItem"
@change="handleParamTableChange"
/>
<paramTable
@ -34,6 +42,7 @@
:height-used="heightUsed"
:show-setting="true"
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_URL_ENCODE"
:default-param-item="defaultParamItem"
@change="handleParamTableChange"
/>
<div v-else-if="innerParams.bodyType === RequestBodyFormat.BINARY">
@ -94,7 +103,7 @@
import { useI18n } from '@/hooks/useI18n';
import { ExecuteBody } from '@/models/apiTest/debug';
import { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum';
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
const props = defineProps<{
@ -110,21 +119,33 @@
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const defaultParamItem = {
key: '',
value: '',
paramType: RequestParamsType.STRING,
description: '',
required: false,
maxLength: undefined,
minLength: undefined,
encode: false,
enable: true,
contentType: RequestContentTypeEnum.TEXT,
};
const columns = computed<ParamTableColumn[]>(() => [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
dataIndex: 'key',
slotName: 'key',
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
dataIndex: 'paramType',
slotName: 'paramType',
hasRequired: true,
typeOptions: Object.keys(RequestParamsType).map((key) => ({
label: RequestParamsType[key],
value: key,
typeOptions: Object.values(RequestParamsType).map((val) => ({
label: val,
value: val,
})),
width: 120,
},
@ -141,16 +162,17 @@
align: 'center',
width: 200,
},
{
title: 'apiTestDebug.desc',
dataIndex: 'description',
slotName: 'description',
},
{
title: 'apiTestDebug.encode',
dataIndex: 'encode',
slotName: 'encode',
titleSlotName: 'encodeTitle',
width: 80,
},
{
title: 'apiTestDebug.desc',
dataIndex: 'description',
slotName: 'description',
},
{
title: '',
@ -254,8 +276,8 @@
emit('change');
}
function changeBodyFormat(val: string) {
innerParams.value.bodyType = val as RequestBodyFormat;
function changeBodyFormat(val: RequestBodyFormat) {
innerParams.value.bodyType = val;
emit('change');
}
</script>

View File

@ -38,12 +38,12 @@
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const defaultParamItem = ref<EnableKeyValueParam>({
const defaultParamItem = {
key: '',
value: '',
description: '',
enable: true,
});
};
const columns: ParamTableColumn[] = [
{

View File

@ -122,12 +122,12 @@
/>
<precondition
v-else-if="activeDebug.activeTab === RequestComposition.PRECONDITION"
v-model:params="activeDebug.children[0].preProcessorConfig.processors"
v-model:config="activeDebug.children[0].preProcessorConfig"
@change="handleActiveDebugChange"
/>
<postcondition
v-else-if="activeDebug.activeTab === RequestComposition.POST_CONDITION"
v-model:params="activeDebug.children[0].postProcessorConfig.processors"
v-model:config="activeDebug.children[0].postProcessorConfig"
:response="activeDebug.response.requestResults[0]?.responseResult.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@ -199,6 +199,7 @@
</template>
<script setup lang="ts">
import { useVModel } from '@vueuse/core';
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep, debounce } from 'lodash-es';
@ -215,7 +216,7 @@
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import { addDebug, executeDebug, getDebugDetail } from '@/api/modules/api-test/debug';
import { addDebug, executeDebug, getDebugDetail, updateDebug } from '@/api/modules/api-test/debug';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { useI18n } from '@/hooks/useI18n';
@ -244,11 +245,14 @@
const props = defineProps<{
moduleTree: ModuleTreeNode[]; //
detailLoading: boolean; //
}>();
const emit = defineEmits(['update:detailLoading', 'addDone']);
const appStore = useAppStore();
const { t } = useI18n();
const loading = useVModel(props, 'detailLoading', emit);
const initDefaultId = `debug-${Date.now()}`;
const activeRequestTab = ref<string | number>(initDefaultId);
const defaultBodyParams: ExecuteBody = {
@ -313,7 +317,7 @@
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
username: '',
userName: '',
password: '',
},
children: [
@ -365,7 +369,10 @@
}
function handleActiveDebugChange() {
activeDebug.value.unSaved = true;
if (!loading.value) {
// change
activeDebug.value.unSaved = true;
}
}
function addDebugTab(defaultProps?: Partial<TabItem>) {
@ -373,6 +380,7 @@
debugTabs.value.push({
...cloneDeep(defaultDebugParams),
id,
isNew: !defaultProps?.id, // tabidid
...defaultProps,
});
activeRequestTab.value = defaultProps?.id || id;
@ -593,18 +601,30 @@
function makeRequestParams() {
const polymorphicName = protocolOptions.value.find((e) => e.value === activeDebug.value.protocol)?.polymorphicName; //
let requestParams;
if (isHttpProtocol.value) {
requestParams = {
authConfig: activeDebug.value.authConfig,
body: { ...activeDebug.value.body, binaryBody: undefined },
headers: activeDebug.value.headers,
body: {
...activeDebug.value.body,
binaryBody: undefined,
formDataBody: {
formValues: activeDebug.value.body.formDataBody.formValues.filter(
(e, i) => i !== activeDebug.value.body.formDataBody.formValues.length - 1
), //
},
wwwFormBody: {
formValues: activeDebug.value.body.wwwFormBody.formValues.filter(
(e, i) => i !== activeDebug.value.body.wwwFormBody.formValues.length - 1
), //
},
}, // TODO:binaryBody
headers: activeDebug.value.headers.filter((e, i) => i !== activeDebug.value.headers.length - 1), //
method: activeDebug.value.method,
otherConfig: activeDebug.value.otherConfig,
path: activeDebug.value.url,
query: activeDebug.value.query,
rest: activeDebug.value.rest,
query: activeDebug.value.query.filter((e, i) => i !== activeDebug.value.query.length - 1), //
rest: activeDebug.value.rest.filter((e, i) => i !== activeDebug.value.rest.length - 1), //
url: activeDebug.value.url,
polymorphicName,
};
@ -631,14 +651,8 @@
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
postProcessorConfig: activeDebug.value.children[0].postProcessorConfig,
preProcessorConfig: activeDebug.value.children[0].preProcessorConfig,
},
],
},
@ -709,7 +723,7 @@
await fApi.value?.validate();
}
saveModalForm.value = {
name: '',
name: activeDebug.value.name || '',
path: activeDebug.value.url || '',
moduleId: 'root',
};
@ -734,19 +748,36 @@
if (!errors) {
try {
saveLoading.value = true;
await addDebug({
...makeRequestParams(),
...saveModalForm.value,
protocol: activeDebug.value.protocol,
method: isHttpProtocol.value ? activeDebug.value.method : activeDebug.value.protocol,
uploadFileIds: [],
linkFileIds: [],
});
if (activeDebug.value.isNew) {
//
await addDebug({
...makeRequestParams(),
...saveModalForm.value,
protocol: activeDebug.value.protocol,
method: isHttpProtocol.value ? activeDebug.value.method : activeDebug.value.protocol,
uploadFileIds: [],
linkFileIds: [],
});
} else {
await updateDebug({
...makeRequestParams(),
...saveModalForm.value,
protocol: activeDebug.value.protocol,
method: isHttpProtocol.value ? activeDebug.value.method : activeDebug.value.protocol,
uploadFileIds: [],
linkFileIds: [],
deleteFileIds: [], // TODO:
unLinkRefIds: [], // TODO:
});
}
saveLoading.value = false;
saveModalVisible.value = false;
done(true);
activeDebug.value.unSaved = false;
Message.success(t('common.saveSuccess'));
activeDebug.value.name = saveModalForm.value.name;
activeDebug.value.label = saveModalForm.value.name;
emit('addDone');
Message.success(activeDebug.value.isNew ? t('common.saveSuccess') : t('common.updateSuccess'));
} catch (error) {
saveLoading.value = false;
}
@ -755,10 +786,15 @@
done(false);
}
const apiDetailLoading = ref(false);
async function openApiTab(apiInfo: ModuleTreeNode) {
const isLoadedTabIndex = debugTabs.value.findIndex((e) => e.id === apiInfo.id);
if (isLoadedTabIndex > -1) {
// tabtab
activeRequestTab.value = apiInfo.id;
return;
}
try {
apiDetailLoading.value = true;
loading.value = true;
const res = await getDebugDetail(apiInfo.id);
addDebugTab({
label: apiInfo.name,
@ -766,12 +802,16 @@
response: cloneDeep(defaultResponse),
...res.request,
url: res.path,
name: res.name, // requestnamenull
});
nextTick(() => {
// loading
loading.value = false;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
apiDetailLoading.value = false;
loading.value = false;
}
}

View File

@ -1,6 +1,6 @@
<template>
<condition
v-model:list="postConditions"
v-model:list="innerConfig.processors"
:condition-types="[RequestConditionProcessor.SCRIPT]"
add-text="apiTestDebug.postCondition"
:response="props.response"
@ -8,7 +8,7 @@
@change="emit('change')"
>
<!-- <template #titleRight>
<a-switch v-model:model-value="openGlobalPostCondition" size="small" type="line"></a-switch>
<a-switch v-model:model-value="innerConfig.enableGlobal" size="small" type="line"></a-switch>
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPostCondition') }}</div>
<a-tooltip :content="t('apiTestDebug.openGlobalPostConditionTip')" position="left">
<icon-question-circle
@ -25,13 +25,13 @@
import condition from '@/views/api-test/components/condition/index.vue';
import { ExecuteConditionProcessor } from '@/models/apiTest/debug';
import { ExecuteConditionConfig, ExecuteConditionProcessor } from '@/models/apiTest/debug';
import { RequestConditionProcessor } from '@/enums/apiEnum';
// import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: ExecuteConditionProcessor[];
config: ExecuteConditionConfig;
secondBoxHeight?: number;
layout: 'horizontal' | 'vertical';
response?: string; //
@ -42,9 +42,7 @@
}>();
// const { t } = useI18n();
//
// const openGlobalPostCondition = ref(false);
const postConditions = useVModel(props, 'params', emit);
const innerConfig = useVModel(props, 'config', emit);
const heightUsed = computed(() => {
if (props.layout === 'horizontal') {
return 428;

View File

@ -1,12 +1,12 @@
<template>
<condition
v-model:list="preconditions"
:condition-types="['SCRIPT', 'TIME_WAITING']"
v-model:list="innerConfig.processors"
:condition-types="[RequestConditionProcessor.SCRIPT, RequestConditionProcessor.TIME_WAITING]"
add-text="apiTestDebug.precondition"
@change="emit('change')"
>
<!-- <template #titleRight>
<a-switch v-model:model-value="openGlobalPrecondition" size="small" type="line"></a-switch>
<a-switch v-model:model-value="innerConfig.enableGlobal" size="small" type="line"></a-switch>
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPrecondition') }}</div>
<a-tooltip :content="t('apiTestDebug.openGlobalPreconditionTip')" position="left">
<icon-question-circle
@ -23,22 +23,21 @@
import condition from '@/views/api-test/components/condition/index.vue';
import { ExecuteConditionProcessor } from '@/models/apiTest/debug';
import { ExecuteConditionConfig } from '@/models/apiTest/debug';
import { RequestConditionProcessor } from '@/enums/apiEnum';
// import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: ExecuteConditionProcessor[];
config: ExecuteConditionConfig;
}>();
const emit = defineEmits<{
(e: 'update:params', params: ExecuteConditionProcessor[]): void;
(e: 'update:config', params: ExecuteConditionConfig): void;
(e: 'change'): void;
}>();
// const { t } = useI18n();
//
// const openGlobalPrecondition = ref(false);
const preconditions = useVModel(props, 'params', emit);
const innerConfig = useVModel(props, 'config', emit);
</script>
<style lang="less" scoped></style>

View File

@ -9,13 +9,14 @@
/>
</a-tooltip>
</div>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" />
<batchAddKeyVal :params="innerParams" :default-param-item="defaultParamItem" @apply="handleBatchParamApply" />
</div>
<paramTable
v-model:params="innerParams"
:columns="columns"
:height-used="heightUsed"
:scroll="{ minWidth: 1160 }"
:default-param-item="defaultParamItem"
@change="handleParamTableChange"
/>
</template>
@ -44,23 +45,34 @@
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const defaultParamItem = {
key: '',
value: '',
paramType: RequestParamsType.STRING,
description: '',
required: false,
maxLength: undefined,
minLength: undefined,
encode: false,
enable: true,
};
const columns: ParamTableColumn[] = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
dataIndex: 'key',
slotName: 'key',
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
dataIndex: 'paramType',
slotName: 'paramType',
hasRequired: true,
typeOptions: Object.keys(RequestParamsType)
.filter((key) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(key as RequestParamsType))
.map((key) => ({
label: RequestParamsType[key],
value: key,
typeOptions: Object.values(RequestParamsType)
.filter((val) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(val))
.map((val) => ({
label: val,
value: val,
})),
width: 120,
},
@ -81,6 +93,7 @@
dataIndex: 'encode',
slotName: 'encode',
titleSlotName: 'encodeTitle',
width: 80,
},
{
title: 'apiTestDebug.desc',

View File

@ -9,13 +9,14 @@
/>
</a-tooltip>
</div>
<batchAddKeyVal :params="innerParams" @apply="handleBatchParamApply" />
<batchAddKeyVal :params="innerParams" :default-param-item="defaultParamItem" @apply="handleBatchParamApply" />
</div>
<paramTable
v-model:params="innerParams"
:columns="columns"
:height-used="heightUsed"
:scroll="{ minWidth: 1160 }"
:default-param-item="defaultParamItem"
@change="handleParamTableChange"
/>
</template>
@ -44,23 +45,34 @@
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const defaultParamItem = {
key: '',
value: '',
paramType: RequestParamsType.STRING,
description: '',
required: false,
maxLength: undefined,
minLength: undefined,
encode: false,
enable: true,
};
const columns: ParamTableColumn[] = [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
slotName: 'name',
dataIndex: 'key',
slotName: 'key',
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'type',
slotName: 'type',
dataIndex: 'paramType',
slotName: 'paramType',
hasRequired: true,
typeOptions: Object.keys(RequestParamsType)
.filter((key) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(key as RequestParamsType))
.map((key) => ({
label: RequestParamsType[key],
value: key,
typeOptions: Object.values(RequestParamsType)
.filter((val) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(val as RequestParamsType))
.map((val) => ({
label: val,
value: val,
})),
width: 120,
},
@ -81,6 +93,7 @@
dataIndex: 'encode',
slotName: 'encode',
titleSlotName: 'encodeTitle',
width: 80,
},
{
title: 'apiTestDebug.desc',

View File

@ -42,8 +42,17 @@
/>
</a-form-item>
<a-form-item :label="t('apiTestDebug.redirect')">
<a-radio v-model:model-value="settingForm.followRedirects">{{ t('apiTestDebug.follow') }}</a-radio>
<a-radio v-model:model-value="settingForm.autoRedirects" class="ml-[24px]">
<a-radio
v-model:model-value="settingForm.followRedirects"
@change="(val) => handleFollowRedirectsChange(val as boolean)"
>
{{ t('apiTestDebug.follow') }}
</a-radio>
<a-radio
v-model:model-value="settingForm.autoRedirects"
class="ml-[24px]"
@change="val => handleAutoRedirectsChange(val as boolean)"
>
{{ t('apiTestDebug.auto') }}
</a-radio>
</a-form-item>
@ -76,6 +85,18 @@
},
{ deep: true }
);
function handleFollowRedirectsChange(val: boolean) {
if (val) {
settingForm.value.autoRedirects = false;
}
}
function handleAutoRedirectsChange(val: boolean) {
if (val) {
settingForm.value.followRedirects = false;
}
}
</script>
<style lang="less" scoped></style>

View File

@ -337,6 +337,7 @@
defineExpose({
initModules,
initModuleCount,
});
</script>

View File

@ -1,9 +1,11 @@
<template>
<MsCard simple no-content-padding>
<!-- TODO:接口请求超过5S以上可展示取消请求按钮避免用户过长等待 -->
<MsCard :loading="loading" simple no-content-padding>
<MsSplitBox :size="0.25" :max="0.5">
<template #first>
<div class="p-[24px]">
<moduleTree
ref="moduleTreeRef"
@init="(val) => (folderTree = val)"
@new-api="newApi"
@click-api-node="handleApiNodeClick"
@ -13,7 +15,12 @@
</template>
<template #second>
<div class="flex h-full flex-col">
<debug ref="debugRef" :module-tree="folderTree" />
<debug
ref="debugRef"
v-model:detail-loading="loading"
:module-tree="folderTree"
@add-done="handleDebugAddDone"
/>
</div>
</template>
</MsSplitBox>
@ -62,14 +69,16 @@
import { parseCurlScript } from '@/utils';
import { ModuleTreeNode } from '@/models/common';
import { RequestContentTypeEnum } from '@/enums/apiEnum';
import { RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
const { t } = useI18n();
const moduleTreeRef = ref<InstanceType<typeof moduleTree>>();
const debugRef = ref<InstanceType<typeof debug>>();
const folderTree = ref<ModuleTreeNode[]>([]);
const importDrawerVisible = ref(false);
const curlCode = ref('');
const loading = ref(false);
function newApi() {
debugRef.value?.addDebugTab();
@ -79,31 +88,20 @@
const { url, headers, queryParameters } = parseCurlScript(curlCode.value);
debugRef.value?.addDebugTab({
url,
headerParams: headers?.map((e) => ({
required: false,
type: 'string',
min: undefined,
max: undefined,
headers: headers?.map((e) => ({
contentType: RequestContentTypeEnum.TEXT,
tag: [],
desc: '',
encode: false,
enable: false,
mustContain: false,
description: '',
enable: true,
...e,
})),
value: '',
queryParams: queryParameters?.map((e) => ({
query: queryParameters?.map((e) => ({
paramType: RequestParamsType.STRING,
description: '',
required: false,
type: 'string',
min: undefined,
max: undefined,
contentType: RequestContentTypeEnum.TEXT,
tag: [],
desc: '',
maxLength: undefined,
minLength: undefined,
encode: false,
enable: false,
mustContain: false,
enable: true,
...e,
})),
});
@ -114,6 +112,11 @@
function handleApiNodeClick(node: ModuleTreeNode) {
debugRef.value?.openApiTab(node);
}
function handleDebugAddDone() {
moduleTreeRef.value?.initModules();
moduleTreeRef.value?.initModuleCount();
}
</script>
<style lang="less" scoped></style>