feat(接口测试): 接口调试-执行+保存接口
This commit is contained in:
parent
31c9897746
commit
ac9a0e5c78
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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');
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
});
|
|
@ -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",
|
||||
|
|
|
@ -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 });
|
||||
}
|
|
@ -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 });
|
||||
}
|
|
@ -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 });
|
||||
}
|
|
@ -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'; // 删除模块
|
|
@ -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'; // 获取插件表单选项
|
|
@ -369,6 +369,7 @@
|
|||
}
|
||||
}
|
||||
.arco-checkbox {
|
||||
padding-left: 0;
|
||||
.arco-checkbox-icon {
|
||||
border: 1px solid var(--color-text-input-border);
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
v-model:model-value="currentOrg"
|
||||
:options="orgOptions"
|
||||
:loading="orgLoading"
|
||||
class="w-[300px]"
|
||||
class="!w-[300px]"
|
||||
@change="handleOrgChange"
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -465,5 +465,6 @@ export default function useTableProps<T>(
|
|||
getSelectedCount,
|
||||
resetSelector,
|
||||
getTableQueryParams,
|
||||
setTableSelected,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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',
|
||||
};
|
||||
|
|
|
@ -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', // 指定匹配
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: [],
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
});
|
||||
|
|
|
@ -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 = '';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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_DATA或X_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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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[] = [
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -5,6 +5,7 @@ export default {
|
|||
'apiTestDebug.serverExec': '服务端执行',
|
||||
'apiTestDebug.localExec': '本地执行',
|
||||
'apiTestDebug.noMatchModule': '暂无匹配的模块数据',
|
||||
'apiTestDebug.pluginData': '请求数据',
|
||||
'apiTestDebug.header': '请求头',
|
||||
'apiTestDebug.body': '请求体',
|
||||
'apiTestDebug.prefix': '前置',
|
||||
|
|
|
@ -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',
|
||||
},
|
||||
]);
|
||||
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
|
||||
const store = useProjectEnvStore();
|
||||
|
||||
const params = computed({
|
||||
// TODO: 参数类型
|
||||
const params = computed<any>({
|
||||
set: (value: any) => {
|
||||
store.currentEnvDetailInfo.config.postScript = value;
|
||||
},
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
|
||||
const store = useProjectEnvStore();
|
||||
|
||||
const params = computed({
|
||||
// TODO:参数类型
|
||||
const params = computed<any>({
|
||||
set: (value: any) => {
|
||||
store.currentEnvDetailInfo.config.preScript = value;
|
||||
},
|
||||
|
|
|
@ -28,18 +28,26 @@
|
|||
"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,
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue