feat(接口测试): 场景脚本断言&响应内容&样式调整

This commit is contained in:
baiqi 2024-08-06 14:27:22 +08:00 committed by 刘瑞斌
parent de429a90fb
commit 5c9162f898
10 changed files with 162 additions and 90 deletions

View File

@ -2,6 +2,7 @@
<div class="ms-detail-card"> <div class="ms-detail-card">
<div class="ms-detail-card-title"> <div class="ms-detail-card-title">
<div class="flex flex-1 items-center gap-[8px]"> <div class="flex flex-1 items-center gap-[8px]">
<slot name="titlePrefix"></slot>
<a-tooltip :content="t(props.title)"> <a-tooltip :content="t(props.title)">
<div class="one-line-text max-w-[300px] font-medium text-[var(--color-text-1)]"> <div class="one-line-text max-w-[300px] font-medium text-[var(--color-text-1)]">
{{ t(props.title) }} {{ t(props.title) }}

View File

@ -233,8 +233,6 @@ export interface ExecuteConditionProcessorCommon {
beforeStepScript: boolean; // 是否是步骤内前置脚本前 beforeStepScript: boolean; // 是否是步骤内前置脚本前
assertionType?: RequestConditionProcessor; assertionType?: RequestConditionProcessor;
} }
// 执行请求-前后置操作-脚本处理器
export type ScriptProcessor = ScriptCommonConfig & ExecuteConditionProcessorCommon;
// 执行请求-前后置操作-SQL脚本处理器 // 执行请求-前后置操作-SQL脚本处理器
export interface SQLProcessor extends ExecuteConditionProcessorCommon { export interface SQLProcessor extends ExecuteConditionProcessorCommon {
name: string; // 描述 name: string; // 描述
@ -278,16 +276,6 @@ export interface XPathExtract extends ExpressionCommonConfig {
export interface ExtractProcessor extends ExecuteConditionProcessorCommon { export interface ExtractProcessor extends ExecuteConditionProcessorCommon {
extractors: (RegexExtract | JSONPathExtract | XPathExtract)[]; extractors: (RegexExtract | JSONPathExtract | XPathExtract)[];
} }
// 执行请求-前后置操作配置
export type ExecuteConditionProcessor = Partial<
ScriptProcessor & SQLProcessor & TimeWaitingProcessor & ExtractProcessor
> &
ExecuteConditionProcessorCommon;
export interface ExecuteConditionConfig {
enableGlobal?: boolean; // 是否启用全局前/后置 默认为 true
processors: ExecuteConditionProcessor[];
activeItemId?: number;
}
// 执行请求-断言配置子项 // 执行请求-断言配置子项
export type ExecuteAssertionItem = ResponseAssertionCommon & export type ExecuteAssertionItem = ResponseAssertionCommon &
ResponseCodeAssertion & ResponseCodeAssertion &
@ -301,6 +289,24 @@ export interface ExecuteAssertionConfig {
enableGlobal?: boolean; // 是否启用全局断言,部分地方没有 enableGlobal?: boolean; // 是否启用全局断言,部分地方没有
assertions: ExecuteAssertionItem[]; assertions: ExecuteAssertionItem[];
} }
// 执行请求-前后置操作-脚本处理器
export interface ScriptProcessorChild {
polymorphicName: string; // 协议多态名称写死MsCommonElement
assertionConfig: ExecuteAssertionConfig;
}
export interface ScriptProcessor extends ScriptCommonConfig, ExecuteConditionProcessorCommon {
children: ScriptProcessorChild[]; // 协议共有的子项配置
}
// 执行请求-前后置操作配置
export type ExecuteConditionProcessor = Partial<
ScriptProcessor & SQLProcessor & TimeWaitingProcessor & ExtractProcessor
> &
ExecuteConditionProcessorCommon;
export interface ExecuteConditionConfig {
enableGlobal?: boolean; // 是否启用全局前/后置 默认为 true
processors: ExecuteConditionProcessor[];
activeItemId?: number;
}
// 执行请求-共用配置子项 // 执行请求-共用配置子项
export interface ExecuteCommonChild { export interface ExecuteCommonChild {
polymorphicName: string; // 协议多态名称写死MsCommonElement polymorphicName: string; // 协议多态名称写死MsCommonElement

View File

@ -94,7 +94,7 @@
> >
<template #content> <template #content>
<div class="ms-params-popover-title"> <div class="ms-params-popover-title">
{{ t('apiTestDebug.paramName') }} {{ t(columnConfig.title as string) }}
</div> </div>
<div class="ms-params-popover-value"> <div class="ms-params-popover-value">
{{ record[columnConfig.dataIndex as string] }} {{ record[columnConfig.dataIndex as string] }}

View File

@ -1,7 +1,7 @@
<template> <template>
<div <div
v-if="props.requestResult?.responseResult?.responseCode" v-if="props.requestResult?.responseResult?.responseCode"
class="flex items-center justify-between gap-[24px] text-[14px]" class="flex items-center justify-between gap-[16px] text-[14px]"
> >
<a-tooltip :content="props.requestResult.fakeErrorCode" :disabled="!props.requestResult.fakeErrorCode"> <a-tooltip :content="props.requestResult.fakeErrorCode" :disabled="!props.requestResult.fakeErrorCode">
<executeStatus :status="finalStatus" size="small" class="ml-[4px]" /> <executeStatus :status="finalStatus" size="small" class="ml-[4px]" />

View File

@ -1,8 +1,11 @@
<template> <template>
<div v-show="props.requestResult?.responseResult.responseCode" class="h-full"> <div v-show="props.requestResult?.responseResult.responseCode" class="h-full">
<a-tabs v-model:active-key="activeTab" class="no-content border-b border-[var(--color-text-n8)]"> <div class="flex items-center" :class="$slots.tabRight ? 'border-b border-[var(--color-text-n8)]' : ''">
<a-tabs v-model:active-key="activeTab" class="no-content flex-1">
<a-tab-pane v-for="item of responseCompositionTabList" :key="item.value" :title="item.label" /> <a-tab-pane v-for="item of responseCompositionTabList" :key="item.value" :title="item.label" />
</a-tabs> </a-tabs>
<slot name="tabRight"></slot>
</div>
<div class="response-container"> <div class="response-container">
<ResBody <ResBody
v-if="activeTab === ResponseComposition.BODY" v-if="activeTab === ResponseComposition.BODY"
@ -138,7 +141,7 @@
<style lang="less" scoped> <style lang="less" scoped>
.response-container { .response-container {
margin-top: 8px; margin-top: 8px;
height: calc(100% - 48px); height: calc(100% - 58px);
} }
:deep(.arco-table-th) { :deep(.arco-table-th) {
background-color: var(--color-text-n9); background-color: var(--color-text-n9);

View File

@ -360,7 +360,6 @@
import { TabErrorMessage } from '@/views/api-test/components/requestComposition/index.vue'; import { TabErrorMessage } from '@/views/api-test/components/requestComposition/index.vue';
import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue'; import postcondition from '@/views/api-test/components/requestComposition/postcondition.vue';
import precondition from '@/views/api-test/components/requestComposition/precondition.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 setting from '@/views/api-test/components/requestComposition/setting.vue';
import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common'; import { getPluginScript, getProtocolList } from '@/api/modules/api-test/common';
@ -416,6 +415,9 @@
const httpBody = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/body.vue')); const httpBody = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/body.vue'));
const httpQuery = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/query.vue')); const httpQuery = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/query.vue'));
const httpRest = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/rest.vue')); const httpRest = defineAsyncComponent(() => import('@/views/api-test/components/requestComposition/rest.vue'));
const response = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/index.vue')
);
// const addDependencyDrawer = defineAsyncComponent( // const addDependencyDrawer = defineAsyncComponent(
// () => import('@/views/api-test/management/components/addDependencyDrawer.vue') // () => import('@/views/api-test/management/components/addDependencyDrawer.vue')
// ); // );

View File

@ -21,31 +21,17 @@
<div class="flex h-full flex-col"> <div class="flex h-full flex-col">
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" /> <loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
<div class="flex-1 overflow-y-hidden"> <div class="flex-1 overflow-y-hidden">
<div v-if="step.stepType === ScenarioStepType.SCRIPT" class="flex h-full flex-col p-[8px]"> <div class="response-result">
<div class="mb-[8px] flex gap-[8px] text-[14px] font-medium text-[var(--color-text-1)]">
{{ t('apiScenario.executionResult') }}
<div class="one-line-text text-[var(--color-text-4)]">({{ step.name }})</div>
</div>
<div class="flex-1 bg-[var(--color-text-n9)] p-[12px]">
<pre class="response-header-pre">{{ currentResponse?.console }}</pre>
</div>
</div>
<div v-else class="response-result">
<responseResult <responseResult
:active-tab="ResponseComposition.BODY" :active-tab="ResponseComposition.BODY"
:request-result="currentResponse" :request-result="currentResponse"
:console="currentResponse?.console" :console="currentResponse?.console"
:show-empty="false" :show-empty="false"
:is-edit="false" :is-priority-local-exec="false"
is-definition is-definition
> >
<template #titleLeft> <template #tabRight>
<div class="flex items-center text-[14px]"> <responseCodeTimeSize :request-result="currentResponse" />
<div class="font-medium text-[var(--color-text-1)]">{{ t('apiScenario.response') }}</div>
<a-tooltip :content="props.step.name">
<div class="one-line-text">({{ props.step.name }})</div>
</a-tooltip>
</div>
</template> </template>
</responseResult> </responseResult>
</div> </div>
@ -66,8 +52,6 @@
import executeStatus from './executeStatus.vue'; import executeStatus from './executeStatus.vue';
import loopPagination from './loopPagination.vue'; import loopPagination from './loopPagination.vue';
import { useI18n } from '@/hooks/useI18n';
import { RequestResult } from '@/models/apiTest/common'; import { RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario'; import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { import {
@ -78,7 +62,10 @@
} from '@/enums/apiEnum'; } from '@/enums/apiEnum';
const responseResult = defineAsyncComponent( const responseResult = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/index.vue') () => import('@/views/api-test/components/requestComposition/response/result.vue')
);
const responseCodeTimeSize = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/responseCodeTimeSize.vue')
); );
const props = defineProps<{ const props = defineProps<{
@ -88,8 +75,6 @@
}>(); }>();
const emit = defineEmits(['visibleChange']); const emit = defineEmits(['visibleChange']);
const { t } = useI18n();
const currentLoop = ref(1); const currentLoop = ref(1);
const currentResponse = computed(() => props.stepResponses?.[props.step.uniqueId]?.[currentLoop.value - 1]); const currentResponse = computed(() => props.stepResponses?.[props.step.uniqueId]?.[currentLoop.value - 1]);
const loopTotal = computed(() => props.stepResponses?.[props.step.uniqueId]?.length || 0); const loopTotal = computed(() => props.stepResponses?.[props.step.uniqueId]?.length || 0);
@ -119,10 +104,11 @@
<style lang="less"> <style lang="less">
.scenario-step-response-popover { .scenario-step-response-popover {
width: 540px; padding: 8px 16px;
height: 500px; width: 640px;
height: 440px;
.arco-popover-content { .arco-popover-content {
@apply h-full; @apply mt-0 h-full;
.response-header-pre { .response-header-pre {
@apply h-full overflow-auto bg-white; @apply h-full overflow-auto bg-white;
.ms-scroll-bar(); .ms-scroll-bar();
@ -133,6 +119,9 @@
.response-result { .response-result {
@apply h-full overflow-auto bg-white; @apply h-full overflow-auto bg-white;
.ms-scroll-bar(); .ms-scroll-bar();
.arco-tabs-tab:first-child {
margin-left: 0;
}
.response { .response {
.response-head { .response-head {
background-color: var(--color-text-n9); background-color: var(--color-text-n9);

View File

@ -10,6 +10,13 @@
@cancel="handleDrawerCancel" @cancel="handleDrawerCancel"
> >
<div class="scenario-script-drawer-content"> <div class="scenario-script-drawer-content">
<MsTab
v-model:active-key="activeTab"
:content-tab-list="contentTabList"
:get-text-func="() => ''"
class="sticky-content no-content relative top-0 border-b px-[16px]"
/>
<template v-if="activeTab === 'script'">
<div class="ml-[16px] mt-[10px]"> <div class="ml-[16px] mt-[10px]">
<!-- <stepTypeVue v-if="props.step" :step="props.step" /> --> <!-- <stepTypeVue v-if="props.step" :step="props.step" /> -->
{{ t('apiScenario.scriptOperationName') }} {{ t('apiScenario.scriptOperationName') }}
@ -25,7 +32,7 @@
<div class="mt-[10px] flex flex-1 gap-[8px]"> <div class="mt-[10px] flex flex-1 gap-[8px]">
<conditionContent <conditionContent
v-if="visible" v-if="visible"
v-model:data="activeItem" v-model:data="scriptVModel"
condition-type="scenario" condition-type="scenario"
:disabled="isReadonly" :disabled="isReadonly"
:is-build-in="true" :is-build-in="true"
@ -33,13 +40,41 @@
@change="unSaved = true" @change="unSaved = true"
/> />
</div> </div>
<div v-if="currentResponse?.console" class="p-[8px]"> <!-- <div v-if="currentResponse?.console" class="p-[8px]">
<div class="mb-[8px] font-medium text-[var(--color-text-1)]">{{ t('apiScenario.executionResult') }}</div> <div class="mb-[8px] font-medium text-[var(--color-text-1)]">{{ t('apiScenario.executionResult') }}</div>
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" /> <loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
<div class="h-[300px] bg-[var(--color-text-n9)] p-[12px]"> <div class="h-[300px] bg-[var(--color-text-n9)] p-[12px]">
<pre class="response-header-pre">{{ currentResponse?.console }}</pre> <pre class="response-header-pre">{{ currentResponse?.console }}</pre>
</div> </div>
</div> -->
</template>
<div v-else-if="scriptVModel.children" class="p-[16px]">
<assertion
v-model:params="scriptVModel.children[0].assertionConfig.assertions"
is-definition
:disabled="isReadonly"
:assertion-config="scriptVModel.children[0].assertionConfig"
/>
</div> </div>
<response
v-if="visible && currentResponse && currentResponse.responseResult.responseCode"
ref="responseRef"
v-model:active-layout="activeLayout"
v-model:active-tab="responseActiveTab"
class="response"
:is-http-protocol="false"
:is-priority-local-exec="isPriorityLocalExec"
request-url=""
:is-expanded="isVerticalExpanded"
:request-result="currentResponse"
:console="currentResponse?.console"
:is-edit="false"
hide-layout-switch
>
<template #titleRight>
<loopPagination v-model:current-loop="currentLoop" :loop-total="loopTotal" />
</template>
</response>
</div> </div>
<template v-if="!props.detail" #footer> <template v-if="!props.detail" #footer>
<a-button type="secondary" @click="handleDrawerCancel"> <a-button type="secondary" @click="handleDrawerCancel">
@ -60,6 +95,8 @@
import { LanguageEnum } from '@/components/pure/ms-code-editor/types'; import { LanguageEnum } from '@/components/pure/ms-code-editor/types';
import MsDrawer from '@/components/pure/ms-drawer/index.vue'; import MsDrawer from '@/components/pure/ms-drawer/index.vue';
import MsTab from '@/components/pure/ms-tab/index.vue';
import assertion from '@/components/business/ms-assertion/index.vue';
import loopPagination from './loopPagination.vue'; import loopPagination from './loopPagination.vue';
// import stepTypeVue from './stepType/stepType.vue'; // import stepTypeVue from './stepType/stepType.vue';
@ -67,9 +104,12 @@
import { ExecuteConditionProcessor, RequestResult } from '@/models/apiTest/common'; import { ExecuteConditionProcessor, RequestResult } from '@/models/apiTest/common';
import { ScenarioStepItem } from '@/models/apiTest/scenario'; import { ScenarioStepItem } from '@/models/apiTest/scenario';
import { RequestConditionProcessor } from '@/enums/apiEnum'; import { RequestConditionProcessor, ResponseComposition } from '@/enums/apiEnum';
const conditionContent = defineAsyncComponent(() => import('@/views/api-test/components/condition/content.vue')); const conditionContent = defineAsyncComponent(() => import('@/views/api-test/components/condition/content.vue'));
const response = defineAsyncComponent(
() => import('@/views/api-test/components/requestComposition/response/index.vue')
);
const props = defineProps<{ const props = defineProps<{
detail?: ExecuteConditionProcessor; detail?: ExecuteConditionProcessor;
@ -89,9 +129,17 @@
scriptLanguage: LanguageEnum.BEANSHELL_JSR233, scriptLanguage: LanguageEnum.BEANSHELL_JSR233,
commonScriptInfo: {}, commonScriptInfo: {},
polymorphicName: 'MsScriptElement', polymorphicName: 'MsScriptElement',
children: [
{
polymorphicName: 'MsCommonElement',
assertionConfig: {
assertions: [],
},
},
],
} as unknown as ExecuteConditionProcessor; } as unknown as ExecuteConditionProcessor;
const scriptName = ref(''); const scriptName = ref('');
const activeItem = ref<ExecuteConditionProcessor>(cloneDeep(defaultScript)); const scriptVModel = ref<ExecuteConditionProcessor>(cloneDeep(defaultScript));
const { t } = useI18n(); const { t } = useI18n();
@ -105,13 +153,17 @@
} }
}); });
const loopTotal = computed(() => (props.step?.uniqueId && props.stepResponses?.[props.step?.uniqueId]?.length) || 0); const loopTotal = computed(() => (props.step?.uniqueId && props.stepResponses?.[props.step?.uniqueId]?.length) || 0);
const activeLayout = ref<'horizontal' | 'vertical'>('vertical');
const responseActiveTab = ref<ResponseComposition>(ResponseComposition.BODY);
const isPriorityLocalExec = inject<Ref<boolean>>('isPriorityLocalExec');
const isVerticalExpanded = computed(() => activeLayout.value === 'vertical');
watch( watch(
() => visible.value, () => visible.value,
(val) => { (val) => {
if (val) { if (val) {
scriptName.value = props.detail ? props.name || '' : ''; scriptName.value = props.detail ? props.name || '' : '';
activeItem.value = cloneDeep( scriptVModel.value = cloneDeep(
props.detail props.detail
? { ? {
...props.detail, ...props.detail,
@ -124,25 +176,38 @@
} }
); );
const activeTab = ref<'script' | 'assertion'>('script');
const contentTabList = [
{
value: 'script',
label: t('apiTestManagement.script'),
},
{
value: 'assertion',
label: t('apiTestDebug.assertion'),
},
];
function handleDrawerCancel() { function handleDrawerCancel() {
visible.value = false; visible.value = false;
} }
function saveAndContinue() { function saveAndContinue() {
emit('add', scriptName.value, activeItem.value); emit('add', scriptName.value, scriptVModel.value);
} }
function save() { function save() {
emit('add', scriptName.value, activeItem.value); emit('add', scriptName.value, scriptVModel.value);
visible.value = false; visible.value = false;
} }
function handleClose() { function handleClose() {
if (props.detail) { if (props.detail) {
emit('save', scriptName.value, activeItem.value, unSaved.value); emit('save', scriptName.value, scriptVModel.value, unSaved.value);
} }
scriptName.value = ''; scriptName.value = '';
activeItem.value = defaultScript as unknown as ExecuteConditionProcessor; scriptVModel.value = defaultScript as unknown as ExecuteConditionProcessor;
activeTab.value = 'script';
} }
</script> </script>
@ -156,6 +221,9 @@
</style> </style>
<style lang="less" scoped> <style lang="less" scoped>
:deep(.arco-tabs-tab:first-child) {
margin-left: 0;
}
.response-header-pre { .response-header-pre {
@apply h-full overflow-auto bg-white; @apply h-full overflow-auto bg-white;
.ms-scroll-bar(); .ms-scroll-bar();
@ -163,4 +231,9 @@
padding: 8px 12px; padding: 8px 12px;
border-radius: var(--border-radius-small); border-radius: var(--border-radius-small);
} }
.sticky-content {
@apply sticky overflow-visible bg-white;
z-index: 101;
}
</style> </style>

View File

@ -48,8 +48,9 @@
<template v-if="scenario.executeTime"> <template v-if="scenario.executeTime">
<div class="action-group"> <div class="action-group">
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeTime') }}</div> <div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeTime') }}</div>
<div class="text-[var(--color-text-4)]">{{ scenario.executeTime }}</div> <div class="text-[var(--color-text-1)]">{{ scenario.executeTime }}</div>
</div> </div>
<a-divider direction="vertical" :margin="8"></a-divider>
<div class="action-group"> <div class="action-group">
<div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeResult') }}</div> <div class="text-[var(--color-text-4)]">{{ t('apiScenario.executeResult') }}</div>
<div class="flex items-center gap-[4px]"> <div class="flex items-center gap-[4px]">
@ -67,8 +68,10 @@
<MsButton <MsButton
v-if="scenario.isDebug === false && !scenario.executeLoading && !scenario.isNew" v-if="scenario.isDebug === false && !scenario.executeLoading && !scenario.isNew"
type="text" type="text"
class="ml-[8px]"
@click="checkReport" @click="checkReport"
> >
<icon-eye class="mr-[4px] text-[rgb(var(--primary-5))]" />
{{ t('apiScenario.checkReport') }} {{ t('apiScenario.checkReport') }}
</MsButton> </MsButton>
</div> </div>

View File

@ -2,6 +2,10 @@
<div class="h-full w-full overflow-hidden"> <div class="h-full w-full overflow-hidden">
<div class="px-[24px] pt-[16px]"> <div class="px-[24px] pt-[16px]">
<MsDetailCard :title="`【${scenario.num}】${scenario.name}`" :description="description" class="!py-[8px]"> <MsDetailCard :title="`【${scenario.num}】${scenario.name}`" :description="description" class="!py-[8px]">
<template #titlePrefix>
<apiStatus :status="scenario.status" size="small" />
<caseLevel :case-level="scenario.priority as CaseLevel" />
</template>
<template #titleAppend> <template #titleAppend>
<a-tooltip :content="t(scenario.follow ? 'common.forked' : 'common.notForked')"> <a-tooltip :content="t(scenario.follow ? 'common.forked' : 'common.notForked')">
<MsIcon <MsIcon
@ -21,10 +25,6 @@
@click="share" @click="share"
/> />
</a-tooltip> </a-tooltip>
<apiStatus :status="scenario.status" size="small" />
</template>
<template #priority="{ value }">
<caseLevel :case-level="value as CaseLevel" />
</template> </template>
</MsDetailCard> </MsDetailCard>
</div> </div>
@ -190,11 +190,6 @@
}); });
const description = computed(() => [ const description = computed(() => [
{
key: 'priority',
locale: 'apiScenario.scenarioLevel',
value: scenario.value.priority,
},
{ {
key: 'tag', key: 'tag',
locale: 'common.tag', locale: 'common.tag',