feat(接口定义): 初步页面完成

This commit is contained in:
fit2-zhao 2020-11-02 16:21:30 +08:00
parent 32f130c05e
commit 0c89fd3c14
52 changed files with 8113 additions and 14 deletions

View File

@ -0,0 +1,56 @@
package io.metersphere.api.controller;
import io.metersphere.api.dto.delimit.ApiModuleDTO;
import io.metersphere.api.dto.delimit.DragModuleRequest;
import io.metersphere.api.service.ApiModuleService;
import io.metersphere.base.domain.ApiModule;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.service.CheckOwnerService;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
@RequestMapping("/api/module")
@RestController
@RequiresRoles(value = {RoleConstants.ADMIN, RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER, RoleConstants.ORG_ADMIN}, logical = Logical.OR)
public class ApiModuleController {
@Resource
ApiModuleService apiModuleService;
@Resource
private CheckOwnerService checkOwnerService;
@GetMapping("/list/{projectId}")
public List<ApiModuleDTO> getNodeByProjectId(@PathVariable String projectId,@PathVariable String protocol) {
checkOwnerService.checkProjectOwner(projectId);
return apiModuleService.getNodeTreeByProjectId(projectId);
}
@PostMapping("/add")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public String addNode(@RequestBody ApiModule node) {
return apiModuleService.addNode(node);
}
@PostMapping("/edit")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public int editNode(@RequestBody DragModuleRequest node) {
return apiModuleService.editNode(node);
}
@PostMapping("/delete")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public int deleteNode(@RequestBody List<String> nodeIds) {
//nodeIds 包含删除节点ID及其所有子节点ID
return apiModuleService.deleteNode(nodeIds);
}
@PostMapping("/drag")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public void dragNode(@RequestBody DragModuleRequest node) {
apiModuleService.dragNode(node);
}
}

View File

@ -0,0 +1,14 @@
package io.metersphere.api.dto.delimit;
import io.metersphere.base.domain.TestCaseWithBLOBs;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class ApiDTO extends TestCaseWithBLOBs {
private String maintainerName;
private String apiName;
private String performName;
}

View File

@ -0,0 +1,16 @@
package io.metersphere.api.dto.delimit;
import io.metersphere.base.domain.ApiModule;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class ApiModuleDTO extends ApiModule {
private String label;
private List<ApiModuleDTO> children;
}

View File

@ -0,0 +1,15 @@
package io.metersphere.api.dto.delimit;
import io.metersphere.base.domain.ApiModule;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class DragModuleRequest extends ApiModule {
List<String> nodeIds;
ApiModuleDTO nodeTree;
}

View File

@ -0,0 +1,240 @@
package io.metersphere.api.service;
import io.metersphere.api.dto.delimit.ApiModuleDTO;
import io.metersphere.api.dto.delimit.DragModuleRequest;
import io.metersphere.base.domain.ApiModule;
import io.metersphere.base.domain.ApiModuleExample;
import io.metersphere.base.domain.TestCaseExample;
import io.metersphere.base.mapper.ApiModuleMapper;
import io.metersphere.base.mapper.TestCaseMapper;
import io.metersphere.base.mapper.ext.ExtTestCaseMapper;
import io.metersphere.commons.constants.TestCaseConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.i18n.Translator;
import io.metersphere.track.dto.TestCaseDTO;
import io.metersphere.track.request.testcase.QueryTestCaseRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Transactional(rollbackFor = Exception.class)
public class ApiModuleService {
@Resource
ApiModuleMapper apiModuleMapper;
@Resource
ExtTestCaseMapper extTestCaseMapper;
@Resource
SqlSessionFactory sqlSessionFactory;
public List<ApiModuleDTO> getNodeTreeByProjectId(String projectId) {
ApiModuleExample testCaseNodeExample = new ApiModuleExample();
testCaseNodeExample.createCriteria().andProjectIdEqualTo(projectId);
testCaseNodeExample.setOrderByClause("create_time asc");
List<ApiModule> nodes = apiModuleMapper.selectByExample(testCaseNodeExample);
return getNodeTrees(nodes);
}
public List<ApiModuleDTO> getNodeTrees(List<ApiModule> nodes) {
List<ApiModuleDTO> nodeTreeList = new ArrayList<>();
Map<Integer, List<ApiModule>> nodeLevelMap = new HashMap<>();
nodes.forEach(node -> {
Integer level = node.getLevel();
if (nodeLevelMap.containsKey(level)) {
nodeLevelMap.get(level).add(node);
} else {
List<ApiModule> apiModules = new ArrayList<>();
apiModules.add(node);
nodeLevelMap.put(node.getLevel(), apiModules);
}
});
List<ApiModule> rootNodes = Optional.ofNullable(nodeLevelMap.get(1)).orElse(new ArrayList<>());
rootNodes.forEach(rootNode -> nodeTreeList.add(buildNodeTree(nodeLevelMap, rootNode)));
return nodeTreeList;
}
/**
* 递归构建节点树
*
* @param nodeLevelMap
* @param rootNode
* @return
*/
private ApiModuleDTO buildNodeTree(Map<Integer, List<ApiModule>> nodeLevelMap, ApiModule rootNode) {
ApiModuleDTO nodeTree = new ApiModuleDTO();
BeanUtils.copyBean(nodeTree, rootNode);
nodeTree.setLabel(rootNode.getName());
List<ApiModule> lowerNodes = nodeLevelMap.get(rootNode.getLevel() + 1);
if (lowerNodes == null) {
return nodeTree;
}
List<ApiModuleDTO> children = Optional.ofNullable(nodeTree.getChildren()).orElse(new ArrayList<>());
lowerNodes.forEach(node -> {
if (node.getParentId() != null && node.getParentId().equals(rootNode.getId())) {
children.add(buildNodeTree(nodeLevelMap, node));
nodeTree.setChildren(children);
}
});
return nodeTree;
}
public String addNode(ApiModule node) {
validateNode(node);
node.setCreateTime(System.currentTimeMillis());
node.setUpdateTime(System.currentTimeMillis());
node.setId(UUID.randomUUID().toString());
apiModuleMapper.insertSelective(node);
return node.getId();
}
private void validateNode(ApiModule node) {
if (node.getLevel() > TestCaseConstants.MAX_NODE_DEPTH) {
throw new RuntimeException(Translator.get("test_case_node_level_tip")
+ TestCaseConstants.MAX_NODE_DEPTH + Translator.get("test_case_node_level"));
}
checkTestCaseNodeExist(node);
}
private void checkTestCaseNodeExist(ApiModule node) {
if (node.getName() != null) {
ApiModuleExample example = new ApiModuleExample();
ApiModuleExample.Criteria criteria = example.createCriteria();
criteria.andNameEqualTo(node.getName())
.andProjectIdEqualTo(node.getProjectId());
if (StringUtils.isNotBlank(node.getParentId())) {
criteria.andParentIdEqualTo(node.getParentId());
} else {
criteria.andParentIdIsNull();
}
if (StringUtils.isNotBlank(node.getId())) {
criteria.andIdNotEqualTo(node.getId());
}
if (apiModuleMapper.selectByExample(example).size() > 0) {
MSException.throwException(Translator.get("test_case_module_already_exists"));
}
}
}
private List<TestCaseDTO> QueryTestCaseByNodeIds(List<String> nodeIds) {
QueryTestCaseRequest testCaseRequest = new QueryTestCaseRequest();
testCaseRequest.setNodeIds(nodeIds);
return extTestCaseMapper.list(testCaseRequest);
}
public int editNode(DragModuleRequest request) {
request.setUpdateTime(System.currentTimeMillis());
checkTestCaseNodeExist(request);
List<TestCaseDTO> apiModule = QueryTestCaseByNodeIds(request.getNodeIds());
apiModule.forEach(testCase -> {
StringBuilder path = new StringBuilder(testCase.getNodePath());
List<String> pathLists = Arrays.asList(path.toString().split("/"));
pathLists.set(request.getLevel(), request.getName());
path.delete(0, path.length());
for (int i = 1; i < pathLists.size(); i++) {
path = path.append("/").append(pathLists.get(i));
}
testCase.setNodePath(path.toString());
});
batchUpdateTestCase(apiModule);
return apiModuleMapper.updateByPrimaryKeySelective(request);
}
public int deleteNode(List<String> nodeIds) {
TestCaseExample testCaseExample = new TestCaseExample();
testCaseExample.createCriteria().andNodeIdIn(nodeIds);
ApiModuleExample testCaseNodeExample = new ApiModuleExample();
testCaseNodeExample.createCriteria().andIdIn(nodeIds);
return apiModuleMapper.deleteByExample(testCaseNodeExample);
}
private void batchUpdateTestCase(List<TestCaseDTO> apiModule) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
TestCaseMapper testCaseMapper = sqlSession.getMapper(TestCaseMapper.class);
apiModule.forEach((value) -> {
testCaseMapper.updateByPrimaryKey(value);
});
sqlSession.flushStatements();
}
public void dragNode(DragModuleRequest request) {
checkTestCaseNodeExist(request);
List<String> nodeIds = request.getNodeIds();
List<TestCaseDTO> apiModule = QueryTestCaseByNodeIds(nodeIds);
ApiModuleDTO nodeTree = request.getNodeTree();
List<ApiModule> updateNodes = new ArrayList<>();
buildUpdateTestCase(nodeTree, apiModule, updateNodes, "/", "0", 1);
updateNodes = updateNodes.stream()
.filter(item -> nodeIds.contains(item.getId()))
.collect(Collectors.toList());
batchUpdateTestCaseNode(updateNodes);
batchUpdateTestCase(apiModule);
}
private void buildUpdateTestCase(ApiModuleDTO rootNode, List<TestCaseDTO> apiModule,
List<ApiModule> updateNodes, String rootPath, String pId, int level) {
rootPath = rootPath + rootNode.getName();
if (level > 8) {
MSException.throwException(Translator.get("node_deep_limit"));
}
ApiModule testCaseNode = new ApiModule();
testCaseNode.setId(rootNode.getId());
testCaseNode.setLevel(level);
testCaseNode.setParentId(pId);
updateNodes.add(testCaseNode);
for (TestCaseDTO item : apiModule) {
if (StringUtils.equals(item.getNodeId(), rootNode.getId())) {
item.setNodePath(rootPath);
}
}
List<ApiModuleDTO> children = rootNode.getChildren();
if (children != null && children.size() > 0) {
for (int i = 0; i < children.size(); i++) {
buildUpdateTestCase(children.get(i), apiModule, updateNodes, rootPath + '/', rootNode.getId(), level + 1);
}
}
}
private void batchUpdateTestCaseNode(List<ApiModule> updateNodes) {
SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
ApiModuleMapper apiModuleMapper = sqlSession.getMapper(ApiModuleMapper.class);
updateNodes.forEach((value) -> {
apiModuleMapper.updateByPrimaryKeySelective(value);
});
sqlSession.flushStatements();
}
}

View File

@ -0,0 +1,26 @@
package io.metersphere.base.domain;
import lombok.Data;
import java.io.Serializable;
@Data
public class ApiModule implements Serializable {
private String id;
private String projectId;
private String name;
private String parentId;
private Integer level;
private String protocol;
private Long createTime;
private Long updateTime;
private static final long serialVersionUID = 1L;
}

View File

@ -0,0 +1,660 @@
package io.metersphere.base.domain;
import java.util.ArrayList;
import java.util.List;
public class ApiModuleExample {
protected String orderByClause;
protected boolean distinct;
protected List<Criteria> oredCriteria;
public ApiModuleExample() {
oredCriteria = new ArrayList<Criteria>();
}
public void setOrderByClause(String orderByClause) {
this.orderByClause = orderByClause;
}
public String getOrderByClause() {
return orderByClause;
}
public void setDistinct(boolean distinct) {
this.distinct = distinct;
}
public boolean isDistinct() {
return distinct;
}
public List<Criteria> getOredCriteria() {
return oredCriteria;
}
public void or(Criteria criteria) {
oredCriteria.add(criteria);
}
public Criteria or() {
Criteria criteria = createCriteriaInternal();
oredCriteria.add(criteria);
return criteria;
}
public Criteria createCriteria() {
Criteria criteria = createCriteriaInternal();
if (oredCriteria.size() == 0) {
oredCriteria.add(criteria);
}
return criteria;
}
protected Criteria createCriteriaInternal() {
Criteria criteria = new Criteria();
return criteria;
}
public void clear() {
oredCriteria.clear();
orderByClause = null;
distinct = false;
}
protected abstract static class GeneratedCriteria {
protected List<Criterion> criteria;
protected GeneratedCriteria() {
super();
criteria = new ArrayList<Criterion>();
}
public boolean isValid() {
return criteria.size() > 0;
}
public List<Criterion> getAllCriteria() {
return criteria;
}
public List<Criterion> getCriteria() {
return criteria;
}
protected void addCriterion(String condition) {
if (condition == null) {
throw new RuntimeException("Value for condition cannot be null");
}
criteria.add(new Criterion(condition));
}
protected void addCriterion(String condition, Object value, String property) {
if (value == null) {
throw new RuntimeException("Value for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value));
}
protected void addCriterion(String condition, Object value1, Object value2, String property) {
if (value1 == null || value2 == null) {
throw new RuntimeException("Between values for " + property + " cannot be null");
}
criteria.add(new Criterion(condition, value1, value2));
}
public Criteria andIdIsNull() {
addCriterion("id is null");
return (Criteria) this;
}
public Criteria andIdIsNotNull() {
addCriterion("id is not null");
return (Criteria) this;
}
public Criteria andIdEqualTo(String value) {
addCriterion("id =", value, "id");
return (Criteria) this;
}
public Criteria andIdNotEqualTo(String value) {
addCriterion("id <>", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThan(String value) {
addCriterion("id >", value, "id");
return (Criteria) this;
}
public Criteria andIdGreaterThanOrEqualTo(String value) {
addCriterion("id >=", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThan(String value) {
addCriterion("id <", value, "id");
return (Criteria) this;
}
public Criteria andIdLessThanOrEqualTo(String value) {
addCriterion("id <=", value, "id");
return (Criteria) this;
}
public Criteria andIdLike(String value) {
addCriterion("id like", value, "id");
return (Criteria) this;
}
public Criteria andIdNotLike(String value) {
addCriterion("id not like", value, "id");
return (Criteria) this;
}
public Criteria andIdIn(List<String> values) {
addCriterion("id in", values, "id");
return (Criteria) this;
}
public Criteria andIdNotIn(List<String> values) {
addCriterion("id not in", values, "id");
return (Criteria) this;
}
public Criteria andIdBetween(String value1, String value2) {
addCriterion("id between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andIdNotBetween(String value1, String value2) {
addCriterion("id not between", value1, value2, "id");
return (Criteria) this;
}
public Criteria andProjectIdIsNull() {
addCriterion("project_id is null");
return (Criteria) this;
}
public Criteria andProjectIdIsNotNull() {
addCriterion("project_id is not null");
return (Criteria) this;
}
public Criteria andProjectIdEqualTo(String value) {
addCriterion("project_id =", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotEqualTo(String value) {
addCriterion("project_id <>", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdGreaterThan(String value) {
addCriterion("project_id >", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdGreaterThanOrEqualTo(String value) {
addCriterion("project_id >=", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLessThan(String value) {
addCriterion("project_id <", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLessThanOrEqualTo(String value) {
addCriterion("project_id <=", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdLike(String value) {
addCriterion("project_id like", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotLike(String value) {
addCriterion("project_id not like", value, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdIn(List<String> values) {
addCriterion("project_id in", values, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotIn(List<String> values) {
addCriterion("project_id not in", values, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdBetween(String value1, String value2) {
addCriterion("project_id between", value1, value2, "projectId");
return (Criteria) this;
}
public Criteria andProjectIdNotBetween(String value1, String value2) {
addCriterion("project_id not between", value1, value2, "projectId");
return (Criteria) this;
}
public Criteria andNameIsNull() {
addCriterion("name is null");
return (Criteria) this;
}
public Criteria andNameIsNotNull() {
addCriterion("name is not null");
return (Criteria) this;
}
public Criteria andNameEqualTo(String value) {
addCriterion("name =", value, "name");
return (Criteria) this;
}
public Criteria andNameNotEqualTo(String value) {
addCriterion("name <>", value, "name");
return (Criteria) this;
}
public Criteria andNameGreaterThan(String value) {
addCriterion("name >", value, "name");
return (Criteria) this;
}
public Criteria andNameGreaterThanOrEqualTo(String value) {
addCriterion("name >=", value, "name");
return (Criteria) this;
}
public Criteria andNameLessThan(String value) {
addCriterion("name <", value, "name");
return (Criteria) this;
}
public Criteria andNameLessThanOrEqualTo(String value) {
addCriterion("name <=", value, "name");
return (Criteria) this;
}
public Criteria andNameLike(String value) {
addCriterion("name like", value, "name");
return (Criteria) this;
}
public Criteria andNameNotLike(String value) {
addCriterion("name not like", value, "name");
return (Criteria) this;
}
public Criteria andNameIn(List<String> values) {
addCriterion("name in", values, "name");
return (Criteria) this;
}
public Criteria andNameNotIn(List<String> values) {
addCriterion("name not in", values, "name");
return (Criteria) this;
}
public Criteria andNameBetween(String value1, String value2) {
addCriterion("name between", value1, value2, "name");
return (Criteria) this;
}
public Criteria andNameNotBetween(String value1, String value2) {
addCriterion("name not between", value1, value2, "name");
return (Criteria) this;
}
public Criteria andParentIdIsNull() {
addCriterion("parent_id is null");
return (Criteria) this;
}
public Criteria andParentIdIsNotNull() {
addCriterion("parent_id is not null");
return (Criteria) this;
}
public Criteria andParentIdEqualTo(String value) {
addCriterion("parent_id =", value, "parentId");
return (Criteria) this;
}
public Criteria andParentIdNotEqualTo(String value) {
addCriterion("parent_id <>", value, "parentId");
return (Criteria) this;
}
public Criteria andParentIdGreaterThan(String value) {
addCriterion("parent_id >", value, "parentId");
return (Criteria) this;
}
public Criteria andParentIdGreaterThanOrEqualTo(String value) {
addCriterion("parent_id >=", value, "parentId");
return (Criteria) this;
}
public Criteria andParentIdLessThan(String value) {
addCriterion("parent_id <", value, "parentId");
return (Criteria) this;
}
public Criteria andParentIdLessThanOrEqualTo(String value) {
addCriterion("parent_id <=", value, "parentId");
return (Criteria) this;
}
public Criteria andParentIdLike(String value) {
addCriterion("parent_id like", value, "parentId");
return (Criteria) this;
}
public Criteria andParentIdNotLike(String value) {
addCriterion("parent_id not like", value, "parentId");
return (Criteria) this;
}
public Criteria andParentIdIn(List<String> values) {
addCriterion("parent_id in", values, "parentId");
return (Criteria) this;
}
public Criteria andParentIdNotIn(List<String> values) {
addCriterion("parent_id not in", values, "parentId");
return (Criteria) this;
}
public Criteria andParentIdBetween(String value1, String value2) {
addCriterion("parent_id between", value1, value2, "parentId");
return (Criteria) this;
}
public Criteria andParentIdNotBetween(String value1, String value2) {
addCriterion("parent_id not between", value1, value2, "parentId");
return (Criteria) this;
}
public Criteria andLevelIsNull() {
addCriterion("level is null");
return (Criteria) this;
}
public Criteria andLevelIsNotNull() {
addCriterion("level is not null");
return (Criteria) this;
}
public Criteria andLevelEqualTo(Integer value) {
addCriterion("level =", value, "level");
return (Criteria) this;
}
public Criteria andLevelNotEqualTo(Integer value) {
addCriterion("level <>", value, "level");
return (Criteria) this;
}
public Criteria andLevelGreaterThan(Integer value) {
addCriterion("level >", value, "level");
return (Criteria) this;
}
public Criteria andLevelGreaterThanOrEqualTo(Integer value) {
addCriterion("level >=", value, "level");
return (Criteria) this;
}
public Criteria andLevelLessThan(Integer value) {
addCriterion("level <", value, "level");
return (Criteria) this;
}
public Criteria andLevelLessThanOrEqualTo(Integer value) {
addCriterion("level <=", value, "level");
return (Criteria) this;
}
public Criteria andLevelIn(List<Integer> values) {
addCriterion("level in", values, "level");
return (Criteria) this;
}
public Criteria andLevelNotIn(List<Integer> values) {
addCriterion("level not in", values, "level");
return (Criteria) this;
}
public Criteria andLevelBetween(Integer value1, Integer value2) {
addCriterion("level between", value1, value2, "level");
return (Criteria) this;
}
public Criteria andLevelNotBetween(Integer value1, Integer value2) {
addCriterion("level not between", value1, value2, "level");
return (Criteria) this;
}
public Criteria andCreateTimeIsNull() {
addCriterion("create_time is null");
return (Criteria) this;
}
public Criteria andCreateTimeIsNotNull() {
addCriterion("create_time is not null");
return (Criteria) this;
}
public Criteria andCreateTimeEqualTo(Long value) {
addCriterion("create_time =", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeNotEqualTo(Long value) {
addCriterion("create_time <>", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeGreaterThan(Long value) {
addCriterion("create_time >", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeGreaterThanOrEqualTo(Long value) {
addCriterion("create_time >=", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeLessThan(Long value) {
addCriterion("create_time <", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeLessThanOrEqualTo(Long value) {
addCriterion("create_time <=", value, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeIn(List<Long> values) {
addCriterion("create_time in", values, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeNotIn(List<Long> values) {
addCriterion("create_time not in", values, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeBetween(Long value1, Long value2) {
addCriterion("create_time between", value1, value2, "createTime");
return (Criteria) this;
}
public Criteria andCreateTimeNotBetween(Long value1, Long value2) {
addCriterion("create_time not between", value1, value2, "createTime");
return (Criteria) this;
}
public Criteria andUpdateTimeIsNull() {
addCriterion("update_time is null");
return (Criteria) this;
}
public Criteria andUpdateTimeIsNotNull() {
addCriterion("update_time is not null");
return (Criteria) this;
}
public Criteria andUpdateTimeEqualTo(Long value) {
addCriterion("update_time =", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeNotEqualTo(Long value) {
addCriterion("update_time <>", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeGreaterThan(Long value) {
addCriterion("update_time >", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeGreaterThanOrEqualTo(Long value) {
addCriterion("update_time >=", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeLessThan(Long value) {
addCriterion("update_time <", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeLessThanOrEqualTo(Long value) {
addCriterion("update_time <=", value, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeIn(List<Long> values) {
addCriterion("update_time in", values, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeNotIn(List<Long> values) {
addCriterion("update_time not in", values, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeBetween(Long value1, Long value2) {
addCriterion("update_time between", value1, value2, "updateTime");
return (Criteria) this;
}
public Criteria andUpdateTimeNotBetween(Long value1, Long value2) {
addCriterion("update_time not between", value1, value2, "updateTime");
return (Criteria) this;
}
}
public static class Criteria extends GeneratedCriteria {
protected Criteria() {
super();
}
}
public static class Criterion {
private String condition;
private Object value;
private Object secondValue;
private boolean noValue;
private boolean singleValue;
private boolean betweenValue;
private boolean listValue;
private String typeHandler;
public String getCondition() {
return condition;
}
public Object getValue() {
return value;
}
public Object getSecondValue() {
return secondValue;
}
public boolean isNoValue() {
return noValue;
}
public boolean isSingleValue() {
return singleValue;
}
public boolean isBetweenValue() {
return betweenValue;
}
public boolean isListValue() {
return listValue;
}
public String getTypeHandler() {
return typeHandler;
}
protected Criterion(String condition) {
super();
this.condition = condition;
this.typeHandler = null;
this.noValue = true;
}
protected Criterion(String condition, Object value, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.typeHandler = typeHandler;
if (value instanceof List<?>) {
this.listValue = true;
} else {
this.singleValue = true;
}
}
protected Criterion(String condition, Object value) {
this(condition, value, null);
}
protected Criterion(String condition, Object value, Object secondValue, String typeHandler) {
super();
this.condition = condition;
this.value = value;
this.secondValue = secondValue;
this.typeHandler = typeHandler;
this.betweenValue = true;
}
protected Criterion(String condition, Object value, Object secondValue) {
this(condition, value, secondValue, null);
}
}
}

View File

@ -0,0 +1,34 @@
package io.metersphere.base.mapper;
import io.metersphere.base.domain.ApiModule;
import io.metersphere.base.domain.ApiModuleExample;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface ApiModuleMapper {
long countByExample(ApiModuleExample example);
int deleteByExample(ApiModuleExample example);
int deleteByPrimaryKey(String id);
int insert(ApiModule record);
int insertBatch(@Param("records") List<ApiModule> records);
int insertSelective(ApiModule record);
List<ApiModule> selectByExample(ApiModuleExample example);
ApiModule selectByPrimaryKey(String id);
int updateByExampleSelective(@Param("record") ApiModule record, @Param("example") ApiModuleExample example);
int updateByExample(@Param("record") ApiModule record, @Param("example") ApiModuleExample example);
int updateByPrimaryKeySelective(ApiModule record);
int updateByPrimaryKey(ApiModule record);
}

View File

@ -0,0 +1,274 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="io.metersphere.base.mapper.ApiModuleMapper">
<resultMap id="BaseResultMap" type="io.metersphere.base.domain.ApiModule">
<id column="id" jdbcType="VARCHAR" property="id"/>
<result column="project_id" jdbcType="VARCHAR" property="projectId"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="parent_id" jdbcType="VARCHAR" property="parentId"/>
<result column="level" jdbcType="INTEGER" property="level"/>
<result column="protocol" jdbcType="VARCHAR" property="protocol"/>
<result column="create_time" jdbcType="BIGINT" property="createTime"/>
<result column="update_time" jdbcType="BIGINT" property="updateTime"/>
</resultMap>
<sql id="Example_Where_Clause">
<where>
<foreach collection="oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="("
separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<sql id="Update_By_Example_Where_Clause">
<where>
<foreach collection="example.oredCriteria" item="criteria" separator="or">
<if test="criteria.valid">
<trim prefix="(" prefixOverrides="and" suffix=")">
<foreach collection="criteria.criteria" item="criterion">
<choose>
<when test="criterion.noValue">
and ${criterion.condition}
</when>
<when test="criterion.singleValue">
and ${criterion.condition} #{criterion.value}
</when>
<when test="criterion.betweenValue">
and ${criterion.condition} #{criterion.value} and #{criterion.secondValue}
</when>
<when test="criterion.listValue">
and ${criterion.condition}
<foreach close=")" collection="criterion.value" item="listItem" open="("
separator=",">
#{listItem}
</foreach>
</when>
</choose>
</foreach>
</trim>
</if>
</foreach>
</where>
</sql>
<sql id="Base_Column_List">
id, project_id, name, parent_id, level, create_time, update_time
</sql>
<select id="selectByExample" parameterType="io.metersphere.base.domain.TestCaseNodeExample"
resultMap="BaseResultMap">
select
<if test="distinct">
distinct
</if>
<include refid="Base_Column_List"/>
from api_module
<if test="_parameter != null">
<include refid="Example_Where_Clause"/>
</if>
<if test="orderByClause != null">
order by ${orderByClause}
</if>
</select>
<delete id="deleteByPrimaryKey" parameterType="java.lang.String">
delete from api_module
where id = #{id,jdbcType=VARCHAR}
</delete>
<delete id="deleteByExample" parameterType="io.metersphere.base.domain.TestCaseNodeExample">
delete from api_module
<if test="_parameter != null">
<include refid="Example_Where_Clause"/>
</if>
</delete>
<insert id="insertBatch" parameterType="io.metersphere.base.domain.ApiModule">
insert into api_module (id, project_id, name,protocol
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.protocol,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.ApiModule">
insert into api_module (id, project_id, name,protocol
parent_id, level, create_time,
update_time)
values (#{id,jdbcType=VARCHAR}, #{projectId,jdbcType=VARCHAR}, #{name,jdbcType=VARCHAR},#{protocol,jdbcType=VARCHAR},
#{parentId,jdbcType=VARCHAR}, #{level,jdbcType=INTEGER}, #{createTime,jdbcType=BIGINT},
#{updateTime,jdbcType=BIGINT})
</insert>
<insert id="insertSelective" parameterType="io.metersphere.base.domain.ApiModule">
insert into api_module
<trim prefix="(" suffix=")" suffixOverrides=",">
<if test="id != null">
id,
</if>
<if test="projectId != null">
project_id,
</if>
<if test="name != null">
name,
</if>
<if test="protocol != null">
protocol,
</if>
<if test="parentId != null">
parent_id,
</if>
<if test="level != null">
level,
</if>
<if test="createTime != null">
create_time,
</if>
<if test="updateTime != null">
update_time,
</if>
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="id != null">
#{id,jdbcType=VARCHAR},
</if>
<if test="projectId != null">
#{projectId,jdbcType=VARCHAR},
</if>
<if test="name != null">
#{name,jdbcType=VARCHAR},
</if>
<if test="protocol != null">
#{protocol,jdbcType=VARCHAR},
</if>
<if test="parentId != null">
#{parentId,jdbcType=VARCHAR},
</if>
<if test="level != null">
#{level,jdbcType=INTEGER},
</if>
<if test="createTime != null">
#{createTime,jdbcType=BIGINT},
</if>
<if test="updateTime != null">
#{updateTime,jdbcType=BIGINT},
</if>
</trim>
</insert>
<select id="countByExample" parameterType="io.metersphere.base.domain.TestCaseNodeExample"
resultType="java.lang.Long">
select count(*) from api_module
<if test="_parameter != null">
<include refid="Example_Where_Clause"/>
</if>
</select>
<update id="updateByExampleSelective" parameterType="map">
update api_module
<set>
<if test="record.id != null">
id = #{record.id,jdbcType=VARCHAR},
</if>
<if test="record.projectId != null">
project_id = #{record.projectId,jdbcType=VARCHAR},
</if>
<if test="record.name != null">
name = #{record.name,jdbcType=VARCHAR},
</if>
<if test="record.protocol != null">
protocol = #{record.protocol,jdbcType=VARCHAR},
</if>
<if test="record.parentId != null">
parent_id = #{record.parentId,jdbcType=VARCHAR},
</if>
<if test="record.level != null">
level = #{record.level,jdbcType=INTEGER},
</if>
<if test="record.createTime != null">
create_time = #{record.createTime,jdbcType=BIGINT},
</if>
<if test="record.updateTime != null">
update_time = #{record.updateTime,jdbcType=BIGINT},
</if>
</set>
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause"/>
</if>
</update>
<update id="updateByExample" parameterType="map">
update api_module
set id = #{record.id,jdbcType=VARCHAR},
project_id = #{record.projectId,jdbcType=VARCHAR},
name = #{record.name,jdbcType=VARCHAR},
protocol = #{protocol,jdbcType=VARCHAR},
parent_id = #{record.parentId,jdbcType=VARCHAR},
level = #{record.level,jdbcType=INTEGER},
create_time = #{record.createTime,jdbcType=BIGINT},
update_time = #{record.updateTime,jdbcType=BIGINT}
<if test="_parameter != null">
<include refid="Update_By_Example_Where_Clause"/>
</if>
</update>
<update id="updateByPrimaryKeySelective" parameterType="io.metersphere.base.domain.ApiModule">
update api_module
<set>
<if test="projectId != null">
project_id = #{projectId,jdbcType=VARCHAR},
</if>
<if test="name != null">
name = #{name,jdbcType=VARCHAR},
</if>
<if test="protocol != null">
protocol = #{protocol,jdbcType=VARCHAR},
</if>
<if test="parentId != null">
parent_id = #{parentId,jdbcType=VARCHAR},
</if>
<if test="level != null">
level = #{level,jdbcType=INTEGER},
</if>
<if test="createTime != null">
create_time = #{createTime,jdbcType=BIGINT},
</if>
<if test="updateTime != null">
update_time = #{updateTime,jdbcType=BIGINT},
</if>
</set>
where id = #{id,jdbcType=VARCHAR}
</update>
<update id="updateByPrimaryKey" parameterType="io.metersphere.base.domain.ApiModule">
update api_module
set project_id = #{projectId,jdbcType=VARCHAR},
name = #{name,jdbcType=VARCHAR},
protocol = #{protocol,jdbcType=VARCHAR},
parent_id = #{parentId,jdbcType=VARCHAR},
level = #{level,jdbcType=INTEGER},
create_time = #{createTime,jdbcType=BIGINT},
update_time = #{updateTime,jdbcType=BIGINT}
where id = #{id,jdbcType=VARCHAR}
</update>
</mapper>

View File

@ -0,0 +1,13 @@
CREATE TABLE IF NOT EXISTS `api_module` (
`id` varchar(50) NOT NULL COMMENT 'Test case node ID',
`project_id` varchar(50) NOT NULL COMMENT 'Project ID this node belongs to',
`name` varchar(64) NOT NULL COMMENT 'Node name',
`protocol` varchar(64) NOT NULL COMMENT 'Node protocol',
`parent_id` varchar(50) DEFAULT NULL COMMENT 'Parent node ID',
`level` int(10) DEFAULT 1 COMMENT 'Node level',
`create_time` bigint(13) NOT NULL COMMENT 'Create timestamp',
`update_time` bigint(13) NOT NULL COMMENT 'Update timestamp',
PRIMARY KEY (`id`)
)
ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

View File

@ -0,0 +1,219 @@
<template>
<ms-container>
<ms-aside-container>
<ms-node-tree></ms-node-tree>
</ms-aside-container>
<ms-main-container>
<el-dropdown size="small" split-button type="primary" class="ms-api-buttion" @click="handleCommand('add')"
@command="handleCommand">
{{$t('commons.add')}}
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="debug">{{$t('api_test.delimit.request.fast_debug')}}</el-dropdown-item>
<el-dropdown-item command="add">{{$t('api_test.delimit.request.title')}}</el-dropdown-item>
<el-dropdown-item command="closeAll">{{$t('api_test.delimit.request.close_all_label')}}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
<el-tabs v-model="editableTabsValue" @edit="handleTabsEdit">
<el-tab-pane
:key="item.name"
v-for="(item) in editableTabs"
:label="item.title"
:closable="item.closable"
:name="item.name">
<!-- 列表集合 -->
<ms-api-list
v-if="item.type === 'list'"
:current-project="currentProject"
:select-node-ids="selectApi"
:select-parent-nodes="selectParentNodes"
@testCaseEdit="editTestCase"
@handleCase="handleCase"
@batchMove="batchMove"
@refresh="refresh"
@moveToNode="moveToNode"
ref="testCaseList">
</ms-api-list>
<div v-else-if="item.type=== 'add'">
<ms-api-config @runTest="runTest"></ms-api-config>
</div>
<div v-else-if="item.type=== 'debug'">
<ms-debug-http-page @saveAs="saveAs"></ms-debug-http-page>
</div>
<div v-else-if="item.type=== 'test'">
<!-- 测试-->
<ms-run-test-http-page></ms-run-test-http-page>
</div>
</el-tab-pane>
</el-tabs>
</ms-main-container>
</ms-container>
</template>
<script>
import MsNodeTree from './components/ApiModule';
import MsApiList from './components/ApiList';
import MsContainer from "../../common/components/MsContainer";
import MsMainContainer from "../../common/components/MsMainContainer";
import MsAsideContainer from "../../common/components/MsAsideContainer";
import MsBottomContainer from "./components/BottomContainer";
import MsApiConfig from "./components/ApiConfig";
import MsDebugHttpPage from "./components/debug/DebugHttpPage";
import MsRunTestHttpPage from "./components/runtest/RunTestHttpPage";
export default {
name: "TestCase",
components: {
MsNodeTree,
MsApiList,
MsMainContainer,
MsContainer,
MsAsideContainer,
MsBottomContainer,
MsApiConfig,
MsDebugHttpPage,
MsRunTestHttpPage
},
comments: {},
data() {
return {
projectId: null,
isHide: true,
editableTabsValue: '1',
editableTabs: [{
title: this.$t('api_test.delimit.api_title'),
name: '1',
type: "list",
closable: false
}],
tabIndex: 1,
result: {},
currentPage: 1,
pageSize: 5,
total: 0,
projects: [],
currentProject: null,
treeNodes: [],
selectApi: {},
selectParentNodes: [],
testCaseReadOnly: true,
selectNode: {},
nodeTreeDraggable: true,
}
},
created() {
},
watch: {
'$route': 'init',
},
methods: {
handleCommand(e) {
if (e === "add") {
this.handleTabsEdit(this.$t('api_test.delimit.request.title'), e);
} else if (e === "test") {
this.handleTabsEdit(this.$t("commons.api"), e);
}
else if (e === "closeAll") {
let tabs = this.editableTabs[0];
this.editableTabs = [];
this.editableTabsValue = tabs.name;
this.editableTabs.push(tabs);
}
else {
this.handleTabsEdit(this.$t('api_test.delimit.request.fast_debug'), "debug");
}
},
handleTabsEdit(targetName, action) {
if (action === 'remove') {
let tabs = this.editableTabs;
let activeName = this.editableTabsValue;
if (activeName === targetName) {
tabs.forEach((tab, index) => {
if (tab.name === targetName) {
let nextTab = tabs[index + 1] || tabs[index - 1];
if (nextTab) {
activeName = nextTab.name;
}
}
});
}
this.editableTabsValue = activeName;
this.editableTabs = tabs.filter(tab => tab.name !== targetName);
} else {
if (targetName === undefined || targetName === null) {
targetName = this.$t('api_test.delimit.request.title');
}
let newTabName = ++this.tabIndex + '';
this.editableTabs.push({
title: targetName,
name: newTabName,
closable: true,
type: action,
content: MsApiConfig
});
this.editableTabsValue = newTabName;
}
},
editTestCase(testCase) {
this.handleTabsEdit(testCase.api_name, "add");
},
handleCase(testCase) {
this.selectApi = testCase;
this.isHide = false;
},
apiCaseClose() {
this.isHide = true;
},
batchMove(selectIds) {
},
refresh() {
},
moveToNode() {
},
saveAs(data) {
this.handleCommand("add");
},
runTest(data) {
console.log(data);
this.handleCommand("test");
},
search() {
alert(1);
}
}
}
</script>
<style scoped>
.ms-api-buttion {
position: absolute;
top: 100px;
right: 20px;
padding: 0;
background: 0 0;
border: none;
outline: 0;
cursor: pointer;
margin-right: 10px;
font-size: 16px;
}
/deep/ .el-tabs__header {
margin-right: 82px;
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<div>
<el-form :model="request" ref="request" label-width="100px">
<el-form-item :label="$t('api_test.request.connect_timeout')" prop="connectTimeout">
<el-input-number size="small" :disabled="isReadOnly" v-model="request.connectTimeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
</el-form-item>
<el-form-item :label="$t('api_test.request.response_timeout')" prop="responseTimeout">
<el-input-number size="small" :disabled="isReadOnly" v-model="request.responseTimeout" :placeholder="$t('commons.millisecond')" :max="1000*10000000" :min="0"/>
</el-form-item>
</el-form>
</div>
</template>
<script>
export default {
name: "MsApiAdvancedConfig",
props: {
request: Object,
isReadOnly: {
type: Boolean,
default: false
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,305 @@
<template>
<div v-loading="result.loading">
<el-container>
<el-header style="width: 100% ;padding: 0px">
<el-card>
<el-row>
<el-col :span="3">
<div class="variable-combine"> {{api.api_name}}</div>
</el-col>
<el-col :span="1">
<template>
<div>
<ms-tag v-if="api.api_status == 'Prepare'" type="info"
:content="$t('test_track.plan.plan_status_prepare')"/>
<ms-tag v-if="api.api_status == 'Underway'" type="primary"
:content="$t('test_track.plan.plan_status_running')"/>
<ms-tag v-if="api.api_status == 'Completed'" type="success"
:content="$t('test_track.plan.plan_status_completed')"/>
</div>
</template>
</el-col>
<el-col :span="4">
<div class="variable-combine">{{api.api_path}}</div>
</el-col>
<el-col :span="2">
<div>{{$t('test_track.plan_view.case_count')}}5</div>
</el-col>
<el-col :span="4">
<div>
<el-select size="small" :placeholder="$t('api_test.delimit.request.grade_info')" v-model="grdValue"
class="ms-api-header-select">
<el-option v-for="grd in grades" :key="grd.id" :label="grd.name" :value="grd.id"/>
</el-select>
</div>
</el-col>
<el-col :span="4">
<div>
<el-select size="small" class="ms-api-header-select" v-model="envValue"
:placeholder="$t('api_test.delimit.request.run_env')">
<el-option v-for="env in environments" :key="env.id" :label="env.name" :value="env.id"/>
</el-select>
</div>
</el-col>
<el-col :span="3">
<div class="ms-api-header-select">
<el-input size="small" :placeholder="$t('api_test.delimit.request.select_case')"></el-input>
</div>
</el-col>
<el-col :span="1">
<div class="ms-api-header-select">
<el-button size="small" @click="createCase">+{{$t('api_test.delimit.request.case')}}</el-button>
</div>
</el-col>
<el-col :span="2">
<button type="button" aria-label="Close" class="el-card-btn" @click="apiCaseClose()"><i
class="el-dialog__close el-icon el-icon-close"></i></button>
</el-col>
</el-row>
</el-card>
</el-header>
<!-- 用例部分 -->
<el-main>
<div v-for="(item,index) in apiCaseList" :key="index">
<el-card style="margin-top: 10px">
<el-row>
<el-col :span="5">
<div class="el-step__icon is-text ms-api-col">
<div class="el-step__icon-inner">{{index+1}}</div>
</div>
<label class="ms-api-label">{{$t('test_track.case.priority')}}</label>
<el-select size="small" v-model="item.priority" class="ms-api-select">
<el-option v-for="grd in priority" :key="grd.id" :label="grd.name" :value="grd.id"/>
</el-select>
</el-col>
<el-col :span="10">
<i class="icon el-icon-arrow-right" :class="{'is-active': item.isActive}"
@click="active(item)"/>
<el-input v-if="item.type==='create'" size="small" v-model="item.name" :name="index"
:key="index" class="ms-api-header-select"/>
{{item.type!= 'create'? item.name : ''}}
<div v-if="item.type!='create'" style="color: #999999;font-size: 12px">
{{item.create_time}}
{{item.create_user}} 创建
{{item.update_time}}
{{item.update_user}} 更新
</div>
</el-col>
<el-col :span="4">
<ms-tip-button @click="runCase" :tip="$t('api_test.run')" icon="el-icon-video-play"
style="background-color: #409EFF;color: white" size="mini" circle/>
<ms-tip-button @click="copyCase(item)" :tip="$t('commons.copy')" icon="el-icon-document-copy"
size="mini" circle/>
<ms-tip-button @click="deleteCase(index)" :tip="$t('commons.delete')" icon="el-icon-delete" size="mini"
circle/>
</el-col>
<el-col :span="4">
<div v-if="item.type!='create'">{{getResult(item.execute_result)}}</div>
<div v-if="item.type!='create'" style="color: #999999;font-size: 12px">{{item.update_time}}
{{item.update_user}}
</div>
</el-col>
</el-row>
<el-collapse-transition>
<div v-show="item.isActive">
<ms-api-request-form :debug-report-id="debugReportId" @runDebug="runDebug"
:request="selected" :scenario="currentScenario" v-if="handleChange"/>
</div>
</el-collapse-transition>
</el-card>
</div>
<el-button type="primary" size="small" style="margin-top: 20px; float: right">{{$t('commons.save')}}</el-button>
</el-main>
</el-container>
</div>
</template>
<script>
import MsTag from "../../../common/components/MsTag";
import MsTipButton from "../../../common/components/MsTipButton";
import MsApiRequestForm from "./request/ApiRequestForm";
import {Request, Scenario} from "../model/ScenarioModel";
export default {
name: 'ApiCaseList',
components: {
MsTag,
MsTipButton,
MsApiRequestForm
},
props: {
api: {
type: Object
},
},
data() {
return {
result: {},
grades: [],
environments: [],
envValue: "",
grdValue: "",
value: "",
priority: [
{name: 'P0', id: 'P0'},
{name: 'P1', id: 'P1'},
{name: 'P2', id: 'P2'},
{name: 'P3', id: 'P3'}
],
apiCaseList: [],
reportVisible: false,
create: false,
projects: [],
change: false,
isReadOnly: false,
debugReportId: '',
type: "",
activeName: 0,
selected: [Scenario, Request],
currentScenario: {}
}
},
watch: {
//
api() {
let obj = {
name: '测试用例',
priority: 'P0',
execute_result: 'PASS',
create_time: '2020-11-1: 10:05',
update_time: '2020-11-1: 10:05',
create_user: '赵勇',
isActive: false,
update_user: '赵勇'
};
this.apiCaseList.push(obj);
},
},
methods: {
getResult(data) {
if (data === 'PASS') {
return '执行结果:通过';
} else if (data === 'UN_PASS') {
return '执行结果:未通过';
} else {
return '执行结果:未执行';
}
},
apiCaseClose() {
this.apiCaseList = [];
this.$emit('apiCaseClose');
},
runCase() {
},
deleteCase(index) {
this.apiCaseList.splice(index, 1);
},
copyCase(data) {
let obj = {
name: data.name,
priority: data.priority,
type: 'create'
};
this.apiCaseList.push(obj);
},
createCase() {
let obj = {
id: this.apiCaseList.size + 1,
name: '',
priority: 'P0',
type: 'create',
isActive: true,
};
this.apiCaseList.push(obj);
},
runDebug() {
},
handleChange(v) {
return v != "";
},
active(item) {
item.isActive = !item.isActive;
},
}
}
</script>
<style scoped>
.ms-api-select {
margin-left: 20px;
width: 80px;
}
.ms-api-header-select {
margin-left: 20px;
width: 150px;
}
.el-card-btn {
float: right;
top: 20px;
right: 20px;
padding: 0;
background: 0 0;
border: none;
outline: 0;
cursor: pointer;
font-size: 18px;
margin-left: 30px;
}
.ms-api-label {
color: #CCCCCC;
}
.ms-api-col {
background-color: #7C3985;
border-color: #7C3985;
margin-right: 10px;
color: white;
}
.ms-ap-collapse {
padding-top: 0px;
border: 0px;
}
/deep/ .el-collapse-item__header {
border-bottom: 0px;
margin-top: 0px;
width: 300px;
line-height: 0px;
color: #666666;
}
.variable-combine {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
width: 100%;
}
.icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="card-container">
<ms-add-complete-http-api @runTest="runTest" :httpData="result" v-if="reqType === 'HTTP'"></ms-add-complete-http-api>
</div>
</template>
<script>
import MsAddCompleteHttpApi from "./complete/AddCompleteHttpApi";
import {RequestFactory} from "../model/ScenarioModel";
export default {
name: "ApiConfig",
components: {MsAddCompleteHttpApi},
data() {
return {
reqType: RequestFactory.TYPES.HTTP,
result: {},
}
},
props: {},
methods: {
runTest(data){
this.$emit('runTest',data);
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,134 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in items" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col class="kv-checkbox">
<input type="checkbox" v-if="!isDisable(index)" v-model="item.enable"
:disabled="isReadOnly"/>
</el-col>
<el-col>
<el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="200"
@change="change"
:placeholder="keyText" show-word-limit/>
<el-autocomplete :disabled="isReadOnly" :maxlength="200" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText"
show-word-limit/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="item.value" size="small" @change="change"
:placeholder="valueText" show-word-limit/>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
:disabled="isDisable(index) || isReadOnly"/>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import {KeyValue} from "../model/ScenarioModel";
export default {
name: "MsApiKeyValue",
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
items: Array,
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
data() {
return {
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
//
this.items.splice(index, 1);
this.$emit('change', this.items);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.items.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.items.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.items.push(new KeyValue({enable: true}));
}
this.$emit('change', this.items);
// TODO key
},
isDisable: function (index) {
return this.items.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
},
created() {
if (this.items.length === 0 || this.items[this.items.length - 1].name) {
this.items.push(new KeyValue({enable: true}));
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.kv-delete {
width: 60px;
}
.el-autocomplete {
width: 100%;
}
</style>

View File

@ -0,0 +1,258 @@
<template>
<div class="card-container">
<el-card class="card-content" v-loading="result.loading">
<template v-slot:header>
<ms-table-header :showCreate="false" :condition.sync="condition"
@search="search"
:title="$t('api_test.delimit.api_title')"/>
</template>
<el-table
border
:data="tableData"
row-key="id"
class="test-content adjust-table">
<el-table-column
prop="api_name"
:label="$t('api_test.delimit.api_name')"
show-overflow-tooltip/>
<el-table-column
prop="api_status"
column-key="api_status"
:label="$t('api_test.delimit.api_status')"
show-overflow-tooltip>
<template v-slot:default="scope">
<span class="el-dropdown-link">
<template>
<div>
<ms-tag v-if="scope.row.api_status == 'Prepare'" type="info"
:content="$t('test_track.plan.plan_status_prepare')"/>
<ms-tag v-if="scope.row.api_status == 'Underway'" type="primary"
:content="$t('test_track.plan.plan_status_running')"/>
<ms-tag v-if="scope.row.api_status == 'Completed'" type="success"
:content="$t('test_track.plan.plan_status_completed')"/>
</div>
</template>
</span>
</template>
</el-table-column>
<el-table-column
prop="api_type"
:label="$t('api_test.delimit.api_type')"
show-overflow-tooltip>
<template v-slot:default="scope">
<span class="el-dropdown-link">
<template>
<div class="request-method">
<el-tag size="mini"
:style="{'background-color': getColor(true, scope.row.api_type)}" class="api-el-tag"> {{ scope.row.api_type }}</el-tag>
</div>
</template>
</span>
</template>
</el-table-column>
<el-table-column
prop="api_path"
:label="$t('api_test.delimit.api_path')"
show-overflow-tooltip/>
<el-table-column
prop="api_principal"
:label="$t('api_test.delimit.api_principal')"
show-overflow-tooltip/>
<el-table-column
prop="api_last_time"
:label="$t('api_test.delimit.api_last_time')"
show-overflow-tooltip/>
<el-table-column
prop="api_case_number"
:label="$t('api_test.delimit.api_case_number')"
show-overflow-tooltip/>
<el-table-column
prop="api_case_status"
:label="$t('api_test.delimit.api_case_status')"
show-overflow-tooltip/>
<el-table-column
prop="api_case_passing_rate"
:label="$t('api_test.delimit.api_case_passing_rate')"
show-overflow-tooltip/>
<el-table-column
:label="$t('commons.operating')" min-width="100">
<template v-slot:default="scope">
<el-button type="text" @click="testCaseEdit(scope.row)">编辑</el-button>
<el-button type="text" @click="handleTestCase(scope.row)">用例</el-button>
<el-button type="text" @click="handleDelete(scope.row)" style="color: #F56C6C">删除</el-button>
</template>
</el-table-column>
</el-table>
<ms-table-pagination :change="initTableData" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-card>
<ms-bottom-container v-bind:enableAsideHidden="isHide">
<ms-api-case-list @apiCaseClose="apiCaseClose" :api="selectApi"></ms-api-case-list>
</ms-bottom-container>
</div>
</template>
<script>
import MsTableHeader from '../../../../components/common/components/MsTableHeader';
import MsTableOperator from "../../../common/components/MsTableOperator";
import MsTableOperatorButton from "../../../common/components/MsTableOperatorButton";
import MsTableButton from "../../../common/components/MsTableButton";
import {TEST_CASE_CONFIGS} from "../../../common/components/search/search-components";
import {LIST_CHANGE, TrackEvent} from "@/business/components/common/head/ListEvent";
import MsTablePagination from "../../../common/pagination/TablePagination";
import MsTag from "../../../common/components/MsTag";
import MsApiCaseList from "./ApiCaseList";
import MsContainer from "../../../common/components/MsContainer";
import MsBottomContainer from "./BottomContainer";
export default {
name: "ApiList",
components: {
MsTableButton,
MsTableOperatorButton,
MsTableOperator,
MsTableHeader,
MsTablePagination,
MsTag,
MsApiCaseList,
MsContainer,
MsBottomContainer
},
data() {
return {
result: {},
condition: {},
isHide: true,
selectApi: {},
deletePath: "/test/case/delete",
methodColorMap: new Map([
['GET', "#61AFFE"], ['POST', '#49CC90'], ['PUT', '#fca130'],
['PATCH', '#E2EE11'], ['DELETE', '#f93e3d'], ['OPTIONS', '#0EF5DA'],
['HEAD', '#8E58E7'], ['CONNECT', '#90AFAE'],
['DUBBO', '#C36EEF'], ['SQL', '#0AEAD4'], ['TCP', '#0A52DF'],
]),
tableData: [{
api_name: "接口名称",
api_status: "Completed",
api_type: "GET",
api_path: "/api/common/login.do",
api_principal: "负责人",
api_last_time: "最后更新时间",
api_case_number: "用例数",
api_case_status: "失败",
api_case_passing_rate: "100%"
}, {
api_name: "接口名称",
api_status: "Prepare",
api_type: "GET",
api_path: "/api/common/login.do",
api_principal: "负责人",
api_last_time: "最后更新时间",
api_case_number: "用例数",
api_case_status: "通过",
api_case_passing_rate: "100%"
}, {
api_name: "接口名称",
api_status: "Underway",
api_type: "POST",
api_path: "/api/common/login.do",
api_principal: "负责人",
api_last_time: "最后更新时间",
api_case_number: "用例数",
api_case_status: "通过",
api_case_passing_rate: "-"
}],
currentPage: 1,
pageSize: 10,
total: 0,
}
},
props: {
currentProject: {
type: Object
},
selectParentNodes: {
type: Array
}
},
created: function () {
this.initTableData();
},
watch: {
currentProject() {
this.initTableData();
},
},
methods: {
initTableData() {
},
search() {
alert("s");
},
buildPagePath(path) {
return path + "/" + this.currentPage + "/" + this.pageSize;
},
testCaseEdit(testCase) {
this.$emit('testCaseEdit', testCase);
},
handleTestCase(testCase) {
this.selectApi = testCase;
this.isHide = false;
},
handleDelete(testCase) {
},
apiCaseClose() {
this.isHide = true;
},
refresh() {
this.condition = {components: TEST_CASE_CONFIGS};
// this.selectIds.clear();
this.selectRows.clear();
this.$emit('refresh');
},
getColor(enable, method) {
if (enable) {
return this.methodColorMap.get(method);
}
},
}
}
</script>
<style scoped>
.operate-button > div {
display: inline-block;
margin-left: 10px;
}
.request-method {
padding: 0 5px;
color: #1E90FF;
}
.api-el-tag {
color: white;
}
</style>

View File

@ -0,0 +1,399 @@
<template>
<div v-loading="result.loading">
<select-menu
:data="projects"
:current-data="currentProject"
:title="$t('test_track.project')"
@dataChange="changeProject" style="margin-bottom: 20px"/>
<el-select style="width: 100px ;height: 30px" size="small" v-model="value">
<el-option
v-for="item in options"
:key="item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled">
</el-option>
</el-select>
<el-input style="width: 175px; padding-left: 3px" :placeholder="$t('test_track.module.search')" v-model="filterText"
size="small">
<template v-slot:append>
<el-button icon="el-icon-folder-add" @click="addApi"></el-button>
</template>
</el-input>
<el-tree :data="data"
class="filter-tree node-tree"
node-key="id"
default-expand-all
:expand-on-click-node="false"
@node-click="nodeClick"
@node-drag-start="handleDragStart"
@node-drag-enter="handleDragEnter"
@node-drag-leave="handleDragLeave"
@node-drag-over="handleDragOver"
@node-drag-end="handleDragEnd"
@node-drop="handleDrop"
draggable
:allow-drop="allowDrop"
:allow-drag="allowDrag">
<span class="custom-tree-node father"
slot-scope="{ node, data }">
<!-- 如果是编辑状态 -->
<template v-if="data.isEdit==1">
<el-input ref="input"
@blur="() => submitEdit(node,data)"
v-model="newLabel"
class="ms-el-input" size="mini"></el-input>
</template>
<!-- 如果不是编辑状态 -->
<span class="node-title" v-else v-text="data.label"></span>
<span class="node-operate child">
<el-tooltip
v-if="data.id!=1"
class="item"
effect="dark"
:open-delay="200"
:content="$t('test_track.module.rename')"
placement="top">
<i @click.stop="() => edit(node,data)" class="el-icon-edit"></i>
</el-tooltip>
<el-tooltip
class="item"
effect="dark"
:open-delay="200"
:content="$t('test_track.module.add_submodule')"
placement="top">
<i @click.stop="() => append(node,data)" class="el-icon-circle-plus-outline"></i>
</el-tooltip>
<el-tooltip
v-if="data.id!=1"
class="item"
effect="dark"
:open-delay="200"
:content="$t('commons.delete')"
placement="top">
<i @click.stop="() => remove(node, data)" class="el-icon-delete"></i>
</el-tooltip>
</span>
</span>
</el-tree>
<ms-add-basis-http-api ref="httpApi"></ms-add-basis-http-api>
</div>
</template>
<script>
import MsAddBasisHttpApi from "./basis/AddBasisHttpApi";
import SelectMenu from "../../../track/common/SelectMenu";
export default {
name: 'MsApiModule',
components: {
MsAddBasisHttpApi,
SelectMenu
},
data() {
return {
options: [{
value: 'HTTP',
label: 'HTTP'
}, {
value: 'DUBBO',
label: 'DUBBO'
}, {
value: 'TCP',
label: 'TCP'
}, {
value: 'SQL',
label: 'SQL'
}],
value: 'HTTP',
httpVisible: false,
result: {},
filterText: "",
nextFlag: true,
currentProject: {},
projects: [],
data: [
{
"id": 1,
"label": "技术部",
"level": 1,
"children": [
{
"id": 2,
"label": "运维组",
"level": 2,
"children": [
{
"id": 3,
"label": "godo",
"level": 3,
"children": []
}
]
},
{
"id": 4,
"label": "测试组",
"level": 2,
"children": []
}
]
}
],
newLabel: '',
defaultProps: {
children: 'children',
label: 'label'
}
}
},
created() {
this.getApiGroupData()
},
methods: {
getApiModuleTree() {
if (this.currentProject) {
this.result = this.$get("/api/module/list/" + this.currentProject.id + "/" + this.value, response => {
this.data = response.data;
});
}
},
// api
getApiGroupData() {
this.getProjects();
this.getApiModuleTree();
},
handleDragStart(node, ev) {
console.log('drag start', node.data.label)
},
handleDragEnter(draggingNode, dropNode, ev) {
console.log('tree drag enter: ', dropNode.data.label)
},
handleDragLeave(draggingNode, dropNode, ev) {
console.log('tree drag leave: ', dropNode.data.label)
},
handleDragOver(draggingNode, dropNode, ev) {
console.log('tree drag over: ', dropNode.data.label)
},
handleDragEnd(draggingNode, dropNode, dropType, ev) {
console.log(
'tree drag end: ',
dropNode && dropNode.data.label,
dropType
)
//
this.updateApiGroup(this.data)
},
handleDrop(draggingNode, dropNode, dropType, ev) {
console.log('tree drop: ', dropNode.data.label, dropType)
},
allowDrop(draggingNode, dropNode, type) {
if (dropNode.data.id === 1) {
return false
} else {
return true
}
},
allowDrag(draggingNode) {
//
if (draggingNode.data.id === 1) {
return false
} else {
return true
}
},
append(node, data) {
// var pid = data.parentApiGroupId + ':' + data.id
if (this.nextFlag) {
var timestamp = new Date().getTime()
const newChild = {
id: timestamp,
isEdit: 0,
label: '',
children: []
}
if (!data.children) {
this.$set(data, 'children', [])
}
data.children.push(newChild)
this.updateApiGroup(this.data)
this.edit(node, newChild);
}
},
remove(node, data) {
if (data.label === "") {
this.nextFlag = true;
}
const parent = node.parent
const children = parent.data.children || parent.data
const index = children.findIndex(d => d.id === data.id)
children.splice(index, 1)
this.updateApiGroup(this.data)
},
edit(node, data) {
this.$set(data, 'isEdit', 1)
this.newLabel = data.label
this.$nextTick(() => {
this.$refs.input.focus()
})
console.log('after:', data.id, data.label, data.isEdit)
},
submitEdit(node, data) {
//
if (this.newLabel === "") {
this.nextFlag = false;
this.$message.warning(this.$t('commons.input_name'));
return;
}
if (data.label == this.newLabel) {
this.newLabel = ''
this.$set(data, 'isEdit', 0)
} else {
this.$set(data, 'label', this.newLabel)
this.newLabel = ''
this.$set(data, 'isEdit', 0)
this.updateApiGroup(node, data);
}
this.nextFlag = true;
},
cancelEdit(node, data) {
this.newLabel = ''
this.$set(data, 'isEdit', 0)
},
//
updateApiGroup(node, data) {
let url = '';
if (!data.id) {
url = '/api/module/add';
data.level = 1;
if (node.parent) {
//
data.parentId = node.parent.id;
data.level = node.parent.level + 1;
}
} else if (this.type === 'edit') {
url = '/api/module/edit';
data.id = this.node.id;
data.level = this.node.level;
data.nodeIds = this.nodeIds;
}
data.name = data.label;
data.projectId = this.currentProject.id;
this.$post(url, data, () => {
this.$success(this.$t('commons.save_success'));
this.getApiGroupData();
});
},
nodeClick(node, data, obj) {
console.log('点击了:', node.id, node.label)
},
addApi() {
this.$refs.httpApi.open();
},
//
changeProject(project) {
this.setCurrentProject(project);
},
setCurrentProject(project) {
if (project) {
this.currentProject = project;
localStorage.setItem("current_project", JSON.stringify(project));
}
//
},
getProjects() {
this.$get("/project/listAll", (response) => {
this.projects = response.data;
let lastProject = JSON.parse(localStorage.getItem("current_project"));
if (lastProject) {
let hasCurrentProject = false;
for (let i = 0; i < this.projects.length; i++) {
if (this.projects[i].id == lastProject.id) {
this.currentProject = lastProject;
hasCurrentProject = true;
break;
}
}
if (!hasCurrentProject) {
this.setCurrentProject(this.projects[0]);
}
} else {
if (this.projects.length > 0) {
this.setCurrentProject(this.projects[0]);
}
}
});
},
}
}
</script>
<style scoped>
.node-tree {
margin-top: 15px;
margin-bottom: 15px;
}
.ms-el-input {
height: 25px;
line-height: 25px;
}
.custom-tree-node {
flex: 1 1 auto;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 14px;
padding-right: 8px;
width: 100%;
}
.father .child {
display: none;
}
.father:hover .child {
display: block;
}
.node-title {
width: 0px;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0px 5px;
overflow: hidden;
}
.node-operate > i {
color: #409eff;
margin: 0px 5px;
}
/deep/ .el-tree-node__content {
height: 33px;
}
</style>

View File

@ -0,0 +1,216 @@
<template>
<div>
<span class="kv-description" v-if="description">
{{ description }}
</span>
<div class="kv-row" v-for="(item, index) in parameters" :key="index">
<el-row type="flex" :gutter="20" justify="space-between" align="middle">
<el-col class="kv-checkbox">
<input type="checkbox" v-if="!isDisable(index)" v-model="item.enable"
:disabled="isReadOnly"/>
</el-col>
<el-col>
<el-input v-if="!suggestions" :disabled="isReadOnly" v-model="item.name" size="small" maxlength="200"
@change="change" :placeholder="keyText" show-word-limit>
<template v-slot:prepend>
<el-select v-if="type === 'body'" :disabled="isReadOnly" class="kv-type" v-model="item.type" @change="typeChange(item)">
<el-option value="text"/>
<el-option value="file"/>
</el-select>
</template>
</el-input>
<el-autocomplete :disabled="isReadOnly" v-if="suggestions" v-model="item.name" size="small"
:fetch-suggestions="querySearch" @change="change" :placeholder="keyText" show-word-limit/>
</el-col>
<el-col v-if="item.type !== 'file'">
<el-autocomplete
:disabled="isReadOnly"
size="small"
class="input-with-autocomplete"
v-model="item.value"
:fetch-suggestions="funcSearch"
:placeholder="valueText"
value-key="name"
highlight-first-item
@select="change">
<i slot="suffix" class="el-input__icon el-icon-edit pointer" @click="advanced(item)"></i>
</el-autocomplete>
</el-col>
<el-col v-if="item.type === 'file'">
<ms-api-body-file-upload :parameter="item"/>
</el-col>
<el-col v-if="type === 'body'">
<el-input :disabled="isReadOnly" v-model="item.contentType" size="small" maxlength="100"
@change="change" :placeholder="$t('api_test.request.content_type')" show-word-limit>
</el-input>
</el-col>
<el-col class="kv-delete">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
:disabled="isDisable(index) || isReadOnly"/>
</el-col>
</el-row>
</div>
<ms-api-variable-advance ref="variableAdvance" :environment="environment" :scenario="scenario"
:parameters="parameters"
:current-item="currentItem"/>
</div>
</template>
<script>
import {KeyValue, Scenario} from "../model/ScenarioModel";
import {JMETER_FUNC, MOCKJS_FUNC} from "@/common/js/constants";
import MsApiVariableAdvance from "@/business/components/api/test/components/ApiVariableAdvance";
import MsApiBodyFileUpload from "./body/ApiBodyFileUpload";
export default {
name: "MsApiVariable",
components: {MsApiBodyFileUpload, MsApiVariableAdvance},
props: {
keyPlaceholder: String,
valuePlaceholder: String,
description: String,
parameters: Array,
environment: Object,
scenario: Scenario,
type: {
type: String,
default: ''
},
isReadOnly: {
type: Boolean,
default: false
},
suggestions: Array
},
data() {
return {
currentItem: null,
}
},
computed: {
keyText() {
return this.keyPlaceholder || this.$t("api_test.key");
},
valueText() {
return this.valuePlaceholder || this.$t("api_test.value");
}
},
methods: {
remove: function (index) {
//
this.parameters.splice(index, 1);
this.$emit('change', this.parameters);
},
change: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.parameters.forEach((item, index) => {
if (!item.name && !item.value) {
//
if (index !== this.parameters.length - 1) {
removeIndex = index;
}
//
isNeedCreate = false;
}
});
if (isNeedCreate) {
this.parameters.push(new KeyValue({type: 'text', enable: true, uuid: this.uuid(), contentType: 'text/plain'}));
}
this.$emit('change', this.parameters);
// TODO key
},
isDisable: function (index) {
return this.parameters.length - 1 === index;
},
querySearch(queryString, cb) {
let suggestions = this.suggestions;
let results = queryString ? suggestions.filter(this.createFilter(queryString)) : suggestions;
cb(results);
},
createFilter(queryString) {
return (restaurant) => {
return (restaurant.value.toLowerCase().indexOf(queryString.toLowerCase()) === 0);
};
},
funcSearch(queryString, cb) {
let funcs = MOCKJS_FUNC.concat(JMETER_FUNC);
let results = queryString ? funcs.filter(this.funcFilter(queryString)) : funcs;
// callback
cb(results);
},
funcFilter(queryString) {
return (func) => {
return (func.name.toLowerCase().indexOf(queryString.toLowerCase()) > -1);
};
},
uuid: function () {
return (((1 + Math.random()) * 0x100000) | 0).toString(16).substring(1);
},
advanced(item) {
this.$refs.variableAdvance.open();
this.currentItem = item;
},
typeChange(item) {
if (item.type === 'file') {
item.contentType = 'application/octet-stream';
} else {
item.contentType = 'text/plain';
}
}
},
created() {
if (this.parameters.length === 0 || this.parameters[this.parameters.length - 1].name) {
this.parameters.push(new KeyValue( {type: 'text', enable: true, uuid: this.uuid(), contentType: 'text/plain'}));
}
}
}
</script>
<style scoped>
.kv-description {
font-size: 13px;
}
.kv-row {
margin-top: 10px;
}
.kv-delete {
width: 60px;
}
.el-autocomplete {
width: 100%;
}
.kv-checkbox {
width: 20px;
margin-right: 10px;
}
.advanced-item-value >>> .el-dialog__body {
padding: 15px 25px;
}
.el-row {
margin-bottom: 5px;
}
.kv-type {
width: 70px;
}
.pointer {
cursor: pointer;
color: #1E90FF;
}
</style>

View File

@ -0,0 +1,116 @@
<template>
<div class="variable-input">
<el-input class="el-input__inner_pd" :disabled="isReadOnly" :value="value" v-bind="$attrs" :size="size" @change="change" @input="input"/>
<div :class="{'hidden': !showVariable}" class="variable-combine" v-if="value">
<div v-if="showCopy" class="variable">{{variable}}</div>
<el-tooltip v-if="showCopy" :content="$t('api_test.copied')" manual v-model="visible" placement="top" :visible-arrow="false">
<i class="el-icon-copy-document copy" @click="copy"/>
</el-tooltip>
</div>
</div>
</template>
<script>
export default {
name: "MsApiVariableInput",
props: {
value: String,
size: String,
isReadOnly: {
type: Boolean,
default: false
},
showVariable: {
type: Boolean,
default: true
},
showCopy: {
type: Boolean,
default: true
},
showCopyTipWithMultiple: {
type: Boolean,
default: false
},
},
data() {
return {
visible: false
}
},
methods: {
copy() {
let input = document.createElement("input");
document.body.appendChild(input);
input.value = this.variable;
input.select();
if (input.setSelectionRange) {
input.setSelectionRange(0, input.value.length);
}
document.execCommand("copy");
document.body.removeChild(input);
this.visible = true;
setTimeout(() => {
this.visible = false;
}, 1000);
},
change(value) {
this.$emit('change', value);
},
input(value) {
this.$emit('input', value);
}
},
computed: {
variable() {
return "${" + (this.showCopyTipWithMultiple ? (this.value + "_n") : this.value) + "}";
}
}
}
</script>
<style scoped>
.variable-input {
position: relative;
}
.el-input__inner_pd >>> .el-input__inner {
padding-right: 135px;
}
.variable-combine {
color: #7F7F7F;
max-width: 80px;
line-height: 32px;
position: absolute;
top: 0;
right: 70px;
margin-right: -20px;
display: flex;
align-items: center;
}
.variable-combine .variable {
display: inline-block;
max-width: 60px;
margin-right: 10px;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.variable-combine .copy {
font-size: 14px;
cursor: pointer;
color: #1E90FF;
}
.hidden {
visibility: hidden;
}
</style>

View File

@ -0,0 +1,42 @@
<template>
<el-aside :width="width" class="ms-aside-container"
:style="{'margin-left': !enableAsideHidden ? 0 : '-' + width}">
<slot></slot>
</el-aside>
</template>
<script>
export default {
name: "MsBottomContainer",
props: {
width: {
type: String,
default: '100%'
},
enableAsideHidden: {
type: Boolean,
default: true
},
}
}
</script>
<style scoped>
.ms-aside-container {
border: 1px solid #E6E6E6;
border-radius: 2px;
margin-top: 10px;
box-sizing: border-box;
background-color: #FFF;
}
.hiddenBottom i {
margin-left: -2px;
}
.hiddenBottom:hover i {
margin-left: 0;
color: white;
}
</style>

View File

@ -0,0 +1,57 @@
<template>
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col>
<el-input :disabled="isReadOnly" :value="value" v-bind="$attrs" step="100" size="small" type="number" @change="change" @input="input"
:placeholder="$t('api_test.request.assertions.response_in_time')"/>
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>Add</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import {Duration} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionDuration",
props: {
duration: Duration,
value: [Number, String],
edit: Boolean,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
methods: {
add() {
this.duration.value = this.value;
this.callback();
},
remove() {
this.duration.value = undefined;
},
change(value) {
this.$emit('change', value);
},
input(value) {
this.$emit('input', value);
}
}
}
</script>
<style scoped>
.assertion-btn {
text-align: center;
width: 60px;
}
</style>

View File

@ -0,0 +1,93 @@
<template>
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col>
<el-input :disabled="isReadOnly" v-model="jsonPath.expression" maxlength="200" size="small" show-word-limit
:placeholder="$t('api_test.request.extract.json_path_expression')"/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="jsonPath.expect" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.expect')"/>
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>Add</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import {JSONPath} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionJsonPath",
props: {
jsonPath: {
type: JSONPath,
default: () => {
return new JSONPath();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
}
},
watch: {
'jsonPath.expect'() {
this.setJSONPathDescription();
},
'jsonPath.expression'() {
this.setJSONPathDescription();
}
},
methods: {
add: function () {
this.list.push(this.getJSONPath());
this.callback();
},
remove: function () {
this.list.splice(this.index, 1);
},
getJSONPath() {
let jsonPath = new JSONPath(this.jsonPath);
jsonPath.description = jsonPath.expression + " expect: " + (jsonPath.expect ? jsonPath.expect : '');
return jsonPath;
},
setJSONPathDescription() {
this.jsonPath.description = this.jsonPath.expression + " expect: " + (this.jsonPath.expect ? this.jsonPath.expect : '');
}
}
}
</script>
<style scoped>
.assertion-select {
width: 250px;
}
.assertion-item {
width: 100%;
}
.assertion-btn {
text-align: center;
width: 60px;
}
</style>

View File

@ -0,0 +1,109 @@
<template>
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col class="assertion-select">
<el-select :disabled="isReadOnly" class="assertion-item" v-model="regex.subject" size="small"
:placeholder="$t('api_test.request.assertions.select_subject')">
<el-option label="Response Code" :value="subjects.RESPONSE_CODE"/>
<el-option label="Response Headers" :value="subjects.RESPONSE_HEADERS"/>
<el-option label="Response Data" :value="subjects.RESPONSE_DATA"/>
</el-select>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="regex.expression" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.expression')"/>
</el-col>
<el-col class="assertion-checkbox">
<el-checkbox v-model="regex.assumeSuccess" :disabled="isReadOnly">
{{ $t('api_test.request.assertions.ignore_status') }}
</el-checkbox>
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove"
v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>Add</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import {ASSERTION_REGEX_SUBJECT, Regex} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionRegex",
props: {
regex: {
type: Regex,
default: () => {
return new Regex();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
subjects: ASSERTION_REGEX_SUBJECT,
}
},
watch: {
'regex.subject'() {
this.setRegexDescription();
},
'regex.expression'() {
this.setRegexDescription();
}
},
methods: {
add: function () {
this.list.push(this.getRegex());
this.callback();
},
remove: function () {
this.list.splice(this.index, 1);
},
getRegex() {
let regex = new Regex(this.regex);
regex.description = regex.subject + " has: " + regex.expression;
return regex;
},
setRegexDescription() {
this.regex.description = this.regex.subject + " has: " + this.regex.expression;
}
}
}
</script>
<style scoped>
.assertion-select {
width: 250px;
}
.assertion-item {
width: 100%;
}
.assertion-checkbox {
text-align: center;
width: 120px;
}
.assertion-btn {
text-align: center;
width: 60px;
}
</style>

View File

@ -0,0 +1,122 @@
<template>
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col class="assertion-select">
<el-select :disabled="isReadOnly" class="assertion-item" v-model="subject" size="small"
:placeholder="$t('api_test.request.assertions.select_subject')">
<el-option label="Response Code" :value="subjects.RESPONSE_CODE"/>
<el-option label="Response Headers" :value="subjects.RESPONSE_HEADERS"/>
<el-option label="Response Data" :value="subjects.RESPONSE_DATA"/>
</el-select>
</el-col>
<el-col class="assertion-select">
<el-select :disabled="isReadOnly" class="assertion-item" v-model="condition" size="small"
:placeholder="$t('api_test.request.assertions.select_condition')">
<el-option :label="$t('api_test.request.assertions.contains')" value="CONTAINS"/>
<el-option :label="$t('api_test.request.assertions.not_contains')" value="NOT_CONTAINS"/>
<el-option :label="$t('api_test.request.assertions.equals')" value="EQUALS"/>
<el-option :label="$t('api_test.request.assertions.start_with')" value="START_WITH"/>
<el-option :label="$t('api_test.request.assertions.end_with')" value="END_WITH"/>
</el-select>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="value" maxlength="200" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.value')"/>
</el-col>
<el-col class="assertion-checkbox">
<el-checkbox v-model="assumeSuccess" :disabled="isReadOnly">
{{ $t('api_test.request.assertions.ignore_status') }}
</el-checkbox>
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add">Add</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import {Regex, ASSERTION_REGEX_SUBJECT} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionText",
props: {
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
subjects: ASSERTION_REGEX_SUBJECT,
subject: "",
condition: "",
assumeSuccess: false,
value: ""
}
},
methods: {
add: function () {
this.list.push(this.toRegex());
this.callback();
},
toRegex: function () {
let expression = "";
let description = this.subject;
switch (this.condition) {
case "CONTAINS":
expression = ".*" + this.value + ".*";
description += " contains: " + this.value;
break;
case "NOT_CONTAINS":
expression = "(?s)^((?!" + this.value + ").)*$";
description += " not contains: " + this.value;
break;
case "EQUALS":
expression = "^" + this.value + "$";
description += " equals: " + this.value;
break;
case "START_WITH":
expression = "^" + this.value;
description += " start with: " + this.value;
break;
case "END_WITH":
expression = this.value + "$";
description += " end with: " + this.value;
break;
}
return new Regex({
subject: this.subject,
expression: expression,
description: description,
assumeSuccess: this.assumeSuccess
}
);
}
}
}
</script>
<style scoped>
.assertion-select {
width: 250px;
}
.assertion-item {
width: 100%;
}
.assertion-checkbox {
text-align: center;
width: 120px;
}
.assertion-btn {
width: 60px;
}
</style>

View File

@ -0,0 +1,142 @@
<template>
<div>
<div class="assertion-add">
<el-row :gutter="10">
<el-col :span="4">
<el-select :disabled="isReadOnly" class="assertion-item" v-model="type" :placeholder="$t('api_test.request.assertions.select_type')"
size="small">
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
<el-option :label="'JSONPath'" :value="options.JSON_PATH"/>
<el-option :label="$t('api_test.request.assertions.response_time')" :value="options.DURATION"/>
</el-select>
</el-col>
<el-col :span="20">
<ms-api-assertion-text :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.TEXT" :callback="after"/>
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" v-if="type === options.REGEX" :callback="after"/>
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" v-if="type === options.JSON_PATH" :callback="after"/>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
v-if="type === options.DURATION" :callback="after"/>
<el-button v-if="!type" :disabled="true" type="primary" size="small">Add</el-button>
</el-col>
</el-row>
</div>
<div>
<el-row :gutter="10" class="json-path-suggest-button">
<el-button size="small" type="primary" @click="suggestJsonOpen">
{{$t('api_test.request.assertions.json_path_suggest')}}
</el-button>
<el-button size="small" type="danger" @click="clearJson">
{{$t('api_test.request.assertions.json_path_clear')}}
</el-button>
</el-row>
</div>
<ms-api-jsonpath-suggest-list @addJsonpathSuggest="addJsonpathSuggest" :request="request" ref="jsonpathSuggestList"/>
<ms-api-assertions-edit :is-read-only="isReadOnly" :assertions="assertions"/>
</div>
</template>
<script>
import MsApiAssertionText from "./ApiAssertionText";
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionDuration from "./ApiAssertionDuration";
import {ASSERTION_TYPE, Assertions, HttpRequest, JSONPath} from "../../model/ScenarioModel";
import MsApiAssertionsEdit from "./ApiAssertionsEdit";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
import MsApiJsonpathSuggestList from "./ApiJsonpathSuggestList";
export default {
name: "MsApiAssertions",
components: {
MsApiJsonpathSuggestList,
MsApiAssertionJsonPath,
MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText},
props: {
assertions: Assertions,
request: HttpRequest,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
options: ASSERTION_TYPE,
time: "",
type: "",
}
},
methods: {
after() {
this.type = "";
},
suggestJsonOpen() {
if (!this.request.debugRequestResult) {
this.$message(this.$t('api_test.request.assertions.debug_first'));
return;
}
this.$refs.jsonpathSuggestList.open();
},
addJsonpathSuggest(jsonPathList) {
jsonPathList.forEach(jsonPath => {
let jsonItem = new JSONPath();
jsonItem.expression = jsonPath.json_path;
jsonItem.expect = jsonPath.json_value;
jsonItem.setJSONPathDescription();
this.assertions.jsonPath.push(jsonItem);
});
},
clearJson() {
this.assertions.jsonPath = [];
}
}
}
</script>
<style scoped>
.assertion-item {
width: 100%;
}
.assertion-add {
padding: 10px;
border: #DCDFE6 solid 1px;
margin: 5px 0;
border-radius: 5px;
}
.bg-purple-dark {
background: #99a9bf;
}
.bg-purple {
background: #d3dce6;
}
.bg-purple-light {
background: #e5e9f2;
}
.grid-content {
border-radius: 4px;
min-height: 36px;
}
.row-bg {
padding: 10px 0;
background-color: #f9fafc;
}
.json-path-suggest-button {
text-align: right;
}
</style>

View File

@ -0,0 +1,82 @@
<template>
<div>
<div class="assertion-item-editing regex" v-if="assertions.regex.length > 0">
<div>
{{$t("api_test.request.assertions.regex")}}
</div>
<div class="regex-item" v-for="(regex, index) in assertions.regex" :key="index">
<ms-api-assertion-regex :is-read-only="isReadOnly" :list="assertions.regex" :regex="regex" :edit="true" :index="index"/>
</div>
</div>
<div class="assertion-item-editing json_path" v-if="assertions.jsonPath.length > 0">
<div>
{{'JSONPath'}}
</div>
<div class="regex-item" v-for="(jsonPath, index) in assertions.jsonPath" :key="index">
<ms-api-assertion-json-path :is-read-only="isReadOnly" :list="assertions.jsonPath" :json-path="jsonPath" :edit="true" :index="index"/>
</div>
</div>
<div class="assertion-item-editing response-time" v-if="isShow">
<div>
{{$t("api_test.request.assertions.response_time")}}
</div>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="assertions.duration.value" :duration="assertions.duration" :edit="true"/>
</div>
</div>
</template>
<script>
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionDuration from "./ApiAssertionDuration";
import {Assertions} from "../../model/ScenarioModel";
import MsApiAssertionJsonPath from "./ApiAssertionJsonPath";
export default {
name: "MsApiAssertionsEdit",
components: {MsApiAssertionJsonPath, MsApiAssertionDuration, MsApiAssertionRegex},
props: {
assertions: Assertions,
isReadOnly: {
type: Boolean,
default: false
}
},
computed: {
isShow() {
let rt = this.assertions.duration;
return rt.value !== undefined;
}
}
}
</script>
<style scoped>
.assertion-item-editing {
padding-left: 10px;
margin-top: 10px;
}
.assertion-item-editing.regex {
border-left: 2px solid #7B0274;
}
.assertion-item-editing.json_path {
border-left: 2px solid #44B3D2;
}
.assertion-item-editing.response-time {
border-left: 2px solid #DD0240;
}
.regex-item {
margin-top: 10px;
}
</style>

View File

@ -0,0 +1,115 @@
<template>
<el-dialog :title="$t('api_test.request.assertions.json_path_add')"
:visible.sync="dialogFormVisible"
@close="close"
width="60%" v-loading="result.loading"
:close-on-click-modal="false"
top="50px">
<el-container class="main-content">
<el-container>
<el-main class="case-content">
<el-table
:data="jsonPathList"
row-key="id"
@select-all="handleSelectAll"
@select="handleSelectionChange"
height="50vh"
ref="table">
<el-table-column type="selection"/>
<el-table-column
prop="name"
:label="$t('api_test.request.extract.json_path_expression')"
style="width: 100%">
<template v-slot:default="scope">
{{scope.row.json_path}}
</template>
</el-table-column>
<el-table-column
prop="name"
:label="$t('api_test.request.assertions.value')"
style="width: 100%">
<template v-slot:default="scope">
{{scope.row.json_value}}
</template>
</el-table-column>
</el-table>
</el-main>
</el-container>
</el-container>
<template v-slot:footer>
<ms-dialog-footer @cancel="dialogFormVisible = false" @confirm="commit"/>
</template>
</el-dialog>
</template>
<script>
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
import {HttpRequest} from "../../model/ScenarioModel";
export default {
name: "MsApiJsonpathSuggestList",
components: {MsDialogFooter},
data() {
return {
result: {},
dialogFormVisible: false,
isCheckAll: false,
selectItems: new Set(),
jsonPathList: [],
};
},
props: {
request: HttpRequest,
},
methods: {
close() {
this.selectItems.clear();
},
open() {
this.getJsonPaths();
},
getJsonPaths() {
if (this.request.debugRequestResult) {
let param = {
jsonPath: this.request.debugRequestResult.responseResult.body
};
this.result = this.$post("/api/getJsonPaths", param).then(response => {
this.jsonPathList = response.data.data;
this.dialogFormVisible = true;
}).catch(() => {
this.$warning(this.$t('api_test.request.assertions.json_path_err'));
this.dialogFormVisible = false;
});
}
},
handleSelectAll(selection) {
if (selection.length > 0) {
this.selectItems = new Set(this.jsonPathList);
} else {
this.selectItems = new Set();
}
},
handleSelectionChange(selection, row) {
if (this.selectItems.has(row)) {
this.selectItems.delete(row);
} else {
this.selectItems.add(row);
}
},
commit() {
this.$emit("addJsonpathSuggest", this.selectItems);
this.dialogFormVisible = false;
}
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,99 @@
<template>
<el-dialog :close-on-click-modal="false" :title="$t('api_test.delimit.request.title')" :visible.sync="httpVisible"
width="35%"
@closed="closeApi"
:destroy-on-close="true">
<el-form :model="httpForm" label-position="right" label-width="120px" size="small" :rules="rule" ref="httpForm">
<el-form-item :label="$t('commons.name')" prop="name">
<el-input v-model="httpForm.name" autocomplete="off" :placeholder="$t('commons.name')"/>
</el-form-item>
<el-form-item :label="$t('api_report.request')" prop="request">
<el-select v-model="value" style="width: 20%">
<el-option v-for="item in options" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
<el-input style="margin-left:10px;width: 76%" v-model="httpForm.request" autocomplete="off"
:placeholder="$t('api_test.delimit.request.path_info')"/>
</el-form-item>
<el-form-item :label="$t('api_test.delimit.request.responsible')" prop="responsible">
<el-input v-model="httpForm.responsible" autocomplete="off"
:placeholder="$t('api_test.delimit.request.responsible')"/>
</el-form-item>
<el-form-item :label="$t('commons.description')" prop="description" style="margin-bottom: 29px">
<el-input v-model="httpForm.description"
:placeholder="$t('commons.description')"/>
</el-form-item>
</el-form>
<template v-slot:footer>
<ms-dialog-footer
@cancel="httpVisible = false"
@confirm="saveApi"/>
</template>
</el-dialog>
</template>
<script>
import MsDialogFooter from "../../../../common/components/MsDialogFooter";
export default {
name: "MsAddBasisHttpApi",
components: {MsDialogFooter},
props: {},
data() {
return {
httpForm: {},
httpVisible: false,
rule: {},
value: "",
options: [{
id: 'GET',
label: 'GET'
}, {
id: 'POST',
label: 'POST'
}],
}
},
created() {
},
methods: {
saveApi() {
},
closeApi() {
},
open() {
this.httpVisible = true;
},
}
}
</script>
<style scoped>
.ht-btn-remove {
color: white;
background-color: #DCDFE6;
}
.ht-btn-confirm {
color: white;
background-color: #1483F6;
}
.ht-btn-add {
border: 0px;
margin-top: 10px;
color: #1483F6;
background-color: white;
}
.ht-tb {
background-color: white;
border: 0px;
}
</style>

View File

@ -0,0 +1,98 @@
<template>
<div>
<el-radio-group v-model="body.type" size="mini">
<el-radio-button :disabled="isReadOnly" :label="type.KV">
{{ $t('api_test.request.body_kv') }}
</el-radio-button>
<el-radio-button :disabled="isReadOnly" :label="type.RAW">
{{ $t('api_test.request.body_text') }}
</el-radio-button>
</el-radio-group>
<ms-dropdown :default-command="body.format" v-if="body.type == 'Raw'" :commands="modes" @command="modeChange"/>
<ms-api-variable :is-read-only="isReadOnly"
:parameters="body.kvs"
:environment="environment"
:scenario="scenario"
:extract="extract"
type="body"
:description="$t('api_test.request.parameters_desc')"
v-if="body.isKV()"/>
<div class="body-raw" v-if="body.type == 'Raw'">
<ms-code-edit :mode="body.format" :read-only="isReadOnly" :data.sync="body.raw" :modes="modes" ref="codeEdit"/>
</div>
</div>
</template>
<script>
import MsApiKeyValue from "../ApiKeyValue";
import {Body, BODY_FORMAT, BODY_TYPE, Scenario} from "../../model/ScenarioModel";
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsDropdown from "../../../../common/components/MsDropdown";
import MsApiVariable from "../ApiVariable";
export default {
name: "MsApiBody",
components: {MsApiVariable, MsDropdown, MsCodeEdit, MsApiKeyValue},
props: {
body: Body,
scenario: Scenario,
environment: Object,
extract: Object,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
type: BODY_TYPE,
modes: ['text', 'json', 'xml', 'html']
};
},
methods: {
modeChange(mode) {
this.body.format = mode;
}
},
created() {
if (!this.body.type) {
this.body.type = BODY_TYPE.KV;
}
if (!this.body.format) {
this.body.format = BODY_FORMAT.TEXT;
}
this.body.kvs.forEach(param => {
if (!param.type) {
param.type = 'text';
}
});
}
}
</script>
<style scoped>
.textarea {
margin-top: 10px;
}
.body-raw {
padding: 15px 0;
height: 300px;
}
.el-dropdown {
margin-left: 20px;
line-height: 30px;
}
.ace_editor {
border-radius: 5px;
}
</style>

View File

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

View File

@ -0,0 +1,73 @@
<template>
<div role="tablist" aria-multiselectable="true">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'MsApiCollapse',
componentName: 'MsApiCollapse',
props: {
accordion: Boolean,
value: {
type: [Array, String, Number],
default() {
return [];
}
}
},
data() {
return {
activeNames: [].concat(this.value)
};
},
provide() {
return {
collapse: this
};
},
watch: {
value(value) {
this.activeNames = [].concat(value);
}
},
methods: {
setActiveNames(activeNames, item) {
activeNames = [].concat(activeNames);
let value = this.accordion ? activeNames[0] : activeNames;
this.activeNames = activeNames;
this.$emit('input', value);
},
handleItemCollapseClick(item) {
if (this.accordion) {
this.setActiveNames(
(this.activeNames[0] || this.activeNames[0] === 0) && item.name, item);
} else {
let activeNames = this.activeNames.slice(0);
let index = activeNames.indexOf(item.name);
if (index > -1) {
activeNames.splice(index, 1);
} else {
activeNames.push(item.name);
}
this.setActiveNames(activeNames, item);
}
},
handleItemClick(item) {
this.$emit('change', item.name);
}
},
created() {
this.$on('item-click', this.handleItemClick);
this.$on('collapse-click', this.handleItemCollapseClick);
}
};
</script>

View File

@ -0,0 +1,134 @@
<template>
<div class="el-collapse-item"
:class="{'is-active': isActive, 'is-disabled': disabled }">
<div
role="tab"
:aria-expanded="isActive"
:aria-controls="`el-collapse-content-${id}`"
:aria-describedby="`el-collapse-content-${id}`"
@click="handleHeaderClick"
>
<div
class="el-collapse-item__header"
role="button"
:id="`el-collapse-head-${id}`"
:tabindex="disabled ? undefined : 0"
@keyup.space.enter.stop="handleEnterClick"
:class="{
'focusing': focusing,
'is-active': isActive
}"
@focus="handleFocus"
@blur="focusing = false"
>
<div @click.stop="handleCollapseClick">
<i class="el-collapse-item__arrow el-icon-arrow-right"
:class="{'is-active': isActive}">
</i>
</div>
<slot name="title">{{title}}</slot>
</div>
</div>
<el-collapse-transition>
<div
class="el-collapse-item__wrap"
v-show="isActive"
role="tabpanel"
:aria-hidden="!isActive"
:aria-labelledby="`el-collapse-head-${id}`"
:id="`el-collapse-content-${id}`"
>
<div class="el-collapse-item__content">
<slot></slot>
</div>
</div>
</el-collapse-transition>
</div>
</template>
<script>
import Emitter from 'element-ui/src/mixins/emitter';
import {generateId} from 'element-ui/src/utils/util';
export default {
name: 'MsApiCollapseItem',
componentName: 'MsApiCollapseItem',
mixins: [Emitter],
data() {
return {
contentWrapStyle: {
height: 'auto',
display: 'block'
},
contentHeight: 0,
focusing: false,
isClick: false,
id: generateId()
};
},
inject: ['collapse'],
props: {
title: String,
name: {
type: [String, Number],
default() {
return this._uid;
}
},
disabled: Boolean
},
computed: {
isActive() {
return this.collapse.activeNames.indexOf(this.name) > -1;
}
},
methods: {
handleFocus() {
setTimeout(() => {
if (!this.isClick) {
this.focusing = true;
} else {
this.isClick = false;
}
}, 50);
},
handleHeaderClick() {
if (this.disabled) return;
this.dispatch('MsApiCollapse', 'item-click', this);
this.focusing = false;
this.isClick = true;
},
handleCollapseClick() {
if (this.disabled) return;
this.dispatch('MsApiCollapse', 'collapse-click', this);
this.focusing = false;
this.isClick = true;
},
handleEnterClick() {
this.dispatch('MsApiCollapse', 'item-click', this);
}
}
};
</script>
<style scoped>
.el-collapse-item__header {
padding-left: 7px;
border-right: 2px solid #409eff;
}
.el-collapse-item__header.is-active {
background-color: #E9E9E9;
}
.el-collapse-item__content {
padding-bottom: 0;
}
</style>

View File

@ -0,0 +1,30 @@
<template>
<el-select :disabled="isReadOnly" v-model="request.method" class="request-method-select" @change="change">
<el-option label="GET" value="GET"/>
<el-option label="POST" value="POST"/>
<el-option label="PUT" value="PUT"/>
<el-option label="PATCH" value="PATCH"/>
<el-option label="DELETE" value="DELETE"/>
<el-option label="OPTIONS" value="OPTIONS"/>
<el-option label="HEAD" value="HEAD"/>
<el-option label="CONNECT" value="CONNECT"/>
</el-select>
</template>
<script>
export default {
name: "ApiRequestMethodSelect",
props: ['isReadOnly', 'request'],
methods: {
change(value) {
this.$emit('change', value);
}
}
}
</script>
<style scoped>
.request-method-select {
width: 110px;
}
</style>

View File

@ -0,0 +1,118 @@
<template>
<div class="card-container">
<el-card class="card-content" v-loading="result.loading">
<el-form :model="httpForm" :rules="rule" ref="httpForm" :inline="true" :label-position="labelPosition">
<div style="float: right;margin-right: 20px">
<el-button type="primary" size="small">{{$t('commons.save')}}</el-button>
<el-button type="primary" size="small" @click="runTest(httpForm)">{{$t('commons.test')}}</el-button>
</div>
<br/>
<div style="font-size: 16px;color: #333333">{{$t('test_track.plan_view.base_info')}}</div>
<br/>
<el-form-item :label="$t('commons.name')" prop="name">
<el-input class="ms-http-input" size="small"/>
</el-form-item>
<el-form-item :label="$t('test_track.module.module')" prop="module">
<el-select class="ms-http-input" size="small" v-model="moduleValue">
<el-option v-for="item in options" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-form-item>
<el-form-item :label="$t('api_test.delimit.request.responsible')" prop="responsible">
<el-input class="ms-http-input" size="small"/>
</el-form-item>
<el-form-item :label="$t('commons.status')" prop="status">
<el-select class="ms-http-input" size="small" v-model="moduleValue">
<el-option v-for="item in options" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-form-item>
<el-form-item :label="$t('api_report.request')" prop="responsible">
<el-input :placeholder="$t('api_test.delimit.request.path_info')" v-model="httpForm.request"
class="ms-http-input" size="small" style="margin-top: 5px">
<el-select v-model="reqValue" slot="prepend" style="width: 100px" size="small">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
</el-form-item>
<el-form-item :label="$t('commons.description')" prop="description">
<el-input class="ms-http-textarea"
type="textarea"
:autosize="{ minRows: 2, maxRows: 10}"
:rows="2" size="small"/>
</el-form-item>
<div style="font-size: 16px;color: #333333;padding-top: 30px">请求参数</div>
<br/>
<!-- HTTP 请求参数 -->
<ms-api-request-form :debug-report-id="debugReportId" :request="selected" :scenario="currentScenario"/>
</el-form>
<div style="font-size: 16px;color: #333333 ;padding-top: 30px">响应内容</div>
<br/>
<ms-response-text :response="responseData"></ms-response-text>
</el-card>
</div>
</template>
<script>
import MsApiRequestForm from "../request/ApiRequestForm";
import {Request, Scenario} from "../../model/ScenarioModel";
import MsResponseText from "../../../report/components/ResponseText";
export default {
name: "MsAddCompleteHttpApi",
components: {MsResponseText, MsApiRequestForm},
data() {
return {
result: {},
rule: {},
labelPosition: 'right',
httpForm: {},
options: [],
reqValue: '',
debugReportId: '',
responseData: {},
currentScenario: Scenario,
selected: [Scenario, Request],
reqOptions: [{
id: 'GET',
label: 'GET'
}, {
id: 'POST',
label: 'POST'
}],
moduleValue: '',
}
},
props: {httpData: {},},
methods: {
runTest(data) {
this.$emit('runTest', data);
}
},
watch: {
httpData(v) {
this.httpForm = v;
}
}
}
</script>
<style scoped>
.ms-http-input {
width: 500px;
}
.ms-http-textarea {
width: 500px;
}
</style>

View File

@ -0,0 +1,97 @@
<template>
<div class="card-container">
<el-card class="card-content" v-loading="result.loading">
<el-form :model="httpForm" :rules="rule" ref="httpForm" :inline="true" :label-position="labelPosition">
<div style="font-size: 16px;color: #333333">{{$t('test_track.plan_view.base_info')}}</div>
<br/>
<el-form-item :label="$t('api_report.request')" prop="responsible">
<el-input :placeholder="$t('api_test.delimit.request.path_info')" v-model="httpForm.request"
class="ms-http-input" size="small">
<el-select v-model="reqValue" slot="prepend" style="width: 100px" size="small">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
</el-form-item>
<el-form-item>
<el-dropdown split-button type="primary" class="ms-api-buttion" @click="handleCommand"
@command="handleCommand" size="small">
{{$t('commons.test')}}
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="save_as">{{$t('api_test.delimit.request.save_as')}}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-form-item>
<div style="font-size: 16px;color: #333333;padding-top: 30px">请求参数</div>
<br/>
<!-- HTTP 请求参数 -->
<ms-api-request-form :debug-report-id="debugReportId" :request="selected" :scenario="currentScenario"/>
</el-form>
<div style="font-size: 16px;color: #333333 ;padding-top: 30px">响应内容</div>
<br/>
<ms-response-text :response="responseData"></ms-response-text>
</el-card>
</div>
</template>
<script>
import MsApiRequestForm from "../request/ApiRequestForm";
import {Request, Scenario} from "../../model/ScenarioModel";
import MsResponseText from "../../../report/components/ResponseText";
export default {
name: "ApiConfig",
components: {MsResponseText, MsApiRequestForm},
data() {
return {
result: {},
rule: {},
labelPosition: 'right',
httpForm: {},
options: [],
reqValue: '',
debugReportId: '',
responseData: {},
currentScenario: Scenario,
selected: [Scenario, Request],
reqOptions: [{
id: 'GET',
label: 'GET'
}, {
id: 'POST',
label: 'POST'
}],
moduleValue: '',
}
},
props: {httpData: {},},
methods: {
handleCommand(e) {
if (e === "save_as") {
this.saveAs();
}
},
saveAs() {
this.$emit('saveAs', this.httpForm);
}
},
watch: {
httpData(v) {
this.httpForm = v;
}
}
}
</script>
<style scoped>
.ms-http-input {
width: 500px;
margin-top:5px;
}
</style>

View File

@ -0,0 +1,95 @@
<template>
<div>
<div class="extract-description">
{{$t('api_test.request.extract.description')}}
</div>
<div class="extract-add">
<el-row :gutter="10">
<el-col :span="2">
<el-select :disabled="isReadOnly" class="extract-item" v-model="type" :placeholder="$t('api_test.request.extract.select_type')"
size="small">
<el-option :label="$t('api_test.request.extract.regex')" :value="options.REGEX"/>
<el-option label="JSONPath" :value="options.JSON_PATH"/>
<el-option label="XPath" :value="options.XPATH"/>
</el-select>
</el-col>
<el-col :span="22">
<ms-api-extract-common :is-read-only="isReadOnly" :extract-type="type" :list="list" v-if="type" :callback="after"/>
</el-col>
<el-button v-if="!type" :disabled="true" type="primary" size="small">Add</el-button>
</el-row>
</div>
<ms-api-extract-edit :is-read-only="isReadOnly" :extract="extract"/>
</div>
</template>
<script>
import {EXTRACT_TYPE, Extract} from "../../model/ScenarioModel";
import MsApiExtractEdit from "./ApiExtractEdit";
import MsApiExtractCommon from "./ApiExtractCommon";
export default {
name: "MsApiExtract",
components: {
MsApiExtractCommon,
MsApiExtractEdit,
},
props: {
extract: Extract,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
options: EXTRACT_TYPE,
type: "",
}
},
methods: {
after() {
this.type = "";
}
},
computed: {
list() {
switch (this.type) {
case EXTRACT_TYPE.REGEX:
return this.extract.regex;
case EXTRACT_TYPE.JSON_PATH:
return this.extract.json;
case EXTRACT_TYPE.XPATH:
return this.extract.xpath;
default:
return [];
}
}
}
}
</script>
<style scoped>
.extract-description {
font-size: 13px;
margin-bottom: 10px;
}
.extract-item {
width: 100%;
}
.extract-add {
padding: 10px;
border: #DCDFE6 solid 1px;
margin: 5px 0;
border-radius: 5px;
}
</style>

View File

@ -0,0 +1,173 @@
<template>
<div>
<el-row :gutter="10" type="flex" justify="space-between" align="middle">
<el-col v-if="extractType === 'Regex'" :span="5">
<el-select :disabled="isReadOnly" class="extract-item" v-model="common.useHeaders" :placeholder="$t('api_test.request.assertions.select_subject')" size="small">
<el-option v-for="item in useHeadersOption" :key="item.value" :label="item.label" :value="item.value"/>
</el-select>
</el-col>
<el-col>
<ms-api-variable-input :is-read-only="isReadOnly" v-model="common.variable" size="small" maxlength="60"
@change="change" :show-copy-tip-with-multiple="common.multipleMatching" show-word-limit :placeholder="$t('api_test.variable_name')"/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="common.expression" size="small" show-word-limit
:placeholder="expression"/>
</el-col>
<el-col class="multiple_checkbox">
<el-checkbox v-model="common.multipleMatching" :disabled="isReadOnly">
{{ $t('api_test.request.extract.multiple_matching') }}
</el-checkbox>
</el-col>
<el-col class="extract-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove"
v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>Add</el-button>
</el-col>
</el-row>
</div>
</template>
<script>
import {EXTRACT_TYPE, ExtractCommon} from "../../model/ScenarioModel";
import MsApiVariableInput from "../ApiVariableInput";
export default {
name: "MsApiExtractCommon",
components: {MsApiVariableInput},
props: {
extractType: {
type: String,
validator: function (value) {
return [EXTRACT_TYPE.XPATH, EXTRACT_TYPE.JSON_PATH, EXTRACT_TYPE.REGEX].indexOf(value) !== -1
}
},
common: {
type: ExtractCommon,
default: () => {
return new ExtractCommon();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
visible: false,
useHeadersOption: [
{label: 'Body',value:'false'},
{label: 'Request Headers',value:'request_headers'},
{label: 'Body (unescaped)', value:'unescaped'},
{label: 'Body as a Document', value:'as_document'},
{label: 'Response Headers', value:'true'},
{label: 'URL', value:'URL'},
{label: 'Response Code', value:'code'},
{label: 'Response Message', value:'message'}
]
}
},
methods: {
add() {
this.common.type = this.extractType;
this.list.push(new ExtractCommon(this.extractType, this.common));
this.clear();
this.callback();
},
change(variable) {
this.common.value = "${" + variable + "}";
},
remove() {
this.list.splice(this.index, 1);
},
clear() {
this.common.variable = null;
this.common.expression = null;
this.common.value = null;
},
copy() {
let input = document.createElement("input");
document.body.appendChild(input);
input.value = this.common.value;
input.select();
if (input.setSelectionRange) {
input.setSelectionRange(0, input.value.length);
}
document.execCommand("copy");
document.body.removeChild(input);
this.visible = true;
setTimeout(() => {
this.visible = false;
}, 1000);
}
},
computed: {
expression() {
switch (this.extractType) {
case EXTRACT_TYPE.REGEX:
return this.$t('api_test.request.extract.regex_expression');
case EXTRACT_TYPE.JSON_PATH:
return this.$t('api_test.request.extract.json_path_expression');
case EXTRACT_TYPE.XPATH:
return this.$t('api_test.request.extract.xpath_expression');
default:
return "";
}
}
}
}
</script>
<style scoped>
.variable {
position: relative;
}
.variable-combine {
color: #7F7F7F;
max-width: 80px;
line-height: 32px;
position: absolute;
top: 0;
right: 25px;
margin-right: -20px;
display: flex;
align-items: center;
}
.variable-combine .value {
display: inline-block;
max-width: 60px;
margin-right: 10px;
overflow-x: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.variable-combine .copy {
font-size: 14px;
cursor: pointer;
color: #1E90FF;
}
.extract-btn {
width: 60px;
}
.multiple_checkbox {
text-align: center;
width: 120px;
}
</style>

View File

@ -0,0 +1,84 @@
<template>
<div>
<div class="extract-item-editing regex" v-if="extract.regex.length > 0">
<div>
{{$t("api_test.request.extract.regex")}}
</div>
<div class="regex-item" v-for="(regex, index) in extract.regex" :key="index">
<ms-api-extract-common :is-read-only="isReadOnly" :extract-type="type.REGEX" :list="extract.regex" :common="regex"
:edit="true" :index="index"/>
</div>
</div>
<div class="extract-item-editing json" v-if="extract.json.length > 0">
<div>
JSONPath
</div>
<div class="regex-item" v-for="(json, index) in extract.json" :key="index">
<ms-api-extract-common :is-read-only="isReadOnly" :extract-type="type.JSON_PATH" :list="extract.json" :common="json"
:edit="true" :index="index"/>
</div>
</div>
<div class="extract-item-editing xpath" v-if="extract.xpath.length > 0">
<div>
XPath
</div>
<div class="regex-item" v-for="(xpath, index) in extract.xpath" :key="index">
<ms-api-extract-common :is-read-only="isReadOnly" :extract-type="type.XPATH" :list="extract.xpath" :common="xpath"
:edit="true" :index="index"/>
</div>
</div>
</div>
</template>
<script>
import {Extract, EXTRACT_TYPE} from "../../model/ScenarioModel";
import MsApiExtractCommon from "./ApiExtractCommon";
export default {
name: "MsApiExtractEdit",
components: {MsApiExtractCommon},
props: {
extract: Extract,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
type: EXTRACT_TYPE
}
}
}
</script>
<style scoped>
.extract-item-editing {
padding-left: 10px;
margin-top: 10px;
}
.extract-item-editing.regex {
border-left: 2px solid #7B0274;
}
.extract-item-editing.json {
border-left: 2px solid #44B3D2;
}
.extract-item-editing.xpath {
border-left: 2px solid #E6A200;
}
.regex-item {
margin-top: 10px;
}
</style>

View File

@ -0,0 +1,152 @@
<template>
<div >
<el-row>
<el-col :span="20" class="script-content">
<ms-code-edit v-if="isCodeEditAlive" :mode="codeEditModeMap[jsr223Processor.language]" :read-only="isReadOnly" :data.sync="jsr223Processor.script" theme="eclipse" :modes="['java','python']" ref="codeEdit"/>
</el-col>
<el-col :span="4" class="script-index">
<ms-dropdown :default-command="jsr223Processor.language" :commands="languages" @command="languageChange"/>
<div class="template-title">{{$t('api_test.request.processor.code_template')}}</div>
<div v-for="(template, index) in codeTemplates" :key="index" class="code-template">
<el-link :disabled="template.disabled" @click="addTemplate(template)">{{template.title}}</el-link>
</div>
<div class="document-url">
<el-link href="https://jmeter.apache.org/usermanual/component_reference.html#BeanShell_PostProcessor" type="primary">{{$t('commons.reference_documentation')}}</el-link>
<ms-instructions-icon :content="$t('api_test.request.processor.bean_shell_processor_tip')"/>
</div>
</el-col>
</el-row>
</div>
</template>
<script>
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsInstructionsIcon from "../../../../common/components/MsInstructionsIcon";
import MsDropdown from "../../../../common/components/MsDropdown";
export default {
name: "MsJsr233Processor",
components: {MsDropdown, MsInstructionsIcon, MsCodeEdit},
data() {
return {
codeTemplates: [
{
title: this.$t('api_test.request.processor.code_template_get_variable'),
value: 'vars.get("variable_name")',
},
{
title: this.$t('api_test.request.processor.code_template_set_variable'),
value: 'vars.put("variable_name", "variable_value")',
},
{
title: this.$t('api_test.request.processor.code_template_get_global_variable'),
value: 'props.get("variable_name")',
},
{
title: this.$t('api_test.request.processor.code_template_set_global_variable'),
value: 'props.put("variable_name", "variable_value")',
},
{
title: this.$t('api_test.request.processor.code_template_get_response_header'),
value: 'prev.getResponseHeaders()',
disabled: this.isPreProcessor
},
{
title: this.$t('api_test.request.processor.code_template_get_response_code'),
value: 'prev.getResponseCode()',
disabled: this.isPreProcessor
},
{
title: this.$t('api_test.request.processor.code_template_get_response_result'),
value: 'prev.getResponseDataAsString()',
disabled: this.isPreProcessor
}
],
isCodeEditAlive: true,
languages: [
'beanshell',"python"
],
codeEditModeMap: {
beanshell: 'java',
python: 'python'
}
}
},
props: {
type: {
type: String,
},
isReadOnly: {
type: Boolean,
default: false
},
jsr223Processor: {
type: Object,
},
isPreProcessor: {
type: Boolean,
default: false
}
},
watch: {
jsr223Processor() {
this.reload();
}
},
methods: {
addTemplate(template) {
if (!this.jsr223Processor.script) {
this.jsr223Processor.script = "";
}
this.jsr223Processor.script += template.value;
if (this.jsr223Processor.language === 'beanshell') {
this.jsr223Processor.script += ';';
}
this.reload();
},
reload() {
this.isCodeEditAlive = false;
this.$nextTick(() => (this.isCodeEditAlive = true));
},
languageChange(language) {
this.jsr223Processor.language = language;
}
}
}
</script>
<style scoped>
.ace_editor {
border-radius: 5px;
}
.script-content {
height: calc(100vh - 570px);
}
.script-index {
padding: 0 20px;
}
.template-title {
margin-bottom: 5px;
font-weight: bold;
font-size: 15px;
}
.document-url {
margin-top: 10px;
}
.instructions-icon {
margin-left: 5px;
}
.ms-dropdown {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,191 @@
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
<el-form :model="request" :rules="rules" ref="request" label-width="100px" :disabled="isReadOnly">
<el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_test.request.parameters')" name="parameters">
<ms-api-variable :is-read-only="isReadOnly"
:parameters="request.parameters"
:environment="scenario.environment"
:scenario="scenario"
:extract="request.extract"
:description="$t('api_test.request.parameters_desc')"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.headers')" name="headers">
<ms-api-key-value :is-read-only="isReadOnly" :isShowEnable="true" :suggestions="headerSuggestions" :items="request.headers"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.body')" name="body">
<ms-api-body :is-read-only="isReadOnly"
:body="request.body"
:scenario="scenario"
:extract="request.extract"
:environment="scenario.environment"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
<ms-api-assertions :request="request" :is-read-only="isReadOnly" :assertions="request.assertions"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract.label')" name="extract">
<ms-api-extract :is-read-only="isReadOnly" :extract="request.extract"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.processor.pre_exec_script')" name="jsr223PreProcessor">
<ms-jsr233-processor :is-read-only="isReadOnly" :jsr223-processor="request.jsr223PreProcessor"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.processor.post_exec_script')" name="jsr223PostProcessor">
<ms-jsr233-processor :is-read-only="isReadOnly" :jsr223-processor="request.jsr223PostProcessor"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.timeout_config')" name="advancedConfig">
<ms-api-advanced-config :is-read-only="isReadOnly" :request="request"/>
</el-tab-pane>
</el-tabs>
</el-form>
</template>
<script>
import MsApiKeyValue from "../ApiKeyValue";
import MsApiBody from "../body/ApiBody";
import MsApiAssertions from "../assertion/ApiAssertions";
import {HttpRequest, KeyValue, Scenario} from "../../model/ScenarioModel";
import MsApiExtract from "../extract/ApiExtract";
import ApiRequestMethodSelect from "../collapse/ApiRequestMethodSelect";
import {REQUEST_HEADERS} from "@/common/js/constants";
import MsApiVariable from "../ApiVariable";
import MsJsr233Processor from "../processor/Jsr233Processor";
import MsApiAdvancedConfig from "../ApiAdvancedConfig";
export default {
name: "MsApiHttpRequestForm",
components: {
MsJsr233Processor,
MsApiAdvancedConfig,
MsApiVariable, ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue},
props: {
request: HttpRequest,
jsonPathList: Array,
scenario: Scenario,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
let validateURL = (rule, value, callback) => {
try {
new URL(this.addProtocol(this.request.url));
} catch (e) {
callback(this.$t('api_test.request.url_invalid'));
}
};
return {
activeName: "parameters",
rules: {
name: [
{max: 300, message: this.$t('commons.input_limit', [1, 300]), trigger: 'blur'}
],
url: [
{max: 500, required: true, message: this.$t('commons.input_limit', [1, 500]), trigger: 'blur'},
{validator: validateURL, trigger: 'blur'}
],
path: [
{max: 500, message: this.$t('commons.input_limit', [0, 500]), trigger: 'blur'},
]
},
headerSuggestions: REQUEST_HEADERS
}
},
methods: {
urlChange() {
if (!this.request.url) return;
let url = this.getURL(this.addProtocol(this.request.url));
if (url) {
this.request.url = decodeURIComponent(url.origin + url.pathname);
}
},
pathChange() {
if (!this.request.path) return;
let url = this.getURL(this.displayUrl);
let urlStr = url.origin + url.pathname;
let envUrl = this.scenario.environment.config.httpConfig.protocol + '://' + this.scenario.environment.config.httpConfig.socket;
this.request.path = decodeURIComponent(urlStr.substring(envUrl.length, urlStr.length));
},
getURL(urlStr) {
try {
let url = new URL(urlStr);
url.searchParams.forEach((value, key) => {
if (key && value) {
this.request.parameters.splice(0, 0, new KeyValue({name: key, value: value}));
}
});
return url;
} catch (e) {
this.$error(this.$t('api_test.request.url_invalid'), 2000);
}
},
methodChange(value) {
if (value === 'GET' && this.activeName === 'body') {
this.activeName = 'parameters';
}
},
useEnvironmentChange(value) {
if (value && !this.scenario.environment) {
this.$error(this.$t('api_test.request.please_add_environment_to_scenario'), 2000);
this.request.useEnvironment = false;
}
this.$refs["request"].clearValidate();
},
addProtocol(url) {
if (url) {
if (!url.toLowerCase().startsWith("https") && !url.toLowerCase().startsWith("http")) {
return "https://" + url;
}
}
return url;
},
runDebug() {
this.$emit('runDebug');
}
},
computed: {
displayUrl() {
return (this.scenario.environment && this.scenario.environment.config.httpConfig.socket) ?
this.scenario.environment.config.httpConfig.protocol + '://' + this.scenario.environment.config.httpConfig.socket + (this.request.path ? this.request.path : '')
: '';
}
}
}
</script>
<style scoped>
.el-tag {
width: 100%;
height: 40px;
line-height: 40px;
}
.environment-display {
font-size: 14px;
}
.environment-name {
font-weight: bold;
font-style: italic;
}
.adjust-margin-bottom {
margin-bottom: 10px;
}
.environment-url-tip {
color: #F56C6C;
}
.follow-redirects-item {
margin-left: 30px;
}
.do-multipart-post {
margin-left: 10px;
}
</style>

View File

@ -0,0 +1,136 @@
<template>
<div class="request-form">
<component @runDebug="runDebug" :is="component" :is-read-only="isReadOnly" :request="request" :scenario="scenario"/>
<el-divider v-if="isCompleted"></el-divider>
<ms-request-result-tail v-loading="debugReportLoading" v-if="isCompleted" :request="request.debugRequestResult ? request.debugRequestResult : {responseResult: {}, subRequestResults: []}"
:scenario-name="request.debugScenario ? request.debugScenario.name : ''" ref="msDebugResult"/>
</div>
</template>
<script>
import {JSR223Processor, Request, RequestFactory, Scenario} from "../../model/ScenarioModel";
import MsApiHttpRequestForm from "./ApiHttpRequestForm";
// import MsApiTcpRequestForm from "./ApiTcpRequestForm";
// import MsApiDubboRequestForm from "./ApiDubboRequestForm";
import MsScenarioResults from "../../../report/components/ScenarioResults";
import MsRequestResultTail from "../../../report/components/RequestResultTail";
// import MsApiSqlRequestForm from "./ApiSqlRequestForm";
export default {
name: "MsApiRequestForm",
components: { MsRequestResultTail, MsScenarioResults, MsApiHttpRequestForm},
props: {
scenario: Scenario,
request: Request,
isReadOnly: {
type: Boolean,
default: false
},
debugReportId: String
},
data() {
return {
reportId: "",
content: {scenarios:[]},
debugReportLoading: false,
showDebugReport: false,
jsonPathList:[]
}
},
computed: {
component({request: {type}}) {
let name;
switch (type) {
case RequestFactory.TYPES.DUBBO:
name = "MsApiDubboRequestForm";
break;
case RequestFactory.TYPES.SQL:
name = "MsApiSqlRequestForm";
break;
case RequestFactory.TYPES.TCP:
name = "MsApiTcpRequestForm";
break;
default:
name = "MsApiHttpRequestForm";
}
return name;
},
isCompleted() {
return !!this.request.debugReport;
}
},
watch: {
debugReportId() {
this.getReport();
}
},
mounted() {
// beanshell
if (!this.request.jsr223PreProcessor.script && this.request.beanShellPreProcessor) {
this.request.jsr223PreProcessor = new JSR223Processor(this.request.beanShellPreProcessor);
}
if (!this.request.jsr223PostProcessor.script && this.request.beanShellPostProcessor) {
this.request.jsr223PostProcessor = new JSR223Processor(this.request.beanShellPostProcessor);
}
},
methods: {
getReport() {
if (this.debugReportId) {
this.debugReportLoading = true;
this.showDebugReport = true;
this.request.debugReport = {};
let url = "/api/report/get/" + this.debugReportId;
this.$get(url, response => {
let report = response.data || {};
let res = {};
if (response.data) {
try {
res = JSON.parse(report.content);
} catch (e) {
throw e;
}
if (res) {
this.debugReportLoading = false;
this.request.debugReport = res;
if (res.scenarios && res.scenarios.length > 0) {
this.request.debugScenario = res.scenarios[0];
this.request.debugRequestResult = this.request.debugScenario.requestResults[0];
this.deleteReport(this.debugReportId);
} else {
this.request.debugScenario = new Scenario();
this.request.debugRequestResult = {responseResult: {}, subRequestResults: []};
}
this.$refs.msDebugResult.reload();
} else {
setTimeout(this.getReport, 2000)
}
} else {
this.debugReportLoading = false;
}
});
}
},
deleteReport(reportId) {
this.$post('/api/report/delete', {id: reportId});
},
runDebug() {
this.$emit('runDebug', this.request);
}
}
}
</script>
<style scoped>
.scenario-results {
margin-top: 20px;
}
.request-form >>> .debug-button {
margin-left: auto;
display: block;
margin-right: 10px;
}
</style>

View File

@ -0,0 +1,36 @@
<template>
<el-table :data="assertions" :row-style="getRowStyle" :header-cell-style="getRowStyle">
<el-table-column prop="name" :label="$t('api_report.assertions_name')" width="300"/>
<el-table-column prop="message" :label="$t('api_report.assertions_error_message')"/>
<el-table-column prop="pass" :label="$t('api_report.assertions_is_success')" width="180">
<template v-slot:default="{row}">
<el-tag size="mini" type="success" v-if="row.pass">
{{$t('api_report.success')}}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{$t('api_report.fail')}}
</el-tag>
</template>
</el-table-column>
</el-table>
</template>
<script>
export default {
name: "MsAssertionResults",
props: {
assertions: Array
},
methods: {
getRowStyle() {
return {backgroundColor: "#F5F5F5"};
}
}
}
</script>
<style scoped>
</style>

View File

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

View File

@ -0,0 +1,130 @@
<template>
<div class="card-container">
<el-card class="card-content" v-loading="result.loading">
<el-form :model="httpForm" :rules="rule" ref="httpForm" :inline="true" :label-position="labelPosition">
<div style="font-size: 16px;color: #333333">{{$t('test_track.plan_view.base_info')}}</div>
<br/>
<el-form-item :label="$t('api_report.request')" prop="responsible">
<el-input :placeholder="$t('api_test.delimit.request.path_info')" v-model="httpForm.request"
class="ms-http-input" size="small">
<el-select v-model="reqValue" slot="prepend" style="width: 100px">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
</el-form-item>
<el-form-item>
<el-dropdown split-button type="primary" class="ms-api-buttion" @click="handleCommand('add')"
@command="handleCommand" size="small">
{{$t('commons.test')}}
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="load_case">{{$t('api_test.delimit.request.load_case')}}
</el-dropdown-item>
<el-dropdown-item command="save_as_case">{{$t('api_test.delimit.request.save_as_case')}}
</el-dropdown-item>
<el-dropdown-item command="update_api">{{$t('api_test.delimit.request.update_api')}}</el-dropdown-item>
<el-dropdown-item command="save_as_api">{{$t('api_test.delimit.request.save_as')}}</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</el-form-item>
<div style="font-size: 16px;color: #333333;padding-top: 30px">请求参数</div>
<br/>
<!-- HTTP 请求参数 -->
<ms-api-request-form :debug-report-id="debugReportId" :request="selected" :scenario="currentScenario"/>
</el-form>
<div style="font-size: 16px;color: #333333 ;padding-top: 30px">响应内容</div>
<br/>
<ms-response-text :response="responseData"></ms-response-text>
</el-card>
<!-- 加载用例 -->
<ms-bottom-container v-bind:enableAsideHidden="isHide">
<ms-api-case-list @apiCaseClose="apiCaseClose" :api="httpForm"></ms-api-case-list>
</ms-bottom-container>
</div>
</template>
<script>
import MsApiRequestForm from "../request/ApiRequestForm";
import {Request, Scenario} from "../../model/ScenarioModel";
import MsResponseText from "../../../report/components/ResponseText";
import MsApiCaseList from "../ApiCaseList";
import MsContainer from "../../../../common/components/MsContainer";
import MsBottomContainer from "../BottomContainer";
export default {
name: "ApiConfig",
components: {MsResponseText, MsApiRequestForm, MsApiCaseList, MsContainer, MsBottomContainer},
data() {
return {
result: {},
rule: {},
isHide: true,
labelPosition: 'right',
httpForm: {},
options: [],
reqValue: '',
debugReportId: '',
responseData: {},
currentScenario: Scenario,
selected: [Scenario, Request],
reqOptions: [{
id: 'GET',
label: 'GET'
}, {
id: 'POST',
label: 'POST'
}],
moduleValue: '',
}
},
props: {httpData: {},},
methods: {
handleCommand(e) {
switch (e) {
case "load_case":
return this.loadCase();
case "save_as_case":
return "";
case "update_api":
return "update_api";
case "save_as_api":
return "save_as_api";
default:
return [];
}
},
saveAs() {
this.$emit('saveAs', this.httpForm);
},
loadCase() {
console.log(this.httpForm)
this.isHide = false;
},
apiCaseClose() {
this.isHide = true;
}
},
watch: {
httpData(v) {
this.httpForm = v;
}
}
}
</script>
<style scoped>
.ms-http-input {
width: 500px;
margin-top: 5px;
}
</style>

View File

@ -0,0 +1,127 @@
import {BaseConfig, DatabaseConfig, KeyValue} from "./ScenarioModel";
import {TCPConfig} from "@/business/components/api/test/model/ScenarioModel";
export class Environment extends BaseConfig {
constructor(options = {}) {
super();
this.projectId = undefined;
this.name = undefined;
this.id = undefined;
this.config = undefined;
this.set(options);
this.sets({}, options);
}
initOptions(options = {}) {
this.config = new Config(options.config);
return options;
}
}
export class Config extends BaseConfig {
constructor(options = {}) {
super();
this.commonConfig = undefined;
this.httpConfig = undefined;
this.databaseConfigs = [];
this.tcpConfig = undefined;
this.set(options);
this.sets({databaseConfigs: DatabaseConfig}, options);
}
initOptions(options = {}) {
this.commonConfig = new CommonConfig(options.commonConfig);
this.httpConfig = new HttpConfig(options.httpConfig);
options.databaseConfigs = options.databaseConfigs || [];
options.tcpConfig = new TCPConfig(options.tcpConfig);
return options;
}
}
export class CommonConfig extends BaseConfig {
constructor(options = {}) {
super();
this.variables = [];
this.enableHost = false;
this.hosts = [];
this.set(options);
this.sets({variables: KeyValue, hosts: Host}, options);
}
initOptions(options = {}) {
options.variables = options.variables || [new KeyValue()];
options.hosts = options.hosts || [];
return options;
}
}
export class HttpConfig extends BaseConfig {
constructor(options = {}) {
super();
this.socket = undefined;
this.domain = undefined;
this.headers = [];
this.protocol = 'https';
this.port = undefined;
this.set(options);
this.sets({headers: KeyValue}, options);
}
initOptions(options = {}) {
options.headers = options.headers || [new KeyValue()];
return options;
}
}
export class Host extends BaseConfig {
constructor(options = {}) {
super();
this.ip = undefined;
this.domain = undefined;
this.status = undefined;
this.annotation = undefined;
this.uuid = undefined;
this.set(options);
}
}
/* ---------- Functions ------- */
export function compatibleWithEnvironment(environment) {
//兼容旧版本
if (!environment.config) {
let config = new Config();
if (!(environment.variables instanceof Array)) {
config.commonConfig.variables = JSON.parse(environment.variables);
}
if (environment.hosts && !(environment.hosts instanceof Array)) {
config.commonConfig.hosts = JSON.parse(environment.hosts);
config.commonConfig.enableHost = true;
}
if (!(environment.headers instanceof Array)) {
config.httpConfig.headers = JSON.parse(environment.headers);
}
config.httpConfig.port = environment.port;
config.httpConfig.protocol = environment.protocol;
config.httpConfig.domain = environment.domain;
config.httpConfig.socket = environment.socket;
environment.config = JSON.stringify(config);
}
}
export function parseEnvironment(environment) {
compatibleWithEnvironment(environment);
if (!(environment.config instanceof Config)) {
environment.config = new Config(JSON.parse(environment.config));
}
}

View File

@ -0,0 +1,679 @@
const INDENT = ' '; // 缩进2空格
export class Element {
constructor(name, attributes, value) {
this.indent = '';
this.name = name; // 标签名
this.attributes = attributes || {}; // 属性
this.value = undefined; // 基础类型的内容
this.elements = []; // 子节点
if (value instanceof Element) {
this.elements.push(value);
} else {
this.value = value;
}
}
set(value) {
this.elements = [];
this.value = value;
}
add(element) {
if (element instanceof Element) {
this.value = undefined;
this.elements.push(element);
return element;
}
}
getDefault(value, defaultValue) {
return value === undefined ? defaultValue : value;
}
commonValue(tag, name, value, defaultValue) {
let v = this.getDefault(value, defaultValue);
return this.add(new Element(tag, {name: name}, v));
}
boolProp(name, value, defaultValue) {
return this.commonValue('boolProp', name, value, defaultValue);
}
intProp(name, value, defaultValue) {
return this.commonValue('intProp', name, value, defaultValue);
}
longProp(name, value, defaultValue) {
return this.commonValue('longProp', name, value, defaultValue);
}
stringProp(name, value, defaultValue) {
return this.commonValue('stringProp', name, value, defaultValue);
}
collectionProp(name) {
return this.commonValue('collectionProp', name);
}
elementProp(name, elementType) {
return this.add(new Element('elementProp', {name: name, elementType: elementType}));
}
isEmptyValue() {
return this.value === undefined || this.value === '';
}
isEmptyElement() {
return this.elements.length === 0;
}
isEmpty() {
return this.isEmptyValue() && this.isEmptyElement();
}
replace(str) {
if (!str || !(typeof str === 'string')) return str;
return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/'/g, "&apos;").replace(/"/g, "&quot;");
}
toXML(indent) {
if (indent) {
this.indent = indent;
}
let str = this.start();
str += this.content();
str += this.end();
return str;
}
start() {
let str = this.indent + '<' + this.replace(this.name);
for (let key in this.attributes) {
if (this.attributes.hasOwnProperty(key)) {
str += ' ' + this.replace(key) + '="' + this.replace(this.attributes[key]) + '"';
}
}
if (this.isEmpty()) {
str += '/>';
} else {
str += '>';
}
return str;
}
content() {
if (!this.isEmptyValue()) {
return this.replace(this.value);
}
let str = '';
let parent = this;
if (this.elements.length > 0) {
str += '\n';
this.elements.forEach(e => {
e.indent += parent.indent + INDENT;
str += e.toXML();
});
}
return str;
}
end() {
if (this.isEmpty()) {
return '\n';
}
let str = '</' + this.replace(this.name) + '>\n';
if (!this.isEmptyValue()) {
return str;
}
if (!this.isEmptyElement()) {
return this.indent + str;
}
}
}
// HashTree, 只能添加TestElement的子元素没有基础类型内容
export class HashTree extends Element {
constructor() {
super('hashTree');
}
add(te) {
if (te instanceof TestElement) {
super.add(te);
}
}
}
// TestElement包含2部分Element 和 HashTree
export class TestElement extends Element {
constructor(name, attributes, value) {
// Element, 只能添加Element
super(name, attributes, value);
// HashTree, 只能添加TestElement
this.hashTree = new HashTree();
}
put(te) {
this.hashTree.add(te);
}
toXML() {
let str = super.toXML();
str += this.hashTree.toXML(this.indent);
return str;
}
}
export class DefaultTestElement extends TestElement {
constructor(tag, guiclass, testclass, testname, enabled) {
super(tag, {
guiclass: guiclass,
testclass: testclass,
testname: testname === undefined ? tag + ' Name' : testname,
enabled: enabled || true
});
}
}
export class TestPlan extends DefaultTestElement {
constructor(testName, props) {
super('TestPlan', 'TestPlanGui', 'TestPlan', testName);
props = props || {};
this.boolProp("TestPlan.functional_mode", props.mode, false);
this.boolProp("TestPlan.serialize_threadgroups", props.stg, true);
this.boolProp("TestPlan.tearDown_on_shutdown", props.tos, true);
this.stringProp("TestPlan.comments", props.comments);
this.stringProp("TestPlan.user_define_classpath", props.classpath);
this.add(new ElementArguments(props.args, "TestPlan.user_defined_variables", "User Defined Variables"));
}
}
export class ThreadGroup extends DefaultTestElement {
constructor(testName, props) {
super('ThreadGroup', 'ThreadGroupGui', 'ThreadGroup', testName);
props = props || {};
this.intProp("ThreadGroup.num_threads", props.threads, 1);
this.intProp("ThreadGroup.ramp_time", props.ramp, 1);
this.longProp("ThreadGroup.delay", props.delay, 0);
this.longProp("ThreadGroup.duration", props.delay, 0);
this.stringProp("ThreadGroup.on_sample_error", props.error, "continue");
this.boolProp("ThreadGroup.scheduler", props.scheduler, false);
let loopAttrs = {
name: "ThreadGroup.main_controller",
elementType: "LoopController",
guiclass: "LoopControlPanel",
testclass: "LoopController",
testname: "Loop Controller",
enabled: "true"
};
let loopProps = props.loopProps || {};
let loopController = this.add(new Element('elementProp', loopAttrs));
loopController.boolProp('LoopController.continue_forever', loopProps.continue, false);
loopController.stringProp('LoopController.loops', loopProps.loops, 1);
}
}
export class DubboSample extends DefaultTestElement {
constructor(testName, request = {}) {
super('io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample',
'io.github.ningyu.jmeter.plugin.dubbo.gui.DubboSampleGui',
'io.github.ningyu.jmeter.plugin.dubbo.sample.DubboSample', testName);
this.request = request;
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_PROTOCOL", this.request.configCenter.protocol);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_GROUP", this.request.configCenter.group);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_NAMESPACE", this.request.configCenter.namespace);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_USER_NAME", this.request.configCenter.username);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_PASSWORD", this.request.configCenter.password);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_ADDRESS", this.request.configCenter.address);
this.stringProp("FIELD_DUBBO_CONFIG_CENTER_TIMEOUT", this.request.configCenter.timeout);
this.stringProp("FIELD_DUBBO_REGISTRY_PROTOCOL", this.request.registryCenter.protocol);
this.stringProp("FIELD_DUBBO_REGISTRY_GROUP", this.request.registryCenter.group);
this.stringProp("FIELD_DUBBO_REGISTRY_USER_NAME", this.request.registryCenter.username);
this.stringProp("FIELD_DUBBO_REGISTRY_PASSWORD", this.request.registryCenter.password);
this.stringProp("FIELD_DUBBO_ADDRESS", this.request.registryCenter.address);
this.stringProp("FIELD_DUBBO_REGISTRY_TIMEOUT", this.request.registryCenter.timeout);
this.stringProp("FIELD_DUBBO_TIMEOUT", this.request.consumerAndService.timeout);
this.stringProp("FIELD_DUBBO_VERSION", this.request.consumerAndService.version);
this.stringProp("FIELD_DUBBO_RETRIES", this.request.consumerAndService.retries);
this.stringProp("FIELD_DUBBO_GROUP", this.request.consumerAndService.group);
this.stringProp("FIELD_DUBBO_CONNECTIONS", this.request.consumerAndService.connections);
this.stringProp("FIELD_DUBBO_LOADBALANCE", this.request.consumerAndService.loadBalance);
this.stringProp("FIELD_DUBBO_ASYNC", this.request.consumerAndService.async);
this.stringProp("FIELD_DUBBO_CLUSTER", this.request.consumerAndService.cluster);
this.stringProp("FIELD_DUBBO_RPC_PROTOCOL", this.request.protocol);
this.stringProp("FIELD_DUBBO_INTERFACE", this.request.interface);
this.stringProp("FIELD_DUBBO_METHOD", this.request.method);
this.intProp("FIELD_DUBBO_METHOD_ARGS_SIZE", this.request.args.length);
this.intProp("FIELD_DUBBO_ATTACHMENT_ARGS_SIZE", this.request.attachmentArgs.length);
this.request.args.forEach((arg, i) => {
if (!!arg.name || !!arg.value) {
let index = i + 1;
this.stringProp("FIELD_DUBBO_METHOD_ARGS_PARAM_TYPE" + index, arg.name);
this.stringProp("FIELD_DUBBO_METHOD_ARGS_PARAM_VALUE" + index, arg.value);
}
})
this.request.attachmentArgs.forEach((arg, i) => {
if (!!arg.name || !!arg.value) {
let index = i + 1;
this.stringProp("FIELD_DUBBO_ATTACHMENT_ARGS_KEY" + index, arg.name);
this.stringProp("FIELD_DUBBO_ATTACHMENT_ARGS_VALUE" + index, arg.value);
}
})
}
}
export class JDBCSampler extends DefaultTestElement {
constructor(testName, request = {}) {
super('JDBCSampler', 'TestBeanGUI', 'JDBCSampler', testName);
this.stringProp("dataSource", request.dataSource);
this.stringProp("query", request.query);
this.stringProp("queryTimeout", request.queryTimeout);
this.stringProp("resultVariable", request.resultVariable);
this.stringProp("variableNames", request.variableNames);
this.stringProp("queryArguments");
this.stringProp("queryArgumentsTypes");
this.stringProp("resultSetMaxRows");
this.stringProp("resultSetHandler", 'Store as String');
this.stringProp("queryType", 'Callable Statement');
}
}
export class TCPSampler extends DefaultTestElement {
constructor(testName, request = {}) {
super('TCPSampler', 'TCPSamplerGui', 'TCPSampler', testName);
this.stringProp("TCPSampler.classname", request.classname);
this.stringProp("TCPSampler.server", request.server);
this.stringProp("TCPSampler.port", request.port);
this.stringProp("TCPSampler.ctimeout", request.ctimeout);
this.stringProp("TCPSampler.timeout", request.timeout);
this.boolProp("TCPSampler.reUseConnection", request.reUseConnection);
this.boolProp("TCPSampler.nodelay", request.nodelay);
this.boolProp("TCPSampler.closeConnection", request.closeConnection);
this.stringProp("TCPSampler.soLinger", request.soLinger);
this.stringProp("TCPSampler.EolByte", request.eolByte);
this.stringProp("TCPSampler.request", request.request);
this.stringProp("ConfigTestElement.username", request.username);
this.stringProp("ConfigTestElement.password", request.password);
}
}
export class HTTPSamplerProxy extends DefaultTestElement {
constructor(testName, options = {}) {
super('HTTPSamplerProxy', 'HttpTestSampleGui', 'HTTPSamplerProxy', testName);
this.stringProp("HTTPSampler.domain", options.domain);
this.stringProp("HTTPSampler.protocol", options.protocol);
this.stringProp("HTTPSampler.path", options.path);
this.stringProp("HTTPSampler.method", options.method);
this.stringProp("HTTPSampler.contentEncoding", options.encoding, "UTF-8");
if (!options.port) {
this.stringProp("HTTPSampler.port", "");
} else {
this.stringProp("HTTPSampler.port", options.port);
}
if (options.connectTimeout) {
this.stringProp('HTTPSampler.connect_timeout', options.connectTimeout);
}
if (options.responseTimeout) {
this.stringProp('HTTPSampler.response_timeout', options.responseTimeout);
}
if (options.followRedirects) {
this.boolProp('HTTPSampler.follow_redirects', options.followRedirects, true);
}
this.boolProp("HTTPSampler.use_keepalive", options.keepalive, true);
this.boolProp("HTTPSampler.DO_MULTIPART_POST", options.doMultipartPost, false);
}
}
// 这是一个Element
export class HTTPSamplerArguments extends Element {
constructor(args) {
super('elementProp', {
name: "HTTPsampler.Arguments", // s必须小写
elementType: "Arguments",
guiclass: "HTTPArgumentsPanel",
testclass: "Arguments",
enabled: "true"
});
this.args = args || [];
let collectionProp = this.collectionProp('Arguments.arguments');
this.args.forEach(arg => {
if (arg.enable === true || arg.enable === undefined) { // 非禁用的条件加入执行
let elementProp = collectionProp.elementProp(arg.name, 'HTTPArgument');
elementProp.boolProp('HTTPArgument.always_encode', arg.encode, true);
elementProp.boolProp('HTTPArgument.use_equals', arg.equals, true);
if (arg.name) {
elementProp.stringProp('Argument.name', arg.name);
}
elementProp.stringProp('Argument.value', arg.value);
elementProp.stringProp('Argument.metadata', arg.metadata || "=");
if (arg.contentType) {
elementProp.stringProp('HTTPArgument.content_type', arg.contentType, "");
}
}
});
}
}
export class HTTPsamplerFiles extends Element {
constructor(args) {
super('elementProp', {
name: "HTTPsampler.Files",
elementType: "HTTPFileArgs",
});
this.args = args || {};
let collectionProp = this.collectionProp('HTTPFileArgs.files');
this.args.forEach(arg => {
let elementProp = collectionProp.elementProp(arg.value, 'HTTPFileArg');
elementProp.stringProp('File.path', arg.value);
elementProp.stringProp('File.paramname', arg.name);
elementProp.stringProp('File.mimetype', arg.contentType || "application/octet-stream");
});
}
}
export class CookieManager extends DefaultTestElement {
constructor(testName) {
super('CookieManager', 'CookiePanel', 'CookieManager', testName);
this.collectionProp('CookieManager.cookies');
this.boolProp('CookieManager.clearEachIteration', false, false);
this.boolProp('CookieManager.controlledByThreadGroup', false, false);
}
}
export class DurationAssertion extends DefaultTestElement {
constructor(testName, duration) {
super('DurationAssertion', 'DurationAssertionGui', 'DurationAssertion', testName);
this.duration = duration || 0;
this.stringProp('DurationAssertion.duration', this.duration);
}
}
export class ResponseAssertion extends DefaultTestElement {
constructor(testName, assertion) {
super('ResponseAssertion', 'AssertionGui', 'ResponseAssertion', testName);
this.assertion = assertion || {};
this.stringProp('Assertion.test_field', this.assertion.field);
this.boolProp('Assertion.assume_success', this.assertion.assumeSuccess);
this.intProp('Assertion.test_type', this.assertion.type);
this.stringProp('Assertion.custom_message', this.assertion.message);
let collectionProp = this.collectionProp('Asserion.test_strings');
let random = Math.floor(Math.random() * 10000);
collectionProp.stringProp(random, this.assertion.value);
}
}
export class JSONPathAssertion extends DefaultTestElement {
constructor(testName, jsonPath) {
super('JSONPathAssertion', 'JSONPathAssertionGui', 'JSONPathAssertion', testName);
this.jsonPath = jsonPath || {};
this.stringProp('JSON_PATH', this.jsonPath.expression);
this.stringProp('EXPECTED_VALUE', this.jsonPath.expect);
this.boolProp('JSONVALIDATION', true);
this.boolProp('EXPECT_NULL', false);
this.boolProp('INVERT', false);
this.boolProp('ISREGEX', true);
}
}
export class ResponseCodeAssertion extends ResponseAssertion {
constructor(testName, type, value, assumeSuccess, message) {
let assertion = {
field: 'Assertion.response_code',
type: type,
value: value,
assumeSuccess: assumeSuccess,
message: message,
}
super(testName, assertion)
}
}
export class ResponseDataAssertion extends ResponseAssertion {
constructor(testName, type, value, assumeSuccess, message) {
let assertion = {
field: 'Assertion.response_data',
type: type,
value: value,
assumeSuccess: assumeSuccess,
message: message,
}
super(testName, assertion)
}
}
export class ResponseHeadersAssertion extends ResponseAssertion {
constructor(testName, type, value, assumeSuccess, message) {
let assertion = {
field: 'Assertion.response_headers',
type: type,
value: value,
assumeSuccess: assumeSuccess,
message: message,
}
super(testName, assertion)
}
}
export class BeanShellProcessor extends DefaultTestElement {
constructor(tag, guiclass, testclass, testname, processor) {
super(tag, guiclass, testclass, testname);
this.processor = processor || {};
this.boolProp('resetInterpreter', false);
this.stringProp('parameters');
this.stringProp('filename');
this.stringProp('script', processor.script);
}
}
export class JSR223Processor extends DefaultTestElement {
constructor(tag, guiclass, testclass, testname, processor) {
super(tag, guiclass, testclass, testname);
this.processor = processor || {};
this.stringProp('cacheKey', 'true');
this.stringProp('filename');
this.stringProp('parameters');
this.stringProp('script', this.processor.script);
this.stringProp('scriptLanguage', this.processor.language);
}
}
export class JSR223PreProcessor extends JSR223Processor {
constructor(testName, processor) {
super('JSR223PreProcessor', 'TestBeanGUI', 'JSR223PreProcessor', testName, processor)
}
}
export class JSR223PostProcessor extends JSR223Processor {
constructor(testName, processor) {
super('JSR223PostProcessor', 'TestBeanGUI', 'JSR223PostProcessor', testName, processor)
}
}
export class BeanShellPreProcessor extends BeanShellProcessor {
constructor(testName, processor) {
super('BeanShellPreProcessor', 'TestBeanGUI', 'BeanShellPreProcessor', testName, processor)
}
}
export class BeanShellPostProcessor extends BeanShellProcessor {
constructor(testName, processor) {
super('BeanShellPostProcessor', 'TestBeanGUI', 'BeanShellPostProcessor', testName, processor)
}
}
export class IfController extends DefaultTestElement {
constructor(testName, controller = {}) {
super('IfController', 'IfControllerPanel', 'IfController', testName);
this.stringProp('IfController.comments', controller.comments);
this.stringProp('IfController.condition', controller.condition);
this.boolProp('IfController.evaluateAll', controller.evaluateAll, false);
this.boolProp('IfController.useExpression', controller.useExpression, true);
}
}
export class ConstantTimer extends DefaultTestElement {
constructor(testName, timer = {}) {
super('ConstantTimer', 'ConstantTimerGui', 'ConstantTimer', testName);
this.stringProp('ConstantTimer.delay', timer.delay);
}
}
export class HeaderManager extends DefaultTestElement {
constructor(testName, headers) {
super('HeaderManager', 'HeaderPanel', 'HeaderManager', testName);
this.headers = headers || [];
let collectionProp = this.collectionProp('HeaderManager.headers');
this.headers.forEach(header => {
if (header.enable === true || header.enable === undefined) {
let elementProp = collectionProp.elementProp('', 'Header');
elementProp.stringProp('Header.name', header.name);
elementProp.stringProp('Header.value', header.value);
}
});
}
}
export class DNSCacheManager extends DefaultTestElement {
constructor(testName, hosts) {
super('DNSCacheManager', 'DNSCachePanel', 'DNSCacheManager', testName);
let collectionPropServers = this.collectionProp('DNSCacheManager.servers');
let collectionPropHosts = this.collectionProp('DNSCacheManager.hosts');
hosts.forEach(host => {
let elementProp = collectionPropHosts.elementProp(host.domain, 'StaticHost');
elementProp.stringProp('StaticHost.Name', host.domain);
elementProp.stringProp('StaticHost.Address', host.ip);
});
let boolProp = this.boolProp('DNSCacheManager.isCustomResolver', true);
}
}
export class JDBCDataSource extends DefaultTestElement {
constructor(testName, datasource) {
super('JDBCDataSource', 'TestBeanGUI', 'JDBCDataSource', testName);
this.boolProp('autocommit', true);
this.boolProp('keepAlive', true);
this.boolProp('preinit', false);
this.stringProp('dataSource', datasource.name);
this.stringProp('dbUrl', datasource.dbUrl);
this.stringProp('driver', datasource.driver);
this.stringProp('username', datasource.username);
this.stringProp('password', datasource.password);
this.stringProp('poolMax', datasource.poolMax);
this.stringProp('timeout', datasource.timeout);
this.stringProp('connectionAge', '5000');
this.stringProp('trimInterval', '60000');
this.stringProp('transactionIsolation', 'DEFAULT');
this.stringProp('checkQuery');
this.stringProp('initQuery');
this.stringProp('connectionProperties');
}
}
export class Arguments extends DefaultTestElement {
constructor(testName, args) {
super('Arguments', 'ArgumentsPanel', 'Arguments', testName);
this.args = args || [];
let collectionProp = this.collectionProp('Arguments.arguments');
this.args.forEach(arg => {
if (arg.enable === true || arg.enable === undefined) { // 非禁用的条件加入执行
let elementProp = collectionProp.elementProp(arg.name, 'Argument');
elementProp.stringProp('Argument.name', arg.name);
elementProp.stringProp('Argument.value', arg.value);
elementProp.stringProp('Argument.desc', arg.desc);
elementProp.stringProp('Argument.metadata', arg.metadata, "=");
}
});
}
}
export class ElementArguments extends Element {
constructor(args, name, testName) {
super('elementProp', {
name: name || "arguments",
elementType: "Arguments",
guiclass: "ArgumentsPanel",
testclass: "Arguments",
testname: testName || "",
enabled: "true"
});
let collectionProp = this.collectionProp('Arguments.arguments');
if (args) {
args.forEach(arg => {
if (arg.enable === true || arg.enable === undefined) { // 非禁用的条件加入执行
let elementProp = collectionProp.elementProp(arg.name, 'Argument');
elementProp.stringProp('Argument.name', arg.name);
elementProp.stringProp('Argument.value', arg.value);
elementProp.stringProp('Argument.metadata', arg.metadata, "=");
}
});
}
}
}
export class RegexExtractor extends DefaultTestElement {
constructor(testName, props) {
super('RegexExtractor', 'RegexExtractorGui', 'RegexExtractor', testName);
this.props = props || {}
this.stringProp('RegexExtractor.useHeaders', props.headers);
this.stringProp('RegexExtractor.refname', props.name);
this.stringProp('RegexExtractor.regex', props.expression);
this.stringProp('RegexExtractor.template', props.template);
this.stringProp('RegexExtractor.default', props.default);
this.stringProp('RegexExtractor.match_number', props.match);
}
}
export class JSONPostProcessor extends DefaultTestElement {
constructor(testName, props) {
super('JSONPostProcessor', 'JSONPostProcessorGui', 'JSONPostProcessor', testName);
this.props = props || {}
this.stringProp('JSONPostProcessor.referenceNames', props.name);
this.stringProp('JSONPostProcessor.jsonPathExprs', props.expression);
this.stringProp('JSONPostProcessor.match_numbers', props.match);
}
}
export class XPath2Extractor extends DefaultTestElement {
constructor(testName, props) {
super('XPath2Extractor', 'XPath2ExtractorGui', 'XPath2Extractor', testName);
this.props = props || {}
this.stringProp('XPathExtractor2.default', props.default);
this.stringProp('XPathExtractor2.refname', props.name);
this.stringProp('XPathExtractor2.xpathQuery', props.expression);
this.stringProp('XPathExtractor2.namespaces', props.namespaces);
this.stringProp('XPathExtractor2.matchNumber', props.match);
}
}

File diff suppressed because it is too large Load Diff

View File

@ -7,6 +7,10 @@
{{ $t("i18n.home") }}
</el-menu-item>
<el-menu-item :index="'/api/delimit'">
{{ $t("i18n.delimit") }}
</el-menu-item>
<el-submenu :class="{'deactivation':!isProjectActivation}" v-permission="['test_manager','test_user','test_viewer']" index="3">
<template v-slot:title>{{ $t('commons.project') }}</template>
<ms-recent-list ref="projectRecent" :options="projectRecent"/>

View File

@ -38,6 +38,11 @@ export default {
path: "report/view/:reportId",
name: "ApiReportView",
component: () => import('@/business/components/api/report/ApiReportView'),
},
{
path: "delimit",
name: "ApiDelimit",
component: () => import('@/business/components/api/delimit/ApiDelimit'),
}
]
}

View File

@ -1,6 +1,6 @@
export default {
commons: {
comment:'comment',
comment: 'comment',
examples: 'examples',
help_documentation: 'Help documentation',
delete_cancelled: 'Delete cancelled',
@ -216,8 +216,8 @@ export default {
select: 'Select Organization',
service_integration: 'Service integration',
defect_manage: 'Defect management platform',
message_settings:'Message settings',
message:{
message_settings: 'Message settings',
message: {
jenkins_task_notification: 'Jenkins task notification',
test_plan_task_notification: 'Test plan task notification',
test_review_task_notice: 'Test review task notice',
@ -454,6 +454,33 @@ export default {
file_exist: "The name already exists in the project",
upload_limit_size: "Upload file size cannot exceed 30MB!",
},
delimit: {
api_title: "Api test",
api_name: "Api name",
api_status: "Api status",
api_type: "Api type",
api_path: "Api path",
api_principal: "Api principal",
api_last_time: "Last update time",
api_case_number: "Number use case",
api_case_status: "Ise case status",
api_case_passing_rate: "Use case pass rate",
request: {
grade_info: "From high to low",
run_env: "Operating environment",
select_case: "Search use cases",
case: "Case",
title: "Create api",
path_info:"Please enter the URL of the interface, such as /api/demo/#{id}, where id is the path parameter",
fast_debug: "Fast debug",
close_all_label: "close all label",
save_as: "Save as new interface",
load_case: "Load use case",
save_as_case: "Save as new use case",
update_api: "Update interface",
}
},
environment: {
name: "Environment Name",
socket: "Socket",
@ -984,7 +1011,8 @@ export default {
},
i18n: {
home: 'Home'
home: 'Home',
delimit: 'ApiDelimit',
},
ldap: {
url: 'LDAP URL',

View File

@ -1,6 +1,6 @@
export default {
commons: {
comment:'评论',
comment: '评论',
examples: '示例',
help_documentation: '帮助文档',
delete_cancelled: '已取消删除',
@ -217,8 +217,8 @@ export default {
delete_warning: '删除该组织将同步删除该组织下所有相关工作空间和相关工作空间下的所有项目,以及项目中的所有用例、接口测试、性能测试等,确定要删除吗?',
service_integration: '服务集成',
defect_manage: '缺陷管理平台',
message_settings:'消息设置',
message:{
message_settings: '消息设置',
message: {
jenkins_task_notification: 'Jenkins接口调用任务通知',
test_plan_task_notification: '测试计划任务通知',
test_review_task_notice: '测试评审任务通知',
@ -454,6 +454,33 @@ export default {
file_exist: "该项目下已存在改jar包",
upload_limit_size: "上传文件大小不能超过 30MB!",
},
delimit: {
api_title: "接口列表",
api_name: "接口名称",
api_status: "接口状态",
api_type: "请求类型",
api_path: "路径",
api_principal: "负责人",
api_last_time: "最后更新时间",
api_case_number: "用例数",
api_case_status: "用例状态",
api_case_passing_rate: "用例通过率",
request: {
grade_info: "按等级从高到低",
run_env: "运行环境",
select_case: "搜索用例",
case: "用例",
responsible: "责任人",
title: "创建接口",
path_info: "请输入接口的URL如/api/demo/#{id}其中id为路径参数",
fast_debug: "快捷调试",
close_all_label: "关闭所有标签",
save_as: "另存为新接口",
load_case: "加载用例",
save_as_case: "另存为新用例",
update_api: "更新接口",
}
},
environment: {
name: "环境名称",
socket: "环境域名",
@ -983,7 +1010,8 @@ export default {
account: '账户不能为空',
},
i18n: {
home: '首页'
home: '首页',
delimit: '接口定义',
},
ldap: {
url: 'LDAP地址',

View File

@ -1,6 +1,6 @@
export default {
commons: {
comment:'評論',
comment: '評論',
examples: '示例',
help_documentation: '幫助文檔',
delete_cancelled: '已取消刪除',
@ -217,8 +217,8 @@ export default {
delete_warning: '刪除該組織將同步刪除該組織下所有相關工作空間和相關工作空間下的所有項目,以及項目中的所有用例、接口測試、性能測試等,確定要刪除嗎?',
service_integration: '服務集成',
defect_manage: '缺陷管理平臺',
message_settings:'消息設置',
message:{
message_settings: '消息設置',
message: {
jenkins_task_notification: 'Jenkins接口調用任務通知',
test_plan_task_notification: '測試計劃任務通知',
test_review_task_notice: '測試評審任務通知',
@ -230,8 +230,8 @@ export default {
nail_robot: '釘釘機器人',
enterprise_wechat_robot: '企業微信機器人',
notes: '註意: 1.事件,接收方式,接收人為必填項;\n' +
' 2.接收方式除郵件外webhook為必填\n' +
' 3.機器人選擇為群機器人,安全驗證選擇“自定義關鍵詞” "任務通知"',
' 2.接收方式除郵件外webhook為必填\n' +
' 3.機器人選擇為群機器人,安全驗證選擇“自定義關鍵詞” "任務通知"',
message: '事件,接收人,接收方式為必填項',
message_webhook: '接收方式為釘釘和企業機器人時webhook為必填項'
@ -454,6 +454,35 @@ export default {
file_exist: "該項目下已存在改jar包",
upload_limit_size: "上傳文件大小不能超過 30MB!",
},
delimit: {
api_title: "接口列表",
api_name: "接口名稱",
api_status: "接口狀態",
api_type: "請求類型",
api_path: "路徑",
api_principal: "負責人",
api_last_time: "最後更新時間",
api_case_number: "用例數",
api_case_status: "用例狀態",
api_case_passing_rate: "用例通過率",
request: {
grade_info: "按等級從高到低",
run_env: "運行環境",
select_case: "搜索用例",
case: "用例",
responsible: "责任人",
title: "创建接口",
path_info:"請輸入接口的URL如/api/demo/#{id}其中id為路徑參數",
fast_debug: "快捷調試",
close_all_label: "關閉所有標簽",
save_as: "另存為新接口",
load_case: "加载用例",
save_as_case: "另存為新用例",
update_api: "更新接口",
}
},
environment: {
name: "環境名稱",
socket: "環境域名",
@ -983,7 +1012,8 @@ export default {
account: '賬戶不能為空',
},
i18n: {
home: '首頁'
home: '首頁',
delimit: '接口定義',
},
ldap: {
url: 'LDAP地址',