feat(接口测试): 接口调试-执行+保存接口

This commit is contained in:
baiqi 2024-02-07 18:00:19 +08:00 committed by Craftsman
parent 31c9897746
commit ac9a0e5c78
44 changed files with 1450 additions and 787 deletions

View File

@ -1,29 +0,0 @@
import Footer from '@/components/pure/footer/index.vue';
import { mount } from '@vue/test-utils';
import { describe, expect, test } from 'vitest';
describe('Footer', () => {
test('renders the correct text', () => {
const wrapper = mount(Footer, {
props: {
text: 'Custom Text',
},
});
expect(wrapper.text()).toBe('Custom Text');
});
test('renders the default text if no prop is provided', () => {
const wrapper = mount(Footer);
expect(wrapper.text()).toBe('MeterSphere');
});
test('applies the correct styles', () => {
const wrapper = mount(Footer);
expect(wrapper.find('.footer').exists()).toBe(true);
expect(wrapper.classes()).toContain('footer');
});
});

View File

@ -1,99 +0,0 @@
import { nextTick } from 'vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable';
import { getTableList } from '@/api/modules/api-test/index';
import { mount } from '@vue/test-utils';
import { describe, expect, test } from 'vitest';
const columns: MsTableColumn = [
{
title: 'ID',
dataIndex: 'num',
},
{
title: '接口名称',
dataIndex: 'name',
},
{
title: '请求类型',
dataIndex: 'method',
},
{
title: '责任人',
dataIndex: 'username',
},
{
title: '路径',
dataIndex: 'path',
},
{
title: '标签',
dataIndex: 'tags',
},
{
title: '更新时间',
slotName: 'updataTime',
},
{
title: '用例数',
dataIndex: 'caseTotal',
},
{
title: '用例状态',
dataIndex: 'caseStatus',
},
{
title: '用例通过率',
dataIndex: 'casePassingRate',
},
{
title: '接口状态',
dataIndex: 'status',
},
{
title: '创建时间',
slotName: 'createTime',
},
{
title: '描述',
dataIndex: 'description',
},
{
title: '操作',
slotName: 'action',
fixed: 'right',
width: 200,
},
];
describe('MS-Table', () => {
test('init table with useTable', async () => {
const { propsRes, propsEvent, loadList, setProps } = useTable(getTableList, {
columns,
scroll: { y: 750, x: 2000 },
selectable: true,
});
const wrapper = mount(MsBaseTable as any, {
vOn: propsEvent,
vBind: propsRes,
});
loadList();
await nextTick();
let content = wrapper.find('.arco-table-td-content').element.innerHTML;
expect(propsRes.value.data.length).toBe(20);
expect(content).toBe('e7bd7179-d63a-43a5-1a65-218473ee69ca');
setProps({});
loadList();
await nextTick();
content = wrapper.find('.arco-table-td-content').element.innerHTML;
expect(content).toBe('937be890-79bb-1b68-e03e-7d37a8b0a607');
});
});

View File

@ -1,121 +0,0 @@
import logo from '@/assets/svg/logo.svg';
import {
isArray,
isBlob,
isEmptyObject,
isExist,
isFile,
isFunction,
isNull,
isNumber,
isObject,
isRegExp,
isString,
isUndefined,
} from '@/utils/is';
import { describe, expect, test } from 'vitest';
describe('Is tool', () => {
test('isArray', () => {
const res = isArray([]);
expect(res).toBe(true);
const res2 = isArray('');
expect(res2).toBe(false);
});
test('isObject', () => {
const res = isObject({ a: 'a' });
expect(res).toBe(true);
const res2 = isObject([]);
expect(res2).toBe(false);
});
test('isEmptyObject', () => {
const res = isEmptyObject({});
expect(res).toBe(true);
const res2 = isEmptyObject({ a: 'a' });
expect(res2).toBe(false);
const res3 = isEmptyObject([]);
expect(res3).toBe(false);
});
test('isExist', () => {
const res = isExist(0);
expect(res).toBe(true);
const res2 = isExist(null);
expect(res2).toBe(false);
});
test('isFunction', () => {
const res = isFunction(() => ({}));
expect(res).toBe(true);
const res2 = isFunction({});
expect(res2).toBe(false);
});
test('isNull', () => {
const res = isNull(null);
expect(res).toBe(true);
const res2 = isNull(undefined);
expect(res2).toBe(false);
});
test('isUndefined', () => {
const res = isUndefined(undefined);
expect(res).toBe(true);
const res2 = isUndefined(null);
expect(res2).toBe(false);
});
test('isNumber', () => {
const res = isNumber(0);
expect(res).toBe(true);
const res2 = isNumber(null);
expect(res2).toBe(false);
});
test('isString', () => {
const res = isString('');
expect(res).toBe(true);
const res2 = isString(0);
expect(res2).toBe(false);
});
test('isRegExp', () => {
const res = isRegExp(/^a/);
expect(res).toBe(true);
const res2 = isRegExp('');
expect(res2).toBe(false);
});
test('isFile', () => {
const file = new File([logo], 'logo.svg');
const res = isFile(file);
expect(res).toBe(true);
const res2 = isFile({});
expect(res2).toBe(false);
});
test('isBlob', () => {
const blob = new Blob();
const res = isBlob(blob);
expect(res).toBe(true);
const res2 = isBlob(logo);
expect(res2).toBe(false);
});
});

View File

@ -47,6 +47,7 @@
"@tiptap/suggestion": "^2.1.13",
"@tiptap/vue-3": "^2.1.13",
"@types/color": "^3.0.4",
"@types/node": "^20.11.16",
"@vueuse/core": "^10.4.1",
"@xmldom/xmldom": "^0.8.10",
"ace-builds": "^1.24.2",

View File

@ -0,0 +1,66 @@
import MSR from '@/api/http/index';
import {
AddApiDebugUrl,
AddDebugModuleUrl,
DeleteDebugModuleUrl,
ExecuteApiDebugUrl,
GetDebugModuleCountUrl,
GetDebugModulesUrl,
MoveDebugModuleUrl,
UpdateApiDebugUrl,
UpdateDebugModuleUrl,
} from '@/api/requrls/api-test/debug';
import {
AddDebugModuleParams,
ExecuteRequestParams,
SaveDebugParams,
UpdateDebugModule,
UpdateDebugParams,
} from '@/models/apiTest/debug';
import { ModuleTreeNode, MoveModules } from '@/models/common';
// 获取模块树
export function getDebugModules() {
return MSR.get<ModuleTreeNode[]>({ url: GetDebugModulesUrl });
}
// 删除模块
export function deleteDebugModule(deleteId: string) {
return MSR.get({ url: DeleteDebugModuleUrl, params: deleteId });
}
// 添加模块
export function addDebugModule(data: AddDebugModuleParams) {
return MSR.post({ url: AddDebugModuleUrl, data });
}
// 移动模块
export function moveDebugModule(data: MoveModules) {
return MSR.post({ url: MoveDebugModuleUrl, data });
}
// 更新模块
export function updateDebugModule(data: UpdateDebugModule) {
return MSR.post({ url: UpdateDebugModuleUrl, data });
}
// 模块数量统计
export function getDebugModuleCount(data: { keyword: string }) {
return MSR.post({ url: GetDebugModuleCountUrl, data });
}
// 执行调试
export function executeDebug(data: ExecuteRequestParams) {
return MSR.post<ExecuteRequestParams>({ url: ExecuteApiDebugUrl, data });
}
// 新增调试
export function addDebug(data: SaveDebugParams) {
return MSR.post({ url: AddApiDebugUrl, data });
}
// 更新调试
export function updateDebug(data: UpdateDebugParams) {
return MSR.post({ url: UpdateApiDebugUrl, data });
}

View File

@ -1,16 +0,0 @@
import MSR from '@/api/http/index';
import { GetApiTestList, GetApiTestListUrl } from '@/api/requrls/api-test';
import { CommonList, TableQueryParams } from '@/models/common';
export function getTableList(params: TableQueryParams) {
const { current, pageSize, sort, filter, keyword } = params;
return MSR.post<CommonList<any>>({
url: GetApiTestList,
data: { current, pageSize, sort, filter, keyword, projectId: 'test-project-id' },
});
}
export function getlist() {
return MSR.get<CommonList<any>>({ url: GetApiTestListUrl });
}

View File

@ -0,0 +1,19 @@
import MSR from '@/api/http/index';
import { GetPluginOptionsUrl, GetPluginScriptUrl, GetProtocolListUrl } from '@/api/requrls/api-test/management';
import { GetPluginOptionsParams, PluginOption, ProtocolItem } from '@/models/apiTest/common';
// 获取协议列表
export function getProtocolList(organizationId: string) {
return MSR.get<ProtocolItem[]>({ url: GetProtocolListUrl, params: organizationId });
}
// 获取插件表单选项
export function getPluginOptions(data: GetPluginOptionsParams) {
return MSR.get<PluginOption[]>({ url: GetPluginOptionsUrl, data });
}
// 获取插件配置
export function getPluginScript(pluginId: string) {
return MSR.get({ url: GetPluginScriptUrl, params: pluginId });
}

View File

@ -0,0 +1,9 @@
export const ExecuteApiDebugUrl = '/api/debug/debug'; // 执行调试
export const AddApiDebugUrl = '/api/debug/add'; // 新增调试
export const UpdateApiDebugUrl = '/api/debug/update'; // 更新调试
export const UpdateDebugModuleUrl = '/api/debug/module/update'; // 更新模块
export const MoveDebugModuleUrl = '/api/debug/module/move'; // 移动模块
export const GetDebugModuleCountUrl = '/api/debug/module/count'; // 模块统计数量
export const AddDebugModuleUrl = '/api/debug/module/add'; // 添加模块
export const GetDebugModulesUrl = '/api/debug/module/tree'; // 查询模块树
export const DeleteDebugModuleUrl = '/api/debug/module/delete'; // 删除模块

View File

@ -0,0 +1,3 @@
export const GetProtocolListUrl = '/api/test/protocol'; // 获取协议列表
export const GetPluginOptionsUrl = '/api/test/plugin/form/option'; // 获取插件表单选项
export const GetPluginScriptUrl = '/api/test/plugin/script'; // 获取插件表单选项

View File

@ -369,6 +369,7 @@
}
}
.arco-checkbox {
padding-left: 0;
.arco-checkbox-icon {
border: 1px solid var(--color-text-input-border);
}

View File

@ -6,7 +6,7 @@
v-model:model-value="currentOrg"
:options="orgOptions"
:loading="orgLoading"
class="w-[300px]"
class="!w-[300px]"
@change="handleOrgChange"
/>
</div>

View File

@ -49,6 +49,8 @@
</template>
<script setup lang="ts">
import { defineModel } from 'vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTag from '../ms-tag/ms-tag.vue';
import FilterForm from './FilterForm.vue';

View File

@ -91,7 +91,7 @@
});
//
editor.onDidBlurEditorText(() => {
editor.onDidChangeModelContent(() => {
const value = editor.getValue(); //
emit('update:modelValue', value);
emit('change', value);
@ -219,6 +219,13 @@
}
}
function format() {
if (editor) {
//
editor.getAction('editor.action.formatDocument')?.run();
}
}
watch(
() => props.modelValue,
(newValue) => {
@ -274,6 +281,7 @@
insertContent,
undo,
redo,
format,
};
},
});

View File

@ -22,6 +22,8 @@
v-if="attrs.selectable && props.selectedKeys"
:width="props.firstColumnWidth || 60"
fixed="left"
cell-class="arco-table-operation"
body-cell-class="arco-table-operation"
>
<template #title>
<SelectALL
@ -322,11 +324,11 @@
// -
const selectTotal = computed(() => {
const { selectorStatus } = props;
if (!attrs.showPagination) {
// total
return (attrs.data as MsTableDataItem<TableData>[]).length;
}
if (selectorStatus === SelectAllEnum.CURRENT) {
if (!attrs.showPagination) {
// total
return (attrs.data as MsTableDataItem<TableData>[]).length;
}
const { pageSize, total } = attrs.msPagination as MsPaginationI;
if (pageSize > total) {
return total;
@ -606,6 +608,11 @@
background-color: var(--color-text-n9);
}
}
:deep(.arco-table-operation) {
.arco-table-td-content {
@apply justify-center;
}
}
:deep(.ms-table-select-all) {
.dropdown-icon {
background: none !important;

View File

@ -465,5 +465,6 @@ export default function useTableProps<T>(
getSelectedCount,
resetSelector,
getTableQueryParams,
setTableSelected,
};
}

View File

@ -1,12 +1,23 @@
import { ConditionType } from '@/models/apiTest/debug';
import { RequestBodyFormat, RequestConditionProcessor } from '@/enums/apiEnum';
// 条件操作类型
export type ConditionTypeNameMap = Record<ConditionType, string>;
export const conditionTypeNameMap = {
script: 'apiTestDebug.script',
sql: 'apiTestDebug.sql',
waitTime: 'apiTestDebug.waitTime',
extract: 'apiTestDebug.extractParameter',
[RequestConditionProcessor.SCRIPT]: 'apiTestDebug.script',
[RequestConditionProcessor.SQL]: 'apiTestDebug.sql',
[RequestConditionProcessor.TIME_WAITING]: 'apiTestDebug.waitTime',
[RequestConditionProcessor.EXTRACT]: 'apiTestDebug.extractParameter',
};
// 代码字符集
export const codeCharset = ['UTF-8', 'UTF-16', 'GBK', 'GB2312', 'ISO-8859-1', 'Shift_JIS', 'ASCII', 'BIG5', 'KOI8-R'];
// 请求体类型显示映射
export const requestBodyTypeMap = {
[RequestBodyFormat.FORM_DATA]: 'form-data',
[RequestBodyFormat.WWW_FORM]: 'x-www-form-urlencoded',
[RequestBodyFormat.RAW]: 'raw',
[RequestBodyFormat.BINARY]: 'binary',
[RequestBodyFormat.JSON]: 'json',
[RequestBodyFormat.XML]: 'xml',
[RequestBodyFormat.NONE]: 'none',
};

View File

@ -11,6 +11,7 @@ export enum RequestMethods {
}
// 接口组成部分
export enum RequestComposition {
PLUGIN = 'PLUGIN',
HEADER = 'HEADER',
BODY = 'BODY',
QUERY = 'QUERY',
@ -23,13 +24,13 @@ export enum RequestComposition {
}
// 接口请求体格式
export enum RequestBodyFormat {
NONE = 'none',
FORM_DATA = 'form-data',
X_WWW_FORM_URLENCODED = 'x-www-form-urlencoded',
JSON = 'json',
XML = 'xml',
RAW = 'raw',
BINARY = 'binary',
NONE = 'NONE',
FORM_DATA = 'FORM_DATA',
WWW_FORM = 'WWW_FORM',
JSON = 'JSON',
XML = 'XML',
RAW = 'RAW',
BINARY = 'BINARY',
}
// 接口响应体格式
export enum RequestContentTypeEnum {
@ -63,3 +64,124 @@ export enum RequestDefinitionStatus {
export enum RequestImportFormat {
SWAGGER = 'SWAGGER',
}
// 接口认证设置类型
export enum RequestAuthType {
BASIC = 'BASIC',
DIGEST = 'DIGEST',
NONE = 'NONE',
}
// 接口参数表格的参数类型
export enum RequestParamsType {
STRING = 'string',
INTEGER = 'integer',
NUMBER = 'number',
ARRAY = 'array',
JSON = 'json',
FILE = 'file',
}
// 接口断言类型
export enum ResponseAssertionType {
RESPONSE_CODE = 'RESPONSE_CODE',
RESPONSE_HEADER = 'RESPONSE_HEADER',
RESPONSE_BODY = 'RESPONSE_BODY',
RESPONSE_TIME = 'RESPONSE_TIME',
SCRIPT = 'SCRIPT',
VARIABLE = 'VARIABLE',
}
// 接口断言-响应体断言类型
export enum ResponseBodyAssertionType {
DOCUMENT = 'DOCUMENT',
JSON_PATH = 'JSON_PATH',
REGEX = 'REGEX', // 正则表达式
XPATH = 'XPATH',
}
// 接口断言-响应体断言-文档类型
export enum ResponseBodyAssertionDocumentType {
JSON = 'JSON',
XML = 'XML',
}
// 接口断言-响应体断言-文档断言类型
export enum ResponseBodyDocumentAssertionType {
ARRAY = 'array',
BOOLEAN = 'boolean',
INTEGER = 'integer',
NUMBER = 'number',
STRING = 'string',
}
// 接口断言-响应体断言-Xpath断言类型
export enum ResponseBodyXPathAssertionFormat {
HTML = 'HTML',
XML = 'XML',
}
// 接口断言-断言匹配条件
export enum RequestAssertionCondition {
CONTAINS = 'CONTAINS', // 包含
EMPTY = 'EMPTY', // 为空
END_WITH = 'END_WITH', // 以 xx 结尾
EQUALS = 'EQUALS', // 等于
GT = 'GT', // 大于
GT_OR_EQUALS = 'GT_OR_EQUALS', // 大于等于
LENGTH_EQUALS = 'LENGTH_EQUALS', // 长度等于
LENGTH_GT = 'LENGTH_GT', // 长度大于
LENGTH_GT_OR_EQUALS = 'LENGTH_GT_OR_EQUALS', // 长度大于等于
LENGTH_LT = 'LENGTH_LT', // 长度小于
LENGTH_LT_OR_EQUALS = 'LENGTH_LT_OR_EQUALS', // 长度小于等于
LENGTH_NOT_EQUALS = 'LENGTH_NOT_EQUALS', // 长度不等于
LT = 'LT', // 小于
LT_OR_EQUALS = 'LT_OR_EQUALS', // 小于等于
NOT_CONTAINS = 'NOT_CONTAINS', // 不包含
NOT_EMPTY = 'NOT_EMPTY', // 不为空
NOT_EQUALS = 'NOT_EQUALS', // 不等于
REGEX = 'REGEX', // 正则表达式
START_WITH = 'START_WITH', // 以 xx 开头
UNCHECKED = 'UNCHECKED', // 不校验
}
// 接口请求-前后置条件-处理器类型
export enum RequestConditionProcessor {
SCRIPT = 'SCRIPT', // 脚本操作
SQL = 'SQL', // SQL操作
TIME_WAITING = 'TIME_WAITING', // 等待时间
EXTRACT = 'EXTRACT', // 参数提取
}
// 接口请求-前后置条件-脚本处理器语言
export enum RequestConditionScriptLanguage {
BEANSHELL = 'BEANSHELL', // Beanshell
BEANSHELL_JSR233 = 'BEANSHELL_JSR233', // Beanshell JSR233
GROOVY = 'GROOVY', // Groovy
JAVASCRIPT = 'JAVASCRIPT', // JavaScript
PYTHON = 'PYTHON', // Python
}
// 接口请求-参数提取-环境类型
export enum RequestExtractEnvType {
ENVIRONMENT = 'ENVIRONMENT', // 环境参数
GLOBAL = 'GLOBAL', // 全局参数
TEMPORARY = 'TEMPORARY', // 临时参数
}
// 接口请求-参数提取-表达式类型
export enum RequestExtractExpressionEnum {
REGEX = 'REGEX', // 正则表达式
JSON_PATH = 'JSON_PATH', // JSONPath
X_PATH = 'X_PATH', // Xpath
}
// 接口请求-参数提取-表达式匹配规则类型
export enum RequestExtractExpressionRuleType {
EXPRESSION = 'EXPRESSION', // 匹配表达式
GROUP = 'GROUP', // 匹配组
}
// 接口请求-参数提取-提取范围
export enum RequestExtractScope {
BODY = 'BODY',
BODY_AS_DOCUMENT = 'BODY_AS_DOCUMENT',
UNESCAPED_BODY = 'UNESCAPED_BODY',
REQUEST_HEADERS = 'REQUEST_HEADERS',
RESPONSE_CODE = 'RESPONSE_CODE',
RESPONSE_HEADERS = 'RESPONSE_HEADERS',
RESPONSE_MESSAGE = 'RESPONSE_MESSAGE',
URL = 'URL',
}
// 接口请求-参数提取-表达式匹配结果规则类型
export enum RequestExtractResultMatchingRule {
ALL = 'ALL', // 全部匹配
RANDOM = 'RANDOM', // 随机匹配
SPECIFIC = 'SPECIFIC', // 指定匹配
}

View File

@ -0,0 +1,18 @@
// 获取插件表单选项参数
export interface GetPluginOptionsParams {
orgId: string;
pluginId: string;
optionMethod: string;
queryParam: Record<string, any>;
}
// 插件表单选项子项
export interface PluginOption {
text: string;
value: string;
}
// 协议列表子项
export interface ProtocolItem {
protocol: string;
polymorphicName: string;
pluginId: string;
}

View File

@ -1,25 +1,324 @@
import {
RequestAssertionCondition,
RequestAuthType,
RequestBodyFormat,
RequestConditionProcessor,
RequestConditionScriptLanguage,
RequestContentTypeEnum,
RequestExtractEnvType,
RequestExtractExpressionEnum,
RequestExtractExpressionRuleType,
RequestExtractResultMatchingRule,
RequestExtractScope,
RequestMethods,
RequestParamsType,
ResponseAssertionType,
ResponseBodyAssertionDocumentType,
ResponseBodyAssertionType,
ResponseBodyDocumentAssertionType,
ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum';
// 条件操作类型
export type ConditionType = 'script' | 'sql' | 'waitTime' | 'extract';
// 表达式类型
export type ExpressionType = 'regular' | 'JSONPath' | 'XPath';
// 表达式配置
export interface ExpressionConfig {
expression: string;
expressionType?: ExpressionType;
regexpMatchRule?: 'expression' | 'group'; // 正则表达式匹配规则
resultMatchRule?: 'random' | 'specify' | 'all'; // 结果匹配规则
specifyMatchNum?: number; // 指定匹配下标
xmlMatchContentType?: 'xml' | 'html'; // 响应内容格式
}
export type ConditionType = keyof typeof RequestConditionProcessor;
// 断言-匹配条件规则
export type RequestAssertionConditionType = keyof typeof RequestAssertionCondition;
// 前后置条件-脚本语言类型
export type RequestConditionScriptLanguageType = keyof typeof RequestConditionScriptLanguage;
// 响应时间信息
export interface ResponseTiming {
ready: number;
socketInit: number;
dnsQuery: number;
tcpHandshake: number;
sslHandshake: number;
waitingTTFB: number;
downloadContent: number;
deal: number;
total: number;
dnsLookupTime: number;
tcpHandshakeTime: number;
sslHandshakeTime: number;
socketInitTime: number;
latency: number;
downloadTime: number;
transferStartTime: number;
responseTime: number;
}
// key-value参数信息
export interface KeyValueParam {
key: string;
value: string;
}
// 接口请求公共参数集合信息
export interface ExecuteRequestCommonParam {
encode: boolean; // 是否编码
maxLength: number;
minLength: number;
paramType: keyof typeof RequestParamsType; // 参数类型
required: boolean;
description: string;
enable: boolean; // 参数是否启用
}
// 接口请求form-data、x-www-form-urlencoded参数集合信息
export type ExecuteRequestFormBodyFormValue = ExecuteRequestCommonParam & {
files?: {
fileId: string;
fileName: string;
}[];
contentType?: keyof typeof RequestContentTypeEnum & string;
};
export interface ExecuteRequestFormBody {
formValues: ExecuteRequestFormBodyFormValue[];
}
// 接口请求binary-body参数集合信息
export interface ExecuteBinaryBody {
description: string;
file?: {
fileId: string;
fileName: string;
};
}
// 接口请求json-body参数集合信息
export interface ExecuteJsonBody {
enableJsonSchema?: boolean;
enableTransition?: boolean;
jsonSchema?: string;
jsonValue: string;
}
// 接口请求-带开启关闭的参数集合信息
export interface EnableKeyValueParam extends KeyValueParam {
description: string;
enable: boolean; // 参数是否启用
}
// 执行请求配置
export interface ExecuteOtherConfig {
autoRedirects: boolean; // 是否自动重定向 默认 false
certificateAlias: string; // 证书别名
connectTimeout: number; // 连接超时时间
followRedirects: boolean; // 是否跟随重定向 默认 false
responseTimeout: number; // 响应超时时间
}
// 断言-断言公共信息
export interface ResponseAssertionCommon {
name: string; // 断言名称
enable: boolean; // 是否启用断言
assertionType: keyof typeof ResponseAssertionType; // 断言类型
}
// 断言-断言列表泛型
export interface ResponseAssertionGenerics<T> {
assertions: T[];
responseFormat?: keyof typeof ResponseBodyXPathAssertionFormat;
}
// 断言-响应头断言子项
export interface ResponseHeaderAssertionItem {
condition: RequestAssertionConditionType;
enable: boolean;
expectedValue: string;
header: string; // 响应头
}
// 断言-状态码断言
export type ResponseCodeAssertion = Pick<ResponseHeaderAssertionItem, 'condition' | 'expectedValue'>;
// 断言-文档断言-JSON断言\XML断言
export interface ResponseDocumentAssertionElement {
id?: string;
arrayVerification: boolean; // 是否组内验证
children: ResponseDocumentAssertionElement[];
condition: RequestAssertionConditionType;
expectedResult: Record<string, any>; // 匹配值 即预期结果
include: boolean; // 是否必含
paramName: string; // 参数名
type: keyof typeof ResponseBodyDocumentAssertionType; // 断言类型
typeVerification: boolean; // 是否类型验证
}
// 断言-文档断言
export interface ResponseDocumentAssertion {
enable: boolean; // 是否启用
documentType: keyof typeof ResponseBodyAssertionDocumentType; // 文档类型
followApiId: string; // 跟随定义的apiId 传空为不跟随接口定义
jsonAssertion: ResponseDocumentAssertionElement;
xmlAssertion: ResponseDocumentAssertionElement;
}
// 断言-断言列表的断言子项
export interface ResponseAssertionItem {
condition: RequestAssertionConditionType;
expectedValue: string;
expression: string;
enable?: boolean;
}
// 断言-JSONPath断言子项
export type ResponseJSONPathAssertionItem = ResponseAssertionItem;
// 断言-正则断言子项
export type ResponseRegexAssertionItem = Pick<ResponseAssertionItem, 'expression'>;
// 断言-Xpath断言子项
export type ResponseXPathAssertionItem = Pick<ResponseAssertionItem, 'expression' | 'expectedValue'>;
// 脚本公共配置
export interface ScriptCommonConfig {
enableCommonScript: boolean; // 是否启用公共脚本
script: string; // 脚本内容
scriptId: string; // 脚本id
scriptLanguage: RequestConditionScriptLanguageType; // 脚本语言
params: KeyValueParam[]; // 公共脚本参数
}
// 断言-响应体断言
export interface ResponseBodyAssertion {
assertionBodyType: keyof typeof ResponseBodyAssertionType; // 断言类型
documentAssertion: ResponseDocumentAssertion; // 文档断言
jsonPathAssertion: ResponseAssertionGenerics<ResponseJSONPathAssertionItem>; // JSONPath断言
regexAssertion: ResponseAssertionGenerics<ResponseRegexAssertionItem>; // 正则断言
xpathAssertion: ResponseAssertionGenerics<ResponseXPathAssertionItem>; // XPath断言
}
// 断言-响应时间断言
export type ResponseTimeAssertion = Pick<ResponseAssertionItem, 'expectedValue'>;
// 断言-脚本断言
export type ResponseScriptAssertion = ScriptCommonConfig;
// 断言-变量断言
export interface ResponseVariableAssertion {
variableAssertionItems: ResponseAssertionItem[];
}
// 执行请求-前后置条件处理器
export interface ExecuteConditionProcessorCommon {
enable: boolean; // 是否启用
name: string; // 请求名称
processorType: keyof typeof RequestConditionProcessor;
}
// 执行请求-前后置条件-脚本处理器
export type ScriptProcessor = ScriptCommonConfig;
// 执行请求-前后置条件-SQL脚本处理器
export interface SQLProcessor {
dataSourceId: string; // 数据源ID
environmentId: string; // 环境ID
queryTimeout: number; // 超时时间
resultVariable: string; // 按结果存储时的结果变量
script: string; // 脚本内容
variableNames: string; // 按列存储时的变量名集合,多个列可以使用逗号分隔
variables: EnableKeyValueParam[]; // 变量列表
extractParams: KeyValueParam[]; // 提取参数列表
}
// 执行请求-前后置条件-等待时间处理器
export interface TimeWaitingProcessor {
delay: number; // 等待时间 单位:毫秒
}
// 表达式类型
export type ExpressionType = keyof typeof RequestExtractExpressionEnum;
// 表达式配置
export interface ExpressionCommonConfig {
enable: boolean; // 是否启用
expression: string;
extractType: ExpressionType; // 表达式类型
variableName: string;
variableType: keyof typeof RequestExtractEnvType;
resultMatchingRule: keyof typeof RequestExtractResultMatchingRule; // 结果匹配规则
resultMatchingRuleNum: number; // 匹配第几条结果
}
// 正则提取配置
export interface RegexExtract extends ExpressionCommonConfig {
expressionMatchingRule: keyof typeof RequestExtractExpressionRuleType; // 正则表达式匹配规则
extractScope: keyof typeof RequestExtractScope; // 正则提取范围
}
// JSONPath提取配置
export type JSONPathExtract = ExpressionCommonConfig;
// XPath提取配置
export interface XPathExtract extends ExpressionCommonConfig {
responseFormat: keyof typeof ResponseBodyXPathAssertionFormat; // 响应格式
}
// 执行请求-前后置条件-参数提取处理器
export interface ExtractProcessor {
extractors: (RegexExtract | JSONPathExtract | XPathExtract)[];
}
// 执行请求-前后置条件配置
export type ExecuteConditionProcessor = ExecuteConditionProcessorCommon &
(ScriptProcessor | SQLProcessor | TimeWaitingProcessor | ExtractProcessor);
export interface ExecuteConditionConfig {
enableGlobal: boolean; // 是否启用全局前置 默认为 true
processors: ExecuteConditionProcessor[];
}
// 执行请求-断言配置子项
export type ExecuteAssertionItem = ResponseAssertionCommon &
ResponseCodeAssertion &
ResponseAssertionGenerics<ResponseHeaderAssertionItem> &
ResponseBodyAssertion &
ResponseTimeAssertion &
ResponseScriptAssertion &
ResponseVariableAssertion;
// 执行请求-断言配置
export interface ExecuteAssertionConfig {
enableGlobal: boolean; // 是否启用全局断言
assertions: ExecuteAssertionItem[];
}
// 执行请求-共用配置子项
export interface ExecuteCommonChild {
polymorphicName: 'MsCommonElement'; // 协议多态名称写死MsCommonElement
assertionConfig: ExecuteAssertionConfig;
postProcessorConfig: ExecuteConditionConfig; // 后置处理器配置
preProcessorConfig: ExecuteConditionConfig; // 前置处理器配置
}
// 执行请求-认证配置
export interface ExecuteAuthConfig {
authType: keyof typeof RequestAuthType;
password: string;
username: string;
}
// 执行请求- body 配置-文本格式的 body
export interface ExecuteValueBody {
value: string;
}
// 执行请求- body 配置
export interface ExecuteBody {
bodyType: keyof typeof RequestBodyFormat;
binaryBody: ExecuteBinaryBody;
formDataBody: ExecuteRequestFormBody;
jsonBody: ExecuteJsonBody;
rawBody: ExecuteValueBody;
wwwFormBody: ExecuteRequestFormBody;
xmlBody: ExecuteValueBody;
}
// 执行HTTP请求入参
export interface ExecuteHTTPRequestFullParams {
authConfig: ExecuteAuthConfig;
body: ExecuteBody;
headers: EnableKeyValueParam[];
method: keyof typeof RequestMethods;
otherConfig: ExecuteOtherConfig;
path: string;
query: ExecuteRequestCommonParam[];
rest: ExecuteRequestCommonParam[];
url: string;
polymorphicName: string; // 协议多态名称
children: ExecuteCommonChild[]; // 协议共有的子项配置
}
// 执行插件请求入参
export interface ExecutePluginRequestParams {
[key: string]: any; // key-value形式的插件参数
polymorphicName: string; // 协议多态名称
children: ExecuteCommonChild[]; // 协议共有的子项配置
}
// 执行接口调试入参
export interface ExecuteRequestParams {
id?: string;
reportId: string;
environmentId: string;
tempFileIds: string[];
request: ExecuteHTTPRequestFullParams | ExecutePluginRequestParams;
projectId: string;
}
// 保存接口调试入参
export interface SaveDebugParams {
name: string;
protocol: string;
method: keyof typeof RequestMethods;
path: string;
projectId: string;
moduleId: string;
request: ExecuteHTTPRequestFullParams | ExecutePluginRequestParams;
uploadFileIds: string[];
linkFileIds: string[];
}
// 更新接口调试入参
export interface UpdateDebugParams extends SaveDebugParams {
id: string;
deleteFileIds: string[];
unLinkRefIds: string[];
}
// 更新模块入参
export interface UpdateDebugModule {
id: string;
name: string;
}
// 添加模块入参
export interface AddDebugModuleParams {
projectId: string;
name: string;
parentId: string;
}

View File

@ -1,7 +1,7 @@
<template>
<div class="condition-content">
<!-- 脚本操作 -->
<template v-if="condition.type === 'script'">
<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>
@ -128,7 +128,7 @@
</div>
</template>
<!-- SQL操作 -->
<template v-else-if="condition.type === 'sql'">
<template v-else-if="condition.type === RequestConditionProcessor.SQL">
<div class="mb-[16px]">
<div class="mb-[8px] text-[var(--color-text-1)]">{{ t('common.desc') }}</div>
<a-input
@ -205,7 +205,7 @@
</div>
</template>
<!-- 等待时间 -->
<div v-else-if="condition.type === 'waitTime'">
<div v-else-if="condition.type === RequestConditionProcessor.TIME_WAITING">
<div class="mb-[8px] flex items-center">
{{ t('apiTestDebug.waitTime') }}
<div class="text-[var(--color-text-4)]">(ms)</div>
@ -213,7 +213,7 @@
<a-input-number v-model:model-value="condition.time" mode="button" :step="100" :min="0" class="w-[160px]" />
</div>
<!-- 提取参数 -->
<div v-else-if="condition.type === 'extract'">
<div v-else-if="condition.type === RequestConditionProcessor.EXTRACT">
<paramTable
ref="extractParamsTableRef"
v-model:params="condition.extractParams"
@ -224,7 +224,7 @@
:response="props.response"
:height-used="(props.heightUsed || 0) + 62"
@change="handleExtractParamTableChange"
@more-action-select="handleExtractParamMoreActionSelect"
@more-action-select="(e,r)=> handleExtractParamMoreActionSelect(e,r as ExpressionConfig)"
>
<template #expression="{ record }">
<a-popover
@ -320,7 +320,17 @@
import { useI18n } from '@/hooks/useI18n';
import { ExpressionConfig } from '@/models/apiTest/debug';
import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
import {
RequestConditionProcessor,
RequestExtractEnvType,
RequestExtractExpressionEnum,
RequestExtractResultMatchingRule,
RequestExtractScope,
ResponseBodyXPathAssertionFormat,
} from '@/enums/apiEnum';
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
const props = defineProps<{
data: Record<string, any>;
@ -490,8 +500,8 @@ org.apache.http.client.method . . . '' at line number 2
},
{
title: 'apiTestDebug.mode',
dataIndex: 'expressionType',
slotName: 'expressionType',
dataIndex: 'extractType',
slotName: 'extractType',
typeOptions: [
{
label: t('apiTestDebug.regular'),
@ -510,8 +520,8 @@ org.apache.http.client.method . . . '' at line number 2
},
{
title: 'apiTestDebug.range',
dataIndex: 'range',
slotName: 'range',
dataIndex: 'extractScope',
slotName: 'extractScope',
typeOptions: [
{
label: 'Body',
@ -581,22 +591,23 @@ org.apache.http.client.method . . . '' at line number 2
}
const extractParamsTableRef = ref<InstanceType<typeof paramTable>>();
const defaultExtractParamItem: Record<string, any> = {
name: '',
type: 'temp',
range: 'body',
const defaultExtractParamItem: ExpressionConfig = {
enable: true,
variableName: '',
variableType: RequestExtractEnvType.TEMPORARY,
extractScope: RequestExtractScope.BODY,
expression: '',
expressionType: 'regular',
extractType: RequestExtractExpressionEnum.REGEX,
regexpMatchRule: 'expression',
resultMatchRule: 'random',
specifyMatchNum: 1,
xmlMatchContentType: 'xml',
resultMatchingRule: RequestExtractResultMatchingRule.RANDOM,
resultMatchingRuleNum: 1,
responseFormat: ResponseBodyXPathAssertionFormat.XML,
moreSettingPopoverVisible: false,
};
const fastExtractionVisible = ref(false);
const activeRecord = ref<any>({ ...defaultExtractParamItem }); //
const activeRecord = ref({ ...defaultExtractParamItem }); //
function showFastExtraction(record: Record<string, any>) {
function showFastExtraction(record: ExpressionConfig) {
activeRecord.value = { ...record };
fastExtractionVisible.value = true;
}
@ -608,7 +619,7 @@ org.apache.http.client.method . . . '' at line number 2
/**
* 处理提取参数表格更多操作
*/
function handleExtractParamMoreActionSelect(event: ActionsItem, record: Record<string, any>) {
function handleExtractParamMoreActionSelect(event: ActionsItem, record: ExpressionConfig) {
activeRecord.value = { ...record };
if (event.eventTag === 'copy') {
emit('copy');
@ -620,7 +631,7 @@ org.apache.http.client.method . . . '' at line number 2
/**
* 提取参数表格-应用更多设置
*/
function applyMoreSetting(record: Record<string, any>) {
function applyMoreSetting(record: ExpressionConfig) {
condition.value.extractParams = condition.value.extractParams.map((e) => {
if (e.id === activeRecord.value.id) {
record.moreSettingPopoverVisible = false;
@ -637,7 +648,7 @@ org.apache.http.client.method . . . '' at line number 2
/**
* 提取参数表格-保存快速提取的配置
*/
function handleFastExtractionApply(config: ExpressionConfig) {
function handleFastExtractionApply(config: RegexExtract | JSONPathExtract | XPathExtract) {
condition.value.extractParams = condition.value.extractParams.map((e) => {
if (e.id === activeRecord.value.id) {
return {

View File

@ -17,7 +17,7 @@
<slot name="titleRight"></slot>
</div>
</div>
<div v-show="data.length > 0" class="flex h-[calc(100%-110px)] gap-[8px]">
<div v-show="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"
@ -47,6 +47,7 @@
import { useI18n } from '@/hooks/useI18n';
import { ConditionType } from '@/models/apiTest/debug';
import { RequestConditionProcessor } from '@/enums/apiEnum';
const props = defineProps<{
list: Array<Record<string, any>>;
@ -113,10 +114,10 @@ org.apache.http.client.method . . . '' at line number 2
function addPrecondition(value: string | number | Record<string, any> | undefined) {
const id = new Date().getTime();
switch (value) {
case 'script':
case RequestConditionProcessor.SCRIPT:
data.value.push({
id,
type: 'script',
type: RequestConditionProcessor.SCRIPT,
name: t('apiTestDebug.preconditionScriptName'),
scriptType: 'manual',
enable: true,
@ -127,10 +128,10 @@ org.apache.http.client.method . . . '' at line number 2
},
});
break;
case 'sql':
case RequestConditionProcessor.SQL:
data.value.push({
id,
type: 'sql',
type: RequestConditionProcessor.SQL,
desc: '',
enable: true,
sqlSource: {
@ -141,18 +142,18 @@ org.apache.http.client.method . . . '' at line number 2
},
});
break;
case 'waitTime':
case RequestConditionProcessor.TIME_WAITING:
data.value.push({
id,
type: 'waitTime',
type: RequestConditionProcessor.TIME_WAITING,
enable: true,
time: 1000,
});
break;
case 'extract':
case RequestConditionProcessor.EXTRACT:
data.value.push({
id,
type: 'extract',
type: RequestConditionProcessor.EXTRACT,
enable: true,
extractParams: [],
});

View File

@ -151,16 +151,16 @@
import { useI18n } from '@/hooks/useI18n';
import { matchXMLWithXPath } from '@/utils/xpath';
import { ExpressionConfig } from '@/models/apiTest/debug';
import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
const props = defineProps<{
visible: boolean;
config: ExpressionConfig;
config: (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
response?: string; //
}>();
const emit = defineEmits<{
(e: 'update:visible', value: boolean): void;
(e: 'apply', config: ExpressionConfig): void;
(e: 'apply', config: (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>): void;
}>();
const { t } = useI18n();

View File

@ -96,7 +96,9 @@
import { useI18n } from '@/hooks/useI18n';
import { ExpressionConfig } from '@/models/apiTest/debug';
import { JSONPathExtract, RegexExtract, XPathExtract } from '@/models/apiTest/debug';
export type ExpressionConfig = (RegexExtract | JSONPathExtract | XPathExtract) & Record<string, any>;
const props = defineProps<{
config: ExpressionConfig;

View File

@ -58,7 +58,7 @@
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
@click="record.required = !record.required"
@click="toggleRequired(record)"
>
<div>*</div>
</MsButton>
@ -186,11 +186,7 @@
:list="getMoreActionList(columnConfig.moreAction, record)"
@select="(e) => handleMoreActionSelect(e, record)"
/>
<a-trigger
v-if="columnConfig.format && columnConfig.format !== RequestBodyFormat.X_WWW_FORM_URLENCODED"
trigger="click"
position="br"
>
<a-trigger v-if="columnConfig.format === RequestBodyFormat.FORM_DATA" trigger="click" position="br">
<MsButton type="icon" class="mr-[8px]"><icon-more /></MsButton>
<template #content>
<div class="content-type-trigger-content">
@ -296,6 +292,7 @@
</template>
<script async setup lang="ts">
import { useVModel } from '@vueuse/core';
import { isEqual } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
@ -313,7 +310,7 @@
import { useI18n } from '@/hooks/useI18n';
import useTableStore from '@/hooks/useTableStore';
import { RequestBodyFormat, RequestContentTypeEnum } from '@/enums/apiEnum';
import { RequestBodyFormat, RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
interface Param {
@ -340,11 +337,12 @@
typeTitleTooltip?: string; // type tooltip
hasEnable?: boolean; // operation enable
moreAction?: ActionsItem[]; // operation
format?: RequestBodyFormat | 'query' | 'rest'; // operation
format?: RequestBodyFormat; // operation
};
const props = withDefaults(
defineProps<{
selectedKeys?: string[];
params: any[];
defaultParamItem?: Partial<Param>; //
columns: ParamTableColumn[];
@ -372,7 +370,7 @@
defaultParamItem: () => ({
required: false,
name: '',
type: 'string',
type: RequestParamsType.STRING,
value: '',
min: undefined,
max: undefined,
@ -386,6 +384,7 @@
}
);
const emit = defineEmits<{
(e: 'update:selectedKeys', value: string[]): void;
(e: 'change', data: any[], isInit?: boolean): void; //
(e: 'moreActionSelect', event: ActionsItem, record: Record<string, any>): void;
(e: 'projectChange', projectId: string): void;
@ -393,6 +392,8 @@
const { t } = useI18n();
const innerSelectedKeys = useVModel(props, 'selectedKeys', emit);
const tableStore = useTableStore();
async function initColumns() {
if (props.showSetting && props.tableKey) {
@ -402,6 +403,7 @@
initColumns();
const { propsRes, propsEvent } = useTable(() => Promise.resolve([]), {
firstColumnWidth: 24,
tableKey: props.showSetting ? props.tableKey : undefined,
scroll: props.scroll,
heightUsed: props.heightUsed,
@ -412,8 +414,16 @@
disabled: props.disabled,
showSelectorAll: props.showSelectorAll,
isSimpleSetting: props.isSimpleSetting,
showPagination: false,
});
watch(
() => propsRes.value.selectedKeys,
(val) => {
innerSelectedKeys.value = Array.from(val);
}
);
watch(
() => props.heightUsed,
(val) => {
@ -447,10 +457,12 @@
? isEqual(lastData, props.defaultParamItem)
: isEqual(lastData[key], props.defaultParamItem[key]);
if (isForce || (val !== '' && !isNotChange)) {
const id = new Date().getTime().toString();
propsRes.value.data.push({
id: new Date().getTime(),
id,
...props.defaultParamItem,
} as any);
propsRes.value.selectedKeys.add(id);
emit('change', propsRes.value.data);
}
}
@ -467,12 +479,14 @@
addTableLine();
}
} else {
const id = new Date().getTime().toString();
propsRes.value.data = [
{
id: new Date().getTime(), // id props.defaultParamItem id
id, // id props.defaultParamItem id
...props.defaultParamItem,
},
] as any[];
propsRes.value.selectedKeys.add(id);
emit('change', propsRes.value.data, true);
}
},
@ -481,6 +495,11 @@
}
);
function toggleRequired(record: Record<string, any>) {
record.required = !record.required;
emit('change', propsRes.value.data);
}
const showQuickInputParam = ref(false);
const activeQuickInputRecord = ref<any>({});
const quickInputParamValue = ref('');
@ -500,7 +519,6 @@
activeQuickInputRecord.value.value = quickInputParamValue.value;
showQuickInputParam.value = false;
clearQuickInputParam();
addTableLine(quickInputParamValue.value, 'value', true);
emit('change', propsRes.value.data);
}
@ -526,7 +544,6 @@
activeQuickInputRecord.value.desc = quickInputDescValue.value;
showQuickInputDesc.value = false;
clearQuickInputDesc();
addTableLine(quickInputDescValue.value, 'desc', true);
emit('change', propsRes.value.data);
}
@ -605,6 +622,11 @@
padding: 12px 2px;
}
}
:deep(.arco-table-col-fixed-right) {
.arco-table-cell-align-left {
padding: 16px;
}
}
:deep(.param-input:not(.arco-input-focus, .arco-select-view-focus)) {
&:not(:hover) {
border-color: transparent !important;

View File

@ -51,7 +51,7 @@
import { ref, watch } from 'vue';
import { Message } from '@arco-design/web-vue';
import { addReviewModule, updateReviewModule } from '@/api/modules/case-management/caseReview';
import { addDebugModule, updateDebugModule } from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
@ -123,7 +123,7 @@
loading.value = true;
if (props.mode === 'add') {
//
await addReviewModule({
await addDebugModule({
projectId: appStore.currentProjectId,
parentId: props.parentId || '',
name: form.value.field,
@ -132,7 +132,7 @@
emit('addFinish', form.value.field);
} else if (props.mode === 'rename') {
//
await updateReviewModule({
await updateDebugModule({
id: props.nodeId || '',
name: form.value.field,
});

View File

@ -2,12 +2,11 @@
<div class="flex w-full gap-[8px] rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]">
<div class="text-item-wrapper">
<div class="light-item">{{ t('apiTestDebug.responseStage') }}</div>
<div class="light-item">{{ t('apiTestDebug.ready') }}</div>
<div class="normal-item">{{ t('apiTestDebug.socketInit') }}</div>
<div class="normal-item">{{ t('apiTestDebug.dnsQuery') }}</div>
<div class="normal-item">{{ t('apiTestDebug.tcpHandshake') }}</div>
<div class="normal-item">{{ t('apiTestDebug.sslHandshake') }}</div>
<div class="normal-item">{{ t('apiTestDebug.waitingTTFB') }}</div>
<div class="normal-item">{{ t('apiTestDebug.socketInit') }}</div>
<!-- <div class="normal-item">{{ t('apiTestDebug.waitingTTFB') }}</div> -->
<div class="normal-item">{{ t('apiTestDebug.downloadContent') }}</div>
<div class="light-item">{{ t('apiTestDebug.deal') }}</div>
<div class="total-item">{{ t('apiTestDebug.total') }}</div>
@ -29,15 +28,14 @@
<a-divider direction="vertical" margin="0" />
<div class="text-item-wrapper--right">
<div class="light-item">{{ t('apiTestDebug.time') }}</div>
<div class="light-item">{{ props.responseTiming.ready }} ms</div>
<div class="normal-item">{{ props.responseTiming.socketInit }} ms</div>
<div class="normal-item">{{ props.responseTiming.dnsQuery }} ms</div>
<div class="normal-item">{{ props.responseTiming.tcpHandshake }} ms</div>
<div class="normal-item">{{ props.responseTiming.sslHandshake }} ms</div>
<div class="normal-item">{{ props.responseTiming.waitingTTFB }} ms</div>
<div class="normal-item">{{ props.responseTiming.downloadContent }} ms</div>
<div class="light-item">{{ props.responseTiming.deal }} ms</div>
<div class="total-item">{{ props.responseTiming.total }} ms</div>
<div class="normal-item">{{ props.responseTiming.dnsLookupTime }} ms</div>
<div class="normal-item">{{ props.responseTiming.tcpHandshakeTime }} ms</div>
<div class="normal-item">{{ props.responseTiming.sslHandshakeTime }} ms</div>
<div class="normal-item">{{ props.responseTiming.socketInitTime }} ms</div>
<!-- <div class="normal-item">{{ props.responseTiming.latency }} ms</div> -->
<div class="normal-item">{{ props.responseTiming.downloadTime }} ms</div>
<div class="light-item">{{ props.responseTiming.transferStartTime }} ms</div>
<div class="total-item">{{ props.responseTiming.responseTime }} ms</div>
</div>
</div>
</template>
@ -58,13 +56,16 @@
const keys = Object.keys(props.responseTiming).filter((key) => key !== 'total');
let preLinesTotalLeft = 0;
keys.forEach((key, index) => {
const itemWidth = (props.responseTiming[key] / props.responseTiming.total) * 100;
arr.push({
key,
width: `${itemWidth}%`,
left: index !== 0 ? `${preLinesTotalLeft}%` : '',
});
preLinesTotalLeft += itemWidth;
if (key !== 'responseTime' && key !== 'latency') {
// 100%
const itemWidth = (props.responseTiming[key] / props.responseTiming.responseTime) * 100;
arr.push({
key,
width: `${itemWidth}%`,
left: index !== 0 ? `${preLinesTotalLeft}%` : '',
});
preLinesTotalLeft += itemWidth;
}
});
return arr;
});

View File

@ -3,14 +3,14 @@
<div class="rounded-[var(--border-radius-small)] border border-[var(--color-text-n8)] p-[16px]">
<div class="mb-[8px]">{{ t('apiTestDebug.authType') }}</div>
<a-radio-group v-model:model-value="authForm.authType" class="mb-[16px]" @change="authTypeChange">
<a-radio value="none">No Auth</a-radio>
<a-radio value="basic">Basic Auth</a-radio>
<a-radio value="digest">Digest Auth</a-radio>
<a-radio :value="RequestAuthType.NONE">No Auth</a-radio>
<a-radio :value="RequestAuthType.BASIC">Basic Auth</a-radio>
<a-radio :value="RequestAuthType.DIGEST">Digest Auth</a-radio>
</a-radio-group>
<a-form v-if="authForm.authType !== 'none'" ref="authFormRef" :model="authForm" layout="vertical">
<a-form-item :label="t('apiTestDebug.account')">
<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.account"
v-model:model-value="authForm.username"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
:max-length="255"
@ -34,17 +34,15 @@
import { useI18n } from '@/hooks/useI18n';
interface AuthForm {
authType: string;
account: string;
password: string;
}
import { ExecuteAuthConfig } from '@/models/apiTest/debug';
import { RequestAuthType } from '@/enums/apiEnum';
const props = defineProps<{
params: AuthForm;
params: ExecuteAuthConfig;
}>();
const emit = defineEmits<{
(e: 'update:params', val: AuthForm): void;
(e: 'change', val: AuthForm): void;
(e: 'update:params', val: ExecuteAuthConfig): void;
(e: 'change', val: ExecuteAuthConfig): void;
}>();
const { t } = useI18n();
@ -61,7 +59,7 @@
function authTypeChange(val: string | number | boolean) {
if (val === 'none') {
authForm.value.account = '';
authForm.value.username = '';
authForm.value.password = '';
}
}

View File

@ -47,7 +47,7 @@
import { useI18n } from '@/hooks/useI18n';
import { RequestContentTypeEnum } from '@/enums/apiEnum';
import { RequestContentTypeEnum, RequestParamsType } from '@/enums/apiEnum';
const props = defineProps<{
params: Record<string, any>[];
@ -90,7 +90,7 @@
name: name.trim(),
value: value?.trim(),
required: false,
type: 'string',
type: RequestParamsType.STRING,
min: undefined,
max: undefined,
contentType: RequestContentTypeEnum.TEXT,

View File

@ -1,33 +1,42 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<div class="font-medium">{{ t('apiTestDebug.body') }}</div>
<div class="flex items-center gap-[16px]">
<batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" />
<a-radio-group v-model:model-value="format" type="button" size="small" @change="formatChange">
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">{{ item }}</a-radio>
</a-radio-group>
</div>
<batchAddKeyVal v-if="showParamTable" :params="currentTableParams" @apply="handleBatchParamApply" />
<a-radio-group v-model:model-value="bodyType" type="button" size="small" @change="formatChange">
<a-radio v-for="item of RequestBodyFormat" :key="item" :value="item">{{ requestBodyTypeMap[item] }}</a-radio>
</a-radio-group>
</div>
<div
v-if="format === RequestBodyFormat.NONE"
v-if="bodyType === RequestBodyFormat.NONE"
class="flex h-[100px] items-center justify-center rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] text-[var(--color-text-4)]"
>
{{ t('apiTestDebug.noneBody') }}
</div>
<paramTable
v-else-if="showParamTable"
v-else-if="bodyType === RequestBodyFormat.FORM_DATA"
v-model:params="currentTableParams"
:scroll="{ minWidth: 1160 }"
:columns="columns"
:height-used="heightUsed"
@change="handleParamTableChange"
/>
<div v-else-if="format === RequestBodyFormat.BINARY">
<paramTable
v-else-if="bodyType === RequestBodyFormat.WWW_FORM"
v-model:params="currentTableParams"
:scroll="{ minWidth: 1160 }"
:columns="columns"
:height-used="heightUsed"
@change="handleParamTableChange"
/>
<div v-else-if="bodyType === RequestBodyFormat.BINARY">
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
<a-input v-model:model-value="innerParams.binaryDesc" :placeholder="t('common.desc')" :max-length="255" />
<a-input
v-model:model-value="innerParams.binaryBody.description"
:placeholder="t('common.desc')"
:max-length="255"
/>
</div>
<div class="flex items-center">
<a-switch v-model:model-value="innerParams.binarySend" class="mr-[8px]" size="small" type="line"></a-switch>
<!-- <a-switch v-model:model-value="innerParams.binarySend" class="mr-[8px]" size="small" type="line"></a-switch> -->
<span>{{ t('apiTestDebug.sendAsMainText') }}</span>
<a-tooltip position="right">
<template #content>
@ -72,23 +81,14 @@
import paramTable, { type ParamTableColumn } from '../../../components/paramTable.vue';
import batchAddKeyVal from './batchAddKeyVal.vue';
import { requestBodyTypeMap } from '@/config/apiTest';
import { useI18n } from '@/hooks/useI18n';
import { RequestBodyFormat } from '@/enums/apiEnum';
import { ExecuteBody } from '@/models/apiTest/debug';
import { RequestBodyFormat, RequestParamsType } from '@/enums/apiEnum';
export interface BodyParams {
format: RequestBodyFormat;
formData: Record<string, any>[];
formUrlEncode: Record<string, any>[];
json: string;
xml: string;
binary: string;
binaryDesc: string;
binarySend: boolean;
raw: string;
}
const props = defineProps<{
params: BodyParams;
params: ExecuteBody;
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
}>();
@ -100,8 +100,9 @@
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const bodyType = ref(RequestBodyFormat.NONE);
const columns: ParamTableColumn[] = [
const columns = computed<ParamTableColumn[]>(() => [
{
title: 'apiTestDebug.paramName',
dataIndex: 'name',
@ -112,32 +113,10 @@
dataIndex: 'type',
slotName: 'type',
hasRequired: true,
typeOptions: [
{
label: 'string',
value: 'string',
},
{
label: 'integer',
value: 'integer',
},
{
label: 'number',
value: 'number',
},
{
label: 'array',
value: 'array',
},
{
label: 'json',
value: 'json',
},
{
label: 'file',
value: 'file',
},
],
typeOptions: Object.keys(RequestParamsType).map((key) => ({
label: RequestParamsType[key],
value: key,
})),
width: 120,
},
{
@ -168,9 +147,10 @@
title: '',
slotName: 'operation',
fixed: 'right',
width: 50,
format: bodyType.value,
width: bodyType.value === RequestBodyFormat.FORM_DATA ? 90 : 50,
},
];
]);
const heightUsed = ref<number | undefined>(undefined);
@ -196,61 +176,60 @@
}
);
const format = ref(RequestBodyFormat.NONE);
const showParamTable = computed(() => {
// FORM_DATAX_WWW_FORM_URLENCODED
return [RequestBodyFormat.FORM_DATA, RequestBodyFormat.X_WWW_FORM_URLENCODED].includes(format.value);
return [RequestBodyFormat.FORM_DATA, RequestBodyFormat.WWW_FORM].includes(bodyType.value);
});
//
const currentTableParams = computed({
get() {
if (format.value === RequestBodyFormat.FORM_DATA) {
return innerParams.value.formData;
if (bodyType.value === RequestBodyFormat.FORM_DATA) {
return innerParams.value.formDataBody.formValues;
}
return innerParams.value.formUrlEncode;
return innerParams.value.wwwFormBody.formValues;
},
set(val) {
if (format.value === RequestBodyFormat.FORM_DATA) {
innerParams.value.formData = val;
if (bodyType.value === RequestBodyFormat.FORM_DATA) {
innerParams.value.formDataBody.formValues = val;
} else {
innerParams.value.formUrlEncode = val;
innerParams.value.wwwFormBody.formValues = val;
}
},
});
//
const currentBodyCode = computed({
get() {
if (format.value === RequestBodyFormat.JSON) {
return innerParams.value.json;
if (bodyType.value === RequestBodyFormat.JSON) {
return innerParams.value.jsonBody.jsonValue;
}
if (format.value === RequestBodyFormat.XML) {
return innerParams.value.xml;
if (bodyType.value === RequestBodyFormat.XML) {
return innerParams.value.xmlBody.value;
}
return innerParams.value.raw;
return innerParams.value.rawBody.value;
},
set(val) {
if (format.value === RequestBodyFormat.JSON) {
innerParams.value.json = val;
} else if (format.value === RequestBodyFormat.XML) {
innerParams.value.xml = val;
if (bodyType.value === RequestBodyFormat.JSON) {
innerParams.value.jsonBody.jsonValue = val;
} else if (bodyType.value === RequestBodyFormat.XML) {
innerParams.value.xmlBody.value = val;
} else {
innerParams.value.raw = val;
innerParams.value.rawBody.value = val;
}
},
});
//
const currentCodeLanguage = computed(() => {
if (format.value === RequestBodyFormat.JSON) {
if (bodyType.value === RequestBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (format.value === RequestBodyFormat.XML) {
if (bodyType.value === RequestBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
function formatChange() {
console.log('formatChange', format.value);
console.log('formatChange', bodyType.value);
}
/**

View File

@ -5,6 +5,7 @@
</div>
<paramTable
v-model:params="innerParams"
v-model:selected-keys="selectedKeys"
:columns="columns"
:height-used="heightUsed"
:scroll="scroll"
@ -21,19 +22,24 @@
import { useI18n } from '@/hooks/useI18n';
import { EnableKeyValueParam } from '@/models/apiTest/debug';
const props = defineProps<{
params: any[];
selectedKeys?: string[];
params: EnableKeyValueParam[];
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'update:selectedKeys', value: string[]): void;
(e: 'update:params', value: EnableKeyValueParam[]): void;
(e: 'change'): void; //
}>();
const { t } = useI18n();
const innerParams = useVModel(props, 'params', emit);
const selectedKeys = useVModel(props, 'selectedKeys', emit);
const columns: ParamTableColumn[] = [
{

View File

@ -11,7 +11,7 @@
@more-action-select="handleMoreActionSelect"
>
<template #label="{ tab }">
<apiMethodName :method="tab.method" class="mr-[4px]" />
<apiMethodName v-if="isHttpProtocol" :method="tab.method" class="mr-[4px]" />
{{ tab.label }}
</template>
</MsEditableTab>
@ -20,12 +20,13 @@
<div class="mb-[4px] flex items-center justify-between">
<div class="flex flex-1">
<a-select
v-model:model-value="activeDebug.moduleProtocol"
:options="moduleProtocolOptions"
v-model:model-value="activeDebug.protocol"
:options="protocolOptions"
:loading="protocolLoading"
class="mr-[4px] w-[90px]"
@change="handleActiveDebugChange"
@change="(val) => handleActiveDebugProtocolChange(val as string)"
/>
<a-input-group class="flex-1">
<a-input-group v-if="isHttpProtocol" class="flex-1">
<apiMethodSelect
v-model:model-value="activeDebug.method"
class="w-[140px]"
@ -40,7 +41,12 @@
</a-input-group>
</div>
<div class="ml-[16px]">
<a-dropdown-button class="exec-btn">
<a-dropdown-button
:button-props="{ loading: executeLoading }"
:disabled="executeLoading"
class="exec-btn"
@click="execute"
>
{{ t('apiTestDebug.serverExec') }}
<template #icon>
<icon-down />
@ -49,7 +55,7 @@
<a-doption>{{ t('apiTestDebug.localExec') }}</a-doption>
</template>
</a-dropdown-button>
<a-button type="secondary">
<a-button type="secondary" @click="handleSaveShortcut">
<div class="flex items-center">
{{ t('common.save') }}
<div class="text-[var(--color-text-4)]">(<icon-command size="14" />+S)</div>
@ -69,62 +75,75 @@
@expand-change="handleExpandChange"
>
<template #first>
<div :class="`h-full min-w-[800px] px-[24px] pb-[16px] ${activeLayout === 'horizontal' ? ' pr-[16px]' : ''}`">
<a-tabs v-model:active-key="activeDebug.activeTab" class="no-content">
<a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" />
</a-tabs>
<a-divider margin="0" class="!mb-[16px]"></a-divider>
<debugHeader
v-if="activeDebug.activeTab === RequestComposition.HEADER"
v-model:params="activeDebug.headerParams"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugBody
v-else-if="activeDebug.activeTab === RequestComposition.BODY"
v-model:params="activeDebug.bodyParams"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugQuery
v-else-if="activeDebug.activeTab === RequestComposition.QUERY"
v-model:params="activeDebug.queryParams"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugRest
v-else-if="activeDebug.activeTab === RequestComposition.REST"
v-model:params="activeDebug.restParams"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<precondition
v-else-if="activeDebug.activeTab === RequestComposition.PRECONDITION"
v-model:params="activeDebug.preconditions"
@change="handleActiveDebugChange"
/>
<postcondition
v-else-if="activeDebug.activeTab === RequestComposition.POST_CONDITION"
v-model:params="activeDebug.postConditions"
:response="activeDebug.response.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugAuth
v-else-if="activeDebug.activeTab === RequestComposition.AUTH"
v-model:params="activeDebug.authParams"
@change="handleActiveDebugChange"
/>
<debugSetting
v-else-if="activeDebug.activeTab === RequestComposition.SETTING"
v-model:params="activeDebug.setting"
@change="handleActiveDebugChange"
/>
<div
:class="`flex h-full min-w-[800px] flex-col px-[24px] pb-[16px] ${
activeLayout === 'horizontal' ? ' pr-[16px]' : ''
}`"
>
<div>
<a-tabs v-model:active-key="activeDebug.activeTab" class="no-content">
<a-tab-pane v-for="item of contentTabList" :key="item.value" :title="item.label" />
</a-tabs>
<a-divider margin="0" class="!mb-[16px]"></a-divider>
</div>
<div class="tab-pane-container">
<template v-if="isInitPluginForm || activeDebug.activeTab === RequestComposition.PLUGIN">
<a-spin v-show="activeDebug.activeTab === RequestComposition.PLUGIN" :loading="pluginLoading">
<MsFormCreate v-model:api="fApi" :rule="currentPluginScript" :option="options" />
</a-spin>
</template>
<debugHeader
v-if="activeDebug.activeTab === RequestComposition.HEADER"
v-model:params="activeDebug.headers"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugBody
v-else-if="activeDebug.activeTab === RequestComposition.BODY"
v-model:params="activeDebug.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugQuery
v-else-if="activeDebug.activeTab === RequestComposition.QUERY"
v-model:params="activeDebug.query"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugRest
v-else-if="activeDebug.activeTab === RequestComposition.REST"
v-model:params="activeDebug.rest"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<precondition
v-else-if="activeDebug.activeTab === RequestComposition.PRECONDITION"
v-model:params="activeDebug.children[0].preProcessorConfig.processors"
@change="handleActiveDebugChange"
/>
<postcondition
v-else-if="activeDebug.activeTab === RequestComposition.POST_CONDITION"
v-model:params="activeDebug.children[0].postProcessorConfig.processors"
:response="activeDebug.response.requestResults[0]?.responseResult.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<debugAuth
v-else-if="activeDebug.activeTab === RequestComposition.AUTH"
v-model:params="activeDebug.authConfig"
@change="handleActiveDebugChange"
/>
<debugSetting
v-else-if="activeDebug.activeTab === RequestComposition.SETTING"
v-model:params="activeDebug.otherConfig"
@change="handleActiveDebugChange"
/>
</div>
</div>
</template>
<template #second>
@ -147,6 +166,7 @@
title-align="start"
body-class="!p-0"
@before-ok="handleSave"
@cancel="handleCancel"
>
<a-form ref="saveModalFormRef" :model="saveModalForm" layout="vertical">
<a-form-item
@ -158,16 +178,17 @@
<a-input v-model:model-value="saveModalForm.name" :placeholder="t('apiTestDebug.requestNamePlaceholder')" />
</a-form-item>
<a-form-item
field="url"
v-if="isHttpProtocol"
field="path"
:label="t('apiTestDebug.requestUrl')"
:rules="[{ required: true, message: t('apiTestDebug.requestUrlRequired') }]"
asterisk-position="end"
>
<a-input v-model:model-value="saveModalForm.url" :placeholder="t('apiTestDebug.commonPlaceholder')" />
<a-input v-model:model-value="saveModalForm.path" :placeholder="t('apiTestDebug.commonPlaceholder')" />
</a-form-item>
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
<a-tree-select
v-model:modelValue="saveModalForm.module"
v-model:modelValue="saveModalForm.moduleId"
:data="props.moduleTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
allow-search
@ -178,150 +199,163 @@
</template>
<script setup lang="ts">
import { FormInstance, Message } from '@arco-design/web-vue';
import { FormInstance, Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep, debounce } from 'lodash-es';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import debugAuth from './auth.vue';
import debugBody, { BodyParams } from './body.vue';
import debugHeader from './header.vue';
import postcondition from './postcondition.vue';
import precondition from './precondition.vue';
import debugQuery from './query.vue';
import response from './response.vue';
import debugRest from './rest.vue';
import debugSetting from './setting.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import { addDebug, executeDebug } 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';
import { useAppStore } from '@/store';
import { getGenerateId } from '@/utils';
import { scrollIntoView } from '@/utils/dom';
import { registerCatchSaveShortcut, removeCatchSaveShortcut } from '@/utils/event';
import { ExecuteBody, ExecuteHTTPRequestFullParams } from '@/models/apiTest/debug';
import { ModuleTreeNode } from '@/models/common';
import { RequestBodyFormat, RequestComposition, RequestMethods, ResponseComposition } from '@/enums/apiEnum';
// Http
const debugHeader = defineAsyncComponent(() => import('./header.vue'));
const debugBody = defineAsyncComponent(() => import('./body.vue'));
const debugQuery = defineAsyncComponent(() => import('./query.vue'));
const debugRest = defineAsyncComponent(() => import('./rest.vue'));
export type DebugTabParam = ExecuteHTTPRequestFullParams & TabItem & Record<string, any>;
const props = defineProps<{
module: string; //
moduleTree: ModuleTreeNode[]; //
}>();
const appStore = useAppStore();
const { t } = useI18n();
const initDefaultId = `debug-${Date.now()}`;
const activeRequestTab = ref<string | number>(initDefaultId);
const defaultBodyParams: BodyParams = {
format: RequestBodyFormat.NONE,
formData: [],
formUrlEncode: [],
json: '',
xml: '',
binary: '',
binaryDesc: '',
binarySend: false,
raw: '',
const defaultBodyParams: ExecuteBody = {
bodyType: RequestBodyFormat.NONE,
formDataBody: {
formValues: [],
},
wwwFormBody: {
formValues: [],
},
jsonBody: {
jsonValue: '',
},
xmlBody: { value: '' },
binaryBody: {
description: '',
file: undefined,
},
rawBody: { value: '' },
};
const defaultDebugParams = {
const defaultDebugParams: DebugTabParam = {
id: initDefaultId,
module: 'root',
moduleProtocol: 'http',
moduleId: 'root',
protocol: 'HTTP',
url: '',
activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSaved: false,
headerParams: [],
bodyParams: cloneDeep(defaultBodyParams),
queryParams: [],
restParams: [],
authParams: {
authType: 'none',
account: '',
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: 'NONE',
username: '',
password: '',
},
preconditions: [],
postConditions: [],
setting: {
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
redirect: 'follow',
followRedirects: false,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: {
status: 200,
headers: [],
timing: 12938,
size: 8734,
env: 'Mock',
resource: '66',
timingInfo: {
ready: 10,
socketInit: 50,
dnsQuery: 20,
tcpHandshake: 80,
sslHandshake: 40,
waitingTTFB: 30,
downloadContent: 10,
deal: 10,
total: 250,
},
extract: {
a: 'asdasd',
b: 'asdasdasd43f43',
},
console: `GET https://qa-release.fit2cloud.com/test`,
content: `请求地址:
https://qa-release.fit2cloud.com/test
请求头:
Connection: keep-alive
Content-Length: 0
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
Host: qa-release.fit2cloud.com
User-Agent: Apache-HttpClient/4.5.14 (Java/17.0.9)
Body:
POST https://qa-release.fit2cloud.com/test
POST data:
[no cookies]
`,
header: `HTTP/ 1.1 200 OK
Content-Length: 2381
Content-Type: text/html
Server: bfe
Date: Wed, 13 Dec 2023 08:53:25 GMTHTTP/ 1.1 200 OK
Content-Length: 2381
Content-Type: text/html
Server: bfe
Date: Wed, 13 Dec 2023 08:53:25 GMT`,
body: `<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
<connectionStrings>
<add name="MyDB"
connectionString="value for the deployed Web.config file"
xdt:Transform="SetAttributes" xdt:Locator="Match(name)"/>
</connectionStrings>
<a>哈哈哈哈哈哈哈</a>
<system.web>
<customErrors defaultRedirect="GenericError.htm"
mode="RemoteOnly" xdt:Transform="Replace">
<error statusCode="500" redirect="InternalError.htm"/>
</customErrors>
</system.web>
</configuration>`,
requestResults: [
{
body: '',
responseResult: {
body: '',
contentType: '',
headers: '',
dnsLookupTime: 0,
downloadTime: 0,
latency: 0,
responseCode: 0,
responseTime: 0,
responseSize: 0,
socketInitTime: 0,
tcpHandshakeTime: 0,
transferStartTime: 0,
},
},
],
console: '',
}, //
};
const debugTabs = ref<TabItem[]>([cloneDeep(defaultDebugParams)]);
const activeDebug = ref<TabItem>(debugTabs.value[0]);
const debugTabs = ref<DebugTabParam[]>([cloneDeep(defaultDebugParams)]);
const activeDebug = ref<DebugTabParam>(debugTabs.value[0]);
const isHttpProtocol = computed(() => activeDebug.value.protocol === 'HTTP');
const isInitPluginForm = ref(false); //
watch(
() => activeDebug.value.protocol,
(val) => {
if (val !== 'HTTP') {
isInitPluginForm.value = true;
}
},
{
immediate: true,
}
);
function setActiveDebug(item: TabItem) {
activeDebug.value = item;
activeDebug.value = item as DebugTabParam;
}
function handleActiveDebugChange() {
@ -332,7 +366,7 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
const id = `debug-${Date.now()}`;
debugTabs.value.push({
...cloneDeep(defaultDebugParams),
module: props.module,
moduleId: props.module,
id,
...defaultProps,
});
@ -365,7 +399,21 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
}
}
const contentTabList = [
// tabKey
const commonContentTabKey = [
RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION,
RequestComposition.ASSERTION,
];
// tab
const pluginContentTab = [
{
value: RequestComposition.PLUGIN,
label: t('apiTestDebug.pluginData'),
},
];
// Http tab
const httpContentTabList = [
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
@ -403,13 +451,82 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
label: t('apiTestDebug.setting'),
},
];
// tab
const contentTabList = computed(() =>
isHttpProtocol.value
? httpContentTabList
: [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))]
);
const protocolLoading = ref(false);
const protocolOptions = ref<SelectOptionData[]>([]);
const moduleProtocolOptions = ref([
{
label: 'HTTP',
value: 'http',
async function initProtocolList() {
try {
protocolLoading.value = true;
const res = await getProtocolList(appStore.currentOrgId);
protocolOptions.value = res.map((e) => ({
label: e.protocol,
value: e.protocol,
polymorphicName: e.polymorphicName,
pluginId: e.pluginId,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
protocolLoading.value = false;
}
}
const pluginScriptMap = ref<Record<string, any>>({}); //
const pluginLoading = ref(false);
const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[activeDebug.value.protocol] || []
);
async function initPluginScript() {
if (pluginScriptMap.value[activeDebug.value.protocol] !== undefined) {
//
return;
}
try {
pluginLoading.value = true;
const res = await getPluginScript(
protocolOptions.value.find((e) => e.value === activeDebug.value.protocol)?.pluginId || ''
);
pluginScriptMap.value[activeDebug.value.protocol] = res.script;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
pluginLoading.value = false;
}
}
function handleActiveDebugProtocolChange(val: string) {
if (val !== 'HTTP') {
activeDebug.value.activeTab = RequestComposition.PLUGIN;
initPluginScript();
} else {
activeDebug.value.activeTab = RequestComposition.HEADER;
}
handleActiveDebugChange();
}
const fApi = ref();
const options = {
form: {
layout: 'vertical',
labelPosition: 'right',
size: 'small',
labelWidth: '00px',
hideRequiredAsterisk: false,
showMessage: true,
inlineMessage: false,
scrollToFirstError: true,
},
]);
submitBtn: false,
resetBtn: false,
};
const splitBoxSize = ref<string | number>(0.6);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
@ -455,11 +572,120 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
splitBoxRef.value?.expand(0.6);
}
const executeLoading = ref(false);
const reportId = ref('');
const websocket = ref<WebSocket>();
function debugSocket() {
websocket.value = getSocket(reportId.value);
websocket.value.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
activeDebug.value.response = data.taskResult;
executeLoading.value = false;
}
});
websocket.value.addEventListener('close', (event) => {
console.log('关闭:', event);
});
websocket.value.addEventListener('error', (event) => {
console.error('错误:', event);
});
}
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,
method: activeDebug.value.method,
otherConfig: activeDebug.value.otherConfig,
path: activeDebug.value.url,
query: activeDebug.value.query,
rest: activeDebug.value.rest,
url: activeDebug.value.url,
polymorphicName,
};
} else {
requestParams = {
...fApi.value.form,
polymorphicName,
};
}
reportId.value = getGenerateId();
debugSocket(); // websocket
return {
id: activeDebug.value.id.toString(),
reportId: reportId.value,
environmentId: '',
tempFileIds: [],
request: {
...requestParams,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
// TODO:
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
},
projectId: appStore.currentProjectId,
};
}
async function execute() {
if (isHttpProtocol.value) {
try {
executeLoading.value = true;
await executeDebug(makeRequestParams());
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
executeLoading.value = false;
}
} else {
//
fApi.value?.validate(async (valid) => {
if (valid === true) {
try {
executeLoading.value = true;
await executeDebug(makeRequestParams());
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
executeLoading.value = false;
}
} else {
activeDebug.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
});
}
}
const saveModalVisible = ref(false);
const saveModalForm = ref({
name: '',
url: activeDebug.value.url,
module: activeDebug.value.module,
path: activeDebug.value.url || '',
moduleId: activeDebug.value.module,
});
const saveModalFormRef = ref<FormInstance>();
const saveLoading = ref(false);
@ -473,22 +699,46 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
}
);
function handleSaveShortcut() {
saveModalForm.value = {
name: '',
url: activeDebug.value.url,
module: activeDebug.value.module,
};
saveModalVisible.value = true;
async function handleSaveShortcut() {
try {
if (!isHttpProtocol.value) {
//
await fApi.value?.validate();
}
saveModalForm.value = {
name: '',
path: activeDebug.value.url || '',
moduleId: activeDebug.value.module,
};
saveModalVisible.value = true;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
//
activeDebug.value.activeTab = RequestComposition.PLUGIN;
nextTick(() => {
scrollIntoView(document.querySelector('.arco-form-item-message'), { block: 'center' });
});
}
}
function handleSave(done: (closed: boolean) => void) {
function handleCancel() {
saveModalFormRef.value?.resetFields();
}
async function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
saveLoading.value = true;
// eslint-disable-next-line no-promise-executor-return
await new Promise((resolve) => setTimeout(resolve, 2000));
await addDebug({
...makeRequestParams(),
...saveModalForm.value,
protocol: activeDebug.value.protocol,
method: isHttpProtocol.value ? activeDebug.value.method : activeDebug.value.protocol,
uploadFileIds: [],
linkFileIds: [],
});
saveLoading.value = false;
saveModalVisible.value = false;
done(true);
@ -497,12 +747,15 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
} catch (error) {
saveLoading.value = false;
}
} else {
done(false);
}
});
done(false);
}
onBeforeMount(() => {
initProtocolList();
});
onMounted(() => {
registerCatchSaveShortcut(handleSaveShortcut);
});
@ -527,6 +780,10 @@ Date: Wed, 13 Dec 2023 08:53:25 GMT`,
.btn-base-primary-disabled();
}
}
.tab-pane-container {
@apply flex-1 overflow-y-auto;
.ms-scroll-bar();
}
:deep(.no-content) {
.arco-tabs-content {
display: none;

View File

@ -1,13 +1,13 @@
<template>
<condition
v-model:list="postConditions"
:condition-types="['script', 'sql', 'extract']"
:condition-types="['SCRIPT']"
add-text="apiTestDebug.postCondition"
:response="props.response"
:height-used="heightUsed"
@change="emit('change')"
>
<template #titleRight>
<!-- <template #titleRight>
<a-switch v-model:model-value="openGlobalPostCondition" 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">
@ -16,7 +16,7 @@
size="16"
/>
</a-tooltip>
</template>
</template> -->
</condition>
</template>
@ -25,22 +25,24 @@
import condition from '@/views/api-test/components/condition/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { ExecuteConditionProcessor } from '@/models/apiTest/debug';
// import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: any[];
params: ExecuteConditionProcessor[];
secondBoxHeight?: number;
layout: 'horizontal' | 'vertical';
response?: string; //
}>();
const emit = defineEmits<{
(e: 'update:params', params: any[]): void;
(e: 'update:params', params: ExecuteConditionProcessor[]): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
// const { t } = useI18n();
//
const openGlobalPostCondition = ref(false);
// const openGlobalPostCondition = ref(false);
const postConditions = useVModel(props, 'params', emit);
const heightUsed = computed(() => {
if (props.layout === 'horizontal') {

View File

@ -1,11 +1,11 @@
<template>
<condition
v-model:list="preconditions"
:condition-types="['script', 'sql', 'waitTime']"
:condition-types="['SCRIPT', 'TIME_WAITING']"
add-text="apiTestDebug.precondition"
@change="emit('change')"
>
<template #titleRight>
<!-- <template #titleRight>
<a-switch v-model:model-value="openGlobalPrecondition" 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">
@ -14,7 +14,7 @@
size="16"
/>
</a-tooltip>
</template>
</template> -->
</condition>
</template>
@ -23,19 +23,21 @@
import condition from '@/views/api-test/components/condition/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { ExecuteConditionProcessor } from '@/models/apiTest/debug';
// import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
params: any[];
params: ExecuteConditionProcessor[];
}>();
const emit = defineEmits<{
(e: 'update:params', params: any[]): void;
(e: 'update:params', params: ExecuteConditionProcessor[]): void;
(e: 'change'): void;
}>();
const { t } = useI18n();
// const { t } = useI18n();
//
const openGlobalPrecondition = ref(false);
// const openGlobalPrecondition = ref(false);
const preconditions = useVModel(props, 'params', emit);
</script>

View File

@ -28,8 +28,11 @@
import { useI18n } from '@/hooks/useI18n';
import { ExecuteRequestCommonParam } from '@/models/apiTest/debug';
import { RequestParamsType } from '@/enums/apiEnum';
const props = defineProps<{
params: any[];
params: ExecuteRequestCommonParam[];
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
}>();
@ -53,24 +56,12 @@
dataIndex: 'type',
slotName: 'type',
hasRequired: true,
typeOptions: [
{
label: 'string',
value: 'string',
},
{
label: 'integer',
value: 'integer',
},
{
label: 'number',
value: 'number',
},
{
label: 'array',
value: 'array',
},
],
typeOptions: Object.keys(RequestParamsType)
.filter((key) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(key as RequestParamsType))
.map((key) => ({
label: RequestParamsType[key],
value: key,
})),
width: 120,
},
{
@ -100,8 +91,7 @@
title: '',
slotName: 'operation',
fixed: 'right',
format: 'query',
width: 80,
width: 50,
},
];

View File

@ -32,36 +32,51 @@
<a-radio value="horizontal">{{ t('apiTestDebug.horizontal') }}</a-radio>
</a-radio-group>
</div>
<div v-if="props.response.status" class="flex items-center justify-between gap-[24px]">
<div
v-if="props.response.requestResults[0]?.responseResult?.responseCode"
class="flex items-center justify-between gap-[24px]"
>
<a-popover position="left" content-class="response-popover-content">
<div class="text-[rgb(var(--danger-7))]">{{ props.response.status }}</div>
<div :style="{ color: statusCodeColor }">
{{ props.response.requestResults[0].responseResult.responseCode }}
</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.statusCode') }}</div>
<div class="text-[rgb(var(--danger-7))]">{{ props.response.status }}</div>
<div :style="{ color: statusCodeColor }">
{{ props.response.requestResults[0].responseResult.responseCode }}
</div>
</div>
</template>
</a-popover>
<a-popover position="left" content-class="w-[400px]">
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.timing }} ms</div>
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseTime }} ms
</div>
<template #content>
<div class="mb-[8px] flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseTime') }}</div>
<div class="text-[rgb(var(--success-7))]">{{ props.response.timing }} ms</div>
<div class="text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseTime }} ms
</div>
</div>
<responseTimeLine :response-timing="$props.response.timingInfo" />
<responseTimeLine :response-timing="timingInfo" />
</template>
</a-popover>
<a-popover position="left" content-class="response-popover-content">
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.size }} bytes</div>
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseSize }} bytes
</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
<div class="text-[var(--color-text-4)]">{{ t('apiTestDebug.responseSize') }}</div>
<div class="one-line-text text-[rgb(var(--success-7))]">{{ props.response.size }} bytes</div>
<div class="one-line-text text-[rgb(var(--success-7))]">
{{ props.response.requestResults[0].responseResult.responseSize }} bytes
</div>
</div>
</template>
</a-popover>
<a-popover position="left" content-class="response-popover-content">
<!-- <a-popover position="left" content-class="response-popover-content">
<div class="text-[var(--color-text-1)]">{{ props.response.env }}</div>
<template #content>
<div class="flex items-center gap-[8px] text-[14px]">
@ -78,7 +93,7 @@
<div class="text-[var(--color-text-1)]">{{ props.response.resource }}</div>
</div>
</template>
</a-popover>
</a-popover> -->
</div>
</div>
<div class="h-[calc(100%-42px)] px-[16px] pb-[16px]">
@ -88,8 +103,9 @@
<div class="response-container">
<MsCodeEditor
v-if="activeTab === ResponseComposition.BODY"
:model-value="props.response.body"
language="json"
ref="responseEditorRef"
:model-value="props.response.requestResults[0]?.responseResult?.body || ''"
:language="responseLanguage"
theme="vs"
height="100%"
:languages="['json', 'html', 'xml', 'plaintext']"
@ -143,21 +159,28 @@
import { useI18n } from '@/hooks/useI18n';
import { ResponseTiming } from '@/models/apiTest/debug';
import { ResponseComposition } from '@/enums/apiEnum';
export interface Response {
status: number;
timing: number;
size: number;
env: string;
resource: string;
body: string;
header: string;
content: string;
requestResults: {
body: string;
responseResult: {
body: string;
contentType: string;
headers: string;
dnsLookupTime: number;
downloadTime: number;
latency: number;
responseCode: number;
responseTime: number;
responseSize: number;
socketInitTime: number;
sslHandshakeTime: number;
tcpHandshakeTime: number;
transferStartTime: number;
};
}[]; //
console: string;
extract: Record<string, any>;
timingInfo: ResponseTiming;
}
const props = defineProps<{
@ -177,6 +200,66 @@
const innerLayout = useVModel(props, 'activeLayout', emit);
const activeTab = useVModel(props, 'activeTab', emit);
//
const timingInfo = computed(() => {
const {
dnsLookupTime,
downloadTime,
latency,
responseTime,
socketInitTime,
sslHandshakeTime,
tcpHandshakeTime,
transferStartTime,
} = props.response.requestResults[0].responseResult;
return {
dnsLookupTime,
tcpHandshakeTime,
sslHandshakeTime,
socketInitTime,
latency,
downloadTime,
transferStartTime,
responseTime,
};
});
//
const statusCodeColor = computed(() => {
const code = props.response.requestResults[0].responseResult.responseCode;
if (code >= 200 && code < 300) {
return 'rgb(var(--success-7)';
}
if (code >= 300 && code < 400) {
return 'rgb(var(--warning-7)';
}
return 'rgb(var(--danger-7)';
});
//
const responseLanguage = computed(() => {
const { contentType } = props.response.requestResults[0].responseResult;
if (contentType.includes('json')) {
return 'json';
}
if (contentType.includes('html')) {
return 'html';
}
if (contentType.includes('xml')) {
return 'xml';
}
return 'plaintext';
});
const responseEditorRef = ref<InstanceType<typeof MsCodeEditor>>();
watch(
() => props.response,
(obj) => {
if (obj.requestResults[0].responseResult.body.trim() !== '') {
nextTick(() => {
responseEditorRef.value?.format();
});
}
}
);
const responseTabList = [
{
@ -195,21 +278,21 @@
label: t('apiTestDebug.console'),
value: ResponseComposition.CONSOLE,
},
{
label: t('apiTestDebug.extract'),
value: ResponseComposition.EXTRACT,
},
{
label: t('apiTestDebug.assertion'),
value: ResponseComposition.ASSERTION,
},
// {
// label: t('apiTestDebug.extract'),
// value: ResponseComposition.EXTRACT,
// },
// {
// label: t('apiTestDebug.assertion'),
// value: ResponseComposition.ASSERTION,
// },
];
const { copy, isSupported } = useClipboard();
function copyScript() {
if (isSupported) {
copy(props.response.body);
copy(props.response.requestResults[0].responseResult.body);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
@ -219,15 +302,15 @@
function getResponsePreContent(type: keyof typeof ResponseComposition) {
switch (type) {
case ResponseComposition.HEADER:
return props.response.header.trim();
return props.response.requestResults[0].responseResult.headers.trim();
case ResponseComposition.REAL_REQUEST:
return props.response.content.trim();
return props.response.requestResults[0].body.trim();
case ResponseComposition.CONSOLE:
return props.response.console.trim();
case ResponseComposition.EXTRACT:
return Object.keys(props.response.extract)
.map((e) => `${e}: ${props.response.extract[e]}`)
.join('\n');
// case ResponseComposition.EXTRACT:
// return Object.keys(props.response.extract)
// .map((e) => `${e}: ${props.response.extract[e]}`)
// .join('\n');
default:
return '';
}

View File

@ -28,8 +28,11 @@
import { useI18n } from '@/hooks/useI18n';
import { ExecuteRequestCommonParam } from '@/models/apiTest/debug';
import { RequestParamsType } from '@/enums/apiEnum';
const props = defineProps<{
params: any[];
params: ExecuteRequestCommonParam[];
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
}>();
@ -53,24 +56,12 @@
dataIndex: 'type',
slotName: 'type',
hasRequired: true,
typeOptions: [
{
label: 'string',
value: 'string',
},
{
label: 'integer',
value: 'integer',
},
{
label: 'number',
value: 'number',
},
{
label: 'array',
value: 'array',
},
],
typeOptions: Object.keys(RequestParamsType)
.filter((key) => ![RequestParamsType.JSON, RequestParamsType.FILE].includes(key as RequestParamsType))
.map((key) => ({
label: RequestParamsType[key],
value: key,
})),
width: 120,
},
{
@ -100,8 +91,7 @@
title: '',
slotName: 'operation',
fixed: 'right',
format: 'query',
width: 80,
width: 50,
},
];

View File

@ -43,7 +43,7 @@
/>
</a-form-item>
<a-form-item :label="t('apiTestDebug.redirect')">
<a-radio-group v-model:model-value="settingForm.redirect">
<a-radio-group v-model:model-value="settingForm.autoRedirects">
<a-radio value="follow">{{ t('apiTestDebug.follow') }}</a-radio>
<a-radio value="auto">{{ t('apiTestDebug.auto') }}</a-radio>
</a-radio-group>
@ -58,18 +58,14 @@
import { useI18n } from '@/hooks/useI18n';
interface SettingForm {
connectTimeout: number;
responseTimeout: number;
certificateAlias: string;
redirect: 'follow' | 'auto';
}
import { ExecuteOtherConfig } from '@/models/apiTest/debug';
const props = defineProps<{
params: SettingForm;
params: ExecuteOtherConfig;
}>();
const emit = defineEmits<{
(e: 'update:params', val: SettingForm): void;
(e: 'change', val: SettingForm): void;
(e: 'update:params', val: ExecuteOtherConfig): void;
(e: 'change', val: ExecuteOtherConfig): void;
}>();
const { t } = useI18n();

View File

@ -108,21 +108,24 @@
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import popConfirm from '@/views/api-test/components/popConfirm.vue';
import { deleteReviewModule, getReviewModules, moveReviewModule } from '@/api/modules/case-management/caseReview';
import {
deleteDebugModule,
getDebugModuleCount,
getDebugModules,
moveDebugModule,
updateDebugModule,
} from '@/api/modules/api-test/debug';
import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal';
import useAppStore from '@/store/modules/app';
import { mapTree } from '@/utils';
import { ModuleTreeNode } from '@/models/common';
const props = defineProps<{
modulesCount?: Record<string, number>; //
isExpandAll?: boolean; //
}>();
const emit = defineEmits(['init', 'change', 'newApi', 'import']);
const appStore = useAppStore();
const { t } = useI18n();
const { openModal } = useModal();
@ -147,7 +150,6 @@
});
const activeFolder = ref<string>('all');
const allFileCount = ref(0);
const isExpandAll = ref(props.isExpandAll);
const rootModulesName = ref<string[]>([]); //
@ -199,7 +201,7 @@
async function initModules() {
try {
loading.value = true;
const res = await getReviewModules(appStore.currentProjectId);
const res = await getDebugModules();
folderTree.value = mapTree<ModuleTreeNode>(res, (e) => {
return {
...e,
@ -217,6 +219,26 @@
}
}
const modulesCount = ref<Record<string, number>>({});
const allFileCount = computed(() => modulesCount.value.all || 0);
async function initModuleCount() {
try {
const res = await getDebugModuleCount({
keyword: moduleKeyword.value,
});
modulesCount.value = res;
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: res[node.id] || 0,
};
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
/**
* 删除文件夹
* @param node 节点信息
@ -233,7 +255,7 @@
maskClosable: false,
onBeforeOk: async () => {
try {
await deleteReviewModule(node.id);
await deleteDebugModule(node.id);
Message.success(t('apiTestDebug.deleteSuccess'));
initModules();
} catch (error) {
@ -288,7 +310,7 @@
) {
try {
loading.value = true;
await moveReviewModule({
await moveDebugModule({
dragNodeId: dragNode.id as string,
dropNodeId: dropNode.id || '',
dropPosition,
@ -300,6 +322,7 @@
} finally {
loading.value = false;
initModules();
initModuleCount();
}
}
@ -312,23 +335,9 @@
onBeforeMount(() => {
initModules();
initModuleCount();
});
/**
* 初始化模块文件数量
*/
watch(
() => props.modulesCount,
(obj) => {
folderTree.value = mapTree<ModuleTreeNode>(folderTree.value, (node) => {
return {
...node,
count: obj?.[node.id] || 0,
};
});
}
);
defineExpose({
initModules,
});

View File

@ -5,6 +5,7 @@ export default {
'apiTestDebug.serverExec': 'Server execution',
'apiTestDebug.localExec': 'Local execution',
'apiTestDebug.noMatchModule': 'No matching module data yet',
'apiTestDebug.pluginData': 'Request data',
'apiTestDebug.header': 'Header',
'apiTestDebug.body': 'Body',
'apiTestDebug.prefix': 'Precondition',

View File

@ -5,6 +5,7 @@ export default {
'apiTestDebug.serverExec': '服务端执行',
'apiTestDebug.localExec': '本地执行',
'apiTestDebug.noMatchModule': '暂无匹配的模块数据',
'apiTestDebug.pluginData': '请求数据',
'apiTestDebug.header': '请求头',
'apiTestDebug.body': '请求体',
'apiTestDebug.prefix': '前置',

View File

@ -154,11 +154,11 @@
const { t } = useI18n();
const { openModal } = useModal();
const moduleProtocol = ref('http');
const moduleProtocol = ref('HTTP');
const moduleProtocolOptions = ref([
{
label: 'HTTP',
value: 'http',
value: 'HTTP',
},
]);

View File

@ -9,7 +9,8 @@
const store = useProjectEnvStore();
const params = computed({
// TODO:
const params = computed<any>({
set: (value: any) => {
store.currentEnvDetailInfo.config.postScript = value;
},

View File

@ -9,7 +9,8 @@
const store = useProjectEnvStore();
const params = computed({
// TODO:
const params = computed<any>({
set: (value: any) => {
store.currentEnvDetailInfo.config.preScript = value;
},

View File

@ -28,17 +28,25 @@
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["esnext", "dom"],
"lib": [
"esnext",
"dom"
],
"skipLibCheck": true, // node
"types": [
"node",
// "vitest/globals",
// "vite-plugin-svg-icons/client"
], // TS
"baseUrl": ".",
"paths": {
//
"@/*": ["./src/*"],
"#/*": ["types/*"]
"@/*": [
"./src/*"
],
"#/*": [
"types/*"
]
},
"noImplicitAny": false,
}