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: {
'/ws': {
target: 'http://172.16.200.18:8081/',
target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/ws/, ''),
ws: true,
},
'/front': {
target: 'http://172.16.200.18:8081/',
target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front/, ''),
},
'/file': {
target: 'http://172.16.200.18:8081/',
target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/file/, ''),
},
'/attachment': {
target: 'http://172.16.200.18:8081/',
target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/attachment/, ''),
},
'/bug/attachment': {
target: 'http://172.16.200.18:8081/',
target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/bug\/attachment/, ''),
},
'/plugin/image': {
target: 'http://172.16.200.18:8081/',
target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/plugin\/image/, ''),
},
'/base-display': {
target: 'http://172.16.200.18:8081/',
target: 'https://qadevtest.fit2cloud.com/',
changeOrigin: true,
rewrite: (path: string) => path.replace(/^\/front\/base-display/, ''),
},

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -402,6 +402,7 @@ export interface Scenario {
executeTime?: string | number; // 执行时间
executeSuccessCount: number; // 执行成功数量
executeFailCount: number; // 执行失败数量
executeFakeErrorCount: number; // 执行误报数量
reportId: string | number; // 场景报告 id
stepResponses: Record<string | number, Array<RequestResult>>; // 步骤响应集合key 为步骤 idvalue 为步骤响应内容
isExecute?: boolean; // 是否从列表执行进去场景详情

View File

@ -261,7 +261,7 @@ export function mapTree<T>(
if (newNode) {
newNode.level = _level;
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;

View File

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

View File

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

View File

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

View File

@ -68,7 +68,7 @@
import MsDetailDrawer from '@/components/business/ms-detail-drawer/index.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 { useAppStore } from '@/store';

View File

@ -22,6 +22,23 @@
</div>
</a-tooltip>
</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
v-if="!props.step || props.step?.stepType === ScenarioStepType.CUSTOM_REQUEST"
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 assertion from '@/components/business/ms-assertion/index.vue';
import loopPagination from './loopPagination.vue';
import replaceButton from './replaceButton.vue';
import stepTypeVue from './stepType/stepType.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiMethodSelect from '@/views/api-test/components/apiMethodSelect.vue';
@ -391,6 +409,7 @@
const props = defineProps<{
request?: RequestParam; //
step?: ScenarioStepItem;
steps: ScenarioStepItem[];
detailLoading?: boolean; //
permissionMap?: {
execute: string;
@ -404,6 +423,7 @@
(e: 'applyStep', request: RequestParam): void;
(e: 'execute', request: RequestParam, executeType?: 'localExec' | 'serverExec'): void;
(e: 'stopDebug'): void;
(e: 'replace', newStep: ScenarioStepItem): void;
}>();
const appStore = useAppStore();
@ -492,7 +512,11 @@
});
//
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 t('apiScenario.customApi');
@ -1014,6 +1038,7 @@
}
function stopDebug() {
requestVModel.value.executeLoading = false;
emit('stopDebug');
}
@ -1097,6 +1122,14 @@
}
}
/**
* 替换步骤
* @param newStep 替换的新步骤
*/
function handleReplace(newStep: ScenarioStepItem) {
emit('replace', newStep);
}
watch(
() => visible.value,
async (val) => {

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@
</div>
<ms-base-table
v-bind="currentTable.propsRes.value"
v-model:selected-key="selectedKey"
no-disable
filter-icon-align-left
v-on="currentTable.propsEvent.value"
@ -87,7 +88,7 @@
import MsButton from '@/components/pure/ms-button/index.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 { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
@ -114,6 +115,9 @@
selectedCases: MsTableDataItem<ApiCaseDetail>[]; //
selectedScenarios: MsTableDataItem<ApiScenarioTableItem>[]; //
scenarioId?: string | number;
caseId?: string | number;
apiId?: string | number;
singleSelect?: boolean; //
}>();
const emit = defineEmits<{
(e: 'select', data: MsTableDataItem<ApiCaseDetail | ApiDefinitionDetail | ApiScenarioTableItem>[]): void;
@ -129,6 +133,7 @@
selectable: true,
showSelectorAll: false,
heightUsed: 300,
selectorType: props.singleSelect ? 'radio' : ('checkbox' as 'checkbox' | 'radio' | 'none' | undefined),
};
//
const useApiTable = useTable(getDefinitionPage, {
@ -357,6 +362,17 @@
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() {
tableSelectedData.value = [];
currentTable.value.clearSelector();
@ -399,7 +415,7 @@
status: statusFilters.value,
method: methodFilters.value,
},
excludeIds: [props.scenarioId || ''],
excludeIds: [props.scenarioId || '', props.caseId || '', props.apiId || ''],
});
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>
<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"
content-class="scenario-step-response-popover"
@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>
<div class="flex h-full flex-col">
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
@ -40,6 +52,13 @@
</div>
</template>
</a-popover>
<executeStatus
v-else-if="step.executeStatus"
:status="props.finalExecuteStatus"
:extra-text="getExecuteStatusExtraText(step)"
size="small"
class="ml-[4px]"
/>
</template>
<script lang="ts" setup>
@ -50,7 +69,12 @@
import { RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { ResponseComposition, ScenarioExecuteStatus, ScenarioStepType } from '@/enums/apiEnum';
import {
ResponseComposition,
ScenarioExecuteStatus,
ScenarioStepLoopTypeEnum,
ScenarioStepType,
} from '@/enums/apiEnum';
const responseResult = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/index.vue')
@ -59,6 +83,7 @@
const props = defineProps<{
step: ScenarioStepItem;
stepResponses: Record<string | number, Array<RequestResult>>;
finalExecuteStatus?: ScenarioExecuteStatus;
}>();
const emit = defineEmits(['visibleChange']);
@ -67,15 +92,27 @@
const currentLoop = ref(1);
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.uniqueId] && props.stepResponses[props.step.uniqueId].length > 0) {
//
return props.stepResponses[props.step.uniqueId].some((report) => !report.isSuccessful)
? ScenarioExecuteStatus.FAILED
: ScenarioExecuteStatus.SUCCESS;
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 (
[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 props.step.executeStatus;
});
return undefined;
}
</script>
<style lang="less">

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -152,7 +152,7 @@
<div
:class="`one-line-text mr-[4px] ${
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') }}
</div>
@ -190,27 +190,11 @@
</template>
<template #extraEnd="step">
<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-responses="scenario.stepResponses"
:final-execute-status="getExecuteStatus(step)"
@visible-change="handleResponsePopoverVisibleChange"
/>
<executeStatus
v-else-if="step.executeStatus"
:status="getExecuteStatus(step)"
:extra-text="getExecuteStatusExtraText(step)"
size="small"
class="ml-[4px]"
/>
</template>
<template v-if="steps.length === 0 && stepKeyword.trim() !== ''" #empty>
<div
@ -240,24 +224,30 @@
:request="currentStepDetail as unknown as RequestParam"
:file-params="currentStepFileParams"
:step="activeStep"
:scenario-id="scenario.id"
:step-responses="scenario.stepResponses"
:permission-map="permissionMap"
:steps="steps"
@add-step="addCustomApiStep"
@apply-step="applyApiStep"
@stop-debug="handleStopExecute(activeStep)"
@execute="handleApiExecute"
@replace="handleReplaceStep"
/>
<customCaseDrawer
v-model:visible="customCaseDrawerVisible"
:active-step="activeStep"
:request="currentStepDetail as unknown as RequestParam"
:scenario-id="scenario.id"
:file-params="currentStepFileParams"
:steps="steps"
:step-responses="scenario.stepResponses"
:permission-map="permissionMap"
@apply-step="applyApiStep"
@delete-step="deleteCaseStep(activeStep)"
@stop-debug="handleStopExecute(activeStep)"
@execute="(request, executeType) => handleApiExecute((request as unknown as RequestParam), executeType)"
@replace="handleReplaceStep"
/>
<importApiDrawer
v-if="importApiDrawerVisible"
@ -451,7 +441,6 @@
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTree from '@/components/business/ms-tree/index.vue';
import { MsTreeExpandedData, MsTreeNodeData } from '@/components/business/ms-tree/types';
import executeStatus from '../common/executeStatus.vue';
import { ImportData } from '../common/importApiDrawer/index.vue';
import stepType from '../common/stepType/stepType.vue';
import createStepActions from './createAction/createStepActions.vue';
@ -496,7 +485,6 @@
RequestDefinitionStatus,
ScenarioAddStepActionType,
ScenarioExecuteStatus,
ScenarioStepLoopTypeEnum,
ScenarioStepRefType,
ScenarioStepType,
} from '@/enums/apiEnum';
@ -569,32 +557,20 @@
function getExecuteStatus(step: ScenarioStepItem) {
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)
? ScenarioExecuteStatus.FAILED
: ScenarioExecuteStatus.SUCCESS;
if (scenario.value.stepResponses[step.uniqueId].some((report) => !report.isSuccessful)) {
return ScenarioExecuteStatus.FAILED;
}
return ScenarioExecuteStatus.SUCCESS;
}
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) {
if (visible) {
setFocusNodeKey(step.uniqueId);
@ -914,8 +890,8 @@
...cloneDeep(
mapTree<ScenarioStepItem>(node, (childNode) => {
const childId = getGenerateId();
const childStepDetail = stepDetails.value[node.id];
const childStepFileParam = scenario.value.stepFileParam[node.id];
const childStepDetail = stepDetails.value[childNode.id];
const childStepFileParam = scenario.value.stepFileParam[childNode.id];
let childCopyFromStepId = childNode.id;
if (childStepDetail) {
//
@ -923,7 +899,7 @@
}
if (childStepFileParam) {
//
scenario.value.stepFileParam[id] = cloneDeep(childStepFileParam);
scenario.value.stepFileParam[childNode.id] = cloneDeep(childStepFileParam);
}
if (!isQuoteScenario) {
// id
@ -1211,7 +1187,7 @@
const res = await debugScenario({
id: scenario.value.id || '',
grouped: false,
environmentId: currentEnvConfig?.value.id || '',
environmentId: currentEnvConfig?.value?.id || '',
projectId: appStore.currentProjectId,
scenarioConfig: scenario.value.scenarioConfig,
frontendDebug: executeType === 'localExec',
@ -1302,6 +1278,7 @@
} else {
//
const reportId = getGenerateId();
delete scenario.value.stepResponses[request.stepId]; //
request.executeLoading = true;
activeStep.value = {
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;
const offspringIds: string[] = [];
mapTree(dragNode.children || [], (e) => {
mapTree(cloneDeep(dragNode.children || []), (e) => {
offspringIds.push(e.uniqueId);
return e;
});

View File

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

View File

@ -17,7 +17,10 @@
</template>
</MsEditableTab>
<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
ref="executeButtonRef"
v-permission="['PROJECT_API_SCENARIO:READ+EXECUTE']"
@ -161,6 +164,7 @@
id: 'all',
label: t('apiScenario.allScenario'),
closable: false,
environmentId: '',
} as ScenarioParams,
]);
const activeScenarioTab = ref<ScenarioParams>(scenarioTabs.value[0] as ScenarioParams);
@ -195,7 +199,9 @@
...result,
console: data.taskResult.console,
});
if (result.isSuccessful) {
if (result.status === ScenarioExecuteStatus.FAKE_ERROR) {
scenario.executeFakeErrorCount += 1;
} else if (result.isSuccessful) {
scenario.executeSuccessCount += 1;
} else {
scenario.executeFailCount += 1;
@ -233,6 +239,7 @@
activeScenarioTab.value.executeTime = dayjs().format('YYYY-MM-DD HH:mm:ss');
activeScenarioTab.value.executeSuccessCount = 0;
activeScenarioTab.value.executeFailCount = 0;
activeScenarioTab.value.executeFakeErrorCount = 0;
activeScenarioTab.value.stepResponses = {};
activeScenarioTab.value.reportId = executeParams.reportId; // ID
activeScenarioTab.value.isDebug = !isExecute;
@ -403,6 +410,7 @@
scenarioTabs.value.push({
...cloneDeep(defaultScenario),
id: getGenerateId(),
environmentId: currentEnvConfig.value?.id || '',
label: `${t('apiScenario.createScenario')}${scenarioTabs.value.length}`,
moduleId: activeModule.value === 'all' ? 'root' : activeModule.value,
projectId: appStore.currentProjectId,

View File

@ -70,6 +70,7 @@ export default {
'apiScenario.executionResult': '执行结果',
'apiScenario.refreshRefScenario': '刷新引用场景数据',
'apiScenario.updateRefScenarioSuccess': '引用场景数据已更新',
'apiScenario.replaceSuccess': '步骤替换成功',
// 批量操作文案
'api_scenario.batch_operation.success': '成功{opt}至 {name}',
'api_scenario.table.batchMoveConfirm': '{opt}{count}个场景至已选模块',