feat(接口自动化): 完成场景调试,和基础报告

This commit is contained in:
fit2-zhao 2020-12-04 18:00:37 +08:00
parent 1665a0f91a
commit 57986195e7
47 changed files with 1980 additions and 203 deletions

View File

@ -0,0 +1,27 @@
package io.metersphere.api.controller;
import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.service.ApiScenarioReportService;
import io.metersphere.commons.constants.RoleConstants;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
@RestController
@RequestMapping(value = "/api/scenario/report")
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)
public class APIScenarioReportController {
@Resource
private ApiScenarioReportService apiReportService;
@GetMapping("/get/{reportId}")
public APIReportResult get(@PathVariable String reportId) {
return apiReportService.getCacheResult(reportId);
}
}

View File

@ -70,10 +70,9 @@ public class ApiAutomationController {
return apiAutomationService.getApiScenarios(ids);
}
@PostMapping(value = "/run/debug")
@PostMapping(value = "/run")
public void runDebug(@RequestPart("request") RunDefinitionRequest request, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
apiAutomationService.run(request, bodyFiles);
}
}

View File

@ -1,5 +1,5 @@
package io.metersphere.api.dto.automation;
public enum ScenarioStatus {
Saved, Success, Fail, Trash
Saved, Success, Fail, Trash,Underway
}

View File

@ -15,6 +15,8 @@ public class RunDefinitionRequest {
private String reportId;
private String environmentId;
private MsTestElement testElement;
private Response response;

View File

@ -1,10 +1,14 @@
package io.metersphere.api.dto.definition.request;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import com.google.gson.Gson;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import io.metersphere.api.service.ApiAutomationService;
import io.metersphere.api.service.ApiTestEnvironmentService;
import io.metersphere.base.domain.ApiScenario;
import io.metersphere.base.domain.ApiTestEnvironmentWithBLOBs;
import io.metersphere.commons.utils.CommonBeanFactory;
import lombok.Data;
import lombok.EqualsAndHashCode;
@ -21,23 +25,36 @@ public class MsScenario extends MsTestElement {
private String type = "scenario";
@JSONField(ordinal = 10)
private String name;
@JSONField(ordinal = 11)
private String referenced;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
@JSONField(ordinal = 12)
private String environmentId;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
if (environmentId != null) {
ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class);
ApiTestEnvironmentWithBLOBs environment = environmentService.get(environmentId);
config = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class);
}
if (this.getReferenced() != null && this.getReferenced().equals("Deleted")) {
return;
} else if (this.getReferenced() != null && this.getReferenced().equals("REF")) {
ApiAutomationService apiAutomationService = CommonBeanFactory.getBean(ApiAutomationService.class);
ApiScenario scenario = apiAutomationService.getApiScenario(this.getId());
Gson gs = new Gson();
MsTestElement element = gs.fromJson(scenario.getScenarioDefinition(), MsTestElement.class);
hashTree.add(element);
JSONObject element = JSON.parseObject(scenario.getScenarioDefinition());
List<MsTestElement> dataArr = JSON.parseArray(element.getString("hashTree"), MsTestElement.class);
if (hashTree == null) {
hashTree = dataArr;
} else {
hashTree.addAll(dataArr);
}
}
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(tree, el.getHashTree());
});
for (MsTestElement el : hashTree) {
el.toHashTree(tree, el.getHashTree(), config);
}
}
}
}

View File

@ -16,6 +16,7 @@ import io.metersphere.api.dto.definition.request.sampler.MsHTTPSamplerProxy;
import io.metersphere.api.dto.definition.request.sampler.MsJDBCSampler;
import io.metersphere.api.dto.definition.request.sampler.MsTCPSampler;
import io.metersphere.api.dto.definition.request.timer.MsConstantTimer;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import io.metersphere.commons.utils.LogUtil;
import lombok.Data;
import org.apache.jmeter.protocol.http.control.AuthManager;
@ -61,9 +62,10 @@ public abstract class MsTestElement {
@JSONField(ordinal = 4)
private LinkedList<MsTestElement> hashTree;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
// 公共环境逐层传递如果自身有环境 以自身引用环境为准否则以公共环境作为请求环境
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
for (MsTestElement el : hashTree) {
el.toHashTree(tree, el.hashTree);
el.toHashTree(tree, el.hashTree, config);
}
}
@ -85,9 +87,15 @@ public abstract class MsTestElement {
return null;
}
public HashTree generateHashTree(EnvironmentConfig config) {
HashTree jmeterTestPlanHashTree = new ListedHashTree();
this.toHashTree(jmeterTestPlanHashTree, this.hashTree, config);
return jmeterTestPlanHashTree;
}
public HashTree generateHashTree() {
HashTree jmeterTestPlanHashTree = new ListedHashTree();
this.toHashTree(jmeterTestPlanHashTree, this.hashTree);
this.toHashTree(jmeterTestPlanHashTree, this.hashTree, null);
return jmeterTestPlanHashTree;
}

View File

@ -1,6 +1,7 @@
package io.metersphere.api.dto.definition.request;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -18,11 +19,11 @@ import java.util.List;
public class MsTestPlan extends MsTestElement {
private String type = "TestPlan";
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
final HashTree testPlanTree = tree.add(getPlan());
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(testPlanTree, el.getHashTree());
el.toHashTree(testPlanTree, el.getHashTree(), config);
});
}
}

View File

@ -1,6 +1,7 @@
package io.metersphere.api.dto.definition.request;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -18,11 +19,11 @@ import java.util.List;
public class MsThreadGroup extends MsTestElement {
private String type = "ThreadGroup";
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
final HashTree groupTree = tree.add(getThreadGroup());
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(groupTree, el.getHashTree());
el.toHashTree(groupTree, el.getHashTree(), config);
});
}
}
@ -37,7 +38,7 @@ public class MsThreadGroup extends MsTestElement {
ThreadGroup threadGroup = new ThreadGroup();
threadGroup.setEnabled(true);
threadGroup.setName(this.getName() + "ThreadGroup");
threadGroup.setName(this.getName());
threadGroup.setProperty(TestElement.TEST_CLASS, ThreadGroup.class.getName());
threadGroup.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("ThreadGroupGui"));
threadGroup.setNumThreads(1);

View File

@ -2,6 +2,7 @@ package io.metersphere.api.dto.definition.request.assertions;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -23,10 +24,10 @@ public class MsAssertions extends MsTestElement {
private MsAssertionDuration duration;
private String type = "Assertions";
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
addAssertions(tree);
}
private void addAssertions(HashTree hashTree) {
if (CollectionUtils.isNotEmpty(this.getRegex())) {
this.getRegex().stream().filter(MsAssertionRegex::isValid).forEach(assertion ->

View File

@ -50,7 +50,7 @@ public class MsAuthManager extends MsTestElement {
@JSONField(ordinal = 18)
private String environment;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
AuthManager authManager = new AuthManager();
authManager.setEnabled(true);
authManager.setName(this.getUsername() + "AuthManager");
@ -63,7 +63,7 @@ public class MsAuthManager extends MsTestElement {
if (environment != null) {
ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class);
ApiTestEnvironmentWithBLOBs environmentWithBLOBs = environmentService.get(environment);
EnvironmentConfig config = JSONObject.parseObject(environmentWithBLOBs.getConfig(), EnvironmentConfig.class);
config = JSONObject.parseObject(environmentWithBLOBs.getConfig(), EnvironmentConfig.class);
this.url = config.getHttpConfig().getProtocol() + "://" + config.getHttpConfig().getSocket();
}
}

View File

@ -4,6 +4,7 @@ import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -24,7 +25,7 @@ public class MsHeaderManager extends MsTestElement {
@JSONField(ordinal = 10)
private List<KeyValue> headers;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
HeaderManager headerManager = new HeaderManager();
headerManager.setEnabled(true);
headerManager.setName(this.getName() + "Headers");
@ -36,7 +37,7 @@ public class MsHeaderManager extends MsTestElement {
final HashTree headersTree = tree.add(headerManager);
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(headersTree, el.getHashTree());
el.toHashTree(headersTree, el.getHashTree(), config);
});
}
}

View File

@ -2,6 +2,7 @@ package io.metersphere.api.dto.definition.request.controller;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -24,11 +25,11 @@ public class MsIfController extends MsTestElement {
private String operator;
private String value;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
final HashTree groupTree = tree.add(ifController());
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(groupTree, el.getHashTree());
el.toHashTree(groupTree, el.getHashTree(), config);
});
}
}

View File

@ -23,9 +23,9 @@ import java.util.List;
@JSONType(typeName = "DNSCacheManager")
public class MsDNSCacheManager extends MsTestElement {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
for (MsTestElement el : hashTree) {
el.toHashTree(tree, el.getHashTree());
el.toHashTree(tree, el.getHashTree(), config);
}
}

View File

@ -2,6 +2,7 @@ package io.metersphere.api.dto.definition.request.extract;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -23,7 +24,7 @@ public class MsExtract extends MsTestElement {
private List<MsExtractXPath> xpath;
private String type = "Extract";
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
addRequestExtractors(tree);
}

View File

@ -3,13 +3,14 @@ package io.metersphere.api.dto.definition.request.processors;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
import org.apache.jmeter.protocol.java.sampler.JSR223Sampler;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.testelement.TestElement;
import org.apache.jorphan.collections.HashTree;
import org.apache.jmeter.protocol.java.sampler.JSR223Sampler;
import java.util.List;
@ -25,7 +26,7 @@ public class MsJSR223Processor extends MsTestElement {
@JSONField(ordinal = 11)
private String scriptLanguage;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
JSR223Sampler processor = new JSR223Sampler();
processor.setEnabled(true);
processor.setName(this.getName() + "JSR223Processor");
@ -38,7 +39,7 @@ public class MsJSR223Processor extends MsTestElement {
final HashTree jsr223PreTree = tree.add(processor);
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(jsr223PreTree, el.getHashTree());
el.toHashTree(jsr223PreTree, el.getHashTree(), config);
});
}
}

View File

@ -3,6 +3,7 @@ package io.metersphere.api.dto.definition.request.processors.post;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -26,7 +27,7 @@ public class MsJSR223PostProcessor extends MsTestElement {
private String scriptLanguage;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
JSR223PostProcessor processor = new JSR223PostProcessor();
processor.setEnabled(true);
processor.setName(this.getName() + "JSR223PostProcessor");
@ -39,7 +40,7 @@ public class MsJSR223PostProcessor extends MsTestElement {
final HashTree jsr223PostTree = tree.add(processor);
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(jsr223PostTree, el.getHashTree());
el.toHashTree(jsr223PostTree, el.getHashTree(), config);
});
}
}

View File

@ -3,6 +3,7 @@ package io.metersphere.api.dto.definition.request.processors.pre;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -25,7 +26,7 @@ public class MsJSR223PreProcessor extends MsTestElement {
@JSONField(ordinal = 11)
private String scriptLanguage;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
JSR223PreProcessor processor = new JSR223PreProcessor();
processor.setEnabled(true);
processor.setName(this.getName() + "JSR223PreProcessor");
@ -38,7 +39,7 @@ public class MsJSR223PreProcessor extends MsTestElement {
final HashTree jsr223PreTree = tree.add(processor);
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(jsr223PreTree, el.getHashTree());
el.toHashTree(jsr223PreTree, el.getHashTree(), config);
});
}
}

View File

@ -11,6 +11,7 @@ import io.metersphere.api.dto.definition.request.sampler.dubbo.MsConfigCenter;
import io.metersphere.api.dto.definition.request.sampler.dubbo.MsConsumerAndService;
import io.metersphere.api.dto.definition.request.sampler.dubbo.MsRegistryCenter;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -50,13 +51,13 @@ public class MsDubboSampler extends MsTestElement {
@JSONField(ordinal = 59)
private List<KeyValue> attachmentArgs;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
final HashTree testPlanTree = new ListedHashTree();
testPlanTree.add(dubboConfig());
tree.set(dubboSample(), testPlanTree);
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(testPlanTree, el.getHashTree());
el.toHashTree(testPlanTree, el.getHashTree(), config);
});
}
}

View File

@ -52,12 +52,12 @@ public class MsHTTPSamplerProxy extends MsTestElement {
@JSONField(ordinal = 15)
private String connectTimeout;
@JSONField(ordinal = 16)
private String responseTimeout;
@JSONField(ordinal = 17)
private List<KeyValue> arguments;
@JSONField(ordinal = 17)
private List<KeyValue> headers;
@JSONField(ordinal = 18)
private Body body;
@ -78,9 +78,9 @@ public class MsHTTPSamplerProxy extends MsTestElement {
private String useEnvironment;
@JSONField(ordinal = 24)
private List<KeyValue> headers;
private List<KeyValue> arguments;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
HTTPSamplerProxy sampler = new HTTPSamplerProxy();
sampler.setEnabled(true);
sampler.setName(this.getName());
@ -93,7 +93,6 @@ public class MsHTTPSamplerProxy extends MsTestElement {
sampler.setFollowRedirects(this.isFollowRedirects());
sampler.setUseKeepAlive(true);
sampler.setDoMultipart(this.isDoMultipartPost());
EnvironmentConfig config = null;
if (useEnvironment != null) {
ApiTestEnvironmentService environmentService = CommonBeanFactory.getBean(ApiTestEnvironmentService.class);
ApiTestEnvironmentWithBLOBs environment = environmentService.get(useEnvironment);
@ -145,16 +144,18 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
final HashTree httpSamplerTree = tree.add(sampler);
setHeader(httpSamplerTree);
if (CollectionUtils.isNotEmpty(this.headers)) {
setHeader(httpSamplerTree);
}
//判断是否要开启DNS
if (config != null && config.getCommonConfig() != null && config.getCommonConfig().isEnableHost()) {
MsDNSCacheManager.addEnvironmentVariables(httpSamplerTree, this.getName(), config);
MsDNSCacheManager.addEnvironmentDNS(httpSamplerTree, this.getName(), config);
}
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(httpSamplerTree, el.getHashTree());
});
for (MsTestElement el : hashTree) {
el.toHashTree(httpSamplerTree, el.getHashTree(), config);
}
}
}

View File

@ -5,6 +5,7 @@ import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.DatabaseConfig;
import io.metersphere.api.dto.scenario.KeyValue;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -38,13 +39,13 @@ public class MsJDBCSampler extends MsTestElement {
@JSONField(ordinal = 16)
private String environmentId;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
final HashTree samplerHashTree = tree.add(jdbcSampler());
tree.add(jdbcDataSource());
tree.add(arguments(this.getName() + " Variables", this.getVariables()));
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(samplerHashTree, el.getHashTree());
el.toHashTree(samplerHashTree, el.getHashTree(), config);
});
}
}

View File

@ -3,6 +3,7 @@ package io.metersphere.api.dto.definition.request.sampler;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -48,13 +49,13 @@ public class MsTCPSampler extends MsTestElement {
@JSONField(ordinal = 23)
private String request;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
final HashTree samplerHashTree = new ListedHashTree();
samplerHashTree.add(tcpConfig());
tree.set(tcpSampler(), samplerHashTree);
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(samplerHashTree, el.getHashTree());
el.toHashTree(samplerHashTree, el.getHashTree(), config);
});
}
}

View File

@ -3,6 +3,7 @@ package io.metersphere.api.dto.definition.request.timer;
import com.alibaba.fastjson.annotation.JSONField;
import com.alibaba.fastjson.annotation.JSONType;
import io.metersphere.api.dto.definition.request.MsTestElement;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
@ -25,11 +26,11 @@ public class MsConstantTimer extends MsTestElement {
@JSONField(ordinal = 12)
private String delay;
public void toHashTree(HashTree tree, List<MsTestElement> hashTree) {
public void toHashTree(HashTree tree, List<MsTestElement> hashTree, EnvironmentConfig config) {
final HashTree groupTree = tree.add(constantTimer());
if (CollectionUtils.isNotEmpty(hashTree)) {
hashTree.forEach(el -> {
el.toHashTree(groupTree, el.getHashTree());
el.toHashTree(groupTree, el.getHashTree(), config);
});
}
}

View File

@ -168,13 +168,9 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
apiDefinitionExecResultService.saveApiResult(testResult);
}
} else if (StringUtils.equals(this.runMode, ApiRunMode.SCENARIO.name())) {
// 调试操作不需要存储结果
if (StringUtils.isBlank(debugReportId)) {
apiScenarioReportService.addResult(testResult);
} else {
apiScenarioReportService.addResult(testResult);
//apiScenarioReportService.saveApiResult(testResult);
}
// 执行报告不需要存储由用户确认后在存储
testResult.setTestId(debugReportId);
apiScenarioReportService.complete(testResult);
} else {
apiTestService.changeStatus(testId, APITestStatus.Completed);
report = apiReportService.getRunningReport(testResult.getTestId());

View File

@ -1,26 +1,28 @@
package io.metersphere.api.service;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.google.gson.Gson;
import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.dto.automation.ApiScenarioDTO;
import io.metersphere.api.dto.automation.ApiScenarioRequest;
import io.metersphere.api.dto.automation.SaveApiScenarioRequest;
import io.metersphere.api.dto.automation.ScenarioStatus;
import io.metersphere.api.dto.definition.RunDefinitionRequest;
import io.metersphere.api.dto.scenario.environment.EnvironmentConfig;
import io.metersphere.api.jmeter.JMeterService;
import io.metersphere.base.domain.ApiScenario;
import io.metersphere.base.domain.ApiScenarioExample;
import io.metersphere.base.domain.ApiTag;
import io.metersphere.base.domain.ApiTagExample;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.ApiScenarioMapper;
import io.metersphere.base.mapper.ApiTagMapper;
import io.metersphere.base.mapper.ext.ExtApiScenarioMapper;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.constants.ApiRunMode;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.i18n.Translator;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jorphan.collections.HashTree;
import org.aspectj.util.FileUtil;
import org.springframework.stereotype.Service;
@ -32,6 +34,7 @@ import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.Collectors;
@Service
@ -45,6 +48,10 @@ public class ApiAutomationService {
private ApiTagMapper apiTagMapper;
@Resource
private JMeterService jMeterService;
@Resource
private ApiTestEnvironmentService environmentService;
@Resource
private ApiScenarioReportService apiReportService;
private static final String BODY_FILE_DIR = "/opt/metersphere/data/body";
@ -92,7 +99,11 @@ public class ApiAutomationService {
scenario.setScenarioDefinition(request.getScenarioDefinition());
scenario.setCreateTime(System.currentTimeMillis());
scenario.setUpdateTime(System.currentTimeMillis());
scenario.setStatus(ScenarioStatus.Saved.name());
if (StringUtils.isNotEmpty(request.getStatus())) {
scenario.setStatus(request.getStatus());
} else {
scenario.setStatus(ScenarioStatus.Underway.name());
}
if (request.getUserId() == null) {
scenario.setUserId(SessionUtils.getUserId());
} else {
@ -117,7 +128,11 @@ public class ApiAutomationService {
scenario.setStepTotal(request.getStepTotal());
scenario.setScenarioDefinition(request.getScenarioDefinition());
scenario.setUpdateTime(System.currentTimeMillis());
scenario.setStatus(ScenarioStatus.Saved.name());
if (StringUtils.isNotEmpty(request.getStatus())) {
scenario.setStatus(request.getStatus());
} else {
scenario.setStatus(ScenarioStatus.Underway.name());
}
scenario.setUserId(request.getUserId());
scenario.setDescription(request.getDescription());
apiScenarioMapper.updateByPrimaryKeySelective(scenario);
@ -143,8 +158,7 @@ public class ApiAutomationService {
private void checkNameExist(SaveApiScenarioRequest request) {
ApiScenarioExample example = new ApiScenarioExample();
example.createCriteria().andNameEqualTo(request.getName()).andProjectIdEqualTo(request.getProjectId())
.andApiScenarioModuleIdEqualTo(request.getApiScenarioModuleId()).andIdNotEqualTo(request.getId());
example.createCriteria().andNameEqualTo(request.getName()).andProjectIdEqualTo(request.getProjectId()).andIdNotEqualTo(request.getId());
if (apiScenarioMapper.countByExample(example) > 0) {
MSException.throwException(Translator.get("automation_name_already_exists"));
}
@ -204,10 +218,26 @@ public class ApiAutomationService {
public String run(RunDefinitionRequest request, List<MultipartFile> bodyFiles) {
List<String> bodyUploadIds = new ArrayList<>(request.getBodyUploadIds());
createBodyFiles(bodyUploadIds, bodyFiles);
HashTree hashTree = request.getTestElement().generateHashTree();
EnvironmentConfig config = null;
if (request.getEnvironmentId() != null) {
ApiTestEnvironmentWithBLOBs environment = environmentService.get(request.getEnvironmentId());
config = JSONObject.parseObject(environment.getConfig(), EnvironmentConfig.class);
}
HashTree hashTree = request.getTestElement().generateHashTree(config);
request.getTestElement().getJmx(hashTree);
// 调用执行方法
jMeterService.runDefinition(request.getId(), hashTree, request.getReportId(), ApiRunMode.SCENARIO.name());
APIReportResult report = new APIReportResult();
report.setId(UUID.randomUUID().toString());
report.setTestId(request.getReportId());
report.setName("RUN");
report.setTriggerMode(null);
report.setCreateTime(System.currentTimeMillis());
report.setUpdateTime(System.currentTimeMillis());
report.setStatus(APITestStatus.Running.name());
report.setUserId(SessionUtils.getUserId());
apiReportService.addResult(report);
return request.getId();
}

View File

@ -1,24 +1,67 @@
package io.metersphere.api.service;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.api.dto.APIReportResult;
import io.metersphere.api.jmeter.TestResult;
import io.metersphere.base.domain.ApiTestReportDetail;
import io.metersphere.commons.constants.APITestStatus;
import io.metersphere.commons.exception.MSException;
import io.metersphere.i18n.Translator;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import sun.security.util.Cache;
import java.nio.charset.StandardCharsets;
@Service
@Transactional(rollbackFor = Exception.class)
public class ApiScenarioReportService {
private static Cache cache = Cache.newHardMemoryCache(0, 3600 * 24);
public void addResult(TestResult res) {
if (!res.getScenarios().isEmpty()) {
cache.put(res.getTestId(), res);
} else {
MSException.throwException(Translator.get("test_not_found"));
public void complete(TestResult result) {
Object obj = cache.get(result.getTestId());
if (obj == null) {
MSException.throwException(Translator.get("api_report_is_null"));
}
APIReportResult report = (APIReportResult) obj;
// report detail
ApiTestReportDetail detail = new ApiTestReportDetail();
detail.setReportId(result.getTestId());
detail.setTestId(report.getTestId());
detail.setContent(JSONObject.toJSONString(result).getBytes(StandardCharsets.UTF_8));
// report
report.setUpdateTime(System.currentTimeMillis());
if (!StringUtils.equals(report.getStatus(), APITestStatus.Debug.name())) {
if (result.getError() > 0) {
report.setStatus(APITestStatus.Error.name());
} else {
report.setStatus(APITestStatus.Success.name());
}
}
report.setContent(new String(detail.getContent(), StandardCharsets.UTF_8));
cache.put(report.getTestId(), report);
}
public void addResult(APIReportResult res) {
cache.put(res.getTestId(), res);
}
/**
* 获取零时执行报告
*
* @param testId
*/
public APIReportResult getCacheResult(String testId) {
Object res = cache.get(testId);
if (res != null) {
APIReportResult reportResult = (APIReportResult) res;
if (!reportResult.getStatus().equals(APITestStatus.Running.name())) {
cache.remove(testId);
}
return reportResult;
}
return null;
}
}

View File

@ -21,7 +21,7 @@
:name="item.name"
closable>
<div class="ms-api-scenario-div">
<ms-edit-api-scenario :current-project="currentProject" :currentScenario="currentScenario" :moduleOptions="moduleOptions"/>
<ms-edit-api-scenario :current-project="currentProject" :currentScenario="item.currentScenario" :moduleOptions="moduleOptions"/>
</div>
</el-tab-pane>
@ -37,77 +37,82 @@
<script>
import MsContainer from "@/business/components/common/components/MsContainer";
import MsAsideContainer from "@/business/components/common/components/MsAsideContainer";
import MsMainContainer from "@/business/components/common/components/MsMainContainer";
import MsApiScenarioList from "@/business/components/api/automation/scenario/ApiScenarioList";
import {getUUID} from "@/common/js/utils";
import MsApiScenarioModule from "@/business/components/api/automation/scenario/ApiScenarioModule";
import MsEditApiScenario from "./scenario/EditApiScenario";
import MsContainer from "@/business/components/common/components/MsContainer";
import MsAsideContainer from "@/business/components/common/components/MsAsideContainer";
import MsMainContainer from "@/business/components/common/components/MsMainContainer";
import MsApiScenarioList from "@/business/components/api/automation/scenario/ApiScenarioList";
import {getUUID} from "@/common/js/utils";
import MsApiScenarioModule from "@/business/components/api/automation/scenario/ApiScenarioModule";
import MsEditApiScenario from "./scenario/EditApiScenario";
export default {
name: "ApiAutomation",
components: {MsApiScenarioModule, MsApiScenarioList, MsMainContainer, MsAsideContainer, MsContainer,MsEditApiScenario},
comments: {},
data() {
return {
isHide: true,
activeName: 'default',
currentProject: null,
currentModule: null,
currentScenario: {},
moduleOptions: {},
tabs: [],
}
},
watch: {},
methods: {
addTab(tab) {
if (tab.name === 'add') {
let label = this.$t('api_test.automation.add_scenario');
let name = getUUID().substring(0, 8);
this.tabs.push({label: label, name: name});
this.activeName = name;
export default {
name: "ApiAutomation",
components: {MsApiScenarioModule, MsApiScenarioList, MsMainContainer, MsAsideContainer, MsContainer, MsEditApiScenario},
comments: {},
data() {
return {
isHide: true,
activeName: 'default',
currentProject: null,
currentModule: null,
moduleOptions: {},
tabs: [],
}
},
removeTab(targetName) {
this.tabs = this.tabs.filter(tab => tab.name !== targetName);
if (this.tabs.length > 0) {
this.activeName = this.tabs[this.tabs.length - 1].name;
} else {
this.activeName = "default"
}
},
setTabLabel(data) {
for (const tab of this.tabs) {
if (tab.name === this.activeName) {
tab.label = data.name;
break;
watch: {},
methods: {
addTab(tab) {
if (tab.name === 'add') {
let label = this.$t('api_test.automation.add_scenario');
let name = getUUID().substring(0, 8);
this.activeName = name;
this.tabs.push({label: label, name: name, currentScenario: {}});
}
}
},
selectModule(data) {
this.currentModule = data;
},
saveScenario(data) {
this.setTabLabel(data);
this.$refs.apiScenarioList.search(data);
},
initTree(data) {
this.moduleOptions = data;
},
changeProject(data) {
this.currentProject = data;
},
refresh(data) {
this.$refs.apiScenarioList.search(data);
},
editScenario(row) {
this.currentScenario = row;
this.addTab({name: 'add'});
},
if (tab.name === 'edit') {
let label = this.$t('api_test.automation.add_scenario');
let name = getUUID().substring(0, 8);
this.activeName = name;
label = tab.currentScenario.name;
this.tabs.push({label: label, name: name, currentScenario: tab.currentScenario});
}
},
removeTab(targetName) {
this.tabs = this.tabs.filter(tab => tab.name !== targetName);
if (this.tabs.length > 0) {
this.activeName = this.tabs[this.tabs.length - 1].name;
} else {
this.activeName = "default"
}
},
setTabLabel(data) {
for (const tab of this.tabs) {
if (tab.name === this.activeName) {
tab.label = data.name;
break;
}
}
},
selectModule(data) {
this.currentModule = data;
},
saveScenario(data) {
this.setTabLabel(data);
this.$refs.apiScenarioList.search(data);
},
initTree(data) {
this.moduleOptions = data;
},
changeProject(data) {
this.currentProject = data;
},
refresh(data) {
this.$refs.apiScenarioList.search(data);
},
editScenario(row) {
this.addTab({name: 'edit', currentScenario: row});
},
}
}
}
</script>
<style scoped>

View File

@ -1,21 +0,0 @@
<template>
<div> ApiReport</div>
</template>
<script>
export default {
name: "ApiReport",
components: {},
comments: {},
data() {
return {}
},
watch: {},
methods: {}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,220 @@
<template>
<ms-container v-loading="loading">
<ms-main-container>
<el-card>
<section class="report-container" v-if="this.report.testId">
<ms-api-report-view-header :report="report" @reportExport="handleExport"/>
<main v-if="this.isNotRunning">
<ms-metric-chart :content="content" :totalTime="totalTime"/>
<div @click="active">
<ms-scenario-results :scenarios="content.scenarios" v-on:requestResult="requestResult"/>
</div>
<el-collapse-transition>
<div v-show="isActive" style="width: 99%">
<ms-request-result-tail v-if="isRequestResult" :request-type="requestType" :request="request"
:scenario-name="scenarioName"/>
</div>
</el-collapse-transition>
<ms-api-report-export v-if="reportExportVisible" id="apiTestReport" :title="report.testName"
:content="content" :total-time="totalTime"/>
</main>
</section>
</el-card>
</ms-main-container>
</ms-container>
</template>
<script>
import MsRequestResult from "./components/RequestResult";
import MsRequestResultTail from "./components/RequestResultTail";
import MsScenarioResult from "./components/ScenarioResult";
import MsMetricChart from "./components/MetricChart";
import MsScenarioResults from "./components/ScenarioResults";
import MsContainer from "@/business/components/common/components/MsContainer";
import MsMainContainer from "@/business/components/common/components/MsMainContainer";
import MsApiReportExport from "./ApiReportExport";
import MsApiReportViewHeader from "./ApiReportViewHeader";
import {RequestFactory} from "../../definition/model/ApiTestModel";
import {windowPrint} from "@/common/js/utils";
export default {
name: "MsApiReport",
components: {
MsApiReportViewHeader,
MsApiReportExport,
MsMainContainer,
MsContainer, MsScenarioResults, MsRequestResultTail, MsMetricChart, MsScenarioResult, MsRequestResult
},
data() {
return {
activeName: "total",
content: {},
report: {},
loading: true,
fails: [],
totalTime: 0,
isRequestResult: false,
request: {},
isActive: false,
scenarioName: null,
reportExportVisible: false,
requestType: undefined,
}
},
props: ['reportId'],
activated() {
this.isRequestResult = false;
},
watch: {
reportId() {
this.getReport();
}
},
methods: {
init() {
this.loading = true;
this.report = {};
this.content = {};
this.fails = [];
this.report = {};
this.isRequestResult = false;
},
handleClick(tab, event) {
this.isRequestResult = false
},
active() {
this.isActive = !this.isActive;
},
getReport() {
this.init();
if (this.reportId) {
let url = "/api/scenario/report/get/" + this.reportId;
this.$get(url, response => {
this.report = response.data || {};
if (response.data) {
if (this.isNotRunning) {
try {
this.content = JSON.parse(this.report.content);
} catch (e) {
// console.log(this.report.content)
throw e;
}
this.getFails();
this.loading = false;
} else {
setTimeout(this.getReport, 2000)
}
} else {
this.loading = false;
this.$error(this.$t('api_report.not_exist'));
}
});
}
},
getFails() {
if (this.isNotRunning) {
this.fails = [];
this.totalTime = 0
this.content.scenarios.forEach((scenario) => {
this.totalTime = this.totalTime + Number(scenario.responseTime)
let failScenario = Object.assign({}, scenario);
if (scenario.error > 0) {
this.fails.push(failScenario);
failScenario.requestResults = [];
scenario.requestResults.forEach((request) => {
if (!request.success) {
let failRequest = Object.assign({}, request);
failScenario.requestResults.push(failRequest);
}
})
}
})
}
},
requestResult(requestResult) {
this.isRequestResult = false;
this.requestType = undefined;
if (requestResult.request.body.indexOf('[Callable Statement]') > -1) {
this.requestType = RequestFactory.TYPES.SQL;
}
this.$nextTick(function () {
this.isRequestResult = true;
this.request = requestResult.request;
this.scenarioName = requestResult.scenarioName;
});
},
handleExport() {
this.reportExportVisible = true;
let reset = this.exportReportReset;
this.$nextTick(() => {
windowPrint('apiTestReport', 0.57);
reset();
});
},
exportReportReset() {
this.$router.go(0);
}
},
created() {
this.getReport();
},
computed: {
path() {
return "/api/test/edit?id=" + this.report.testId;
},
isNotRunning() {
return "Running" !== this.report.status;
}
}
}
</script>
<style>
.report-container .el-tabs__header {
margin-bottom: 1px;
}
</style>
<style scoped>
.report-container {
height: calc(100vh - 155px);
min-height: 600px;
overflow-y: auto;
}
.report-header {
font-size: 15px;
}
.report-header a {
text-decoration: none;
}
.report-header .time {
color: #909399;
margin-left: 10px;
}
.report-container .fail {
color: #F56C6C;
}
.report-container .is-active .fail {
color: inherit;
}
.export-button {
float: right;
}
.scenario-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<ms-report-export-template :title="title" :type="$t('report.api_test_report')">
<ms-metric-chart :content="content" :totalTime="totalTime"/>
<div class="scenario-result" v-for="(scenario, index) in content.scenarios" :key="index" :scenario="scenario">
<div>
<el-card>
<template v-slot:header>
{{$t('api_report.scenario_name')}}{{scenario.name}}
</template>
<div class="ms-border clearfix" v-for="(request, index) in scenario.requestResults" :key="index" :request="request">
<div class="request-top">
<div>
{{request.name}}
</div>
<div class="url">
{{request.url}}
</div>
</div>
<el-divider/>
<div class="request-bottom">
<api-report-reqest-header-item :title="$t('api_test.request.method')">
<span class="method"> {{request.method}}</span>
</api-report-reqest-header-item>
<api-report-reqest-header-item :title="$t('api_report.response_time')">
{{request.responseResult.responseTime}} ms
</api-report-reqest-header-item>
<api-report-reqest-header-item :title="$t('api_report.latency')">
{{request.responseResult.latency}} ms
</api-report-reqest-header-item>
<api-report-reqest-header-item :title="$t('api_report.request_size')">
{{request.requestSize}} bytes
</api-report-reqest-header-item>
<api-report-reqest-header-item :title="$t('api_report.response_size')">
{{request.responseResult.responseSize}} bytes
</api-report-reqest-header-item>
<api-report-reqest-header-item :title="$t('api_report.error')">
{{request.error}}
</api-report-reqest-header-item>
<api-report-reqest-header-item :title="$t('api_report.assertions')">
{{request.passAssertions + " / " + request.totalAssertions}}
</api-report-reqest-header-item>
<api-report-reqest-header-item :title="$t('api_report.response_code')">
{{request.responseResult.responseCode}}
</api-report-reqest-header-item>
<api-report-reqest-header-item :title="$t('api_report.result')">
<el-tag size="mini" type="success" v-if="request.success">
{{$t('api_report.success')}}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{$t('api_report.fail')}}
</el-tag>
</api-report-reqest-header-item>
</div>
</div>
</el-card>
</div>
</div>
</ms-report-export-template>
</template>
<script>
import MsScenarioResult from "./components/ScenarioResult";
import MsRequestResultTail from "./components/RequestResultTail";
import ApiReportReqestHeaderItem from "./ApiReportReqestHeaderItem";
import MsMetricChart from "./components/MetricChart";
import MsReportTitle from "../../../common/components/report/MsReportTitle";
import MsReportExportTemplate from "../../../common/components/report/MsReportExportTemplate";
export default {
name: "MsApiReportExport",
components: {
MsReportExportTemplate,
MsReportTitle, MsMetricChart, ApiReportReqestHeaderItem, MsRequestResultTail, MsScenarioResult
},
props: {
content: Object,
totalTime: Number,
title: String
},
data() {
return {}
},
}
</script>
<style scoped>
.scenario-result {
margin-top: 20px;
margin-bottom: 20px;
}
.method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
}
.request-top, .request-bottom {
margin-left: 20px;
}
.url {
color: #409EFF;
font-size: 14px;
font-weight: bold;
font-style: italic;
}
.el-card {
border-style: none;
padding: 10px 30px;
}
.request-top div {
margin-top: 10px;
}
</style>

View File

@ -0,0 +1,31 @@
<template>
<div class="item">
<div class="item-title">
{{title}}
</div>
<div>
<slot></slot>
</div>
</div>
</template>
<script>
export default {
name: "ApiReportReqestHeaderItem",
props: {title: String}
}
</script>
<style scoped>
.item {
width: 120px;
height: 50px;
display: inline-block;
}
.item-title {
margin-bottom: 20px;
}
</style>

View File

@ -0,0 +1,41 @@
<template>
<div>
<el-tag size="mini" type="info" v-if="row.status === 'Starting'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="primary" effect="plain" v-else-if="row.status === 'Running'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="success" v-else-if="row.status === 'Success'">
{{ row.status }}
</el-tag>
<el-tag size="mini" type="warning" v-else-if="row.status === 'Reporting'">
{{ row.status }}
</el-tag>
<el-tooltip placement="top" v-else-if="row.status === 'Error'" effect="light">
<template v-slot:content>
<div>{{row.description}}</div>
</template>
<el-tag size="mini" type="danger">
{{ row.status }}
</el-tag>
</el-tooltip>
<el-tag v-else size="mini" type="info">
{{ row.status }}
</el-tag>
</div>
</template>
<script>
export default {
name: "MsApiReportStatus",
props: {
row: Object
}
}
</script>
<style scoped>
</style>

View File

@ -0,0 +1,52 @@
<template>
<header class="report-header">
<el-row>
<el-col>
<span>{{ report.projectName === null || report.projectName ==='' ? "场景执行报告": report.projectName}} / </span>
<router-link :to="path">{{ report.testName }}</router-link>
<span class="time"> {{ report.createTime | timestampFormatDate }}</span>
<el-button :disabled="isReadOnly" class="export-button" plain type="primary" size="mini" @click="handleExport(report.name)"
style="margin-left: 1200px">
{{$t('test_track.plan_view.export_report')}}
</el-button>
</el-col>
</el-row>
</header>
</template>
<script>
import {checkoutTestManagerOrTestUser} from "@/common/js/utils";
export default {
name: "MsApiReportViewHeader",
props: ['report'],
computed: {
path() {
return "/api/test/edit?id=" + this.report.testId;
},
},
data() {
return {
isReadOnly: false,
}
},
created() {
if (!checkoutTestManagerOrTestUser()) {
this.isReadOnly = true;
}
},
methods: {
handleExport(name) {
this.$emit('reportExport', name);
}
}
}
</script>
<style scoped>
.export-button {
float: right;
}
</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,255 @@
<template>
<div class="metric-container">
<el-row type="flex" align="middle">
<div style="width: 50%">
<el-row type="flex" justify="center" align="middle">
<el-row>
<div class="metric-time">
<div class="value" style="margin-right: 50px">{{ time }}</div>
</div>
</el-row>
<ms-chart id="chart" ref="chart" :options="options" :autoresize="true"></ms-chart>
<el-row type="flex" justify="center" align="middle">
<i class="circle success"/>
<div class="metric-box">
<div class="value">{{ content.success }}</div>
<div class="name">{{ $t('api_report.success') }}</div>
</div>
<div style="width: 40px"></div>
<i class="circle fail"/>
<div class="metric-box">
<div class="value">{{ content.error }}</div>
<div class="name">{{ $t('api_report.fail') }}</div>
</div>
</el-row>
</el-row>
</div>
<div class="split"></div>
<div style="width: 50%">
<el-row type="flex" justify="space-around" align="middle">
<div class="metric-icon-box">
<i class="el-icon-warning-outline fail"></i>
<div class="value">{{ fail }}</div>
<div class="name">{{ $t('api_report.fail') }}</div>
</div>
<div class="metric-icon-box">
<i class="el-icon-document-checked assertions"></i>
<div class="value">{{ assertions }}</div>
<div class="name">{{ $t('api_report.assertions_pass') }}</div>
</div>
<div class="metric-icon-box">
<i class="el-icon-document-copy total"></i>
<div class="value">{{ this.content.total }}</div>
<div class="name">{{ $t('api_report.request') }}</div>
</div>
</el-row>
</div>
</el-row>
</div>
</template>
<script>
import MsChart from "@/business/components/common/chart/MsChart";
export default {
name: "MsMetricChart",
components: {MsChart},
props: {
content: Object,
totalTime: Number
},
data() {
return {
hour: 0,
minutes: 0,
seconds: 0,
time: 0,
}
},
created() {
this.initTime()
},
methods: {
initTime() {
this.time = this.totalTime
this.seconds = Math.floor(this.time / 1000)
if (this.seconds >= 1) {
if (this.seconds > 60) {
this.minutes = Math.round(this.time / 60)
this.seconds = Math.round(this.time % 60)
this.time = this.minutes + "min" + this.seconds + "s"
}
if (this.seconds > 60) {
this.minutes = Math.round(this.time / 60)
this.seconds = Math.round(this.time % 60)
this.time = this.minutes + "min" + this.seconds + "s"
}
if (this.minutes > 60) {
this.hour = Math.round(this.minutes / 60)
this.minutes = Math.round(this.minutes % 60)
this.time = this.hour + "hour" + this.minutes + "min" + this.seconds + "s"
}
this.time = (this.seconds) + "s"
} else {
this.time = this.totalTime + "ms"
}
},
},
computed: {
options() {
return {
color: ['#67C23A', '#F56C6C'],
tooltip: {
trigger: 'item',
formatter: '{b}: {c} ({d}%)'
},
title: [{
text: this.content.total,
subtext: this.$t('api_report.request'),
top: 'center',
left: 'center',
textStyle: {
rich: {
align: 'center',
value: {
fontSize: 32,
fontWeight: 'bold',
padding: [10, 0]
},
name: {
fontSize: 14,
fontWeight: 'normal',
color: '#7F7F7F',
}
}
}
}],
series: [
{
type: 'pie',
radius: ['80%', '90%'],
avoidLabelOverlap: false,
hoverAnimation: false,
itemStyle: {
normal: {
borderColor: "#FFF",
shadowColor: '#E1E1E1',
shadowBlur: 10
}
},
labelLine: {
show: false
},
data: [
{value: this.content.success},
{value: this.content.error},
]
}
]
};
},
fail() {
return (this.content.error / this.content.total * 100).toFixed(0) + "%";
},
assertions() {
return this.content.passAssertions + " / " + this.content.totalAssertions;
}
},
}
</script>
<style scoped>
.metric-container {
padding: 20px;
}
.metric-container #chart {
width: 140px;
height: 140px;
margin-right: 40px;
}
.metric-container .split {
margin: 20px;
height: 100px;
border-left: 1px solid #D8DBE1;
}
.metric-container .circle {
width: 12px;
height: 12px;
border-radius: 50%;
box-shadow: 0 0 20px 1px rgba(200, 216, 226, .42);
display: inline-block;
margin-right: 10px;
vertical-align: middle;
}
.metric-container .circle.success {
background-color: #67C23A;
}
.metric-container .circle.fail {
background-color: #F56C6C;
}
.metric-box {
display: inline-block;
text-align: center;
}
.metric-box .value {
font-size: 32px;
font-weight: 600;
letter-spacing: -.5px;
}
.metric-time .value {
font-size: 25px;
font-weight: 400;
letter-spacing: -.5px;
}
.metric-box .name {
font-size: 16px;
letter-spacing: -.2px;
color: #404040;
}
.metric-icon-box {
text-align: center;
margin: 0 10px;
}
.metric-icon-box .value {
font-size: 20px;
font-weight: 600;
letter-spacing: -.4px;
line-height: 28px;
vertical-align: middle;
}
.metric-icon-box .name {
font-size: 13px;
letter-spacing: 1px;
color: #BFBFBF;
line-height: 18px;
}
.metric-icon-box .fail {
color: #F56C6C;
font-size: 40px;
}
.metric-icon-box .assertions {
font-size: 40px;
}
.metric-icon-box .total {
font-size: 40px;
}
</style>

View File

@ -0,0 +1,106 @@
<template>
<div class="metric-container">
<el-row type="flex">
<div class="metric">
<div class="value">{{request.responseResult.responseTime}} ms</div>
<div class="name">{{$t('api_report.response_time')}}</div>
<br>
<div class="value">{{request.responseResult.latency}} ms</div>
<div class="name">{{$t('api_report.latency')}}</div>
</div>
<div class="metric">
<div class="value">{{request.requestSize}} bytes</div>
<div class="name">{{$t('api_report.request_size')}}</div>
<br>
<div class="value">{{request.responseResult.responseSize}} bytes</div>
<div class="name">{{$t('api_report.response_size')}}</div>
</div>
<div class="metric horizontal">
<el-row type="flex">
<div class="code">
<div class="value" :class="{'error': error}">{{request.responseResult.responseCode}}</div>
<div class="name">{{$t('api_report.response_code')}}</div>
</div>
<div class="split"></div>
<div class="message">
<div class="value">{{request.responseResult.responseMessage}}</div>
<div class="name">{{$t('api_report.response_message')}}</div>
</div>
</el-row>
</div>
</el-row>
</div>
</template>
<script>
export default {
name: "MsRequestMetric",
props: {
request: Object
},
computed: {
error() {
return this.request.responseResult.responseCode >= 400;
}
}
}
</script>
<style scoped>
.metric-container {
padding: 20px;
}
.metric {
padding: 20px;
border: 1px solid #EBEEF5;
min-width: 120px;
height: 114px;
}
.metric + .metric {
margin-left: 20px;
}
.metric .value {
font-size: 16px;
font-weight: 500;
word-break: break-all;
}
.metric .name {
color: #404040;
opacity: 0.5;
padding: 5px 0;
}
.metric.horizontal {
width: 100%;
}
.metric .code {
min-width: 120px;
}
.metric .code .value {
color: #67C23A;
}
.metric .code .value.error {
color: #F56C6C;
}
.metric .split {
height: 114px;
border-left: 1px solid #EBEEF5;
margin-right: 20px;
}
.metric .message {
max-height: 114px;
overflow-y: auto;
}
</style>

View File

@ -0,0 +1,144 @@
<template>
<div class="request-result">
<p class="el-divider--horizontal"></p>
<div @click="active">
<el-row :gutter="10" type="flex" align="middle" class="info">
<el-col :span="10" v-if="indexNumber!=undefined">
<div class="method">
<div class="el-step__icon is-text ms-api-col" v-if="indexNumber%2 ==0">
<div class="el-step__icon-inner"> {{ indexNumber+1 }}</div>
</div>
<div class="el-step__icon is-text ms-api-col-create" v-else>
<div class="el-step__icon-inner"> {{ indexNumber+1 }}</div>
</div>
{{ request.name }}
</div>
</el-col>
<el-col :span="4">
<div class="method">
{{ request.method }}
</div>
</el-col>
<el-col :span="5">
<el-tooltip effect="dark" :content="request.responseResult.responseCode" placement="bottom" :open-delay="800">
<div class="url">{{ request.responseResult.responseCode }}</div>
</el-tooltip>
</el-col>
<el-col :span="3">
{{request.responseResult.responseTime}} ms
</el-col>
<el-col :span="2">
<div class="success">
<el-tag size="mini" type="success" v-if="request.success">
{{ $t('api_report.success') }}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{ $t('api_report.fail') }}
</el-tag>
</div>
</el-col>
</el-row>
</div>
</div>
</template>
<script>
import MsRequestMetric from "./RequestMetric";
import MsAssertionResults from "./AssertionResults";
import MsRequestText from "./RequestText";
import MsResponseText from "./ResponseText";
export default {
name: "MsRequestResult",
components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric},
props: {
request: Object,
scenarioName: String,
indexNumber: Number,
},
data() {
return {}
},
methods: {
active() {
this.$emit("requestResult", {request: this.request, scenarioName: this.scenarioName});
}
},
}
</script>
<style scoped>
.request-result {
width: 100%;
min-height: 40px;
padding: 2px 0;
}
.request-result .info {
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
line-height: 40px;
padding-left: 5px;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #FFF;
}
.sub-result .method {
border-left: 5px solid #1E90FF;
padding-left: 20px;
}
.sub-result:last-child {
border-bottom: 1px solid #EBEEF5;
}
.ms-api-col {
background-color: #FCF1F1;
border-color: #67C23A;
margin-right: 10px;
color: #67C23A;
}
.ms-api-col-create {
background-color: #EBF2F2;
border-color: #008080;
margin-right: 10px;
color: #008080;
}
.el-divider--horizontal {
margin: 2px 0;
background: 0 0;
border-top: 1px solid #e8eaec;
}
</style>

View File

@ -0,0 +1,195 @@
<template>
<div class="request-result">
<div @click="active">
<el-row :gutter="10" type="flex" align="middle" class="info">
<el-col :span="12">
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{scenarioName}}
</el-col>
<el-col :span="4">
{{$t('api_report.start_time')}}
</el-col>
<el-col :span="2">
{{$t('api_report.response_time')}}
</el-col>
<el-col :span="2">
{{$t('api_report.error')}}
</el-col>
<el-col :span="2">
{{$t('api_report.assertions')}}
</el-col>
<el-col :span="2">
{{$t('api_report.result')}}
</el-col>
</el-row>
<el-row :gutter="10" type="flex" align="middle" class="info">
<el-col :span="2">
<div class="method">
{{request.method}}
</div>
</el-col>
<el-col :span="10">
<div class="name">{{request.name}}</div>
<el-tooltip effect="dark" :content="request.url" placement="bottom" :open-delay="800">
<div class="url">{{request.url}}</div>
</el-tooltip>
</el-col>
<el-col :span="4">
{{request.startTime | timestampFormatDate(true) }}
</el-col>
<el-col :span="2">
<div class="time">
{{request.responseResult.responseTime}}
</div>
</el-col>
<el-col :span="2">
{{request.error}}
</el-col>
<el-col :span="2">
{{assertion}}
</el-col>
<el-col :span="2">
<el-tag size="mini" type="success" v-if="request.success">
{{$t('api_report.success')}}
</el-tag>
<el-tag size="mini" type="danger" v-else>
{{$t('api_report.fail')}}
</el-tag>
</el-col>
</el-row>
</div>
<el-collapse-transition>
<div v-show="isActive">
<el-tabs v-model="activeName" v-show="isActive" v-if="hasSub">
<el-tab-pane :label="$t('api_report.sub_result')" name="sub">
<ms-request-result class="sub-result" v-for="(sub, index) in request.subRequestResults"
:key="index" :request="sub"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_report.request_result')" name="result">
<ms-request-metric :request="request"/>
<ms-request-text :request="request"/>
<br>
<ms-response-text :request-type="requestType" :response="request.responseResult"/>
</el-tab-pane>
</el-tabs>
<div v-else>
<ms-request-metric :request="request"/>
<ms-request-text v-if="isCodeEditAlive" :request="request"/>
<br>
<ms-response-text :request-type="requestType" v-if="isCodeEditAlive" :response="request.responseResult"/>
</div>
</div>
</el-collapse-transition>
</div>
</template>
<script>
import MsRequestMetric from "./RequestMetric";
import MsAssertionResults from "./AssertionResults";
import MsRequestText from "./RequestText";
import MsResponseText from "./ResponseText";
import MsRequestResult from "./RequestResult";
export default {
name: "MsRequestResultTail",
components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric, MsRequestResult},
props: {
request: Object,
scenarioName: String,
requestType: String
},
data() {
return {
isActive: true,
activeName: "sub",
isCodeEditAlive: true
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
reload() {
this.isCodeEditAlive = false;
this.$nextTick(() => (this.isCodeEditAlive = true));
}
},
watch: {
'request.responseResult'() {
this.reload();
}
},
computed: {
assertion() {
return this.request.passAssertions + " / " + this.request.totalAssertions;
},
hasSub() {
return this.request.subRequestResults.length > 0;
},
}
}
</script>
<style scoped>
.request-result {
width: 100%;
min-height: 40px;
padding: 2px 0;
}
.request-result .info {
background-color: #F9F9F9;
margin-left: 20px;
cursor: pointer;
}
.request-result .method {
color: #1E90FF;
font-size: 14px;
font-weight: 500;
line-height: 40px;
padding-left: 5px;
}
.request-result .url {
color: #7f7f7f;
font-size: 12px;
font-weight: 400;
margin-top: 4px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
word-break: break-all;
}
.request-result .tab .el-tabs__header {
margin: 0;
}
.request-result .text {
height: 300px;
overflow-y: auto;
}
.sub-result .info {
background-color: #FFF;
}
.sub-result .method {
border-left: 5px solid #1E90FF;
padding-left: 20px;
}
.sub-result:last-child {
border-bottom: 1px solid #EBEEF5;
}
.request-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,73 @@
<template>
<div class="text-container">
<div @click="active" class="collapse">
<i class="icon el-icon-arrow-right" :class="{'is-active': isActive}"/>
{{$t('api_report.request')}}
</div>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane label="Body" name="body" class="pane">
<pre>{{request.body}}</pre>
</el-tab-pane>
<el-tab-pane label="Headers" name="headers" class="pane">
<pre>{{request.headers}}</pre>
</el-tab-pane>
<el-tab-pane label="Cookies" name="cookies" class="pane">
<pre>{{request.cookies}}</pre>
</el-tab-pane>
</el-tabs>
</el-collapse-transition>
</div>
</template>
<script>
export default {
name: "MsRequestText",
props: {
request: Object
},
data() {
return {
isActive: true,
activeName: "body",
}
},
methods: {
active() {
this.isActive = !this.isActive;
}
},
}
</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: #F9F9F9;
padding: 10px;
height: 250px;
overflow-y: auto;
}
pre {
margin: 0;
}
</style>

View File

@ -0,0 +1,135 @@
<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 :class="'body-pane'" label="Body" name="body" class="pane">
<ms-sql-result-table v-if="isSqlType" :body="response.body"/>
<ms-code-edit v-if="!isSqlType" :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 v-if="!isSqlType" :commands="modes" :default-command="mode" @command="modeChange"/>
<ms-dropdown v-if="isSqlType" :commands="sqlModes" :default-command="mode" @command="sqlModeChange"/>
</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, RequestFactory, Request, SqlRequest} from "../../../definition/model/ApiTestModel";
import MsSqlResultTable from "./SqlResultTable";
export default {
name: "MsResponseText",
components: {
MsSqlResultTable,
MsDropdown,
MsCodeEdit,
MsAssertionResults,
},
props: {
requestType: String,
response: Object
},
data() {
return {
isActive: true,
activeName: "body",
modes: ['text', 'json', 'xml', 'html'],
sqlModes: ['text', 'table'],
mode: BODY_FORMAT.TEXT
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
modeChange(mode) {
this.mode = mode;
},
sqlModeChange(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;
}
},
computed: {
isSqlType() {
return (this.requestType === RequestFactory.TYPES.SQL && this.response.responseCode === '200');
}
}
}
</script>
<style scoped>
.body-pane {
padding: 10px !important;
background: white !important;
}
.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,71 @@
<template>
<div class="scenario-result">
<div v-for="(request, index) in scenario.requestResults" :key="index">
<ms-request-result :key="index" :request="request" :indexNumber="index"
v-on:requestResult="requestResult"
:scenarioName="scenario.name"/>
</div>
</div>
</template>
<script>
import MsRequestResult from "./RequestResult";
export default {
name: "MsScenarioResult",
components: {MsRequestResult},
props: {
scenario: Object,
},
data() {
return {
isActive: false,
}
},
methods: {
active() {
this.isActive = !this.isActive;
},
requestResult(requestResult) {
this.$emit("requestResult", requestResult);
}
},
computed: {
assertion() {
return this.scenario.passAssertions + " / " + this.scenario.totalAssertions;
},
success() {
return this.scenario.error === 0;
}
}
}
</script>
<style scoped>
.scenario-result {
width: 100%;
padding: 2px 0;
}
.scenario-result + .scenario-result {
border-top: 1px solid #DCDFE6;
}
.scenario-result .info {
height: 40px;
cursor: pointer;
}
.scenario-result .icon {
padding: 5px;
}
.scenario-result .icon.is-active {
transform: rotate(90deg);
}
</style>

View File

@ -0,0 +1,35 @@
<template>
<el-card class="scenario-results">
<ms-scenario-result v-for="(scenario, index) in scenarios" :key="index" :scenario="scenario" :indexNumber="index"
v-on:requestResult="requestResult"/>
</el-card>
</template>
<script>
import MsScenarioResult from "./ScenarioResult";
export default {
name: "MsScenarioResults",
components: {MsScenarioResult},
props: {
scenarios: Array
},
methods: {
requestResult(requestResult) {
this.$emit("requestResult", requestResult);
}
}
}
</script>
<style scoped>
.scenario-header {
border: 1px solid #EBEEF5;
background-color: #F9FCFF;
border-left: 0;
border-right: 0;
padding: 5px 0;
}
</style>

View File

@ -0,0 +1,121 @@
<template>
<div>
<el-table
v-for="(table, index) in tables"
:key="index"
:data="table.tableData"
border
size="mini"
highlight-current-row>
<el-table-column v-for="(title, index) in table.titles" :key="index" :label="title" min-width="150px">
<template v-slot:default="scope">
<el-popover
placement="top"
trigger="click">
<el-container>
<div>{{ scope.row[title] }}</div>
</el-container>
<span class="table-content" slot="reference">{{ scope.row[title] }}</span>
</el-popover>
</template>
</el-table-column>
</el-table>
</div>
</template>
<script>
export default {
name: "MsSqlResultTable",
data() {
return {
tables: [],
titles: []
}
},
props: {
body: String
},
created() {
if (!this.body) {
return;
}
let rowArry = this.body.split("\n");
this.getTableData(rowArry);
if (this.tables.length > 1) {
for (let i = 0; i < this.tables.length; i++) {
if (this.tables[i].titles.length === 1 && i < this.tables.length - 1) {
this.tables[i].tableData.splice(this.tables[i].tableData.length - 1, 1);
}
}
let lastTable = this.tables[this.tables.length - 1];
if (lastTable.titles.length === 1) {
if (lastTable.tableData.length > 4) {
lastTable.tableData.splice(lastTable.tableData.length - 4, 4);
} else {
this.tables.splice(this.tables.length - 1, 1);
}
} else {
this.tables.splice(this.tables.length - 1, 1);
}
} else {
let table = this.tables[0];
table.tableData.splice(table.tableData.length - 4, 4);
}
},
methods: {
getTableData(rowArry) {
let titles;
let result = [];
for (let i = 0; i < rowArry.length; i++) {
let colArray = rowArry[i].split("\t");
if (i === 0) {
titles = colArray;
} else {
if (colArray.length != titles.length) {
//
if (colArray.length === 1 && colArray[0] === '') {
this.getTableData(rowArry.slice(i + 1));
} else {
this.getTableData(rowArry.slice(i));
}
break;
} else {
let item = {};
for (let j = 0; j < colArray.length; j++) {
item[titles[j]] = (colArray[j] ? colArray[j] : "");
}
result.push(item);
}
}
}
this.tables.splice(0, 0, {
titles: titles,
tableData: result
});
}
}
}
</script>
<style scoped>
.el-table {
margin-bottom: 20px;
}
.el-table >>> .cell {
white-space: nowrap;
}
.table-content {
cursor: pointer;
}
.el-container {
overflow:auto;
max-height: 500px;
}
</style>

View File

@ -64,6 +64,7 @@
import MsTablePagination from "@/business/components/common/pagination/TablePagination";
import ShowMoreBtn from "@/business/components/track/case/components/ShowMoreBtn";
import MsTag from "../../../common/components/MsTag";
import {getUUID} from "@/common/js/utils";
export default {
name: "MsApiScenarioList",
@ -101,7 +102,7 @@
},
methods: {
search() {
this.condition.filters = ["Saved", "Success", "Fail"];
this.condition.filters = ["Prepare", "Underway", "Completed"];
if (this.currentModule != null) {
if (this.currentModule.id === "root") {
this.condition.moduleIds = [];
@ -158,10 +159,11 @@
},
copy(row) {
row.id = getUUID();
this.$emit('edit', row);
},
remove(row) {
if (this.currentModule !== undefined && this.currentModule.id === "gc") {
if (this.currentModule !== undefined && this.currentModule != null && this.currentModule.id === "gc") {
this.$get('/api/automation/delete/' + row.id, () => {
this.$success(this.$t('commons.delete_success'));
this.search();

View File

@ -132,9 +132,9 @@
</el-col>
<el-col :span="8">
{{$t('api_test.definition.request.run_env')}}:
<el-select v-model="currentScenario.environmentId" size="small" class="ms-htt-width"
<el-select v-model="currentEnvironmentId" size="small" class="ms-htt-width"
:placeholder="$t('api_test.definition.request.run_env')"
@change="environmentChange" clearable>
clearable>
<el-option v-for="(environment, index) in environments" :key="index"
:label="environment.name + (environment.config.httpConfig.socket ? (': ' + environment.config.httpConfig.protocol + '://' + environment.config.httpConfig.socket) : '')"
:value="environment.id"/>
@ -237,8 +237,8 @@
<!--接口列表-->
<el-drawer :visible.sync="apiListVisible" :destroy-on-close="true" direction="ltr" :withHeader="false" :title="$t('api_test.automation.api_list_import')" :modal="false" size="90%">
<ms-api-definition :visible="true" :currentRow="currentRow"/>
<el-button style="float: right;margin: 20px" type="primary" @click="copyApi('REF')">{{$t('api_test.scenario.reference')}}</el-button>
<el-button style="float: right;margin: 20px 0px 0px " @click="copyApi('Copy')">{{ $t('commons.copy') }}</el-button>
<!--<el-button style="float: right;margin: 20px" type="primary" @click="copyApi('REF')">{{$t('api_test.scenario.reference')}}</el-button>-->
<el-button style="float: right;margin: 20px 0px 0px " type="primary" @click="copyApi('Copy')">{{ $t('commons.copy') }}</el-button>
</el-drawer>
<!--自定义接口-->
@ -257,8 +257,12 @@
<!--TAG-->
<ms-add-tag @refreshTags="refreshTags" ref="tag"/>
<!--执行组件-->
<ms-run :debug="true" :environment="currentEnvironment" :reportId="reportId" :run-data="scenarioDefinition"
<ms-run :debug="true" :environment="currentEnvironmentId" :reportId="reportId" :run-data="debugData"
@runRefresh="runRefresh" ref="runTest"/>
<!-- 调试结果 -->
<el-drawer :visible.sync="debugVisible" :destroy-on-close="true" direction="ltr" :withHeader="false" :title="$t('test_track.plan_view.test_result')" :modal="false" size="90%">
<ms-api-report-detail :report-id="reportId"/>
</el-drawer>
</div>
</el-card>
</template>
@ -283,6 +287,8 @@
import MsRun from "./Run";
import MsImportApiScenario from "./ImportApiScenario";
import MsApiScenarioComponent from "./ApiScenarioComponent";
import MsApiReportDetail from "../report/ApiReportDetail";
export default {
name: "EditApiScenario",
@ -291,7 +297,7 @@
currentProject: {},
currentScenario: {},
},
components: {ApiEnvironmentConfig, MsAddTag, MsRun, MsApiScenarioComponent, MsImportApiScenario, MsJsr233Processor, MsConstantTimer, MsIfController, MsApiAssertions, MsApiExtract, MsApiDefinition, MsApiComponent, MsApiCustomize},
components: {ApiEnvironmentConfig, MsApiReportDetail, MsAddTag, MsRun, MsApiScenarioComponent, MsImportApiScenario, MsJsr233Processor, MsConstantTimer, MsIfController, MsApiAssertions, MsApiExtract, MsApiDefinition, MsApiComponent, MsApiCustomize},
data() {
return {
props: {
@ -309,7 +315,7 @@
},
environments: [],
tags: [],
currentEnvironment: {},
currentEnvironmentId: "",
maintainerOptions: [],
value: API_STATUS[0].id,
options: API_STATUS,
@ -319,6 +325,7 @@
apiListVisible: false,
customizeVisible: false,
scenarioVisible: false,
debugVisible: false,
customizeRequest: {protocol: "HTTP", type: "API", hashTree: [], referenced: 'Created', active: false},
operatingElements: [],
currentRow: {cases: [], apis: []},
@ -326,6 +333,7 @@
expandedNode: [],
scenarioDefinition: [],
path: "/api/automation/create",
debugData: [],
reportId: "",
}
},
@ -527,8 +535,14 @@
},
runDebug() {
/*触发执行操作*/
if (!this.currentEnvironmentId) {
this.$error(this.$t('api_test.environment.select_environment'));
return;
}
let scenario = {id: this.currentScenario.id, name: this.currentScenario.name, type: "scenario", referenced: 'Created', environmentId: this.currentEnvironmentId, hashTree: this.scenarioDefinition};
this.debugData = [];
this.debugData.push(scenario);
this.reportId = getUUID().substring(0, 8);
//this.isReloadData = true;
},
getEnvironments() {
if (this.currentProject) {
@ -547,14 +561,6 @@
}
this.$refs.environmentConfig.open(this.currentProject.id);
},
environmentChange(value) {
for (let i in this.environments) {
if (this.environments[i].id === value) {
this.currentEnvironment = this.environments[i];
break;
}
}
},
environmentConfigClose() {
this.getEnvironments();
},
@ -599,14 +605,18 @@
})
},
getApiScenario() {
if (this.currentScenario.tagId != undefined) {
if (this.currentScenario.tagId != undefined && !(this.currentScenario.tagId instanceof Array)) {
this.currentScenario.tagId = JSON.parse(this.currentScenario.tagId);
}
if (this.currentScenario.id) {
this.path = "/api/automation/update";
this.result = this.$get("/api/automation/getApiScenario/" + this.currentScenario.id, response => {
if (response.data) {
this.scenarioDefinition = JSON.parse(response.data.scenarioDefinition);
this.path = "/api/automation/update";
if (response.data.scenarioDefinition != null) {
let obj = JSON.parse(response.data.scenarioDefinition);
this.currentEnvironmentId = obj.environmentId;
this.scenarioDefinition = obj.hashTree;
}
}
})
}
@ -614,11 +624,13 @@
setParameter() {
this.currentScenario.projectId = this.currentProject.id;
if (!this.currentScenario.id) {
this.currentScenario.id = getUUID().substring(0, 8);
this.currentScenario.id = getUUID();
}
this.currentScenario.stepTotal = this.scenarioDefinition.length;
this.currentScenario.modulePath = this.getPath(this.currentScenario.apiScenarioModuleId);
this.currentScenario.scenarioDefinition = JSON.stringify(this.scenarioDefinition);
// 便
let scenario = {id: this.currentScenario.id, name: this.currentScenario.name, type: "scenario", referenced: 'Created', environmentId: this.currentEnvironmentId, hashTree: this.scenarioDefinition};
this.currentScenario.scenarioDefinition = JSON.stringify(scenario);
this.currentScenario.tagId = JSON.stringify(this.currentScenario.tagId);
if (this.currentModule != null) {
this.currentScenario.modulePath = this.currentModule.method !== undefined ? this.currentModule.method : null;
@ -626,6 +638,7 @@
}
},
runRefresh() {
this.debugVisible = true;
this.isReloadData = false;
}
}

View File

@ -56,6 +56,7 @@
importApiScenario() {
let scenarios = [];
if (this.currentScenario) {
console.log(this.currentScenario)
this.currentScenario.forEach(item => {
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'REF', resourceId: getUUID()};
scenarios.push(obj);

View File

@ -10,7 +10,7 @@
name: 'MsRun',
components: {},
props: {
environment: Object,
environment: String,
debug: Boolean,
reportId: String,
runData: Array,
@ -96,26 +96,21 @@
},
run() {
let testPlan = new TestPlan();
let threadGroup = new ThreadGroup();
threadGroup.hashTree = [];
testPlan.hashTree = [threadGroup];
this.runData.forEach(item => {
let threadGroup = new ThreadGroup();
threadGroup.hashTree = [];
threadGroup.name = item.name;
threadGroup.hashTree.push(item);
testPlan.hashTree.push(threadGroup);
})
let reqObj = {id: this.reportId, testElement: testPlan};
console.log("====",testPlan)
let reqObj = {id: this.reportId, reportId: this.reportId, environmentId: this.environment, testElement: testPlan};
let bodyFiles = this.getBodyUploadFiles(reqObj);
let url = "";
if (this.debug) {
url = "/api/automation/run/debug";
} else {
reqObj.reportId = "run";
url = "/api/definition/run";
}
let url = "/api/automation/run";
this.$fileUpload(url, null, bodyFiles, reqObj, response => {
this.runId = response.data;
this.getResult();
}, erro => {
this.$emit('runRefresh', {});
}, erro => {
});
}
}

View File

@ -18,7 +18,7 @@ export default class TestPlan extends HashTreeElement {
this.serializeThreadGroups = this.initBoolProp('TestPlan.serialize_threadgroups', false);
this.tearDownOnShutdown = this.initBoolProp('TestPlan.tearDown_on_shutdown', true);
this.userDefineClasspath = this.initStringProp('TestPlan.user_define_classpath');
this.hashTree = [];
this.userDefinedVariables = [];
let elementProp = this.initElementProp('TestPlan.user_defined_variables', 'Arguments');