fix(接口场景): 引用场景响应&部分 bug 修复

This commit is contained in:
baiqi 2024-04-02 20:53:37 +08:00 committed by Craftsman
parent b628f853cb
commit ffd19fae21
26 changed files with 489 additions and 380 deletions

View File

@ -21,6 +21,7 @@ import {
GetModuleTreeUrl, GetModuleTreeUrl,
GetScenarioStepUrl, GetScenarioStepUrl,
GetScenarioUrl, GetScenarioUrl,
GetStepProjectInfoUrl,
GetSystemRequestUrl, GetSystemRequestUrl,
GetTrashModuleCountUrl, GetTrashModuleCountUrl,
GetTrashModuleTreeUrl, 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 }); 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) { export function updateScenarioPro(id: string | number, priority: CaseLevel | undefined) {
return MSR.get({ url: `${UpdateScenarioPriorityUrl}/${id}/${priority}` }); return MSR.get({ url: `${UpdateScenarioPriorityUrl}/${id}/${priority}` });
} }
// 获取跨项目信息
export function getStepProjectInfo(id: string | number) {
return MSR.get({ url: GetStepProjectInfoUrl, params: id });
}

View File

@ -19,6 +19,7 @@ export const GetSystemRequestUrl = '/api/scenario/get/system-request'; // 获取
export const FollowScenarioUrl = '/api/scenario/follow'; // 关注/取消关注接口场景 export const FollowScenarioUrl = '/api/scenario/follow'; // 关注/取消关注接口场景
export const ScenarioScheduleConfigUrl = '/api/scenario/schedule-config'; // 场景定时任务 export const ScenarioScheduleConfigUrl = '/api/scenario/schedule-config'; // 场景定时任务
export const ScenarioScheduleConfigDeleteUrl = '/api/scenario/schedule-config-delete/'; // 场景定时任务 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 BatchRecycleScenarioUrl = '/api/scenario/batch-operation/delete-gc'; // 批量删除接口场景
export const BatchMoveScenarioUrl = '/api/scenario/batch-operation/move'; // 批量移动接口场景 export const BatchMoveScenarioUrl = '/api/scenario/batch-operation/move'; // 批量移动接口场景
export const BatchCopyScenarioUrl = '/api/scenario/batch-operation/copy'; // 批量复制接口场景 export const BatchCopyScenarioUrl = '/api/scenario/batch-operation/copy'; // 批量复制接口场景

View File

@ -739,9 +739,12 @@
.arco-switch-type-circle { .arco-switch-type-circle {
background-color: var(--color-text-brand) !important; 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; 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-type-line.arco-switch-small {
.arco-switch-handle { .arco-switch-handle {
width: 14px; width: 14px;

View File

@ -4,7 +4,7 @@ import localforage from 'localforage';
import { MsTableColumn, MsTableColumnData } from '@/components/pure/ms-table/type'; import { MsTableColumn, MsTableColumnData } from '@/components/pure/ms-table/type';
import { useAppStore } from '@/store'; 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 { isArraysEqualWithOrder } from '@/utils/equal';
import { SpecialColumnEnum } from '@/enums/tableEnum'; import { SpecialColumnEnum } from '@/enums/tableEnum';
@ -15,19 +15,6 @@ export default function useTableStore() {
operationBaseIndex: 100, 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 () => { const getPageSizeMap = async () => {
try { try {
const pageSizeMap = await localforage.getItem<PageSizeMap>('pageSizeMap'); const pageSizeMap = await localforage.getItem<PageSizeMap>('pageSizeMap');
@ -77,32 +64,30 @@ export default function useTableStore() {
showSubdirectory?: boolean showSubdirectory?: boolean
) { ) {
try { try {
const selectorColumnMap = await getSelectorColumnMap(); const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(tableKey);
if (!selectorColumnMap[tableKey]) { if (!tableColumnsMap) {
// 如果没有在indexDB里初始化 // 如果没有在indexDB里初始化
column = columnsTransform(column); column = columnsTransform(column);
selectorColumnMap[tableKey] = { localforage.setItem(tableKey, {
mode, mode,
showSubdirectory, showSubdirectory,
column, column,
columnBackup: JSON.parse(JSON.stringify(column)), columnBackup: JSON.parse(JSON.stringify(column)),
}; });
await localforage.setItem('selectorColumnMap', selectorColumnMap);
} else { } else {
// 初始化过了,但是可能有新变动,如列的顺序,列的显示隐藏,列的拖拽 // 初始化过了,但是可能有新变动,如列的顺序,列的显示隐藏,列的拖拽
column = columnsTransform(column); column = columnsTransform(column);
const { columnBackup: oldColumn } = selectorColumnMap[tableKey]; const { columnBackup: oldColumn } = tableColumnsMap;
// 比较页面上定义的 column 和 浏览器备份的column 是否相同 // 比较页面上定义的 column 和 浏览器备份的column 是否相同
const isEqual = isArraysEqualWithOrder<MsTableColumnData>(oldColumn, column); const isEqual = isArraysEqualWithOrder(oldColumn, column);
if (!isEqual) { if (!isEqual) {
// 如果不相等说明有变动将新的column存入indexDB // 如果不相等说明有变动将新的column存入indexDB
selectorColumnMap[tableKey] = { localforage.setItem(tableKey, {
mode, mode,
showSubdirectory, showSubdirectory,
column, column,
columnBackup: JSON.parse(JSON.stringify(column)), columnBackup: JSON.parse(JSON.stringify(column)),
}; });
await localforage.setItem('selectorColumnMap', selectorColumnMap);
} }
} }
} catch (e) { } catch (e) {
@ -112,13 +97,10 @@ export default function useTableStore() {
} }
async function setMode(key: string, mode: TableOpenDetailMode) { async function setMode(key: string, mode: TableOpenDetailMode) {
try { try {
const selectorColumnMap = await getSelectorColumnMap(); const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
if (selectorColumnMap[key]) { if (tableColumnsMap) {
const item = selectorColumnMap[key]; tableColumnsMap.mode = mode;
if (item) { await localforage.setItem(key, tableColumnsMap);
item.mode = mode;
}
await localforage.setItem('selectorColumnMap', selectorColumnMap);
} }
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -128,13 +110,10 @@ export default function useTableStore() {
async function setSubdirectory(key: string, val: boolean) { async function setSubdirectory(key: string, val: boolean) {
try { try {
const selectorColumnMap = await getSelectorColumnMap(); const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
if (selectorColumnMap[key]) { if (tableColumnsMap) {
const item = selectorColumnMap[key]; tableColumnsMap.showSubdirectory = val;
if (item) { await localforage.setItem(key, tableColumnsMap);
item.showSubdirectory = val;
}
await localforage.setItem('selectorColumnMap', selectorColumnMap);
} }
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
@ -155,23 +134,22 @@ export default function useTableStore() {
item.sortIndex = state.baseSortIndex + idx; item.sortIndex = state.baseSortIndex + idx;
} }
}); });
const selectorColumnMap = await getSelectorColumnMap(); const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
if (!selectorColumnMap) { if (!tableColumnsMap) {
return; return;
} }
if (isSimple) { if (isSimple) {
const oldColumns = selectorColumnMap[key].column; const oldColumns = tableColumnsMap.column;
const operationColumn = oldColumns.find((i) => i.dataIndex === SpecialColumnEnum.OPERATION); const operationColumn = oldColumns.find((i) => i.dataIndex === SpecialColumnEnum.OPERATION);
if (operationColumn) columns.push(operationColumn); if (operationColumn) columns.push(operationColumn);
} }
selectorColumnMap[key] = { await localforage.setItem(key, {
mode, mode,
showSubdirectory, showSubdirectory,
column: JSON.parse(JSON.stringify(columns)), column: JSON.parse(JSON.stringify(columns)),
columnBackup: selectorColumnMap[key].columnBackup, columnBackup: tableColumnsMap.columnBackup,
}; });
await localforage.setItem('selectorColumnMap', selectorColumnMap);
} catch (e) { } catch (e) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error('tableStore.setColumns', e); console.error('tableStore.setColumns', e);
@ -184,25 +162,25 @@ export default function useTableStore() {
} }
async function getMode(key: string) { async function getMode(key: string) {
const selectorColumnMap = await getSelectorColumnMap(); const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
if (selectorColumnMap[key]) { if (tableColumnsMap) {
return selectorColumnMap[key].mode; return tableColumnsMap.mode;
} }
return 'drawer'; return 'drawer';
} }
async function getSubShow(key: string) { async function getSubShow(key: string) {
const selectorColumnMap = await getSelectorColumnMap(); const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
if (selectorColumnMap[key]) { if (tableColumnsMap) {
return selectorColumnMap[key].showSubdirectory; return tableColumnsMap.showSubdirectory;
} }
return true as boolean; return true as boolean;
} }
async function getColumns(key: string, isSimple?: boolean) { async function getColumns(key: string, isSimple?: boolean) {
const selectorColumnMap = await getSelectorColumnMap(); const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
if (selectorColumnMap[key]) { if (tableColumnsMap) {
const tmpArr = selectorColumnMap[key].column; const tmpArr = tableColumnsMap.column;
const { nonSortableColumns, couldSortableColumns } = tmpArr.reduce( const { nonSortableColumns, couldSortableColumns } = tmpArr.reduce(
(result: { nonSortableColumns: MsTableColumnData[]; couldSortableColumns: MsTableColumnData[] }, item) => { (result: { nonSortableColumns: MsTableColumnData[]; couldSortableColumns: MsTableColumnData[] }, item) => {
if (isSimple && item.dataIndex === SpecialColumnEnum.OPERATION) { if (isSimple && item.dataIndex === SpecialColumnEnum.OPERATION) {
@ -222,9 +200,9 @@ export default function useTableStore() {
return { nonSort: [], couldSort: [] }; return { nonSort: [], couldSort: [] };
} }
async function getShowInTableColumns(key: string) { async function getShowInTableColumns(key: string) {
const selectorColumnMap = await getSelectorColumnMap(); const tableColumnsMap = await localforage.getItem<MsTableSelectorItem>(key);
if (selectorColumnMap[key]) { if (tableColumnsMap) {
const tmpArr: MsTableColumn = selectorColumnMap[key].column; const tmpArr: MsTableColumn = tableColumnsMap.column;
return orderBy( return orderBy(
filter(tmpArr, (i) => i.showInTable), filter(tmpArr, (i) => i.showInTable),
['sortIndex'], ['sortIndex'],

View File

@ -360,6 +360,7 @@ export interface ScenarioStepItem {
executeStatus?: ScenarioExecuteStatus; executeStatus?: ScenarioExecuteStatus;
isExecuting?: boolean; // 是否正在执行 isExecuting?: boolean; // 是否正在执行
reportId?: string | number; // 步骤单个调试时的报告id reportId?: string | number; // 步骤单个调试时的报告id
uniqueId: string | number; // 获取报告时的步骤唯一标识(用来区分重复引用的步骤)
isQuoteScenarioStep?: boolean; // 是否是引用场景下的步骤(不分是不是完全引用,只要是引用类型就是),不可修改引用 api 的参数值 isQuoteScenarioStep?: boolean; // 是否是引用场景下的步骤(不分是不是完全引用,只要是引用类型就是),不可修改引用 api 的参数值
isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用 isRefScenarioStep?: boolean; // 是否是完全引用的场景下的步骤,是的话不允许启用禁用
} }
@ -401,7 +402,7 @@ export interface Scenario {
executeTime?: string | number; // 执行时间 executeTime?: string | number; // 执行时间
executeSuccessCount: number; // 执行成功数量 executeSuccessCount: number; // 执行成功数量
executeFailCount: number; // 执行失败数量 executeFailCount: number; // 执行失败数量
reportId?: string | number; // 场景报告 id reportId: string | number; // 场景报告 id
stepResponses: Record<string | number, Array<RequestResult>>; // 步骤响应集合key 为步骤 idvalue 为步骤响应内容 stepResponses: Record<string | number, Array<RequestResult>>; // 步骤响应集合key 为步骤 idvalue 为步骤响应内容
isExecute?: boolean; // 是否从列表执行进去场景详情 isExecute?: boolean; // 是否从列表执行进去场景详情
isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤 isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤

View File

@ -198,8 +198,8 @@ export interface TreeNode<T> {
*/ */
export function traverseTree<T>( export function traverseTree<T>(
tree: TreeNode<T> | TreeNode<T>[] | T | T[], tree: TreeNode<T> | TreeNode<T>[] | T | T[],
customNodeFn: (node: TreeNode<T>) => void,
continueCondition?: (node: TreeNode<T>) => boolean, continueCondition?: (node: TreeNode<T>) => boolean,
customNodeFn: (node: TreeNode<T>) => TreeNode<T> | null = (node) => node,
customChildrenKey = 'children' customChildrenKey = 'children'
) { ) {
if (!Array.isArray(tree)) { if (!Array.isArray(tree)) {
@ -215,7 +215,7 @@ export function traverseTree<T>(
// 如果有继续递归的条件,则判断是否继续递归 // 如果有继续递归的条件,则判断是否继续递归
break; break;
} }
traverseTree(node[customChildrenKey], continueCondition, customNodeFn, customChildrenKey); traverseTree(node[customChildrenKey], customNodeFn, continueCondition, customChildrenKey);
} }
} }
} }
@ -488,6 +488,7 @@ export function handleTreeDragDrop<T>(
return false; return false;
} }
const index = parentChildren.findIndex((node: TreeNode<T>) => node[customKey] === dragNode[customKey]); const index = parentChildren.findIndex((node: TreeNode<T>) => node[customKey] === dragNode[customKey]);
console.log('index', parentChildren, dragNode, index);
if (index !== -1) { if (index !== -1) {
parentChildren.splice(index, 1); parentChildren.splice(index, 1);

View File

@ -346,6 +346,7 @@
offspringIds: string[]; offspringIds: string[];
protocol: string; // protocol: string; //
readOnly?: boolean; // readOnly?: boolean; //
refreshTimeStamp?: number;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'openApiTab', record: ApiDefinitionDetail, isExecute?: boolean): void; (e: 'openApiTab', record: ApiDefinitionDetail, isExecute?: boolean): void;
@ -358,6 +359,7 @@
const appStore = useAppStore(); const appStore = useAppStore();
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
const tableStore = useTableStore();
const folderTreePathMap = inject('folderTreePathMap'); const folderTreePathMap = inject('folderTreePathMap');
const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree'); const refreshModuleTree: (() => Promise<any>) | undefined = inject('refreshModuleTree');
@ -459,6 +461,14 @@
width: hasOperationPermission.value ? 220 : 50, 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( const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(
getDefinitionPage, getDefinitionPage,
{ {
@ -523,7 +533,6 @@
const statusFilterVisible = ref(false); const statusFilterVisible = ref(false);
const statusFilters = ref<string[]>([]); const statusFilters = ref<string[]>([]);
const tableStore = useTableStore();
async function getModuleIds() { async function getModuleIds() {
let moduleIds: string[] = []; let moduleIds: string[] = [];
if (props.activeModule !== 'all') { if (props.activeModule !== 'all') {
@ -552,6 +561,15 @@
loadList(); loadList();
} }
watch(
() => props.refreshTimeStamp,
(val) => {
if (val) {
loadApiList();
}
}
);
watch( watch(
() => props.activeModule, () => props.activeModule,
() => { () => {
@ -915,18 +933,6 @@
console.log(error); 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> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,11 +1,11 @@
<template> <template>
<div class="flex flex-1 flex-col overflow-hidden"> <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 <apiTable
ref="apiTableRef"
:active-module="props.activeModule" :active-module="props.activeModule"
:offspring-ids="props.offspringIds" :offspring-ids="props.offspringIds"
:protocol="props.protocol" :protocol="props.protocol"
:refresh-time-stamp="refreshTableTimeStamp"
@open-api-tab="(record, isExecute) => openApiTab(record, false, isExecute)" @open-api-tab="(record, isExecute) => openApiTab(record, false, isExecute)"
@open-copy-api-tab="openApiTab($event, true)" @open-copy-api-tab="openApiTab($event, true)"
@add-api-tab="addApiTab" @add-api-tab="addApiTab"
@ -288,14 +288,14 @@
activeApiTab.value = apiTabs.value[apiTabs.value.length - 1]; activeApiTab.value = apiTabs.value[apiTabs.value.length - 1];
} }
const apiTableRef = ref<InstanceType<typeof apiTable>>();
const caseTableRef = ref<InstanceType<typeof caseTable>>(); const caseTableRef = ref<InstanceType<typeof caseTable>>();
const refreshTableTimeStamp = ref(0);
watch( watch(
() => activeApiTab.value.id, () => activeApiTab.value.id,
(id) => { (id) => {
if (id === 'all') { if (id === 'all') {
apiTableRef.value?.loadApiList(); refreshTableTimeStamp.value = Date.now();
} }
if (activeApiTab.value.definitionActiveKey === 'case') { if (activeApiTab.value.definitionActiveKey === 'case') {
caseTableRef.value?.loadCaseList(); caseTableRef.value?.loadCaseList();
@ -366,7 +366,7 @@
} }
function refreshTable() { function refreshTable() {
apiTableRef.value?.loadApiList(); refreshTableTimeStamp.value = Date.now();
} }
function changeDefinitionActiveKey(val: string | number) { function changeDefinitionActiveKey(val: string | number) {

View File

@ -558,7 +558,6 @@
}, },
]; ];
const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getCasePage, { const { propsRes, propsEvent, loadList, setLoadListParams, resetSelector } = useTable(getCasePage, {
columns,
scroll: { x: '100%' }, scroll: { x: '100%' },
tableKey: TableKeyEnum.API_TEST_MANAGEMENT_CASE, tableKey: TableKeyEnum.API_TEST_MANAGEMENT_CASE,
showSetting: true, showSetting: true,

View File

@ -32,6 +32,7 @@
<a-select <a-select
v-model:model-value="requestVModel.customizeRequestEnvEnable" v-model:model-value="requestVModel.customizeRequestEnvEnable"
class="w-[150px]" class="w-[150px]"
:disabled="props.step?.isQuoteScenarioStep"
popup-container=".customApiDrawer-title-right" popup-container=".customApiDrawer-title-right"
@change="handleUseEnvChange" @change="handleUseEnvChange"
> >
@ -61,7 +62,7 @@
v-model:model-value="requestVModel.protocol" v-model:model-value="requestVModel.protocol"
:options="protocolOptions" :options="protocolOptions"
:loading="protocolLoading" :loading="protocolLoading"
:disabled="_stepType.isQuoteApi" :disabled="_stepType.isQuoteApi || props.step?.isQuoteScenarioStep"
class="w-[90px]" class="w-[90px]"
@change="(val) => handleActiveDebugProtocolChange(val as string)" @change="(val) => handleActiveDebugProtocolChange(val as string)"
/> />
@ -81,7 +82,7 @@
<apiMethodSelect <apiMethodSelect
v-model:model-value="requestVModel.method" v-model:model-value="requestVModel.method"
class="w-[140px]" class="w-[140px]"
:disabled="_stepType.isQuoteApi" :disabled="_stepType.isQuoteApi || props.step?.isQuoteScenarioStep"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
<a-input <a-input
@ -91,7 +92,7 @@
allow-clear allow-clear
class="hover:z-10" class="hover:z-10"
:style="isUrlError ? 'border: 1px solid rgb(var(--danger-6);z-index: 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)" @input="() => (isUrlError = false)"
@change="handleUrlChange" @change="handleUrlChange"
> >
@ -130,7 +131,7 @@
v-model:model-value="requestVModel.name" v-model:model-value="requestVModel.name"
:max-length="255" :max-length="255"
:placeholder="t('apiTestManagement.apiNamePlaceholder')" :placeholder="t('apiTestManagement.apiNamePlaceholder')"
:disabled="!isEditableApi || isQuoteScenarioStep" :disabled="!isEditableApi"
allow-clear allow-clear
class="mt-[8px]" class="mt-[8px]"
/> />
@ -184,7 +185,7 @@
<httpHeader <httpHeader
v-if="requestVModel.activeTab === RequestComposition.HEADER" v-if="requestVModel.activeTab === RequestComposition.HEADER"
v-model:params="requestVModel.headers" v-model:params="requestVModel.headers"
:disabled-param-value="isQuoteScenarioStep" :disabled-param-value="!isEditableApi"
:disabled-except-param="!isEditableApi" :disabled-except-param="!isEditableApi"
:layout="activeLayout" :layout="activeLayout"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@ -194,7 +195,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.BODY" v-else-if="requestVModel.activeTab === RequestComposition.BODY"
v-model:params="requestVModel.body" v-model:params="requestVModel.body"
:layout="activeLayout" :layout="activeLayout"
:disabled-param-value="isQuoteScenarioStep" :disabled-param-value="!isEditableApi"
:disabled-except-param="!isEditableApi" :disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
:upload-temp-file-api="uploadTempFile" :upload-temp-file-api="uploadTempFile"
@ -207,7 +208,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.QUERY" v-else-if="requestVModel.activeTab === RequestComposition.QUERY"
v-model:params="requestVModel.query" v-model:params="requestVModel.query"
:layout="activeLayout" :layout="activeLayout"
:disabled-param-value="isQuoteScenarioStep" :disabled-param-value="!isEditableApi"
:disabled-except-param="!isEditableApi" :disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
@ -216,7 +217,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.REST" v-else-if="requestVModel.activeTab === RequestComposition.REST"
v-model:params="requestVModel.rest" v-model:params="requestVModel.rest"
:layout="activeLayout" :layout="activeLayout"
:disabled-param-value="isQuoteScenarioStep" :disabled-param-value="!isEditableApi"
:disabled-except-param="!isEditableApi" :disabled-except-param="!isEditableApi"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
@ -225,7 +226,7 @@
v-else-if="requestVModel.activeTab === RequestComposition.PRECONDITION" v-else-if="requestVModel.activeTab === RequestComposition.PRECONDITION"
v-model:config="requestVModel.children[0].preProcessorConfig" v-model:config="requestVModel.children[0].preProcessorConfig"
is-definition is-definition
:disabled="!isEditableApi || isQuoteScenarioStep" :disabled="!isEditableApi"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
<postcondition <postcondition
@ -233,7 +234,7 @@
v-model:config="requestVModel.children[0].postProcessorConfig" v-model:config="requestVModel.children[0].postProcessorConfig"
:response="responseResultBody" :response="responseResultBody"
:layout="activeLayout" :layout="activeLayout"
:disabled="!isEditableApi || isQuoteScenarioStep" :disabled="!isEditableApi"
:second-box-height="secondBoxHeight" :second-box-height="secondBoxHeight"
is-definition is-definition
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
@ -243,19 +244,19 @@
v-model:params="requestVModel.children[0].assertionConfig.assertions" v-model:params="requestVModel.children[0].assertionConfig.assertions"
:response="responseResultBody" :response="responseResultBody"
is-definition is-definition
:disabled="!isEditableApi || isQuoteScenarioStep" :disabled="!isEditableApi"
:assertion-config="requestVModel.children[0].assertionConfig" :assertion-config="requestVModel.children[0].assertionConfig"
/> />
<auth <auth
v-else-if="requestVModel.activeTab === RequestComposition.AUTH" v-else-if="requestVModel.activeTab === RequestComposition.AUTH"
v-model:params="requestVModel.authConfig" v-model:params="requestVModel.authConfig"
:disabled="!isEditableApi || isQuoteScenarioStep" :disabled="!isEditableApi"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
<setting <setting
v-else-if="requestVModel.activeTab === RequestComposition.SETTING" v-else-if="requestVModel.activeTab === RequestComposition.SETTING"
v-model:params="requestVModel.otherConfig" v-model:params="requestVModel.otherConfig"
:disabled="!isEditableApi || isQuoteScenarioStep" :disabled="!isEditableApi"
@change="handleActiveDebugChange" @change="handleActiveDebugChange"
/> />
</div> </div>
@ -506,11 +507,11 @@
); );
const currentLoop = ref(1); const currentLoop = ref(1);
const currentResponse = computed(() => { const currentResponse = computed(() => {
if (props.step?.id) { if (props.step?.uniqueId) {
return props.stepResponses?.[props.step?.id]?.[currentLoop.value - 1]; 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 // body
const responseResultBody = computed(() => { const responseResultBody = computed(() => {
return currentResponse.value?.responseResult.body; return currentResponse.value?.responseResult.body;
@ -531,10 +532,11 @@
// api props.request // api props.request
const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request === undefined); const isCopyApiNeedInit = computed(() => _stepType.value.isCopyApi && props.request === undefined);
const isEditableApi = computed( 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 isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const isQuoteScenarioStep = computed(() => props.step?.isQuoteScenarioStep);
const isInitPluginForm = ref(false); const isInitPluginForm = ref(false);
@ -1087,7 +1089,6 @@
} }
nextTick(() => { nextTick(() => {
// loading // loading
requestVModel.value.activeTab = contentTabList.value[0].value;
loading.value = false; loading.value = false;
}); });
} catch (error) { } catch (error) {
@ -1130,6 +1131,7 @@
stepId: getGenerateId(), stepId: getGenerateId(),
}); });
} }
requestVModel.value.activeTab = contentTabList.value[0].value;
} }
}, },
{ {

View File

@ -404,11 +404,13 @@
); );
const currentLoop = ref(1); const currentLoop = ref(1);
const currentResponse = computed(() => { const currentResponse = computed(() => {
if (activeStep.value?.id) { if (activeStep.value?.uniqueId) {
return props.stepResponses?.[activeStep.value?.id]?.[currentLoop.value - 1]; 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 // body
const responseResultBody = computed(() => { const responseResultBody = computed(() => {
return currentResponse.value?.responseResult.body; return currentResponse.value?.responseResult.body;

View File

@ -46,6 +46,7 @@
:selected-apis="selectedApis" :selected-apis="selectedApis"
:selected-cases="selectedCases" :selected-cases="selectedCases"
:selected-scenarios="selectedScenarios" :selected-scenarios="selectedScenarios"
:scenario-id="props.scenarioId"
@select="handleTableSelect" @select="handleTableSelect"
/> />
</div> </div>
@ -112,6 +113,9 @@
scenario: MsTableDataItem<ApiScenarioTableItem>[]; scenario: MsTableDataItem<ApiScenarioTableItem>[];
} }
const props = defineProps<{
scenarioId?: string | number;
}>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'copy', data: ImportData): void; (e: 'copy', data: ImportData): void;
(e: 'quote', data: ImportData): void; (e: 'quote', data: ImportData): void;
@ -235,19 +239,12 @@
fullScenarioArr.push(...res); fullScenarioArr.push(...res);
}); });
if (refType === ScenarioStepRefType.COPY) { if (refType === ScenarioStepRefType.COPY) {
fullScenarioArr = fullScenarioArr.map((e) => { fullScenarioArr = mapTree<MsTableDataItem<ApiScenarioTableItem>>(fullScenarioArr, (node) => {
return { return {
...e, ...node,
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => { copyFromStepId: node.id,
return { originProjectId: node.projectId,
...node, id: getGenerateId(),
copyFromStepId: node.id,
originProjectId: node.projectId,
id: getGenerateId(),
};
}),
copyFromStepId: e.resourceId,
originProjectId: e.projectId,
}; };
}); });
emit( emit(
@ -266,13 +263,12 @@
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => { children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => {
return { return {
...node, ...node,
copyFromStepId: node.id,
originProjectId: node.projectId, originProjectId: node.projectId,
id: getGenerateId(),
isQuoteScenarioStep: true, isQuoteScenarioStep: true,
isRefScenarioStep: true, // isRefScenarioStep: true, //
}; };
}), }),
id: getGenerateId(),
originProjectId: e.projectId, originProjectId: e.projectId,
}; };
}); });

View File

@ -113,6 +113,7 @@
selectedApis: MsTableDataItem<ApiDefinitionDetail>[]; // selectedApis: MsTableDataItem<ApiDefinitionDetail>[]; //
selectedCases: MsTableDataItem<ApiCaseDetail>[]; // selectedCases: MsTableDataItem<ApiCaseDetail>[]; //
selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; // selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; //
scenarioId?: string | number;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void; (e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void;
@ -335,6 +336,7 @@
status: statusFilters.value, status: statusFilters.value,
method: methodFilters.value, method: methodFilters.value,
}, },
excludeIds: [props.scenarioId || ''],
}); });
currentTable.value.loadList(); currentTable.value.loadList();
}); });

View File

@ -1,6 +1,6 @@
<template> <template>
<a-popover <a-popover
position="br" position="lt"
content-class="scenario-step-response-popover" content-class="scenario-step-response-popover"
@popup-visible-change="emit('visibleChange', $event, props.step)" @popup-visible-change="emit('visibleChange', $event, props.step)"
> >
@ -8,8 +8,18 @@
<template #content> <template #content>
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" /> <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 <responseResult
v-else
:active-tab="ResponseComposition.BODY" :active-tab="ResponseComposition.BODY"
:request-result="currentResponse" :request-result="currentResponse"
:console="currentResponse?.console" :console="currentResponse?.console"
@ -40,7 +50,7 @@
import { RequestResult } from '@/models/apiTest/common'; import { RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario'; import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ResponseComposition, ScenarioExecuteStatus } from '@/enums/apiEnum'; import { ResponseComposition, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
const responseResult = defineAsyncComponent( const responseResult = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/index.vue') () => import('@/views/api-test/components/requestComposition/response/index.vue')
@ -55,12 +65,12 @@
const { t } = useI18n(); const { t } = useI18n();
const currentLoop = ref(1); const currentLoop = ref(1);
const currentResponse = computed(() => props.stepResponses?.[props.step.id]?.[currentLoop.value - 1]); const currentResponse = computed(() => props.stepResponses?.[props.step.uniqueId]?.[currentLoop.value - 1]);
const loopTotal = computed(() => props.stepResponses?.[props.step.id]?.length || 0); const loopTotal = computed(() => props.stepResponses?.[props.step.uniqueId]?.length || 0);
const finalExecuteStatus = computed(() => { 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.FAILED
: ScenarioExecuteStatus.SUCCESS; : ScenarioExecuteStatus.SUCCESS;
} }
@ -74,6 +84,13 @@
height: 500px; height: 500px;
.arco-popover-content { .arco-popover-content {
@apply h-full; @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 {
.response-head { .response-head {
background-color: var(--color-text-n9); background-color: var(--color-text-n9);

View File

@ -88,11 +88,11 @@
const visible = defineModel<boolean>('visible', { required: true }); const visible = defineModel<boolean>('visible', { required: true });
const currentLoop = ref(1); const currentLoop = ref(1);
const currentResponse = computed(() => { const currentResponse = computed(() => {
if (props.step?.id) { if (props.step?.uniqueId) {
return props.stepResponses?.[props.step?.id]?.[currentLoop.value - 1]; 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( watch(
() => visible.value, () => visible.value,

View File

@ -120,6 +120,7 @@ export const defaultScenario: Scenario = {
executeFailCount: 0, executeFailCount: 0,
uploadFileIds: [], uploadFileIds: [],
linkFileIds: [], linkFileIds: [],
reportId: '',
// 前端渲染字段 // 前端渲染字段
label: '', label: '',
closable: true, closable: true,

View File

@ -132,10 +132,12 @@ export default function useCreateActions() {
return { return {
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id, id,
uniqueId: getGenerateId(), // 生成唯一 ID避免重复引用的步骤无法读取正确的执行结果
config: { config: {
...defaultStepItemCommon.config, ...defaultStepItemCommon.config,
...config, ...config,
}, },
draggable: stepType !== ScenarioStepType.API_SCENARIO ? !item.config?.isQuoteScenarioStep : true, // 引用场景下的任何子步骤不可拖拽,除了场景本身
isQuoteScenarioStep: item.config?.isQuoteScenarioStep || false, isQuoteScenarioStep: item.config?.isQuoteScenarioStep || false,
isRefScenarioStep: item.config?.isRefScenarioStep || false, isRefScenarioStep: item.config?.isRefScenarioStep || false,
children: item.children || [], children: item.children || [],

View File

@ -1,101 +1,102 @@
<template> <template>
<div class="flex h-full flex-col gap-[8px]"> <div class="flex h-full flex-col gap-[8px]">
<div class="action-line"> <a-spin class="h-full w-full" :loading="loading">
<div class="action-group"> <div class="action-line">
<a-checkbox <div class="action-group">
v-show="scenario.steps.length > 0" <a-checkbox
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
v-show="scenario.steps.length > 0" v-show="scenario.steps.length > 0"
type="outline" v-model:model-value="checkedAll"
class="expand-step-btn arco-btn-outline--secondary" :indeterminate="indeterminate"
size="mini" :disabled="scenarioExecuteLoading"
@click="expandAllStep" @change="handleChangeAll"
>
<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-button <div class="flex items-center gap-[4px]">
v-if="!props.isNew" {{ t('apiScenario.sum') }}
type="outline" <div class="text-[rgb(var(--primary-5))]">{{ totalStepCount }}</div>
class="arco-btn-outline--secondary !mr-0 !p-[8px]" {{ t('apiScenario.steps') }}
@click="refreshStepInfo" </div>
> </div>
<template #icon> <div class="action-group">
<icon-refresh class="text-[var(--color-text-4)]" /> <a-tooltip :content="isExpandAll ? t('apiScenario.collapseAllStep') : t('apiScenario.expandAllStep')">
</template> <a-button
</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>
</div> <div class="h-[calc(100%-30px)]">
<div class="h-[calc(100%-30px)]"> <stepTree
<stepTree ref="stepTreeRef"
ref="stepTreeRef" v-model:selected-keys="selectedKeys"
v-model:steps="scenario.steps" v-model:steps="scenario.steps"
v-model:checked-keys="checkedKeys" v-model:checked-keys="checkedKeys"
v-model:stepKeyword="keyword" v-model:stepKeyword="keyword"
v-model:scenario="scenario" v-model:scenario="scenario"
:expand-all="isExpandAll" :expand-all="isExpandAll"
:step-details="scenario.stepDetails" :step-details="scenario.stepDetails"
@update-resource="handleUpdateResource" @step-add="handleAddStepDone"
/> @update-resource="handleUpdateResource"
</div> />
</div>
</a-spin>
</div> </div>
<a-modal <a-modal
v-model:visible="batchToggleVisible" v-model:visible="batchToggleVisible"
@ -120,18 +121,20 @@
// import dayjs from 'dayjs'; // import dayjs from 'dayjs';
import { Message } from '@arco-design/web-vue'; import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import stepTree from './stepTree.vue'; import stepTree from './stepTree.vue';
import { getScenarioDetail } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage'; 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 { countNodes } from '@/utils/tree';
import { ApiScenarioDebugRequest, Scenario } from '@/models/apiTest/scenario'; import { ApiScenarioDebugRequest, Scenario, ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum'; import { ScenarioExecuteStatus, ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import { ApiTestRouteEnum } from '@/enums/routeEnum'; import { ApiTestRouteEnum } from '@/enums/routeEnum';
const props = defineProps<{ const props = defineProps<{
@ -148,11 +151,13 @@
required: true, required: true,
}); });
const scenarioExecuteLoading = inject<Ref<boolean>>('scenarioExecuteLoading'); const scenarioExecuteLoading = inject<Ref<boolean>>('scenarioExecuteLoading');
const loading = ref(false);
const checkedAll = ref(false); // const checkedAll = ref(false); //
const indeterminate = ref(false); // const indeterminate = ref(false); //
const isExpandAll = ref(false); // const isExpandAll = ref(false); //
const checkedKeys = ref<(string | number)[]>([]); // key const checkedKeys = ref<(string | number)[]>([]); // key
const selectedKeys = ref<(string | number)[]>([]); //
const stepTreeRef = ref<InstanceType<typeof stepTree>>(); const stepTreeRef = ref<InstanceType<typeof stepTree>>();
const keyword = ref(''); const keyword = ref('');
@ -184,10 +189,17 @@
} }
); );
function handleAddStepDone() {
checkedKeys.value = [];
checkedAll.value = false;
indeterminate.value = false;
}
watch( watch(
() => scenario.value.steps.length, () => scenario.value.id,
() => { () => {
checkedKeys.value = []; checkedKeys.value = [];
selectedKeys.value = [];
checkedAll.value = false; checkedAll.value = false;
indeterminate.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() { function batchDebug() {
@ -272,14 +349,8 @@
// id便waitingDebugStepDetails // id便waitingDebugStepDetails
checkedKeysSet.delete(node.id); checkedKeysSet.delete(node.id);
node.executeStatus = undefined; node.executeStatus = undefined;
} else if (
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(node.stepType)
) {
//
node.executeStatus = ScenarioExecuteStatus.EXECUTING;
} else { } else {
// node.executeStatus = ScenarioExecuteStatus.EXECUTING;
node.executeStatus = undefined;
} }
return !!node.enable; return !!node.enable;
} }
@ -329,8 +400,9 @@
.action-line { .action-line {
@apply flex items-center; @apply flex items-center;
gap: 16px; margin-bottom: 8px;
height: 32px; height: 32px;
gap: 16px;
.action-group { .action-group {
@apply flex items-center; @apply flex items-center;

View File

@ -7,6 +7,7 @@
class="w-[100px] px-[8px]" class="w-[100px] px-[8px]"
:max-length="255" :max-length="255"
:placeholder="t('apiScenario.variable', { suffix: '${var}' })" :placeholder="t('apiScenario.variable', { suffix: '${var}' })"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -15,6 +16,7 @@
v-model:model-value="innerData.condition" v-model:model-value="innerData.condition"
size="mini" size="mini"
class="w-[90px] px-[8px]" class="w-[90px] px-[8px]"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
<a-option v-for="opt of conditionOptions" :key="opt.value" :value="opt.value"> <a-option v-for="opt of conditionOptions" :key="opt.value" :value="opt.value">
@ -28,6 +30,7 @@
size="mini" size="mini"
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.value')" :placeholder="t('apiScenario.value')"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -45,6 +48,7 @@
const props = defineProps<{ const props = defineProps<{
data: ConditionStepDetail; data: ConditionStepDetail;
stepId: string | number; stepId: string | number;
disabled: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', innerData: ConditionStepDetail): void; (e: 'change', innerData: ConditionStepDetail): void;

View File

@ -6,6 +6,7 @@
:options="loopOptions" :options="loopOptions"
size="mini" size="mini"
class="w-[85px] px-[8px]" class="w-[85px] px-[8px]"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
/> />
<a-tooltip <a-tooltip
@ -22,6 +23,7 @@
hide-button hide-button
:precision="0" :precision="0"
model-event="input" model-event="input"
:disabled="props.disabled"
@blur="handleInputChange" @blur="handleInputChange"
> >
<template #prefix> <template #prefix>
@ -38,6 +40,7 @@
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
:max-length="255" :max-length="255"
:placeholder="t('apiScenario.variable')" :placeholder="t('apiScenario.variable')"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -50,6 +53,7 @@
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.valuePrefix')" :placeholder="t('apiScenario.valuePrefix')"
:max-length="255" :max-length="255"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -61,6 +65,7 @@
:options="whileOptions" :options="whileOptions"
size="mini" size="mini"
class="w-[75px] px-[8px]" class="w-[75px] px-[8px]"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
/> />
<template v-if="innerData.whileController.conditionType === WhileConditionType.CONDITION"> <template v-if="innerData.whileController.conditionType === WhileConditionType.CONDITION">
@ -74,6 +79,7 @@
class="w-[100px] px-[8px]" class="w-[100px] px-[8px]"
:max-length="255" :max-length="255"
:placeholder="t('apiScenario.variable', { suffix: '${var}' })" :placeholder="t('apiScenario.variable', { suffix: '${var}' })"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -82,6 +88,7 @@
v-model:model-value="innerData.whileController.msWhileVariable.condition" v-model:model-value="innerData.whileController.msWhileVariable.condition"
size="mini" size="mini"
class="w-[90px] px-[8px]" class="w-[90px] px-[8px]"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
<a-option v-for="opt of conditionOptions" :key="opt.value" :value="opt.value"> <a-option v-for="opt of conditionOptions" :key="opt.value" :value="opt.value">
@ -98,6 +105,7 @@
size="mini" size="mini"
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
:placeholder="t('apiScenario.value')" :placeholder="t('apiScenario.value')"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -114,6 +122,7 @@
size="mini" size="mini"
class="w-[200px] px-[8px]" class="w-[200px] px-[8px]"
:placeholder="t('apiScenario.expression')" :placeholder="t('apiScenario.expression')"
:disabled="props.disabled"
@change="handleInputChange" @change="handleInputChange"
> >
</a-input> </a-input>
@ -131,6 +140,7 @@
hide-button hide-button
:precision="0" :precision="0"
model-event="input" model-event="input"
:disabled="props.disabled"
@blur="handleInputChange" @blur="handleInputChange"
> >
<template #prefix> <template #prefix>
@ -154,6 +164,7 @@
hide-button hide-button
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
model-event="input" model-event="input"
:disabled="props.disabled"
@blur="handleInputChange" @blur="handleInputChange"
> >
<template #prefix> <template #prefix>
@ -170,6 +181,7 @@
hide-button hide-button
class="w-[110px] px-[8px]" class="w-[110px] px-[8px]"
model-event="input" model-event="input"
:disabled="props.disabled"
@blur="handleInputChange" @blur="handleInputChange"
> >
<template #prefix> <template #prefix>
@ -191,6 +203,7 @@
const props = defineProps<{ const props = defineProps<{
data: LoopStepDetail; data: LoopStepDetail;
stepId: string | number; stepId: string | number;
disabled: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', innerData: LoopStepDetail): void; (e: 'change', innerData: LoopStepDetail): void;
@ -227,7 +240,6 @@
watchEffect(() => { watchEffect(() => {
innerData.value = props.data; innerData.value = props.data;
console.log('watchEffect', props.data);
}); });
// //

View File

@ -5,14 +5,19 @@
" "
class="flex items-center gap-[4px]" 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))]" /> <MsIcon type="icon-icon-draft" class="text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]" />
<template #content> <template #content>
<div class="flex flex-col gap-[16px]"> <div class="flex flex-col gap-[16px]">
<div> <div>
<div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div> <div class="mb-[2px] text-[var(--color-text-4)]">{{ t('apiScenario.belongProject') }}</div>
<div class="text-[14px] text-[var(--color-text-1)]"> <div class="text-[14px] text-[var(--color-text-1)]">
<!-- {{ props.data.belongProjectName }} --> {{ originProjectName }}
</div> </div>
</div> </div>
<div> <div>
@ -43,6 +48,7 @@
import MsIcon from '@/components/pure/ms-icon-font/index.vue'; import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import MsTag from '@/components/pure/ms-tag/ms-tag.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 { useI18n } from '@/hooks/useI18n';
import useOpenNewPage from '@/hooks/useOpenNewPage'; import useOpenNewPage from '@/hooks/useOpenNewPage';
import useAppStore from '@/store/modules/app'; import useAppStore from '@/store/modules/app';
@ -61,6 +67,15 @@
const { t } = useI18n(); const { t } = useI18n();
const { openNewPage } = useOpenNewPage(); 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() { function goDetail() {
const _stepType = getStepType(props.data); const _stepType = getStepType(props.data);
switch (true) { switch (true) {
@ -91,8 +106,8 @@
} }
</script> </script>
<style lang="less" scoped> <style lang="less">
.detail-popover { .quote-content-detail-popover {
width: 350px; width: 300px;
} }
</style> </style>

View File

@ -10,6 +10,7 @@
hide-button hide-button
:precision="0" :precision="0"
model-event="input" model-event="input"
:disabled="props.disabled"
@blur="handleInputChange" @blur="handleInputChange"
> >
<template #prefix> <template #prefix>
@ -30,6 +31,7 @@
const props = defineProps<{ const props = defineProps<{
data: WaitTimeContentProps; data: WaitTimeContentProps;
disabled: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'change', innerData: WaitTimeContentProps): void; (e: 'change', innerData: WaitTimeContentProps): void;

View File

@ -60,8 +60,8 @@
<div class="mr-[8px] flex items-center gap-[8px]"> <div class="mr-[8px] flex items-center gap-[8px]">
<!-- 步骤启用/禁用完全引用的场景下的子孙步骤不可禁用 --> <!-- 步骤启用/禁用完全引用的场景下的子孙步骤不可禁用 -->
<a-switch <a-switch
v-show="step.isRefScenarioStep !== true"
v-model:model-value="step.enable" v-model:model-value="step.enable"
:disabled="step.isRefScenarioStep"
size="small" size="small"
@click.stop="handleStepToggleEnable(step)" @click.stop="handleStepToggleEnable(step)"
></a-switch> ></a-switch>
@ -90,6 +90,7 @@
:is="getStepContent(step)" :is="getStepContent(step)"
:data="checkStepIsApi(step) || step.stepType === ScenarioStepType.API_SCENARIO ? step : step.config" :data="checkStepIsApi(step) || step.stepType === ScenarioStepType.API_SCENARIO ? step : step.config"
:step-id="step.id" :step-id="step.id"
:disabled="!!step.isQuoteScenarioStep"
@quick-input="setQuickInput(step, $event)" @quick-input="setQuickInput(step, $event)"
@change="handleStepContentChange($event, step)" @change="handleStepContentChange($event, step)"
@click.stop @click.stop
@ -117,6 +118,7 @@
{{ step.name }} {{ step.name }}
</div> </div>
<MsIcon <MsIcon
v-if="!step.isQuoteScenarioStep"
type="icon-icon_edit_outlined" type="icon-icon_edit_outlined"
class="edit-script-name-icon" class="edit-script-name-icon"
@click.stop="handleStepNameClick(step)" @click.stop="handleStepNameClick(step)"
@ -155,6 +157,7 @@
{{ step.name || t('apiScenario.pleaseInputStepDesc') }} {{ step.name || t('apiScenario.pleaseInputStepDesc') }}
</div> </div>
<MsIcon <MsIcon
v-if="!step.isQuoteScenarioStep"
type="icon-icon_edit_outlined" type="icon-icon_edit_outlined"
class="edit-script-name-icon" class="edit-script-name-icon"
@click.stop="handleStepDescClick(step)" @click.stop="handleStepDescClick(step)"
@ -174,17 +177,17 @@
@click="setFocusNodeKey(step.id)" @click="setFocusNodeKey(step.id)"
@other-create="handleOtherCreate" @other-create="handleOtherCreate"
@close="setFocusNodeKey('')" @close="setFocusNodeKey('')"
@add-done="scenario.unSaved = true" @add-done="handleAddStepDone"
/> />
</template> </template>
<template #extraEnd="step"> <template #extraEnd="step">
<responsePopover <responsePopover
v-if=" v-if="
![ [
ScenarioStepType.LOOP_CONTROLLER, ScenarioStepType.API,
ScenarioStepType.IF_CONTROLLER, ScenarioStepType.API_CASE,
ScenarioStepType.ONCE_ONLY_CONTROLLER, ScenarioStepType.SCRIPT,
ScenarioStepType.CONSTANT_TIMER, ScenarioStepType.CUSTOM_REQUEST,
].includes(step.stepType) && ].includes(step.stepType) &&
(getExecuteStatus(step) === ScenarioExecuteStatus.SUCCESS || (getExecuteStatus(step) === ScenarioExecuteStatus.SUCCESS ||
getExecuteStatus(step) === ScenarioExecuteStatus.FAILED) getExecuteStatus(step) === ScenarioExecuteStatus.FAILED)
@ -213,7 +216,7 @@
<createStepActions <createStepActions
v-model:selected-keys="selectedKeys" v-model:selected-keys="selectedKeys"
v-model:steps="steps" v-model:steps="steps"
@add-done="scenario.unSaved = true" @add-done="handleAddStepDone"
@other-create="handleOtherCreate" @other-create="handleOtherCreate"
> >
<a-button type="dashed" class="add-step-btn" long> <a-button type="dashed" class="add-step-btn" long>
@ -248,6 +251,7 @@
<importApiDrawer <importApiDrawer
v-if="importApiDrawerVisible" v-if="importApiDrawerVisible"
v-model:visible="importApiDrawerVisible" v-model:visible="importApiDrawerVisible"
:scenario-id="scenario.id"
@copy="handleImportApiApply('copy', $event)" @copy="handleImportApiApply('copy', $event)"
@quote="handleImportApiApply('quote', $event)" @quote="handleImportApiApply('quote', $event)"
/> />
@ -452,6 +456,7 @@
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'updateResource', uploadFileIds: string[], linkFileIds: string[]): void; (e: 'updateResource', uploadFileIds: string[], linkFileIds: string[]): void;
(e: 'stepAdd'): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
@ -470,11 +475,13 @@
const scenario = defineModel<Scenario>('scenario', { const scenario = defineModel<Scenario>('scenario', {
required: true, required: true,
}); });
const selectedKeys = defineModel<(string | number)[]>('selectedKeys', {
required: true,
}); //
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec'); const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
const localExecuteUrl = inject<Ref<string>>('localExecuteUrl'); const localExecuteUrl = inject<Ref<string>>('localExecuteUrl');
const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig'); const currentEnvConfig = inject<Ref<EnvConfig>>('currentEnvConfig');
const selectedKeys = ref<(string | number)[]>([]); //
const loading = ref(false); const loading = ref(false);
const treeRef = ref<InstanceType<typeof MsTree>>(); const treeRef = ref<InstanceType<typeof MsTree>>();
const focusStepKey = ref<string | number>(''); // key const focusStepKey = ref<string | number>(''); // key
@ -489,9 +496,9 @@
} }
function getExecuteStatus(step: ScenarioStepItem) { 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.FAILED
: ScenarioExecuteStatus.SUCCESS; : ScenarioExecuteStatus.SUCCESS;
} }
@ -509,8 +516,8 @@
const firstHasResultChild = step.children?.find((child) => { const firstHasResultChild = step.children?.find((child) => {
return checkStepIsApi(child) || child.stepType === ScenarioStepType.SCRIPT; return checkStepIsApi(child) || child.stepType === ScenarioStepType.SCRIPT;
}); });
return firstHasResultChild && scenario.value.stepResponses[firstHasResultChild.id] return firstHasResultChild && scenario.value.stepResponses[firstHasResultChild.uniqueId]
? `${scenario.value.stepResponses[firstHasResultChild.id].length}/${step.config.msCountController.loops}` ? `${scenario.value.stepResponses[firstHasResultChild.uniqueId].length}/${step.config.msCountController.loops}`
: undefined; : undefined;
} }
return undefined; return undefined;
@ -798,6 +805,7 @@
executeStatus: undefined, executeStatus: undefined,
copyFromStepId: childCopyFromStepId, copyFromStepId: childCopyFromStepId,
id: childId, id: childId,
uniqueId: childId,
}; };
})[0] })[0]
), ),
@ -806,6 +814,7 @@
sort: node.sort + 1, sort: node.sort + 1,
isNew: true, isNew: true,
id, id,
uniqueId: id,
}, },
'after', 'after',
selectedIfNeed, selectedIfNeed,
@ -950,6 +959,11 @@
} }
} }
function handleAddStepDone() {
emit('stepAdd');
scenario.value.unSaved = true;
}
/** /**
* 处理步骤选中事件 * 处理步骤选中事件
* @param _selectedKeys 选中的 key集合 * @param _selectedKeys 选中的 key集合
@ -1000,27 +1014,16 @@
} }
const websocketMap: Record<string | number, WebSocket> = {}; const websocketMap: Record<string | number, WebSocket> = {};
let temporaryStepReportMap = {}; // websockettab
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监听接收执行结果 * 开启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( websocketMap[reportId] = getSocket(
reportId || '', reportId || '',
executeType === 'localExec' ? '/ws/debug' : '', executeType === 'localExec' ? '/ws/debug' : '',
@ -1032,34 +1035,21 @@
if (step.reportId === data.reportId) { if (step.reportId === data.reportId) {
// tabtab // tabtab
data.taskResult.requestResults.forEach((result) => { data.taskResult.requestResults.forEach((result) => {
if (scenario.value.stepResponses[result.stepId] === undefined) { if (_scenario.stepResponses[result.stepId] === undefined) {
scenario.value.stepResponses[result.stepId] = []; _scenario.stepResponses[result.stepId] = [];
} }
scenario.value.stepResponses[result.stepId].push({ _scenario.stepResponses[result.stepId].push({
...result, ...result,
console: data.taskResult.console, 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') { } else if (data.msgType === 'EXEC_END') {
// websocket // websocket
websocketMap[reportId].close(); websocketMap[reportId]?.close();
if (step.reportId === data.reportId) { if (step.reportId === data.reportId) {
step.isExecuting = false; step.isExecuting = false;
updateStepStatus([step], scenario.value.stepResponses); updateStepStatus([step], _scenario.stepResponses);
} }
} }
}); });
@ -1076,7 +1066,7 @@
try { try {
currentStep.isExecuting = true; currentStep.isExecuting = true;
currentStep.executeStatus = ScenarioExecuteStatus.EXECUTING; currentStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
debugSocket(currentStep, executeParams.reportId, executeType); // websocket debugSocket(currentStep, scenario.value, executeParams.reportId, executeType); // websocket
const res = await debugScenario({ const res = await debugScenario({
id: scenario.value.id || '', id: scenario.value.id || '',
grouped: false, grouped: false,
@ -1100,6 +1090,7 @@
console.log(error); console.log(error);
websocketMap[executeParams.reportId].close(); websocketMap[executeParams.reportId].close();
currentStep.isExecuting = false; currentStep.isExecuting = false;
updateStepStatus([currentStep], scenario.value.stepResponses);
} }
} }
@ -1110,25 +1101,24 @@
if (node.isExecuting) { if (node.isExecuting) {
return; return;
} }
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.id, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, node.uniqueId, 'uniqueId');
if (realStep) { if (realStep) {
realStep.reportId = getGenerateId(); realStep.reportId = getGenerateId();
const _stepDetails = {}; const _stepDetails = {};
const stepFileParam = scenario.value.stepFileParam[realStep.id]; const stepFileParam = scenario.value.stepFileParam[realStep.id];
traverseTree( traverseTree(
realStep, realStep,
(step) => {
//
return step.enable;
},
(step) => { (step) => {
if (step.enable) { if (step.enable) {
// //
_stepDetails[step.id] = stepDetails.value[step.id]; _stepDetails[step.id] = stepDetails.value[step.id];
step.executeStatus = ScenarioExecuteStatus.EXECUTING; step.executeStatus = ScenarioExecuteStatus.EXECUTING;
} }
delete scenario.value.stepResponses[step.id]; // delete scenario.value.stepResponses[step.uniqueId]; //
return step; },
(step) => {
//
return step.enable;
} }
); );
realExecute( realExecute(
@ -1152,7 +1142,7 @@
function handleApiExecute(request: RequestParam, executeType?: 'localExec' | 'serverExec') { function handleApiExecute(request: RequestParam, executeType?: 'localExec' | 'serverExec') {
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, request.stepId, 'id'); const realStep = findNodeByKey<ScenarioStepItem>(steps.value, request.stepId, 'id');
if (realStep) { if (realStep) {
delete scenario.value.stepResponses[realStep.id]; // delete scenario.value.stepResponses[realStep.uniqueId]; //
realStep.reportId = getGenerateId(); realStep.reportId = getGenerateId();
realStep.executeStatus = ScenarioExecuteStatus.EXECUTING; realStep.executeStatus = ScenarioExecuteStatus.EXECUTING;
request.executeLoading = true; request.executeLoading = true;
@ -1184,6 +1174,7 @@
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
isExecuting: false, isExecuting: false,
reportId, reportId,
uniqueId: request.stepId,
}; };
realExecute( realExecute(
{ {
@ -1288,6 +1279,7 @@
} else { } else {
steps.value = steps.value.concat(insertSteps); steps.value = steps.value.concat(insertSteps);
} }
emit('stepAdd');
scenario.value.unSaved = true; scenario.value.unSaved = true;
} }
@ -1318,6 +1310,7 @@
method: request.method, method: request.method,
}, },
id: request.stepId, id: request.stepId,
uniqueId: request.stepId,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}, },
activeStep.value, activeStep.value,
@ -1333,6 +1326,7 @@
method: request.method, method: request.method,
}, },
id: request.stepId, id: request.stepId,
uniqueId: request.stepId,
sort: steps.value.length + 1, sort: steps.value.length + 1,
stepType: ScenarioStepType.CUSTOM_REQUEST, stepType: ScenarioStepType.CUSTOM_REQUEST,
refType: ScenarioStepRefType.DIRECT, refType: ScenarioStepRefType.DIRECT,
@ -1340,6 +1334,7 @@
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}); });
} }
emit('stepAdd');
scenario.value.unSaved = true; scenario.value.unSaved = true;
} }
@ -1347,6 +1342,14 @@
* API 详情抽屉关闭时应用更改 * API 详情抽屉关闭时应用更改
*/ */
function applyApiStep(request: RequestParam | CaseRequestParam) { 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) { if (request.unSaved) {
scenario.value.unSaved = true; scenario.value.unSaved = true;
} }
@ -1400,6 +1403,7 @@
steps.value.push({ steps.value.push({
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id, id,
uniqueId: id,
sort: steps.value.length + 1, sort: steps.value.length + 1,
stepType: ScenarioStepType.SCRIPT, stepType: ScenarioStepType.SCRIPT,
refType: ScenarioStepRefType.DIRECT, refType: ScenarioStepRefType.DIRECT,
@ -1407,6 +1411,7 @@
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
}); });
} }
emit('stepAdd');
scenario.value.unSaved = true; scenario.value.unSaved = true;
} }
@ -1482,6 +1487,7 @@
return true; return true;
}); });
} }
console.log(dragNode, dropNode);
const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id'); const dragResult = handleTreeDragDrop(steps.value, dragNode, dropNode, dropPosition, 'id');
if (dragResult) { if (dragResult) {
Message.success(t('common.moveSuccess')); Message.success(t('common.moveSuccess'));

View File

@ -17,9 +17,14 @@ export default function updateStepStatus(
ScenarioStepType.LOOP_CONTROLLER, ScenarioStepType.LOOP_CONTROLLER,
ScenarioStepType.IF_CONTROLLER, ScenarioStepType.IF_CONTROLLER,
ScenarioStepType.ONCE_ONLY_CONTROLLER, ScenarioStepType.ONCE_ONLY_CONTROLLER,
ScenarioStepType.API_SCENARIO,
].includes(node.stepType) ].includes(node.stepType)
) { ) {
// 逻辑控制器内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的 if (!node.executeStatus) {
// 没有执行状态,说明未参与执行,直接跳过
break;
}
// 逻辑控制器和场景内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的
let hasNotExecuted = false; let hasNotExecuted = false;
let hasFailure = false; let hasFailure = false;
if (!node.children || node.children.length === 0) { if (!node.children || node.children.length === 0) {
@ -54,8 +59,8 @@ export default function updateStepStatus(
node.executeStatus = ScenarioExecuteStatus.SUCCESS; node.executeStatus = ScenarioExecuteStatus.SUCCESS;
} else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) { } else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) {
// 非逻辑控制器直接更改本身状态 // 非逻辑控制器直接更改本身状态
if (stepResponses[node.id] && stepResponses[node.id].length > 0) { if (stepResponses[node.uniqueId] && stepResponses[node.uniqueId].length > 0) {
node.executeStatus = stepResponses[node.id].some((report) => !report.isSuccessful) node.executeStatus = stepResponses[node.uniqueId].some((report) => !report.isSuccessful)
? ScenarioExecuteStatus.FAILED ? ScenarioExecuteStatus.FAILED
: ScenarioExecuteStatus.SUCCESS; : ScenarioExecuteStatus.SUCCESS;
} else { } else {

View File

@ -158,65 +158,48 @@
const currentEnvConfig = ref<EnvConfig>(); const currentEnvConfig = ref<EnvConfig>();
const executeButtonRef = ref<InstanceType<typeof executeButton>>(); const executeButtonRef = ref<InstanceType<typeof executeButton>>();
const websocket = ref<WebSocket>(); const websocketMap: Record<string | number, WebSocket> = {};
const temporaryScenarioReportMap = {}; // websockettab
function setStepExecuteStatus() { function setStepExecuteStatus(scenario: Scenario) {
updateStepStatus(activeScenarioTab.value.steps, activeScenarioTab.value.stepResponses); updateStepStatus(scenario.steps, scenario.stepResponses);
} }
/** /**
* 开启websocket监听接收执行结果 * 开启websocket监听接收执行结果
*/ */
function debugSocket(reportId?: string | number, executeType?: 'localExec' | 'serverExec', localExecuteUrl?: string) { function debugSocket(scenario: Scenario, executeType?: 'localExec' | 'serverExec', localExecuteUrl?: string) {
websocket.value = getSocket( websocketMap[scenario.reportId] = getSocket(
reportId || '', scenario.reportId || '',
executeType === 'localExec' ? '/ws/debug' : '', executeType === 'localExec' ? '/ws/debug' : '',
executeType === 'localExec' ? localExecuteUrl : '' executeType === 'localExec' ? localExecuteUrl : ''
); );
websocket.value.addEventListener('message', (event) => { websocketMap[scenario.reportId].addEventListener('message', (event) => {
const data = JSON.parse(event.data); const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') { if (data.msgType === 'EXEC_RESULT') {
if (activeScenarioTab.value.reportId === data.reportId) { if (scenario.reportId === data.reportId) {
// tabtab // tabtab
data.taskResult.requestResults.forEach((result) => { data.taskResult.requestResults.forEach((result) => {
if (activeScenarioTab.value.stepResponses[result.stepId] === undefined) { if (scenario.stepResponses[result.stepId] === undefined) {
activeScenarioTab.value.stepResponses[result.stepId] = []; scenario.stepResponses[result.stepId] = [];
} }
activeScenarioTab.value.stepResponses[result.stepId].push({ scenario.stepResponses[result.stepId].push({
...result, ...result,
console: data.taskResult.console, console: data.taskResult.console,
}); });
if (result.isSuccessful) { if (result.isSuccessful) {
activeScenarioTab.value.executeSuccessCount += 1; scenario.executeSuccessCount += 1;
} else { } else {
activeScenarioTab.value.executeFailCount += 1; scenario.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,
});
} }
}); });
} }
} else if (data.msgType === 'EXEC_END') { } else if (data.msgType === 'EXEC_END') {
// websocket // websocket
websocket.value?.close(); websocketMap[scenario.reportId]?.close();
if (activeScenarioTab.value.reportId === data.reportId) { if (scenario.reportId === data.reportId) {
activeScenarioTab.value.executeLoading = false; scenario.executeLoading = false;
activeScenarioTab.value.isExecute = false; scenario.isExecute = false;
setStepExecuteStatus(); setStepExecuteStatus(scenario);
} }
} }
}); });
@ -237,7 +220,6 @@
) { ) {
try { try {
activeScenarioTab.value.executeLoading = true; activeScenarioTab.value.executeLoading = true;
debugSocket(executeParams.reportId, executeType, localExecuteUrl); // websocket
// //
activeScenarioTab.value.executeTime = dayjs().format('YYYY-MM-DD HH:mm:ss'); activeScenarioTab.value.executeTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
activeScenarioTab.value.executeSuccessCount = 0; activeScenarioTab.value.executeSuccessCount = 0;
@ -245,6 +227,7 @@
activeScenarioTab.value.stepResponses = {}; activeScenarioTab.value.stepResponses = {};
activeScenarioTab.value.reportId = executeParams.reportId; // ID activeScenarioTab.value.reportId = executeParams.reportId; // ID
activeScenarioTab.value.isDebug = !isExecute; activeScenarioTab.value.isDebug = !isExecute;
debugSocket(activeScenarioTab.value, executeType, localExecuteUrl); // websocket
let res; let res;
if (isExecute && executeType !== 'localExec' && !activeScenarioTab.value.isNew) { if (isExecute && executeType !== 'localExec' && !activeScenarioTab.value.isNew) {
// //
@ -290,9 +273,9 @@
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
websocket.value?.close(); websocketMap[activeScenarioTab.value.reportId]?.close();
activeScenarioTab.value.executeLoading = false; activeScenarioTab.value.executeLoading = false;
setStepExecuteStatus(); setStepExecuteStatus(activeScenarioTab.value);
} }
} }
@ -307,12 +290,6 @@
if (node.enable) { if (node.enable) {
node.executeStatus = ScenarioExecuteStatus.EXECUTING; node.executeStatus = ScenarioExecuteStatus.EXECUTING;
waitingDebugStepDetails[node.id] = activeScenarioTab.value.stepDetails[node.id]; 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; return !!node.enable;
}); });
@ -329,63 +306,59 @@
} }
function handleStopExecute() { function handleStopExecute() {
websocket.value?.close(); websocketMap[activeScenarioTab.value.reportId]?.close();
activeScenarioTab.value.executeLoading = false; 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') { function newTab(defaultScenarioInfo?: Scenario, action?: 'copy' | 'execute') {
if (defaultScenarioInfo) { if (defaultScenarioInfo) {
const isCopy = action === 'copy'; const isCopy = action === 'copy';
let copySteps: ScenarioStepItem[] = []; let copySteps: ScenarioStepItem[] = [];
if (isCopy) { if (isCopy) {
copySteps = mapTree(defaultScenarioInfo.steps, (node) => { // copyFromStepId
return {
...node,
copyFromStepId: node.id,
id: getGenerateId(),
};
});
} else {
copySteps = mapTree(defaultScenarioInfo.steps, (node) => { copySteps = mapTree(defaultScenarioInfo.steps, (node) => {
if ( if (
node.parent && node.parent &&
node.parent.stepType === ScenarioStepType.API_SCENARIO && node.parent.stepType === ScenarioStepType.API_SCENARIO &&
[ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(node.parent.refType) [ScenarioStepRefType.REF, ScenarioStepRefType.PARTIAL_REF].includes(node.parent.refType)
) { ) {
// //
node.isQuoteScenarioStep = true; // 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) { } else if (node.parent) {
// //
node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; // node.isQuoteScenarioStep = node.parent.isQuoteScenarioStep; //
node.isRefScenarioStep = node.parent.isRefScenarioStep; // 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; return node;
}); });
} }
@ -393,6 +366,7 @@
...defaultScenarioInfo, ...defaultScenarioInfo,
steps: copySteps, steps: copySteps,
id: isCopy ? getGenerateId() : defaultScenarioInfo.id || '', id: isCopy ? getGenerateId() : defaultScenarioInfo.id || '',
uniqueId: getGenerateId(),
label: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name, label: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name,
name: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name, name: isCopy ? `copy-${defaultScenarioInfo.name}` : defaultScenarioInfo.name,
isNew: isCopy, isNew: isCopy,

View File

@ -65,6 +65,8 @@ export default {
'apiScenario.sumLoop': '共{count}次循环', 'apiScenario.sumLoop': '共{count}次循环',
'apiScenario.times': '次', 'apiScenario.times': '次',
'apiScenario.executionResult': '执行结果', 'apiScenario.executionResult': '执行结果',
'apiScenario.refreshRefScenario': '刷新引用场景数据',
'apiScenario.updateRefScenarioSuccess': '引用场景数据已更新',
// 批量操作文案 // 批量操作文案
'api_scenario.batch_operation.success': '成功{opt}至 {name}', 'api_scenario.batch_operation.success': '成功{opt}至 {name}',
'api_scenario.table.batchMoveConfirm': '{opt}{count}个场景至已选模块', 'api_scenario.table.batchMoveConfirm': '{opt}{count}个场景至已选模块',