feat(接口测试): 接口测试前端页面

This commit is contained in:
Jianguo-Genius 2024-10-18 15:08:16 +08:00 committed by Craftsman
parent a404376e51
commit f05e536cb3
9 changed files with 288 additions and 13 deletions

View File

@ -647,13 +647,16 @@ public class ApiScenarioDataTransferService {
} }
} }
}); });
List<ApiTestCaseWithBlob> apiTestCaseWithBlobs = extApiTestCaseMapper.selectAllDetailByApiIds(apiCaseIdList); List<ApiTestCaseWithBlob> apiTestCaseWithBlobs = null;
if (CollectionUtils.isNotEmpty(apiCaseIdList)) { if (CollectionUtils.isNotEmpty(apiCaseIdList)) {
apiTestCaseWithBlobs.forEach(item -> { apiTestCaseWithBlobs = extApiTestCaseMapper.selectAllDetailByApiIds(apiCaseIdList);
if (!apiDefinitionIdList.contains(item.getApiDefinitionId())) { if (CollectionUtils.isNotEmpty(apiCaseIdList)) {
apiDefinitionIdList.add(item.getApiDefinitionId()); apiTestCaseWithBlobs.forEach(item -> {
} if (!apiDefinitionIdList.contains(item.getApiDefinitionId())) {
}); apiDefinitionIdList.add(item.getApiDefinitionId());
}
});
}
} }
Map<String, Map<String, String>> projectApiModuleIdMap = new HashMap<>(); Map<String, Map<String, String>> projectApiModuleIdMap = new HashMap<>();

View File

@ -1,6 +1,7 @@
import type { CaseLevel } from '@/components/business/ms-case-associate/types'; import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import MSR from '@/api/http/index'; import MSR from '@/api/http/index';
import { ExportDefinitionUrl, StopApiExportUrl } from '@/api/requrls/api-test/management';
import { import {
AddModuleUrl, AddModuleUrl,
AddScenarioUrl, AddScenarioUrl,
@ -19,6 +20,7 @@ import {
ExecuteScenarioUrl, ExecuteScenarioUrl,
ExportScenarioUrl, ExportScenarioUrl,
FollowScenarioUrl, FollowScenarioUrl,
GetExportScenarioFileUrl,
GetModuleCountUrl, GetModuleCountUrl,
GetModuleTreeUrl, GetModuleTreeUrl,
GetScenarioBatchExportParamsUrl, GetScenarioBatchExportParamsUrl,
@ -44,6 +46,7 @@ import {
ScenarioTransferModuleOptionsUrl, ScenarioTransferModuleOptionsUrl,
ScenarioTrashPageUrl, ScenarioTrashPageUrl,
ScenarioUploadTempFileUrl, ScenarioUploadTempFileUrl,
StopExportScenarioUrl,
UpdateModuleUrl, UpdateModuleUrl,
UpdateScenarioPriorityUrl, UpdateScenarioPriorityUrl,
UpdateScenarioStatusUrl, UpdateScenarioStatusUrl,
@ -51,6 +54,7 @@ import {
} from '@/api/requrls/api-test/scenario'; } from '@/api/requrls/api-test/scenario';
import { ExecuteConditionProcessor } from '@/models/apiTest/common'; import { ExecuteConditionProcessor } from '@/models/apiTest/common';
import type { ApiDefinitionBatchExportParams } from '@/models/apiTest/management';
import { import {
ApiScenarioBatchDeleteParams, ApiScenarioBatchDeleteParams,
ApiScenarioBatchEditParams, ApiScenarioBatchEditParams,
@ -341,6 +345,22 @@ export function importScenario(params: ImportScenarioParams) {
} }
// 导出场景 // 导出场景
export function exportScenario(data: ExportScenarioParams) { export function exportScenario(data: ExportScenarioParams, type: string) {
return MSR.post({ url: ExportScenarioUrl, data }); return MSR.post({ url: `${ExportScenarioUrl}/${type}`, data });
}
// 停止导出场景
export function stopScenarioExport(taskId: string) {
return MSR.get({ url: `${StopExportScenarioUrl}/${taskId}` });
}
// 获取导出的文件
export function getScenarioDownloadFile(projectId: string, fileId: string) {
return MSR.get(
{
url: `${GetExportScenarioFileUrl}/${projectId}/${fileId}`,
responseType: 'blob',
},
{ isTransformResponse: false }
);
} }

View File

@ -28,9 +28,11 @@ export const BatchEditScenarioUrl = '/api/scenario/batch-operation/edit'; // 批
export const BatchRunScenarioUrl = '/api/scenario/batch-operation/run'; // 批量执行接口场景 export const BatchRunScenarioUrl = '/api/scenario/batch-operation/run'; // 批量执行接口场景
export const UpdateScenarioPriorityUrl = '/api/scenario/update-priority'; // 场景更新等级 export const UpdateScenarioPriorityUrl = '/api/scenario/update-priority'; // 场景更新等级
export const UpdateScenarioStatusUrl = '/api/scenario/update-status'; // 场景更新状态 export const UpdateScenarioStatusUrl = '/api/scenario/update-status'; // 场景更新状态
// 场景导入导出相关
export const ImportScenarioUrl = '/api/scenario/import'; // 导入场景 export const ImportScenarioUrl = '/api/scenario/import'; // 导入场景
export const ExportScenarioUrl = '/api/scenario/export'; // 导入场景 export const ExportScenarioUrl = '/api/scenario/export'; // 导出场景
export const StopExportScenarioUrl = '/api/scenario/stop';
export const GetExportScenarioFileUrl = '/api/scenario/download/file';
// 场景拖拽排序 // 场景拖拽排序
export const dragSortUrl = '/api/scenario/edit/pos'; export const dragSortUrl = '/api/scenario/edit/pos';
// 回收站相关 // 回收站相关

View File

@ -256,6 +256,12 @@ export enum ScenarioExecuteStatus {
UN_EXECUTE = 'UN_EXECUTE', UN_EXECUTE = 'UN_EXECUTE',
FAKE_ERROR = 'FAKE_ERROR', FAKE_ERROR = 'FAKE_ERROR',
} }
// 场景导出配置
export enum ScenarioExportType {
SIMPLE = 'METERSPHERE_SIMPLE',
ALL = 'METERSPHERE_ALL_DATA',
}
// 场景步骤类型 // 场景步骤类型
export enum ScenarioStepType { export enum ScenarioStepType {
API_CASE = 'API_CASE', // 接口用例 API_CASE = 'API_CASE', // 接口用例

View File

@ -108,7 +108,7 @@
async function downloadFile(id: string) { async function downloadFile(id: string) {
try { try {
const response = await getApiDownload.value(appStore.currentProjectId, id); const response = await getApiDownload.value(appStore.currentProjectId, id);
downloadByteFile(response, 'metersphere-export.json'); downloadByteFile(response, 'metersphere-definition.zip');
} catch (error) { } catch (error) {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.log(error); console.log(error);

View File

@ -0,0 +1,221 @@
<template>
<a-modal
v-model:visible="visible"
:title="t('common.export')"
title-align="start"
class="ms-modal-upload ms-modal-medium"
:width="400"
>
<a-radio-group v-model:model-value="exportTypeRadio">
<a-radio :value="ScenarioExportType.SIMPLE"
>{{ t('apiScenario.export.type.simple') }}
<a-tooltip :content="t('apiScenario.export.simple.tooltip')" position="tl">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</a-tooltip>
</a-radio>
<a-radio :value="ScenarioExportType.ALL">{{ t('apiScenario.export.type.all') }}</a-radio>
</a-radio-group>
<template #footer>
<div class="flex justify-end">
<a-button type="secondary" :disabled="exportLoading" @click="cancelExport">
{{ t('common.cancel') }}
</a-button>
<a-button class="ml-3" type="primary" :loading="exportLoading" @click="exportApi">
{{ t('common.export') }}
</a-button>
</div>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { Message } from '@arco-design/web-vue';
import MsButton from '@/components/pure/ms-button/index.vue';
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import { exportScenario, getScenarioDownloadFile, stopScenarioExport } from '@/api/modules/api-test/scenario';
import { useI18n } from '@/hooks/useI18n';
import useWebsocket from '@/hooks/useWebsocket';
import useAppStore from '@/store/modules/app';
import { downloadByteFile, getGenerateId } from '@/utils';
import { ScenarioExportType } from '@/enums/apiEnum';
const appStore = useAppStore();
const { t } = useI18n();
const props = defineProps<{
batchParams: BatchActionQueryParams;
conditionParams: Record<string, any> | (() => Record<string, any>);
sorter?: Record<string, any>;
isShare?: boolean; // id
}>();
const visible = defineModel<boolean>('visible', { required: true });
const exportLoading = ref(false);
const exportTypeRadio = ref(ScenarioExportType.SIMPLE);
function cancelExport() {
visible.value = false;
}
const websocket = ref<WebSocket>();
const reportId = ref('');
const isShowExportingMessage = ref(false); //
const exportingMessage = ref();
//
async function downloadFile(id: string) {
try {
const response = await getScenarioDownloadFile(appStore.currentProjectId, id);
downloadByteFile(response, 'metersphere-scenaro.zip');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
function showExportSuccessfulMessage(id: string, count: number) {
Message.success({
content: () =>
h('div', { class: 'flex flex-col gap-[8px] items-start' }, [
h('div', { class: 'font-medium' }, t('common.exportSuccessful')),
h('div', { class: 'flex items-center gap-[12px]' }, [
h('div', t('caseManagement.featureCase.exportApiCount', { number: count })),
h(
MsButton,
{
type: 'text',
onClick() {
downloadFile(id);
},
},
{ default: () => t('common.downloadFile') }
),
]),
]),
duration: 10000, // 10s
closable: true,
});
}
// websocket
async function startWebsocketGetExportResult() {
const { createSocket, websocket: _websocket } = useWebsocket({
reportId: reportId.value,
socketUrl: '/ws/export',
onMessage: (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
exportingMessage.value.close();
reportId.value = data.fileId;
// taskId.value = data.taskId;
if (data.isSuccessful) {
showExportSuccessfulMessage(reportId.value, data.count);
} else {
Message.error({
content: t('common.exportFailed'),
duration: 999999999, //
closable: true,
});
}
websocket.value?.close();
}
},
});
await createSocket();
websocket.value = _websocket.value;
}
//
async function stopExport(taskId: string) {
try {
await stopScenarioExport(taskId);
exportingMessage.value.close();
websocket.value?.close();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
//
function showExportingMessage(taskId: string) {
if (isShowExportingMessage.value) return;
isShowExportingMessage.value = true;
exportingMessage.value = Message.loading({
content: () =>
h('div', { class: 'flex items-center gap-[12px]' }, [
h('div', t('common.exporting')),
h(
MsButton,
{
type: 'text',
onClick() {
stopExport(taskId);
},
},
{ default: () => t('common.cancel') }
),
]),
duration: 999999999, //
closable: true,
onClose() {
isShowExportingMessage.value = false;
},
});
}
/**
* 导出接口
*/
async function exportApi() {
try {
exportLoading.value = true;
reportId.value = getGenerateId();
await startWebsocketGetExportResult();
const batchConditionParams =
typeof props.conditionParams === 'function' ? await props.conditionParams() : props.conditionParams;
const { selectedIds, selectAll, excludeIds } = props.batchParams;
const res = await exportScenario(
{
selectIds: selectedIds || [],
selectAll: !!selectAll,
excludeIds: excludeIds || [],
...batchConditionParams,
sort: props.sorter || {},
fileId: reportId.value,
},
exportTypeRadio.value
);
showExportingMessage(res);
visible.value = false;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
exportLoading.value = false;
}
}
</script>
<style scoped lang="less">
.import-item {
@apply flex cursor-pointer items-center bg-white;
padding: 8px;
width: 150px;
border: 1px solid var(--color-text-n8);
border-radius: var(--border-radius-small);
gap: 6px;
}
.import-item--active {
border: 1px solid rgb(var(--primary-5));
background-color: rgb(var(--primary-1));
}
</style>

View File

@ -465,6 +465,13 @@
is-scenario is-scenario
:report-id="tableRecord?.lastReportId || ''" :report-id="tableRecord?.lastReportId || ''"
/> />
<!-- 场景导出-->
<ScenarioExportModal
v-model:visible="showExportModal"
:batch-params="batchParams"
:condition-params="getBatchConditionParams"
:sorter="propsRes.sorter || {}"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -488,8 +495,10 @@
import type { MsTreeNodeData } from '@/components/business/ms-tree/types'; import type { MsTreeNodeData } from '@/components/business/ms-tree/types';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
import caseAndScenarioReportDrawer from '@/views/api-test/components/caseAndScenarioReportDrawer.vue'; import caseAndScenarioReportDrawer from '@/views/api-test/components/caseAndScenarioReportDrawer.vue';
import ApiExportModal from '@/views/api-test/management/components/management/api/apiExportModal.vue';
import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue'; import ExecutionStatus from '@/views/api-test/report/component/reportStatus.vue';
import BatchRunModal from '@/views/api-test/scenario/components/batchRunModal.vue'; import BatchRunModal from '@/views/api-test/scenario/components/batchRunModal.vue';
import ScenarioExportModal from '@/views/api-test/scenario/components/common/exportScenario/scenarioExportModal.vue';
import operationScenarioModuleTree from '@/views/api-test/scenario/components/operationScenarioModuleTree.vue'; import operationScenarioModuleTree from '@/views/api-test/scenario/components/operationScenarioModuleTree.vue';
import { getEnvList } from '@/api/modules/api-test/common'; import { getEnvList } from '@/api/modules/api-test/common';
@ -547,7 +556,7 @@
const appStore = useAppStore(); const appStore = useAppStore();
const cacheStore = useCacheStore(); const cacheStore = useCacheStore();
const showExportModal = ref(false);
const { t } = useI18n(); const { t } = useI18n();
const { openModal } = useModal(); const { openModal } = useModal();
const tableRecord = ref<ApiScenarioTableItem>(); const tableRecord = ref<ApiScenarioTableItem>();
@ -824,6 +833,11 @@
); );
const batchActions = { const batchActions = {
baseAction: [ baseAction: [
{
label: 'common.export',
eventTag: 'export',
permission: ['PROJECT_API_SCENARIO:READ+EXPORT'],
},
{ {
label: 'common.edit', label: 'common.edit',
eventTag: 'edit', eventTag: 'edit',
@ -1067,7 +1081,7 @@
const tableSelected = ref<(string | number)[]>([]); const tableSelected = ref<(string | number)[]>([]);
const batchParams = ref<BatchActionQueryParams>(); const batchParams = ref<BatchActionQueryParams>({ selectAll: false });
const batchOptionParams = ref<any>(); const batchOptionParams = ref<any>();
async function getBatchConditionParams() { async function getBatchConditionParams() {
const selectModules = await getModuleIds(); const selectModules = await getModuleIds();
@ -1445,6 +1459,9 @@
batchParams.value = { ...params }; batchParams.value = { ...params };
switch (event.eventTag) { switch (event.eventTag) {
case 'export':
showExportModal.value = true;
break;
case 'delete': case 'delete':
deleteScenario(undefined, true, batchParams.value); deleteScenario(undefined, true, batchParams.value);
break; break;

View File

@ -286,4 +286,7 @@ export default {
'apiScenario.csvQuote': 'CSV quote', 'apiScenario.csvQuote': 'CSV quote',
'apiScenario.csvNameNotNull': 'CSV name cannot be empty', 'apiScenario.csvNameNotNull': 'CSV name cannot be empty',
'apiScenario.csvFileNotNull': 'CSV file cannot be empty', 'apiScenario.csvFileNotNull': 'CSV file cannot be empty',
'apiScenario.export.type.simple': 'Simple',
'apiScenario.export.type.all': 'All data',
'apiScenario.export.simple.tooltip': 'Process referenced or copied request steps as custom requests',
}; };

View File

@ -280,4 +280,7 @@ export default {
'apiScenario.csvQuote': 'CSV 引用', 'apiScenario.csvQuote': 'CSV 引用',
'apiScenario.csvNameNotNull': 'CSV 名称不能为空', 'apiScenario.csvNameNotNull': 'CSV 名称不能为空',
'apiScenario.csvFileNotNull': 'CSV 文件不能为空', 'apiScenario.csvFileNotNull': 'CSV 文件不能为空',
'apiScenario.export.type.simple': '普通导出',
'apiScenario.export.type.all': '保留引用关系',
'apiScenario.export.simple.tooltip': '将引用或复制的请求步骤处理为自定义请求',
}; };