parent
58fdf00c19
commit
d2ee9bc0f1
|
@ -7,8 +7,11 @@ import io.metersphere.base.domain.ApiTestEnvironmentExample;
|
||||||
import io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs;
|
import io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs;
|
||||||
import io.metersphere.base.mapper.ApiTestEnvironmentMapper;
|
import io.metersphere.base.mapper.ApiTestEnvironmentMapper;
|
||||||
import io.metersphere.commons.exception.MSException;
|
import io.metersphere.commons.exception.MSException;
|
||||||
|
import io.metersphere.commons.utils.CommonBeanFactory;
|
||||||
import io.metersphere.controller.request.EnvironmentRequest;
|
import io.metersphere.controller.request.EnvironmentRequest;
|
||||||
|
import io.metersphere.dto.BaseSystemConfigDTO;
|
||||||
import io.metersphere.i18n.Translator;
|
import io.metersphere.i18n.Translator;
|
||||||
|
import io.metersphere.service.SystemParameterService;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
@ -90,6 +93,18 @@ public class ApiTestEnvironmentService {
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
public synchronized ApiTestEnvironmentWithBLOBs getMockEnvironmentByProjectId(String projectId, String protocal, String baseUrl) {
|
public synchronized ApiTestEnvironmentWithBLOBs getMockEnvironmentByProjectId(String projectId, String protocal, String baseUrl) {
|
||||||
|
//创建的时候检查当前站点
|
||||||
|
SystemParameterService systemParameterService = CommonBeanFactory.getBean(SystemParameterService.class);
|
||||||
|
BaseSystemConfigDTO baseSystemConfigDTO = systemParameterService.getBaseInfo();
|
||||||
|
if (baseSystemConfigDTO != null && StringUtils.isNotEmpty(baseSystemConfigDTO.getUrl())) {
|
||||||
|
baseUrl = baseSystemConfigDTO.getUrl();
|
||||||
|
if (baseUrl.startsWith("http:")) {
|
||||||
|
protocal = "http";
|
||||||
|
} else if (baseUrl.startsWith("https:")) {
|
||||||
|
protocal = "https";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
String apiName = MockConfigStaticData.MOCK_EVN_NAME;
|
String apiName = MockConfigStaticData.MOCK_EVN_NAME;
|
||||||
ApiTestEnvironmentWithBLOBs returnModel = null;
|
ApiTestEnvironmentWithBLOBs returnModel = null;
|
||||||
ApiTestEnvironmentExample example = new ApiTestEnvironmentExample();
|
ApiTestEnvironmentExample example = new ApiTestEnvironmentExample();
|
||||||
|
@ -117,6 +132,21 @@ public class ApiTestEnvironmentService {
|
||||||
JSONArray conditions = httpObj.getJSONArray("conditions");
|
JSONArray conditions = httpObj.getJSONArray("conditions");
|
||||||
if (conditions.isEmpty()) {
|
if (conditions.isEmpty()) {
|
||||||
needUpdate = true;
|
needUpdate = true;
|
||||||
|
} else {
|
||||||
|
for (int i = 0; i < conditions.size(); i++) {
|
||||||
|
JSONObject obj = conditions.getJSONObject(i);
|
||||||
|
String socket = url;
|
||||||
|
if (socket.startsWith("http://")) {
|
||||||
|
socket = socket.substring(7);
|
||||||
|
} else if (socket.startsWith("https://")) {
|
||||||
|
socket = socket.substring(8);
|
||||||
|
}
|
||||||
|
if (!obj.containsKey("socket") || !StringUtils.equals(socket, String.valueOf(obj.get("socket")))) {
|
||||||
|
needUpdate = true;
|
||||||
|
} else if (!obj.containsKey("protocol") || !StringUtils.equals(protocal, String.valueOf(obj.get("protocol")))) {
|
||||||
|
needUpdate = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -228,4 +258,20 @@ public class ApiTestEnvironmentService {
|
||||||
|
|
||||||
return blobs;
|
return blobs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void checkMockEvnInfoByBaseUrl(String baseUrl) {
|
||||||
|
List<ApiTestEnvironmentWithBLOBs> allEvnList = this.selectByExampleWithBLOBs(null);
|
||||||
|
for (ApiTestEnvironmentWithBLOBs model : allEvnList) {
|
||||||
|
if (StringUtils.equals(model.getName(), MockConfigStaticData.MOCK_EVN_NAME)) {
|
||||||
|
String protocal = "";
|
||||||
|
if (baseUrl.startsWith("http:")) {
|
||||||
|
protocal = "http";
|
||||||
|
} else if (baseUrl.startsWith("https:")) {
|
||||||
|
protocal = "https";
|
||||||
|
}
|
||||||
|
|
||||||
|
model = this.checkMockEvnIsRightful(model, protocal, model.getProjectId(), model.getName(), baseUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,87 @@
|
||||||
|
package io.metersphere.excel.handler;
|
||||||
|
|
||||||
|
import com.alibaba.excel.write.metadata.holder.WriteSheetHolder;
|
||||||
|
import com.alibaba.excel.write.metadata.holder.WriteTableHolder;
|
||||||
|
import com.alibaba.excel.write.style.row.AbstractRowHeightStyleStrategy;
|
||||||
|
import io.metersphere.i18n.Translator;
|
||||||
|
import org.apache.poi.ss.usermodel.Comment;
|
||||||
|
import org.apache.poi.ss.usermodel.Drawing;
|
||||||
|
import org.apache.poi.ss.usermodel.Row;
|
||||||
|
import org.apache.poi.ss.usermodel.Sheet;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFClientAnchor;
|
||||||
|
import org.apache.poi.xssf.usermodel.XSSFRichTextString;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author song.tianyang
|
||||||
|
* @Date 2021/5/7 2:17 下午
|
||||||
|
* @Description
|
||||||
|
*/
|
||||||
|
public class FunctionCaseTemplateWriteHandler extends AbstractRowHeightStyleStrategy {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void beforeRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Integer rowIndex, Integer relativeRowIndex, Boolean isHead) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterRowCreate(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void afterRowDispose(WriteSheetHolder writeSheetHolder, WriteTableHolder writeTableHolder, Row row, Integer relativeRowIndex, Boolean isHead) {
|
||||||
|
super.afterRowDispose(writeSheetHolder, writeTableHolder, row, relativeRowIndex, isHead);
|
||||||
|
if (isHead) {
|
||||||
|
Sheet sheet = writeSheetHolder.getSheet();
|
||||||
|
Drawing<?> drawingPatriarch = sheet.createDrawingPatriarch();
|
||||||
|
|
||||||
|
// 在第一行 第3列创建一个批注
|
||||||
|
Comment comment1 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 2, 0, (short) 3, 1));
|
||||||
|
// 输入批注信息
|
||||||
|
comment1.setString(new XSSFRichTextString(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase")));
|
||||||
|
|
||||||
|
// 在第一行 第4列创建一个批注
|
||||||
|
Comment comment2 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 3, 0, (short) 3, 1));
|
||||||
|
// 输入批注信息
|
||||||
|
comment2.setString(new XSSFRichTextString(Translator.get("module_created_automatically")));
|
||||||
|
|
||||||
|
// 在第一行 第5列创建一个批注
|
||||||
|
Comment comment3 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 4, 0, (short) 3, 1));
|
||||||
|
// 输入批注信息
|
||||||
|
comment3.setString(new XSSFRichTextString(Translator.get("options") + "(functional、performance、api)"));
|
||||||
|
|
||||||
|
|
||||||
|
// 在第一行 第6列创建一个批注
|
||||||
|
Comment comment4 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 5, 0, (short) 3, 1));
|
||||||
|
// 输入批注信息
|
||||||
|
comment4.setString(new XSSFRichTextString(Translator.get("please_input_workspace_member")));
|
||||||
|
|
||||||
|
// 在第一行 第7列创建一个批注
|
||||||
|
Comment comment5 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 6, 0, (short) 3, 1));
|
||||||
|
// 输入批注信息
|
||||||
|
comment5.setString(new XSSFRichTextString(Translator.get("options") + "(P0、P1、P2、P3)"));
|
||||||
|
|
||||||
|
// 在第一行 第8列创建一个批注
|
||||||
|
Comment comment6 = drawingPatriarch.createCellComment(new XSSFClientAnchor(0, 0, 0, 0, (short) 7, 0, (short) 3, 1));
|
||||||
|
// 输入批注信息
|
||||||
|
comment6.setString(new XSSFRichTextString(Translator.get("tag_tip_pattern")));
|
||||||
|
|
||||||
|
// 将批注添加到单元格对象中
|
||||||
|
sheet.getRow(0).getCell(1).setCellComment(comment1);
|
||||||
|
sheet.getRow(0).getCell(1).setCellComment(comment2);
|
||||||
|
sheet.getRow(0).getCell(1).setCellComment(comment3);
|
||||||
|
sheet.getRow(0).getCell(1).setCellComment(comment4);
|
||||||
|
sheet.getRow(0).getCell(1).setCellComment(comment5);
|
||||||
|
sheet.getRow(0).getCell(1).setCellComment(comment6);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setHeadColumnHeight(Row row, int relativeRowIndex) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void setContentColumnHeight(Row row, int relativeRowIndex) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,338 @@
|
||||||
|
package io.metersphere.excel.listener;
|
||||||
|
|
||||||
|
import com.alibaba.excel.context.AnalysisContext;
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import io.metersphere.base.domain.TestCaseWithBLOBs;
|
||||||
|
import io.metersphere.commons.constants.TestCaseConstants;
|
||||||
|
import io.metersphere.commons.utils.BeanUtils;
|
||||||
|
import io.metersphere.commons.utils.CommonBeanFactory;
|
||||||
|
import io.metersphere.commons.utils.LogUtil;
|
||||||
|
import io.metersphere.excel.domain.ExcelErrData;
|
||||||
|
import io.metersphere.excel.domain.TestCaseExcelData;
|
||||||
|
import io.metersphere.excel.utils.ExcelValidateHelper;
|
||||||
|
import io.metersphere.i18n.Translator;
|
||||||
|
import io.metersphere.track.service.TestCaseService;
|
||||||
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import java.util.*;
|
||||||
|
import java.util.regex.Matcher;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
public class TestCaseDataIgnoreErrorListener extends EasyExcelListener<TestCaseExcelData> {
|
||||||
|
|
||||||
|
private TestCaseService testCaseService;
|
||||||
|
|
||||||
|
private String projectId;
|
||||||
|
|
||||||
|
protected List<TestCaseExcelData> updateList = new ArrayList<>(); //存储待更新用例的集合
|
||||||
|
|
||||||
|
protected boolean isUpdated = false; //判断是否更新过用例,将会传给前端
|
||||||
|
|
||||||
|
Set<String> testCaseNames;
|
||||||
|
|
||||||
|
Set<String> userIds;
|
||||||
|
|
||||||
|
public boolean isUpdated() {
|
||||||
|
return isUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TestCaseDataIgnoreErrorListener(Class clazz, String projectId, Set<String> testCaseNames, Set<String> userIds) {
|
||||||
|
this.clazz = clazz;
|
||||||
|
this.testCaseService = (TestCaseService) CommonBeanFactory.getBean("testCaseService");
|
||||||
|
this.projectId = projectId;
|
||||||
|
this.testCaseNames = testCaseNames;
|
||||||
|
this.userIds = userIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String validate(TestCaseExcelData data, String errMsg) {
|
||||||
|
String nodePath = data.getNodePath();
|
||||||
|
StringBuilder stringBuilder = new StringBuilder(errMsg);
|
||||||
|
//校验”所属模块"
|
||||||
|
if (nodePath != null) {
|
||||||
|
String[] nodes = nodePath.split("/");
|
||||||
|
//校验模块深度
|
||||||
|
if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) {
|
||||||
|
stringBuilder.append(Translator.get("test_case_node_level_tip") +
|
||||||
|
TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level") + "; ");
|
||||||
|
}
|
||||||
|
//模块名不能为空
|
||||||
|
for (int i = 0; i < nodes.length; i++) {
|
||||||
|
if (i != 0 && StringUtils.equals(nodes[i].trim(), "")) {
|
||||||
|
stringBuilder.append(Translator.get("module_not_null") + "; ");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//增加字数校验,每一层不能超过100字
|
||||||
|
for (int i = 0; i < nodes.length; i++) {
|
||||||
|
String nodeStr = nodes[i];
|
||||||
|
if (StringUtils.isNotEmpty(nodeStr)) {
|
||||||
|
if (nodeStr.trim().length() > 100) {
|
||||||
|
stringBuilder.append(Translator.get("module") + Translator.get("test_track.length_less_than") + "100:" + nodeStr);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//校验维护人
|
||||||
|
if (!userIds.contains(data.getMaintainer())) {
|
||||||
|
stringBuilder.append(Translator.get("user_not_exists") + ":" + data.getMaintainer() + "; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
校验Excel中是否有ID
|
||||||
|
有的话校验ID是否已在当前项目中存在,存在则更新用例,
|
||||||
|
不存在则继续校验看是否重复,不重复则新建用例。
|
||||||
|
*/
|
||||||
|
if (null != data.getNum()) { //当前读取的数据有ID
|
||||||
|
if (null != testCaseService.checkIdExist(data.getNum(), projectId)) { //该ID在当前项目中存在
|
||||||
|
//如果前面所经过的校验都没报错
|
||||||
|
if (StringUtils.isEmpty(stringBuilder)) {
|
||||||
|
updateList.add(data); //将当前数据存入更新列表
|
||||||
|
stringBuilder.append("update_testcase"); //该信息用于在invoke方法中判断是否该更新用例
|
||||||
|
}
|
||||||
|
return stringBuilder.toString();
|
||||||
|
} else {
|
||||||
|
/*
|
||||||
|
该ID在当前数据库中不存在,应当继续校验用例是否重复,
|
||||||
|
在下面的校验过程中,num的值会被用于判断是否重复,所以应当先设置为null
|
||||||
|
*/
|
||||||
|
data.setNum(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/*
|
||||||
|
校验用例
|
||||||
|
*/
|
||||||
|
if (testCaseNames.contains(data.getName())) {
|
||||||
|
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
|
||||||
|
BeanUtils.copyBean(testCase, data);
|
||||||
|
testCase.setProjectId(projectId);
|
||||||
|
String steps = getSteps(data);
|
||||||
|
testCase.setSteps(steps);
|
||||||
|
boolean dbExist = testCaseService.exist(testCase);
|
||||||
|
boolean excelExist = false;
|
||||||
|
if (dbExist) {
|
||||||
|
// db exist
|
||||||
|
stringBuilder.append(Translator.get("test_case_already_exists") + ":" + data.getName() + "; ");
|
||||||
|
} else {
|
||||||
|
// @Data 重写了 equals 和 hashCode 方法
|
||||||
|
excelExist = excelDataList.contains(data);
|
||||||
|
}
|
||||||
|
if (excelExist) {
|
||||||
|
// excel exist
|
||||||
|
stringBuilder.append(Translator.get("test_case_already_exists_excel") + ":" + data.getName() + "; ");
|
||||||
|
} else {
|
||||||
|
excelDataList.add(data);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
testCaseNames.add(data.getName());
|
||||||
|
excelDataList.add(data);
|
||||||
|
}
|
||||||
|
return stringBuilder.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void saveData() {
|
||||||
|
if (!(list.size() == 0)) {
|
||||||
|
Collections.reverse(list); //因为saveImportData里面是先分配最大的ID,这个ID应该先发给list中最后的数据,所以要reverse
|
||||||
|
List<TestCaseWithBLOBs> result = list.stream()
|
||||||
|
.map(item -> this.convert2TestCase(item))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
testCaseService.saveImportData(result, projectId);
|
||||||
|
this.isUpdated = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(updateList.size() == 0)) {
|
||||||
|
List<TestCaseWithBLOBs> result2 = updateList.stream()
|
||||||
|
.map(item -> this.convert2TestCaseForUpdate(item))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
testCaseService.updateImportDataCarryId(result2, projectId);
|
||||||
|
this.isUpdated = true;
|
||||||
|
updateList.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
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());
|
||||||
|
testCase.setCustomNum(data.getCustomNum());
|
||||||
|
String nodePath = data.getNodePath();
|
||||||
|
|
||||||
|
if (!nodePath.startsWith("/")) {
|
||||||
|
nodePath = "/" + nodePath;
|
||||||
|
}
|
||||||
|
if (nodePath.endsWith("/")) {
|
||||||
|
nodePath = nodePath.substring(0, nodePath.length() - 1);
|
||||||
|
}
|
||||||
|
testCase.setNodePath(nodePath);
|
||||||
|
|
||||||
|
//将标签设置为前端可解析的格式
|
||||||
|
String modifiedTags = modifyTagPattern(data);
|
||||||
|
testCase.setTags(modifiedTags);
|
||||||
|
|
||||||
|
if (StringUtils.isNotBlank(data.getStepModel())
|
||||||
|
&& StringUtils.equals(data.getStepModel(), TestCaseConstants.StepModel.TEXT.name())) {
|
||||||
|
testCase.setStepDescription(data.getStepDesc());
|
||||||
|
testCase.setExpectedResult(data.getStepResult());
|
||||||
|
} else {
|
||||||
|
String steps = getSteps(data);
|
||||||
|
testCase.setSteps(steps);
|
||||||
|
}
|
||||||
|
return testCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将Excel中的数据对象转换为用于更新操作的用例数据对象,
|
||||||
|
*
|
||||||
|
* @param data
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
private TestCaseWithBLOBs convert2TestCaseForUpdate(TestCaseExcelData data) {
|
||||||
|
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
|
||||||
|
BeanUtils.copyBean(testCase, data);
|
||||||
|
testCase.setProjectId(this.projectId);
|
||||||
|
testCase.setUpdateTime(System.currentTimeMillis());
|
||||||
|
|
||||||
|
//调整nodePath格式
|
||||||
|
String nodePath = data.getNodePath();
|
||||||
|
if (!nodePath.startsWith("/")) {
|
||||||
|
nodePath = "/" + nodePath;
|
||||||
|
}
|
||||||
|
if (nodePath.endsWith("/")) {
|
||||||
|
nodePath = nodePath.substring(0, nodePath.length() - 1);
|
||||||
|
}
|
||||||
|
testCase.setNodePath(nodePath);
|
||||||
|
|
||||||
|
String steps = getSteps(data);
|
||||||
|
testCase.setSteps(steps);
|
||||||
|
|
||||||
|
//将标签设置为前端可解析的格式
|
||||||
|
String modifiedTags = modifyTagPattern(data);
|
||||||
|
testCase.setTags(modifiedTags);
|
||||||
|
|
||||||
|
return testCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 调整tags格式,便于前端进行解析。
|
||||||
|
* 例如对于:标签1,标签2。将调整为:["标签1","标签2"]。
|
||||||
|
*/
|
||||||
|
public String modifyTagPattern(TestCaseExcelData data) {
|
||||||
|
String tags = data.getTags();
|
||||||
|
try {
|
||||||
|
if (StringUtils.isNotBlank(tags)) {
|
||||||
|
JSONArray.parse(tags);
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
return "[]";
|
||||||
|
} catch (Exception e) {
|
||||||
|
if (tags != null) {
|
||||||
|
Stream<String> stringStream = Arrays.stream(tags.split("[,;,;]")); //当标签值以中英文的逗号和分号分隔时才能正确解析
|
||||||
|
List<String> tagList = stringStream.map(tag -> tag = "\"" + tag + "\"")
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
String modifiedTags = StringUtils.join(tagList, ",");
|
||||||
|
modifiedTags = "[" + modifiedTags + "]";
|
||||||
|
return modifiedTags;
|
||||||
|
} else {
|
||||||
|
return "[]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getSteps(TestCaseExcelData data) {
|
||||||
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
|
||||||
|
String[] stepDesc = new String[1];
|
||||||
|
String[] stepRes = new String[1];
|
||||||
|
|
||||||
|
if (data.getStepDesc() != null) {
|
||||||
|
stepDesc = data.getStepDesc().split("\r\n|\n");
|
||||||
|
} else {
|
||||||
|
stepDesc[0] = "";
|
||||||
|
}
|
||||||
|
if (data.getStepResult() != null) {
|
||||||
|
stepRes = data.getStepResult().split("\r\n|\n");
|
||||||
|
} else {
|
||||||
|
stepRes[0] = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
String pattern = "(^\\d+)(\\.)?";
|
||||||
|
int index = stepDesc.length > stepRes.length ? stepDesc.length : stepRes.length;
|
||||||
|
|
||||||
|
for (int i = 0; i < index; i++) {
|
||||||
|
|
||||||
|
// 保持插入顺序,判断用例是否有相同的steps
|
||||||
|
JSONObject step = new JSONObject(true);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
return jsonArray.toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void invoke(TestCaseExcelData testCaseExcelData, AnalysisContext analysisContext) {
|
||||||
|
String errMsg;
|
||||||
|
Integer rowIndex = analysisContext.readRowHolder().getRowIndex();
|
||||||
|
String updateMsg = "update_testcase";
|
||||||
|
try {
|
||||||
|
//根据excel数据实体中的javax.validation + 正则表达式来校验excel数据
|
||||||
|
errMsg = ExcelValidateHelper.validateEntity(testCaseExcelData);
|
||||||
|
//自定义校验规则
|
||||||
|
errMsg = validate(testCaseExcelData, errMsg);
|
||||||
|
} catch (NoSuchFieldException e) {
|
||||||
|
errMsg = Translator.get("parse_data_error");
|
||||||
|
LogUtil.error(e.getMessage(), e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!StringUtils.isEmpty(errMsg)) {
|
||||||
|
|
||||||
|
//如果errMsg只有"update testcase",说明用例待更新
|
||||||
|
if (!errMsg.equals(updateMsg)) {
|
||||||
|
ExcelErrData excelErrData = new ExcelErrData(testCaseExcelData, rowIndex,
|
||||||
|
Translator.get("number") + " " + rowIndex + " " + Translator.get("row") + Translator.get("error")
|
||||||
|
+ ":" + errMsg);
|
||||||
|
|
||||||
|
errList.add(excelErrData);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
list.add(testCaseExcelData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (list.size() > BATCH_COUNT) {
|
||||||
|
saveData();
|
||||||
|
list.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,7 @@
|
||||||
package io.metersphere.excel.utils;
|
package io.metersphere.excel.utils;
|
||||||
|
|
||||||
import com.alibaba.excel.EasyExcel;
|
import com.alibaba.excel.EasyExcel;
|
||||||
|
import com.alibaba.excel.write.handler.WriteHandler;
|
||||||
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
|
import com.alibaba.excel.write.metadata.style.WriteCellStyle;
|
||||||
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
|
import com.alibaba.excel.write.style.HorizontalCellStyleStrategy;
|
||||||
import io.metersphere.commons.utils.LogUtil;
|
import io.metersphere.commons.utils.LogUtil;
|
||||||
|
@ -38,4 +39,19 @@ public class EasyExcelExporter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void exportByCustomWriteHandler(HttpServletResponse response, List data, String fileName, String sheetName, WriteHandler writeHandler) {
|
||||||
|
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(), this.clazz).registerWriteHandler(writeHandler).sheet(sheetName).doWrite(data);
|
||||||
|
} catch (UnsupportedEncodingException e) {
|
||||||
|
LogUtil.error(e.getMessage(), e);
|
||||||
|
throw new ExcelException("Utf-8 encoding is not supported");
|
||||||
|
} catch (IOException e) {
|
||||||
|
LogUtil.error(e.getMessage(), e);
|
||||||
|
throw new ExcelException("IO exception");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package io.metersphere.service;
|
package io.metersphere.service;
|
||||||
|
|
||||||
|
import io.metersphere.api.service.ApiTestEnvironmentService;
|
||||||
import io.metersphere.base.domain.*;
|
import io.metersphere.base.domain.*;
|
||||||
import io.metersphere.base.mapper.SystemHeaderMapper;
|
import io.metersphere.base.mapper.SystemHeaderMapper;
|
||||||
import io.metersphere.base.mapper.SystemParameterMapper;
|
import io.metersphere.base.mapper.SystemParameterMapper;
|
||||||
|
@ -42,6 +43,8 @@ public class SystemParameterService {
|
||||||
private ExtSystemParameterMapper extSystemParameterMapper;
|
private ExtSystemParameterMapper extSystemParameterMapper;
|
||||||
@Resource
|
@Resource
|
||||||
private SystemHeaderMapper systemHeaderMapper;
|
private SystemHeaderMapper systemHeaderMapper;
|
||||||
|
@Resource
|
||||||
|
private ApiTestEnvironmentService apiTestEnvironmentService;
|
||||||
|
|
||||||
public String searchEmail() {
|
public String searchEmail() {
|
||||||
return extSystemParameterMapper.email();
|
return extSystemParameterMapper.email();
|
||||||
|
@ -237,6 +240,7 @@ public class SystemParameterService {
|
||||||
|
|
||||||
public void saveBaseInfo(List<SystemParameter> parameters) {
|
public void saveBaseInfo(List<SystemParameter> parameters) {
|
||||||
SystemParameterExample example = new SystemParameterExample();
|
SystemParameterExample example = new SystemParameterExample();
|
||||||
|
|
||||||
parameters.forEach(param -> {
|
parameters.forEach(param -> {
|
||||||
// 去掉路径最后的 /
|
// 去掉路径最后的 /
|
||||||
param.setParamValue(StringUtils.removeEnd(param.getParamValue(), "/"));
|
param.setParamValue(StringUtils.removeEnd(param.getParamValue(), "/"));
|
||||||
|
@ -247,6 +251,10 @@ public class SystemParameterService {
|
||||||
systemParameterMapper.insert(param);
|
systemParameterMapper.insert(param);
|
||||||
}
|
}
|
||||||
example.clear();
|
example.clear();
|
||||||
|
|
||||||
|
if (StringUtils.equals(param.getParamKey(), "base.url")) {
|
||||||
|
apiTestEnvironmentService.checkMockEvnInfoByBaseUrl(param.getParamValue());
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -154,6 +154,13 @@ public class TestCaseController {
|
||||||
return testCaseService.testCaseImport(file, projectId, userId);
|
return testCaseService.testCaseImport(file, projectId, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/importIgnoreError/{projectId}/{userId}")
|
||||||
|
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
||||||
|
public ExcelResponse testCaseImportIgnoreError(MultipartFile file, @PathVariable String projectId, @PathVariable String userId) {
|
||||||
|
checkPermissionService.checkProjectOwner(projectId);
|
||||||
|
return testCaseService.testCaseImportIgnoreError(file, projectId, userId);
|
||||||
|
}
|
||||||
|
|
||||||
@GetMapping("/export/template")
|
@GetMapping("/export/template")
|
||||||
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
||||||
public void testCaseTemplateExport(HttpServletResponse response) {
|
public void testCaseTemplateExport(HttpServletResponse response) {
|
||||||
|
|
|
@ -19,6 +19,8 @@ import io.metersphere.excel.domain.ExcelErrData;
|
||||||
import io.metersphere.excel.domain.ExcelResponse;
|
import io.metersphere.excel.domain.ExcelResponse;
|
||||||
import io.metersphere.excel.domain.TestCaseExcelData;
|
import io.metersphere.excel.domain.TestCaseExcelData;
|
||||||
import io.metersphere.excel.domain.TestCaseExcelDataFactory;
|
import io.metersphere.excel.domain.TestCaseExcelDataFactory;
|
||||||
|
import io.metersphere.excel.handler.FunctionCaseTemplateWriteHandler;
|
||||||
|
import io.metersphere.excel.listener.TestCaseDataIgnoreErrorListener;
|
||||||
import io.metersphere.excel.listener.TestCaseDataListener;
|
import io.metersphere.excel.listener.TestCaseDataListener;
|
||||||
import io.metersphere.excel.utils.EasyExcelExporter;
|
import io.metersphere.excel.utils.EasyExcelExporter;
|
||||||
import io.metersphere.i18n.Translator;
|
import io.metersphere.i18n.Translator;
|
||||||
|
@ -97,7 +99,8 @@ public class TestCaseService {
|
||||||
TestCaseFileMapper testCaseFileMapper;
|
TestCaseFileMapper testCaseFileMapper;
|
||||||
@Resource
|
@Resource
|
||||||
TestCaseTestMapper testCaseTestMapper;
|
TestCaseTestMapper testCaseTestMapper;
|
||||||
private void setNode(TestCaseWithBLOBs testCase){
|
|
||||||
|
private void setNode(TestCaseWithBLOBs testCase) {
|
||||||
if (StringUtils.isEmpty(testCase.getNodeId()) || "default-module".equals(testCase.getNodeId())) {
|
if (StringUtils.isEmpty(testCase.getNodeId()) || "default-module".equals(testCase.getNodeId())) {
|
||||||
TestCaseNodeExample example = new TestCaseNodeExample();
|
TestCaseNodeExample example = new TestCaseNodeExample();
|
||||||
example.createCriteria().andProjectIdEqualTo(testCase.getProjectId()).andNameEqualTo("默认模块");
|
example.createCriteria().andProjectIdEqualTo(testCase.getProjectId()).andNameEqualTo("默认模块");
|
||||||
|
@ -224,7 +227,7 @@ public class TestCaseService {
|
||||||
String remark = tc.getRemark();
|
String remark = tc.getRemark();
|
||||||
String prerequisite = tc.getPrerequisite();
|
String prerequisite = tc.getPrerequisite();
|
||||||
if (StringUtils.equals(steps, caseSteps) && StringUtils.equals(remark, caseRemark) && StringUtils.equals(prerequisite, casePrerequisite)) {
|
if (StringUtils.equals(steps, caseSteps) && StringUtils.equals(remark, caseRemark) && StringUtils.equals(prerequisite, casePrerequisite)) {
|
||||||
//MSException.throwException(Translator.get("test_case_already_exists"));
|
//MSException.throwException(Translator.get("test_case_already_exists"));
|
||||||
return tc;
|
return tc;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -487,7 +490,7 @@ public class TestCaseService {
|
||||||
testcase.setCustomNum(String.valueOf(number));
|
testcase.setCustomNum(String.valueOf(number));
|
||||||
}
|
}
|
||||||
testcase.setReviewStatus(TestCaseReviewStatus.Prepare.name());
|
testcase.setReviewStatus(TestCaseReviewStatus.Prepare.name());
|
||||||
mapper.insert(testcase);
|
mapper.insert(testcase);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
sqlSession.flushStatements();
|
sqlSession.flushStatements();
|
||||||
|
@ -516,6 +519,7 @@ public class TestCaseService {
|
||||||
/**
|
/**
|
||||||
* 把Excel中带ID的数据更新到数据库
|
* 把Excel中带ID的数据更新到数据库
|
||||||
* feat(测试跟踪):通过Excel导入导出时有ID字段,可通过Excel导入来更新用例。 (#1727)
|
* feat(测试跟踪):通过Excel导入导出时有ID字段,可通过Excel导入来更新用例。 (#1727)
|
||||||
|
*
|
||||||
* @param testCases
|
* @param testCases
|
||||||
* @param projectId
|
* @param projectId
|
||||||
*/
|
*/
|
||||||
|
@ -555,8 +559,9 @@ public class TestCaseService {
|
||||||
public void testCaseTemplateExport(HttpServletResponse response) {
|
public void testCaseTemplateExport(HttpServletResponse response) {
|
||||||
try {
|
try {
|
||||||
EasyExcelExporter easyExcelExporter = new EasyExcelExporter(new TestCaseExcelDataFactory().getExcelDataByLocal());
|
EasyExcelExporter easyExcelExporter = new EasyExcelExporter(new TestCaseExcelDataFactory().getExcelDataByLocal());
|
||||||
easyExcelExporter.export(response, generateExportTemplate(),
|
FunctionCaseTemplateWriteHandler handler = new FunctionCaseTemplateWriteHandler();
|
||||||
Translator.get("test_case_import_template_name"), Translator.get("test_case_import_template_sheet"));
|
easyExcelExporter.exportByCustomWriteHandler(response, generateExportTemplate(),
|
||||||
|
Translator.get("test_case_import_template_name"), Translator.get("test_case_import_template_sheet"), handler);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
MSException.throwException(e);
|
MSException.throwException(e);
|
||||||
}
|
}
|
||||||
|
@ -619,16 +624,15 @@ public class TestCaseService {
|
||||||
}
|
}
|
||||||
|
|
||||||
list.add(new TestCaseExcelData());
|
list.add(new TestCaseExcelData());
|
||||||
TestCaseExcelData explain = new TestCaseExcelData();
|
// TestCaseExcelData explain = new TestCaseExcelData();
|
||||||
explain.setName(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase"));
|
// explain.setName(Translator.get("do_not_modify_header_order") + "," + Translator.get("num_needed_modify_testcase") + "," + Translator.get("num_needless_create_testcase"));
|
||||||
explain.setNodePath(Translator.get("module_created_automatically"));
|
// explain.setNodePath(Translator.get("module_created_automatically"));
|
||||||
explain.setType(Translator.get("options") + "(functional、performance、api)");
|
// explain.setType(Translator.get("options") + "(functional、performance、api)");
|
||||||
explain.setTags(Translator.get("tag_tip_pattern"));
|
// explain.setTags(Translator.get("tag_tip_pattern"));
|
||||||
// explain.setMethod(Translator.get("options") + "(manual、auto)");
|
//// explain.setMethod(Translator.get("options") + "(manual、auto)");
|
||||||
explain.setPriority(Translator.get("options") + "(P0、P1、P2、P3)");
|
// explain.setPriority(Translator.get("options") + "(P0、P1、P2、P3)");
|
||||||
explain.setMaintainer(Translator.get("please_input_workspace_member"));
|
// explain.setMaintainer(Translator.get("please_input_workspace_member"));
|
||||||
|
// list.add(explain);
|
||||||
list.add(explain);
|
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1020,9 +1024,95 @@ public class TestCaseService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新项目下用例的CustomNum值
|
* 更新项目下用例的CustomNum值
|
||||||
|
*
|
||||||
* @param projectId 项目ID
|
* @param projectId 项目ID
|
||||||
*/
|
*/
|
||||||
public void updateTestCaseCustomNumByProjectId(String projectId) {
|
public void updateTestCaseCustomNumByProjectId(String projectId) {
|
||||||
extTestCaseMapper.updateTestCaseCustomNumByProjectId(projectId);
|
extTestCaseMapper.updateTestCaseCustomNumByProjectId(projectId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ExcelResponse testCaseImportIgnoreError(MultipartFile multipartFile, String projectId, String userId) {
|
||||||
|
|
||||||
|
ExcelResponse excelResponse = new ExcelResponse();
|
||||||
|
boolean isUpdated = false; //判断是否更新了用例
|
||||||
|
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());
|
||||||
|
List<ExcelErrData<TestCaseExcelData>> errList = null;
|
||||||
|
if (multipartFile == null) {
|
||||||
|
MSException.throwException(Translator.get("upload_fail"));
|
||||||
|
}
|
||||||
|
if (multipartFile.getOriginalFilename().endsWith(".xmind")) {
|
||||||
|
try {
|
||||||
|
XmindCaseParser xmindParser = new XmindCaseParser(this, userId, projectId, testCaseNames);
|
||||||
|
errList = xmindParser.parse(multipartFile);
|
||||||
|
if (CollectionUtils.isEmpty(xmindParser.getNodePaths())
|
||||||
|
&& CollectionUtils.isEmpty(xmindParser.getTestCase())
|
||||||
|
&& CollectionUtils.isEmpty(xmindParser.getUpdateTestCase())) {
|
||||||
|
if (errList == null) {
|
||||||
|
errList = new ArrayList<>();
|
||||||
|
}
|
||||||
|
ExcelErrData excelErrData = new ExcelErrData(null, 1, Translator.get("upload_fail") + ":" + Translator.get("upload_content_is_null"));
|
||||||
|
errList.add(excelErrData);
|
||||||
|
excelResponse.setErrList(errList);
|
||||||
|
}
|
||||||
|
List<TestCaseWithBLOBs> continueCaseList = xmindParser.getContinueValidatedCase();
|
||||||
|
if (CollectionUtils.isNotEmpty(continueCaseList) || CollectionUtils.isNotEmpty(xmindParser.getUpdateTestCase())) {
|
||||||
|
if (CollectionUtils.isNotEmpty(xmindParser.getUpdateTestCase())) {
|
||||||
|
continueCaseList.removeAll(xmindParser.getUpdateTestCase());
|
||||||
|
this.updateImportData(xmindParser.getUpdateTestCase(), projectId);
|
||||||
|
}
|
||||||
|
List<String> nodePathList = xmindParser.getValidatedNodePath();
|
||||||
|
if (CollectionUtils.isNotEmpty(nodePathList)) {
|
||||||
|
testCaseNodeService.createNodes(nodePathList, projectId);
|
||||||
|
}
|
||||||
|
if (CollectionUtils.isNotEmpty(continueCaseList)) {
|
||||||
|
Collections.reverse(continueCaseList);
|
||||||
|
this.saveImportData(continueCaseList, projectId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xmindParser.clear();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtil.error(e.getMessage(), e);
|
||||||
|
MSException.throwException(e.getMessage());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
UserRoleExample userRoleExample = new UserRoleExample();
|
||||||
|
userRoleExample.createCriteria()
|
||||||
|
.andRoleIdIn(Arrays.asList(RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER))
|
||||||
|
.andSourceIdEqualTo(currentWorkspaceId);
|
||||||
|
|
||||||
|
Set<String> userIds = userRoleMapper.selectByExample(userRoleExample).stream().map(UserRole::getUserId).collect(Collectors.toSet());
|
||||||
|
|
||||||
|
try {
|
||||||
|
//根据本地语言环境选择用哪种数据对象进行存放读取的数据
|
||||||
|
Class clazz = new TestCaseExcelDataFactory().getExcelDataByLocal();
|
||||||
|
|
||||||
|
TestCaseDataIgnoreErrorListener easyExcelListener = new TestCaseDataIgnoreErrorListener(clazz, projectId, testCaseNames, userIds);
|
||||||
|
|
||||||
|
//读取excel数据
|
||||||
|
EasyExcelFactory.read(multipartFile.getInputStream(), clazz, easyExcelListener).sheet().doRead();
|
||||||
|
|
||||||
|
errList = easyExcelListener.getErrList();
|
||||||
|
isUpdated = easyExcelListener.isUpdated();
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtil.error(e.getMessage(), e);
|
||||||
|
MSException.throwException(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//如果包含错误信息就导出错误信息
|
||||||
|
if (!errList.isEmpty()) {
|
||||||
|
excelResponse.setSuccess(false);
|
||||||
|
excelResponse.setErrList(errList);
|
||||||
|
excelResponse.setIsUpdated(isUpdated);
|
||||||
|
} else {
|
||||||
|
excelResponse.setSuccess(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return excelResponse;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -59,6 +59,10 @@ public class XmindCaseParser {
|
||||||
*/
|
*/
|
||||||
private List<String> nodePaths;
|
private List<String> nodePaths;
|
||||||
|
|
||||||
|
private List<TestCaseWithBLOBs> continueValidatedCase;
|
||||||
|
|
||||||
|
private List<String> errorPath;
|
||||||
|
|
||||||
public XmindCaseParser(TestCaseService testCaseService, String userId, String projectId, Set<String> testCaseNames) {
|
public XmindCaseParser(TestCaseService testCaseService, String userId, String projectId, Set<String> testCaseNames) {
|
||||||
this.testCaseService = testCaseService;
|
this.testCaseService = testCaseService;
|
||||||
this.maintainer = userId;
|
this.maintainer = userId;
|
||||||
|
@ -69,11 +73,14 @@ public class XmindCaseParser {
|
||||||
compartDatas = new ArrayList<>();
|
compartDatas = new ArrayList<>();
|
||||||
process = new DetailUtil();
|
process = new DetailUtil();
|
||||||
nodePaths = new ArrayList<>();
|
nodePaths = new ArrayList<>();
|
||||||
|
continueValidatedCase = new ArrayList<>();
|
||||||
|
errorPath = new ArrayList<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static final String TC_REGEX = "(?:tc:|tc:|tc)";
|
private static final String TC_REGEX = "(?:tc:|tc:|tc)";
|
||||||
private static final String PC_REGEX = "(?:pc:|pc:|pc)";
|
private static final String PC_REGEX = "(?:pc:|pc:|pc)";
|
||||||
private static final String RC_REGEX = "(?:rc:|rc:|rc)";
|
private static final String RC_REGEX = "(?:rc:|rc:|rc)";
|
||||||
|
private static final String ID_REGEX = "(?:id:|id:|id)";
|
||||||
private static final String TAG_REGEX = "(?:tag:|tag:|tag)";
|
private static final String TAG_REGEX = "(?:tag:|tag:|tag)";
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
|
@ -126,6 +133,7 @@ public class XmindCaseParser {
|
||||||
* 验证用例的合规性
|
* 验证用例的合规性
|
||||||
*/
|
*/
|
||||||
private boolean validate(TestCaseWithBLOBs data) {
|
private boolean validate(TestCaseWithBLOBs data) {
|
||||||
|
boolean validatePass = true;
|
||||||
String nodePath = data.getNodePath();
|
String nodePath = data.getNodePath();
|
||||||
if (!nodePath.startsWith("/")) {
|
if (!nodePath.startsWith("/")) {
|
||||||
nodePath = "/" + nodePath;
|
nodePath = "/" + nodePath;
|
||||||
|
@ -137,27 +145,41 @@ public class XmindCaseParser {
|
||||||
|
|
||||||
|
|
||||||
if (data.getName().length() > 200) {
|
if (data.getName().length() > 200) {
|
||||||
|
validatePass = false;
|
||||||
process.add(Translator.get("test_case") + Translator.get("test_track.length_less_than") + "200", nodePath + data.getName());
|
process.add(Translator.get("test_case") + Translator.get("test_track.length_less_than") + "200", nodePath + data.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!StringUtils.isEmpty(nodePath)) {
|
if (!StringUtils.isEmpty(nodePath)) {
|
||||||
String[] nodes = nodePath.split("/");
|
String[] nodes = nodePath.split("/");
|
||||||
if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) {
|
if (nodes.length > TestCaseConstants.MAX_NODE_DEPTH + 1) {
|
||||||
|
validatePass = false;
|
||||||
process.add(Translator.get("test_case_node_level_tip") +
|
process.add(Translator.get("test_case_node_level_tip") +
|
||||||
TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level"), nodePath);
|
TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level"), nodePath);
|
||||||
|
if (!errorPath.contains(nodePath)) {
|
||||||
|
errorPath.add(nodePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
for (int i = 0; i < nodes.length; i++) {
|
for (int i = 0; i < nodes.length; i++) {
|
||||||
if (i != 0 && StringUtils.equals(nodes[i].trim(), "")) {
|
if (i != 0 && StringUtils.equals(nodes[i].trim(), "")) {
|
||||||
|
validatePass = false;
|
||||||
process.add(Translator.get("test_case") + Translator.get("module_not_null"), nodePath + data.getName());
|
process.add(Translator.get("test_case") + Translator.get("module_not_null"), nodePath + data.getName());
|
||||||
|
if (!errorPath.contains(nodePath)) {
|
||||||
|
errorPath.add(nodePath);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
} else if (nodes[i].trim().length() > 100) {
|
} else if (nodes[i].trim().length() > 100) {
|
||||||
|
validatePass = false;
|
||||||
process.add(Translator.get("module") + Translator.get("test_track.length_less_than") + "100 ", nodes[i].trim());
|
process.add(Translator.get("module") + Translator.get("test_track.length_less_than") + "100 ", nodes[i].trim());
|
||||||
|
if (!errorPath.contains(nodePath)) {
|
||||||
|
errorPath.add(nodePath);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) {
|
if (StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) {
|
||||||
|
validatePass = false;
|
||||||
process.add(Translator.get("functional_method_tip"), nodePath + data.getName());
|
process.add(Translator.get("functional_method_tip"), nodePath + data.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -176,9 +198,11 @@ public class XmindCaseParser {
|
||||||
|
|
||||||
// 用例等级和用例性质处理
|
// 用例等级和用例性质处理
|
||||||
if (!priorityList.contains(data.getPriority())) {
|
if (!priorityList.contains(data.getPriority())) {
|
||||||
|
validatePass = false;
|
||||||
process.add(Translator.get("test_case_priority") + Translator.get("incorrect_format"), nodePath + data.getName());
|
process.add(Translator.get("test_case_priority") + Translator.get("incorrect_format"), nodePath + data.getName());
|
||||||
}
|
}
|
||||||
if (data.getType() == null) {
|
if (data.getType() == null) {
|
||||||
|
validatePass = false;
|
||||||
process.add(Translator.get("test_case_type") + Translator.get("incorrect_format"), nodePath + data.getName());
|
process.add(Translator.get("test_case_type") + Translator.get("incorrect_format"), nodePath + data.getName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -186,9 +210,13 @@ public class XmindCaseParser {
|
||||||
TestCaseExcelData compartData = new TestCaseExcelData();
|
TestCaseExcelData compartData = new TestCaseExcelData();
|
||||||
BeanUtils.copyBean(compartData, data);
|
BeanUtils.copyBean(compartData, data);
|
||||||
if (compartDatas.contains(compartData)) {
|
if (compartDatas.contains(compartData)) {
|
||||||
|
validatePass = false;
|
||||||
process.add(Translator.get("test_case_already_exists_excel"), nodePath + "/" + compartData.getName());
|
process.add(Translator.get("test_case_already_exists_excel"), nodePath + "/" + compartData.getName());
|
||||||
}
|
}
|
||||||
compartDatas.add(compartData);
|
compartDatas.add(compartData);
|
||||||
|
if (validatePass) {
|
||||||
|
this.continueValidatedCase.add(data);
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -305,6 +333,7 @@ public class XmindCaseParser {
|
||||||
List<Attached> steps = new LinkedList<>();
|
List<Attached> steps = new LinkedList<>();
|
||||||
StringBuilder rc = new StringBuilder();
|
StringBuilder rc = new StringBuilder();
|
||||||
List<String> tags = new LinkedList<>();
|
List<String> tags = new LinkedList<>();
|
||||||
|
StringBuilder customId = new StringBuilder();
|
||||||
if (attacheds != null && !attacheds.isEmpty()) {
|
if (attacheds != null && !attacheds.isEmpty()) {
|
||||||
attacheds.forEach(item -> {
|
attacheds.forEach(item -> {
|
||||||
if (isAvailable(item.getTitle(), PC_REGEX)) {
|
if (isAvailable(item.getTitle(), PC_REGEX)) {
|
||||||
|
@ -314,12 +343,15 @@ public class XmindCaseParser {
|
||||||
rc.append("\n");
|
rc.append("\n");
|
||||||
} else if (isAvailable(item.getTitle(), TAG_REGEX)) {
|
} else if (isAvailable(item.getTitle(), TAG_REGEX)) {
|
||||||
tags.add(replace(item.getTitle(), TAG_REGEX));
|
tags.add(replace(item.getTitle(), TAG_REGEX));
|
||||||
|
} else if (isAvailable(item.getTitle(), ID_REGEX)) {
|
||||||
|
customId.append(replace(item.getTitle(), ID_REGEX));
|
||||||
} else {
|
} else {
|
||||||
steps.add(item);
|
steps.add(item);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
testCase.setRemark(rc.toString());
|
testCase.setRemark(rc.toString());
|
||||||
|
testCase.setCustomNum(customId.toString());
|
||||||
testCase.setTags(JSON.toJSONString(tags));
|
testCase.setTags(JSON.toJSONString(tags));
|
||||||
testCase.setSteps(this.getSteps(steps));
|
testCase.setSteps(this.getSteps(steps));
|
||||||
// 校验合规性
|
// 校验合规性
|
||||||
|
@ -364,8 +396,23 @@ public class XmindCaseParser {
|
||||||
//检查目录合规性
|
//检查目录合规性
|
||||||
this.validate();
|
this.validate();
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
|
ex.printStackTrace();
|
||||||
return process.parse(ex.getMessage());
|
return process.parse(ex.getMessage());
|
||||||
}
|
}
|
||||||
return process.parse();
|
return process.parse();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<TestCaseWithBLOBs> getContinueValidatedCase() {
|
||||||
|
return this.continueValidatedCase;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<String> getValidatedNodePath() {
|
||||||
|
List<String> returnPathList = new ArrayList<>(nodePaths);
|
||||||
|
if (CollectionUtils.isNotEmpty(returnPathList)) {
|
||||||
|
if (CollectionUtils.isNotEmpty(errorPath)) {
|
||||||
|
returnPathList.removeAll(errorPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnPathList;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,13 +2,13 @@
|
||||||
<el-dialog class="testcase-import" :title="$t('test_track.case.import.case_import')" :visible.sync="dialogVisible"
|
<el-dialog class="testcase-import" :title="$t('test_track.case.import.case_import')" :visible.sync="dialogVisible"
|
||||||
@close="close">
|
@close="close">
|
||||||
|
|
||||||
<el-tabs v-model="activeName" simple>
|
<el-tabs v-model="activeName" @tab-click="clickTabs" simple>
|
||||||
<el-tab-pane :label="$t('test_track.case.import.excel_title')" name="excelImport">
|
<el-tab-pane :label="$t('test_track.case.import.excel_title')" name="excelImport">
|
||||||
|
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-link type="primary" class="download-template"
|
<el-link type="primary" class="download-template"
|
||||||
@click="downloadTemplate"
|
@click="downloadTemplate"
|
||||||
>{{$t('test_track.case.import.download_template')}}
|
>{{ $t('test_track.case.import.download_template') }}
|
||||||
</el-link>
|
</el-link>
|
||||||
</el-row>
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
|
@ -39,10 +39,20 @@
|
||||||
<el-row>
|
<el-row>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="errFile in errList" :key="errFile.rowNum">
|
<li v-for="errFile in errList" :key="errFile.rowNum">
|
||||||
{{errFile.errMsg}}
|
{{ errFile.errMsg }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
<el-row style="text-align: right" v-if="showExcelImportContinueBtn">
|
||||||
|
<div style="margin-right: 20px;margin-bottom: 10px;">
|
||||||
|
<el-checkbox v-model="uploadIgnoreError">{{ $t('test_track.case.import.ignore_error') }}</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="uploadContinue(false)">{{ $t('test_track.case.import.continue_upload') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="close">{{ $t('commons.cancel') }}</el-button>
|
||||||
|
</el-row>
|
||||||
|
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<!-- Xmind 导入 -->
|
<!-- Xmind 导入 -->
|
||||||
<el-tab-pane :label="$t('test_track.case.import.xmind_title')" name="xmindImport" style="border: 0px">
|
<el-tab-pane :label="$t('test_track.case.import.xmind_title')" name="xmindImport" style="border: 0px">
|
||||||
|
@ -98,10 +108,18 @@
|
||||||
<el-row>
|
<el-row>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="errFile in xmindErrList" :key="errFile.rowNum">
|
<li v-for="errFile in xmindErrList" :key="errFile.rowNum">
|
||||||
{{errFile.errMsg}}
|
{{ errFile.errMsg }}
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
<el-row style="text-align: right" v-if="showXmindImportContinueBtn">
|
||||||
|
<div style="margin-right: 20px;margin-bottom: 10px;">
|
||||||
|
<el-checkbox v-model="uploadXmindIgnoreError">{{ $t('test_track.case.import.ignore_error') }}</el-checkbox>
|
||||||
|
</div>
|
||||||
|
<el-button type="primary" @click="uploadContinue(true)">{{ $t('test_track.case.import.continue_upload') }}
|
||||||
|
</el-button>
|
||||||
|
<el-button @click="close">{{ $t('commons.cancel') }}</el-button>
|
||||||
|
</el-row>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
|
@ -124,16 +142,34 @@
|
||||||
activeName: 'excelImport',
|
activeName: 'excelImport',
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
fileList: [],
|
fileList: [],
|
||||||
|
lastXmindFile: null,
|
||||||
|
lastExcelFile: null,
|
||||||
errList: [],
|
errList: [],
|
||||||
xmindErrList: [],
|
xmindErrList: [],
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
isUpdated: false
|
isUpdated: false,
|
||||||
|
clickTabsName: "",
|
||||||
|
showExcelImportContinueBtn: false,
|
||||||
|
showXmindImportContinueBtn: false,
|
||||||
|
uploadIgnoreError: false,
|
||||||
|
uploadXmindIgnoreError: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
created() {
|
||||||
|
this.showExcelImportContinueBtn = false;
|
||||||
|
this.showXmindImportContinueBtn = false;
|
||||||
|
},
|
||||||
|
activated() {
|
||||||
|
this.showExcelImportContinueBtn = false;
|
||||||
|
this.showXmindImportContinueBtn = false;
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
handleExceed(files, fileList) {
|
handleExceed(files, fileList) {
|
||||||
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
|
this.$warning(this.$t('test_track.case.import.upload_limit_count'));
|
||||||
},
|
},
|
||||||
|
clickTabs(tab, event) {
|
||||||
|
this.clickTabsName = tab.name;
|
||||||
|
},
|
||||||
uploadValidate(file) {
|
uploadValidate(file) {
|
||||||
let suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
|
let suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
|
||||||
if (suffix != 'xls' && suffix != 'xlsx') {
|
if (suffix != 'xls' && suffix != 'xlsx') {
|
||||||
|
@ -179,11 +215,13 @@
|
||||||
removeGoBackListener(this.close);
|
removeGoBackListener(this.close);
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.fileList = [];
|
this.fileList = [];
|
||||||
|
this.showExcelImportContinueBtn = false;
|
||||||
|
this.showXmindImportContinueBtn = false;
|
||||||
this.errList = [];
|
this.errList = [];
|
||||||
this.xmindErrList = [];
|
this.xmindErrList = [];
|
||||||
|
|
||||||
//通过excel导入更新过数据的话就刷新页面
|
//通过excel导入更新过数据的话就刷新页面
|
||||||
if (this.isUpdated === true){
|
if (this.isUpdated === true) {
|
||||||
this.$emit("refreshAll");
|
this.$emit("refreshAll");
|
||||||
this.isUpdated = false;
|
this.isUpdated = false;
|
||||||
}
|
}
|
||||||
|
@ -202,8 +240,10 @@
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
upload(file) {
|
upload(file) {
|
||||||
this.isLoading = false;
|
this.lastExcelFile = file.file;
|
||||||
this.fileList.push(file.file);
|
this.fileList.push(file.file);
|
||||||
|
this.isLoading = false;
|
||||||
|
|
||||||
let user = JSON.parse(localStorage.getItem(TokenKey));
|
let user = JSON.parse(localStorage.getItem(TokenKey));
|
||||||
|
|
||||||
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
|
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
|
||||||
|
@ -212,18 +252,57 @@
|
||||||
this.$success(this.$t('test_track.case.import.success'));
|
this.$success(this.$t('test_track.case.import.success'));
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.$emit("refreshAll");
|
this.$emit("refreshAll");
|
||||||
|
this.lastXmindFile = null;
|
||||||
|
this.lastExcelFile = null;
|
||||||
|
this.showExcelImportContinueBtn = false;
|
||||||
|
this.showXmindImportContinueBtn = false;
|
||||||
} else {
|
} else {
|
||||||
this.errList = res.errList;
|
this.errList = res.errList;
|
||||||
this.isUpdated = res.isUpdated;
|
this.isUpdated = res.isUpdated;
|
||||||
|
this.showExcelImportContinueBtn = true;
|
||||||
}
|
}
|
||||||
this.fileList = [];
|
this.fileList = [];
|
||||||
}, erro => {
|
}, erro => {
|
||||||
this.fileList = [];
|
this.fileList = [];
|
||||||
|
this.lastXmindFile = null;
|
||||||
|
this.lastExcelFile = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
uploadContinue(isImportXmind) {
|
||||||
|
this.isLoading = false;
|
||||||
|
let user = JSON.parse(localStorage.getItem(TokenKey));
|
||||||
|
let file = null;
|
||||||
|
if (isImportXmind) {
|
||||||
|
this.uploadXmindIgnoreError = true;
|
||||||
|
file = this.lastXmindFile;
|
||||||
|
} else {
|
||||||
|
this.uploadIgnoreError = true;
|
||||||
|
file = this.lastExcelFile;
|
||||||
|
}
|
||||||
|
this.result = this.$fileUpload('/test/case/importIgnoreError/' + this.projectId + '/' + user.id, file, null, {}, response => {
|
||||||
|
let res = response.data;
|
||||||
|
this.$success(this.$t('test_track.case.import.success'));
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.$emit("refreshAll");
|
||||||
|
this.fileList = [];
|
||||||
|
this.lastXmindFile = null;
|
||||||
|
this.lastExcelFile = null;
|
||||||
|
this.showExcelImportContinueBtn = false;
|
||||||
|
this.showXmindImportContinueBtn = false;
|
||||||
|
this.uploadIgnoreError = false;
|
||||||
|
this.uploadXmindIgnoreError = false;
|
||||||
|
}, erro => {
|
||||||
|
this.fileList = [];
|
||||||
|
this.lastXmindFile = null;
|
||||||
|
this.lastExcelFile = null;
|
||||||
|
this.uploadIgnoreError = false;
|
||||||
|
this.uploadXmindIgnoreError = false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
uploadXmind(file) {
|
uploadXmind(file) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.fileList.push(file.file);
|
this.fileList.push(file.file);
|
||||||
|
this.lastXmindFile = file.file;
|
||||||
let user = JSON.parse(localStorage.getItem(TokenKey));
|
let user = JSON.parse(localStorage.getItem(TokenKey));
|
||||||
|
|
||||||
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
|
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
|
||||||
|
@ -232,8 +311,13 @@
|
||||||
this.$success(this.$t('test_track.case.import.success'));
|
this.$success(this.$t('test_track.case.import.success'));
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.$emit("refreshAll");
|
this.$emit("refreshAll");
|
||||||
|
this.lastXmindFile = null;
|
||||||
|
this.lastExcelFile = null;
|
||||||
|
this.showExcelImportContinueBtn = false;
|
||||||
|
this.showXmindImportContinueBtn = false;
|
||||||
} else {
|
} else {
|
||||||
this.xmindErrList = res.errList;
|
this.xmindErrList = res.errList;
|
||||||
|
this.showXmindImportContinueBtn = true;
|
||||||
}
|
}
|
||||||
this.fileList = [];
|
this.fileList = [];
|
||||||
}, erro => {
|
}, erro => {
|
||||||
|
|
|
@ -1342,6 +1342,8 @@ export default {
|
||||||
xmind_title: "Xmind",
|
xmind_title: "Xmind",
|
||||||
import_desc: "Import instructions",
|
import_desc: "Import instructions",
|
||||||
import_file: "upload files",
|
import_file: "upload files",
|
||||||
|
ignore_error: "Ignore errors ",
|
||||||
|
continue_upload: "Upload continue",
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
export: "Export cases"
|
export: "Export cases"
|
||||||
|
|
|
@ -1347,6 +1347,8 @@ export default {
|
||||||
xmind_title: "思维导图",
|
xmind_title: "思维导图",
|
||||||
import_desc: "导入说明",
|
import_desc: "导入说明",
|
||||||
import_file: "上传文件",
|
import_file: "上传文件",
|
||||||
|
ignore_error: "忽略错误",
|
||||||
|
continue_upload: "继续上传",
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
export: "导出用例"
|
export: "导出用例"
|
||||||
|
|
|
@ -1347,6 +1347,8 @@ export default {
|
||||||
xmind_title: "思維導圖",
|
xmind_title: "思維導圖",
|
||||||
import_desc: "導入說明",
|
import_desc: "導入說明",
|
||||||
import_file: "上傳文件",
|
import_file: "上傳文件",
|
||||||
|
ignore_error: "忽略錯誤",
|
||||||
|
continue_upload: "繼續上傳",
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
export: "導出用例"
|
export: "導出用例"
|
||||||
|
|
Loading…
Reference in New Issue