fix(接口场景): 引用场景响应&部分 bug 修复
This commit is contained in:
parent
b628f853cb
commit
ffd19fae21
|
@ -21,6 +21,7 @@ import {
|
|||
GetModuleTreeUrl,
|
||||
GetScenarioStepUrl,
|
||||
GetScenarioUrl,
|
||||
GetStepProjectInfoUrl,
|
||||
GetSystemRequestUrl,
|
||||
GetTrashModuleCountUrl,
|
||||
GetTrashModuleTreeUrl,
|
||||
|
@ -223,7 +224,7 @@ export function addScenario(params: Scenario) {
|
|||
}
|
||||
|
||||
// 获取场景详情
|
||||
export function getScenarioDetail(id: string) {
|
||||
export function getScenarioDetail(id: string | number) {
|
||||
return MSR.get<ScenarioDetail>({ url: GetScenarioUrl, params: id });
|
||||
}
|
||||
|
||||
|
@ -278,3 +279,8 @@ export function updateScenarioStatus(id: string | number, status: ApiScenarioSta
|
|||
export function updateScenarioPro(id: string | number, priority: CaseLevel | undefined) {
|
||||
return MSR.get({ url: `${UpdateScenarioPriorityUrl}/${id}/${priority}` });
|
||||
}
|
||||
|
||||
// 获取跨项目信息
|
||||
export function getStepProjectInfo(id: string | number) {
|
||||
return MSR.get({ url: GetStepProjectInfoUrl, params: id });
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ export const GetSystemRequestUrl = '/api/scenario/get/system-request'; // 获取
|
|||
export const FollowScenarioUrl = '/api/scenario/follow'; // 关注/取消关注接口场景
|
||||
export const ScenarioScheduleConfigUrl = '/api/scenario/schedule-config'; // 场景定时任务
|
||||
export const ScenarioScheduleConfigDeleteUrl = '/api/scenario/schedule-config-delete/'; // 场景定时任务
|
||||
export const GetStepProjectInfoUrl = '/api/scenario/step/project-ifo'; // 获取跨项目信息
|
||||
export const BatchRecycleScenarioUrl = '/api/scenario/batch-operation/delete-gc'; // 批量删除接口场景
|
||||
export const BatchMoveScenarioUrl = '/api/scenario/batch-operation/move'; // 批量移动接口场景
|
||||
export const BatchCopyScenarioUrl = '/api/scenario/batch-operation/copy'; // 批量复制接口场景
|
||||
|
|
|
@ -739,9 +739,12 @@
|
|||
.arco-switch-type-circle {
|
||||
background-color: var(--color-text-brand) !important;
|
||||
}
|
||||
.arco-switch-type-circle.arco-switch-checked {
|
||||
.arco-switch-type-circle.arco-switch-checked:not(:disabled) {
|
||||
background-color: rgb(var(--primary-5)) !important;
|
||||
}
|
||||
.arco-switch-disabled {
|
||||
background-color: rgb(var(--primary-3)) !important;
|
||||
}
|
||||
.arco-switch-type-line.arco-switch-small {
|
||||
.arco-switch-handle {
|
||||
width: 14px;
|
||||
|
|
|
@ -4,7 +4,7 @@ import localforage from 'localforage';
|
|||
import { MsTableColumn, MsTableColumnData } from '@/components/pure/ms-table/type';
|
||||
|
||||
import { useAppStore } from '@/store';
|
||||
import { PageSizeMap, SelectorColumnMap, TableOpenDetailMode } from '@/store/modules/components/ms-table/types';
|
||||
import { MsTableSelectorItem, PageSizeMap, TableOpenDetailMode } from '@/store/modules/components/ms-table/types';
|
||||
import { isArraysEqualWithOrder } from '@/utils/equal';
|
||||
|
||||
import { SpecialColumnEnum } from '@/enums/tableEnum';
|
||||
|
@ -15,19 +15,6 @@ export default function useTableStore() {
|
|||
operationBaseIndex: 100,
|
||||
});
|
||||
|
||||
const getSelectorColumnMap = async () => {
|
||||
try {
|
||||
const selectorColumnMap = await localforage.getItem<SelectorColumnMap>('selectorColumnMap');
|
||||
if (!selectorColumnMap) {
|
||||
return {};
|
||||
}
|
||||
return selectorColumnMap;
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(e);
|
||||
return {};
|
||||
}
|
||||
};
|
||||
const getPageSizeMap = async () => {
|
||||
try {
|
||||
const pageSizeMap = await localforage.getItem<PageSizeMap>('pageSizeMap');
|
||||
|
@ -77,32 +64,30 @@ export default function useTableStore() {
|
|||
showSubdirectory?: boolean
|
||||
) {
|
||||
try {
|
||||
const selectorColumnMap = await getSelectorColumnMap();
|
||||
if (!selectorColumnMap[tableKey]) {
|
||||
const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(tableKey);
|
||||
if (!tableColumnsMap) {
|
||||
// 如果没有在indexDB里初始化
|
||||
column = columnsTransform(column);
|
||||
selectorColumnMap[tableKey] = {
|
||||
localforage.setItem(tableKey, {
|
||||
mode,
|
||||
showSubdirectory,
|
||||
column,
|
||||
columnBackup: JSON.parse(JSON.stringify(column)),
|
||||
};
|
||||
await localforage.setItem('selectorColumnMap', selectorColumnMap);
|
||||
});
|
||||
} else {
|
||||
// 初始化过了,但是可能有新变动,如列的顺序,列的显示隐藏,列的拖拽
|
||||
column = columnsTransform(column);
|
||||
const { columnBackup: oldColumn } = selectorColumnMap[tableKey];
|
||||
const { columnBackup: oldColumn } = tableColumnsMap;
|
||||
// 比较页面上定义的 column 和 浏览器备份的column 是否相同
|
||||
const isEqual = isArraysEqualWithOrder<MsTableColumnData>(oldColumn, column);
|
||||
const isEqual = isArraysEqualWithOrder(oldColumn, column);
|
||||
if (!isEqual) {
|
||||
// 如果不相等,说明有变动将新的column存入indexDB
|
||||
selectorColumnMap[tableKey] = {
|
||||
localforage.setItem(tableKey, {
|
||||
mode,
|
||||
showSubdirectory,
|
||||
column,
|
||||
columnBackup: JSON.parse(JSON.stringify(column)),
|
||||
};
|
||||
await localforage.setItem('selectorColumnMap', selectorColumnMap);
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
|
@ -112,13 +97,10 @@ export default function useTableStore() {
|
|||
}
|
||||
async function setMode(key: string, mode: TableOpenDetailMode) {
|
||||
try {
|
||||
const selectorColumnMap = await getSelectorColumnMap();
|
||||
if (selectorColumnMap[key]) {
|
||||
const item = selectorColumnMap[key];
|
||||
if (item) {
|
||||
item.mode = mode;
|
||||
}
|
||||
await localforage.setItem('selectorColumnMap', selectorColumnMap);
|
||||
const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
|
||||
if (tableColumnsMap) {
|
||||
tableColumnsMap.mode = mode;
|
||||
await localforage.setItem(key, tableColumnsMap);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -128,13 +110,10 @@ export default function useTableStore() {
|
|||
|
||||
async function setSubdirectory(key: string, val: boolean) {
|
||||
try {
|
||||
const selectorColumnMap = await getSelectorColumnMap();
|
||||
if (selectorColumnMap[key]) {
|
||||
const item = selectorColumnMap[key];
|
||||
if (item) {
|
||||
item.showSubdirectory = val;
|
||||
}
|
||||
await localforage.setItem('selectorColumnMap', selectorColumnMap);
|
||||
const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
|
||||
if (tableColumnsMap) {
|
||||
tableColumnsMap.showSubdirectory = val;
|
||||
await localforage.setItem(key, tableColumnsMap);
|
||||
}
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -155,23 +134,22 @@ export default function useTableStore() {
|
|||
item.sortIndex = state.baseSortIndex + idx;
|
||||
}
|
||||
});
|
||||
const selectorColumnMap = await getSelectorColumnMap();
|
||||
if (!selectorColumnMap) {
|
||||
const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
|
||||
if (!tableColumnsMap) {
|
||||
return;
|
||||
}
|
||||
if (isSimple) {
|
||||
const oldColumns = selectorColumnMap[key].column;
|
||||
const oldColumns = tableColumnsMap.column;
|
||||
const operationColumn = oldColumns.find((i) => i.dataIndex === SpecialColumnEnum.OPERATION);
|
||||
if (operationColumn) columns.push(operationColumn);
|
||||
}
|
||||
|
||||
selectorColumnMap[key] = {
|
||||
await localforage.setItem(key, {
|
||||
mode,
|
||||
showSubdirectory,
|
||||
column: JSON.parse(JSON.stringify(columns)),
|
||||
columnBackup: selectorColumnMap[key].columnBackup,
|
||||
};
|
||||
await localforage.setItem('selectorColumnMap', selectorColumnMap);
|
||||
columnBackup: tableColumnsMap.columnBackup,
|
||||
});
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error('tableStore.setColumns', e);
|
||||
|
@ -184,25 +162,25 @@ export default function useTableStore() {
|
|||
}
|
||||
|
||||
async function getMode(key: string) {
|
||||
const selectorColumnMap = await getSelectorColumnMap();
|
||||
if (selectorColumnMap[key]) {
|
||||
return selectorColumnMap[key].mode;
|
||||
const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
|
||||
if (tableColumnsMap) {
|
||||
return tableColumnsMap.mode;
|
||||
}
|
||||
return 'drawer';
|
||||
}
|
||||
|
||||
async function getSubShow(key: string) {
|
||||
const selectorColumnMap = await getSelectorColumnMap();
|
||||
if (selectorColumnMap[key]) {
|
||||
return selectorColumnMap[key].showSubdirectory;
|
||||
const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
|
||||
if (tableColumnsMap) {
|
||||
return tableColumnsMap.showSubdirectory;
|
||||
}
|
||||
return true as boolean;
|
||||
}
|
||||
|
||||
async function getColumns(key: string, isSimple?: boolean) {
|
||||
const selectorColumnMap = await getSelectorColumnMap();
|
||||
if (selectorColumnMap[key]) {
|
||||
const tmpArr = selectorColumnMap[key].column;
|
||||
const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
|
||||
if (tableColumnsMap) {
|
||||
const tmpArr = tableColumnsMap.column;
|
||||
const { nonSortableColumns, couldSortableColumns } = tmpArr.reduce(
|
||||
(result: { nonSortableColumns: MsTableColumnData[]; couldSortableColumns: MsTableColumnData[] }, item) => {
|
||||
if (isSimple && item.dataIndex === SpecialColumnEnum.OPERATION) {
|
||||
|
@ -222,9 +200,9 @@ export default function useTableStore() {
|
|||
return { nonSort: [], couldSort: [] };
|
||||
}
|
||||
async function getShowInTableColumns(key: string) {
|
||||
const selectorColumnMap = await getSelectorColumnMap();
|
||||
if (selectorColumnMap[key]) {
|
||||
const tmpArr: MsTableColumn = selectorColumnMap[key].column;
|
||||
const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
|
||||
if (tableColumnsMap) {
|
||||
const tmpArr: MsTableColumn = tableColumnsMap.column;
|
||||
return orderBy(
|
||||
filter(tmpArr, (i) => i.showInTable),
|
||||
['sortIndex'],
|
||||
|
|
|
@ -360,6 +360,7 @@ export interface ScenarioStepItem {
|
|||
executeStatus?: ScenarioExecuteStatus;
|
||||
isExecuting?: boolean; // 是否正在执行
|
||||
reportId?: string | number; // 步骤单个调试时的报告id
|
||||
uniqueId: string | number; // 获取报告时的步骤唯一标识(用来区分重复引用的步骤)
|
||||
isQuoteScenarioStep?: boolean; // 是否是引用场景下的步骤(不分是不是完全引用,只要是引用类型就是),不可修改引用 api 的参数值
|
||||
isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用
|
||||
}
|
||||
|
@ -401,7 +402,7 @@ export interface Scenario {
|
|||
executeTime?: string | number; // 执行时间
|
||||
executeSuccessCount: number; // 执行成功数量
|
||||
executeFailCount: number; // 执行失败数量
|
||||
reportId?: string | number; // 场景报告 id
|
||||
reportId: string | number; // 场景报告 id
|
||||
stepResponses: Record<string | number, Array<RequestResult>>; // 步骤响应集合,key 为步骤 id,value 为步骤响应内容
|
||||
isExecute?: boolean; // 是否从列表执行进去场景详情
|
||||
isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤
|
||||
|
|
|
@ -198,8 +198,8 @@ export interface TreeNode<T> {
|
|||
*/
|
||||
export function traverseTree<T>(
|
||||
tree: TreeNode<T> | TreeNode<T>[] | T | T[],
|
||||
customNodeFn: (node: TreeNode<T>) => void,
|
||||
continueCondition?: (node: TreeNode<T>) => boolean,
|
||||
customNodeFn: (node: TreeNode<T>) => TreeNode<T> | null = (node) => node,
|
||||
customChildrenKey = 'children'
|
||||
) {
|
||||
if (!Array.isArray(tree)) {
|
||||
|
@ -215,7 +215,7 @@ export function traverseTree<T>(
|
|||
// 如果有继续递归的条件,则判断是否继续递归
|
||||
break;
|
||||
}
|
||||
traverseTree(node[customChildrenKey], continueCondition, customNodeFn, customChildrenKey);
|
||||
traverseTree(node[customChildrenKey], customNodeFn, continueCondition, customChildrenKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -488,6 +488,7 @@ export function handleTreeDragDrop<T>(
|
|||
return false;
|
||||
}
|
||||
const index = parentChildren.findIndex((node: TreeNode<T>) => node[customKey] === dragNode[customKey]);
|
||||
console.log('index', parentChildren, dragNode, index);
|
||||
if (index !== -1) {
|
||||
parentChildren.splice(index, 1);
|
||||
|
||||
|
|
|
@ -346,6 +346,7 @@
|
|||
offspringIds: string[];
|
||||
protocol: string; // 查看的协议类型
|
||||
readOnly?: boolean; // 是否是只读模式
|
||||
refreshTimeStamp?: number;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'openApiTab', record: ApiDefinitionDetail, isExecute?: boolean): void;
|
||||
|
@ -358,6 +359,7 @@
|
|||
const appStore = useAppStore();
|
||||
const { t } = useI18n();
|
||||
const { openModal } = useModal();
|
||||
const tableStore = useTableStore();
|
||||
|
||||
const folderTreePathMap = inject('folderTreePathMap');
|
||||
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
|
||||
|
@ -459,6 +461,14 @@
|
|||
width: hasOperationPermission.value ? 220 : 50,
|
||||
},
|
||||
];
|
||||
|
||||
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer', true);
|
||||
if (props.readOnly) {
|
||||
columns = columns.filter(
|
||||
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
|
||||
);
|
||||
}
|
||||
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
|
||||
getDefinitionPage,
|
||||
{
|
||||
|
@ -523,7 +533,6 @@
|
|||
const statusFilterVisible = ref(false);
|
||||
const statusFilters = ref<string[]>([]);
|
||||
|
||||
const tableStore = useTableStore();
|
||||
async function getModuleIds() {
|
||||
let moduleIds: string[] = [];
|
||||
if (props.activeModule !== 'all') {
|
||||
|
@ -552,6 +561,15 @@
|
|||
loadList();
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.refreshTimeStamp,
|
||||
(val) => {
|
||||
if (val) {
|
||||
loadApiList();
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.activeModule,
|
||||
() => {
|
||||
|
@ -915,18 +933,6 @@
|
|||
console.log(error);
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({
|
||||
loadApiList,
|
||||
});
|
||||
|
||||
if (!props.readOnly) {
|
||||
await tableStore.initColumn(TableKeyEnum.API_TEST, columns, 'drawer', true);
|
||||
} else {
|
||||
columns = columns.filter(
|
||||
(item) => !['version', 'createTime', 'updateTime', 'operation'].includes(item.dataIndex as string)
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="flex flex-1 flex-col overflow-hidden">
|
||||
<div v-show="activeApiTab.id === 'all'" class="flex-1 pt-[16px]">
|
||||
<div v-if="activeApiTab.id === 'all'" class="flex-1 pt-[16px]">
|
||||
<apiTable
|
||||
ref="apiTableRef"
|
||||
:active-module="props.activeModule"
|
||||
:offspring-ids="props.offspringIds"
|
||||
:protocol="props.protocol"
|
||||
:refresh-time-stamp="refreshTableTimeStamp"
|
||||
@open-api-tab="(record, isExecute) => openApiTab(record, false, isExecute)"
|
||||
@open-copy-api-tab="openApiTab($event, true)"
|
||||
@add-api-tab="addApiTab"
|
||||
|
@ -288,14 +288,14 @@
|
|||
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
|
||||
}
|
||||
|
||||
const apiTableRef = ref<InstanceType<typeof apiTable>>();
|
||||
const caseTableRef = ref<InstanceType<typeof caseTable>>();
|
||||
const refreshTableTimeStamp = ref(0);
|
||||
|
||||
watch(
|
||||
() => activeApiTab.value.id,
|
||||
(id) => {
|
||||
if (id === 'all') {
|
||||
apiTableRef.value?.loadApiList();
|
||||
refreshTableTimeStamp.value = Date.now();
|
||||
}
|
||||
if (activeApiTab.value.definitionActiveKey === 'case') {
|
||||
caseTableRef.value?.loadCaseList();
|
||||
|
@ -366,7 +366,7 @@
|
|||
}
|
||||
|
||||
function refreshTable() {
|
||||
apiTableRef.value?.loadApiList();
|
||||
refreshTableTimeStamp.value = Date.now();
|
||||
}
|
||||
|
||||
function changeDefinitionActiveKey(val: string | number) {
|
||||
|
|
|
@ -558,7 +558,6 @@
|
|||
},
|
||||
];
|
||||
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getCasePage, {
|
||||
columns,
|
||||
scroll: { x: '100%' },
|
||||
tableKey: TableKeyEnum.API_TEST_MANAGEMENT_CASE,
|
||||
showSetting: true,
|
||||
|
|
|
@ -32,6 +32,7 @@
|
|||
<a-select
|
||||
v-model:model-value="requestVModel.customizeRequestEnvEnable"
|
||||
class="w-[150px]"
|
||||
:disabled="props.step?.isQuoteScenarioStep"
|
||||
popup-container=".customApiDrawer-title-right"
|
||||
@change="handleUseEnvChange"
|
||||
>
|
||||
|
@ -61,7 +62,7 @@
|
|||
v-model:model-value="requestVModel.protocol"
|
||||
:options="protocolOptions"
|
||||
:loading="protocolLoading"
|
||||
:disabled="_stepType.isQuoteApi"
|
||||
:disabled="_stepType.isQuoteApi || props.step?.isQuoteScenarioStep"
|
||||
class="w-[90px]"
|
||||
@change="(val) => handleActiveDebugProtocolChange(val as string)"
|
||||
/>
|
||||
|
@ -81,7 +82,7 @@
|
|||
<apiMethodSelect
|
||||
v-model:model-value="requestVModel.method"
|
||||
class="w-[140px]"
|
||||
:disabled="_stepType.isQuoteApi"
|
||||
:disabled="_stepType.isQuoteApi || props.step?.isQuoteScenarioStep"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<a-input
|
||||
|
@ -91,7 +92,7 @@
|
|||
allow-clear
|
||||
class="hover:z-10"
|
||||
:style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 10' : ''"
|
||||
:disabled="_stepType.isQuoteApi"
|
||||
:disabled="_stepType.isQuoteApi || props.step?.isQuoteScenarioStep"
|
||||
@input="() => (isUrlError = false)"
|
||||
@change="handleUrlChange"
|
||||
>
|
||||
|
@ -130,7 +131,7 @@
|
|||
v-model:model-value="requestVModel.name"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
|
||||
:disabled="!isEditableApi || isQuoteScenarioStep"
|
||||
:disabled="!isEditableApi"
|
||||
allow-clear
|
||||
class="mt-[8px]"
|
||||
/>
|
||||
|
@ -184,7 +185,7 @@
|
|||
<httpHeader
|
||||
v-if="requestVModel.activeTab === RequestComposition.HEADER"
|
||||
v-model:params="requestVModel.headers"
|
||||
:disabled-param-value="isQuoteScenarioStep"
|
||||
:disabled-param-value="!isEditableApi"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:layout="activeLayout"
|
||||
:second-box-height="secondBoxHeight"
|
||||
|
@ -194,7 +195,7 @@
|
|||
v-else-if="requestVModel.activeTab === RequestComposition.BODY"
|
||||
v-model:params="requestVModel.body"
|
||||
:layout="activeLayout"
|
||||
:disabled-param-value="isQuoteScenarioStep"
|
||||
:disabled-param-value="!isEditableApi"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
:upload-temp-file-api="uploadTempFile"
|
||||
|
@ -207,7 +208,7 @@
|
|||
v-else-if="requestVModel.activeTab === RequestComposition.QUERY"
|
||||
v-model:params="requestVModel.query"
|
||||
:layout="activeLayout"
|
||||
:disabled-param-value="isQuoteScenarioStep"
|
||||
:disabled-param-value="!isEditableApi"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
@change="handleActiveDebugChange"
|
||||
|
@ -216,7 +217,7 @@
|
|||
v-else-if="requestVModel.activeTab === RequestComposition.REST"
|
||||
v-model:params="requestVModel.rest"
|
||||
:layout="activeLayout"
|
||||
:disabled-param-value="isQuoteScenarioStep"
|
||||
:disabled-param-value="!isEditableApi"
|
||||
:disabled-except-param="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
@change="handleActiveDebugChange"
|
||||
|
@ -225,7 +226,7 @@
|
|||
v-else-if="requestVModel.activeTab === RequestComposition.PRECONDITION"
|
||||
v-model:config="requestVModel.children[0].preProcessorConfig"
|
||||
is-definition
|
||||
:disabled="!isEditableApi || isQuoteScenarioStep"
|
||||
:disabled="!isEditableApi"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<postcondition
|
||||
|
@ -233,7 +234,7 @@
|
|||
v-model:config="requestVModel.children[0].postProcessorConfig"
|
||||
:response="responseResultBody"
|
||||
:layout="activeLayout"
|
||||
:disabled="!isEditableApi || isQuoteScenarioStep"
|
||||
:disabled="!isEditableApi"
|
||||
:second-box-height="secondBoxHeight"
|
||||
is-definition
|
||||
@change="handleActiveDebugChange"
|
||||
|
@ -243,19 +244,19 @@
|
|||
v-model:params="requestVModel.children[0].assertionConfig.assertions"
|
||||
:response="responseResultBody"
|
||||
is-definition
|
||||
:disabled="!isEditableApi || isQuoteScenarioStep"
|
||||
:disabled="!isEditableApi"
|
||||
:assertion-config="requestVModel.children[0].assertionConfig"
|
||||
/>
|
||||
<auth
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.AUTH"
|
||||
v-model:params="requestVModel.authConfig"
|
||||
:disabled="!isEditableApi || isQuoteScenarioStep"
|
||||
:disabled="!isEditableApi"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
<setting
|
||||
v-else-if="requestVModel.activeTab === RequestComposition.SETTING"
|
||||
v-model:params="requestVModel.otherConfig"
|
||||
:disabled="!isEditableApi || isQuoteScenarioStep"
|
||||
:disabled="!isEditableApi"
|
||||
@change="handleActiveDebugChange"
|
||||
/>
|
||||
</div>
|
||||
|
@ -506,11 +507,11 @@
|
|||
);
|
||||
const currentLoop = ref(1);
|
||||
const currentResponse = computed(() => {
|
||||
if (props.step?.id) {
|
||||
return props.stepResponses?.[props.step?.id]?.[currentLoop.value - 1];
|
||||
if (props.step?.uniqueId) {
|
||||
return props.stepResponses?.[props.step?.uniqueId]?.[currentLoop.value - 1];
|
||||
}
|
||||
});
|
||||
const loopTotal = computed(() => (props.step?.id && props.stepResponses?.[props.step?.id]?.length) || 0);
|
||||
const loopTotal = computed(() => (props.step?.uniqueId && props.stepResponses?.[props.step?.uniqueId]?.length) || 0);
|
||||
// 执行响应结果 body 部分
|
||||
const responseResultBody = computed(() => {
|
||||
return currentResponse.value?.responseResult.body;
|
||||
|
@ -531,10 +532,11 @@
|
|||
// 复制 api 只要加载过一次后就会保存,所以 props.request 是不为空的
|
||||
const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request === undefined);
|
||||
const isEditableApi = computed(
|
||||
() => _stepType.value.isCopyApi || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.step
|
||||
() =>
|
||||
!props.step?.isQuoteScenarioStep &&
|
||||
(_stepType.value.isCopyApi || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST || !props.step)
|
||||
);
|
||||
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
|
||||
const isQuoteScenarioStep = computed(() => props.step?.isQuoteScenarioStep);
|
||||
|
||||
const isInitPluginForm = ref(false);
|
||||
|
||||
|
@ -1087,7 +1089,6 @@
|
|||
}
|
||||
nextTick(() => {
|
||||
// 等待内容渲染出来再隐藏loading
|
||||
requestVModel.value.activeTab = contentTabList.value[0].value;
|
||||
loading.value = false;
|
||||
});
|
||||
} catch (error) {
|
||||
|
@ -1130,6 +1131,7 @@
|
|||
stepId: getGenerateId(),
|
||||
});
|
||||
}
|
||||
requestVModel.value.activeTab = contentTabList.value[0].value;
|
||||
}
|
||||
},
|
||||
{
|
||||
|
|
|
@ -404,11 +404,13 @@
|
|||
);
|
||||
const currentLoop = ref(1);
|
||||
const currentResponse = computed(() => {
|
||||
if (activeStep.value?.id) {
|
||||
return props.stepResponses?.[activeStep.value?.id]?.[currentLoop.value - 1];
|
||||
if (activeStep.value?.uniqueId) {
|
||||
return props.stepResponses?.[activeStep.value?.uniqueId]?.[currentLoop.value - 1];
|
||||
}
|
||||
});
|
||||
const loopTotal = computed(() => (activeStep.value?.id && props.stepResponses?.[activeStep.value?.id]?.length) || 0);
|
||||
const loopTotal = computed(
|
||||
() => (activeStep.value?.uniqueId && props.stepResponses?.[activeStep.value?.uniqueId]?.length) || 0
|
||||
);
|
||||
// 执行响应结果 body 部分
|
||||
const responseResultBody = computed(() => {
|
||||
return currentResponse.value?.responseResult.body;
|
||||
|
|
|
@ -46,6 +46,7 @@
|
|||
:selected-apis="selectedApis"
|
||||
:selected-cases="selectedCases"
|
||||
:selected-scenarios="selectedScenarios"
|
||||
:scenario-id="props.scenarioId"
|
||||
@select="handleTableSelect"
|
||||
/>
|
||||
</div>
|
||||
|
@ -112,6 +113,9 @@
|
|||
scenario: MsTableDataItem<ApiScenarioTableItem>[];
|
||||
}
|
||||
|
||||
const props = defineProps<{
|
||||
scenarioId?: string | number;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'copy', data: ImportData): void;
|
||||
(e: 'quote', data: ImportData): void;
|
||||
|
@ -235,19 +239,12 @@
|
|||
fullScenarioArr.push(...res);
|
||||
});
|
||||
if (refType === ScenarioStepRefType.COPY) {
|
||||
fullScenarioArr = fullScenarioArr.map((e) => {
|
||||
fullScenarioArr = mapTree<MsTableDataItem<ApiScenarioTableItem>>(fullScenarioArr, (node) => {
|
||||
return {
|
||||
...e,
|
||||
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => {
|
||||
return {
|
||||
...node,
|
||||
copyFromStepId: node.id,
|
||||
originProjectId: node.projectId,
|
||||
id: getGenerateId(),
|
||||
};
|
||||
}),
|
||||
copyFromStepId: e.resourceId,
|
||||
originProjectId: e.projectId,
|
||||
...node,
|
||||
copyFromStepId: node.id,
|
||||
originProjectId: node.projectId,
|
||||
id: getGenerateId(),
|
||||
};
|
||||
});
|
||||
emit(
|
||||
|
@ -266,13 +263,12 @@
|
|||
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => {
|
||||
return {
|
||||
...node,
|
||||
copyFromStepId: node.id,
|
||||
originProjectId: node.projectId,
|
||||
id: getGenerateId(),
|
||||
isQuoteScenarioStep: true,
|
||||
isRefScenarioStep: true, // 默认是完全引用的
|
||||
};
|
||||
}),
|
||||
id: getGenerateId(),
|
||||
originProjectId: e.projectId,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -113,6 +113,7 @@
|
|||
selectedApis: MsTableDataItem<ApiDefinitionDetail>[]; // 已选中的接口
|
||||
selectedCases: MsTableDataItem<ApiCaseDetail>[]; // 已选中的用例
|
||||
selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; // 已选中的场景
|
||||
scenarioId?: string | number;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void;
|
||||
|
@ -335,6 +336,7 @@
|
|||
status: statusFilters.value,
|
||||
method: methodFilters.value,
|
||||
},
|
||||
excludeIds: [props.scenarioId || ''],
|
||||
});
|
||||
currentTable.value.loadList();
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<a-popover
|
||||
position="br"
|
||||
position="lt"
|
||||
content-class="scenario-step-response-popover"
|
||||
@popup-visible-change="emit('visibleChange', $event, props.step)"
|
||||
>
|
||||
|
@ -8,8 +8,18 @@
|
|||
<template #content>
|
||||
<div class="flex h-full flex-col">
|
||||
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
|
||||
<div class="flex-1">
|
||||
<div class="flex-1 overflow-y-hidden">
|
||||
<div v-if="step.stepType === ScenarioStepType.SCRIPT" class="flex h-full flex-col p-[8px]">
|
||||
<div class="mb-[8px] flex gap-[8px] text-[14px] font-medium text-[var(--color-text-1)]">
|
||||
{{ t('apiScenario.executionResult') }}
|
||||
<div class="one-line-text text-[var(--color-text-4)]">({{ step.name }})</div>
|
||||
</div>
|
||||
<div class="flex-1 bg-[var(--color-text-n9)] p-[12px]">
|
||||
<pre class="response-header-pre">{{ currentResponse?.console }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<responseResult
|
||||
v-else
|
||||
:active-tab="ResponseComposition.BODY"
|
||||
:request-result="currentResponse"
|
||||
:console="currentResponse?.console"
|
||||
|
@ -40,7 +50,7 @@
|
|||
|
||||
import { RequestResult } from '@/models/apiTest/common';
|
||||
import { ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import { ResponseComposition, ScenarioExecuteStatus } from '@/enums/apiEnum';
|
||||
import { ResponseComposition, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
|
||||
const responseResult = defineAsyncComponent(
|
||||
() => import('@/views/api-test/components/requestComposition/response/index.vue')
|
||||
|
@ -55,12 +65,12 @@
|
|||
const { t } = useI18n();
|
||||
|
||||
const currentLoop = ref(1);
|
||||
const currentResponse = computed(() => props.stepResponses?.[props.step.id]?.[currentLoop.value - 1]);
|
||||
const loopTotal = computed(() => props.stepResponses?.[props.step.id]?.length || 0);
|
||||
const currentResponse = computed(() => props.stepResponses?.[props.step.uniqueId]?.[currentLoop.value - 1]);
|
||||
const loopTotal = computed(() => props.stepResponses?.[props.step.uniqueId]?.length || 0);
|
||||
const finalExecuteStatus = computed(() => {
|
||||
if (props.stepResponses[props.step.id] && props.stepResponses[props.step.id].length > 0) {
|
||||
if (props.stepResponses[props.step.uniqueId] && props.stepResponses[props.step.uniqueId].length > 0) {
|
||||
// 有一次失败就是失败
|
||||
return props.stepResponses[props.step.id].some((report) => !report.isSuccessful)
|
||||
return props.stepResponses[props.step.uniqueId].some((report) => !report.isSuccessful)
|
||||
? ScenarioExecuteStatus.FAILED
|
||||
: ScenarioExecuteStatus.SUCCESS;
|
||||
}
|
||||
|
@ -74,6 +84,13 @@
|
|||
height: 500px;
|
||||
.arco-popover-content {
|
||||
@apply h-full;
|
||||
.response-header-pre {
|
||||
@apply h-full overflow-auto bg-white;
|
||||
.ms-scroll-bar();
|
||||
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--border-radius-small);
|
||||
}
|
||||
.response {
|
||||
.response-head {
|
||||
background-color: var(--color-text-n9);
|
||||
|
|
|
@ -88,11 +88,11 @@
|
|||
const visible = defineModel<boolean>('visible', { required: true });
|
||||
const currentLoop = ref(1);
|
||||
const currentResponse = computed(() => {
|
||||
if (props.step?.id) {
|
||||
return props.stepResponses?.[props.step?.id]?.[currentLoop.value - 1];
|
||||
if (props.step?.uniqueId) {
|
||||
return props.stepResponses?.[props.step?.uniqueId]?.[currentLoop.value - 1];
|
||||
}
|
||||
});
|
||||
const loopTotal = computed(() => (props.step?.id && props.stepResponses?.[props.step?.id]?.length) || 0);
|
||||
const loopTotal = computed(() => (props.step?.uniqueId && props.stepResponses?.[props.step?.uniqueId]?.length) || 0);
|
||||
|
||||
watch(
|
||||
() => visible.value,
|
||||
|
|
|
@ -120,6 +120,7 @@ export const defaultScenario: Scenario = {
|
|||
executeFailCount: 0,
|
||||
uploadFileIds: [],
|
||||
linkFileIds: [],
|
||||
reportId: '',
|
||||
// 前端渲染字段
|
||||
label: '',
|
||||
closable: true,
|
||||
|
|
|
@ -132,10 +132,12 @@ export default function useCreateActions() {
|
|||
return {
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id,
|
||||
uniqueId: getGenerateId(), // 生成唯一 ID,避免重复引用的步骤无法读取正确的执行结果
|
||||
config: {
|
||||
...defaultStepItemCommon.config,
|
||||
...config,
|
||||
},
|
||||
draggable: stepType !== ScenarioStepType.API_SCENARIO ? !item.config?.isQuoteScenarioStep : true, // 引用场景下的任何子步骤不可拖拽,除了场景本身
|
||||
isQuoteScenarioStep: item.config?.isQuoteScenarioStep || false,
|
||||
isRefScenarioStep: item.config?.isRefScenarioStep || false,
|
||||
children: item.children || [],
|
||||
|
|
|
@ -1,101 +1,102 @@
|
|||
<template>
|
||||
<div class="flex h-full flex-col gap-[8px]">
|
||||
<div class="action-line">
|
||||
<div class="action-group">
|
||||
<a-checkbox
|
||||
v-show="scenario.steps.length > 0"
|
||||
v-model:model-value="checkedAll"
|
||||
:indeterminate="indeterminate"
|
||||
:disabled="scenarioExecuteLoading"
|
||||
@change="handleChangeAll"
|
||||
/>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
{{ t('apiScenario.sum') }}
|
||||
<div class="text-[rgb(var(--primary-5))]">{{ totalStepCount }}</div>
|
||||
{{ t('apiScenario.steps') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<a-tooltip :content="isExpandAll ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
|
||||
<a-button
|
||||
<a-spin class="h-full w-full" :loading="loading">
|
||||
<div class="action-line">
|
||||
<div class="action-group">
|
||||
<a-checkbox
|
||||
v-show="scenario.steps.length > 0"
|
||||
type="outline"
|
||||
class="expand-step-btn arco-btn-outline--secondary"
|
||||
size="mini"
|
||||
@click="expandAllStep"
|
||||
>
|
||||
<MsIcon v-if="isExpandAll" type="icon-icon_comment_collapse_text_input" />
|
||||
<MsIcon v-else type="icon-icon_comment_expand_text_input" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<template v-if="checkedAll || indeterminate">
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchEnable">
|
||||
{{ t('common.batchEnable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDisable">
|
||||
{{ t('common.batchDisable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDebug">
|
||||
{{ t('common.batchDebug') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDelete">
|
||||
{{ t('common.batchDelete') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="action-group ml-auto">
|
||||
<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)]">{{ 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))]">{{ 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))]">{{ scenario.executeFailCount }}</div>
|
||||
</div>
|
||||
<MsButton v-if="scenario.isDebug === false" type="text" @click="checkReport">
|
||||
{{ t('apiScenario.checkReport') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!checkedAll && !indeterminate" class="action-group ml-auto">
|
||||
<a-input
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('apiScenario.searchByName')"
|
||||
allow-clear
|
||||
class="w-[200px]"
|
||||
v-model:model-value="checkedAll"
|
||||
:indeterminate="indeterminate"
|
||||
:disabled="scenarioExecuteLoading"
|
||||
@change="handleChangeAll"
|
||||
/>
|
||||
<a-button
|
||||
v-if="!props.isNew"
|
||||
type="outline"
|
||||
class="arco-btn-outline--secondary !mr-0 !p-[8px]"
|
||||
@click="refreshStepInfo"
|
||||
>
|
||||
<template #icon>
|
||||
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||
</template>
|
||||
</a-button>
|
||||
<div class="flex items-center gap-[4px]">
|
||||
{{ t('apiScenario.sum') }}
|
||||
<div class="text-[rgb(var(--primary-5))]">{{ totalStepCount }}</div>
|
||||
{{ t('apiScenario.steps') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="action-group">
|
||||
<a-tooltip :content="isExpandAll ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
|
||||
<a-button
|
||||
v-show="scenario.steps.length > 0"
|
||||
type="outline"
|
||||
class="expand-step-btn arco-btn-outline--secondary"
|
||||
size="mini"
|
||||
@click="expandAllStep"
|
||||
>
|
||||
<MsIcon v-if="isExpandAll" type="icon-icon_comment_collapse_text_input" />
|
||||
<MsIcon v-else type="icon-icon_comment_expand_text_input" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
<template v-if="checkedAll || indeterminate">
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchEnable">
|
||||
{{ t('common.batchEnable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDisable">
|
||||
{{ t('common.batchDisable') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDebug">
|
||||
{{ t('common.batchDebug') }}
|
||||
</a-button>
|
||||
<a-button type="outline" size="mini" :disabled="scenarioExecuteLoading" @click="batchDelete">
|
||||
{{ t('common.batchDelete') }}
|
||||
</a-button>
|
||||
</template>
|
||||
</div>
|
||||
<div class="action-group ml-auto">
|
||||
<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)]">{{ 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))]">{{ 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))]">{{ scenario.executeFailCount }}</div>
|
||||
</div>
|
||||
<MsButton v-if="scenario.isDebug === false" type="text" @click="checkReport">
|
||||
{{ t('apiScenario.checkReport') }}
|
||||
</MsButton>
|
||||
</div>
|
||||
</template>
|
||||
<div v-if="!checkedAll && !indeterminate" class="action-group ml-auto">
|
||||
<a-input
|
||||
v-model:model-value="keyword"
|
||||
:placeholder="t('apiScenario.searchByName')"
|
||||
allow-clear
|
||||
class="w-[200px]"
|
||||
/>
|
||||
<a-tooltip v-if="!props.isNew" position="left" :content="t('apiScenario.refreshRefScenario')">
|
||||
<a-button type="outline" class="arco-btn-outline--secondary !mr-0 !p-[8px]" @click="refreshStepInfo">
|
||||
<template #icon>
|
||||
<icon-refresh class="text-[var(--color-text-4)]" />
|
||||
</template>
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="h-[calc(100%-30px)]">
|
||||
<stepTree
|
||||
ref="stepTreeRef"
|
||||
v-model:steps="scenario.steps"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:stepKeyword="keyword"
|
||||
v-model:scenario="scenario"
|
||||
:expand-all="isExpandAll"
|
||||
:step-details="scenario.stepDetails"
|
||||
@update-resource="handleUpdateResource"
|
||||
/>
|
||||
</div>
|
||||
<div class="h-[calc(100%-30px)]">
|
||||
<stepTree
|
||||
ref="stepTreeRef"
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:steps="scenario.steps"
|
||||
v-model:checked-keys="checkedKeys"
|
||||
v-model:stepKeyword="keyword"
|
||||
v-model:scenario="scenario"
|
||||
:expand-all="isExpandAll"
|
||||
:step-details="scenario.stepDetails"
|
||||
@step-add="handleAddStepDone"
|
||||
@update-resource="handleUpdateResource"
|
||||
/>
|
||||
</div>
|
||||
</a-spin>
|
||||
</div>
|
||||
<a-modal
|
||||
v-model:visible="batchToggleVisible"
|
||||
|
@ -120,18 +121,20 @@
|
|||
// import dayjs from 'dayjs';
|
||||
|
||||
import { Message } from '@arco-design/web-vue';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
|
||||
import MsButton from '@/components/pure/ms-button/index.vue';
|
||||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import stepTree from './stepTree.vue';
|
||||
|
||||
import { getScenarioDetail } from '@/api/modules/api-test/scenario';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
import { deleteNodes, filterTree, getGenerateId, mapTree } from '@/utils';
|
||||
import { deleteNodes, filterTree, getGenerateId, mapTree, traverseTree } from '@/utils';
|
||||
import { countNodes } from '@/utils/tree';
|
||||
|
||||
import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario';
|
||||
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { ApiScenarioDebugRequest, Scenario, ScenarioStepItem } from '@/models/apiTest/scenario';
|
||||
import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
|
||||
import { ApiTestRouteEnum } from '@/enums/routeEnum';
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -148,11 +151,13 @@
|
|||
required: true,
|
||||
});
|
||||
const scenarioExecuteLoading = inject<Ref<boolean>>('scenarioExecuteLoading');
|
||||
const loading = ref(false);
|
||||
|
||||
const checkedAll = ref(false); // 是否全选
|
||||
const indeterminate = ref(false); // 是否半选
|
||||
const isExpandAll = ref(false); // 是否展开全部
|
||||
const checkedKeys = ref<(string | number)[]>([]); // 选中的key
|
||||
const selectedKeys = ref<(string | number)[]>([]); // 没啥用,现在用来展示选中样式
|
||||
const stepTreeRef = ref<InstanceType<typeof stepTree>>();
|
||||
const keyword = ref('');
|
||||
|
||||
|
@ -184,10 +189,17 @@
|
|||
}
|
||||
);
|
||||
|
||||
function handleAddStepDone() {
|
||||
checkedKeys.value = [];
|
||||
checkedAll.value = false;
|
||||
indeterminate.value = false;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => scenario.value.steps.length,
|
||||
() => scenario.value.id,
|
||||
() => {
|
||||
checkedKeys.value = [];
|
||||
selectedKeys.value = [];
|
||||
checkedAll.value = false;
|
||||
indeterminate.value = false;
|
||||
}
|
||||
|
@ -259,8 +271,73 @@
|
|||
});
|
||||
}
|
||||
|
||||
function refreshStepInfo() {
|
||||
console.log('刷新步骤信息');
|
||||
/**
|
||||
* 刷新引用场景的步骤数据
|
||||
*/
|
||||
async function refreshStepInfo() {
|
||||
try {
|
||||
loading.value = true;
|
||||
if (scenario.value.id) {
|
||||
const res = await getScenarioDetail(scenario.value.id);
|
||||
const refScenarioMap = new Map<string, ScenarioStepItem>();
|
||||
traverseTree(
|
||||
res.steps,
|
||||
(node) => {
|
||||
if (
|
||||
node.stepType === ScenarioStepType.API_SCENARIO &&
|
||||
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(node.refType)
|
||||
) {
|
||||
// 是引用的场景就存储起来
|
||||
refScenarioMap.set(node.id, node as ScenarioStepItem);
|
||||
}
|
||||
},
|
||||
(node) => {
|
||||
// 是引用的场景就没必要再递归子孙节点了
|
||||
return (
|
||||
node.stepType !== ScenarioStepType.API_SCENARIO &&
|
||||
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(node.refType)
|
||||
);
|
||||
}
|
||||
);
|
||||
scenario.value.steps = mapTree(scenario.value.steps, (node) => {
|
||||
const newStep = refScenarioMap.get(node.id);
|
||||
if (newStep) {
|
||||
node = {
|
||||
...cloneDeep(node), // 避免前端初始化的东西被丢弃
|
||||
...newStep,
|
||||
};
|
||||
node.children = mapTree(newStep.children || [], (child) => {
|
||||
if (
|
||||
child.parent &&
|
||||
child.parent.stepType === ScenarioStepType.API_SCENARIO &&
|
||||
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(child.parent.refType)
|
||||
) {
|
||||
// 如果根节点是引用场景
|
||||
child.isQuoteScenarioStep = true; // 标记为引用场景下的子步骤
|
||||
child.isRefScenarioStep = child.parent.refType === ScenarioStepRefType.REF; // 标记为完全引用场景
|
||||
child.draggable = false; // 引用场景下的任何步骤不可拖拽
|
||||
} else if (child.parent) {
|
||||
// 如果有父节点
|
||||
child.isQuoteScenarioStep = child.parent.isQuoteScenarioStep; // 复用父节点的引用场景标记
|
||||
child.isRefScenarioStep = child.parent.isRefScenarioStep; // 复用父节点的是否完全引用场景标记
|
||||
}
|
||||
if (selectedKeys.value.includes(node.id) && !selectedKeys.value.includes(child.id)) {
|
||||
// 如果有新增的子步骤,且当前步骤被选中,则这个新增的子步骤也要选中
|
||||
selectedKeys.value.push(child.id);
|
||||
}
|
||||
return child;
|
||||
}) as ScenarioStepItem[];
|
||||
}
|
||||
return node;
|
||||
});
|
||||
Message.success(t('apiScenario.updateRefScenarioSuccess'));
|
||||
}
|
||||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function batchDebug() {
|
||||
|
@ -272,14 +349,8 @@
|
|||
// 如果步骤未开启,则删除已选 id,方便下面waitingDebugStepDetails详情判断是否携带
|
||||
checkedKeysSet.delete(node.id);
|
||||
node.executeStatus = undefined;
|
||||
} else if (
|
||||
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(node.stepType)
|
||||
) {
|
||||
// 请求和场景类型才直接显示执行中,其他控制器需要等待执行完毕才结算执行结果
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
} else {
|
||||
// 其他类型步骤不显示执行状态
|
||||
node.executeStatus = undefined;
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
}
|
||||
return !!node.enable;
|
||||
}
|
||||
|
@ -329,8 +400,9 @@
|
|||
.action-line {
|
||||
@apply flex items-center;
|
||||
|
||||
gap: 16px;
|
||||
margin-bottom: 8px;
|
||||
height: 32px;
|
||||
gap: 16px;
|
||||
.action-group {
|
||||
@apply flex items-center;
|
||||
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
class="w-[100px] px-[8px]"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiScenario.variable', { suffix: '${var}' })"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
|
@ -15,6 +16,7 @@
|
|||
v-model:model-value="innerData.condition"
|
||||
size="mini"
|
||||
class="w-[90px] px-[8px]"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
<a-option v-for="opt of conditionOptions" :key="opt.value" :value="opt.value">
|
||||
|
@ -28,6 +30,7 @@
|
|||
size="mini"
|
||||
class="w-[110px] px-[8px]"
|
||||
:placeholder="t('apiScenario.value')"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
|
@ -45,6 +48,7 @@
|
|||
const props = defineProps<{
|
||||
data: ConditionStepDetail;
|
||||
stepId: string | number;
|
||||
disabled: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', innerData: ConditionStepDetail): void;
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
:options="loopOptions"
|
||||
size="mini"
|
||||
class="w-[85px] px-[8px]"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
/>
|
||||
<a-tooltip
|
||||
|
@ -22,6 +23,7 @@
|
|||
hide-button
|
||||
:precision="0"
|
||||
model-event="input"
|
||||
:disabled="props.disabled"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
|
@ -38,6 +40,7 @@
|
|||
class="w-[110px] px-[8px]"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiScenario.variable')"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
|
@ -50,6 +53,7 @@
|
|||
class="w-[110px] px-[8px]"
|
||||
:placeholder="t('apiScenario.valuePrefix')"
|
||||
:max-length="255"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
|
@ -61,6 +65,7 @@
|
|||
:options="whileOptions"
|
||||
size="mini"
|
||||
class="w-[75px] px-[8px]"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
/>
|
||||
<template v-if="innerData.whileController.conditionType === WhileConditionType.CONDITION">
|
||||
|
@ -74,6 +79,7 @@
|
|||
class="w-[100px] px-[8px]"
|
||||
:max-length="255"
|
||||
:placeholder="t('apiScenario.variable', { suffix: '${var}' })"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
|
@ -82,6 +88,7 @@
|
|||
v-model:model-value="innerData.whileController.msWhileVariable.condition"
|
||||
size="mini"
|
||||
class="w-[90px] px-[8px]"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
<a-option v-for="opt of conditionOptions" :key="opt.value" :value="opt.value">
|
||||
|
@ -98,6 +105,7 @@
|
|||
size="mini"
|
||||
class="w-[110px] px-[8px]"
|
||||
:placeholder="t('apiScenario.value')"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
|
@ -114,6 +122,7 @@
|
|||
size="mini"
|
||||
class="w-[200px] px-[8px]"
|
||||
:placeholder="t('apiScenario.expression')"
|
||||
:disabled="props.disabled"
|
||||
@change="handleInputChange"
|
||||
>
|
||||
</a-input>
|
||||
|
@ -131,6 +140,7 @@
|
|||
hide-button
|
||||
:precision="0"
|
||||
model-event="input"
|
||||
:disabled="props.disabled"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
|
@ -154,6 +164,7 @@
|
|||
hide-button
|
||||
class="w-[110px] px-[8px]"
|
||||
model-event="input"
|
||||
:disabled="props.disabled"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
|
@ -170,6 +181,7 @@
|
|||
hide-button
|
||||
class="w-[110px] px-[8px]"
|
||||
model-event="input"
|
||||
:disabled="props.disabled"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
|
@ -191,6 +203,7 @@
|
|||
const props = defineProps<{
|
||||
data: LoopStepDetail;
|
||||
stepId: string | number;
|
||||
disabled: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', innerData: LoopStepDetail): void;
|
||||
|
@ -227,7 +240,6 @@
|
|||
|
||||
watchEffect(() => {
|
||||
innerData.value = props.data;
|
||||
console.log('watchEffect', props.data);
|
||||
});
|
||||
|
||||
// 接收全局双击时间戳
|
||||
|
|
|
@ -5,14 +5,19 @@
|
|||
"
|
||||
class="flex items-center gap-[4px]"
|
||||
>
|
||||
<a-popover position="bl" content-class="detail-popover" arrow-class="hidden">
|
||||
<a-popover
|
||||
position="bl"
|
||||
content-class="quote-content-detail-popover"
|
||||
arrow-class="hidden"
|
||||
@popup-visible-change="handleVisibleChange"
|
||||
>
|
||||
<MsIcon type="icon-icon-draft" class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" />
|
||||
<template #content>
|
||||
<div class="flex flex-col gap-[16px]">
|
||||
<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 }} -->
|
||||
{{ originProjectName }}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
|
@ -43,6 +48,7 @@
|
|||
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
|
||||
import MsTag from '@/components/pure/ms-tag/ms-tag.vue';
|
||||
|
||||
import { getStepProjectInfo } from '@/api/modules/api-test/scenario';
|
||||
import { useI18n } from '@/hooks/useI18n';
|
||||
import useOpenNewPage from '@/hooks/useOpenNewPage';
|
||||
import useAppStore from '@/store/modules/app';
|
||||
|
@ -61,6 +67,15 @@
|
|||
const { t } = useI18n();
|
||||
const { openNewPage } = useOpenNewPage();
|
||||
|
||||
const originProjectName = ref('');
|
||||
|
||||
async function handleVisibleChange(val: boolean) {
|
||||
if (val && props.data.originProjectId) {
|
||||
const res = await getStepProjectInfo(props.data.originProjectId);
|
||||
originProjectName.value = res.name;
|
||||
}
|
||||
}
|
||||
|
||||
function goDetail() {
|
||||
const _stepType = getStepType(props.data);
|
||||
switch (true) {
|
||||
|
@ -91,8 +106,8 @@
|
|||
}
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.detail-popover {
|
||||
width: 350px;
|
||||
<style lang="less">
|
||||
.quote-content-detail-popover {
|
||||
width: 300px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -10,6 +10,7 @@
|
|||
hide-button
|
||||
:precision="0"
|
||||
model-event="input"
|
||||
:disabled="props.disabled"
|
||||
@blur="handleInputChange"
|
||||
>
|
||||
<template #prefix>
|
||||
|
@ -30,6 +31,7 @@
|
|||
|
||||
const props = defineProps<{
|
||||
data: WaitTimeContentProps;
|
||||
disabled: boolean;
|
||||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'change', innerData: WaitTimeContentProps): void;
|
||||
|
|
|
@ -60,8 +60,8 @@
|
|||
<div class="mr-[8px] flex items-center gap-[8px]">
|
||||
<!-- 步骤启用/禁用,完全引用的场景下的子孙步骤不可禁用 -->
|
||||
<a-switch
|
||||
v-show="step.isRefScenarioStep !== true"
|
||||
v-model:model-value="step.enable"
|
||||
:disabled="step.isRefScenarioStep"
|
||||
size="small"
|
||||
@click.stop="handleStepToggleEnable(step)"
|
||||
></a-switch>
|
||||
|
@ -90,6 +90,7 @@
|
|||
:is="getStepContent(step)"
|
||||
:data="checkStepIsApi(step) || step.stepType === ScenarioStepType.API_SCENARIO ? step : step.config"
|
||||
:step-id="step.id"
|
||||
:disabled="!!step.isQuoteScenarioStep"
|
||||
@quick-input="setQuickInput(step, $event)"
|
||||
@change="handleStepContentChange($event, step)"
|
||||
@click.stop
|
||||
|
@ -117,6 +118,7 @@
|
|||
{{ step.name }}
|
||||
</div>
|
||||
<MsIcon
|
||||
v-if="!step.isQuoteScenarioStep"
|
||||
type="icon-icon_edit_outlined"
|
||||
class="edit-script-name-icon"
|
||||
@click.stop="handleStepNameClick(step)"
|
||||
|
@ -155,6 +157,7 @@
|
|||
{{ step.name || t('apiScenario.pleaseInputStepDesc') }}
|
||||
</div>
|
||||
<MsIcon
|
||||
v-if="!step.isQuoteScenarioStep"
|
||||
type="icon-icon_edit_outlined"
|
||||
class="edit-script-name-icon"
|
||||
@click.stop="handleStepDescClick(step)"
|
||||
|
@ -174,17 +177,17 @@
|
|||
@click="setFocusNodeKey(step.id)"
|
||||
@other-create="handleOtherCreate"
|
||||
@close="setFocusNodeKey('')"
|
||||
@add-done="scenario.unSaved = true"
|
||||
@add-done="handleAddStepDone"
|
||||
/>
|
||||
</template>
|
||||
<template #extraEnd="step">
|
||||
<responsePopover
|
||||
v-if="
|
||||
![
|
||||
ScenarioStepType.LOOP_CONTROLLER,
|
||||
ScenarioStepType.IF_CONTROLLER,
|
||||
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
||||
ScenarioStepType.CONSTANT_TIMER,
|
||||
[
|
||||
ScenarioStepType.API,
|
||||
ScenarioStepType.API_CASE,
|
||||
ScenarioStepType.SCRIPT,
|
||||
ScenarioStepType.CUSTOM_REQUEST,
|
||||
].includes(step.stepType) &&
|
||||
(getExecuteStatus(step) === ScenarioExecuteStatus.SUCCESS ||
|
||||
getExecuteStatus(step) === ScenarioExecuteStatus.FAILED)
|
||||
|
@ -213,7 +216,7 @@
|
|||
<createStepActions
|
||||
v-model:selected-keys="selectedKeys"
|
||||
v-model:steps="steps"
|
||||
@add-done="scenario.unSaved = true"
|
||||
@add-done="handleAddStepDone"
|
||||
@other-create="handleOtherCreate"
|
||||
>
|
||||
<a-button type="dashed" class="add-step-btn" long>
|
||||
|
@ -248,6 +251,7 @@
|
|||
<importApiDrawer
|
||||
v-if="importApiDrawerVisible"
|
||||
v-model:visible="importApiDrawerVisible"
|
||||
:scenario-id="scenario.id"
|
||||
@copy="handleImportApiApply('copy', $event)"
|
||||
@quote="handleImportApiApply('quote', $event)"
|
||||
/>
|
||||
|
@ -452,6 +456,7 @@
|
|||
}>();
|
||||
const emit = defineEmits<{
|
||||
(e: 'updateResource', uploadFileIds: string[], linkFileIds: string[]): void;
|
||||
(e: 'stepAdd'): void;
|
||||
}>();
|
||||
|
||||
const appStore = useAppStore();
|
||||
|
@ -470,11 +475,13 @@
|
|||
const scenario = defineModel<Scenario>('scenario', {
|
||||
required: true,
|
||||
});
|
||||
const selectedKeys = defineModel<(string | number)[]>('selectedKeys', {
|
||||
required: true,
|
||||
}); // 没啥用,目前用来展示选中样式
|
||||
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
|
||||
const localExecuteUrl = inject<Ref<string>>('localExecuteUrl');
|
||||
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
|
||||
|
||||
const selectedKeys = ref<(string | number)[]>([]); // 没啥用,目前用来展示选中样式
|
||||
const loading = ref(false);
|
||||
const treeRef = ref<InstanceType<typeof MsTree>>();
|
||||
const focusStepKey = ref<string | number>(''); // 聚焦的key
|
||||
|
@ -489,9 +496,9 @@
|
|||
}
|
||||
|
||||
function getExecuteStatus(step: ScenarioStepItem) {
|
||||
if (scenario.value.stepResponses && scenario.value.stepResponses[step.id]) {
|
||||
if (scenario.value.stepResponses && scenario.value.stepResponses[step.uniqueId]) {
|
||||
// 有一次失败就是失败
|
||||
return scenario.value.stepResponses[step.id].some((report) => !report.isSuccessful)
|
||||
return scenario.value.stepResponses[step.uniqueId].some((report) => !report.isSuccessful)
|
||||
? ScenarioExecuteStatus.FAILED
|
||||
: ScenarioExecuteStatus.SUCCESS;
|
||||
}
|
||||
|
@ -509,8 +516,8 @@
|
|||
const firstHasResultChild = step.children?.find((child) => {
|
||||
return checkStepIsApi(child) || child.stepType === ScenarioStepType.SCRIPT;
|
||||
});
|
||||
return firstHasResultChild && scenario.value.stepResponses[firstHasResultChild.id]
|
||||
? `${scenario.value.stepResponses[firstHasResultChild.id].length}/${step.config.msCountController.loops}`
|
||||
return firstHasResultChild && scenario.value.stepResponses[firstHasResultChild.uniqueId]
|
||||
? `${scenario.value.stepResponses[firstHasResultChild.uniqueId].length}/${step.config.msCountController.loops}`
|
||||
: undefined;
|
||||
}
|
||||
return undefined;
|
||||
|
@ -798,6 +805,7 @@
|
|||
executeStatus: undefined,
|
||||
copyFromStepId: childCopyFromStepId,
|
||||
id: childId,
|
||||
uniqueId: childId,
|
||||
};
|
||||
})[0]
|
||||
),
|
||||
|
@ -806,6 +814,7 @@
|
|||
sort: node.sort + 1,
|
||||
isNew: true,
|
||||
id,
|
||||
uniqueId: id,
|
||||
},
|
||||
'after',
|
||||
selectedIfNeed,
|
||||
|
@ -950,6 +959,11 @@
|
|||
}
|
||||
}
|
||||
|
||||
function handleAddStepDone() {
|
||||
emit('stepAdd');
|
||||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理步骤选中事件
|
||||
* @param _selectedKeys 选中的 key集合
|
||||
|
@ -1000,27 +1014,16 @@
|
|||
}
|
||||
|
||||
const websocketMap: Record<string | number, WebSocket> = {};
|
||||
let temporaryStepReportMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换场景tab导致报告丢失
|
||||
|
||||
watch(
|
||||
() => scenario.value.id,
|
||||
() => {
|
||||
const stepKeys = Object.keys(temporaryStepReportMap);
|
||||
if (stepKeys.length > 0) {
|
||||
stepKeys.forEach((key) => {
|
||||
const report = temporaryStepReportMap[key];
|
||||
scenario.value.stepResponses[report.stepId] = temporaryStepReportMap[key];
|
||||
});
|
||||
temporaryStepReportMap = {};
|
||||
updateStepStatus(steps.value, scenario.value.stepResponses);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
*/
|
||||
function debugSocket(step: ScenarioStepItem, reportId: string | number, executeType?: 'localExec' | 'serverExec') {
|
||||
function debugSocket(
|
||||
step: ScenarioStepItem,
|
||||
_scenario: Scenario,
|
||||
reportId: string | number,
|
||||
executeType?: 'localExec' | 'serverExec'
|
||||
) {
|
||||
websocketMap[reportId] = getSocket(
|
||||
reportId || '',
|
||||
executeType === 'localExec' ? '/ws/debug' : '',
|
||||
|
@ -1032,34 +1035,21 @@
|
|||
if (step.reportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||
data.taskResult.requestResults.forEach((result) => {
|
||||
if (scenario.value.stepResponses[result.stepId] === undefined) {
|
||||
scenario.value.stepResponses[result.stepId] = [];
|
||||
if (_scenario.stepResponses[result.stepId] === undefined) {
|
||||
_scenario.stepResponses[result.stepId] = [];
|
||||
}
|
||||
scenario.value.stepResponses[result.stepId].push({
|
||||
_scenario.stepResponses[result.stepId].push({
|
||||
...result,
|
||||
console: data.taskResult.console,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// 不是则需要把报告缓存起来,等切换到对应的tab再赋值
|
||||
data.taskResult.requestResults.forEach((result) => {
|
||||
if (step.reportId) {
|
||||
if (temporaryStepReportMap[step.reportId] === undefined) {
|
||||
temporaryStepReportMap[step.reportId] = [];
|
||||
}
|
||||
temporaryStepReportMap[step.reportId].push({
|
||||
...result,
|
||||
console: data.taskResult.console,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
websocketMap[reportId].close();
|
||||
websocketMap[reportId]?.close();
|
||||
if (step.reportId === data.reportId) {
|
||||
step.isExecuting = false;
|
||||
updateStepStatus([step], scenario.value.stepResponses);
|
||||
updateStepStatus([step], _scenario.stepResponses);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -1076,7 +1066,7 @@
|
|||
try {
|
||||
currentStep.isExecuting = true;
|
||||
currentStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
debugSocket(currentStep, executeParams.reportId, executeType); // 开启websocket
|
||||
debugSocket(currentStep, scenario.value, executeParams.reportId, executeType); // 开启websocket
|
||||
const res = await debugScenario({
|
||||
id: scenario.value.id || '',
|
||||
grouped: false,
|
||||
|
@ -1100,6 +1090,7 @@
|
|||
console.log(error);
|
||||
websocketMap[executeParams.reportId].close();
|
||||
currentStep.isExecuting = false;
|
||||
updateStepStatus([currentStep], scenario.value.stepResponses);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1110,25 +1101,24 @@
|
|||
if (node.isExecuting) {
|
||||
return;
|
||||
}
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.id, 'id');
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.uniqueId, 'uniqueId');
|
||||
if (realStep) {
|
||||
realStep.reportId = getGenerateId();
|
||||
const _stepDetails = {};
|
||||
const stepFileParam = scenario.value.stepFileParam[realStep.id];
|
||||
traverseTree(
|
||||
realStep,
|
||||
(step) => {
|
||||
// 当前步骤是启用的情况,才需要继续递归子孙步骤;否则无需向下递归
|
||||
return step.enable;
|
||||
},
|
||||
(step) => {
|
||||
if (step.enable) {
|
||||
// 启用的步骤才执行
|
||||
_stepDetails[step.id] = stepDetails.value[step.id];
|
||||
step.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
}
|
||||
delete scenario.value.stepResponses[step.id]; // 先移除上一次的执行结果
|
||||
return step;
|
||||
delete scenario.value.stepResponses[step.uniqueId]; // 先移除上一次的执行结果
|
||||
},
|
||||
(step) => {
|
||||
// 当前步骤是启用的情况,才需要继续递归子孙步骤;否则无需向下递归
|
||||
return step.enable;
|
||||
}
|
||||
);
|
||||
realExecute(
|
||||
|
@ -1152,7 +1142,7 @@
|
|||
function handleApiExecute(request: RequestParam, executeType?: 'localExec' | 'serverExec') {
|
||||
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, request.stepId, 'id');
|
||||
if (realStep) {
|
||||
delete scenario.value.stepResponses[realStep.id]; // 先移除上一次的执行结果
|
||||
delete scenario.value.stepResponses[realStep.uniqueId]; // 先移除上一次的执行结果
|
||||
realStep.reportId = getGenerateId();
|
||||
realStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
request.executeLoading = true;
|
||||
|
@ -1184,6 +1174,7 @@
|
|||
projectId: appStore.currentProjectId,
|
||||
isExecuting: false,
|
||||
reportId,
|
||||
uniqueId: request.stepId,
|
||||
};
|
||||
realExecute(
|
||||
{
|
||||
|
@ -1288,6 +1279,7 @@
|
|||
} else {
|
||||
steps.value = steps.value.concat(insertSteps);
|
||||
}
|
||||
emit('stepAdd');
|
||||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
||||
|
@ -1318,6 +1310,7 @@
|
|||
method: request.method,
|
||||
},
|
||||
id: request.stepId,
|
||||
uniqueId: request.stepId,
|
||||
projectId: appStore.currentProjectId,
|
||||
},
|
||||
activeStep.value,
|
||||
|
@ -1333,6 +1326,7 @@
|
|||
method: request.method,
|
||||
},
|
||||
id: request.stepId,
|
||||
uniqueId: request.stepId,
|
||||
sort: steps.value.length + 1,
|
||||
stepType: ScenarioStepType.CUSTOM_REQUEST,
|
||||
refType: ScenarioStepRefType.DIRECT,
|
||||
|
@ -1340,6 +1334,7 @@
|
|||
projectId: appStore.currentProjectId,
|
||||
});
|
||||
}
|
||||
emit('stepAdd');
|
||||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
||||
|
@ -1347,6 +1342,14 @@
|
|||
* API 详情抽屉关闭时应用更改
|
||||
*/
|
||||
function applyApiStep(request: RequestParam | CaseRequestParam) {
|
||||
if (activeStep.value) {
|
||||
const _stepType = getStepType(activeStep.value);
|
||||
if (_stepType.isQuoteCase || activeStep.value.isQuoteScenarioStep) {
|
||||
// 引用的 case 和引用的场景步骤都不可更改
|
||||
stepDetails.value[activeStep.value.id] = request; // 为了设置一次正确的polymorphicName
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (request.unSaved) {
|
||||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
@ -1400,6 +1403,7 @@
|
|||
steps.value.push({
|
||||
...cloneDeep(defaultStepItemCommon),
|
||||
id,
|
||||
uniqueId: id,
|
||||
sort: steps.value.length + 1,
|
||||
stepType: ScenarioStepType.SCRIPT,
|
||||
refType: ScenarioStepRefType.DIRECT,
|
||||
|
@ -1407,6 +1411,7 @@
|
|||
projectId: appStore.currentProjectId,
|
||||
});
|
||||
}
|
||||
emit('stepAdd');
|
||||
scenario.value.unSaved = true;
|
||||
}
|
||||
|
||||
|
@ -1482,6 +1487,7 @@
|
|||
return true;
|
||||
});
|
||||
}
|
||||
console.log(dragNode, dropNode);
|
||||
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
|
||||
if (dragResult) {
|
||||
Message.success(t('common.moveSuccess'));
|
||||
|
|
|
@ -17,9 +17,14 @@ export default function updateStepStatus(
|
|||
ScenarioStepType.LOOP_CONTROLLER,
|
||||
ScenarioStepType.IF_CONTROLLER,
|
||||
ScenarioStepType.ONCE_ONLY_CONTROLLER,
|
||||
ScenarioStepType.API_SCENARIO,
|
||||
].includes(node.stepType)
|
||||
) {
|
||||
// 逻辑控制器内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的
|
||||
if (!node.executeStatus) {
|
||||
// 没有执行状态,说明未参与执行,直接跳过
|
||||
break;
|
||||
}
|
||||
// 逻辑控制器和场景内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的
|
||||
let hasNotExecuted = false;
|
||||
let hasFailure = false;
|
||||
if (!node.children || node.children.length === 0) {
|
||||
|
@ -54,8 +59,8 @@ export default function updateStepStatus(
|
|||
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
|
||||
} else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) {
|
||||
// 非逻辑控制器直接更改本身状态
|
||||
if (stepResponses[node.id] && stepResponses[node.id].length > 0) {
|
||||
node.executeStatus = stepResponses[node.id].some((report) => !report.isSuccessful)
|
||||
if (stepResponses[node.uniqueId] && stepResponses[node.uniqueId].length > 0) {
|
||||
node.executeStatus = stepResponses[node.uniqueId].some((report) => !report.isSuccessful)
|
||||
? ScenarioExecuteStatus.FAILED
|
||||
: ScenarioExecuteStatus.SUCCESS;
|
||||
} else {
|
||||
|
|
|
@ -158,65 +158,48 @@
|
|||
const currentEnvConfig = ref<EnvConfig>();
|
||||
const executeButtonRef = ref<InstanceType<typeof executeButton>>();
|
||||
|
||||
const websocket = ref<WebSocket>();
|
||||
const temporaryScenarioReportMap = {}; // 缓存websocket返回的报告内容,避免执行接口后切换tab导致报告丢失
|
||||
const websocketMap: Record<string | number, WebSocket> = {};
|
||||
|
||||
function setStepExecuteStatus() {
|
||||
updateStepStatus(activeScenarioTab.value.steps, activeScenarioTab.value.stepResponses);
|
||||
function setStepExecuteStatus(scenario: Scenario) {
|
||||
updateStepStatus(scenario.steps, scenario.stepResponses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 开启websocket监听,接收执行结果
|
||||
*/
|
||||
function debugSocket(reportId?: string | number, executeType?: 'localExec' | 'serverExec', localExecuteUrl?: string) {
|
||||
websocket.value = getSocket(
|
||||
reportId || '',
|
||||
function debugSocket(scenario: Scenario, executeType?: 'localExec' | 'serverExec', localExecuteUrl?: string) {
|
||||
websocketMap[scenario.reportId] = getSocket(
|
||||
scenario.reportId || '',
|
||||
executeType === 'localExec' ? '/ws/debug' : '',
|
||||
executeType === 'localExec' ? localExecuteUrl : ''
|
||||
);
|
||||
websocket.value.addEventListener('message', (event) => {
|
||||
websocketMap[scenario.reportId].addEventListener('message', (event) => {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.msgType === 'EXEC_RESULT') {
|
||||
if (activeScenarioTab.value.reportId === data.reportId) {
|
||||
if (scenario.reportId === data.reportId) {
|
||||
// 判断当前查看的tab是否是当前返回的报告的tab,是的话直接赋值
|
||||
data.taskResult.requestResults.forEach((result) => {
|
||||
if (activeScenarioTab.value.stepResponses[result.stepId] === undefined) {
|
||||
activeScenarioTab.value.stepResponses[result.stepId] = [];
|
||||
if (scenario.stepResponses[result.stepId] === undefined) {
|
||||
scenario.stepResponses[result.stepId] = [];
|
||||
}
|
||||
activeScenarioTab.value.stepResponses[result.stepId].push({
|
||||
scenario.stepResponses[result.stepId].push({
|
||||
...result,
|
||||
console: data.taskResult.console,
|
||||
});
|
||||
if (result.isSuccessful) {
|
||||
activeScenarioTab.value.executeSuccessCount += 1;
|
||||
scenario.executeSuccessCount += 1;
|
||||
} else {
|
||||
activeScenarioTab.value.executeFailCount += 1;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// 不是则需要把报告缓存起来,等切换到对应的tab再赋值
|
||||
data.taskResult.requestResults.forEach((result) => {
|
||||
if (activeScenarioTab.value.reportId) {
|
||||
if (temporaryScenarioReportMap[activeScenarioTab.value.reportId] === undefined) {
|
||||
temporaryScenarioReportMap[activeScenarioTab.value.reportId] = {};
|
||||
}
|
||||
if (temporaryScenarioReportMap[activeScenarioTab.value.reportId][result.stepId]) {
|
||||
temporaryScenarioReportMap[activeScenarioTab.value.reportId][result.stepId] = [];
|
||||
}
|
||||
temporaryScenarioReportMap[activeScenarioTab.value.reportId][result.stepId].push({
|
||||
...result,
|
||||
console: data.taskResult.console,
|
||||
});
|
||||
scenario.executeFailCount += 1;
|
||||
}
|
||||
});
|
||||
}
|
||||
} else if (data.msgType === 'EXEC_END') {
|
||||
// 执行结束,关闭websocket
|
||||
websocket.value?.close();
|
||||
if (activeScenarioTab.value.reportId === data.reportId) {
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
activeScenarioTab.value.isExecute = false;
|
||||
setStepExecuteStatus();
|
||||
websocketMap[scenario.reportId]?.close();
|
||||
if (scenario.reportId === data.reportId) {
|
||||
scenario.executeLoading = false;
|
||||
scenario.isExecute = false;
|
||||
setStepExecuteStatus(scenario);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -237,7 +220,6 @@
|
|||
) {
|
||||
try {
|
||||
activeScenarioTab.value.executeLoading = true;
|
||||
debugSocket(executeParams.reportId, executeType, localExecuteUrl); // 开启websocket
|
||||
// 重置执行结果
|
||||
activeScenarioTab.value.executeTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
|
||||
activeScenarioTab.value.executeSuccessCount = 0;
|
||||
|
@ -245,6 +227,7 @@
|
|||
activeScenarioTab.value.stepResponses = {};
|
||||
activeScenarioTab.value.reportId = executeParams.reportId; // 存储报告ID
|
||||
activeScenarioTab.value.isDebug = !isExecute;
|
||||
debugSocket(activeScenarioTab.value, executeType, localExecuteUrl); // 开启websocket
|
||||
let res;
|
||||
if (isExecute && executeType !== 'localExec' && !activeScenarioTab.value.isNew) {
|
||||
// 执行场景且非本地执行且非未保存场景
|
||||
|
@ -290,9 +273,9 @@
|
|||
} catch (error) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.log(error);
|
||||
websocket.value?.close();
|
||||
websocketMap[activeScenarioTab.value.reportId]?.close();
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
setStepExecuteStatus();
|
||||
setStepExecuteStatus(activeScenarioTab.value);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -307,12 +290,6 @@
|
|||
if (node.enable) {
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
waitingDebugStepDetails[node.id] = activeScenarioTab.value.stepDetails[node.id];
|
||||
if (
|
||||
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(node.stepType)
|
||||
) {
|
||||
// 请求和场景类型才直接显示执行中,其他控制器需要等待执行完毕才结算执行结果
|
||||
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
|
||||
}
|
||||
}
|
||||
return !!node.enable;
|
||||
});
|
||||
|
@ -329,63 +306,59 @@
|
|||
}
|
||||
|
||||
function handleStopExecute() {
|
||||
websocket.value?.close();
|
||||
websocketMap[activeScenarioTab.value.reportId]?.close();
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
setStepExecuteStatus();
|
||||
setStepExecuteStatus(activeScenarioTab.value);
|
||||
}
|
||||
|
||||
watch(
|
||||
() => activeScenarioTab.value.id,
|
||||
(val) => {
|
||||
if (val !== 'all' && activeScenarioTab.value.reportId && !activeScenarioTab.value.executeLoading) {
|
||||
// 当前查看的 tab 非全部场景 tab 页,且当前场景有报告ID,且不是正在执行中,则读取缓存报告
|
||||
const cacheReport = temporaryScenarioReportMap[activeScenarioTab.value.reportId];
|
||||
if (cacheReport) {
|
||||
// 如果有缓存的报告未读取,则直接赋值
|
||||
Object.keys(cacheReport).forEach((stepId) => {
|
||||
const result = cacheReport[stepId];
|
||||
activeScenarioTab.value.stepResponses[stepId] = result;
|
||||
if (result.isSuccessful) {
|
||||
activeScenarioTab.value.executeSuccessCount += 1;
|
||||
} else {
|
||||
activeScenarioTab.value.executeFailCount += 1;
|
||||
}
|
||||
});
|
||||
activeScenarioTab.value.executeLoading = false;
|
||||
delete temporaryScenarioReportMap[activeScenarioTab.value.reportId]; // 取完释放缓存
|
||||
setStepExecuteStatus();
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function newTab(defaultScenarioInfo?: Scenario, action?: 'copy' | 'execute') {
|
||||
if (defaultScenarioInfo) {
|
||||
const isCopy = action === 'copy';
|
||||
let copySteps: ScenarioStepItem[] = [];
|
||||
if (isCopy) {
|
||||
copySteps = mapTree(defaultScenarioInfo.steps, (node) => {
|
||||
return {
|
||||
...node,
|
||||
copyFromStepId: node.id,
|
||||
id: getGenerateId(),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// 场景被复制,递归处理节点,增加copyFromStepId
|
||||
copySteps = mapTree(defaultScenarioInfo.steps, (node) => {
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.stepType === ScenarioStepType.API_SCENARIO &&
|
||||
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(node.parent.refType)
|
||||
) {
|
||||
// 如果父节点是引用场景
|
||||
// 如果根节点是引用场景
|
||||
node.isQuoteScenarioStep = true; // 标记为引用场景下的子步骤
|
||||
node.isRefScenarioStep = node.parent.refType === ScenarioStepRefType.REF; // 标记为引用场景下的子步骤
|
||||
node.isRefScenarioStep = node.parent.refType === ScenarioStepRefType.REF; // 标记为完全引用场景
|
||||
node.draggable = false; // 引用场景下的任何步骤不可拖拽
|
||||
node.id = getGenerateId(); // 重新生成 ID
|
||||
} else if (node.parent) {
|
||||
// 如果有父节点
|
||||
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; // 复用父节点的引用场景标记
|
||||
node.isRefScenarioStep = node.parent.isRefScenarioStep; // 复用父节点的是否完全引用场景标记
|
||||
}
|
||||
if (!node.isQuoteScenarioStep && !node.isRefScenarioStep) {
|
||||
// 非引用场景步骤
|
||||
node.id = getGenerateId(); // 重新生成 ID
|
||||
}
|
||||
node.copyFromStepId = node.id;
|
||||
node.uniqueId = getGenerateId();
|
||||
return node;
|
||||
});
|
||||
} else {
|
||||
// 正常打开场景详情,递归处理节点,标记引用场景下的子步骤
|
||||
copySteps = mapTree(defaultScenarioInfo.steps, (node) => {
|
||||
if (
|
||||
node.parent &&
|
||||
node.parent.stepType === ScenarioStepType.API_SCENARIO &&
|
||||
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(node.parent.refType)
|
||||
) {
|
||||
// 如果根节点是引用场景
|
||||
node.isQuoteScenarioStep = true; // 标记为引用场景下的子步骤
|
||||
node.isRefScenarioStep = node.parent.refType === ScenarioStepRefType.REF; // 标记为完全引用场景
|
||||
node.draggable = false; // 引用场景下的任何步骤不可拖拽
|
||||
} else if (node.parent) {
|
||||
// 如果有父节点
|
||||
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; // 复用父节点的引用场景标记
|
||||
node.isRefScenarioStep = node.parent.isRefScenarioStep; // 复用父节点的是否完全引用场景标记
|
||||
}
|
||||
node.uniqueId = getGenerateId();
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
@ -393,6 +366,7 @@
|
|||
...defaultScenarioInfo,
|
||||
steps: copySteps,
|
||||
id: isCopy ? getGenerateId() : defaultScenarioInfo.id || '',
|
||||
uniqueId: getGenerateId(),
|
||||
label: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name,
|
||||
name: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name,
|
||||
isNew: isCopy,
|
||||
|
|
|
@ -65,6 +65,8 @@ export default {
|
|||
'apiScenario.sumLoop': '共{count}次循环',
|
||||
'apiScenario.times': '次',
|
||||
'apiScenario.executionResult': '执行结果',
|
||||
'apiScenario.refreshRefScenario': '刷新引用场景数据',
|
||||
'apiScenario.updateRefScenarioSuccess': '引用场景数据已更新',
|
||||
// 批量操作文案
|
||||
'api_scenario.batch_operation.success': '成功{opt}至 {name}',
|
||||
'api_scenario.table.batchMoveConfirm': '{opt}{count}个场景至已选模块',
|
||||
|
|
Loading…
Reference in New Issue