fix(all): 场景api、case替换&修复部分 bug

This commit is contained in:
baiqi 2024-04-08 21:03:20 +08:00 committed by Craftsman
parent 6e2dc8068d
commit 6178b24103
29 changed files with 379 additions and 125 deletions

View File

@ -13,38 +13,38 @@ export default mergeConfig(
}, },
proxy: { proxy: {
'/ws': { '/ws': {
target: 'http://172.16.200.18:8081/', target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/ws/, ''), rewrite: (path: string) => path.replace(/^\/front\/ws/, ''),
ws: true, ws: true,
}, },
'/front': { '/front': {
target: 'http://172.16.200.18:8081/', target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front/, ''), rewrite: (path: string) => path.replace(/^\/front/, ''),
}, },
'/file': { '/file': {
target: 'http://172.16.200.18:8081/', target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/file/, ''), rewrite: (path: string) => path.replace(/^\/front\/file/, ''),
}, },
'/attachment': { '/attachment': {
target: 'http://172.16.200.18:8081/', target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/attachment/, ''), rewrite: (path: string) => path.replace(/^\/front\/attachment/, ''),
}, },
'/bug/attachment': { '/bug/attachment': {
target: 'http://172.16.200.18:8081/', target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/bug\/attachment/, ''), rewrite: (path: string) => path.replace(/^\/front\/bug\/attachment/, ''),
}, },
'/plugin/image': { '/plugin/image': {
target: 'http://172.16.200.18:8081/', target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/plugin\/image/, ''), rewrite: (path: string) => path.replace(/^\/front\/plugin\/image/, ''),
}, },
'/base-display': { '/base-display': {
target: 'http://172.16.200.18:8081/', target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true, changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/base-display/, ''), rewrite: (path: string) => path.replace(/^\/front\/base-display/, ''),
}, },

View File

@ -745,6 +745,9 @@
background-color: rgb(var(--primary-5)) !important; background-color: rgb(var(--primary-5)) !important;
} }
.arco-switch-type-circle.arco-switch-disabled { .arco-switch-type-circle.arco-switch-disabled {
background-color: var(--color-text-n8) !important;
}
.arco-switch-type-circle.arco-switch-checked.arco-switch-disabled {
background-color: rgb(var(--primary-3)) !important; background-color: rgb(var(--primary-3)) !important;
} }
.arco-switch-type-line.arco-switch-small { .arco-switch-type-line.arco-switch-small {
@ -774,13 +777,14 @@
display: flex; display: flex;
align-items: center; align-items: center;
padding: 2px 8px; padding: 2px 8px;
border-radius: var(--border-radius-small);
background: var(--color-text-n9); background: var(--color-text-n9);
gap: 8px; gap: 8px;
.ms-pagination-jumper-input { .ms-pagination-jumper-input {
padding: 3px 8px; padding: 3px 8px;
width: 57px; width: 57px;
border: 1px solid var(--color-text-input-border); border: 1px solid var(--color-text-input-border);
border-radius: 3px; border-radius: var(--border-radius-small);
color: var(--color-text-1); color: var(--color-text-1);
background: var(--color-text-10); background: var(--color-text-10);
box-sizing: border-box; box-sizing: border-box;

View File

@ -132,7 +132,7 @@
v-model:data="getCurrentItemState" v-model:data="getCurrentItemState"
:disabled="props.disabled" :disabled="props.disabled"
@change="handleChange" @change="handleChange"
@deleteScriptItem="deleteScriptItem" @delete-script-item="deleteScriptItem"
/> />
</div> </div>
</div> </div>
@ -157,6 +157,7 @@
import ScriptTab from './comp/ScriptTab.vue'; import ScriptTab from './comp/ScriptTab.vue';
import StatusCodeTab from './comp/StatusCodeTab.vue'; import StatusCodeTab from './comp/StatusCodeTab.vue';
import VariableTab from './comp/VariableTab.vue'; import VariableTab from './comp/VariableTab.vue';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import useModal from '@/hooks/useModal'; import useModal from '@/hooks/useModal';
import { characterLimit } from '@/utils'; import { characterLimit } from '@/utils';

View File

@ -207,11 +207,11 @@
</template> </template>
</a-table> </a-table>
<div <div
v-if="attrs.showFooterActionWrap" v-if="showBatchAction || !!attrs.showPagination"
class="mt-[16px] flex h-[32px] flex-row flex-nowrap items-center" class="mt-[16px] flex h-[32px] flex-row flex-nowrap items-center"
:class="{ 'justify-between': showBatchAction }" :class="{ 'justify-between': showBatchAction }"
> >
<span v-if="!props.actionConfig && selectedCount > 0" class="title text-[var(--color-text-2)]"> <span v-if="props.actionConfig && selectedCount > 0" class="title text-[var(--color-text-2)]">
{{ t('msTable.batch.selected', { count: selectedCount }) }} {{ t('msTable.batch.selected', { count: selectedCount }) }}
<a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')"> <a-button class="clear-btn ml-[12px] px-2" type="text" @click="emit('clearSelector')">
{{ t('msTable.batch.clear') }} {{ t('msTable.batch.clear') }}
@ -232,7 +232,8 @@
v-if="!!attrs.showPagination" v-if="!!attrs.showPagination"
size="small" size="small"
v-bind="(attrs.msPagination as MsPaginationI)" v-bind="(attrs.msPagination as MsPaginationI)"
:simple="showBatchAction" :simple="!!showBatchAction"
:show-jumper="(attrs.msPagination as MsPaginationI).total / (attrs.msPagination as MsPaginationI).pageSize > 5"
@change="pageChange" @change="pageChange"
@page-size-change="pageSizeChange" @page-size-change="pageSizeChange"
/> />
@ -293,7 +294,6 @@
const props = defineProps<{ const props = defineProps<{
selectedKeys: Set<string>; selectedKeys: Set<string>;
selectedKey: string;
excludeKeys: Set<string>; excludeKeys: Set<string>;
selectorStatus: SelectAllEnum; selectorStatus: SelectAllEnum;
actionConfig?: BatchActionConfig; actionConfig?: BatchActionConfig;
@ -316,7 +316,6 @@
firstColumnWidth?: number; // firstColumnWidth?: number; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:selectedKey', value: string): void;
(e: 'batchAction', value: BatchActionParams, queryParams: BatchActionQueryParams): void; (e: 'batchAction', value: BatchActionParams, queryParams: BatchActionQueryParams): void;
(e: 'pageChange', value: number): void; (e: 'pageChange', value: number): void;
(e: 'pageSizeChange', value: number): void; (e: 'pageSizeChange', value: number): void;
@ -413,15 +412,15 @@
} }
}; };
const innerSelectedKey = defineModel<string>('selectedKey', { default: '' }); // const selectedKey = defineModel<string>('selectedKey', { default: '' }); //
const tempRecord = ref<TableData>({}); const tempRecord = ref<TableData>({});
watch( watch(
() => attrs.data, () => attrs.data,
(arr) => { (arr) => {
if (innerSelectedKey.value && Array.isArray(arr) && arr.length > 0) { if (selectedKey.value && Array.isArray(arr) && arr.length > 0) {
arr = arr.map((item: TableData) => { arr = arr.map((item: TableData) => {
if (item.id === innerSelectedKey.value) { if (item.id === selectedKey.value) {
item.tableChecked = true; item.tableChecked = true;
tempRecord.value = item; tempRecord.value = item;
} }
@ -436,7 +435,7 @@
function handleRadioChange(val: boolean, record: TableData) { function handleRadioChange(val: boolean, record: TableData) {
if (val) { if (val) {
innerSelectedKey.value = record.id; selectedKey.value = record.id;
record.tableChecked = true; record.tableChecked = true;
tempRecord.value.tableChecked = false; tempRecord.value.tableChecked = false;
tempRecord.value = record; tempRecord.value = record;
@ -469,7 +468,7 @@
}); });
const showBatchAction = computed(() => { const showBatchAction = computed(() => {
return selectedCount.value > 0 && !!attrs.selectable; return selectedCount.value > 0 && !!attrs.selectable && props.actionConfig;
}); });
const handleBatchAction = (value: BatchActionParams) => { const handleBatchAction = (value: BatchActionParams) => {

View File

@ -79,7 +79,6 @@ export default function useTableProps<T>(
emptyDataShowLine: true, // 空数据是否显示 "-" emptyDataShowLine: true, // 空数据是否显示 "-"
/** Column Selector */ /** Column Selector */
showJumpMethod: false, // 是否显示跳转方法 showJumpMethod: false, // 是否显示跳转方法
showFooterActionWrap: false, // 是否显示底部操作区域
isSimpleSetting: false, // 是否是简易column设置 isSimpleSetting: false, // 是否是简易column设置
filterIconAlignLeft: true, // 筛选图标是否靠左 filterIconAlignLeft: true, // 筛选图标是否靠左
...props, ...props,
@ -446,22 +445,9 @@ export default function useTableProps<T>(
}); });
watchEffect(() => { watchEffect(() => {
const { heightUsed, showPagination, selectedKeys, msPagination } = propsRes.value; const { heightUsed } = propsRes.value;
let hasFooterAction = false;
if (showPagination) {
const { pageSize, total } = msPagination as Pagination;
/*
*
* 1.
* 2.
*/
hasFooterAction = total > pageSize || selectedKeys.size > 0;
}
propsRes.value.showFooterActionWrap = hasFooterAction;
if (props?.heightUsed) { if (props?.heightUsed) {
const currentY = const currentY = appStore.innerHeight - (heightUsed || defaultHeightUsed);
appStore.innerHeight - (heightUsed || defaultHeightUsed) + (hasFooterAction ? 0 : footerActionWrapHeight);
propsRes.value.scroll = { ...propsRes.value.scroll, y: currentY }; propsRes.value.scroll = { ...propsRes.value.scroll, y: currentY };
} }
}); });

View File

@ -249,6 +249,7 @@ export enum ScenarioExecuteStatus {
FAILED = 'FAILED', FAILED = 'FAILED',
STOP = 'STOP', STOP = 'STOP',
UN_EXECUTE = 'UN_EXECUTE', UN_EXECUTE = 'UN_EXECUTE',
FAKE_ERROR = 'FAKE_ERROR',
} }
// 场景步骤类型 // 场景步骤类型
export enum ScenarioStepType { export enum ScenarioStepType {

View File

@ -148,7 +148,7 @@ export default {
'common.value.notNull': '属性值不能为空', 'common.value.notNull': '属性值不能为空',
'common.nameNotNull': '名称不能为空', 'common.nameNotNull': '名称不能为空',
'common.namePlaceholder': '请输入名称,按回车键保存', 'common.namePlaceholder': '请输入名称,按回车键保存',
'common.unsavedLeave': '有标签页的内容未保存,离开后未保存的内容将丢失,确定要离开吗?', 'common.unsavedLeave': '有标签页的内容未保存,离开后未保存的内容将丢失,确定要离开吗?',
'common.image': '图片', 'common.image': '图片',
'common.text': '文本', 'common.text': '文本',
}; };

View File

@ -402,6 +402,7 @@ export interface Scenario {
executeTime?: string | number; // 执行时间 executeTime?: string | number; // 执行时间
executeSuccessCount: number; // 执行成功数量 executeSuccessCount: number; // 执行成功数量
executeFailCount: number; // 执行失败数量 executeFailCount: number; // 执行失败数量
executeFakeErrorCount: 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; // 是否从列表执行进去场景详情

View File

@ -261,7 +261,7 @@ export function mapTree<T>(
if (newNode) { if (newNode) {
newNode.level = _level; newNode.level = _level;
if (newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) { if (newNode[customChildrenKey] && newNode[customChildrenKey].length > 0) {
newNode[customChildrenKey] = mapFunc(newNode[customChildrenKey], fullPath, _level + 1, node); newNode[customChildrenKey] = mapFunc(newNode[customChildrenKey], fullPath, _level + 1, newNode);
} }
} }
return newNode; return newNode;

View File

@ -61,9 +61,11 @@
value: item.id, value: item.id,
})); }));
currentEnv.value = currentEnv.value.length ? currentEnv.value : res[0]?.id; currentEnv.value = currentEnv.value.length ? currentEnv.value : res[0]?.id;
nextTick(() => {
if (currentEnv.value) { if (currentEnv.value) {
await initEnvironment(); initEnvironment();
} }
});
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);
@ -82,6 +84,18 @@
openNewPage(ProjectManagementRouteEnum.PROJECT_MANAGEMENT_ENVIRONMENT_MANAGEMENT); openNewPage(ProjectManagementRouteEnum.PROJECT_MANAGEMENT_ENVIRONMENT_MANAGEMENT);
} }
watch(
() => currentEnv.value,
(val) => {
if (!val) {
currentEnv.value = (envOptions.value[0]?.value as string) || '';
nextTick(() => {
initEnvironment();
});
}
}
);
onBeforeMount(() => { onBeforeMount(() => {
initEnvList(); initEnvList();
}); });

View File

@ -14,7 +14,7 @@
<MsCodeEditor <MsCodeEditor
v-show="!showImg || showType === 'text'" v-show="!showImg || showType === 'text'"
ref="responseEditorRef" ref="responseEditorRef"
:model-value="props.requestResult?.responseResult.body" :model-value="props.requestResult?.responseResult.body || ''"
:language="responseLanguage" :language="responseLanguage"
theme="vs" theme="vs"
:height="showImg ? 'calc(100% - 26px)' : '100%'" :height="showImg ? 'calc(100% - 26px)' : '100%'"

View File

@ -1,6 +1,6 @@
<template> <template>
<MsCodeEditor <MsCodeEditor
:model-value="props.console?.trim()" :model-value="props.console?.trim() || ''"
:language="LanguageEnum.PLAINTEXT" :language="LanguageEnum.PLAINTEXT"
theme="MS-text" theme="MS-text"
height="100%" height="100%"

View File

@ -68,7 +68,7 @@
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue'; import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.vue';
import CaseReportCom from './caseReportCom.vue'; import CaseReportCom from './caseReportCom.vue';
import { getShareInfo, getShareTime, reportCaseDetail } from '@/api/modules/api-test/report'; import { getShareInfo, reportCaseDetail } from '@/api/modules/api-test/report';
import { useI18n } from '@/hooks/useI18n'; import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store'; import { useAppStore } from '@/store';

View File

@ -22,6 +22,23 @@
</div> </div>
</a-tooltip> </a-tooltip>
</div> </div>
<div
v-if="
props.step &&
!props.step.isQuoteScenarioStep &&
props.step.resourceId &&
props.step?.stepType !== ScenarioStepType.CUSTOM_REQUEST
"
class="ml-auto"
>
<replaceButton
:steps="props.steps"
:step="props.step"
:resource-id="props.step.resourceId"
:scenario-id="scenarioId"
@replace="handleReplace"
/>
</div>
<div <div
v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST" v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST"
class="customApiDrawer-title-right ml-auto flex items-center gap-[16px]" class="customApiDrawer-title-right ml-auto flex items-center gap-[16px]"
@ -306,6 +323,7 @@
import MsTab from '@/components/pure/ms-tab/index.vue'; import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue'; import assertion from '@/components/business/ms-assertion/index.vue';
import loopPagination from './loopPagination.vue'; import loopPagination from './loopPagination.vue';
import replaceButton from './replaceButton.vue';
import stepTypeVue from './stepType/stepType.vue'; import stepTypeVue from './stepType/stepType.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue'; import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
@ -391,6 +409,7 @@
const props = defineProps<{ const props = defineProps<{
request?: RequestParam; // request?: RequestParam; //
step?: ScenarioStepItem; step?: ScenarioStepItem;
steps: ScenarioStepItem[];
detailLoading?: boolean; // detailLoading?: boolean; //
permissionMap?: { permissionMap?: {
execute: string; execute: string;
@ -404,6 +423,7 @@
(e: 'applyStep', request: RequestParam): void; (e: 'applyStep', request: RequestParam): void;
(e: 'execute', request: RequestParam, executeType?: 'localExec' | 'serverExec'): void; (e: 'execute', request: RequestParam, executeType?: 'localExec' | 'serverExec'): void;
(e: 'stopDebug'): void; (e: 'stopDebug'): void;
(e: 'replace', newStep: ScenarioStepItem): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
@ -492,7 +512,11 @@
}); });
// //
const title = computed(() => { const title = computed(() => {
if (_stepType.value.isCopyApi || _stepType.value.isQuoteApi) { if (
_stepType.value.isCopyApi ||
_stepType.value.isQuoteApi ||
props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST
) {
return props.step?.name; return props.step?.name;
} }
return t('apiScenario.customApi'); return t('apiScenario.customApi');
@ -1014,6 +1038,7 @@
} }
function stopDebug() { function stopDebug() {
requestVModel.value.executeLoading = false;
emit('stopDebug'); emit('stopDebug');
} }
@ -1097,6 +1122,14 @@
} }
} }
/**
* 替换步骤
* @param newStep 替换的新步骤
*/
function handleReplace(newStep: ScenarioStepItem) {
emit('replace', newStep);
}
watch( watch(
() => visible.value, () => visible.value,
async (val) => { async (val) => {

View File

@ -28,11 +28,17 @@
</a-tooltip> </a-tooltip>
<MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" /> <MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" />
</div> </div>
<div class="right-operation-button-icon flex items-center"> <div
<MsButton type="icon" status="secondary"> v-if="activeStep && !activeStep.isQuoteScenarioStep && requestVModel.resourceId"
<MsIcon type="icon-icon_swich" /> class="right-operation-button-icon flex items-center"
{{ t('common.replace') }} >
</MsButton> <replaceButton
:steps="props.steps"
:step="activeStep"
:resource-id="requestVModel.resourceId"
:scenario-id="scenarioId"
@replace="handleReplace"
/>
<MsButton class="mr-4" type="icon" status="secondary" @click="handleDelete"> <MsButton class="mr-4" type="icon" status="secondary" @click="handleDelete">
<MsIcon type="icon-icon_delete-trash_outlined" /> <MsIcon type="icon-icon_delete-trash_outlined" />
{{ t('common.delete') }} {{ t('common.delete') }}
@ -255,6 +261,7 @@
import MsTab from '@/components/pure/ms-tab/index.vue'; import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue'; import assertion from '@/components/business/ms-assertion/index.vue';
import loopPagination from './loopPagination.vue'; import loopPagination from './loopPagination.vue';
import replaceButton from './replaceButton.vue';
import stepType from './stepType/stepType.vue'; import stepType from './stepType/stepType.vue';
import auth from '@/views/api-test/components/requestComposition/auth.vue'; import auth from '@/views/api-test/components/requestComposition/auth.vue';
import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue'; import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue';
@ -306,6 +313,7 @@
const props = defineProps<{ const props = defineProps<{
request?: RequestParam; // request?: RequestParam; //
steps: ScenarioStepItem[];
stepResponses?: Record<string | number, RequestResult[]>; stepResponses?: Record<string | number, RequestResult[]>;
fileParams?: ScenarioStepFileParams; fileParams?: ScenarioStepFileParams;
permissionMap?: { permissionMap?: {
@ -317,6 +325,7 @@
(e: 'deleteStep'): void; (e: 'deleteStep'): void;
(e: 'execute', request: RequestParam, executeType?: 'localExec' | 'serverExec'): void; (e: 'execute', request: RequestParam, executeType?: 'localExec' | 'serverExec'): void;
(e: 'stopDebug'): void; (e: 'stopDebug'): void;
(e: 'replace', newStep: ScenarioStepItem): void;
}>(); }>();
const appStore = useAppStore(); const appStore = useAppStore();
@ -903,6 +912,7 @@
} }
function stopDebug() { function stopDebug() {
requestVModel.value.executeLoading = false;
emit('stopDebug'); emit('stopDebug');
} }
@ -956,6 +966,14 @@
} }
} }
/**
* 替换步骤
* @param newStep 替换的新步骤
*/
function handleReplace(newStep: ScenarioStepItem) {
emit('replace', newStep);
}
watch( watch(
() => visible.value, () => visible.value,
async (val) => { async (val) => {
@ -968,6 +986,7 @@
...defaultApiParams, ...defaultApiParams,
...props.request, ...props.request,
isNew: false, isNew: false,
stepId: activeStep.value?.uniqueId || '',
}); });
if (isQuote.value || isCopyNeedInit.value) { if (isQuote.value || isCopyNeedInit.value) {
// (request.requestrequest null) // (request.requestrequest null)

View File

@ -38,6 +38,11 @@
color: 'rgb(var(--danger-6))', color: 'rgb(var(--danger-6))',
text: 'common.fail', text: 'common.fail',
}, },
[ScenarioExecuteStatus.FAKE_ERROR]: {
bgColor: 'rgb(var(--warning-2))',
color: 'rgb(var(--warning-5))',
text: 'report.fake.error',
},
[ScenarioExecuteStatus.SUCCESS]: { [ScenarioExecuteStatus.SUCCESS]: {
bgColor: 'rgb(var(--success-2))', bgColor: 'rgb(var(--success-2))',
color: 'rgb(var(--success-6))', color: 'rgb(var(--success-6))',

View File

@ -1,7 +1,7 @@
<template> <template>
<MsDrawer <MsDrawer
v-model:visible="visible" v-model:visible="visible"
:title="t('apiScenario.importSystemApi')" :title="props.singleSelect ? t('common.replace') : t('apiScenario.importSystemApi')"
:width="1200" :width="1200"
no-content-padding no-content-padding
disabled-width-drag disabled-width-drag
@ -47,6 +47,9 @@
:selected-cases="selectedCases" :selected-cases="selectedCases"
:selected-scenarios="selectedScenarios" :selected-scenarios="selectedScenarios"
:scenario-id="props.scenarioId" :scenario-id="props.scenarioId"
:case-id="props.caseId"
:api-id="props.apiId"
:single-select="props.singleSelect"
@select="handleTableSelect" @select="handleTableSelect"
/> />
</div> </div>
@ -67,7 +70,12 @@
<div class="second-text">{{ t('apiScenario.scenario') }}</div> <div class="second-text">{{ t('apiScenario.scenario') }}</div>
<div class="main-text">{{ selectedScenarios.length }}</div> <div class="main-text">{{ selectedScenarios.length }}</div>
<a-divider v-show="totalSelected > 0" direction="vertical" :margin="4"></a-divider> <a-divider v-show="totalSelected > 0" direction="vertical" :margin="4"></a-divider>
<MsButton v-show="totalSelected > 0" type="text" class="!mr-0 ml-[4px]" @click="clearAll"> <MsButton
v-show="totalSelected > 0 && !props.singleSelect"
type="text"
class="!mr-0 ml-[4px]"
@click="clearAll"
>
{{ t('common.clear') }} {{ t('common.clear') }}
</MsButton> </MsButton>
</div> </div>
@ -115,6 +123,9 @@
const props = defineProps<{ const props = defineProps<{
scenarioId?: string | number; scenarioId?: string | number;
caseId?: string | number;
apiId?: string | number;
singleSelect?: boolean;
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'copy', data: ImportData): void; (e: 'copy', data: ImportData): void;
@ -138,6 +149,11 @@
}); });
function handleTableSelect(data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]) { function handleTableSelect(data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]) {
if (props.singleSelect) {
selectedApis.value = [];
selectedCases.value = [];
selectedScenarios.value = [];
}
if (activeKey.value === 'api') { if (activeKey.value === 'api') {
selectedApis.value = data as MsTableDataItem<ApiDefinitionDetail>[]; selectedApis.value = data as MsTableDataItem<ApiDefinitionDetail>[];
} else if (activeKey.value === 'case') { } else if (activeKey.value === 'case') {

View File

@ -19,6 +19,7 @@
</div> </div>
<ms-base-table <ms-base-table
v-bind="currentTable.propsRes.value" v-bind="currentTable.propsRes.value"
v-model:selected-key="selectedKey"
no-disable no-disable
filter-icon-align-left filter-icon-align-left
v-on="currentTable.propsEvent.value" v-on="currentTable.propsEvent.value"
@ -87,7 +88,7 @@
import MsButton from '@/components/pure/ms-button/index.vue'; import MsButton from '@/components/pure/ms-button/index.vue';
import MsBaseTable from '@/components/pure/ms-table/base-table.vue'; import MsBaseTable from '@/components/pure/ms-table/base-table.vue';
import { MsTableColumn, MsTableDataItem } from '@/components/pure/ms-table/type'; import { MsTableDataItem } from '@/components/pure/ms-table/type';
import useTable from '@/components/pure/ms-table/useTable'; import useTable from '@/components/pure/ms-table/useTable';
import { MsTreeNodeData } from '@/components/business/ms-tree/types'; import { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
@ -114,6 +115,9 @@
selectedCases: MsTableDataItem<ApiCaseDetail>[]; // selectedCases: MsTableDataItem<ApiCaseDetail>[]; //
selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; // selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; //
scenarioId?: string | number; scenarioId?: string | number;
caseId?: string | number;
apiId?: string | number;
singleSelect?: boolean; //
}>(); }>();
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void; (e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void;
@ -129,6 +133,7 @@
selectable: true, selectable: true,
showSelectorAll: false, showSelectorAll: false,
heightUsed: 300, heightUsed: 300,
selectorType: props.singleSelect ? 'radio' : ('checkbox' as 'checkbox' | 'radio' | 'none' | undefined),
}; };
// //
const useApiTable = useTable(getDefinitionPage, { const useApiTable = useTable(getDefinitionPage, {
@ -357,6 +362,17 @@
emit('select', tableSelectedData.value); emit('select', tableSelectedData.value);
} }
const selectedKey = ref('');
watch(
() => selectedKey.value,
(val) => {
const selectedData = currentTable.value.propsRes.value.data.find((e: any) => e.id === val);
tableSelectedData.value = selectedData ? [selectedData] : [];
emit('select', tableSelectedData.value);
}
);
function clearSelector() { function clearSelector() {
tableSelectedData.value = []; tableSelectedData.value = [];
currentTable.value.clearSelector(); currentTable.value.clearSelector();
@ -399,7 +415,7 @@
status: statusFilters.value, status: statusFilters.value,
method: methodFilters.value, method: methodFilters.value,
}, },
excludeIds: [props.scenarioId || ''], excludeIds: [props.scenarioId || '', props.caseId || '', props.apiId || ''],
}); });
currentTable.value.loadList(); currentTable.value.loadList();
}); });

View File

@ -0,0 +1,86 @@
<template>
<MsButton type="icon" status="secondary" @click="importApiDrawerVisible = true">
<MsIcon type="icon-icon_swich" class="mr-[8px]" />
{{ t('common.replace') }}
</MsButton>
<importApiDrawer
v-if="importApiDrawerVisible"
v-model:visible="importApiDrawerVisible"
:scenario-id="props.scenarioId"
:case-id="props.resourceId"
:api-id="props.resourceId"
single-select
@copy="handleImportApiApply('copy', $event)"
@quote="handleImportApiApply('quote', $event)"
/>
</template>
<script setup lang="ts">
import MsButton from '@/components/pure/ms-button/index.vue';
import MsIcon from '@/components/pure/ms-icon-font/index.vue';
import { ImportData } from './importApiDrawer/index.vue';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ScenarioStepRefType, ScenarioStepType } from '@/enums/apiEnum';
import useCreateActions from '../step/createAction/useCreateActions';
const importApiDrawer = defineAsyncComponent(() => import('./importApiDrawer/index.vue'));
const props = defineProps<{
steps: ScenarioStepItem[];
step: ScenarioStepItem;
resourceId: string | number;
scenarioId?: string | number;
}>();
const emit = defineEmits<{
(e: 'replace', replaceItem: ScenarioStepItem): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const { buildInsertStepInfos } = useCreateActions();
const importApiDrawerVisible = ref(false);
/**
* 处理导入系统请求
* @param type 导入类型
* @param data 导入数据
*/
function handleImportApiApply(type: 'copy' | 'quote', data: ImportData) {
const refType = type === 'copy' ? ScenarioStepRefType.COPY : ScenarioStepRefType.REF;
let replaceItem: ScenarioStepItem[];
if (data.api.length > 0) {
replaceItem = buildInsertStepInfos(
data.api,
ScenarioStepType.API,
refType,
props.step.sort,
props.step.projectId || appStore.currentProjectId
);
} else if (data.case.length > 0) {
replaceItem = buildInsertStepInfos(
data.case,
ScenarioStepType.API_CASE,
refType,
props.step.sort,
props.step.projectId || appStore.currentProjectId
);
} else {
replaceItem = buildInsertStepInfos(
data.scenario,
ScenarioStepType.API_SCENARIO,
refType,
props.step.sort,
props.step.projectId || appStore.currentProjectId
);
}
emit('replace', replaceItem[0]);
}
</script>
<style lang="less" scoped></style>

View File

@ -1,10 +1,22 @@
<template> <template>
<a-popover <a-popover
v-if="
[
ScenarioStepType.API,
ScenarioStepType.API_CASE,
ScenarioStepType.SCRIPT,
ScenarioStepType.CUSTOM_REQUEST,
].includes(step.stepType) &&
props.finalExecuteStatus &&
[ScenarioExecuteStatus.SUCCESS, ScenarioExecuteStatus.FAILED, ScenarioExecuteStatus.FAKE_ERROR].includes(
props.finalExecuteStatus
)
"
position="lt" 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)"
> >
<executeStatus :status="finalExecuteStatus" size="small" class="ml-[4px]" /> <executeStatus :status="props.finalExecuteStatus" size="small" class="ml-[4px]" />
<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" />
@ -40,6 +52,13 @@
</div> </div>
</template> </template>
</a-popover> </a-popover>
<executeStatus
v-else-if="step.executeStatus"
:status="props.finalExecuteStatus"
:extra-text="getExecuteStatusExtraText(step)"
size="small"
class="ml-[4px]"
/>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
@ -50,7 +69,12 @@
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, ScenarioStepType } from '@/enums/apiEnum'; import {
ResponseComposition,
ScenarioExecuteStatus,
ScenarioStepLoopTypeEnum,
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')
@ -59,6 +83,7 @@
const props = defineProps<{ const props = defineProps<{
step: ScenarioStepItem; step: ScenarioStepItem;
stepResponses: Record<string | number, Array<RequestResult>>; stepResponses: Record<string | number, Array<RequestResult>>;
finalExecuteStatus?: ScenarioExecuteStatus;
}>(); }>();
const emit = defineEmits(['visibleChange']); const emit = defineEmits(['visibleChange']);
@ -67,15 +92,27 @@
const currentLoop = ref(1); const currentLoop = ref(1);
const currentResponse = computed(() => props.stepResponses?.[props.step.uniqueId]?.[currentLoop.value - 1]); const currentResponse = computed(() => props.stepResponses?.[props.step.uniqueId]?.[currentLoop.value - 1]);
const loopTotal = computed(() => props.stepResponses?.[props.step.uniqueId]?.length || 0); const loopTotal = computed(() => props.stepResponses?.[props.step.uniqueId]?.length || 0);
const finalExecuteStatus = computed(() => {
if (props.stepResponses[props.step.uniqueId] && props.stepResponses[props.step.uniqueId].length > 0) { function getExecuteStatusExtraText(step: ScenarioStepItem) {
// if (
return props.stepResponses[props.step.uniqueId].some((report) => !report.isSuccessful) step.stepType === ScenarioStepType.LOOP_CONTROLLER &&
? ScenarioExecuteStatus.FAILED step.config.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT &&
: ScenarioExecuteStatus.SUCCESS; step.config.msCountController &&
} step.config.msCountController.loops > 0
return props.step.executeStatus; ) {
// /
const firstHasResultChild = step.children?.find((child) => {
return (
[ScenarioStepType.API, ScenarioStepType.API_CASE, ScenarioStepType.CUSTOM_REQUEST].includes(step.stepType) ||
child.stepType === ScenarioStepType.SCRIPT
);
}); });
return firstHasResultChild && props.stepResponses[firstHasResultChild.uniqueId]
? `${props.stepResponses[firstHasResultChild.uniqueId].length}/${step.config.msCountController.loops}`
: undefined;
}
return undefined;
}
</script> </script>
<style lang="less"> <style lang="less">

View File

@ -37,10 +37,10 @@
<a-button type="secondary" @click="handleDrawerCancel"> <a-button type="secondary" @click="handleDrawerCancel">
{{ t('common.cancel') }} {{ t('common.cancel') }}
</a-button> </a-button>
<a-button type="secondary" @click="saveAndContinue"> <a-button type="secondary" :disabled="!scriptName" @click="saveAndContinue">
{{ t('common.saveAndContinue') }} {{ t('common.saveAndContinue') }}
</a-button> </a-button>
<a-button type="primary" @click="save"> <a-button type="primary" :disabled="!scriptName" @click="save">
{{ t('common.add') }} {{ t('common.add') }}
</a-button> </a-button>
</template> </template>

View File

@ -21,7 +21,7 @@ export const defaultLoopController = {
}, },
whileController: { whileController: {
conditionType: WhileConditionType.CONDITION, // 条件类型 conditionType: WhileConditionType.CONDITION, // 条件类型
timeout: 0, // 超时时间 timeout: 3000, // 超时时间
msWhileScript: { msWhileScript: {
scriptValue: '', // 脚本值 scriptValue: '', // 脚本值
}, // 脚本 }, // 脚本
@ -118,6 +118,7 @@ export const defaultScenario: Scenario = {
executeTime: 0, executeTime: 0,
executeSuccessCount: 0, executeSuccessCount: 0,
executeFailCount: 0, executeFailCount: 0,
executeFakeErrorCount: 0,
uploadFileIds: [], uploadFileIds: [],
linkFileIds: [], linkFileIds: [],
reportId: '', reportId: '',

View File

@ -133,8 +133,8 @@ export default function useCreateActions() {
} }
return { return {
...cloneDeep(defaultStepItemCommon), ...cloneDeep(defaultStepItemCommon),
id, id: item.uniqueId || id,
uniqueId: getGenerateId(), // 生成唯一 ID避免重复引用的步骤无法读取正确的执行结果 uniqueId: item.uniqueId || id, // 生成唯一 ID避免重复引用的步骤无法读取正确的执行结果
config: { config: {
...defaultStepItemCommon.config, ...defaultStepItemCommon.config,
...config, ...config,

View File

@ -58,7 +58,11 @@
</div> </div>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
<div class="text-[var(--color-text-1)]">{{ t('common.fail') }}</div> <div class="text-[var(--color-text-1)]">{{ t('common.fail') }}</div>
<div class="text-[rgb(var(--success-6))]">{{ scenario.executeFailCount }}</div> <div class="text-[rgb(var(--danger-6))]">{{ scenario.executeFailCount }}</div>
</div>
<div class="flex items-center gap-[4px]">
<div class="text-[var(--color-text-1)]">{{ t('report.fake.error') }}</div>
<div class="text-[rgb(var(--warning-5))]">{{ scenario.executeFakeErrorCount }}</div>
</div> </div>
<MsButton <MsButton
v-if="scenario.isDebug === false && !scenario.executeLoading && !scenario.isNew" v-if="scenario.isDebug === false && !scenario.executeLoading && !scenario.isNew"

View File

@ -133,7 +133,7 @@
> >
<a-input-number <a-input-number
v-model:model-value="innerData.whileController.timeout" v-model:model-value="innerData.whileController.timeout"
class="w-[100px] px-[8px]" class="w-[120px] px-[8px]"
size="mini" size="mini"
:step="1" :step="1"
:min="0" :min="0"

View File

@ -152,7 +152,7 @@
<div <div
:class="`one-line-text mr-[4px] ${ :class="`one-line-text mr-[4px] ${
step.stepType === ScenarioStepType.ONCE_ONLY_CONTROLLER ? 'max-w-[750px]' : 'max-w-[150px]' step.stepType === ScenarioStepType.ONCE_ONLY_CONTROLLER ? 'max-w-[750px]' : 'max-w-[150px]'
} font-normal text-[var(--color-text-4)]`" } font-normal text-[var(--color-text-1)]`"
> >
{{ step.name || t('apiScenario.pleaseInputStepDesc') }} {{ step.name || t('apiScenario.pleaseInputStepDesc') }}
</div> </div>
@ -190,27 +190,11 @@
</template> </template>
<template #extraEnd="step"> <template #extraEnd="step">
<responsePopover <responsePopover
v-if="
[
ScenarioStepType.API,
ScenarioStepType.API_CASE,
ScenarioStepType.SCRIPT,
ScenarioStepType.CUSTOM_REQUEST,
].includes(step.stepType) &&
(getExecuteStatus(step) === ScenarioExecuteStatus.SUCCESS ||
getExecuteStatus(step) === ScenarioExecuteStatus.FAILED)
"
:step="step" :step="step"
:step-responses="scenario.stepResponses" :step-responses="scenario.stepResponses"
:final-execute-status="getExecuteStatus(step)"
@visible-change="handleResponsePopoverVisibleChange" @visible-change="handleResponsePopoverVisibleChange"
/> />
<executeStatus
v-else-if="step.executeStatus"
:status="getExecuteStatus(step)"
:extra-text="getExecuteStatusExtraText(step)"
size="small"
class="ml-[4px]"
/>
</template> </template>
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty> <template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
<div <div
@ -240,24 +224,30 @@
:request="currentStepDetail as unknown as RequestParam" :request="currentStepDetail as unknown as RequestParam"
:file-params="currentStepFileParams" :file-params="currentStepFileParams"
:step="activeStep" :step="activeStep"
:scenario-id="scenario.id"
:step-responses="scenario.stepResponses" :step-responses="scenario.stepResponses"
:permission-map="permissionMap" :permission-map="permissionMap"
:steps="steps"
@add-step="addCustomApiStep" @add-step="addCustomApiStep"
@apply-step="applyApiStep" @apply-step="applyApiStep"
@stop-debug="handleStopExecute(activeStep)" @stop-debug="handleStopExecute(activeStep)"
@execute="handleApiExecute" @execute="handleApiExecute"
@replace="handleReplaceStep"
/> />
<customCaseDrawer <customCaseDrawer
v-model:visible="customCaseDrawerVisible" v-model:visible="customCaseDrawerVisible"
:active-step="activeStep" :active-step="activeStep"
:request="currentStepDetail as unknown as RequestParam" :request="currentStepDetail as unknown as RequestParam"
:scenario-id="scenario.id"
:file-params="currentStepFileParams" :file-params="currentStepFileParams"
:steps="steps"
:step-responses="scenario.stepResponses" :step-responses="scenario.stepResponses"
:permission-map="permissionMap" :permission-map="permissionMap"
@apply-step="applyApiStep" @apply-step="applyApiStep"
@delete-step="deleteCaseStep(activeStep)" @delete-step="deleteCaseStep(activeStep)"
@stop-debug="handleStopExecute(activeStep)" @stop-debug="handleStopExecute(activeStep)"
@execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)" @execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)"
@replace="handleReplaceStep"
/> />
<importApiDrawer <importApiDrawer
v-if="importApiDrawerVisible" v-if="importApiDrawerVisible"
@ -451,7 +441,6 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types'; import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTree from '@/components/business/ms-tree/index.vue'; import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types'; import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import executeStatus from '../common/executeStatus.vue';
import { ImportData } from '../common/importApiDrawer/index.vue'; import { ImportData } from '../common/importApiDrawer/index.vue';
import stepType from '../common/stepType/stepType.vue'; import stepType from '../common/stepType/stepType.vue';
import createStepActions from './createAction/createStepActions.vue'; import createStepActions from './createAction/createStepActions.vue';
@ -496,7 +485,6 @@
RequestDefinitionStatus, RequestDefinitionStatus,
ScenarioAddStepActionType, ScenarioAddStepActionType,
ScenarioExecuteStatus, ScenarioExecuteStatus,
ScenarioStepLoopTypeEnum,
ScenarioStepRefType, ScenarioStepRefType,
ScenarioStepType, ScenarioStepType,
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
@ -569,32 +557,20 @@
function getExecuteStatus(step: ScenarioStepItem) { function getExecuteStatus(step: ScenarioStepItem) {
if (scenario.value.stepResponses && scenario.value.stepResponses[step.uniqueId]) { if (scenario.value.stepResponses && scenario.value.stepResponses[step.uniqueId]) {
if (
scenario.value.stepResponses[step.uniqueId].some((report) => report.status === ScenarioExecuteStatus.FAKE_ERROR)
) {
return ScenarioExecuteStatus.FAKE_ERROR;
}
// //
return scenario.value.stepResponses[step.uniqueId].some((report) => !report.isSuccessful) if (scenario.value.stepResponses[step.uniqueId].some((report) => !report.isSuccessful)) {
? ScenarioExecuteStatus.FAILED return ScenarioExecuteStatus.FAILED;
: ScenarioExecuteStatus.SUCCESS; }
return ScenarioExecuteStatus.SUCCESS;
} }
return step.executeStatus; return step.executeStatus;
} }
function getExecuteStatusExtraText(step: ScenarioStepItem) {
if (
step.stepType === ScenarioStepType.LOOP_CONTROLLER &&
step.config.loopType === ScenarioStepLoopTypeEnum.LOOP_COUNT &&
step.config.msCountController &&
step.config.msCountController.loops > 0
) {
// /
const firstHasResultChild = step.children?.find((child) => {
return checkStepIsApi(child) || child.stepType === ScenarioStepType.SCRIPT;
});
return firstHasResultChild && scenario.value.stepResponses[firstHasResultChild.uniqueId]
? `${scenario.value.stepResponses[firstHasResultChild.uniqueId].length}/${step.config.msCountController.loops}`
: undefined;
}
return undefined;
}
function handleResponsePopoverVisibleChange(visible: boolean, step: ScenarioStepItem) { function handleResponsePopoverVisibleChange(visible: boolean, step: ScenarioStepItem) {
if (visible) { if (visible) {
setFocusNodeKey(step.uniqueId); setFocusNodeKey(step.uniqueId);
@ -914,8 +890,8 @@
...cloneDeep( ...cloneDeep(
mapTree<ScenarioStepItem>(node, (childNode) => { mapTree<ScenarioStepItem>(node, (childNode) => {
const childId = getGenerateId(); const childId = getGenerateId();
const childStepDetail = stepDetails.value[node.id]; const childStepDetail = stepDetails.value[childNode.id];
const childStepFileParam = scenario.value.stepFileParam[node.id]; const childStepFileParam = scenario.value.stepFileParam[childNode.id];
let childCopyFromStepId = childNode.id; let childCopyFromStepId = childNode.id;
if (childStepDetail) { if (childStepDetail) {
// //
@ -923,7 +899,7 @@
} }
if (childStepFileParam) { if (childStepFileParam) {
// //
scenario.value.stepFileParam[id] = cloneDeep(childStepFileParam); scenario.value.stepFileParam[childNode.id] = cloneDeep(childStepFileParam);
} }
if (!isQuoteScenario) { if (!isQuoteScenario) {
// id // id
@ -1211,7 +1187,7 @@
const res = await debugScenario({ const res = await debugScenario({
id: scenario.value.id || '', id: scenario.value.id || '',
grouped: false, grouped: false,
environmentId: currentEnvConfig?.value.id || '', environmentId: currentEnvConfig?.value?.id || '',
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,
scenarioConfig: scenario.value.scenarioConfig, scenarioConfig: scenario.value.scenarioConfig,
frontendDebug: executeType === 'localExec', frontendDebug: executeType === 'localExec',
@ -1302,6 +1278,7 @@
} else { } else {
// //
const reportId = getGenerateId(); const reportId = getGenerateId();
delete scenario.value.stepResponses[request.stepId]; //
request.executeLoading = true; request.executeLoading = true;
activeStep.value = { activeStep.value = {
id: request.stepId, id: request.stepId,
@ -1343,6 +1320,41 @@
} }
} }
function handleReplaceStep(newStep: ScenarioStepItem) {
if (activeStep.value) {
//
delete scenario.value.stepResponses[activeStep.value.uniqueId];
delete scenario.value.stepFileParam[activeStep.value.id];
delete stepDetails.value[activeStep.value.id];
const realStep = findNodeByKey<ScenarioStepItem>(steps.value, activeStep.value.uniqueId, 'uniqueId');
if (realStep) {
//
if (realStep.parent?.children) {
// children
const index = realStep.parent.children.findIndex((item) => item.uniqueId === realStep.uniqueId);
realStep.parent.children.splice(index, 1, newStep);
} else {
//
const index = steps.value.findIndex((item) => item.uniqueId === realStep.uniqueId);
steps.value.splice(index, 1, newStep);
}
}
}
Message.success(t('apiScenario.replaceSuccess'));
scenario.value.unSaved = true;
if (newStep.stepType === ScenarioStepType.API_SCENARIO) {
customCaseDrawerVisible.value = false;
customApiDrawerVisible.value = false;
} else {
customCaseDrawerVisible.value = false;
customApiDrawerVisible.value = false;
nextTick(() => {
//
handleStepSelect([newStep.uniqueId], newStep);
});
}
}
/** /**
* 处理抽屉资源类型步骤创建动作 * 处理抽屉资源类型步骤创建动作
*/ */
@ -1609,7 +1621,7 @@
} }
loading.value = true; loading.value = true;
const offspringIds: string[] = []; const offspringIds: string[] = [];
mapTree(dragNode.children || [], (e) => { mapTree(cloneDeep(dragNode.children || []), (e) => {
offspringIds.push(e.uniqueId); offspringIds.push(e.uniqueId);
return e; return e;
}); });

View File

@ -27,6 +27,7 @@ export default function updateStepStatus(
// 逻辑控制器和场景内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的 // 逻辑控制器和场景内部可以放入任意步骤,所以它的最终执行结果是根据内部步骤的执行结果来判断的
let hasNotExecuted = false; let hasNotExecuted = false;
let hasFailure = false; let hasFailure = false;
let hasFakeError = false;
if (!node.children || node.children.length === 0) { if (!node.children || node.children.length === 0) {
// 逻辑控制器内无步骤,则直接是未执行 // 逻辑控制器内无步骤,则直接是未执行
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE; node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
@ -43,13 +44,18 @@ export default function updateStepStatus(
} else if (childNode.executeStatus === ScenarioExecuteStatus.FAILED) { } else if (childNode.executeStatus === ScenarioExecuteStatus.FAILED) {
// 子节点有一个失败,逻辑控制器就是失败 // 子节点有一个失败,逻辑控制器就是失败
hasFailure = true; hasFailure = true;
} else if (childNode.executeStatus === ScenarioExecuteStatus.FAKE_ERROR) {
// 子节点有一个误报,逻辑控制器就是误报
hasFakeError = true;
} }
} }
// 递归完子节点后,判断当前逻辑控制器的状态 // 递归完子节点后,判断当前逻辑控制器的状态
if (hasFailure) { if (hasNotExecuted) {
node.executeStatus = ScenarioExecuteStatus.FAILED;
} else if (hasNotExecuted) {
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE; node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
} else if (hasFailure) {
node.executeStatus = ScenarioExecuteStatus.FAILED;
} else if (hasFakeError) {
node.executeStatus = ScenarioExecuteStatus.FAKE_ERROR;
} else { } else {
node.executeStatus = ScenarioExecuteStatus.SUCCESS; node.executeStatus = ScenarioExecuteStatus.SUCCESS;
} }
@ -60,9 +66,13 @@ export default function updateStepStatus(
} else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) { } else if (node.executeStatus === ScenarioExecuteStatus.EXECUTING) {
// 非逻辑控制器直接更改本身状态 // 非逻辑控制器直接更改本身状态
if (stepResponses[node.uniqueId] && stepResponses[node.uniqueId].length > 0) { if (stepResponses[node.uniqueId] && stepResponses[node.uniqueId].length > 0) {
node.executeStatus = stepResponses[node.uniqueId].some((report) => !report.isSuccessful) if (stepResponses[node.uniqueId].some((report) => !report.isSuccessful)) {
? ScenarioExecuteStatus.FAILED node.executeStatus = ScenarioExecuteStatus.FAILED;
: ScenarioExecuteStatus.SUCCESS; } else if (stepResponses[node.uniqueId].some((report) => report.status === ScenarioExecuteStatus.FAKE_ERROR)) {
node.executeStatus = ScenarioExecuteStatus.FAKE_ERROR;
} else {
node.executeStatus = ScenarioExecuteStatus.SUCCESS;
}
} else { } else {
node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE; node.executeStatus = ScenarioExecuteStatus.UN_EXECUTE;
} }

View File

@ -17,7 +17,10 @@
</template> </template>
</MsEditableTab> </MsEditableTab>
<div v-show="activeScenarioTab.id !== 'all'" class="flex items-center gap-[8px]"> <div v-show="activeScenarioTab.id !== 'all'" class="flex items-center gap-[8px]">
<environmentSelect v-model:current-env-config="currentEnvConfig" /> <environmentSelect
v-model:currentEnv="activeScenarioTab.environmentId"
v-model:current-env-config="currentEnvConfig"
/>
<executeButton <executeButton
ref="executeButtonRef" ref="executeButtonRef"
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']" v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
@ -161,6 +164,7 @@
id: 'all', id: 'all',
label: t('apiScenario.allScenario'), label: t('apiScenario.allScenario'),
closable: false, closable: false,
environmentId: '',
} as ScenarioParams, } as ScenarioParams,
]); ]);
const activeScenarioTab = ref<ScenarioParams>(scenarioTabs.value[0] as ScenarioParams); const activeScenarioTab = ref<ScenarioParams>(scenarioTabs.value[0] as ScenarioParams);
@ -195,7 +199,9 @@
...result, ...result,
console: data.taskResult.console, console: data.taskResult.console,
}); });
if (result.isSuccessful) { if (result.status === ScenarioExecuteStatus.FAKE_ERROR) {
scenario.executeFakeErrorCount += 1;
} else if (result.isSuccessful) {
scenario.executeSuccessCount += 1; scenario.executeSuccessCount += 1;
} else { } else {
scenario.executeFailCount += 1; scenario.executeFailCount += 1;
@ -233,6 +239,7 @@
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;
activeScenarioTab.value.executeFailCount = 0; activeScenarioTab.value.executeFailCount = 0;
activeScenarioTab.value.executeFakeErrorCount = 0;
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;
@ -403,6 +410,7 @@
scenarioTabs.value.push({ scenarioTabs.value.push({
...cloneDeep(defaultScenario), ...cloneDeep(defaultScenario),
id: getGenerateId(), id: getGenerateId(),
environmentId: currentEnvConfig.value?.id || '',
label: `${t('apiScenario.createScenario')}${scenarioTabs.value.length}`, label: `${t('apiScenario.createScenario')}${scenarioTabs.value.length}`,
moduleId: activeModule.value === 'all' ? 'root' : activeModule.value, moduleId: activeModule.value === 'all' ? 'root' : activeModule.value,
projectId: appStore.currentProjectId, projectId: appStore.currentProjectId,

View File

@ -70,6 +70,7 @@ export default {
'apiScenario.executionResult': '执行结果', 'apiScenario.executionResult': '执行结果',
'apiScenario.refreshRefScenario': '刷新引用场景数据', 'apiScenario.refreshRefScenario': '刷新引用场景数据',
'apiScenario.updateRefScenarioSuccess': '引用场景数据已更新', 'apiScenario.updateRefScenarioSuccess': '引用场景数据已更新',
'apiScenario.replaceSuccess': '步骤替换成功',
// 批量操作文案 // 批量操作文案
'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}个场景至已选模块',