feat(接口测试): 批量执行场景前后端联调

This commit is contained in:
AgAngle 2024-03-21 20:58:16 +08:00 committed by jianxing
parent 364d8c9a18
commit 96b0df3330
7 changed files with 305 additions and 189 deletions

View File

@ -7,6 +7,7 @@ import {
BatchMoveScenarioUrl,
BatchRecoverScenarioUrl,
BatchRecycleScenarioUrl,
BatchRunScenarioUrl,
DeleteModuleUrl,
DeleteScenarioUrl,
ExecuteHistoryUrl,
@ -27,6 +28,7 @@ import {
import {
ApiScenarioBatchDeleteParams,
ApiScenarioBatchEditParams,
ApiScenarioBatchRunParams,
ApiScenarioGetModuleParams,
ApiScenarioModuleUpdateParams,
ApiScenarioPageParams,
@ -130,6 +132,11 @@ export function batchEditScenario(params: ApiScenarioBatchEditParams) {
return MSR.post({ url: BatchEditScenarioUrl, params });
}
// 批量编辑场景
export function batchRunScenario(params: ApiScenarioBatchRunParams) {
return MSR.post({ url: BatchRunScenarioUrl, params });
}
// 场景执行历史接口
export function getExecuteHistory(data: ExecutePageParams) {
return MSR.post<CommonList<ExecuteHistoryItem>>({ url: ExecuteHistoryUrl, data });

View File

@ -40,7 +40,7 @@ export const DefinitionReferenceUrl = '/api/definition/get-reference'; // 获取
* Mock
*/
export const DefinitionMockPageUrl = '/api/definition/mock/page'; // mock列表
export const UpdateMockStatusUrl = '/api/definition/mock/enable/'; // 更新mock状态
export const UpdateMockStatusUrl = '/api/definition/mock/enable'; // 更新mock状态
export const DeleteMockUrl = '/api/definition/mock/delete'; // 刪除mock
/**

View File

@ -11,6 +11,7 @@ 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'; // 批量复制接口场景
export const BatchEditScenarioUrl = '/api/scenario/batch-operation/edit'; // 批量编辑接口场景
export const BatchRunScenarioUrl = '/api/scenario/batch-operation/run'; // 批量执行接口场景
// 回收站相关
export const GetTrashModuleTreeUrl = '/api/scenario/module/trash/tree';

View File

@ -1,7 +1,7 @@
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import { ScenarioStepInfo } from '@/views/api-test/scenario/components/step/index.vue';
import { ApiDefinitionCustomField } from '@/models/apiTest/management';
import { ApiDefinitionCustomField, ApiRunModeRequest } from '@/models/apiTest/management';
import { ApiScenarioStatus, RequestComposition, RequestDefinitionStatus } from '@/enums/apiEnum';
import { BatchApiParams, TableQueryParams } from '../common';
@ -104,13 +104,22 @@ export interface BatchOptionParams extends BatchApiParams {
refId?: string;
}
// 场景批量编辑参数
export interface ApiScenarioBatchParams extends BatchApiParams {
projectId?: string;
moduleIds?: string[];
apiScenarioId?: string;
versionId?: string;
refId?: string;
}
// 批量移动场景参数
export interface ApiScenarioBatchMoveParams extends BatchOptionParams {
export interface ApiScenarioBatchMoveParams extends ApiScenarioBatchParams {
targetModuleId: string | number;
}
// 批量编辑场景参数
export interface ApiScenarioBatchEditParams extends BatchOptionParams {
export interface ApiScenarioBatchEditParams extends ApiScenarioBatchParams {
// 修改操作的类型
type?: string;
@ -130,8 +139,14 @@ export interface ApiScenarioBatchEditParams extends BatchOptionParams {
priority?: string;
}
// 批量编辑场景参数
export interface ApiScenarioBatchRunParams extends ApiScenarioBatchParams {
// 运行模式配置
runModeConfig?: ApiRunModeRequest;
}
// 批量删除场景参数
export interface ApiScenarioBatchDeleteParams extends BatchApiParams {
export interface ApiScenarioBatchDeleteParams extends ApiScenarioBatchParams {
deleteAll: boolean;
}

View File

@ -0,0 +1,232 @@
<template>
<a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
<template #title>
{{ t('report.trigger.batch.execution') }}
<div class="text-[var(--color-text-4)]">
{{
t('case.batchModalSubTitle', {
count: props.batchParams?.currentSelectCount || tableSelected.length,
})
}}
</div>
</template>
<a-form ref="batchExecuteFormRef" class="rounded-[4px]" :model="batchExecuteForm" layout="vertical">
<a-form-item field="defaultEnv" :label="t('case.execute.selectEnv')">
<a-radio-group v-model="batchExecuteForm.defaultEnv">
<a-radio value="true"
>{{ t('case.execute.defaultEnv') }}
<a-tooltip :content="t('case.execute.defaultEnvTip')" position="top">
<icon-question-circle
class="text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</a-radio>
<a-radio value="false">{{ t('case.execute.newEnv') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="batchExecuteForm.defaultEnv == 'false'"
field="environmentId"
:label="t('case.execute.newEnv')"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
asterisk-position="end"
required
>
<a-select v-model="batchExecuteForm.environmentId" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of environmentList" :key="item.id" :value="item.id">
{{ t(item.name) }}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="runMode" :label="t('case.execute.model')">
<a-radio-group v-model="batchExecuteForm.runMode">
<a-radio value="SERIAL">{{ t('case.execute.serial') }}</a-radio>
<a-radio value="PARALLEL">{{ t('case.execute.parallel') }}</a-radio>
</a-radio-group>
</a-form-item>
<div v-if="batchExecuteForm.runMode == 'SERIAL'" class="ms-switch">
<a-switch
v-model="batchExecuteForm.stopOnFailure"
type="line"
class="ms-form-table-input-switch execute-form-table-input-switch"
size="small"
/>
<span class="ml-3 font-normal text-[var(--color-text-1)]">{{ t('case.execute.StopOnFailure') }}</span>
</div>
<a-form-item field="integratedReport" :label="t('case.execute.reportSetting')">
<a-radio-group v-model="batchExecuteForm.integratedReport" type="button">
<a-radio value="false">{{ t('case.execute.independentReporting') }}</a-radio>
<a-radio value="true">{{ t('case.execute.CollectionReport') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="batchExecuteForm.integratedReport == 'true'"
field="integratedReport"
:label="t('case.execute.reportName')"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
:validate-trigger="['blur', 'input']"
>
<a-input
v-model="batchExecuteForm.integratedReportName"
:max-length="255"
:placeholder="t('formCreate.PleaseEnter')"
/>
</a-form-item>
<a-form-item field="poolId" :label="t('case.execute.pool')">
<a-select v-model="batchExecuteForm.poolId" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of resourcePoolList" :key="item.id" :value="item.id">
{{ t(item.name) }}
</a-option>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<a-button type="secondary" :disabled="batchExecuteLoading" @click="cancelBatchExecute">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="batchExecuteLoading" @click="handleBatchExecuteCase">
{{ t('system.log.operateType.execute') }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { FormInstance, Message } from '@arco-design/web-vue';
import { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import { getEnvList, getPoolId, getPoolOption } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import useAppStore from '@/store/modules/app';
import { Environment } from '@/models/apiTest/management';
import { ResourcePoolItem } from '@/models/setting/resourcePool';
const { t } = useI18n();
const batchExecuteFormRef = ref<FormInstance>();
const batchExecuteForm = ref({
defaultEnv: 'true',
runMode: 'SERIAL',
integratedReport: 'false',
integratedReportName: '',
stopOnFailure: false,
poolId: '',
grouped: false,
environmentId: '',
});
const environmentList = ref<Environment[]>();
const resourcePoolList = ref<ResourcePoolItem[]>();
const defaultPoolId = ref<string>();
const appStore = useAppStore();
const showBatchExecute = ref(false);
const props = withDefaults(
defineProps<{
tableSelected: (string | number)[];
visible: boolean;
batchParams?: BatchActionQueryParams;
batchConditionParams: any;
batchRunFunc: (a: any) => Promise<any>;
}>(),
{
visible: false,
}
);
//
async function initPoolList() {
resourcePoolList.value = await getPoolOption(appStore.currentProjectId);
}
//
async function getDefaultPoolId() {
try {
defaultPoolId.value = await getPoolId(appStore.currentProjectId);
batchExecuteForm.value.poolId = defaultPoolId.value || '';
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
async function initEnvList() {
environmentList.value = await getEnvList(appStore.currentProjectId);
}
const batchExecuteLoading = ref(false);
const emit = defineEmits(['update:visible', 'finished']);
function cancelBatchExecute() {
showBatchExecute.value = false;
}
function handleBatchExecuteCase() {
batchExecuteFormRef.value?.validate(async (errors) => {
const { batchParams } = props;
const { batchConditionParams } = props;
if (!errors) {
try {
batchExecuteLoading.value = true;
await props?.batchRunFunc({
selectIds: batchParams?.selectedIds || [],
selectAll: !!batchParams?.selectAll,
excludeIds: batchParams?.excludeIds || [],
projectId: appStore.currentProjectId,
...batchConditionParams,
runModeConfig: {
runMode: batchExecuteForm.value.runMode,
integratedReport: batchExecuteForm.value.integratedReport === 'true',
integratedReportName: batchExecuteForm.value.integratedReportName,
stopOnFailure: batchExecuteForm.value.stopOnFailure,
poolId: batchExecuteForm.value.poolId,
grouped: batchExecuteForm.value.grouped,
environmentId: batchExecuteForm.value.environmentId,
},
apiDefinitionId: '',
versionId: '',
refId: '',
});
Message.success(t('case.detail.execute.success'));
cancelBatchExecute();
emit('finished');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
batchExecuteLoading.value = false;
}
}
});
}
watch(
() => props.visible,
(val) => {
if (val) {
showBatchExecute.value = true;
initEnvList();
initPoolList();
getDefaultPoolId();
}
},
{
immediate: true,
}
);
watch(
() => showBatchExecute.value,
(val) => {
emit('update:visible', val);
}
);
</script>
<style scoped lang="less"></style>

View File

@ -270,97 +270,14 @@
@load-case="(id: string) => loadCase(id)"
@delete-case="deleteCaseByDetail"
/>
<a-modal v-model:visible="showBatchExecute" title-align="start" class="ms-modal-upload ms-modal-medium" :width="480">
<template #title>
{{ t('report.trigger.batch.execution') }}
<div class="text-[var(--color-text-4)]">
{{
t('case.batchModalSubTitle', {
count: batchParams.currentSelectCount || tableSelected.length,
})
}}
</div>
</template>
<a-form ref="batchExecuteFormRef" class="rounded-[4px]" :model="batchExecuteForm" layout="vertical">
<a-form-item field="defaultEnv" :label="t('case.execute.selectEnv')">
<a-radio-group v-model="batchExecuteForm.defaultEnv">
<a-radio value="true"
>{{ t('case.execute.defaultEnv') }}
<a-tooltip :content="t('case.execute.defaultEnvTip')" position="top">
<icon-question-circle
class="text-[var(--color-text-brand)] hover:text-[rgb(var(--primary-5))]"
size="16"
/> </a-tooltip
></a-radio>
<a-radio value="false">{{ t('case.execute.newEnv') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="batchExecuteForm.defaultEnv == 'false'"
field="environmentId"
:label="t('case.execute.newEnv')"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
asterisk-position="end"
required
>
<a-select v-model="batchExecuteForm.environmentId" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of environmentList" :key="item.id" :value="item.id">
{{ t(item.name) }}
</a-option>
</a-select>
</a-form-item>
<a-form-item field="runMode" :label="t('case.execute.model')">
<a-radio-group v-model="batchExecuteForm.runMode">
<a-radio value="SERIAL">{{ t('case.execute.serial') }}</a-radio>
<a-radio value="PARALLEL">{{ t('case.execute.parallel') }}</a-radio>
</a-radio-group>
</a-form-item>
<div v-if="batchExecuteForm.runMode == 'SERIAL'" class="ms-switch">
<a-switch
v-model="batchExecuteForm.stopOnFailure"
type="line"
class="ms-form-table-input-switch execute-form-table-input-switch"
size="small"
/>
<span class="ml-3 font-normal text-[var(--color-text-1)]">{{ t('case.execute.StopOnFailure') }}</span>
</div>
<a-form-item field="integratedReport" :label="t('case.execute.reportSetting')">
<a-radio-group v-model="batchExecuteForm.integratedReport" type="button">
<a-radio value="false">{{ t('case.execute.independentReporting') }}</a-radio>
<a-radio value="true">{{ t('case.execute.CollectionReport') }}</a-radio>
</a-radio-group>
</a-form-item>
<a-form-item
v-if="batchExecuteForm.integratedReport == 'true'"
field="integratedReport"
:label="t('case.execute.reportName')"
:rules="[{ required: true, message: t('apiTestManagement.valueRequired') }]"
:validate-trigger="['blur', 'input']"
>
<a-input
v-model="batchExecuteForm.integratedReportName"
:max-length="255"
:placeholder="t('formCreate.PleaseEnter')"
/>
</a-form-item>
<a-form-item field="poolId" :label="t('case.execute.pool')">
<a-select v-model="batchExecuteForm.poolId" :placeholder="t('common.pleaseSelect')">
<a-option v-for="item of resourcePoolList" :key="item.id" :value="item.id">
{{ t(item.name) }}
</a-option>
</a-select>
</a-form-item>
</a-form>
<template #footer>
<a-button type="secondary" :disabled="batchExecuteLoading" @click="cancelBatchExecute">
{{ t('common.cancel') }}
</a-button>
<a-button type="primary" :loading="batchExecuteLoading" @click="handleBatchExecuteCase">
{{ t('system.log.operateType.execute') }}
</a-button>
</template>
</a-modal>
<batchRunModal
v-model:visible="showBatchExecute"
:batch-condition-params="batchConditionParams"
:batch-params="batchParams"
:table-selected="tableSelected"
:batch-run-func="batchExecuteCase"
@finished="loadCaseListAndResetSelector"
/>
</template>
<script setup lang="ts">
@ -379,6 +296,7 @@
import caseDetailDrawer from './caseDetailDrawer.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import BatchRunModal from '@/views/api-test/components/batchRunModal.vue';
import {
batchDeleteCase,
@ -389,9 +307,6 @@
executeCase,
getCaseDetail,
getCasePage,
getEnvList,
getPoolId,
getPoolOption,
updateCasePriority,
updateCaseStatus,
} from '@/api/modules/api-test/management';
@ -402,9 +317,8 @@
import useAppStore from '@/store/modules/app';
import { hasAnyPermission } from '@/utils/permission';
import { ApiCaseDetail, Environment } from '@/models/apiTest/management';
import { ApiCaseDetail } from '@/models/apiTest/management';
import { DragSortParams } from '@/models/common';
import { ResourcePoolItem } from '@/models/setting/resourcePool';
import { RequestDefinitionStatus } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
@ -615,11 +529,6 @@
const lastReportStatusList = ['error', 'FakeError', 'success'];
const lastReportStatusFilters = ref<string[]>([...lastReportStatusList]);
const environmentList = ref<Environment[]>();
const resourcePoolList = ref<ResourcePoolItem[]>();
const defaultPoolId = ref<string>();
async function getModuleIds() {
let moduleIds: string[] = [];
if (props.activeModule !== 'all') {
@ -661,30 +570,9 @@
caseFilters.value = caseLevelFields.value?.options.map((item: any) => item.text);
}
//
async function initEnvList() {
environmentList.value = await getEnvList(appStore.currentProjectId);
}
//
async function initPoolList() {
resourcePoolList.value = await getPoolOption(appStore.getCurrentProjectId);
}
async function getDefaultPoolId() {
try {
defaultPoolId.value = await getPoolId(appStore.getCurrentProjectId);
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
onBeforeMount(() => {
loadCaseList();
initPoolList();
getCaseLevelFields();
getDefaultPoolId();
});
function handleFilterHidden(val: boolean) {
@ -830,24 +718,13 @@
//
const showBatchExecute = ref(false);
const batchEditLoading = ref(false);
const batchExecuteLoading = ref(false);
const batchFormRef = ref<FormInstance>();
const batchForm = ref({
attr: '',
value: '',
values: [],
});
const batchExecuteFormRef = ref<FormInstance>();
const batchExecuteForm = ref({
defaultEnv: 'true',
runMode: 'SERIAL',
integratedReport: 'false',
integratedReportName: '',
stopOnFailure: false,
poolId: '',
grouped: false,
environmentId: '',
});
const attrOptions = [
{
name: 'case.caseLevel',
@ -937,51 +814,7 @@
});
}
function cancelBatchExecute() {
showBatchExecute.value = false;
batchFormRef.value?.resetFields();
batchForm.value = {
attr: '',
value: '',
values: [],
};
}
function handleBatchExecuteCase() {
batchExecuteFormRef.value?.validate(async (errors) => {
if (!errors) {
try {
batchExecuteLoading.value = true;
const batchConditionParams = await genBatchConditionParams();
await batchExecuteCase({
selectIds: batchParams.value?.selectedIds || [],
selectAll: !!batchParams.value?.selectAll,
excludeIds: batchParams.value?.excludeIds || [],
...batchConditionParams,
runModeConfig: {
runMode: batchExecuteForm.value.runMode,
integratedReport: batchExecuteForm.value.integratedReport === 'true',
integratedReportName: batchExecuteForm.value.integratedReportName,
stopOnFailure: batchExecuteForm.value.stopOnFailure,
poolId: batchExecuteForm.value.poolId,
grouped: batchExecuteForm.value.grouped,
environmentId: batchExecuteForm.value.environmentId,
},
apiDefinitionId: '',
versionId: '',
refId: '',
});
Message.success(t('case.detail.execute.success'));
cancelBatchExecute();
loadCaseListAndResetSelector();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
batchExecuteLoading.value = false;
}
}
});
}
const batchConditionParams = ref<any>();
//
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
@ -995,9 +828,10 @@
showBatchEditModal.value = true;
break;
case 'execute':
showBatchExecute.value = true;
batchExecuteForm.value.poolId = defaultPoolId.value || '';
initEnvList();
genBatchConditionParams().then((data) => {
batchConditionParams.value = data;
showBatchExecute.value = true;
});
break;
default:
break;

View File

@ -275,6 +275,14 @@
@folder-node-select="folderNodeSelect"
/>
</a-modal>
<batchRunModal
v-model:visible="showBatchExecute"
:batch-condition-params="batchConditionParams"
:batch-params="batchParams"
:table-selected="tableSelected"
:batch-run-func="batchRunScenario"
@finished="loadScenarioList"
/>
</template>
<script setup lang="ts">
@ -289,10 +297,10 @@
import MsTableMoreAction from '@/components/pure/ms-table-more-action/index.vue';
import { ActionsItem } from '@/components/pure/ms-table-more-action/types';
import MsTagsInput from '@/components/pure/ms-tags-input/index.vue';
import caseLevel from '@/components/business/ms-case-associate/caseLevel.vue';
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiStatus from '@/views/api-test/components/apiStatus.vue';
import BatchRunModal from '@/views/api-test/components/batchRunModal.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import operationScenarioModuleTree from '@/views/api-test/scenario/components/operationScenarioModuleTree.vue';
@ -300,6 +308,7 @@
batchEditScenario,
batchOptionScenario,
batchRecycleScenario,
batchRunScenario,
getScenarioPage,
recycleScenario,
updateScenario,
@ -351,6 +360,7 @@
const moveModalVisible = ref(false);
const isBatchMove = ref(false); //
const isBatchCopy = ref(false); //
const showBatchExecute = ref(false);
let columns: MsTableColumn = [
{
@ -540,6 +550,23 @@
const statusFilters = ref(Object.keys(ApiScenarioStatus));
const tableStore = useTableStore();
const activeModules = computed(() => {
return props.activeModule === 'all' ? [] : [props.activeModule];
});
const batchConditionParams = computed(() => {
return {
condition: {
keyword: keyword.value,
filter: {
status: statusFilters.value,
},
},
projectId: appStore.currentProjectId,
moduleIds: activeModules.value,
};
});
async function loadScenarioList(refreshTreeCount?: boolean) {
let moduleIds: string[] = [];
if (props.activeModule && props.activeModule !== 'all') {
@ -873,7 +900,7 @@
moveModalVisible.value = true;
break;
case 'execute':
Message.info('// todo @ba1q1');
showBatchExecute.value = true;
break;
default:
break;