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' : '',
]"
bordered
:row-class="rowClass"
v-on="propsEvent"
@drag-change="tableChange"
@init-end="validateAndUpdateErrorMessageList"
@ -294,6 +295,7 @@
disabled?: boolean; //
showSelectorAll?: boolean; //
rowSelection?: TableRowSelection;
diffMode?: 'add' | 'delete';
spanMethod?: (data: {
record: TableData;
column: TableColumnData | TableOperationColumn;
@ -537,6 +539,18 @@
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({
validateAndUpdateErrorMessageList,
});
@ -719,4 +733,20 @@
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>

View File

@ -47,9 +47,26 @@
</div>
</template>
<template #num="{ record }">
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">
{{ record.num }}
</MsButton>
<div class="flex items-center">
<MsButton type="text" @click="isApi ? openCaseDetailDrawer(record.id) : openCaseTab(record)">
{{ 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 #protocol="{ record }">
<apiMethodName :method="record.protocol" />
@ -281,6 +298,17 @@
/>
<!-- 执行结果抽屉 -->
<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>
<script setup lang="ts">
@ -299,6 +327,8 @@
import type { CaseLevel } from '@/components/business/ms-case-associate/types';
import caseDetailDrawer from './caseDetailDrawer.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 apiStatus from '@/views/api-test/components/apiStatus.vue';
import BatchRunModal from '@/views/api-test/components/batchRunModal.vue';
@ -390,8 +420,7 @@
sorter: true,
},
fixed: 'left',
width: 130,
showTooltip: true,
width: 150,
columnSelectorDisabled: true,
},
{
@ -568,6 +597,11 @@
eventTag: 'execute',
permission: ['PROJECT_API_DEFINITION_CASE:READ+EXECUTE'],
},
{
label: 'case.apiSyncChange',
eventTag: 'sync',
permission: ['PROJECT_API_DEFINITION_CASE:READ+UPDATE'],
},
{
label: 'common.delete',
eventTag: 'delete',
@ -820,9 +854,14 @@
}
});
}
const batchConditionParams = ref<any>();
const showSyncModal = ref<boolean>(false);
// TODO
function syncParams() {
showSyncModal.value = true;
}
//
function handleTableBatch(event: BatchActionParams, params: BatchActionQueryParams) {
tableSelected.value = params?.selectedIds || [];
@ -840,6 +879,9 @@
showBatchExecute.value = true;
});
break;
case 'sync':
syncParams();
break;
default:
break;
}
@ -920,6 +962,23 @@
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({
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.createCase': 'Create 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.detail.changeHistoryTip':
"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.createCase': '创建用例',
'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.detail.changeHistoryTip': '查看、对比历史修改,根据管理员设置规则,变更历史数据将自动删除',
'case.detail.noReminders': '不再提醒',