Merge branch 'v1.0' of github.com:metersphere/metersphere into v1.0

This commit is contained in:
王振 2020-06-18 10:30:53 +08:00
commit 6a8892d166
72 changed files with 978 additions and 255 deletions

View File

@ -9,7 +9,6 @@ import io.metersphere.api.dto.SaveAPITestRequest;
import io.metersphere.api.service.APITestService;
import io.metersphere.base.domain.ApiTest;
import io.metersphere.base.domain.ApiTestWithBLOBs;
import io.metersphere.base.domain.LoadTest;
import io.metersphere.commons.constants.RoleConstants;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
@ -19,10 +18,10 @@ import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.List;
import javax.annotation.Resource;
@RestController
@RequestMapping(value = "/api")
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)

View File

@ -1,8 +1,11 @@
package io.metersphere.api.dto;
import io.metersphere.api.dto.scenario.Scenario;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Setter
@Getter
public class SaveAPITestRequest {
@ -13,5 +16,5 @@ public class SaveAPITestRequest {
private String name;
private String scenarioDefinition;
private List<Scenario> scenarioDefinition;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto.scenario;
import lombok.Data;
import java.util.List;
@Data
public class Body {
private String type;
private String raw;
private List<KeyValue> kvs;
}

View File

@ -0,0 +1,9 @@
package io.metersphere.api.dto.scenario;
import lombok.Data;
@Data
public class KeyValue {
private String name;
private String value;
}

View File

@ -0,0 +1,19 @@
package io.metersphere.api.dto.scenario;
import io.metersphere.api.dto.scenario.assertions.Assertions;
import io.metersphere.api.dto.scenario.extract.Extract;
import lombok.Data;
import java.util.List;
@Data
public class Request {
private String name;
private String url;
private String method;
private List<KeyValue> parameters;
private List<KeyValue> headers;
private Body body;
private Assertions assertions;
private Extract extract;
}

View File

@ -0,0 +1,14 @@
package io.metersphere.api.dto.scenario;
import lombok.Data;
import java.util.List;
@Data
public class Scenario {
private String name;
private String url;
private List<KeyValue> variables;
private List<KeyValue> headers;
private List<Request> requests;
}

View File

@ -0,0 +1,14 @@
package io.metersphere.api.dto.scenario.assertions;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class AssertionDuration extends AssertionType {
private long value;
public AssertionDuration() {
setType(AssertionType.DURATION);
}
}

View File

@ -0,0 +1,16 @@
package io.metersphere.api.dto.scenario.assertions;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class AssertionRegex extends AssertionType {
private String subject;
private String expression;
private String description;
public AssertionRegex() {
setType(AssertionType.REGEX);
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto.scenario.assertions;
import lombok.Data;
@Data
public class AssertionType {
public final static String REGEX = "Regex";
public final static String DURATION = "Duration";
public final static String TEXT = "Text";
private String type;
}

View File

@ -0,0 +1,11 @@
package io.metersphere.api.dto.scenario.assertions;
import lombok.Data;
import java.util.List;
@Data
public class Assertions {
private List<AssertionRegex> regex;
private AssertionDuration duration;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto.scenario.extract;
import lombok.Data;
import java.util.List;
@Data
public class Extract {
private List<ExtractRegex> regex;
private List<ExtractJSONPath> json;
private List<ExtractXPath> xpath;
}

View File

@ -0,0 +1,13 @@
package io.metersphere.api.dto.scenario.extract;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class ExtractCommon extends ExtractType {
private String variable;
private String value; // value: ${variable}
private String expression;
private String description;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto.scenario.extract;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class ExtractJSONPath extends ExtractCommon {
public ExtractJSONPath() {
setType(ExtractType.JSON_PATH);
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto.scenario.extract;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class ExtractRegex extends ExtractCommon {
public ExtractRegex() {
setType(ExtractType.REGEX);
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto.scenario.extract;
import lombok.Data;
@Data
public class ExtractType {
public final static String REGEX = "Regex";
public final static String JSON_PATH = "JSONPath";
public final static String XPATH = "XPath";
private String type;
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto.scenario.extract;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class ExtractXPath extends ExtractCommon {
public ExtractXPath() {
setType(ExtractType.XPATH);
}
}

View File

@ -1,5 +1,6 @@
package io.metersphere.api.service;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.api.dto.APITestResult;
import io.metersphere.api.dto.QueryAPITestRequest;
import io.metersphere.api.dto.SaveAPITestRequest;
@ -25,6 +26,7 @@ import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.util.List;
import java.util.Objects;
import java.util.Random;
import java.util.UUID;
import java.util.stream.Collectors;
@ -73,10 +75,17 @@ public class APITestService {
}
public void copy(SaveAPITestRequest request) {
request.setName(request.getName() + " Copy");
try {
checkNameExist(request);
} catch (Exception e) {
request.setName(request.getName() + " " + new Random().nextInt(1000));
}
// copy test
ApiTestWithBLOBs copy = get(request.getId());
copy.setId(UUID.randomUUID().toString());
copy.setName(copy.getName() + " Copy");
copy.setName(request.getName());
copy.setCreateTime(System.currentTimeMillis());
copy.setUpdateTime(System.currentTimeMillis());
copy.setStatus(APITestStatus.Saved.name());
@ -96,6 +105,10 @@ public class APITestService {
return apiTestMapper.selectByPrimaryKey(id);
}
public List<ApiTest> getApiTestByProjectId(String projectId) {
return extApiTestMapper.getApiTestByProjectId(projectId);
}
public void delete(String testId) {
deleteFileByTestId(testId);
apiReportService.deleteByTestId(testId);
@ -138,7 +151,7 @@ public class APITestService {
test.setId(request.getId());
test.setName(request.getName());
test.setProjectId(request.getProjectId());
test.setScenarioDefinition(request.getScenarioDefinition());
test.setScenarioDefinition(JSONObject.toJSONString(request.getScenarioDefinition()));
test.setUpdateTime(System.currentTimeMillis());
test.setStatus(APITestStatus.Saved.name());
apiTestMapper.updateByPrimaryKeySelective(test);
@ -151,7 +164,7 @@ public class APITestService {
test.setId(request.getId());
test.setName(request.getName());
test.setProjectId(request.getProjectId());
test.setScenarioDefinition(request.getScenarioDefinition());
test.setScenarioDefinition(JSONObject.toJSONString(request.getScenarioDefinition()));
test.setCreateTime(System.currentTimeMillis());
test.setUpdateTime(System.currentTimeMillis());
test.setStatus(APITestStatus.Saved.name());
@ -194,7 +207,4 @@ public class APITestService {
}
}
public List<ApiTest> getApiTestByProjectId(String projectId) {
return extApiTestMapper.getApiTestByProjectId(projectId);
}
}

View File

@ -36,6 +36,9 @@
#{nodeId}
</foreach>
</if>
<if test="request.status != null">
and test_plan_test_case.status = #{request.status}
</if>
<if test="request.executor != null">
and test_plan_test_case.executor = #{request.executor}
</if>

View File

@ -27,4 +27,5 @@ public interface ExtUserRoleMapper {
List<User> getBesideOrgMemberList(@Param("orgId") String orgId);
List<User> getTestManagerAndTestUserList(@Param("request") QueryMemberRequest request);
}

View File

@ -89,7 +89,12 @@
join role r on r.id = ur.role_id
where w.id = #{workspaceId} and ur.user_id = #{userId}
</select>
<select id="getTestManagerAndTestUserList" resultType="io.metersphere.base.domain.User">
SELECT distinct `user`.* FROM user_role INNER JOIN `user` ON user_role.user_id = `user`.id
WHERE user_role.source_id = #{request.workspaceId} And user_role.role_id in ('test_manager', 'test_user')
<if test="request.name != null">
AND `user`.name like CONCAT('%', #{request.name},'%')
</if>
order by user_role.update_time desc
</select>
</mapper>

View File

@ -0,0 +1,18 @@
package io.metersphere.commons.utils;
import java.math.BigDecimal;
public class MathUtils {
/**
* 获取百分比
* 保留一位小数
* @param value
* @return
*/
public static double getPercentWithDecimal(double value) {
return new BigDecimal(value * 100)
.setScale(1, BigDecimal.ROUND_HALF_UP)
.doubleValue();
}
}

View File

@ -274,4 +274,14 @@ public class UserController {
return userService.updateUserPassword(request);
}
/**
* 获取工作空间成员用户 不分页
*/
@PostMapping("/ws/member/tester/list")
@RequiresRoles(value = {RoleConstants.ORG_ADMIN, RoleConstants.TEST_MANAGER,
RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)
public List<User> getTestManagerAndTestUserList(@RequestBody QueryMemberRequest request) {
return userService.getTestManagerAndTestUserList(request);
}
}

View File

@ -86,7 +86,12 @@ public class DockerTestEngine extends AbstractEngine {
testRequest.setTestData(context.getTestData());
testRequest.setEnv(context.getEnv());
restTemplate.postForObject(uri, testRequest, String.class);
try {
restTemplate.postForObject(uri, testRequest, String.class);
} catch (Exception e) {
LogUtil.error(e);
MSException.throwException(Translator.get("start_engine_fail"));
}
}
@Override

View File

@ -85,6 +85,7 @@ public class JmeterDocumentParser implements DocumentParser {
if (nodeNameEquals(ele, HASH_TREE_ELEMENT)) {
parseHashTree(ele);
} else if (nodeNameEquals(ele, TEST_PLAN)) {
processSetupTestPlan(ele);
processTearDownTestPlan(ele);
processCheckoutConfigTestElement(ele);
processCheckoutDnsCacheManager(ele);
@ -436,6 +437,87 @@ public class JmeterDocumentParser implements DocumentParser {
}
}
private void processSetupTestPlan(Element ele) {
Document document = ele.getOwnerDocument();
Node hashTree = ele.getNextSibling();
while (!(hashTree instanceof Element)) {
hashTree = hashTree.getNextSibling();
}
KafkaProperties kafkaProperties = CommonBeanFactory.getBean(KafkaProperties.class);
String bootstrapServers = kafkaProperties.getBootstrapServers();
String[] servers = StringUtils.split(bootstrapServers, ",");
for (String s : servers) {
String[] ipAndPort = StringUtils.split(s, ":");
Element setupElement = document.createElement("SetupThreadGroup");
setupElement.setAttribute("guiclass", "SetupThreadGroupGui");
setupElement.setAttribute("testclass", "SetupThreadGroup");
setupElement.setAttribute("testname", "setUp Thread Group");
setupElement.setAttribute("enabled", "true");
setupElement.appendChild(createStringProp(document, "ThreadGroup.on_sample_error", "stoptestnow"));
Element elementProp = document.createElement("elementProp");
elementProp.setAttribute("name", "ThreadGroup.main_controller");
elementProp.setAttribute("elementType", "LoopController");
elementProp.setAttribute("guiclass", "LoopControlPanel");
elementProp.setAttribute("testclass", "LoopController");
elementProp.setAttribute("testname", "Loop Controller");
elementProp.setAttribute("enabled", "true");
elementProp.appendChild(createBoolProp(document, "LoopController.continue_forever", false));
elementProp.appendChild(createIntProp(document, "LoopController.loops", 1));
setupElement.appendChild(elementProp);
setupElement.appendChild(createStringProp(document, "ThreadGroup.num_threads", "1"));
setupElement.appendChild(createStringProp(document, "ThreadGroup.ramp_time", "1"));
setupElement.appendChild(createStringProp(document, "ThreadGroup.duration", ""));
setupElement.appendChild(createStringProp(document, "ThreadGroup.delay", ""));
setupElement.appendChild(createBoolProp(document, "ThreadGroup.scheduler", false));
setupElement.appendChild(createBoolProp(document, "ThreadGroup.same_user_on_next_iteration", true));
hashTree.appendChild(setupElement);
Element setupHashTree = document.createElement(HASH_TREE_ELEMENT);
Element tcpSampler = document.createElement("TCPSampler");
tcpSampler.setAttribute("guiclass", "TCPSamplerGui");
tcpSampler.setAttribute("testclass", "TCPSampler");
tcpSampler.setAttribute("testname", "TCP Sampler");
tcpSampler.setAttribute("enabled", "true");
tcpSampler.appendChild(createStringProp(document, "TCPSampler.classname", "TCPClientImpl"));
tcpSampler.appendChild(createStringProp(document, "TCPSampler.server", ipAndPort[0]));
tcpSampler.appendChild(createBoolProp(document, "TCPSampler.reUseConnection", true));
tcpSampler.appendChild(createStringProp(document, "TCPSampler.port", ipAndPort[1]));
tcpSampler.appendChild(createBoolProp(document, "TCPSampler.nodelay", false));
tcpSampler.appendChild(createStringProp(document, "TCPSampler.timeout", "100"));
tcpSampler.appendChild(createStringProp(document, "TCPSampler.ctimeout", "100"));
tcpSampler.appendChild(createStringProp(document, "TCPSampler.request", "1010"));
tcpSampler.appendChild(createBoolProp(document, "TCPSampler.closeConnection", false));
tcpSampler.appendChild(createStringProp(document, "TCPSampler.EolByte", "0"));
tcpSampler.appendChild(createStringProp(document, "ConfigTestElement.username", ""));
tcpSampler.appendChild(createStringProp(document, "ConfigTestElement.password", ""));
Element tcpSamplerHashTree = document.createElement(HASH_TREE_ELEMENT);
Element responseAssertion = document.createElement("ResponseAssertion");
responseAssertion.setAttribute("guiclass", "AssertionGui");
responseAssertion.setAttribute("testclass", "ResponseAssertion");
responseAssertion.setAttribute("testname", "Response Assertion");
responseAssertion.setAttribute("enabled", "true");
Element collectionProp = document.createElement("collectionProp");
collectionProp.setAttribute("name", "Asserion.test_strings");
collectionProp.appendChild(createStringProp(document, "49586", "200"));
responseAssertion.appendChild(collectionProp);
responseAssertion.appendChild(createStringProp(document, "Assertion.custom_message", ""));
responseAssertion.appendChild(createStringProp(document, "Assertion.test_field", "Assertion.response_code"));
responseAssertion.appendChild(createBoolProp(document, "Assertion.assume_success", false));
responseAssertion.appendChild(createIntProp(document, "Assertion.test_type", 8));
tcpSamplerHashTree.appendChild(responseAssertion);
// 添加空的hashtree
tcpSamplerHashTree.appendChild(document.createElement(HASH_TREE_ELEMENT));
setupHashTree.appendChild(tcpSampler);
setupHashTree.appendChild(tcpSamplerHashTree);
hashTree.appendChild(setupHashTree);
}
}
private void processTearDownTestPlan(Element ele) {
/*<boolProp name="TestPlan.tearDown_on_shutdown">true</boolProp>*/

View File

@ -11,6 +11,7 @@ import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.config.KafkaProperties;
import io.metersphere.controller.request.OrderRequest;
import io.metersphere.dto.DashboardTestDTO;
import io.metersphere.dto.LoadTestDTO;
@ -28,6 +29,9 @@ import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.io.OutputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.*;
@ -62,6 +66,8 @@ public class PerformanceTestService {
private TestResourceService testResourceService;
@Resource
private ReportService reportService;
@Resource
private KafkaProperties kafkaProperties;
public List<LoadTestDTO> list(QueryTestPlanRequest request) {
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
@ -197,6 +203,8 @@ public class PerformanceTestService {
if (StringUtils.equalsAny(loadTest.getStatus(), PerformanceTestStatus.Running.name(), PerformanceTestStatus.Starting.name())) {
MSException.throwException(Translator.get("load_test_is_running"));
}
// check kafka
checkKafka();
LogUtil.info("Load test started " + loadTest.getName());
// engine type (NODE)
@ -212,6 +220,31 @@ public class PerformanceTestService {
return engine.getReportId();
}
private void checkKafka() {
String bootstrapServers = kafkaProperties.getBootstrapServers();
String[] servers = StringUtils.split(bootstrapServers, ",");
try {
for (String s : servers) {
String[] ipAndPort = s.split(":");
//1,建立tcp
String ip = ipAndPort[0];
int port = Integer.parseInt(ipAndPort[1]);
Socket soc = new Socket();
soc.connect(new InetSocketAddress(ip, port), 1000); // 1s timeout
//2.输入内容
String content = "1010";
byte[] bs = content.getBytes();
OutputStream os = soc.getOutputStream();
os.write(bs);
//3.关闭
soc.close();
}
} catch (Exception e) {
LogUtil.error(e);
MSException.throwException(Translator.get("load_test_kafka_invalid"));
}
}
private void startEngine(LoadTestWithBLOBs loadTest, Engine engine) {
LoadTestReport testReport = new LoadTestReport();
testReport.setId(engine.getReportId());

View File

@ -457,4 +457,7 @@ public class UserService {
return extUserMapper.getDefaultLanguage(key);
}
public List<User> getTestManagerAndTestUserList(QueryMemberRequest request) {
return extUserRoleMapper.getTestManagerAndTestUserList(request);
}
}

View File

@ -0,0 +1,36 @@
package io.metersphere.track.Factory;
import io.metersphere.track.domain.*;
import io.metersphere.track.dto.TestPlanDTO;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
public class ReportComponentFactory {
public static ReportComponent createComponent(String componentId, TestPlanDTO testPlan) {
if (StringUtils.equals("1", componentId)) {
return new ReportBaseInfoComponent(testPlan);
} else if (StringUtils.equals("2", componentId)) {
return new ReportResultComponent(testPlan);
} else if (StringUtils.equals("3", componentId)) {
return new ReportResultChartComponent(testPlan);
} else if (StringUtils.equals("4", componentId)) {
return new ReportFailureResultComponent(testPlan);
}
return null;
}
public static List<ReportComponent> createComponents(List<String> componentIds, TestPlanDTO testPlan) {
List<ReportComponent> components = new ArrayList<>();
componentIds.forEach(id -> {
ReportComponent component = createComponent(id, testPlan);
if (component != null) {
components.add(component);
}
});
return components;
}
}

View File

@ -0,0 +1,30 @@
package io.metersphere.track.domain;
import io.metersphere.track.dto.TestCaseReportMetricDTO;
import io.metersphere.track.dto.TestPlanCaseDTO;
import io.metersphere.track.dto.TestPlanDTO;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.Set;
public class ReportBaseInfoComponent extends ReportComponent {
private Set<String> executorsSet = new HashSet<>();
public ReportBaseInfoComponent(TestPlanDTO testPlan) {
super(testPlan);
componentId = "1";
}
@Override
public void readRecord(TestPlanCaseDTO testCase) {
executorsSet.add(testCase.getExecutor());
}
@Override
public void afterBuild(TestCaseReportMetricDTO testCaseReportMetric) {
testCaseReportMetric.setProjectName(testPlan.getProjectName());
testCaseReportMetric.setPrincipal(testPlan.getPrincipal());
testCaseReportMetric.setExecutors(new ArrayList<>(this.executorsSet));
}
}

View File

@ -0,0 +1,15 @@
package io.metersphere.track.domain;
import io.metersphere.track.dto.TestCaseReportMetricDTO;
import io.metersphere.track.dto.TestPlanCaseDTO;
import io.metersphere.track.dto.TestPlanDTO;
public abstract class ReportComponent {
protected String componentId;
protected TestPlanDTO testPlan;
public ReportComponent(TestPlanDTO testPlan) {
this.testPlan = testPlan;
}
public abstract void readRecord(TestPlanCaseDTO testCase);
public abstract void afterBuild(TestCaseReportMetricDTO testCaseReportMetric);
}

View File

@ -0,0 +1,30 @@
package io.metersphere.track.domain;
import io.metersphere.commons.constants.TestPlanTestCaseStatus;
import io.metersphere.track.dto.TestCaseReportMetricDTO;
import io.metersphere.track.dto.TestPlanCaseDTO;
import io.metersphere.track.dto.TestPlanDTO;
import org.apache.commons.lang3.StringUtils;
import java.util.ArrayList;
import java.util.List;
public class ReportFailureResultComponent extends ReportComponent {
private List<TestPlanCaseDTO> failureTestCases = new ArrayList<>();
public ReportFailureResultComponent(TestPlanDTO testPlan) {
super(testPlan);
componentId = "4";
}
@Override
public void readRecord(TestPlanCaseDTO testCase) {
if (StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Failure.name())) {
this.failureTestCases.add(testCase);
}
}
@Override
public void afterBuild(TestCaseReportMetricDTO testCaseReportMetric) {
testCaseReportMetric.setFailureTestCases(failureTestCases);
}
}

View File

@ -0,0 +1,41 @@
package io.metersphere.track.domain;
import io.metersphere.track.dto.TestCaseReportMetricDTO;
import io.metersphere.track.dto.TestCaseReportStatusResultDTO;
import io.metersphere.track.dto.TestPlanCaseDTO;
import io.metersphere.track.dto.TestPlanDTO;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
public class ReportResultChartComponent extends ReportComponent {
Map<String, TestCaseReportStatusResultDTO> reportStatusResultMap = new HashMap<>();
public ReportResultChartComponent(TestPlanDTO testPlan) {
super(testPlan);
componentId = "3";
}
@Override
public void readRecord(TestPlanCaseDTO testCase) {
getStatusResultMap(reportStatusResultMap, testCase);
}
@Override
public void afterBuild(TestCaseReportMetricDTO testCaseReportMetric) {
testCaseReportMetric.setExecuteResult(new ArrayList<>(reportStatusResultMap.values()));
}
private void getStatusResultMap(Map<String, TestCaseReportStatusResultDTO> reportStatusResultMap, TestPlanCaseDTO testCase) {
TestCaseReportStatusResultDTO statusResult = reportStatusResultMap.get(testCase.getStatus());
if (statusResult == null) {
statusResult = new TestCaseReportStatusResultDTO();
statusResult.setStatus(testCase.getStatus());
statusResult.setCount(0);
}
statusResult.setCount(statusResult.getCount() + 1);
reportStatusResultMap.put(testCase.getStatus(), statusResult);
}
}

View File

@ -0,0 +1,105 @@
package io.metersphere.track.domain;
import com.alibaba.fastjson.JSON;
import io.metersphere.base.domain.TestCaseNode;
import io.metersphere.base.domain.TestCaseNodeExample;
import io.metersphere.base.mapper.TestCaseNodeMapper;
import io.metersphere.commons.constants.TestPlanTestCaseStatus;
import io.metersphere.commons.utils.CommonBeanFactory;
import io.metersphere.commons.utils.MathUtils;
import io.metersphere.track.dto.*;
import io.metersphere.track.service.TestCaseNodeService;
import org.apache.commons.lang3.StringUtils;
import java.util.*;
public class ReportResultComponent extends ReportComponent {
private List<TestCaseNodeDTO> nodeTrees = new ArrayList<>();
private Map<String, Set<String>> childIdMap = new HashMap<>();
private Map<String, TestCaseReportModuleResultDTO> moduleResultMap = new HashMap<>();
public ReportResultComponent(TestPlanDTO testPlan) {
super(testPlan);
componentId = "2";
init();
}
public void init() {
TestCaseNodeService testCaseNodeService = (TestCaseNodeService) CommonBeanFactory.getBean("testCaseNodeService");
TestCaseNodeMapper testCaseNodeMapper = (TestCaseNodeMapper) CommonBeanFactory.getBean("testCaseNodeMapper");
TestCaseNodeExample testCaseNodeExample = new TestCaseNodeExample();
testCaseNodeExample.createCriteria().andProjectIdEqualTo(testPlan.getProjectId());
List<TestCaseNode> nodes = testCaseNodeMapper.selectByExample(testCaseNodeExample);
nodeTrees = testCaseNodeService.getNodeTrees(nodes);
nodeTrees.forEach(item -> {
Set<String> childIds = new HashSet<>();
getChildIds(item, childIds);
childIdMap.put(item.getId(), childIds);
});
}
@Override
public void readRecord(TestPlanCaseDTO testCase) {
getModuleResultMap(childIdMap, moduleResultMap, testCase, nodeTrees);
}
@Override
public void afterBuild(TestCaseReportMetricDTO testCaseReportMetric) {
nodeTrees.forEach(rootNode -> {
TestCaseReportModuleResultDTO moduleResult = moduleResultMap.get(rootNode.getId());
if (moduleResult != null) {
moduleResult.setModuleName(rootNode.getName());
}
});
for (TestCaseReportModuleResultDTO moduleResult : moduleResultMap.values()) {
moduleResult.setPassRate(MathUtils.getPercentWithDecimal(moduleResult.getPassCount()*1.0f/moduleResult.getCaseCount()));
if (moduleResult.getCaseCount() <= 0) {
moduleResultMap.remove(moduleResult.getModuleId());
}
}
testCaseReportMetric.setModuleExecuteResult(new ArrayList<>(moduleResultMap.values()));
}
private void getChildIds(TestCaseNodeDTO rootNode, Set<String> childIds) {
childIds.add(rootNode.getId());
List<TestCaseNodeDTO> children = rootNode.getChildren();
if(children != null) {
Iterator<TestCaseNodeDTO> iterator = children.iterator();
while(iterator.hasNext()){
getChildIds(iterator.next(), childIds);
}
}
}
private void getModuleResultMap(Map<String, Set<String>> childIdMap, Map<String, TestCaseReportModuleResultDTO> moduleResultMap, TestPlanCaseDTO testCase, List<TestCaseNodeDTO> nodeTrees) {
childIdMap.forEach((rootNodeId, childIds) -> {
if (childIds.contains(testCase.getNodeId())) {
TestCaseReportModuleResultDTO moduleResult = moduleResultMap.get(rootNodeId);
if (moduleResult == null) {
moduleResult = new TestCaseReportModuleResultDTO();
moduleResult.setCaseCount(0);
moduleResult.setPassCount(0);
moduleResult.setIssuesCount(0);
moduleResult.setModuleId(rootNodeId);
}
moduleResult.setCaseCount(moduleResult.getCaseCount() + 1);
if (StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Pass.name())) {
moduleResult.setPassCount(moduleResult.getPassCount() + 1);
}
if (StringUtils.isNotBlank(testCase.getIssues())) {
if (JSON.parseObject(testCase.getIssues()).getBoolean("hasIssues")) {
moduleResult.setIssuesCount(moduleResult.getIssuesCount() + 1);
};
}
moduleResultMap.put(rootNodeId, moduleResult);
return;
}
});
}
}

View File

@ -1,5 +1,9 @@
package io.metersphere.track.dto;
import io.metersphere.track.domain.ReportBaseInfoComponent;
import io.metersphere.track.domain.ReportFailureResultComponent;
import io.metersphere.track.domain.ReportResultChartComponent;
import io.metersphere.track.domain.ReportResultComponent;
import lombok.Getter;
import lombok.Setter;
@ -11,6 +15,7 @@ public class TestCaseReportMetricDTO {
private List<TestCaseReportStatusResultDTO> executeResult;
private List<TestCaseReportModuleResultDTO> moduleExecuteResult;
private List<TestPlanCaseDTO> failureTestCases;
private List<String> executors;
private String principal;
private Long startTime;

View File

@ -25,4 +25,6 @@ public class QueryTestPlanCaseRequest extends TestPlanTestCase {
private String workspaceId;
private String name;
private String status;
}

View File

@ -2,11 +2,10 @@ package io.metersphere.track.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.TestCaseMapper;
import io.metersphere.base.mapper.TestCaseNodeMapper;
import io.metersphere.base.mapper.TestPlanMapper;
import io.metersphere.base.mapper.TestPlanTestCaseMapper;
import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtProjectMapper;
import io.metersphere.base.mapper.ext.ExtTestPlanMapper;
import io.metersphere.base.mapper.ext.ExtTestPlanTestCaseMapper;
@ -14,10 +13,14 @@ import io.metersphere.commons.constants.TestPlanStatus;
import io.metersphere.commons.constants.TestPlanTestCaseStatus;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.user.SessionUser;
import io.metersphere.commons.utils.MathUtils;
import io.metersphere.commons.utils.ServiceUtils;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.controller.request.ProjectRequest;
import io.metersphere.controller.request.member.QueryMemberRequest;
import io.metersphere.dto.ProjectDTO;
import io.metersphere.track.Factory.ReportComponentFactory;
import io.metersphere.track.domain.ReportComponent;
import io.metersphere.track.dto.*;
import io.metersphere.track.request.testcase.PlanCaseRelevanceRequest;
import io.metersphere.track.request.testcase.QueryTestPlanRequest;
@ -27,6 +30,7 @@ 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.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -58,15 +62,16 @@ public class TestPlanService {
@Resource
SqlSessionFactory sqlSessionFactory;
@Lazy
@Resource
TestCaseNodeMapper testCaseNodeMapper;
@Resource
TestCaseNodeService testCaseNodeService;
TestPlanTestCaseService testPlanTestCaseService;
@Resource
ExtProjectMapper extProjectMapper;
@Resource
TestCaseReportMapper testCaseReportMapper;
public void addTestPlan(TestPlan testPlan) {
if (getTestPlanByName(testPlan.getName()).size() > 0) {
MSException.throwException(Translator.get("plan_name_already_exists"));
@ -226,23 +231,17 @@ public class TestPlanService {
}
});
}
testPlan.setPassRate(getPercentWithTwoDecimals(testPlan.getTested() == 0 ? 0 : testPlan.getPassed()*1.0/testPlan.getTested()));
testPlan.setTestRate(getPercentWithTwoDecimals(testPlan.getTotal() == 0 ? 0 : testPlan.getTested()*1.0/testPlan.getTotal()));
testPlan.setPassRate(MathUtils.getPercentWithDecimal(testPlan.getTested() == 0 ? 0 : testPlan.getPassed()*1.0/testPlan.getTested()));
testPlan.setTestRate(MathUtils.getPercentWithDecimal(testPlan.getTotal() == 0 ? 0 : testPlan.getTested()*1.0/testPlan.getTotal()));
});
return testPlans;
}
private double getPercentWithTwoDecimals(double value) {
return new BigDecimal(value * 100)
.setScale(1, BigDecimal.ROUND_HALF_UP)
.doubleValue();
}
public List<TestPlanCaseDTO> listTestCaseByPlanId(String planId) {
QueryTestPlanCaseRequest request = new QueryTestPlanCaseRequest();
request.setPlanId(planId);
return extTestPlanTestCaseMapper.list(request);
return testPlanTestCaseService.list(request);
}
public List<TestPlanCaseDTO> listTestCaseByProjectIds(List<String> projectIds) {
@ -255,107 +254,26 @@ public class TestPlanService {
QueryTestPlanRequest queryTestPlanRequest = new QueryTestPlanRequest();
queryTestPlanRequest.setId(planId);
TestPlanDTO testPlan = extTestPlanMapper.list(queryTestPlanRequest).get(0);
TestCaseReport testCaseReport = testCaseReportMapper.selectByPrimaryKey(testPlan.getReportId());
JSONObject content = JSONObject.parseObject(testCaseReport.getContent());
JSONArray componentIds = content.getJSONArray("components");
Set<String> executors = new HashSet<>();
Map<String, TestCaseReportStatusResultDTO> reportStatusResultMap = new HashMap<>();
TestCaseNodeExample testCaseNodeExample = new TestCaseNodeExample();
testCaseNodeExample.createCriteria().andProjectIdEqualTo(testPlan.getProjectId());
List<TestCaseNode> nodes = testCaseNodeMapper.selectByExample(testCaseNodeExample);
List<TestCaseNodeDTO> nodeTrees = testCaseNodeService.getNodeTrees(nodes);
Map<String, Set<String>> childIdMap = new HashMap<>();
nodeTrees.forEach(item -> {
Set<String> childIds = new HashSet<>();
getChildIds(item, childIds);
childIdMap.put(item.getId(), childIds);
});
List<ReportComponent> components = ReportComponentFactory.createComponents(componentIds.toJavaList(String.class), testPlan);
List<TestPlanCaseDTO> testPlanTestCases = listTestCaseByPlanId(planId);
Map<String, TestCaseReportModuleResultDTO> moduleResultMap = new HashMap<>();
for (TestPlanCaseDTO testCase: testPlanTestCases) {
executors.add(testCase.getExecutor());
getStatusResultMap(reportStatusResultMap, testCase);
getModuleResultMap(childIdMap, moduleResultMap, testCase, nodeTrees);
}
nodeTrees.forEach(rootNode -> {
TestCaseReportModuleResultDTO moduleResult = moduleResultMap.get(rootNode.getId());
if (moduleResult != null) {
moduleResult.setModuleName(rootNode.getName());
}
});
for (TestCaseReportModuleResultDTO moduleResult : moduleResultMap.values()) {
moduleResult.setPassRate(getPercentWithTwoDecimals(moduleResult.getPassCount()*1.0f/moduleResult.getCaseCount()));
if (moduleResult.getCaseCount() <= 0) {
moduleResultMap.remove(moduleResult.getModuleId());
}
components.forEach(component -> {
component.readRecord(testCase);
});
}
TestCaseReportMetricDTO testCaseReportMetricDTO = new TestCaseReportMetricDTO();
testCaseReportMetricDTO.setProjectName(testPlan.getProjectName());
testCaseReportMetricDTO.setPrincipal(testPlan.getPrincipal());
testCaseReportMetricDTO.setExecutors(new ArrayList<>(executors));
testCaseReportMetricDTO.setExecuteResult(new ArrayList<>(reportStatusResultMap.values()));
testCaseReportMetricDTO.setModuleExecuteResult(new ArrayList<>(moduleResultMap.values()));
return testCaseReportMetricDTO;
}
private void getStatusResultMap(Map<String, TestCaseReportStatusResultDTO> reportStatusResultMap, TestPlanCaseDTO testCase) {
TestCaseReportStatusResultDTO statusResult = reportStatusResultMap.get(testCase.getStatus());
if (statusResult == null) {
statusResult = new TestCaseReportStatusResultDTO();
statusResult.setStatus(testCase.getStatus());
statusResult.setCount(0);
}
statusResult.setCount(statusResult.getCount() + 1);
reportStatusResultMap.put(testCase.getStatus(), statusResult);
}
private void getModuleResultMap(Map<String, Set<String>> childIdMap, Map<String, TestCaseReportModuleResultDTO> moduleResultMap, TestPlanCaseDTO testCase, List<TestCaseNodeDTO> nodeTrees) {
childIdMap.forEach((rootNodeId, childIds) -> {
if (childIds.contains(testCase.getNodeId())) {
TestCaseReportModuleResultDTO moduleResult = moduleResultMap.get(rootNodeId);
if (moduleResult == null) {
moduleResult = new TestCaseReportModuleResultDTO();
moduleResult.setCaseCount(0);
moduleResult.setPassCount(0);
moduleResult.setIssuesCount(0);
moduleResult.setModuleId(rootNodeId);
}
moduleResult.setCaseCount(moduleResult.getCaseCount() + 1);
if (StringUtils.equals(testCase.getStatus(), TestPlanTestCaseStatus.Pass.name())) {
moduleResult.setPassCount(moduleResult.getPassCount() + 1);
}
if (StringUtils.isNotBlank(testCase.getIssues())) {
if (JSON.parseObject(testCase.getIssues()).getBoolean("hasIssues")) {
moduleResult.setIssuesCount(moduleResult.getIssuesCount() + 1);
};
}
moduleResultMap.put(rootNodeId, moduleResult);
return;
}
components.forEach(component -> {
component.afterBuild(testCaseReportMetricDTO);
});
}
private void getChildIds(TestCaseNodeDTO rootNode, Set<String> childIds) {
childIds.add(rootNode.getId());
List<TestCaseNodeDTO> children = rootNode.getChildren();
if(children != null) {
Iterator<TestCaseNodeDTO> iterator = children.iterator();
while(iterator.hasNext()){
getChildIds(iterator.next(), childIds);
}
}
return testCaseReportMetricDTO;
}
public List<TestPlan> getTestPlanByIds(List<String> planIds) {

View File

@ -64,5 +64,4 @@ kafka.ssl.provider=
kafka.ssl.truststore-type=
# jmeter
jmeter.image=registry.fit2cloud.com/metersphere/jmeter-master:0.0.4
jmeter.home=/opt/jmeter

View File

@ -34,6 +34,7 @@ run_load_test_file_not_found=Unable to run test, unable to get test file meta in
run_load_test_file_content_not_found=Cannot run test, cannot get test file content, test ID=
run_load_test_file_init_error=Failed to run test, failed to initialize run environment, test ID=
load_test_is_running=Load test is running, please wait.
load_test_kafka_invalid=Kafka is not available, please check the configuration
cannot_edit_load_test_running=Cannot modify the running test
test_not_found=Test cannot be found:
test_not_running=Test is not running

View File

@ -34,6 +34,7 @@ run_load_test_file_not_found=无法运行测试,无法获取测试文件元信
run_load_test_file_content_not_found=无法运行测试无法获取测试文件内容测试ID
run_load_test_file_init_error=无法运行测试初始化运行环境失败测试ID
load_test_is_running=测试正在运行, 请等待
load_test_kafka_invalid=Kafka 不可用,请检查配置
cannot_edit_load_test_running=不能修改正在运行的测试
test_not_found=测试不存在:
test_not_running=测试未运行

View File

@ -34,6 +34,7 @@ run_load_test_file_not_found=無法運行測試,無法獲取測試文件元信
run_load_test_file_content_not_found=無法運行測試無法獲取測試文件內容測試ID
run_load_test_file_init_error=無法運行測試初始化運行環境失敗測試ID
load_test_is_running=測試正在運行, 請等待
load_test_kafka_invalid=Kafka 不可用,請檢查配置
cannot_edit_load_test_running=不能修改正在運行的測試
test_not_found=測試不存在:
test_not_running=測試未運行

View File

@ -35,7 +35,7 @@
</el-col>
<el-col :span="8">
<el-row type="flex" justify="center">
<ms-create-test v-permission="['test_manager','test_user']" :to="'/api/test/create'"/>
<ms-create-test :to="'/api/test/create'"/>
</el-row>
</el-col>
<el-col :span="8"/>

View File

@ -63,7 +63,12 @@
this.$get(url, response => {
this.report = response.data || {};
if (this.isNotRunning) {
this.content = JSON.parse(this.report.content);
try {
this.content = JSON.parse(this.report.content);
} catch (e) {
console.log(this.report.content)
throw e;
}
this.getFails();
this.loading = false;
} else {

View File

@ -5,7 +5,8 @@
<el-container class="test-container" v-loading="result.loading">
<el-header>
<el-row type="flex" align="middle">
<el-input :disabled="isReadOnly" class="test-name" v-model="test.name" maxlength="60" :placeholder="$t('api_test.input_name')"
<el-input :disabled="isReadOnly" class="test-name" v-model="test.name" maxlength="60"
:placeholder="$t('api_test.input_name')"
show-word-limit>
<el-select :disabled="isReadOnly" class="test-project" v-model="test.projectId" slot="prepend"
:placeholder="$t('api_test.select_project')">
@ -17,7 +18,8 @@
{{$t('commons.save')}}
</el-button>
<el-button type="primary" plain v-if="!isShowRun" :disabled="isDisabled || isReadOnly" @click="saveRunTest">
<el-button type="primary" plain v-if="!isShowRun" :disabled="isDisabled || isReadOnly"
@click="saveRunTest">
{{$t('load_test.save_and_run')}}
</el-button>
@ -25,7 +27,8 @@
{{$t('api_test.run')}}
</el-button>
<el-button :disabled="isReadOnly" type="warning" plain @click="cancel">{{$t('commons.cancel')}}</el-button>
<el-button :disabled="isReadOnly" type="warning" plain @click="cancel">{{$t('commons.cancel')}}
</el-button>
<el-dropdown trigger="click" @command="handleCommand">
<el-button class="el-dropdown-link more" icon="el-icon-more" plain/>
@ -36,6 +39,9 @@
<el-dropdown-item command="performance" :disabled="create || isReadOnly">
{{$t('api_test.create_performance_test')}}
</el-dropdown-item>
<el-dropdown-item command="export" :disabled="isDisabled || isReadOnly">
{{$t('api_test.export_config')}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
@ -54,7 +60,7 @@
import {Test} from "./model/ScenarioModel"
import MsApiReportStatus from "../report/ApiReportStatus";
import MsApiReportDialog from "./ApiReportDialog";
import {checkoutTestManagerOrTestUser} from "../../../../common/js/utils";
import {checkoutTestManagerOrTestUser, downloadFile} from "../../../../common/js/utils";
export default {
name: "MsApiTestConfig",
@ -165,13 +171,7 @@
},
getOptions(url) {
let formData = new FormData();
let request = {
id: this.test.id,
projectId: this.test.projectId,
name: this.test.name,
scenarioDefinition: JSON.stringify(this.test.scenarioDefinition)
}
let requestJson = JSON.stringify(request);
let requestJson = JSON.stringify(this.test);
formData.append('request', new Blob([requestJson], {
type: "application/json"
@ -204,6 +204,9 @@
path: "/performance/test/create"
})
break;
case "export":
downloadFile(this.test.name + ".json", this.test.export());
break;
}
}
},

View File

@ -143,7 +143,7 @@
});
},
handleCopy(test) {
this.result = this.$post("/api/copy", {id: test.id}, () => {
this.result = this.$post("/api/copy", {projectId: test.projectId, id: test.id, name: test.name}, () => {
this.$success(this.$t('commons.copy_success'));
this.search();
});

View File

@ -10,7 +10,7 @@
:placeholder="$t('api_test.key')" show-word-limit/>
</el-col>
<el-col>
<el-input :disabled="isReadOnly" v-model="item.value" size="small" maxlength="100" @change="change"
<el-input :disabled="isReadOnly" v-model="item.value" size="small" maxlength="500" @change="change"
:placeholder="$t('api_test.value')" show-word-limit/>
</el-col>
<el-col class="kv-delete">

View File

@ -5,9 +5,10 @@
</el-form-item>
<el-form-item :label="$t('api_test.request.url')" prop="url">
<el-input :disabled="isReadOnly" v-model="request.url" maxlength="100" :placeholder="$t('api_test.request.url_description')"
@change="urlChange" clearable>
<el-select :disabled="isReadOnly" v-model="request.method" slot="prepend" class="request-method-select" @change="methodChange">
<el-input :disabled="isReadOnly" v-model="request.url" maxlength="500"
:placeholder="$t('api_test.request.url_description')" @change="urlChange" clearable>
<el-select :disabled="isReadOnly" v-model="request.method" slot="prepend" class="request-method-select"
@change="methodChange">
<el-option label="GET" value="GET"/>
<el-option label="POST" value="POST"/>
<el-option label="PUT" value="PUT"/>
@ -22,8 +23,8 @@
<el-tabs v-model="activeName">
<el-tab-pane :label="$t('api_test.request.parameters')" name="parameters">
<ms-api-key-value :is-read-only="isReadOnly" :items="request.parameters" :description="$t('api_test.request.parameters_desc')"
@change="parametersChange"/>
<ms-api-key-value :is-read-only="isReadOnly" :items="request.parameters"
:description="$t('api_test.request.parameters_desc')" @change="parametersChange"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.headers')" name="headers">
<ms-api-key-value :is-read-only="isReadOnly" :items="request.headers"/>
@ -74,7 +75,7 @@
{max: 100, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'}
],
url: [
{max: 100, required: true, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'},
{max: 500, required: true, message: this.$t('commons.input_limit', [0, 500]), trigger: 'blur'},
{validator: validateURL, trigger: 'blur'}
]
}
@ -96,7 +97,7 @@
//
parameters.push(new KeyValue());
this.request.parameters = parameters;
this.request.url = url.toString();
this.request.url = this.getURL(url);
} catch (e) {
this.$error(this.$t('api_test.request.url_invalid'), 2000)
}
@ -116,7 +117,7 @@
url.searchParams.append(parameter.name, parameter.value);
}
})
this.request.url = url.toString();
this.request.url = this.getURL(url);
},
addProtocol(url) {
if (url) {
@ -125,6 +126,9 @@
}
}
return url;
},
getURL(url) {
return decodeURIComponent(url.origin + url.pathname) + "?" + url.searchParams.toString();
}
},

View File

@ -15,13 +15,13 @@
<script>
import {ResponseTime} from "../../model/ScenarioModel";
import {Duration} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionResponseTime",
name: "MsApiAssertionDuration",
props: {
duration: ResponseTime,
duration: Duration,
value: [Number, String],
edit: Boolean,
callback: Function,

View File

@ -6,14 +6,14 @@
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="$t('api_test.request.assertions.response_time')" :value="options.RESPONSE_TIME"/>
<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-response-time :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
v-if="type === options.RESPONSE_TIME" :callback="after"/>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="time" :duration="assertions.duration"
v-if="type === options.DURATION" :callback="after"/>
</el-col>
</el-row>
@ -24,14 +24,14 @@
<script>
import MsApiAssertionText from "./ApiAssertionText";
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionResponseTime from "./ApiAssertionResponseTime";
import {ASSERTION_TYPE, Assertions, ResponseTime} from "../../model/ScenarioModel";
import MsApiAssertionDuration from "./ApiAssertionDuration";
import {ASSERTION_TYPE, Assertions} from "../../model/ScenarioModel";
import MsApiAssertionsEdit from "./ApiAssertionsEdit";
export default {
name: "MsApiAssertions",
components: {MsApiAssertionsEdit, MsApiAssertionResponseTime, MsApiAssertionRegex, MsApiAssertionText},
components: {MsApiAssertionsEdit, MsApiAssertionDuration, MsApiAssertionRegex, MsApiAssertionText},
props: {
assertions: Assertions,

View File

@ -13,7 +13,7 @@
<div>
{{$t("api_test.request.assertions.response_time")}}
</div>
<ms-api-assertion-response-time :is-read-only="isReadOnly" v-model="assertions.duration.value" :duration="assertions.duration" :edit="true"/>
<ms-api-assertion-duration :is-read-only="isReadOnly" v-model="assertions.duration.value" :duration="assertions.duration" :edit="true"/>
</div>
</div>
@ -21,13 +21,13 @@
<script>
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionResponseTime from "./ApiAssertionResponseTime";
import MsApiAssertionDuration from "./ApiAssertionDuration";
import {Assertions} from "../../model/ScenarioModel";
export default {
name: "MsApiAssertionsEdit",
components: {MsApiAssertionResponseTime, MsApiAssertionRegex},
components: {MsApiAssertionDuration, MsApiAssertionRegex},
props: {
assertions: Assertions,

View File

@ -33,7 +33,7 @@ export class Element {
}
commonValue(tag, name, value, defaultValue) {
let v = this.getDefault(value, defaultValue)
let v = this.getDefault(value, defaultValue);
return this.add(new Element(tag, {name: name}, v));
}
@ -73,6 +73,11 @@ export class Element {
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;
@ -85,10 +90,10 @@ export class Element {
}
start() {
let str = this.indent + '<' + this.name;
let str = this.indent + '<' + this.replace(this.name);
for (let key in this.attributes) {
if (this.attributes.hasOwnProperty(key)) {
str += ' ' + key + '="' + this.attributes[key] + '"';
str += ' ' + this.replace(key) + '="' + this.replace(this.attributes[key]) + '"';
}
}
if (this.isEmpty()) {
@ -101,7 +106,7 @@ export class Element {
content() {
if (!this.isEmptyValue()) {
return this.value;
return this.replace(this.value);
}
let str = '';
@ -120,7 +125,7 @@ export class Element {
if (this.isEmpty()) {
return '\n';
}
let str = '</' + this.name + '>\n';
let str = '</' + this.replace(this.name) + '>\n';
if (!this.isEmptyValue()) {
return str;
}

View File

@ -40,7 +40,7 @@ export const BODY_TYPE = {
export const ASSERTION_TYPE = {
TEXT: "Text",
REGEX: "Regex",
RESPONSE_TIME: "Response Time"
DURATION: "Duration"
}
export const ASSERTION_REGEX_SUBJECT = {
@ -94,6 +94,7 @@ export class BaseConfig {
export class Test extends BaseConfig {
constructor(options) {
super();
this.type = "MS API CONFIG";
this.version = '1.0.0';
this.id = uuid();
this.name = undefined;
@ -104,6 +105,16 @@ export class Test extends BaseConfig {
this.sets({scenarioDefinition: Scenario}, options);
}
export() {
let obj = {
type: this.type,
version: this.version,
scenarios: this.scenarioDefinition
};
return JSON.stringify(obj);
}
initOptions(options) {
options = options || {};
options.scenarioDefinition = options.scenarioDefinition || [new Scenario()];
@ -253,7 +264,7 @@ export class Assertions extends BaseConfig {
initOptions(options) {
options = options || {};
options.duration = new ResponseTime(options.duration);
options.duration = new Duration(options.duration);
return options;
}
}
@ -291,9 +302,9 @@ export class Regex extends AssertionType {
}
}
export class ResponseTime extends AssertionType {
export class Duration extends AssertionType {
constructor(options) {
super(ASSERTION_TYPE.RESPONSE_TIME);
super(ASSERTION_TYPE.DURATION);
this.value = undefined;
this.set(options);
@ -377,8 +388,8 @@ class JMXRequest {
if (request && request instanceof Request && request.url) {
let url = new URL(request.url);
this.method = request.method;
this.hostname = url.hostname;
this.pathname = url.pathname;
this.hostname = decodeURIComponent(url.hostname);
this.pathname = decodeURIComponent(url.pathname);
this.port = url.port;
this.protocol = url.protocol.split(":")[0];
if (this.method.toUpperCase() !== "GET") {
@ -418,7 +429,6 @@ class JMXGenerator {
addScenarios(testPlan, scenarios) {
scenarios.forEach(s => {
let scenario = s.clone();
scenario.name = this.replace(scenario.name);
let threadGroup = new ThreadGroup(scenario.name || "");
@ -429,8 +439,6 @@ class JMXGenerator {
scenario.requests.forEach(request => {
if (!request.isValid()) return;
request.name = this.replace(request.name);
let httpSamplerProxy = new HTTPSamplerProxy(request.name || "", new JMXRequest(request));
this.addRequestHeader(httpSamplerProxy, request);
@ -453,7 +461,7 @@ class JMXGenerator {
}
addScenarioVariables(threadGroup, scenario) {
let args = this.replaceKV(scenario.variables);
let args = this.filterKV(scenario.variables);
if (args.length > 0) {
let name = scenario.name + " Variables"
threadGroup.put(new Arguments(name, args));
@ -461,7 +469,7 @@ class JMXGenerator {
}
addScenarioHeaders(threadGroup, scenario) {
let headers = this.replaceKV(scenario.headers);
let headers = this.filterKV(scenario.headers);
if (headers.length > 0) {
let name = scenario.name + " Headers"
threadGroup.put(new HeaderManager(name, headers));
@ -470,14 +478,14 @@ class JMXGenerator {
addRequestHeader(httpSamplerProxy, request) {
let name = request.name + " Headers";
let headers = this.replaceKV(request.headers);
let headers = this.filterKV(request.headers);
if (headers.length > 0) {
httpSamplerProxy.put(new HeaderManager(name, headers));
}
}
addRequestArguments(httpSamplerProxy, request) {
let args = this.replaceKV(request.parameters);
let args = this.filterKV(request.parameters);
if (args.length > 0) {
httpSamplerProxy.add(new HTTPSamplerArguments(args));
}
@ -486,10 +494,10 @@ class JMXGenerator {
addRequestBody(httpSamplerProxy, request) {
let body = [];
if (request.body.isKV()) {
body = this.replaceKV(request.body.kvs);
body = this.filterKV(request.body.kvs);
} else {
httpSamplerProxy.boolProp('HTTPSampler.postBodyRaw', true);
body.push({name: '', value: this.replace(request.body.raw), encode: false});
body.push({name: '', value: request.body.raw, encode: false});
}
httpSamplerProxy.add(new HTTPSamplerArguments(body));
@ -510,9 +518,9 @@ class JMXGenerator {
}
getAssertion(regex) {
let name = this.replace(regex.description);
let name = regex.description;
let type = JMX_ASSERTION_CONDITION.CONTAINS; // 固定用Match自己写正则
let value = this.replace(regex.expression);
let value = regex.expression;
switch (regex.subject) {
case ASSERTION_REGEX_SUBJECT.RESPONSE_CODE:
return new ResponseCodeAssertion(name, type, value);
@ -546,8 +554,8 @@ class JMXGenerator {
getExtractor(extractCommon) {
let props = {
name: this.replace(extractCommon.variable),
expression: this.replace(extractCommon.expression),
name: extractCommon.variable,
expression: extractCommon.expression,
}
let testName = props.name
switch (extractCommon.type) {
@ -569,19 +577,8 @@ class JMXGenerator {
return config.isValid();
}
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;");
}
replaceKV(kvs) {
let results = [];
kvs.filter(this.filter).forEach(kv => {
let name = this.replace(kv.name);
let value = this.replace(kv.value);
results.push(new KeyValue(name, value));
});
return results;
filterKV(kvs) {
return kvs.filter(this.filter);
}
toXML() {

View File

@ -1,5 +1,5 @@
<template>
<router-link v-if="this.show" class="create-test" :to="this.to" v-permission="this.permission">
<router-link class="create-test" :to="this.to" v-permission="this.permission">
<el-button type="primary" size="small">{{this.title}}</el-button>
</router-link>
</template>
@ -8,7 +8,6 @@
export default {
name: "MsCreateTest",
props: {
show: Boolean,
to: [String, Object],
title: {
type: String,

View File

@ -36,7 +36,7 @@
</el-col>
<el-col :span="8">
<el-row type="flex" justify="center">
<ms-create-test v-permission="['test_manager','test_user']" :to="'/performance/test/create'"/>
<ms-create-test :to="'/performance/test/create'"/>
</el-row>
</el-col>
<el-col :span="8"/>

View File

@ -3,17 +3,9 @@
<ms-main-container>
<el-card class="table-card" v-loading="result.loading">
<template v-slot:header>
<div>
<el-row type="flex" justify="space-between" align="middle">
<span class="title">{{$t('commons.test')}}</span>
<span class="search">
<el-input type="text" size="small" :placeholder="$t('load_test.search_by_name')"
prefix-icon="el-icon-search"
maxlength="60"
v-model="condition.name" @change="search" clearable/>
</span>
</el-row>
</div>
<ms-table-header :is-tester-permission="true" :condition.sync="condition" @search="search"
:title="$t('commons.test')"
@create="create" :createTip="$t('load_test.create')"/>
</template>
<el-table :data="tableData" class="test-content"
@ -89,9 +81,11 @@
import MsPerformanceTestStatus from "./PerformanceTestStatus";
import MsTableOperators from "../../common/components/MsTableOperators";
import {_filter, _sort} from "../../../../common/js/utils";
import MsTableHeader from "../../common/components/MsTableHeader";
export default {
components: {
MsTableHeader,
MsPerformanceTestStatus,
MsTablePagination,
MsTableOperator,
@ -210,6 +204,9 @@
this.$router.push({
path: '/performance/test/edit/' + row.id,
})
},
create() {
this.$router.push('/performance/test/create');
}
}
}

View File

@ -304,6 +304,12 @@
return false;
}
if (!this.threadNumber || !this.duration || !this.rampUpTime || !this.step || !this.rpsLimit) {
this.$warning(this.$t('load_test.pressure_config_params_is_empty'));
this.$emit('changeActive', '1');
return false;
}
return true;
},
convertProperty() {

View File

@ -57,7 +57,7 @@
</el-dialog>
<!--Change personal password-->
<el-dialog :title="$t('member.edit_password')" :visible.sync="editPasswordVisible" width="30%" left>
<el-dialog :title="$t('member.edit_password')" :visible.sync="editPasswordVisible" width="35%" left>
<el-form :model="ruleForm" :rules="rules" ref="editPasswordForm" label-width="120px" class="demo-ruleForm">
<el-form-item :label="$t('member.old_password')" prop="password" style="margin-bottom: 29px">
<el-input v-model="ruleForm.password" autocomplete="off" show-password/>
@ -128,12 +128,6 @@
rules:{
password: [
{required: true, message: this.$t('user.input_password'), trigger: 'blur'},
{
required: true,
pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[^]{8,16}$/,
message: this.$t('member.password_format_is_incorrect'),
trigger: 'blur'
}
],
newpassword: [
{required: true, message: this.$t('user.input_password'), trigger: 'blur'},

View File

@ -61,7 +61,7 @@
<el-input v-model="form.name" autocomplete="off"/>
</el-form-item>
<el-form-item :label="$t('commons.description')" prop="description">
<el-input v-model="form.description" autocomplete="off"/>
<el-input v-model="form.description" autocomplete="off" type="textarea"/>
</el-form-item>
</el-form>
<template v-slot:footer>

View File

@ -61,7 +61,7 @@
<el-form-item :label="$t('commons.name')" prop="name">
<el-input v-model="form.name" autocomplete="off"/>
</el-form-item>
<el-form-item :label="$t('commons.description')">
<el-form-item :label="$t('commons.description')" prop="description">
<el-input type="textarea" v-model="form.description"></el-input>
</el-form-item>
<el-form-item :label="$t('workspace.organization_name')" prop="organizationId">

View File

@ -64,8 +64,9 @@
<el-form-item :label="$t('commons.phone')" prop="phone">
<el-input v-model="form.phone" autocomplete="off" :placeholder="$t('user.input_phone')"/>
</el-form-item>
<el-form-item :label="$t('commons.password')" prop="password">
<el-input v-model="form.password" autocomplete="new-password" show-password :placeholder="$t('user.input_password')"/>
<el-form-item :label="$t('commons.password')" prop="password" style="margin-bottom: 29px">
<el-input v-model="form.password" autocomplete="new-password" show-password
:placeholder="$t('user.input_password')"/>
</el-form-item>
<div v-for="(role, index) in form.roles" :key="index">
<el-form-item :label="$t('commons.role')+index"

View File

@ -409,7 +409,7 @@
},
getMaintainerOptions() {
let workspaceId = localStorage.getItem(WORKSPACE_ID);
this.$post('/user/ws/member/list/all', {workspaceId:workspaceId}, response => {
this.$post('/user/ws/member/tester/list', {workspaceId:workspaceId}, response => {
this.maintainerOptions = response.data;
});
},

View File

@ -188,7 +188,7 @@
},
setPrincipalOptions() {
let workspaceId = localStorage.getItem(WORKSPACE_ID);
this.$post('/user/ws/member/list/all', {workspaceId:workspaceId}, response => {
this.$post('/user/ws/member/tester/list', {workspaceId:workspaceId}, response => {
this.principalOptions = response.data;
});
},

View File

@ -30,24 +30,23 @@
show-overflow-tooltip>
<template v-slot:default="scope">
<span @click.stop="clickt = 'stop'">
<el-dropdown v-permission="['test_manager','test_user']" class="test-case-status" @command="statusChange">
<el-dropdown class="test-case-status" @command="statusChange">
<span class="el-dropdown-link">
<plan-status-table-item :value="scope.row.status"/>
</span>
<el-dropdown-menu slot="dropdown" chang>
<el-dropdown-item :command="{id: scope.row.id, status: 'Prepare'}">
<el-dropdown-item :disabled="!isTestManagerOrTestUser" :command="{id: scope.row.id, status: 'Prepare'}">
{{$t('test_track.plan.plan_status_prepare')}}
</el-dropdown-item>
<el-dropdown-item :command="{id: scope.row.id, status: 'Underway'}">
<el-dropdown-item :disabled="!isTestManagerOrTestUser" :command="{id: scope.row.id, status: 'Underway'}">
{{$t('test_track.plan.plan_status_running')}}
</el-dropdown-item>
<el-dropdown-item :command="{id: scope.row.id, status: 'Completed'}">
<el-dropdown-item :disabled="!isTestManagerOrTestUser" :command="{id: scope.row.id, status: 'Completed'}">
{{$t('test_track.plan.plan_status_completed')}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
<plan-status-table-item v-permission="['test_viewer']" :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column
@ -115,7 +114,7 @@
import MsTableOperator from "../../../common/components/MsTableOperator";
import PlanStatusTableItem from "../../common/tableItems/plan/PlanStatusTableItem";
import PlanStageTableItem from "../../common/tableItems/plan/PlanStageTableItem";
import {_filter, _sort} from "../../../../../common/js/utils";
import {_filter, _sort, checkoutTestManagerOrTestUser} from "../../../../../common/js/utils";
import TestReportTemplateList from "../view/comonents/TestReportTemplateList";
import TestCaseReportView from "../view/comonents/report/TestCaseReportView";
import MsDeleteConfirm from "../../../common/components/MsDeleteConfirm";
@ -137,6 +136,7 @@
condition: {},
currentPage: 1,
pageSize: 10,
isTestManagerOrTestUser: false,
total: 0,
tableData: [],
statusFilters: [
@ -160,6 +160,7 @@
},
created() {
this.projectId = this.$route.params.projectId;
this.isTestManagerOrTestUser = checkoutTestManagerOrTestUser();
this.initTableData();
},
methods: {

View File

@ -39,7 +39,7 @@
methods: {
setMaintainerOptions() {
let workspaceId = localStorage.getItem(WORKSPACE_ID);
this.$post('/user/ws/member/list/all', {workspaceId:workspaceId}, response => {
this.$post('/user/ws/member/tester/list', {workspaceId:workspaceId}, response => {
this.executorOptions = response.data;
});
},

View File

@ -289,9 +289,21 @@
param.issues = JSON.stringify(this.testCase.issues);
this.$post('/test/plan/case/edit', param, () => {
this.$success(this.$t('commons.save_success'));
this.updateTestCases(param);
this.setPlanStatus(this.testCase.planId);
});
},
updateTestCases(param) {
for (let i = 0; i < this.testCases.length; i++) {
let testCase = this.testCases[i];
if (testCase.id === param.id) {
testCase.results = param.results;
testCase.issues = param.issues;
testCase.status = param.status;
return;
}
}
},
handleNext() {
this.index++;
this.getTestCase(this.index);

View File

@ -89,27 +89,26 @@
:label="$t('test_track.plan_view.execute_result')">
<template v-slot:default="scope">
<span @click.stop="clickt = 'stop'">
<el-dropdown v-permission="['test_manager','test_user']" class="test-case-status" @command="statusChange" >
<el-dropdown class="test-case-status" @command="statusChange" >
<span class="el-dropdown-link">
<status-table-item :value="scope.row.status"/>
</span>
<el-dropdown-menu slot="dropdown" chang>
<el-dropdown-item :command="{id: scope.row.id, status: 'Pass'}">
<el-dropdown-item :disabled="!isTestManagerOrTestUser" :command="{id: scope.row.id, status: 'Pass'}">
{{$t('test_track.plan_view.pass')}}
</el-dropdown-item>
<el-dropdown-item :command="{id: scope.row.id, status: 'Failure'}">
<el-dropdown-item :disabled="!isTestManagerOrTestUser" :command="{id: scope.row.id, status: 'Failure'}">
{{$t('test_track.plan_view.failure')}}
</el-dropdown-item>
<el-dropdown-item :command="{id: scope.row.id, status: 'Blocking'}">
<el-dropdown-item :disabled="!isTestManagerOrTestUser" :command="{id: scope.row.id, status: 'Blocking'}">
{{$t('test_track.plan_view.blocking')}}
</el-dropdown-item>
<el-dropdown-item :command="{id: scope.row.id, status: 'Skip'}">
<el-dropdown-item :disabled="!isTestManagerOrTestUser" :command="{id: scope.row.id, status: 'Skip'}">
{{$t('test_track.plan_view.skip')}}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
<status-table-item v-permission="['test_viewer']" :value="scope.row.status"/>
</template>
</el-table-column>
@ -199,6 +198,7 @@
selectIds: new Set(),
testPlan: {},
isReadOnly: false,
isTestManagerOrTestUser: false,
priorityFilters: [
{text: 'P0', value: 'P0'},
{text: 'P1', value: 'P1'},
@ -244,6 +244,7 @@
},
mounted() {
this.refreshTableAndPlan();
this.isTestManagerOrTestUser = checkoutTestManagerOrTestUser();
},
methods: {
initTableData() {

View File

@ -0,0 +1,120 @@
<template>
<common-component :title="$t('test_track.plan_view.failure_case')">
<template>
<el-table
row-key="id"
:data="failureTestCases">
<el-table-column
prop="name"
:label="$t('commons.name')"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="priority"
column-key="priority"
:label="$t('test_track.case.priority')">
<template v-slot:default="scope">
<priority-table-item :value="scope.row.priority" ref="priority"/>
</template>
</el-table-column>
<el-table-column
prop="type"
column-key="type"
:label="$t('test_track.case.type')"
show-overflow-tooltip>
<template v-slot:default="scope">
<type-table-item :value="scope.row.type"/>
</template>
</el-table-column>
<el-table-column
prop="method"
column-key="method"
:label="$t('test_track.case.method')"
show-overflow-tooltip>
<template v-slot:default="scope">
<method-table-item :value="scope.row.method"/>
</template>
</el-table-column>
<el-table-column
prop="nodePath"
:label="$t('test_track.case.module')"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="executorName"
:label="$t('test_track.plan_view.executor')">
</el-table-column>
<el-table-column
prop="status"
column-key="status"
:label="$t('test_track.plan_view.execute_result')">
<template v-slot:default="scope">
<status-table-item :value="scope.row.status"/>
</template>
</el-table-column>
<el-table-column
prop="updateTime"
:label="$t('commons.update_time')"
show-overflow-tooltip>
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
</template>
</el-table-column>
</el-table>
</template>
</common-component>
</template>
<script>
import CommonComponent from "./CommonComponent";
import PriorityTableItem from "../../../../../common/tableItems/planview/PriorityTableItem";
import TypeTableItem from "../../../../../common/tableItems/planview/TypeTableItem";
import MethodTableItem from "../../../../../common/tableItems/planview/MethodTableItem";
import StatusTableItem from "../../../../../common/tableItems/planview/StatusTableItem";
export default {
name: "FailureResultComponent",
components: {StatusTableItem, MethodTableItem, TypeTableItem, PriorityTableItem, CommonComponent},
props: {
failureTestCases: {
type: Array,
default() {
return [
{
name: 'testCase1',
priority: 'P1',
type: 'api',
method: 'auto',
nodePath: '/module1/module2',
executorName: "Tom",
status: "Failure",
updateTime: new Date(),
},
{
name: 'testCase2',
priority: 'P0',
type: 'functional',
method: 'manual',
nodePath: '/module1',
executorName: "Micheal",
status: "Failure",
updateTime: new Date(),
}
]
}
}
}
}
</script>
<style scoped>
</style>

View File

@ -6,6 +6,7 @@
<base-info-component :is-report="false" v-if="preview.id == 1"/>
<test-result-component v-if="preview.id == 2"/>
<test-result-chart-component v-if="preview.id == 3"/>
<failure-result-component v-if="preview.id == 4"/>
<rich-text-component :preview="preview" v-if="preview.type != 'system'"/>
</div>
@ -14,6 +15,7 @@
<base-info-component :report-info="metric" v-if="preview.id == 1"/>
<test-result-component :test-results="metric.moduleExecuteResult" v-if="preview.id == 2"/>
<test-result-chart-component :execute-result="metric.executeResult" v-if="preview.id == 3"/>
<failure-result-component :failure-test-cases="metric.failureTestCases" v-if="preview.id == 4"/>
<rich-text-component :is-report-view="isReportView" :preview="preview" v-if="preview.type != 'system'"/>
</div>
@ -25,9 +27,12 @@
import TestResultComponent from "./TestResultComponent";
import TestResultChartComponent from "./TestResultChartComponent";
import RichTextComponent from "./RichTextComponent";
import FailureResultComponent from "./FailureResultComponent";
export default {
name: "TemplateComponent",
components: {RichTextComponent, TestResultChartComponent, TestResultComponent, BaseInfoComponent},
components: {
FailureResultComponent,
RichTextComponent, TestResultChartComponent, TestResultComponent, BaseInfoComponent},
props: {
preview: {
type: Object

View File

@ -76,10 +76,11 @@
[1, { name: this.$t('test_track.plan_view.base_info'), id: 1 , type: 'system'}],
[2, { name: this.$t('test_track.plan_view.test_result'), id: 2 , type: 'system'}],
[3, { name: this.$t('test_track.plan_view.result_distribution'), id: 3 ,type: 'system'}],
[4, { name: this.$t('test_track.plan_view.custom_component'), id: 4 ,type: 'custom'}]
[4, { name: this.$t('test_track.plan_view.failure_case'), id: 4 ,type: 'system'}],
[5, { name: this.$t('test_track.plan_view.custom_component'), id: 5 ,type: 'custom'}]
]
),
components: [4],
components: [5],
previews: [],
template: {},
isReport: false
@ -107,13 +108,13 @@
}
this.template = {
name: '',
content: {
components: [1,2,3,4],
customComponent: new Map()
content: {
components: [1,2,3,4,5],
customComponent: new Map()
}
};
this.previews = [];
this.components = [4];
this.components = [5];
if (id) {
this.type = 'edit';
this.getTemplateById(id);

View File

@ -68,7 +68,8 @@
[1, { name: this.$t('test_track.plan_view.base_info'), id: 1 , type: 'system'}],
[2, { name: this.$t('test_track.plan_view.test_result'), id: 2 , type: 'system'}],
[3, { name: this.$t('test_track.plan_view.result_distribution'), id: 3 ,type: 'system'}],
[4, { name: this.$t('test_track.plan_view.custom_component'), id: 4 ,type: 'custom'}]
[4, { name: this.$t('test_track.plan_view.failure_case'), id: 4 ,type: 'system'}],
[5, { name: this.$t('test_track.plan_view.custom_component'), id: 5 ,type: 'custom'}]
]
),
isTestManagerOrTestUser: false
@ -165,6 +166,15 @@
getMetric() {
this.result = this.$get('/test/plan/get/metric/' + this.planId, response => {
this.metric = response.data;
if (!this.metric.failureTestCases) {
this.metric.failureTestCases = [];
}
if (!this.metric.executeResult) {
this.metric.executeResult = [];
}
if (!this.metric.moduleExecuteResult) {
this.metric.moduleExecuteResult = [];
}
if (this.report.startTime) {
this.metric.startTime = new Date(this.report.startTime);
}

View File

@ -125,11 +125,13 @@ export function _filter(filters, condition) {
if (!condition.filters) {
condition.filters = {};
}
for(let filter in filters) {
if (filters[filter] && filters[filter].length > 0) {
condition.filters[filter] = filters[filter];
} else {
condition.filters[filter] = null;
for (let filter in filters) {
if (filters.hasOwnProperty(filter)) {
if (filters[filter] && filters[filter].length > 0) {
condition.filters[filter] = filters[filter];
} else {
condition.filters[filter] = null;
}
}
}
}
@ -137,7 +139,7 @@ export function _filter(filters, condition) {
//表格数据排序
export function _sort(column, condition) {
column.prop = humpToLine(column.prop);
if (column.order == 'descending') {
if (column.order === 'descending') {
column.order = 'desc';
} else {
column.order = 'asc';
@ -147,13 +149,28 @@ export function _sort(column, condition) {
}
let hasProp = false;
condition.orders.forEach(order => {
if (order.name == column.prop) {
if (order.name === column.prop) {
order.type = column.order;
hasProp = true;
return;
}
});
if (!hasProp) {
condition.orders.push({name: column.prop, type: column.order});
}
}
export function downloadFile(name, content) {
const blob = new Blob([content]);
if ("download" in document.createElement("a")) {
// 非IE下载
// chrome/firefox
let aTag = document.createElement('a');
aTag.download = name;
aTag.href = URL.createObjectURL(blob);
aTag.click();
URL.revokeObjectURL(aTag.href)
} else {
// IE10+下载
navigator.msSaveBlob(blob, name)
}
}

View File

@ -155,7 +155,7 @@ export default {
'special_characters_are_not_supported': 'Special characters are not supported',
'mobile_number_format_is_incorrect': 'Mobile number format is incorrect',
'email_format_is_incorrect': 'Email format is incorrect',
'password_format_is_incorrect': 'Password format is incorrect (At least 8-16 characters, at least 1 uppercase letter, 1 lowercase letter and 1 number)',
'password_format_is_incorrect': 'Valid password: 8-16 digits, English upper and lower case letters + numbers ',
'old_password': 'Old Password',
'new_password': 'New Password',
'remove_member': 'Are you sure you want to remove this member',
@ -268,6 +268,7 @@ export default {
'download_log_file': 'Download',
'user_name': 'Creator',
'special_characters_are_not_supported': 'Test name does not support special characters',
'pressure_config_params_is_empty': 'Pressure configuration parameters cannot be empty!'
},
api_test: {
creator: "Creator",
@ -283,6 +284,7 @@ export default {
key: "Key",
value: "Value",
create_performance_test: "Create Performance Test",
export_config: "Export Configuration",
scenario: {
config: "Scenario Config",
input_name: "Please enter the scenario name",
@ -517,6 +519,7 @@ export default {
create_template: "Create template",
report_template: "Report template",
test_detail: "Test detail",
failure_case: "Failure case",
}
},
test_resource_pool: {

View File

@ -154,7 +154,7 @@ export default {
'special_characters_are_not_supported': '不支持特殊字符',
'mobile_number_format_is_incorrect': '手机号码格式不正确',
'email_format_is_incorrect': '邮箱格式不正确',
'password_format_is_incorrect': '密码格式不正确(至少8-16个字符至少1个大写字母1个小写字母和1个数字)',
'password_format_is_incorrect': '有效密码8-16位英文大小写字母+数字',
'old_password': '旧密码',
'new_password': '新密码',
'remove_member': '确定要移除该成员吗',
@ -268,6 +268,7 @@ export default {
'pressure_prediction_chart': '压力预估图',
'user_name': '创建人',
'special_characters_are_not_supported': '测试名称不支持特殊字符',
'pressure_config_params_is_empty': '压力配置参数不能为空!'
},
api_test: {
creator: "创建人",
@ -282,6 +283,7 @@ export default {
key: "键",
value: "值",
create_performance_test: "创建性能测试",
export_config: "导出配置",
scenario: {
config: "场景配置",
input_name: "请输入场景名称",
@ -516,6 +518,7 @@ export default {
create_template: "新建模版",
report_template: "测试报告模版",
test_detail: "测试详情",
failure_case: "失败用例",
}
},
test_resource_pool: {

View File

@ -152,7 +152,7 @@ export default {
'special_characters_are_not_supported': '不支持特殊字符',
'mobile_number_format_is_incorrect': '手機號碼格式不正確',
'email_format_is_incorrect': '郵箱格式不正確',
'password_format_is_incorrect': '密碼格式不正確(至少8-16個字符,至少1個大寫字母,1個小寫字母和1個數字)',
'password_format_is_incorrect': '有效密碼8-16比特英文大小寫字母+數位',
'old_password': '舊密碼',
'new_password': '新密碼',
'remove_member': '確定要移除該成員嗎',
@ -266,6 +266,7 @@ export default {
'pressure_prediction_chart': '壓力預估圖',
'user_name': '創建人',
'special_characters_are_not_supported': '測試名稱不支持特殊字符',
'pressure_config_params_is_empty': '壓力配置參數不能為空!'
},
api_test: {
title: "測試",
@ -280,6 +281,7 @@ export default {
key: "鍵",
value: "值",
create_performance_test: "創建性能測試",
export_config: "匯出配寘",
scenario: {
creator: "創建人",
config: "場景配寘",
@ -515,6 +517,7 @@ export default {
create_template: "新建模版",
report_template: "測試報告模版",
test_detail: "測試詳情",
failure_case: "失敗用例",
}
},
test_resource_pool: {