feat(接口场景): 场景步骤自定义请求保存&查看详情

This commit is contained in:
baiqi 2024-03-23 22:53:55 +08:00 committed by Craftsman
parent 960f1e3505
commit 886d566c21
39 changed files with 1111 additions and 747 deletions

View File

@ -1,6 +1,7 @@
import MSR from '@/api/http/index';
import {
AddModuleUrl,
AddScenarioUrl,
BatchCopyScenarioUrl,
BatchDeleteScenarioUrl,
BatchEditScenarioUrl,
@ -13,6 +14,7 @@ import {
ExecuteHistoryUrl,
GetModuleCountUrl,
GetModuleTreeUrl,
GetScenarioUrl,
GetTrashModuleCountUrl,
GetTrashModuleTreeUrl,
MoveModuleUrl,
@ -36,6 +38,8 @@ import {
ApiScenarioUpdateDTO,
ExecuteHistoryItem,
ExecutePageParams,
Scenario,
ScenarioDetail,
ScenarioHistoryItem,
ScenarioHistoryPageParams,
} from '@/models/apiTest/scenario';
@ -180,3 +184,13 @@ export function batchDeleteScenario(data: {
}) {
return MSR.post({ url: BatchDeleteScenarioUrl, data });
}
// 添加场景
export function addScenario(params: Scenario) {
return MSR.post({ url: AddScenarioUrl, params });
}
// 获取场景详情
export function getScenarioDetail(id: string) {
return MSR.get<ScenarioDetail>({ url: GetScenarioUrl, params: id });
}

View File

@ -5,6 +5,8 @@ export const GetModuleCountUrl = '/api/scenario/module/count'; // 获取模块
export const AddModuleUrl = '/api/scenario/module/add'; // 添加模块
export const DeleteModuleUrl = '/api/scenario/module/delete'; // 删除模块
export const ScenarioPageUrl = '/api/scenario/page'; // 接口场景列表
export const AddScenarioUrl = '/api/scenario/add'; // 添加接口场景
export const GetScenarioUrl = '/api/scenario/get'; // 获取接口场景详情
export const UpdateScenarioUrl = '/api/scenario/update'; // 更新接口场景
export const RecycleScenarioUrl = '/api/scenario/delete-to-gc'; // 删除接口场景
export const BatchRecycleScenarioUrl = '/api/scenario/batch-operation/delete-gc'; // 批量删除接口场景

View File

@ -146,8 +146,8 @@
import { useI18n } from '@/hooks/useI18n';
import { ExecuteAssertionConfig, ExecuteConditionProcessor } from '@/models/apiTest/common';
import { RequestConditionScriptLanguage, ResponseAssertionType, ResponseBodyAssertionType } from '@/enums/apiEnum';
import { ExecuteAssertionConfig } from '@/models/apiTest/common';
import { ResponseAssertionType, ResponseBodyAssertionType } from '@/enums/apiEnum';
import { ExecuteAssertion, MsAssertionItem } from './type';
@ -175,7 +175,6 @@
const innerConfig = useVModel(props, 'assertionConfig', emit);
const activeIds = ref('');
// Itemkey
const activeKey = ref<string>('');
// value

View File

@ -330,7 +330,7 @@
.handle {
@apply absolute left-0 top-0 flex h-full items-center;
z-index: 1;
z-index: 10;
width: 8px;
background-color: var(--color-neutral-3);
cursor: col-resize;

View File

@ -52,7 +52,7 @@
const checked = computed({
get: () => {
return props.selectedKeys.size === props.total;
return props.selectedKeys.size > 0 && props.selectedKeys.size === props.total;
},
set: (value) => {
return value;

View File

@ -231,6 +231,11 @@ export enum ScenarioCreateComposition {
// 接口场景详情组成部分
export enum ScenarioDetailComposition {
BASE_INFO = 'BASE_INFO',
STEP = 'STEP',
PARAMS = 'PARAMS',
PRE_POST = 'PRE_POST',
ASSERTION = 'ASSERTION',
SETTING = 'SETTING',
EXECUTE_HISTORY = 'EXECUTE_HISTORY',
CHANGE_HISTORY = 'CHANGE_HISTORY',
DEPENDENCY = 'DEPENDENCY',
@ -244,18 +249,6 @@ export enum ScenarioExecuteStatus {
}
// 场景步骤类型
export enum ScenarioStepType {
QUOTE_API = 'QUOTE_API',
COPY_API = 'COPY_API',
QUOTE_CASE = 'QUOTE_CASE',
COPY_CASE = 'COPY_CASE',
QUOTE_SCENARIO = 'QUOTE_SCENARIO',
COPY_SCENARIO = 'COPY_SCENARIO',
WAIT_TIME = 'WAIT_TIME',
LOOP_CONTROL = 'LOOP_CONTROL',
CONDITION_CONTROL = 'CONDITION_CONTROL',
ONLY_ONCE_CONTROL = 'ONLY_ONCE_CONTROL',
SCRIPT_OPERATION = 'SCRIPT_OPERATION',
CUSTOM_API = 'CUSTOM_API',
API_CASE = 'API_CASE', // 接口用例
LOOP_CONTROLLER = 'LOOP_CONTROLLER', // 循环控制器
API = 'API', // 接口定义
@ -263,6 +256,14 @@ export enum ScenarioStepType {
API_SCENARIO = ' API_SCENARIO', // 场景
IF_CONTROLLER = 'IF_CONTROLLER', // 条件控制器
ONCE_ONLY_CONTROLLER = 'ONCE_ONLY_CONTROLLER', // 一次控制器
CONSTANT_TIMER = 'CONSTANT_TIMER', // 等待控制器
SCRIPT = 'SCRIPT', // 脚本
}
export enum ScenarioStepRefType {
COPY = 'COPY', // 复制
DIRECT = 'DIRECT', // 在场景中直接创建的步骤 例如 自定义请求,逻辑控制器
PARTIAL_REF = 'PARTIAL_REF', // 部分引用
REF = 'REF', // 完全引用
}
// 场景添加步骤操作类型
export enum ScenarioAddStepActionType {
@ -293,3 +294,25 @@ export enum ChangeHistoryStatusFilters {
IMPORT = 'IMPORT',
DELETE = 'DELETE',
}
// 场景步骤-循环控制器类型
export enum ScenarioStepLoopTypeEnum {
WHILE = 'WHILE',
LOOP_COUNT = 'LOOP_COUNT',
FOREACH = 'FOREACH',
}
// 场景步骤-循环控制器-while循环类型
export enum WhileConditionType {
CONDITION = 'CONDITION',
SCRIPT = 'SCRIPT',
}
export enum ScenarioStepPolymorphicName {
COMMON_SCRIPT = 'MsCommentScriptElement',
IF_CONTROLLER = 'MsIfController',
LOOP_CONTROLLER = 'MsLoopController',
ONLY_ONCE = 'MsOnceOnlyController',
TIME_CONTROLLER = 'MsConstantTimerController',
}
export enum ScenarioFailureStrategy {
CONTINUE = 'CONTINUE',
STOP = 'STOP',
}

View File

@ -314,7 +314,7 @@ export type ExecuteAssertionItem = ResponseAssertionCommon &
ResponseVariableAssertion;
// 执行请求-断言配置
export interface ExecuteAssertionConfig {
enableGlobal: boolean; // 是否启用全局断言
enableGlobal?: boolean; // 是否启用全局断言,部分地方没有
assertions: ExecuteAssertionItem[];
}
// 执行请求-共用配置子项

View File

@ -1,11 +1,28 @@
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
import { ApiDefinitionCustomField, ApiRunModeRequest } from '@/models/apiTest/management';
import { ApiScenarioStatus, RequestComposition, RequestDefinitionStatus } from '@/enums/apiEnum';
import {
ApiScenarioStatus,
RequestAssertionCondition,
RequestComposition,
RequestDefinitionStatus,
RequestMethods,
ScenarioExecuteStatus,
ScenarioFailureStrategy,
ScenarioStepLoopTypeEnum,
ScenarioStepPolymorphicName,
ScenarioStepRefType,
ScenarioStepType,
WhileConditionType,
} from '@/enums/apiEnum';
import { BatchApiParams, TableQueryParams } from '../common';
import { ExecuteApiRequestFullParams, ResponseDefinition } from './common';
import {
ExecuteApiRequestFullParams,
ExecuteAssertionItem,
ExecuteConditionConfig,
ResponseDefinition,
} from './common';
// 场景-更新模块参数
export interface ApiScenarioModuleUpdateParams {
@ -27,11 +44,11 @@ export interface ApiScenarioGetModuleParams {
// 场景修改参数
export interface ApiScenarioUpdateDTO {
id: string;
id: string | number;
name?: string;
priority?: string;
status?: ApiScenarioStatus;
moduleId?: string;
moduleId?: string | number;
description?: string;
tags?: string[];
grouped?: boolean;
@ -196,25 +213,171 @@ export type CustomApiStep = ExecuteApiRequestFullParams & {
useEnv: string;
};
// 场景步骤-循环控制器类型
export type ScenarioStepLoopType = 'num' | 'while' | 'forEach';
// 场景步骤-循环控制器-循环类型
export type ScenarioStepLoopWhileType = 'condition' | 'expression';
export type ScenarioStepLoopType = ScenarioStepLoopTypeEnum;
// 场景步骤-步骤插入类型
export type CreateStepAction = 'inside' | 'before' | 'after';
// 场景步骤
export interface Scenario {
export interface OtherConfig {
enableGlobalCookie: boolean;
enableCookieShare: boolean;
stepWaitTime: number;
enableStepWait: boolean;
failureStrategy: ScenarioFailureStrategy;
}
export interface AssertionConfig {
assertions: ExecuteAssertionItem[];
}
export interface CsvVariable {
id: string;
fileId: string;
scenarioId: string;
name: string;
fileName: string;
scope: string;
enable: boolean;
association: boolean;
encoding: string;
random: boolean;
variableNames: string;
ignoreFirstLine: boolean;
delimiter: string;
allowQuotedData: boolean;
recycleOnEof: boolean;
stopThreadOnEof: boolean;
}
export interface CommonVariable {
id: string | number;
key: string;
paramType: string;
value: string;
enable: boolean;
description: string;
tags: string[];
}
export interface Variable {
commonVariables: CommonVariable[];
csvVariables: CsvVariable[];
}
export interface ScenarioConfig {
variable: Variable;
preProcessorConfig: ExecuteConditionConfig;
postProcessorConfig: ExecuteConditionConfig;
assertionConfig: AssertionConfig;
otherConfig: OtherConfig;
}
export interface ForEachController {
loopTime: number; // 循环间隔时间
value: string; // 变量值
variable: string; // 变量名
}
export interface CountController {
loops: number; // 循环次数
}
export interface WhileScript {
scriptValue: string; // 脚本值
}
export interface WhileVariable {
condition: RequestAssertionCondition; // 条件操作符
value: string; // 变量值
variable: string; // 变量名
}
export interface WhileController {
conditionType: WhileConditionType; // 条件类型
timeout: number; // 超时时间
msWhileScript: WhileScript; // 脚本
msWhileVariable: WhileVariable; // 变量
}
export type ExtendedScenarioStepPolymorphicName = ScenarioStepPolymorphicName | string;
// 场景步骤详情公共部分
export interface StepDetailsCommon {
id: string | number;
copyFromStepId?: string; // 如果步骤是复制的这个字段是复制的步骤id
name: string;
enable: boolean;
polymorphicName: ExtendedScenarioStepPolymorphicName; // 多态名称,用于后台区分使用的是哪个组件
}
// 自定义请求
export interface CustomApiStepDetail extends StepDetailsCommon {
customizeRequest: boolean; // 是否自定义请求
customizeRequestEnvEnable: boolean; // 是否启用环境
}
// 条件控制器
export interface ConditionStepDetail extends StepDetailsCommon {
value: string; // 变量值
variable: string; // 变量名
condition: RequestAssertionCondition; // 条件操作符
}
// 循环控制器
export interface LoopStepDetail extends StepDetailsCommon {
loopType: ScenarioStepLoopType;
forEachController: ForEachController;
msCountController: CountController;
whileController: WhileController;
}
export type ScenarioStepDetail = Partial<CustomApiStepDetail & ConditionStepDetail & LoopStepDetail>;
export interface ScenarioStepItem {
id: string | number;
sort: number;
name: string;
executeStatus?: ScenarioExecuteStatus;
enable: boolean; // 是否启用
resourceId?: string; // 详情或者引用的类型才有
resourceNum?: string; // 详情或者引用的类型才有
stepType: ScenarioStepType;
refType: ScenarioStepRefType;
config?: ScenarioStepDetail; // 对应场景里stepDetails里的详情信息只有逻辑控制器需要
csvFileIds?: string[];
projectId?: string;
versionId?: string;
children?: ScenarioStepItem[];
// 页面渲染以及交互需要字段
checked?: boolean; // 是否选中
expanded?: boolean; // 是否展开
createActionsVisible?: boolean; // 是否展示创建步骤下拉
parent?: ScenarioStepItem; // 父级节点第一层的父级节点为undefined
resourceName?: string; // 引用复制接口、用例、场景时的源资源名称
method?: RequestMethods;
}
// 场景
export interface Scenario {
id?: string | number;
num?: number;
name: string;
moduleId: string | number;
stepInfo: ScenarioStepInfo;
priority: CaseLevel;
status: RequestDefinitionStatus;
status: ApiScenarioStatus;
tags: string[];
params: Record<string, any>[];
projectId: string;
description: string;
grouped?: boolean;
environmentId?: string;
scenarioConfig: ScenarioConfig;
steps: ScenarioStepItem[];
stepDetails: Record<string, ScenarioStepDetail>;
follow?: boolean;
uploadFileIds: string[];
linkFileIds: string[];
// 前端渲染字段
label: string;
closable: boolean;
isNew: boolean;
unSaved: boolean;
executeLoading: boolean; // 执行loading
executeTime?: string | number; // 执行时间
executeSuccessCount?: number; // 执行成功数量
executeFailCount?: number; // 执行失败数量
}
export interface ScenarioDetail extends Scenario {
stepTotal: number;
requestPassRate: string;
lastReportStatus?: string;
lastReportId?: string;
deleted: boolean;
versionId: string;
refId: string;
latest: boolean;
modulePath: string;
createUser: string;
createTime: number;
updateTime: number;
updateUser: string;
}

View File

@ -224,7 +224,7 @@ export function mapTree<T>(
return _tree
.map((node: TreeNode<T>, i: number) => {
const fullPath = node.path ? `${_parentPath}/${node.path}`.replace(/\/+/g, '/') : '';
node.order = i + 1; // order从 1 开始
node.sort = i + 1; // order从 1 开始
node.parent = _parent || undefined; // 没有父节点说明是树的第一层
const newNode = typeof customNodeFn === 'function' ? customNodeFn(node, fullPath) : node;
if (newNode) {
@ -382,19 +382,19 @@ export function insertNodes<T>(
// 插入节点数组
newNodes.forEach((newNode, index) => {
newNode.parent = parent;
newNode.order = startOrder + index;
newNode.sort = startOrder + index;
});
array.splice(startIndex, 0, ...newNodes);
} else {
// 插入单个节点
newNodes.parent = parent;
newNodes.order = startOrder;
newNodes.sort = startOrder;
array.splice(startIndex, 0, newNodes);
}
// 更新插入节点之后的节点的 order
// 更新插入节点之后的节点的 sort
const newLength = Array.isArray(newNodes) ? newNodes.length : 1;
for (let j = startIndex + newLength; j < array.length; j++) {
array[j].order += newLength;
array[j].sort += newLength;
}
}
@ -406,9 +406,9 @@ export function insertNodes<T>(
const parentChildren = parent ? parent.children || [] : treeArr; // 父节点没有 children 属性,说明是树的第一层,使用 treeArr
const index = parentChildren.findIndex((item) => item[customKey] === node[customKey]);
if (position === 'before') {
insertNewNodes(parentChildren, index, parent || node.parent, node.order);
insertNewNodes(parentChildren, index, parent || node.parent, node.sort);
} else if (position === 'after') {
insertNewNodes(parentChildren, index + 1, parent || node.parent, node.order + 1);
insertNewNodes(parentChildren, index + 1, parent || node.parent, node.sort + 1);
} else if (position === 'inside') {
if (!node.children) {
node.children = [];
@ -460,9 +460,9 @@ export function handleTreeDragDrop<T>(
if (index !== -1) {
parentChildren.splice(index, 1);
// 更新删除节点后的节点的 order
// 更新删除节点后的节点的 sort
for (let i = index; i < parentChildren.length; i++) {
parentChildren[i].order -= 1;
parentChildren[i].sort -= 1;
}
}

View File

@ -857,6 +857,7 @@
nextTick(() => {
// form-create tab
fApi.value?.resetFields();
isInitPluginForm.value = true;
});
}
}
@ -1136,7 +1137,9 @@
}
reportId.value = getGenerateId();
requestVModel.value.reportId = reportId.value; // ID
debugSocket(executeType); // websocket
if (isExecute) {
debugSocket(executeType); // websocket
}
let requestName = '';
let requestModuleId = '';
let apiDefinitionParams: Record<string, any> = {};

View File

@ -3,8 +3,6 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepType } from '@/enums/apiEnum';
@ -15,17 +13,19 @@
}>();
//
const scenarioStepMap = {
[ScenarioStepType.QUOTE_API]: { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.COPY_API]: { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.QUOTE_CASE]: { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.COPY_CASE]: { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.QUOTE_SCENARIO]: { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.COPY_SCENARIO]: { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.WAIT_TIME]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' },
// [ScenarioStepType.QUOTE_API]: { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' },
// [ScenarioStepType.COPY_API]: { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' },
// [ScenarioStepType.QUOTE_CASE]: { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' },
// [ScenarioStepType.COPY_CASE]: { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' },
// [ScenarioStepType.QUOTE_SCENARIO]: { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' },
// [ScenarioStepType.COPY_SCENARIO]: { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' },
// [ScenarioStepType.WAIT_TIME]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' },
[ScenarioStepType.LOOP_CONTROLLER]: { label: 'apiScenario.loopControl', color: 'rgba(167, 98, 191, 1)' },
[ScenarioStepType.IF_CONTROLLER]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' },
[ScenarioStepType.ONCE_ONLY_CONTROLLER]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' },
[ScenarioStepType.SCRIPT_OPERATION]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
[ScenarioStepType.SCRIPT]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
// [ScenarioStepType.SCRIPT_OPERATION]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
// [ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
[ScenarioStepType.API_CASE]: { label: 'report.detail.api.apiCase', color: 'rgb(var(--link-4))' },
[ScenarioStepType.CUSTOM_REQUEST]: { label: 'report.detail.api.apiCase', color: 'rgb(var(--link-4))' },
[ScenarioStepType.API]: { label: 'report.detail.api', color: 'rgb(var(--link-4))' },

View File

@ -5,10 +5,11 @@
<script setup lang="ts">
import assertion from '@/components/business/ms-assertion/index.vue';
import { AssertionConfig } from '@/models/apiTest/scenario';
const assertions = ref([]);
const assertionConfig = ref({
enableGlobal: false,
assertions: [],
const assertionConfig = defineModel<AssertionConfig>('assertionConfig', {
required: true,
});
</script>

View File

@ -0,0 +1,40 @@
<template>
<MsDescription :descriptions="descriptions"> </MsDescription>
</template>
<script setup lang="ts">
import dayjs from 'dayjs';
import MsDescription, { Description } from '@/components/pure/ms-description/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { ScenarioDetail } from '@/models/apiTest/scenario';
const props = defineProps<{
scenario: ScenarioDetail;
}>();
const { t } = useI18n();
const descriptions = computed<Description[]>(() => [
{
label: t('apiScenario.belongModule'),
value: props.scenario.modulePath,
},
{
label: t('apiScenario.table.columns.createUser'),
value: props.scenario.createUser,
},
{
label: t('apiScenario.table.columns.createTime'),
value: dayjs(props.scenario.createTime).format('YYYY-MM-DD HH:mm:ss'),
},
{
label: t('apiScenario.table.columns.updateTime'),
value: dayjs(props.scenario.updateTime).format('YYYY-MM-DD HH:mm:ss'),
},
]);
</script>
<style lang="less" scoped></style>

View File

@ -1,6 +1,6 @@
<template>
<div>
<a-alert v-if="isShowTip" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited">
<a-alert v-if="!getIsVisited()" :show-icon="false" class="mb-[16px]" type="warning" closable @close="addVisited">
{{ t('apiScenario.historyListTip') }}
<template #close-element>
<span class="text-[14px]">{{ t('common.notRemind') }}</span>
@ -11,7 +11,6 @@
</template>
<script setup lang="ts">
import { ref } from 'vue';
import dayjs from 'dayjs';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
@ -26,11 +25,10 @@
const appStore = useAppStore();
const { t } = useI18n();
const isShowTip = ref<boolean>(true);
const visitedKey = 'scenarioHistoryTip';
const { addVisited, getIsVisited } = useVisit(visitedKey);
const props = defineProps<{
sourceId: string | number;
sourceId?: string | number;
}>();
const columns: MsTableColumn = [
{

View File

@ -11,10 +11,10 @@
>
<template #title>
<div class="flex items-center gap-[8px]">
<stepType
v-if="props.requestType"
v-show="props.requestType !== ScenarioStepType.CUSTOM_API"
:type="props.requestType"
<stepTypeVue
v-if="props.step"
v-show="props.step.stepType !== ScenarioStepType.CUSTOM_REQUEST"
:step="props.step"
/>
{{ title }}
</div>
@ -22,23 +22,24 @@
<div v-show="requestVModel.useEnv === 'false'" class="text-[14px] font-normal text-[var(--color-text-4)]">
{{ t('apiScenario.env', { name: props.envDetailItem?.name }) }}
</div>
<MsSelect
v-model:model-value="requestVModel.useEnv"
:allow-search="false"
:options="[
{ label: t('common.quote'), value: 'true' },
{ label: t('common.notQuote'), value: 'false' },
]"
:multiple="false"
value-key="value"
label-key="label"
:prefix="t('project.environmental.env')"
class="w-[150px]"
@change="handleUseEnvChange"
>
</MsSelect>
<a-select v-model:model-value="requestVModel.useEnv" class="w-[150px]" @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>
</template>
<a-empty
v-if="pluginError && !isHttpProtocol"
:description="t('apiTestDebug.noPlugin')"
class="h-[200px] items-center justify-center"
>
<template #image>
<MsIcon type="icon-icon_plugin_outlined" size="48" />
</template>
</a-empty>
<div v-show="!pluginError || isHttpProtocol" class="flex h-full flex-col">
<div class="px-[18px] pt-[8px]">
<div class="flex flex-wrap items-center justify-between gap-[12px]">
@ -48,7 +49,7 @@
v-model:model-value="requestVModel.protocol"
:options="protocolOptions"
:loading="protocolLoading"
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
:disabled="_stepType.isQuoteApi"
class="w-[90px]"
@change="(val) => handleActiveDebugProtocolChange(val as string)"
/>
@ -60,15 +61,15 @@
is-tag
class="flex items-center"
/>
<a-tooltip v-if="!isHttpProtocol" :content="requestVModel.label" :mouse-enter-delay="500">
<div class="one-line-text max-w-[350px]"> {{ requestVModel.label }}</div>
<a-tooltip v-if="!isHttpProtocol" :content="requestVModel.name" :mouse-enter-delay="500">
<div class="one-line-text max-w-[350px]"> {{ requestVModel.name }}</div>
</a-tooltip>
</div>
<a-input-group v-if="isHttpProtocol" class="flex-1">
<apiMethodSelect
v-model:model-value="requestVModel.method"
class="w-[140px]"
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
:disabled="_stepType.isQuoteApi"
@change="handleActiveDebugChange"
/>
<a-input
@ -78,7 +79,7 @@
allow-clear
class="hover:z-10"
:style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''"
:disabled="props.requestType === ScenarioStepType.QUOTE_API"
:disabled="_stepType.isQuoteApi"
@input="() => (isUrlError = false)"
@change="handleUrlChange"
/>
@ -109,11 +110,7 @@
</div>
</div>
<a-input
v-if="
props.requestType &&
[ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(props.requestType) &&
isHttpProtocol
"
v-if="props.step?.stepType && (_stepType.isCopyApi || _stepType.isQuoteApi) && isHttpProtocol"
v-model:model-value="requestVModel.name"
:max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
@ -130,7 +127,7 @@
class="no-content relative mt-[8px] border-b"
/>
</div>
<div ref="splitContainerRef" class="request-and-response h-[calc(100%-87px)]">
<div ref="splitContainerRef" class="h-[calc(100%-87px)]">
<MsSplitBox
ref="verticalSplitBoxRef"
v-model:size="splitBoxSize"
@ -150,7 +147,7 @@
>
<div class="tab-pane-container">
<a-spin
v-if="requestVModel.activeTab === RequestComposition.PLUGIN"
v-show="requestVModel.activeTab === RequestComposition.PLUGIN"
:loading="pluginLoading"
class="min-h-[100px] w-full"
>
@ -267,15 +264,6 @@
</MsSplitBox>
</div>
</div>
<a-empty
v-if="pluginError && !isHttpProtocol"
:description="t('apiTestDebug.noPlugin')"
class="h-[200px] items-center justify-center"
>
<template #image>
<MsIcon type="icon-icon_plugin_outlined" size="48" />
</template>
</a-empty>
<!-- <addDependencyDrawer v-model:visible="showAddDependencyDrawer" :mode="addDependencyMode" /> -->
</MsDrawer>
</template>
@ -291,8 +279,7 @@
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue';
import MsSelect from '@/components/business/ms-select';
import stepType from './stepType.vue';
import stepTypeVue from './stepType/stepType.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
import auth from '@/views/api-test/components/requestComposition/auth.vue';
@ -316,6 +303,7 @@
PluginConfig,
RequestTaskResult,
} from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import {
RequestAuthType,
@ -327,6 +315,7 @@
ScenarioStepType,
} from '@/enums/apiEnum';
import getStepType from './stepType/utils';
import {
defaultBodyParams,
defaultBodyParamsItem,
@ -366,8 +355,7 @@
const props = defineProps<{
request?: RequestParam; //
requestType?: ScenarioStepType;
stepName: string;
step?: ScenarioStepItem;
detailLoading?: boolean; //
envDetailItem?: {
id?: string;
@ -402,12 +390,9 @@
type: 'api',
id: '',
useEnv: 'false',
moduleId: 'root',
protocol: 'HTTP',
url: '',
activeTab: RequestComposition.HEADER,
label: t('apiTestDebug.newApi'),
closable: true,
method: RequestMethods.GET,
unSaved: false,
headers: [],
@ -415,9 +400,7 @@
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
@ -455,16 +438,25 @@
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseActiveTab: ResponseComposition.BODY,
isNew: true,
executeLoading: false,
};
const requestVModel = ref<RequestParam>(props.request || defaultDebugParams);
const requestVModel = ref<RequestParam>(defaultDebugParams);
const _stepType = computed(() => {
if (props.step) {
return getStepType(props.step);
}
return {
isCopyApi: false,
isQuoteApi: false,
};
});
const title = computed(() => {
if (props.requestType && [ScenarioStepType.COPY_API, ScenarioStepType.QUOTE_API].includes(props.requestType)) {
return props.stepName;
if (_stepType.value.isCopyApi || _stepType.value.isQuoteApi) {
return props.step?.name;
}
return t('apiScenario.customApi');
});
@ -604,11 +596,9 @@
const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[requestVModel.value.protocol]?.script || []
);
const isCopyApiNeedInit = computed(
() => props.requestType === ScenarioStepType.COPY_API && props.request?.request === null
);
const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request?.request === null);
const isEditableApi = computed(
() => props.requestType === ScenarioStepType.COPY_API || props.requestType === ScenarioStepType.CUSTOM_API
() => _stepType.value.isCopyApi || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.step
);
//
@ -649,7 +639,7 @@
controlPluginFormFields().forEach((key) => {
form[key] = formData[key];
});
fApi.value?.setValue(form);
fApi.value?.setValue(cloneDeep(form));
setTimeout(() => {
// 300ms handlePluginFormChange
isInitPluginForm.value = true;
@ -657,8 +647,10 @@
});
}
} else {
nextTick(() => {
fApi.value?.nextTick(() => {
controlPluginFormFields();
fApi.value?.resetFields();
isInitPluginForm.value = true;
});
}
}
@ -802,11 +794,13 @@
watch(
() => showResponse.value,
(val) => {
if (val) {
changeVerticalExpand(true);
} else {
changeVerticalExpand(false);
}
nextTick(() => {
if (val) {
changeVerticalExpand(true);
} else {
changeVerticalExpand(false);
}
});
}
);
@ -869,13 +863,13 @@
*/
function makeRequestParams(executeType?: 'localExec' | 'serverExec') {
const isExecute = executeType === 'localExec' || executeType === 'serverExec';
const { formDataBody, wwwFormBody } = requestVModel.value.body;
const polymorphicName = protocolOptions.value.find(
(e) => e.value === requestVModel.value.protocol
)?.polymorphicName; //
let parseRequestBodyResult;
let requestParams;
if (isHttpProtocol.value) {
const { formDataBody, wwwFormBody } = requestVModel.value.body;
const realFormDataBodyValues = filterKeyValParams(
formDataBody.formValues,
defaultBodyParamsItem,
@ -903,7 +897,6 @@
},
},
headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem, isExecute).validParams,
method: requestVModel.value.method,
otherConfig: requestVModel.value.otherConfig,
path: requestVModel.value.url || requestVModel.value.path,
query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem, isExecute).validParams,
@ -917,40 +910,26 @@
polymorphicName,
};
}
reportId.value = getGenerateId();
requestVModel.value.reportId = reportId.value; // ID
debugSocket(executeType); // websocket
let requestName = '';
let requestModuleId = '';
const apiDefinitionParams: Record<string, any> = {};
requestName = requestVModel.value.name;
requestModuleId = requestVModel.value.moduleId;
if (isExecute) {
debugSocket(executeType); // websocket
}
return {
id: requestVModel.value.id.toString(),
reportId: reportId.value,
environmentId: props.envDetailItem?.id || '',
name: requestName,
moduleId: requestModuleId,
...apiDefinitionParams,
...requestParams,
id: requestVModel.value.id,
activeTab: requestVModel.value.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
responseActiveTab: ResponseComposition.BODY,
protocol: requestVModel.value.protocol,
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
path: isHttpProtocol.value ? requestVModel.value.url || requestVModel.value.path : undefined,
request: {
...requestParams,
name: requestName,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: requestVModel.value.children[0].assertionConfig,
postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
},
],
},
name: requestVModel.value.name,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: requestVModel.value.children[0].assertionConfig,
postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
},
],
...parseRequestBodyResult,
projectId: appStore.currentProjectId,
frontendDebug: executeType === 'localExec',
isNew: requestVModel.value.isNew,
};
}
@ -959,7 +938,6 @@
* @param val 执行类型
*/
async function execute(executeType?: 'localExec' | 'serverExec') {
// todo
if (isHttpProtocol.value) {
try {
if (!props.executeApi) return;
@ -1008,8 +986,7 @@
}
function handleContinue() {
requestVModel.value.isNew = false; //
emit('addStep', requestVModel.value);
emit('addStep', cloneDeep(makeRequestParams()));
}
function handleSave() {
@ -1020,7 +997,7 @@
function handleClose() {
// applyStep
if (!requestVModel.value.isNew) {
emit('applyStep', { ...requestVModel.value, ...makeRequestParams() });
emit('applyStep', cloneDeep(makeRequestParams()));
}
}
@ -1037,7 +1014,6 @@
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
}
requestVModel.value = {
responseActiveTab: ResponseComposition.BODY,
executeLoading: false,
activeTab: res.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
unSaved: false,
@ -1046,7 +1022,6 @@
...res.request,
...res,
response: cloneDeep(defaultResponse),
responseDefinition: res.response.map((e) => ({ ...e, responseActiveTab: ResponseComposition.BODY })),
url: res.path,
name: res.name, // requestnamenull
id: res.id,
@ -1068,9 +1043,10 @@
async (val) => {
if (val) {
if (props.request) {
requestVModel.value = { ...defaultDebugParams, ...props.request };
console.log('props.request', props.request);
requestVModel.value = cloneDeep(props.request);
if (
props.requestType === ScenarioStepType.QUOTE_API ||
_stepType.value.isQuoteApi ||
isCopyApiNeedInit.value
// (request.requestrequest null)
) {
@ -1106,12 +1082,20 @@
// });
// }
// }
} else {
requestVModel.value = cloneDeep({
...defaultDebugParams,
id: getGenerateId(),
});
}
await initProtocolList();
if (props.request) {
handleActiveDebugProtocolChange(requestVModel.value.protocol);
}
}
},
{
immediate: true,
}
);
</script>

View File

@ -1,50 +0,0 @@
<template>
<div
class="text-nowrap rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
:style="{
borderColor: type.color,
color: type.color,
}"
>
{{ type.label }}
</div>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepType } from '@/enums/apiEnum';
const props = defineProps<{
type: ScenarioStepType;
}>();
const { t } = useI18n();
//
const scenarioStepMap = {
[ScenarioStepType.QUOTE_API]: { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.COPY_API]: { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' },
[ScenarioStepType.QUOTE_CASE]: { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.COPY_CASE]: { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' },
[ScenarioStepType.QUOTE_SCENARIO]: { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.COPY_SCENARIO]: { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' },
[ScenarioStepType.WAIT_TIME]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' },
[ScenarioStepType.LOOP_CONTROL]: { label: 'apiScenario.loopControl', color: 'rgba(167, 98, 191, 1)' },
[ScenarioStepType.CONDITION_CONTROL]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' },
[ScenarioStepType.ONLY_ONCE_CONTROL]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' },
[ScenarioStepType.SCRIPT_OPERATION]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
[ScenarioStepType.CUSTOM_API]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
};
const type = computed(() => {
const config = scenarioStepMap[props.type];
return {
border: `1px solid ${config?.color}`,
color: config?.color,
label: t(config?.label),
};
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,63 @@
<template>
<div
class="text-nowrap rounded-[0_999px_999px_0] border border-solid px-[8px] py-[2px] text-[12px] leading-[16px]"
:style="{
borderColor: type.color,
color: type.color,
}"
>
{{ type.label }}
</div>
</template>
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum';
import getStepType from './utils';
const props = defineProps<{
step: ScenarioStepItem;
}>();
const { t } = useI18n();
//
const scenarioStepMap = {
[ScenarioStepType.CONSTANT_TIMER]: { label: 'apiScenario.waitTime', color: 'rgb(var(--warning-6))' },
[ScenarioStepType.LOOP_CONTROLLER]: { label: 'apiScenario.loopControl', color: 'rgba(167, 98, 191, 1)' },
[ScenarioStepType.IF_CONTROLLER]: { label: 'apiScenario.conditionControl', color: 'rgba(238, 80, 163, 1)' },
[ScenarioStepType.ONCE_ONLY_CONTROLLER]: { label: 'apiScenario.onlyOnceControl', color: 'rgba(211, 68, 0, 1)' },
[ScenarioStepType.SCRIPT]: { label: 'apiScenario.scriptOperation', color: 'rgba(20, 225, 198, 1)' },
[ScenarioStepType.CUSTOM_REQUEST]: { label: 'apiScenario.customApi', color: 'rgb(var(--link-4))' },
};
const type = computed(() => {
let config = scenarioStepMap[props.step.stepType];
const stepType = getStepType(props.step);
if (!config) {
if (stepType.isQuoteApi) {
config = { label: 'apiScenario.quoteApi', color: 'rgb(var(--link-7))' };
} else if (stepType.isCopyApi) {
config = { label: 'apiScenario.copyApi', color: 'rgb(var(--link-7))' };
} else if (stepType.isQuoteCase) {
config = { label: 'apiScenario.quoteCase', color: 'rgb(var(--success-7))' };
} else if (stepType.isCopyCase) {
config = { label: 'apiScenario.copyCase', color: 'rgb(var(--success-7))' };
} else if (stepType.isQuoteScenario) {
config = { label: 'apiScenario.quoteScenario', color: 'rgb(var(--primary-7))' };
} else if (stepType.isCopyScenario) {
config = { label: 'apiScenario.copyScenario', color: 'rgb(var(--primary-7))' };
}
}
return {
border: `1px solid ${config?.color}`,
color: config?.color,
label: t(config?.label),
};
});
</script>
<style lang="less" scoped></style>

View File

@ -0,0 +1,22 @@
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
export default function getStepType(step: ScenarioStepItem) {
const isCopyApi = step.stepType === ScenarioStepType.API && step.refType === ScenarioStepRefType.COPY;
const isQuoteApi = step.stepType === ScenarioStepType.API && step.refType === ScenarioStepRefType.REF;
const isCopyCase = step.stepType === ScenarioStepType.API_CASE && step.refType === ScenarioStepRefType.COPY;
const isQuoteCase = step.stepType === ScenarioStepType.API_CASE && step.refType === ScenarioStepRefType.REF;
const isCopyScenario = step.stepType === ScenarioStepType.API_SCENARIO && step.refType === ScenarioStepRefType.COPY;
const isQuoteScenario =
step.stepType === ScenarioStepType.API_SCENARIO &&
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(step.refType);
return {
isCopyApi,
isQuoteApi,
isCopyCase,
isQuoteCase,
isCopyScenario,
isQuoteScenario,
};
}

View File

@ -1,64 +1,144 @@
import { ScenarioStepLoopType, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
import { Scenario } from '@/models/apiTest/scenario';
import {
ApiScenarioStatus,
RequestAssertionCondition,
ScenarioFailureStrategy,
ScenarioStepLoopTypeEnum,
WhileConditionType,
} from '@/enums/apiEnum';
export const defaultStepItemCommon = {
checked: false,
expanded: false,
enabled: true,
enable: true,
children: [],
loopNum: 0,
loopType: 'num' as ScenarioStepLoopType,
loopSpace: 0,
variableName: '',
variablePrefix: '',
loopWhileType: 'condition' as ScenarioStepLoopWhileType,
variableVal: '',
condition: 'equal',
overTime: 0,
expression: '',
waitTime: 0,
description: '',
config: {
id: '',
copyFromStepId: '', // 如果步骤是复制的这个字段是复制的步骤id
name: '',
enable: true,
polymorphicName: '', // 多态名称,用于后台区分使用的是哪个组件
// 自定义请求
customizeRequest: false, // 是否自定义请求
customizeRequestEnvEnable: false, // 是否启用环境
// 条件控制器
value: '', // 变量值
variable: '', // 变量名
condition: RequestAssertionCondition.EQUALS, // 条件操作符
loopType: ScenarioStepLoopTypeEnum.LOOP_COUNT,
forEachController: {
loopTime: 0, // 循环间隔时间
value: '', // 变量值
variable: '', // 变量名
},
msCountController: {
loops: 0, // 循环次数
},
whileController: {
conditionType: WhileConditionType.CONDITION, // 条件类型
timeout: 0, // 超时时间
msWhileScript: {
scriptValue: '', // 脚本值
}, // 脚本
msWhileVariable: {
condition: RequestAssertionCondition.EQUALS, // 条件操作符
value: '', // 变量值
variable: '', // 变量名
}, // 变量
},
waitTime: 0, // 等待时间
},
createActionsVisible: false,
};
export const defaultScenario: Scenario = {
name: '',
moduleId: '',
priority: 'P0',
status: ApiScenarioStatus.UNDERWAY,
tags: [],
projectId: '',
description: '',
grouped: false,
environmentId: '',
scenarioConfig: {
variable: {
commonVariables: [],
csvVariables: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
assertionConfig: {
assertions: [],
},
otherConfig: {
enableGlobalCookie: true,
enableCookieShare: false,
enableStepWait: false,
stepWaitTime: 1000,
failureStrategy: ScenarioFailureStrategy.CONTINUE,
},
},
steps: [],
stepDetails: {},
executeTime: 0,
executeSuccessCount: 0,
executeFailCount: 0,
uploadFileIds: [],
linkFileIds: [],
// 前端渲染字段
label: '',
closable: true,
isNew: true,
unSaved: false,
executeLoading: false, // 执行loading
};
export const conditionOptions = [
{
value: 'equal',
value: RequestAssertionCondition.EQUALS,
label: 'apiScenario.equal',
},
{
value: 'notEqualTo',
value: RequestAssertionCondition.NOT_EQUALS,
label: 'apiScenario.notEqualTo',
},
{
value: 'greater',
value: RequestAssertionCondition.GT,
label: 'apiScenario.greater',
},
{
value: 'less',
value: RequestAssertionCondition.LT,
label: 'apiScenario.less',
},
{
value: 'greaterOrEqual',
value: RequestAssertionCondition.GT_OR_EQUALS,
label: 'apiScenario.greaterOrEqual',
},
{
value: 'lessOrEqual',
value: RequestAssertionCondition.LT_OR_EQUALS,
label: 'apiScenario.lessOrEqual',
},
{
value: 'include',
value: RequestAssertionCondition.CONTAINS,
label: 'apiScenario.include',
},
{
value: 'notInclude',
value: RequestAssertionCondition.NOT_CONTAINS,
label: 'apiScenario.notInclude',
},
{
value: 'null',
value: RequestAssertionCondition.EMPTY,
label: 'apiScenario.null',
},
{
value: 'notNull',
value: RequestAssertionCondition.NOT_EMPTY,
label: 'apiScenario.notNull',
},
];

View File

@ -96,7 +96,6 @@
import { getExecuteHistory } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ExecuteHistoryItem } from '@/models/apiTest/scenario';
import { ExecuteStatusFilters } from '@/enums/apiEnum';
@ -108,11 +107,10 @@
const statusFilters = ref(Object.keys(ExecuteStatusFilters));
const tableQueryParams = ref<any>();
const appStore = useAppStore();
const keyword = ref('');
const { t } = useI18n();
const props = defineProps<{
scenarioId: string; // id
scenarioId?: string | number; // id
readOnly?: boolean;
}>();
const columns: MsTableColumn = [
@ -174,7 +172,7 @@
},
];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
const { propsRes, propsEvent, loadList, setLoadListParams } = useTable(
getExecuteHistory,
{
columns,

View File

@ -42,21 +42,22 @@
import batchAddKeyVal from '@/views/api-test/components/batchAddKeyVal.vue';
import paramTable, { ParamTableColumn } from '@/views/api-test/components/paramTable.vue';
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<{
activeKey?: string;
params: any[];
params: CommonVariable[];
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;
(e: 'update:params', value: CommonVariable[]): void;
(e: 'change'): void; //
}>();
const innerParams = useVModel(props, 'params', emit);
const isShowTip = ref<boolean>(true);
const { t } = useI18n();
const activeRadio = ref('convention');
const searchValue = ref('');
const firstSearch = ref(true);
const backupParams = ref(props.params);

View File

@ -14,22 +14,22 @@
import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue';
import precondition from '@/views/api-test/components/requestComposition/precondition.vue';
import { ExecuteConditionConfig } from '@/models/apiTest/common';
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const preProcessorConfig = ref({
enableGlobal: false,
processors: [],
const preProcessorConfig = defineModel<ExecuteConditionConfig>('preProcessorConfig', {
required: true,
});
const postProcessorConfig = ref({
enableGlobal: false,
processors: [],
const postProcessorConfig = defineModel<ExecuteConditionConfig>('postProcessorConfig', {
required: true,
});
</script>
<style lang="less" scoped>
.condition {
flex-shrink: 0;
overflow: overlay;
width: 100%;
height: 700px;
overflow: overlay;
flex-shrink: 0;
}
</style>

View File

@ -171,7 +171,7 @@
readOnly: false,
}
);
const emit = defineEmits(['init', 'newScenario', 'import', 'folderNodeSelect', 'clickScenario', 'changeProtocol']);
const emit = defineEmits(['init', 'newScenario', 'import', 'folderNodeSelect', 'changeProtocol']);
const appStore = useAppStore();
const { t } = useI18n();

View File

@ -129,7 +129,7 @@
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
type="text"
class="!mr-0"
@click="Message.info('// todo @ba1q1')"
@click="openScenarioTab(record)"
>
{{ t('apiScenario.execute') }}
</MsButton>
@ -138,7 +138,7 @@
v-permission="['PROJECT_API_SCENARIO:READ+ADD']"
type="text"
class="!mr-0"
@click="Message.info('// todo @ba1q1')"
@click="openScenarioTab(record, true)"
>
{{ t('common.copy') }}
</MsButton>
@ -317,6 +317,7 @@
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
@ -349,6 +350,11 @@
offspringIds: string[];
readOnly?: boolean; //
}>();
const emit = defineEmits<{
(e: 'openScenario', record: ApiScenarioTableItem, isCopy?: boolean): void;
(e: 'refreshModuleTree', params: any): void;
}>();
const lastReportStatusFilterVisible = ref(false);
const lastReportStatusListFilters = ref<string[]>(Object.keys(ReportStatus[ReportEnum.API_SCENARIO_REPORT]));
const lastReportStatusFilters = computed(() => {
@ -375,7 +381,6 @@
text: 'P3',
},
]);
const emit = defineEmits(['refreshModuleTree']);
const keyword = ref('');
const moveModalVisible = ref(false);
const isBatchMove = ref(false); //
@ -869,6 +874,7 @@
loadScenarioList(true);
resetSelector();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
scenarioBatchOptTreeLoading.value = false;
@ -933,8 +939,8 @@
}
}
function openScenarioTab(record: ApiScenarioTableItem) {
Message.info('// todo @ba1q1');
function openScenarioTab(record: ApiScenarioTableItem, isCopy = false) {
emit('openScenario', record, isCopy);
}
defineExpose({

View File

@ -12,11 +12,11 @@
</a-tooltip>
</div>
<div class="mb-[16px] mt-[10px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.envCookie" type="line" size="small" />
<a-switch v-model:model-value="form.enableGlobalCookie" type="line" size="small" />
{{ t('apiScenario.setting.environment.cookie') }}
</div>
<div class="mb-[16px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.shareCookie" type="line" size="small" />
<a-switch v-model:model-value="form.enableCookieShare" type="line" size="small" />
{{ t('apiScenario.setting.share.cookie') }}
<a-tooltip :content="t('apiScenario.setting.share.cookie.tip')" position="right">
<div>
@ -31,7 +31,7 @@
{{ t('apiScenario.setting.run.config') }}
</div>
<div class="mb-[16px] mt-[10px] flex items-center gap-[8px]">
<a-switch v-model:model-value="form.waitTime" type="line" size="small" />
<a-switch v-model:model-value="form.enableStepWait" type="line" size="small" />
{{ t('apiScenario.setting.step.waitTime') }}
<a-tooltip :content="t('apiScenario.setting.waitTime.tip')">
<div>
@ -43,14 +43,14 @@
</a-tooltip>
</div>
<a-form-item v-if="form.waitTime" class="flex-1">
<a-form-item v-if="form.stepWaitTime" class="flex-1">
<template #label>
<div class="flex items-center">
{{ t('apiScenario.setting.waitTime') }}
<div class="text-[var(--color-text-brand)]">(ms)</div>
</div>
</template>
<a-input-number v-model:model-value="form.connectTimeout" mode="button" :step="100" :min="0" class="w-[160px]" />
<a-input-number v-model:model-value="form.stepWaitTime" mode="button" :step="100" :min="0" class="w-[160px]" />
</a-form-item>
<a-form-item class="flex-1">
@ -59,9 +59,9 @@
{{ t('apiScenario.setting.step.rule') }}
</div>
</template>
<a-radio-group v-model:model-value="form.rule">
<a-radio value="ignore">{{ t('apiScenario.setting.step.rule.ignore') }}</a-radio>
<a-radio value="stop">{{ t('apiScenario.setting.step.rule.stop') }}</a-radio>
<a-radio-group v-model:model-value="form.failureStrategy">
<a-radio :value="ScenarioFailureStrategy.CONTINUE">{{ t('apiScenario.setting.step.rule.ignore') }}</a-radio>
<a-radio :value="ScenarioFailureStrategy.STOP">{{ t('apiScenario.setting.step.rule.stop') }}</a-radio>
</a-radio-group>
</a-form-item>
</a-form>
@ -70,21 +70,14 @@
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { OtherConfig } from '@/models/apiTest/scenario';
import { ScenarioFailureStrategy } from '@/enums/apiEnum';
const { t } = useI18n();
// const emit = defineEmits(['update:formModeValue']); // ?
// const props = defineProps<{
//
// }>();
const initForm = {
envCookie: false,
shareCookie: false,
waitTime: false,
connectTimeout: 0,
rule: 'ignore',
};
const form = ref({ ...initForm });
const form = defineModel<OtherConfig>('otherConfig', {
required: true,
});
</script>
<style lang="less" scoped></style>

View File

@ -45,13 +45,13 @@
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import { ScenarioStepItem } from '../stepTree.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { findNodeByKey, getGenerateId } from '@/utils';
import { CreateStepAction } from '@/models/apiTest/scenario';
import { ScenarioAddStepActionType, ScenarioStepType } from '@/enums/apiEnum';
import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioAddStepActionType, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import useCreateActions from './useCreateActions';
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
@ -74,6 +74,7 @@
);
}>();
const appStore = useAppStore();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', {
@ -101,9 +102,10 @@
if (step.value && props.createStepAction) {
handleCreateStep(
{
type: ScenarioStepType.LOOP_CONTROL,
stepType: ScenarioStepType.LOOP_CONTROLLER,
name: t('apiScenario.loopControl'),
} as ScenarioStepItem,
projectId: appStore.currentProjectId,
},
step.value,
steps.value,
props.createStepAction,
@ -112,10 +114,12 @@
} else {
steps.value.push({
...cloneDeep(defaultStepItemCommon),
stepId: getGenerateId(),
order: steps.value.length + 1,
type: ScenarioStepType.LOOP_CONTROL,
id: getGenerateId(),
sort: steps.value.length + 1,
stepType: ScenarioStepType.LOOP_CONTROLLER,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.loopControl'),
projectId: appStore.currentProjectId,
});
}
break;
@ -123,9 +127,10 @@
if (step.value && props.createStepAction) {
handleCreateStep(
{
type: ScenarioStepType.CONDITION_CONTROL,
stepType: ScenarioStepType.IF_CONTROLLER,
name: t('apiScenario.conditionControl'),
} as ScenarioStepItem,
projectId: appStore.currentProjectId,
},
step.value,
steps.value,
props.createStepAction,
@ -134,10 +139,12 @@
} else {
steps.value.push({
...cloneDeep(defaultStepItemCommon),
stepId: getGenerateId(),
order: steps.value.length + 1,
type: ScenarioStepType.CONDITION_CONTROL,
id: getGenerateId(),
sort: steps.value.length + 1,
stepType: ScenarioStepType.IF_CONTROLLER,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.conditionControl'),
projectId: appStore.currentProjectId,
});
}
break;
@ -145,9 +152,10 @@
if (step.value && props.createStepAction) {
handleCreateStep(
{
type: ScenarioStepType.ONLY_ONCE_CONTROL,
stepType: ScenarioStepType.ONCE_ONLY_CONTROLLER,
name: t('apiScenario.onlyOnceControl'),
} as ScenarioStepItem,
projectId: appStore.currentProjectId,
},
step.value,
steps.value,
props.createStepAction,
@ -156,10 +164,12 @@
} else {
steps.value.push({
...cloneDeep(defaultStepItemCommon),
stepId: getGenerateId(),
order: steps.value.length + 1,
type: ScenarioStepType.ONLY_ONCE_CONTROL,
id: getGenerateId(),
sort: steps.value.length + 1,
stepType: ScenarioStepType.ONCE_ONLY_CONTROLLER,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.onlyOnceControl'),
projectId: appStore.currentProjectId,
});
}
break;
@ -167,9 +177,10 @@
if (step.value && props.createStepAction) {
handleCreateStep(
{
type: ScenarioStepType.WAIT_TIME,
stepType: ScenarioStepType.CONSTANT_TIMER,
name: t('apiScenario.waitTime'),
} as ScenarioStepItem,
projectId: appStore.currentProjectId,
},
step.value,
steps.value,
props.createStepAction,
@ -178,10 +189,12 @@
} else {
steps.value.push({
...cloneDeep(defaultStepItemCommon),
stepId: getGenerateId(),
order: steps.value.length + 1,
type: ScenarioStepType.WAIT_TIME,
id: getGenerateId(),
sort: steps.value.length + 1,
stepType: ScenarioStepType.CONSTANT_TIMER,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.waitTime'),
projectId: appStore.currentProjectId,
});
}
break;
@ -189,7 +202,7 @@
case ScenarioAddStepActionType.CUSTOM_API:
case ScenarioAddStepActionType.SCRIPT_OPERATION:
if (step.value) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.stepId, 'stepId');
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.value.id, 'id');
if (realStep) {
emit('otherCreate', val, realStep as ScenarioStepItem);
}

View File

@ -6,7 +6,7 @@
position="br"
@popup-visible-change="handleActionTriggerChange"
>
<MsButton :id="step.stepId" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')">
<MsButton :id="step.id" type="icon" class="ms-tree-node-extra__btn !mr-[4px]" @click="emit('click')">
<MsIcon type="icon-icon_add_outlined" size="14" class="text-[var(--color-text-4)]" />
</MsButton>
<template #content>
@ -63,13 +63,12 @@
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { ScenarioStepItem } from '../stepTree.vue';
import createStepActions from './createStepActions.vue';
import { useI18n } from '@/hooks/useI18n';
import { CreateStepAction } from '@/models/apiTest/scenario';
import { ScenarioAddStepActionType, ScenarioStepType } from '@/enums/apiEnum';
import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioAddStepActionType, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
const props = defineProps<{
step: ScenarioStepItem;
@ -106,12 +105,15 @@
);
const showAddChildStep = computed(() => {
return [
ScenarioStepType.LOOP_CONTROL,
ScenarioStepType.CONDITION_CONTROL,
ScenarioStepType.ONLY_ONCE_CONTROL,
ScenarioStepType.COPY_SCENARIO,
].includes(innerStep.value.type);
return (
[
ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.IF_CONTROLLER,
ScenarioStepType.ONCE_ONLY_CONTROLLER,
].includes(innerStep.value.stepType) ||
(innerStep.value.stepType === ScenarioStepType.API_SCENARIO &&
innerStep.value.refType === ScenarioStepRefType.COPY)
);
});
const activeCreateAction = ref<CreateStepAction>();
@ -131,7 +133,7 @@
function handleActionsClose() {
activeCreateAction.value = undefined;
innerStep.value.createActionsVisible = false;
document.getElementById(innerStep.value.stepId.toString())?.click();
document.getElementById(innerStep.value.id.toString())?.click();
}
</script>

View File

@ -1,12 +1,10 @@
import { cloneDeep } from 'lodash-es';
import { ScenarioStepItem } from '../stepTree.vue';
import { useI18n } from '@/hooks/useI18n';
import { getGenerateId, insertNodes, TreeNode } from '@/utils';
import { CreateStepAction } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum';
import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import { defaultStepItemCommon } from '../../config';
@ -15,7 +13,7 @@ export default function useCreateActions() {
/**
*
* @param selectedKeys stepId
* @param selectedKeys id
* @param steps
* @param parent
*/
@ -24,9 +22,9 @@ export default function useCreateActions() {
steps: (ScenarioStepItem | TreeNode<ScenarioStepItem>)[],
parent?: TreeNode<ScenarioStepItem>
) {
if (parent && selectedKeys.includes(parent.stepId)) {
if (parent && selectedKeys.includes(parent.id)) {
// 添加子节点时,当前节点已选中,则需要把新节点也需要选中(因为父级选中子级也会展示选中状态)
selectedKeys.push(...steps.map((item) => item.stepId));
selectedKeys.push(...steps.map((item) => item.id));
}
}
@ -36,10 +34,10 @@ export default function useCreateActions() {
* @param step
* @param steps
* @param createStepAction
* @param selectedKeys stepId
* @param selectedKeys id
*/
function handleCreateStep(
defaultStepInfo: ScenarioStepItem,
defaultStepInfo: Record<string, any>,
step: ScenarioStepItem,
steps: ScenarioStepItem[],
createStepAction: CreateStepAction,
@ -47,94 +45,71 @@ export default function useCreateActions() {
) {
const newStep = {
...cloneDeep(defaultStepItemCommon),
id: getGenerateId(),
...defaultStepInfo,
stepId: getGenerateId(),
};
switch (createStepAction) {
case 'inside':
newStep.order = step.children ? step.children.length : 0;
break;
case 'before':
newStep.order = step.order;
break;
case 'after':
default:
newStep.order = step.order + 1;
break;
}
console.log('newStep', newStep);
insertNodes<ScenarioStepItem>(
step.parent?.children || steps,
step.stepId,
step.id,
newStep,
createStepAction,
(newNode, parent) => checkedIfNeed(selectedKeys, [newNode], parent),
'stepId'
'id'
);
}
/**
*
* @param newSteps
* @param type
* @param stepType
* @param startOrder
*/
function buildInsertStepInfos(
newSteps: Record<string, any>[],
type: ScenarioStepType,
stepType: ScenarioStepType,
refType: ScenarioStepRefType,
startOrder: number,
stepsDetailMap: Record<string, any>
stepDetails: Record<string, any>,
projectId: string
): ScenarioStepItem[] {
let name: string;
switch (type) {
case ScenarioStepType.LOOP_CONTROL:
switch (stepType) {
case ScenarioStepType.LOOP_CONTROLLER:
name = t('apiScenario.loopControl');
break;
case ScenarioStepType.CONDITION_CONTROL:
case ScenarioStepType.IF_CONTROLLER:
name = t('apiScenario.conditionControl');
break;
case ScenarioStepType.ONLY_ONCE_CONTROL:
case ScenarioStepType.ONCE_ONLY_CONTROLLER:
name = t('apiScenario.onlyOnceControl');
break;
case ScenarioStepType.WAIT_TIME:
case ScenarioStepType.CONSTANT_TIMER:
name = t('apiScenario.waitTime');
break;
case ScenarioStepType.QUOTE_API:
name = t('apiScenario.quoteApi');
break;
case ScenarioStepType.COPY_API:
name = t('apiScenario.copyApi');
break;
case ScenarioStepType.QUOTE_CASE:
name = t('apiScenario.quoteCase');
break;
case ScenarioStepType.COPY_CASE:
name = t('apiScenario.copyCase');
break;
case ScenarioStepType.QUOTE_SCENARIO:
name = t('apiScenario.quoteScenario');
break;
case ScenarioStepType.COPY_SCENARIO:
name = t('apiScenario.copyScenario');
break;
case ScenarioStepType.CUSTOM_API:
case ScenarioStepType.CUSTOM_REQUEST:
name = t('apiScenario.customApi');
break;
case ScenarioStepType.SCRIPT_OPERATION:
case ScenarioStepType.SCRIPT:
name = t('apiScenario.scriptOperation');
break;
default:
break;
}
return newSteps.map((item, index) => {
const stepId = getGenerateId();
stepsDetailMap[stepId] = item; // 导入系统请求的引用接口和 case 的时候需要先存储一下引用的接口/用例信息
const id = getGenerateId();
stepDetails[id] = item; // 导入系统请求的引用接口和 case 的时候需要先存储一下引用的接口/用例信息
return {
...cloneDeep(defaultStepItemCommon),
...item,
stepId,
type,
name,
order: startOrder + index,
id,
stepType,
refType,
resourceId: item.id,
resourceName: item.name,
name: name || item.name,
sort: startOrder + index,
projectId,
};
});
}
@ -145,8 +120,7 @@ export default function useCreateActions() {
* @param readyInsertSteps buildInsertStepInfos得到构建后的步骤信息
* @param steps
* @param createStepAction
* @param type
* @param selectedKeys stepId
* @param selectedKeys id
*/
function handleCreateSteps(
step: ScenarioStepItem,
@ -157,11 +131,11 @@ export default function useCreateActions() {
) {
insertNodes<ScenarioStepItem>(
step.parent?.children || steps,
step.stepId,
step.id,
readyInsertSteps,
createStepAction,
undefined,
'stepId'
'id'
);
checkedIfNeed(selectedKeys, readyInsertSteps, step);
}

View File

@ -3,7 +3,7 @@
<div class="action-line">
<div class="action-group">
<a-checkbox
v-show="stepInfo.steps.length > 0"
v-show="scenario.steps.length > 0"
v-model:model-value="checkedAll"
:indeterminate="indeterminate"
@change="handleChangeAll"
@ -17,7 +17,7 @@
<div class="action-group">
<a-tooltip :content="isExpandAll ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
<a-button
v-show="stepInfo.steps.length > 0"
v-show="scenario.steps.length > 0"
type="outline"
class="expand-step-btn arco-btn-outline--secondary"
size="mini"
@ -42,20 +42,20 @@
</a-button>
</template>
</div>
<template v-if="stepInfo.executeTime">
<template v-if="scenario.executeTime">
<div class="action-group">
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeTime') }}</div>
<div class="text-[var(--color-text-4)]">{{ stepInfo.executeTime }}</div>
<div class="text-[var(--color-text-4)]">{{ scenario.executeTime }}</div>
</div>
<div class="action-group">
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeResult') }}</div>
<div class="flex items-center gap-[4px]">
<div class="text-[var(--color-text-1)]">{{ t('common.success') }}</div>
<div class="text-[rgb(var(--success-6))]">{{ stepInfo.executeSuccessCount }}</div>
<div class="text-[rgb(var(--success-6))]">{{ scenario.executeSuccessCount }}</div>
</div>
<div class="flex items-center gap-[4px]">
<div class="text-[var(--color-text-1)]">{{ t('common.fail') }}</div>
<div class="text-[rgb(var(--success-6))]">{{ stepInfo.executeFailCount }}</div>
<div class="text-[rgb(var(--success-6))]">{{ scenario.executeFailCount }}</div>
</div>
<MsButton type="text" @click="checkReport">{{ t('apiScenario.checkReport') }}</MsButton>
</div>
@ -82,11 +82,11 @@
<div class="h-[calc(100%-48px)]">
<stepTree
ref="stepTreeRef"
v-model:steps="stepInfo.steps"
v-model:steps="scenario.steps"
v-model:checked-keys="checkedKeys"
v-model:stepKeyword="keyword"
:expand-all="isExpandAll"
:steps-detail-map="stepInfo.stepsDetailMap"
:step-details="scenario.stepDetails"
/>
</div>
</div>
@ -116,29 +116,20 @@
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import stepTree, { ScenarioStepItem } from './stepTree.vue';
import stepTree from './stepTree.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { countNodes } from '@/utils/tree';
export interface ScenarioStepInfo {
id: string | number;
steps: ScenarioStepItem[];
executeTime?: string; //
executeSuccessCount?: number; //
executeFailCount?: number; //
stepsDetailMap: Record<string, any>; //
}
import { Scenario } from '@/models/apiTest/scenario';
const props = defineProps<{
isNew?: boolean; //
}>();
const appStore = useAppStore();
const { t } = useI18n();
const stepInfo = defineModel<ScenarioStepInfo>('step', {
const scenario = defineModel<Scenario>('scenario', {
required: true,
});
@ -149,7 +140,7 @@
const stepTreeRef = ref<InstanceType<typeof stepTree>>();
const keyword = ref('');
const totalStepCount = computed(() => countNodes(stepInfo.value.steps));
const totalStepCount = computed(() => countNodes(scenario.value.steps));
function handleChangeAll(value: boolean | (string | number | boolean)[]) {
indeterminate.value = false;
@ -202,7 +193,7 @@
try {
let ids = checkedKeys.value;
if (batchToggleRange.value === 'top') {
ids = stepInfo.value.steps.map((item) => item.stepId);
ids = scenario.value.steps.map((item) => item.id);
}
console.log('ids', ids);
await new Promise((resolve) => {

View File

@ -1,12 +1,12 @@
<template>
<div class="flex items-center gap-[4px]" draggable="false">
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName">
<a-tooltip :content="innerData.variable" :disabled="!innerData.variable">
<a-input
v-model:model-value="innerData.variableName"
v-model:model-value="innerData.variable"
size="mini"
class="w-[100px] px-[8px]"
:max-length="255"
:placeholder="t('apiScenario.variableName', { suffix: '${var}' })"
:placeholder="t('apiScenario.variable', { suffix: '${var}' })"
@change="handleInputChange"
>
</a-input>
@ -21,13 +21,13 @@
{{ t(opt.label) }}
</a-option>
</a-select>
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal">
<a-tooltip :content="innerData.value" :disabled="!innerData.value">
<a-input
:id="innerData.stepId"
v-model:model-value="innerData.variableVal"
:id="innerData.id"
v-model:model-value="innerData.value"
size="mini"
class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.variableVal')"
:placeholder="t('apiScenario.value')"
@change="handleInputChange"
>
</a-input>
@ -38,21 +38,16 @@
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { ConditionStepDetail } from '@/models/apiTest/scenario';
import { conditionOptions } from '@/views/api-test/scenario/components/config';
export interface ConditionContentProps {
stepId: string;
variableName: string;
condition: string;
variableVal: string;
}
const props = defineProps<{
data: ConditionContentProps;
data: ConditionStepDetail;
}>();
const emit = defineEmits<{
(e: 'change', innerData: ConditionContentProps): void;
(e: 'quickInput', dataKey: keyof ConditionContentProps): void;
(e: 'change', innerData: ConditionStepDetail): void;
(e: 'quickInput', dataKey: keyof ConditionStepDetail): void;
}>();
const { t } = useI18n();
@ -75,8 +70,8 @@
() => dbClick?.value.timeStamp,
() => {
// @ts-ignore
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.stepId)) {
emit('quickInput', 'variableVal');
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.id)) {
emit('quickInput', 'value');
}
}
);

View File

@ -9,12 +9,12 @@
@change="handleInputChange"
/>
<a-tooltip
v-if="innerData.loopType === 'num'"
:content="innerData.loopNum.toString()"
:disabled="!innerData.loopNum"
v-if="innerData.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT"
:content="innerData.msCountController.loops.toString()"
:disabled="!innerData.msCountController.loops"
>
<a-input-number
v-model:model-value="innerData.loopNum"
v-model:model-value="innerData.msCountController.loops"
class="w-[80px] px-[8px]"
size="mini"
:step="1"
@ -30,53 +30,56 @@
</a-input-number>
</a-tooltip>
</a-input-group>
<template v-if="innerData.loopType === 'forEach'">
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName">
<template v-if="innerData.loopType === ScenarioStepLoopTypeEnum.FOREACH">
<a-tooltip :content="innerData.forEachController.variable" :disabled="!innerData.forEachController.variable">
<a-input
v-model:model-value="innerData.variableName"
v-model:model-value="innerData.forEachController.variable"
size="mini"
class="w-[110px] px-[8px]"
:max-length="255"
:placeholder="t('apiScenario.variableName')"
:placeholder="t('apiScenario.variable')"
@change="handleInputChange"
>
</a-input>
</a-tooltip>
<div class="font-medium">in</div>
<a-tooltip :content="innerData.variablePrefix" :disabled="!innerData.variablePrefix">
<a-tooltip :content="innerData.forEachController.value" :disabled="!innerData.forEachController.value">
<a-input
v-model:model-value="innerData.variablePrefix"
v-model:model-value="innerData.forEachController.value"
size="mini"
class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.variablePrefix')"
:placeholder="t('apiScenario.valuePrefix')"
:max-length="255"
@change="handleInputChange"
>
</a-input>
</a-tooltip>
</template>
<template v-else-if="innerData.loopType === 'while'">
<template v-else-if="innerData.loopType === ScenarioStepLoopTypeEnum.WHILE">
<a-select
v-model:model-value="innerData.loopWhileType"
v-model:model-value="innerData.whileController.conditionType"
:options="whileOptions"
size="mini"
class="w-[75px] px-[8px]"
@change="handleInputChange"
/>
<template v-if="innerData.loopWhileType === 'condition'">
<a-tooltip :content="innerData.variableName" :disabled="!innerData.variableName">
<template v-if="innerData.whileController.conditionType === WhileConditionType.CONDITION">
<a-tooltip
:content="innerData.whileController.msWhileVariable.variable"
:disabled="!innerData.whileController.msWhileVariable.variable"
>
<a-input
v-model:model-value="innerData.variableName"
v-model:model-value="innerData.whileController.msWhileVariable.variable"
size="mini"
class="w-[100px] px-[8px]"
:max-length="255"
:placeholder="t('apiScenario.variableName', { suffix: '${var}' })"
:placeholder="t('apiScenario.variable', { suffix: '${var}' })"
@change="handleInputChange"
>
</a-input>
</a-tooltip>
<a-select
v-model:model-value="innerData.condition"
v-model:model-value="innerData.whileController.msWhileVariable.condition"
size="mini"
class="w-[90px] px-[8px]"
@change="handleInputChange"
@ -85,22 +88,29 @@
{{ t(opt.label) }}
</a-option>
</a-select>
<a-tooltip :content="innerData.variableVal" :disabled="!innerData.variableVal">
<a-tooltip
:content="innerData.whileController.msWhileVariable.value"
:disabled="!innerData.whileController.msWhileVariable.value"
>
<a-input
:id="innerData.stepId"
v-model:model-value="innerData.variableVal"
:id="stepId"
v-model:model-value="innerData.whileController.msWhileVariable.value"
size="mini"
class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.variableVal')"
:placeholder="t('apiScenario.value')"
@change="handleInputChange"
>
</a-input>
</a-tooltip>
</template>
<a-tooltip v-else :content="innerData.expression" :disabled="!innerData.expression">
<a-tooltip
v-else
:content="innerData.whileController.msWhileScript.scriptValue"
:disabled="!innerData.whileController.msWhileScript.scriptValue"
>
<a-input
:id="innerData.stepId"
v-model:model-value="innerData.expression"
:id="stepId"
v-model:model-value="innerData.whileController.msWhileScript.scriptValue"
size="mini"
class="w-[200px] px-[8px]"
:placeholder="t('apiScenario.expression')"
@ -108,9 +118,9 @@
>
</a-input>
</a-tooltip>
<a-tooltip :content="innerData.overTime.toString()" :disabled="!innerData.overTime">
<a-tooltip :content="innerData.whileController.timeout.toString()" :disabled="!innerData.whileController.timeout">
<a-input-number
v-model:model-value="innerData.overTime"
v-model:model-value="innerData.whileController.timeout"
class="w-[100px] px-[8px]"
size="mini"
:step="1"
@ -121,18 +131,18 @@
@blur="handleInputChange"
>
<template #prefix>
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.overTime') }}:</div>
<div class="text-[12px] text-[var(--color-text-4)]">{{ t('apiScenario.timeout') }}:</div>
</template>
</a-input-number>
</a-tooltip>
</template>
<a-tooltip
v-if="innerData.loopType !== 'while'"
:content="innerData.loopSpace.toString()"
:disabled="!innerData.loopSpace"
v-if="innerData.loopType !== ScenarioStepLoopTypeEnum.WHILE"
:content="innerData.forEachController.loopTime.toString()"
:disabled="!innerData.forEachController.loopTime"
>
<a-input-number
v-model:model-value="innerData.loopSpace"
v-model:model-value="innerData.forEachController.loopTime"
size="mini"
:step="1"
:min="0"
@ -153,34 +163,18 @@
<script setup lang="ts">
import { useI18n } from '@/hooks/useI18n';
import { ScenarioStepLoopType, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
import { ScenarioStepType } from '@/enums/apiEnum';
import { LoopStepDetail } from '@/models/apiTest/scenario';
import { ScenarioStepLoopTypeEnum, WhileConditionType } from '@/enums/apiEnum';
import { conditionOptions } from '@/views/api-test/scenario/components/config';
export interface LoopContentProps {
stepId: string | number;
num: number;
name: string;
type: ScenarioStepType;
loopNum: number;
loopType: ScenarioStepLoopType;
loopSpace: number;
variableName: string;
variablePrefix: string;
loopWhileType: ScenarioStepLoopWhileType;
variableVal: string;
condition: string;
overTime: number;
expression: string;
}
const props = defineProps<{
data: LoopContentProps;
data: LoopStepDetail;
stepId: string | number;
}>();
const emit = defineEmits<{
(e: 'change', innerData: LoopContentProps): void;
(e: 'quickInput', dataKey: keyof LoopContentProps): void;
(e: 'change', innerData: LoopStepDetail): void;
(e: 'quickInput', dataKey: string): void;
}>();
const { t } = useI18n();
@ -188,25 +182,25 @@
const innerData = ref(props.data);
const loopOptions = [
{
value: 'num',
value: ScenarioStepLoopTypeEnum.LOOP_COUNT,
label: t('apiScenario.num'),
},
{
value: 'while',
value: ScenarioStepLoopTypeEnum.WHILE,
label: 'while',
},
{
value: 'forEach',
value: ScenarioStepLoopTypeEnum.FOREACH,
label: 'forEach',
},
];
const whileOptions = [
{
value: 'condition',
value: WhileConditionType.CONDITION,
label: t('apiScenario.condition'),
},
{
value: 'expression',
value: WhileConditionType.SCRIPT,
label: t('apiScenario.expression'),
},
];
@ -227,8 +221,13 @@
() => dbClick?.value.timeStamp,
() => {
// @ts-ignore
if ((dbClick?.value.e?.target as Element).parentNode?.id.includes(innerData.value.stepId)) {
emit('quickInput', innerData.value.loopWhileType === 'condition' ? 'variableVal' : 'expression');
if ((dbClick?.value.e?.target as Element).parentNode?.id === props.stepId) {
emit(
'quickInput',
innerData.value.whileController.conditionType === WhileConditionType.CONDITION
? 'whileController.msWhileVariable.value'
: 'whileController.msWhileScript.scriptValue'
);
}
}
);

View File

@ -7,20 +7,20 @@
<div>
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div>
<div class="text-[14px] text-[var(--color-text-1)]">
{{ props.data.belongProjectName }}
<!-- {{ props.data.belongProjectName }} -->
</div>
</div>
<div>
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.detailName') }}</div>
<div class="cursor-pointer text-[14px] text-[rgb(var(--primary-5))]" @click="goDetail">
{{ `${props.data.num}${props.data.name}` }}
{{ `${props.data.resourceNum}${props.data.resourceName}` }}
</div>
</div>
</div>
</template>
</a-popover>
<MsTag
v-if="props.data.belongProjectId !== props.data.currentProjectId"
v-if="props.data.projectId !== appStore.currentProjectId"
theme="outline"
size="small"
:self-style="{
@ -40,39 +40,35 @@
import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage';
import useAppStore from '@/store/modules/app';
import { ScenarioStepType } from '@/enums/apiEnum';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
const props = defineProps<{
data: {
id: string | number;
stepId: string | number;
belongProjectId: string;
belongProjectName: string;
num: number;
name: string;
type: ScenarioStepType;
currentProjectId: string;
};
data: ScenarioStepItem;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const { openNewPage } = useOpenNewPage();
function goDetail() {
switch (props.data.type) {
case ScenarioStepType.COPY_API:
case ScenarioStepType.QUOTE_API:
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { dId: props.data.id });
const _stepType = getStepType(props.data);
switch (true) {
case _stepType.isCopyApi:
case _stepType.isQuoteApi:
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { dId: props.data.id, pId: props.data.projectId });
break;
case ScenarioStepType.QUOTE_SCENARIO:
case ScenarioStepType.COPY_SCENARIO:
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { sId: props.data.id });
case _stepType.isCopyScenario:
case _stepType.isQuoteScenario:
openNewPage(ApiTestRouteEnum.API_TEST_SCENARIO, { sId: props.data.id, pId: props.data.projectId });
break;
case ScenarioStepType.COPY_CASE:
case ScenarioStepType.QUOTE_CASE:
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { cId: props.data.id });
case _stepType.isQuoteCase:
case _stepType.isCopyCase:
openNewPage(ApiTestRouteEnum.API_TEST_MANAGEMENT, { cId: props.data.id, pId: props.data.projectId });
break;
default:
break;

View File

@ -24,7 +24,7 @@
import { useI18n } from '@/hooks/useI18n';
export interface WaitTimeContentProps {
stepId: string | number;
id: string | number;
waitTime: number;
}

View File

@ -11,7 +11,7 @@
:expand-all="props.expandAll"
:node-more-actions="stepMoreActions"
:filter-more-action-func="setStepMoreAction"
:field-names="{ title: 'name', key: 'stepId', children: 'children' }"
:field-names="{ title: 'name', key: 'id', children: 'children' }"
:virtual-list-props="{
height: '100%',
threshold: 20,
@ -37,7 +37,7 @@
<div
class="flex h-[16px] min-w-[16px] items-center justify-center rounded-full bg-[var(--color-text-brand)] px-[2px] !text-white"
>
{{ step.order }}
{{ step.sort }}
</div>
<div class="step-node-content">
<!-- 步骤展开折叠按钮 -->
@ -60,9 +60,9 @@
<div class="mr-[8px] flex items-center gap-[8px]">
<!-- 步骤启用/禁用 -->
<a-switch
:default-checked="step.enabled"
:default-checked="step.enable"
size="small"
@click.stop="step.enabled = !step.enabled"
@click.stop="handleStepToggleEnable(step)"
></a-switch>
<!-- 步骤执行 -->
<MsIcon
@ -73,13 +73,14 @@
/>
</div>
<!-- 步骤类型 -->
<stepType :type="step.type" />
<stepType :step="step" />
<!-- 步骤整体内容 -->
<div class="relative flex flex-1 items-center gap-[4px]">
<!-- 步骤差异内容按步骤类型展示不同组件 -->
<component
:is="getStepContent(step)"
:data="step"
:data="step.config"
:step-id="step.id"
@quick-input="setQuickInput(step, $event)"
@change="handleStepContentChange($event, step)"
@click.stop
@ -88,7 +89,7 @@
<template v-if="checkStepIsApi(step)">
<apiMethodName v-if="checkStepShowMethod(step)" :method="step.method" />
<div
v-if="step.stepId === showStepNameEditInputStepId"
v-if="step.id === showStepNameEditInputStepId"
class="name-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
@click.stop
>
@ -117,12 +118,12 @@
<!-- 其他步骤描述 -->
<template v-else>
<div
v-if="step.stepId === showStepDescEditInputStepId"
v-if="step.id === showStepDescEditInputStepId"
class="desc-warp absolute left-0 top-[-2px] z-10 w-[calc(100%-24px)]"
>
<a-input
v-model:model-value="tempStepDesc"
:default-value="step.description || t('apiScenario.pleaseInputStepDesc')"
:default-value="step.name || t('apiScenario.pleaseInputStepDesc')"
:placeholder="t('apiScenario.pleaseInputStepDesc')"
:max-length="255"
size="small"
@ -135,14 +136,14 @@
</template>
</a-input>
</div>
<a-tooltip :content="step.description" :disabled="!step.description">
<a-tooltip :content="step.name" :disabled="!step.name">
<div class="step-name-container">
<div
:class="`one-line-text mr-[4px] ${
step.type === ScenarioStepType.ONLY_ONCE_CONTROL ? 'max-w-[750px]' : 'max-w-[150px]'
step.stepType === ScenarioStepType.ONCE_ONLY_CONTROLLER ? 'max-w-[750px]' : 'max-w-[150px]'
} font-normal text-[var(--color-text-4)]`"
>
{{ step.description || t('apiScenario.pleaseInputStepDesc') }}
{{ step.name || t('apiScenario.pleaseInputStepDesc') }}
</div>
<MsIcon
type="icon-icon_edit_outlined"
@ -161,7 +162,7 @@
v-model:selected-keys="selectedKeys"
v-model:steps="steps"
:step="step"
@click="setFocusNodeKey(step.stepId)"
@click="setFocusNodeKey(step.id)"
@other-create="handleOtherCreate"
@close="setFocusNodeKey('')"
/>
@ -190,8 +191,7 @@
v-model:visible="customApiDrawerVisible"
:env-detail-item="{ id: 'demp-id-112233', projectId: '123456', name: 'demo环境' }"
:request="currentStepDetail"
:request-type="activeStep?.type"
:step-name="activeStep?.name || ''"
:step="activeStep"
@add-step="addCustomApiStep"
@apply-step="applyApiStep"
/>
@ -257,7 +257,7 @@
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import executeStatus from '../common/executeStatus.vue';
import { ImportData } from '../common/importApiDrawer/index.vue';
import stepType from '../common/stepType.vue';
import stepType from '../common/stepType/stepType.vue';
import createStepActions from './createAction/createStepActions.vue';
import stepInsertStepTrigger from './createAction/stepInsertStepTrigger.vue';
import conditionContent from './stepNodeComposition/conditionContent.vue';
@ -281,11 +281,12 @@
} from '@/utils';
import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import { CreateStepAction, ScenarioStepLoopWhileType } from '@/models/apiTest/scenario';
import { RequestMethods, ScenarioAddStepActionType, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
import { CreateStepAction, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioAddStepActionType, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import type { RequestParam } from '../common/customApiDrawer.vue';
import useCreateActions from './createAction/useCreateActions';
import getStepType from '@/views/api-test/scenario/components/common/stepType/utils';
import { defaultStepItemCommon } from '@/views/api-test/scenario/components/config';
//
@ -295,41 +296,6 @@
const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue'));
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
export interface ScenarioStepItem {
stepId: string | number;
order: number;
enabled: boolean; //
type: ScenarioStepType;
name: string;
description: string;
method?: RequestMethods;
executeStatus?: ScenarioExecuteStatus;
num?: number; //
//
belongProjectId?: string;
belongProjectName?: string;
children?: ScenarioStepItem[];
//
request?: RequestParam;
//
script?: ExecuteConditionProcessor;
//
// renderId: string; // id
checked: boolean; //
expanded: boolean; //
createActionsVisible?: boolean; //
parent?: ScenarioStepItem; // undefined
loopNum: number;
loopType: 'num' | 'while' | 'forEach';
loopSpace: number;
variableName: string;
variablePrefix: string;
loopWhileType: ScenarioStepLoopWhileType;
variableVal: string;
condition: string;
overTime: number;
}
const props = defineProps<{
stepKeyword: string;
expandAll?: boolean;
@ -345,7 +311,7 @@
required: true,
});
//
const stepsDetailMap = defineModel<Record<string, any>>('stepsDetailMap', {
const stepDetails = defineModel<Record<string, any>>('stepDetails', {
required: true,
});
@ -358,18 +324,18 @@
* 根据步骤类型获取步骤内容组件
*/
function getStepContent(step: ScenarioStepItem) {
switch (step.type) {
case ScenarioStepType.QUOTE_API:
case ScenarioStepType.QUOTE_CASE:
case ScenarioStepType.QUOTE_SCENARIO:
return quoteContent;
case ScenarioStepType.CUSTOM_API:
const _stepType = getStepType(step);
if (_stepType.isQuoteApi || _stepType.isQuoteCase || _stepType.isQuoteScenario) {
return quoteContent;
}
switch (step.stepType) {
case ScenarioStepType.CUSTOM_REQUEST:
return customApiContent;
case ScenarioStepType.LOOP_CONTROL:
case ScenarioStepType.LOOP_CONTROLLER:
return loopControlContent;
case ScenarioStepType.CONDITION_CONTROL:
case ScenarioStepType.IF_CONTROLLER:
return conditionContent;
case ScenarioStepType.WAIT_TIME:
case ScenarioStepType.CONSTANT_TIMER:
return waitTimeContent;
default:
return () => null;
@ -381,34 +347,25 @@
}
function checkStepIsApi(step: ScenarioStepItem) {
return [
ScenarioStepType.QUOTE_API,
ScenarioStepType.COPY_API,
ScenarioStepType.QUOTE_CASE,
ScenarioStepType.COPY_CASE,
ScenarioStepType.CUSTOM_API,
].includes(step.type);
return [ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(step.stepType);
}
function checkStepShowMethod(step: ScenarioStepItem) {
return [
ScenarioStepType.QUOTE_API,
ScenarioStepType.COPY_API,
ScenarioStepType.QUOTE_CASE,
ScenarioStepType.COPY_CASE,
ScenarioStepType.CUSTOM_API,
ScenarioStepType.QUOTE_SCENARIO,
ScenarioStepType.COPY_SCENARIO,
].includes(step.type);
ScenarioStepType.API,
ScenarioStepType.API_CASE,
ScenarioStepType.CUSTOM_REQUEST,
ScenarioStepType.API_SCENARIO,
].includes(step.stepType);
}
/**
* 增加步骤时判断父节点是否选中如果选中则需要把新节点也选中
*/
function checkedIfNeed(step: TreeNode<ScenarioStepItem>, parent?: TreeNode<ScenarioStepItem>) {
if (parent && selectedKeys.value.includes(parent.stepId)) {
if (parent && selectedKeys.value.includes(parent.id)) {
//
selectedKeys.value.push(step.stepId);
selectedKeys.value.push(step.id);
}
}
@ -425,7 +382,8 @@
];
function setStepMoreAction(items: ActionsItem[], node: MsTreeNodeData) {
if ((node as ScenarioStepItem).type === ScenarioStepType.CUSTOM_API) {
const _stepType = getStepType(node as ScenarioStepItem);
if ((node as ScenarioStepItem).stepType === ScenarioStepType.CUSTOM_REQUEST) {
//
return [
{
@ -443,7 +401,7 @@
},
];
}
if ((node as ScenarioStepItem).type === ScenarioStepType.QUOTE_SCENARIO) {
if (_stepType.isQuoteScenario) {
return [
{
label: 'common.copy',
@ -460,7 +418,7 @@
},
];
}
if ((node as ScenarioStepItem).type === ScenarioStepType.QUOTE_CASE) {
if (_stepType.isQuoteCase) {
return [
{
label: 'common.copy',
@ -486,30 +444,30 @@
const id = getGenerateId();
insertNodes<ScenarioStepItem>(
steps.value,
node.stepId,
node.id,
{
...cloneDeep(
mapTree<ScenarioStepItem>(node, (childNode) => {
return {
...childNode,
stepId: getGenerateId(), // TODO: ID
id: getGenerateId(), // TODO: ID
};
})[0]
),
name: `copy-${node.name}`,
order: node.order + 1,
stepId: id,
sort: node.sort + 1,
id,
},
'after',
checkedIfNeed,
'stepId'
'id'
);
break;
case 'config':
console.log('config', node);
break;
case 'delete':
deleteNode(steps.value, node.stepId, 'stepId');
deleteNode(steps.value, node.id, 'id');
break;
default:
break;
@ -527,7 +485,7 @@
const tempStepName = ref('');
function handleStepNameClick(step: ScenarioStepItem) {
tempStepName.value = step.name;
showStepNameEditInputStepId.value = step.stepId;
showStepNameEditInputStepId.value = step.id;
nextTick(() => {
//
const input = treeRef.value?.$el.querySelector('.name-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
@ -536,7 +494,7 @@
}
function applyStepNameChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) {
realStep.name = tempStepName.value;
}
@ -549,8 +507,8 @@
const showStepDescEditInputStepId = ref<string | number>('');
const tempStepDesc = ref('');
function handleStepDescClick(step: ScenarioStepItem) {
tempStepDesc.value = step.description;
showStepDescEditInputStepId.value = step.stepId;
tempStepDesc.value = step.name;
showStepDescEditInputStepId.value = step.id;
nextTick(() => {
//
const input = treeRef.value?.$el.querySelector('.desc-warp .arco-input-wrapper .arco-input') as HTMLInputElement;
@ -559,19 +517,21 @@
}
function applyStepDescChange(step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) {
realStep.description = tempStepDesc.value;
realStep.name = tempStepDesc.value;
}
showStepDescEditInputStepId.value = '';
}
function handleStepContentChange($event, step: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) {
Object.keys($event).forEach((key) => {
realStep[key] = $event[key];
});
// Object.keys($event).forEach((key) => {
// realStep.config[key] = $event[key];
// });
realStep.config = $event;
console.log('handleStepContentChange', $event);
}
}
@ -579,12 +539,19 @@
* 处理步骤展开折叠
*/
function handleStepExpand(data: MsTreeExpandedData) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.stepId, 'stepId');
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.node?.id, 'id');
if (realStep) {
realStep.expanded = !realStep.expanded;
}
}
function handleStepToggleEnable(data: ScenarioStepItem) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, data.id, 'id');
if (realStep) {
realStep.enable = !realStep.enable;
}
}
const importApiDrawerVisible = ref(false);
const customCaseDrawerVisible = ref(false);
const customApiDrawerVisible = ref(false);
@ -594,7 +561,7 @@
const currentStepDetail = computed<any>(() => {
// TODO:
if (activeStep.value) {
return stepsDetailMap.value[activeStep.value.stepId];
return stepDetails.value[activeStep.value.id];
}
return undefined;
});
@ -605,19 +572,20 @@
* @param step 点击的步骤节点
*/
function handleStepSelect(_selectedKeys: Array<string | number>, step: ScenarioStepItem) {
const _stepType = getStepType(step);
const offspringIds: string[] = [];
mapTree(step.children || [], (e) => {
offspringIds.push(e.stepId);
offspringIds.push(e.id);
return e;
});
selectedKeys.value = [step.stepId, ...offspringIds];
if ([ScenarioStepType.CUSTOM_API, ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(step.type)) {
selectedKeys.value = [step.id, ...offspringIds];
if (_stepType.isCopyApi || _stepType.isQuoteApi || step.stepType === ScenarioStepType.CUSTOM_REQUEST) {
activeStep.value = step;
customApiDrawerVisible.value = true;
} else if ([ScenarioStepType.QUOTE_CASE, ScenarioStepType.COPY_CASE].includes(step.type)) {
activeStep.value = step;
customCaseDrawerVisible.value = true;
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) {
} else if (step.stepType === ScenarioStepType.SCRIPT) {
activeStep.value = step;
scriptOperationDrawerVisible.value = true;
}
@ -663,39 +631,46 @@
* @param data 导入数据
*/
function handleImportApiApply(type: 'copy' | 'quote', data: ImportData) {
let order = steps.value.length + 1;
let sort = steps.value.length + 1;
if (activeStep.value && activeCreateAction.value) {
switch (activeCreateAction.value) {
case 'inside':
order = activeStep.value.children ? activeStep.value.children.length : 0;
sort = activeStep.value.children ? activeStep.value.children.length : 0;
break;
case 'before':
order = activeStep.value.order;
sort = activeStep.value.sort;
break;
case 'after':
order = activeStep.value.order + 1;
sort = activeStep.value.sort + 1;
break;
default:
break;
}
}
const refType = type === 'copy' ? ScenarioStepRefType.COPY : ScenarioStepRefType.REF;
const insertApiSteps = buildInsertStepInfos(
data.api,
type === 'copy' ? ScenarioStepType.COPY_API : ScenarioStepType.QUOTE_API,
order,
stepsDetailMap.value
ScenarioStepType.API,
refType,
sort,
stepDetails.value,
appStore.currentProjectId
);
const insertCaseSteps = buildInsertStepInfos(
data.case,
type === 'copy' ? ScenarioStepType.COPY_CASE : ScenarioStepType.QUOTE_CASE,
order + insertApiSteps.length,
stepsDetailMap.value
ScenarioStepType.API_CASE,
refType,
sort + insertApiSteps.length,
stepDetails.value,
appStore.currentProjectId
);
const insertScenarioSteps = buildInsertStepInfos(
data.scenario,
type === 'copy' ? ScenarioStepType.COPY_SCENARIO : ScenarioStepType.QUOTE_SCENARIO,
order + insertApiSteps.length + insertCaseSteps.length,
stepsDetailMap.value
ScenarioStepType.API_SCENARIO,
refType,
sort + insertApiSteps.length + insertCaseSteps.length,
stepDetails.value,
appStore.currentProjectId
);
const insertSteps = insertApiSteps.concat(insertCaseSteps).concat(insertScenarioSteps);
if (activeStep.value && activeCreateAction.value) {
@ -709,14 +684,17 @@
* 添加自定义 API 步骤
*/
function addCustomApiStep(request: RequestParam) {
const id = getGenerateId();
stepsDetailMap.value[id] = request;
request.isNew = false;
stepDetails.value[request.id] = request;
if (activeStep.value && activeCreateAction.value) {
handleCreateStep(
{
type: ScenarioStepType.CUSTOM_API,
stepType: ScenarioStepType.CUSTOM_REQUEST,
name: t('apiScenario.customApi'),
} as ScenarioStepItem,
method: request.method,
id: request.id,
projectId: appStore.currentProjectId,
},
activeStep.value,
steps.value,
activeCreateAction.value,
@ -725,11 +703,14 @@
} else {
steps.value.push({
...cloneDeep(defaultStepItemCommon),
stepId: id,
order: steps.value.length + 1,
type: ScenarioStepType.CUSTOM_API,
id: request.id,
sort: steps.value.length + 1,
stepType: ScenarioStepType.CUSTOM_REQUEST,
refType: ScenarioStepRefType.DIRECT,
name: t('apiScenario.customApi'),
} as ScenarioStepItem);
method: request.method,
projectId: appStore.currentProjectId,
});
}
}
@ -738,7 +719,7 @@
*/
function applyApiStep(request: RequestParam | CaseRequestParam) {
if (activeStep.value) {
stepsDetailMap.value[activeStep.value?.stepId] = request;
stepDetails.value[activeStep.value?.id] = request;
activeStep.value = undefined;
}
}
@ -760,13 +741,14 @@
*/
function addScriptStep(name: string, scriptProcessor: ExecuteConditionProcessor) {
const id = getGenerateId();
stepsDetailMap.value[id] = cloneDeep(scriptProcessor);
stepDetails.value[id] = cloneDeep(scriptProcessor);
if (activeStep.value && activeCreateAction.value) {
handleCreateStep(
{
type: ScenarioStepType.SCRIPT_OPERATION,
stepType: ScenarioStepType.SCRIPT,
name,
} as ScenarioStepItem,
projectId: appStore.currentProjectId,
},
activeStep.value,
steps.value,
activeCreateAction.value,
@ -775,11 +757,13 @@
} else {
steps.value.push({
...cloneDeep(defaultStepItemCommon),
stepId: id,
order: steps.value.length + 1,
type: ScenarioStepType.SCRIPT_OPERATION,
id,
sort: steps.value.length + 1,
stepType: ScenarioStepType.SCRIPT,
refType: ScenarioStepRefType.DIRECT,
name,
} as ScenarioStepItem);
projectId: appStore.currentProjectId,
});
}
}
@ -789,10 +773,10 @@
*/
function isAllowDropInside(dropNode: MsTreeNodeData) {
return [
ScenarioStepType.LOOP_CONTROL,
ScenarioStepType.CONDITION_CONTROL,
ScenarioStepType.ONLY_ONCE_CONTROL,
].includes(dropNode.type);
ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.IF_CONTROLLER,
ScenarioStepType.ONCE_ONLY_CONTROLLER,
].includes(dropNode.stepType);
}
/**
@ -816,20 +800,20 @@
loading.value = true;
const offspringIds: string[] = [];
mapTree(dragNode.children || [], (e) => {
offspringIds.push(e.stepId);
offspringIds.push(e.id);
return e;
});
const stepIdAndOffspringIds = [dragNode.stepId, ...offspringIds];
const stepIdAndOffspringIds = [dragNode.id, ...offspringIds];
if (dropPosition === 0) {
//
if (selectedKeys.value.includes(dropNode.stepId)) {
if (selectedKeys.value.includes(dropNode.id)) {
//
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
}
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.stepId)) {
} else if (dropNode.parent && selectedKeys.value.includes(dropNode.parent.id)) {
//
selectedKeys.value = selectedKeys.value.concat(stepIdAndOffspringIds);
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.stepId)) {
} else if (dragNode.parent && selectedKeys.value.includes(dragNode.parent.id)) {
//
selectedKeys.value = selectedKeys.value.filter((e) => {
for (let i = 0; i < stepIdAndOffspringIds.length; i++) {
@ -842,7 +826,7 @@
return true;
});
}
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'stepId');
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
if (dragResult) {
Message.success(t('common.moveSuccess'));
}
@ -861,12 +845,12 @@
const quickInputDataKey = ref('');
function setQuickInput(step: ScenarioStepItem, dataKey: string) {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.stepId, 'stepId');
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, step.id, 'id');
if (realStep) {
activeStep.value = realStep as ScenarioStepItem;
}
quickInputDataKey.value = dataKey;
quickInputParamValue.value = step.variableVal;
quickInputParamValue.value = step.config?.[dataKey] || '';
showQuickInput.value = true;
}

View File

@ -3,19 +3,32 @@
<template #first>
<a-tabs v-model:active-key="activeKey" class="h-full" animation lazy-load>
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="p-[16px]">
<step v-if="activeKey === ScenarioCreateComposition.STEP" v-model:step="scenario.stepInfo" is-new />
<step v-if="activeKey === ScenarioCreateComposition.STEP" v-model:scenario="scenario" is-new />
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.PARAMS" :title="t('apiScenario.params')" class="p-[16px]">
<params v-if="activeKey === ScenarioCreateComposition.PARAMS" v-model:params="scenario.params" />
<params
v-if="activeKey === ScenarioCreateComposition.PARAMS"
v-model:params="scenario.scenarioConfig.variable.commonVariables"
/>
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.PRE_POST" :title="t('apiScenario.prePost')" class="p-[16px]">
<prePost v-if="activeKey === ScenarioCreateComposition.PRE_POST" />
<prePost
v-if="activeKey === ScenarioCreateComposition.PRE_POST"
v-model:post-processor-config="scenario.scenarioConfig.postProcessorConfig"
v-model:pre-processor-config="scenario.scenarioConfig.preProcessorConfig"
/>
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.ASSERTION" :title="t('apiScenario.assertion')" class="p-[16px]">
<assertion v-if="activeKey === ScenarioCreateComposition.ASSERTION" />
<assertion
v-if="activeKey === ScenarioCreateComposition.ASSERTION"
v-model:assertion-config="scenario.scenarioConfig.assertionConfig"
/>
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.SETTING" :title="t('common.setting')" class="p-[16px]">
<setting v-if="activeKey === ScenarioCreateComposition.SETTING" />
<setting
v-if="activeKey === ScenarioCreateComposition.SETTING"
v-model:other-config="scenario.scenarioConfig.otherConfig"
/>
</a-tab-pane>
</a-tabs>
</template>

View File

@ -1,9 +1,9 @@
<template>
<div class="h-full w-full overflow-hidden">
<div class="px-[24px] pt-[16px]">
<MsDetailCard :title="`【${previewDetail.num}】${previewDetail.name}`" :description="description">
<MsDetailCard :title="`【${scenario.num}】${scenario.name}`" :description="description">
<template #titleAppend>
<apiStatus :status="previewDetail.status" size="small" />
<apiStatus :status="scenario.status" size="small" />
</template>
<template #titleRight>
<a-button
@ -15,11 +15,11 @@
>
<div class="flex items-center gap-[4px]">
<MsIcon
:type="previewDetail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${previewDetail.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
:type="scenario.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
:class="`${scenario.follow ? 'text-[rgb(var(--warning-6))]' : 'text-[var(--color-text-4)]'}`"
:size="14"
/>
{{ t(previewDetail.follow ? 'common.forked' : 'common.fork') }}
{{ t(scenario.follow ? 'common.forked' : 'common.fork') }}
</div>
</a-button>
<a-button type="outline" size="mini" class="arco-btn-outline--secondary !bg-transparent" @click="share">
@ -29,8 +29,8 @@
</div>
</a-button>
</template>
<template #type="{ value }">
<apiMethodName :method="value as RequestMethods" tag-size="small" is-tag />
<template #priority="{ value }">
<caseLevel :case-level="value as CaseLevel" />
</template>
</MsDetailCard>
</div>
@ -41,50 +41,57 @@
:title="t('apiScenario.baseInfo')"
class="px-[24px] py-[16px]"
>
BASE_INFO
<baseInfo :scenario="scenario as ScenarioDetail" />
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.STEP" :title="t('apiScenario.step')" class="px-[24px] py-[16px]">
<step v-if="activeKey === ScenarioCreateComposition.STEP" :step="previewDetail.step" />
<a-tab-pane :key="ScenarioDetailComposition.STEP" :title="t('apiScenario.step')" class="px-[24px] py-[16px]">
<step v-if="activeKey === ScenarioDetailComposition.STEP" v-model:scenario="scenario" />
</a-tab-pane>
<a-tab-pane
:key="ScenarioCreateComposition.PARAMS"
:key="ScenarioDetailComposition.PARAMS"
:title="t('apiScenario.params')"
class="px-[24px] py-[16px]"
>
<params v-if="activeKey === ScenarioCreateComposition.PARAMS" v-model:params="allParams" />
<params
v-if="activeKey === ScenarioDetailComposition.PARAMS"
v-model:params="scenario.scenarioConfig.variable.commonVariables"
/>
</a-tab-pane>
<a-tab-pane
:key="ScenarioCreateComposition.PRE_POST"
:key="ScenarioDetailComposition.PRE_POST"
:title="t('apiScenario.prePost')"
class="px-[24px] py-[16px]"
>
<prePost v-if="activeKey === ScenarioCreateComposition.PRE_POST" />
<prePost
v-if="activeKey === ScenarioDetailComposition.PRE_POST"
v-model:post-processor-config="scenario.scenarioConfig.postProcessorConfig"
v-model:pre-processor-config="scenario.scenarioConfig.preProcessorConfig"
/>
</a-tab-pane>
<a-tab-pane
:key="ScenarioCreateComposition.ASSERTION"
:key="ScenarioDetailComposition.ASSERTION"
:title="t('apiScenario.assertion')"
class="px-[24px] py-[16px]"
>
<assertion v-if="activeKey === ScenarioCreateComposition.ASSERTION" />
<assertion
v-if="activeKey === ScenarioDetailComposition.ASSERTION"
v-model:assertion-config="scenario.scenarioConfig.assertionConfig"
/>
</a-tab-pane>
<a-tab-pane
:key="ScenarioDetailComposition.EXECUTE_HISTORY"
:title="t('apiScenario.executeHistory')"
class="px-[24px] py-[16px]"
>
<executeHistory
v-if="activeKey === ScenarioDetailComposition.EXECUTE_HISTORY"
:scenario-id="previewDetail.id"
/>
<executeHistory v-if="activeKey === ScenarioDetailComposition.EXECUTE_HISTORY" :scenario-id="scenario.id" />
</a-tab-pane>
<a-tab-pane
:key="ScenarioDetailComposition.CHANGE_HISTORY"
:title="t('apiScenario.changeHistory')"
class="px-[24px] py-[16px]"
>
<changeHistory v-if="activeKey === ScenarioDetailComposition.CHANGE_HISTORY" :source-id="previewDetail.id" />
<changeHistory v-if="activeKey === ScenarioDetailComposition.CHANGE_HISTORY" :source-id="scenario.id" />
</a-tab-pane>
<a-tab-pane
<!-- <a-tab-pane
:key="ScenarioDetailComposition.DEPENDENCY"
:title="t('apiScenario.dependency')"
class="px-[24px] py-[16px]"
@ -93,9 +100,12 @@
</a-tab-pane>
<a-tab-pane :key="ScenarioDetailComposition.QUOTE" :title="t('apiScenario.quote')" class="px-[24px] py-[16px]">
<quote v-if="activeKey === ScenarioDetailComposition.QUOTE" />
</a-tab-pane>
<a-tab-pane :key="ScenarioCreateComposition.SETTING" :title="t('common.setting')" class="px-[24px] py-[16px]">
<setting v-if="activeKey === ScenarioCreateComposition.SETTING" />
</a-tab-pane> -->
<a-tab-pane :key="ScenarioDetailComposition.SETTING" :title="t('common.setting')" class="px-[24px] py-[16px]">
<setting
v-if="activeKey === ScenarioDetailComposition.SETTING"
v-model:other-config="scenario.scenarioConfig.otherConfig"
/>
</a-tab-pane>
</a-tabs>
</div>
@ -106,47 +116,52 @@
import { useI18n } from 'vue-i18n';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsDetailCard from '@/components/pure/ms-detail-card/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import baseInfo from '../components/baseInfo.vue';
import step from '../components/step/index.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import { RequestMethods, ScenarioCreateComposition, ScenarioDetailComposition } from '@/enums/apiEnum';
import { Scenario, ScenarioDetail } from '@/models/apiTest/scenario';
import { ScenarioDetailComposition } from '@/enums/apiEnum';
//
const step = defineAsyncComponent(() => import('../components/step/index.vue'));
const params = defineAsyncComponent(() => import('../components/params.vue'));
const prePost = defineAsyncComponent(() => import('../components/prePost.vue'));
const assertion = defineAsyncComponent(() => import('../components/assertion.vue'));
const executeHistory = defineAsyncComponent(() => import('../components/executeHistory.vue'));
const changeHistory = defineAsyncComponent(() => import('../components/changeHistory.vue'));
const dependency = defineAsyncComponent(() => import('../components/dependency.vue'));
const quote = defineAsyncComponent(() => import('../components/quote.vue'));
// const dependency = defineAsyncComponent(() => import('../components/dependency.vue'));
// const quote = defineAsyncComponent(() => import('../components/quote.vue'));
const setting = defineAsyncComponent(() => import('../components/setting.vue'));
const allParams = ref<any[]>([]);
const props = defineProps<{
detail: Record<string, any>;
}>();
const emit = defineEmits(['updateFollow']);
const { copy, isSupported } = useClipboard();
const { t } = useI18n();
const previewDetail = ref<Record<string, any>>(cloneDeep(props.detail));
const scenario = defineModel<Scenario>('scenario', {
required: true,
});
const description = computed(() => [
{
key: 'type',
locale: 'something.type',
value: 'type',
key: 'priority',
locale: 'apiScenario.scenarioLevel',
value: scenario.value.priority,
},
{
key: 'path',
locale: 'something.path',
value: 'path',
key: 'tag',
locale: 'common.tag',
value: scenario.value.tags,
},
{
key: 'description',
locale: 'common.desc',
value: scenario.value.description,
},
]);
@ -154,7 +169,7 @@
async function toggleFollowReview() {
try {
followLoading.value = true;
Message.success(previewDetail.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
Message.success(scenario.value.follow ? t('common.unFollowSuccess') : t('common.followSuccess'));
emit('updateFollow');
} catch (error) {
// eslint-disable-next-line no-console
@ -166,17 +181,20 @@
function share() {
if (isSupported) {
copy(`${window.location.href}&dId=${previewDetail.value.id}`);
copy(`${window.location.href}&dId=${scenario.value.id}`);
Message.success(t('common.copySuccess'));
} else {
Message.error(t('common.copyNotSupport'));
}
}
const activeKey = ref<ScenarioCreateComposition | ScenarioDetailComposition>(ScenarioDetailComposition.BASE_INFO);
const activeKey = ref<ScenarioDetailComposition>(ScenarioDetailComposition.STEP);
</script>
<style lang="less" scoped>
:deep(.arco-tabs-nav) {
@apply border-b;
}
:deep(.arco-tabs-content) {
@apply pt-0;
}

View File

@ -5,7 +5,7 @@
v-model:active-tab="activeScenarioTab"
v-model:tabs="apiTabs"
class="flex-1 overflow-hidden"
@add="newTab"
@add="() => newTab()"
>
<template #label="{ tab }">
<a-tooltip :content="tab.label" :mouse-enter-delay="500">
@ -34,7 +34,7 @@
:is-show-scenario="isShowScenario"
@folder-node-select="handleNodeSelect"
@init="handleModuleInit"
@new-scenario="newTab"
@new-scenario="() => newTab()"
></scenarioModuleTree>
</div>
<div class="flex-1">
@ -55,6 +55,7 @@
:active-module="activeModule"
:offspring-ids="offspringIds"
@refresh-module-tree="refreshTree"
@open-scenario="openScenarioTab"
/>
</template>
</MsSplitBox>
@ -63,7 +64,7 @@
<create v-model:scenario="activeScenarioTab" :module-tree="folderTree"></create>
</div>
<div v-else class="pageWrap">
<detail :detail="activeScenarioTab"></detail>
<detail v-model:scenario="activeScenarioTab"></detail>
</div>
</MsCard>
</template>
@ -74,66 +75,69 @@
*/
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsCard from '@/components/pure/ms-card/index.vue';
import MsEditableTab from '@/components/pure/ms-editable-tab/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsSplitBox from '@/components/pure/ms-split-box/index.vue';
import scenarioModuleTree from './components/scenarioModuleTree.vue';
import { ScenarioStepInfo } from './components/step/index.vue';
import environmentSelect from '@/views/api-test/components/environmentSelect.vue';
// import executeButton from '@/views/api-test/components/executeButton.vue';
import ScenarioTable from '@/views/api-test/scenario/components/scenarioTable.vue';
import { getTrashModuleCount } from '@/api/modules/api-test/scenario';
import { addScenario, getScenarioDetail, getTrashModuleCount, updateScenario } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import router from '@/router';
import useAppStore from '@/store/modules/app';
import { getGenerateId, mapTree, TreeNode } from '@/utils';
import { ApiScenarioGetModuleParams, Scenario } from '@/models/apiTest/scenario';
import {
ApiScenarioGetModuleParams,
ApiScenarioTableItem,
Scenario,
ScenarioStepItem,
} from '@/models/apiTest/scenario';
import { ModuleTreeNode } from '@/models/common';
import { RequestDefinitionStatus } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum';
import { defaultScenario } from './components/config';
//
const detail = defineAsyncComponent(() => import('./detail/index.vue'));
const create = defineAsyncComponent(() => import('./create/index.vue'));
export type ScenarioParams = Scenario & TabItem;
const { t } = useI18n();
const apiTabs = ref<Scenario[]>([
const apiTabs = ref<ScenarioParams[]>([
{
id: 'all',
label: t('apiScenario.allScenario'),
closable: false,
} as Scenario,
} as ScenarioParams,
]);
const activeScenarioTab = ref<Scenario>(apiTabs.value[0]);
const activeScenarioTab = ref<ScenarioParams>(apiTabs.value[0] as ScenarioParams);
function newTab() {
apiTabs.value.push({
id: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
label: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
closable: true,
isNew: true,
name: '',
moduleId: 'root',
priority: 'P0',
stepInfo: {
id: new Date().getTime(),
steps: [],
executeTime: '',
executeSuccessCount: 0,
executeFailCount: 0,
stepsDetailMap: {},
} as ScenarioStepInfo,
status: RequestDefinitionStatus.PROCESSING,
tags: [],
params: [],
executeLoading: false,
unSaved: false,
});
activeScenarioTab.value = apiTabs.value[apiTabs.value.length - 1];
function newTab(defaultScenarioInfo?: Scenario, isCopy = false) {
if (defaultScenarioInfo) {
apiTabs.value.push({
...defaultScenarioInfo,
id: isCopy ? getGenerateId() : defaultScenarioInfo.id || '',
label: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name,
});
} else {
apiTabs.value.push({
...cloneDeep(defaultScenario),
id: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
label: `${t('apiScenario.createScenario')}${apiTabs.value.length}`,
moduleId: 'root',
priority: 'P0',
});
}
activeScenarioTab.value = apiTabs.value[apiTabs.value.length - 1] as ScenarioParams;
}
const folderTree = ref<ModuleTreeNode[]>([]);
@ -185,16 +189,49 @@
const saveLoading = ref(false);
async function saveScenario() {
saveLoading.value = true;
await new Promise((resolve) => {
setTimeout(() => {
resolve('');
}, 1000);
});
Message.success(activeScenarioTab.value.isNew ? t('common.createSuccess') : t('common.saveSuccess'));
activeScenarioTab.value.isNew = false;
activeScenarioTab.value.unSaved = false;
saveLoading.value = false;
try {
saveLoading.value = true;
if (activeScenarioTab.value.isNew) {
const res = await addScenario({
...activeScenarioTab.value,
projectId: appStore.currentProjectId,
});
const scenarioDetail = await getScenarioDetail(res.id);
activeScenarioTab.value = scenarioDetail as ScenarioParams;
} else {
await updateScenario({
...activeScenarioTab.value,
});
}
Message.success(activeScenarioTab.value.isNew ? t('common.createSuccess') : t('common.saveSuccess'));
activeScenarioTab.value.unSaved = false;
saveLoading.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
saveLoading.value = false;
}
}
async function openScenarioTab(record: ApiScenarioTableItem, isCopy?: boolean) {
try {
appStore.showLoading();
const res = await getScenarioDetail(record.id);
res.stepDetails = {};
// mapTree<ScenarioStepItem>(res.steps, (node: TreeNode<ScenarioStepItem>) => {
// res.stepDetails[node.id] = node.config;
// return node;
// });
newTab(res, isCopy);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
nextTick(() => {
appStore.hideLoading();
});
}
}
</script>

View File

@ -110,12 +110,14 @@ export default {
'apiScenario.after': '在之后插入步骤',
'apiScenario.num': '次数',
'apiScenario.space': '间隔(ms)',
'apiScenario.overTime': '超时(ms)',
'apiScenario.timeout': '超时(ms)',
'apiScenario.waitTimeMs': '等待(ms)',
'apiScenario.pleaseInputStepDesc': '请输入步骤描述',
'apiScenario.variableName': '变量名称{suffix}',
'apiScenario.variablePrefix': '变量前缀',
'apiScenario.variableVal': '变量值',
'apiScenario.variable': '变量名称{suffix}',
'apiScenario.valuePrefix': '变量前缀',
'apiScenario.value': '变量值',
'apiScenario.whileController.msWhileVariable.value': '变量值',
'apiScenario.whileController.msWhileScript.scriptValue': '表达式',
'apiScenario.condition': '条件',
'apiScenario.expression': '表达式',
'apiScenario.equal': '等于',

View File

@ -43,7 +43,7 @@
</template>
</MsSplitBox>
</div>
<detail v-else :detail="activeApiTab"></detail>
<detail v-else v-model:scenario="activeApiTab"></detail>
</MsCard>
</template>