fix(all): 修复一堆 bug

This commit is contained in:
baiqi 2024-04-10 18:41:44 +08:00 committed by 刘瑞斌
parent 3e1a69fa41
commit c6b443d96c
32 changed files with 424 additions and 153 deletions

View File

@ -134,7 +134,9 @@
const menuSwitchOrgVisible = ref(false);
const orgKeyword = ref('');
const originOrgList = ref<{ id: string; name: string }[]>([]);
const orgList = computed(() => originOrgList.value.filter((e) => e.name.includes(orgKeyword.value)));
const orgList = computed(() =>
originOrgList.value.filter((e) => e.name.toLowerCase().includes(orgKeyword.value.toLowerCase()))
);
async function switchOrg(id: string) {
try {
@ -291,7 +293,7 @@
}}
>
<a-tooltip content={item.name}>
<div class="one-line-text max-w-[220px]">{item.name}</div>
<div class="one-line-text flex-1">{item.name}</div>
</a-tooltip>
{item.id === appStore.currentOrgId ? (
<MsTag

View File

@ -187,9 +187,6 @@
}
async function testApi() {
if (apiConfig.value.userUrl.trim() === '') {
return;
}
try {
testApiLoading.value = true;
const res = await validLocalConfig(apiConfig.value.userUrl.trim());
@ -217,6 +214,7 @@
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
apiConfig.value.status = 2;
} finally {
testApiLoading.value = false;
}

View File

@ -1,7 +1,7 @@
<template>
<div ref="treeContainerRef" :class="['ms-tree-container', containerStatusClass]">
<a-tree
v-show="data.length > 0"
v-show="filterTreeData.length > 0"
v-bind="props"
ref="treeRef"
v-model:expanded-keys="expandedKeys"
@ -63,7 +63,7 @@
</a-tree>
<slot name="empty">
<div
v-show="data.length === 0 && props.emptyText"
v-show="filterTreeData.length === 0 && props.emptyText"
class="rounded-[var(--border-radius-small)] bg-[var(--color-fill-1)] p-[8px] text-[12px] leading-[16px] text-[var(--color-text-4)]"
>
{{ props.emptyText }}
@ -74,7 +74,7 @@
<script setup lang="ts">
import { nextTick, onBeforeMount, Ref, ref, watch } from 'vue';
import { cloneDeep, debounce } from 'lodash-es';
import { debounce } from 'lodash-es';
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import type { ActionsItem } from '@/components/pure/ms-table-more-action/types';

View File

@ -29,6 +29,7 @@
);
const emit = defineEmits<{
(e: 'init', parseJson: string | Record<string, any>): void;
(e: 'pick', path: string, parseJson: string | Record<string, any>, result: any[]): void;
}>();
@ -45,6 +46,7 @@
}) as Record<string, any>;
}
JPPicker.jsonPathPicker(jr.value, json.value, [ip.value], props.opt);
emit('init', json.value);
} catch (error) {
JPPicker.jsonPathPicker(jr.value, props.data, [ip.value], props.opt);
}

View File

@ -19,6 +19,7 @@ import {
ResponseBodyDocumentAssertionType,
ResponseBodyFormat,
ResponseBodyXPathAssertionFormat,
ScenarioExecuteStatus,
} from '@/enums/apiEnum';
// 获取插件表单选项参数
@ -411,6 +412,8 @@ export interface RequestResult {
responseResult: ResponseResult;
isSuccessful?: boolean;
console?: string;
status?: ScenarioExecuteStatus;
fakeErrorCode?: string;
[key: string]: any;
}
export interface RequestTaskResult {

View File

@ -1,4 +1,10 @@
import { RequestDefinitionStatus, RequestImportFormat, RequestImportType } from '@/enums/apiEnum';
import {
RequestCaseStatus,
RequestDefinitionStatus,
RequestImportFormat,
RequestImportType,
RequestMethods,
} from '@/enums/apiEnum';
import { BatchApiParams, ModuleTreeNode, TableQueryParams } from '../common';
import { ExecuteRequestParams, ResponseDefinition } from './common';
@ -19,7 +25,6 @@ export interface ApiDefinitionCreateParams extends ExecuteRequestParams {
customFields: ApiDefinitionCustomField[];
moduleId: string;
versionId: string;
[key: string]: any; // 其他前端定义的参数
}
@ -55,7 +60,7 @@ export interface ApiDefinitionDetail extends ApiDefinitionCreateParams {
id: string;
name: string;
protocol: string;
method: string;
method: RequestMethods;
path: string;
num: number;
pos: number;
@ -181,7 +186,7 @@ export interface ApiDefinitionBatchParams extends BatchApiParams {
export interface ApiDefinitionBatchUpdateParams extends ApiDefinitionBatchParams {
type?: string;
append?: boolean;
method?: string;
method?: RequestMethods;
status?: RequestDefinitionStatus;
versionId?: string;
tags?: string[];
@ -300,7 +305,7 @@ export interface ApiCaseDetail extends ExecuteRequestParams {
name: string;
priority: string;
num: number;
status: string;
status: RequestCaseStatus;
protocol: string;
lastReportStatus: string;
lastReportId: string;
@ -309,7 +314,7 @@ export interface ApiCaseDetail extends ExecuteRequestParams {
environmentId: string;
environmentName: string;
follow: boolean;
method: string;
method: RequestMethods;
path: string;
tags: string[];
passRate: string;
@ -335,7 +340,7 @@ export interface ApiCaseBatchParams extends BatchApiParams {
export interface ApiCaseBatchEditParams extends ApiCaseBatchParams {
priority?: string;
tags?: string[];
status?: string;
status?: RequestCaseStatus;
environmentId?: string;
type: string;
append?: boolean;
@ -344,7 +349,7 @@ export interface ApiCaseBatchEditParams extends ApiCaseBatchParams {
export interface AddApiCaseParams extends ExecuteRequestParams {
name: string;
priority: string;
status: string;
status: RequestCaseStatus;
tags: string[];
deleteFileIds?: string[];
unLinkFileIds?: string[];
@ -393,7 +398,7 @@ export interface ApiCaseExecuteHistoryItem {
operationUser: string;
createUser: string;
startTime: number;
status: string;
status: RequestCaseStatus;
triggerMode: string;
deleted: boolean;
}

View File

@ -352,6 +352,7 @@ export interface ScenarioStepItem {
// 页面渲染以及交互需要字段
checked?: boolean; // 是否选中
expanded?: boolean; // 是否展开
draggable?: boolean; // 是否可拖拽
createActionsVisible?: boolean; // 是否展示创建步骤下拉
responsePopoverVisible?: boolean; // 是否展示步骤响应 popover
parent?: ScenarioStepItem; // 父级节点第一层的父级节点为undefined

View File

@ -183,11 +183,13 @@ const useUserStore = defineStore('user', {
async initLocalConfig() {
try {
const res = await getLocalConfig();
const apiLocalExec = res.find((e) => e.type === 'API');
if (apiLocalExec) {
this.hasLocalExec = true;
this.isPriorityLocalExec = apiLocalExec.enable || false;
this.localExecuteUrl = apiLocalExec.userUrl || '';
if (res) {
const apiLocalExec = res.find((e) => e.type === 'API');
if (apiLocalExec) {
this.hasLocalExec = true;
this.isPriorityLocalExec = apiLocalExec.enable || false;
this.localExecuteUrl = apiLocalExec.userUrl || '';
}
}
} catch (error) {
// eslint-disable-next-line no-console

View File

@ -348,12 +348,13 @@
:disabled="props.disabled"
mode="button"
:step="100"
:min="0"
:min="1"
:precision="0"
:max="600000"
:default-value="1000"
class="w-[160px]"
model-event="input"
@blur="handleDelayBlur"
/>
</div>
<!-- 提取参数 -->
@ -644,6 +645,7 @@ if (!result){
try {
emit('delete', condition.value.id);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
},
@ -742,6 +744,12 @@ if (!result){
}
}
function handleDelayBlur() {
if (!condition.value.delay) {
condition.value.delay = 1000;
}
}
const extractParamsColumns: ParamTableColumn[] = [
{
title: 'apiTestDebug.paramName',

View File

@ -21,7 +21,7 @@
/>
</div>
<div v-else-if="expressionForm.extractType === RequestExtractExpressionEnum.JSON_PATH" class="code-container">
<MsJsonPathPicker :data="props.response || ''" class="bg-white" @pick="handlePathPick" />
<MsJsonPathPicker :data="props.response || ''" class="bg-white" @init="initJsonPath" @pick="handlePathPick" />
</div>
<div v-else-if="expressionForm.extractType === RequestExtractExpressionEnum.X_PATH" class="code-container">
<MsXPathPicker :xml-string="props.response || ''" class="bg-white" @pick="handlePathPick" />
@ -204,9 +204,14 @@
}
);
function initJsonPath(_parseJson: string | Record<string, any>) {
parseJson.value = _parseJson;
}
function handlePathPick(path: string, _parseJson: string | Record<string, any>) {
expressionForm.value.expression = path;
parseJson.value = _parseJson;
expressionFormRef.value?.clearValidate();
}
/*
@ -271,6 +276,7 @@
matchResult.value = [];
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(`正则匹配异常:${error}`);
matchResult.value = [];
}

View File

@ -76,6 +76,7 @@
addModuleApi?: (params: { projectId: string; parentId: string; name: string }) => Promise<any>;
updateModuleApi?: (params: { id: string; name: string }) => Promise<any>;
updateApiNodeApi?: (params: { id: string; name: string }) => Promise<any>;
repeatMessage?: string;
}>();
const emit = defineEmits(['update:visible', 'close', 'addFinish', 'renameFinish']);
@ -174,7 +175,7 @@
function validateName(value: any, callback: (error?: string | undefined) => void) {
if (props.allNames.includes(value)) {
callback(t('project.fileManagement.nameExist'));
callback(props.repeatMessage || t('project.fileManagement.nameExist'));
}
}

View File

@ -34,6 +34,7 @@
:field-config="{ field: t(tab.label || tab.name) }"
:all-names="responseTabs.map((e) => t(e.label || e.name))"
:popup-offset="20"
:repeat-message="t('apiTestDebug.responseRepeatMessage')"
@rename-finish="
(val) => {
tab.label = val;

View File

@ -3,6 +3,9 @@
v-if="props.requestResult?.responseResult?.responseCode"
class="flex items-center justify-between gap-[24px] text-[14px]"
>
<a-tooltip :content="props.requestResult.fakeErrorCode">
<executeStatus :status="props.requestResult.status" size="small" class="ml-[4px]" />
</a-tooltip>
<a-popover position="left" content-class="response-popover-content">
<div class="one-line-text max-w-[200px]" :style="{ color: statusCodeColor }">
{{ props.requestResult.responseResult.responseCode }}
@ -44,6 +47,7 @@
<script setup lang="ts">
import responseTimeLine from '@/views/api-test/components/responseTimeLine.vue';
import executeStatus from '@/views/api-test/scenario/components/common/executeStatus.vue';
import { useI18n } from '@/hooks/useI18n';

View File

@ -204,4 +204,5 @@ export default {
'apiTestDebug.regexMatchRules': 'Expression matching rules',
'apiTestDebug.extractValueTitleTip':
'Enter the column name and corresponding value in column storage. If you want to extract the first value of the name column, enter name_1',
'apiTestDebug.responseRepeatMessage': 'The name is duplicated, please re-enter it.',
};

View File

@ -190,4 +190,5 @@ export default {
'apiTestDebug.searchByDataBaseName': '按数据源名称搜索',
'apiTestDebug.regexMatchRules': '表达式匹配规则',
'apiTestDebug.extractValueTitleTip': '输入按列存储中的列名和对应的数值如提取name列的第一个值则输入name_1',
'apiTestDebug.responseRepeatMessage': '名称重复,请重新输入',
};

View File

@ -332,8 +332,10 @@
return;
}
try {
appStore.showLoading();
loading.value = true;
const res = await getDefinitionDetail(typeof apiInfo === 'string' ? apiInfo : apiInfo.id);
appStore.hideLoading();
let name = isCopy ? `copy_${res.name}` : res.name;
if (name.length > 255) {
name = name.slice(0, 255);
@ -367,6 +369,7 @@
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
appStore.hideLoading();
}
}

View File

@ -13,6 +13,12 @@
>
<template #title>
<div class="flex max-w-[60%] items-center gap-[8px]">
<div
v-if="props.step"
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] pr-[2px] !text-white"
>
{{ props.step.sort }}
</div>
<stepTypeVue
v-if="props.step && [ScenarioStepType.API, ScenarioStepType.CUSTOM_REQUEST].includes(props.step?.stepType)"
:step="props.step"
@ -23,44 +29,46 @@
</div>
</a-tooltip>
</div>
<div
v-if="props.step && !props.step.isQuoteScenarioStep"
class="right-operation-button-icon ml-auto flex items-center"
>
<replaceButton
v-if="props.step.resourceId && props.step?.stepType !== ScenarioStepType.CUSTOM_REQUEST"
:steps="props.steps"
:step="props.step"
:resource-id="props.step.resourceId"
:scenario-id="scenarioId"
@replace="handleReplace"
/>
<MsButton class="mr-4" type="icon" status="secondary" @click="emit('deleteStep')">
<MsIcon type="icon-icon_delete-trash_outlined" />
{{ t('common.delete') }}
</MsButton>
</div>
<div
v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST"
class="customApiDrawer-title-right ml-auto flex items-center gap-[16px]"
>
<a-tooltip :content="currentEnvConfig?.name" :disabled="!currentEnvConfig?.name">
<div class="one-line-text max-w-[250px] text-[14px] font-normal text-[var(--color-text-4)]">
{{ t('apiScenario.env', { name: currentEnvConfig?.name }) }}
</div>
</a-tooltip>
<a-select
v-model:model-value="requestVModel.customizeRequestEnvEnable"
class="w-[150px]"
:disabled="props.step?.isQuoteScenarioStep"
@change="handleUseEnvChange"
<div class="ml-auto flex items-center gap-[16px]">
<div
v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST"
class="customApiDrawer-title-right flex items-center gap-[16px]"
>
<template #prefix>
<div> {{ t('project.environmental.env') }} </div>
</template>
<a-option :value="true">{{ t('common.quote') }}</a-option>
<a-option :value="false">{{ t('common.notQuote') }}</a-option>
</a-select>
<a-tooltip :content="currentEnvConfig?.name" :disabled="!currentEnvConfig?.name">
<div class="one-line-text max-w-[250px] text-[14px] font-normal text-[var(--color-text-4)]">
{{ t('apiScenario.env', { name: currentEnvConfig?.name }) }}
</div>
</a-tooltip>
<a-select
v-model:model-value="requestVModel.customizeRequestEnvEnable"
class="w-[150px]"
:disabled="props.step?.isQuoteScenarioStep"
@change="handleUseEnvChange"
>
<template #prefix>
<div> {{ t('project.environmental.env') }} </div>
</template>
<a-option :value="true">{{ t('common.quote') }}</a-option>
<a-option :value="false">{{ t('common.notQuote') }}</a-option>
</a-select>
</div>
<div
v-if="props.step && !props.step.isQuoteScenarioStep"
class="right-operation-button-icon ml-auto flex items-center"
>
<replaceButton
v-if="props.step.resourceId && props.step?.stepType !== ScenarioStepType.CUSTOM_REQUEST"
:steps="props.steps"
:step="props.step"
:resource-id="props.step.resourceId"
:scenario-id="scenarioId"
@replace="handleReplace"
/>
<MsButton type="icon" status="secondary" @click="emit('deleteStep')">
<MsIcon type="icon-icon_delete-trash_outlined" />
{{ t('common.delete') }}
</MsButton>
</div>
</div>
</template>
<a-empty

View File

@ -8,6 +8,12 @@
@close="handleClose"
>
<template #title>
<div
v-if="activeStep"
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] pr-[2px] !text-white"
>
{{ activeStep.sort }}
</div>
<stepType v-if="activeStep?.stepType" :step="activeStep" class="mr-[4px]" />
<a-input
v-if="activeStep?.name"

View File

@ -506,6 +506,7 @@
tableSelectedData.value = props.selectedScenarios;
break;
}
keyword.value = '';
}
);

View File

@ -1,15 +1,5 @@
<template>
<div>
<div class="mb-[8px] flex items-center justify-end">
<a-input-search
v-model:model-value="keyword"
:placeholder="t('apiScenario.executeHistory.searchPlaceholder')"
allow-clear
class="mr-[8px] w-[240px]"
@search="loadExecuteHistoryList"
@press-enter="loadExecuteHistoryList"
/>
</div>
<ms-base-table v-bind="propsRes" no-disable filter-icon-align-left v-on="propsEvent">
<template #num="{ record }">
<span type="text" class="px-0">{{ record.num }}</span>

View File

@ -20,7 +20,7 @@
/>
<batchAddKeyVal
:params="innerParams"
:default-param-item="defaultHeaderParamsItem"
:default-param-item="defaultParamItem"
no-param-type
@apply="handleBatchParamApply"
/>
@ -44,7 +44,6 @@
import { CommonVariable } from '@/models/apiTest/scenario';
import { defaultHeaderParamsItem } from '@/views/api-test/components/config';
import { filterKeyValParams } from '@/views/api-test/components/utils';
const props = defineProps<{
@ -146,7 +145,7 @@
* 批量参数代码转换为参数表格数据
*/
function handleBatchParamApply(resultArr: any[]) {
const filterResult = filterKeyValParams(innerParams.value, defaultHeaderParamsItem);
const filterResult = filterKeyValParams(innerParams.value, defaultParamItem);
if (filterResult.lastDataIsDefault) {
innerParams.value = [...resultArr, innerParams.value[innerParams.value.length - 1]].filter(Boolean);
} else {

View File

@ -415,7 +415,13 @@
</a-modal>
<!-- 表格批量操作-->
<a-modal v-model:visible="showBatchModal" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
<a-modal
v-model:visible="showBatchModal"
title-align="start"
class="ms-modal-upload ms-modal-medium"
:width="480"
@close="cancelBatch"
>
<template #title>
{{ t('common.batchEdit') }}
<div class="text-[var(--color-text-4)]">

View File

@ -6,7 +6,7 @@
class="max-w-[500px] px-[8px]"
size="mini"
:step="1000"
:min="0"
:min="1"
:max="600000"
:precision="0"
model-event="input"
@ -46,6 +46,10 @@
});
function handleInputChange() {
console.log('innerData.value.delay', innerData.value.delay);
if (!innerData.value.delay) {
innerData.value.delay = 1;
}
nextTick(() => {
emit('change', innerData.value);
});

View File

@ -64,14 +64,14 @@
></a-switch>
<!-- 步骤执行 -->
<MsIcon
v-show="!step.isExecuting && step.enable"
v-show="!step.isExecuting"
type="icon-icon_play-round_filled"
:size="18"
class="cursor-pointer text-[rgb(var(--link-6))]"
@click.stop="executeStep(step)"
/>
<MsIcon
v-show="step.isExecuting && step.enable"
v-show="step.isExecuting"
type="icon-icon_stop"
:size="20"
class="cursor-pointer text-[rgb(var(--link-6))]"
@ -378,12 +378,9 @@
<a-modal
v-model:visible="saveNewApiModalVisible"
:title="t('common.save')"
:ok-loading="saveLoading"
class="ms-modal-form"
title-align="start"
body-class="!p-0"
@before-ok="handleSave"
@cancel="handleCancel"
>
<a-form ref="saveModalFormRef" :model="saveModalForm" layout="vertical">
<a-form-item
@ -414,7 +411,7 @@
<a-form-item :label="t('apiTestDebug.requestModule')" class="mb-0">
<a-tree-select
v-model:modelValue="saveModalForm.moduleId"
:data="moduleTree || []"
:data="apiModuleTree"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:tree-props="{
virtualListProps: {
@ -426,6 +423,64 @@
/>
</a-form-item>
</a-form>
<template #footer>
<div class="flex items-center justify-between">
<div class="flex items-center gap-[4px]">
<a-checkbox v-model:model-value="saveModalForm.saveApiAsCase"></a-checkbox>
{{ t('apiScenario.syncSaveAsCase') }}
</div>
<div class="flex items-center gap-[12px]">
<a-button type="secondary" :disabled="saveLoading" @click="handleSaveApiCancel">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="saveLoading" @click="handleSaveApi">{{ t('common.confirm') }}</a-button>
</div>
</div>
</template>
</a-modal>
<a-modal
v-model:visible="saveCaseModalVisible"
:title="t('apiTestManagement.saveAsCase')"
:ok-loading="saveCaseLoading"
class="ms-modal-form"
title-align="start"
body-class="!p-0"
@before-ok="saveAsCase"
@cancel="handleSaveCaseCancel"
>
<a-form ref="saveCaseModalFormRef" :model="saveCaseModalForm" layout="vertical">
<a-form-item
field="name"
:label="t('case.caseName')"
:rules="[{ required: true, message: t('case.caseNameRequired') }]"
asterisk-position="end"
>
<a-input
v-model:model-value="saveCaseModalForm.name"
:placeholder="t('case.caseNamePlaceholder')"
:max-length="255"
/>
</a-form-item>
<a-form-item field="priority" :label="t('case.caseLevel')">
<a-select v-model:model-value="saveCaseModalForm.priority" :options="casePriorityOptions"></a-select>
</a-form-item>
<a-form-item field="status" :label="t('common.status')">
<a-select v-model:model-value="saveCaseModalForm.status">
<a-option v-for="item in caseStatusOptions" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="tags" :label="t('common.tag')">
<MsTagsInput
v-model:model-value="saveCaseModalForm.tags"
placeholder="common.tagsInputPlaceholder"
allow-clear
unique-value
retain-input-value
/>
</a-form-item>
</a-form>
</a-modal>
</div>
</template>
@ -437,6 +492,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import { ImportData } from '../common/importApiDrawer/index.vue';
@ -451,7 +507,12 @@
import { RequestParam as CaseRequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import { addDefinition } from '@/api/modules/api-test/management';
import {
addCase,
addDefinition,
getDefinitionDetail,
getModuleTreeOnlyModules,
} from '@/api/modules/api-test/management';
import { debugScenario, getScenarioStep } from '@/api/modules/api-test/scenario';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { useI18n } from '@/hooks/useI18n';
@ -467,7 +528,12 @@
TreeNode,
} from '@/utils';
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import {
ExecuteApiRequestFullParams,
ExecuteConditionProcessor,
ExecutePluginRequestParams,
} from '@/models/apiTest/common';
import { AddApiCaseParams } from '@/models/apiTest/management';
import {
ApiScenarioDebugRequest,
CreateStepAction,
@ -477,9 +543,9 @@
ScenarioStepFileParams,
ScenarioStepItem,
} from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common';
import { EnvConfig } from '@/models/projectManagement/environmental';
import {
RequestCaseStatus,
RequestDefinitionStatus,
ScenarioAddStepActionType,
ScenarioExecuteStatus,
@ -490,7 +556,7 @@
import type { RequestParam } from '../common/customApiDrawer.vue';
import updateStepStatus from '../utils';
import useCreateActions from './createAction/useCreateActions';
import { defaultResponseItem } from '@/views/api-test/components/config';
import { casePriorityOptions, caseStatusOptions, defaultResponseItem } from '@/views/api-test/components/config';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
@ -534,8 +600,6 @@
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
const localExecuteUrl = inject<Ref<string>>('localExecuteUrl');
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const moduleTree = inject<Ref<ModuleTreeNode[]>>('moduleTree');
const activeModule = inject<Ref<string>>('activeModule');
const permissionMap = {
execute: 'PROJECT_API_SCENARIO:READ+EXECUTE',
@ -666,7 +730,10 @@
},
];
}
if (_stepType.isQuoteCase) {
if ((node as ScenarioStepItem).isQuoteScenarioStep) {
return [];
}
if (_stepType.isQuoteApi || _stepType.isCopyApi) {
return [
{
label: 'common.copy',
@ -683,9 +750,6 @@
},
];
}
if ((node as ScenarioStepItem).isQuoteScenarioStep) {
return [];
}
return stepMoreActions;
}
@ -805,28 +869,95 @@
}
}
async function getStepDetail(step: ScenarioStepItem) {
try {
appStore.showLoading();
const res = await getScenarioStep(step.copyFromStepId || step.id);
let parseRequestBodyResult;
if (step.config.protocol === 'HTTP' && res.body) {
parseRequestBodyResult = parseRequestBodyFiles(res.body); // id
}
stepDetails.value[step.id] = {
...res,
stepId: step.id,
protocol: step.config.protocol,
method: step.config.method,
...parseRequestBodyResult,
};
scenario.value.stepFileParam[step.id] = parseRequestBodyResult;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
appStore.hideLoading();
}
}
const apiModuleTree = ref<MsTreeNodeData[]>([]);
async function initApiModuleTree(protocol: string) {
try {
apiModuleTree.value = await getModuleTreeOnlyModules({
keyword: '',
protocol,
projectId: appStore.currentProjectId,
moduleIds: [],
});
} catch (error) {
// eslint-disable-next-line no-console
console.error(error);
}
}
const saveNewApiModalVisible = ref(false);
const saveModalForm = ref({
name: '',
path: '',
moduleId: activeModule?.value || 'root',
moduleId: 'root',
saveApiAsCase: false,
});
const saveModalFormRef = ref<FormInstance>();
const saveLoading = ref(false);
async function saveApiAsCase(id: string) {
if (activeStep.value) {
const detail = stepDetails.value[activeStep.value.id] as RequestParam;
const fileParams = scenario.value.stepFileParam[activeStep.value.id];
const url = new URL(saveModalForm.value.path);
const path = url.pathname + url.search + url.hash;
const params: AddApiCaseParams = {
name: saveModalForm.value.name,
projectId: appStore.currentProjectId,
environmentId: currentEnvConfig?.value.id || '',
apiDefinitionId: id,
request: {
...detail,
url: path,
},
priority: 'P0',
status: RequestCaseStatus.PROCESSING,
tags: [],
uploadFileIds: fileParams?.uploadFileIds || [],
linkFileIds: fileParams?.linkFileIds || [],
};
await addCase(params);
}
}
/**
* 保存请求
* @param fullParams 保存时传入的参数
* @param silence 是否静默保存接口定义另存为用例时要先静默保存接口
* @param isSaveCase 是否需要保存用例
*/
async function realSave() {
async function realSaveAsApi() {
try {
saveLoading.value = true;
if (activeStep.value) {
const detail = stepDetails.value[activeStep.value.id] as RequestParam;
const fileParams = scenario.value.stepFileParam[activeStep.value.id];
await addDefinition({
const url = new URL(saveModalForm.value.path);
const path = url.pathname + url.search + url.hash;
const res = await addDefinition({
...saveModalForm.value,
path,
projectId: appStore.currentProjectId,
tags: [],
description: '',
@ -834,13 +965,20 @@
customFields: [],
versionId: '',
environmentId: currentEnvConfig?.value.id || '',
request: detail,
request: {
...detail,
url: path,
path,
},
uploadFileIds: fileParams?.uploadFileIds || [],
linkFileIds: fileParams?.linkFileIds || [],
response: [defaultResponseItem],
method: detail?.method,
protocol: detail?.protocol,
});
if (saveModalForm.value.saveApiAsCase) {
await saveApiAsCase(res.id);
}
Message.success(t('common.saveSuccess'));
saveNewApiModalVisible.value = false;
saveLoading.value = false;
@ -852,18 +990,99 @@
}
}
function handleSave(done: (closed: boolean) => void) {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
await realSave();
done(true);
}
});
done(false);
function handleSaveApiCancel() {
saveModalFormRef.value?.resetFields();
saveNewApiModalVisible.value = false;
}
function handleCancel() {
saveModalFormRef.value?.resetFields();
function handleSaveApi() {
saveModalFormRef.value?.validate(async (errors) => {
if (!errors) {
await realSaveAsApi();
handleSaveApiCancel();
}
});
}
const saveCaseModalVisible = ref(false);
const saveCaseLoading = ref(false);
const saveCaseModalForm = ref({
name: '',
priority: 'P0',
status: RequestCaseStatus.PROCESSING,
tags: [],
});
const saveCaseModalFormRef = ref<FormInstance>();
function handleSaveCaseCancel() {
saveCaseModalForm.value = {
name: '',
priority: 'P0',
status: RequestCaseStatus.PROCESSING,
tags: [],
};
saveCaseModalVisible.value = false;
}
function saveAsCase(done: (closed: boolean) => void) {
saveCaseModalFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
if (activeStep.value) {
saveCaseLoading.value = true;
let detail = stepDetails.value[activeStep.value.id] as
| ExecuteApiRequestFullParams
| ExecutePluginRequestParams;
if (!detail) {
//
if (
(stepDetails.value[activeStep.value.id] === undefined &&
activeStep.value.copyFromStepId &&
!activeStep.value.isNew) ||
(stepDetails.value[activeStep.value.id] === undefined && !activeStep.value.isNew)
) {
//
await getStepDetail(activeStep.value);
detail = stepDetails.value[activeStep.value.id] as
| ExecuteApiRequestFullParams
| ExecutePluginRequestParams;
} else {
const apiDetail = await getDefinitionDetail(activeStep.value.resourceId || '');
detail = {
...apiDetail.request,
...apiDetail,
};
}
}
const fileParams = scenario.value.stepFileParam[activeStep.value.id];
const params: AddApiCaseParams = {
projectId: appStore.currentProjectId,
environmentId: currentEnvConfig?.value.id || '',
apiDefinitionId: activeStep.value.resourceId || '',
request: detail,
...saveCaseModalForm.value,
uploadFileIds: fileParams?.uploadFileIds || [],
linkFileIds: fileParams?.linkFileIds || [],
};
await addCase(params);
done(true);
Message.success(t('common.saveSuccess'));
handleSaveCaseCancel();
saveCaseLoading.value = false;
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
done(false);
} finally {
handleSaveCaseCancel();
saveCaseLoading.value = false;
}
} else {
saveCaseLoading.value = false;
done(false);
}
});
}
function handleStepMoreActionSelect(item: ActionsItem, node: MsTreeNodeData) {
@ -946,9 +1165,14 @@
break;
case 'saveAsApi':
activeStep.value = node as ScenarioStepItem;
initApiModuleTree((stepDetails.value[node.id] as RequestParam)?.protocol);
saveModalForm.value.path = (stepDetails.value[node.id] as RequestParam)?.url;
saveNewApiModalVisible.value = true;
break;
case 'saveAsCase':
activeStep.value = node as ScenarioStepItem;
saveCaseModalVisible.value = true;
break;
default:
break;
}
@ -971,12 +1195,17 @@
const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
input?.focus();
});
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
if (realStep) {
realStep.draggable = false; //
}
}
function applyStepNameChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
if (realStep) {
realStep.name = tempStepName.value;
realStep.draggable = true; //
}
showStepNameEditInputStepId.value = '';
scenario.value.unSaved = true;
@ -995,12 +1224,17 @@
const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
input?.focus();
});
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
if (realStep) {
realStep.draggable = false; //
}
}
function applyStepDescChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.uniqueId, 'uniqueId');
if (realStep) {
realStep.name = tempStepDesc.value;
realStep.draggable = true; //
}
showStepDescEditInputStepId.value = '';
scenario.value.unSaved = true;
@ -1050,30 +1284,6 @@
}
});
async function getStepDetail(step: ScenarioStepItem) {
try {
appStore.showLoading();
const res = await getScenarioStep(step.copyFromStepId || step.id);
let parseRequestBodyResult;
if (step.config.protocol === 'HTTP' && res.body) {
parseRequestBodyResult = parseRequestBodyFiles(res.body); // id
}
stepDetails.value[step.id] = {
...res,
stepId: step.id,
protocol: step.config.protocol,
method: step.config.method,
...parseRequestBodyResult,
};
scenario.value.stepFileParam[step.id] = parseRequestBodyResult;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
appStore.hideLoading();
}
}
function handleAddStepDone(newStep: ScenarioStepItem) {
selectedKeys.value = [newStep.uniqueId]; //
emit('stepAdd');
@ -1165,7 +1375,7 @@
websocketMap[reportId]?.close();
if (step.reportId === data.reportId) {
step.isExecuting = false;
updateStepStatus([step], _scenario.stepResponses);
updateStepStatus([step], _scenario.stepResponses, step.uniqueId);
}
}
});
@ -1194,6 +1404,7 @@
steps: mapTree(executeParams.steps, (node) => {
return {
...node,
enable: node.uniqueId === currentStep.uniqueId || node.enable, //
parent: null, // axios
};
}),
@ -1206,7 +1417,7 @@
console.log(error);
websocketMap[executeParams.reportId].close();
currentStep.isExecuting = false;
updateStepStatus([currentStep], scenario.value.stepResponses);
updateStepStatus([currentStep], scenario.value.stepResponses, currentStep.uniqueId);
}
}
@ -1225,16 +1436,18 @@
traverseTree(
realStep,
(step) => {
if (step.enable) {
//
if (step.enable || step.uniqueId === realStep.uniqueId) {
//
_stepDetails[step.id] = stepDetails.value[step.id];
step.executeStatus = ScenarioExecuteStatus.EXECUTING;
} else {
step.executeStatus = undefined;
}
delete scenario.value.stepResponses[step.uniqueId]; //
},
(step) => {
//
return step.enable;
//
return step.enable || step.uniqueId === realStep.uniqueId;
}
);
realExecute(
@ -1314,7 +1527,7 @@
websocketMap[step.reportId].close();
if (realStep) {
realStep.isExecuting = false;
updateStepStatus([realStep as ScenarioStepItem], scenario.value.stepResponses);
updateStepStatus([realStep as ScenarioStepItem], scenario.value.stepResponses, realStep.uniqueId);
}
}
}

View File

@ -5,15 +5,18 @@ import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
/**
*
* @param steps
* @param stepResponses
* @param singleStepId ID
*/
export default function updateStepStatus(
steps: ScenarioStepItem[],
stepResponses: Record<string | number, RequestResult[]>
stepResponses: Record<string | number, RequestResult[]>,
singleStepId?: string | number
) {
for (let i = 0; i < steps.length; i++) {
const node = steps[i];
if (node.enable) {
// 启用的步骤才计算
if (node.enable || singleStepId === node.uniqueId) {
// 启用的步骤才计算/如果是单步骤执行,无视顶层步骤的启用状态
if (
[
ScenarioStepType.LOOP_CONTROLLER,
@ -64,7 +67,6 @@ export default function updateStepStatus(
}
}
} else if (node.stepType === ScenarioStepType.CONSTANT_TIMER) {
// 等待时间直接设置为成功
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
} else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) {
// 非逻辑控制器直接更改本身状态

View File

@ -245,11 +245,11 @@
activeScenarioTab.value.executeFakeErrorCount = 0;
activeScenarioTab.value.stepResponses = {};
activeScenarioTab.value.reportId = executeParams.reportId; // ID
activeScenarioTab.value.isDebug = !isExecute;
debugSocket(activeScenarioTab.value, executeType, localExecuteUrl); // websocket
let res;
if (isExecute && executeType !== 'localExec' && !activeScenarioTab.value.isNew) {
//
activeScenarioTab.value.isDebug = false;
res = await executeScenario({
id: activeScenarioTab.value.id,
grouped: false,
@ -267,6 +267,7 @@
}),
});
} else {
activeScenarioTab.value.isDebug = true;
res = await debugScenario({
id: activeScenarioTab.value.id,
grouped: false,

View File

@ -191,6 +191,7 @@ export default {
'apiScenario.sourceScenarioEnvTip': 'Runtime environment, including environment parameters',
'apiScenario.setSuccess': 'Set Successful',
'apiScenario.pleaseInputUrl': 'Please enter URL',
'apiScenario.syncSaveAsCase': 'Synchronously add test interface case',
// Execution History
'apiScenario.executeHistory.searchPlaceholder': 'Search by ID or name',
'apiScenario.executeHistory.num': 'No.',

View File

@ -180,6 +180,7 @@ export default {
'apiScenario.sourceScenarioEnvTip': '运行环境,含环境参数',
'apiScenario.setSuccess': '设置成功',
'apiScenario.pleaseInputUrl': '请输入 url',
'apiScenario.syncSaveAsCase': '同步添加测试接口用例',
// 执行历史
'apiScenario.executeHistory.searchPlaceholder': '通过ID或名称搜索',
'apiScenario.executeHistory.num': '序号',

View File

@ -10,7 +10,7 @@ export default {
'caseManagement.caseReview.creator': 'Creator',
'caseManagement.caseReview.reviewer': 'Reviewer',
'caseManagement.caseReview.reviewerRequired': 'Please select at least one reviewer',
'caseManagement.caseReview.type': 'Review criteria',
'caseManagement.caseReview.type': 'Review mode',
'caseManagement.caseReview.status': 'Review status',
'caseManagement.caseReview.caseCount': 'Use case number',
'caseManagement.caseReview.passRate': 'Passing rate',

View File

@ -10,7 +10,7 @@ export default {
'caseManagement.caseReview.creator': '创建人',
'caseManagement.caseReview.reviewer': '评审人',
'caseManagement.caseReview.reviewerRequired': '请至少选择一位评审人',
'caseManagement.caseReview.type': '评审标准',
'caseManagement.caseReview.type': '评审模式',
'caseManagement.caseReview.status': '评审状态',
'caseManagement.caseReview.caseCount': '用例数量',
'caseManagement.caseReview.passRate': '通过率',

View File

@ -150,6 +150,12 @@
if (!errors) {
setLoading(true);
try {
try {
await userStore.logout(); //
} catch (error) {
// eslint-disable-next-line no-console
console.log('logout error', error);
}
await userStore.login({
username: encrypted(values.username),
password: encrypted(values.password),

View File

@ -23,12 +23,7 @@
/>
</a-form-item>
<a-form-item class="mb-0" field="userGroup" :label="t('system.user.createUserUserGroup')">
<a-select
v-model="emailForm.userGroup"
multiple
:placeholder="t('system.user.createUserUserGroupPlaceholder')"
allow-clear
>
<a-select v-model="emailForm.userGroup" multiple :placeholder="t('system.user.createUserUserGroupPlaceholder')">
<a-option
v-for="item of userGroupOptions"
:key="item.id"