This commit is contained in:
shiziyuan9527 2020-08-13 17:15:40 +08:00
commit e8842accc2
19 changed files with 657 additions and 416 deletions

View File

@ -10,27 +10,9 @@
</header> </header>
<main v-if="this.isNotRunning"> <main v-if="this.isNotRunning">
<ms-metric-chart :content="content" :totalTime="totalTime"/> <ms-metric-chart :content="content" :totalTime="totalTime"/>
<!--<el-container>
<el-aside width="900px">
<el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_report.total')" name="total">
<ms-scenario-results :scenarios="content.scenarios"/>
</el-tab-pane>
<el-tab-pane name="fail">
<template slot="label">
<span class="fail">{{ $t('api_report.fail') }}</span>
</template>
<ms-scenario-results :scenarios="fails"/>
</el-tab-pane>
</el-tabs>
</el-aside>
<el-main style="margin-top: 20px">
</el-main>
</el-container>-->
<el-row :gutter="20"> <el-row :gutter="20">
<el-col :span="8"> <el-col :span="8">
<el-tabs v-model="activeName"> <el-tabs v-model="activeName" @tab-click="handleClick">
<el-tab-pane :label="$t('api_report.total')" name="total"> <el-tab-pane :label="$t('api_report.total')" name="total">
<ms-scenario-results :scenarios="content.scenarios" v-on:requestResult="requestResult"/> <ms-scenario-results :scenarios="content.scenarios" v-on:requestResult="requestResult"/>
</el-tab-pane> </el-tab-pane>
@ -60,6 +42,7 @@
import MsScenarioResult from "./components/ScenarioResult"; import MsScenarioResult from "./components/ScenarioResult";
import MsMetricChart from "./components/MetricChart"; import MsMetricChart from "./components/MetricChart";
import MsScenarioResults from "./components/ScenarioResults"; import MsScenarioResults from "./components/ScenarioResults";
import {Scenario} from "../test/model/ScenarioModel";
export default { export default {
name: "MsApiReportView", name: "MsApiReportView",
@ -85,6 +68,9 @@
this.content = {}; this.content = {};
this.fails = []; this.fails = [];
}, },
handleClick(tab, event) {
this.isRequestResult = false
},
getReport() { getReport() {
this.init(); this.init();
@ -134,9 +120,12 @@
} }
}, },
requestResult(requestResult) { requestResult(requestResult) {
this.isRequestResult = true; this.isRequestResult = false;
this.request = requestResult.request; this.$nextTick(function () {
this.scenarioName = requestResult.scenarioName; this.isRequestResult = true;
this.request = requestResult.request;
this.scenarioName = requestResult.scenarioName;
});
} }
}, },

View File

@ -3,9 +3,12 @@
<el-row type="flex" align="middle"> <el-row type="flex" align="middle">
<div style="width: 50%"> <div style="width: 50%">
<el-row type="flex" justify="center" align="middle"> <el-row type="flex" justify="center" align="middle">
<div class="metric-time"> <el-row>
<div class="value" style="margin-left: -150px">{{time}}</div> <div class="metric-time">
</div> <div class="value" style="margin-right: 50px">{{time}}</div>
</div>
</el-row>
<chart id="chart" ref="chart" :options="options" :autoresize="true"></chart> <chart id="chart" ref="chart" :options="options" :autoresize="true"></chart>
<el-row type="flex" justify="center" align="middle"> <el-row type="flex" justify="center" align="middle">
<i class="circle success"/> <i class="circle success"/>

View File

@ -39,17 +39,18 @@
methods: { methods: {
active() { active() {
this.$emit("requestResult", {request: this.request, scenarioName: this.scenarioName}); this.$emit("requestResult", {request: this.request, scenarioName: this.scenarioName});
} }
}, },
computed: { /* computed: {
assertion() { assertion() {
return this.request.passAssertions + " / " + this.request.totalAssertions; return this.request.passAssertions + " / " + this.request.totalAssertions;
}, },
hasSub() { hasSub() {
return this.request.subRequestResults.length > 0; return this.request.subRequestResults.length > 0;
} }
} }*/
} }
</script> </script>

View File

@ -94,7 +94,7 @@
data() { data() {
return { return {
isActive: false, isActive: true,
activeName: "sub", activeName: "sub",
} }
}, },

View File

@ -30,7 +30,7 @@
data() { data() {
return { return {
isActive: false, isActive: true,
activeName: "body", activeName: "body",
} }
}, },

View File

@ -48,7 +48,7 @@
data() { data() {
return { return {
isActive: false, isActive: true,
activeName: "body", activeName: "body",
modes: ['text', 'json', 'xml', 'html'], modes: ['text', 'json', 'xml', 'html'],
mode: BODY_FORMAT.TEXT mode: BODY_FORMAT.TEXT

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"/>
@ -21,24 +21,25 @@
</template> </template>
<script> <script>
import MsApiKeyValue from "./ApiKeyValue"; import MsApiKeyValue from "./ApiKeyValue";
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: {
type: Boolean, type: Boolean,
default: false default: false
} }
}, },
data() { data() {
return { return {
type: BODY_TYPE, type: BODY_TYPE,
modes: ['text', 'json', 'xml', 'html'] modes: ['text', 'json', 'xml', 'html']
}; };

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,59 +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="50%">
<el-form>
<el-form-item>
<el-input :autosize="{ minRows: 2, maxRows: 4}" type="textarea" :placeholder="valueText"
v-model="itemValue"/>
</el-form-item>
</el-form>
<div>
<el-row type="flex" align="middle">
<el-col :span="6">
<el-button size="small" type="primary" plain @click="saveAdvanced()">
{{ $t('commons.save') }}
</el-button>
<el-button size="small" type="success" plain @click="showPreview(itemValue)">
{{ $t('api_test.request.parameters_preview') }}
</el-button>
</el-col>
<el-col>
<div> {{ itemValuePreview }}</div>
</el-col>
</el-row>
</div>
<div class="format-tip">
<div>
<p>{{ $t('api_test.request.parameters_filter') }}
<el-tag size="mini" v-for="func in funcs" :key="func" @click="appendFunc(func)"
style="margin-left: 2px;cursor: pointer;">
<span>{{ func }}</span>
</el-tag>
</p>
</div>
<div>
<span>{{ $t('api_test.request.parameters_filter_desc') }}
<el-link href="http://mockjs.com/examples.html" target="_blank">http://mockjs.com/examples.html</el-link>
</span>
<p>{{ $t('api_test.request.parameters_filter_example') }}@string(10) | md5 | substr: 1, 3</p>
<p>{{ $t('api_test.request.parameters_filter_example') }}@integer(1, 5) | concat:_metersphere</p>
<p><strong>{{ $t('api_test.request.parameters_filter_tips') }}</strong></p>
</div>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import {KeyValue} from "../model/ScenarioModel"; import {KeyValue} from "../model/ScenarioModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
import {calculate} from "@/business/components/api/test/model/ScenarioModel";
export default { export default {
name: "MsApiKeyValue", name: "MsApiKeyValue",
@ -103,16 +45,6 @@ export default {
suggestions: Array suggestions: Array
}, },
data() {
return {
itemValueVisible: false,
itemValue: null,
funcs: ["md5", "sha1", "sha224", "sha256", "sha384", "sha512", "base64",
"unbase64", "substr", "concat", "lconcat", "lower", "upper", "length", "number"],
itemValuePreview: null
}
},
computed: { computed: {
keyText() { keyText() {
return this.keyPlaceholder || this.$t("api_test.key"); return this.keyPlaceholder || this.$t("api_test.key");
@ -159,37 +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.concat(JMETER_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(itemValue) {
this.itemValuePreview = calculate(itemValue);
},
appendFunc(func) {
if (this.itemValue) {
this.itemValue += " | " + func;
} else {
this.$warning(this.$t("api_test.request.parameters_preview_warning"));
}
},
advanced(item) {
this.currentItem = item;
this.itemValueVisible = true;
this.itemValue = item.value;
this.itemValuePreview = null;
},
saveAdvanced() {
this.currentItem.value = this.itemValue;
this.itemValueVisible = false;
}
}, },
created() { created() {
if (this.items.length === 0) { if (this.items.length === 0) {
@ -215,19 +116,4 @@ export default {
.el-autocomplete { .el-autocomplete {
width: 100%; width: 100%;
} }
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.format-tip {
background: #EDEDED;
}
.format-tip {
border: solid #E1E1E1 1px;
margin: 10px 0;
padding: 10px;
border-radius: 3px;
}
</style> </style>

View File

@ -36,7 +36,8 @@
<el-main class="scenario-main"> <el-main class="scenario-main">
<div class="scenario-form"> <div class="scenario-form">
<ms-api-scenario-form :is-read-only="isReadOnly" :scenario="selected" :project-id="projectId" v-if="isScenario"/> <ms-api-scenario-form :is-read-only="isReadOnly" :scenario="selected" :project-id="projectId" v-if="isScenario"/>
<ms-api-request-form :debug-report-id="debugReportId" @runDebug="runDebug" :is-read-only="isReadOnly" :request="selected" v-if="isRequest"/> <ms-api-request-form :debug-report-id="debugReportId" @runDebug="runDebug" :is-read-only="isReadOnly"
:request="selected" :scenario="currentScenario" v-if="isRequest"/>
</div> </div>
</el-main> </el-main>
</el-container> </el-container>
@ -44,25 +45,25 @@
<script> <script>
import MsApiCollapseItem from "./collapse/ApiCollapseItem"; import MsApiCollapseItem from "./collapse/ApiCollapseItem";
import MsApiCollapse from "./collapse/ApiCollapse"; import MsApiCollapse from "./collapse/ApiCollapse";
import MsApiRequestConfig from "./request/ApiRequestConfig"; import MsApiRequestConfig from "./request/ApiRequestConfig";
import MsApiRequestForm from "./request/ApiRequestForm"; import MsApiRequestForm from "./request/ApiRequestForm";
import MsApiScenarioForm from "./ApiScenarioForm"; import MsApiScenarioForm from "./ApiScenarioForm";
import {Scenario, Request} from "../model/ScenarioModel"; import {Request, Scenario} from "../model/ScenarioModel";
import draggable from 'vuedraggable'; import draggable from 'vuedraggable';
export default { export default {
name: "MsApiScenarioConfig", name: "MsApiScenarioConfig",
components: { components: {
MsApiRequestConfig, MsApiRequestConfig,
MsApiScenarioForm, MsApiScenarioForm,
MsApiRequestForm, MsApiRequestForm,
MsApiCollapse, MsApiCollapse,
MsApiCollapseItem, MsApiCollapseItem,
draggable draggable
}, },
props: { props: {
scenarios: Array, scenarios: Array,

View File

@ -11,18 +11,18 @@
:label="environment.name + ': ' + environment.protocol + '://' + environment.socket" :label="environment.name + ': ' + environment.protocol + '://' + environment.socket"
:value="environment.id"/> :value="environment.id"/>
<el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig"> <el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">
{{$t('api_test.environment.environment_config')}} {{ $t('api_test.environment.environment_config') }}
</el-button> </el-button>
<template v-slot:empty> <template v-slot:empty>
<div class="empty-environment"> <div class="empty-environment">
<el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig"> <el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">
{{$t('api_test.environment.environment_config')}} {{ $t('api_test.environment.environment_config') }}
</el-button> </el-button>
</div> </div>
</template> </template>
</el-select> </el-select>
<el-form-item class="cookie-item"> <el-form-item class="cookie-item">
<el-checkbox v-model="scenario.enableCookieShare">{{'共享 Cookie'}}</el-checkbox> <el-checkbox v-model="scenario.enableCookieShare">{{ '共享 Cookie' }}</el-checkbox>
</el-form-item> </el-form-item>
</el-form-item> </el-form-item>
@ -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">
@ -52,127 +53,127 @@
</template> </template>
<script> <script>
import MsApiKeyValue from "./ApiKeyValue"; import MsApiKeyValue from "./ApiKeyValue";
import {Scenario} from "../model/ScenarioModel"; import {Scenario} from "../model/ScenarioModel";
import MsApiScenarioVariables from "./ApiScenarioVariables"; import MsApiScenarioVariables from "./ApiScenarioVariables";
import ApiEnvironmentConfig from "./ApiEnvironmentConfig"; import ApiEnvironmentConfig from "./ApiEnvironmentConfig";
import {REQUEST_HEADERS} from "@/common/js/constants"; import {REQUEST_HEADERS} from "@/common/js/constants";
import MsDubboRegistryCenter from "@/business/components/api/test/components/request/dubbo/RegistryCenter"; import MsDubboRegistryCenter from "@/business/components/api/test/components/request/dubbo/RegistryCenter";
import MsDubboConfigCenter from "@/business/components/api/test/components/request/dubbo/ConfigCenter"; import MsDubboConfigCenter from "@/business/components/api/test/components/request/dubbo/ConfigCenter";
import MsDubboConsumerService from "@/business/components/api/test/components/request/dubbo/ConsumerAndService"; import MsDubboConsumerService from "@/business/components/api/test/components/request/dubbo/ConsumerAndService";
export default { export default {
name: "MsApiScenarioForm", name: "MsApiScenarioForm",
components: { components: {
MsDubboConsumerService, MsDubboConsumerService,
MsDubboConfigCenter, MsDubboRegistryCenter, ApiEnvironmentConfig, MsApiScenarioVariables, MsApiKeyValue MsDubboConfigCenter, MsDubboRegistryCenter, ApiEnvironmentConfig, MsApiScenarioVariables, MsApiKeyValue
}, },
props: { props: {
scenario: Scenario, scenario: Scenario,
projectId: String, projectId: String,
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
default: false default: false
} }
}, },
created() { created() {
this.getEnvironments();
},
data() {
return {
result: {},
activeName: "parameters",
environments: [],
rules: {
name: [
{max: 100, message: this.$t('commons.input_limit', [1, 100]), trigger: 'blur'}
],
url: [
{max: 100, message: this.$t('commons.input_limit', [1, 100]), trigger: 'blur'}
]
},
headerSuggestions: REQUEST_HEADERS
}
},
watch: {
projectId() {
this.getEnvironments(); this.getEnvironments();
}, }
data() { },
return { methods: {
result: {}, getEnvironments() {
activeName: "parameters", if (this.projectId) {
environments: [], this.result = this.$get('/api/environment/list/' + this.projectId, response => {
rules: { this.environments = response.data;
name: [ let hasEnvironment = false;
{max: 100, message: this.$t('commons.input_limit', [1, 100]), trigger: 'blur'} for (let i in this.environments) {
], if (this.environments[i].id === this.scenario.environmentId) {
url: [ this.scenario.environment = this.environments[i];
{max: 100, message: this.$t('commons.input_limit', [1, 100]), trigger: 'blur'} hasEnvironment = true;
] break;
},
headerSuggestions: REQUEST_HEADERS
}
},
watch: {
projectId() {
this.getEnvironments();
}
},
methods: {
getEnvironments() {
if (this.projectId) {
this.result = this.$get('/api/environment/list/' + this.projectId, response => {
this.environments = response.data;
let hasEnvironment = false;
for (let i in this.environments) {
if (this.environments[i].id === this.scenario.environmentId) {
this.scenario.environment = this.environments[i];
hasEnvironment = true;
break;
}
} }
if (!hasEnvironment) {
this.scenario.environmentId = '';
this.scenario.environment = undefined;
}
});
} else {
this.scenario.environmentId = '';
this.scenario.environment = undefined;
}
},
environmentChange(value) {
for (let i in this.environments) {
if (this.environments[i].id === value) {
this.scenario.environment = this.environments[i];
break;
} }
} if (!hasEnvironment) {
if (!value) { this.scenario.environmentId = '';
this.scenario.environment = undefined; this.scenario.environment = undefined;
this.scenario.requests.forEach(request => { }
request.environment = undefined; });
}); } else {
} this.scenario.environmentId = '';
}, this.scenario.environment = undefined;
openEnvironmentConfig() {
if (!this.projectId) {
this.$error(this.$t('api_test.select_project'));
return;
}
this.$refs.environmentConfig.open(this.projectId);
},
environmentConfigClose() {
this.getEnvironments();
} }
},
environmentChange(value) {
for (let i in this.environments) {
if (this.environments[i].id === value) {
this.scenario.environment = this.environments[i];
break;
}
}
if (!value) {
this.scenario.environment = undefined;
this.scenario.requests.forEach(request => {
request.environment = undefined;
});
}
},
openEnvironmentConfig() {
if (!this.projectId) {
this.$error(this.$t('api_test.select_project'));
return;
}
this.$refs.environmentConfig.open(this.projectId);
},
environmentConfigClose() {
this.getEnvironments();
} }
} }
}
</script> </script>
<style scoped> <style scoped>
.environment-select { .environment-select {
width: 100%; width: 100%;
} }
.environment-button { .environment-button {
margin-left: 20px; margin-left: 20px;
padding: 7px; padding: 7px;
} }
.empty-environment { .empty-environment {
padding: 10px 0; padding: 10px 0;
} }
.dubbo-config-title { .dubbo-config-title {
margin-bottom: 10px; margin-bottom: 10px;
font-size: 15px; font-size: 15px;
font-weight: 600; font-weight: 600;
} }
.cookie-item { .cookie-item {
margin-top: 15px; margin-top: 15px;
} }
</style> </style>

View File

@ -0,0 +1,343 @@
<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>
<div v-if="scenario">
<el-tree :data="scenarioParams" :props="{ children: 'children', label: 'name'}"></el-tree>
</div>
</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, Scenario} 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,
scenario: Scenario,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
mounted() {
if (this.scenario) {
let variables = this.scenario.variables;
this.scenarioParams = [
{
name: this.scenario.name,
children: variables.filter(v => v.name),
}
];
}
if (this.environment) {
let variables = JSON.parse(this.environment.variables);
this.environmentParams = [
{
name: this.environment.name,
children: variables.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: [],
scenarioParams: [],
}
},
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

@ -26,9 +26,10 @@
<el-form-item v-if="request.useEnvironment" :label="$t('api_test.request.address')" class="adjust-margin-bottom"> <el-form-item v-if="request.useEnvironment" :label="$t('api_test.request.address')" class="adjust-margin-bottom">
<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,12 +40,15 @@
</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"
:description="$t('api_test.request.parameters_desc')"/> :environment="request.environment"
:scenario="scenario"
: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">
<ms-api-key-value :is-read-only="isReadOnly" :suggestions="headerSuggestions" :items="request.headers"/> <ms-api-key-value :is-read-only="isReadOnly" :suggestions="headerSuggestions" :items="request.headers"/>
@ -66,16 +70,18 @@
import MsApiKeyValue from "../ApiKeyValue"; import MsApiKeyValue from "../ApiKeyValue";
import MsApiBody from "../ApiBody"; import MsApiBody from "../ApiBody";
import MsApiAssertions from "../assertion/ApiAssertions"; import MsApiAssertions from "../assertion/ApiAssertions";
import {HttpRequest, KeyValue} from "../../model/ScenarioModel"; import {HttpRequest, KeyValue, Scenario} 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,
scenario: Scenario,
isReadOnly: { isReadOnly: {
type: Boolean, type: Boolean,
default: false default: false
@ -91,109 +97,109 @@ export default {
} }
}; };
return { return {
activeName: "parameters", activeName: "parameters",
rules: { rules: {
name: [ name: [
{max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'} {max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'}
], ],
url: [ url: [
{max: 500, required: true, message: this.$t('commons.input_limit', [1, 500]), trigger: 'blur'}, {max: 500, required: true, message: this.$t('commons.input_limit', [1, 500]), trigger: 'blur'},
{validator: validateURL, trigger: 'blur'} {validator: validateURL, trigger: 'blur'}
], ],
path: [ path: [
{max: 500, message: this.$t('commons.input_limit', [0, 500]), trigger: 'blur'}, {max: 500, message: this.$t('commons.input_limit', [0, 500]), trigger: 'blur'},
] ]
}, },
headerSuggestions: REQUEST_HEADERS headerSuggestions: REQUEST_HEADERS
}
},
methods: {
urlChange() {
if (!this.request.url) return;
let url = this.getURL(this.addProtocol(this.request.url));
if (url) {
this.request.url = decodeURIComponent(url.origin + url.pathname);
} }
}, },
pathChange() {
methods: { if (!this.request.path) return;
urlChange() { let url = this.getURL(this.displayUrl);
if (!this.request.url) return; let urlStr = url.origin + url.pathname;
let url = this.getURL(this.addProtocol(this.request.url)); let envUrl = this.request.environment.protocol + '://' + this.request.environment.socket;
if (url) { this.request.path = decodeURIComponent(urlStr.substring(envUrl.length, urlStr.length));
this.request.url = decodeURIComponent(url.origin + url.pathname); },
} getURL(urlStr) {
}, try {
pathChange() { let url = new URL(urlStr);
if (!this.request.path) return; url.searchParams.forEach((value, key) => {
let url = this.getURL(this.displayUrl); if (key && value) {
let urlStr = url.origin + url.pathname; this.request.parameters.splice(0, 0, new KeyValue(key, value));
let envUrl = this.request.environment.protocol + '://' + this.request.environment.socket;
this.request.path = decodeURIComponent(urlStr.substring(envUrl.length, urlStr.length));
},
getURL(urlStr) {
try {
let url = new URL(urlStr);
url.searchParams.forEach((value, key) => {
if (key && value) {
this.request.parameters.splice(0, 0, new KeyValue(key, value));
}
});
return url;
} catch (e) {
this.$error(this.$t('api_test.request.url_invalid'), 2000);
}
},
methodChange(value) {
if (value === 'GET' && this.activeName === 'body') {
this.activeName = 'parameters';
}
},
useEnvironmentChange(value) {
if (value && !this.request.environment) {
this.$error(this.$t('api_test.request.please_add_environment_to_scenario'), 2000);
this.request.useEnvironment = false;
}
this.$refs["request"].clearValidate();
},
addProtocol(url) {
if (url) {
if (!url.toLowerCase().startsWith("https") && !url.toLowerCase().startsWith("http")) {
return "https://" + url;
} }
} });
return url; return url;
}, } catch (e) {
runDebug() { this.$error(this.$t('api_test.request.url_invalid'), 2000);
this.$emit('runDebug');
} }
}, },
methodChange(value) {
computed: { if (value === 'GET' && this.activeName === 'body') {
isNotGet() { this.activeName = 'parameters';
return this.request.method !== "GET";
},
displayUrl() {
return this.request.environment ? this.request.environment.protocol + '://' + this.request.environment.socket + (this.request.path ? this.request.path : '') : '';
} }
},
useEnvironmentChange(value) {
if (value && !this.request.environment) {
this.$error(this.$t('api_test.request.please_add_environment_to_scenario'), 2000);
this.request.useEnvironment = false;
}
this.$refs["request"].clearValidate();
},
addProtocol(url) {
if (url) {
if (!url.toLowerCase().startsWith("https") && !url.toLowerCase().startsWith("http")) {
return "https://" + url;
}
}
return url;
},
runDebug() {
this.$emit('runDebug');
}
},
computed: {
isNotGet() {
return this.request.method !== "GET";
},
displayUrl() {
return this.request.environment ? this.request.environment.protocol + '://' + this.request.environment.socket + (this.request.path ? this.request.path : '') : '';
} }
} }
}
</script> </script>
<style scoped> <style scoped>
.el-tag { .el-tag {
width: 100%; width: 100%;
height: 40px; height: 40px;
line-height: 40px; line-height: 40px;
} }
.environment-display { .environment-display {
font-size: 14px; font-size: 14px;
} }
.environment-name { .environment-name {
font-weight: bold; font-weight: bold;
font-style: italic; font-style: italic;
} }
.adjust-margin-bottom { .adjust-margin-bottom {
margin-bottom: 10px; margin-bottom: 10px;
} }
.environment-url-tip { .environment-url-tip {
color: #F56C6C; color: #F56C6C;
} }
</style> </style>

View File

@ -1,27 +1,28 @@
<template> <template>
<div class="request-form"> <div class="request-form">
<component @runDebug="runDebug" :is="component" :is-read-only="isReadOnly" :request="request"/> <component @runDebug="runDebug" :is="component" :is-read-only="isReadOnly" :request="request" :scenario="scenario"/>
<ms-scenario-results v-loading="debugReportLoading" v-if="isCompleted" :scenarios="isCompleted ? request.debugReport.scenarios : []"/> <ms-scenario-results v-loading="debugReportLoading" v-if="isCompleted" :scenarios="isCompleted ? request.debugReport.scenarios : []"/>
</div> </div>
</template> </template>
<script> <script>
import {Request, RequestFactory} from "../../model/ScenarioModel"; import {Request, RequestFactory, Scenario} from "../../model/ScenarioModel";
import MsApiHttpRequestForm from "./ApiHttpRequestForm"; import MsApiHttpRequestForm from "./ApiHttpRequestForm";
import MsApiDubboRequestForm from "./ApiDubboRequestForm"; import MsApiDubboRequestForm from "./ApiDubboRequestForm";
import MsScenarioResults from "../../../report/components/ScenarioResults"; import MsScenarioResults from "../../../report/components/ScenarioResults";
export default { export default {
name: "MsApiRequestForm", name: "MsApiRequestForm",
components: {MsScenarioResults, MsApiDubboRequestForm, MsApiHttpRequestForm}, components: {MsScenarioResults, MsApiDubboRequestForm, MsApiHttpRequestForm},
props: { props: {
request: Request, scenario: Scenario,
isReadOnly: { request: Request,
type: Boolean, isReadOnly: {
default: false type: Boolean,
}, default: false
debugReportId: String
}, },
debugReportId: String
},
data() { data() {
return { return {
reportId: "", reportId: "",

View File

@ -873,9 +873,6 @@ class JMXGenerator {
this.addEnvironments(environment.headers, scenario.headers) this.addEnvironments(environment.headers, scenario.headers)
} }
let headers = this.filterKV(scenario.headers); let headers = this.filterKV(scenario.headers);
headers.forEach(h => {
h.value = calculate(h.value);
});
if (headers.length > 0) { if (headers.length > 0) {
let name = scenario.name + " Headers" let name = scenario.name + " Headers"
threadGroup.put(new HeaderManager(name, headers)); threadGroup.put(new HeaderManager(name, headers));
@ -886,9 +883,6 @@ class JMXGenerator {
let name = request.name + " Headers"; let name = request.name + " Headers";
this.addBodyFormat(request); this.addBodyFormat(request);
let headers = this.filterKV(request.headers); let headers = this.filterKV(request.headers);
headers.forEach(h => {
h.value = calculate(h.value);
});
if (headers.length > 0) { if (headers.length > 0) {
httpSamplerProxy.put(new HeaderManager(name, headers)); httpSamplerProxy.put(new HeaderManager(name, headers));
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<editor v-model="formatData" :lang="mode" @init="editorInit" theme="chrome"/> <editor v-model="formatData" :lang="mode" @init="editorInit" theme="chrome"/>
</template> </template>
<script> <script>

View File

@ -73,7 +73,7 @@ export const funcFilters = {
lconcat: function (str, ...args) { lconcat: function (str, ...args) {
args.forEach(item => { args.forEach(item => {
str = item + this._string; str = item + str;
}); });
return str; return str;
}, },

View File

@ -403,6 +403,11 @@ export default {
parameters_advance: "Advanced parameter settings", parameters_advance: "Advanced parameter settings",
parameters_preview: "Preview", parameters_preview: "Preview",
parameters_preview_warning: "Please enter the template first", parameters_preview_warning: "Please enter the template first",
parameters_advance_mock: "Mock Data",
parameters_advance_add_func: "Add Function",
parameters_advance_add_func_limit: "Support up to 5 functions",
parameters_advance_add_func_error: "Please select function first",
parameters_advance_add_param_error: "Please enter function parameters",
parameters_desc: "Parameters will be appended to the URL e.g. https://fit2cloud.com?Name=Value&Name2=Value2", parameters_desc: "Parameters will be appended to the URL e.g. https://fit2cloud.com?Name=Value&Name2=Value2",
headers: "Headers", headers: "Headers",
body: "Body", body: "Body",

View File

@ -406,6 +406,11 @@ export default {
parameters_advance: "高级参数设置", parameters_advance: "高级参数设置",
parameters_preview: "预览", parameters_preview: "预览",
parameters_preview_warning: "请先输入模版", parameters_preview_warning: "请先输入模版",
parameters_advance_mock: "Mock 数据",
parameters_advance_add_func: "添加函数",
parameters_advance_add_func_limit: "最多支持5个函数",
parameters_advance_add_func_error: "请先选择函数",
parameters_advance_add_param_error: "请输入函数参数",
parameters_desc: "参数追加到URL例如https://fit2cloud.com/entries?key1=Value1&Key2=Value2", parameters_desc: "参数追加到URL例如https://fit2cloud.com/entries?key1=Value1&Key2=Value2",
headers: "请求头", headers: "请求头",
body: "请求内容", body: "请求内容",

View File

@ -403,6 +403,11 @@ export default {
parameters_advance: "高級參數設置", parameters_advance: "高級參數設置",
parameters_preview: "預覽", parameters_preview: "預覽",
parameters_preview_warning: "請先輸入模版", parameters_preview_warning: "請先輸入模版",
parameters_advance_mock: "Mock 數據",
parameters_advance_add_func: "添加函數",
parameters_advance_add_func_limit: "最多支持5個函數",
parameters_advance_add_func_error: "請先選擇函數",
parameters_advance_add_param_error: "請輸入函數參數",
parameters_desc: "參數追加到URL,例如https://fit2cloud.com/entrieskey1=Value1&amp;Key2=Value2", parameters_desc: "參數追加到URL,例如https://fit2cloud.com/entrieskey1=Value1&amp;Key2=Value2",
headers: "請求頭", headers: "請求頭",
body: "請求內容", body: "請求內容",