feat(接口测试): 用例列表同步&对比table抽屉Drawer

This commit is contained in:
xinxin.wu 2024-08-01 11:14:49 +08:00 committed by Craftsman
parent 122c54691a
commit 1f77a8db41
7 changed files with 1138 additions and 6 deletions

View File

@ -12,6 +12,7 @@
props.disabled ? 'ms-form-table--disabled' : '', props.disabled ? 'ms-form-table--disabled' : '',
]" ]"
bordered bordered
:row-class="rowClass"
v-on="propsEvent" v-on="propsEvent"
@drag-change="tableChange" @drag-change="tableChange"
@init-end="validateAndUpdateErrorMessageList" @init-end="validateAndUpdateErrorMessageList"
@ -294,6 +295,7 @@
disabled?: boolean; // disabled?: boolean; //
showSelectorAll?: boolean; // showSelectorAll?: boolean; //
rowSelection?: TableRowSelection; rowSelection?: TableRowSelection;
diffMode?: 'add' | 'delete';
spanMethod?: (data: { spanMethod?: (data: {
record: TableData; record: TableData;
column: TableColumnData | TableOperationColumn; column: TableColumnData | TableOperationColumn;
@ -537,6 +539,18 @@
emit('selectAll', checked); emit('selectAll', checked);
} }
function rowClass(record: TableData, rowIndex: number) {
if (record.diff) {
if (props.diffMode === 'add') {
return 'add-row-class';
}
if (props.diffMode === 'delete') {
return 'delete-row-class';
}
}
return '';
}
defineExpose({ defineExpose({
validateAndUpdateErrorMessageList, validateAndUpdateErrorMessageList,
}); });
@ -719,4 +733,20 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
} }
:deep(.add-row-class) {
.arco-table-td {
background: rgb(var(--success-1));
.arco-table-td-content {
background: rgb(var(--success-1));
}
}
}
:deep(.delete-row-class) {
.arco-table-td {
background: rgb(var(--danger-1));
.arco-table-td-content {
background: rgb(var(--danger-1));
}
}
}
</style> </style>

View File

@ -47,9 +47,26 @@
</div> </div>
</template> </template>
<template #num="{ record }"> <template #num="{ record }">
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)"> <div class="flex items-center">
{{ record.num }} <MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">
</MsButton> {{ record.num }}
</MsButton>
<!-- TODO 后台缺少字段 等待联调 -->
<a-tooltip v-if="record.apiChange" class="ms-tooltip-white">
<!-- 接口参数发生变更提示 -->
<MsIcon type="icon-icon_warning_colorful" size="16" />
<template #content>
<div class="flex flex-row">
<span class="text-[var(--color-text-1)]">
{{ t('case.apiParamsHasChange') }}
</span>
<MsButton class="ml-[8px]" @click="showDifferences(record)">
{{ t('case.changeDifferences') }}
</MsButton>
</div>
</template>
</a-tooltip>
</div>
</template> </template>
<template #protocol="{ record }"> <template #protocol="{ record }">
<apiMethodName :method="record.protocol" /> <apiMethodName :method="record.protocol" />
@ -281,6 +298,17 @@
/> />
<!-- 执行结果抽屉 --> <!-- 执行结果抽屉 -->
<caseAndScenarioReportDrawer v-model:visible="showExecuteResult" :report-id="activeReportId" /> <caseAndScenarioReportDrawer v-model:visible="showExecuteResult" :report-id="activeReportId" />
<!-- 同步抽屉 -->
<SyncModal v-model:visible="showSyncModal" :batch-params="batchParams" />
<!-- diff对比抽屉 -->
<DifferentDrawer
v-model:visible="showDifferentDrawer"
:detail="caseDetail as RequestParam"
:api-detail="apiDetail as RequestParam"
:active-api-case-id="activeApiCaseId"
:active-defined-id="activeDefinedId"
@close="closeDifferent"
/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -299,6 +327,8 @@
import type { CaseLevel } from '@/components/business/ms-case-associate/types'; import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import caseDetailDrawer from './caseDetailDrawer.vue'; import caseDetailDrawer from './caseDetailDrawer.vue';
import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue'; import createAndEditCaseDrawer from './createAndEditCaseDrawer.vue';
import DifferentDrawer from './differentDrawer.vue';
import SyncModal from './syncModal.vue';
import apiMethodName from '@/views/api-test/components/apiMethodName.vue'; import apiMethodName from '@/views/api-test/components/apiMethodName.vue';
import apiStatus from '@/views/api-test/components/apiStatus.vue'; import apiStatus from '@/views/api-test/components/apiStatus.vue';
import BatchRunModal from '@/views/api-test/components/batchRunModal.vue'; import BatchRunModal from '@/views/api-test/components/batchRunModal.vue';
@ -390,8 +420,7 @@
sorter: true, sorter: true,
}, },
fixed: 'left', fixed: 'left',
width: 130, width: 150,
showTooltip: true,
columnSelectorDisabled: true, columnSelectorDisabled: true,
}, },
{ {
@ -568,6 +597,11 @@
eventTag: 'execute', eventTag: 'execute',
permission: ['PROJECT_API_DEFINITION_CASE:READ+EXECUTE'], permission: ['PROJECT_API_DEFINITION_CASE:READ+EXECUTE'],
}, },
{
label: 'case.apiSyncChange',
eventTag: 'sync',
permission: ['PROJECT_API_DEFINITION_CASE:READ+UPDATE'],
},
{ {
label: 'common.delete', label: 'common.delete',
eventTag: 'delete', eventTag: 'delete',
@ -820,9 +854,14 @@
} }
}); });
} }
const batchConditionParams = ref<any>(); const batchConditionParams = ref<any>();
const showSyncModal = ref<boolean>(false);
// TODO
function syncParams() {
showSyncModal.value = true;
}
// //
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) { function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
tableSelected.value = params?.selectedIds || []; tableSelected.value = params?.selectedIds || [];
@ -840,6 +879,9 @@
showBatchExecute.value = true; showBatchExecute.value = true;
}); });
break; break;
case 'sync':
syncParams();
break;
default: default:
break; break;
} }
@ -920,6 +962,23 @@
showExecuteResult.value = true; showExecuteResult.value = true;
} }
const activeApiCaseId = ref<string>('');
const activeDefinedId = ref<string>('');
const showDifferentDrawer = ref<boolean>(false);
// TODO
async function showDifferences(record: ApiCaseDetail) {
activeApiCaseId.value = record.id;
activeDefinedId.value = record.apiDefinitionId;
showDifferentDrawer.value = true;
}
// TODO
function closeDifferent() {
showDifferentDrawer.value = false;
activeApiCaseId.value = '';
activeDefinedId.value = '';
}
defineExpose({ defineExpose({
loadCaseList, loadCaseList,
}); });

View File

@ -0,0 +1,470 @@
<template>
<div v-if="showDiff(RequestComposition.HEADER)" class="title">{{ t('apiTestDebug.header') }}</div>
<div
v-if="showDiff(RequestComposition.HEADER) && hiddenEmptyTable(RequestComposition.HEADER)"
:style="{ 'padding-bottom': `${getBottomDistance(RequestComposition.HEADER)}px` }"
>
<MsFormTable
:columns="headerColumns"
:data="previewDetail?.headers?.filter((e) => e.key !== '') || []"
:selectable="false"
:diff-mode="props.mode"
/>
</div>
<div
v-if="showDiff(RequestComposition.HEADER) && !hiddenEmptyTable(RequestComposition.HEADER)"
class="not-setting-data"
:style="{ height: `${getBottomDistance(RequestComposition.HEADER, true)}px` }"
>
{{ t('case.notSetData') }}
</div>
<div v-if="showDiff(RequestComposition.QUERY)" class="title">Query</div>
<div
v-if="showDiff(RequestComposition.QUERY) && hiddenEmptyTable(RequestComposition.QUERY)"
:style="{ 'padding-bottom': `${getBottomDistance(RequestComposition.QUERY)}px` }"
>
<MsFormTable
:columns="queryRestColumns"
:data="previewDetail?.query?.filter((e) => e.key !== '') || []"
:selectable="false"
:diff-mode="props.mode"
/>
</div>
<div
v-if="showDiff(RequestComposition.QUERY) && !hiddenEmptyTable(RequestComposition.QUERY)"
class="not-setting-data"
:style="{ height: `${getBottomDistance(RequestComposition.QUERY, true)}px` }"
>
{{ t('case.notSetData') }}
</div>
<div v-if="showDiff(RequestComposition.REST)" class="title">REST</div>
<div
v-if="showDiff(RequestComposition.REST)"
:style="{ 'padding-bottom': `${getBottomDistance(RequestComposition.REST)}px` }"
>
<MsFormTable
:columns="queryRestColumns?.filter((e) => e.key !== '')"
:data="previewDetail?.rest || []"
:selectable="false"
:diff-mode="props.mode"
/>
</div>
<div
v-if="showDiff(RequestComposition.REST) && !hiddenEmptyTable(RequestComposition.REST)"
class="not-setting-data"
:style="{ height: `${getBottomDistance(RequestComposition.REST, true)}px` }"
>
{{ t('case.notSetData') }}
</div>
<div class="title flex items-center justify-between">
<div class="detail-item-title-text">
{{ `${t('apiTestManagement.requestBody')}-${previewDetail?.body?.bodyType}` }}
</div>
<a-radio-group
v-if="previewDetail?.body?.bodyType === RequestBodyFormat.JSON && props.isApi"
v-model:model-value="bodyShowType"
type="button"
size="mini"
>
<a-radio value="schema">Schema</a-radio>
<a-radio value="json">JSON</a-radio>
</a-radio-group>
</div>
<div
v-if="
(previewDetail?.body?.bodyType === RequestBodyFormat.FORM_DATA ||
previewDetail?.body?.bodyType === RequestBodyFormat.WWW_FORM) &&
showDiff(previewDetail?.body?.bodyType) &&
hiddenEmptyTable(previewDetail?.body?.bodyType)
"
:style="{ 'padding-bottom': `${getBottomDistance(previewDetail.value?.body?.bodyType)}px` }"
>
<MsFormTable
:columns="bodyColumns"
:data="bodyTableData"
:selectable="false"
:show-setting="true"
:table-key="TableKeyEnum.API_TEST_DEBUG_FORM_DATA"
:diff-mode="props.mode"
/>
</div>
<div
v-if="showDiff(previewDetail?.body?.bodyType) && !hiddenEmptyTable(previewDetail?.body?.bodyType)"
class="not-setting-data"
:style="{ height: `${getBottomDistance(previewDetail?.body?.bodyType, true)}px` }"
>
{{ t('case.notSetData') }}
</div>
<template
v-else-if="
[RequestBodyFormat.JSON, RequestBodyFormat.RAW, RequestBodyFormat.XML].includes(previewDetail?.body?.bodyType)
"
>
<MsJsonSchema
v-if="previewDetail?.body?.bodyType === RequestBodyFormat.JSON && bodyShowType === 'schema' && props.isApi"
:data="previewDetail.body.jsonBody.jsonSchemaTableData"
disabled
/>
<MsCodeEditor
v-else
:model-value="bodyCode"
theme="vs"
height="200px"
:language="bodyCodeLanguage"
:show-full-screen="false"
:show-theme-change="false"
read-only
>
<template #rightTitle>
<a-button
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="copyScript(bodyCode)"
>
<template #icon>
<MsIcon type="icon-icon_copy_outlined" class="text-var(--color-text-4)" size="12" />
</template>
</a-button>
</template>
</MsCodeEditor>
</template>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { useClipboard } from '@vueuse/core';
import { Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsCodeEditor from '@/components/pure/ms-code-editor/index.vue';
import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsFormTable, { FormTableColumn } from '@/components/pure/ms-form-table/index.vue';
import MsJsonSchema from '@/components/pure/ms-json-schema/index.vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestBodyFormat, RequestComposition, RequestParamsType } from '@/enums/apiEnum';
import { TableKeyEnum } from '@/enums/tableEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
const { copy, isSupported } = useClipboard({ legacy: true });
const { t } = useI18n();
const props = defineProps<{
diffDistanceMap: Record<string, any>; //
detail: RequestParam;
mode: 'add' | 'delete';
isApi?: boolean;
}>();
const previewDetail = ref<RequestParam>(props.detail);
/**
* 请求头
*/
const headerColumns: FormTableColumn[] = [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
width: 220,
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
width: 220,
},
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
},
];
/**
* Query & Rest
*/
const queryRestColumns: FormTableColumn[] = [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
width: 220,
columnSelectorDisabled: true,
},
{
title: 'apiTestManagement.required',
dataIndex: 'required',
slotName: 'required',
inputType: 'text',
columnSelectorDisabled: true,
valueFormat: (record) => {
return record.required ? t('common.yes') : t('common.no');
},
width: 68,
},
{
title: 'apiTestDebug.paramType',
dataIndex: 'paramType',
inputType: 'text',
width: 96,
columnSelectorDisabled: true,
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
},
{
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
inputType: 'text',
showInTable: false,
valueFormat: (record) => {
return [null, undefined].includes(record.minLength) && [null, undefined].includes(record.maxLength)
? '-'
: `${record.minLength} ${t('common.to')} ${record.maxLength}`;
},
width: 110,
},
{
title: 'apiTestDebug.encode',
dataIndex: 'encode',
slotName: 'encode',
inputType: 'text',
showInTable: false,
valueFormat: (record) => {
return record.encode ? t('common.yes') : t('common.no');
},
width: 68,
},
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
},
{
title: '',
dataIndex: 'operation',
slotName: 'operation',
fixed: 'right',
width: 100,
},
];
const bodyShowType = ref('schema');
/**
* 请求体
*/
const bodyColumns = computed<FormTableColumn[]>(() => {
if ([RequestBodyFormat.FORM_DATA, RequestBodyFormat.WWW_FORM].includes(previewDetail.value?.body?.bodyType)) {
return [
{
title: 'apiTestManagement.paramName',
dataIndex: 'key',
inputType: 'text',
width: 220,
columnSelectorDisabled: true,
},
{
title: 'apiTestManagement.required',
dataIndex: 'required',
slotName: 'required',
inputType: 'text',
columnSelectorDisabled: true,
valueFormat: (record) => {
return record.required ? t('common.yes') : t('common.no');
},
width: 68,
},
{
title: 'apiTestManagement.paramsType',
dataIndex: 'paramType',
inputType: 'text',
width: 96,
columnSelectorDisabled: true,
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
showTooltip: true,
},
{
title: 'apiTestDebug.paramLengthRange',
dataIndex: 'lengthRange',
slotName: 'lengthRange',
inputType: 'text',
showInTable: false,
valueFormat: (record) => {
return [null, undefined].includes(record.minLength) && [null, undefined].includes(record.maxLength)
? '-'
: `${record.minLength} ${t('common.to')} ${record.maxLength}`;
},
width: 110,
},
{
title: 'apiTestDebug.encode',
dataIndex: 'encode',
slotName: 'encode',
inputType: 'text',
showInTable: false,
valueFormat: (record) => {
return record.encode ? t('common.yes') : t('common.no');
},
width: 68,
},
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
},
{
title: '',
dataIndex: 'operation',
slotName: 'operation',
fixed: 'right',
width: 100,
},
];
}
return [
{
title: 'common.desc',
dataIndex: 'description',
inputType: 'text',
showTooltip: true,
},
{
title: 'apiTestManagement.paramVal',
dataIndex: 'value',
inputType: 'text',
showTooltip: true,
},
];
});
const bodyTableData = computed(() => {
switch (previewDetail.value?.body?.bodyType) {
case RequestBodyFormat.FORM_DATA:
return (previewDetail.value.body.formDataBody?.formValues || [])
.map((e) => ({
...e,
value: e.paramType === RequestParamsType.FILE ? e.files?.map((file) => file.fileName).join('、') : e.value,
}))
?.filter((e) => e.key !== '');
case RequestBodyFormat.WWW_FORM:
return previewDetail.value.body.wwwFormBody?.formValues?.filter((e) => e.key !== '') || [];
case RequestBodyFormat.BINARY:
return [
{
description: previewDetail.value.body.binaryBody.description,
value: previewDetail.value.body.binaryBody.file?.fileName,
},
];
default:
return [];
}
});
const bodyCode = computed(() => {
switch (previewDetail.value?.body?.bodyType) {
case RequestBodyFormat.FORM_DATA:
return previewDetail.value.body.formDataBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.WWW_FORM:
return previewDetail.value.body.wwwFormBody?.formValues?.map((item) => `${item.key}:${item.value}`).join('\n');
case RequestBodyFormat.RAW:
return previewDetail.value.body.rawBody?.value;
case RequestBodyFormat.JSON:
return previewDetail.value.body.jsonBody?.jsonValue;
case RequestBodyFormat.XML:
return previewDetail.value.body.xmlBody?.value;
default:
return '';
}
});
const bodyCodeLanguage = computed(() => {
if (previewDetail.value?.body?.bodyType === RequestBodyFormat.JSON) {
return LanguageEnum.JSON;
}
if (previewDetail.value?.body?.bodyType === RequestBodyFormat.XML) {
return LanguageEnum.XML;
}
return LanguageEnum.PLAINTEXT;
});
function copyScript(val: string) {
if (isSupported) {
copy(val);
Message.success(t('common.copySuccess'));
} else {
Message.warning(t('apiTestDebug.copyNotSupport'));
}
}
const typeKey = computed(() => (props.isApi ? 'api' : 'case'));
//
function getBottomDistance(type: string, isEmpty = false) {
const isEmptyDefaultDis = isEmpty ? 34 : 0;
if (props.diffDistanceMap[type] && props.diffDistanceMap[type][typeKey.value]) {
return props.diffDistanceMap[type][typeKey.value] * 32 + isEmptyDefaultDis;
}
return 0;
}
//
function showDiff(type: string) {
return props.diffDistanceMap[type]?.display;
}
//
function hiddenEmptyTable(type: string) {
if (props.isApi) {
return props.diffDistanceMap[type]?.showEmptyApiTable;
}
return props.diffDistanceMap[type]?.showEmptyCaseTable;
}
watchEffect(() => {
if (props.detail) {
previewDetail.value = cloneDeep(props.detail);
}
});
</script>
<style scoped lang="less">
.title-type {
color: var(--color-text-1);
@apply font-medium;
}
.title {
color: var(--color-text-1);
@apply my-4;
}
.detail-item-title {
margin-bottom: 8px;
gap: 16px;
@apply flex items-center justify-between;
.detail-item-title-text {
@apply font-medium;
color: var(--color-text-1);
}
}
.not-setting-data {
border: 1px solid var(--color-border-2);
border-radius: 4px;
@apply flex items-center justify-center;
}
</style>

View File

@ -0,0 +1,407 @@
<template>
<MsDrawer
v-model:visible="showDiffVisible"
:title="t('case.apiAndCaseDiff')"
width="100%"
class="diff-modal"
:footer="false"
no-content-padding
unmount-on-close
:closable="false"
>
<template #title>
<div class="flex w-full items-center justify-between">
<div>{{ t('case.apiAndCaseDiff') }}</div>
<div class="flex items-center text-[14px]">
<div class="-mt-[2px] mr-[8px]"> {{ t('case.syncItem') }}</div>
<a-checkbox-group v-model="form.checkType">
<a-checkbox v-for="item of checkList" :key="item.value" :value="item.value">
<div class="flex items-center"
>{{ item.label }}
<a-tooltip v-if="item.tooltip" :content="item.tooltip" position="top">
<div class="flex items-center">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</div>
</a-tooltip>
</div>
</a-checkbox>
</a-checkbox-group>
<a-divider direction="vertical" :margin="0" class="!mr-[8px]"></a-divider>
<a-switch v-model:model-value="form.ignoreUpdate" size="small" />
<a-select
v-model="form.ignoreUpdateType"
class="ml-[8px] w-[160px]"
:placeholder="t('caseManagement.featureCase.PleaseSelect')"
:disabled="!form.ignoreUpdate"
@change="changeIgnoreType"
>
<a-option v-for="item of ignoreList" :key="item.value" :value="item.value">
{{ t(item.label) }}
</a-option>
</a-select>
<a-divider direction="vertical" :margin="8"></a-divider>
<a-switch v-model:model-value="form.deleteParams" size="small" />
<div class="ml-[8px] font-normal text-[var(--color-text-1)]">{{ t('case.deleteNotCorrespondValue') }}</div>
<a-divider direction="vertical" :margin="0" class="!ml-[8px]"></a-divider>
<a-button class="mx-[12px]" type="secondary" @click="cancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :loading="syncLoading" :disabled="!form.checkType.length" @click="confirmBatchSync">
{{ t('case.apiSyncChange') }}
</a-button>
</div>
</div>
</template>
<!-- 图例 -->
<div class="legend-container">
<div class="item mr-[24px]">
<div class="legend add"></div>
{{ t('case.diffAdd') }}
</div>
<div class="item">
<div class="legend delete"></div>
{{ t('common.delete') }}
</div>
</div>
<!-- 对比 -->
<div class="diff-container">
<div class="diff-item ml-[16px] mr-[8px]">
<div class="title-type"> [{{ apiDetailInfo?.num }}] {{ apiDetailInfo?.name }} </div>
<DiffItem :diff-distance-map="diffDistanceMap" mode="add" is-api :detail="apiDefinedRequest as RequestParam" />
</div>
<div class="diff-item ml-[8px] mr-[16px]">
<div class="title-type"> [{{ caseDetail?.num }}] {{ caseDetail?.name }} </div>
<DiffItem :diff-distance-map="diffDistanceMap" mode="delete" :detail="caseDetail as RequestParam" />
</div>
</div>
</MsDrawer>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import { cloneDeep } from 'lodash-es';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { TabItem } from '@/components/pure/ms-editable-tab/types';
import DiffItem from './diffItem.vue';
import { getCaseDetail, getDefinitionDetail } from '@/api/modules/api-test/management';
import { useI18n } from '@/hooks/useI18n';
import { EnableKeyValueParam, ExecuteRequestCommonParam } from '@/models/apiTest/common';
import { ApiDefinitionDetail } from '@/models/apiTest/management';
import { RequestBodyFormat, RequestComposition } from '@/enums/apiEnum';
import type { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
const { t } = useI18n();
const props = defineProps<{
activeApiCaseId: string;
activeDefinedId: string;
}>();
const emit = defineEmits<{
(e: 'close'): void;
}>();
const showDiffVisible = defineModel<boolean>('visible', {
required: true,
});
const checkList = ref([
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
},
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
tooltip: t('case.onlySyncNewParamsOrValue'),
},
{
value: RequestComposition.QUERY,
label: RequestComposition.QUERY,
},
{
value: RequestComposition.REST,
label: RequestComposition.REST,
},
]);
const initForm = {
deleteParams: false,
checkType: [],
noticeApiCaseCreator: true,
noticeApiScenarioCreator: true,
ignoreUpdate: false,
ignoreUpdateType: ['THIS_TIME'],
};
const form = ref({ ...initForm });
const ignoreList = ref([
{
value: 'THIS_TIME',
label: t('case.ignoreThisChange'),
},
{
value: 'ALL',
label: t('case.ignoreAllChange'),
},
]);
//
function changeIgnoreType() {}
function cancel() {
showDiffVisible.value = false;
emit('close');
}
const syncLoading = ref<boolean>(false);
//
function confirmBatchSync() {}
const defaultCaseParams = inject<RequestParam>('defaultCaseParams');
const caseDetail = ref<Record<string, any>>({});
const apiDetailInfo = ref<Record<string, any>>({});
const apiDefinedRequest = ref<Record<string, any>>({});
const diffDistanceMap = ref<Record<string, any>>({});
/**
* 设置对比
* @params apiValue 接口数据
* @params caseValue 用例数据
* @params typeKey 用于增加间距Map的对应KEY\HEADER\QUERY等
* @params nameKey 对应参数名称key
*/
function setDiff(
apiValue: Record<string, any>[],
caseValue: Record<string, any>[],
typeKey: string,
nameKey = 'key'
) {
const apiDefinedValue = (apiValue || []).filter((e) => e.key !== '') || [];
const apiCaseValue = (caseValue || []).filter((e) => e.key !== '') || [];
const caseValueMap = new Map();
const apiValueMap = new Map();
apiDefinedValue.forEach((item: any) => apiValueMap.set(item[nameKey], item));
apiCaseValue.forEach((item: any) => caseValueMap.set(item[nameKey], item));
const definedData: Record<string, any>[] = apiDefinedValue.map((item) => {
if (!caseValueMap.has(item[nameKey])) {
return {
...cloneDeep(item),
diff: 'change',
};
}
return item;
});
const caseData: Record<string, any>[] = apiCaseValue.map((item) => {
if (!apiValueMap.has(item[nameKey])) {
return {
...cloneDeep(item),
diff: 'change',
};
}
return item;
});
//
const disAbs = Math.abs(caseData.length - definedData.length);
diffDistanceMap.value[typeKey] = {
case: caseData.length < definedData.length ? disAbs : 0,
api: caseData.length > definedData.length ? disAbs : 0,
display: caseData.length !== 0 || definedData.length !== 0, //
showEmptyCaseTable: caseData.length !== 0, //
showEmptyApiTable: definedData.length !== 0, // api
};
return {
caseData,
definedData,
};
}
//
function processData() {
//
const headersObj = setDiff(
apiDefinedRequest.value?.headers as any,
caseDetail.value.headers,
RequestComposition.HEADER
);
if (apiDefinedRequest.value?.headers) {
apiDefinedRequest.value.headers = headersObj.definedData as EnableKeyValueParam[];
caseDetail.value.headers = headersObj.caseData;
}
// query
const queryDiffObj = setDiff(
apiDefinedRequest.value?.query as any,
caseDetail.value.query,
RequestComposition.QUERY
);
if (apiDefinedRequest.value?.query) {
apiDefinedRequest.value.query = queryDiffObj.definedData as ExecuteRequestCommonParam[];
caseDetail.value.query = queryDiffObj.caseData;
}
// rest
const restDiffObj = setDiff(apiDefinedRequest.value?.rest as any, caseDetail.value.rest, RequestComposition.REST);
if (apiDefinedRequest.value?.rest) {
apiDefinedRequest.value.rest = restDiffObj.definedData as ExecuteRequestCommonParam[];
caseDetail.value.rest = restDiffObj.caseData;
}
//
if (apiDefinedRequest.value?.body?.bodyType) {
switch (apiDefinedRequest.value?.body?.bodyType) {
// FORM_DATA
case RequestBodyFormat.FORM_DATA:
const bodyFormDataDiffObj = setDiff(
apiDefinedRequest.value.body.formDataBody?.formValues as any,
caseDetail.value.body.formDataBody?.formValues,
RequestBodyFormat.FORM_DATA
);
apiDefinedRequest.value.body.formDataBody.formValues =
bodyFormDataDiffObj.definedData as ExecuteRequestCommonParam[];
caseDetail.value.body.formDataBody.formValues = bodyFormDataDiffObj.caseData;
break;
// WWW_FORM
case RequestBodyFormat.WWW_FORM:
const bodyWwwFormDiffObj = setDiff(
apiDefinedRequest.value.body.wwwFormBody?.formValues as any,
caseDetail.value.body.wwwFormBody?.formValues,
RequestBodyFormat.WWW_FORM
);
apiDefinedRequest.value.body.wwwFormBody.formValues =
bodyWwwFormDiffObj.definedData as ExecuteRequestCommonParam[];
caseDetail.value.body.wwwFormBody.formValues = bodyWwwFormDiffObj.caseData;
break;
default:
break;
}
}
}
//
async function getCaseDetailInfo(id: string) {
try {
const res = await getCaseDetail(id);
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
}
caseDetail.value = {
...cloneDeep(defaultCaseParams as RequestParam),
...({
...res.request,
...res,
url: res.path,
...parseRequestBodyResult,
} as Partial<TabItem>),
};
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function getApiDetail(apiDefinitionId: string) {
try {
const detail = await getDefinitionDetail(apiDefinitionId);
apiDetailInfo.value = detail as ApiDefinitionDetail;
apiDefinedRequest.value = detail.request as unknown as RequestParam;
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
async function getRequestDetail(definedId: string, apiCaseId: string) {
try {
await Promise.all([getApiDetail(definedId), getCaseDetailInfo(apiCaseId)]);
processData();
} catch (error) {
console.error(error);
}
}
watchEffect(() => {
if (props.activeDefinedId && props.activeApiCaseId) {
getRequestDetail(props.activeDefinedId, props.activeApiCaseId);
}
});
</script>
<style scoped lang="less">
.legend-container {
padding: 8px 0;
@apply flex items-center justify-center;
.item {
@apply flex items-center;
.legend {
margin-right: 8px;
width: 8px;
height: 8px;
border-radius: 2px;
&.add {
border: 0.5px solid rgb(var(--success-6));
}
&.delete {
border: 0.5px solid rgb(var(--danger-6));
}
}
}
}
.diff-container {
@apply flex;
.diff-item {
overflow-y: auto;
padding: 16px;
min-height: calc(100vh - 110px);
border-radius: 12px;
background: white;
box-shadow: 0 0 10px rgba(120 56 135/ 5%);
@apply flex-1;
.title-type {
color: var(--color-text-1);
@apply font-medium;
}
.title {
color: var(--color-text-1);
@apply my-4;
}
.detail-item-title {
margin-bottom: 8px;
gap: 16px;
@apply flex items-center justify-between;
.detail-item-title-text {
@apply font-medium;
color: var(--color-text-1);
}
}
}
}
:deep(.arco-table-td-content) {
padding: 5px 8px;
}
:deep(.ms-json-schema) .arco-table-td-content {
padding: 0;
}
</style>
<style lang="less">
.diff-modal {
.ms-drawer-body {
background: var(--color-text-n9);
}
}
</style>

View File

@ -0,0 +1,130 @@
<template>
<a-modal
v-model:visible="showBatchSyncModal"
title-align="start"
class="ms-modal-upload ms-modal-medium"
:width="600"
@close="cancel"
>
<template #title>
{{ t('case.apiSyncChange') }}
<div class="text-[var(--color-text-4)]">
{{
t('common.selectedCount', {
count: props.batchParams.currentSelectCount || 0,
})
}}
</div>
</template>
<a-alert class="mb-[16px]" type="warning">{{ t('case.apiSyncModalAlert') }}</a-alert>
<div class="mb-[8px]">
{{ t('case.syncItem') }}
</div>
<a-checkbox-group v-model="form.checkType">
<a-checkbox v-for="item of checkList" :key="item.value" :value="item.value">
<div class="flex items-center">
{{ item.label }}
<a-tooltip v-if="item.tooltip" :content="item.tooltip" position="top">
<div class="flex items-center">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</div>
</a-tooltip>
</div>
</a-checkbox>
</a-checkbox-group>
<div class="my-[16px] flex items-center">
<a-switch v-model:model-value="form.deleteParams" size="small" />
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('case.deleteNotCorrespondValue') }}</div>
</div>
<div class="my-[16px] flex items-center">
{{ t('case.changeNotice') }}
<a-tooltip :content="t('case.confirmMessageStatusEnable')" position="bl">
<div class="flex items-center">
<icon-question-circle
class="ml-[4px] text-[var(--color-text-4)] hover:text-[rgb(var(--primary-5))]"
size="16"
/>
</div>
</a-tooltip>
</div>
<div class="my-[16px] flex items-center">
<a-switch v-model:model-value="form.noticeApiCaseCreator" size="small" />
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('case.NoticeApiCaseCreator') }}</div>
</div>
<div class="my-[16px] flex items-center">
<a-switch v-model:model-value="form.noticeApiScenarioCreator" size="small" />
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('case.NoticeApiScenarioCreator') }}</div>
</div>
<template #footer>
<a-button type="secondary" @click="cancel">{{ t('common.cancel') }}</a-button>
<a-button type="primary" :loading="syncLoading" :disabled="!form.checkType.length" @click="confirmBatchSync">
{{ t('case.apiSyncChange') }}
</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import type { BatchActionQueryParams } from '@/components/pure/ms-table/type';
import { useI18n } from '@/hooks/useI18n';
import { RequestComposition } from '@/enums/apiEnum';
const { t } = useI18n();
const props = defineProps<{
batchParams: BatchActionQueryParams;
}>();
const showBatchSyncModal = defineModel<boolean>('visible', {
required: true,
});
const initForm = {
deleteParams: false,
checkType: [],
noticeApiCaseCreator: true,
noticeApiScenarioCreator: true,
};
const form = ref({ ...initForm });
const checkList = ref([
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
},
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
tooltip: t('case.onlySyncNewParamsOrValue'),
},
{
value: RequestComposition.QUERY,
label: RequestComposition.QUERY,
},
{
value: RequestComposition.REST,
label: RequestComposition.REST,
},
]);
const syncLoading = ref<boolean>(false);
function cancel() {
showBatchSyncModal.value = false;
}
// TODO
function confirmBatchSync() {}
</script>
<style scoped></style>

View File

@ -205,6 +205,26 @@ export default {
'case.recycle.confirmRecovery': 'Confirm Recovery', 'case.recycle.confirmRecovery': 'Confirm Recovery',
'case.createCase': 'Create Case', 'case.createCase': 'Create Case',
'case.updateCase': 'Update Case', 'case.updateCase': 'Update Case',
'case.changeDifferences': 'Interface versus use case differences',
'case.apiParamsHasChange': 'Interface parameter is changed',
'case.apiSyncChange': 'Sync',
'case.apiSyncModalAlert':
'Interface definition request parameter synchronization to the interface use case, may result in abnormal interfaces to perform case execution!',
'case.syncItem': 'Sync item',
'case.onlySyncNewParamsOrValue':
'JSON format, only the synchronous interface definition of new parameters and parameter values',
'case.deleteNotCorrespondValue':
'Remove parameters in the use case that cannot correspond to the interface definition',
'case.changeNotice': 'Change notice',
'case.confirmMessageStatusEnable':
'Verify that the "CASE Update "event in the Message Managementinterface test is configured for receivers and is on',
'case.NoticeApiCaseCreator': 'Notifies the creator of the interface use case',
'case.NoticeApiScenarioCreator': 'Notify the founder of citing the use case scenario',
'case.apiAndCaseDiff': 'Interface vs. use case differences',
'case.ignoreThisChange': 'Ignore this change',
'case.ignoreAllChange': 'Ignore all changes',
'case.diffAdd': 'Add',
'case.notSetData': 'No data has been set',
'case.saveContinueText': 'Save and Continue Creating', 'case.saveContinueText': 'Save and Continue Creating',
'case.detail.changeHistoryTip': 'case.detail.changeHistoryTip':
"View and compare historical changes. According to the administrator's settings, historical data will be automatically deleted", "View and compare historical changes. According to the administrator's settings, historical data will be automatically deleted",

View File

@ -196,6 +196,22 @@ export default {
'case.recycle.confirmRecovery': '确认恢复', 'case.recycle.confirmRecovery': '确认恢复',
'case.createCase': '创建用例', 'case.createCase': '创建用例',
'case.updateCase': '更新用例', 'case.updateCase': '更新用例',
'case.changeDifferences': '接口与用例差异对比',
'case.apiParamsHasChange': '接口参数发生变更',
'case.apiSyncChange': '同步',
'case.apiSyncModalAlert': '接口定义请求参数同步到接口用例中,可能导致待执行的接口用例执行异常!',
'case.syncItem': '同步项',
'case.onlySyncNewParamsOrValue': 'JSON 格式,仅同步接口定义新增的参数与参数值',
'case.deleteNotCorrespondValue': '删除用例中无法与接口定义对应的参数',
'case.changeNotice': '变更通知',
'case.confirmMessageStatusEnable': '请确认消息管理-接口测试中的"CASE更新"事件已配置接收人且状态为开启',
'case.NoticeApiCaseCreator': '通知接口用例的创建人',
'case.NoticeApiScenarioCreator': '通知引用该用例的场景创建人',
'case.apiAndCaseDiff': '接口与用例差异对比',
'case.ignoreThisChange': '忽略本次变更差异',
'case.ignoreAllChange': '忽略全部变更差异',
'case.diffAdd': '新增',
'case.notSetData': '暂未设置数据',
'case.saveContinueText': '保存并继续创建', 'case.saveContinueText': '保存并继续创建',
'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除', 'case.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
'case.detail.noReminders': '不再提醒', 'case.detail.noReminders': '不再提醒',