feat(测试跟踪): 支持思维导图导入用例
This commit is contained in:
parent
d71969b2fa
commit
76b09b4751
|
@ -297,6 +297,31 @@
|
||||||
<version>0.15.2</version>
|
<version>0.15.2</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-compress</artifactId>
|
||||||
|
<version>1.20</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.dom4j</groupId>
|
||||||
|
<artifactId>dom4j</artifactId>
|
||||||
|
<version>2.1.3</version>
|
||||||
|
</dependency>
|
||||||
|
<!--xpath不加这个依赖会报错-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>jaxen</groupId>
|
||||||
|
<artifactId>jaxen</artifactId>
|
||||||
|
<version>1.2.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.json</groupId>
|
||||||
|
<artifactId>json</artifactId>
|
||||||
|
<version>20171018</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|
|
@ -2,9 +2,10 @@ package io.metersphere.base.mapper;
|
||||||
|
|
||||||
import io.metersphere.base.domain.TestCaseNode;
|
import io.metersphere.base.domain.TestCaseNode;
|
||||||
import io.metersphere.base.domain.TestCaseNodeExample;
|
import io.metersphere.base.domain.TestCaseNodeExample;
|
||||||
import java.util.List;
|
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public interface TestCaseNodeMapper {
|
public interface TestCaseNodeMapper {
|
||||||
long countByExample(TestCaseNodeExample example);
|
long countByExample(TestCaseNodeExample example);
|
||||||
|
|
||||||
|
@ -14,6 +15,9 @@ public interface TestCaseNodeMapper {
|
||||||
|
|
||||||
int insert(TestCaseNode record);
|
int insert(TestCaseNode record);
|
||||||
|
|
||||||
|
int insertBatch(@Param("records") List<TestCaseNode> records);
|
||||||
|
|
||||||
|
|
||||||
int insertSelective(TestCaseNode record);
|
int insertSelective(TestCaseNode record);
|
||||||
|
|
||||||
List<TestCaseNode> selectByExample(TestCaseNodeExample example);
|
List<TestCaseNode> selectByExample(TestCaseNodeExample example);
|
||||||
|
|
|
@ -101,6 +101,19 @@
|
||||||
<include refid="Example_Where_Clause" />
|
<include refid="Example_Where_Clause" />
|
||||||
</if>
|
</if>
|
||||||
</delete>
|
</delete>
|
||||||
|
|
||||||
|
<insert id="insertBatch" parameterType="io.metersphere.base.domain.TestCaseNode">
|
||||||
|
insert into test_case_node (id, project_id, name,
|
||||||
|
parent_id, level, create_time,
|
||||||
|
update_time)
|
||||||
|
values
|
||||||
|
<foreach collection="records" item="emp" separator=",">
|
||||||
|
(#{emp.id,jdbcType=VARCHAR}, #{emp.projectId,jdbcType=VARCHAR}, #{emp.name,jdbcType=VARCHAR},
|
||||||
|
#{emp.parentId,jdbcType=VARCHAR}, #{emp.level,jdbcType=INTEGER}, #{emp.createTime,jdbcType=BIGINT},
|
||||||
|
#{emp.updateTime,jdbcType=BIGINT})
|
||||||
|
</foreach>
|
||||||
|
</insert>
|
||||||
|
|
||||||
<insert id="insert" parameterType="io.metersphere.base.domain.TestCaseNode">
|
<insert id="insert" parameterType="io.metersphere.base.domain.TestCaseNode">
|
||||||
insert into test_case_node (id, project_id, name,
|
insert into test_case_node (id, project_id, name,
|
||||||
parent_id, level, create_time,
|
parent_id, level, create_time,
|
||||||
|
@ -109,6 +122,7 @@
|
||||||
#{parentId,jdbcType=VARCHAR}, #{level,jdbcType=INTEGER}, #{createTime,jdbcType=BIGINT},
|
#{parentId,jdbcType=VARCHAR}, #{level,jdbcType=INTEGER}, #{createTime,jdbcType=BIGINT},
|
||||||
#{updateTime,jdbcType=BIGINT})
|
#{updateTime,jdbcType=BIGINT})
|
||||||
</insert>
|
</insert>
|
||||||
|
|
||||||
<insert id="insertSelective" parameterType="io.metersphere.base.domain.TestCaseNode">
|
<insert id="insertSelective" parameterType="io.metersphere.base.domain.TestCaseNode">
|
||||||
insert into test_case_node
|
insert into test_case_node
|
||||||
<trim prefix="(" suffix=")" suffixOverrides=",">
|
<trim prefix="(" suffix=")" suffixOverrides=",">
|
||||||
|
|
|
@ -99,10 +99,10 @@ public class TestCaseController {
|
||||||
return testCaseService.deleteTestCase(testCaseId);
|
return testCaseService.deleteTestCase(testCaseId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/import/{projectId}")
|
@PostMapping("/import/{projectId}/{userId}")
|
||||||
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
||||||
public ExcelResponse testCaseImport(MultipartFile file, @PathVariable String projectId) throws NoSuchFieldException {
|
public ExcelResponse testCaseImport(MultipartFile file, @PathVariable String projectId,@PathVariable String userId) throws NoSuchFieldException {
|
||||||
return testCaseService.testCaseImport(file, projectId);
|
return testCaseService.testCaseImport(file, projectId,userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/export/template")
|
@GetMapping("/export/template")
|
||||||
|
@ -110,6 +110,11 @@ public class TestCaseController {
|
||||||
public void testCaseTemplateExport(HttpServletResponse response) {
|
public void testCaseTemplateExport(HttpServletResponse response) {
|
||||||
testCaseService.testCaseTemplateExport(response);
|
testCaseService.testCaseTemplateExport(response);
|
||||||
}
|
}
|
||||||
|
@GetMapping("/export/xmindTemplate")
|
||||||
|
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
||||||
|
public void xmindTemplate(HttpServletResponse response) {
|
||||||
|
testCaseService.testCaseXmindTemplateExport(response);
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/export/testcase")
|
@PostMapping("/export/testcase")
|
||||||
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
|
||||||
|
|
|
@ -27,6 +27,7 @@ import io.metersphere.i18n.Translator;
|
||||||
import io.metersphere.track.dto.TestCaseDTO;
|
import io.metersphere.track.dto.TestCaseDTO;
|
||||||
import io.metersphere.track.request.testcase.QueryTestCaseRequest;
|
import io.metersphere.track.request.testcase.QueryTestCaseRequest;
|
||||||
import io.metersphere.track.request.testcase.TestCaseBatchRequest;
|
import io.metersphere.track.request.testcase.TestCaseBatchRequest;
|
||||||
|
import io.metersphere.xmind.XmindToTestCaseParser;
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.ibatis.session.ExecutorType;
|
import org.apache.ibatis.session.ExecutorType;
|
||||||
import org.apache.ibatis.session.SqlSession;
|
import org.apache.ibatis.session.SqlSession;
|
||||||
|
@ -38,6 +39,8 @@ import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import javax.annotation.Resource;
|
import javax.annotation.Resource;
|
||||||
import javax.servlet.http.HttpServletResponse;
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.*;
|
||||||
|
import java.net.URLEncoder;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.concurrent.atomic.AtomicInteger;
|
import java.util.concurrent.atomic.AtomicInteger;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
@ -67,9 +70,6 @@ public class TestCaseService {
|
||||||
@Resource
|
@Resource
|
||||||
TestCaseNodeService testCaseNodeService;
|
TestCaseNodeService testCaseNodeService;
|
||||||
|
|
||||||
@Resource
|
|
||||||
UserMapper userMapper;
|
|
||||||
|
|
||||||
@Resource
|
@Resource
|
||||||
UserRoleMapper userRoleMapper;
|
UserRoleMapper userRoleMapper;
|
||||||
|
|
||||||
|
@ -236,10 +236,10 @@ public class TestCaseService {
|
||||||
return projectMapper.selectByPrimaryKey(testCaseWithBLOBs.getProjectId());
|
return projectMapper.selectByPrimaryKey(testCaseWithBLOBs.getProjectId());
|
||||||
}
|
}
|
||||||
|
|
||||||
public ExcelResponse testCaseImport(MultipartFile file, String projectId) {
|
|
||||||
|
public ExcelResponse testCaseImport(MultipartFile multipartFile, String projectId, String userId) {
|
||||||
|
|
||||||
ExcelResponse excelResponse = new ExcelResponse();
|
ExcelResponse excelResponse = new ExcelResponse();
|
||||||
|
|
||||||
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
|
String currentWorkspaceId = SessionUtils.getCurrentWorkspaceId();
|
||||||
QueryTestCaseRequest queryTestCaseRequest = new QueryTestCaseRequest();
|
QueryTestCaseRequest queryTestCaseRequest = new QueryTestCaseRequest();
|
||||||
queryTestCaseRequest.setProjectId(projectId);
|
queryTestCaseRequest.setProjectId(projectId);
|
||||||
|
@ -247,7 +247,25 @@ public class TestCaseService {
|
||||||
Set<String> testCaseNames = testCases.stream()
|
Set<String> testCaseNames = testCases.stream()
|
||||||
.map(TestCase::getName)
|
.map(TestCase::getName)
|
||||||
.collect(Collectors.toSet());
|
.collect(Collectors.toSet());
|
||||||
|
List<ExcelErrData<TestCaseExcelData>> errList = null;
|
||||||
|
|
||||||
|
if (multipartFile.getOriginalFilename().endsWith(".xmind")) {
|
||||||
|
try {
|
||||||
|
errList = new ArrayList<>();
|
||||||
|
String processLog = new XmindToTestCaseParser(this, userId, projectId, testCaseNames).importXmind(multipartFile);
|
||||||
|
if (!StringUtils.isEmpty(processLog)) {
|
||||||
|
excelResponse.setSuccess(false);
|
||||||
|
ExcelErrData excelErrData = new ExcelErrData(null, 1, processLog);
|
||||||
|
errList.add(excelErrData);
|
||||||
|
excelResponse.setErrList(errList);
|
||||||
|
} else {
|
||||||
|
excelResponse.setSuccess(true);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
UserRoleExample userRoleExample = new UserRoleExample();
|
UserRoleExample userRoleExample = new UserRoleExample();
|
||||||
userRoleExample.createCriteria()
|
userRoleExample.createCriteria()
|
||||||
.andRoleIdIn(Arrays.asList(RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER))
|
.andRoleIdIn(Arrays.asList(RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER))
|
||||||
|
@ -256,10 +274,9 @@ public class TestCaseService {
|
||||||
Set<String> userIds = userRoleMapper.selectByExample(userRoleExample).stream().map(UserRole::getUserId).collect(Collectors.toSet());
|
Set<String> userIds = userRoleMapper.selectByExample(userRoleExample).stream().map(UserRole::getUserId).collect(Collectors.toSet());
|
||||||
|
|
||||||
EasyExcelListener easyExcelListener = null;
|
EasyExcelListener easyExcelListener = null;
|
||||||
List<ExcelErrData<TestCaseExcelData>> errList = null;
|
|
||||||
try {
|
try {
|
||||||
easyExcelListener = new TestCaseDataListener(this, projectId, testCaseNames, userIds);
|
easyExcelListener = new TestCaseDataListener(this, projectId, testCaseNames, userIds);
|
||||||
EasyExcelFactory.read(file.getInputStream(), TestCaseExcelData.class, easyExcelListener).sheet().doRead();
|
EasyExcelFactory.read(multipartFile.getInputStream(), TestCaseExcelData.class, easyExcelListener).sheet().doRead();
|
||||||
errList = easyExcelListener.getErrList();
|
errList = easyExcelListener.getErrList();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LogUtil.error(e.getMessage(), e);
|
LogUtil.error(e.getMessage(), e);
|
||||||
|
@ -267,6 +284,8 @@ public class TestCaseService {
|
||||||
} finally {
|
} finally {
|
||||||
easyExcelListener.close();
|
easyExcelListener.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
//如果包含错误信息就导出错误信息
|
//如果包含错误信息就导出错误信息
|
||||||
if (!errList.isEmpty()) {
|
if (!errList.isEmpty()) {
|
||||||
excelResponse.setSuccess(false);
|
excelResponse.setSuccess(false);
|
||||||
|
@ -309,6 +328,35 @@ public class TestCaseService {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void download(HttpServletResponse res) throws IOException {
|
||||||
|
// 发送给客户端的数据
|
||||||
|
OutputStream outputStream = res.getOutputStream();
|
||||||
|
byte[] buff = new byte[1024];
|
||||||
|
// 读取filename
|
||||||
|
String filePath = ClassLoader.getSystemResource("template/testcase.xmind").getPath();
|
||||||
|
try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream(new File(filePath)));) {
|
||||||
|
int i = bis.read(buff);
|
||||||
|
while (i != -1) {
|
||||||
|
outputStream.write(buff, 0, buff.length);
|
||||||
|
outputStream.flush();
|
||||||
|
i = bis.read(buff);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
LogUtil.error(ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void testCaseXmindTemplateExport(HttpServletResponse response) {
|
||||||
|
try {
|
||||||
|
response.setContentType("application/vnd.ms-excel");
|
||||||
|
response.setCharacterEncoding("utf-8");
|
||||||
|
response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode("思维导图用例模版", "UTF-8") + ".xmind");
|
||||||
|
download(response);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private List<TestCaseExcelData> generateExportTemplate() {
|
private List<TestCaseExcelData> generateExportTemplate() {
|
||||||
List<TestCaseExcelData> list = new ArrayList<>();
|
List<TestCaseExcelData> list = new ArrayList<>();
|
||||||
StringBuilder path = new StringBuilder("");
|
StringBuilder path = new StringBuilder("");
|
||||||
|
|
|
@ -0,0 +1,248 @@
|
||||||
|
package io.metersphere.xmind;
|
||||||
|
|
||||||
|
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.LogUtil;
|
||||||
|
import io.metersphere.excel.domain.TestCaseExcelData;
|
||||||
|
import io.metersphere.i18n.Translator;
|
||||||
|
import io.metersphere.track.service.TestCaseService;
|
||||||
|
import io.metersphere.xmind.parser.XmindParser;
|
||||||
|
import io.metersphere.xmind.parser.domain.Attached;
|
||||||
|
import io.metersphere.xmind.parser.domain.JsonRootBean;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.OutputStream;
|
||||||
|
import java.util.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据转换
|
||||||
|
*/
|
||||||
|
public class XmindToTestCaseParser {
|
||||||
|
|
||||||
|
private TestCaseService testCaseService;
|
||||||
|
private String maintainer;
|
||||||
|
private String projectId;
|
||||||
|
private StringBuffer process; // 过程校验记录
|
||||||
|
private Set<String> testCaseNames;
|
||||||
|
|
||||||
|
public XmindToTestCaseParser(TestCaseService testCaseService, String userId, String projectId, Set<String> testCaseNames) {
|
||||||
|
this.testCaseService = testCaseService;
|
||||||
|
this.maintainer = userId;
|
||||||
|
this.projectId = projectId;
|
||||||
|
this.testCaseNames = testCaseNames;
|
||||||
|
testCaseWithBLOBs = new LinkedList<>();
|
||||||
|
xmindDataList = new ArrayList<>();
|
||||||
|
process = new StringBuffer();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 案例详情
|
||||||
|
private List<TestCaseWithBLOBs> testCaseWithBLOBs;
|
||||||
|
// 用于重复对比
|
||||||
|
protected List<TestCaseExcelData> xmindDataList;
|
||||||
|
|
||||||
|
// 递归处理案例数据
|
||||||
|
private void makeXmind(StringBuffer processBuffer, String nodeId, int level, String nodePath, List<Attached> attacheds) {
|
||||||
|
for (Attached item : attacheds) {
|
||||||
|
if (!StringUtils.isEmpty(item.getTitle()) && item.getTitle().startsWith("tc")) { // 用例
|
||||||
|
this.newTestCase(item.getTitle(), nodePath, item.getChildren() != null ? item.getChildren().getAttached() : null);
|
||||||
|
} else {
|
||||||
|
nodePath = nodePath + "/" + item.getTitle();
|
||||||
|
if (item.getChildren() != null && !item.getChildren().getAttached().isEmpty())
|
||||||
|
makeXmind(processBuffer, nodeId, level + 1, nodePath, item.getChildren().getAttached());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取步骤数据
|
||||||
|
public String getSteps(List<Attached> attacheds) {
|
||||||
|
JSONArray jsonArray = new JSONArray();
|
||||||
|
for (int i = 0; i < attacheds.size(); i++) {
|
||||||
|
// 保持插入顺序,判断用例是否有相同的steps
|
||||||
|
JSONObject step = new JSONObject(true);
|
||||||
|
step.put("num", i + 1);
|
||||||
|
step.put("desc", attacheds.get(i).getTitle());
|
||||||
|
if (attacheds.get(i).getChildren() != null && !attacheds.get(i).getChildren().getAttached().isEmpty()) {
|
||||||
|
step.put("result", attacheds.get(i).getChildren().getAttached().get(0).getTitle());
|
||||||
|
}
|
||||||
|
jsonArray.add(step);
|
||||||
|
}
|
||||||
|
return jsonArray.toJSONString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void newTestCase(String title, String nodePath, List<Attached> attacheds) {
|
||||||
|
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
|
||||||
|
testCase.setProjectId(projectId);
|
||||||
|
testCase.setMaintainer(maintainer);
|
||||||
|
testCase.setPriority("P0");
|
||||||
|
testCase.setMethod("manual");
|
||||||
|
testCase.setType("functional");
|
||||||
|
|
||||||
|
String tc = title.replace(":", ":");
|
||||||
|
String tcArr[] = tc.split(":");
|
||||||
|
if (tcArr.length != 2) {
|
||||||
|
process.append(Translator.get("test_case_name") + "【 " + title + " 】" + Translator.get("incorrect_format"));
|
||||||
|
}
|
||||||
|
// 用例名称
|
||||||
|
testCase.setName(tcArr[1].replace("tc:|tc:", ""));
|
||||||
|
|
||||||
|
if (!nodePath.startsWith("/")) {
|
||||||
|
nodePath = "/" + nodePath;
|
||||||
|
}
|
||||||
|
if (nodePath.endsWith("/")) {
|
||||||
|
nodePath = nodePath.substring(0, nodePath.length() - 1);
|
||||||
|
}
|
||||||
|
testCase.setNodePath(nodePath);
|
||||||
|
|
||||||
|
// 用例等级和用例性质处理
|
||||||
|
if (tcArr[0].indexOf("-") != -1) {
|
||||||
|
String otArr[] = tcArr[0].split("-");
|
||||||
|
for (int i = 0; i < otArr.length; i++) {
|
||||||
|
if (otArr[i].startsWith("P") || otArr[i].startsWith("p")) {
|
||||||
|
testCase.setPriority(otArr[i]);
|
||||||
|
} else if (otArr[i].endsWith("功能测试")) {
|
||||||
|
testCase.setType("functional");
|
||||||
|
} else if (otArr[i].endsWith("性能测试")) {
|
||||||
|
testCase.setType("performance");
|
||||||
|
} else if (otArr[i].endsWith("接口测试")) {
|
||||||
|
testCase.setType("api");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 测试步骤处理
|
||||||
|
if (attacheds != null && !attacheds.isEmpty()) {
|
||||||
|
List<Attached> steps = new LinkedList<>();
|
||||||
|
attacheds.forEach(item -> {
|
||||||
|
if (item.getTitle().startsWith("pc")) {
|
||||||
|
testCase.setPrerequisite(item.getTitle().replaceAll("(?:pc:|pc:)", ""));
|
||||||
|
} else if (item.getTitle().startsWith("rc")) {
|
||||||
|
testCase.setRemark(item.getTitle().replaceAll("(?:rc:|rc:)", ""));
|
||||||
|
} else {
|
||||||
|
steps.add(item);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!steps.isEmpty()) {
|
||||||
|
testCase.setSteps(this.getSteps(steps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
TestCaseExcelData compartData = new TestCaseExcelData();
|
||||||
|
BeanUtils.copyBean(compartData, testCase);
|
||||||
|
if (xmindDataList.contains(compartData)) {
|
||||||
|
process.append(Translator.get("test_case_already_exists_excel") + ":" + testCase.getName() + "; ");
|
||||||
|
} else if (validate(testCase)) {
|
||||||
|
testCase.setId(UUID.randomUUID().toString());
|
||||||
|
testCase.setCreateTime(System.currentTimeMillis());
|
||||||
|
testCase.setUpdateTime(System.currentTimeMillis());
|
||||||
|
testCaseWithBLOBs.add(testCase);
|
||||||
|
}
|
||||||
|
xmindDataList.add(compartData);
|
||||||
|
}
|
||||||
|
|
||||||
|
//获取流文件
|
||||||
|
private static void inputStreamToFile(InputStream ins, File file) {
|
||||||
|
try (OutputStream os = new FileOutputStream(file);) {
|
||||||
|
int bytesRead = 0;
|
||||||
|
byte[] buffer = new byte[8192];
|
||||||
|
while ((bytesRead = ins.read(buffer, 0, 8192)) != -1) {
|
||||||
|
os.write(buffer, 0, bytesRead);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
LogUtil.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MultipartFile 转 File
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @throws Exception
|
||||||
|
*/
|
||||||
|
private File multipartFileToFile(MultipartFile file) throws Exception {
|
||||||
|
if (file != null && file.getSize() > 0) {
|
||||||
|
try (InputStream ins = file.getInputStream();) {
|
||||||
|
File toFile = new File(file.getOriginalFilename());
|
||||||
|
inputStreamToFile(ins, toFile);
|
||||||
|
return toFile;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public boolean validate(TestCaseWithBLOBs data) {
|
||||||
|
String nodePath = data.getNodePath();
|
||||||
|
StringBuilder stringBuilder = new StringBuilder();
|
||||||
|
|
||||||
|
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 && org.apache.commons.lang3.StringUtils.equals(nodes[i].trim(), "")) {
|
||||||
|
stringBuilder.append(Translator.get("module_not_null") + "; ");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (org.apache.commons.lang3.StringUtils.equals(data.getType(), TestCaseConstants.Type.Functional.getValue()) && org.apache.commons.lang3.StringUtils.equals(data.getMethod(), TestCaseConstants.Method.Auto.getValue())) {
|
||||||
|
stringBuilder.append(Translator.get("functional_method_tip") + "; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (testCaseNames.contains(data.getName())) {
|
||||||
|
boolean dbExist = testCaseService.exist(data);
|
||||||
|
boolean excelExist = false;
|
||||||
|
|
||||||
|
if (dbExist) {
|
||||||
|
// db exist
|
||||||
|
stringBuilder.append(Translator.get("test_case_already_exists_excel") + ":" + data.getName() + "; ");
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
testCaseNames.add(data.getName());
|
||||||
|
}
|
||||||
|
if (!StringUtils.isEmpty(stringBuilder.toString())) {
|
||||||
|
process.append(stringBuilder.toString());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String importXmind(MultipartFile multipartFile) {
|
||||||
|
StringBuffer processBuffer = new StringBuffer();
|
||||||
|
try {
|
||||||
|
File file = multipartFileToFile(multipartFile);
|
||||||
|
JsonRootBean root = XmindParser.parseObject(file);
|
||||||
|
if (root != null && root.getRootTopic() != null && root.getRootTopic().getChildren() != null) {
|
||||||
|
// 判断是模块还是用例
|
||||||
|
root.getRootTopic().getChildren().getAttached().forEach(item -> {
|
||||||
|
if (!StringUtils.isEmpty(item.getTitle()) && item.getTitle().startsWith("tc")) { // 用例
|
||||||
|
this.newTestCase(item.getTitle(), "", item.getChildren() != null ? item.getChildren().getAttached() : null);
|
||||||
|
} else {
|
||||||
|
item.setPath(item.getTitle());
|
||||||
|
if (item.getChildren() != null && !item.getChildren().getAttached().isEmpty())
|
||||||
|
makeXmind(processBuffer, null, 1, item.getPath(), item.getChildren().getAttached());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (StringUtils.isEmpty(process.toString()) && !testCaseWithBLOBs.isEmpty()) {
|
||||||
|
testCaseService.saveImportData(testCaseWithBLOBs, projectId);
|
||||||
|
}
|
||||||
|
} catch (Exception ex) {
|
||||||
|
processBuffer.append(Translator.get("incorrect_format"));
|
||||||
|
LogUtil.error(ex.getMessage());
|
||||||
|
ex.printStackTrace();
|
||||||
|
} finally {
|
||||||
|
testCaseWithBLOBs.clear();
|
||||||
|
}
|
||||||
|
return process.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,79 @@
|
||||||
|
package io.metersphere.xmind.parser;
|
||||||
|
|
||||||
|
import org.dom4j.*;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
import org.json.XML;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public class XmindLegacy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回content.xml和comments.xml合并后的json
|
||||||
|
*
|
||||||
|
* @param xmlContent
|
||||||
|
* @param xmlComments
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @throws DocumentException
|
||||||
|
*/
|
||||||
|
public static String getContent(String xmlContent, String xmlComments) throws IOException, DocumentException {
|
||||||
|
// 删除content.xml里面不能识别的字符串
|
||||||
|
xmlContent = xmlContent.replace("xmlns=\"urn:xmind:xmap:xmlns:content:2.0\"", "");
|
||||||
|
xmlContent = xmlContent.replace("xmlns:fo=\"http://www.w3.org/1999/XSL/Format\"", "");
|
||||||
|
// 删除<topic>节点
|
||||||
|
xmlContent = xmlContent.replace("<topics type=\"attached\">", "");
|
||||||
|
xmlContent = xmlContent.replace("</topics>", "");
|
||||||
|
|
||||||
|
// 去除title中svg:width属性
|
||||||
|
xmlContent = xmlContent.replaceAll("<title svg:width=\"[0-9]*\">", "<title>");
|
||||||
|
|
||||||
|
Document document = DocumentHelper.parseText(xmlContent);// 读取XML文件,获得document对象
|
||||||
|
Element root = document.getRootElement();
|
||||||
|
List<Node> topics = root.selectNodes("//topic");
|
||||||
|
|
||||||
|
if (xmlComments != null) {
|
||||||
|
// 删除comments.xml里面不能识别的字符串
|
||||||
|
xmlComments = xmlComments.replace("xmlns=\"urn:xmind:xmap:xmlns:comments:2.0\"", "");
|
||||||
|
|
||||||
|
// 添加评论到content中
|
||||||
|
Document commentDocument = DocumentHelper.parseText(xmlComments);
|
||||||
|
List<Node> commentsList = commentDocument.selectNodes("//comment");
|
||||||
|
|
||||||
|
for (Node topic : topics) {
|
||||||
|
for (Node commentNode : commentsList) {
|
||||||
|
Element commentElement = (Element) commentNode;
|
||||||
|
Element topicElement = (Element) topic;
|
||||||
|
if (topicElement.attribute("id").getValue()
|
||||||
|
.equals(commentElement.attribute("object-id").getValue())) {
|
||||||
|
Element comment = topicElement.addElement("comments");
|
||||||
|
comment.addAttribute("creationTime", commentElement.attribute("time").getValue());
|
||||||
|
comment.addAttribute("author", commentElement.attribute("author").getValue());
|
||||||
|
comment.addAttribute("content", commentElement.element("content").getText());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 第一个topic转换为json中的rootTopic
|
||||||
|
Node rootTopic = root.selectSingleNode("/xmap-content/sheet/topic");
|
||||||
|
rootTopic.setName("rootTopic");
|
||||||
|
|
||||||
|
// 将xml中topic节点转换为attached节点
|
||||||
|
List<Node> topicList = rootTopic.selectNodes("//topic");
|
||||||
|
|
||||||
|
for (Node node : topicList) {
|
||||||
|
node.setName("attached");
|
||||||
|
}
|
||||||
|
// 选取第一个sheet
|
||||||
|
Element sheet = root.elements("sheet").get(0);
|
||||||
|
String res = sheet.asXML();
|
||||||
|
// 将xml转为json
|
||||||
|
JSONObject xmlJSONObj = XML.toJSONObject(res);
|
||||||
|
JSONObject jsonObject = xmlJSONObj.getJSONObject("sheet");
|
||||||
|
// 设置缩进
|
||||||
|
return jsonObject.toString(4);
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,116 @@
|
||||||
|
package io.metersphere.xmind.parser;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON;
|
||||||
|
import io.metersphere.xmind.parser.domain.JsonRootBean;
|
||||||
|
import org.apache.commons.compress.archivers.ArchiveException;
|
||||||
|
import org.dom4j.DocumentException;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description 解析主体
|
||||||
|
*/
|
||||||
|
public class XmindParser {
|
||||||
|
public static final String xmindZenJson = "content.json";
|
||||||
|
public static final String xmindLegacyContent = "content.xml";
|
||||||
|
public static final String xmindLegacyComments = "comments.xml";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析脑图文件,返回content整合后的内容
|
||||||
|
*
|
||||||
|
* @param file
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ArchiveException
|
||||||
|
* @throws DocumentException
|
||||||
|
*/
|
||||||
|
public static String parseJson(File file) throws IOException, ArchiveException, DocumentException {
|
||||||
|
String res = ZipUtils.extract(file);
|
||||||
|
|
||||||
|
String content = null;
|
||||||
|
if (isXmindZen(res, file)) {
|
||||||
|
content = getXmindZenContent(file, res);
|
||||||
|
} else {
|
||||||
|
content = getXmindLegacyContent(file, res);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除生成的文件夹
|
||||||
|
File dir = new File(res);
|
||||||
|
boolean flag = deleteDir(dir);
|
||||||
|
if (flag) {
|
||||||
|
// do something
|
||||||
|
}
|
||||||
|
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
|
||||||
|
return (JSON.toJSONString(jsonRootBean, false));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static JsonRootBean parseObject(File file) throws DocumentException, ArchiveException, IOException {
|
||||||
|
String content = parseJson(file);
|
||||||
|
JsonRootBean jsonRootBean = JSON.parseObject(content, JsonRootBean.class);
|
||||||
|
return jsonRootBean;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean deleteDir(File dir) {
|
||||||
|
if (dir.isDirectory()) {
|
||||||
|
String[] children = dir.list();
|
||||||
|
// 递归删除目录中的子目录下
|
||||||
|
for (int i = 0; i < children.length; i++) {
|
||||||
|
boolean success = deleteDir(new File(dir, children[i]));
|
||||||
|
if (!success) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 目录此时为空,可以删除
|
||||||
|
return dir.delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String getXmindZenContent(File file, String extractFileDir)
|
||||||
|
throws IOException, ArchiveException {
|
||||||
|
List<String> keys = new ArrayList<>();
|
||||||
|
keys.add(xmindZenJson);
|
||||||
|
Map<String, String> map = ZipUtils.getContents(keys, file, extractFileDir);
|
||||||
|
String content = map.get(xmindZenJson);
|
||||||
|
content = XmindZen.getContent(content);
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public static String getXmindLegacyContent(File file, String extractFileDir)
|
||||||
|
throws IOException, ArchiveException, DocumentException {
|
||||||
|
List<String> keys = new ArrayList<>();
|
||||||
|
keys.add(xmindLegacyContent);
|
||||||
|
keys.add(xmindLegacyComments);
|
||||||
|
Map<String, String> map = ZipUtils.getContents(keys, file, extractFileDir);
|
||||||
|
|
||||||
|
String contentXml = map.get(xmindLegacyContent);
|
||||||
|
String commentsXml = map.get(xmindLegacyComments);
|
||||||
|
String xmlContent = XmindLegacy.getContent(contentXml, commentsXml);
|
||||||
|
|
||||||
|
return xmlContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean isXmindZen(String res, File file) throws IOException, ArchiveException {
|
||||||
|
// 解压
|
||||||
|
File parent = new File(res);
|
||||||
|
if (parent.isDirectory()) {
|
||||||
|
String[] files = parent.list(new ZipUtils.FileFilter());
|
||||||
|
for (int i = 0; i < Objects.requireNonNull(files).length; i++) {
|
||||||
|
if (files[i].equals(xmindZenJson)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package io.metersphere.xmind.parser;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSONArray;
|
||||||
|
import com.alibaba.fastjson.JSONObject;
|
||||||
|
import org.dom4j.DocumentException;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class XmindZen {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param jsonContent
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @throws DocumentException
|
||||||
|
*/
|
||||||
|
public static String getContent(String jsonContent) {
|
||||||
|
JSONObject jsonObject = JSONArray.parseArray(jsonContent).getJSONObject(0);
|
||||||
|
JSONObject rootTopic = jsonObject.getJSONObject("rootTopic");
|
||||||
|
transferNotes(rootTopic);
|
||||||
|
JSONObject children = rootTopic.getJSONObject("children");
|
||||||
|
recursionChildren(children);
|
||||||
|
return jsonObject.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 递归转换children
|
||||||
|
*
|
||||||
|
* @param children
|
||||||
|
*/
|
||||||
|
private static void recursionChildren(JSONObject children) {
|
||||||
|
if (children == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONArray attachedArray = children.getJSONArray("attached");
|
||||||
|
if (attachedArray == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Object attached : attachedArray) {
|
||||||
|
JSONObject attachedObject = (JSONObject) attached;
|
||||||
|
transferNotes(attachedObject);
|
||||||
|
JSONObject childrenObject = attachedObject.getJSONObject("children");
|
||||||
|
if (childrenObject == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
recursionChildren(childrenObject);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void transferNotes(JSONObject object) {
|
||||||
|
JSONObject notes = object.getJSONObject("notes");
|
||||||
|
if (notes == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject plain = notes.getJSONObject("plain");
|
||||||
|
if (plain != null) {
|
||||||
|
String content = plain.getString("content");
|
||||||
|
notes.remove("plain");
|
||||||
|
notes.put("content", content);
|
||||||
|
} else {
|
||||||
|
notes.put("content", null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,87 @@
|
||||||
|
package io.metersphere.xmind.parser;
|
||||||
|
|
||||||
|
import org.apache.commons.compress.archivers.ArchiveException;
|
||||||
|
import org.apache.commons.compress.archivers.examples.Expander;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Description zip解压工具
|
||||||
|
*/
|
||||||
|
public class ZipUtils {
|
||||||
|
|
||||||
|
private static final String currentPath = System.getProperty("user.dir");
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 找到压缩文件中匹配的子文件,返回的为 getContents("comments.xml, unzip
|
||||||
|
*
|
||||||
|
* @param subFileNames
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
public static Map<String, String> getContents(List<String> subFileNames, File file, String extractFileDir)
|
||||||
|
throws IOException, ArchiveException {
|
||||||
|
String destFilePath = extractFileDir;
|
||||||
|
Map<String, String> map = new HashMap<>();
|
||||||
|
File destFile = new File(destFilePath);
|
||||||
|
if (destFile.isDirectory()) {
|
||||||
|
String[] res = destFile.list(new FileFilter());
|
||||||
|
for (int i = 0; i < Objects.requireNonNull(res).length; i++) {
|
||||||
|
if (subFileNames.contains(res[i])) {
|
||||||
|
String s = destFilePath + File.separator + res[i];
|
||||||
|
String content = getFileContent(s);
|
||||||
|
map.put(res[i], content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 返回解压后的文件夹名字
|
||||||
|
*
|
||||||
|
* @return
|
||||||
|
* @throws IOException
|
||||||
|
* @throws ArchiveException
|
||||||
|
*/
|
||||||
|
public static String extract(File file) throws IOException, ArchiveException {
|
||||||
|
Expander expander = new Expander();
|
||||||
|
String destFileName = currentPath + File.separator + "XMind" + System.currentTimeMillis(); // 目标文件夹名字
|
||||||
|
expander.expand(file, new File(destFileName));
|
||||||
|
return destFileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 这是一个内部类过滤器,策略模式
|
||||||
|
static class FileFilter implements FilenameFilter {
|
||||||
|
@Override
|
||||||
|
public boolean accept(File dir, String name) {
|
||||||
|
// String的 endsWith(String str)方法 筛选出以str结尾的字符串
|
||||||
|
if (name.endsWith(".xml") || name.endsWith(".json")) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getFileContent(String fileName) throws IOException {
|
||||||
|
File file;
|
||||||
|
try {
|
||||||
|
file = new File(fileName);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("找不到该文件");
|
||||||
|
}
|
||||||
|
FileReader fileReader = new FileReader(file);
|
||||||
|
BufferedReader bufferedReder = new BufferedReader(fileReader);
|
||||||
|
StringBuilder stringBuffer = new StringBuilder();
|
||||||
|
while (bufferedReder.ready()) {
|
||||||
|
stringBuffer.append(bufferedReder.readLine());
|
||||||
|
}
|
||||||
|
// 打开的文件需关闭,在unix下可以删除,否则在windows下不能删除(file.delete())
|
||||||
|
bufferedReder.close();
|
||||||
|
fileReader.close();
|
||||||
|
return stringBuffer.toString();
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
|
||||||
|
package io.metersphere.xmind.parser.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Attached {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String title;
|
||||||
|
private Notes notes;
|
||||||
|
private String path;
|
||||||
|
private List<Comments> comments;
|
||||||
|
private Children children;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.metersphere.xmind.parser.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Children {
|
||||||
|
|
||||||
|
private List<Attached> attached;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.metersphere.xmind.parser.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Comments {
|
||||||
|
|
||||||
|
private long creationTime;
|
||||||
|
private String author;
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
package io.metersphere.xmind.parser.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class JsonRootBean {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String title;
|
||||||
|
private RootTopic rootTopic;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
package io.metersphere.xmind.parser.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class Notes {
|
||||||
|
|
||||||
|
private String content;
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package io.metersphere.xmind.parser.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class RootTopic {
|
||||||
|
|
||||||
|
private String id;
|
||||||
|
private String title;
|
||||||
|
private Notes notes;
|
||||||
|
private List<Comments> comments;
|
||||||
|
private Children children;
|
||||||
|
|
||||||
|
}
|
Binary file not shown.
Binary file not shown.
After Width: | Height: | Size: 82 KiB |
|
@ -2,10 +2,15 @@
|
||||||
<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-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')}}</el-link></el-row>
|
>{{$t('test_track.case.import.download_template')}}
|
||||||
|
</el-link>
|
||||||
|
</el-row>
|
||||||
<el-row>
|
<el-row>
|
||||||
<el-upload
|
<el-upload
|
||||||
v-loading="result.loading"
|
v-loading="result.loading"
|
||||||
|
@ -30,6 +35,7 @@
|
||||||
</el-upload>
|
</el-upload>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
|
||||||
|
|
||||||
<el-row>
|
<el-row>
|
||||||
<ul>
|
<ul>
|
||||||
<li v-for="errFile in errList" :key="errFile.rowNum">
|
<li v-for="errFile in errList" :key="errFile.rowNum">
|
||||||
|
@ -37,7 +43,68 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</el-row>
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
<!-- Xmind 导入 -->
|
||||||
|
<el-tab-pane :label="$t('test_track.case.import.xmind_title')" name="xmindImport" style="border: 0px">
|
||||||
|
<el-row class="import-row">
|
||||||
|
<div class="el-step__icon is-text" style="background-color: #C9E6F8;border-color: #C9E6F8;margin-right: 10px">
|
||||||
|
<div class="el-step__icon-inner">1</div>
|
||||||
|
</div>
|
||||||
|
<label class="ms-license-label">{{$t('test_track.case.import.import_desc')}}</label>
|
||||||
|
</el-row>
|
||||||
|
<el-row class="import-row">
|
||||||
|
<el-card :body-style="{ padding: '0px' }">
|
||||||
|
<img src="../../../../../assets/xmind.jpg" width="700px" height="250px"
|
||||||
|
class="image">
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
</el-row>
|
||||||
|
<el-row class="import-row">
|
||||||
|
<div class="el-step__icon is-text"
|
||||||
|
style="background-color: #C9E6F8;border-color: #C9E6F8;margin-right: 10px ">
|
||||||
|
<div class="el-step__icon-inner">2</div>
|
||||||
|
</div>
|
||||||
|
<label class="ms-license-label">{{$t('test_track.case.import.import_file')}}</label>
|
||||||
|
</el-row>
|
||||||
|
<el-row class="import-row">
|
||||||
|
<el-link type="primary" class="download-template"
|
||||||
|
@click="downloadXmindTemplate"
|
||||||
|
>{{$t('test_track.case.import.download_template')}}
|
||||||
|
</el-link>
|
||||||
|
</el-row>
|
||||||
|
<el-row class="import-row">
|
||||||
|
<el-upload
|
||||||
|
v-loading="result.loading"
|
||||||
|
:element-loading-text="$t('test_track.case.import.importing')"
|
||||||
|
element-loading-spinner="el-icon-loading"
|
||||||
|
class="upload-demo"
|
||||||
|
multiple
|
||||||
|
:limit="1"
|
||||||
|
action=""
|
||||||
|
:on-exceed="handleExceed"
|
||||||
|
:beforeUpload="uploadValidateXmind"
|
||||||
|
:on-error="handleError"
|
||||||
|
:show-file-list="false"
|
||||||
|
:http-request="uploadXmind"
|
||||||
|
:file-list="fileList">
|
||||||
|
<template v-slot:trigger>
|
||||||
|
<el-button size="mini" type="success" plain>{{$t('test_track.case.import.click_upload')}}</el-button>
|
||||||
|
</template>
|
||||||
|
<template v-slot:tip>
|
||||||
|
<div class="el-upload__tip">{{$t('test_track.case.import.upload_xmind')}}</div>
|
||||||
|
</template>
|
||||||
|
</el-upload>
|
||||||
|
</el-row>
|
||||||
|
<el-row>
|
||||||
|
<ul>
|
||||||
|
<li v-for="errFile in xmindErrList" :key="errFile.rowNum">
|
||||||
|
{{errFile.errMsg}}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</el-row>
|
||||||
|
</el-tab-pane>
|
||||||
|
|
||||||
|
</el-tabs>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -45,6 +112,7 @@
|
||||||
import ElUploadList from "element-ui/packages/upload/src/upload-list";
|
import ElUploadList from "element-ui/packages/upload/src/upload-list";
|
||||||
import MsTableButton from '../../../../components/common/components/MsTableButton';
|
import MsTableButton from '../../../../components/common/components/MsTableButton';
|
||||||
import {listenGoBack, removeGoBackListener} from "../../../../../common/js/utils";
|
import {listenGoBack, removeGoBackListener} from "../../../../../common/js/utils";
|
||||||
|
import {TokenKey, WORKSPACE_ID} from '../../../../../common/js/constants';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "TestCaseImport",
|
name: "TestCaseImport",
|
||||||
|
@ -52,9 +120,11 @@
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
result: {},
|
result: {},
|
||||||
|
activeName: 'excelImport',
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
fileList: [],
|
fileList: [],
|
||||||
errList: [],
|
errList: [],
|
||||||
|
xmindErrList: [],
|
||||||
isLoading: false
|
isLoading: false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -69,7 +139,7 @@
|
||||||
},
|
},
|
||||||
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' && suffix != 'xmind') {
|
if (suffix != 'xls' && suffix != 'xlsx') {
|
||||||
this.$warning(this.$t('test_track.case.import.upload_limit_format'));
|
this.$warning(this.$t('test_track.case.import.upload_limit_format'));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -80,6 +150,23 @@
|
||||||
}
|
}
|
||||||
this.isLoading = true;
|
this.isLoading = true;
|
||||||
this.errList = [];
|
this.errList = [];
|
||||||
|
this.xmindErrList = [];
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
uploadValidateXmind(file) {
|
||||||
|
let suffix = file.name.substring(file.name.lastIndexOf('.') + 1);
|
||||||
|
if (suffix != 'xmind') {
|
||||||
|
this.$warning(this.$t('test_track.case.import.upload_xmind_format'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size / 1024 / 1024 > 20) {
|
||||||
|
this.$warning(this.$t('test_track.case.import.upload_limit_size'));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
this.isLoading = true;
|
||||||
|
this.errList = [];
|
||||||
|
this.xmindErrList = [];
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
handleError(err, file, fileList) {
|
handleError(err, file, fileList) {
|
||||||
|
@ -95,14 +182,20 @@
|
||||||
this.dialogVisible = false;
|
this.dialogVisible = false;
|
||||||
this.fileList = [];
|
this.fileList = [];
|
||||||
this.errList = [];
|
this.errList = [];
|
||||||
|
this.xmindErrList = [];
|
||||||
},
|
},
|
||||||
downloadTemplate() {
|
downloadTemplate() {
|
||||||
this.$fileDownload('/test/case/export/template');
|
this.$fileDownload('/test/case/export/template');
|
||||||
},
|
},
|
||||||
|
downloadXmindTemplate() {
|
||||||
|
this.$fileDownload('/test/case/export/xmindTemplate');
|
||||||
|
},
|
||||||
upload(file) {
|
upload(file) {
|
||||||
this.isLoading = false;
|
this.isLoading = false;
|
||||||
this.fileList.push(file.file);
|
this.fileList.push(file.file);
|
||||||
this.result = this.$fileUpload('/test/case/import/' + this.projectId, file.file, null, {}, response => {
|
let user = JSON.parse(localStorage.getItem(TokenKey));
|
||||||
|
|
||||||
|
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
|
||||||
let res = response.data;
|
let res = response.data;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
this.$success(this.$t('test_track.case.import.success'));
|
this.$success(this.$t('test_track.case.import.success'));
|
||||||
|
@ -115,6 +208,25 @@
|
||||||
}, erro => {
|
}, erro => {
|
||||||
this.fileList = [];
|
this.fileList = [];
|
||||||
});
|
});
|
||||||
|
},
|
||||||
|
uploadXmind(file) {
|
||||||
|
this.isLoading = false;
|
||||||
|
this.fileList.push(file.file);
|
||||||
|
let user = JSON.parse(localStorage.getItem(TokenKey));
|
||||||
|
|
||||||
|
this.result = this.$fileUpload('/test/case/import/' + this.projectId + '/' + user.id, file.file, null, {}, response => {
|
||||||
|
let res = response.data;
|
||||||
|
if (res.success) {
|
||||||
|
this.$success(this.$t('test_track.case.import.success'));
|
||||||
|
this.dialogVisible = false;
|
||||||
|
this.$emit("refresh");
|
||||||
|
} else {
|
||||||
|
this.xmindErrList = res.errList;
|
||||||
|
}
|
||||||
|
this.fileList = [];
|
||||||
|
}, erro => {
|
||||||
|
this.fileList = [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -130,8 +242,12 @@
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.import-row {
|
||||||
|
padding-top: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
.testcase-import >>> .el-dialog {
|
.testcase-import >>> .el-dialog {
|
||||||
width: 400px;
|
width: 700px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -653,13 +653,19 @@ export default {
|
||||||
case_import: "Import test case",
|
case_import: "Import test case",
|
||||||
download_template: "Download template",
|
download_template: "Download template",
|
||||||
click_upload: "Upload",
|
click_upload: "Upload",
|
||||||
upload_limit: "Only XLS/XLSX files can be uploaded, and no more than 20M",
|
upload_limit: "Only XLS/XLSX/XMIND files can be uploaded, and no more than 20M",
|
||||||
|
upload_xmind_format: "Upload files can only be .xmind format",
|
||||||
|
upload_xmind: "Only xmind files can be uploaded, and no more than 500",
|
||||||
upload_limit_count: "Only one file can be uploaded at a time",
|
upload_limit_count: "Only one file can be uploaded at a time",
|
||||||
upload_limit_format: "Upload files can only be XLS, XLSX format!",
|
upload_limit_format: "Upload files can only be XLS, XLSX format!",
|
||||||
upload_limit_size: "Upload file size cannot exceed 20MB!",
|
upload_limit_size: "Upload file size cannot exceed 20MB!",
|
||||||
upload_limit_other_size: "Upload file size cannot exceed",
|
upload_limit_other_size: "Upload file size cannot exceed",
|
||||||
success: "Import success!",
|
success: "Import success!",
|
||||||
importing: "Importing...",
|
importing: "Importing...",
|
||||||
|
excel_title: "Excel ",
|
||||||
|
xmind_title: "Xmind",
|
||||||
|
import_desc: "Import instructions",
|
||||||
|
import_file: "upload files",
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
export: "Export cases"
|
export: "Export cases"
|
||||||
|
|
|
@ -657,12 +657,18 @@ export default {
|
||||||
download_template: "下载模版",
|
download_template: "下载模版",
|
||||||
click_upload: "点击上传",
|
click_upload: "点击上传",
|
||||||
upload_limit: "只能上传xls/xlsx文件,且不超过20M",
|
upload_limit: "只能上传xls/xlsx文件,且不超过20M",
|
||||||
|
upload_xmind: "支持文件类型:.xmind;一次至多导入500 条用例",
|
||||||
|
upload_xmind_format: "上传文件只能是 .xmind 格式",
|
||||||
upload_limit_other_size: "上传文件大小不能超过",
|
upload_limit_other_size: "上传文件大小不能超过",
|
||||||
upload_limit_count: "一次只能上传一个文件",
|
upload_limit_count: "一次只能上传一个文件",
|
||||||
upload_limit_format: "上传文件只能是 xls、xlsx格式!",
|
upload_limit_format: "上传文件只能是 xls、xlsx格式!",
|
||||||
upload_limit_size: "上传文件大小不能超过 20MB!",
|
upload_limit_size: "上传文件大小不能超过 20MB!",
|
||||||
success: "导入成功!",
|
success: "导入成功!",
|
||||||
importing: "导入中...",
|
importing: "导入中...",
|
||||||
|
excel_title: "表格文件",
|
||||||
|
xmind_title: "思维导图",
|
||||||
|
import_desc: "导入说明",
|
||||||
|
import_file: "上传文件",
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
export: "导出用例"
|
export: "导出用例"
|
||||||
|
|
|
@ -651,13 +651,20 @@ export default {
|
||||||
case_import: "導入測試用例",
|
case_import: "導入測試用例",
|
||||||
download_template: "下載模版",
|
download_template: "下載模版",
|
||||||
click_upload: "點擊上傳",
|
click_upload: "點擊上傳",
|
||||||
upload_limit: "只能上傳xls/xlsx文件,且不超過20M",
|
upload_limit: "只能上傳xls/xlsx/xmind文件,且不超過20M",
|
||||||
|
upload_xmind: "支持文件類型:.xmind;壹次至多導入500 條用例",
|
||||||
|
upload_xmind_format: "上傳文件只能是 .xmind 格式",
|
||||||
upload_limit_count: "一次只能上傳一個文件",
|
upload_limit_count: "一次只能上傳一個文件",
|
||||||
upload_limit_format: "上傳文件只能是 xls、xlsx格式!",
|
upload_limit_format: "上傳文件只能是 xls、xlsx格式!",
|
||||||
upload_limit_size: "上傳文件大小不能超過 20MB!",
|
upload_limit_size: "上傳文件大小不能超過 20MB!",
|
||||||
upload_limit_other_size: "上傳文件大小不能超過",
|
upload_limit_other_size: "上傳文件大小不能超過",
|
||||||
success: "導入成功!",
|
success: "導入成功!",
|
||||||
importing: "導入中...",
|
importing: "導入中...",
|
||||||
|
excel_title: "表格文件",
|
||||||
|
xmind_title: "思維導圖",
|
||||||
|
import_desc: "導入說明",
|
||||||
|
import_file: "上傳文件",
|
||||||
|
|
||||||
},
|
},
|
||||||
export: {
|
export: {
|
||||||
export: "導出用例"
|
export: "導出用例"
|
||||||
|
|
Loading…
Reference in New Issue