Merge remote-tracking branch 'origin/dev' into dev
# Conflicts: # backend/pom.xml
This commit is contained in:
commit
e9e27f4211
|
@ -36,10 +36,6 @@
|
|||
<artifactId>spring-boot-starter-tomcat</artifactId>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
</exclusion>
|
||||
<exclusion>
|
||||
<artifactId>hibernate-validator</artifactId>
|
||||
<groupId>org.hibernate.validator</groupId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
|
@ -138,6 +134,14 @@
|
|||
<artifactId>ApacheJMeter_core</artifactId>
|
||||
<version>${jmeter.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- easyexcel -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba</groupId>
|
||||
<artifactId>easyexcel</artifactId>
|
||||
<version>2.1.7</version>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
|
|
|
@ -1,194 +0,0 @@
|
|||
package io.metersphere.base.domain;
|
||||
|
||||
public class ZaleniumTest {
|
||||
|
||||
private String seleniumSessionId;
|
||||
private String testName;
|
||||
private String timestamp;
|
||||
private String addedToDashboardTime;
|
||||
private String browser;
|
||||
private String browserVersion;
|
||||
private String proxyName;
|
||||
private String platform;
|
||||
private String fileName;
|
||||
private String fileExtension;
|
||||
private String videoFolderPath;
|
||||
private String logsFolderPath;
|
||||
private String testNameNoExtension;
|
||||
private String screenDimension;
|
||||
private String timeZone;
|
||||
private String build;
|
||||
private String testFileNameTemplate;
|
||||
private String browserDriverLogFileName;
|
||||
private String retentionDate;
|
||||
private String testStatus;
|
||||
private boolean videoRecorded;
|
||||
|
||||
public String getSeleniumSessionId() {
|
||||
return seleniumSessionId;
|
||||
}
|
||||
|
||||
public void setSeleniumSessionId(String seleniumSessionId) {
|
||||
this.seleniumSessionId = seleniumSessionId;
|
||||
}
|
||||
|
||||
public String getTestName() {
|
||||
return testName;
|
||||
}
|
||||
|
||||
public void setTestName(String testName) {
|
||||
this.testName = testName;
|
||||
}
|
||||
|
||||
public String getTimestamp() {
|
||||
return timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(String timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public String getAddedToDashboardTime() {
|
||||
return addedToDashboardTime;
|
||||
}
|
||||
|
||||
public void setAddedToDashboardTime(String addedToDashboardTime) {
|
||||
this.addedToDashboardTime = addedToDashboardTime;
|
||||
}
|
||||
|
||||
public String getBrowser() {
|
||||
return browser;
|
||||
}
|
||||
|
||||
public void setBrowser(String browser) {
|
||||
this.browser = browser;
|
||||
}
|
||||
|
||||
public String getBrowserVersion() {
|
||||
return browserVersion;
|
||||
}
|
||||
|
||||
public void setBrowserVersion(String browserVersion) {
|
||||
this.browserVersion = browserVersion;
|
||||
}
|
||||
|
||||
public String getProxyName() {
|
||||
return proxyName;
|
||||
}
|
||||
|
||||
public void setProxyName(String proxyName) {
|
||||
this.proxyName = proxyName;
|
||||
}
|
||||
|
||||
public String getPlatform() {
|
||||
return platform;
|
||||
}
|
||||
|
||||
public void setPlatform(String platform) {
|
||||
this.platform = platform;
|
||||
}
|
||||
|
||||
public String getFileName() {
|
||||
return fileName;
|
||||
}
|
||||
|
||||
public void setFileName(String fileName) {
|
||||
this.fileName = fileName;
|
||||
}
|
||||
|
||||
public String getFileExtension() {
|
||||
return fileExtension;
|
||||
}
|
||||
|
||||
public void setFileExtension(String fileExtension) {
|
||||
this.fileExtension = fileExtension;
|
||||
}
|
||||
|
||||
public String getVideoFolderPath() {
|
||||
return videoFolderPath;
|
||||
}
|
||||
|
||||
public void setVideoFolderPath(String videoFolderPath) {
|
||||
this.videoFolderPath = videoFolderPath;
|
||||
}
|
||||
|
||||
public String getLogsFolderPath() {
|
||||
return logsFolderPath;
|
||||
}
|
||||
|
||||
public void setLogsFolderPath(String logsFolderPath) {
|
||||
this.logsFolderPath = logsFolderPath;
|
||||
}
|
||||
|
||||
public String getTestNameNoExtension() {
|
||||
return testNameNoExtension;
|
||||
}
|
||||
|
||||
public void setTestNameNoExtension(String testNameNoExtension) {
|
||||
this.testNameNoExtension = testNameNoExtension;
|
||||
}
|
||||
|
||||
public String getScreenDimension() {
|
||||
return screenDimension;
|
||||
}
|
||||
|
||||
public void setScreenDimension(String screenDimension) {
|
||||
this.screenDimension = screenDimension;
|
||||
}
|
||||
|
||||
public String getTimeZone() {
|
||||
return timeZone;
|
||||
}
|
||||
|
||||
public void setTimeZone(String timeZone) {
|
||||
this.timeZone = timeZone;
|
||||
}
|
||||
|
||||
public String getBuild() {
|
||||
return build;
|
||||
}
|
||||
|
||||
public void setBuild(String build) {
|
||||
this.build = build;
|
||||
}
|
||||
|
||||
public String getTestFileNameTemplate() {
|
||||
return testFileNameTemplate;
|
||||
}
|
||||
|
||||
public void setTestFileNameTemplate(String testFileNameTemplate) {
|
||||
this.testFileNameTemplate = testFileNameTemplate;
|
||||
}
|
||||
|
||||
public String getBrowserDriverLogFileName() {
|
||||
return browserDriverLogFileName;
|
||||
}
|
||||
|
||||
public void setBrowserDriverLogFileName(String browserDriverLogFileName) {
|
||||
this.browserDriverLogFileName = browserDriverLogFileName;
|
||||
}
|
||||
|
||||
public String getRetentionDate() {
|
||||
return retentionDate;
|
||||
}
|
||||
|
||||
public void setRetentionDate(String retentionDate) {
|
||||
this.retentionDate = retentionDate;
|
||||
}
|
||||
|
||||
public String getTestStatus() {
|
||||
return testStatus;
|
||||
}
|
||||
|
||||
public void setTestStatus(String testStatus) {
|
||||
this.testStatus = testStatus;
|
||||
}
|
||||
|
||||
public boolean isVideoRecorded() {
|
||||
return videoRecorded;
|
||||
}
|
||||
|
||||
public void setVideoRecorded(boolean videoRecorded) {
|
||||
this.videoRecorded = videoRecorded;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,5 @@
|
|||
package io.metersphere.commons.constants;
|
||||
|
||||
public class TestCaseConstants {
|
||||
public static final int MAX_NODE_DEPTH = 5;
|
||||
}
|
|
@ -6,16 +6,14 @@ import io.metersphere.base.domain.*;
|
|||
import io.metersphere.commons.utils.PageUtils;
|
||||
import io.metersphere.commons.utils.Pager;
|
||||
import io.metersphere.controller.request.testcase.QueryTestCaseRequest;
|
||||
import io.metersphere.controller.request.testplan.QueryTestPlanRequest;
|
||||
import io.metersphere.dto.LoadTestDTO;
|
||||
import io.metersphere.dto.TestCaseNodeDTO;
|
||||
import io.metersphere.dto.TestPlanCaseDTO;
|
||||
import io.metersphere.service.TestCaseNodeService;
|
||||
import io.metersphere.excel.domain.ExcelResponse;
|
||||
import io.metersphere.service.TestCaseService;
|
||||
import io.metersphere.user.SessionUtils;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.util.List;
|
||||
|
||||
@RequestMapping("/test/case")
|
||||
|
@ -74,5 +72,15 @@ public class TestCaseController {
|
|||
return testCaseService.deleteTestCase(testCaseId);
|
||||
}
|
||||
|
||||
@PostMapping("/import/{projectId}")
|
||||
public ExcelResponse testCaseImport(MultipartFile file, @PathVariable String projectId){
|
||||
return testCaseService.testCaseImport(file, projectId);
|
||||
}
|
||||
|
||||
@GetMapping("/export/template")
|
||||
public void testCaseTemplateExport(HttpServletResponse response){
|
||||
testCaseService.testCaseTemplateExport(response);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,42 @@
|
|||
package io.metersphere.excel.domain;
|
||||
|
||||
public class ExcelErrData<T> {
|
||||
|
||||
private T t;
|
||||
|
||||
private Integer rowNum;
|
||||
|
||||
private String errMsg;
|
||||
|
||||
public ExcelErrData(){}
|
||||
|
||||
public ExcelErrData(T t, Integer rowNum,String errMsg){
|
||||
this.t = t;
|
||||
this.rowNum = rowNum;
|
||||
this.errMsg = errMsg;
|
||||
}
|
||||
|
||||
public T getT() {
|
||||
return t;
|
||||
}
|
||||
|
||||
public void setT(T t) {
|
||||
this.t = t;
|
||||
}
|
||||
|
||||
public String getErrMsg() {
|
||||
return errMsg;
|
||||
}
|
||||
|
||||
public void setErrMsg(String errMsg) {
|
||||
this.errMsg = errMsg;
|
||||
}
|
||||
|
||||
public Integer getRowNum() {
|
||||
return rowNum;
|
||||
}
|
||||
|
||||
public void setRowNum(Integer rowNum) {
|
||||
this.rowNum = rowNum;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package io.metersphere.excel.domain;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class ExcelResponse<T> {
|
||||
|
||||
private Boolean success;
|
||||
private List<ExcelErrData<T>> errList;
|
||||
|
||||
public Boolean getSuccess() {
|
||||
return success;
|
||||
}
|
||||
|
||||
public void setSuccess(Boolean success) {
|
||||
this.success = success;
|
||||
}
|
||||
|
||||
public List<ExcelErrData<T>> getErrList() {
|
||||
return errList;
|
||||
}
|
||||
|
||||
public void setErrList(List<ExcelErrData<T>> errList) {
|
||||
this.errList = errList;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
package io.metersphere.excel.domain;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.annotation.write.style.ColumnWidth;
|
||||
import com.alibaba.excel.annotation.write.style.ContentRowHeight;
|
||||
import org.hibernate.validator.constraints.Length;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
@ColumnWidth(15)
|
||||
public class TestCaseExcelData {
|
||||
|
||||
@NotBlank
|
||||
@Length(max=50)
|
||||
@ExcelProperty("用例名称")
|
||||
private String name;
|
||||
|
||||
@NotBlank
|
||||
@Length(max=1000)
|
||||
@ExcelProperty("所属模块")
|
||||
@ColumnWidth(30)
|
||||
@Pattern(regexp = "^(?!.*//).*$", message = "格式不正确")
|
||||
private String nodePath;
|
||||
|
||||
@NotBlank
|
||||
@ExcelProperty("用例类型")
|
||||
@Pattern(regexp = "(^functional$)|(^performance$)|(^api$)", message = "必须为functional、performance、api")
|
||||
private String type;
|
||||
|
||||
@NotBlank
|
||||
@ExcelProperty("维护人")
|
||||
private String maintainer;
|
||||
|
||||
@NotBlank
|
||||
@ExcelProperty("优先级")
|
||||
@Pattern(regexp = "(^P0$)|(^P1$)|(^P2$)|(^P3$)", message = "必须为P0、P1、P2、P3")
|
||||
private String priority;
|
||||
|
||||
@NotBlank
|
||||
@ExcelProperty("测试方式")
|
||||
@Pattern(regexp = "(^manual$)|(^auto$)", message = "必须为manual、auto")
|
||||
private String method;
|
||||
|
||||
@ColumnWidth(50)
|
||||
@ExcelProperty("前置条件")
|
||||
@Length(min=0, max=1000)
|
||||
private String prerequisite;
|
||||
|
||||
@ColumnWidth(50)
|
||||
@ExcelProperty("备注")
|
||||
@Length(max=1000)
|
||||
private String remark;
|
||||
|
||||
@ColumnWidth(50)
|
||||
@ExcelProperty("步骤描述")
|
||||
@Length(max=1000)
|
||||
private String stepDesc;
|
||||
|
||||
@ColumnWidth(50)
|
||||
@ExcelProperty("预期结果")
|
||||
@Length(max=1000)
|
||||
private String stepResult;
|
||||
|
||||
public String getNodePath() {
|
||||
return nodePath;
|
||||
}
|
||||
|
||||
public void setNodePath(String nodePath) {
|
||||
this.nodePath = nodePath;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(String type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public String getMaintainer() {
|
||||
return maintainer;
|
||||
}
|
||||
|
||||
public void setMaintainer(String maintainer) {
|
||||
this.maintainer = maintainer;
|
||||
}
|
||||
|
||||
public String getPriority() {
|
||||
return priority;
|
||||
}
|
||||
|
||||
public void setPriority(String priority) {
|
||||
this.priority = priority;
|
||||
}
|
||||
|
||||
public String getMethod() {
|
||||
return method;
|
||||
}
|
||||
|
||||
public void setMethod(String method) {
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public String getPrerequisite() {
|
||||
return prerequisite;
|
||||
}
|
||||
|
||||
public void setPrerequisite(String prerequisite) {
|
||||
this.prerequisite = prerequisite;
|
||||
}
|
||||
|
||||
public String getRemark() {
|
||||
return remark;
|
||||
}
|
||||
|
||||
public void setRemark(String remark) {
|
||||
this.remark = remark;
|
||||
}
|
||||
|
||||
public String getStepDesc() {
|
||||
return stepDesc;
|
||||
}
|
||||
|
||||
public void setStepDesc(String stepDesc) {
|
||||
this.stepDesc = stepDesc;
|
||||
}
|
||||
|
||||
public String getStepResult() {
|
||||
return stepResult;
|
||||
}
|
||||
|
||||
public void setStepResult(String stepResult) {
|
||||
this.stepResult = stepResult;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
package io.metersphere.excel.listener;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
import com.alibaba.excel.context.AnalysisContext;
|
||||
import com.alibaba.excel.event.AnalysisEventListener;
|
||||
import com.alibaba.excel.exception.ExcelAnalysisException;
|
||||
import com.alibaba.excel.util.StringUtils;
|
||||
import io.metersphere.commons.utils.LogUtil;
|
||||
import io.metersphere.excel.utils.ExcelValidateHelper;
|
||||
import io.metersphere.excel.domain.ExcelErrData;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.*;
|
||||
|
||||
|
||||
public abstract class EasyExcelListener <T> extends AnalysisEventListener<T> {
|
||||
|
||||
protected List<ExcelErrData<T>> errList = new ArrayList<>();
|
||||
|
||||
protected List<T> list = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 每隔2000条存储数据库,然后清理list ,方便内存回收
|
||||
*/
|
||||
protected static final int BATCH_COUNT = 2000;
|
||||
|
||||
protected Class<T> clazz;
|
||||
|
||||
|
||||
public EasyExcelListener(Class<T> clazz){
|
||||
this.clazz = clazz;
|
||||
}
|
||||
|
||||
/**
|
||||
* 这个每一条数据解析都会来调用
|
||||
*
|
||||
* @param t
|
||||
* @param analysisContext
|
||||
*/
|
||||
@Override
|
||||
public void invoke(T t, AnalysisContext analysisContext) {
|
||||
String errMsg;
|
||||
Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
|
||||
try {
|
||||
//根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
|
||||
errMsg = ExcelValidateHelper.validateEntity(t);
|
||||
//自定义校验规则
|
||||
errMsg = validate(t, errMsg);
|
||||
} catch (NoSuchFieldException e) {
|
||||
errMsg = "解析数据出错";
|
||||
LogUtil.error(e.getMessage(), e);
|
||||
}
|
||||
|
||||
if (!StringUtils.isEmpty(errMsg)) {
|
||||
ExcelErrData excelErrData = new ExcelErrData(t, rowIndex, "第" + rowIndex + "行出错:" + errMsg);
|
||||
errList.add(excelErrData);
|
||||
} else {
|
||||
list.add(t);
|
||||
}
|
||||
|
||||
if (list.size() > BATCH_COUNT) {
|
||||
saveData();
|
||||
list.clear();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 可重写该方法
|
||||
* 自定义校验规则
|
||||
* @param data
|
||||
* @param errMsg
|
||||
* @return
|
||||
*/
|
||||
public String validate(T data, String errMsg) {
|
||||
return errMsg;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义数据保存操作
|
||||
*/
|
||||
public abstract void saveData();
|
||||
|
||||
@Override
|
||||
public void doAfterAllAnalysed(AnalysisContext analysisContext) {
|
||||
saveData();
|
||||
list.clear();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 校验excel头部
|
||||
* @param headMap 传入excel的头部(第一行数据)数据的index,name
|
||||
* @param context
|
||||
*/
|
||||
@Override
|
||||
public void invokeHeadMap(Map<Integer, String> headMap, AnalysisContext context) {
|
||||
super.invokeHeadMap(headMap, context);
|
||||
if (clazz != null){
|
||||
try {
|
||||
Set<String> fieldNameSet = getFieldNameSet(clazz);
|
||||
Collection<String> values = headMap.values();
|
||||
for (String key : fieldNameSet) {
|
||||
if (!values.contains(key)){
|
||||
throw new ExcelAnalysisException("缺少头部信息:" + key);
|
||||
}
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @description: 获取注解里ExcelProperty的value
|
||||
*/
|
||||
public Set<String> getFieldNameSet(Class clazz) throws NoSuchFieldException {
|
||||
Set<String> result = new HashSet<>();
|
||||
Field field;
|
||||
Field[] fields = clazz.getDeclaredFields();
|
||||
for (int i = 0; i < fields.length ; i++) {
|
||||
field = clazz.getDeclaredField(fields[i].getName());
|
||||
field.setAccessible(true);
|
||||
ExcelProperty excelProperty = field.getAnnotation(ExcelProperty.class);
|
||||
if(excelProperty != null){
|
||||
StringBuilder value = new StringBuilder();
|
||||
for (String v : excelProperty.value()) {
|
||||
value.append(v);
|
||||
}
|
||||
result.add(value.toString());
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
public List<ExcelErrData<T>> getErrList() {
|
||||
return errList;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package io.metersphere.excel.listener;
|
||||
|
||||
import com.alibaba.fastjson.JSONArray;
|
||||
import com.alibaba.fastjson.JSONObject;
|
||||
import io.metersphere.excel.domain.TestCaseExcelData;
|
||||
import io.metersphere.base.domain.TestCaseWithBLOBs;
|
||||
import io.metersphere.commons.constants.TestCaseConstants;
|
||||
import io.metersphere.commons.utils.BeanUtils;
|
||||
import io.metersphere.service.TestCaseService;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class TestCaseDataListener extends EasyExcelListener<TestCaseExcelData> {
|
||||
|
||||
private TestCaseService testCaseService;
|
||||
|
||||
private String projectId;
|
||||
|
||||
Set<String> testCaseNames;
|
||||
|
||||
Set<String> userNames;
|
||||
|
||||
public TestCaseDataListener(TestCaseService testCaseService, String projectId,
|
||||
Set<String> testCaseNames, Set<String> userNames, Class<TestCaseExcelData> clazz) {
|
||||
super(clazz);
|
||||
this.testCaseService = testCaseService;
|
||||
this.projectId = projectId;
|
||||
this.testCaseNames = testCaseNames;
|
||||
this.userNames = userNames;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String validate(TestCaseExcelData data, String errMsg) {
|
||||
String nodePath = data.getNodePath();
|
||||
StringBuilder stringBuilder = new StringBuilder(errMsg);
|
||||
if ( nodePath.split("/").length > TestCaseConstants.MAX_NODE_DEPTH + 1) {
|
||||
stringBuilder.append("节点最多为" + TestCaseConstants.MAX_NODE_DEPTH + "层;");
|
||||
}
|
||||
if ( nodePath.trim().contains(" ")) {
|
||||
stringBuilder.append("所属模块不能包含空格");
|
||||
}
|
||||
if (!userNames.contains(data.getMaintainer())) {
|
||||
stringBuilder.append("该工作空间下无该用户:" + data.getMaintainer() + ";");
|
||||
}
|
||||
if (testCaseNames.contains(data.getName())) {
|
||||
stringBuilder.append("该项目下已存在该测试用例:" + data.getName() + ";");
|
||||
}
|
||||
return stringBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveData() {
|
||||
|
||||
//无错误数据才插入数据
|
||||
if (!errList.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<TestCaseWithBLOBs> result = list.stream()
|
||||
.map(item -> this.convert2TestCase(item))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
testCaseService.saveImportData(result, projectId);
|
||||
|
||||
}
|
||||
|
||||
|
||||
private TestCaseWithBLOBs convert2TestCase(TestCaseExcelData data) {
|
||||
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
|
||||
BeanUtils.copyBean(testCase, data);
|
||||
testCase.setId(UUID.randomUUID().toString());
|
||||
testCase.setProjectId(this.projectId);
|
||||
testCase.setCreateTime(System.currentTimeMillis());
|
||||
testCase.setUpdateTime(System.currentTimeMillis());
|
||||
String nodePath = data.getNodePath();
|
||||
|
||||
if (!nodePath.startsWith("/")) {
|
||||
nodePath = "/" + nodePath;
|
||||
}
|
||||
if (nodePath.endsWith("/")) {
|
||||
nodePath = nodePath.substring(0, nodePath.length() - 1);
|
||||
}
|
||||
|
||||
testCase.setNodePath(nodePath);
|
||||
|
||||
|
||||
JSONArray jsonArray = new JSONArray();
|
||||
|
||||
String[] stepDesc = new String[0];
|
||||
String[] stepRes = new String[0];
|
||||
|
||||
if (data.getStepDesc() != null) {
|
||||
stepDesc = data.getStepDesc().split("\n");
|
||||
}
|
||||
if (data.getStepResult() != null) {
|
||||
stepRes = data.getStepResult().split("\n");
|
||||
}
|
||||
|
||||
String pattern = "(^\\d+)(\\.)?";
|
||||
int index = stepDesc.length > stepRes.length ? stepDesc.length : stepRes.length;
|
||||
|
||||
for (int i = 0; i < index; i++){
|
||||
|
||||
JSONObject step = new JSONObject();
|
||||
step.put("num", i + 1);
|
||||
|
||||
Pattern descPattern = Pattern.compile(pattern);
|
||||
Pattern resPattern = Pattern.compile(pattern);
|
||||
|
||||
if (i < stepDesc.length) {
|
||||
Matcher descMatcher = descPattern.matcher(stepDesc[i]);
|
||||
if (descMatcher.find()) {
|
||||
step.put("desc", descMatcher.replaceAll(""));
|
||||
} else {
|
||||
step.put("desc", stepDesc[i]);
|
||||
}
|
||||
}
|
||||
|
||||
if (i < stepRes.length) {
|
||||
Matcher resMatcher = resPattern.matcher(stepRes[i]);
|
||||
if (resMatcher.find()) {
|
||||
step.put("result", resMatcher.replaceAll(""));
|
||||
} else {
|
||||
step.put("result", stepRes[i]);
|
||||
}
|
||||
}
|
||||
|
||||
jsonArray.add(step);
|
||||
}
|
||||
|
||||
testCase.setSteps(jsonArray.toJSONString());
|
||||
|
||||
return testCase;
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package io.metersphere.excel.utils;
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import io.metersphere.commons.utils.LogUtil;
|
||||
import io.metersphere.exception.ExcelException;
|
||||
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.List;
|
||||
|
||||
public class EasyExcelUtil {
|
||||
|
||||
public static void export(HttpServletResponse response, Class clazz, List data, String fileName, String sheetName) {
|
||||
response.setContentType("application/vnd.ms-excel");
|
||||
response.setCharacterEncoding("utf-8");
|
||||
try {
|
||||
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(fileName, "UTF-8") + ".xlsx");
|
||||
EasyExcel.write(response.getOutputStream(), clazz).sheet(sheetName).doWrite(data);
|
||||
} catch (UnsupportedEncodingException e) {
|
||||
LogUtil.error(e.getMessage(), e);
|
||||
throw new ExcelException("不支持UTF-8编码");
|
||||
} catch (IOException e) {
|
||||
LogUtil.error(e.getMessage(), e);
|
||||
throw new ExcelException("IO异常");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package io.metersphere.excel.utils;
|
||||
|
||||
import com.alibaba.excel.annotation.ExcelProperty;
|
||||
|
||||
import javax.validation.ConstraintViolation;
|
||||
import javax.validation.Validation;
|
||||
import javax.validation.Validator;
|
||||
import javax.validation.groups.Default;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Set;
|
||||
|
||||
|
||||
public class ExcelValidateHelper {
|
||||
|
||||
private ExcelValidateHelper(){}
|
||||
|
||||
private static Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
|
||||
public static <T> String validateEntity(T obj) throws NoSuchFieldException {
|
||||
StringBuilder result = new StringBuilder();
|
||||
Set<ConstraintViolation<T>> set = validator.validate(obj, Default.class);
|
||||
if (set != null && !set.isEmpty()) {
|
||||
for (ConstraintViolation<T> cv : set) {
|
||||
Field declaredField = obj.getClass().getDeclaredField(cv.getPropertyPath().toString());
|
||||
ExcelProperty annotation = declaredField.getAnnotation(ExcelProperty.class);
|
||||
//拼接错误信息,包含当前出错数据的标题名字+错误信息
|
||||
result.append(annotation.value()[0]+cv.getMessage()).append(";");
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package io.metersphere.exception;
|
||||
|
||||
/**
|
||||
* @author jianxing.chen
|
||||
*/
|
||||
public class ExcelException extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
public ExcelException(String message, Exception e){
|
||||
super(message, e);
|
||||
}
|
||||
|
||||
public ExcelException(String message){
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
|
@ -6,8 +6,11 @@ import io.metersphere.base.mapper.TestCaseMapper;
|
|||
import io.metersphere.base.mapper.TestCaseNodeMapper;
|
||||
import io.metersphere.base.mapper.TestPlanMapper;
|
||||
import io.metersphere.base.mapper.TestPlanTestCaseMapper;
|
||||
import io.metersphere.commons.constants.TestCaseConstants;
|
||||
import io.metersphere.commons.utils.BeanUtils;
|
||||
import io.metersphere.dto.TestCaseNodeDTO;
|
||||
import io.metersphere.exception.ExcelException;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
|
@ -30,8 +33,8 @@ public class TestCaseNodeService {
|
|||
|
||||
public int addNode(TestCaseNode node) {
|
||||
|
||||
if(node.getLevel() > 5){
|
||||
throw new RuntimeException("模块树最大深度为5层!");
|
||||
if(node.getLevel() > TestCaseConstants.MAX_NODE_DEPTH){
|
||||
throw new RuntimeException("模块树最大深度为" + TestCaseConstants.MAX_NODE_DEPTH + "层!");
|
||||
}
|
||||
node.setCreateTime(System.currentTimeMillis());
|
||||
node.setUpdateTime(System.currentTimeMillis());
|
||||
|
@ -196,4 +199,140 @@ public class TestCaseNodeService {
|
|||
TestPlan testPlan = testPlanMapper.selectByPrimaryKey(planId);
|
||||
return getNodeTreeByProjectId(testPlan.getProjectId());
|
||||
}
|
||||
|
||||
public Map<String, Integer> createNodeByTestCases(List<TestCaseWithBLOBs> testCases, String projectId) {
|
||||
|
||||
List<TestCaseNodeDTO> nodeTrees = getNodeTreeByProjectId(projectId);
|
||||
|
||||
Map<String, Integer> pathMap = new HashMap<>();
|
||||
|
||||
List<String> nodePaths = testCases.stream()
|
||||
.map(TestCase::getNodePath)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
nodePaths.forEach(path -> {
|
||||
|
||||
if (path == null) {
|
||||
throw new ExcelException("所属模块不能为空!");
|
||||
}
|
||||
List<String> nodeNameList = new ArrayList<>(Arrays.asList(path.split("/")));
|
||||
Iterator<String> pathIterator = nodeNameList.iterator();
|
||||
|
||||
Boolean hasNode = false;
|
||||
String rootNodeName = null;
|
||||
|
||||
if (nodeNameList.size() <= 1) {
|
||||
throw new ExcelException("创建模块失败:" + path);
|
||||
} else {
|
||||
pathIterator.next();
|
||||
pathIterator.remove();
|
||||
|
||||
rootNodeName = pathIterator.next().trim();
|
||||
//原来没有,新建的树nodeTrees也不包含
|
||||
for (TestCaseNodeDTO nodeTree : nodeTrees) {
|
||||
if (StringUtils.equals(rootNodeName, nodeTree.getName())) {
|
||||
hasNode = true;
|
||||
createNodeByPathIterator(pathIterator, "/" + rootNodeName, nodeTree,
|
||||
pathMap, projectId, 2);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (!hasNode) {
|
||||
createNodeByPath(pathIterator, rootNodeName, null, projectId, 1, "", pathMap);
|
||||
}
|
||||
});
|
||||
|
||||
return pathMap;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据目标节点路径,创建相关节点
|
||||
* @param pathIterator 遍历子路径
|
||||
* @param path 当前路径
|
||||
* @param treeNode 当前节点
|
||||
* @param pathMap 记录节点路径对应的nodeId
|
||||
*/
|
||||
private void createNodeByPathIterator(Iterator<String> pathIterator, String path, TestCaseNodeDTO treeNode,
|
||||
Map<String, Integer> pathMap, String projectId, Integer level) {
|
||||
|
||||
List<TestCaseNodeDTO> children = treeNode.getChildren();
|
||||
|
||||
if (children == null || children.isEmpty() || !pathIterator.hasNext()) {
|
||||
pathMap.put(path , treeNode.getId());
|
||||
if (pathIterator.hasNext()) {
|
||||
createNodeByPath(pathIterator, pathIterator.next().trim(), treeNode, projectId, level, path, pathMap);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
String nodeName = pathIterator.next().trim();
|
||||
|
||||
Boolean hasNode = false;
|
||||
|
||||
for (TestCaseNodeDTO child : children) {
|
||||
if (StringUtils.equals(nodeName, child.getName())) {
|
||||
hasNode = true;
|
||||
createNodeByPathIterator(pathIterator, path + "/" + child.getName(),
|
||||
child, pathMap, projectId, level + 1);
|
||||
};
|
||||
}
|
||||
|
||||
//若子节点中不包含该目标节点,则在该节点下创建
|
||||
if (!hasNode) {
|
||||
createNodeByPath(pathIterator, nodeName, treeNode, projectId, level, path, pathMap);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param pathIterator 迭代器,遍历子节点
|
||||
* @param nodeName 当前节点
|
||||
* @param pNode 父节点
|
||||
*/
|
||||
private void createNodeByPath(Iterator<String> pathIterator, String nodeName,
|
||||
TestCaseNodeDTO pNode, String projectId, Integer level,
|
||||
String rootPath, Map<String, Integer> pathMap) {
|
||||
|
||||
StringBuilder path = new StringBuilder(rootPath);
|
||||
|
||||
path.append("/" + nodeName);
|
||||
|
||||
Integer pid = null;
|
||||
//创建过不创建
|
||||
if (pathMap.get(path.toString()) != null) {
|
||||
pid = pathMap.get(path.toString());
|
||||
level++;
|
||||
} else {
|
||||
pid = insertTestCaseNode(nodeName, pNode == null ? null : pNode.getId(), projectId, level);
|
||||
pathMap.put(path.toString(), pid);
|
||||
}
|
||||
|
||||
while (pathIterator.hasNext()) {
|
||||
String nextNodeName = pathIterator.next();
|
||||
path.append("/" + nextNodeName);
|
||||
if (pathMap.get(path.toString()) != null) {
|
||||
pid = pathMap.get(path.toString());
|
||||
level++;
|
||||
} else {
|
||||
pid = insertTestCaseNode(nextNodeName, pid, projectId, level);
|
||||
pathMap.put(path.toString(), pid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Integer insertTestCaseNode(String nodeName, Integer pId, String projectId, Integer level) {
|
||||
TestCaseNode testCaseNode = new TestCaseNode();
|
||||
testCaseNode.setName(nodeName.trim());
|
||||
testCaseNode.setpId(pId);
|
||||
testCaseNode.setProjectId(projectId);
|
||||
testCaseNode.setCreateTime(System.currentTimeMillis());
|
||||
testCaseNode.setUpdateTime(System.currentTimeMillis());
|
||||
testCaseNode.setLevel(level);
|
||||
testCaseNodeMapper.insert(testCaseNode);
|
||||
return testCaseNode.getId();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,36 @@
|
|||
package io.metersphere.service;
|
||||
|
||||
|
||||
import com.alibaba.excel.EasyExcel;
|
||||
import com.alibaba.excel.EasyExcelFactory;
|
||||
import com.alibaba.fastjson.JSON;
|
||||
import com.github.pagehelper.PageHelper;
|
||||
import io.metersphere.base.domain.*;
|
||||
import io.metersphere.base.mapper.ProjectMapper;
|
||||
import io.metersphere.base.mapper.TestCaseMapper;
|
||||
import io.metersphere.base.mapper.TestPlanMapper;
|
||||
import io.metersphere.base.mapper.TestPlanTestCaseMapper;
|
||||
import io.metersphere.base.mapper.*;
|
||||
import io.metersphere.base.mapper.ext.ExtTestCaseMapper;
|
||||
import io.metersphere.commons.utils.LogUtil;
|
||||
import io.metersphere.controller.request.testcase.QueryTestCaseRequest;
|
||||
import io.metersphere.dto.TestPlanCaseDTO;
|
||||
import io.metersphere.excel.domain.ExcelErrData;
|
||||
import io.metersphere.excel.domain.ExcelResponse;
|
||||
import io.metersphere.excel.domain.TestCaseExcelData;
|
||||
import io.metersphere.excel.listener.EasyExcelListener;
|
||||
import io.metersphere.excel.listener.TestCaseDataListener;
|
||||
import io.metersphere.excel.utils.EasyExcelUtil;
|
||||
import io.metersphere.user.SessionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.apache.ibatis.session.ExecutorType;
|
||||
import org.apache.ibatis.session.SqlSession;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.net.URLEncoder;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Service
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
|
@ -41,6 +51,15 @@ public class TestCaseService {
|
|||
@Resource
|
||||
ProjectMapper projectMapper;
|
||||
|
||||
@Resource
|
||||
SqlSessionFactory sqlSessionFactory;
|
||||
|
||||
@Resource
|
||||
TestCaseNodeService testCaseNodeService;
|
||||
|
||||
@Resource
|
||||
UserMapper userMapper;
|
||||
|
||||
public void addTestCase(TestCaseWithBLOBs testCase) {
|
||||
testCase.setId(UUID.randomUUID().toString());
|
||||
testCase.setCreateTime(System.currentTimeMillis());
|
||||
|
@ -144,4 +163,97 @@ public class TestCaseService {
|
|||
}
|
||||
return projectMapper.selectByPrimaryKey(testCaseWithBLOBs.getProjectId());
|
||||
}
|
||||
|
||||
public ExcelResponse testCaseImport(MultipartFile file, String projectId) {
|
||||
|
||||
try {
|
||||
|
||||
ExcelResponse excelResponse = new ExcelResponse();
|
||||
|
||||
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
|
||||
QueryTestCaseRequest queryTestCaseRequest = new QueryTestCaseRequest();
|
||||
queryTestCaseRequest.setProjectId(projectId);
|
||||
List<TestCase> testCases = extTestCaseMapper.getTestCaseNames(queryTestCaseRequest);
|
||||
Set<String> testCaseNames = testCases.stream()
|
||||
.map(TestCase::getName)
|
||||
.collect(Collectors.toSet());
|
||||
|
||||
UserExample userExample = new UserExample();
|
||||
userExample.createCriteria().andLastWorkspaceIdEqualTo(currentWorkspaceId);
|
||||
List<User> users = userMapper.selectByExample(userExample);
|
||||
Set<String> userNames = users.stream().map(User::getName).collect(Collectors.toSet());
|
||||
|
||||
EasyExcelListener easyExcelListener = new TestCaseDataListener(this, projectId,
|
||||
testCaseNames, userNames, TestCaseExcelData.class);
|
||||
EasyExcelFactory.read(file.getInputStream(), TestCaseExcelData.class, easyExcelListener).sheet().doRead();
|
||||
|
||||
List<ExcelErrData<TestCaseExcelData>> errList = easyExcelListener.getErrList();
|
||||
//如果包含错误信息就导出错误信息
|
||||
if (!errList.isEmpty()) {
|
||||
excelResponse.setSuccess(false);
|
||||
excelResponse.setErrList(errList);
|
||||
} else {
|
||||
excelResponse.setSuccess(true);
|
||||
}
|
||||
return excelResponse;
|
||||
|
||||
} catch (IOException e) {
|
||||
LogUtil.error(e.getMessage(), e);
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public void saveImportData(List<TestCaseWithBLOBs> testCases, String projectId) {
|
||||
|
||||
Map<String, Integer> nodePathMap = testCaseNodeService.createNodeByTestCases(testCases, projectId);
|
||||
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
|
||||
TestCaseMapper mapper = sqlSession.getMapper(TestCaseMapper.class);
|
||||
if (!testCases.isEmpty()) {
|
||||
testCases.forEach(testcase -> {
|
||||
testcase.setNodeId(nodePathMap.get(testcase.getNodePath()));
|
||||
mapper.insert(testcase);
|
||||
});
|
||||
}
|
||||
sqlSession.flushStatements();
|
||||
}
|
||||
|
||||
public void testCaseTemplateExport(HttpServletResponse response) {
|
||||
EasyExcelUtil.export(response, TestCaseExcelData.class, generateExportTemplate(), "测试用例模版", "模版");
|
||||
}
|
||||
|
||||
private List<TestCaseExcelData> generateExportTemplate() {
|
||||
List<TestCaseExcelData> list = new ArrayList<TestCaseExcelData>();
|
||||
StringBuilder path = new StringBuilder("");
|
||||
List<String> types = Arrays.asList("functional", "performance", "api");
|
||||
List<String> methods = Arrays.asList("manual", "auto");
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
TestCaseExcelData data = new TestCaseExcelData();
|
||||
data.setName("测试用例" + i);
|
||||
path.append("/" + "模块" + i);
|
||||
data.setNodePath(path.toString());
|
||||
data.setPriority("P" + i%4);
|
||||
data.setType(types.get(i%3));
|
||||
data.setMethod(methods.get(i%2));
|
||||
data.setPrerequisite("前置条件选填");
|
||||
data.setStepDesc("1. 每个步骤以换行分隔\n2. 步骤前可标序号\n3. 测试步骤和结果选填");
|
||||
data.setStepResult("1. 每条结果以换行分隔\n2. 结果前可标序号\n3. 测试步骤和结果选填");
|
||||
data.setMaintainer("admin");
|
||||
data.setRemark("备注选填");
|
||||
list.add(data);
|
||||
}
|
||||
|
||||
list.add(new TestCaseExcelData());
|
||||
TestCaseExcelData explain = new TestCaseExcelData();
|
||||
explain.setName("同一项目下测试用例名称不能重复!");
|
||||
explain.setNodePath("模块名称请按照'/模块1/模块2'的格式书写; 错误格式示例:('/', '/tes//test'); 若无该模块,则自动创建模块");
|
||||
explain.setType("用例类型必须为:functional、performance、api");
|
||||
explain.setMethod("测试方式必须为:manual、auto");
|
||||
explain.setPriority("优先级必须为:P0、P1、P2、P3");
|
||||
explain.setMaintainer("维护人必须为该工作空间相关人员");
|
||||
|
||||
list.add(explain);
|
||||
return list;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,170 @@
|
|||
<template>
|
||||
<div class="container">
|
||||
<div class="main-content">
|
||||
<el-card>
|
||||
<el-container class="scenario-container">
|
||||
<el-header>
|
||||
<span class="scenario-title">场景配置</span>
|
||||
</el-header>
|
||||
|
||||
<el-container>
|
||||
<el-aside class="scenario-aside">
|
||||
<div class="scenario-list">
|
||||
<ms-api-collapse v-model="activeName" @change="handleChange" accordion>
|
||||
<ms-api-collapse-item v-for="(scenario, index) in scenarios" :key="index"
|
||||
:title="scenario.name" :name="index">
|
||||
<template slot="title">
|
||||
<div class="scenario-name">{{scenario.name}}</div>
|
||||
<el-dropdown trigger="click" @command="handleCommand">
|
||||
<span class="el-dropdown-link el-icon-more scenario-btn"/>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{type:'delete', index:index}">删除场景</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</template>
|
||||
<ms-api-request :requests="scenario.requests" :open="select"/>
|
||||
</ms-api-collapse-item>
|
||||
</ms-api-collapse>
|
||||
</div>
|
||||
<el-button class="scenario-create" type="primary" size="mini" icon="el-icon-plus" plain @click="create"/>
|
||||
</el-aside>
|
||||
|
||||
<el-main class="scenario-main">
|
||||
<div class="scenario-form">
|
||||
<ms-api-scenario-form :scenario="selected"></ms-api-scenario-form>
|
||||
<ms-api-request-form :request="selected"></ms-api-request-form>
|
||||
</div>
|
||||
</el-main>
|
||||
</el-container>
|
||||
</el-container>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import MsApiCollapseItem from "./components/ApiCollapseItem";
|
||||
import MsApiCollapse from "./components/ApiCollapse";
|
||||
import MsApiRequest from "./components/ApiRequest";
|
||||
import MsApiRequestForm from "./components/ApiRequestForm";
|
||||
import MsApiScenarioForm from "./components/ApiScenarioForm";
|
||||
|
||||
export default {
|
||||
name: "MsApiScenarioConfig",
|
||||
|
||||
components: {MsApiScenarioForm, MsApiRequestForm, MsApiRequest, MsApiCollapse, MsApiCollapseItem},
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeName: 0,
|
||||
scenarios: [],
|
||||
selected: Object
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleChange: function (index) {
|
||||
this.select(this.scenarios[index]);
|
||||
},
|
||||
handleCommand: function (command) {
|
||||
switch (command.type) {
|
||||
case "delete":
|
||||
this.deleteScenario(command.index);
|
||||
break;
|
||||
}
|
||||
},
|
||||
createScenario: function () {
|
||||
return {
|
||||
type: "Scenario",
|
||||
name: "Scenario",
|
||||
address: "",
|
||||
file: "",
|
||||
variables: [],
|
||||
headers: [],
|
||||
requests: []
|
||||
}
|
||||
},
|
||||
deleteScenario: function (index) {
|
||||
this.scenarios.splice(index, 1);
|
||||
if (this.scenarios.length === 0) {
|
||||
this.create();
|
||||
}
|
||||
},
|
||||
create: function () {
|
||||
let scenario = this.createScenario();
|
||||
this.scenarios.push(scenario);
|
||||
},
|
||||
select: function (obj) {
|
||||
this.selected = obj;
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.scenarios.length === 0) {
|
||||
this.create();
|
||||
this.select(this.scenarios[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scenario-container {
|
||||
height: calc(100vh - 150px);
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
.scenario-title {
|
||||
font-size: 16px;
|
||||
margin-left: -20px;
|
||||
}
|
||||
|
||||
.scenario-aside {
|
||||
position: relative;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #EBEEF5;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.scenario-list {
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 28px;
|
||||
}
|
||||
|
||||
.scenario-name {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scenario-btn {
|
||||
text-align: center;
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.scenario-create {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scenario-main {
|
||||
position: relative;
|
||||
margin-left: 20px;
|
||||
border: 1px solid #EBEEF5;
|
||||
}
|
||||
|
||||
.scenario-form {
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
</style>
|
|
@ -1,242 +0,0 @@
|
|||
<template>
|
||||
<div class="edit-testplan-container" >
|
||||
<div class="main-content">
|
||||
<el-card>
|
||||
<el-row>
|
||||
<el-col :span="10">
|
||||
<el-input :placeholder="$t('load_test.input_name')" v-model="testPlan.name" class="input-with-select">
|
||||
<template v-slot:prepend>
|
||||
<el-select v-model="testPlan.projectId" :placeholder="$t('load_test.select_project')">
|
||||
<el-option
|
||||
v-for="item in projects"
|
||||
:key="item.id"
|
||||
:label="item.name"
|
||||
:value="item.id">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</template>
|
||||
</el-input>
|
||||
</el-col>
|
||||
<el-button type="primary" plain @click="save">{{$t('commons.save')}}</el-button>
|
||||
<el-button type="primary" plain @click="saveAndRun">{{$t('load_test.save_and_run')}}</el-button>
|
||||
<el-button type="warning" plain @click="cancel">{{$t('commons.cancel')}}</el-button>
|
||||
</el-row>
|
||||
|
||||
<el-tabs class="testplan-config" v-model="active" type="border-card" :stretch="true">
|
||||
<el-tab-pane :label="$t('load_test.basic_config')">
|
||||
<api-test-scene-config :test-plan="testPlan" />
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('load_test.runtime_config')">
|
||||
<api-test-runtime-config :test-plan="testPlan" ref="runtimeConfig"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-card>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ApiTestSceneConfig from './components/ApiTestSceneConfig';
|
||||
import ApiTestRuntimeConfig from './components/ApiTestRuntimeConfig';
|
||||
|
||||
export default {
|
||||
name: "EditApiTest",
|
||||
data() {
|
||||
return {
|
||||
result: {},
|
||||
testPlan: {},
|
||||
listProjectPath: "/project/listAll",
|
||||
savePath: "/api/save",
|
||||
editPath: "/api/edit",
|
||||
runPath: "/api/run",
|
||||
projects: [],
|
||||
active: '0',
|
||||
tabs: [{
|
||||
title: this.$t('load_test.basic_config'),
|
||||
id: '0',
|
||||
component: 'ApiTestSceneConfig'
|
||||
}, {
|
||||
title: this.$t('load_test.runtime_config'),
|
||||
id: '1',
|
||||
component: 'ApiTestRuntimeConfig'
|
||||
}]
|
||||
}
|
||||
},
|
||||
components: {
|
||||
ApiTestSceneConfig,
|
||||
ApiTestRuntimeConfig,
|
||||
},
|
||||
watch: {
|
||||
'$route'(to) {
|
||||
// 如果是创建测试
|
||||
if (to.name === 'createFucTest') {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
let testId = to.path.split('/')[4]; // find testId
|
||||
if (testId) {
|
||||
this.$get('/api/get/' + testId, response => {
|
||||
this.testPlan = response.data;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
created() {
|
||||
let testId = this.$route.path.split('/')[4];
|
||||
if (testId) {
|
||||
this.$get('/api/get/' + testId, response => {
|
||||
this.testPlan = response.data;
|
||||
});
|
||||
}
|
||||
|
||||
this.listProjects();
|
||||
},
|
||||
methods: {
|
||||
listProjects() {
|
||||
this.result = this.$get(this.listProjectPath, response => {
|
||||
this.projects = response.data;
|
||||
})
|
||||
},
|
||||
save() {
|
||||
if (!this.validTestPlan()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let options = this.getSaveOption();
|
||||
|
||||
this.result = this.$request(options, () => {
|
||||
this.$message({
|
||||
message: this.$t('commons.save_success'),
|
||||
type: 'success'
|
||||
});
|
||||
this.$refs.runtimeConfig.cancelAllEdit();
|
||||
this.$router.push({path: '/api/test/all'})
|
||||
});
|
||||
},
|
||||
saveAndRun() {
|
||||
if (!this.validTestPlan()) {
|
||||
return;
|
||||
}
|
||||
|
||||
let options = this.getSaveOption();
|
||||
|
||||
this.result = this.$request(options, (response) => {
|
||||
this.testPlan.id = response.data;
|
||||
this.$message({
|
||||
message: this.$t('commons.save_success'),
|
||||
type: 'success'
|
||||
});
|
||||
|
||||
this.result = this.$post(this.runPath, {id: this.testPlan.id}, () => {
|
||||
this.$message({
|
||||
message: this.$t('load_test.is_running'),
|
||||
type: 'success'
|
||||
});
|
||||
})
|
||||
});
|
||||
},
|
||||
getSaveOption() {
|
||||
let formData = new FormData();
|
||||
let url = this.testPlan.id ? this.editPath : this.savePath;
|
||||
|
||||
if (!this.testPlan.file.id) {
|
||||
formData.append("file", this.testPlan.file);
|
||||
}
|
||||
|
||||
this.testPlan.runtimeConfiguration = JSON.stringify(this.$refs.runtimeConfig.configurations());
|
||||
|
||||
// file属性不需要json化
|
||||
let requestJson = JSON.stringify(this.testPlan, function (key, value) {
|
||||
return key === "file" ? undefined : value
|
||||
});
|
||||
|
||||
formData.append('request', new Blob([requestJson], {
|
||||
type: "application/json"
|
||||
}));
|
||||
|
||||
return {
|
||||
method: 'POST',
|
||||
url: url,
|
||||
data: formData,
|
||||
headers: {
|
||||
'Content-Type': undefined
|
||||
}
|
||||
};
|
||||
},
|
||||
cancel() {
|
||||
this.$router.push({path: '/api/test/all'})
|
||||
},
|
||||
validTestPlan() {
|
||||
if (!this.testPlan.name) {
|
||||
this.$message({
|
||||
message: this.$t('load_test.test_name_is_null'),
|
||||
type: 'error'
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.testPlan.projectId) {
|
||||
this.$message({
|
||||
message: this.$t('load_test.project_is_null'),
|
||||
type: 'error'
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.testPlan.file) {
|
||||
this.$message({
|
||||
message: this.$t('load_test.jmx_is_null'),
|
||||
type: 'error'
|
||||
});
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!this.$refs.runtimeConfig.validConfig()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/// todo: 其他校验
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.edit-testplan-container {
|
||||
float: none;
|
||||
text-align: center;
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.edit-testplan-container .main-content {
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
}
|
||||
|
||||
.edit-testplan-container .testplan-config {
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.el-select {
|
||||
min-width: 130px;
|
||||
}
|
||||
|
||||
.edit-testplan-container .input-with-select .el-input-group__prepend {
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.advanced-config {
|
||||
height: calc(100vh - 280px);
|
||||
overflow: auto;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,47 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-radio-group v-model="body.type" size="mini">
|
||||
<el-radio-button label="kv">
|
||||
{{$t('api_test.request.body_kv')}}
|
||||
</el-radio-button>
|
||||
<el-radio-button label="text">
|
||||
{{$t('api_test.request.body_text')}}
|
||||
</el-radio-button>
|
||||
</el-radio-group>
|
||||
|
||||
<ms-api-key-value :items="body.kvs" v-if="isKV"/>
|
||||
|
||||
<el-input class="textarea" type="textarea" v-model="body.text" :autosize="{ minRows: 10, maxRows: 25}" resize="none"
|
||||
v-else/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiKeyValue from "./ApiKeyValue";
|
||||
|
||||
export default {
|
||||
name: "MsApiBody",
|
||||
components: {MsApiKeyValue},
|
||||
props: {
|
||||
body: Object
|
||||
},
|
||||
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
|
||||
methods: {},
|
||||
|
||||
computed: {
|
||||
isKV() {
|
||||
return this.body.type === "kv";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.textarea {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,70 @@
|
|||
<template>
|
||||
<div role="tablist" aria-multiselectable="true">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
name: 'MsApiCollapse',
|
||||
|
||||
componentName: 'MsApiCollapse',
|
||||
|
||||
props: {
|
||||
accordion: Boolean,
|
||||
value: {
|
||||
type: [Array, String, Number],
|
||||
default() {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeNames: [].concat(this.value)
|
||||
};
|
||||
},
|
||||
|
||||
provide() {
|
||||
return {
|
||||
collapse: this
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
value(value) {
|
||||
this.activeNames = [].concat(value);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
setActiveNames(activeNames) {
|
||||
activeNames = [].concat(activeNames);
|
||||
let value = this.accordion ? activeNames[0] : activeNames;
|
||||
this.activeNames = activeNames;
|
||||
this.$emit('input', value);
|
||||
this.$emit('change', value);
|
||||
},
|
||||
handleItemClick(item) {
|
||||
if (this.accordion) {
|
||||
this.setActiveNames(
|
||||
(this.activeNames[0] || this.activeNames[0] === 0) && item.name);
|
||||
} else {
|
||||
let activeNames = this.activeNames.slice(0);
|
||||
let index = activeNames.indexOf(item.name);
|
||||
|
||||
if (index > -1) {
|
||||
activeNames.splice(index, 1);
|
||||
} else {
|
||||
activeNames.push(item.name);
|
||||
}
|
||||
this.setActiveNames(activeNames);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
this.$on('item-click', this.handleItemClick);
|
||||
}
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,125 @@
|
|||
<template>
|
||||
<div class="el-collapse-item"
|
||||
:class="{'is-active': isActive, 'is-disabled': disabled }">
|
||||
<div
|
||||
role="tab"
|
||||
:aria-expanded="isActive"
|
||||
:aria-controls="`el-collapse-content-${id}`"
|
||||
:aria-describedby="`el-collapse-content-${id}`"
|
||||
>
|
||||
<div
|
||||
class="el-collapse-item__header"
|
||||
@click="handleHeaderClick"
|
||||
role="button"
|
||||
:id="`el-collapse-head-${id}`"
|
||||
:tabindex="disabled ? undefined : 0"
|
||||
@keyup.space.enter.stop="handleEnterClick"
|
||||
:class="{
|
||||
'focusing': focusing,
|
||||
'is-active': isActive
|
||||
}"
|
||||
@focus="handleFocus"
|
||||
@blur="focusing = false"
|
||||
>
|
||||
<i
|
||||
class="el-collapse-item__arrow el-icon-arrow-right"
|
||||
:class="{'is-active': isActive}">
|
||||
</i>
|
||||
<slot name="title">{{title}}</slot>
|
||||
</div>
|
||||
</div>
|
||||
<el-collapse-transition>
|
||||
<div
|
||||
class="el-collapse-item__wrap"
|
||||
v-show="isActive"
|
||||
role="tabpanel"
|
||||
:aria-hidden="!isActive"
|
||||
:aria-labelledby="`el-collapse-head-${id}`"
|
||||
:id="`el-collapse-content-${id}`"
|
||||
>
|
||||
<div class="el-collapse-item__content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</el-collapse-transition>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import Emitter from 'element-ui/src/mixins/emitter';
|
||||
import {generateId} from 'element-ui/src/utils/util';
|
||||
|
||||
export default {
|
||||
name: 'MsApiCollapseItem',
|
||||
|
||||
componentName: 'MsApiCollapseItem',
|
||||
|
||||
mixins: [Emitter],
|
||||
|
||||
data() {
|
||||
return {
|
||||
contentWrapStyle: {
|
||||
height: 'auto',
|
||||
display: 'block'
|
||||
},
|
||||
contentHeight: 0,
|
||||
focusing: false,
|
||||
isClick: false,
|
||||
id: generateId()
|
||||
};
|
||||
},
|
||||
|
||||
inject: ['collapse'],
|
||||
|
||||
props: {
|
||||
title: String,
|
||||
name: {
|
||||
type: [String, Number],
|
||||
default() {
|
||||
return this._uid;
|
||||
}
|
||||
},
|
||||
disabled: Boolean
|
||||
},
|
||||
|
||||
computed: {
|
||||
isActive() {
|
||||
return this.collapse.activeNames.indexOf(this.name) > -1;
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
handleFocus() {
|
||||
setTimeout(() => {
|
||||
if (!this.isClick) {
|
||||
this.focusing = true;
|
||||
} else {
|
||||
this.isClick = false;
|
||||
}
|
||||
}, 50);
|
||||
},
|
||||
handleHeaderClick() {
|
||||
if (this.disabled) return;
|
||||
this.dispatch('MsApiCollapse', 'item-click', this);
|
||||
this.focusing = false;
|
||||
this.isClick = true;
|
||||
},
|
||||
handleEnterClick() {
|
||||
this.dispatch('MsApiCollapse', 'item-click', this);
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.el-collapse-item__header {
|
||||
padding-left: 7px;
|
||||
}
|
||||
|
||||
.el-collapse-item__header.is-active {
|
||||
background-color: #E9E9E9;
|
||||
}
|
||||
|
||||
.el-collapse-item__content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div>
|
||||
<span class="kv-description" v-if="description">
|
||||
{{description}}
|
||||
</span>
|
||||
<div class="kv-row" v-for="(item, index) in items" :key="index">
|
||||
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
|
||||
<el-col :span="11">
|
||||
<el-input v-model="item.key" placeholder="Key" size="small" maxlength="100" @change="check"/>
|
||||
</el-col>
|
||||
<el-col :span="11">
|
||||
<el-input v-model="item.value" placeholder="Value" size="small" maxlength="100" @change="check"/>
|
||||
</el-col>
|
||||
<el-col :span="1">
|
||||
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"/>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "MsApiKeyValue",
|
||||
|
||||
props: {
|
||||
description: String,
|
||||
items: Array
|
||||
},
|
||||
|
||||
methods: {
|
||||
create: function () {
|
||||
return {
|
||||
key: "",
|
||||
value: ""
|
||||
}
|
||||
},
|
||||
remove: function (index) {
|
||||
this.items.splice(index, 1);
|
||||
if (this.items.length === 0) {
|
||||
this.items.push(this.create());
|
||||
}
|
||||
},
|
||||
check: function () {
|
||||
let isNeedCreate = true;
|
||||
let removeIndex = -1;
|
||||
this.items.forEach((item, index) => {
|
||||
if (item.key === "" && item.value === "") {
|
||||
// 多余的空行
|
||||
if (index !== this.items.length - 1) {
|
||||
removeIndex = index;
|
||||
}
|
||||
// 没有空行,需要创建空行
|
||||
isNeedCreate = false;
|
||||
}
|
||||
});
|
||||
if (isNeedCreate) {
|
||||
this.items.push(this.create());
|
||||
}
|
||||
if (removeIndex !== -1) {
|
||||
this.remove(removeIndex);
|
||||
}
|
||||
// TODO 检查key重复
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.items.length === 0) {
|
||||
this.items.push(this.create());
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.kv-description {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.kv-row {
|
||||
margin-top: 10px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,151 @@
|
|||
<template>
|
||||
<div class="request-container">
|
||||
<div class="request-item" v-for="(request, index) in requests" :key="index" @click="select(request)"
|
||||
:class="{'selected': isSelected(request)}">
|
||||
<span class="request-method">
|
||||
{{request.method}}
|
||||
</span>
|
||||
<span class="request-name">
|
||||
{{request.name}}
|
||||
</span>
|
||||
<span class="request-btn">
|
||||
<el-dropdown trigger="click" @command="handleCommand">
|
||||
<span class="el-dropdown-link el-icon-more"></span>
|
||||
<el-dropdown-menu slot="dropdown">
|
||||
<el-dropdown-item :command="{type: 'copy', index: index}">复制请求</el-dropdown-item>
|
||||
<el-dropdown-item :command="{type: 'delete', index: index}">删除请求</el-dropdown-item>
|
||||
</el-dropdown-menu>
|
||||
</el-dropdown>
|
||||
</span>
|
||||
</div>
|
||||
<el-button class="request-create" type="primary" size="mini" icon="el-icon-plus" plain @click="create"/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {generateId} from 'element-ui/src/utils/util';
|
||||
|
||||
export default {
|
||||
name: "MsApiRequest",
|
||||
props: {
|
||||
requests: Array,
|
||||
open: Function
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
selected: 0
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isSelected() {
|
||||
return function (request) {
|
||||
return this.selected.randomId === request.randomId;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
create: function () {
|
||||
let request = this.createRequest();
|
||||
this.requests.push(request);
|
||||
},
|
||||
handleCommand: function (command) {
|
||||
switch (command.type) {
|
||||
case "copy":
|
||||
this.copyRequest(command.index);
|
||||
break;
|
||||
case "delete":
|
||||
this.deleteRequest(command.index);
|
||||
break;
|
||||
}
|
||||
},
|
||||
copyRequest: function (index) {
|
||||
let request = this.requests[index];
|
||||
this.requests.push(JSON.parse(JSON.stringify(request)));
|
||||
},
|
||||
deleteRequest: function (index) {
|
||||
this.requests.splice(index, 1);
|
||||
if (this.requests.length === 0) {
|
||||
this.create();
|
||||
}
|
||||
},
|
||||
createRequest: function () {
|
||||
return {
|
||||
randomId: generateId(),
|
||||
type: "Request",
|
||||
method: "GET",
|
||||
name: "",
|
||||
parameters: [],
|
||||
headers: [],
|
||||
body: {
|
||||
type: "kv",
|
||||
kvs: [],
|
||||
text: ""
|
||||
},
|
||||
assertions: [],
|
||||
extract: []
|
||||
}
|
||||
},
|
||||
select: function (request) {
|
||||
this.selected = request;
|
||||
this.open(request);
|
||||
}
|
||||
},
|
||||
|
||||
created() {
|
||||
if (this.requests.length === 0) {
|
||||
this.create();
|
||||
this.select(this.requests[0]);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.request-item {
|
||||
border-left: 5px solid #1E90FF;
|
||||
line-height: 40px;
|
||||
max-height: 40px;
|
||||
border-top: 1px solid #EBEEF5;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.request-item:first-child {
|
||||
border-top: 0;
|
||||
}
|
||||
|
||||
.request-item:hover, .request-item.selected:hover {
|
||||
background-color: #ECF5FF;
|
||||
}
|
||||
|
||||
.request-item.selected {
|
||||
background-color: #F5F5F5;
|
||||
}
|
||||
|
||||
.request-method {
|
||||
padding: 0 5px;
|
||||
width: 60px;
|
||||
color: #1E90FF;
|
||||
}
|
||||
|
||||
.request-name {
|
||||
font-size: 14px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.request-btn {
|
||||
float: right;
|
||||
text-align: center;
|
||||
height: 40px;
|
||||
}
|
||||
|
||||
.request-btn .el-icon-more {
|
||||
padding: 13px;
|
||||
}
|
||||
|
||||
.request-create {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,75 @@
|
|||
<template>
|
||||
<el-form :model="request" :rules="rules" ref="request" label-width="100px" label-position="left" v-if="isRequest">
|
||||
<el-form-item :label="$t('api_test.request.name')" prop="name">
|
||||
<el-input v-model="request.name"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('api_test.request.url')" prop="url">
|
||||
<el-input v-model="request.url" :placeholder="$t('api_test.request.url_describe')">
|
||||
<el-select v-model="request.method" slot="prepend" class="request-method-select">
|
||||
<el-option label="GET" value="GET"></el-option>
|
||||
<el-option label="POST" value="POST"></el-option>
|
||||
<el-option label="PUT" value="PUT"></el-option>
|
||||
<el-option label="PATCH" value="PATCH"></el-option>
|
||||
<el-option label="DELETE" value="DELETE"></el-option>
|
||||
<el-option label="OPTIONS" value="OPTIONS"></el-option>
|
||||
<el-option label="HEAD" value="HEAD"></el-option>
|
||||
<el-option label="CONNECT" value="CONNECT"></el-option>
|
||||
</el-select>
|
||||
</el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane :label="$t('api_test.request.parameters')" name="parameters">
|
||||
<ms-api-key-value :items="request.parameters" :description="$t('api_test.request.parameters_desc')"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.headers')" name="headers">
|
||||
<ms-api-key-value :items="request.headers"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.body')" name="body" v-if="isNotGet">
|
||||
<ms-api-body :body="request.body"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.assertions')" name="assertions" v-if="false">
|
||||
TODO
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.request.extract')" name="extract" v-if="false">
|
||||
TODO
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiKeyValue from "./ApiKeyValue";
|
||||
import MsApiBody from "./ApiBody";
|
||||
|
||||
export default {
|
||||
name: "MsApiRequestForm",
|
||||
components: {MsApiBody, MsApiKeyValue},
|
||||
props: {
|
||||
request: Object
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeName: "parameters",
|
||||
rules: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isRequest() {
|
||||
return this.request.type === "Request";
|
||||
},
|
||||
isNotGet() {
|
||||
return this.request.method !== "GET";
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.request-method-select {
|
||||
width: 110px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,49 @@
|
|||
<template>
|
||||
<el-form :model="scenario" :rules="rules" ref="scenario" label-width="100px" label-position="left" v-if="isScenario">
|
||||
<el-form-item :label="$t('api_test.scenario.name')" prop="name">
|
||||
<el-input v-model="scenario.name"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-form-item :label="$t('api_test.scenario.base_url')" prop="url">
|
||||
<el-input :placeholder="$t('api_test.scenario.base_url_describe')" v-model="scenario.url"></el-input>
|
||||
</el-form-item>
|
||||
|
||||
<el-tabs v-model="activeName">
|
||||
<el-tab-pane :label="$t('api_test.scenario.variables')" name="variables">
|
||||
<ms-api-key-value :items="scenario.variables"/>
|
||||
</el-tab-pane>
|
||||
<el-tab-pane :label="$t('api_test.scenario.headers')" name="headers">
|
||||
<ms-api-key-value :items="scenario.headers"/>
|
||||
</el-tab-pane>
|
||||
</el-tabs>
|
||||
</el-form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MsApiKeyValue from "./ApiKeyValue";
|
||||
|
||||
export default {
|
||||
name: "MsApiScenarioForm",
|
||||
components: {MsApiKeyValue},
|
||||
props: {
|
||||
scenario: Object
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
activeName: "variables",
|
||||
rules: {}
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
isScenario() {
|
||||
return this.scenario.type === "Scenario";
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -1,121 +0,0 @@
|
|||
<template>
|
||||
|
||||
<div>
|
||||
<el-row>
|
||||
<el-col :span="5" :offset="6">
|
||||
<span>浏览器</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row >
|
||||
<el-col :span="20" :offset="2">
|
||||
<el-radio-group v-model="browser.value" class="browser-radio">
|
||||
<el-radio v-for="item in browser.options" :key="item.label" :label="item.label">
|
||||
<img :src="item.url"/>
|
||||
</el-radio>
|
||||
</el-radio-group>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="5" :offset="6">
|
||||
<span>资源池</span>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-col :span="18" :offset="1">
|
||||
<el-select v-model="resourcePool.value" filterable placeholder="请选择">
|
||||
<el-option
|
||||
v-for="item in resourcePool.options"
|
||||
:key="item.value"
|
||||
:label="item.label"
|
||||
:value="item.value">
|
||||
</el-option>
|
||||
</el-select>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
export default {
|
||||
name: "ApiTestRuntimeConfig",
|
||||
data() {
|
||||
return {
|
||||
resourcePool: {
|
||||
options: [
|
||||
{
|
||||
value: '选项1',
|
||||
label: '资源池1'
|
||||
},
|
||||
{
|
||||
value: '选项2',
|
||||
label: '资源池3'
|
||||
},
|
||||
{
|
||||
value: '选项3',
|
||||
label: '资源池3'
|
||||
}],
|
||||
value: ''
|
||||
},
|
||||
browser: {
|
||||
options: [{
|
||||
url: require('@/assets/browser/firefox.svg'),
|
||||
label: 'firefox',
|
||||
},
|
||||
{
|
||||
url: require('@/assets/browser/chrome.svg'),
|
||||
label: 'chrome',
|
||||
},
|
||||
{
|
||||
url: require('@/assets/browser/ie.svg'),
|
||||
label: 'ie',
|
||||
},
|
||||
{
|
||||
url: require('@/assets/browser/opera.svg'),
|
||||
label: 'opera',
|
||||
}
|
||||
],
|
||||
value: 'firefox'
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
validConfig() {
|
||||
if (this.resourcePool.value == '') {
|
||||
this.$message.error(this.$t('api_test.select_resource_pool'));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
configurations() {
|
||||
return {
|
||||
resourcePool: this.resourcePool,
|
||||
browser: this.browser
|
||||
}
|
||||
},
|
||||
cancelAllEdit() {
|
||||
this.browser.value = 'firefox';
|
||||
this.resourcePool.value = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.el-row {
|
||||
margin-top: 30px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: dimgray;
|
||||
}
|
||||
|
||||
</style>
|
|
@ -1,196 +0,0 @@
|
|||
<template>
|
||||
<div v-loading="result.loading">
|
||||
<el-upload
|
||||
accept=".jmx"
|
||||
drag
|
||||
action=""
|
||||
:limit="1"
|
||||
:show-file-list="false"
|
||||
:before-upload="beforeUpload"
|
||||
:http-request="handleUpload"
|
||||
:on-exceed="handleExceed"
|
||||
:file-list="fileList">
|
||||
<i class="el-icon-upload"/>
|
||||
<div class="el-upload__text" v-html="$t('load_test.upload_tips')"></div>
|
||||
<template v-slot:tip>
|
||||
<div class="el-upload__tip">{{$t('load_test.upload_type')}}</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
<el-table class="basic-config" :data="tableData">
|
||||
<el-table-column
|
||||
prop="name"
|
||||
:label="$t('load_test.file_name')">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="size"
|
||||
:label="$t('load_test.file_size')">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="type"
|
||||
:label="$t('load_test.file_type')">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('load_test.last_modify_time')">
|
||||
<template v-slot:default="scope">
|
||||
<i class="el-icon-time"/>
|
||||
<span class="last-modified">{{ scope.row.lastModified | timestampFormatDate }}</span>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
prop="status"
|
||||
:label="$t('load_test.file_status')">
|
||||
</el-table-column>
|
||||
<el-table-column
|
||||
:label="$t('commons.operating')">
|
||||
<template v-slot:default="scope">
|
||||
<el-button @click="handleDownload(scope.row)" :disabled="!scope.row.id" type="primary" icon="el-icon-download"
|
||||
size="mini" circle/>
|
||||
<el-button @click="handleDelete(scope.row, scope.$index)" type="danger" icon="el-icon-delete" size="mini"
|
||||
circle/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {Message} from "element-ui";
|
||||
|
||||
export default {
|
||||
name: "ApiTestSceneConfig",
|
||||
props: ["testPlan"],
|
||||
data() {
|
||||
return {
|
||||
result: {},
|
||||
getFileMetadataPath: "/api/file/metadata",
|
||||
jmxDownloadPath: '/api/file/download',
|
||||
jmxDeletePath: '/api/file/delete',
|
||||
fileList: [],
|
||||
tableData: [],
|
||||
};
|
||||
},
|
||||
created() {
|
||||
if (this.testPlan.id) {
|
||||
this.getFileMetadata(this.testPlan)
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
testPlan() {
|
||||
if (this.testPlan.id) {
|
||||
this.getFileMetadata(this.testPlan)
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getFileMetadata(testPlan) {
|
||||
this.fileList = [];// 一个测试只有一个文件
|
||||
this.tableData = [];// 一个测试只有一个文件
|
||||
this.result = this.$get(this.getFileMetadataPath + "/" + testPlan.id, response => {
|
||||
let file = response.data;
|
||||
|
||||
if (!file) {
|
||||
Message.error({message: this.$t('load_test.related_file_not_found'), showClose: true});
|
||||
return;
|
||||
}
|
||||
|
||||
this.testPlan.file = file;
|
||||
this.fileList.push({
|
||||
id: file.id,
|
||||
name: file.name
|
||||
});
|
||||
|
||||
this.tableData.push({
|
||||
id: file.id,
|
||||
name: file.name,
|
||||
size: file.size + 'Byte', /// todo: 按照大小显示Byte、KB、MB等
|
||||
type: 'JMX',
|
||||
lastModified: file.updateTime,
|
||||
status: 'todo',
|
||||
});
|
||||
})
|
||||
},
|
||||
beforeUpload(file) {
|
||||
if (!this.fileValidator(file)) {
|
||||
/// todo: 显示错误信息
|
||||
return false;
|
||||
}
|
||||
|
||||
this.tableData.push({
|
||||
name: file.name,
|
||||
size: file.size + 'Byte', /// todo: 按照大小显示Byte、KB、MB等
|
||||
type: 'JMX',
|
||||
lastModified: file.lastModified,
|
||||
status: 'todo',
|
||||
});
|
||||
|
||||
return true;
|
||||
},
|
||||
handleUpload(uploadResources) {
|
||||
this.testPlan.file = uploadResources.file;
|
||||
},
|
||||
handleDownload(file) {
|
||||
let data = {
|
||||
name: file.name,
|
||||
id: file.id,
|
||||
};
|
||||
let config = {
|
||||
url: this.jmxDownloadPath,
|
||||
method: 'post',
|
||||
data: data,
|
||||
responseType: 'blob'
|
||||
};
|
||||
this.result = this.$request(config).then(response => {
|
||||
const content = response.data;
|
||||
const blob = new Blob([content]);
|
||||
if ("download" in document.createElement("a")) {
|
||||
// 非IE下载
|
||||
// chrome/firefox
|
||||
let aTag = document.createElement('a');
|
||||
aTag.download = file.name;
|
||||
aTag.href = URL.createObjectURL(blob);
|
||||
aTag.click();
|
||||
URL.revokeObjectURL(aTag.href)
|
||||
} else {
|
||||
// IE10+下载
|
||||
navigator.msSaveBlob(blob, this.filename)
|
||||
}
|
||||
}).catch(e => {
|
||||
Message.error({message: e.message, showClose: true});
|
||||
});
|
||||
},
|
||||
handleDelete(file, index) {
|
||||
this.$alert(this.$t('commons.delete_file_confirm') + file.name + "?", '', {
|
||||
confirmButtonText: this.$t('commons.confirm'),
|
||||
callback: (action) => {
|
||||
if (action === 'confirm') {
|
||||
this._handleDelete(file, index);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
_handleDelete(file, index) {
|
||||
this.fileList.splice(index, 1);
|
||||
this.tableData.splice(index, 1);
|
||||
this.testPlan.file = null;
|
||||
},
|
||||
handleExceed() {
|
||||
this.$message.error(this.$t('load_test.delete_file'));
|
||||
},
|
||||
fileValidator(file) {
|
||||
/// todo: 是否需要对文件内容和大小做限制
|
||||
return file.size > 0;
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.basic-config {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.last-modified {
|
||||
margin-left: 5px;
|
||||
}
|
||||
</style>
|
|
@ -18,7 +18,7 @@ import PerformanceTestReport from "../../performance/report/PerformanceTestRepor
|
|||
import ApiTestReport from "../../api/report/ApiTestReport";
|
||||
import ApiTest from "../../api/ApiTest";
|
||||
import PerformanceTest from "../../performance/PerformanceTest";
|
||||
import EditApiTest from "../../api/test/EditApiTest";
|
||||
import ApiScenarioConfig from "../../api/test/ApiScenarioConfig";
|
||||
import PerformanceTestHome from "../../performance/home/PerformanceTestHome";
|
||||
import ApiTestList from "../../api/test/ApiTestList";
|
||||
import ApiTestHome from "../../api/home/ApiTestHome";
|
||||
|
@ -96,13 +96,13 @@ const router = new VueRouter({
|
|||
},
|
||||
{
|
||||
path: 'test/create',
|
||||
name: "createFucTest",
|
||||
component: EditApiTest,
|
||||
name: "createAPITest",
|
||||
component: ApiScenarioConfig,
|
||||
},
|
||||
{
|
||||
path: "test/edit/:testId",
|
||||
name: "editFucTest",
|
||||
component: EditApiTest,
|
||||
name: "editAPITest",
|
||||
component: ApiScenarioConfig,
|
||||
props: {
|
||||
content: (route) => {
|
||||
return {
|
||||
|
|
|
@ -96,6 +96,10 @@
|
|||
})
|
||||
this.$get("/performance/report/content/load_chart/" + this.id, res => {
|
||||
let data = res.data;
|
||||
let userList = data.filter(m => m.groupName === "users").map(m => m.yAxis);
|
||||
let hitsList = data.filter(m => m.groupName === "hits").map(m => m.yAxis);
|
||||
let userMax = this._getChartMax(userList);
|
||||
let hitsMax = this._getChartMax(hitsList);
|
||||
let loadOption = {
|
||||
title: {
|
||||
text: 'Load',
|
||||
|
@ -105,30 +109,57 @@
|
|||
color: '#65A2FF'
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {},
|
||||
xAxis: {},
|
||||
yAxis: [{
|
||||
name: 'User',
|
||||
type: 'value',
|
||||
min: 0,
|
||||
max: userMax,
|
||||
splitNumber: 5,
|
||||
// interval: 10 / 5
|
||||
interval: userMax / 5
|
||||
},
|
||||
{
|
||||
name: 'Hits/s',
|
||||
type: 'value',
|
||||
splitNumber: 5,
|
||||
min: 0,
|
||||
// max: 5,
|
||||
// interval: 5 / 5
|
||||
max: hitsMax,
|
||||
interval: hitsMax / 5
|
||||
}
|
||||
],
|
||||
series: []
|
||||
};
|
||||
let setting = {
|
||||
series: [
|
||||
{
|
||||
name: 'users',
|
||||
color: '#0CA74A',
|
||||
},
|
||||
{
|
||||
name: 'hits',
|
||||
yAxisIndex: '1',
|
||||
color: '#65A2FF',
|
||||
},
|
||||
{
|
||||
name: 'errors',
|
||||
yAxisIndex: '1',
|
||||
color: '#E6113C',
|
||||
}
|
||||
]
|
||||
}
|
||||
this.loadOption = this.generateOption(loadOption, data);
|
||||
this.loadOption = this.generateOption(loadOption, data, setting);
|
||||
})
|
||||
this.$get("/performance/report/content/res_chart/" + this.id, res => {
|
||||
let data = res.data;
|
||||
let userList = data.filter(m => m.groupName === "users").map(m => m.yAxis);
|
||||
let responseTimeList = data.filter(m => m.groupName === "responseTime").map(m => m.yAxis);
|
||||
let userMax = this._getChartMax(userList);
|
||||
let resMax = this._getChartMax(responseTimeList);
|
||||
let resOption = {
|
||||
title: {
|
||||
text: 'Response Time',
|
||||
|
@ -138,28 +169,55 @@
|
|||
color: '#99743C'
|
||||
},
|
||||
},
|
||||
tooltip: {
|
||||
show: true,
|
||||
trigger: 'axis'
|
||||
},
|
||||
legend: {},
|
||||
xAxis: {},
|
||||
yAxis: [{
|
||||
name: 'User',
|
||||
type: 'value',
|
||||
splitNumber: 5,
|
||||
min: 0
|
||||
min: 0,
|
||||
max: userMax,
|
||||
interval: userMax / 5
|
||||
},
|
||||
{
|
||||
name: 'Response Time',
|
||||
type: 'value',
|
||||
splitNumber: 5,
|
||||
min: 0
|
||||
min: 0,
|
||||
max: resMax,
|
||||
interval: resMax / 5
|
||||
}
|
||||
],
|
||||
series: []
|
||||
}
|
||||
this.resOption = this.generateOption(resOption, data);
|
||||
let setting = {
|
||||
series: [
|
||||
{
|
||||
name: 'users',
|
||||
color: '#0CA74A',
|
||||
},
|
||||
{
|
||||
name: "responseTime",
|
||||
yAxisIndex: '1',
|
||||
color: '#99743C',
|
||||
}
|
||||
]
|
||||
}
|
||||
this.resOption = this.generateOption(resOption, data, setting);
|
||||
})
|
||||
},
|
||||
generateOption(option, data) {
|
||||
generateOption(option, data, setting) {
|
||||
let chartData = data;
|
||||
let seriesArray = [];
|
||||
for (let set in setting) {
|
||||
if (set === "series") {
|
||||
seriesArray = setting[set];
|
||||
continue;
|
||||
}
|
||||
this.$set(option, set, setting[set]);
|
||||
}
|
||||
let legend = [], series = {}, xAxis = [], seriesData = [];
|
||||
chartData.forEach(item => {
|
||||
if (!xAxis.includes(item.xAxis)) {
|
||||
|
@ -183,11 +241,24 @@
|
|||
type: 'line',
|
||||
data: data
|
||||
};
|
||||
let seriesArrayNames = seriesArray.map(m => m.name);
|
||||
if (seriesArrayNames.includes(name)) {
|
||||
for (let j = 0; j < seriesArray.length; j++) {
|
||||
let seriesObj = seriesArray[j];
|
||||
if (seriesObj['name'] === name) {
|
||||
Object.assign(items, seriesObj);
|
||||
}
|
||||
}
|
||||
}
|
||||
seriesData.push(items);
|
||||
}
|
||||
this.$set(option, "series", seriesData);
|
||||
return option;
|
||||
},
|
||||
_getChartMax(arr) {
|
||||
const max = Math.max(...arr);
|
||||
return Math.ceil(max / 4.5) * 5;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
status() {
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
:current-project="currentProject"
|
||||
@openTestCaseEditDialog="openTestCaseEditDialog"
|
||||
@testCaseEdit="openTestCaseEditDialog"
|
||||
@refresh="refresh"
|
||||
ref="testCaseList">
|
||||
</test-case-list>
|
||||
</el-main>
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<div>
|
||||
<el-tooltip class="item" effect="dark" content="导出用例" placement="right">
|
||||
<el-button type="info" icon="el-icon-download" size="mini" circle></el-button>
|
||||
</el-tooltip>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "TestCaseImport"
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
|
@ -0,0 +1,135 @@
|
|||
<template>
|
||||
|
||||
<div>
|
||||
<el-tooltip class="item" effect="dark" content="导入用例" placement="right">
|
||||
<el-button type="info" icon="el-icon-upload2" size="mini" circle
|
||||
@click="dialogVisible = true"></el-button>
|
||||
</el-tooltip>
|
||||
|
||||
<el-dialog width="30%" title="导入测试用例" :visible.sync="dialogVisible"
|
||||
@close="init">
|
||||
|
||||
<el-row>
|
||||
<el-link type="primary" class="download-template"
|
||||
href="/test/case/export/template">下载模版</el-link>
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<el-upload
|
||||
class="upload-demo"
|
||||
:action="'/test/case/import/' + projectId"
|
||||
:on-preview="handlePreview"
|
||||
multiple
|
||||
:limit="1"
|
||||
:on-exceed="handleExceed"
|
||||
:beforeUpload="UploadValidate"
|
||||
:on-success="handleSuccess"
|
||||
:on-error="handleError"
|
||||
:file-list="fileList">
|
||||
<template v-slot:trigger>
|
||||
<el-button size="mini" type="success" plain>点击上传</el-button>
|
||||
</template>
|
||||
<template v-slot:tip>
|
||||
<div class="el-upload__tip">只能上传xls/xlsx文件,且不超过20M</div>
|
||||
</template>
|
||||
</el-upload>
|
||||
|
||||
</el-row>
|
||||
|
||||
<el-row>
|
||||
<ul>
|
||||
<li v-for="errFile in errList" :key="errFile.rowNum">
|
||||
{{errFile.errMsg}}
|
||||
</li>
|
||||
</ul>
|
||||
</el-row>
|
||||
|
||||
</el-dialog>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import ElUploadList from "element-ui/packages/upload/src/upload-list";
|
||||
export default {
|
||||
name: "TestCaseImport",
|
||||
components: {ElUploadList},
|
||||
data() {
|
||||
return {
|
||||
dialogVisible: false,
|
||||
fileList: [],
|
||||
errList: []
|
||||
}
|
||||
},
|
||||
props: {
|
||||
projectId: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
handlePreview(file) {
|
||||
console.log("init");
|
||||
this.init();
|
||||
},
|
||||
handleExceed(files, fileList) {
|
||||
this.$message.warning(`当前限制选择 1 个文件,本次选择了 ${files.length} 个文件`);
|
||||
},
|
||||
UploadValidate(file) {
|
||||
var suffix =file.name.substring(file.name.lastIndexOf('.') + 1);
|
||||
if (suffix != 'xls' && suffix != 'xlsx') {
|
||||
this.$message({
|
||||
message: '上传文件只能是 xls、xlsx格式!',
|
||||
type: 'warning'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (file.size / 1024 / 1024 > 20) {
|
||||
this.$message({
|
||||
message: '上传文件大小不能超过 20MB!',
|
||||
type: 'warning'
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
},
|
||||
handleSuccess(response) {
|
||||
let res = response.data;
|
||||
if (res.success) {
|
||||
this.$message.success("导入成功!");
|
||||
this.dialogVisible = false;
|
||||
this.$emit("refresh");
|
||||
} else {
|
||||
this.errList = res.errList;
|
||||
}
|
||||
this.fileList = [];
|
||||
},
|
||||
handleError(err, file, fileList) {
|
||||
this.$message.error(err.message);
|
||||
},
|
||||
init() {
|
||||
this.fileList = [];
|
||||
this.errList = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
|
||||
.el-dialog__body {
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.download-template {
|
||||
padding-top: 0px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
|
||||
|
||||
</style>
|
|
@ -4,12 +4,21 @@
|
|||
<el-card v-loading="result.loading">
|
||||
<template v-slot:header>
|
||||
<div>
|
||||
<el-row type="flex" justify="space-between" align="middle">
|
||||
<el-row type="flex" justify="start" align="middle">
|
||||
<el-col :span="5">
|
||||
<span class="title">{{$t('test_track.test_case')}}</span>
|
||||
<ms-create-box :tips="$t('test_track.create')" :exec="testCaseCreate"/>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="1" :offset="12">
|
||||
<test-case-import :projectId="currentProject == null? null : currentProject.id"
|
||||
@refresh="refresh"/>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="1">
|
||||
<test-case-export/>
|
||||
</el-col>
|
||||
|
||||
<el-col :span="5">
|
||||
<span class="search">
|
||||
<el-input type="text" size="small" :placeholder="$t('load_test.search_by_name')"
|
||||
|
@ -105,10 +114,12 @@
|
|||
<script>
|
||||
|
||||
import MsCreateBox from '../../../settings/CreateBox';
|
||||
import TestCaseImport from '../components/TestCaseImport';
|
||||
import TestCaseExport from '../components/TestCaseExport';
|
||||
|
||||
export default {
|
||||
name: "TestCaseList",
|
||||
components: {MsCreateBox},
|
||||
components: {MsCreateBox, TestCaseImport, TestCaseExport},
|
||||
data() {
|
||||
return {
|
||||
result: {},
|
||||
|
@ -195,6 +206,9 @@
|
|||
type: 'success'
|
||||
});
|
||||
});
|
||||
},
|
||||
refresh() {
|
||||
this.$emit('refresh');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,7 +168,27 @@ export default {
|
|||
'resource_pool_is_null': '资源池为空',
|
||||
},
|
||||
api_test: {
|
||||
'select_resource_pool': '请选择资源池'
|
||||
scenario: {
|
||||
name: "场景名称",
|
||||
base_url: "基础URL",
|
||||
base_url_describe: "基础URL作为所有请求的URL前缀",
|
||||
variables: "变量",
|
||||
headers: "请求头"
|
||||
},
|
||||
request: {
|
||||
name: "请求名称",
|
||||
method: "请求方法",
|
||||
url: "请求URL",
|
||||
url_describe: "例如: https://fit2cloud.com",
|
||||
parameters: "请求参数",
|
||||
parameters_desc: "参数追加到URL,例如https://fit2cloud.com/entries?key1=Value1&Key2=Value2",
|
||||
headers: "请求头",
|
||||
body: "请求内容",
|
||||
body_kv: "键值对",
|
||||
body_text: "文本",
|
||||
assertions: "断言",
|
||||
extract: "提取"
|
||||
}
|
||||
},
|
||||
test_track: {
|
||||
'test_track': '测试跟踪',
|
||||
|
|
Loading…
Reference in New Issue