feat(接口场景): 引用CASE

This commit is contained in:
teukkk 2024-03-23 11:04:08 +08:00 committed by Craftsman
parent b28e266675
commit a0a2e389df
36 changed files with 1100 additions and 45 deletions

View File

@ -1,6 +1,6 @@
<template>
<a-dropdown v-model:popup-visible="dropdownVisible" position="tl" trigger="click">
<a-button size="mini" type="outline">
<a-dropdown v-model:popup-visible="dropdownVisible" :disabled="props.disabled" position="tl" trigger="click">
<a-button :disabled="props.disabled" size="mini" type="outline">
<template #icon> <icon-upload class="text-[14px] !text-[rgb(var(--primary-5))]" /> </template>
</a-button>
<template #content>
@ -40,6 +40,10 @@
import { useI18n } from '@/hooks/useI18n';
const props = defineProps<{
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: 'upload', file: File): void;
(e: 'change', _fileList: MsFileItem[], fileItem: MsFileItem): void;

View File

@ -3,8 +3,13 @@
<!-- TODO:跟下面统一样式 -->
<div class="flex flex-col">
<div class="mb-1">
<a-dropdown v-model:popup-visible="buttonDropDownVisible" position="tr" trigger="click">
<a-button type="outline">
<a-dropdown
v-model:popup-visible="buttonDropDownVisible"
:disabled="props.disabled"
position="tr"
trigger="click"
>
<a-button :disabled="props.disabled" type="outline">
<template #icon> <icon-plus class="text-[14px]" /> </template>
{{ t('system.orgTemplate.addAttachment') }}
</a-button>
@ -65,6 +70,7 @@
>
<MsTagsInput
v-model:model-value="inputFiles"
:disabled="props.disabled"
:input-class="props.inputClass"
placeholder=" "
:max-tag-count="1"
@ -151,9 +157,10 @@
</a-popover>
</div>
<div v-else class="flex w-full items-center gap-[4px]">
<dropdownMenu @link-file="associatedFile" @change="handleChange" />
<dropdownMenu :disabled="props.disabled" @link-file="associatedFile" @change="handleChange" />
<a-input
v-model:model-value="inputFileName"
:disabled="props.disabled"
:class="props.inputClass"
:size="props.inputSize"
allow-clear
@ -198,6 +205,7 @@
const props = withDefaults(
defineProps<{
disabled?: boolean;
mode?: 'button' | 'input';
multiple?: boolean;
inputClass?: string;

View File

@ -12,6 +12,7 @@
<paramsTable
ref="extractParamsTableRef"
v-model:params="condition.jsonPathAssertion.assertions"
:disabled="props.disabled"
:selectable="false"
:columns="jsonPathColumns"
:scroll="{ minWidth: '700px' }"
@ -37,6 +38,7 @@
v-model:model-value="record.expression"
class="ms-params-input"
:max-length="255"
:disabled="props.disabled"
@input="() => handleExpressionChange(rowIndex)"
@change="() => handleExpressionChange(rowIndex)"
>
@ -48,10 +50,13 @@
<div>{{ t('apiTestDebug.expressionTip3') }}</div>
</template>
<MsIcon
:disabled="props.disabled"
type="icon-icon_flashlamp"
:size="15"
:class="
disabledExpressionSuffix ? 'ms-params-input-suffix-icon--disabled' : 'ms-params-input-suffix-icon'
disabledExpressionSuffix || props.disabled
? 'ms-params-input-suffix-icon--disabled'
: 'ms-params-input-suffix-icon'
"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.JSON_PATH)"
/>
@ -95,6 +100,7 @@
<paramsTable
ref="extractParamsTableRef"
v-model:params="condition.xpathAssertion.assertions"
:disabled="props.disabled"
:selectable="false"
:columns="xPathColumns"
:scroll="{ minWidth: '700px' }"
@ -118,6 +124,7 @@
</template>
<a-input
v-model:model-value="record.expression"
:disabled="props.disabled"
class="ms-params-input"
:max-length="255"
@input="() => handleExpressionChange(rowIndex)"
@ -132,9 +139,12 @@
</template>
<MsIcon
type="icon-icon_flashlamp"
:disabled="props.disabled"
:size="15"
:class="
disabledExpressionSuffix ? 'ms-params-input-suffix-icon--disabled' : 'ms-params-input-suffix-icon'
disabledExpressionSuffix || props.disabled
? 'ms-params-input-suffix-icon--disabled'
: 'ms-params-input-suffix-icon'
"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.X_PATH)"
/>
@ -178,13 +188,14 @@
<a-radio value="XML">XML</a-radio>
</a-radio-group>
<div class="mt-[16px]">
<a-checkbox v-model:model-value="condition.document.followApi">
<a-checkbox v-model:model-value="condition.document.followApi" :disabled="props.disabled">
<span class="text-[var(--color-text-1)]">{{ t('ms.assertion.followApi') }}</span>
</a-checkbox>
</div>
<div class="mt-[16px]">
<paramsTable
v-model:params="condition.document.jsonAssertion"
:disabled="props.disabled"
:selectable="false"
:columns="documentColumns"
:scroll="{
@ -202,6 +213,7 @@
v-if="showDeleteSingle && (record.rowSpan > 1 || record.groupId)"
class="ml-[8px] cursor-pointer text-[var(--color-text-4)]"
size="20"
:disabled="props.disabled"
@click="deleteSingleParam(record)"
/>
</template>
@ -236,6 +248,7 @@
ref="extractParamsTableRef"
v-model:params="condition.regexAssertion.assertions"
:selectable="false"
:disabled="props.disabled"
:columns="xPathColumns"
:scroll="{ minWidth: '700px' }"
:default-param-item="xPathDefaultParamItem"
@ -258,6 +271,7 @@
</template>
<a-input
v-model:model-value="record.expression"
:disabled="props.disabled"
class="ms-params-input"
:max-length="255"
@input="() => handleExpressionChange(rowIndex)"
@ -273,8 +287,11 @@
<MsIcon
type="icon-icon_flashlamp"
:size="15"
:disabled="props.disabled"
:class="
disabledExpressionSuffix ? 'ms-params-input-suffix-icon--disabled' : 'ms-params-input-suffix-icon'
disabledExpressionSuffix || props.disabled
? 'ms-params-input-suffix-icon--disabled'
: 'ms-params-input-suffix-icon'
"
@click.stop="() => showFastExtraction(record, RequestExtractExpressionEnum.REGEX)"
/>
@ -379,6 +396,7 @@
const props = defineProps<{
data: Param;
response?: string;
disabled?: boolean;
}>();
const activeTab = ref(ResponseBodyAssertionType.JSON_PATH);
const activeResponseFormat = ref('XML');
@ -765,6 +783,7 @@
function deleteListItem(id: string | number) {}
function showFastExtraction(record: ExpressionConfig, type: ExpressionType) {
if (props.disabled) return;
activeRecord.value = { ...record, extractType: type };
fastExtractionVisible.value = true;
}

View File

@ -6,6 +6,7 @@
:columns="columns"
:scroll="{ minWidth: '700px' }"
:default-param-item="defaultParamItem"
:disabled="props.disabled"
@change="handleParamTableChange"
/>
</div>
@ -28,6 +29,7 @@
const props = defineProps<{
data: Param;
disabled?: boolean;
}>();
const emit = defineEmits<{

View File

@ -6,6 +6,7 @@
</div>
<a-input-number
v-model="condition.expectedValue"
:disabled="props.disabled"
:step="100"
:min="0"
mode="button"
@ -29,6 +30,7 @@
const props = defineProps<{
data: ExecuteAssertion;
disabled?: boolean;
}>();
const emit = defineEmits<{

View File

@ -6,7 +6,7 @@
height: 'calc(100vh - 490px)',
}"
>
<conditionContent v-model:data="condition" is-build-in @change="handleChange" />
<conditionContent v-model:data="condition" :disabled="disabled" is-build-in @change="handleChange" />
</a-scrollbar>
</div>
</template>
@ -26,6 +26,7 @@
interface ScriptTabProps {
data: any;
disabled?: boolean;
}
const props = defineProps<ScriptTabProps>();

View File

@ -4,6 +4,7 @@
<div class="mb-[8px]">{{ t('ms.assertion.statusCode') }}</div>
<a-select
v-model="condition.condition"
:disabled="props.disabled"
class="w-[157px]"
@change="
emit('change', {
@ -19,6 +20,7 @@
<a-input
v-if="showInput"
v-model="condition.expectedValue"
:disabled="props.disabled"
hide-button
class="w-[157px]"
@change="
@ -49,6 +51,7 @@
const props = defineProps<{
data: Param;
disabled?: boolean;
}>();
const emit = defineEmits<{

View File

@ -1,8 +1,8 @@
<template>
<div class="ms-assertion">
<div class="mb-[8px] flex items-center justify-between">
<a-dropdown trigger="hover" @select="handleSelect">
<a-button class="w-[84px]" type="outline">
<a-dropdown trigger="hover" :disabled="props.disabled" @select="handleSelect">
<a-button class="w-[84px]" type="outline" :disabled="props.disabled">
<div class="flex flex-row items-center gap-[8px]">
<icon-plus />
<span>{{ t('ms.assertion.button') }}</span>
@ -15,7 +15,7 @@
</template>
</a-dropdown>
<div v-if="props.isDefinition && innerConfig" class="flex items-center">
<a-switch v-model:model-value="innerConfig.enableGlobal" size="small" type="line" />
<a-switch v-model:model-value="innerConfig.enableGlobal" :disabled="props.disabled" size="small" type="line" />
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('ms.assertion.openGlobal') }}</div>
<a-tooltip :content="t('ms.assertion.openGlobalTip')" position="left">
<icon-question-circle
@ -71,7 +71,7 @@
</MsTableMoreAction>
</div>
<a-switch v-model:model-value="item.enable" type="line" size="small" />
<a-switch v-model:model-value="item.enable" :disabled="props.disabled" type="line" size="small" />
</div>
</div>
</VueDraggable>
@ -81,18 +81,21 @@
<ResponseHeaderTab
v-if="valueKey === ResponseAssertionType.RESPONSE_HEADER"
v-model:data="getCurrentItemState"
:disabled="props.disabled"
@change="handleChange"
/>
<!-- 状态码 -->
<StatusCodeTab
v-if="valueKey === ResponseAssertionType.RESPONSE_CODE"
v-model:data="getCurrentItemState"
:disabled="props.disabled"
@change="handleChange"
/>
<!-- 响应体 -->
<ResponseBodyTab
v-if="valueKey === ResponseAssertionType.RESPONSE_BODY"
v-model:data="getCurrentItemState"
:disabled="props.disabled"
:response="props.response"
@change="handleChange"
/>
@ -100,18 +103,21 @@
<ResponseTimeTab
v-if="valueKey === ResponseAssertionType.RESPONSE_TIME"
v-model:data="getCurrentItemState"
:disabled="props.disabled"
@change="handleChange"
/>
<!-- 变量 -->
<VariableTab
v-if="valueKey === ResponseAssertionType.VARIABLE"
v-model:data="getCurrentItemState"
:disabled="props.disabled"
@change="handleChange"
/>
<!-- 脚本 -->
<ScriptTab
v-if="valueKey === ResponseAssertionType.SCRIPT"
v-model:data="getCurrentItemState"
:disabled="props.disabled"
@change="handleChange"
/>
<!-- </a-scrollbar> -->
@ -159,6 +165,7 @@
isDefinition?: boolean; //
assertionConfig?: ExecuteAssertionConfig; //
response?: string; //
disabled?: boolean;
}>();
const emit = defineEmits<{

View File

@ -13,6 +13,7 @@
<a-select
v-model="innerLanguageType"
:disabled="props.disabled"
class="max-w-[50%]"
:placeholder="t('project.commonScript.pleaseSelected')"
@change="changeHandler"
@ -54,6 +55,7 @@
const props = defineProps<{
expand: boolean;
disabled?: boolean;
languagesType: Language;
}>();
@ -119,6 +121,7 @@
}
function handleClick(obj: CommonScriptMenu) {
if (props.disabled) return;
let code = '';
if (obj.command) {
code = _handleCommand(obj.command);

View File

@ -29,7 +29,7 @@
height="460px"
theme="vs"
:language="innerLanguagesType"
:read-only="false"
:read-only="props.disabled"
:show-full-screen="false"
:show-theme-change="false"
>
@ -37,6 +37,7 @@
<MsScriptMenu
v-model:expand="expandMenu"
v-model:languagesType="innerLanguagesType"
:disabled="props.disabled"
@insert="insertHandler"
@form-api-import="formApiImport"
@insert-common-script="insertCommonScript"
@ -91,6 +92,7 @@
const props = withDefaults(
defineProps<{
showType: 'commonScript' | 'executionResult'; //
disabled?: boolean;
language: Language;
code: string;
enableRadioSelected?: boolean;

View File

@ -32,7 +32,7 @@
<slot name="tbutton"></slot>
</div>
</slot>
<div>
<div class="right-operation-button-icon">
<MsButton
v-if="props.showFullScreen"
type="icon"
@ -268,16 +268,16 @@
@apply w-full;
line-height: 24px;
.ms-drawer-fullscreen-btn {
.right-operation-button-icon .ms-button-icon {
border-radius: var(--border-radius-small);
color: var(--color-text-1);
.ms-drawer-fullscreen-btn-icon {
.arco-icon {
margin-right: 8px;
color: var(--color-text-1);
}
&:hover {
color: rgb(var(--primary-5));
.ms-drawer-fullscreen-btn-icon {
.arco-icon {
color: rgb(var(--primary-5));
}
}

View File

@ -23,9 +23,11 @@
<slot name="title" :item="item" :index="index"></slot>
</div>
<div class="flex items-center gap-[4px]">
<icon-drag-dot-vertical v-if="props.draggable" class="ms-list-drag-icon" />
<icon-drag-dot-vertical v-if="props.draggable && !props.disabled" class="ms-list-drag-icon" />
<div
v-if="$slots['itemAction'] || (props.itemMoreActions && props.itemMoreActions.length > 0)"
v-if="
$slots['itemAction'] || (props.itemMoreActions && props.itemMoreActions.length > 0 && !props.disabled)
"
class="ms-list-item-actions"
>
<slot name="itemAction" :item="item" :index="index"></slot>
@ -98,6 +100,7 @@
itemClass?: string;
activeItemClass?: string;
draggable?: boolean;
disabled?: boolean;
virtualListProps?: VirtualListProps;
}>(),
{

View File

@ -1,5 +1,5 @@
<template>
<a-tabs v-model:active-key="innerActiveKey" :class="props.class">
<a-tabs v-model:active-key="innerActiveKey" :class="[props.class, props.noContent ? 'no-content' : '']">
<a-tab-pane v-for="item of props.contentTabList" :key="item.value" :title="item.label">
<template #title>
<a-badge
@ -27,6 +27,7 @@
contentTabList: { label: string; value: string }[];
class?: string;
getTextFunc?: (value: any) => string;
noContent?: boolean;
}>(),
{
getTextFunc: (value: any) => value,
@ -55,4 +56,9 @@
background-color: rgb(var(--primary-5));
}
}
.no-content {
:deep(.arco-tabs-content) {
display: none;
}
}
</style>

View File

@ -142,5 +142,6 @@ export default {
'common.batchDebug': 'Batch debug',
'common.quote': 'Quote',
'common.notQuote': 'Not quote',
'common.execute': '执行',
'common.execute': 'execute',
'common.replace': 'replace',
};

View File

@ -144,4 +144,5 @@ export default {
'common.quote': '引用',
'common.notQuote': '不引用',
'common.execute': '执行',
'common.replace': '替换',
};

View File

@ -1,5 +1,5 @@
<template>
<a-button type="outline" size="mini" @click="showBatchAddParamDrawer = true">
<a-button :disabled="props.disabled" type="outline" size="mini" @click="showBatchAddParamDrawer = true">
{{ t('apiTestDebug.batchAdd') }}
</a-button>
<MsDrawer
@ -54,6 +54,7 @@
defaultParamItem?: Record<string, any>; //
noParamType?: boolean; //
addTypeText?: string; //
disabled?: boolean;
}>(),
{
noParamType: false,

View File

@ -88,7 +88,12 @@
<div class="one-line-text mr-[4px] max-w-[110px] font-medium text-[var(--color-text-1)]">
{{ condition.name }}
</div>
<MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" />
<MsIcon
v-show="!props.disabled"
type="icon-icon_edit_outlined"
class="edit-script-name-icon"
@click="showEditScriptNameInput"
/>
</div>
</a-tooltip>
<a-popover class="h-auto" position="right">
@ -127,6 +132,7 @@
<div class="flex items-center gap-[8px]">
<a-button
v-if="props.isFormat"
:disabled="props.disabled"
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@ -137,13 +143,25 @@
</template>
{{ t('project.commonScript.formatting') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="undoScript">
<a-button
:disabled="props.disabled"
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="undoScript"
>
<template #icon>
<MsIcon type="icon-icon_undo_outlined" class="text-var(--color-text-4)" size="12" />
</template>
{{ t('common.revoke') }}
</a-button>
<a-button type="outline" class="arco-btn-outline--secondary p-[0_8px]" size="mini" @click="clearScript">
<a-button
:disabled="props.disabled"
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@click="clearScript"
>
<template #icon>
<MsIcon type="icon-icon_clear" class="text-var(--color-text-4)" size="12" />
</template>
@ -151,6 +169,7 @@
</a-button>
<a-button
v-if="!props.isBuildIn && !props.showPrePostRequest"
:disabled="props.disabled"
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@ -160,6 +179,7 @@
</a-button>
<a-button
v-if="!props.isBuildIn"
:disabled="props.disabled"
type="outline"
class="arco-btn-outline--secondary p-[0_8px]"
size="mini"
@ -175,6 +195,7 @@
ref="scriptDefinedRef"
v-model:code="condition.script"
v-model:language="condition.scriptLanguage"
:disabled="props.disabled"
show-type="commonScript"
:show-header="false"
/>
@ -248,6 +269,7 @@
<div class="mb-[16px] h-[300px]">
<MsCodeEditor
v-model:model-value="condition.script"
:read-only="props.disabled"
theme="vs"
height="276px"
:language="LanguageEnum.SQL"
@ -310,6 +332,7 @@
</div>
<a-input-number
v-model:model-value="condition.delay"
:disabled="props.disabled"
mode="button"
:step="100"
:min="0"
@ -467,6 +490,7 @@
const props = withDefaults(
defineProps<{
data: ExecuteConditionProcessor;
disabled?: boolean;
response?: string; //
heightUsed?: number;
isBuildIn?: boolean; //

View File

@ -1,7 +1,7 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<a-dropdown @select="(val) => addCondition(val as ConditionType)">
<a-button type="outline">
<a-button type="outline" :disabled="props.disabled">
<template #icon>
<icon-plus :size="14" />
</template>
@ -21,6 +21,7 @@
<div class="h-full w-[20%] min-w-[220px]">
<conditionList
v-model:list="data"
:disabled="props.disabled"
:active-id="activeItem.id"
:show-associated-scene="props.showAssociatedScene"
:show-pre-post-request="props.showPrePostRequest"
@ -30,6 +31,7 @@
</div>
<conditionContent
v-model:data="activeItem"
:disabled="props.disabled"
:total-list="data"
:response="props.response"
:height-used="props.heightUsed"
@ -56,6 +58,7 @@
const props = withDefaults(
defineProps<{
disabled?: boolean;
list: ExecuteConditionProcessor[];
conditionTypes: Array<ConditionType>;
addText: string;

View File

@ -5,6 +5,7 @@
v-model:data="data"
mode="static"
item-key-field="id"
:disabled="props.disabled"
:item-border="false"
class="h-full rounded-[var(--border-radius-small)] bg-[var(--color-text-n9)] p-[12px]"
item-class="mb-[4px] bg-white !p-[4px_8px]"
@ -47,7 +48,13 @@
</div>
</template>
<template #itemRight="{ item }">
<a-switch v-model:model-value="item.enable" size="small" type="line" @change="() => emit('change')" />
<a-switch
v-model:model-value="item.enable"
:disabled="props.disabled"
size="small"
type="line"
@change="() => emit('change')"
/>
</template>
</MsList>
</template>
@ -68,6 +75,7 @@
list: ExecuteConditionProcessor[];
activeId?: string | number;
showAssociatedScene?: boolean;
disabled?: boolean;
showPrePostRequest?: boolean; //
}>();
const emit = defineEmits<{

View File

@ -1,6 +1,6 @@
<template>
<a-dropdown-button
v-if="!caseDetail.executeLoading"
v-if="!caseDetail?.executeLoading && !props.executeLoading"
v-permission="['PROJECT_API_DEFINITION_CASE:READ+EXECUTE']"
class="exec-btn"
@click="() => execute(isPriorityLocalExec ? 'localExec' : 'serverExec')"
@ -36,17 +36,24 @@
import { defaultResponse } from '@/views/api-test/components/config';
const props = defineProps<{
environmentId: string;
environmentId?: string;
request?: (...args) => Record<string, any>;
isCaseDetail?: boolean;
executeCase?: boolean;
executeLoading?: boolean;
isEmit?: boolean;
}>();
const emit = defineEmits<{
(e: 'execute', executeType?: 'localExec' | 'serverExec'): void;
(e: 'stopDebug'): void;
}>();
const { t } = useI18n();
const appStore = useAppStore();
const caseDetail = defineModel<RequestParam>('detail', {
required: true,
required: false,
});
const apiLocalExec = inject<Ref<LocalConfig>>('apiLocalExec');
@ -66,6 +73,7 @@
executeType === 'localExec' ? localExecuteUrl.value : ''
);
websocket.value.addEventListener('message', (event) => {
if (!caseDetail.value || props.isEmit) return;
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
if (caseDetail.value.reportId === data.reportId) {
@ -85,6 +93,10 @@
}
async function execute(executeType?: 'localExec' | 'serverExec') {
if (!caseDetail.value || props.isEmit) {
emit('execute', executeType);
return;
}
try {
caseDetail.value.executeLoading = true;
caseDetail.value.response = cloneDeep(defaultResponse);
@ -129,6 +141,10 @@
}
function stopDebug() {
if (!caseDetail.value || props.isEmit) {
emit('stopDebug');
return;
}
websocket.value?.close();
caseDetail.value.executeLoading = false;
}
@ -146,6 +162,7 @@
defineExpose({
isPriorityLocalExec,
execute,
localExecuteUrl,
});
</script>

View File

@ -12,6 +12,7 @@
ref="inputRef"
v-model:model-value="innerValue"
:max-length="255"
:disabled="props.disabled"
:size="props.size"
:placeholder="t('ms.paramsInput.commonPlaceholder')"
class="ms-form-table-input"
@ -29,6 +30,7 @@
const props = defineProps<{
desc: string;
disabled?: boolean;
size?: 'small' | 'large' | 'medium' | 'mini';
}>();
const emit = defineEmits<{

View File

@ -37,6 +37,7 @@
<template #documentMustIncludeTitle>
<div class="flex flex-row items-center gap-[4px]">
<a-checkbox
:disabled="props.disabled"
:model-value="mustIncludeAllChecked"
:indeterminate="mustIncludeIndeterminate"
@change="(v) => handleMustIncludeChange(v as boolean)"
@ -47,6 +48,7 @@
<template #documentTypeCheckingTitle>
<div class="flex flex-row items-center gap-[4px]">
<a-checkbox
:disabled="props.disabled"
:model-value="typeCheckingAllChecked"
:indeterminate="typeCheckingIndeterminate"
@change="(v) => handleTypeCheckingChange(v as boolean)"
@ -73,6 +75,7 @@
<a-auto-complete
v-if="columnConfig.inputType === 'autoComplete'"
v-model:model-value="record[columnConfig.dataIndex as string]"
:disabled="props.disabled"
:data="columnConfig.autoCompleteParams?.filter((e) => e.isShow === true)"
class="ms-form-table-input"
:trigger-props="{ contentClass: 'ms-form-table-input-trigger' }"
@ -96,6 +99,7 @@
<a-input
v-else
v-model:model-value="record[columnConfig.dataIndex as string]"
:disabled="props.disabled"
:placeholder="t('apiTestDebug.paramNamePlaceholder')"
class="ms-form-table-input"
:max-length="255"
@ -111,6 +115,7 @@
:content="t(record.required ? 'apiTestDebug.paramRequired' : 'apiTestDebug.paramNotRequired')"
>
<MsButton
:disabled="props.disabled"
type="icon"
:class="[
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
@ -124,6 +129,7 @@
</a-tooltip>
<a-select
v-model:model-value="record.paramType"
:disabled="props.disabled"
:options="columnConfig.typeOptions || []"
class="ms-form-table-input w-full"
size="mini"
@ -134,6 +140,7 @@
<template #extractType="{ record, columnConfig, rowIndex }">
<a-select
v-model:model-value="record.extractType"
:disabled="props.disabled"
:options="columnConfig.typeOptions || []"
class="ms-form-table-input w-[110px]"
size="mini"
@ -144,6 +151,7 @@
<template #variableType="{ record, columnConfig, rowIndex }">
<a-select
v-model:model-value="record.variableType"
:disabled="props.disabled"
:options="columnConfig.typeOptions || []"
class="ms-form-table-input w-[110px]"
size="mini"
@ -154,6 +162,7 @@
<template #extractScope="{ record, columnConfig, rowIndex }">
<a-select
v-model:model-value="record.extractScope"
:disabled="props.disabled"
:options="columnConfig.typeOptions || []"
class="ms-form-table-input w-[180px]"
size="mini"
@ -192,6 +201,7 @@
<MsAddAttachment
v-else-if="record.paramType === RequestParamsType.FILE"
v-model:file-list="record.files"
:disabled="props.disabled"
mode="input"
:multiple="true"
:fields="{
@ -210,6 +220,7 @@
<MsParamsInput
v-else
v-model:value="record.value"
:disabled="props.disabled"
size="mini"
@change="() => addTableLine(rowIndex)"
@dblclick="quickInputParams(record)"
@ -221,6 +232,7 @@
<div class="flex items-center justify-between">
<a-input-number
v-model:model-value="record.minLength"
:disabled="props.disabled"
:placeholder="t('apiTestDebug.paramMin')"
:min="0"
class="ms-form-table-input ms-form-table-input-number"
@ -231,6 +243,7 @@
<div class="mx-[4px]">{{ t('common.to') }}</div>
<a-input-number
v-model:model-value="record.maxLength"
:disabled="props.disabled"
:placeholder="t('apiTestDebug.paramMax')"
:min="0"
class="ms-form-table-input"
@ -257,6 +270,7 @@
</template>
<MsTagsInput
v-model:model-value="record[columnConfig.dataIndex as string]"
:disabled="props.disabled"
:max-tag-count="1"
input-class="ms-form-table-input"
size="mini"
@ -269,6 +283,7 @@
<template #description="{ record, columnConfig, rowIndex }">
<paramDescInput
v-model:desc="record[columnConfig.dataIndex as string]"
:disabled="props.disabled"
size="mini"
@input="() => addTableLine(rowIndex)"
@dblclick="quickInputDesc(record)"
@ -279,6 +294,7 @@
<template #encode="{ record, rowIndex }">
<a-switch
v-model:model-value="record.encode"
:disabled="props.disabled"
size="small"
class="ms-form-table-input-switch"
type="line"
@ -289,6 +305,7 @@
<template #mustContain="{ record, columnConfig }">
<a-checkbox
v-model:model-value="record[columnConfig.dataIndex as string]"
:disabled="props.disabled"
@change="handleMustContainColChange(false)"
/>
</template>
@ -296,18 +313,25 @@
<template #typeChecking="{ record, columnConfig }">
<a-checkbox
v-model:model-value="record[columnConfig.dataIndex as string]"
:disabled="props.disabled"
@change="handleTypeCheckingColChange(false)"
/>
</template>
<!-- 响应头 -->
<template #header="{ record, columnConfig, rowIndex }">
<a-select v-model="record.header" class="ms-form-table-input" size="mini" @change="() => addTableLine(rowIndex)">
<a-select
v-model="record.header"
:disabled="props.disabled"
class="ms-form-table-input"
size="mini"
@change="() => addTableLine(rowIndex)"
>
<a-option v-for="item in columnConfig.options" :key="item.value">{{ t(item.label) }}</a-option>
</a-select>
</template>
<!-- 匹配条件 -->
<template #condition="{ record, columnConfig }">
<a-select v-model="record.condition" size="mini" class="ms-form-table-input">
<a-select v-model="record.condition" :disabled="props.disabled" size="mini" class="ms-form-table-input">
<a-option v-for="item in columnConfig.options" :key="item.value" :value="item.value">{{
t(item.label)
}}</a-option>
@ -325,18 +349,20 @@
record.required ? '!text-[rgb(var(--danger-5))]' : '!text-[var(--color-text-brand)]',
'!mr-[4px] !p-[4px]',
]"
:disabled="props.disabled"
size="mini"
@click="toggleRequired(record, rowIndex)"
>
<div>*</div>
</MsButton>
</a-tooltip>
<a-input v-model="record.expectedValue" size="mini" class="ms-form-table-input" />
<a-input v-model="record.expectedValue" :disabled="props.disabled" size="mini" class="ms-form-table-input" />
</template>
<!-- 项目选择 -->
<template #project="{ record, rowIndex }">
<a-select
v-model:model-value="record.projectId"
:disabled="props.disabled"
class="ms-form-table-input w-max-[200px] focus-within:!bg-[var(--color-text-n8)] hover:!bg-[var(--color-text-n8)]"
:bordered="false"
allow-search
@ -364,7 +390,7 @@
v-if="record.projectId"
v-model:model-value="record.environmentId"
v-model:input-value="record.environmentInput"
:disabled="!record.projectId"
:disabled="props.disabled || !record.projectId"
:options="[]"
mode="remote"
value-key="id"
@ -395,6 +421,7 @@
<a-switch
v-if="columnConfig.hasDisable"
v-model="record.enable"
:disabled="props.disabled"
size="small"
type="line"
class="mr-[8px]"
@ -415,6 +442,7 @@
<div class="mb-[8px] text-[var(--color-text-1)]">Content-Type</div>
<a-select
v-model:model-value="record.contentType"
:disabled="props.disabled"
:options="Object.values(RequestContentTypeEnum).map((e) => ({ label: e, value: e }))"
allow-create
size="mini"
@ -607,6 +635,7 @@
const paramsLength = computed(() => paramsData.value.length);
function deleteParam(record: Record<string, any>, rowIndex: number) {
if (props.disabled) return;
if (props.isTreeTable) {
emit('treeDelete', record);
return;

View File

@ -0,0 +1,500 @@
<template>
<div class="request-and-response flex-1">
<MsTab
v-model:active-key="requestVModel.activeTab"
:content-tab-list="contentTabList"
:get-text-func="getTabBadge"
no-content
class="relative border-b"
/>
<a-spin class="block h-[300px] w-full p-[16px]" :loading="requestVModel.executeLoading || loading">
<a-spin
v-show="requestVModel.activeTab === RequestComposition.PLUGIN"
:loading="pluginLoading"
class="min-h-[100px] w-full"
>
<MsFormCreate
v-model:api="fApi"
:rule="currentPluginScript"
:option="currentPluginOptions"
@change="
() => {
if (isInitPluginForm) {
handlePluginFormChange();
}
}
"
/>
</a-spin>
<httpHeader
v-if="requestVModel.activeTab === RequestComposition.HEADER"
v-model:params="requestVModel.headers"
:layout="activeLayout"
:disabled="props.disabledExceptParam"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<httpBody
v-else-if="requestVModel.activeTab === RequestComposition.BODY"
v-model:params="requestVModel.body"
:layout="activeLayout"
:disabled="props.disabledExceptParam"
:second-box-height="secondBoxHeight"
:upload-temp-file-api="props.uploadTempFileApi"
:file-save-as-source-id="props.fileSaveAsSourceId"
:file-save-as-api="props.fileSaveAsApi"
:file-module-options-api="props.fileModuleOptionsApi"
@change="handleActiveDebugChange"
/>
<httpQuery
v-else-if="requestVModel.activeTab === RequestComposition.QUERY"
v-model:params="requestVModel.query"
:layout="activeLayout"
:disabled="props.disabledExceptParam"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<httpRest
v-else-if="requestVModel.activeTab === RequestComposition.REST"
v-model:params="requestVModel.rest"
:layout="activeLayout"
:disabled="props.disabledExceptParam"
:second-box-height="secondBoxHeight"
@change="handleActiveDebugChange"
/>
<precondition
v-else-if="requestVModel.activeTab === RequestComposition.PRECONDITION"
v-model:config="requestVModel.children[0].preProcessorConfig"
:disabled="props.disabledExceptParam"
:is-definition="false"
@change="handleActiveDebugChange"
/>
<postcondition
v-else-if="requestVModel.activeTab === RequestComposition.POST_CONDITION"
v-model:config="requestVModel.children[0].postProcessorConfig"
:disabled="props.disabledExceptParam"
:response="requestVModel.response?.requestResults[0]?.responseResult.body"
:layout="activeLayout"
:second-box-height="secondBoxHeight"
:is-definition="false"
@change="handleActiveDebugChange"
/>
<assertion
v-else-if="requestVModel.activeTab === RequestComposition.ASSERTION"
v-model:params="requestVModel.children[0].assertionConfig.assertions"
:disabled="props.disabledExceptParam"
:is-definition="false"
:assertion-config="requestVModel.children[0].assertionConfig"
/>
<auth
v-else-if="requestVModel.activeTab === RequestComposition.AUTH"
v-model:params="requestVModel.authConfig"
:disabled="props.disabledExceptParam"
@change="handleActiveDebugChange"
/>
<setting
v-else-if="requestVModel.activeTab === RequestComposition.SETTING"
v-model:params="requestVModel.otherConfig"
:disabled="props.disabledExceptParam"
@change="handleActiveDebugChange"
/>
</a-spin>
<response
v-model:active-layout="activeLayout"
v-model:active-tab="requestVModel.responseActiveTab"
:is-http-protocol="isHttpProtocol"
:is-priority-local-exec="props.isPriorityLocalExec"
:request-url="requestVModel.url"
:is-expanded="true"
:request-task-result="requestVModel.response"
:is-edit="false"
hide-layout-switch
:upload-temp-file-api="props.uploadTempFileApi"
:loading="requestVModel.executeLoading || loading"
:is-definition="true"
@change="handleActiveDebugChange"
@execute="(val) => emit('execute', val)"
/>
</div>
</template>
<script setup lang="ts">
import { Message, SelectOptionData } from '@arco-design/web-vue';
import { cloneDeep, debounce } from 'lodash-es';
import MsFormCreate from '@/components/pure/ms-form-create/formCreate.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue';
import auth from '@/views/api-test/components/requestComposition/auth.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue';
import precondition from '@/views/api-test/components/requestComposition/precondition.vue';
import response from '@/views/api-test/components/requestComposition/response/index.vue';
import setting from '@/views/api-test/components/requestComposition/setting.vue';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
import { useI18n } from '@/hooks/useI18n';
import { useAppStore } from '@/store';
import { ExecuteConditionConfig, PluginConfig } from '@/models/apiTest/common';
import { ModuleTreeNode, TransferFileParams } from '@/models/common';
import { RequestAuthType, RequestBodyFormat, RequestComposition, RequestConditionProcessor } from '@/enums/apiEnum';
import {
defaultBodyParamsItem,
defaultHeaderParamsItem,
defaultKeyValueParamItem,
defaultRequestParamsItem,
} from '@/views/api-test/components/config';
import { filterKeyValParams, parseRequestBodyFiles } from '@/views/api-test/components/utils';
import type { Api } from '@form-create/arco-design';
// Http
const httpHeader = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/header.vue'));
const httpBody = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/body.vue'));
const httpQuery = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/query.vue'));
const httpRest = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/rest.vue'));
const props = defineProps<{
request?: RequestParam; //
defaultParams?: RequestParam;
detailLoading?: boolean; //
isPriorityLocalExec?: boolean; //
disabledExceptParam?: boolean; //
isShowCommonContentTabKey?: boolean; // tabKey
uploadTempFileApi?: (...args) => Promise<any>; //
fileSaveAsSourceId?: string | number; // id
fileSaveAsApi?: (params: TransferFileParams) => Promise<string>; //
fileModuleOptionsApi?: (projectId: string) => Promise<ModuleTreeNode[]>; //
}>();
const emit = defineEmits<{
(e: 'execute', executeType?: 'localExec' | 'serverExec'): void;
}>();
const appStore = useAppStore();
const { t } = useI18n();
const loading = defineModel<boolean>('detailLoading', { default: false });
const requestVModel = defineModel<RequestParam>('request', { required: true });
const isHttpProtocol = computed(() => requestVModel.value.protocol === 'HTTP');
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const secondBoxHeight = ref(0);
// tabKey
const commonContentTabKey = [
RequestComposition.PRECONDITION,
RequestComposition.POST_CONDITION,
RequestComposition.ASSERTION,
];
// tab
const pluginContentTab = [
{
value: RequestComposition.PLUGIN,
label: t('apiTestDebug.pluginData'),
},
];
// Http tab
const httpContentTabList = [
{
value: RequestComposition.HEADER,
label: t('apiTestDebug.header'),
},
{
value: RequestComposition.BODY,
label: t('apiTestDebug.body'),
},
{
value: RequestComposition.QUERY,
label: RequestComposition.QUERY,
},
{
value: RequestComposition.REST,
label: RequestComposition.REST,
},
{
value: RequestComposition.PRECONDITION,
label: t('apiTestDebug.prefix'),
},
{
value: RequestComposition.POST_CONDITION,
label: t('apiTestDebug.post'),
},
{
value: RequestComposition.ASSERTION,
label: t('apiTestDebug.assertion'),
},
{
value: RequestComposition.AUTH,
label: t('apiTestDebug.auth'),
},
{
value: RequestComposition.SETTING,
label: t('apiTestDebug.setting'),
},
];
// tab
const contentTabList = computed(() => {
// HTTP tabs
if (isHttpProtocol.value) {
return props.isShowCommonContentTabKey
? httpContentTabList
: httpContentTabList.filter((e) => !commonContentTabKey.includes(e.value));
}
return [...pluginContentTab, ...httpContentTabList.filter((e) => commonContentTabKey.includes(e.value))];
});
// tab
function getTabBadge(tabKey: RequestComposition) {
switch (tabKey) {
case RequestComposition.HEADER:
const headerNum = filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem).validParams.length;
return `${headerNum > 0 ? headerNum : ''}`;
case RequestComposition.BODY:
return requestVModel.value.body?.bodyType !== RequestBodyFormat.NONE ? '1' : '';
case RequestComposition.QUERY:
const queryNum = filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem).validParams.length;
return `${queryNum > 0 ? queryNum : ''}`;
case RequestComposition.REST:
const restNum = filterKeyValParams(requestVModel.value.rest, defaultRequestParamsItem).validParams.length;
return `${restNum > 0 ? restNum : ''}`;
case RequestComposition.PRECONDITION:
return `${requestVModel.value.children[0].preProcessorConfig.processors.length || ''}`;
case RequestComposition.POST_CONDITION:
return `${requestVModel.value.children[0].postProcessorConfig.processors.length || ''}`;
case RequestComposition.ASSERTION:
return `${requestVModel.value.children[0].assertionConfig.assertions.length || ''}`;
case RequestComposition.AUTH:
return requestVModel.value.authConfig.authType !== RequestAuthType.NONE ? '1' : '';
default:
return '';
}
}
const protocolLoading = ref(false);
const protocolOptions = ref<SelectOptionData[]>([]);
async function initProtocolList() {
try {
protocolLoading.value = true;
const res = await getProtocolList(appStore.currentOrgId);
protocolOptions.value = res.map((e) => ({
label: e.protocol,
value: e.protocol,
polymorphicName: e.polymorphicName,
pluginId: e.pluginId,
}));
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
protocolLoading.value = false;
}
}
const pluginError = ref(false);
const pluginLoading = ref(false);
const isInitPluginForm = ref(false);
const pluginScriptMap = ref<Record<string, PluginConfig>>({}); //
const temporaryPluginFormMap: Record<string, any> = {}; // tab
const fApi = ref<Api>();
const currentPluginOptions = computed<Record<string, any>>(
() => pluginScriptMap.value[requestVModel.value.protocol]?.options || {}
);
const currentPluginScript = computed<Record<string, any>[]>(
() => pluginScriptMap.value[requestVModel.value.protocol]?.script || []
);
function handleActiveDebugChange() {
if (!loading.value || (!isHttpProtocol.value && isInitPluginForm.value)) {
// change
requestVModel.value.unSaved = true;
}
}
//
const handlePluginFormChange = debounce(() => {
temporaryPluginFormMap[requestVModel.value.id] = fApi.value?.formData();
handleActiveDebugChange();
}, 300);
//
function controlPluginFormFields() {
const allFields = fApi.value?.fields();
let fields: string[] = [];
if (requestVModel.value.useEnv === 'true') {
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDefinitionFields || [];
} else {
fields = pluginScriptMap.value[requestVModel.value.protocol].apiDebugFields || [];
}
fApi.value?.hidden(true, allFields?.filter((e) => !fields.includes(e)) || []);
return fields;
}
//
function setPluginFormData() {
const tempForm = temporaryPluginFormMap[requestVModel.value.id];
if (tempForm || !requestVModel.value.isNew || requestVModel.value.isCopy) {
//
const formData = tempForm || requestVModel.value;
if (fApi.value) {
fApi.value.nextTick(() => {
const form = {};
controlPluginFormFields().forEach((key) => {
form[key] = formData[key];
});
fApi.value?.setValue(form);
setTimeout(() => {
// 300ms handlePluginFormChange
isInitPluginForm.value = true;
}, 300);
});
}
} else {
fApi.value?.nextTick(() => {
controlPluginFormFields();
});
nextTick(() => {
// form-create tab
fApi.value?.resetFields();
});
}
}
async function initPluginScript() {
const pluginId = protocolOptions.value.find((e) => e.value === requestVModel.value.protocol)?.pluginId;
if (!pluginId) {
Message.warning(t('apiTestDebug.noPluginTip'));
pluginError.value = true;
return;
}
pluginError.value = false;
isInitPluginForm.value = false;
if (pluginScriptMap.value[requestVModel.value.protocol] !== undefined) {
//
setPluginFormData();
return;
}
try {
pluginLoading.value = true;
const res = await getPluginScript(pluginId);
pluginScriptMap.value[requestVModel.value.protocol] = res;
setPluginFormData();
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
} finally {
pluginLoading.value = false;
}
}
function filterConditionsSqlValidParams(condition: ExecuteConditionConfig) {
const conditionCopy = cloneDeep(condition);
conditionCopy.processors = conditionCopy.processors.map((processor) => {
if (processor.processorType === RequestConditionProcessor.SQL) {
processor.extractParams = filterKeyValParams(
processor.extractParams || [],
defaultKeyValueParamItem
).validParams;
}
return processor;
});
return conditionCopy;
}
//
function makeRequestParams(executeType?: 'localExec' | 'serverExec') {
const isExecute = executeType === 'localExec' || executeType === 'serverExec';
const { formDataBody, wwwFormBody } = requestVModel.value.body;
const polymorphicName = protocolOptions.value.find(
(e) => e.value === requestVModel.value.protocol
)?.polymorphicName; //
let parseRequestBodyResult;
let requestParams;
if (isHttpProtocol.value) {
const realFormDataBodyValues = filterKeyValParams(
formDataBody.formValues,
defaultBodyParamsItem,
isExecute
).validParams;
const realWwwFormBodyValues = filterKeyValParams(
wwwFormBody.formValues,
defaultBodyParamsItem,
isExecute
).validParams;
parseRequestBodyResult = parseRequestBodyFiles(
requestVModel.value.body,
requestVModel.value.uploadFileIds, //
requestVModel.value.linkFileIds //
);
requestParams = {
authConfig: requestVModel.value.authConfig,
body: {
...requestVModel.value.body,
formDataBody: {
formValues: realFormDataBodyValues,
},
wwwFormBody: {
formValues: realWwwFormBodyValues,
},
},
headers: filterKeyValParams(requestVModel.value.headers, defaultHeaderParamsItem, isExecute).validParams,
method: requestVModel.value.method,
otherConfig: requestVModel.value.otherConfig,
path: requestVModel.value.url || requestVModel.value.path,
query: filterKeyValParams(requestVModel.value.query, defaultRequestParamsItem, isExecute).validParams,
rest: filterKeyValParams(requestVModel.value.rest, defaultRequestParamsItem, isExecute).validParams,
url: requestVModel.value.url,
polymorphicName,
};
} else {
requestParams = {
...fApi.value?.formData(),
polymorphicName,
};
}
const requestName = requestVModel.value.name ?? '';
const requestModuleId = requestVModel.value.moduleId ?? '';
return {
id: requestVModel.value.id.toString(),
name: requestName,
moduleId: requestModuleId,
protocol: requestVModel.value.protocol,
method: isHttpProtocol.value ? requestVModel.value.method : requestVModel.value.protocol,
path: isHttpProtocol.value ? requestVModel.value.url || requestVModel.value.path : undefined,
request: {
...requestParams,
name: requestName,
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: requestVModel.value.children[0].assertionConfig,
postProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].postProcessorConfig),
preProcessorConfig: filterConditionsSqlValidParams(requestVModel.value.children[0].preProcessorConfig),
},
],
},
...parseRequestBodyResult,
projectId: appStore.currentProjectId,
frontendDebug: executeType === 'localExec',
isNew: requestVModel.value.isNew,
};
}
onBeforeMount(() => {
initProtocolList();
});
defineExpose({
initPluginScript,
handleActiveDebugChange,
makeRequestParams,
});
</script>
<style lang="less" scoped>
.request-and-response {
:deep(.response) {
height: 400px; // TODO:
}
}
</style>

View File

@ -1,7 +1,7 @@
<template>
<div class="h-full rounded-[var(--border-radius-small)] border border-[var(--color-text-n8)] p-[16px]">
<div class="mb-[8px]">{{ t('apiTestDebug.authType') }}</div>
<a-radio-group v-model:model-value="authForm.authType" class="mb-[16px]">
<a-radio-group v-model:model-value="authForm.authType" :disabled="props.disabled" class="mb-[16px]">
<a-radio :value="RequestAuthType.NONE">No Auth</a-radio>
<a-radio :value="RequestAuthType.BASIC">Basic Auth</a-radio>
<a-radio :value="RequestAuthType.DIGEST">Digest Auth</a-radio>
@ -10,6 +10,7 @@
<a-form-item :label="t('apiTestDebug.username')">
<a-input
v-model:model-value="authForm.basicAuth.userName"
:disabled="props.disabled"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
:max-length="255"
@ -18,6 +19,7 @@
<a-form-item :label="t('apiTestDebug.password')">
<a-input-password
v-model:model-value="authForm.basicAuth.password"
:disabled="props.disabled"
autocomplete="new-password"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
@ -28,6 +30,7 @@
<a-form-item :label="t('apiTestDebug.username')">
<a-input
v-model:model-value="authForm.digestAuth.userName"
:disabled="props.disabled"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
:max-length="255"
@ -36,6 +39,7 @@
<a-form-item :label="t('apiTestDebug.password')">
<a-input-password
v-model:model-value="authForm.digestAuth.password"
:disabled="props.disabled"
autocomplete="new-password"
:placeholder="t('apiTestDebug.commonPlaceholder')"
class="w-[450px]"
@ -56,6 +60,7 @@
const props = defineProps<{
params: ExecuteAuthConfig;
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:params', val: ExecuteAuthConfig): void;

View File

@ -12,6 +12,7 @@
</a-radio-group>
<batchAddKeyVal
v-if="showParamTable"
:disabled="props.disabled"
:params="currentTableParams"
:default-param-item="defaultBodyParamsItem"
@apply="handleBatchParamApply"
@ -25,6 +26,7 @@
</div>
<paramTable
v-else-if="innerParams.bodyType === RequestBodyFormat.FORM_DATA"
:disabled="props.disabled"
:params="currentTableParams"
:scroll="{ minWidth: 1160 }"
:columns="columns"
@ -40,6 +42,7 @@
/>
<paramTable
v-else-if="innerParams.bodyType === RequestBodyFormat.WWW_FORM"
:disabled="props.disabled"
:params="currentTableParams"
:scroll="{ minWidth: 1160 }"
:columns="columns"
@ -53,11 +56,13 @@
<div class="mb-[16px] flex justify-between gap-[8px] bg-[var(--color-text-n9)] p-[12px]">
<a-input
v-model:model-value="innerParams.binaryBody.description"
:disabled="props.disabled"
:placeholder="t('common.desc')"
:max-length="255"
/>
<MsAddAttachment
v-model:file-list="fileList"
:disabled="props.disabled"
mode="input"
:multiple="false"
:fields="{
@ -85,6 +90,7 @@
<div v-else class="flex h-[calc(100%-34px)]">
<MsCodeEditor
v-model:model-value="currentBodyCode"
:read-only="props.disabled"
class="flex-1"
theme="vs"
height="100%"
@ -121,6 +127,7 @@
const props = defineProps<{
params: ExecuteBody;
layout: 'horizontal' | 'vertical';
disabled?: boolean;
secondBoxHeight: number;
uploadTempFileApi?: (file: File) => Promise<any>; //
fileSaveAsSourceId?: string | number; // id

View File

@ -1,6 +1,7 @@
<template>
<div class="mb-[8px] flex items-center justify-between">
<batchAddKeyVal
:disabled="props.disabled"
:params="innerParams"
:default-param-item="defaultHeaderParamsItem"
no-param-type
@ -9,6 +10,7 @@
</div>
<paramTable
v-model:params="innerParams"
:disabled="props.disabled"
:columns="columns"
:height-used="heightUsed"
:scroll="scroll"
@ -35,6 +37,7 @@
params: EnableKeyValueParam[];
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:selectedKeys', value: string[]): void;

View File

@ -4,11 +4,17 @@
:condition-types="conditionTypes"
add-text="apiTestDebug.postCondition"
:response="props.response"
:disabled="props.disabled"
:height-used="heightUsed"
@change="emit('change')"
>
<template v-if="props.isDefinition" #titleRight>
<a-switch v-model:model-value="innerConfig.enableGlobal" size="small" type="line"></a-switch>
<a-switch
v-model:model-value="innerConfig.enableGlobal"
:disabled="props.disabled"
size="small"
type="line"
></a-switch>
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPostCondition') }}</div>
<a-tooltip :content="t('apiTestDebug.openGlobalPostConditionTip')" position="left">
<icon-question-circle
@ -36,6 +42,7 @@
layout: 'horizontal' | 'vertical';
response?: string; //
isDefinition?: boolean; //
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:params', params: ExecuteConditionProcessor[]): void;

View File

@ -1,12 +1,18 @@
<template>
<condition
v-model:list="innerConfig.processors"
:disabled="props.disabled"
:condition-types="conditionTypes"
add-text="apiTestDebug.precondition"
@change="emit('change')"
>
<template v-if="props.isDefinition" #titleRight>
<a-switch v-model:model-value="innerConfig.enableGlobal" size="small" type="line"></a-switch>
<a-switch
v-model:model-value="innerConfig.enableGlobal"
:disabled="props.disabled"
size="small"
type="line"
></a-switch>
<div class="ml-[8px] text-[var(--color-text-1)]">{{ t('apiTestDebug.openGlobalPrecondition') }}</div>
<a-tooltip :content="t('apiTestDebug.openGlobalPreconditionTip')" position="left">
<icon-question-circle
@ -31,6 +37,7 @@
const props = defineProps<{
config: ExecuteConditionConfig;
isDefinition?: boolean; //
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:config', params: ExecuteConditionConfig): void;

View File

@ -11,12 +11,14 @@
</div>
<batchAddKeyVal
:params="innerParams"
:disabled="props.disabled"
:default-param-item="defaultRequestParamsItem"
@apply="handleBatchParamApply"
/>
</div>
<paramTable
:params="innerParams"
:disabled="props.disabled"
:columns="columns"
:height-used="heightUsed"
:scroll="{ minWidth: 1160 }"
@ -42,6 +44,7 @@
const props = defineProps<{
params: ExecuteRequestCommonParam[];
layout: 'horizontal' | 'vertical';
disabled?: boolean;
secondBoxHeight: number;
}>();
const emit = defineEmits<{

View File

@ -1,5 +1,5 @@
<template>
<div class="flex h-full min-w-[300px] flex-col">
<div class="response flex h-full min-w-[300px] flex-col">
<div :class="['response-head', props.isExpanded ? '' : 'border-t']">
<div class="flex items-center justify-between">
<template v-if="props.activeLayout === 'vertical'">

View File

@ -246,4 +246,7 @@
:deep(.arco-tabs-tab) {
@apply leading-none;
}
.no-content :deep(.arco-tabs-content) {
display: none;
}
</style>

View File

@ -11,6 +11,7 @@
</div>
<batchAddKeyVal
:params="innerParams"
:disabled="props.disabled"
:default-param-item="defaultRequestParamsItem"
@apply="handleBatchParamApply"
/>
@ -18,6 +19,7 @@
<paramTable
:params="innerParams"
:columns="columns"
:disabled="props.disabled"
:height-used="heightUsed"
:scroll="{ minWidth: 1160 }"
:default-param-item="defaultRequestParamsItem"
@ -43,6 +45,7 @@
params: ExecuteRequestCommonParam[];
layout: 'horizontal' | 'vertical';
secondBoxHeight: number;
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:params', value: any[]): void;

View File

@ -11,6 +11,7 @@
</template>
<a-input-number
v-model:model-value="settingForm.connectTimeout"
:disabled="props.disabled"
mode="button"
:step="100"
:min="0"
@ -26,6 +27,7 @@
</template>
<a-input-number
v-model:model-value="settingForm.responseTimeout"
:disabled="props.disabled"
mode="button"
:step="100"
:min="0"
@ -44,6 +46,7 @@
<a-form-item :label="t('apiTestDebug.redirect')">
<a-checkbox
v-model:model-value="settingForm.followRedirects"
:disabled="props.disabled"
@change="(val) => handleFollowRedirectsChange(val as boolean)"
>
{{ t('apiTestDebug.follow') }}
@ -51,6 +54,7 @@
<a-checkbox
v-model:model-value="settingForm.autoRedirects"
class="ml-[24px]"
:disabled="props.disabled"
@change="val => handleAutoRedirectsChange(val as boolean)"
>
{{ t('apiTestDebug.auto') }}
@ -69,6 +73,7 @@
const props = defineProps<{
params: ExecuteOtherConfig;
disabled?: boolean;
}>();
const emit = defineEmits<{
(e: 'update:params', val: ExecuteOtherConfig): void;

View File

@ -8,18 +8,18 @@
no-content-padding
>
<template #tbutton>
<div class="flex items-center gap-[4px]">
<div class="right-operation-button-icon flex items-center gap-[4px]">
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
type="icon"
status="secondary"
@click="caseDerailRef?.editCase()"
>
<MsIcon type="icon-icon_edit_outlined" class="mr-[8px]" />
<MsIcon type="icon-icon_edit_outlined" />
{{ t('common.edit') }}
</MsButton>
<MsButton type="icon" status="secondary" @click="caseDerailRef?.share()">
<MsIcon type="icon-icon_share1" class="mr-[8px]" />
<MsIcon type="icon-icon_share1" />
{{ t('common.share') }}
</MsButton>
<MsButton
@ -30,7 +30,6 @@
>
<MsIcon
:type="props.detail.follow ? 'icon-icon_collect_filled' : 'icon-icon_collection_outlined'"
class="mr-[8px]"
:class="[props.detail.follow ? 'text-[rgb(var(--warning-6))]' : '']"
/>
{{ t('common.fork') }}
@ -38,7 +37,7 @@
<MsButton type="icon" status="secondary">
<a-dropdown position="br" @select="handleSelect">
<div>
<icon-more class="mr-[8px]" />
<icon-more />
<span> {{ t('common.more') }}</span>
</div>
<template #content>

View File

@ -0,0 +1,357 @@
<template>
<MsDrawer
v-model:visible="visible"
unmount-on-close
:mask="false"
:width="900"
:footer="false"
:show-full-screen="!isShowEditStepNameInput"
no-content-padding
@close="handleClose"
>
<template #title>
<stepType v-if="props.activeStep?.type" :type="props.activeStep?.type" class="mr-[4px]" />
<a-input
v-show="isShowEditStepNameInput"
ref="stepNameInputRef"
v-model:model-value="stepName"
class="flex-1"
:placeholder="t('apiScenario.pleaseInputStepName')"
:max-length="255"
show-word-limit
@press-enter="updateStepName"
@blur="updateStepName"
/>
<div v-show="!isShowEditStepNameInput" class="flex flex-1 items-center justify-between">
<div class="flex items-center gap-[8px]">
<a-tooltip :content="stepName">
<span> {{ characterLimit(stepName) }}</span>
</a-tooltip>
<MsIcon type="icon-icon_edit_outlined" class="edit-script-name-icon" @click="showEditScriptNameInput" />
</div>
<div class="right-operation-button-icon flex items-center">
<MsButton v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']" type="icon" status="secondary">
<MsIcon type="icon-icon_swich" />
{{ t('common.replace') }}
</MsButton>
<MsButton
v-permission="['PROJECT_API_DEFINITION_CASE:READ+UPDATE']"
class="mr-4"
type="icon"
status="secondary"
>
<MsIcon type="icon-icon_delete-trash_outlined" />
{{ t('common.delete') }}
</MsButton>
</div>
</div>
</template>
<div class="flex items-center p-[16px]">
<a-input
v-model:model-value="requestVModel.name"
:placeholder="t('apiTestManagement.apiNamePlaceholder')"
allow-clear
:max-length="255"
:show-word-limit="!isQuote"
:disabled="isQuote"
/>
<executeButton
ref="executeRef"
class="ml-[16px]"
is-emit
:detail="requestVModel"
@execute="handleExecute"
@stop-debug="stopDebug"
/>
</div>
<requestAndResponse
ref="requestAndResponseRef"
:detail-loading="loading"
:disabled-except-param="isQuote"
:default-params="defaultCaseParams"
:request="requestVModel"
:is-priority-local-exec="isPriorityLocalExec"
:file-save-as-source-id="requestVModel.id"
:file-module-options-api="getTransferOptionsCase"
:file-save-as-api="transferFileCase"
:upload-temp-file="uploadTempFileCase"
is-show-common-content-tab-key
@execute="handleExecute"
/>
</MsDrawer>
</template>
<script setup lang="ts">
import { useI18n } from 'vue-i18n';
import { InputInstance, Message } from '@arco-design/web-vue';
import { cloneDeep } from 'lodash-es';
import MsButton from '@/components/pure/ms-button/index.vue';
import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import { ScenarioStepItem } from '../step/stepTree.vue';
import stepType from './stepType.vue';
import executeButton from '@/views/api-test/components/executeButton.vue';
import requestAndResponse from '@/views/api-test/components/requestAndResponse.vue';
import { RequestParam } from '@/views/api-test/components/requestComposition/index.vue';
import { localExecuteApiDebug } from '@/api/modules/api-test/common';
import {
debugCase,
getCaseDetail,
getTransferOptionsCase,
runCase,
transferFileCase,
uploadTempFileCase,
} from '@/api/modules/api-test/management';
import { getSocket } from '@/api/modules/project-management/commonScript';
import { getLocalConfig } from '@/api/modules/user/index';
import { characterLimit, getGenerateId } from '@/utils';
import { LocalConfig } from '@/models/user';
import {
RequestAuthType,
RequestComposition,
RequestMethods,
ResponseComposition,
ScenarioStepType,
} from '@/enums/apiEnum';
import { defaultBodyParams, defaultResponse, defaultResponseItem } from '@/views/api-test/components/config';
import { parseRequestBodyFiles } from '@/views/api-test/components/utils';
const props = defineProps<{
activeStep?: ScenarioStepItem;
request?: RequestParam; //
}>();
const emit = defineEmits<{
(e: 'applyStep', request: RequestParam): void;
}>();
const { t } = useI18n();
const visible = defineModel<boolean>('visible', { required: true });
const defaultCaseParams: RequestParam = {
id: `case-${Date.now()}`,
type: 'case',
moduleId: 'root',
protocol: 'HTTP',
tags: [],
description: '',
priority: 'P0',
url: '',
activeTab: RequestComposition.HEADER,
closable: true,
method: RequestMethods.GET,
headers: [],
body: cloneDeep(defaultBodyParams),
query: [],
rest: [],
polymorphicName: '',
name: '',
path: '',
projectId: '',
uploadFileIds: [],
linkFileIds: [],
authConfig: {
authType: RequestAuthType.NONE,
basicAuth: {
userName: '',
password: '',
},
digestAuth: {
userName: '',
password: '',
},
},
children: [
{
polymorphicName: 'MsCommonElement', // MsCommonElement
assertionConfig: {
enableGlobal: false,
assertions: [],
},
postProcessorConfig: {
enableGlobal: false,
processors: [],
},
preProcessorConfig: {
enableGlobal: false,
processors: [],
},
},
],
otherConfig: {
connectTimeout: 60000,
responseTimeout: 60000,
certificateAlias: '',
followRedirects: true,
autoRedirects: false,
},
responseActiveTab: ResponseComposition.BODY,
response: cloneDeep(defaultResponse),
responseDefinition: [cloneDeep(defaultResponseItem)],
isNew: true,
unSaved: false,
executeLoading: false,
preDependency: [], //
postDependency: [], //
};
const isCopyNeedInit = computed(
() => props.activeStep?.type === ScenarioStepType.COPY_CASE && props.request?.request === null
);
const isQuote = computed(() => props.activeStep?.type === ScenarioStepType.QUOTE_CASE);
const stepName = ref(props.activeStep?.name);
watchEffect(() => {
stepName.value = props.activeStep?.name;
});
const requestVModel = ref<RequestParam>(cloneDeep(defaultCaseParams));
const executeRef = ref<InstanceType<typeof executeButton>>();
const requestAndResponseRef = ref<InstanceType<typeof requestAndResponse>>();
const isPriorityLocalExec = computed(() => executeRef.value?.isPriorityLocalExec ?? false);
const apiLocalExec = ref<Record<string, any> | LocalConfig | undefined>({});
async function initLocalConfig() {
try {
const res = await getLocalConfig();
apiLocalExec.value = res.find((e) => e.type === 'API');
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
}
}
provide('apiLocalExec', readonly(apiLocalExec));
const isShowEditStepNameInput = ref(false);
const stepNameInputRef = ref<InputInstance>();
function showEditScriptNameInput() {
isShowEditStepNameInput.value = true;
nextTick(() => {
stepNameInputRef.value?.focus();
});
}
function updateStepName() {
// TODO:
Message.success(t('common.updateSuccess'));
isShowEditStepNameInput.value = false;
}
const reportId = ref('');
const websocket = ref<WebSocket>();
const temporaryResponseMap = {}; // websockettab
// websocket
function debugSocket(executeType?: 'localExec' | 'serverExec') {
websocket.value = getSocket(
reportId.value,
executeType === 'localExec' ? '/ws/debug' : '',
executeType === 'localExec' ? executeRef.value?.localExecuteUrl : ''
);
websocket.value.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.msgType === 'EXEC_RESULT') {
if (requestVModel.value.reportId === data.reportId) {
// tabtab
requestVModel.value.response = data.taskResult; //
requestVModel.value.executeLoading = false;
} else {
// tab
temporaryResponseMap[data.reportId] = data.taskResult;
}
} else if (data.msgType === 'EXEC_END') {
// websocket
websocket.value?.close();
requestVModel.value.executeLoading = false;
}
});
}
async function handleExecute(executeType?: 'localExec' | 'serverExec') {
try {
requestVModel.value.executeLoading = true;
requestVModel.value.response = cloneDeep(defaultResponse);
const makeRequestParams = requestAndResponseRef.value?.makeRequestParams(executeType); // reportIdreportId
reportId.value = getGenerateId();
requestVModel.value.reportId = reportId.value; // ID
debugSocket(executeType); // websocket
let res;
const params = {
...makeRequestParams,
reportId: reportId.value,
};
if (!(requestVModel.value.id as string).startsWith('c') && executeType === 'serverExec') {
//
res = await runCase(params);
} else {
res = await debugCase(params);
}
if (executeType === 'localExec') {
await localExecuteApiDebug(executeRef.value?.localExecuteUrl ?? '', res);
}
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
requestVModel.value.executeLoading = false;
}
}
function stopDebug() {
websocket.value?.close();
requestVModel.value.executeLoading = false;
}
function handleClose() {
emit('applyStep', requestVModel.value);
}
const loading = ref(false);
async function initQuoteCaseDetail() {
try {
loading.value = true;
const res = await getCaseDetail(requestVModel.value.id as string);
let parseRequestBodyResult;
if (res.protocol === 'HTTP') {
parseRequestBodyResult = parseRequestBodyFiles(res.request.body); // id
}
requestVModel.value = {
responseActiveTab: ResponseComposition.BODY,
executeLoading: false,
activeTab: res.protocol === 'HTTP' ? RequestComposition.HEADER : RequestComposition.PLUGIN,
unSaved: false,
isNew: false,
label: res.name,
...res.request,
...res,
response: cloneDeep(defaultResponse),
url: res.path,
name: res.name, // requestnamenull
id: res.id,
...parseRequestBodyResult,
};
nextTick(() => {
// loading
loading.value = false;
});
} catch (error) {
// eslint-disable-next-line no-console
console.log(error);
loading.value = false;
}
}
watch(
() => visible.value,
async (val) => {
if (val) {
if (props.request) {
requestVModel.value = { ...cloneDeep(defaultCaseParams), ...props.request };
if (isQuote.value || isCopyNeedInit.value) {
// (request.requestrequest null)
initQuoteCaseDetail();
}
}
await initLocalConfig();
}
}
);
</script>

View File

@ -195,6 +195,11 @@
@add-step="addCustomApiStep"
@apply-step="applyApiStep"
/>
<customCaseDrawer
v-model:visible="customCaseDrawerVisible"
:active-step="activeStep"
:request="currentStepDetail"
/>
<importApiDrawer
v-if="importApiDrawerVisible"
v-model:visible="importApiDrawerVisible"
@ -283,6 +288,7 @@
//
const MsCodeEditor = defineAsyncComponent(() => import('@/components/pure/ms-code-editor/index.vue'));
const customApiDrawer = defineAsyncComponent(() => import('../common/customApiDrawer.vue'));
const customCaseDrawer = defineAsyncComponent(() => import('../common/customCaseDrawer.vue'));
const importApiDrawer = defineAsyncComponent(() => import('../common/importApiDrawer/index.vue'));
const scriptOperationDrawer = defineAsyncComponent(() => import('../common/scriptOperationDrawer.vue'));
@ -577,6 +583,7 @@
}
const importApiDrawerVisible = ref(false);
const customCaseDrawerVisible = ref(false);
const customApiDrawerVisible = ref(false);
const scriptOperationDrawerVisible = ref(false);
const activeStep = ref<ScenarioStepItem>(); //
@ -604,6 +611,9 @@
if ([ScenarioStepType.CUSTOM_API, ScenarioStepType.QUOTE_API, ScenarioStepType.COPY_API].includes(step.type)) {
activeStep.value = step;
customApiDrawerVisible.value = true;
} else if ([ScenarioStepType.QUOTE_CASE, ScenarioStepType.COPY_CASE].includes(step.type)) {
activeStep.value = step;
customCaseDrawerVisible.value = true;
} else if (step.type === ScenarioStepType.SCRIPT_OPERATION) {
activeStep.value = step;
scriptOperationDrawerVisible.value = true;