feat(接口测试): Mock期望添加后置脚本功能

Mock期望添加后置脚本功能
This commit is contained in:
song-tianyang 2022-02-16 15:47:00 +08:00 committed by CountryBuilder
parent 723178b2f0
commit 8253ef9608
11 changed files with 648 additions and 351 deletions

View File

@ -16,10 +16,8 @@ import io.metersphere.jmeter.utils.ScriptEngineUtils;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.protocol.java.sampler.JSR223Sampler;
import org.apache.jmeter.samplers.SampleResult;
import javax.script.ScriptException;
import javax.script.ScriptEngine;
import javax.servlet.http.HttpServletRequest;
import java.io.*;
import java.util.*;
@ -300,7 +298,27 @@ public class MockApiUtils {
return returnMap;
}
public static String getResultByResponseResult(JSONObject bodyObj, String url, Map<String, String> headerMap, RequestMockParams requestMockParams) {
public String getResultByResponseResult(JSONObject bodyObj, String url, Map<String, String> headerMap, RequestMockParams requestMockParams, boolean useScript) {
MockScriptEngineUtils scriptEngineUtils = new MockScriptEngineUtils();
ScriptEngine scriptEngine = null;
String scriptLanguage = "beanshell";
String script = null;
if(useScript){
if (bodyObj.containsKey("scriptObject")) {
try {
JSONObject scriptObj = bodyObj.getJSONObject("scriptObject");
scriptLanguage = scriptObj.getString("scriptLanguage");
script = scriptObj.getString("script");
} catch (Exception e) {
LogUtil.error(e);
}
}
}
scriptEngine = scriptEngineUtils.getBaseScriptEngine(scriptLanguage,url,headerMap,requestMockParams);
if(StringUtils.isNotEmpty(script) && scriptEngine != null){
scriptEngineUtils.runScript(scriptEngine,script);
}
if (headerMap == null) {
headerMap = new HashMap<>();
}
@ -357,94 +375,13 @@ public class MockApiUtils {
String raw = bodyObj.getString("apiRspRaw");
returnStr = raw;
}
} else if (StringUtils.equalsAnyIgnoreCase(type, "script")) {
if (bodyObj.containsKey("scriptObject")) {
JSONObject scriptObj = bodyObj.getJSONObject("scriptObject");
String script = scriptObj.getString("script");
String scriptLanguage = scriptObj.getString("scriptLanguage");
String baseScript = parseScript(url, headerMap, requestMockParams);
try {
script = baseScript + script;
if (StringUtils.isEmpty(scriptLanguage)) {
scriptLanguage = "beanshell";
}
returnStr = runScript(script, scriptLanguage);
} catch (Exception e) {
LogUtil.error(e);
}
}
}
}
returnStr = scriptEngineUtils.parseReportString(scriptEngine,returnStr);
return returnStr;
}
}
private static String parseScript(String url, Map<String, String> headerMap, RequestMockParams requestMockParams) {
StringBuffer scriptStringBuffer = new StringBuffer();
scriptStringBuffer.append("import java.util.HashMap;\n\n");
scriptStringBuffer.append("HashMap requestParams = new HashMap();\n");
scriptStringBuffer.append("requestParams.put(\"address\",\"" + url + "\");\n");
//写入请求头
for (Map.Entry<String, String> headEntry : headerMap.entrySet()) {
String headerKey = headEntry.getKey();
String headerValue = headEntry.getValue();
scriptStringBuffer.append("requestParams.put(\"header." + headerKey + "\",\"" + headerValue + "\");\n");
}
//写入body参数
if (requestMockParams.getBodyParams() != null) {
if (requestMockParams.getBodyParams().size() == 1) {
//参数是jsonObject
JSONObject bodyParamObj = requestMockParams.getBodyParams().getJSONObject(0);
for (String key : bodyParamObj.keySet()) {
String value = String.valueOf(bodyParamObj.get(key));
value = StringUtils.replace(value, "\\", "\\\\");
value = StringUtils.replace(value, "\"", "\\\"");
scriptStringBuffer.append("requestParams.put(\"body." + key + "\",\"" + value + "\");\n");
if (StringUtils.equalsIgnoreCase(key, "raw")) {
scriptStringBuffer.append("requestParams.put(\"bodyRaw\",\"" + value + "\");\n");
}
}
String jsonBody = bodyParamObj.toJSONString();
jsonBody = StringUtils.replace(jsonBody, "\\", "\\\\");
jsonBody = StringUtils.replace(jsonBody, "\"", "\\\"");
scriptStringBuffer.append("requestParams.put(\"body.json\",\"" + jsonBody + "\");\n");
} else {
scriptStringBuffer.append("requestParams.put(\"bodyRaw\",\"" + requestMockParams.getBodyParams().toJSONString() + "\");\n");
}
}
//写入query参数
if (requestMockParams.getQueryParamsObj() != null) {
JSONObject queryParamsObj = requestMockParams.getQueryParamsObj();
for (String key : queryParamsObj.keySet()) {
String value = String.valueOf(queryParamsObj.get(key));
value = StringUtils.replace(value, "\\", "\\\\");
value = StringUtils.replace(value, "\"", "\\\"");
scriptStringBuffer.append("requestParams.put(\"query." + key + "\",\"" + value + "\");\n");
}
}
//写入rest参数
if (requestMockParams.getRestParamsObj() != null) {
JSONObject restParamsObj = requestMockParams.getRestParamsObj();
for (String key : restParamsObj.keySet()) {
String value = String.valueOf(restParamsObj.get(key));
scriptStringBuffer.append("requestParams.put(\"rest." + key + "\",\"" + value + "\");\n");
}
}
return scriptStringBuffer.toString();
}
private static String runScript(String script, String scriptLanguage) throws ScriptException {
JSR223Sampler jmeterScriptSampler = new JSR223Sampler();
jmeterScriptSampler.setScriptLanguage(scriptLanguage);
jmeterScriptSampler.setScript(script);
SampleResult result = jmeterScriptSampler.sample(null);
return result.getResponseDataAsString();
}
public static RequestMockParams getParams(String urlParams, String apiPath, JSONObject queryParamsObject, JSON paramJson, boolean isPostRequest) {
RequestMockParams returnParams = getGetParamMap(urlParams, apiPath, queryParamsObject, isPostRequest);
if (paramJson != null) {
@ -719,6 +656,8 @@ public class MockApiUtils {
}
}
}
}else {
return true;
}
return false;
}

View File

@ -0,0 +1,211 @@
package io.metersphere.api.mock.utils;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.api.dto.mock.RequestMockParams;
import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.lang3.StringUtils;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class MockScriptEngineUtils {
public JSONObject getVars(ScriptEngine engine){
try {
return JSONObject.parseObject(JSONObject.toJSONString(engine.get("vars")));
}catch (Exception e){
LogUtil.error(e);
}
return new JSONObject();
}
public String get(ScriptEngine engine, String key){
return String.valueOf(engine.get(key));
}
public void runScript(ScriptEngine engine, String script){
try {
engine.eval(script);
}catch (Exception e){
LogUtil.error(e);
}
}
public ScriptEngine getBaseScriptEngine(String scriptLanguage, String url, Map<String, String> headerMap, RequestMockParams requestMockParams){
ScriptEngine engine = null;
try {
if (StringUtils.isEmpty(scriptLanguage)) {
return null;
}
if (StringUtils.equalsIgnoreCase(scriptLanguage, "beanshell")) {
ScriptEngineManager scriptEngineFactory = new ScriptEngineManager();
engine = scriptEngineFactory.getEngineByName(scriptLanguage);
String preScript = this.genBeanshellPreScript(url, headerMap, requestMockParams);
engine.eval(preScript);
} else if (StringUtils.equalsIgnoreCase(scriptLanguage, "python")) {
ScriptEngineManager scriptEngineFactory = new ScriptEngineManager();
engine = scriptEngineFactory.getEngineByName(scriptLanguage);
String preScript = this.genPythonPreScript(url, headerMap, requestMockParams);
engine.eval(preScript);
}
}catch (Exception e){
LogUtil.error(e);
}
return engine;
}
private String genBeanshellPreScript(String url, Map<String, String> headerMap, RequestMockParams requestMockParams) {
StringBuffer preScriptBuffer = new StringBuffer();
preScriptBuffer.append("Map vars = new HashMap();\n");
preScriptBuffer.append("vars.put(\"address\",\"" + url + "\");\n");
//写入请求头
for (Map.Entry<String, String> headEntry : headerMap.entrySet()) {
String headerKey = headEntry.getKey();
String headerValue = headEntry.getValue();
preScriptBuffer.append("vars.put(\"header." + headerKey + "\",\"" + headerValue + "\");\n");
}
//写入body参数
if (requestMockParams.getBodyParams() != null) {
if (requestMockParams.getBodyParams().size() == 1) {
//参数是jsonObject
JSONObject bodyParamObj = requestMockParams.getBodyParams().getJSONObject(0);
for (String key : bodyParamObj.keySet()) {
String value = String.valueOf(bodyParamObj.get(key));
value = StringUtils.replace(value, "\\", "\\\\");
value = StringUtils.replace(value, "\"", "\\\"");
preScriptBuffer.append("vars.put(\"body." + key + "\",\"" + value + "\");\n");
if (StringUtils.equalsIgnoreCase(key, "raw")) {
preScriptBuffer.append("vars.put(\"bodyRaw\",\"" + value + "\");\n");
}
}
String jsonBody = bodyParamObj.toJSONString();
jsonBody = StringUtils.replace(jsonBody, "\\", "\\\\");
jsonBody = StringUtils.replace(jsonBody, "\"", "\\\"");
preScriptBuffer.append("vars.put(\"body.json\",\"" + jsonBody + "\");\n");
} else {
preScriptBuffer.append("vars.put(\"bodyRaw\",\"" + requestMockParams.getBodyParams().toJSONString() + "\");\n");
}
}
//写入query参数
if (requestMockParams.getQueryParamsObj() != null) {
JSONObject queryParamsObj = requestMockParams.getQueryParamsObj();
for (String key : queryParamsObj.keySet()) {
String value = String.valueOf(queryParamsObj.get(key));
value = StringUtils.replace(value, "\\", "\\\\");
value = StringUtils.replace(value, "\"", "\\\"");
preScriptBuffer.append("vars.put(\"query." + key + "\",\"" + value + "\");\n");
}
}
//写入rest参数
if (requestMockParams.getRestParamsObj() != null) {
JSONObject restParamsObj = requestMockParams.getRestParamsObj();
for (String key : restParamsObj.keySet()) {
String value = String.valueOf(restParamsObj.get(key));
preScriptBuffer.append("vars.put(\"rest." + key + "\",\"" + value + "\");\n");
}
}
return preScriptBuffer.toString();
}
private String genPythonPreScript(String url, Map<String, String> headerMap, RequestMockParams requestMockParams) {
StringBuffer preScriptBuffer = new StringBuffer();
preScriptBuffer.append("vars = {}; \n");
preScriptBuffer.append("vars[\"address\"]=\"" + url + "\";\n");
//写入请求头
for (Map.Entry<String, String> headEntry : headerMap.entrySet()) {
String headerKey = headEntry.getKey();
String headerValue = headEntry.getValue();
preScriptBuffer.append("vars[\"header." + headerKey + "\"]=\"" + headerValue + "\";\n");
}
//写入body参数
if (requestMockParams.getBodyParams() != null) {
if (requestMockParams.getBodyParams().size() == 1) {
//参数是jsonObject
JSONObject bodyParamObj = requestMockParams.getBodyParams().getJSONObject(0);
for (String key : bodyParamObj.keySet()) {
String value = String.valueOf(bodyParamObj.get(key));
value = StringUtils.replace(value, "\\", "\\\\");
value = StringUtils.replace(value, "\"", "\\\"");
preScriptBuffer.append("vars[\"body." + key + "\"]=\"" + value + "\";\n");
if (StringUtils.equalsIgnoreCase(key, "raw")) {
preScriptBuffer.append("vars[\"bodyRaw\"]=\"" + value + "\";\n");
}
}
String jsonBody = bodyParamObj.toJSONString();
jsonBody = StringUtils.replace(jsonBody, "\\", "\\\\");
jsonBody = StringUtils.replace(jsonBody, "\"", "\\\"");
preScriptBuffer.append("vars[\"body.json\"]=\"" + jsonBody + "\";\n");
} else {
preScriptBuffer.append("vars[\"bodyRaw\"]=\"" + requestMockParams.getBodyParams().toJSONString() + "\";\n");
}
}
//写入query参数
if (requestMockParams.getQueryParamsObj() != null) {
JSONObject queryParamsObj = requestMockParams.getQueryParamsObj();
for (String key : queryParamsObj.keySet()) {
String value = String.valueOf(queryParamsObj.get(key));
value = StringUtils.replace(value, "\\", "\\\\");
value = StringUtils.replace(value, "\"", "\\\"");
preScriptBuffer.append("vars[\"query." + key + "\"]=\"" + value + "\";\n");
}
}
//写入rest参数
if (requestMockParams.getRestParamsObj() != null) {
JSONObject restParamsObj = requestMockParams.getRestParamsObj();
for (String key : restParamsObj.keySet()) {
String value = String.valueOf(restParamsObj.get(key));
preScriptBuffer.append("vars[\"rest." + key + "\"]=\"" + value + "\";\n");
}
}
return preScriptBuffer.toString();
}
public String parseReportString(ScriptEngine scriptEngine, String reportString) {
String regStr = "\\$\\{([^${}]+)\\}";
Pattern pattern = Pattern.compile(regStr);
Matcher matcher = pattern.matcher(reportString);
List<String> paramKeys = new ArrayList<>();
while (matcher.find()){
String paramKey = matcher.group(0);
if(!paramKeys.contains(paramKey)){
paramKeys.add(paramKey);
}
}
JSONObject varsObject = this.getVars(scriptEngine);
for (String paramKey : paramKeys) {
String value = this.getValue(scriptEngine,varsObject,paramKey);
reportString = StringUtils.replace(reportString,paramKey,value);
}
return reportString;
}
private String getValue(ScriptEngine scriptEngine, JSONObject varsObject, String paramKey) {
String key = paramKey;
if(key.startsWith("${") && key.endsWith("}")){
key = paramKey.substring(2,key.length()-1);
}
String value = null;
if(varsObject != null && varsObject.containsKey(key)){
value = varsObject.getString(key);
}
if(StringUtils.isEmpty(value)){
try {
value = JSONObject.toJSONString(scriptEngine.get(key));
}catch (Exception e){
LogUtil.error(e);
}
}
if(StringUtils.isEmpty(value)){
value = paramKey;
}
return value;
}
}

View File

@ -724,7 +724,12 @@ public class MockConfigService {
}
}
if (responseJsonObj.containsKey("body")) {
returnStr = MockApiUtils.getResultByResponseResult(responseJsonObj.getJSONObject("body"), url, headerMap, requestMockParams);
MockApiUtils mockApiUtils = new MockApiUtils();
boolean useScript = false;
if(responseJsonObj.containsKey("usePostScript")){
useScript = responseJsonObj.getBoolean("usePostScript");
}
returnStr = mockApiUtils.getResultByResponseResult(responseJsonObj.getJSONObject("body"), url, headerMap, requestMockParams,useScript);
}
if (responseJsonObj.containsKey("httpCode")) {
int httpCodeNum = 500;
@ -1262,7 +1267,7 @@ public class MockConfigService {
List<TcpTreeTableDataStruct> tcpDataList = JSONArray.parseArray(requestJson.getString("xmlDataStruct"), TcpTreeTableDataStruct.class);
xmlStr = TcpTreeTableDataParser.treeTableData2Xml(tcpDataList);
} catch (Exception e) {
LogUtil.error(e);
}
JSONObject matchObj = XMLUtils.XmlToJson(xmlStr);
isMatch = JsonStructUtils.checkJsonObjCompliance(sourceObj, matchObj);

View File

@ -21,7 +21,6 @@ public class TCPServer implements Runnable {
public void openSocket() throws Exception {
this.serverSocket = new ServerSocket(this.port);
int connectIndex = 0;
while (true) {
if (!this.serverSocket.isClosed()) {

View File

@ -57,7 +57,12 @@ public class TCPServicer {
if(responseObj.containsKey("responseResult")){
JSONObject respResultObj = responseObj.getJSONObject("responseResult");
if(respResultObj.containsKey("body")){
returnMsg = MockApiUtils.getResultByResponseResult(respResultObj.getJSONObject("body"),"",null,null);
MockApiUtils mockApiUtils = new MockApiUtils();
boolean useScript = false;
if(respResultObj.containsKey("usePostScript")){
useScript = responseObj.getBoolean("usePostScript");
}
returnMsg = mockApiUtils.getResultByResponseResult(respResultObj.getJSONObject("body"),"",null,null,useScript);
}
try {
if(respResultObj.containsKey("delayed")){
@ -70,7 +75,6 @@ public class TCPServicer {
returnMsg = responseObj.getString("body");
}
try {
Thread.sleep(delayed);
} catch (Exception e) {

View File

@ -224,8 +224,8 @@ public class TestPlanTestCaseService {
* @param testId 接口测试id
*/
public void updateTestCaseStates(String testId, String testName, String planId, String testType) {
TestPlan testPlan = testPlanService.getTestPlan(planId);
if (BooleanUtils.isNotTrue(testPlan.getAutomaticStatusUpdate())) {
TestPlan testPlan1 = testPlanService.getTestPlan(planId);
if (BooleanUtils.isNotTrue(testPlan1.getAutomaticStatusUpdate())) {
return;
}
TestCaseTestExample example = new TestCaseTestExample();

View File

@ -13,63 +13,55 @@
<el-radio :disabled="isReadOnly" :label="type.RAW" @change="modeChange">
{{ $t('api_test.definition.request.body_raw') }}
</el-radio>
<el-radio :disabled="isReadOnly" label="script" @change="modeChange">
{{ $t('api_test.automation.customize_script') }}
</el-radio>
</el-radio-group>
<div class="ms-body" v-if="body.type == 'JSON'">
<div style="padding: 10px">
<el-switch active-text="JSON-SCHEMA" v-model="body.format" @change="formatChange" active-value="JSON-SCHEMA"/>
</div>
<ms-json-code-edit
v-if="body.format==='JSON-SCHEMA'"
:body="body"
ref="jsonCodeEdit"/>
v-if="body.format==='JSON-SCHEMA'"
:body="body"
ref="jsonCodeEdit"/>
<ms-code-edit
v-else-if="codeEditActive && loadIsOver"
:read-only="isReadOnly"
:data.sync="body.raw"
:modes="modes"
:mode="'json'"
height="90%"
ref="codeEdit"/>
v-else-if="codeEditActive && loadIsOver"
:read-only="isReadOnly"
:data.sync="body.raw"
:modes="modes"
:mode="'json'"
height="90%"
ref="codeEdit"/>
</div>
<div class="ms-body" v-if="body.type == 'fromApi'">
<ms-code-edit
:read-only="true"
:data.sync="body.apiRspRaw"
:modes="modes"
:mode="'text'"
v-if="loadIsOver"
height="90%"
ref="fromApiCodeEdit"/>
:read-only="true"
:data.sync="body.apiRspRaw"
:modes="modes"
:mode="'text'"
v-if="loadIsOver"
height="90%"
ref="fromApiCodeEdit"/>
</div>
<div class="ms-body" v-if="body.type == 'XML'">
<el-input v-model="body.xmlHeader" size="small" style="width: 400px;margin-bottom: 5px"/>
<ms-code-edit
:read-only="isReadOnly"
:data.sync="body.xmlRaw"
:modes="modes"
:mode="'xml'"
v-if="loadIsOver"
height="90%"
ref="codeEdit"/>
:read-only="isReadOnly"
:data.sync="body.xmlRaw"
:modes="modes"
:mode="'xml'"
v-if="loadIsOver"
height="90%"
ref="codeEdit"/>
</div>
<div class="ms-body" v-if="body.type == 'Raw'">
<ms-code-edit
:read-only="isReadOnly"
:data.sync="body.raw"
:modes="modes"
height="90%"
ref="codeEdit"/>
</div>
<div class="ms-body" v-if="body.type == 'script'">
<mock-api-script-editor v-if="loadIsOver"
:jsr223-processor="body.scriptObject"/>
:read-only="isReadOnly"
:data.sync="body.raw"
:modes="modes"
height="90%"
ref="codeEdit"/>
</div>
<batch-add-parameter @batchSave="batchSave" ref="batchAddParameter"/>
@ -86,7 +78,6 @@ import MsApiVariable from "@/business/components/api/definition/components/ApiVa
import MsApiFromUrlVariable from "@/business/components/api/definition/components/body/ApiFromUrlVariable";
import BatchAddParameter from "@/business/components/api/definition/components/basis/BatchAddParameter";
import Convert from "@/business/components/common/json-schema/convert/convert";
import MockApiScriptEditor from "@/business/components/api/definition/components/mock/Components/MockApiScriptEditor";
export default {
name: "MockApiResponseBody",
@ -98,10 +89,9 @@ export default {
MsApiFromUrlVariable,
MsJsonCodeEdit,
BatchAddParameter,
MockApiScriptEditor
},
props: {
apiId:String,
apiId: String,
body: {},
headers: Array,
isReadOnly: {
@ -111,7 +101,11 @@ export default {
isShowEnable: {
type: Boolean,
default: true
}
},
usePostScript: {
type: Boolean,
default: false
},
},
data() {
return {
@ -239,33 +233,30 @@ export default {
modeChange(mode) {
switch (this.body.type) {
case "JSON":
// this.setContentType("application/json");
this.refreshMsCodeEdit();
break;
case "XML":
// this.setContentType("text/xml");
this.refreshMsCodeEdit();
break;
case "fromApi":
this.selectApiResponse();
break;
default:
// this.removeContentType();
this.refreshMsCodeEdit();
break;
}
},
refreshMsCodeEdit(){
refreshMsCodeEdit() {
this.loadIsOver = false;
this.$nextTick(() => {
this.loadIsOver = true;
});
},
selectApiResponse(){
selectApiResponse() {
let selectUrl = "/mockConfig/getApiResponse/" + this.apiId;
this.$get(selectUrl, response => {
let apiResponse = response.data;
if(apiResponse && apiResponse.returnMsg){
if (apiResponse && apiResponse.returnMsg) {
this.body.apiRspRaw = apiResponse.returnMsg;
}
this.refreshMsCodeEdit();
@ -317,8 +308,8 @@ export default {
let keyValues = [];
params.forEach(item => {
let line = [];
line[0] = item.substring(0,item.indexOf(":"));
line[1] = item.substring(item.indexOf(":")+1,item.length);
line[0] = item.substring(0, item.indexOf(":"));
line[1] = item.substring(item.indexOf(":") + 1, item.length);
let required = false;
keyValues.unshift(new KeyValue({
name: line[0],

View File

@ -2,10 +2,10 @@
<div>
<el-row type="flex">
<el-col :span="codeSpan" class="script-content">
<ms-code-edit v-if="isCodeEditAlive" :mode="codeEditModeMap[jsr223ProcessorData.scriptLanguage]"
<ms-code-edit v-if="isCodeEditAlive" :mode="codeEditModeMap[jsr223Processor.scriptLanguage]"
:read-only="isReadOnly"
height="90%"
:data.sync="jsr223ProcessorData.script" theme="eclipse" :modes="['java','python']"
:data.sync="jsr223Processor.script" theme="eclipse" :modes="['java','python']"
ref="codeEdit"/>
</el-col>
<div style="width: 14px;margin-right: 5px;">
@ -16,10 +16,12 @@
</div>
</div>
<el-col :span="menuSpan" style="width: 200px" class="script-index">
<ms-dropdown :default-command="jsr223ProcessorData.scriptLanguage" :commands="languages" style="margin-bottom: 5px;margin-left: 15px;"
<ms-dropdown :default-command.sync="jsr223Processor.scriptLanguage" :commands="languages"
style="margin-bottom: 5px;margin-left: 15px;"
@command="languageChange"/>
<mock-script-nav-menu ref="scriptNavMenu" style="width: 90%" :language="jsr223ProcessorData.scriptLanguage" :menus="baseCodeTemplates"
@handleCode="handleCodeTemplate"/>
<mock-script-nav-menu ref="scriptNavMenu" style="width: 90%" :language="jsr223Processor.scriptLanguage"
:menus="baseCodeTemplates"
@handleCode="handleCodeTemplate"/>
</el-col>
</el-row>
</div>
@ -38,60 +40,7 @@ export default {
components: {MsDropdown, MsCodeEdit, CustomFunctionRelate, ApiFuncRelevance, MockScriptNavMenu},
data() {
return {
jsr223ProcessorData: {},
baseCodeTemplates: [],
httpCodeTemplates: [
{
title: "API"+this.$t('api_test.definition.document.request_info'),
children: [
{
title: this.$t('api_test.request.address'),
value: 'var returnMsg = requestParams.get("address");\nreturn returnMsg;',
},
{
title: "Header "+this.$t('api_test.definition.document.request_param'),
value: 'var returnMsg = requestParams.get("header.${param}");\nreturn returnMsg;',
},
{
title: this.$t('api_test.request.body')+this.$t('api_test.variable'),
value: 'var returnMsg = requestParams.get("body.${param}");\nreturn returnMsg;\n' +
"\n"+
'//如果对象是多层JSON需要引入fastjson协助解析:\n' +
'// 以"{\"name\":\"user\",\"rows\":[{\"type\":1}]}" 为demo,取rows第1个的type数据:\n' +
'import com.alibaba.fastjson.JSON;\n'+
'import com.alibaba.fastjson.JSONArray;\n'+
'import com.alibaba.fastjson.JSONObject;\n'+
'\n'+
'var jsonParam = requestParams.get("body.json");\n' +
'JSONObject jsonObject = JSONObject.parseObject(jsonParam);\n' +
'var returnMsg = jsonObject.getJSONArray("rows").getJSONObject(0).getString("type");\n' +
'return returnMsg;\n',
},
{
title: this.$t('api_test.request.body')+this.$t('api_test.variable')+" (Raw)",
value: 'var returnMsg = requestParams.get("bodyRaw");\nreturn returnMsg;',
},
{
title: "Query "+this.$t('api_test.definition.document.request_param'),
value: 'var returnMsg = requestParams.get("query.${param}");\nreturn returnMsg;',
},
{
title: "Rest "+this.$t('api_test.definition.document.request_param'),
value: 'var returnMsg = requestParams.get("rest.${param}");\nreturn returnMsg;',
},
]
},
{
title: this.$t('project.code_segment.code_segment'),
children: [
{
title: this.$t('project.code_segment.insert_segment'),
command: "custom_function",
}
]
},
],
tcpCodeTemplates: [
{
title: this.$t('project.code_segment.code_segment'),
@ -105,8 +54,7 @@ export default {
],
isCodeEditAlive: true,
languages: [
'beanshell'
// , "python", "nashornScript", "rhinoScript"
'beanshell', "python"
],
codeEditModeMap: {
beanshell: 'java',
@ -121,13 +69,62 @@ export default {
}
},
created() {
this.jsr223ProcessorData = this.jsr223Processor;
if(this.showApi){
if (!this.jsr223Processor.scriptLanguage) {
this.jsr223Processor.scriptLanguage = "beanshell";
}
if (this.showApi) {
this.baseCodeTemplates = this.httpCodeTemplates;
}else {
} else {
this.baseCodeTemplates = this.tcpCodeTemplates;
}
},
computed: {
httpCodeTemplates(){
let returnData = [
{
title: "API" + this.$t('api_test.definition.document.request_info'),
children: [
{
title: this.$t('api_test.request.address'),
value: this.getScript("address"),
},
{
title: "Header " + this.$t('api_test.definition.document.request_param'),
value: this.getScript("header"),
},
{
title: this.$t('api_test.request.body') + this.$t('api_test.variable'),
value: this.getScript("body"),
},
{
title: this.$t('api_test.request.body') + this.$t('api_test.variable') + " (Raw)",
value: this.getScript("bodyRaw"),
},
{
title: "Query " + this.$t('api_test.definition.document.request_param'),
value: this.getScript("query"),
},
{
title: "Rest " + this.$t('api_test.definition.document.request_param'),
value: this.getScript("rest"),
},
]
},
{
title: this.$t('project.code_segment.code_segment'),
children: [
{
title: this.$t('project.code_segment.insert_segment'),
command: "custom_function",
}
]
},
];
return returnData;
}
},
props: {
isReadOnly: {
type: Boolean,
@ -137,26 +134,99 @@ export default {
jsr223Processor: {
type: Object,
},
showApi:{
type:Boolean,
default:true,
showApi: {
type: Boolean,
default: true,
},
node: {},
},
watch: {
jsr223Processor() {
this.reload();
},
'jsr223Processor.scriptLanguage'(){
if (this.showApi) {
this.baseCodeTemplates = this.httpCodeTemplates;
} else {
this.baseCodeTemplates = this.tcpCodeTemplates;
}
alert(JSON.stringify(this.baseCodeTemplates));
}
}
,
methods: {
addTemplate(template) {
if (!this.jsr223ProcessorData.script) {
this.jsr223ProcessorData.script = "";
getScript(type) {
let returnScript = "";
let laguanges = "beanshell";
if (this.jsr223Processor) {
laguanges = this.jsr223Processor.scriptLanguage
}
this.jsr223ProcessorData.script += template.value;
if (this.jsr223ProcessorData.scriptLanguage === 'beanshell') {
this.jsr223ProcessorData.script += ';';
switch (type) {
case "address":
if (laguanges === "python") {
returnScript = 'param=vars["address"]';
} else {
returnScript = 'var param=vars.get("address")';
}
break;
case "header":
if (laguanges === "python") {
returnScript = 'param=vars["header.${param}"]';
} else {
returnScript = 'var param=vars.get("header.${param}")';
}
break;
case "body":
if (laguanges === "python") {
returnScript = 'param=vars["body.${param}"]';
} else {
returnScript = 'var param=vars.get(body.${param}")\n' +
'//如果对象是多层JSON需要引入fastjson协助解析:\n' +
'// 以"{\"name\":\"user\",\"rows\":[{\"type\":1}]}" 为demo,取rows第1个的type数据:\n' +
'import com.alibaba.fastjson.JSON;\n' +
'import com.alibaba.fastjson.JSONArray;\n' +
'import com.alibaba.fastjson.JSONObject;\n' +
'\n' +
'var jsonParam = vars.get("body.json");\n' +
'JSONObject jsonObject = JSONObject.parseObject(jsonParam);\n' +
'var value = jsonObject.getJSONArray("rows").getJSONObject(0).getString("type");\n' +
'vars.put("key1","value");\n';
}
break;
case "bodyRaw":
if (laguanges === "python") {
returnScript = 'param=vars["bodyRaw"]';
} else {
returnScript = 'var param=vars.get("bodyRaw")';
}
break;
case "query":
if (laguanges === "python") {
returnScript = 'param=vars["query.${param}"]';
} else {
returnScript = 'var param=vars.get("query.${param}")';
}
break;
case "rest":
if (laguanges === "python") {
returnScript = 'param=vars["rest.${param}"]';
} else {
returnScript = 'var param=vars.get("rest.${param}")';
}
break;
default:
break;
}
return returnScript;
},
addTemplate(template) {
if (!this.jsr223Processor.script) {
this.jsr223Processor.script = "";
}
this.jsr223Processor.script += template.value;
if (this.jsr223Processor.scriptLanguage === 'beanshell') {
this.jsr223Processor.script += ';';
}
this.reload();
},
@ -165,12 +235,12 @@ export default {
this.$nextTick(() => (this.isCodeEditAlive = true));
},
languageChange(language) {
this.jsr223ProcessorData.scriptLanguage = language;
this.jsr223Processor.scriptLanguage = language;
this.$emit("languageChange");
},
addCustomFuncScript(script) {
this.jsr223ProcessorData.script = this.jsr223ProcessorData.script ?
this.jsr223ProcessorData.script + '\n\n' + script : script;
this.jsr223Processor.script = this.jsr223Processor.script ?
this.jsr223Processor.script + '\n\n' + script : script;
this.reload();
},
switchMenu() {
@ -184,10 +254,10 @@ export default {
}
},
handleCodeTemplate(code) {
if (!this.jsr223ProcessorData.script) {
this.jsr223ProcessorData.script = code;
if (!this.jsr223Processor.script) {
this.jsr223Processor.script = code;
} else {
this.jsr223ProcessorData.script = this.jsr223ProcessorData.script + '\n' + code;
this.jsr223Processor.script = this.jsr223Processor.script + '\n' + code;
}
this.reload();
},
@ -230,14 +300,15 @@ export default {
}
.show-menu {
text-align:center;
text-align: center;
font-weight: bold;
color:#935aa1;
color: #935aa1;
font-size: 18px;
cursor: pointer;
}
.show-menu:hover {
color:#935aa1;
color: #935aa1;
}
</style>

View File

@ -47,7 +47,19 @@
<el-col class="item" v-if="isActive && item.type !== 'file'">
<el-input-number v-if="item.rangeType === 'length_eq' || item.rangeType === 'length_not_eq' || item.rangeType === 'length_large_than' || item.rangeType === 'length_shot_than'"
v-model="item.value" size="small" :placeholder="valueText" show-word-limit />
<el-input v-else v-model="item.value" size="small" :placeholder="valueText" show-word-limit />
<el-autocomplete
v-else
: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 pointer" @click="advanced(item)"></i>
</el-autocomplete>
</el-col>
<el-col class="item">

View File

@ -1,35 +1,56 @@
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
<div class="text-container" style="border:1px #DCDFE6 solid; height: 100%;border-radius: 4px ;width: 100%">
<el-form :model="response" ref="response" label-width="100px">
<template>
<div v-if="reloaded">
<div class="text-container" style="border:1px #DCDFE6 solid; height: 100%;border-radius: 4px ;width: 100%">
<el-form :model="response" ref="response" label-width="100px">
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive" style="margin: 20px">
<el-tab-pane v-if="!isTcp" :label="$t('api_test.definition.request.response_header')" name="headers" class="pane">
<ms-api-key-value style="width: 95%" :isShowEnable="false" :suggestions="headerSuggestions" :items="response.headers"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.response_body')" name="body" class="pane">
<mock-api-response-body :isReadOnly="false" :isShowEnable="false" :api-id="apiId" :body="response.body" :headers="response.headers"/>
</el-tab-pane>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive" style="margin: 20px">
<el-tab-pane v-if="!isTcp" :label="$t('api_test.definition.request.response_header')" name="headers"
class="pane">
<ms-api-key-value style="width: 95%" :isShowEnable="false" :suggestions="headerSuggestions"
:items="response.headers"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.definition.request.response_body')" name="body" class="pane">
<mock-api-response-body :isReadOnly="false" :isShowEnable="false" :api-id="apiId" :body="response.body"
:headers="response.headers" :use-post-script="response.usePostScript"/>
</el-tab-pane>
<el-tab-pane v-if="!isTcp" :label="$t('api_test.definition.request.status_code')" name="status_code" class="pane">
<el-row>
<el-col :span="2"/>
<el-col :span="20">
<el-input size="small" style="width: 180px;margin-top: 10px" v-model="response.httpCode"/>
</el-col>
<el-col :span="2"/>
</el-row>
</el-tab-pane>
<el-tab-pane :label="$t('commons.response_time_delay')" name="delayed" class="pane">
<el-row>
<el-input-number v-model="response.delayed" :min="0">
<template slot="append">ms</template>
</el-input-number>
</el-row>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</el-form>
<el-tab-pane v-if="!isTcp" :label="$t('api_test.definition.request.status_code')" name="status_code"
class="pane">
<el-row>
<el-col :span="2"/>
<el-col :span="20">
<el-input size="small" style="width: 180px;margin-top: 10px" v-model="response.httpCode"/>
</el-col>
<el-col :span="2"/>
</el-row>
</el-tab-pane>
<el-tab-pane :label="$t('commons.response_time_delay')" name="delayed" class="pane">
<el-row>
<el-input-number v-model="response.delayed" :min="0">
<template slot="append">ms</template>
</el-input-number>
</el-row>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</el-form>
</div>
<div v-if="response.usePostScript">
<el-row style="margin-top: 10px;">
<el-col :span="12">
<p class="tip">{{ $t('api_test.definition.request.post_script') }}</p>
</el-col>
<el-col :span="12">
<i class="el-icon-close" @click="removePostScript"/>
</el-col>
</el-row>
<div class="text-container" style="border:1px #DCDFE6 solid; height: 100%;border-radius: 4px ;width: 100%">
<div style="padding: 15px 0;">
<mock-api-script-editor :jsr223-processor="response.body.scriptObject"/>
</div>
</div>
</div>
</div>
</template>
@ -44,6 +65,7 @@ import MsApiExtract from "@/business/components/api/definition/components/extrac
import BatchAddParameter from "@/business/components/api/definition/components/basis/BatchAddParameter";
import MsApiAdvancedConfig from "@/business/components/api/definition/components/request/http/ApiAdvancedConfig";
import MsJsr233Processor from "@/business/components/api/automation/scenario/component/Jsr233Processor";
import MockApiScriptEditor from "@/business/components/api/definition/components/mock/Components/MockApiScriptEditor";
import ApiDefinitionStepButton
from "@/business/components/api/definition/components/request/components/ApiDefinitionStepButton";
import {Body, BODY_FORMAT} from "@/business/components/api/definition/model/ApiTestModel";
@ -61,13 +83,14 @@ export default {
MsApiExtract,
MsApiAuthConfig,
MockApiResponseBody,
MockApiScriptEditor,
MsApiKeyValue,
MsApiAssertions
},
props: {
response: {},
apiId:String,
isTcp:{
apiId: String,
isTcp: {
type: Boolean,
default: false,
}
@ -75,6 +98,7 @@ export default {
data() {
return {
isActive: true,
reloaded: true,
activeName: "body",
modes: ['text', 'json', 'xml', 'html'],
sqlModes: ['text', 'table'],
@ -85,16 +109,30 @@ export default {
};
},
watch: {
response(){
response() {
this.setBodyType();
this.setReqMessage();
}
},
},
created() {
this.setBodyType();
this.setReqMessage();
},
methods: {
setUsePostScript() {
this.response.usePostScript = true;
this.refresh();
},
removePostScript() {
this.response.usePostScript = false;
this.refresh();
},
refresh() {
this.reloaded = false;
this.$nextTick(() => {
this.reloaded = true;
})
},
modeChange(mode) {
this.mode = mode;
},
@ -102,13 +140,16 @@ export default {
this.mode = mode;
},
setBodyType() {
if (!this.response.usePostScript) {
this.response.usePostScript = false;
}
if (!this.response || !this.response || !this.response.headers) {
return;
}
if(!this.response.httpCode || this.response.httpCode === ''){
this.$set(this.response,"httpCode","200");
if (!this.response.httpCode || this.response.httpCode === '') {
this.$set(this.response, "httpCode", "200");
}
if(!this.response.delayed){
if (!this.response.delayed) {
this.response.delayed = 0;
}
if (Object.prototype.toString.call(this.response).match(/\[object (\w+)\]/)[1].toLowerCase() !== 'object') {
@ -167,8 +208,8 @@ export default {
this.response.vars = "";
}
this.reqMessages = this.$t('api_test.request.address') + ":\n" + this.response.url + "\n" +
this.$t('api_test.scenario.headers') + ":\n" + this.response.headers + "\n" + "Cookies :\n" +
this.response.cookies + "\n" + "Body:" + "\n" + this.response.body;
this.$t('api_test.scenario.headers') + ":\n" + this.response.headers + "\n" + "Cookies :\n" +
this.response.cookies + "\n" + "Body:" + "\n" + this.response.body;
}
},
},
@ -223,4 +264,12 @@ export default {
pre {
margin: 0;
}
.el-icon-close {
position: absolute;
font-size: 20px;
right: 10px;
top: 10px;
color: gray;
}
</style>

View File

@ -7,39 +7,42 @@
@saveMockExpectConfig="saveMockExpectConfig"
/>
</template>
<el-container>
<el-main>
<!-- 期望详情 -->
<p class="tip">{{ $t('api_test.mock.request_condition') }}</p>
<el-form :model="mockExpectConfig" :rules="rule" ref="mockExpectForm" label-width="80px" label-position="right">
<div>
<!-- 期望详情 -->
<p class="tip">{{ $t('api_test.mock.request_condition') }}</p>
<el-form :model="mockExpectConfig" :rules="rule" ref="mockExpectForm" label-width="80px" label-position="right">
<div class="card">
<div class="base-info">
<el-row>
<tcp-params
v-if="isTcp"
:request="mockExpectConfig.request" style="margin: 10px 10px;" ref="tcpParam"></tcp-params>
<mock-request-param
v-else
:isShowEnable="false"
:referenced="true"
:is-read-only="false"
:api-params="apiParams"
:request="mockExpectConfig.request.params"/>
</el-row>
<div class="card">
<div class="base-info">
<el-row>
<tcp-params
v-if="isTcp"
:request="mockExpectConfig.request" style="margin: 10px 10px;" ref="tcpParam"></tcp-params>
<mock-request-param
v-else
:isShowEnable="false"
:referenced="true"
:is-read-only="false"
:api-params="apiParams"
:request="mockExpectConfig.request.params"/>
</el-row>
<el-row style="margin-top: 10px;">
<el-row style="margin-top: 10px;">
<el-col :span="12">
<p class="tip">{{ $t('api_test.mock.rsp_param') }}</p>
</el-row>
<el-row>
<mock-response-param :api-id="apiId" :is-tcp="isTcp"
:response="mockExpectConfig.response.responseResult"/>
</el-row>
</div>
</el-col>
<el-col :span="12">
<el-button class="ms-right-buttion" size="small" @click="addPostScript">+{{$t('api_test.definition.request.post_script')}}</el-button>
</el-col>
</el-row>
<el-row>
<mock-response-param :api-id="apiId" :is-tcp="isTcp"
:response="mockExpectConfig.response.responseResult" ref="mockResponseParam"/>
</el-row>
</div>
</el-form>
</el-main>
</el-container>
</div>
</el-form>
</div>
</ms-drawer>
</template>
<script>
@ -57,21 +60,21 @@ import TcpParams from "@/business/components/api/definition/components/request/t
export default {
name: 'MockEditDrawer',
components: {
MsDrawer,MockConfigHeader,MockRowVariables,MsCodeEdit,MockRequestParam,MockResponseParam,TcpParams
MsDrawer, MockConfigHeader, MockRowVariables, MsCodeEdit, MockRequestParam, MockResponseParam, TcpParams
},
props: {
apiParams: Object,
apiId:String,
mockConfigId:String,
isTcp:{
type:Boolean,
apiId: String,
mockConfigId: String,
isTcp: {
type: Boolean,
default: false,
}
},
data() {
return {
showDrawer: false,
mockExpectConfig:{},
mockExpectConfig: {},
showHeadTable: true,
headerSuggestions: REQUEST_HEADERS,
baseMockExpectConfig: {
@ -83,13 +86,13 @@ export default {
jsonParam: false,
variables: [],
jsonData: "{}",
params:{
headers:[],
arguments:[],
rest:[],
body:{
params: {
headers: [],
arguments: [],
rest: [],
body: {
type: 'JSON',
binary:[],
binary: [],
kvs: [],
}
}
@ -98,14 +101,14 @@ export default {
httpCode: "",
httpHeads: [],
body: "",
responseResult:{
responseResult: {
delayed: 0,
headers:[],
arguments:[],
rest:[],
body:{
headers: [],
arguments: [],
rest: [],
body: {
type: 'JSON',
binary:[]
binary: []
}
}
},
@ -122,25 +125,26 @@ export default {
},
};
},
watch: {
},
watch: {},
created() {
this.mockExpectConfig = JSON.parse(JSON.stringify(this.baseMockExpectConfig));
},
computed: {
},
computed: {},
methods: {
addPostScript(){
this.$refs.mockResponseParam.setUsePostScript();
},
uuid: function () {
return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1);
},
open(param){
open(param) {
this.mockExpectConfig = JSON.parse(JSON.stringify(this.baseMockExpectConfig));
if(param){
if (param) {
this.mockExpectConfig = param;
if(!this.mockExpectConfig.request.params){
if (!this.mockExpectConfig.request.params) {
let requestParamsObj = {
rest:[],
rest: [],
headers: [],
arguments: [],
body: {
@ -149,56 +153,56 @@ export default {
kvs: [],
},
};
this.$set(this.mockExpectConfig.request,"params",requestParamsObj);
this.$set(this.mockExpectConfig.request, "params", requestParamsObj);
if(this.mockExpectConfig.request.jsonParam && this.mockExpectConfig.request.jsonData){
if (this.mockExpectConfig.request.jsonParam && this.mockExpectConfig.request.jsonData) {
this.mockExpectConfig.request.params.body.type = "JSON";
this.mockExpectConfig.request.params.body.raw = this.mockExpectConfig.request.jsonData;
}else if(this.mockExpectConfig.request.variables){
} else if (this.mockExpectConfig.request.variables) {
this.mockExpectConfig.request.params.body.type = "Form Data";
let headerItem = {};
headerItem.enable = true;
this.mockExpectConfig.request.params.headers.push(headerItem);
this.mockExpectConfig.request.variables.forEach(item => {
this.mockExpectConfig.request.params.arguments.push({
description : "",
type : "text",
name : item.name,
value : item.value,
required : true,
contentType : "text/plain",
uuid : this.uuid(),
description: "",
type: "text",
name: item.name,
value: item.value,
required: true,
contentType: "text/plain",
uuid: this.uuid(),
});
this.mockExpectConfig.request.params.body.kvs.push({
description : "",
type : "text",
name : item.name,
value : item.value,
required : true,
contentType : "text/plain",
uuid : this.uuid(),
description: "",
type: "text",
name: item.name,
value: item.value,
required: true,
contentType: "text/plain",
uuid: this.uuid(),
});
});
}
}
if (!this.mockExpectConfig.response.responseResult) {
let responseResultObj = {
headers:[],
arguments:[],
rest:[],
headers: [],
arguments: [],
rest: [],
httpCode: this.mockExpectConfig.response.httpCode,
delayed: this.mockExpectConfig.response.delayed,
body:{
delayed: this.mockExpectConfig.response.delayed,
body: {
type: "Raw",
raw: this.mockExpectConfig.response.body,
binary:[]
binary: []
}
};
this.$set(this.mockExpectConfig.response,"responseResult",responseResultObj);
if(this.mockExpectConfig.response.httpHeads){
this.$set(this.mockExpectConfig.response, "responseResult", responseResultObj);
if (this.mockExpectConfig.response.httpHeads) {
this.mockExpectConfig.response.httpHeads.forEach(item => {
this.mockExpectConfig.response.responseResult.headers.push({
enable:true,
enable: true,
name: item.name,
value: item.value,
});
@ -210,7 +214,7 @@ export default {
this.showDrawer = true;
this.$refs.mockDrawer.setfullScreen();
},
close(){
close() {
this.showDrawer = false;
},
saveMockExpectConfig() {
@ -231,19 +235,19 @@ export default {
uploadMockExpectConfig(clearForm) {
let url = "/mockConfig/updateMockExpectConfig";
let param = this.mockExpectConfig;
if(!param.name || param.name === ''){
if (!param.name || param.name === '') {
this.$error(this.$t('test_track.case.input_name'));
return false;
}else if(param.name.length > 100){
this.$error(this.$t('test_track.length_less_than')+100);
} else if (param.name.length > 100) {
this.$error(this.$t('test_track.length_less_than') + 100);
return false;
}
if(!param.response.responseResult.httpCode){
if (!param.response.responseResult.httpCode) {
param.response.responseResult.httpCode = 200;
}
if(!param.request.params.id){
if (!param.request.params.id) {
param.request.params.id = getUUID();
}
let obj = {
@ -255,7 +259,7 @@ export default {
this.$fileUpload(url, null, bodyFiles, param, response => {
let returnData = response.data;
this.mockExpectConfig.id = returnData.id;
this.$emit('refreshMockInfo',param.mockConfigId);
this.$emit('refreshMockInfo', param.mockConfigId);
if (clearForm) {
this.cleanMockExpectConfig();
}
@ -357,4 +361,16 @@ export default {
margin-top: 40px;
}
.base-info {
width: 99%;
margin-left: 5px;
}
.ms-right-buttion {
float: right;
margin: 6px 0px 8px 30px;
color: #783887;
background-color: #F2ECF3;
border: #F2ECF3;
}
</style>