feat(接口定制): 基础保存功能完善

This commit is contained in:
fit2-zhao 2020-11-13 09:38:37 +08:00
parent 9bdb4f9625
commit a887840214
59 changed files with 2524 additions and 632 deletions

View File

@ -2,6 +2,7 @@ package io.metersphere.api.controller;
import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.dto.delimit.ApiDelimitRequest;
import io.metersphere.api.dto.delimit.ApiDelimitResult;
import io.metersphere.api.dto.delimit.SaveApiDelimitRequest;
@ -59,4 +60,18 @@ public class ApiDelimitController {
return apiDelimitService.get(id);
}
@PostMapping(value = "/run/debug", consumes = {"multipart/form-data"})
public String runDebug(@RequestPart("request") SaveApiDelimitRequest request, @RequestPart(value = "file") MultipartFile file, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
return apiDelimitService.run(request, file, bodyFiles);
}
@PostMapping(value = "/run", consumes = {"multipart/form-data"})
public String run(@RequestPart("request") SaveApiDelimitRequest request, @RequestPart(value = "file") MultipartFile file, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
return apiDelimitService.run(request, file, bodyFiles);
}
@GetMapping("/report/get/{testId}/{test}")
public APIReportResult getReport(@PathVariable String testId, @PathVariable String test) {
return apiDelimitService.getResult(testId, test);
}
}

View File

@ -1,6 +1,7 @@
package io.metersphere.api.dto.delimit;
import io.metersphere.api.dto.scenario.request.Request;
import io.metersphere.api.dto.delimit.response.Response;
import io.metersphere.base.domain.Schedule;
import lombok.Getter;
import lombok.Setter;
@ -13,6 +14,8 @@ public class SaveApiDelimitRequest {
private String id;
private String reportId;
private String projectId;
private String name;
@ -31,6 +34,10 @@ public class SaveApiDelimitRequest {
private Request request;
private Response response;
private String environmentId;
private String userId;
private Schedule schedule;

View File

@ -0,0 +1,26 @@
package io.metersphere.api.dto.delimit.response;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.scenario.Body;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.request.RequestType;
import lombok.Data;
import lombok.EqualsAndHashCode;
import java.util.List;
@Data
@EqualsAndHashCode(callSuper = true)
@JSONType(typeName = RequestType.HTTP)
public class HttpResponse extends Response {
// type 必须放最前面以便能够转换正确的类
private String type = RequestType.HTTP;
@JSONField(ordinal = 1)
private List<KeyValue> headers;
@JSONField(ordinal = 2)
private List<KeyValue> statusCode;
@JSONField(ordinal = 3)
private Body body;
}

View File

@ -0,0 +1,23 @@
package io.metersphere.api.dto.delimit.response;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import com.fasterxml.jackson.annotation.JsonSubTypes;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import io.metersphere.api.dto.scenario.request.RequestType;
import lombok.Data;
@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type")
@JsonSubTypes({
@JsonSubTypes.Type(value = HttpResponse.class, name = RequestType.HTTP),
})
@JSONType(seeAlso = {HttpResponse.class}, typeKey = "type")
@Data
public abstract class Response {
@JSONField(ordinal = 1)
private String id;
@JSONField(ordinal = 2)
private String name;
@JSONField(ordinal = 3)
private Boolean enable;
}

View File

@ -9,5 +9,7 @@ public class Body {
private String type;
private String raw;
private String format;
private Object json;
private String xml;
private List<KeyValue> kvs;
}

View File

@ -14,15 +14,18 @@ public class KeyValue {
private String description;
private String contentType;
private boolean enable;
private boolean required;
public KeyValue() {
this.enable = true;
this.required = true;
}
public KeyValue(String name, String value) {
this.name = name;
this.value = value;
this.enable = true;
this.required = true;
}
public KeyValue(String name, String value, String description) {
@ -30,5 +33,6 @@ public class KeyValue {
this.value = value;
this.enable = true;
this.description = description;
this.required = true;
}
}

View File

@ -0,0 +1,65 @@
package io.metersphere.api.jmeter;
import com.alibaba.fastjson.JSON;
import io.metersphere.base.domain.ApiDelimitExecResult;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class ApiTestResult {
private String id;
private String name;
private long responseTime;
private int error = 0;
private int success = 0;
private int totalAssertions = 0;
private int passAssertions = 0;
private final List<RequestResult> requestResults = new ArrayList<>();
public void addRequestResult(RequestResult result) {
requestResults.add(result);
}
public ApiTestResult() {
}
public ApiTestResult(ApiDelimitExecResult result) {
this.id = result.getId();
this.responseTime = result.getEndTime();
this.addRequestResult(JSON.parseObject(result.getContent(), RequestResult.class));
}
public void addResponseTime(long time) {
this.responseTime += time;
}
public void addError(int count) {
this.error += count;
}
public void addSuccess() {
this.success++;
}
public int getSuccess() {
return this.success;
}
public void addTotalAssertions(int count) {
this.totalAssertions += count;
}
public void addPassAssertions(int count) {
this.passAssertions += count;
}
}

View File

@ -0,0 +1,165 @@
package io.metersphere.api.jmeter;
import io.metersphere.api.service.ApiDelimitExecResultService;
import io.metersphere.api.service.ApiDelimitService;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.LogUtil;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.assertions.AssertionResult;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.visualizers.backend.AbstractBackendListenerClient;
import org.apache.jmeter.visualizers.backend.BackendListenerContext;
import org.springframework.http.HttpMethod;
import java.io.Serializable;
import java.util.*;
/**
* JMeter BackendListener扩展, jmx脚本中使用
*/
public class DelimitBackendListenerClient extends AbstractBackendListenerClient implements Serializable {
public final static String TEST_ID = "ms.test.id";
private final List<SampleResult> queue = new ArrayList<>();
public String runMode = ApiRunMode.RUN.name();
private ApiDelimitService apiDelimitService;
private ApiDelimitExecResultService apiDelimitExecResultService;
// 测试ID
private String testId;
// 运行结果报告ID
private String reportId;
@Override
public void setupTest(BackendListenerContext context) throws Exception {
setParam(context);
apiDelimitService = CommonBeanFactory.getBean(ApiDelimitService.class);
if (apiDelimitService == null) {
LogUtil.error("apiDelimitService is required");
}
apiDelimitExecResultService = CommonBeanFactory.getBean(ApiDelimitExecResultService.class);
if (apiDelimitExecResultService == null) {
LogUtil.error("apiDelimitExecResultService is required");
}
super.setupTest(context);
}
@Override
public void handleSampleResults(List<SampleResult> sampleResults, BackendListenerContext context) {
queue.addAll(sampleResults);
}
@Override
public void teardownTest(BackendListenerContext context) throws Exception {
ApiTestResult testResult = new ApiTestResult();
testResult.setId(testId);
queue.forEach(result -> {
RequestResult reqResult = getRequestResult(result);
testResult.addRequestResult(reqResult);
testResult.addPassAssertions(reqResult.getPassAssertions());
testResult.addTotalAssertions(reqResult.getTotalAssertions());
});
// 调试操作不需要存储结果
if (StringUtils.isBlank(reportId)) {
apiDelimitService.addResult(testResult);
} else {
apiDelimitExecResultService.saveApiResult(testResult);
}
queue.clear();
super.teardownTest(context);
}
private RequestResult getRequestResult(SampleResult result) {
RequestResult reqResult = new RequestResult();
reqResult.setName(result.getSampleLabel());
reqResult.setUrl(result.getUrlAsString());
reqResult.setMethod(getMethod(result));
reqResult.setBody(result.getSamplerData());
reqResult.setHeaders(result.getRequestHeaders());
reqResult.setRequestSize(result.getSentBytes());
reqResult.setStartTime(result.getStartTime());
reqResult.setTotalAssertions(result.getAssertionResults().length);
reqResult.setSuccess(result.isSuccessful());
reqResult.setError(result.getErrorCount());
for (SampleResult subResult : result.getSubResults()) {
reqResult.getSubRequestResults().add(getRequestResult(subResult));
}
ResponseResult responseResult = reqResult.getResponseResult();
responseResult.setBody(result.getResponseDataAsString());
responseResult.setHeaders(result.getResponseHeaders());
responseResult.setLatency(result.getLatency());
responseResult.setResponseCode(result.getResponseCode());
responseResult.setResponseSize(result.getResponseData().length);
responseResult.setResponseTime(result.getTime());
responseResult.setResponseMessage(result.getResponseMessage());
if (JMeterVars.get(result.hashCode()) != null) {
List<String> vars = new LinkedList<>();
JMeterVars.get(result.hashCode()).entrySet().parallelStream().reduce(vars, (first, second) -> {
first.add(second.getKey() + "" + second.getValue());
return first;
}, (first, second) -> {
if (first == second) {
return first;
}
first.addAll(second);
return first;
});
responseResult.setVars(StringUtils.join(vars, "\n"));
JMeterVars.remove(result.hashCode());
}
for (AssertionResult assertionResult : result.getAssertionResults()) {
ResponseAssertionResult responseAssertionResult = getResponseAssertionResult(assertionResult);
if (responseAssertionResult.isPass()) {
reqResult.addPassAssertions();
}
responseResult.getAssertions().add(responseAssertionResult);
}
return reqResult;
}
private String getMethod(SampleResult result) {
String body = result.getSamplerData();
// Dubbo Protocol
String start = "RPC Protocol: ";
String end = "://";
if (StringUtils.contains(body, start)) {
return StringUtils.substringBetween(body, start, end).toUpperCase();
} else {
// Http Method
String method = StringUtils.substringBefore(body, " ");
for (HttpMethod value : HttpMethod.values()) {
if (StringUtils.equals(method, value.name())) {
return method;
}
}
return "Request";
}
}
private void setParam(BackendListenerContext context) {
this.testId = context.getParameter(TEST_ID);
this.runMode = context.getParameter("runMode");
this.reportId = context.getParameter("reportId");
if (StringUtils.isBlank(this.runMode)) {
this.runMode = ApiRunMode.RUN.name();
}
}
private ResponseAssertionResult getResponseAssertionResult(AssertionResult assertionResult) {
ResponseAssertionResult responseAssertionResult = new ResponseAssertionResult();
responseAssertionResult.setName(assertionResult.getName());
responseAssertionResult.setPass(!assertionResult.isFailure() && !assertionResult.isError());
if (!responseAssertionResult.isPass()) {
responseAssertionResult.setMessage(assertionResult.getFailureMessage());
}
return responseAssertionResult;
}
}

View File

@ -0,0 +1,80 @@
package io.metersphere.api.jmeter;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.config.JmeterProperties;
import io.metersphere.i18n.Translator;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.visualizers.backend.BackendListener;
import org.apache.jorphan.collections.HashTree;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.io.File;
import java.io.InputStream;
import java.lang.reflect.Field;
@Service
public class DelimitJMeterService {
@Resource
private JmeterProperties jmeterProperties;
public void run(String testId, String reportId, InputStream is) {
String home = getJmeterHome();
String jmeterProperties = home + "/bin/jmeter.properties";
JMeterUtils.loadJMeterProperties(jmeterProperties);
JMeterUtils.setJMeterHome(home);
JMeterUtils.setLocale(LocaleContextHolder.getLocale());
try {
Object scriptWrapper = SaveService.loadElement(is);
HashTree testPlan = getHashTree(scriptWrapper);
JMeterVars.addJSR223PostProcessor(testPlan);
addBackendListener(testId, reportId, testPlan);
LocalRunner runner = new LocalRunner(testPlan);
runner.run();
} catch (Exception e) {
LogUtil.error(e.getMessage(), e);
MSException.throwException(Translator.get("api_load_script_error"));
}
}
public String getJmeterHome() {
String home = getClass().getResource("/").getPath() + "jmeter";
try {
File file = new File(home);
if (file.exists()) {
return home;
} else {
return jmeterProperties.getHome();
}
} catch (Exception e) {
return jmeterProperties.getHome();
}
}
private HashTree getHashTree(Object scriptWrapper) throws Exception {
Field field = scriptWrapper.getClass().getDeclaredField("testPlan");
field.setAccessible(true);
return (HashTree) field.get(scriptWrapper);
}
private void addBackendListener(String testId, String debugReportId, HashTree testPlan) {
BackendListener backendListener = new BackendListener();
backendListener.setName(testId);
Arguments arguments = new Arguments();
arguments.addArgument(DelimitBackendListenerClient.TEST_ID, testId);
arguments.addArgument("runMode", ApiRunMode.DEBUG.name());
arguments.addArgument("reportId", debugReportId);
backendListener.setArguments(arguments);
backendListener.setClassname(DelimitBackendListenerClient.class.getCanonicalName());
testPlan.add(testPlan.getArray()[0], backendListener);
}
}

View File

@ -0,0 +1,38 @@
package io.metersphere.api.service;
import com.alibaba.fastjson.JSON;
import io.metersphere.api.jmeter.ApiTestResult;
import io.metersphere.base.domain.ApiDelimitExecResult;
import io.metersphere.base.mapper.ApiDelimitExecResultMapper;
import io.metersphere.commons.utils.SessionUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Objects;
import java.util.UUID;
@Service
@Transactional(rollbackFor = Exception.class)
public class ApiDelimitExecResultService {
@Resource
private ApiDelimitExecResultMapper apiDelimitExecResultMapper;
public void saveApiResult(ApiTestResult result) {
result.getRequestResults().forEach(item -> {
// 清理原始资源每个执行 保留一条结果
apiDelimitExecResultMapper.deleteByResourceId(item.getName());
ApiDelimitExecResult saveResult = new ApiDelimitExecResult();
saveResult.setId(UUID.randomUUID().toString());
saveResult.setUserId(Objects.requireNonNull(SessionUtils.getUser()).getId());
saveResult.setName(item.getUrl());
saveResult.setResourceId(item.getName());
saveResult.setContent(JSON.toJSONString(item));
saveResult.setStartTime(item.getStartTime());
saveResult.setEndTime(item.getResponseResult().getResponseTime());
saveResult.setStatus(item.getResponseResult().getResponseCode().equals("200") ? "success" : "error");
apiDelimitExecResultMapper.insert(saveResult);
});
}
}

View File

@ -1,10 +1,15 @@
package io.metersphere.api.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.dto.delimit.ApiComputeResult;
import io.metersphere.api.dto.delimit.ApiDelimitRequest;
import io.metersphere.api.dto.delimit.ApiDelimitResult;
import io.metersphere.api.dto.delimit.SaveApiDelimitRequest;
import io.metersphere.api.jmeter.ApiTestResult;
import io.metersphere.api.jmeter.DelimitJMeterService;
import io.metersphere.api.parse.JmeterDocumentParser;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.ApiDelimitExecResultMapper;
import io.metersphere.base.mapper.ApiDelimitMapper;
@ -23,6 +28,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import sun.security.util.Cache;
import javax.annotation.Resource;
import java.io.*;
@ -46,6 +52,9 @@ public class ApiDelimitService {
private ApiTestCaseService apiTestCaseService;
@Resource
private ApiDelimitExecResultMapper apiDelimitExecResultMapper;
@Resource
private DelimitJMeterService jMeterService;
private static Cache cache = Cache.newHardMemoryCache(0, 3600 * 24);
private static final String BODY_FILE_DIR = "/opt/metersphere/data/body";
@ -79,7 +88,7 @@ public class ApiDelimitService {
public void create(SaveApiDelimitRequest request, MultipartFile file, List<MultipartFile> bodyFiles) {
List<String> bodyUploadIds = new ArrayList<>(request.getBodyUploadIds());
ApiDelimit test = createTest(request, file);
createBodyFiles(test, bodyUploadIds, bodyFiles);
createBodyFiles(test.getId(), bodyUploadIds, bodyFiles);
}
private ApiDelimit createTest(SaveApiDelimitRequest request, MultipartFile file) {
@ -109,13 +118,13 @@ public class ApiDelimitService {
List<String> bodyUploadIds = new ArrayList<>(request.getBodyUploadIds());
request.setBodyUploadIds(null);
ApiDelimit test = updateTest(request);
createBodyFiles(test, bodyUploadIds, bodyFiles);
createBodyFiles(test.getId(), bodyUploadIds, bodyFiles);
saveFile(test.getId(), file);
}
private void createBodyFiles(ApiDelimit test, List<String> bodyUploadIds, List<MultipartFile> bodyFiles) {
private void createBodyFiles(String testId, List<String> bodyUploadIds, List<MultipartFile> bodyFiles) {
if (bodyUploadIds.size() > 0) {
String dir = BODY_FILE_DIR + "/" + test.getId();
String dir = BODY_FILE_DIR + "/" + testId;
File testDir = new File(dir);
if (!testDir.exists()) {
testDir.mkdirs();
@ -181,6 +190,8 @@ public class ApiDelimitService {
test.setPath(request.getPath());
test.setUrl(request.getUrl());
test.setDescription(request.getDescription());
test.setResponse(JSONObject.toJSONString(request.getResponse()));
test.setEnvironmentId(request.getEnvironmentId());
test.setUserId(request.getUserId());
apiDelimitMapper.updateByPrimaryKeySelective(test);
@ -201,6 +212,8 @@ public class ApiDelimitService {
test.setUpdateTime(System.currentTimeMillis());
test.setStatus(APITestStatus.Underway.name());
test.setModulePath(request.getModulePath());
test.setResponse(JSONObject.toJSONString(request.getResponse()));
test.setEnvironmentId(request.getEnvironmentId());
if (request.getUserId() == null) {
test.setUserId(Objects.requireNonNull(SessionUtils.getUser()).getId());
} else {
@ -212,18 +225,18 @@ public class ApiDelimitService {
}
private void saveFile(String apiId, MultipartFile file) {
final FileMetadata fileMetadata = fileService.saveFile(file);
final FileMetadata metadata = fileService.saveFile(file);
ApiTestFile apiTestFile = new ApiTestFile();
apiTestFile.setTestId(apiId);
apiTestFile.setFileId(fileMetadata.getId());
apiTestFile.setFileId(metadata.getId());
apiTestFileMapper.insert(apiTestFile);
}
private void deleteFileByTestId(String apiId) {
ApiTestFileExample ApiTestFileExample = new ApiTestFileExample();
ApiTestFileExample.createCriteria().andTestIdEqualTo(apiId);
final List<ApiTestFile> ApiTestFiles = apiTestFileMapper.selectByExample(ApiTestFileExample);
apiTestFileMapper.deleteByExample(ApiTestFileExample);
ApiTestFileExample apiTestFileExample = new ApiTestFileExample();
apiTestFileExample.createCriteria().andTestIdEqualTo(apiId);
final List<ApiTestFile> ApiTestFiles = apiTestFileMapper.selectByExample(apiTestFileExample);
apiTestFileMapper.deleteByExample(apiTestFileExample);
if (!CollectionUtils.isEmpty(ApiTestFiles)) {
final List<String> fileIds = ApiTestFiles.stream().map(ApiTestFile::getFileId).collect(Collectors.toList());
@ -231,4 +244,61 @@ public class ApiDelimitService {
}
}
/**
* 测试执行
*
* @param request
* @param file
* @param bodyFiles
* @return
*/
public String run(SaveApiDelimitRequest request, MultipartFile file, List<MultipartFile> bodyFiles) {
if (file == null) {
throw new IllegalArgumentException(Translator.get("file_cannot_be_null"));
}
List<String> bodyUploadIds = new ArrayList<>(request.getBodyUploadIds());
createBodyFiles(request.getId(), bodyUploadIds, bodyFiles);
InputStream is = null;
try {
// 解析 xml 处理 mock 数据
byte[] bytes = JmeterDocumentParser.parse(file.getBytes());
is = new ByteArrayInputStream(bytes);
} catch (IOException e) {
LogUtil.error(e);
}
jMeterService.run(request.getId(), request.getReportId(), is);
return request.getId();
}
public void addResult(ApiTestResult res) {
cache.put(res.getId(), res);
}
/**
* 获取执行结果报告
*
* @param testId
* @param test
* @return
*/
public APIReportResult getResult(String testId, String test) {
if (test.equals("debug")) {
Object res = cache.get(testId);
if (res != null) {
cache.remove(testId);
APIReportResult reportResult = new APIReportResult();
reportResult.setContent(JSON.toJSONString(res));
return reportResult;
}
} else {
ApiDelimitExecResult data = apiDelimitExecResultMapper.selectByResourceId(testId);
if (data == null) {
return null;
}
APIReportResult reportResult = new APIReportResult();
reportResult.setContent(data.getContent());
return reportResult;
}
return null;
}
}

View File

@ -16,10 +16,12 @@ public class ApiDelimit implements Serializable {
private String url;
private String description;
private String environmentId;
private String status;
private String description;
private String userId;
private String moduleId;
@ -32,5 +34,7 @@ public class ApiDelimit implements Serializable {
private String request;
private String response;
private static final long serialVersionUID = 1L;
}

View File

@ -12,7 +12,7 @@ public class ApiDelimitExecResult implements Serializable {
private String content;
private String status;
private String userId;
private String startTime;
private String endTime;
private Long startTime;
private Long endTime;
}

View File

@ -7,4 +7,9 @@ public interface ApiDelimitExecResultMapper {
int deleteByResourceId(String id);
int insert(ApiDelimitExecResult record);
ApiDelimitExecResult selectByResourceId(String resourceId);
ApiDelimitExecResult selectByPrimaryKey(String id);
}

View File

@ -10,7 +10,18 @@
insert into api_delimit_exec_result
(id, resource_id,name,content, status, user_id, start_time, end_time)
values
(#{id,jdbcType=VARCHAR}, #{resourceId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{content,jdbcType=LONGVARCHAR}, #{status,jdbcType=VARCHAR},
(#{id,jdbcType=VARCHAR}, #{resourceId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{content,jdbcType=LONGVARCHAR}, #{status,jdbcType=VARCHAR}, #{userId,jdbcType=VARCHAR},
#{startTime,jdbcType=BIGINT}, #{endTime,jdbcType=BIGINT})
</insert>
<select id="selectByResourceId" parameterType="java.lang.String" resultType="io.metersphere.base.domain.ApiDelimitExecResult">
select * from api_delimit_exec_result
where resource_id = #{resourceId,jdbcType=VARCHAR}
</select>
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultType="io.metersphere.base.domain.ApiDelimitExecResult">
select * from api_delimit_exec_result
where id = #{id,jdbcType=VARCHAR}
</select>
</mapper>

View File

@ -23,7 +23,6 @@ public interface ApiDelimitMapper {
int insert(ApiDelimit record);
int insertSelective(ApiDelimit record);
List<ApiDelimit> selectByExampleWithBLOBs(ApiDelimitExample example);
@ -31,11 +30,6 @@ public interface ApiDelimitMapper {
ApiDelimit selectByPrimaryKey(String id);
int updateByExampleSelective(@Param("record") ApiDelimit record, @Param("example") ApiDelimitExample example);
int updateByExampleWithBLOBs(@Param("record") ApiDelimit record, @Param("example") ApiDelimitExample example);
int updateByExample(@Param("record") ApiDelimit record, @Param("example") ApiDelimitExample example);
int updateByPrimaryKeySelective(ApiDelimit record);

View File

@ -17,6 +17,7 @@
</resultMap>
<resultMap extends="BaseResultMap" id="ResultMapWithBLOBs" type="io.metersphere.base.domain.ApiDelimit">
<result column="request" jdbcType="LONGVARCHAR" property="request"/>
<result column="response" jdbcType="LONGVARCHAR" property="response"/>
</resultMap>
<sql id="Example_Where_Clause">
<where>
@ -204,7 +205,7 @@
<select id="list" resultType="io.metersphere.api.dto.delimit.ApiDelimitResult">
select api_delimit.id, api_delimit.project_id,
api_delimit.name,api_delimit.url,api_delimit.module_id,api_delimit.module_path,api_delimit.path,
api_delimit.description,api_delimit.request,
api_delimit.description,api_delimit.request,api_delimit.response,api_delimit.environment_id,
api_delimit.status, api_delimit.user_id, api_delimit.create_time, api_delimit.update_time, project.name as
project_name, user.name as user_name
from api_delimit
@ -280,6 +281,7 @@
order by ${orderByClause}
</if>
</select>
<select id="selectByPrimaryKey" parameterType="java.lang.String" resultMap="ResultMapWithBLOBs">
select
<include refid="Base_Column_List"/>
@ -292,103 +294,24 @@
delete from api_delimit
where id = #{id,jdbcType=VARCHAR}
</delete>
<delete id="deleteByExample" parameterType="io.metersphere.base.domain.ApiDelimitExample">
delete from api_delimit
<if test="_parameter != null">
<include refid="Example_Where_Clause"/>
</if>
</delete>
<insert id="insert" parameterType="io.metersphere.base.domain.ApiDelimit">
insert into api_delimit (id, project_id, name, url,module_id,module_path,path,
description, status, user_id,
create_time, update_time, request
)
values (#{id,jdbcType=VARCHAR}, #{projectId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{url,jdbcType=VARCHAR}, #{moduleId,jdbcType=VARCHAR},#{modulePath,jdbcType=VARCHAR},#{path,jdbcType=VARCHAR},
#{description,jdbcType=VARCHAR}, #{status,jdbcType=VARCHAR}, #{userId,jdbcType=VARCHAR},
#{createTime,jdbcType=BIGINT}, #{updateTime,jdbcType=BIGINT}, #{request,jdbcType=LONGVARCHAR}
)
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.ApiDelimit">
insert into api_delimit
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="projectId != null">
project_id,
</if>
<if test="name != null">
name,
</if>
<if test="url != null">
url,
</if>
<if test="path != null">
path,
</if>
<if test="moduleId != null">
module_id,
</if>
<if test="modulePath != null">
module_path,
</if>
<if test="description != null">
description,
</if>
<if test="status != null">
status,
</if>
<if test="userId != null">
user_id,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="updateTime != null">
update_time,
</if>
<if test="request != null">
request,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=VARCHAR},
</if>
<if test="projectId != null">
#{projectId,jdbcType=VARCHAR},
</if>
<if test="name != null">
#{name,jdbcType=VARCHAR},
</if>
<if test="url != null">
#{url,jdbcType=VARCHAR},
</if>
<if test="moduleId != null">
#{moduleId,jdbcType=VARCHAR},
</if>
<if test="description != null">
#{description,jdbcType=VARCHAR},
</if>
<if test="status != null">
#{status,jdbcType=VARCHAR},
</if>
<if test="userId != null">
#{userId,jdbcType=VARCHAR},
</if>
<if test="createTime != null">
#{createTime,jdbcType=BIGINT},
</if>
<if test="updateTime != null">
#{updateTime,jdbcType=BIGINT},
</if>
<if test="request != null">
#{request,jdbcType=LONGVARCHAR},
</if>
</trim>
insert into api_delimit (id, project_id, name, url,module_id,module_path,path,
description, status, user_id,create_time, update_time, request,response,environment_id )
values
(#{id,jdbcType=VARCHAR}, #{projectId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR}, #{url,jdbcType=VARCHAR}, #{moduleId,jdbcType=VARCHAR},#{modulePath,jdbcType=VARCHAR},#{path,jdbcType=VARCHAR},
#{description,jdbcType=VARCHAR}, #{status,jdbcType=VARCHAR}, #{userId,jdbcType=VARCHAR},
#{createTime,jdbcType=BIGINT}, #{updateTime,jdbcType=BIGINT}, #{request,jdbcType=LONGVARCHAR},#{response,jdbcType=LONGVARCHAR},#{environmentId,jdbcType=VARCHAR}
)
</insert>
<select id="countByExample" parameterType="io.metersphere.base.domain.ApiDelimitExample"
resultType="java.lang.Long">
select count(*) from api_delimit
@ -396,89 +319,7 @@
<include refid="Example_Where_Clause"/>
</if>
</select>
<update id="updateByExampleSelective" parameterType="map">
update api_delimit
<set>
<if test="record.id != null">
id = #{record.id,jdbcType=VARCHAR},
</if>
<if test="record.projectId != null">
project_id = #{record.projectId,jdbcType=VARCHAR},
</if>
<if test="record.name != null">
name = #{record.name,jdbcType=VARCHAR},
</if>
<if test="record.moduleId != null">
module_id = #{record.moduleId,jdbcType=BIGINT},
</if>
<if test="record.modulePath != null">
module_path = #{record.modulePath,jdbcType=BIGINT},
</if>
<if test="record.url != null">
url = #{record.url,jdbcType=LONGVARCHAR},
</if>
<if test="record.path != null">
path = #{record.path,jdbcType=LONGVARCHAR},
</if>
<if test="record.description != null">
description = #{record.description,jdbcType=VARCHAR},
</if>
<if test="record.status != null">
status = #{record.status,jdbcType=VARCHAR},
</if>
<if test="record.userId != null">
user_id = #{record.userId,jdbcType=VARCHAR},
</if>
<if test="record.createTime != null">
create_time = #{record.createTime,jdbcType=BIGINT},
</if>
<if test="record.updateTime != null">
update_time = #{record.updateTime,jdbcType=BIGINT},
</if>
<if test="record.request != null">
request = #{record.request,jdbcType=LONGVARCHAR},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause"/>
</if>
</update>
<update id="updateByExampleWithBLOBs" parameterType="map">
update api_delimit
set id = #{record.id,jdbcType=VARCHAR},
project_id = #{record.projectId,jdbcType=VARCHAR},
name = #{record.name,jdbcType=VARCHAR},
module_id = #{moduleId,jdbcType=VARCHAR},
module_path = #{modulePath,jdbcType=VARCHAR},
url = #{url,jdbcType=VARCHAR},
path = #{path,jdbcType=VARCHAR},
description = #{record.description,jdbcType=VARCHAR},
status = #{record.status,jdbcType=VARCHAR},
user_id = #{record.userId,jdbcType=VARCHAR},
create_time = #{record.createTime,jdbcType=BIGINT},
update_time = #{record.updateTime,jdbcType=BIGINT},
request = #{record.request,jdbcType=LONGVARCHAR}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause"/>
</if>
</update>
<update id="updateByExample" parameterType="map">
update api_delimit
set id = #{record.id,jdbcType=VARCHAR},
project_id = #{record.projectId,jdbcType=VARCHAR},
name = #{record.name,jdbcType=VARCHAR},
description = #{record.description,jdbcType=VARCHAR},
status = #{record.status,jdbcType=VARCHAR},
user_id = #{record.userId,jdbcType=VARCHAR},
create_time = #{record.createTime,jdbcType=BIGINT},
update_time = #{record.updateTime,jdbcType=BIGINT}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause"/>
</if>
</update>
<update id="updateByPrimaryKeySelective" parameterType="io.metersphere.base.domain.ApiDelimit">
update api_delimit
<set>
@ -519,7 +360,12 @@
<if test="request != null">
request = #{request,jdbcType=LONGVARCHAR},
</if>
<if test="response != null">
response = #{response,jdbcType=LONGVARCHAR},
</if>
<if test="environmentId != null">
environment_id = #{environmentId,jdbcType=VARCHAR},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>
@ -536,7 +382,9 @@
path = #{path,jdbcType=VARCHAR},
create_time = #{createTime,jdbcType=BIGINT},
update_time = #{updateTime,jdbcType=BIGINT},
request = #{request,jdbcType=LONGVARCHAR}
request = #{request,jdbcType=LONGVARCHAR},
response = #{response,jdbcType=LONGVARCHAR},
environment_id = #{environmentId,jdbcType=VARCHAR}
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.base.domain.ApiDelimit">

View File

@ -17,27 +17,28 @@
"@fortawesome/vue-fontawesome": "^0.1.9",
"axios": "^0.19.0",
"core-js": "^3.4.3",
"diffable-html": "^4.0.0",
"echarts": "^4.6.0",
"el-table-infinite-scroll": "^1.0.10",
"element-ui": "^2.13.0",
"html2canvas": "^1.0.0-rc.7",
"js-base64": "^3.4.4",
"json-bigint": "^1.0.0",
"jsoneditor": "^9.1.2",
"jspdf": "^2.1.1",
"md5": "^2.3.0",
"mockjs": "^1.1.0",
"nprogress": "^0.2.0",
"sha.js": "^2.4.11",
"vue": "^2.6.10",
"vue-calendar-heatmap": "^0.8.4",
"vue-echarts": "^4.1.0",
"vue-i18n": "^8.15.3",
"vue-pdf": "^4.2.0",
"vue-router": "^3.1.3",
"vuedraggable": "^2.23.2",
"vuex": "^3.1.2",
"vue-calendar-heatmap": "^0.8.4",
"mockjs": "^1.1.0",
"md5": "^2.3.0",
"sha.js": "^2.4.11",
"js-base64": "^3.4.4",
"json-bigint": "^1.0.0",
"html2canvas": "^1.0.0-rc.7",
"jspdf": "^2.1.1",
"yan-progress": "^1.0.3",
"nprogress": "^0.2.0",
"el-table-infinite-scroll": "^1.0.10",
"vue-pdf": "^4.2.0",
"diffable-html": "^4.0.0"
"yan-progress": "^1.0.3"
},
"devDependencies": {
"@vue/cli-plugin-babel": "^4.1.0",

View File

@ -47,7 +47,7 @@
<!-- 测试-->
<div v-else-if="item.type=== 'test'">
<ms-run-test-http-page :api-data="runTestData" @saveAsApi="editApi"/>
<ms-run-test-http-page :api-data="runTestData" @saveAsApi="editApi" :currentProject="currentProject"/>
</div>
</el-tab-pane>
@ -182,7 +182,7 @@
},
saveApi(data) {
this.setTabTitle(data);
this.$refs.apiList[0].initTableData(data);
this.$refs.apiList[0].initApiTable(data);
},
initTree(data) {
this.moduleOptions = data;

View File

@ -62,7 +62,9 @@
</el-col>
<el-col :span="2">
<div class="ms-api-header-select">
<el-button size="small" @click="createCase">+{{$t('api_test.delimit.request.case')}}</el-button>
<el-button size="small" style="background-color: #783887;color: white" @click="createCase">
+{{$t('api_test.delimit.request.case')}}
</el-button>
</div>
</el-col>
<el-col :span="2">
@ -78,7 +80,7 @@
</el-header>
<!-- 用例部分 -->
<el-main>
<el-main v-loading="loading">
<div v-for="(item,index) in apiCaseList" :key="index">
<el-card style="margin-top: 10px" @click.native="selectTestCase(item,$event)">
<el-row>
@ -97,16 +99,21 @@
<el-col :span="10">
<i class="icon el-icon-arrow-right" :class="{'is-active': item.active}"
@click="active(item)"/>
<el-input v-if="item.type==='create'" size="small" v-model="item.name" :name="index"
:key="index" class="ms-api-header-select" style="width: 180px"/>
{{item.type!= 'create' ? item.name:''}}
<el-input v-if="item.type==='create'" size="small" v-model="item.name" :name="index" :key="index"
class="ms-api-header-select" style="width: 180px"
@blur="saveTestCase(item)"
/>
<span v-else>
{{item.type!= 'create' ? item.name:''}}
<i class="el-icon-edit" style="cursor:pointer" @click="showInput(item)"/>
</span>
<div v-if="item.type!='create'" style="color: #999999;font-size: 12px">
<span> {{item.createTime | timestampFormatDate }}</span> {{item.createUser}} 创建
<span> {{item.updateTime | timestampFormatDate }}</span> {{item.updateUser}} 更新
</div>
</el-col>
<el-col :span="4">
<ms-tip-button @click="runCase" :tip="$t('api_test.run')" icon="el-icon-video-play"
<ms-tip-button @click="runCase(item)" :tip="$t('api_test.run')" icon="el-icon-video-play"
style="background-color: #409EFF;color: white" size="mini" circle/>
<ms-tip-button @click="copyCase(item)" :tip="$t('commons.copy')" icon="el-icon-document-copy"
size="mini" circle/>
@ -126,7 +133,15 @@
<!-- 请求参数-->
<el-collapse-transition>
<div v-if="item.active">
<p class="tip">{{$t('api_test.delimit.request.req_param')}} </p>
<ms-api-request-form :is-read-only="isReadOnly" :request="item.test.request"/>
<p class="tip">{{$t('api_test.delimit.request.assertions_rule')}} </p>
<ms-api-assertions :request="item.test.request" :is-read-only="isReadOnly"
:assertions="item.test.request.assertions"/>
<!-- 保存操作 -->
<el-button type="primary" size="small" style="margin: 20px; float: right" @click="saveTestCase(item)">
{{$t('commons.save')}}
@ -146,11 +161,12 @@
import MsTag from "../../../common/components/MsTag";
import MsTipButton from "../../../common/components/MsTipButton";
import MsApiRequestForm from "./request/ApiRequestForm";
import {Test, RequestFactory} from "../model/ScenarioModel";
import {Test, RequestFactory} from "../model/ApiTestModel";
import {downloadFile, getUUID} from "@/common/js/utils";
import {parseEnvironment} from "../model/EnvironmentModel";
import ApiEnvironmentConfig from "../../test/components/ApiEnvironmentConfig";
import {PRIORITY} from "../model/JsonData";
import MsApiAssertions from "./assertion/ApiAssertions";
export default {
name: 'ApiCaseList',
@ -158,18 +174,21 @@
MsTag,
MsTipButton,
MsApiRequestForm,
ApiEnvironmentConfig
ApiEnvironmentConfig,
MsApiAssertions
},
props: {
api: {
type: Object
},
loaded: Boolean,
currentProject: {},
},
data() {
return {
grades: [],
environments: [],
environment: {},
envValue: {},
name: "",
priorityValue: "",
@ -177,6 +196,7 @@
selectedEvent: Object,
priority: PRIORITY,
apiCaseList: [],
loading: false,
}
},
@ -191,6 +211,7 @@
},
created() {
this.getApiTest();
this.getEnvironments();
},
methods: {
getResult(data) {
@ -202,11 +223,58 @@
return '执行结果:未执行';
}
},
showInput(row) {
row.type = "create";
row.active = true;
this.active(row);
},
apiCaseClose() {
this.apiCaseList = [];
this.$emit('apiCaseClose');
},
runCase() {
getReport(id) {
let url = "/api/delimit/report/get/" + id + "/run";
this.$get(url, response => {
if (response.data) {
this.loading = false;
this.$success(this.$t('schedule.event_success'));
this.$emit('refresh');
} else {
setTimeout(this.getReport, 2000)
}
});
},
runCase(row) {
if (!this.environment) {
this.$warning(this.$t('api_test.environment.select_environment'));
return;
}
row.request = row.test.request;
let url = "/api/delimit/run";
let bodyFiles = this.getBodyUploadFiles(row);
let env = this.environment.config.httpConfig.socket ? (this.environment.config.httpConfig.protocol + '://' + this.environment.config.httpConfig.socket) : '';
if (env.endsWith("/")) {
env = env.substr(0, env.length - 1);
}
let sendUrl = this.api.url;
if (!sendUrl.startsWith("/")) {
sendUrl = "/" + sendUrl;
}
row.test.request.url = env + sendUrl;
row.test.request.path = this.api.path;
row.test.request.name = row.id;
row.test.request.connectTimeout = "6000";
row.test.request.responseTimeout = "0";
row.reportId = "run";
this.loading = true;
let jmx = row.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(url, file, bodyFiles, row, response => {
this.getReport(row.id);
}, erro => {
this.loading = false;
});
},
deleteCase(index, row) {
this.$get('/api/testcase/delete/' + row.id, () => {
@ -222,19 +290,24 @@
active: false,
test: data.test,
};
this.apiCaseList.push(obj);
this.apiCaseList.unshift(obj);
},
createCase() {
let test = new Test();
createCase(row) {
let obj = {
id: this.apiCaseList.size + 1,
name: '',
priority: 'P0',
type: 'create',
active: false,
test: test,
};
this.apiCaseList.push(obj);
let request = {};
if (row) {
request = row.request;
obj.apiDelimitId = row.apiDelimitId;
} else {
request: new RequestFactory(JSON.parse(this.api.request))
}
obj.test = new Test({request: request});
this.apiCaseList.unshift(obj);
},
active(item) {
item.active = !item.active;
@ -289,7 +362,7 @@
row.test.request.url = this.api.url;
row.test.request.path = this.api.path;
row.projectId = this.api.projectId;
row.apiDelimitId = this.api.id;
row.apiDelimitId = row.apiDelimitId || this.api.id;
row.request = row.test.request;
let jmx = row.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
@ -313,19 +386,17 @@
let hasEnvironment = false;
for (let i in this.environments) {
if (this.environments[i].id === this.api.environmentId) {
this.api.environment = this.environments[i];
this.environment = this.environments[i];
hasEnvironment = true;
break;
}
}
if (!hasEnvironment) {
this.api.environmentId = '';
this.api.environment = undefined;
this.environment = undefined;
}
});
} else {
this.api.environmentId = '';
this.api.environment = undefined;
this.environment = undefined;
}
},
openEnvironmentConfig() {
@ -338,7 +409,7 @@
environmentChange(value) {
for (let i in this.environments) {
if (this.environments[i].id === value) {
this.api.environment = this.environments[i];
this.environment = this.environments[i];
break;
}
}
@ -347,7 +418,7 @@
this.getEnvironments();
},
selectTestCase(item, $event) {
if (item.type === "create") {
if (item.type === "create" || !this.loaded) {
return;
}
if ($event.currentTarget.className.indexOf('is-selected') > 0) {
@ -413,6 +484,19 @@
transform: rotate(90deg);
}
.tip {
padding: 3px 5px;
font-size: 16px;
border-radius: 4px;
border-left: 4px solid #783887;
margin: 20px 0;
}
.environment-button {
margin-left: 20px;
padding: 7px;
}
.is-selected {
background: #EFF7FF;
}

View File

@ -10,7 +10,7 @@
<script>
import MsAddCompleteHttpApi from "./complete/AddCompleteHttpApi";
import {RequestFactory, Test} from "../model/ScenarioModel";
import {RequestFactory, ResponseFactory, Test, Body} from "../model/ApiTestModel";
import {getUUID} from "@/common/js/utils";
export default {
@ -30,7 +30,13 @@
},
created() {
this.test = new Test({
request: this.currentApi.request != null ? new RequestFactory(JSON.parse(this.currentApi.request)) : null
request: this.currentApi.request != null ? new RequestFactory(JSON.parse(this.currentApi.request)) : null,
response: this.currentApi.response != null ? new ResponseFactory(JSON.parse(this.currentApi.response)) : {
headers: [],
body: new Body(),
statusCode: [],
type: "HTTP"
}
});
if (this.currentApi != null && this.currentApi.id != null) {
this.reqUrl = "/api/delimit/update";
@ -41,27 +47,34 @@
},
methods: {
runTest(data) {
if (this.editApi(data) === true) {
this.$emit('runTest', data);
}
},
saveApi(data) {
if (this.editApi(data) === true) {
this.$emit('saveApi', data);
}
},
editApi(data) {
data.projectId = this.currentProject.id;
data.request = data.test.request;
let bodyFiles = this.getBodyUploadFiles(data);
let jmx = data.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(this.reqUrl, file, bodyFiles, data, () => {
this.$success(this.$t('commons.save_success'));
this.reqUrl = "/api/delimit/update";
this.$emit('runTest', data);
});
},
saveApi(data) {
data.projectId = this.currentProject.id;
data.request = data.test.request;
data.response = data.test.response;
let bodyFiles = this.getBodyUploadFiles(data);
let jmx = data.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(this.reqUrl, file, bodyFiles, data, () => {
this.$success(this.$t('commons.save_success'));
this.reqUrl = "/api/delimit/update";
return true;
this.$emit('saveApi', data);
});
},
getBodyUploadFiles(data) {
let bodyUploadFiles = [];

View File

@ -5,7 +5,7 @@
</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 class="kv-checkbox">
<el-col class="kv-checkbox" v-if="isShowEnable">
<input type="checkbox" v-if="!isDisable(index)" v-model="item.enable"
:disabled="isReadOnly"/>
</el-col>
@ -34,7 +34,7 @@
</template>
<script>
import {KeyValue} from "../model/ScenarioModel";
import {KeyValue} from "../model/ApiTestModel";
export default {
name: "MsApiKeyValue",
@ -42,6 +42,10 @@
props: {
keyPlaceholder: String,
valuePlaceholder: String,
isShowEnable: {
type: Boolean,
default: false
},
description: String,
items: Array,
isReadOnly: {
@ -51,8 +55,7 @@
suggestions: Array
},
data() {
return {
}
return {}
},
computed: {
keyText() {

View File

@ -54,7 +54,7 @@
:label="$t('api_test.delimit.api_principal')"
show-overflow-tooltip/>
<el-table-column width="200" :label="$t('api_test.delimit.api_last_time')" prop="updateTime">
<el-table-column width="160" :label="$t('api_test.delimit.api_last_time')" prop="updateTime">
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
@ -76,8 +76,7 @@
show-overflow-tooltip/>
<el-table-column
:label="$t('commons.operating')" min-width="100">
<el-table-column :label="$t('commons.operating')" min-width="130" align="center">
<template v-slot:default="scope">
<el-button type="text" @click="editApi(scope.row)">编辑</el-button>
<el-button type="text" @click="handleTestCase(scope.row)">用例</el-button>
@ -92,7 +91,7 @@
</el-card>
<ms-bottom-container v-bind:enableAsideHidden="isHide">
<ms-api-case-list @apiCaseClose="apiCaseClose" :api="selectApi" :current-project="currentProject"/>
<ms-api-case-list @apiCaseClose="apiCaseClose" @refresh="initApiTable" :api="selectApi" :current-project="currentProject"/>
</ms-bottom-container>
</div>

View File

@ -7,16 +7,12 @@
<div class="kv-row" v-for="(item, index) in parameters" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col class="kv-checkbox">
<input type="checkbox" v-if="!isDisable(index)" v-model="item.enable"
:disabled="isReadOnly"/>
</el-col>
<el-col>
<el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="200"
@change="change" :placeholder="keyText" show-word-limit>
<template v-slot:prepend>
<el-select v-if="type === 'body'" :disabled="isReadOnly" class="kv-type" v-model="item.type" @change="typeChange(item)">
<el-select v-if="type === 'body'" :disabled="isReadOnly" class="kv-type" v-model="item.type"
@change="typeChange(item)">
<el-option value="text"/>
<el-option value="file"/>
</el-select>
@ -27,6 +23,13 @@
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col class="kv-select">
<el-select v-model="item.required" size="small">
<el-option v-for="req in requireds" :key="req.id" :label="req.name" :value="req.id"/>
</el-select>
</el-col>
<el-col v-if="item.type !== 'file'">
<el-autocomplete
:disabled="isReadOnly"
@ -42,12 +45,22 @@
</el-autocomplete>
</el-col>
<el-col>
<el-input v-model="item.description" size="small" maxlength="200"
:placeholder="$t('commons.description')" show-word-limit>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col v-if="item.type === 'file'">
<ms-api-body-file-upload :parameter="item"/>
</el-col>
<el-col v-if="type === 'body'">
<el-input :disabled="isReadOnly" v-model="item.contentType" size="small" maxlength="100"
<el-col v-if="type === 'body'" class="kv-select">
<el-input :disabled="isReadOnly" v-model="item.contentType" size="small"
@change="change" :placeholder="$t('api_test.request.content_type')" show-word-limit>
</el-input>
</el-col>
@ -56,6 +69,8 @@
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
:disabled="isDisable(index) || isReadOnly"/>
</el-col>
</el-row>
</div>
<ms-api-variable-advance ref="variableAdvance" :environment="environment" :scenario="scenario"
@ -65,10 +80,11 @@
</template>
<script>
import {KeyValue, Scenario} from "../model/ScenarioModel";
import {KeyValue, Scenario} from "../model/ApiTestModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
import MsApiVariableAdvance from "./ApiVariableAdvance";
import MsApiBodyFileUpload from "./body/ApiBodyFileUpload";
import {REQUIRED} from "../model/JsonData";
export default {
name: "MsApiVariable",
@ -94,6 +110,7 @@
data() {
return {
currentItem: null,
requireds: REQUIRED
}
},
computed: {
@ -124,7 +141,12 @@
}
});
if (isNeedCreate) {
this.parameters.push(new KeyValue({type: 'text', enable: true, uuid: this.uuid(), contentType: 'text/plain'}));
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
uuid: this.uuid(),
contentType: 'text/plain'
}));
}
this.$emit('change', this.parameters);
// TODO key
@ -170,7 +192,13 @@
},
created() {
if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) {
this.parameters.push(new KeyValue( {type: 'text', enable: true, uuid: this.uuid(), contentType: 'text/plain'}));
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
required: true,
uuid: this.uuid(),
contentType: 'text/plain'
}));
}
}
}
@ -189,6 +217,10 @@
width: 60px;
}
.kv-select {
width: 50%;
}
.el-autocomplete {
width: 100%;
}

View File

@ -90,7 +90,7 @@
</template>
<script>
import {calculate, Scenario} from "../model/ScenarioModel";
import {calculate, Scenario} from "../model/ApiTestModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
export default {

View File

@ -15,7 +15,7 @@
<script>
import {Duration} from "../../model/ScenarioModel";
import {Duration} from "../../model/ApiTestModel";
export default {
name: "MsApiAssertionDuration",

View File

@ -18,7 +18,7 @@
</template>
<script>
import {JSONPath} from "../../model/ScenarioModel";
import {JSONPath} from "../../model/ApiTestModel";
export default {
name: "MsApiAssertionJsonPath",

View File

@ -28,7 +28,7 @@
</template>
<script>
import {ASSERTION_REGEX_SUBJECT, Regex} from "../../model/ScenarioModel";
import {ASSERTION_REGEX_SUBJECT, Regex} from "../../model/ApiTestModel";
export default {
name: "MsApiAssertionRegex",

View File

@ -36,7 +36,7 @@
</template>
<script>
import {Regex, ASSERTION_REGEX_SUBJECT} from "../../model/ScenarioModel";
import {Regex, ASSERTION_REGEX_SUBJECT} from "../../model/ApiTestModel";
export default {
name: "MsApiAssertionText",

View File

@ -3,7 +3,8 @@
<div class="assertion-add">
<el-row :gutter="10">
<el-col :span="4">
<el-select :disabled="isReadOnly" class="assertion-item" v-model="type" :placeholder="$t('api_test.request.assertions.select_type')"
<el-select :disabled="isReadOnly" class="assertion-item" v-model="type"
:placeholder="$t('api_test.request.assertions.select_type')"
size="small">
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
@ -12,9 +13,12 @@
</el-select>
</el-col>
<el-col :span="20">
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/>
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/>
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/>
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT"
:callback="after"/>
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX"
:callback="after"/>
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath"
v-if="type === options.JSON_PATH" :callback="after"/>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
v-if="type === options.DURATION" :callback="after"/>
<el-button v-if="!type" :disabled="true" type="primary" size="small">Add</el-button>
@ -24,16 +28,17 @@
<div>
<el-row :gutter="10" class="json-path-suggest-button">
<el-button size="small" type="primary" @click="suggestJsonOpen">
{{$t('api_test.request.assertions.json_path_suggest')}}
</el-button>
<el-button size="small" type="danger" @click="clearJson">
{{$t('api_test.request.assertions.json_path_clear')}}
</el-button>
<el-link size="small" type="primary" @click="suggestJsonOpen">
{{$t('api_test.request.assertions.json_path_suggest')}}
</el-link>
<el-link size="small" type="danger" @click="clearJson" style="margin-left: 20px">
{{$t('api_test.request.assertions.json_path_clear')}}
</el-link>
</el-row>
</div>
<ms-api-jsonpath-suggest-list @addJsonpathSuggest="addJsonpathSuggest" :request="request" ref="jsonpathSuggestList"/>
<ms-api-jsonpath-suggest-list @addJsonpathSuggest="addJsonpathSuggest" :request="request"
ref="jsonpathSuggestList"/>
<ms-api-assertions-edit :is-read-only="isReadOnly" :assertions="assertions"/>
</div>
@ -43,7 +48,7 @@
import MsApiAssertionText from "./ApiAssertionText";
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionDuration from "./ApiAssertionDuration";
import {ASSERTION_TYPE, Assertions, HttpRequest, JSONPath} from "../../model/ScenarioModel";
import {ASSERTION_TYPE, Assertions, HttpRequest, JSONPath} from "../../model/ApiTestModel";
import MsApiAssertionsEdit from "./ApiAssertionsEdit";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList";
@ -54,7 +59,8 @@
components: {
MsApiJsonpathSuggestList,
MsApiAssertionJsonPath,
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText},
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText
},
props: {
assertions: Assertions,
@ -86,11 +92,11 @@
},
addJsonpathSuggest(jsonPathList) {
jsonPathList.forEach(jsonPath => {
let jsonItem = new JSONPath();
jsonItem.expression = jsonPath.json_path;
jsonItem.expect = jsonPath.json_value;
jsonItem.setJSONPathDescription();
this.assertions.jsonPath.push(jsonItem);
let jsonItem = new JSONPath();
jsonItem.expression = jsonPath.json_path;
jsonItem.expect = jsonPath.json_value;
jsonItem.setJSONPathDescription();
this.assertions.jsonPath.push(jsonItem);
});
},
clearJson() {
@ -136,7 +142,10 @@
}
.json-path-suggest-button {
text-align: right;
text-align: left;
margin-left: 20px;
margin-top: 20px;
margin-bottom: 20px;
}
</style>

View File

@ -31,7 +31,7 @@
<script>
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionDuration from "./ApiAssertionDuration";
import {Assertions} from "../../model/ScenarioModel";
import {Assertions} from "../../model/ApiTestModel";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
export default {

View File

@ -51,7 +51,7 @@
<script>
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
import {HttpRequest} from "../../model/ScenarioModel";
import {HttpRequest} from "../../model/ApiTestModel";
export default {
name: "MsApiJsonpathSuggestList",
components: {MsDialogFooter},

View File

@ -59,7 +59,7 @@
</template>
<script>
import {HttpRequest} from "../../model/ScenarioModel";
import {HttpRequest} from "../../model/ApiTestModel";
export default {
name: "MsApiAuthConfig",

View File

@ -48,7 +48,7 @@
<script>
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
import {WORKSPACE_ID} from '../../../../../../common/js/constants';
import {Test} from "../../model/ScenarioModel"
import {Test} from "../../model/ApiTestModel"
import {REQ_METHOD} from "../../model/JsonData";
import {getCurrentUser} from "../../../../../../common/js/utils";

View File

@ -0,0 +1,202 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in parameters" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col>
<el-input v-model="item.description" size="small" maxlength="200"
:placeholder="$t('commons.description')" show-word-limit>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col>
<ms-api-body-file-upload :parameter="item"/>
</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>
<ms-api-variable-advance ref="variableAdvance" :environment="environment" :scenario="scenario"
:parameters="parameters"
:current-item="currentItem"/>
</div>
</template>
<script>
import {KeyValue, Scenario} from "../../model/ApiTestModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
import MsApiVariableAdvance from "../ApiVariableAdvance";
import MsApiBodyFileUpload from "../body/ApiBodyFileUpload";
import {REQUIRED} from "../../model/JsonData";
export default {
name: "MsApiVariable",
components: {MsApiBodyFileUpload, MsApiVariableAdvance},
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
parameters: Array,
rest: Array,
environment: Object,
scenario: Scenario,
type: {
type: String,
default: ''
},
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
data() {
return {
currentItem: null,
requireds: REQUIRED
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
//
this.parameters.splice(index, 1);
this.$emit('change', this.parameters);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.parameters.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.parameters.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
uuid: this.uuid(),
contentType: 'text/plain'
}));
}
this.$emit('change', this.parameters);
// TODO key
},
isDisable: function (index) {
return this.parameters.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.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);
};
},
uuid: function () {
return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1);
},
advanced(item) {
this.$refs.variableAdvance.open();
this.currentItem = item;
},
typeChange(item) {
if (item.type === 'file') {
item.contentType = 'application/octet-stream';
} else {
item.contentType = 'text/plain';
}
}
},
created() {
if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) {
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
required: true,
uuid: this.uuid(),
contentType: 'text/plain'
}));
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.kv-select {
width: 50%;
}
.el-autocomplete {
width: 100%;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.el-row {
margin-bottom: 5px;
}
.kv-type {
width: 70px;
}
.pointer {
cursor: pointer;
color: #1E90FF;
}
</style>

View File

@ -1,66 +1,87 @@
<template>
<div>
<el-radio-group v-model="body.type" size="mini">
<el-radio-button :disabled="isReadOnly" :label="type.KV">
<el-radio :disabled="isReadOnly" :label="type.KV">
{{ $t('api_test.delimit.request.body_form_data') }}
</el-radio-button>
</el-radio>
<el-radio-button :disabled="isReadOnly" :label="type.RAW">
{{ $t('api_test.delimit.request.body_raw') }}
</el-radio-button>
<el-radio-button :disabled="isReadOnly" :label="type.WWW_FORM">
<el-radio :disabled="isReadOnly" :label="type.WWW_FORM">
{{ $t('api_test.delimit.request.body_x_www_from_urlencoded') }}
</el-radio-button>
<el-radio-button :disabled="isReadOnly" :label="type.BINARY">
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.JSON">
{{ $t('api_test.delimit.request.body_json') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.XML">
{{ $t('api_test.delimit.request.body_xml') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.RAW">
{{ $t('api_test.delimit.request.body_raw') }}
</el-radio>
<el-radio :disabled="isReadOnly" :label="type.BINARY">
{{ $t('api_test.delimit.request.body_binary') }}
</el-radio-button>
</el-radio>
</el-radio-group>
<ms-dropdown :default-command="body.format" v-if="body.type == 'Raw'" :commands="modes" @command="modeChange"/>
<ms-api-variable :is-read-only="isReadOnly"
:parameters="body.kvs"
:environment="environment"
:scenario="scenario"
:extract="extract"
type="body"
:description="$t('api_test.request.parameters_desc')"
v-if="body.isKV()"/>
<ms-api-variable :is-read-only="isReadOnly"
:parameters="body.kvs"
:environment="environment"
:scenario="scenario"
:extract="extract"
type="body"
:description="$t('api_test.request.parameters_desc')"
v-if="body.type == 'WWW_Form'"/>
<ms-api-from-url-variable :is-read-only="isReadOnly"
:parameters="body.fromUrlencoded"
:environment="environment"
type="body"
v-if="body.type == 'WWW_FORM'"/>
<div class="body-raw" v-if="body.type == 'JSON'">
<ms-json-code-edit @json-change="jsonChange" @onError="jsonError" :value="body.json" ref="jsonCodeEdit"/>
</div>
<div class="body-raw" v-if="body.type == 'XML'">
<ms-code-edit :mode="body.format" :read-only="isReadOnly" :data.sync="body.xml" :modes="modes" ref="codeEdit"/>
</div>
<ms-api-variable :is-read-only="isReadOnly"
:parameters="body.kvs"
:environment="environment"
:scenario="scenario"
:extract="extract"
type="body"
:description="$t('api_test.request.parameters_desc')"
v-if="body.type == 'BINARY'"/>
<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"/>
</div>
<ms-api-binary-variable :is-read-only="isReadOnly"
:parameters="body.binary"
:environment="environment"
type="body"
v-if="body.type == 'BINARY'"/>
</div>
</template>
<script>
import MsApiKeyValue from "../ApiKeyValue";
import {Body, BODY_FORMAT, BODY_TYPE, Scenario} from "../../model/ScenarioModel";
import {Body, BODY_FORMAT, BODY_TYPE, Scenario} from "../../model/ApiTestModel";
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsJsonCodeEdit from "../../../../common/components/MsJsonCodeEdit";
import MsDropdown from "../../../../common/components/MsDropdown";
import MsApiVariable from "../ApiVariable";
import MsApiBinaryVariable from "./ApiBinaryVariable";
import MsApiFromUrlVariable from "./ApiFromUrlVariable";
export default {
name: "MsApiBody",
components: {MsApiVariable, MsDropdown, MsCodeEdit, MsApiKeyValue},
components: {
MsApiVariable,
MsDropdown,
MsCodeEdit,
MsApiKeyValue,
MsApiBinaryVariable,
MsApiFromUrlVariable,
MsJsonCodeEdit
},
props: {
body: Body,
scenario: Scenario,
@ -82,6 +103,12 @@
methods: {
modeChange(mode) {
this.body.format = mode;
},
jsonChange(json) {
this.body.json = json;
},
jsonError(e) {
this.$error(e);
}
},
@ -108,7 +135,7 @@
.body-raw {
padding: 15px 0;
height: 300px;
height: 400px;
}
.el-dropdown {

View File

@ -12,12 +12,12 @@
<div class="upload-default">
<i class="el-icon-plus"/>
</div>
<div class="upload-item" slot="file" slot-scope="{file}" >
<div class="upload-item" slot="file" slot-scope="{file}">
<span>{{file.file ? file.file.name : file.name}}</span>
<span class="el-upload-list__item-actions">
<!--<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleDownload(file)">-->
<!--<i class="el-icon-download"/>-->
<!--</span>-->
<!--<i class="el-icon-download"/>-->
<!--</span>-->
<span v-if="!disabled" class="el-upload-list__item-delete" @click="handleRemove(file)">
<i class="el-icon-delete"/>
</span>
@ -29,50 +29,50 @@
</template>
<script>
export default {
name: "MsApiBodyFileUpload",
data() {
return {
disabled: false,
};
},
props: {
parameter: Object,
default() {
return {}
export default {
name: "MsApiBodyFileUpload",
data() {
return {
disabled: false,
};
},
props: {
parameter: Object,
default() {
return {}
}
},
methods: {
handleRemove(file) {
this.$refs.upload.handleRemove(file);
for (let i = 0; i < this.parameter.files.length; i++) {
let fileName = file.file ? file.file.name : file.name;
let paramFileName = this.parameter.files[i].file ?
this.parameter.files[i].file.name : this.parameter.files[i].name;
if (fileName === paramFileName) {
this.parameter.files.splice(i, 1);
this.$refs.upload.handleRemove(file);
break;
}
}
},
methods: {
handleRemove(file) {
this.$refs.upload.handleRemove(file);
for (let i = 0; i < this.parameter.files.length; i++) {
let fileName = file.file ? file.file.name : file.name;
let paramFileName = this.parameter.files[i].file ?
this.parameter.files[i].file.name : this.parameter.files[i].name;
if (fileName === paramFileName) {
this.parameter.files.splice(i, 1);
this.$refs.upload.handleRemove(file);
break;
}
}
},
upload(file) {
this.parameter.files.push(file);
},
uploadValidate(file) {
if (file.size / 1024 / 1024 > 500) {
this.$warning(this.$t('api_test.request.body_upload_limit_size'));
return false;
}
return true;
},
upload(file) {
this.parameter.files.push(file);
},
created() {
if (!this.parameter.files) {
this.parameter.files = [];
uploadValidate(file) {
if (file.size / 1024 / 1024 > 500) {
this.$warning(this.$t('api_test.request.body_upload_limit_size'));
return false;
}
return true;
},
},
created() {
if (!this.parameter.files) {
this.parameter.files = [];
}
}
}
</script>
<style scoped>
@ -82,12 +82,12 @@
}
.api-body-upload >>> .el-upload {
height: 32px;
height: 30px;
width: 32px;
}
.upload-default {
min-height: 32px;
min-height: 30px;
width: 32px;
line-height: 32px;
}
@ -97,7 +97,7 @@
}
.api-body-upload >>> .el-upload-list__item {
height: 32px;
height: 30px;
width: auto;
padding: 6px;
margin-bottom: 0px;
@ -107,7 +107,7 @@
}
.api-body-upload {
min-height: 32px;
min-height: 30px;
border: 1px solid #EBEEF5;
padding: 2px;
border-radius: 4px;

View File

@ -0,0 +1,243 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in parameters" :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>
<template v-slot:prepend>
<el-select v-if="type === 'body'" :disabled="isReadOnly" class="kv-type" v-model="item.type"
@change="typeChange(item)">
<el-option value="text"/>
<el-option value="file"/>
</el-select>
</template>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col class="kv-select">
<el-select v-model="item.required" size="small">
<el-option v-for="req in requireds" :key="req.id" :label="req.name" :value="req.id"/>
</el-select>
</el-col>
<el-col v-if="item.type !== 'file'">
<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 pointer" @click="advanced(item)"></i>
</el-autocomplete>
</el-col>
<el-col>
<el-input v-model="item.description" size="small" maxlength="200"
:placeholder="$t('commons.description')" show-word-limit>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col v-if="item.type === 'file'">
<ms-api-body-file-upload :parameter="item"/>
</el-col>
<el-col v-if="type === 'body'" class="kv-select">
<el-input :disabled="isReadOnly" v-model="item.contentType" size="small"
@change="change" :placeholder="$t('api_test.request.content_type')" show-word-limit>
</el-input>
</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>
<ms-api-variable-advance ref="variableAdvance" :environment="environment" :scenario="scenario"
:parameters="parameters"
:current-item="currentItem"/>
</div>
</template>
<script>
import {KeyValue, Scenario} from "../../model/ApiTestModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
import MsApiVariableAdvance from "../ApiVariableAdvance";
import MsApiBodyFileUpload from "../body/ApiBodyFileUpload";
import {REQUIRED} from "../../model/JsonData";
export default {
name: "MsApiVariable",
components: {MsApiBodyFileUpload, MsApiVariableAdvance},
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
parameters: Array,
rest: Array,
environment: Object,
scenario: Scenario,
type: {
type: String,
default: ''
},
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
data() {
return {
currentItem: null,
requireds: REQUIRED
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
//
this.parameters.splice(index, 1);
this.$emit('change', this.parameters);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.parameters.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.parameters.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
uuid: this.uuid(),
contentType: 'text/plain'
}));
}
this.$emit('change', this.parameters);
// TODO key
},
isDisable: function (index) {
return this.parameters.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.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);
};
},
uuid: function () {
return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1);
},
advanced(item) {
this.$refs.variableAdvance.open();
this.currentItem = item;
},
typeChange(item) {
if (item.type === 'file') {
item.contentType = 'application/octet-stream';
} else {
item.contentType = 'text/plain';
}
}
},
created() {
if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) {
this.parameters.push(new KeyValue({type: 'text', enable: true, required:true,uuid: this.uuid(), contentType: 'text/plain'}));
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.kv-select {
width: 50%;
}
.el-autocomplete {
width: 100%;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.el-row {
margin-bottom: 5px;
}
.kv-type {
width: 70px;
}
.pointer {
cursor: pointer;
color: #1E90FF;
}
</style>

View File

@ -0,0 +1,202 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in parameters" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col>
<el-input v-model="item.description" size="small" maxlength="200"
:placeholder="$t('commons.description')" show-word-limit>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col>
<ms-api-body-file-upload :parameter="item"/>
</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>
<ms-api-variable-advance ref="variableAdvance" :environment="environment" :scenario="scenario"
:parameters="parameters"
:current-item="currentItem"/>
</div>
</template>
<script>
import {KeyValue, Scenario} from "../../model/ApiTestModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
import MsApiVariableAdvance from "../ApiVariableAdvance";
import MsApiBodyFileUpload from "../body/ApiBodyFileUpload";
import {REQUIRED} from "../../model/JsonData";
export default {
name: "MsApiVariable",
components: {MsApiBodyFileUpload, MsApiVariableAdvance},
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
parameters: Array,
rest: Array,
environment: Object,
scenario: Scenario,
type: {
type: String,
default: ''
},
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
data() {
return {
currentItem: null,
requireds: REQUIRED
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
//
this.parameters.splice(index, 1);
this.$emit('change', this.parameters);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.parameters.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.parameters.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
uuid: this.uuid(),
contentType: 'text/plain'
}));
}
this.$emit('change', this.parameters);
// TODO key
},
isDisable: function (index) {
return this.parameters.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.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);
};
},
uuid: function () {
return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1);
},
advanced(item) {
this.$refs.variableAdvance.open();
this.currentItem = item;
},
typeChange(item) {
if (item.type === 'file') {
item.contentType = 'application/octet-stream';
} else {
item.contentType = 'text/plain';
}
}
},
created() {
if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) {
this.parameters.push(new KeyValue({
type: 'text',
enable: true,
required: true,
uuid: this.uuid(),
contentType: 'text/plain'
}));
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.kv-select {
width: 50%;
}
.el-autocomplete {
width: 100%;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.el-row {
margin-bottom: 5px;
}
.kv-type {
width: 70px;
}
.pointer {
cursor: pointer;
color: #1E90FF;
}
</style>

View File

@ -9,8 +9,7 @@
<el-button type="primary" size="small" @click="runTest">{{$t('commons.test')}}</el-button>
</div>
<br/>
<div style="font-size: 16px;color: #333333">{{$t('test_track.plan_view.base_info')}}</div>
<br/>
<p class="tip">{{$t('test_track.plan_view.base_info')}} </p>
<el-form-item :label="$t('commons.name')" prop="name">
<el-input class="ms-http-input" size="small" v-model="httpForm.name"/>
</el-form-item>
@ -59,23 +58,21 @@
:rows="2" size="small"/>
</el-form-item>
<div style="font-size: 16px;color: #333333;padding-top: 30px">{{$t('api_test.delimit.request.req_param')}}</div>
<br/>
<!-- HTTP 请求参数 -->
<ms-api-request-form :request="test.request"/>
<p class="tip">{{$t('api_test.delimit.request.req_param')}} </p>
<ms-api-request-form :request="test.request" :isShowEnable="isShowEnable"/>
</el-form>
<div style="font-size: 16px;color: #333333 ;padding-top: 30px">{{$t('api_test.delimit.request.res_param')}}</div>
<br/>
<ms-response-text :response="responseData"></ms-response-text>
<!-- 响应内容-->
<p class="tip">{{$t('api_test.delimit.request.res_param')}} </p>
<ms-response-text :response="test.response"></ms-response-text>
</el-card>
</div>
</template>
<script>
import MsApiRequestForm from "../request/ApiRequestForm";
import MsResponseText from "../../../report/components/ResponseText";
import MsResponseText from "../response/ResponseText";
import {WORKSPACE_ID} from '../../../../../../common/js/constants';
import {REQ_METHOD, API_STATUS} from "../../model/JsonData";
@ -95,8 +92,8 @@
status: [{required: true, message: this.$t('commons.please_select'), trigger: 'change'}],
},
httpForm: {},
isShowEnable: false,
maintainerOptions: [],
responseData: {},
currentModule: {},
reqOptions: REQ_METHOD,
options: API_STATUS
@ -168,6 +165,14 @@
width: 500px;
}
.tip {
padding: 3px 5px;
font-size: 16px;
border-radius: 4px;
border-left: 4px solid #783887;
margin: 20px 0;
}
.ms-http-textarea {
width: 500px;
}

View File

@ -2,14 +2,13 @@
<div class="card-container">
<el-card class="card-content">
<el-form :model="httpForm" :rules="rules" ref="httpForm" :inline="true" label-position="right">
<div style="font-size: 16px;color: #333333">{{$t('test_track.plan_view.base_info')}}</div>
<br/>
<el-form :model="debugForm" :rules="rules" ref="debugForm" :inline="true" label-position="right">
<p class="tip">{{$t('test_track.plan_view.base_info')}} </p>
<el-form-item :label="$t('api_report.request')" prop="responsible">
<el-input :placeholder="$t('api_test.delimit.request.path_info')" v-model="httpForm.url"
<el-form-item :label="$t('api_report.request')" prop="url">
<el-input :placeholder="$t('api_test.delimit.request.path_all_info')" v-model="debugForm.url"
class="ms-http-input" size="small">
<el-select v-model="httpForm.path" slot="prepend" style="width: 100px" size="small">
<el-select v-model="debugForm.path" slot="prepend" style="width: 100px" size="small">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
@ -25,52 +24,122 @@
</el-dropdown>
</el-form-item>
<div style="font-size: 16px;color: #333333;padding-top: 30px">{{$t('api_test.delimit.request.req_param')}}</div>
<br/>
<p class="tip">{{$t('api_test.delimit.request.req_param')}} </p>
<!-- HTTP 请求参数 -->
<ms-api-request-form :request="test.request"/>
</el-form>
<div style="font-size: 16px;color: #333333 ;padding-top: 30px">{{$t('api_test.delimit.request.res_param')}}</div>
<br/>
<ms-response-text :response="responseData"></ms-response-text>
<!-- HTTP 请求返回数据 -->
<p class="tip">{{$t('api_test.delimit.request.res_param')}} </p>
<ms-request-result-tail v-loading="loading" :response="responseData" ref="debugResult"/>
</el-card>
</div>
</template>
<script>
import MsApiRequestForm from "../request/ApiRequestForm";
import {Test} from "../../model/ScenarioModel";
import MsResponseText from "../../../report/components/ResponseText";
import {Test} from "../../model/ApiTestModel";
import MsResponseResult from "../response/ResponseResult";
import MsRequestMetric from "../response/RequestMetric";
import {getUUID, getCurrentUser} from "@/common/js/utils";
import MsResponseText from "../response/ResponseText";
import {REQ_METHOD} from "../../model/JsonData";
import MsRequestResultTail from "../response/RequestResultTail";
export default {
name: "ApiConfig",
components: {MsResponseText, MsApiRequestForm},
components: {MsRequestResultTail, MsResponseResult, MsApiRequestForm, MsRequestMetric, MsResponseText},
data() {
return {
rules: {
path: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],
url: [{required: true, message: this.$t('api_test.delimit.request.path_info'), trigger: 'blur'}],
url: [{required: true, message: this.$t('api_test.delimit.request.path_all_info'), trigger: 'blur'}],
},
httpForm: {path: REQ_METHOD[0].id},
debugForm: {path: REQ_METHOD[0].id},
options: [],
responseData: {},
responseData: {type: 'HTTP', responseResult: {}, subRequestResults: []},
loading: false,
debugResultId: "",
test: new Test(),
reqOptions: REQ_METHOD,
}
},
watch: {
debugResultId() {
this.getResult()
}
},
methods: {
handleCommand(e) {
if (e === "save_as") {
this.saveAs();
} else {
this.runDebug();
}
},
getBodyUploadFiles(data) {
let bodyUploadFiles = [];
data.bodyUploadIds = [];
let request = data.request;
if (request.body) {
request.body.kvs.forEach(param => {
if (param.files) {
param.files.forEach(item => {
if (item.file) {
let fileId = getUUID().substring(0, 8);
item.name = item.file.name;
item.id = fileId;
data.bodyUploadIds.push(fileId);
bodyUploadFiles.push(item.file);
}
});
}
});
}
return bodyUploadFiles;
},
runDebug() {
this.$refs['debugForm'].validate((valid) => {
if (valid) {
this.loading = true;
let url = "/api/delimit/run/debug";
let bodyFiles = this.getBodyUploadFiles(this.test);
this.test.request.url = this.debugForm.url;
this.test.request.path = this.debugForm.path;
this.test.id = getUUID().substring(0, 8);
let jmx = this.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(url, file, bodyFiles, this.test, response => {
this.debugResultId = response.data;
});
}
})
},
getResult() {
if (this.debugResultId) {
let url = "/api/delimit/report/get/" + this.debugResultId + "/" + "debug";
this.$get(url, response => {
if (response.data) {
let testResult = JSON.parse(response.data.content);
this.responseData = testResult.requestResults[0];
this.loading = false;
this.$refs.debugResult.reload();
} else {
setTimeout(this.getResult, 2000)
}
});
}
},
saveAs() {
this.$refs['httpForm'].validate((valid) => {
this.$refs['debugForm'].validate((valid) => {
if (valid) {
this.httpForm.request = JSON.stringify(this.test.request);
this.$emit('saveAs', this.httpForm);
this.debugForm.request = JSON.stringify(this.test.request);
this.debugForm.userId = getCurrentUser().id;
this.debugForm.status = "Underway";
this.$emit('saveAs', this.debugForm);
}
else {
return false;
@ -86,4 +155,12 @@
width: 500px;
margin-top: 5px;
}
.tip {
padding: 3px 5px;
font-size: 16px;
border-radius: 4px;
border-left: 4px solid #783887;
margin: 20px 0;
}
</style>

View File

@ -26,7 +26,7 @@
</template>
<script>
import {EXTRACT_TYPE, Extract} from "../../model/ScenarioModel";
import {EXTRACT_TYPE, Extract} from "../../model/ApiTestModel";
import MsApiExtractEdit from "./ApiExtractEdit";
import MsApiExtractCommon from "./ApiExtractCommon";

View File

@ -29,7 +29,7 @@
</template>
<script>
import {EXTRACT_TYPE, ExtractCommon} from "../../model/ScenarioModel";
import {EXTRACT_TYPE, ExtractCommon} from "../../model/ApiTestModel";
import MsApiVariableInput from "../ApiVariableInput";
export default {

View File

@ -34,7 +34,7 @@
</template>
<script>
import {Extract, EXTRACT_TYPE} from "../../model/ScenarioModel";
import {Extract, EXTRACT_TYPE} from "../../model/ApiTestModel";
import MsApiExtractCommon from "./ApiExtractCommon";
export default {

View File

@ -3,7 +3,7 @@
<el-tabs v-model="activeName">
<!-- 请求头-->
<el-tab-pane :label="$t('api_test.request.headers')" name="headers">
<ms-api-key-value :is-read-only="isReadOnly" :isShowEnable="true" :suggestions="headerSuggestions"
<ms-api-key-value :is-read-only="isReadOnly" :isShowEnable="isShowEnable" :suggestions="headerSuggestions"
:items="request.headers"/>
</el-tab-pane>
@ -12,13 +12,12 @@
<ms-api-variable :is-read-only="isReadOnly"
:parameters="request.parameters"
:environment="request.environment"
:extract="request.extract"
:description="$t('api_test.request.parameters_desc')"/>
:extract="request.extract"/>
</el-tab-pane>
<!--REST 参数-->
<el-tab-pane :label="$t('api_test.delimit.request.rest_param')" name="rest">
<ms-api-key-value :is-read-only="isReadOnly" :isShowEnable="true" :suggestions="headerSuggestions"
<ms-api-key-value :is-read-only="isReadOnly" :isShowEnable="isShowEnable" :suggestions="headerSuggestions"
:items="request.rest"/>
</el-tab-pane>
@ -49,7 +48,7 @@
import MsApiKeyValue from "../ApiKeyValue";
import MsApiBody from "../body/ApiBody";
import MsApiAuthConfig from "../auth/ApiAuthConfig";
import {HttpRequest, KeyValue, Scenario} from "../../model/ScenarioModel";
import {HttpRequest, KeyValue, Scenario} from "../../model/ApiTestModel";
import MsApiExtract from "../extract/ApiExtract";
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
import {REQUEST_HEADERS} from "@/common/js/constants";
@ -66,6 +65,7 @@
},
props: {
request: HttpRequest,
isShowEnable:Boolean,
jsonPathList: Array,
scenario: Scenario,
isReadOnly: {

View File

@ -1,42 +1,29 @@
<template>
<div class="request-form">
<component @runDebug="runDebug" :is="component" :is-read-only="isReadOnly" :request="request" :scenario="scenario"/>
<component :is="component" :is-read-only="isReadOnly" :request="request" :isShowEnable="isShowEnable"/>
<el-divider v-if="isCompleted"></el-divider>
<ms-request-result-tail v-loading="debugReportLoading" v-if="isCompleted"
:request="request.debugRequestResult ? request.debugRequestResult : {responseResult: {}, subRequestResults: []}"
:scenario-name="request.debugScenario ? request.debugScenario.name : ''"
ref="msDebugResult"/>
</div>
</template>
<script>
import {JSR223Processor, Request, RequestFactory, Scenario, Test} from "../../model/ScenarioModel";
import {JSR223Processor, Request, RequestFactory} from "../../model/ApiTestModel";
import MsApiHttpRequestForm from "./ApiHttpRequestForm";
// import MsApiTcpRequestForm from "./ApiTcpRequestForm";
// import MsApiDubboRequestForm from "./ApiDubboRequestForm";
import MsScenarioResults from "../../../report/components/ScenarioResults";
import MsRequestResultTail from "../../../report/components/RequestResultTail";
// import MsApiSqlRequestForm from "./ApiSqlRequestForm";
export default {
name: "MsApiRequestForm",
components: {MsRequestResultTail, MsScenarioResults, MsApiHttpRequestForm},
components: {MsApiHttpRequestForm},
props: {
scenario: Scenario,
request: Request,
isShowEnable: {
type: Boolean,
default: true
},
isReadOnly: {
type: Boolean,
default: false
},
debugReportId: String
},
data() {
return {
reportId: "",
content: {scenarios: []},
debugReportLoading: false,
showDebugReport: false,
jsonPathList: [],
}
},
computed: {
@ -61,11 +48,6 @@
return !!this.request.debugReport;
}
},
watch: {
debugReportId() {
this.getReport();
}
},
mounted() {
// beanshell
if (!this.request.jsr223PreProcessor.script && this.request.beanShellPreProcessor) {
@ -75,51 +57,6 @@
this.request.jsr223PostProcessor = new JSR223Processor(this.request.beanShellPostProcessor);
}
},
methods: {
getReport() {
if (this.debugReportId) {
this.debugReportLoading = true;
this.showDebugReport = true;
this.request.debugReport = {};
let url = "/api/report/get/" + this.debugReportId;
this.$get(url, response => {
let report = response.data || {};
let res = {};
if (response.data) {
try {
res = JSON.parse(report.content);
} catch (e) {
throw e;
}
if (res) {
this.debugReportLoading = false;
this.request.debugReport = res;
if (res.scenarios && res.scenarios.length > 0) {
this.request.debugScenario = res.scenarios[0];
this.request.debugRequestResult = this.request.debugScenario.requestResults[0];
this.deleteReport(this.debugReportId);
} else {
this.request.debugScenario = new Scenario();
this.request.debugRequestResult = {responseResult: {}, subRequestResults: []};
}
this.$refs.msDebugResult.reload();
} else {
setTimeout(this.getReport, 2000)
}
} else {
this.debugReportLoading = false;
}
});
}
},
deleteReport(reportId) {
this.$post('/api/report/delete', {id: reportId});
},
runDebug() {
this.$emit('runDebug', this.request);
}
}
}
</script>

View File

@ -0,0 +1,106 @@
<template>
<div class="metric-container">
<el-row type="flex">
<div class="metric">
<div class="value">{{response.responseResult.responseTime}} ms</div>
<div class="name">{{$t('api_report.response_time')}}</div>
<br>
<div class="value">{{response.responseResult.latency}} ms</div>
<div class="name">{{$t('api_report.latency')}}</div>
</div>
<div class="metric">
<div class="value">{{response.requestSize}} bytes</div>
<div class="name">{{$t('api_report.request_size')}}</div>
<br>
<div class="value">{{response.responseResult.responseSize}} bytes</div>
<div class="name">{{$t('api_report.response_size')}}</div>
</div>
<div class="metric horizontal">
<el-row type="flex">
<div class="code">
<div class="value" :class="{'error': error}">{{response.responseResult.responseCode}}</div>
<div class="name">{{$t('api_report.response_code')}}</div>
</div>
<div class="split"></div>
<div class="message">
<div class="value">{{response.responseResult.responseMessage}}</div>
<div class="name">{{$t('api_report.response_message')}}</div>
</div>
</el-row>
</div>
</el-row>
</div>
</template>
<script>
export default {
name: "MsRequestMetric",
props: {
response: Object
},
computed: {
error() {
return this.response.responseCode >= 400;
}
}
}
</script>
<style scoped>
.metric-container {
padding: 20px;
}
.metric {
padding: 20px;
border: 1px solid #EBEEF5;
min-width: 120px;
height: 114px;
}
.metric + .metric {
margin-left: 20px;
}
.metric .value {
font-size: 16px;
font-weight: 500;
word-break: break-all;
}
.metric .name {
color: #404040;
opacity: 0.5;
padding: 5px 0;
}
.metric.horizontal {
width: 100%;
}
.metric .code {
min-width: 120px;
}
.metric .code .value {
color: #67C23A;
}
.metric .code .value.error {
color: #F56C6C;
}
.metric .split {
height: 114px;
border-left: 1px solid #EBEEF5;
margin-right: 20px;
}
.metric .message {
max-height: 114px;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,92 @@
<template>
<div class="request-result">
<ms-request-metric :response="response"/>
<ms-response-result :response="response.responseResult"/>
</div>
</template>
<script>
import MsResponseResult from "../response/ResponseResult";
import MsRequestMetric from "../response/RequestMetric";
export default {
name: "MsRequestResultTail",
components: {MsRequestMetric, MsResponseResult},
props: {
response: Object,
},
data() {
return {
isCodeEditAlive: true
}
},
methods: {
reload() {
this.isCodeEditAlive = false;
this.$nextTick(() => (this.isCodeEditAlive = true));
}
},
}
</script>
<style scoped>
.request-result {
width: 100%;
min-height: 40px;
padding: 2px 0;
}
.request-result .info {
background-color: #F9F9F9;
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
line-height: 40px;
padding-left: 5px;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #FFF;
}
.sub-result .method {
border-left: 5px solid #1E90FF;
padding-left: 20px;
}
.sub-result:last-child {
border-bottom: 1px solid #EBEEF5;
}
.request-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<div class="text-container">
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane :label="$t('api_test.delimit.request.response_header')" name="headers" class="pane">
<pre>{{ response.headers }}</pre>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.delimit.request.response_body')" name="body" class="pane">
<ms-code-edit :mode="mode" :read-only="true" :modes="modes" :data.sync="response.body" ref="codeEdit"/>
</el-tab-pane>
<el-tab-pane label="Cookie" name="cookie" class="pane cookie">
<pre>{{response.cookies}}</pre>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.delimit.request.console')" name="console" class="pane">
<pre>{{response.console}}</pre>
</el-tab-pane>
<el-tab-pane v-if="activeName == 'body'" :disabled="true" name="mode" class="pane cookie">
<template v-slot:label>
<ms-dropdown :commands="modes" :default-command="mode" @command="modeChange"/>
</template>
</el-tab-pane>
</el-tabs>
</div>
</template>
<script>
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsDropdown from "../../../../common/components/MsDropdown";
import {BODY_FORMAT} from "../../model/ApiTestModel";
export default {
name: "MsResponseResult",
components: {
MsDropdown,
MsCodeEdit,
MsAssertionResults,
},
props: {
response: Object
},
data() {
return {
isActive: true,
activeName: "headers",
modes: ['text', 'json', 'xml', 'html'],
mode: BODY_FORMAT.TEXT
}
},
methods: {
active() {
console.log(this.response.body);
},
modeChange(mode) {
this.mode = mode;
}
},
mounted() {
if (!this.response.headers) {
return;
}
if (this.response.headers.indexOf("Content-Type: application/json") > 0) {
this.mode = BODY_FORMAT.JSON;
}
}
}
</script>
<style scoped>
.text-container .icon {
padding: 5px;
}
.text-container .collapse {
cursor: pointer;
}
.text-container .collapse:hover {
opacity: 0.8;
}
.text-container .icon.is-active {
transform: rotate(90deg);
}
.text-container .pane {
background-color: #F5F5F5;
padding: 0 10px;
height: 250px;
overflow-y: auto;
}
.text-container .pane.cookie {
padding: 0;
}
pre {
margin: 0;
}
</style>

View File

@ -1,33 +1,33 @@
<template>
<div class="text-container">
<div @click="active" class="collapse">
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{ $t('api_report.response') }}
</div>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane label="Body" name="body" class="pane">
<ms-code-edit :mode="mode" :read-only="true" :data="response.body" :modes="modes" ref="codeEdit"/>
</el-tab-pane>
<el-tab-pane label="Headers" name="headers" class="pane">
<pre>{{ response.headers }}</pre>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.assertions')" name="assertions" class="pane assertions">
<ms-assertion-results :assertions="response.assertions"/>
</el-tab-pane>
<el-form :model="response" ref="response" label-width="100px">
<el-tab-pane :label="$t('api_test.request.extract.label')" name="label" class="pane">
<pre>{{response.vars}}</pre>
</el-tab-pane>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane :label="$t('api_test.delimit.request.response_header')" name="headers" class="pane">
<ms-api-key-value :isShowEnable="false" :suggestions="headerSuggestions"
:items="response.headers"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.delimit.request.response_body')" name="body" class="pane">
<ms-api-body
:body="response.body"
:extract="response.extract"/>
</el-tab-pane>
<el-tab-pane v-if="activeName == 'body'" :disabled="true" name="mode" class="pane assertions">
<template v-slot:label>
<ms-dropdown :commands="modes" :default-command="mode" @command="modeChange"/>
</template>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.delimit.request.status_code')" name="status_code" class="pane">
<ms-api-key-value :isShowEnable="false" :suggestions="headerSuggestions"
:items="response.statusCode"/>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
<el-tab-pane v-if="activeName == 'body'" :disabled="true" name="mode" class="pane cookie">
<template v-slot:label>
<ms-dropdown :commands="modes" :default-command="mode" @command="modeChange"/>
</template>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</el-form>
</div>
</template>
@ -35,7 +35,10 @@
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsDropdown from "../../../../common/components/MsDropdown";
import {BODY_FORMAT} from "../../model/ScenarioModel";
import {BODY_FORMAT} from "../../model/ApiTestModel";
import MsApiKeyValue from "../ApiKeyValue";
import {REQUEST_HEADERS} from "@/common/js/constants";
import MsApiBody from "../body/ApiBody";
export default {
name: "MsResponseText",
@ -44,6 +47,8 @@
MsDropdown,
MsCodeEdit,
MsAssertionResults,
MsApiKeyValue,
MsApiBody,
},
props: {
@ -53,9 +58,11 @@
data() {
return {
isActive: true,
activeName: "body",
activeName: "headers",
modes: ['text', 'json', 'xml', 'html'],
mode: BODY_FORMAT.TEXT
mode: BODY_FORMAT.TEXT,
headerSuggestions: REQUEST_HEADERS
}
},
@ -97,13 +104,13 @@
}
.text-container .pane {
background-color: #F5F5F5;
background-color: white;
padding: 0 10px;
height: 250px;
overflow-y: auto;
}
.text-container .pane.assertions {
.text-container .pane.cookie {
padding: 0;
}

View File

@ -1,22 +1,46 @@
<template>
<div class="card-container">
<el-card class="card-content">
<el-card class="card-content" v-loading="loading">
<el-form :model="apiData" ref="apiData" :inline="true" label-position="right">
<div style="font-size: 16px;color: #333333">{{$t('test_track.plan_view.base_info')}}</div>
<br/>
<el-form-item :label="$t('api_report.request')" prop="responsible">
<el-input :placeholder="$t('api_test.delimit.request.path_info')" v-model="url"
class="ms-http-input" size="small" :disabled="false">
<el-select v-model="path" slot="prepend" style="width: 100px">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
<el-form :model="api" :rules="rules" ref="apiData" :inline="true" label-position="right">
<p class="tip">{{$t('test_track.plan_view.base_info')}} </p>
<!-- 请求方法 -->
<el-form-item :label="$t('api_report.request')" prop="path">
<el-select v-model="api.path" style="width: 100px" size="small">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-form-item>
<!-- 执行环境 -->
<el-form-item prop="environmentId">
<el-select v-model="api.environmentId" size="small" class="ms-htt-width"
:placeholder="$t('api_test.delimit.request.run_env')"
@change="environmentChange" clearable>
<el-option v-for="(environment, index) in environments" :key="index"
:label="environment.name + (environment.config.httpConfig.socket ? (': ' + environment.config.httpConfig.protocol + '://' + environment.config.httpConfig.socket) : '')"
:value="environment.id"/>
<el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">
{{ $t('api_test.environment.environment_config') }}
</el-button>
<template v-slot:empty>
<div class="empty-environment">
<el-button class="environment-button" size="mini" type="primary" @click="openEnvironmentConfig">
{{ $t('api_test.environment.environment_config') }}
</el-button>
</div>
</template>
</el-select>
</el-form-item>
<!-- 请求地址 -->
<el-form-item prop="url">
<el-input :placeholder="$t('api_test.delimit.request.path_info')" v-model="api.url" class="ms-htt-width"
size="small" :disabled="false"/>
</el-form-item>
<!-- 操作按钮 -->
<el-form-item>
<el-dropdown split-button type="primary" class="ms-api-buttion" @click="handleCommand('add')"
@command="handleCommand" size="small">
@ -33,50 +57,72 @@
</el-form-item>
<div style="font-size: 16px;color: #333333;padding-top: 30px">{{$t('api_test.delimit.request.req_param')}}</div>
<br/>
<p class="tip">{{$t('api_test.delimit.request.req_param')}} </p>
<!-- HTTP 请求参数 -->
<ms-api-request-form :request="apiData.request"/>
<ms-api-request-form :request="api.request"/>
</el-form>
<div style="font-size: 16px;color: #333333 ;padding-top: 30px">{{$t('api_test.delimit.request.res_param')}}</div>
<br/>
<ms-response-text :response="responseData"></ms-response-text>
<!--返回结果-->
<!-- HTTP 请求返回数据 -->
<p class="tip">{{$t('api_test.delimit.request.res_param')}} </p>
<ms-request-result-tail :response="responseData" ref="runResult"/>
</el-card>
<!-- 加载用例 -->
<ms-bottom-container v-bind:enableAsideHidden="isHide">
<ms-api-case-list @apiCaseClose="apiCaseClose" @selectTestCase="selectTestCase" :api="apiData" ref="caseList"/>
<ms-api-case-list @apiCaseClose="apiCaseClose" @selectTestCase="selectTestCase" :api="api"
:currentProject="currentProject" :loaded="loaded"
ref="caseList"/>
</ms-bottom-container>
<!-- 环境 -->
<api-environment-config ref="environmentConfig" @close="environmentConfigClose"/>
</div>
</template>
<script>
import MsApiRequestForm from "../request/ApiRequestForm";
import {downloadFile, getUUID} from "@/common/js/utils";
import MsResponseText from "../../../report/components/ResponseText";
import MsApiCaseList from "../ApiCaseList";
import MsContainer from "../../../../common/components/MsContainer";
import MsBottomContainer from "../BottomContainer";
import {RequestFactory, Test} from "../../model/ScenarioModel";
import {RequestFactory, Test} from "../../model/ApiTestModel";
import {parseEnvironment} from "../../model/EnvironmentModel";
import ApiEnvironmentConfig from "../../../test/components/ApiEnvironmentConfig";
import MsRequestResultTail from "../response/RequestResultTail";
import {REQ_METHOD} from "../../model/JsonData";
export default {
name: "ApiConfig",
components: {MsResponseText, MsApiRequestForm, MsApiCaseList, MsContainer, MsBottomContainer},
components: {
MsApiRequestForm,
MsApiCaseList,
MsContainer,
MsBottomContainer,
MsRequestResultTail,
ApiEnvironmentConfig
},
data() {
return {
isHide: true,
url: '',
path: '',
api: {},
loaded: false,
loading: false,
currentRequest: {},
responseData: {},
responseData: {type: 'HTTP', responseResult: {}, subRequestResults: []},
reqOptions: REQ_METHOD,
environments: [],
rules: {
path: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],
url: [{required: true, message: this.$t('api_test.delimit.request.path_info'), trigger: 'blur'}],
environmentId: [{required: true, message: this.$t('api_test.delimit.request.run_env'), trigger: 'change'}],
}
}
},
props: {apiData: {}},
props: {apiData: {}, currentProject: {}},
methods: {
handleCommand(e) {
switch (e) {
@ -89,13 +135,59 @@
case "save_as_api":
return this.saveAsApi();
default:
return [];
return this.runTest();
}
},
runTest() {
this.$refs['apiData'].validate((valid) => {
if (valid) {
this.loading = true;
let url = "/api/delimit/run";
let bodyFiles = this.getBodyUploadFiles();
let env = this.api.environment.config.httpConfig.socket ? (this.api.environment.config.httpConfig.protocol + '://' + this.api.environment.config.httpConfig.socket) : '';
if (env.endsWith("/")) {
env = env.substr(0, env.length - 1);
}
let sendUrl = this.api.url;
if (!sendUrl.startsWith("/")) {
sendUrl = "/" + sendUrl;
}
this.api.test.request.url = env + sendUrl;
this.api.test.request.path = this.api.path;
this.api.test.request.name = this.api.id;
this.api.reportId = "run";
let jmx = this.api.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(url, file, bodyFiles, this.api, response => {
this.getResult();
}, erro => {
this.loading = false;
});
}
})
},
getResult() {
if (this.api.id) {
let url = "/api/delimit/report/get/" + this.api.id + "/run";
this.$get(url, response => {
if (response.data) {
let testResult = JSON.parse(response.data.content);
this.responseData = testResult;
this.loading = false;
this.$refs.runResult.reload();
} else {
setTimeout(this.getResult, 2000)
}
});
}
},
saveAs() {
this.$emit('saveAs', this.apiData);
this.$emit('saveAs', this.api);
},
loadCase() {
this.loaded = true;
this.isHide = false;
},
apiCaseClose() {
@ -103,8 +195,8 @@
},
getBodyUploadFiles() {
let bodyUploadFiles = [];
this.apiData.bodyUploadIds = [];
let request = this.apiData.request;
this.api.bodyUploadIds = [];
let request = this.api.request;
if (request.body) {
request.body.kvs.forEach(param => {
if (param.files) {
@ -113,7 +205,7 @@
let fileId = getUUID().substring(0, 8);
item.name = item.file.name;
item.id = fileId;
this.apiData.bodyUploadIds.push(fileId);
this.api.bodyUploadIds.push(fileId);
bodyUploadFiles.push(item.file);
}
});
@ -123,60 +215,109 @@
return bodyUploadFiles;
},
saveAsCase() {
this.isHide = false;
this.loaded = false;
let testCase = {};
let test = new Test();
test.request = this.apiData.request;
testCase.test = test;
testCase.request = this.apiData.request;
testCase.name = this.apiData.name;
testCase.request = this.api.request;
testCase.apiDelimitId = this.api.id;
testCase.priority = "P0";
this.$refs.caseList.saveTestCase(testCase);
this.$refs.caseList.createCase(testCase);
},
saveAsApi() {
let data = {};
data.request = JSON.stringify(this.apiData.request);
data.path = this.apiData.path;
data.url = this.apiData.url;
data.status = this.apiData.status;
data.userId = this.apiData.userId;
data.description = this.apiData.description;
data.request = JSON.stringify(this.api.request);
data.path = this.api.path;
data.url = this.api.url;
data.status = this.api.status;
data.userId = this.api.userId;
data.description = this.api.description;
this.$emit('saveAsApi', data);
},
editApi(url) {
this.apiData.url = this.url;
this.apiData.path = this.path;
let bodyFiles = this.getBodyUploadFiles();
let jmx = this.apiData.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(url, file, bodyFiles, this.apiData, () => {
this.$success(this.$t('commons.save_success'));
this.$emit('saveApi', this.apiData);
});
},
updateApi() {
let url = "/api/delimit/update";
this.editApi(url);
let bodyFiles = this.getBodyUploadFiles();
let jmx = this.api.test.toJMX();
let blob = new Blob([jmx.xml], {type: "application/octet-stream"});
let file = new File([blob], jmx.name);
this.$fileUpload(url, file, bodyFiles, this.api, () => {
this.$success(this.$t('commons.save_success'));
this.$emit('saveApi', this.api);
});
},
selectTestCase(item) {
if (item != null) {
this.apiData.request = new RequestFactory(JSON.parse(item.request));
this.api.request = new RequestFactory(JSON.parse(item.request));
} else {
this.apiData.request = this.currentRequest;
this.api.request = this.currentRequest;
}
}
},
getEnvironments() {
if (this.currentProject) {
this.$get('/api/environment/list/' + this.currentProject.id, response => {
this.environments = response.data;
this.environments.forEach(environment => {
parseEnvironment(environment);
});
let hasEnvironment = false;
for (let i in this.environments) {
if (this.environments[i].id === this.api.environmentId) {
this.api.environment = this.environments[i];
hasEnvironment = true;
break;
}
}
if (!hasEnvironment) {
this.api.environmentId = '';
this.api.environment = undefined;
}
});
} else {
this.api.environmentId = '';
this.api.environment = undefined;
}
},
openEnvironmentConfig() {
if (!this.currentProject) {
this.$error(this.$t('api_test.select_project'));
return;
}
this.$refs.environmentConfig.open(this.currentProject.id);
},
environmentChange(value) {
for (let i in this.environments) {
if (this.environments[i].id === value) {
this.api.environment = this.environments[i];
break;
}
}
},
environmentConfigClose() {
this.getEnvironments();
},
},
created() {
this.currentRequest = this.apiData.request;
this.url = this.apiData.url;
this.path = this.apiData.path;
this.api = this.apiData;
this.getEnvironments();
this.getResult();
}
}
</script>
<style scoped>
.ms-http-input {
width: 500px;
margin-top: 5px;
.ms-htt-width {
width: 350px;
}
.environment-button {
margin-left: 20px;
padding: 7px;
}
.tip {
padding: 3px 5px;
font-size: 16px;
border-radius: 4px;
border-left: 4px solid #783887;
margin: 20px 0;
}
</style>

View File

@ -81,9 +81,10 @@ export const BODY_TYPE = {
KV: "KeyValue",
FORM_DATA: "Form Data",
RAW: "Raw",
WWW_FORM: "WWW_Form",
WWW_FORM: "WWW_FORM",
XML: "XML",
BINARY: "BINARY"
BINARY: "BINARY",
JSON: "JSON"
}
export const BODY_FORMAT = {
@ -295,6 +296,29 @@ export class RequestFactory {
}
}
export class ResponseFactory {
static TYPES = {
HTTP: "HTTP",
DUBBO: "DUBBO",
SQL: "SQL",
TCP: "TCP",
}
constructor(options = {}) {
options.type = options.type || ResponseFactory.TYPES.HTTP
switch (options.type) {
case RequestFactory.TYPES.DUBBO:
return new DubboRequest(options);
case RequestFactory.TYPES.SQL:
return new SqlRequest(options);
case RequestFactory.TYPES.TCP:
return new TCPRequest(options);
default:
return new HttpResponse(options);
}
}
}
export class Request extends BaseConfig {
constructor(type, options = {}) {
super();
@ -388,6 +412,32 @@ export class HttpRequest extends Request {
}
export class Response extends BaseConfig {
constructor(type, options = {}) {
super();
this.type = type;
this.id = options.id || uuid();
this.name = options.name;
this.enable = options.enable === undefined ? true : options.enable;
this.assertions = new Assertions(options.assertions);
this.extract = new Extract(options.extract);
this.jsr223PreProcessor = new JSR223Processor(options.jsr223PreProcessor);
this.jsr223PostProcessor = new JSR223Processor(options.jsr223PostProcessor);
}
}
export class HttpResponse extends Response {
constructor(options) {
super(ResponseFactory.TYPES.HTTP, options);
this.headers = [];
this.body = new Body(options.body);
this.statusCode = [];
this.sets({statusCode: KeyValue,headers: KeyValue}, options);
}
}
export class DubboRequest extends Request {
static PROTOCOLS = {
DUBBO: "dubbo://",
@ -658,9 +708,12 @@ export class Body extends BaseConfig {
this.type = undefined;
this.raw = undefined;
this.kvs = [];
this.fromUrlencoded = [];
this.binary = [];
this.xml = undefined;
this.json = undefined;
this.set(options);
this.sets({kvs: KeyValue}, options);
this.sets({kvs: KeyValue},{fromUrlencoded: KeyValue},{binary: KeyValue}, options);
}
isValid() {

View File

@ -1,4 +1,4 @@
import {BaseConfig, DatabaseConfig, KeyValue} from "./ScenarioModel";
import {BaseConfig, DatabaseConfig, KeyValue} from "./ApiTestModel";
import {TCPConfig} from "@/business/components/api/test/model/ScenarioModel";
export class Environment extends BaseConfig {

View File

@ -37,3 +37,8 @@ export const API_METHOD_COLOUR = [
['HEAD', '#8E58E7'], ['CONNECT', '#90AFAE'],
['DUBBO', '#C36EEF'], ['SQL', '#0AEAD4'], ['TCP', '#0A52DF'],
]
export const REQUIRED = [
{name: '必填', id: true},
{name: '非必填', id: false}
]

View File

@ -1,5 +1,10 @@
<template>
<div class="text-container">
<div @click="active" class="collapse">
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{ $t('api_report.response') }}
</div>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane :class="'body-pane'" label="Body" name="body" class="pane">
<ms-sql-result-table v-if="isSqlType" :body="response.body"/>
@ -23,64 +28,65 @@
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</div>
</template>
<script>
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "../../../common/components/MsCodeEdit";
import MsDropdown from "../../../common/components/MsDropdown";
import {BODY_FORMAT, RequestFactory, Request, SqlRequest} from "../../test/model/ScenarioModel";
import MsSqlResultTable from "./SqlResultTable";
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "../../../common/components/MsCodeEdit";
import MsDropdown from "../../../common/components/MsDropdown";
import {BODY_FORMAT, RequestFactory, Request, SqlRequest} from "../../test/model/ScenarioModel";
import MsSqlResultTable from "./SqlResultTable";
export default {
name: "MsResponseText",
export default {
name: "MsResponseText",
components: {
MsSqlResultTable,
MsDropdown,
MsCodeEdit,
MsAssertionResults,
},
props: {
requestType: String,
response: Object
},
data() {
return {
isActive: true,
activeName: "body",
modes: ['text', 'json', 'xml', 'html'],
mode: BODY_FORMAT.TEXT
}
},
methods: {
active() {
this.isActive = !this.isActive;
components: {
MsSqlResultTable,
MsDropdown,
MsCodeEdit,
MsAssertionResults,
},
modeChange(mode) {
this.mode = mode;
}
},
mounted() {
if (!this.response.headers) {
return;
}
if (this.response.headers.indexOf("Content-Type: application/json") > 0) {
this.mode = BODY_FORMAT.JSON;
}
},
props: {
requestType: String,
response: Object
},
computed: {
isSqlType() {
return (this.requestType === RequestFactory.TYPES.SQL && this.response.responseCode === '200');
data() {
return {
isActive: true,
activeName: "body",
modes: ['text', 'json', 'xml', 'html'],
mode: BODY_FORMAT.TEXT
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
modeChange(mode) {
this.mode = mode;
}
},
mounted() {
if (!this.response.headers) {
return;
}
if (this.response.headers.indexOf("Content-Type: application/json") > 0) {
this.mode = BODY_FORMAT.JSON;
}
},
computed: {
isSqlType() {
return (this.requestType === RequestFactory.TYPES.SQL && this.response.responseCode === '200');
}
}
}
}
</script>
<style scoped>

View File

@ -0,0 +1,165 @@
<template>
<div>
<div class="jsoneditor-vue" style="height: 400px"></div>
</div>
</template>
<script>
import 'jsoneditor/dist/jsoneditor.css';
import JsonEditor from 'jsoneditor'
export default {
props: {
value: [String, Number, Object, Array],
expandedOnStart: {
type: Boolean,
default: false
},
mode: {
type: String,
default: "tree"
},
modes: {
type: Array,
default: function () {
return ["tree", "code"];
}
}
},
watch: {
value: {
immediate: true,
async handler(val) {
if (!this.internalChange) {
await this.setEditor(val);
this.expandAll();
}
},
deep: true
}
},
data() {
return {
editor: null,
error: false,
json: this.value,
internalChange: false,
expandedModes: ["tree", "view", "form"],
};
},
mounted() {
let self = this;
let options = {
mode: this.mode,
modes: this.modes, // allowed modes
onChange() {
try {
let json = self.editor.get();
self.json = json;
self.$emit("json-change", json);
self.internalChange = true;
self.$emit("input", json);
self.$nextTick(function () {
self.internalChange = false;
});
} catch (e) {
self.$emit("has-error", e);
}
},
onModeChange() {
self.expandAll();
},
onError(error) {
self.$emit("onError", error);
}
};
this.editor = new JsonEditor(
this.$el.querySelector(".jsoneditor-vue"),
options,
this.json
);
},
methods: {
expandAll() {
if (
this.expandedOnStart &&
this.expandedModes.includes(this.editor.getMode())
) {
this.editor.expandAll();
}
},
async setEditor(value) {
if (this.editor) this.editor.set(value);
}
}
};
</script>
<style scoped>
.ace_line_group {
text-align: left;
}
.json-editor-container {
display: flex;
width: 100%;
}
.json-editor-container .tree-mode {
width: 50%;
}
.json-editor-container .code-mode {
flex-grow: 1;
}
.jsoneditor-btns {
text-align: center;
margin-top: 10px;
}
.jsoneditor-vue .jsoneditor-outer {
height: 300px;
}
.jsoneditor-vue div.jsoneditor-tree {
min-height: 350px;
}
.json-save-btn {
background-color: #20A0FF;
border: none;
color: #fff;
padding: 5px 10px;
border-radius: 5px;
}
.json-save-btn:focus {
outline: none;
}
.json-save-btn[disabled] {
background-color: #1D8CE0;
}
code {
background-color: #f5f5f5;
}
/deep/ .jsoneditor-poweredBy {
visibility: hidden;
}
/deep/ .jsoneditor-contextmenu .jsoneditor-menu li button.jsoneditor-selected {
background-color: #1E9FFB;
}
/deep/ jsoneditor-tree {
overflow: auto;
}
/deep/ .jsoneditor {
border-color: #1E9FFB;
border-radius: 3px;
}
</style>

View File

@ -480,6 +480,7 @@ export default {
case: "Case",
title: "Create api",
path_info: "Please enter the URL of the interface, such as /api/demo/#{id}, where id is the path parameter",
path_all_info: "Please enter the complete test address",
fast_debug: "Fast debug",
close_all_label: "close all label",
save_as: "Save as new interface",
@ -499,9 +500,14 @@ export default {
verified: "verified",
encryption: "encryption",
req_param: "Request parameter",
res_param: "Response template",
res_param: "Response content",
batch_delete: "Batch deletion",
delete_confirm: "confirm deletion",
delete_confirm: "Confirm deletion",
assertions_rule: "Assertion rule",
response_header: "Response header",
response_body: "Response body",
console: "Console",
status_code: "Status code",
}
},
environment: {

View File

@ -481,6 +481,7 @@ export default {
responsible: "责任人",
title: "创建接口",
path_info: "请输入接口的URL如/api/demo/#{id}其中id为路径参数",
path_all_info: "请输入完整测试地址",
fast_debug: "快捷调试",
close_all_label: "关闭所有标签",
save_as: "另存为新接口",
@ -500,9 +501,15 @@ export default {
verified: "认证",
encryption: "加密",
req_param: "请求参数",
res_param: "响应模版",
res_param: "响应内容",
batch_delete: "批量删除",
delete_confirm: "确认删除接口",
assertions_rule: "断言规则",
response_header: "响应头",
response_body: "响应体",
console: "控制台",
status_code: "状态码",
}
},
environment: {

View File

@ -481,6 +481,7 @@ export default {
responsible: "责任人",
title: "创建接口",
path_info: "請輸入接口的URL如/api/demo/#{id}其中id為路徑參數",
path_all_info: "請輸入完整測試地址",
fast_debug: "快捷調試",
close_all_label: "關閉所有標簽",
save_as: "另存為新接口",
@ -500,9 +501,15 @@ export default {
verified: "認證",
encryption: "加密",
req_param: "請求參數",
res_param: "響應模版",
res_param: "響應内容",
batch_delete: "批量删除",
delete_confirm: "確認刪除接口",
assertions_rule: "斷言規則",
response_header: "響應頭",
response_body: "響應體",
console: "控制臺",
status_code: "狀態碼",
}
},
environment: {