feat(接口测试): Mock调整

This commit is contained in:
baiqi 2024-05-15 15:17:56 +08:00 committed by Craftsman
parent b5bb1805df
commit 0f14d934bb
13 changed files with 296 additions and 33 deletions

View File

@ -394,17 +394,18 @@
class={['flex w-full items-center justify-between', collapsed.value ? 'h-[56px] w-[56px]' : '']} class={['flex w-full items-center justify-between', collapsed.value ? 'h-[56px] w-[56px]' : '']}
key="personalInfo" key="personalInfo"
> >
{collapsed.value ? ( {
<div class="relative flex h-full items-center justify-center hover:!bg-transparent"> <div
<MsAvatar avatar={userStore.avatar} size={30} class="hover:!bg-transparent" /> class={[
collapsed.value
? 'relative flex h-full items-center justify-center hover:!bg-transparent'
: 'relative flex items-center gap-[8px] hover:!bg-transparent',
]}
>
<MsAvatar is-user size={20} class="!mr-0 hover:!bg-transparent" />
{collapsed.value ? null : userStore.name}
</div> </div>
) : ( }
<div class="relative flex items-center gap-[8px] hover:!bg-transparent">
<MsAvatar avatar={userStore.avatar} size={20} />
{userStore.name}
</div>
)}
{collapsed.value ? null : <icon-caret-down class="!m-0" />} {collapsed.value ? null : <icon-caret-down class="!m-0" />}
</a-menu-item> </a-menu-item>
</a-trigger> </a-trigger>

View File

@ -1,6 +1,6 @@
<template> <template>
<MsIcon <MsIcon
v-if="props.avatar === 'default' || props.avatar === null" v-if="innerAvatar === 'default' || innerAvatar === null"
type="icon-icon_that_person" type="icon-icon_that_person"
:size="props.size" :size="props.size"
class="text-[var(--color-text-4)]" class="text-[var(--color-text-4)]"
@ -9,13 +9,13 @@
}" }"
/> />
<a-avatar <a-avatar
v-else-if="props.avatar === 'word'" v-else-if="innerAvatar === 'word'"
:size="props.size" :size="props.size"
class="bg-[rgb(var(--primary-1))] text-[rgb(var(--primary-6))]" class="bg-[rgb(var(--primary-1))] text-[rgb(var(--primary-6))]"
> >
<slot>{{ props.word?.substring(0, 4) || userStore.name?.substring(0, 4) }}</slot> <slot>{{ props.word?.substring(0, 4) || userStore.name?.substring(0, 4) }}</slot>
</a-avatar> </a-avatar>
<a-avatar v-else :image-url="avatar" :size="props.size"></a-avatar> <a-avatar v-else :image-url="innerAvatar" :size="props.size"></a-avatar>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -30,12 +30,23 @@
avatar?: 'default' | 'word' | string; avatar?: 'default' | 'word' | string;
size?: number; size?: number;
word?: string; // word?: string; //
isUser?: boolean; //
}>(), }>(),
{ {
avatar: 'default', avatar: 'default',
size: 40, size: 40,
} }
); );
const innerAvatar = ref(props.avatar);
watchEffect(() => {
if (props.isUser) {
innerAvatar.value = userStore.avatar || 'default';
} else {
innerAvatar.value = props.avatar;
}
});
</script> </script>
<style lang="less" scoped></style> <style lang="less" scoped></style>

View File

@ -35,6 +35,7 @@ export interface MockResponse {
useApiResponse: boolean; useApiResponse: boolean;
apiResponseId?: string; // useApiResponse 为 true 时必填 apiResponseId?: string; // useApiResponse 为 true 时必填
body: ResponseDefinitionBody; body: ResponseDefinitionBody;
delay: number;
} }
// mock 信息-请求通用匹配规则 // mock 信息-请求通用匹配规则
export interface MockMatchRuleCommon { export interface MockMatchRuleCommon {

View File

@ -342,6 +342,7 @@ export const mockDefaultParams: MockParams = {
sendAsBody: false, sendAsBody: false,
}, },
}, },
delay: 0,
}, },
apiDefinitionId: '', apiDefinitionId: '',
uploadFileIds: [], uploadFileIds: [],

View File

@ -112,6 +112,9 @@
</a-select> </a-select>
<apiMethodName v-else :method="record.method" is-tag /> <apiMethodName v-else :method="record.method" is-tag />
</template> </template>
<template #caseTotal="{ record }">
{{ record.caseTotal }}
</template>
<template #status="{ record }"> <template #status="{ record }">
<a-select <a-select
v-if="hasAnyPermission(['PROJECT_API_DEFINITION:READ+UPDATE'])" v-if="hasAnyPermission(['PROJECT_API_DEFINITION:READ+UPDATE'])"
@ -449,6 +452,21 @@
width: 200, width: 200,
showDrag: true, showDrag: true,
}, },
{
title: 'apiTestManagement.belongModule',
dataIndex: 'moduleName',
showTooltip: true,
width: 200,
showDrag: true,
},
{
title: 'apiTestManagement.caseTotal',
dataIndex: 'caseTotal',
showTooltip: true,
width: 100,
showDrag: true,
slotName: 'caseTotal',
},
{ {
title: 'common.tag', title: 'common.tag',
dataIndex: 'tags', dataIndex: 'tags',

View File

@ -339,6 +339,12 @@
const preActiveApiTabId = activeApiTab.value.id; const preActiveApiTabId = activeApiTab.value.id;
let loadedApiTab = apiTabs.value[isLoadedTabIndex] as RequestParam; let loadedApiTab = apiTabs.value[isLoadedTabIndex] as RequestParam;
if (isDebugMock) { if (isDebugMock) {
const mockEnvId = appStore.envList.find((e) => e.mock)?.id;
if (mockEnvId) {
appStore.showLoading();
await appStore.setEnvConfig(mockEnvId);
appStore.hideLoading();
}
loadedApiTab = { loadedApiTab = {
...loadedApiTab, ...loadedApiTab,
...(apiInfo as ApiDefinitionDetail).request, ...(apiInfo as ApiDefinitionDetail).request,
@ -372,6 +378,10 @@
} }
let { request } = res; let { request } = res;
if (isDebugMock) { if (isDebugMock) {
const mockEnvId = appStore.envList.find((e) => e.mock)?.id;
if (mockEnvId) {
await appStore.setEnvConfig(mockEnvId);
}
request = { request = {
...res.request, ...res.request,
...(apiInfo as ApiDefinitionDetail).request, ...(apiInfo as ApiDefinitionDetail).request,

View File

@ -10,6 +10,7 @@
:ok-loading="loading" :ok-loading="loading"
no-content-padding no-content-padding
unmount-on-close unmount-on-close
@continue="() => handleSave(true)"
@confirm="handleSave" @confirm="handleSave"
@cancel="handleCancel" @cancel="handleCancel"
@close="handleCancel" @close="handleCancel"
@ -227,7 +228,7 @@
import { ResponseDefinition } from '@/models/apiTest/common'; import { ResponseDefinition } from '@/models/apiTest/common';
import { MockParams } from '@/models/apiTest/mock'; import { MockParams } from '@/models/apiTest/mock';
import { RequestBodyFormat, RequestComposition } from '@/enums/apiEnum'; import { RequestBodyFormat, RequestComposition, RequestParamsType } from '@/enums/apiEnum';
import { import {
defaultHeaderParamsItem, defaultHeaderParamsItem,
@ -499,7 +500,9 @@
// form-data // form-data
const formDataMatch = res.mockMatchRule.body.formDataBody.matchRules.map((item) => { const formDataMatch = res.mockMatchRule.body.formDataBody.matchRules.map((item) => {
const newParamType = const newParamType =
currentBodyKeyOptions.value.find((e) => e.value === item.key)?.paramType || defaultMatchRuleItem.paramType; currentBodyKeyOptions.value.find((e) => e.value === item.key)?.paramType || item.files
? RequestParamsType.FILE
: defaultMatchRuleItem.paramType;
item.paramType = newParamType; item.paramType = newParamType;
item.files = item.files || []; item.files = item.files || [];
return item; return item;
@ -535,6 +538,15 @@
appendDefaultMatchRuleItem(); appendDefaultMatchRuleItem();
} }
isEdit.value = !!props.isEditMode; isEdit.value = !!props.isEditMode;
if (mockDetail.value.mockMatchRule.body.bodyType !== RequestBodyFormat.NONE) {
activeTab.value = RequestComposition.BODY;
} else if (mockDetail.value.mockMatchRule.header.matchRules.length > 0) {
activeTab.value = RequestComposition.HEADER;
} else if (mockDetail.value.mockMatchRule.query) {
activeTab.value = RequestComposition.QUERY;
} else if (mockDetail.value.mockMatchRule.rest) {
activeTab.value = RequestComposition.REST;
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -612,7 +624,7 @@
handleCancel(); handleCancel();
} }
async function handleSave() { async function handleSave(isContinue = false) {
try { try {
loading.value = true; loading.value = true;
const { body } = mockDetail.value.mockMatchRule; const { body } = mockDetail.value.mockMatchRule;
@ -696,7 +708,11 @@
Message.success(t('common.createSuccess')); Message.success(t('common.createSuccess'));
} }
emit('addDone'); emit('addDone');
if (isContinue) {
mockDetail.value = makeDefaultParams();
} else {
handleCancel(); handleCancel();
}
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);

View File

@ -2,7 +2,7 @@
<a-form ref="formRef" :model="formModel" layout="vertical"> <a-form ref="formRef" :model="formModel" layout="vertical">
<a-spin :loading="loading" class="block"> <a-spin :loading="loading" class="block">
<div <div
v-if="matchRules.length > 0" v-if="matchRules.length > 0 || !disabled"
:class="`flex ${ :class="`flex ${
matchRules.length > 1 ? 'items-stretch' : 'items-center' matchRules.length > 1 ? 'items-stretch' : 'items-center'
} gap-[16px] bg-[var(--color-text-n9)] p-[12px]`" } gap-[16px] bg-[var(--color-text-n9)] p-[12px]`"
@ -30,6 +30,7 @@
:placeholder="t('apiTestDebug.paramName')" :placeholder="t('apiTestDebug.paramName')"
:options="props.keyOptions" :options="props.keyOptions"
allow-search allow-search
allow-create
@change="(val) => selectedKey(item, idx)" @change="(val) => selectedKey(item, idx)"
> >
</a-select> </a-select>

View File

@ -1,10 +1,24 @@
<template> <template>
<a-spin :loading="loading" class="block"> <a-spin :loading="loading" class="block">
<div class="mt-[16px] font-medium">{{ t('apiTestManagement.responseContent') }}</div> <div class="mt-[16px] font-medium">{{ t('apiTestManagement.responseContent') }}</div>
<div class="mt-[8px] flex items-center gap-[4px]"> <div class="mt-[8px] flex items-center gap-[16px]">
<a-switch v-model:model-value="mockResponse.useApiResponse" size="small" :disabled="props.disabled"></a-switch> <div class="flex items-center gap-[4px]">
<a-switch
v-model:model-value="mockResponse.useApiResponse"
size="small"
:disabled="props.disabled"
@change="handleUseApiResponseChange"
></a-switch>
{{ t('mockManagement.followDefinition') }} {{ t('mockManagement.followDefinition') }}
</div> </div>
<a-select
v-if="mockResponse.useApiResponse"
v-model:model-value="mockResponse.apiResponseId"
:options="mockResponseOptions"
class="w-[150px]"
:disabled="props.disabled"
></a-select>
</div>
<template v-if="!mockResponse.useApiResponse"> <template v-if="!mockResponse.useApiResponse">
<MsTab <MsTab
v-model:active-key="activeTab" v-model:active-key="activeTab"
@ -59,7 +73,6 @@
:columns="jsonSchemaColumns" :columns="jsonSchemaColumns"
/> --> /> -->
<MsCodeEditor <MsCodeEditor
ref="responseEditorRef"
v-model:model-value="currentBodyCode" v-model:model-value="currentBodyCode"
:language="currentCodeLanguage" :language="currentCodeLanguage"
theme="vs" theme="vs"
@ -125,23 +138,156 @@
@change="handleResponseTableChange" @change="handleResponseTableChange"
/> />
<a-select <a-select
v-else v-else-if="activeTab === ResponseComposition.CODE"
v-model:model-value="mockResponse.statusCode" v-model:model-value="mockResponse.statusCode"
:options="statusCodeOptions" :options="statusCodeOptions"
class="w-[200px]" class="w-[200px]"
:disabled="props.disabled" :disabled="props.disabled"
@change="() => emit('change')" @change="() => emit('change')"
/> />
<a-input-number
v-else
v-model:model-value="mockResponse.delay"
:disabled="props.disabled"
mode="button"
:step="100"
:precision="0"
:max="600000"
:min="0"
class="w-[200px]"
>
<template #suffix> ms </template>
</a-input-number>
</div> </div>
</template> </template>
<div v-else class="mt-[8px]"> <template v-else-if="currentSelectedDefinitionResponse">
<a-select <MsTab
v-model:model-value="mockResponse.apiResponseId" v-model:active-key="definitionActiveTab"
:options="mockResponseOptions" :content-tab-list="responseCompositionTabList.filter((e) => e.value !== 'DELAY')"
class="w-[150px]" class="no-content relative my-[8px] border-b"
:disabled="props.disabled" :show-badge="false"
></a-select> />
<div class="mt-[8px]">
<template v-if="definitionActiveTab === ResponseComposition.BODY">
<div class="mb-[8px] flex items-center justify-between">
<a-radio-group
v-model:model-value="currentSelectedDefinitionResponse.body.bodyType"
type="button"
size="small"
disabled
@change="(val) => emit('change')"
>
<a-radio
v-for="item of ResponseBodyFormat"
v-show="item !== ResponseBodyFormat.NONE"
:key="item"
:value="item"
>
{{ ResponseBodyFormat[item].toLowerCase() }}
</a-radio>
</a-radio-group>
<!-- <div v-if="currentSelectedDefinitionResponse.body.bodyType === ResponseBodyFormat.JSON" class="ml-auto flex items-center">
<a-radio-group
v-model:model-value="currentSelectedDefinitionResponse.body.jsonBody.enableJsonSchema"
size="mini"
@change="emit('change')"
>
<a-radio :value="false">Json</a-radio>
<a-radio class="mr-0" :value="true"> Json Schema </a-radio>
</a-radio-group>
<div class="flex items-center gap-[8px]">
<a-switch v-model:model-value="currentSelectedDefinitionResponse.body.jsonBody.enableTransition" size="small" type="line" />
{{ t('apiTestManagement.dynamicConversion') }}
</div> </div>
</div> -->
</div>
<div
v-if="
[ResponseBodyFormat.JSON, ResponseBodyFormat.XML, ResponseBodyFormat.RAW].includes(
currentSelectedDefinitionResponse.body.bodyType
)
"
>
<!-- <MsJsonSchema
v-if="currentSelectedDefinitionResponse.body.jsonBody.enableJsonSchema"
:data="currentSelectedDefinitionResponse.body.jsonBody.jsonSchema"
:columns="jsonSchemaColumns"
/> -->
<MsCodeEditor
v-model:model-value="currentSelectedDefinitionBodyCode"
:language="currentSelectedDefinitionCodeLanguage"
theme="vs"
:show-full-screen="false"
:show-theme-change="false"
:show-language-change="false"
:show-charset-change="false"
show-code-format
read-only
>
</MsCodeEditor>
</div>
<div v-else>
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
<a-input
v-model:model-value="currentSelectedDefinitionResponse.body.binaryBody.description"
:placeholder="t('common.desc')"
:max-length="255"
disabled
/>
<MsAddAttachment
v-model:file-list="fileList"
mode="input"
:multiple="false"
:fields="{
id: 'fileId',
name: 'fileName',
}"
disabled
@change="handleFileChange"
/>
</div>
<div class="flex items-center">
<a-switch
v-model:model-value="currentSelectedDefinitionResponse.body.binaryBody.sendAsBody"
class="mr-[8px]"
size="small"
type="line"
disabled
></a-switch>
<span>{{ t('apiTestDebug.sendAsMainText') }}</span>
<a-tooltip position="right">
<template #content>
<div>{{ t('apiTestDebug.sendAsMainTextTip1') }}</div>
<div>{{ t('apiTestDebug.sendAsMainTextTip2') }}</div>
</template>
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</div>
</div>
</template>
<paramTable
v-else-if="definitionActiveTab === ResponseComposition.HEADER"
:params="filterKeyValParams(currentSelectedDefinitionResponse.headers, defaultKeyValueParamItem).validParams"
:columns="columns"
:default-param-item="defaultKeyValueParamItem"
:selectable="false"
disabled-param-value
disabled-except-param
@change="handleResponseTableChange"
/>
<a-select
v-else-if="definitionActiveTab === ResponseComposition.CODE"
v-model:model-value="currentSelectedDefinitionResponse.statusCode"
:options="statusCodeOptions"
class="w-[200px]"
disabled
@change="() => emit('change')"
/>
</div>
</template>
</a-spin> </a-spin>
</template> </template>
@ -162,6 +308,7 @@
import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum'; import { ResponseBodyFormat, ResponseComposition } from '@/enums/apiEnum';
import { defaultKeyValueParamItem, statusCodes } from '@/views/api-test/components/config'; import { defaultKeyValueParamItem, statusCodes } from '@/views/api-test/components/config';
import { filterKeyValParams } from '@/views/api-test/components/utils';
const props = defineProps<{ const props = defineProps<{
definitionResponses: ResponseItem[]; definitionResponses: ResponseItem[];
@ -182,6 +329,7 @@
props.definitionResponses.map((item) => ({ props.definitionResponses.map((item) => ({
label: `${t(item.label || item.name)}(${item.statusCode})`, label: `${t(item.label || item.name)}(${item.statusCode})`,
value: item.id, value: item.id,
defaultFlag: item.defaultFlag,
})) }))
); );
@ -198,6 +346,10 @@
label: t('apiTestManagement.responseCode'), label: t('apiTestManagement.responseCode'),
value: ResponseComposition.CODE, value: ResponseComposition.CODE,
}, },
{
label: t('mockManagement.responseDelay'),
value: 'DELAY',
},
]; ];
const statusCodeOptions = statusCodes.map((e) => ({ const statusCodeOptions = statusCodes.map((e) => ({
@ -300,6 +452,12 @@
} }
} }
function handleUseApiResponseChange(val: string | number | boolean) {
if (val) {
mockResponse.value.apiResponseId = (mockResponseOptions.value.find((e) => e.defaultFlag)?.value as string) || '';
}
}
watch( watch(
() => mockResponse.value.body.binaryBody.file?.fileId, () => mockResponse.value.body.binaryBody.file?.fileId,
() => { () => {
@ -312,6 +470,48 @@
immediate: true, immediate: true,
} }
); );
const currentSelectedDefinitionResponse = computed(() =>
props.definitionResponses.find((e) => e.id === mockResponse.value.apiResponseId)
);
const definitionActiveTab = ref(ResponseComposition.BODY);
//
const currentSelectedDefinitionBodyCode = computed({
get() {
if (currentSelectedDefinitionResponse.value?.body.bodyType === ResponseBodyFormat.JSON) {
return currentSelectedDefinitionResponse.value.body.jsonBody.jsonValue;
}
if (currentSelectedDefinitionResponse.value?.body.bodyType === ResponseBodyFormat.XML) {
return currentSelectedDefinitionResponse.value.body.xmlBody.value;
}
return currentSelectedDefinitionResponse.value?.body.rawBody.value;
},
set(val) {
if (
currentSelectedDefinitionResponse.value?.body.bodyType === ResponseBodyFormat.JSON &&
currentSelectedDefinitionResponse.value.body.jsonBody
) {
currentSelectedDefinitionResponse.value.body.jsonBody.jsonValue = val || '';
} else if (
currentSelectedDefinitionResponse.value?.body.bodyType === ResponseBodyFormat.XML &&
currentSelectedDefinitionResponse.value.body.xmlBody
) {
currentSelectedDefinitionResponse.value.body.xmlBody.value = val || '';
} else if (currentSelectedDefinitionResponse.value) {
currentSelectedDefinitionResponse.value.body.rawBody.value = val || '';
}
},
});
//
const currentSelectedDefinitionCodeLanguage = computed(() => {
if (currentSelectedDefinitionResponse.value?.body.bodyType === ResponseBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (currentSelectedDefinitionResponse.value?.body.bodyType === ResponseBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -163,6 +163,7 @@ export default {
'apiTestManagement.script': 'Script', 'apiTestManagement.script': 'Script',
'apiTestManagement.variable': 'Variable', 'apiTestManagement.variable': 'Variable',
'apiTestManagement.regex': 'Regular Expression', 'apiTestManagement.regex': 'Regular Expression',
'apiTestManagement.caseTotal': 'Case total',
'case.execute.selectEnv': 'Select Environment', 'case.execute.selectEnv': 'Select Environment',
'case.execute.defaultEnv': 'Default Environment', 'case.execute.defaultEnv': 'Default Environment',
'case.execute.newEnv': 'New Environment', 'case.execute.newEnv': 'New Environment',
@ -248,4 +249,5 @@ export default {
'mockManagement.batchEdit': 'Batch Edit', 'mockManagement.batchEdit': 'Batch Edit',
'mockManagement.batchDelete': 'Batch Delete', 'mockManagement.batchDelete': 'Batch Delete',
'mockManagement.noMatchRules': 'No matching rules found', 'mockManagement.noMatchRules': 'No matching rules found',
'mockManagement.responseDelay': 'Response delay',
}; };

View File

@ -156,6 +156,7 @@ export default {
'apiTestManagement.script': '脚本', 'apiTestManagement.script': '脚本',
'apiTestManagement.variable': '变量', 'apiTestManagement.variable': '变量',
'apiTestManagement.regex': '正则表达式', 'apiTestManagement.regex': '正则表达式',
'apiTestManagement.caseTotal': '用例数',
'case.execute.selectEnv': '环境选择', 'case.execute.selectEnv': '环境选择',
'case.execute.defaultEnv': '默认环境', 'case.execute.defaultEnv': '默认环境',
'case.execute.newEnv': '新环境', 'case.execute.newEnv': '新环境',
@ -238,4 +239,5 @@ export default {
'mockManagement.batchEdit': '批量编辑', 'mockManagement.batchEdit': '批量编辑',
'mockManagement.batchDelete': '批量删除', 'mockManagement.batchDelete': '批量删除',
'mockManagement.noMatchRules': '无该类匹配规则', 'mockManagement.noMatchRules': '无该类匹配规则',
'mockManagement.responseDelay': '响应延时',
}; };

View File

@ -1161,7 +1161,7 @@
url: res.path, url: res.path,
name: res.name, // requestnamenull name: res.name, // requestnamenull
resourceId: res.id, resourceId: res.id,
stepId: props.step?.uniqueId || '', stepId: props.step?.id || '',
responseActiveTab: ResponseComposition.BODY, responseActiveTab: ResponseComposition.BODY,
...parseRequestBodyResult, ...parseRequestBodyResult,
}; };

View File

@ -985,7 +985,7 @@
stepName: activeStep.value?.name || res.name, stepName: activeStep.value?.name || res.name,
name: res.name, // requestnamenull name: res.name, // requestnamenull
resourceId: res.id, resourceId: res.id,
stepId: props.request?.stepId || '', stepId: activeStep.value?.id || '',
...parseRequestBodyResult, ...parseRequestBodyResult,
}; };
nextTick(() => { nextTick(() => {