Merge remote-tracking branch 'origin/v1.4' into v1.4

This commit is contained in:
wenyann 2020-10-28 18:54:37 +08:00
commit 753b9da5c1
59 changed files with 2968 additions and 1681 deletions

View File

@ -363,6 +363,13 @@
<version>4.5.6</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-text</artifactId>
<version>1.3</version>
</dependency>
</dependencies>
<build>

View File

@ -14,6 +14,7 @@ import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.controller.request.QueryScheduleRequest;
import io.metersphere.dto.ScheduleDao;
import io.metersphere.service.CheckOwnerService;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.web.bind.annotation.*;
@ -21,8 +22,12 @@ import org.springframework.web.multipart.MultipartFile;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import static io.metersphere.commons.utils.JsonPathUtils.getListJson;
@RestController
@RequestMapping(value = "/api")
@RequiresRoles(value = {RoleConstants.TEST_MANAGER, RoleConstants.TEST_USER, RoleConstants.TEST_VIEWER}, logical = Logical.OR)
@ -79,7 +84,6 @@ public class APITestController {
public void mergeCreate(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "file") MultipartFile file, @RequestPart(value = "selectIds") List<String> selectIds) {
apiTestService.mergeCreate(request, file, selectIds);
}
@PostMapping(value = "/update", consumes = {"multipart/form-data"})
public void update(@RequestPart("request") SaveAPITestRequest request, @RequestPart(value = "file") MultipartFile file, @RequestPart(value = "files") List<MultipartFile> bodyFiles) {
checkownerService.checkApiTestOwner(request.getId());
@ -141,4 +145,9 @@ public class APITestController {
public List<ScheduleDao> listSchedule(@RequestBody QueryScheduleRequest request) {
return apiTestService.listSchedule(request);
}
@PostMapping("/getJsonPaths")
public List<HashMap> getJsonPaths(@RequestBody QueryJsonPathRequest request) {
return getListJson(request.getJsonPath());
}
}

View File

@ -0,0 +1,12 @@
package io.metersphere.api.dto;
import java.io.Serializable;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class QueryJsonPathRequest implements Serializable {
private String jsonPath;
}

View File

@ -0,0 +1,20 @@
package io.metersphere.api.dto.scenario.assertions;
import lombok.Data;
import lombok.EqualsAndHashCode;
@EqualsAndHashCode(callSuper = true)
@Data
public class AssertionJSR223 extends AssertionType {
private String variable;
private String operator;
private String value;
private String desc;
private String name;
private String script;
private String language;
public AssertionJSR223() {
setType(AssertionType.JSR223);
}
}

View File

@ -7,6 +7,7 @@ public class AssertionType {
public final static String REGEX = "Regex";
public final static String DURATION = "Duration";
public final static String JSON_PATH = "JSONPath";
public final static String JSR223 = "JSR223";
public final static String TEXT = "Text";
private String type;

View File

@ -8,5 +8,6 @@ import java.util.List;
public class Assertions {
private List<AssertionRegex> regex;
private List<AssertionJsonPath> jsonPath;
private List<AssertionJSR223> jsr223;
private AssertionDuration duration;
}

View File

@ -0,0 +1,17 @@
package io.metersphere.base.mapper.ext;
import io.metersphere.track.dto.TestCaseCommentDTO;
import org.springframework.data.repository.query.Param;
import java.util.List;
public interface ExtTestCaseCommentMapper {
/**
* 获取用例的评论
* @param caseId
* @return
*/
List<TestCaseCommentDTO> getCaseComments(@Param("caseId") String caseId);
}

View File

@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="io.metersphere.base.mapper.ext.ExtTestCaseCommentMapper">
<select id="getCaseComments" resultType="io.metersphere.track.dto.TestCaseCommentDTO"
parameterType="java.lang.String">
select *, user.name as authorName from test_case_comment, user
where test_case_comment.author = user.id and case_id = #{caseId}
order by test_case_comment.update_time desc
</select>
</mapper>

View File

@ -6,11 +6,11 @@
parameterType="io.metersphere.track.request.testreview.QueryCaseReviewRequest">
select distinct test_case_review.id, test_case_review.name, test_case_review.creator, test_case_review.status,
test_case_review.create_time, test_case_review.update_time, test_case_review.end_time,
test_case_review.description
from test_case_review, project, test_case_review_project
test_case_review.description, user.name as creatorName
from test_case_review join test_case_review_project on test_case_review.id = test_case_review_project.review_id
join project on test_case_review_project.project_id = project.id
left join user on test_case_review.creator = user.id
<where>
test_case_review.id = test_case_review_project.review_id
and test_case_review_project.project_id = project.id
<if test="request.name != null">
and test_case_review.name like CONCAT('%', #{request.name},'%')
</if>

View File

@ -94,6 +94,12 @@
<property name="object" value="${condition}.creator"/>
</include>
</if>
<if test="${condition}.executor != null">
and test_plan_test_case.executor
<include refid="condition">
<property name="object" value="${condition}.executor"/>
</include>
</if>
</sql>
<select id="getReportMetric" parameterType="java.lang.String"

View File

@ -1,6 +1,6 @@
package io.metersphere.commons.constants;
public enum ReportKeys {
LoadChart, ResponseTimeChart, Errors, ErrorsTop5, RequestStatistics, Overview, TimeInfo, ResultStatus
LoadChart, ResponseTimeChart, ResponseCodeChart, Errors, ErrorsTop5, RequestStatistics, Overview, TimeInfo, ResultStatus, ErrorsChart
}

View File

@ -0,0 +1,126 @@
package io.metersphere.commons.utils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.JSONPath;
public class JsonPathUtils {
public static List<HashMap> getListJson(String jsonString) {
JSONObject jsonObject = JSONObject.parseObject(jsonString);
List<HashMap> allJsons =new ArrayList<>();
// 获取到所有jsonpath后获取所有的key
List<String> jsonPaths = JSONPath.paths(jsonObject).keySet()
.stream()
.collect(Collectors.toList());
//去掉根节点key
List<String> parentNode = new ArrayList<>();
//根节点key
parentNode.add("/");
//循环获取父节点key只保留叶子节点
for (int i = 0; i < jsonPaths.size(); i++) {
if (jsonPaths.get(i).lastIndexOf("/") > 0) {
parentNode.add(jsonPaths.get(i).substring(0, jsonPaths.get(i).lastIndexOf("/")));
}
}
//remove父节点key
for (String parentNodeJsonPath : parentNode) {
jsonPaths.remove(parentNodeJsonPath);
}
Iterator<String> jsonPath = jsonPaths.iterator();
///替换为点.
while (jsonPath.hasNext()) {
Map<String,String> item = new HashMap<>();
String o_json_path = "$" + jsonPath.next().replaceAll("/", ".");
String value = JSONPath.eval(jsonObject, o_json_path).toString();
if(o_json_path.toLowerCase().contains("id")) {
continue;
}
if(value.equals("") || value.equals("[]") || o_json_path.equals("")) {
continue;
}
String json_path = formatJson(o_json_path);
item.put("json_path", json_path);
item.put("json_value", addEscapeForString(value));
allJsons.add((HashMap)item);
}
Collections.sort(allJsons, (a, b) ->
( (String)a.get("json_path") )
.compareTo( (String)b.get("json_path") )
);
return allJsons;
}
private static String formatJson(String json_path){
String ret="";
String reg = ".(\\d{1,3}).{0,1}";
Boolean change_flag = false;
Matcher m1 = Pattern.compile(reg).matcher(json_path);
String newStr="";
int rest = 0;
String tail = "";
while (m1.find()) {
int start = m1.start();
int end = m1.end() - 1;
if(json_path.charAt(start) != '.' || json_path.charAt(end) != '.') {
continue;
}
newStr += json_path.substring(rest,m1.start()) +"[" + json_path.substring(start + 1, end) + "]." ;
rest = m1.end();
tail = json_path.substring(m1.end());
change_flag = true;
}
if(change_flag) {
ret = newStr + tail;
} else {
ret = json_path;
}
return ret;
}
private static String addEscapeForString(String input) {
String ret="";
String reg = "[?*/]";
Boolean change_flag = false;
Matcher m1 = Pattern.compile(reg).matcher(input);
String newStr="";
int rest = 0;
String tail = "";
while (m1.find()) {
newStr += input.substring(rest,m1.start()) + "\\" + m1.group(0) ;
rest = m1.end();
tail = input.substring(m1.end());
change_flag = true;
}
if(change_flag) {
ret = newStr + tail;
} else {
ret = input;
}
return ret;
}
}

View File

@ -0,0 +1,84 @@
package io.metersphere.ldap.service;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class CustomSSLSocketFactory extends SSLSocketFactory {
private SSLSocketFactory socketFactory;
public CustomSSLSocketFactory() {
try {
SSLContext ctx = SSLContext.getInstance("TLS");
ctx.init(null, new TrustManager[]{new DummyTrustmanager()}, new SecureRandom());
socketFactory = ctx.getSocketFactory();
} catch (Exception ex) {
ex.printStackTrace(System.err);
}
}
public static SocketFactory getDefault() {
return new CustomSSLSocketFactory();
}
@Override
public String[] getDefaultCipherSuites() {
return socketFactory.getDefaultCipherSuites();
}
@Override
public String[] getSupportedCipherSuites() {
return socketFactory.getSupportedCipherSuites();
}
@Override
public Socket createSocket(Socket socket, String string, int num, boolean bool) throws IOException {
return socketFactory.createSocket(socket, string, num, bool);
}
@Override
public Socket createSocket(String string, int num) throws IOException, UnknownHostException {
return socketFactory.createSocket(string, num);
}
@Override
public Socket createSocket(String string, int num, InetAddress netAdd, int i) throws IOException, UnknownHostException {
return socketFactory.createSocket(string, num, netAdd, i);
}
@Override
public Socket createSocket(InetAddress netAdd, int num) throws IOException {
return socketFactory.createSocket(netAdd, num);
}
@Override
public Socket createSocket(InetAddress netAdd1, int num, InetAddress netAdd2, int i) throws IOException {
return socketFactory.createSocket(netAdd1, num, netAdd2, i);
}
/**
* 证书
*/
public static class DummyTrustmanager implements X509TrustManager {
public void checkClientTrusted(X509Certificate[] cert, String string) throws CertificateException {
}
public void checkServerTrusted(X509Certificate[] cert, String string) throws CertificateException {
}
public X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
}
}

View File

@ -146,8 +146,17 @@ public class LdapService {
preConnect(url, dn, password);
String credentials = EncryptUtils.aesDecrypt(password).toString();
LdapContextSource sourceLdapCtx = new LdapContextSource();
LdapContextSource sourceLdapCtx;
if (StringUtils.startsWith(url, "ldaps://")) {
sourceLdapCtx = new SSLLdapContextSource();
// todo 这里加上strategy 会报错
// DefaultTlsDirContextAuthenticationStrategy strategy = new DefaultTlsDirContextAuthenticationStrategy();
// strategy.setShutdownTlsGracefully(true);
// strategy.setHostnameVerifier((hostname, session) -> true);
// sourceLdapCtx.setAuthenticationStrategy(strategy);
} else {
sourceLdapCtx = new LdapContextSource();
}
sourceLdapCtx.setUrl(url);
sourceLdapCtx.setUserDn(dn);
sourceLdapCtx.setPassword(credentials);

View File

@ -0,0 +1,16 @@
package io.metersphere.ldap.service;
import org.springframework.ldap.core.support.LdapContextSource;
import javax.naming.Context;
import java.util.Hashtable;
public class SSLLdapContextSource extends LdapContextSource {
public Hashtable<String, Object> getAnonymousEnv() {
Hashtable<String, Object> anonymousEnv = super.getAnonymousEnv();
anonymousEnv.put("java.naming.security.protocol", "ssl");
anonymousEnv.put("java.naming.ldap.factory.socket", CustomSSLSocketFactory.class.getName());
anonymousEnv.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
return anonymousEnv;
}
}

View File

@ -95,6 +95,16 @@ public class PerformanceReportController {
return reportService.getResponseTimeChartData(reportId);
}
@GetMapping("/content/error_chart/{reportId}")
public List<ChartsData> getErrorChartData(@PathVariable String reportId) {
return reportService.getErrorChartData(reportId);
}
@GetMapping("/content/response_code_chart/{reportId}")
public List<ChartsData> getResponseCodeChartData(@PathVariable String reportId) {
return reportService.getResponseCodeChartData(reportId);
}
@GetMapping("/{reportId}")
public LoadTestReportWithBLOBs getLoadTestReport(@PathVariable String reportId) {
return reportService.getLoadTestReport(reportId);

View File

@ -256,4 +256,16 @@ public class ReportService {
List<String> ids = reportRequest.getIds();
ids.forEach(this::deleteReport);
}
public List<ChartsData> getErrorChartData(String id) {
checkReportStatus(id);
String content = getContent(id, ReportKeys.ErrorsChart);
return JSON.parseArray(content, ChartsData.class);
}
public List<ChartsData> getResponseCodeChartData(String id) {
checkReportStatus(id);
String content = getContent(id, ReportKeys.ResponseCodeChart);
return JSON.parseArray(content, ChartsData.class);
}
}

View File

@ -1,8 +1,5 @@
package io.metersphere.service;
import static io.metersphere.commons.constants.ResourceStatusEnum.INVALID;
import static io.metersphere.commons.constants.ResourceStatusEnum.VALID;
import com.alibaba.fastjson.JSON;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.LoadTestMapper;
@ -25,6 +22,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import javax.annotation.Resource;
import java.lang.reflect.InvocationTargetException;
import java.util.ArrayList;
import java.util.List;
@ -32,7 +30,8 @@ import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;
import javax.annotation.Resource;
import static io.metersphere.commons.constants.ResourceStatusEnum.INVALID;
import static io.metersphere.commons.constants.ResourceStatusEnum.VALID;
/**
* @author dongbin
@ -210,7 +209,10 @@ public class TestResourcePoolService {
private void updateTestResource(TestResource testResource) {
testResource.setUpdateTime(System.currentTimeMillis());
testResource.setCreateTime(System.currentTimeMillis());
if (StringUtils.isBlank(testResource.getId())) {
testResource.setId(UUID.randomUUID().toString());
}
// 如果是更新操作插入与原来的ID相同的数据
testResourceMapper.insertSelective(testResource);
}

View File

@ -1,6 +1,6 @@
package io.metersphere.track.controller;
import io.metersphere.base.domain.TestCaseComment;
import io.metersphere.track.dto.TestCaseCommentDTO;
import io.metersphere.track.request.testreview.SaveCommentRequest;
import io.metersphere.track.service.TestCaseCommentService;
import org.springframework.web.bind.annotation.*;
@ -13,7 +13,7 @@ import java.util.List;
public class TestCaseCommentController {
@Resource
TestCaseCommentService testCaseCommentService;
private TestCaseCommentService testCaseCommentService;
@PostMapping("/save")
public void saveComment(@RequestBody SaveCommentRequest request) {
@ -21,7 +21,17 @@ public class TestCaseCommentController {
}
@GetMapping("/list/{caseId}")
public List<TestCaseComment> getComments(@PathVariable String caseId) {
return testCaseCommentService.getComments(caseId);
public List<TestCaseCommentDTO> getCaseComments(@PathVariable String caseId) {
return testCaseCommentService.getCaseComments(caseId);
}
@GetMapping("/delete/{commentId}")
public void deleteComment(@PathVariable String commentId) {
testCaseCommentService.delete(commentId);
}
@PostMapping("/edit")
public void editComment(@RequestBody SaveCommentRequest request) {
testCaseCommentService.edit(request);
}
}

View File

@ -106,7 +106,7 @@ public class TestCaseReviewController {
}
@PostMapping("/get/{reviewId}")
@GetMapping("/get/{reviewId}")
public TestCaseReview getTestReview(@PathVariable String reviewId) {
checkOwnerService.checkTestReviewOwner(reviewId);
return testCaseReviewService.getTestReview(reviewId);

View File

@ -0,0 +1,9 @@
package io.metersphere.track.dto;
import io.metersphere.base.domain.TestCaseComment;
import lombok.Data;
@Data
public class TestCaseCommentDTO extends TestCaseComment {
private String authorName;
}

View File

@ -10,4 +10,5 @@ public class TestCaseReviewDTO extends TestCaseReview {
private String projectName;
private String reviewerName;
private String creatorName;
}

View File

@ -3,21 +3,23 @@ package io.metersphere.track.service;
import io.metersphere.base.domain.TestCaseComment;
import io.metersphere.base.domain.TestCaseCommentExample;
import io.metersphere.base.domain.TestCaseWithBLOBs;
import io.metersphere.base.domain.User;
import io.metersphere.base.mapper.TestCaseCommentMapper;
import io.metersphere.base.mapper.TestCaseMapper;
import io.metersphere.base.mapper.TestCaseReviewMapper;
import io.metersphere.base.mapper.UserMapper;
import io.metersphere.base.mapper.ext.ExtTestCaseCommentMapper;
import io.metersphere.commons.constants.NoticeConstants;
import io.metersphere.commons.exception.MSException;
import io.metersphere.commons.utils.LogUtil;
import io.metersphere.commons.utils.SessionUtils;
import io.metersphere.i18n.Translator;
import io.metersphere.notice.domain.MessageDetail;
import io.metersphere.notice.domain.MessageSettingDetail;
import io.metersphere.notice.service.DingTaskService;
import io.metersphere.notice.service.MailService;
import io.metersphere.notice.service.NoticeService;
import io.metersphere.notice.service.WxChatTaskService;
import io.metersphere.track.dto.TestCaseCommentDTO;
import io.metersphere.track.request.testreview.SaveCommentRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@ -33,21 +35,19 @@ import java.util.UUID;
public class TestCaseCommentService {
@Resource
TestCaseCommentMapper testCaseCommentMapper;
private TestCaseCommentMapper testCaseCommentMapper;
@Resource
private TestCaseReviewMapper testCaseReviewMapper;
private MailService mailService;
@Resource
UserMapper userMapper;
private TestCaseMapper testCaseMapper;
@Resource
MailService mailService;
private DingTaskService dingTaskService;
@Resource
TestCaseMapper testCaseMapper;
private WxChatTaskService wxChatTaskService;
@Resource
DingTaskService dingTaskService;
private NoticeService noticeService;
@Resource
WxChatTaskService wxChatTaskService;
@Resource
NoticeService noticeService;
private ExtTestCaseCommentMapper extTestCaseCommentMapper;
public void saveComment(SaveCommentRequest request) {
@ -86,21 +86,11 @@ public class TestCaseCommentService {
}
public List<TestCaseComment> getComments(String caseId) {
TestCaseCommentExample testCaseCommentExample = new TestCaseCommentExample();
testCaseCommentExample.setOrderByClause("update_time desc");
testCaseCommentExample.createCriteria().andCaseIdEqualTo(caseId);
List<TestCaseComment> testCaseComments = testCaseCommentMapper.selectByExampleWithBLOBs(testCaseCommentExample);
testCaseComments.forEach(testCaseComment -> {
String authorId = testCaseComment.getAuthor();
User user = userMapper.selectByPrimaryKey(authorId);
String author = user == null ? authorId : user.getName();
testCaseComment.setAuthor(author);
});
return testCaseComments;
public List<TestCaseCommentDTO> getCaseComments(String caseId) {
return extTestCaseCommentMapper.getCaseComments(caseId);
}
public void deleteComment(String caseId) {
public void deleteCaseComment(String caseId) {
TestCaseCommentExample testCaseCommentExample = new TestCaseCommentExample();
testCaseCommentExample.createCriteria().andCaseIdEqualTo(caseId);
testCaseCommentMapper.deleteByExample(testCaseCommentExample);
@ -118,4 +108,22 @@ public class TestCaseCommentService {
context = "测试评审任务通知:" + testCaseComment.getAuthor() + "" + start + "" + "'" + testCaseWithBLOBs.getName() + "'" + "添加评论:" + testCaseComment.getDescription();
return context;
}
public void delete(String commentId) {
checkCommentOwner(commentId);
testCaseCommentMapper.deleteByPrimaryKey(commentId);
}
public void edit(SaveCommentRequest request) {
checkCommentOwner(request.getId());
testCaseCommentMapper.updateByPrimaryKeySelective(request);
}
private void checkCommentOwner(String commentId) {
TestCaseComment comment = testCaseCommentMapper.selectByPrimaryKey(commentId);
if (!StringUtils.equals(comment.getAuthor(), SessionUtils.getUser().getId())) {
MSException.throwException(Translator.get("check_owner_comment"));
}
}
}

View File

@ -170,7 +170,7 @@ public class TestCaseService {
example.createCriteria().andCaseIdEqualTo(testCaseId);
testPlanTestCaseMapper.deleteByExample(example);
testCaseIssueService.delTestCaseIssues(testCaseId);
testCaseCommentService.deleteComment(testCaseId);
testCaseCommentService.deleteCaseComment(testCaseId);
return testCaseMapper.deleteByPrimaryKey(testCaseId);
}
@ -458,7 +458,7 @@ public class TestCaseService {
String steps = t.getSteps();
String setp = "";
if (steps.contains("null")) {
setp = steps.replace("null", "");
setp = steps.replace("null", "\"\"");
} else {
setp = steps;
}

View File

@ -0,0 +1,2 @@
ALTER TABLE `test_plan_test_case` ADD INDEX index_name ( `case_id` );
ALTER TABLE `test_case_review_test_case` ADD INDEX index_name ( `case_id` );

View File

@ -163,6 +163,7 @@ check_owner_test=The current user does not have permission to operate this test
check_owner_case=The current user does not have permission to operate this use case
check_owner_plan=The current user does not have permission to operate this plan
check_owner_review=The current user does not have permission to operate this review
check_owner_comment=The current user does not have permission to manipulate this comment
upload_content_is_null=Imported content is empty
test_plan_notification=Test plan notification
task_defect_notification=Task defect notification

View File

@ -164,6 +164,7 @@ check_owner_test=当前用户没有操作此测试的权限
check_owner_case=当前用户没有操作此用例的权限
check_owner_plan=当前用户没有操作此计划的权限
check_owner_review=当前用户没有操作此评审的权限
check_owner_comment=当前用户没有操作此评论的权限
upload_content_is_null=导入内容为空
test_plan_notification=测试计划通知
task_defect_notification=缺陷任务通知

View File

@ -165,6 +165,7 @@ check_owner_test=當前用戶沒有操作此測試的權限
check_owner_case=當前用戶沒有操作此用例的權限
check_owner_plan=當前用戶沒有操作此計劃的權限
check_owner_review=當前用戶沒有操作此評審的權限
check_owner_comment=當前用戶沒有操作此評論的權限
upload_content_is_null=導入內容為空
test_plan_notification=測試計畫通知
task_defect_notification=缺陷任務通知

View File

@ -23,7 +23,7 @@
</el-tabs>
</el-col>
<el-col :span="16" style="margin-top: 40px;">
<ms-request-result-tail v-if="isRequestResult" :request="request" :scenario-name="scenarioName"/>
<ms-request-result-tail v-if="isRequestResult" :request-type="requestType" :request="request" :scenario-name="scenarioName"/>
</el-col>
</el-row>
<ms-api-report-export v-if="reportExportVisible" id="apiTestReport" :title="report.testName" :content="content" :total-time="totalTime"/>
@ -47,6 +47,7 @@ import MsApiReportExport from "./ApiReportExport";
import {exportPdf} from "../../../../common/js/utils";
import html2canvas from "html2canvas";
import MsApiReportViewHeader from "./ApiReportViewHeader";
import {RequestFactory} from "../test/model/ScenarioModel";
export default {
name: "MsApiReportViewDetail",
@ -67,7 +68,8 @@ export default {
isRequestResult: false,
request: {},
scenarioName: null,
reportExportVisible: false
reportExportVisible: false,
requestType: undefined,
}
},
props: ['reportId'],
@ -140,6 +142,10 @@ export default {
},
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;

View File

@ -69,14 +69,14 @@
<ms-request-metric :request="request"/>
<ms-request-text :request="request"/>
<br>
<ms-response-text :response="request.responseResult"/>
<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 v-if="isCodeEditAlive" :response="request.responseResult"/>
<ms-response-text :request-type="requestType" v-if="isCodeEditAlive" :response="request.responseResult"/>
</div>
</div>
</el-collapse-transition>
@ -95,7 +95,8 @@
components: {MsResponseText, MsRequestText, MsAssertionResults, MsRequestMetric, MsRequestResult},
props: {
request: Object,
scenarioName: String
scenarioName: String,
requestType: String
},
data() {

View File

@ -6,8 +6,9 @@
</div>
<el-collapse-transition>
<el-tabs v-model="activeName" v-show="isActive">
<el-tab-pane label="Body" name="body" class="pane">
<ms-code-edit :mode="mode" :read-only="true" :data="response.body" :modes="modes" ref="codeEdit"/>
<el-tab-pane :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>
@ -20,7 +21,7 @@
<pre>{{response.vars}}</pre>
</el-tab-pane>
<el-tab-pane v-if="activeName == 'body'" :disabled="true" name="mode" class="pane assertions">
<el-tab-pane v-if="activeName == 'body' && !isSqlType" :disabled="true" name="mode" class="pane assertions">
<template v-slot:label>
<ms-dropdown :commands="modes" :default-command="mode" @command="modeChange"/>
</template>
@ -35,18 +36,21 @@
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "../../../common/components/MsCodeEdit";
import MsDropdown from "../../../common/components/MsDropdown";
import {BODY_FORMAT} from "../../test/model/ScenarioModel";
import {BODY_FORMAT, RequestFactory, Request, SqlRequest} from "../../test/model/ScenarioModel";
import MsSqlResultTable from "./SqlResultTable";
export default {
name: "MsResponseText",
components: {
MsSqlResultTable,
MsDropdown,
MsCodeEdit,
MsAssertionResults,
},
props: {
requestType: String,
response: Object
},
@ -75,39 +79,52 @@ export default {
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>
.text-container .icon {
.body-pane {
padding: 10px !important;
background: white !important;
}
.text-container .icon {
padding: 5px;
}
}
.text-container .collapse {
.text-container .collapse {
cursor: pointer;
}
}
.text-container .collapse:hover {
.text-container .collapse:hover {
opacity: 0.8;
}
}
.text-container .icon.is-active {
.text-container .icon.is-active {
transform: rotate(90deg);
}
}
.text-container .pane {
.text-container .pane {
background-color: #F5F5F5;
padding: 0 10px;
height: 250px;
overflow-y: auto;
}
}
.text-container .pane.assertions {
.text-container .pane.assertions {
padding: 0;
}
}
pre {
pre {
margin: 0;
}
}
</style>

View File

@ -0,0 +1,75 @@
<template>
<el-table
:data="tableData"
border
size="mini"
highlight-current-row>
<el-table-column v-for="(title, index) in titles" :key="index" :label="title" min-width="15%">
<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>
</template>
<script>
export default {
name: "MsSqlResultTable",
data() {
return {
tableData: [],
titles: []
}
},
props: {
body: String
},
created() {
if (!this.body) {
return;
}
let rowArry = this.body.split("\n");
let title;
let result = [];
for (let i = 0; i < rowArry.length; i++) {
let colArray = rowArry[i].split("\t");
if (i === 0) {
title = colArray;
} else {
let item = {};
for (let j = 0; j < colArray.length; j++) {
item[title[j]] = (colArray[j] ? colArray[j] : "");
}
result.push(item);
}
}
this.titles = title;
this.tableData = result;
this.tableData.splice(this.tableData.length - 3, 3);
}
}
</script>
<style scoped>
.el-table >>> .cell {
white-space: nowrap;
}
.table-content {
cursor: pointer;
}
.el-container {
overflow:auto;
max-height: 500px;
}
</style>

View File

@ -7,7 +7,9 @@
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>Add</el-button>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>
{{ $t('api_test.request.assertions.add') }}
</el-button>
</el-col>
</el-row>
</div>

View File

@ -11,7 +11,9 @@
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>Add</el-button>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>
{{ $t('api_test.request.assertions.add') }}
</el-button>
</el-col>
</el-row>
</div>

View File

@ -0,0 +1,261 @@
<template>
<div>
<el-row type="flex" align="middle" v-if="!edit">
<div class="assertion-item label">
{{ assertion.desc }}
</div>
<div class="assertion-item btn">
<el-button :disabled="isReadOnly" type="success" size="small" @click="detail">
{{ $t('commons.edit') }}
</el-button>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add">
{{ $t('api_test.request.assertions.add') }}
</el-button>
</div>
</el-row>
<el-row type="flex" justify="space-between" align="middle" v-else>
<div class="assertion-item label">
{{ assertion.desc }}
</div>
<div class="assertion-item btn circle">
<el-button :disabled="isReadOnly" type="success" size="mini" icon="el-icon-edit" circle @click="detail"/>
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove"/>
</div>
</el-row>
<el-dialog :title="$t('api_test.request.assertions.script')" :visible.sync="visible" width="900px">
<el-row type="flex" justify="space-between" align="middle" class="quick-script-block">
<div class="assertion-item input">
<el-input size="small" v-model="assertion.variable"
:placeholder="$t('api_test.request.assertions.variable_name')" @change="quickScript"/>
</div>
<div class="assertion-item select">
<el-select v-model="assertion.operator" :placeholder="$t('commons.please_select')" size="small"
@change="changeOperator">
<el-option v-for="o in operators" :key="o.value" :label="$t(o.label)" :value="o.value"/>
</el-select>
</div>
<div class="assertion-item input">
<el-input size="small" v-model="assertion.value" :placeholder="$t('api_test.value')"
@change="quickScript" v-if="!hasEmptyOperator"/>
</div>
</el-row>
<el-input size="small" v-model="assertion.desc" :placeholder="$t('api_test.request.assertions.script_name')"
class="quick-script-block"/>
<ms-jsr233-processor ref="jsr233" :is-read-only="isReadOnly" :jsr223-processor="assertion" :templates="templates"
:height="300"/>
<template v-slot:footer v-if="!edit">
<ms-dialog-footer
@cancel="close"
@confirm="confirm"/>
</template>
</el-dialog>
</div>
</template>
<script>
import {AssertionJSR223} from "../../model/ScenarioModel";
import MsJsr233Processor from "@/business/components/api/test/components/processor/Jsr233Processor";
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
export default {
name: "MsApiAssertionJsr223",
components: {MsDialogFooter, MsJsr233Processor},
props: {
assertion: {
type: AssertionJSR223,
default: () => {
return new AssertionJSR223();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array,
callback: Function,
isReadOnly: {
type: Boolean,
default: false
}
},
data() {
return {
visible: false,
operators: {
EQ: {
label: "commons.adv_search.operators.equals",
value: "=="
},
NE: {
label: "commons.adv_search.operators.not_equals",
value: "!="
},
CONTAINS: {
label: "commons.adv_search.operators.like",
value: "contains"
},
NOT_CONTAINS: {
label: "commons.adv_search.operators.not_like",
value: "not contains"
},
GT: {
label: "commons.adv_search.operators.gt",
value: ">"
},
LT: {
label: "commons.adv_search.operators.lt",
value: "<"
},
IS_EMPTY: {
label: "commons.adv_search.operators.is_empty",
value: "is empty"
},
IS_NOT_EMPTY: {
label: "commons.adv_search.operators.is_not_empty",
value: "is not empty"
}
},
templates: [
{
title: this.$t('api_test.request.assertions.set_failure_status'),
value: 'AssertionResult.setFailure(true)',
},
{
title: this.$t('api_test.request.assertions.set_failure_msg'),
value: 'AssertionResult.setFailureMessage("msg")',
}
],
}
},
methods: {
add() {
this.list.push(new AssertionJSR223(this.assertion));
this.callback();
},
remove() {
this.list.splice(this.index, 1);
},
changeOperator(value) {
if (value.indexOf("empty") > 0 && !!this.assertion.value) {
this.assertion.value = "";
}
this.quickScript();
},
quickScript() {
if (this.assertion.variable && this.assertion.operator) {
let variable = this.assertion.variable;
let operator = this.assertion.operator;
let value = this.assertion.value || "";
let desc = "${" + variable + "} " + operator + " '" + value + "'";
let script = "value = vars.get(\"" + variable + "\");\n"
switch (this.assertion.operator) {
case "==":
script += "result = \"" + value + "\".equals(value);\n";
break;
case "!=":
script += "result = !\"" + value + "\".equals(value);\n";
break;
case "contains":
script += "result = value.contains(\"" + value + "\");\n";
break;
case "not contains":
script += "result = !value.contains(\"" + value + "\");\n";
break;
case ">":
desc = "${" + variable + "} " + operator + " " + value;
script += "number = Integer.parseInt(value);\n" +
"result = number > " + value + ";\n";
break;
case "<":
desc = "${" + variable + "} " + operator + " " + value;
script += "number = Integer.parseInt(value);\n" +
"result = number < " + value + ";\n";
break;
case "is empty":
desc = "${" + variable + "} " + operator
script += "result = value == void || value.length() == 0;\n";
break;
case "is not empty":
desc = "${" + variable + "} " + operator
script += "result = value != void && value.length() > 0;\n";
break;
}
let msg = "assertion [" + desc + "]: false;"
script += "if (!result){\n" +
"\tmsg = \"" + msg + "\";\n" +
"\tAssertionResult.setFailureMessage(msg);\n" +
"\tAssertionResult.setFailure(true);\n" +
"}";
this.assertion.desc = desc;
this.assertion.script = script;
this.$refs.jsr233.reload();
}
},
detail() {
this.visible = true;
},
close() {
this.visible = false;
},
confirm() {
if (!this.edit) {
this.add();
}
if (!this.assertion.desc) {
this.assertion.desc = this.assertion.script;
}
this.close();
}
},
computed: {
hasEmptyOperator() {
return !!this.assertion.operator && this.assertion.operator.indexOf("empty") > 0;
}
}
}
</script>
<style scoped>
.assertion-item {
display: inline-block;
}
.assertion-item + .assertion-item {
margin-left: 10px;
}
.assertion-item.input {
width: 100%;
}
.assertion-item.select {
min-width: 150px;
}
.assertion-item.label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.assertion-item.btn {
min-width: 130px;
}
.assertion-item.btn.circle {
text-align: right;
min-width: 80px;
}
.quick-script-block {
margin-bottom: 10px;
}
</style>

View File

@ -21,7 +21,9 @@
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="danger" size="mini" icon="el-icon-delete" circle @click="remove"
v-if="edit"/>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>Add</el-button>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add" v-else>
{{ $t('api_test.request.assertions.add') }}
</el-button>
</el-col>
</el-row>
</div>

View File

@ -29,7 +29,9 @@
</el-checkbox>
</el-col>
<el-col class="assertion-btn">
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add">Add</el-button>
<el-button :disabled="isReadOnly" type="primary" size="small" @click="add">
{{ $t('api_test.request.assertions.add') }}
</el-button>
</el-col>
</el-row>
</div>

View File

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

View File

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

View File

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

View File

@ -1,17 +1,21 @@
<template>
<div >
<div>
<el-row>
<el-col :span="20" class="script-content">
<ms-code-edit v-if="isCodeEditAlive" :mode="codeEditModeMap[jsr223Processor.language]" :read-only="isReadOnly" :data.sync="jsr223Processor.script" theme="eclipse" :modes="['java','python']" ref="codeEdit"/>
<el-col :span="20">
<ms-code-edit v-if="isCodeEditAlive" :mode="codeEditModeMap[jsr223Processor.language]" :read-only="isReadOnly"
:data.sync="jsr223Processor.script" theme="eclipse" :modes="['java','python']" ref="codeEdit"
:height="height"/>
</el-col>
<el-col :span="4" class="script-index">
<ms-dropdown :default-command="jsr223Processor.language" :commands="languages" @command="languageChange"/>
<div class="template-title">{{$t('api_test.request.processor.code_template')}}</div>
<div class="template-title">{{ $t('api_test.request.processor.code_template') }}</div>
<div v-for="(template, index) in codeTemplates" :key="index" class="code-template">
<el-link :disabled="template.disabled" @click="addTemplate(template)">{{template.title}}</el-link>
<el-link :disabled="template.disabled" @click="addTemplate(template)">{{ template.title }}</el-link>
</div>
<div class="document-url">
<el-link href="https://jmeter.apache.org/usermanual/component_reference.html#BeanShell_PostProcessor" type="primary">{{$t('commons.reference_documentation')}}</el-link>
<el-link href="https://jmeter.apache.org/usermanual/component_reference.html#BeanShell_PostProcessor"
type="primary">{{ $t('commons.reference_documentation') }}
</el-link>
<ms-instructions-icon :content="$t('api_test.request.processor.bean_shell_processor_tip')"/>
</div>
</el-col>
@ -20,15 +24,37 @@
</template>
<script>
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsInstructionsIcon from "../../../../common/components/MsInstructionsIcon";
import MsDropdown from "../../../../common/components/MsDropdown";
import MsCodeEdit from "../../../../common/components/MsCodeEdit";
import MsInstructionsIcon from "../../../../common/components/MsInstructionsIcon";
import MsDropdown from "../../../../common/components/MsDropdown";
export default {
export default {
name: "MsJsr233Processor",
components: {MsDropdown, MsInstructionsIcon, MsCodeEdit},
props: {
type: {
type: String,
},
isReadOnly: {
type: Boolean,
default: false
},
jsr223Processor: {
type: Object,
},
isPreProcessor: {
type: Boolean,
default: false
},
templates: {
type: Array,
default: () => []
},
height: {
type: [String, Number],
default: 230
}
},
data() {
return {
codeTemplates: [
@ -62,11 +88,12 @@
title: this.$t('api_test.request.processor.code_template_get_response_result'),
value: 'prev.getResponseDataAsString()',
disabled: this.isPreProcessor
}
},
...this.templates
],
isCodeEditAlive: true,
languages: [
'beanshell',"python"
'beanshell', "python"
],
codeEditModeMap: {
beanshell: 'java',
@ -74,22 +101,6 @@
}
}
},
props: {
type: {
type: String,
},
isReadOnly: {
type: Boolean,
default: false
},
jsr223Processor: {
type: Object,
},
isPreProcessor: {
type: Boolean,
default: false
}
},
watch: {
jsr223Processor() {
this.reload();
@ -114,39 +125,35 @@
this.jsr223Processor.language = language;
}
}
}
}
</script>
<style scoped>
.ace_editor {
.ace_editor {
border-radius: 5px;
}
}
.script-content {
height: calc(100vh - 570px);
}
.script-index {
.script-index {
padding: 0 20px;
}
}
.template-title {
.template-title {
margin-bottom: 5px;
font-weight: bold;
font-size: 15px;
}
}
.document-url {
.document-url {
margin-top: 10px;
}
}
.instructions-icon {
.instructions-icon {
margin-left: 5px;
}
}
.ms-dropdown {
.ms-dropdown {
margin-bottom: 20px;
}
}
</style>

View File

@ -1,4 +1,4 @@
<template>
<template xmlns:v-slot="http://www.w3.org/1999/XSL/Transform">
<el-form :model="request" :rules="rules" ref="request" label-width="100px" :disabled="isReadOnly">
<el-form-item :label="$t('api_test.request.name')" prop="name">
@ -66,7 +66,7 @@
:environment="scenario.environment"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
<ms-api-assertions :is-read-only="isReadOnly" :assertions="request.assertions"/>
<ms-api-assertions :request="request" :is-read-only="isReadOnly" :assertions="request.assertions"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract.label')" name="extract">
<ms-api-extract :is-read-only="isReadOnly" :extract="request.extract"/>
@ -104,6 +104,7 @@ export default {
MsApiVariable, ApiRequestMethodSelect, MsApiExtract, MsApiAssertions, MsApiBody, MsApiKeyValue},
props: {
request: HttpRequest,
jsonPathList: Array,
scenario: Scenario,
isReadOnly: {
type: Boolean,

View File

@ -2,32 +2,23 @@
<div class="request-form">
<component @runDebug="runDebug" :is="component" :is-read-only="isReadOnly" :request="request" :scenario="scenario"/>
<el-divider v-if="isCompleted"></el-divider>
<ms-request-result-tail v-loading="debugReportLoading" v-if="isCompleted"
:request="request.debugRequestResult ? request.debugRequestResult : {responseResult: {}, subRequestResults: []}"
:scenario-name="request.debugScenario ? request.debugScenario.name : ''"
ref="msDebugResult"/>
<ms-request-result-tail v-loading="debugReportLoading" v-if="isCompleted" :requestType="request.type" :request="request.debugRequestResult ? request.debugRequestResult : {responseResult: {}, subRequestResults: []}"
:scenario-name="request.debugScenario ? request.debugScenario.name : ''" ref="msDebugResult"/>
</div>
</template>
<script>
import {JSR223Processor, Request, RequestFactory, Scenario} from "../../model/ScenarioModel";
import MsApiHttpRequestForm from "./ApiHttpRequestForm";
import MsApiTcpRequestForm from "./ApiTcpRequestForm";
import MsApiDubboRequestForm from "./ApiDubboRequestForm";
import MsScenarioResults from "../../../report/components/ScenarioResults";
import MsRequestResultTail from "../../../report/components/RequestResultTail";
import MsApiSqlRequestForm from "./ApiSqlRequestForm";
import {JSR223Processor, Request, RequestFactory, Scenario} from "../../model/ScenarioModel";
import MsApiHttpRequestForm from "./ApiHttpRequestForm";
import MsApiTcpRequestForm from "./ApiTcpRequestForm";
import MsApiDubboRequestForm from "./ApiDubboRequestForm";
import MsScenarioResults from "../../../report/components/ScenarioResults";
import MsRequestResultTail from "../../../report/components/RequestResultTail";
import MsApiSqlRequestForm from "./ApiSqlRequestForm";
export default {
name: "MsApiRequestForm",
components: {
MsApiSqlRequestForm,
MsRequestResultTail,
MsScenarioResults,
MsApiDubboRequestForm,
MsApiHttpRequestForm,
MsApiTcpRequestForm
},
components: {MsApiSqlRequestForm, MsRequestResultTail, MsScenarioResults, MsApiDubboRequestForm, MsApiHttpRequestForm},
props: {
scenario: Scenario,
request: Request,
@ -40,9 +31,10 @@ export default {
data() {
return {
reportId: "",
content: {scenarios: []},
content: {scenarios:[]},
debugReportLoading: false,
showDebugReport: false
showDebugReport: false,
jsonPathList:[]
}
},
computed: {
@ -91,6 +83,7 @@ export default {
this.$get(url, response => {
let report = response.data || {};
let res = {};
if (response.data) {
try {
res = JSON.parse(report.content);
@ -125,19 +118,19 @@ export default {
this.$emit('runDebug', this.request);
}
}
}
}
</script>
<style scoped>
.scenario-results {
.scenario-results {
margin-top: 20px;
}
}
.request-form >>> .debug-button {
.request-form >>> .debug-button {
margin-left: auto;
display: block;
margin-right: 10px;
}
}
</style>

View File

@ -501,6 +501,12 @@ export class JSR223Processor extends DefaultTestElement {
}
}
export class JSR223Assertion extends JSR223Processor {
constructor(testName, processor) {
super('JSR223Assertion', 'TestBeanGUI', 'JSR223Assertion', testName, processor)
}
}
export class JSR223PreProcessor extends JSR223Processor {
constructor(testName, processor) {
super('JSR223PreProcessor', 'TestBeanGUI', 'JSR223PreProcessor', testName, processor)

View File

@ -25,7 +25,7 @@ import {
ThreadGroup,
XPath2Extractor,
IfController as JMXIfController,
ConstantTimer as JMXConstantTimer, TCPSampler,
ConstantTimer as JMXConstantTimer, TCPSampler, JSR223Assertion,
} from "./JMX";
import Mock from "mockjs";
import {funcFilters} from "@/common/js/func-filter";
@ -94,7 +94,8 @@ export const ASSERTION_TYPE = {
TEXT: "Text",
REGEX: "Regex",
JSON_PATH: "JSON",
DURATION: "Duration"
DURATION: "Duration",
JSR223: "JSR223",
}
export const ASSERTION_REGEX_SUBJECT = {
@ -716,16 +717,34 @@ export class KeyValue extends BaseConfig {
}
}
export class BeanShellProcessor extends BaseConfig {
constructor(options) {
super();
this.script = undefined;
this.set(options);
}
}
export class JSR223Processor extends BaseConfig {
constructor(options) {
super();
this.script = undefined;
this.language = "beanshell";
this.set(options);
}
}
export class Assertions extends BaseConfig {
constructor(options) {
super();
this.text = [];
this.regex = [];
this.jsonPath = [];
this.jsr223 = [];
this.duration = undefined;
this.set(options);
this.sets({text: Text, regex: Regex, jsonPath: JSONPath}, options);
this.sets({text: Text, regex: Regex, jsonPath: JSONPath, jsr223: AssertionJSR223}, options);
}
initOptions(options) {
@ -742,22 +761,23 @@ export class AssertionType extends BaseConfig {
}
}
export class BeanShellProcessor extends BaseConfig {
export class AssertionJSR223 extends AssertionType {
constructor(options) {
super();
this.script = undefined;
this.set(options);
}
}
super(ASSERTION_TYPE.JSR223);
this.variable = undefined;
this.operator = undefined;
this.value = undefined;
this.desc = undefined;
export class JSR223Processor extends BaseConfig {
constructor(options) {
super();
this.name = undefined;
this.script = undefined;
this.language = "beanshell";
this.set(options);
}
isValid() {
return !!this.script && !!this.language;
}
}
export class Text extends AssertionType {
@ -797,6 +817,10 @@ export class JSONPath extends AssertionType {
this.set(options);
}
setJSONPathDescription() {
this.description = this.expression + " expect: " + (this.expect ? this.expect : '');
}
isValid() {
return !!this.expression;
}
@ -1402,6 +1426,12 @@ class JMXGenerator {
})
}
if (assertions.jsr223.length > 0) {
assertions.jsr223.filter(this.filter).forEach(item => {
httpSamplerProxy.put(this.getJSR223Assertion(item));
})
}
if (assertions.duration.isValid()) {
let name = "Response In Time: " + assertions.duration.value
httpSamplerProxy.put(new DurationAssertion(name, assertions.duration.value));
@ -1413,6 +1443,11 @@ class JMXGenerator {
return new JSONPathAssertion(name, jsonPath);
}
getJSR223Assertion(item) {
let name = item.desc;
return new JSR223Assertion(name, item);
}
getResponseAssertion(regex) {
let name = regex.description;
let type = JMX_ASSERTION_CONDITION.CONTAINS; // 固定用Match自己写正则

View File

@ -1,5 +1,5 @@
<template>
<editor v-model="formatData" :lang="mode" @init="editorInit" :theme="theme"/>
<editor v-model="formatData" :lang="mode" @init="editorInit" :theme="theme" :height="height"/>
</template>
<script>
@ -12,6 +12,7 @@
}
},
props: {
height: [String, Number],
data: {
type: String
},

View File

@ -143,6 +143,34 @@ export const CREATOR = {
}
}
export const EXECUTOR = {
key: "executor",
name: 'MsTableSearchSelect',
label: 'test_track.plan_view.executor',
operator: {
options: [OPERATORS.IN, OPERATORS.NOT_IN, OPERATORS.CURRENT_USER],
change: function (component, value) { // 运算符change事件
if (value === OPERATORS.CURRENT_USER.value) {
component.value = value;
}
}
},
options: { // 异步获取候选项
url: "/user/list",
labelKey: "name",
valueKey: "id",
showLabel: option => {
return option.label + "(" + option.value + ")";
}
},
props: {
multiple: true
},
isShow: operator => {
return operator !== OPERATORS.CURRENT_USER.value;
}
}
export const TRIGGER_MODE = {
key: "triggerMode",
name: 'MsTableSearchSelect',
@ -287,6 +315,6 @@ export const TEST_CONFIGS = [NAME, UPDATE_TIME, PROJECT_NAME, CREATE_TIME, STATU
export const REPORT_CONFIGS = [NAME, TEST_NAME, PROJECT_NAME, CREATE_TIME, STATUS, CREATOR, TRIGGER_MODE];
export const TEST_CASE_CONFIGS = [NAME, MODULE, PRIORITY, CREATE_TIME, TYPE, UPDATE_TIME, METHOD, CREATOR];
export const TEST_CASE_CONFIGS = [NAME, MODULE, PRIORITY, CREATE_TIME, TYPE, UPDATE_TIME, METHOD, CREATOR, EXECUTOR];
export const TEST_PLAN_CONFIGS = [NAME, UPDATE_TIME, PROJECT_NAME, CREATE_TIME, PRINCIPAL, TEST_PLAN_STATUS, STAGE];

View File

@ -4,7 +4,7 @@
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-1">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{maxUsers}}</span>
<span class="ms-card-data-digital">{{ maxUsers }}</span>
<span class="ms-card-data-unit"> VU</span>
</span>
<span class="ms-card-desc">Max Users</span>
@ -13,7 +13,7 @@
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-2">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{avgThroughput}}</span>
<span class="ms-card-data-digital">{{ avgThroughput }}</span>
<span class="ms-card-data-unit"> Hits/s</span>
</span>
<span class="ms-card-desc">Avg.Throughput</span>
@ -22,7 +22,7 @@
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-3">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{errors}}</span>
<span class="ms-card-data-digital">{{ errors }}</span>
<span class="ms-card-data-unit"> %</span>
</span>
<span class="ms-card-desc">Errors</span>
@ -31,7 +31,7 @@
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-4">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{avgResponseTime}}</span>
<span class="ms-card-data-digital">{{ avgResponseTime }}</span>
<span class="ms-card-data-unit"> s</span>
</span>
<span class="ms-card-desc">Avg.Response Time</span>
@ -40,7 +40,7 @@
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-5">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{responseTime90}}</span>
<span class="ms-card-data-digital">{{ responseTime90 }}</span>
<span class="ms-card-data-unit"> s</span>
</span>
<span class="ms-card-desc">90% Response Time</span>
@ -49,7 +49,7 @@
<el-col :span="4">
<el-card shadow="always" class="ms-card-index-6">
<span class="ms-card-data">
<span class="ms-card-data-digital">{{avgBandwidth}}</span>
<span class="ms-card-data-digital">{{ avgBandwidth }}</span>
<span class="ms-card-data-unit"> KiB/s</span>
</span>
<span class="ms-card-desc">Avg.Bandwidth</span>
@ -65,6 +65,14 @@
<ms-chart ref="chart2" :options="resOption" class="chart-config" :autoresize="true"></ms-chart>
</el-col>
</el-row>
<el-row>
<el-col :span="12">
<ms-chart ref="chart3" :options="errorOption" class="chart-config" :autoresize="true"></ms-chart>
</el-col>
<el-col :span="12">
<ms-chart ref="chart3" :options="resCodeOption" class="chart-config" :autoresize="true"></ms-chart>
</el-col>
</el-row>
</div>
</template>
@ -84,6 +92,8 @@ export default {
avgBandwidth: "0",
loadOption: {},
resOption: {},
errorOption: {},
resCodeOption: {},
id: ''
}
},
@ -106,6 +116,12 @@ export default {
this.avgBandwidth = '0';
this.$warning(this.$t('report.generation_error'));
})
this.getLoadChart();
this.getResChart();
this.getErrorChart();
this.getResponseCodeChart();
},
getLoadChart() {
this.$get("/performance/report/content/load_chart/" + this.id).then(res => {
let data = res.data.data;
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
@ -181,6 +197,8 @@ export default {
}).catch(() => {
this.loadOption = {};
})
},
getResChart() {
this.$get("/performance/report/content/res_chart/" + this.id).then(res => {
let data = res.data.data;
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
@ -264,6 +282,146 @@ export default {
this.resOption = {};
})
},
getErrorChart() {
this.$get("/performance/report/content/error_chart/" + this.id).then(res => {
let data = res.data.data;
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
let yAxisListMax = this._getChartMax(yAxisList);
let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName);
yAxisIndex0List = this._unique(yAxisIndex0List);
let errorOption = {
title: {
text: 'Errors',
left: 'center',
top: 20,
textStyle: {
color: '#99743C'
},
},
tooltip: {
show: true,
trigger: 'axis',
extraCssText: 'z-index: 999;',
formatter: function (params, ticket, callback) {
let result = "";
let name = params[0].name;
result += name + "<br/>";
for (let i = 0; i < params.length; i++) {
let seriesName = params[i].seriesName;
if (seriesName.length > 100) {
seriesName = seriesName.substring(0, 100);
}
let value = params[i].value;
let marker = params[i].marker;
result += marker + seriesName + ": " + value[1] + "<br/>";
}
return result;
}
},
legend: {},
xAxis: {},
yAxis: [
{
name: 'No',
type: 'value',
min: 0,
max: yAxisListMax,
interval: yAxisListMax / 5
}
],
series: []
}
let setting = {
series: [
{
name: 'users',
color: '#0CA74A',
}
]
}
yAxisIndex0List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'})
})
this.errorOption = this.generateOption(errorOption, data, setting);
}).catch(() => {
this.errorOption = {};
})
},
getResponseCodeChart() {
this.$get("/performance/report/content/response_code_chart/" + this.id).then(res => {
let data = res.data.data;
let yAxisList = data.filter(m => m.yAxis2 === -1).map(m => m.yAxis);
let yAxisListMax = this._getChartMax(yAxisList);
let yAxisIndex0List = data.filter(m => m.yAxis2 === -1).map(m => m.groupName);
yAxisIndex0List = this._unique(yAxisIndex0List);
let resCodeOption = {
title: {
text: 'Response code',
left: 'center',
top: 20,
textStyle: {
color: '#99743C'
},
},
tooltip: {
show: true,
trigger: 'axis',
extraCssText: 'z-index: 999;',
formatter: function (params, ticket, callback) {
let result = "";
let name = params[0].name;
result += name + "<br/>";
for (let i = 0; i < params.length; i++) {
let seriesName = params[i].seriesName;
if (seriesName.length > 100) {
seriesName = seriesName.substring(0, 100);
}
let value = params[i].value;
let marker = params[i].marker;
result += marker + seriesName + ": " + value[1] + "<br/>";
}
return result;
}
},
legend: {},
xAxis: {},
yAxis: [
{
name: 'No',
type: 'value',
min: 0,
max: yAxisListMax,
interval: yAxisListMax / 5
}
],
series: []
}
let setting = {
series: [
{
name: 'users',
color: '#0CA74A',
}
]
}
yAxisIndex0List.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '0'})
})
this.resCodeOption = this.generateOption(resCodeOption, data, setting);
}).catch(() => {
this.resCodeOption = {};
})
},
generateOption(option, data, setting) {
let chartData = data;
let seriesArray = [];
@ -344,93 +502,95 @@ export default {
this.avgBandwidth = '0';
this.loadOption = {};
this.resOption = {};
this.errorOption = {};
this.resCodeOption = {};
}
},
deep: true
}
},
props: ['report']
}
}
</script>
<style scoped>
.ms-card-data {
.ms-card-data {
text-align: left;
display: block;
margin-bottom: 5px;
}
}
.ms-card-desc {
.ms-card-desc {
display: block;
text-align: left;
}
}
.ms-card-data-digital {
.ms-card-data-digital {
font-size: 21px;
}
}
.ms-card-data-unit {
.ms-card-data-unit {
color: #8492a6;
font-size: 15px;
}
}
.ms-card-index-1 .ms-card-data-digital {
.ms-card-index-1 .ms-card-data-digital {
color: #44b349;
}
}
.ms-card-index-1 {
.ms-card-index-1 {
border-left-color: #44b349;
border-left-width: 3px;
}
}
.ms-card-index-2 .ms-card-data-digital {
.ms-card-index-2 .ms-card-data-digital {
color: #65A2FF;
}
}
.ms-card-index-2 {
.ms-card-index-2 {
border-left-color: #65A2FF;
border-left-width: 3px;
}
}
.ms-card-index-3 .ms-card-data-digital {
.ms-card-index-3 .ms-card-data-digital {
color: #E6113C;
}
}
.ms-card-index-3 {
.ms-card-index-3 {
border-left-color: #E6113C;
border-left-width: 3px;
}
}
.ms-card-index-4 .ms-card-data-digital {
.ms-card-index-4 .ms-card-data-digital {
color: #99743C;
}
}
.ms-card-index-4 {
.ms-card-index-4 {
border-left-color: #99743C;
border-left-width: 3px;
}
}
.ms-card-index-5 .ms-card-data-digital {
.ms-card-index-5 .ms-card-data-digital {
color: #99743C;
}
}
.ms-card-index-5 {
.ms-card-index-5 {
border-left-color: #99743C;
border-left-width: 3px;
}
}
.ms-card-index-6 .ms-card-data-digital {
.ms-card-index-6 .ms-card-data-digital {
color: #3C9899;
}
}
.ms-card-index-6 {
.ms-card-index-6 {
border-left-color: #3C9899;
border-left-width: 3px;
}
}
.chart-config {
.chart-config {
width: 100%;
}
}
</style>

View File

@ -76,7 +76,6 @@
<ms-delete-confirm :title="$t('project.delete')" @delete="_handleDelete" ref="deleteConfirm"/>
<api-environment-config ref="environmentConfig"/>
<ms-jar-config ref="jarConfig"/>
</ms-container>
</template>

View File

@ -3,7 +3,8 @@
<el-card class="table-card" v-loading="result.loading">
<template v-slot:header>
<ms-table-header :condition.sync="condition" @search="search" @create="create"
:create-tip="$t('test_resource_pool.create_resource_pool')" :title="$t('commons.test_resource_pool')"/>
:create-tip="$t('test_resource_pool.create_resource_pool')"
:title="$t('commons.test_resource_pool')"/>
</template>
<el-table border class="adjust-table" :data="items" style="width: 100%">
<el-table-column prop="name" :label="$t('commons.name')"/>
@ -174,14 +175,14 @@
</template>
<script>
import MsCreateBox from "../CreateBox";
import MsTablePagination from "../../common/pagination/TablePagination";
import MsTableHeader from "../../common/components/MsTableHeader";
import MsTableOperator from "../../common/components/MsTableOperator";
import MsDialogFooter from "../../common/components/MsDialogFooter";
import {listenGoBack, removeGoBackListener} from "../../../../common/js/utils";
import MsCreateBox from "../CreateBox";
import MsTablePagination from "../../common/pagination/TablePagination";
import MsTableHeader from "../../common/components/MsTableHeader";
import MsTableOperator from "../../common/components/MsTableOperator";
import MsDialogFooter from "../../common/components/MsDialogFooter";
import {listenGoBack, removeGoBackListener} from "../../../../common/js/utils";
export default {
export default {
name: "MsTestResourcePool",
components: {MsCreateBox, MsTablePagination, MsTableHeader, MsTableOperator, MsDialogFooter},
data() {
@ -286,7 +287,9 @@
let resources = [];
if (this.form.resources) {
this.form.resources.forEach(function (resource) {
resources.push(JSON.parse(resource.configuration));
let configuration = JSON.parse(resource.configuration);
configuration.id = resource.id
resources.push(configuration);
})
}
this.infoList = resources;
@ -298,7 +301,7 @@
cancelButtonText: this.$t('commons.cancel'),
type: 'warning'
}).then(() => {
this.result = this.$get(`/testresourcepool/delete/${row.id}`,() => {
this.result = this.$get(`/testresourcepool/delete/${row.id}`, () => {
this.initTableData();
this.$success(this.$t('commons.delete_success'));
});
@ -334,7 +337,8 @@
let resources = [];
let poolId = this.form.id;
this.infoList.forEach(function (info) {
let resource = {"configuration": JSON.stringify(info)};
let configuration = JSON.stringify(info);
let resource = {"configuration": configuration, id: info.id};
if (poolId) {
resource.testResourcePoolId = poolId;
}
@ -386,13 +390,13 @@
})
}
}
}
}
</script>
<style scoped>
.box {
.box {
padding-left: 5px;
}
}
</style>

View File

@ -3,18 +3,17 @@
<ms-main-container>
<el-row :gutter="20">
<el-col :span="15">
<el-row>
<related-test-plan-list ref="relatedTestPlanList"/>
</el-row>
<el-row>
<review-list title="我的评审" ref="caseReviewList"/>
</el-row>
</el-col>
<el-col :span="9">
<test-case-side-list :title="$t('test_track.home.recent_test')" ref="testCaseRecentList"/>
</el-col>
</el-row>
<div style="margin-top: 10px"/>
<el-row :gutter="20">
<el-col :span="15">
<review-list title="我的评审" ref="caseReviewList"/>
</el-col>
</el-row>
</ms-main-container>
</ms-container>
</template>
@ -52,4 +51,7 @@ export default {
cursor: pointer;
}
.el-row {
padding-bottom: 20px;
}
</style>

View File

@ -25,7 +25,7 @@
</el-button>
</el-col>
<el-col :span="12" class="head-right">
<el-col :span="11" class="head-right">
<span class="head-right-tip" v-if="index + 1 === testCases.length">
{{ $t('test_track.plan_view.pre_case') }} : {{
testCases[index - 1] ? testCases[index - 1].name : ''
@ -44,11 +44,6 @@
<el-button plain size="mini" icon="el-icon-arrow-down"
:disabled="index + 1 >= testCases.length"
@click="handleNext()"/>
<el-divider direction="vertical"></el-divider>
<el-button type="primary" size="mini" :disabled="isReadOnly" @click="saveCase()">
{{ $t('test_track.save') }}
</el-button>
</el-col>
</el-row>
@ -101,7 +96,7 @@
<el-row>
<el-col :offset="1">
<span class="cast_label">{{ $t('test_track.case.prerequisite') }}</span>
<span class="cast_item">{{ testCase.prerequisite }}</span>
<span class="cast_item"><p>{{ testCase.prerequisite }}</p></span>
</el-col>
</el-row>
@ -577,6 +572,8 @@ export default {
this.isFailure = this.testCase.steptResults.filter(s => {
return s.executeResult === 'Failure' || s.executeResult === 'Blocking';
}).length > 0;
} else {
this.isFailure = false;
}
},
saveIssues() {
@ -684,4 +681,9 @@ export default {
.el-switch >>> .el-switch__label.is-active {
color: #409EFF;
}
p {
white-space: pre-line;
line-height: 20px;
}
</style>

View File

@ -1,7 +1,10 @@
<template>
<div v-loading="result.loading">
<div class="comment-list">
<review-comment-item v-for="(comment,index) in comments" :key="index" :comment="comment"/>
<review-comment-item v-for="(comment,index) in comments"
:key="index"
:comment="comment"
@refresh="refresh"/>
<div v-if="comments.length === 0" style="text-align: center">
<i class="el-icon-chat-line-square" style="font-size: 15px;color: #8a8b8d;">
<span style="font-size: 15px; color: #8a8b8d;">
@ -56,10 +59,13 @@ export default {
}
this.result = this.$post('/test/case/comment/save', comment, () => {
this.$success(this.$t('test_track.comment.send_success'));
this.$emit('getComments');
this.refresh();
this.textarea = '';
});
},
refresh() {
this.$emit('getComments');
},
}
}
</script>

View File

@ -2,30 +2,88 @@
<div class="main">
<div class="comment-left">
<div class="icon-title">
{{ comment.author.substring(0, 1) }}
{{ comment.authorName.substring(0, 1) }}
</div>
</div>
<div class="comment-right">
<span style="font-size: 14px;color: #909399;font-weight: bold">{{ comment.author }}</span>
<span style="font-size: 14px;color: #909399;font-weight: bold">{{ comment.authorName }}</span>
<span style="color: #8a8b8d; margin-left: 8px; font-size: 12px">
{{ comment.createTime | timestampFormatDate }}
</span>
<span class="comment-delete">
<i class="el-icon-edit" style="font-size: 9px;margin-right: 6px;" @click="openEdit"/>
<i class="el-icon-close" @click="deleteComment"/>
</span>
<br/>
<div class="comment-desc" style="font-size: 10px;color: #303133">
<pre>{{ comment.description }}</pre>
</div>
</div>
<el-dialog
:title="$t('commons.edit')"
:visible.sync="visible"
width="30%"
:destroy-on-close="true"
:append-to-body="true"
:close-on-click-modal="false"
show-close>
<el-input
type="textarea"
:rows="5"
v-model="description">
</el-input>
<span slot="footer" class="dialog-footer">
<ms-dialog-footer
@cancel="visible = false"
@confirm="editComment"/>
</span>
</el-dialog>
</div>
</template>
<script>
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
import {getCurrentUser} from "@/common/js/utils";
export default {
name: "ReviewCommentItem",
components: {MsDialogFooter},
props: {
comment: Object
},
data() {
return {}
return {
visible: false,
description: ""
}
},
methods: {
deleteComment() {
if (getCurrentUser().id !== this.comment.author) {
this.$warning(this.$t('test_track.comment.cannot_delete'));
return;
}
this.$parent.result = this.$get("/test/case/comment/delete/" + this.comment.id, () => {
this.$success(this.$t('commons.delete_success'));
this.$emit("refresh");
});
},
openEdit() {
if (getCurrentUser().id !== this.comment.author) {
this.$warning(this.$t('test_track.comment.cannot_edit'));
return;
}
this.description = this.comment.description;
this.visible = true;
},
editComment() {
this.$post("/test/case/comment/edit", {id: this.comment.id, description: this.description}, () => {
this.visible = false;
this.$success(this.$t('commons.modify_success'));
this.$emit("refresh");
});
}
}
}
</script>
@ -72,4 +130,10 @@ export default {
pre {
margin: 0 0;
}
.comment-delete {
float: right;
margin-right: 5px;
cursor: pointer;
}
</style>

View File

@ -30,7 +30,7 @@
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="creator"
prop="creatorName"
:label="$t('test_track.review.review_creator')"
show-overflow-tooltip>
</el-table-column>
@ -66,10 +66,6 @@
<template v-slot:default="scope">
<ms-table-operator :is-tester-permission="true" @editClick="handleEdit(scope.row)"
@deleteClick="handleDelete(scope.row)">
<!-- <template v-slot:middle>-->
<!-- <ms-table-operator-button :isTesterPermission="true" type="success" tip="重新发起" icon="el-icon-document"-->
<!-- @exec="reCreate(scope.row)"/>-->
<!-- </template>-->
</ms-table-operator>
</template>
</el-table-column>

View File

@ -365,7 +365,7 @@ export default {
},
getTestReviewById() {
if (this.reviewId) {
this.$post('/test/case/review/get/' + this.reviewId, {}, response => {
this.$get('/test/case/review/get/' + this.reviewId, response => {
this.testReview = response.data;
this.refreshTestReviewRecent();
});

View File

@ -1,6 +1,6 @@
export default {
commons: {
comment:'comment',
comment: 'comment',
examples: 'examples',
help_documentation: 'Help documentation',
delete_cancelled: 'Delete cancelled',
@ -216,8 +216,8 @@ export default {
select: 'Select Organization',
service_integration: 'Service integration',
defect_manage: 'Defect management platform',
message_settings:'Message settings',
message:{
message_settings: 'Message settings',
message: {
jenkins_task_notification: 'Jenkins task notification',
test_plan_task_notification: 'Test plan task notification',
test_review_task_notice: 'Test review task notice',
@ -557,7 +557,18 @@ export default {
expect: "Expect Value",
expression: "Expression",
response_in_time: "Response in time",
ignore_status: "Ignore Status"
ignore_status: "Ignore Status",
add: "Add",
script_name: "Script name",
script: "Script",
variable_name: "Variable name",
set_failure_status: "Set failure status",
set_failure_msg: "Set failure message",
json_path_add: "Add JONPATH Assertions",
json_path_err: "The response result is not in JSON format",
json_path_suggest: "JSONPath Assertion Suggest",
json_path_clear: "Clear JSONPath Assertion",
debug_first: "First, debug to get the response",
},
extract: {
label: "Extract from response",
@ -848,6 +859,8 @@ export default {
relevance_case: "Relevance Case",
last_page: "It's the end",
execute_result: "Result",
cannot_edit: "Cannot edit this comment",
cannot_delete: "Cannot delete this comment",
},
module: {
search: "Search module",

View File

@ -1,6 +1,6 @@
export default {
commons: {
comment:'评论',
comment: '评论',
examples: '示例',
help_documentation: '帮助文档',
delete_cancelled: '已取消删除',
@ -217,8 +217,8 @@ export default {
delete_warning: '删除该组织将同步删除该组织下所有相关工作空间和相关工作空间下的所有项目,以及项目中的所有用例、接口测试、性能测试等,确定要删除吗?',
service_integration: '服务集成',
defect_manage: '缺陷管理平台',
message_settings:'消息设置',
message:{
message_settings: '消息设置',
message: {
jenkins_task_notification: 'Jenkins接口调用任务通知',
test_plan_task_notification: '测试计划任务通知',
test_review_task_notice: '测试评审任务通知',
@ -545,6 +545,7 @@ export default {
text: "文本",
regex: "正则",
response_time: "响应时间",
jsr223: "脚本",
select_type: "请选择类型",
select_subject: "请选择对象",
select_condition: "请选择条件",
@ -557,7 +558,18 @@ export default {
expect: "期望值",
expression: "Perl型正则表达式",
response_in_time: "响应时间在...毫秒以内",
ignore_status: "忽略状态"
json_path_add: "添加 JONPATH 断言",
json_path_err: "响应结果不是 JSON 格式",
json_path_suggest: "推荐JSONPath断言",
json_path_clear: "清空JSONPath断言",
debug_first: "请先执行调试获取响应结果",
ignore_status: "忽略状态",
add: "添加",
script_name: "脚本名称",
script: "脚本",
variable_name: "变量名称",
set_failure_status: "设置失败状态",
set_failure_msg: "设置失败消息",
},
extract: {
label: "提取",
@ -841,6 +853,8 @@ export default {
send: "发送",
description_is_null: "评论内容不能为空!",
send_success: "评论成功!",
cannot_edit: "无法编辑此评论!",
cannot_delete: "无法删除此评论!",
},
review_view: {
review: "评审",

View File

@ -217,20 +217,25 @@ export default {
delete_warning: '刪除該組織將同步刪除該組織下所有相關工作空間和相關工作空間下的所有項目,以及項目中的所有用例、接口測試、性能測試等,確定要刪除嗎?',
service_integration: '服務集成',
defect_manage: '缺陷管理平臺',
message_settings:'消息設',
message_settings:'消息設',
message:{
jenkins_task_notification: 'Jenkins任務通知',
test_plan_task_notification: '測試計任務通知',
jenkins_task_notification: 'Jenkins接口調用任務通知',
test_plan_task_notification: '測試計任務通知',
test_review_task_notice: '測試評審任務通知',
defect_task_notification: '缺陷任務通知',
create_new_notification: '創建新通知',
select_events: '選擇事件',
select_receiving_method: '選擇接收管道',
defect_task_notification: '缺陷任務通知',
select_receiving_method: '選擇接收方式',
mail: '郵件',
nail_robot: '釘釘機器人',
enterprise_wechat_robot: '企業微信機器人',
message_webhook: '接收管道為釘釘和企業機器人時webhook為必填項\n' +
'\n'
notes: '註意: 1.事件,接收方式,接收人為必填項;\n' +
' 2.接收方式除郵件外webhook為必填\n' +
' 3.機器人選擇為群機器人,安全驗證選擇“自定義關鍵詞” "任務通知"',
message: '事件,接收人,接收方式為必填項',
message_webhook: '接收方式為釘釘和企業機器人時webhook為必填項'
},
integration: {
select_defect_platform: '請選擇要集成的缺陷管理平臺:',
@ -254,17 +259,7 @@ export default {
successful_operation: '操作成功',
not_integrated: '未集成該平臺',
choose_platform: '請選擇集成的平臺',
verified: '驗證通過',
mail: '郵件',
nail_robot: '釘釘機器人',
enterprise_wechat_robot: '企業微信機器人',
notes: '注意1.事件,接收管道,接收人為必填項;\n' +
'\n' +
'2.接收管道除郵件外webhook為必填\n' +
'\n' +
'3.機器人選擇為群機器人,安全驗證選擇“自定義關鍵字”:“任務通知”',
message: '事件,接收人,接收管道為必填項\n' +
'\n'
verified: '驗證通過'
}
},
project: {
@ -562,7 +557,18 @@ export default {
expect: "期望值",
expression: "Perl型正則表達式",
response_in_time: "響應時間在...毫秒以內",
ignore_status: "忽略狀態"
json_path_add: "添加 JONPATH 斷言",
json_path_err: "響應結果不是 JSON 格式",
json_path_suggest: "推薦JSONPath斷言",
json_path_clear: "清空JSONPath斷言",
debug_first: "請先執行調試獲取響應結果",
ignore_status: "忽略狀態",
add: "添加",
script_name: "腳本名稱",
script: "腳本",
variable_name: "變量名稱",
set_failure_status: "設置失敗狀態",
set_failure_msg: "設置失敗消息",
},
extract: {
label: "提取",
@ -846,6 +852,8 @@ export default {
send: "發送",
description_is_null: "評論內容不能為空!",
send_success: "評論成功!",
cannot_edit: "無法編輯此評論!",
cannot_delete: "無法刪除此評論!",
},
review_view: {
review: "評審",