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,
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 });
}

View File

@ -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'; // 批量复制接口场景

View File

@ -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;

View File

@ -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'],

View File

@ -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 为步骤 idvalue 为步骤响应内容
isExecute?: boolean; // 是否从列表执行进去场景详情
isDebug?: boolean; // 是否调试,区分执行场景和批量调试步骤

View File

@ -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);

View File

@ -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>

View File

@ -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) {

View File

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

View File

@ -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;
}
},
{

View File

@ -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;

View File

@ -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,20 +239,13 @@
fullScenarioArr.push(...res);
});
if (refType === ScenarioStepRefType.COPY) {
fullScenarioArr = fullScenarioArr.map((e) => {
return {
...e,
children: mapTree<MsTableDataItem<ApiScenarioTableItem>>(e.children || [], (node) => {
fullScenarioArr = mapTree<MsTableDataItem<ApiScenarioTableItem>>(fullScenarioArr, (node) => {
return {
...node,
copyFromStepId: node.id,
originProjectId: node.projectId,
id: getGenerateId(),
};
}),
copyFromStepId: e.resourceId,
originProjectId: e.projectId,
};
});
emit(
'copy',
@ -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,
};
});

View File

@ -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();
});

View File

@ -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);

View File

@ -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,

View File

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

View File

@ -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 || [],

View File

@ -1,5 +1,6 @@
<template>
<div class="flex h-full flex-col gap-[8px]">
<a-spin class="h-full w-full" :loading="loading">
<div class="action-line">
<div class="action-group">
<a-checkbox
@ -71,31 +72,31 @@
allow-clear
class="w-[200px]"
/>
<a-button
v-if="!props.isNew"
type="outline"
class="arco-btn-outline--secondary !mr-0 !p-[8px]"
@click="refreshStepInfo"
>
<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 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;

View File

@ -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;

View File

@ -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);
});
//

View File

@ -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>

View File

@ -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;

View File

@ -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 = {}; // 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监听接收执行结果
*/
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) {
// tabtab
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'));

View File

@ -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 {

View File

@ -158,65 +158,48 @@
const currentEnvConfig = ref<EnvConfig>();
const executeButtonRef = ref<InstanceType<typeof executeButton>>();
const websocket = ref<WebSocket>();
const temporaryScenarioReportMap = {}; // websockettab
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) {
// tabtab
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,

View File

@ -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}个场景至已选模块',