feat(接口测试): 定义新的组件

This commit is contained in:
Captain.B 2020-08-13 13:34:43 +08:00
parent ad4709e502
commit 6461c1a6a6
5 changed files with 571 additions and 422 deletions

View File

@ -11,7 +11,7 @@
<ms-dropdown :default-command="body.format" v-if="body.type == 'Raw'" :commands="modes" @command="modeChange"/> <ms-dropdown :default-command="body.format" v-if="body.type == 'Raw'" :commands="modes" @command="modeChange"/>
<ms-api-key-value :is-read-only="isReadOnly" :items="body.kvs" v-if="body.isKV()"/> <ms-api-variable :is-read-only="isReadOnly" :items="body.kvs" v-if="body.isKV()"/>
<div class="body-raw" v-if="body.type == 'Raw'"> <div class="body-raw" v-if="body.type == 'Raw'">
<ms-code-edit :mode="body.format" :read-only="isReadOnly" :data.sync="body.raw" :modes="modes" ref="codeEdit"/> <ms-code-edit :mode="body.format" :read-only="isReadOnly" :data.sync="body.raw" :modes="modes" ref="codeEdit"/>
@ -25,10 +25,11 @@
import {Body, BODY_FORMAT, BODY_TYPE} from "../model/ScenarioModel"; import {Body, BODY_FORMAT, BODY_TYPE} from "../model/ScenarioModel";
import MsCodeEdit from "../../../common/components/MsCodeEdit"; import MsCodeEdit from "../../../common/components/MsCodeEdit";
import MsDropdown from "../../../common/components/MsDropdown"; import MsDropdown from "../../../common/components/MsDropdown";
import MsApiVariable from "@/business/components/api/test/components/ApiVariable";
export default { export default {
name: "MsApiBody", name: "MsApiBody",
components: {MsDropdown, MsCodeEdit, MsApiKeyValue}, components: {MsApiVariable, MsDropdown, MsCodeEdit, MsApiKeyValue},
props: { props: {
body: Body, body: Body,
isReadOnly: { isReadOnly: {

View File

@ -15,18 +15,8 @@
</el-col> </el-col>
<el-col> <el-col>
<el-autocomplete <el-input :disabled="isReadOnly" v-model="item.value" size="small" @change="change"
:disabled="isReadOnly" :placeholder="valueText" show-word-limit/>
size="small"
class="input-with-autocomplete"
v-model="item.value"
:fetch-suggestions="funcSearch"
:placeholder="valueText"
value-key="name"
highlight-first-item
@select="change">
<i slot="suffix" class="el-input__icon el-icon-edit" style="cursor: pointer;" @click="advanced(item)"></i>
</el-autocomplete>
</el-col> </el-col>
<el-col class="kv-delete"> <el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)" <el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
@ -34,82 +24,11 @@
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
<el-dialog :title="$t('api_test.request.parameters_advance')"
:visible.sync="itemValueVisible"
class="advanced-item-value"
width="70%">
<el-tabs tab-position="top" style="height: 50vh;">
<el-tab-pane :label="$t('api_test.request.parameters_advance_mock')">
<el-row type="flex" :gutter="20" style="overflow-x: auto;">
<el-col :span="6">
<el-autocomplete
:disabled="isReadOnly"
size="small"
class="input-with-autocomplete"
v-model="itemValue"
:fetch-suggestions="funcSearch"
:placeholder="valueText"
value-key="name"
highlight-first-item
@select="change">
</el-autocomplete>
</el-col>
<el-col :span="6" v-for="(itemFunc, itemIndex) in itemFuncs" :key="itemIndex">
<div v-for="(func, funcIndex) in funcs"
:key="`${itemIndex}-${funcIndex}`">
<el-row>
<el-col :span="12">
<el-radio size="mini" v-model="itemFunc.name" :label="func.name"
@change="methodChange(itemFunc, func)"/>
</el-col>
<el-col :span="12" v-if="itemFunc.name === func.name">
<div v-for="(p, pIndex) in itemFunc.params" :key="`${itemIndex}-${funcIndex}-${pIndex}`">
<el-input :placeholder="p.name" size="mini" v-model="p.value" @change="showPreview"/>
</div>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="变量">
<el-row>
<el-col :span="6">
环境
场景
请求1
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
<div style="padding-top: 10px;">
<el-row type="flex" align="middle">
<el-col :span="12">
<el-button size="small" type="primary" plain @click="saveAdvanced()">
{{ $t('commons.save') }}
</el-button>
<el-button size="small" type="info" plain @click="addFunc()">
{{ $t('api_test.request.parameters_advance_add_func') }}
</el-button>
<el-button size="small" type="success" plain @click="showPreview()">
{{ $t('api_test.request.parameters_preview') }}
</el-button>
</el-col>
<el-col>
<div> {{ itemValuePreview }}</div>
</el-col>
</el-row>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import {KeyValue} from "../model/ScenarioModel"; import {KeyValue} from "../model/ScenarioModel";
import {MOCKJS_FUNC} from "@/common/js/constants";
import {calculate} from "@/business/components/api/test/model/ScenarioModel";
export default { export default {
name: "MsApiKeyValue", name: "MsApiKeyValue",
@ -126,40 +45,6 @@ export default {
suggestions: Array suggestions: Array
}, },
data() {
return {
itemValueVisible: false,
itemValue: null,
funcs: [
{name: "md5"},
{name: "base64"},
{name: "unbase64"},
{
name: "substr",
params: [{name: "start"}, {name: "length"}]
},
{
name: "concat",
params: [{name: "suffix"}]
},
{name: "lconcat", params: [{name: "prefix"}]},
{name: "sha1"},
{name: "sha224"},
{name: "sha256"},
{name: "sha384"},
{name: "sha512"},
{name: "lower"},
{name: "upper"},
{name: "length"},
{name: "number"}
],
itemValuePreview: null,
itemFuncs: [],
currentFunc: "",
mockFuncs: MOCKJS_FUNC,
}
},
computed: { computed: {
keyText() { keyText() {
return this.keyPlaceholder || this.$t("api_test.key"); return this.keyPlaceholder || this.$t("api_test.key");
@ -206,80 +91,6 @@ export default {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0); return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
}; };
}, },
funcSearch(queryString, cb) {
let funcs = MOCKJS_FUNC;
let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs;
// callback
cb(results);
},
funcFilter(queryString) {
return (func) => {
return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1);
};
},
showPreview() {
//
if (!this.itemValue) {
return;
}
let index = this.itemValue.indexOf("|");
if (index > -1) {
this.itemValue = this.itemValue.substring(0, index).trim();
}
this.itemFuncs.forEach(f => {
if (!f.name) {
return;
}
this.itemValue += "|" + f.name;
if (f.params) {
this.itemValue += ":" + f.params.map(p => p.value).join(",");
}
});
this.itemValuePreview = calculate(this.itemValue);
},
methodChange(itemFunc, func) {
let index = this.itemFuncs.indexOf(itemFunc);
this.itemFuncs = this.itemFuncs.slice(0, index);
// deep copy
this.itemFuncs.push(JSON.parse(JSON.stringify(func)));
this.showPreview();
},
addFunc() {
if (this.itemFuncs.length > 4) {
this.$info(this.$t('api_test.request.parameters_advance_add_func_limit'));
return;
}
if (this.itemFuncs.length > 0) {
let func = this.itemFuncs[this.itemFuncs.length - 1];
if (!func.name) {
this.$warning(this.$t('api_test.request.parameters_advance_add_func_error'));
return;
}
if (func.params) {
for (let j = 0; j < func.params.length; j++) {
if (!func.params[j].value) {
this.$warning(this.$t('api_test.request.parameters_advance_add_param_error'));
return;
}
}
}
}
this.itemFuncs.push({name: '', params: []});
},
advanced(item) {
this.currentItem = item;
this.itemValueVisible = true;
this.itemValue = '';
this.itemValuePreview = null;
this.itemFuncs = [];
},
saveAdvanced() {
this.currentItem.value = this.itemValue;
this.itemValueVisible = false;
this.itemFuncs = [];
}
}, },
created() { created() {
if (this.items.length === 0) { if (this.items.length === 0) {
@ -305,13 +116,4 @@ export default {
.el-autocomplete { .el-autocomplete {
width: 100%; width: 100%;
} }
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.el-row {
margin-bottom: 5px;
}
</style> </style>

View File

@ -33,6 +33,7 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('api_test.scenario.headers')" name="headers"> <el-tab-pane :label="$t('api_test.scenario.headers')" name="headers">
<ms-api-key-value :is-read-only="isReadOnly" :items="scenario.headers" :suggestions="headerSuggestions" <ms-api-key-value :is-read-only="isReadOnly" :items="scenario.headers" :suggestions="headerSuggestions"
:environment="scenario.environment"
:description="$t('api_test.scenario.kv_description')"/> :description="$t('api_test.scenario.kv_description')"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('api_test.scenario.dubbo')" name="dubbo"> <el-tab-pane :label="$t('api_test.scenario.dubbo')" name="dubbo">

View File

@ -0,0 +1,341 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in items" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col>
<el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="200"
@change="change"
:placeholder="keyText" show-word-limit/>
<el-autocomplete :disabled="isReadOnly" :maxlength="200" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText"
show-word-limit/>
</el-col>
<el-col>
<el-autocomplete
:disabled="isReadOnly"
size="small"
class="input-with-autocomplete"
v-model="item.value"
:fetch-suggestions="funcSearch"
:placeholder="valueText"
value-key="name"
highlight-first-item
@select="change">
<i slot="suffix" class="el-input__icon el-icon-edit" style="cursor: pointer;" @click="advanced(item)"></i>
</el-autocomplete>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
:disabled="isDisable(index) || isReadOnly"/>
</el-col>
</el-row>
</div>
<el-dialog :title="$t('api_test.request.parameters_advance')"
:visible.sync="itemValueVisible"
class="advanced-item-value"
width="70%">
<el-tabs tab-position="top" style="height: 50vh;">
<el-tab-pane :label="$t('api_test.request.parameters_advance_mock')">
<el-row type="flex" :gutter="20" style="overflow-x: auto;">
<el-col :span="6">
<el-autocomplete
:disabled="isReadOnly"
size="small"
class="input-with-autocomplete"
v-model="itemValue"
:fetch-suggestions="funcSearch"
:placeholder="valueText"
value-key="name"
highlight-first-item
@select="change">
</el-autocomplete>
</el-col>
<el-col :span="6" v-for="(itemFunc, itemIndex) in itemFuncs" :key="itemIndex">
<div v-for="(func, funcIndex) in funcs"
:key="`${itemIndex}-${funcIndex}`">
<el-row>
<el-col :span="12">
<el-radio size="mini" v-model="itemFunc.name" :label="func.name"
@change="methodChange(itemFunc, func)"/>
</el-col>
<el-col :span="12" v-if="itemFunc.name === func.name">
<div v-for="(p, pIndex) in itemFunc.params" :key="`${itemIndex}-${funcIndex}-${pIndex}`">
<el-input :placeholder="p.name" size="mini" v-model="p.value" @change="showPreview"/>
</div>
</el-col>
</el-row>
</div>
</el-col>
</el-row>
</el-tab-pane>
<el-tab-pane label="变量">
<el-row>
<el-col :span="6">
<div v-if="environment">
<el-tree :data="environmentParams" :props="{ children: 'children', label: 'name'}"></el-tree>
</div>
场景
请求1
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
<div style="padding-top: 10px;">
<el-row type="flex" align="middle">
<el-col :span="12">
<el-button size="small" type="primary" plain @click="saveAdvanced()">
{{ $t('commons.save') }}
</el-button>
<el-button size="small" type="info" plain @click="addFunc()">
{{ $t('api_test.request.parameters_advance_add_func') }}
</el-button>
<el-button size="small" type="success" plain @click="showPreview()">
{{ $t('api_test.request.parameters_preview') }}
</el-button>
</el-col>
<el-col>
<div> {{ itemValuePreview }}</div>
</el-col>
</el-row>
</div>
</el-dialog>
</div>
</template>
<script>
import {KeyValue} from "../model/ScenarioModel";
import {MOCKJS_FUNC} from "@/common/js/constants";
import {calculate} from "@/business/components/api/test/model/ScenarioModel";
export default {
name: "MsApiVariable",
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
items: Array,
environment: Object,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
mounted() {
if (this.environment) {
let variables = JSON.parse(this.environment.variables);
let headers = JSON.parse(this.environment.headers);
this.environmentParams = [
{
name: this.environment.name,
children: [
{
name: 'variables',
children: variables.filter(v => v.name)
},
{
name: 'headers',
children: headers.filter(v => v.name)
}
],
}
]
}
},
data() {
return {
itemValueVisible: false,
itemValue: null,
funcs: [
{name: "md5"},
{name: "base64"},
{name: "unbase64"},
{
name: "substr",
params: [{name: "start"}, {name: "length"}]
},
{
name: "concat",
params: [{name: "suffix"}]
},
{name: "lconcat", params: [{name: "prefix"}]},
{name: "sha1"},
{name: "sha224"},
{name: "sha256"},
{name: "sha384"},
{name: "sha512"},
{name: "lower"},
{name: "upper"},
{name: "length"},
{name: "number"}
],
itemValuePreview: null,
itemFuncs: [],
currentFunc: "",
mockFuncs: MOCKJS_FUNC,
environmentParams: [],
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
this.items.splice(index, 1);
this.$emit('change', this.items);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.items.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.items.push(new KeyValue());
}
this.$emit('change', this.items);
// TODO key
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
funcSearch(queryString, cb) {
let funcs = MOCKJS_FUNC;
let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs;
// callback
cb(results);
},
funcFilter(queryString) {
return (func) => {
return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1);
};
},
showPreview() {
//
if (!this.itemValue) {
return;
}
let index = this.itemValue.indexOf("|");
if (index > -1) {
this.itemValue = this.itemValue.substring(0, index).trim();
}
this.itemFuncs.forEach(f => {
if (!f.name) {
return;
}
this.itemValue += "|" + f.name;
if (f.params) {
this.itemValue += ":" + f.params.map(p => p.value).join(",");
}
});
this.itemValuePreview = calculate(this.itemValue);
},
methodChange(itemFunc, func) {
let index = this.itemFuncs.indexOf(itemFunc);
this.itemFuncs = this.itemFuncs.slice(0, index);
// deep copy
this.itemFuncs.push(JSON.parse(JSON.stringify(func)));
this.showPreview();
},
addFunc() {
if (this.itemFuncs.length > 4) {
this.$info(this.$t('api_test.request.parameters_advance_add_func_limit'));
return;
}
if (this.itemFuncs.length > 0) {
let func = this.itemFuncs[this.itemFuncs.length - 1];
if (!func.name) {
this.$warning(this.$t('api_test.request.parameters_advance_add_func_error'));
return;
}
if (func.params) {
for (let j = 0; j < func.params.length; j++) {
if (!func.params[j].value) {
this.$warning(this.$t('api_test.request.parameters_advance_add_param_error'));
return;
}
}
}
}
this.itemFuncs.push({name: '', params: []});
},
advanced(item) {
this.currentItem = item;
this.itemValueVisible = true;
this.itemValue = '';
this.itemValuePreview = null;
this.itemFuncs = [];
},
saveAdvanced() {
this.currentItem.value = this.itemValue;
this.itemValueVisible = false;
this.itemFuncs = [];
}
},
created() {
if (this.items.length === 0) {
this.items.push(new KeyValue());
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.el-autocomplete {
width: 100%;
}
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.el-row {
margin-bottom: 5px;
}
</style>

View File

@ -28,7 +28,8 @@
<el-tag class="environment-display"> <el-tag class="environment-display">
<span class="environment-name">{{ request.environment ? request.environment.name + ': ' : '' }}</span> <span class="environment-name">{{ request.environment ? request.environment.name + ': ' : '' }}</span>
<span class="environment-url">{{ displayUrl }}</span> <span class="environment-url">{{ displayUrl }}</span>
<span v-if="!displayUrl" class="environment-url-tip">{{$t('api_test.request.please_configure_environment_in_scenario')}}</span> <span v-if="!displayUrl"
class="environment-url-tip">{{ $t('api_test.request.please_configure_environment_in_scenario') }}</span>
</el-tag> </el-tag>
</el-form-item> </el-form-item>
@ -39,11 +40,13 @@
</el-switch> </el-switch>
</el-form-item> </el-form-item>
<el-button class="debug-button" size="small" type="primary" @click="runDebug">{{$t('load_test.save_and_run')}}</el-button> <el-button class="debug-button" size="small" type="primary" @click="runDebug">{{ $t('load_test.save_and_run') }}
</el-button>
<el-tabs v-model="activeName"> <el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_test.request.parameters')" name="parameters"> <el-tab-pane :label="$t('api_test.request.parameters')" name="parameters">
<ms-api-key-value :is-read-only="isReadOnly" :items="request.parameters" <ms-api-variable :is-read-only="isReadOnly" :items="request.parameters"
:environment="request.environment"
:description="$t('api_test.request.parameters_desc')"/> :description="$t('api_test.request.parameters_desc')"/>
</el-tab-pane> </el-tab-pane>
<el-tab-pane :label="$t('api_test.request.headers')" name="headers"> <el-tab-pane :label="$t('api_test.request.headers')" name="headers">
@ -70,10 +73,11 @@ import {HttpRequest, KeyValue} from "../../model/ScenarioModel";
import MsApiExtract from "../extract/ApiExtract"; import MsApiExtract from "../extract/ApiExtract";
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect"; import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
import {REQUEST_HEADERS} from "@/common/js/constants"; import {REQUEST_HEADERS} from "@/common/js/constants";
import MsApiVariable from "@/business/components/api/test/components/ApiVariable";
export default { export default {
name: "MsApiHttpRequestForm", name: "MsApiHttpRequestForm",
components: {ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue}, components: {MsApiVariable, ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue},
props: { props: {
request: HttpRequest, request: HttpRequest,
isReadOnly: { isReadOnly: {