提交 22be52a5 编写于 作者: C Captain.B

Merge remote-tracking branch 'origin/dev' into dev

......@@ -3,6 +3,7 @@ package io.metersphere.base.mapper.ext;
import io.metersphere.base.domain.TestCase;
import io.metersphere.controller.request.ReportRequest;
import io.metersphere.controller.request.testcase.QueryTestCaseRequest;
import io.metersphere.controller.request.testplancase.QueryTestPlanCaseRequest;
import io.metersphere.dto.ReportDTO;
import io.metersphere.dto.TestPlanCaseDTO;
import org.apache.ibatis.annotations.Param;
......@@ -13,5 +14,5 @@ public interface ExtTestCaseMapper {
List<TestCase> getTestCaseNames(@Param("request") QueryTestCaseRequest request);
List<TestPlanCaseDTO> getTestPlanTestCases(@Param("request") QueryTestCaseRequest request);
List<TestPlanCaseDTO> getTestPlanTestCases(@Param("request") QueryTestPlanCaseRequest request);
}
......@@ -37,5 +37,8 @@
<if test="request.name != null">
and t2.name like CONCAT('%', #{request.name},'%')
</if>
<if test="request.executor != null">
and t1.executor = #{request.executor}
</if>
</select>
</mapper>
\ No newline at end of file
......@@ -7,6 +7,8 @@ import io.metersphere.base.domain.TestPlanTestCase;
import io.metersphere.commons.utils.PageUtils;
import io.metersphere.commons.utils.Pager;
import io.metersphere.controller.request.testcase.QueryTestCaseRequest;
import io.metersphere.controller.request.testcase.TestCaseBatchRequest;
import io.metersphere.controller.request.testplancase.QueryTestPlanCaseRequest;
import io.metersphere.dto.TestPlanCaseDTO;
import io.metersphere.service.TestPlanTestCaseService;
import org.springframework.web.bind.annotation.*;
......@@ -22,7 +24,7 @@ public class TestPlanTestCaseController {
TestPlanTestCaseService testPlanTestCaseService;
@PostMapping("/list/{goPage}/{pageSize}")
public Pager<List<TestPlanCaseDTO>> getTestPlanCases(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryTestCaseRequest request){
public Pager<List<TestPlanCaseDTO>> getTestPlanCases(@PathVariable int goPage, @PathVariable int pageSize, @RequestBody QueryTestPlanCaseRequest request){
Page<Object> page = PageHelper.startPage(goPage, pageSize, true);
return PageUtils.setPageInfo(page, testPlanTestCaseService.getTestPlanCases(request));
}
......@@ -32,11 +34,14 @@ public class TestPlanTestCaseController {
testPlanTestCaseService.editTestCase(testPlanTestCase);
}
@PostMapping("/batch/edit")
public void editTestCaseBath(@RequestBody TestCaseBatchRequest request){
testPlanTestCaseService.editTestCaseBath(request);
}
@PostMapping("/delete/{id}")
public int deleteTestCase(@PathVariable Integer id){
return testPlanTestCaseService.deleteTestCase(id);
}
}
package io.metersphere.controller.request.testcase;
import io.metersphere.base.domain.TestCase;
import io.metersphere.base.domain.TestPlanTestCase;
import java.util.List;
public class TestCaseBatchRequest extends TestPlanTestCase {
private List<Integer> ids;
public List<Integer> getIds() {
return ids;
}
public void setIds(List<Integer> ids) {
this.ids = ids;
}
}
package io.metersphere.controller.request.testplancase;
import io.metersphere.base.domain.TestCase;
import io.metersphere.base.domain.TestPlanTestCase;
import java.util.List;
public class QueryTestPlanCaseRequest extends TestPlanTestCase {
private List<Integer> nodeIds;
private String workspaceId;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<Integer> getNodeIds() {
return nodeIds;
}
public void setNodeIds(List<Integer> nodeIds) {
this.nodeIds = nodeIds;
}
public String getWorkspaceId() {
return workspaceId;
}
public void setWorkspaceId(String workspaceId) {
this.workspaceId = workspaceId;
}
}
......@@ -11,10 +11,7 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.report.core.Sample;
import org.apache.jmeter.report.core.SampleMetadata;
import org.apache.jmeter.report.dashboard.JsonizerVisitor;
import org.apache.jmeter.report.processor.ListResultData;
import org.apache.jmeter.report.processor.MapResultData;
import org.apache.jmeter.report.processor.ResultData;
import org.apache.jmeter.report.processor.SampleContext;
import org.apache.jmeter.report.processor.*;
import org.apache.jmeter.report.processor.graph.AbstractOverTimeGraphConsumer;
import org.apache.jmeter.report.processor.graph.impl.ActiveThreadsGraphConsumer;
import org.apache.jmeter.report.processor.graph.impl.HitsPerSecondGraphConsumer;
......@@ -345,61 +342,6 @@ public class JtlResolver {
return testOverview;
}
// public static List<ChartsData> getLoadChartData(String jtlString) {
// List<ChartsData> chartsDataList = new ArrayList<>();
// List<Metric> totalMetricList = JtlResolver.resolver(jtlString);
//
// if (totalMetricList != null) {
// for (Metric metric : totalMetricList) {
// metric.setTimestamp(stampToDate(DATE_TIME_PATTERN, metric.getTimestamp()));
// }
// }
// Map<String, List<Metric>> collect = Objects.requireNonNull(totalMetricList).stream().collect(Collectors.groupingBy(Metric::getTimestamp));
// List<Map.Entry<String, List<Metric>>> entries = new ArrayList<>(collect.entrySet());
//
// for (Map.Entry<String, List<Metric>> entry : entries) {
// int failSize = 0;
// List<Metric> metrics = entry.getValue();
// Map<String, List<Metric>> metricsMap = metrics.stream().collect(Collectors.groupingBy(Metric::getThreadName));
// int maxUsers = metricsMap.size();
// for (Metric metric : metrics) {
// String isSuccess = metric.getSuccess();
// if (!"true".equals(isSuccess)) {
// failSize++;
// }
// }
//
// String timeStamp = "";
// try {
// timeStamp = formatDate(entry.getKey());
// } catch (ParseException e) {
// e.printStackTrace();
// }
//
// ChartsData chartsData = new ChartsData();
// chartsData.setxAxis(timeStamp);
// chartsData.setGroupName("hits");
// chartsData.setyAxis(new BigDecimal(metrics.size() * 1.0 / maxUsers));
// chartsDataList.add(chartsData);
//
// chartsData = new ChartsData();
// chartsData.setxAxis(timeStamp);
// chartsData.setGroupName("users");
// chartsData.setyAxis(new BigDecimal(maxUsers));
// chartsDataList.add(chartsData);
//
// chartsData = new ChartsData();
// chartsData.setxAxis(timeStamp);
// chartsData.setGroupName("errors");
// chartsData.setyAxis(new BigDecimal(failSize));
// chartsDataList.add(chartsData);
//
// }
//
// return chartsDataList;
// }
public static List<ChartsData> getLoadChartData(String jtlString) {
Map<String, Object> activeThreadMap = getResultDataMap(jtlString, new ActiveThreadsGraphConsumer());
Map<String, Object> hitsMap = getResultDataMap(jtlString, new HitsPerSecondGraphConsumer());
......@@ -411,45 +353,6 @@ public class JtlResolver {
return activeThreadList;
}
// public static List<ChartsData> getResponseTimeChartData(String jtlString) {
// List<ChartsData> chartsDataList = new ArrayList<>();
// List<Metric> totalMetricList = JtlResolver.resolver(jtlString);
//
// totalMetricList.forEach(metric -> {
// metric.setTimestamp(stampToDate(DATE_TIME_PATTERN, metric.getTimestamp()));
// });
//
// Map<String, List<Metric>> metricMap = totalMetricList.stream().collect(Collectors.groupingBy(Metric::getTimestamp));
// List<Map.Entry<String, List<Metric>>> entries = new ArrayList<>(metricMap.entrySet());
//
// for (Map.Entry<String, List<Metric>> entry : entries) {
// List<Metric> metricList = entry.getValue();
// Map<String, List<Metric>> metricsMap = metricList.stream().collect(Collectors.groupingBy(Metric::getThreadName));
// int maxUsers = metricsMap.size();
// int sumElapsedTime = metricList.stream().mapToInt(metric -> Integer.parseInt(metric.getElapsed())).sum();
// String timeStamp = "";
// try {
// timeStamp = formatDate(entry.getKey());
// } catch (ParseException e) {
// e.printStackTrace();
// }
//
// ChartsData chartsData = new ChartsData();
// chartsData.setxAxis(timeStamp);
// chartsData.setGroupName("users");
// chartsData.setyAxis(new BigDecimal(maxUsers));
// chartsDataList.add(chartsData);
//
// ChartsData chartsData2 = new ChartsData();
// chartsData2.setxAxis(timeStamp);
// chartsData2.setGroupName("responseTime");
// chartsData2.setyAxis(new BigDecimal(sumElapsedTime * 1.0 / metricList.size()));
// chartsDataList.add(chartsData2);
//
// }
//
// return chartsDataList;
// }
public static List<ChartsData> getResponseTimeChartData(String jtlString) {
Map<String, Object> activeThreadMap = getResultDataMap(jtlString, new ActiveThreadsGraphConsumer());
Map<String, Object> responseTimeMap = getResultDataMap(jtlString, new ResponseTimeOverTimeGraphConsumer());
......@@ -463,40 +366,52 @@ public class JtlResolver {
}
public static void mapResolver(Map<String, Object> map, List list, String seriesName) {
// ThreadGroup-1
for (String key : map.keySet()) {
MapResultData mapResultData = (MapResultData) map.get(key);
ResultData maxY = mapResultData.getResult("maxY");
ListResultData series = (ListResultData) mapResultData.getResult("series");
if (series.getSize() > 0) {
MapResultData resultData = (MapResultData) series.get(0);
ListResultData data = (ListResultData) resultData.getResult("data");
if (data.getSize() > 0) {
for (int i = 0; i < data.getSize(); i++) {
ListResultData listResultData = (ListResultData) data.get(i);
String result = listResultData.accept(new JsonizerVisitor());
result = result.substring(1, result.length() - 1);
String[] split = result.split(",");
ChartsData chartsData = new ChartsData();
BigDecimal bigDecimal = new BigDecimal(split[0]);
String timeStamp = bigDecimal.toPlainString();
String time = null;
try {
time = formatDate(stampToDate(DATE_TIME_PATTERN, timeStamp));
} catch (ParseException e) {
e.printStackTrace();
for (int j = 0; j < series.getSize(); j++) {
MapResultData resultData = (MapResultData) series.get(j);
// data, isOverall, label, isController
ListResultData data = (ListResultData) resultData.getResult("data");
ValueResultData label = (ValueResultData) resultData.getResult("label");
if (data.getSize() > 0) {
for (int i = 0; i < data.getSize(); i++) {
ListResultData listResultData = (ListResultData) data.get(i);
String result = listResultData.accept(new JsonizerVisitor());
result = result.substring(1, result.length() - 1);
String[] split = result.split(",");
ChartsData chartsData = new ChartsData();
BigDecimal bigDecimal = new BigDecimal(split[0]);
String timeStamp = bigDecimal.toPlainString();
String time = null;
try {
time = formatDate(stampToDate(DATE_TIME_PATTERN, timeStamp));
} catch (ParseException e) {
e.printStackTrace();
}
chartsData.setxAxis(time);
chartsData.setyAxis(new BigDecimal(split[1].trim()));
if (series.getSize() == 1) {
chartsData.setGroupName(seriesName);
} else {
chartsData.setGroupName((String) label.getValue());
}
list.add(chartsData);
}
chartsData.setxAxis(time);
chartsData.setyAxis(new BigDecimal(split[1].trim()));
chartsData.setGroupName(seriesName);
list.add(chartsData);
}
}
}
}
}
public static Map<String, Object> getResultDataMap(String jtlString, AbstractOverTimeGraphConsumer timeGraphConsumer) {
int row = 0;
AbstractOverTimeGraphConsumer abstractOverTimeGraphConsumer = timeGraphConsumer;
abstractOverTimeGraphConsumer.setGranularity(60000);
// 使用反射获取properties
......@@ -512,7 +427,7 @@ public class JtlResolver {
while (tokenizer.hasMoreTokens()) {
String line = tokenizer.nextToken();
String[] data = line.split(",", -1);
Sample sample = new Sample(0, sampleMetaData, data);
Sample sample = new Sample(row++, sampleMetaData, data);
abstractOverTimeGraphConsumer.consume(sample, 0);
}
abstractOverTimeGraphConsumer.stopConsuming();
......
......@@ -224,7 +224,7 @@ public class TestCaseService {
}
private List<TestCaseExcelData> generateExportTemplate() {
List<TestCaseExcelData> list = new ArrayList<TestCaseExcelData>();
List<TestCaseExcelData> list = new ArrayList<>();
StringBuilder path = new StringBuilder("");
List<String> types = Arrays.asList("functional", "performance", "api");
List<String> methods = Arrays.asList("manual", "auto");
......
package io.metersphere.service;
import io.metersphere.base.domain.TestPlanTestCase;
import io.metersphere.base.domain.TestPlanTestCaseExample;
import io.metersphere.base.mapper.TestPlanTestCaseMapper;
import io.metersphere.base.mapper.ext.ExtTestCaseMapper;
import io.metersphere.commons.utils.BeanUtils;
import io.metersphere.controller.request.testcase.QueryTestCaseRequest;
import io.metersphere.controller.request.testcase.TestCaseBatchRequest;
import io.metersphere.controller.request.testplancase.QueryTestPlanCaseRequest;
import io.metersphere.dto.TestPlanCaseDTO;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
......@@ -21,11 +26,10 @@ public class TestPlanTestCaseService {
@Resource
ExtTestCaseMapper extTestCaseMapper;
public List<TestPlanCaseDTO> getTestPlanCases(QueryTestCaseRequest request) {
public List<TestPlanCaseDTO> getTestPlanCases(QueryTestPlanCaseRequest request) {
return extTestCaseMapper.getTestPlanTestCases(request);
}
public void editTestCase(TestPlanTestCase testPlanTestCase) {
testPlanTestCase.setUpdateTime(System.currentTimeMillis());
testPlanTestCaseMapper.updateByPrimaryKeySelective(testPlanTestCase);
......@@ -34,4 +38,15 @@ public class TestPlanTestCaseService {
public int deleteTestCase(Integer id) {
return testPlanTestCaseMapper.deleteByPrimaryKey(id);
}
public void editTestCaseBath(TestCaseBatchRequest request) {
TestPlanTestCaseExample testPlanTestCaseExample = new TestPlanTestCaseExample();
testPlanTestCaseExample.createCriteria().andIdIn(request.getIds());
TestPlanTestCase testPlanTestCase = new TestPlanTestCase();
BeanUtils.copyBean(testPlanTestCase, request);
testPlanTestCaseMapper.updateByExampleSelective(
testPlanTestCase,
testPlanTestCaseExample);
}
}
error_lang_invalid=语言参数错误
load_test_already_exists=测试名称不能重复
project_name_is_null=项目名称不能为空
project_name_already_exists=项目名称已存在
workspace_name_is_null=工作空间名不能为空
workspace_name_already_exists=工作空间名已存在
workspace_does_not_belong_to_user=当前工作空间不属于当前用户
organization_does_not_belong_to_user=当前组织不属于当前用户
file_cannot_be_null=文件不能为空!
edit_load_test_not_found=无法编辑测试,未找到测试:
run_load_test_not_found=无法运行测试,未找到测试:
run_load_test_file_not_found=无法运行测试,无法获取测试文件元信息,测试ID:
run_load_test_file_content_not_found=无法运行测试,无法获取测试文件内容,测试ID:
run_load_test_file_init_error=无法运行测试,初始化运行环境失败,测试ID:
load_test_is_running=测试正在运行, 请等待
node_deep_limit=节点深度不超过5层!
no_nodes_message=没有节点信息
duplicate_node_ip=节点 IP 重复
only_one_k8s=只能添加一个 K8s
organization_id_is_null=组织 ID 不能为空
max_thread_insufficient=并发用户数超额
error_lang_invalid=\u8BED\u8A00\u53C2\u6570\u9519\u8BEF
load_test_already_exists=\u6D4B\u8BD5\u540D\u79F0\u4E0D\u80FD\u91CD\u590D
project_name_is_null=\u9879\u76EE\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A
project_name_already_exists=\u9879\u76EE\u540D\u79F0\u5DF2\u5B58\u5728
workspace_name_is_null=\u5DE5\u4F5C\u7A7A\u95F4\u540D\u4E0D\u80FD\u4E3A\u7A7A
workspace_name_already_exists=\u5DE5\u4F5C\u7A7A\u95F4\u540D\u5DF2\u5B58\u5728
workspace_does_not_belong_to_user=\u5F53\u524D\u5DE5\u4F5C\u7A7A\u95F4\u4E0D\u5C5E\u4E8E\u5F53\u524D\u7528\u6237
organization_does_not_belong_to_user=\u5F53\u524D\u7EC4\u7EC7\u4E0D\u5C5E\u4E8E\u5F53\u524D\u7528\u6237
file_cannot_be_null=\u6587\u4EF6\u4E0D\u80FD\u4E3A\u7A7A\uFF01
edit_load_test_not_found=\u65E0\u6CD5\u7F16\u8F91\u6D4B\u8BD5\uFF0C\u672A\u627E\u5230\u6D4B\u8BD5\uFF1A
run_load_test_not_found=\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5\uFF0C\u672A\u627E\u5230\u6D4B\u8BD5\uFF1A
run_load_test_file_not_found=\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5\uFF0C\u65E0\u6CD5\u83B7\u53D6\u6D4B\u8BD5\u6587\u4EF6\u5143\u4FE1\u606F\uFF0C\u6D4B\u8BD5ID\uFF1A
run_load_test_file_content_not_found=\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5\uFF0C\u65E0\u6CD5\u83B7\u53D6\u6D4B\u8BD5\u6587\u4EF6\u5185\u5BB9\uFF0C\u6D4B\u8BD5ID\uFF1A
run_load_test_file_init_error=\u65E0\u6CD5\u8FD0\u884C\u6D4B\u8BD5\uFF0C\u521D\u59CB\u5316\u8FD0\u884C\u73AF\u5883\u5931\u8D25\uFF0C\u6D4B\u8BD5ID\uFF1A
load_test_is_running=\u6D4B\u8BD5\u6B63\u5728\u8FD0\u884C, \u8BF7\u7B49\u5F85
node_deep_limit=\u8282\u70B9\u6DF1\u5EA6\u4E0D\u8D85\u8FC75\u5C42\uFF01
no_nodes_message=\u6CA1\u6709\u8282\u70B9\u4FE1\u606F
duplicate_node_ip=\u8282\u70B9 IP \u91CD\u590D
only_one_k8s=\u53EA\u80FD\u6DFB\u52A0\u4E00\u4E2A K8s
organization_id_is_null=\u7EC4\u7EC7 ID \u4E0D\u80FD\u4E3A\u7A7A
max_thread_insufficient=\u5E76\u53D1\u7528\u6237\u6570\u8D85\u989D
\ No newline at end of file
......@@ -26,13 +26,14 @@
</ms-api-collapse-item>
</ms-api-collapse>
</div>
<el-button class="scenario-create" type="primary" size="mini" icon="el-icon-plus" plain @click="create"/>
<el-button class="scenario-create" type="primary" size="mini" icon="el-icon-plus" plain
@click="createScenario"/>
</el-aside>
<el-main class="scenario-main">
<div class="scenario-form">
<ms-api-scenario-form :scenario="selected"></ms-api-scenario-form>
<ms-api-request-form :request="selected"></ms-api-request-form>
<ms-api-scenario-form :scenario="selected" v-if="isScenario"></ms-api-scenario-form>
<ms-api-request-form :request="selected" v-if="isRequest"></ms-api-request-form>
</div>
</el-main>
</el-container>
......@@ -49,6 +50,7 @@
import MsApiRequest from "./components/ApiRequest";
import MsApiRequestForm from "./components/ApiRequestForm";
import MsApiScenarioForm from "./components/ApiScenarioForm";
import {Scenario, Request} from "./model/APIModel";
export default {
name: "MsApiScenarioConfig",
......@@ -64,6 +66,17 @@
},
methods: {
createScenario: function () {
let scenario = new Scenario({name: "Scenario"});
this.scenarios.push(scenario);
},
deleteScenario: function (index) {
this.scenarios.splice(index, 1);
if (this.scenarios.length === 0) {
this.createScenario();
this.select(this.scenarios[0]);
}
},
handleChange: function (index) {
this.select(this.scenarios[index]);
},
......@@ -74,35 +87,23 @@
break;
}
},
createScenario: function () {
return {
type: "Scenario",
name: "Scenario",
address: "",
file: "",
variables: [],
headers: [],
requests: []
}
},
deleteScenario: function (index) {
this.scenarios.splice(index, 1);
if (this.scenarios.length === 0) {
this.create();
}
},
create: function () {
let scenario = this.createScenario();
this.scenarios.push(scenario);
},
select: function (obj) {
this.selected = obj;
}
},
computed: {
isScenario() {
return this.selected instanceof Scenario;
},
isRequest() {
return this.selected instanceof Request;
}
},
created() {
if (this.scenarios.length === 0) {
this.create();
this.createScenario();
this.select(this.scenarios[0]);
}
}
......@@ -137,6 +138,9 @@
}
.scenario-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
width: 100%;
}
......
<template>
<div>
<el-row :gutter="10" type="flex" align="middle">
<el-col :span="4">
<el-select class="assertion-item" v-model="regex.subject" size="small"
:placeholder="$t('api_test.request.assertions.select_subject')">
<el-option label="Http-Code" value="HTTP-CODE"></el-option>
<el-option label="Header" value="HEADER"></el-option>
<el-option label="Body" value="BODY"></el-option>
</el-select>
</el-col>
<el-col :span="19">
<el-input v-model="regex.expression" maxlength="255" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.expression')"/>
</el-col>
<el-col :span="1" class="assertion-btn">
<el-button type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button type="primary" size="small" icon="el-icon-plus" plain @click="add" v-else/>
</el-col>
</el-row>
</div>
</template>
<script>
import {Regex} from "../model/APIModel";
export default {
name: "MsApiAssertionRegex",
props: {
regex: {
type: Regex,
default: () => {
return new Regex();
}
},
edit: {
type: Boolean,
default: false
},
index: Number,
list: Array
},
methods: {
add: function () {
this.list.push(new Regex(this.regex));
},
remove: function () {
this.list.splice(this.index, 1);
}
}
}
</script>
<style scoped>
.assertion-item {
width: 100%;
}
.assertion-btn {
text-align: center;
}
</style>
<template>
<div>
<el-row :gutter="10" align="middle">
<el-col :span="23">
<el-input v-model="time" step="100" size="small" type="number"
:placeholder="$t('api_test.request.assertions.response_in_time')"/>
</el-col>
<el-col :span="1" class="assertion-btn">
<el-button type="danger" size="mini" icon="el-icon-delete" circle @click="remove" v-if="edit"/>
<el-button type="primary" size="small" icon="el-icon-plus" plain @click="add" v-else/>
</el-col>
</el-row>
</div>
</template>
<script>
import {ResponseTime} from "../model/APIModel";
export default {
name: "MsApiAssertionResponseTime",
props: {
edit: Boolean,
responseTime: ResponseTime
},
data() {
return {
time: this.responseTime.responseInTime
}
},
methods: {
add: function () {
this.remove();
setTimeout(() => {
this.responseTime.responseInTime = this.time;
})
},
remove: function () {
this.responseTime.responseInTime = null;
}
}
}
</script>
<style scoped>
.assertion-btn {
text-align: center;
}
</style>
<template>
<div>
<el-row :gutter="10">
<el-col :span="4">
<el-select class="assertion-item" v-model="subject" size="small"
:placeholder="$t('api_test.request.assertions.select_subject')">
<el-option label="Http-Code" value="HTTP-CODE"></el-option>
<el-option label="Header" value="HEADER"></el-option>
<el-option label="Body" value="BODY"></el-option>
</el-select>
</el-col>
<el-col :span="4">
<el-select class="assertion-item" v-model="condition" size="small"
:placeholder="$t('api_test.request.assertions.select_contains')">
<el-option :label="$t('api_test.request.assertions.contains')" value="CONTAINS"></el-option>
<el-option :label="$t('api_test.request.assertions.not_contains')" value="NOT_CONTAINS"></el-option>
<el-option :label="$t('api_test.request.assertions.equals')" value="EQUALS"></el-option>
<el-option :label="$t('api_test.request.assertions.start_with')" value="START_WITH"></el-option>
<el-option :label="$t('api_test.request.assertions.end_with')" value="END_WITH"></el-option>
</el-select>
</el-col>
<el-col :span="15">
<el-input v-model="value" maxlength="255" size="small" show-word-limit
:placeholder="$t('api_test.request.assertions.value')"/>
</el-col>
<el-col :span="1">
<el-button type="primary" size="small" icon="el-icon-plus" plain @click="add"/>
</el-col>
</el-row>
</div>
</template>
<script>
import {Regex} from "../model/APIModel";
export default {
name: "MsApiAssertionText",
props: {
list: Array
},
data() {
return {
subject: "",
condition: "",
value: ""
}
},
methods: {
add: function () {
this.list.push(this.toRegex());
},
toRegex: function () {
let expression = "";
let description = "";
switch (this.condition) {
case "CONTAINS":
expression = ".*" + this.value + ".*";
description = "contains: " + this.value;
break;
case "NOT_CONTAINS":
expression = "^((?!" + this.value + ").)*$";
description = "not contains: " + this.value;
break;
case "EQUALS":
expression = "^" + this.value + "$";
description = "equals: " + this.value;
break;
case "START_WITH":
expression = "^" + this.value;
description = "start with: " + this.value;
break;
case "END_WITH":
expression = this.value + "$";
description = "end with: " + this.value;
break;
}
return new Regex({
subject: this.subject,
expression: expression,
description: description
}
);
}
}
}
</script>
<style scoped>
.assertion-item {
width: 100%;
}
</style>
<template>
<div>
<el-row :gutter="10">
<el-col :span="4">
<el-select class="assertion-item" v-model="type" :placeholder="$t('api_test.request.assertions.select_type')"
size="small">
<el-option :label="$t('api_test.request.assertions.text')" :value="options.TEXT"/>
<el-option :label="$t('api_test.request.assertions.regex')" :value="options.REGEX"/>
<el-option :label="$t('api_test.request.assertions.response_time')" :value="options.RESPONSE_TIME"/>
</el-select>
</el-col>
<el-col :span="20">
<ms-api-assertion-text :list="assertions.regex" v-if="type === options.TEXT"/>
<ms-api-assertion-regex :list="assertions.regex" v-if="type === options.REGEX"/>
<ms-api-assertion-response-time :response-time="assertions.responseTime" v-if="type === options.RESPONSE_TIME"/>
</el-col>
</el-row>
<ms-api-assertions-edit :assertions="assertions"/>
</div>
</template>
<script>
import MsApiAssertionText from "./ApiAssertionText";
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionResponseTime from "./ApiAssertionResponseTime";
import {ASSERTION_TYPE, Assertions, Regex} from "../model/APIModel";
import MsApiAssertionsEdit from "./ApiAssertionsEdit";
export default {
name: "MsApiAssertions",
components: {MsApiAssertionsEdit, MsApiAssertionResponseTime, MsApiAssertionRegex, MsApiAssertionText},
props: {
assertions: Assertions
},
data() {
return {
options: ASSERTION_TYPE,
type: "",
}
},
methods: {
createRegex: function () {
return new Regex();
}
}
}
</script>
<style scoped>
.assertion-item {
width: 100%;
}
</style>
<template>
<div>
<div class="assertion-item-editing regex" v-if="assertions.regex.length > 0">
<div>
{{$t("api_test.request.assertions.regex")}}
</div>
<div class="regex-item" v-for="(regex, index) in assertions.regex" :key="index">
<ms-api-assertion-regex :list="assertions.regex" :regex="regex" :edit="true" :index="index"/>
</div>
</div>
<div class="assertion-item-editing response-time" v-if="assertions.responseTime.isValid()">
<div>
{{$t("api_test.request.assertions.response_time")}}
</div>
<ms-api-assertion-response-time :response-time="assertions.responseTime" :edit="true"/>
</div>
</div>
</template>
<script>
import MsApiAssertionRegex from "./ApiAssertionRegex";
import MsApiAssertionResponseTime from "./ApiAssertionResponseTime";
import {Assertions} from "../model/APIModel";
export default {
name: "MsApiAssertionsEdit",
components: {MsApiAssertionResponseTime, MsApiAssertionRegex},
props: {
assertions: Assertions
}
}
</script>
<style scoped>
.assertion-item-editing {
padding-left: 10px;
margin-top: 10px;
}
.assertion-item-editing.regex {
border-left: 2px solid #7B0274;
}
.assertion-item-editing.response-time {
border-left: 2px solid #DD0240;
}
.regex-item {
margin-top: 10px;
}
</style>
<template>
<div>
<el-radio-group v-model="body.type" size="mini">
<el-radio-button label="kv">
<el-radio-button :label="type.KV">
{{$t('api_test.request.body_kv')}}
</el-radio-button>
<el-radio-button label="text">
<el-radio-button :label="type.TEXT">
{{$t('api_test.request.body_text')}}
</el-radio-button>
</el-radio-group>
<ms-api-key-value :items="body.kvs" v-if="isKV"/>
<ms-api-key-value :items="body.kvs" v-if="body.isKV()"/>
<el-input class="textarea" type="textarea" v-model="body.text" :autosize="{ minRows: 10, maxRows: 25}" resize="none"
v-else/>
......@@ -18,23 +18,26 @@
<script>
import MsApiKeyValue from "./ApiKeyValue";
import {Body, BODY_TYPE} from "../model/APIModel";
export default {
name: "MsApiBody",
components: {MsApiKeyValue},
props: {
body: Object
body: Body
},
data() {
return {};
return {
type: BODY_TYPE
};
},
methods: {},
computed: {
isKV() {
return this.body.type === "kv";
created() {
if (this.body.type === null) {
this.body.type = BODY_TYPE.KV;
}
}
}
......
......@@ -12,7 +12,8 @@
<el-input v-model="item.value" placeholder="Value" size="small" maxlength="100" @change="check"/>
</el-col>
<el-col :span="1">
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"/>
<el-button size="mini" class="el-icon-delete-solid" circle @click="remove(index)"
:disabled="isDisable(index)"/>
</el-col>
</el-row>
</div>
......@@ -20,6 +21,8 @@
</template>
<script>
import {KeyValue} from "../model/APIModel";
export default {
name: "MsApiKeyValue",
......@@ -29,23 +32,20 @@
},
methods: {
create: function () {
return {
key: "",
value: ""
}
add: function () {
this.items.push(new KeyValue());
},
remove: function (index) {
this.items.splice(index, 1);
if (this.items.length === 0) {
this.items.push(this.create());
this.add();
}
},
check: function () {
let isNeedCreate = true;
let removeIndex = -1;
this.items.forEach((item, index) => {
if (item.key === "" && item.value === "") {
if (!item.key && !item.value) {
// 多余的空行
if (index !== this.items.length - 1) {
removeIndex = index;
......@@ -55,18 +55,21 @@
}
});
if (isNeedCreate) {
this.items.push(this.create());
this.add();
}
if (removeIndex !== -1) {
this.remove(removeIndex);
}
// TODO 检查key重复
},
isDisable: function (index) {
return this.items.length - 1 === index;
}
},
created() {
if (this.items.length === 0) {
this.items.push(this.create());
this.add();
}
}
}
......
......@@ -2,28 +2,30 @@
<div class="request-container">
<div class="request-item" v-for="(request, index) in requests" :key="index" @click="select(request)"
:class="{'selected': isSelected(request)}">
<span class="request-method">
{{request.method}}
</span>
<span class="request-name">
{{request.name}}
</span>
<span class="request-btn">
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link el-icon-more"></span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type: 'copy', index: index}">复制请求</el-dropdown-item>
<el-dropdown-item :command="{type: 'delete', index: index}">删除请求</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</span>
<el-row type="flex">
<div class="request-method">
{{request.method}}
</div>
<div class="request-name">
{{request.name}}
</div>
<div class="request-btn">
<el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link el-icon-more"></span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item :command="{type: 'copy', index: index}">复制请求</el-dropdown-item>
<el-dropdown-item :command="{type: 'delete', index: index}">删除请求</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</div>
</el-row>
</div>
<el-button class="request-create" type="primary" size="mini" icon="el-icon-plus" plain @click="create"/>
<el-button class="request-create" type="primary" size="mini" icon="el-icon-plus" plain @click="createRequest"/>
</div>
</template>
<script>
import {generateId} from 'element-ui/src/utils/util';
import {Request} from "../model/APIModel";
export default {
name: "MsApiRequest",
......@@ -34,7 +36,7 @@
data() {
return {
selected: 0
selected: 0,
}
},
......@@ -47,20 +49,10 @@
},
methods: {
create: function () {
let request = this.createRequest();
createRequest: function () {
let request = new Request({method: "GET"});
this.requests.push(request);
},
handleCommand: function (command) {
switch (command.type) {
case "copy":
this.copyRequest(command.index);
break;
case "delete":
this.deleteRequest(command.index);
break;
}
},
copyRequest: function (index) {
let request = this.requests[index];
this.requests.push(JSON.parse(JSON.stringify(request)));
......@@ -68,24 +60,17 @@
deleteRequest: function (index) {
this.requests.splice(index, 1);
if (this.requests.length === 0) {
this.create();
this.createRequest();
}
},
createRequest: function () {
return {
randomId: generateId(),
type: "Request",
method: "GET",
name: "",
parameters: [],
headers: [],
body: {
type: "kv",
kvs: [],
text: ""
},
assertions: [],
extract: []
handleCommand: function (command) {
switch (command.type) {
case "copy":
this.copyRequest(command.index);
break;
case "delete":
this.deleteRequest(command.index);
break;
}
},
select: function (request) {
......@@ -96,7 +81,7 @@
created() {
if (this.requests.length === 0) {
this.create();
this.createRequest();
this.select(this.requests[0]);
}
}
......@@ -126,11 +111,13 @@
.request-method {
padding: 0 5px;
width: 60px;
color: #1E90FF;
}
.request-name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 14px;
width: 100%;
}
......
<template>
<el-form :model="request" :rules="rules" ref="request" label-width="100px" label-position="left" v-if="isRequest">
<el-form :model="request" :rules="rules" ref="request" label-width="100px">
<el-form-item :label="$t('api_test.request.name')" prop="name">
<el-input v-model="request.name"></el-input>
<el-input v-model="request.name" maxlength="100"></el-input>
</el-form-item>
<el-form-item :label="$t('api_test.request.url')" prop="url">
<el-input v-model="request.url" :placeholder="$t('api_test.request.url_describe')">
<el-input v-model="request.url" maxlength="100" :placeholder="$t('api_test.request.url_description')">
<el-select v-model="request.method" slot="prepend" class="request-method-select">
<el-option label="GET" value="GET"></el-option>
<el-option label="POST" value="POST"></el-option>
......@@ -29,8 +29,8 @@
<el-tab-pane :label="$t('api_test.request.body')" name="body" v-if="isNotGet">
<ms-api-body :body="request.body"/>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.assertions')" name="assertions" v-if="false">
TODO
<el-tab-pane :label="$t('api_test.request.assertions.label')" name="assertions">
<ms-api-assertions :assertions="request.assertions"></ms-api-assertions>
</el-tab-pane>
<el-tab-pane :label="$t('api_test.request.extract')" name="extract" v-if="false">
TODO
......@@ -42,10 +42,11 @@
<script>
import MsApiKeyValue from "./ApiKeyValue";
import MsApiBody from "./ApiBody";
import MsApiAssertions from "./ApiAssertions";
export default {
name: "MsApiRequestForm",
components: {MsApiBody, MsApiKeyValue},
components: {MsApiAssertions, MsApiBody, MsApiKeyValue},
props: {
request: Object
},
......@@ -53,14 +54,19 @@
data() {
return {
activeName: "parameters",
rules: {}
rules: {
name: [
{required: true, message: this.$t('api_test.scenario.input_name'), trigger: 'blur'},
{max: 100, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'}
],
url: [
{max: 100, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'}
]
}
}
},
computed: {
isRequest() {
return this.request.type === "Request";
},
isNotGet() {
return this.request.method !== "GET";
}
......
<template>
<el-form :model="scenario" :rules="rules" ref="scenario" label-width="100px" label-position="left" v-if="isScenario">
<el-form :model="scenario" :rules="rules" ref="scenario" label-width="100px">
<el-form-item :label="$t('api_test.scenario.name')" prop="name">
<el-input v-model="scenario.name"></el-input>
<el-input v-model="scenario.name" maxlength="100"></el-input>
</el-form-item>
<el-form-item :label="$t('api_test.scenario.base_url')" prop="url">
<el-input :placeholder="$t('api_test.scenario.base_url_describe')" v-model="scenario.url"></el-input>
<el-input :placeholder="$t('api_test.scenario.base_url_description')" v-model="scenario.url" maxlength="100"/>
</el-form-item>
<el-tabs v-model="activeName">
......@@ -32,15 +32,17 @@
data() {
return {
activeName: "variables",
rules: {}
rules: {
name: [
{required: true, message: this.$t('api_test.scenario.input_name'), trigger: 'blur'},
{max: 100, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'}
],
url: [
{max: 100, message: this.$t('commons.input_limit', [0, 100]), trigger: 'blur'}
]
}
}
},
computed: {
isScenario() {
return this.scenario.type === "Scenario";
}
},
}
}
</script>
......
import {generateId} from "element-ui/src/utils/util";
const assign = function (obj, options) {
for (let name in options) {
if (options.hasOwnProperty(name)) {
obj[name] = options[name];
}
}
}
export const BODY_TYPE = {
KV: "KV",
TEXT: "TEXT"
}
export const ASSERTION_TYPE = {
TEXT: "TEXT",
REGEX: "REGEX",
RESPONSE_TIME: "RESPONSE_TIME"
}
export class Scenario {
constructor(options) {
this.name = null;
this.url = null;
this.variables = [];
this.headers = [];
this.requests = [];
assign(this, options);
}
}
export class Request {
constructor(options) {
this.randomId = generateId();
this.name = null;
this.url = null;
this.method = null;
this.parameters = [];
this.headers = [];
this.body = new Body();
this.assertions = new Assertions();
this.extract = [];
assign(this, options);
}
}
export class Body {
constructor(options) {
this.type = null;
this.text = null;
this.kvs = [];
assign(this, options);
}
isKV() {
return this.type === BODY_TYPE.KV;
}
}
export class KeyValue {
constructor(options) {
this.key = null;
this.value = null;
assign(this, options);
}
}
export class Assertions {
constructor(options) {
this.text = [];
this.regex = [];
this.responseTime = new ResponseTime();
assign(this, options);
}
}
class AssertionType {
constructor(type) {
this.type = type;
}
}
export class Text extends AssertionType {
constructor(options) {
super(ASSERTION_TYPE.TEXT);
this.subject = null;
this.condition = null;
this.value = null;
assign(this, options);
}
}
export class Regex extends AssertionType {
constructor(options) {
super(ASSERTION_TYPE.REGEX);
this.subject = null;
this.expression = null;
this.description = null;
assign(this, options);
}
}
export class ResponseTime extends AssertionType {
constructor(options) {
super(ASSERTION_TYPE.RESPONSE_TIME);
this.responseInTime = null;
assign(this, options);
}
isValid() {
return this.responseInTime !== null && this.responseInTime > 0;
}
}
<template>
<div class="dialog-footer">
<el-button @click="cancel">取 消</el-button>
<el-button type="primary" @click="confirm">确 定</el-button>
</div>
</template>
<script>
export default {
name: "MsDialogFooter",
methods: {
cancel() {
this.$emit("cancel");
},
confirm() {
this.$emit("confirm");
}
}
}
</script>
<style scoped>
</style>
<template>
<el-tooltip :disabled="disabled"
:content="tip"
placement="bottom"
:effect="effect">
<el-button @click="exec()"
circle
:type="type"
:icon="icon"
:size="size"/>
</el-tooltip>
</template>
<script>
export default {
name: "MsTipButton",
props: {
tip: String,
icon: {
type: String,
default: 'el-icon-question'
},
type: {
type: String,
default: null
},
effect: {
type: String,
default: 'dark'
},
size: {
type: String,
default: 'mini'
},
disabled: {
type: Boolean,
default: false
}
},
methods: {
exec() {
this.$emit('click');
}
}
}
</script>
<style scoped>
</style>
......@@ -157,7 +157,8 @@
this.$get("/performance/report/content/res_chart/" + this.id, res => {
let data = res.data;
let userList = data.filter(m => m.groupName === "users").map(m => m.yAxis);
let responseTimeList = data.filter(m => m.groupName === "responseTime").map(m => m.yAxis);
let responseTimeList = data.filter(m => m.groupName != "users").map(m => m.yAxis);
let responseGroupNameList = data.filter(m => m.groupName != "users").map(m => m.groupName);
let userMax = this._getChartMax(userList);
let resMax = this._getChartMax(responseTimeList);
let resOption = {
......@@ -171,7 +172,24 @@
},
tooltip: {
show: true,
trigger: 'axis'
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: {},
......@@ -197,14 +215,12 @@
{
name: 'users',
color: '#0CA74A',
},
{
name: "responseTime",
yAxisIndex: '1',
color: '#99743C',
}
]
}
responseGroupNameList.forEach(item => {
setting["series"].splice(0, 0, {name: item, yAxisIndex: '1'})
})
this.resOption = this.generateOption(resOption, data, setting);
})
},
......@@ -229,10 +245,11 @@
legend.push(name)
series[name] = []
}
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, Math.round(item.yAxis.toFixed(2))]);
series[name].splice(xAxis.indexOf(item.xAxis), 0, [item.xAxis, item.yAxis.toFixed(2)]);
})
this.$set(option.legend, "data", legend);
this.$set(option.legend, "bottom", 10);
this.$set(option.legend, "type", "scroll");
this.$set(option.legend, "bottom", "10px");
this.$set(option.xAxis, "data", xAxis);
for (let name in series) {
let d = series[name];
......@@ -259,9 +276,6 @@
_getChartMax(arr) {
const max = Math.max(...arr);
return Math.ceil(max / 4.5) * 5;
},
_arraySort(a, b) {
return a[0] > b[0];
}
},
watch: {
......
......@@ -89,23 +89,8 @@
</el-table-column>
</el-table>
<div>
<el-row>
<el-col :span="22" :offset="1">
<div class="table-page">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="[5, 10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</el-col>
</el-row>
</div>
<ms-table-pagination :change="initTableData" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-card>
</el-main>
......@@ -116,22 +101,21 @@
import MsCreateBox from '../../../settings/CreateBox';
import TestCaseImport from '../components/TestCaseImport';
import TestCaseExport from '../components/TestCaseExport';
import MsTablePagination from '../../../../components/common/pagination/TablePagination';
export default {
name: "TestCaseList",
components: {MsCreateBox, TestCaseImport, TestCaseExport},
components: {MsCreateBox, TestCaseImport, TestCaseExport, MsTablePagination},
data() {
return {
result: {},
deletePath: "/test/case/delete",
condition: "",
tableData: [],
multipleSelection: [],
currentPage: 1,
pageSize: 5,
total: 0,
loadingRequire: {project: true, testCase: true},
testId: null
}
},
props: {
......@@ -157,7 +141,6 @@
if (this.currentProject) {
param.projectId = this.currentProject.id;
this.result = this.$post(this.buildPagePath('/test/case/list'), param, response => {
this.loadingRequire.testCase = false;
let data = response.data;
this.total = data.itemCount;
this.tableData = data.listObject;
......@@ -173,17 +156,6 @@
testCaseCreate() {
this.$emit('openTestCaseEditDialog');
},
handleSizeChange(size) {
this.pageSize = size;
this.initTableData();
},
handleCurrentChange(current) {
this.currentPage = current;
this.initTableData();
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleEdit(testCase) {
this.$emit('testCaseEdit', testCase);
},
......
<template>
<el-row type="flex" justify="start" :gutter="20">
<el-col>
<el-button type="success" round
:icon="status == 'Pass' ? 'el-icon-check' : ''"
@click="setStatus('Pass')"> {{$t('test_track.pass')}}</el-button>
</el-col>
<el-col >
<el-button type="danger" round
:icon="status == 'Failure' ? 'el-icon-check' : ''"
@click="setStatus('Failure')"> {{$t('test_track.failure')}}</el-button>
</el-col>
<el-col >
<el-button type="warning" round
:icon="status == 'Blocking' ? 'el-icon-check' : ''"
@click="setStatus('Blocking')"> {{$t('test_track.blocking')}}</el-button>
</el-col>
<el-col >
<el-button type="info" round
:icon="status == 'Skip' ? 'el-icon-check' : ''"
@click="setStatus('Skip')"> {{$t('test_track.skip')}}</el-button>
</el-col>
</el-row>
</template>
<script>
export default {
name: "TestPlanTestCaseStatusButton",
data() {
return {
}
},
props: {
status: {
type: String
}
},
methods: {
setStatus(status) {
this.$emit('statusChange', status);
}
}
}
</script>
<style scoped>
.el-row {
width: 50%;
}
</style>
<template>
<el-dialog title="更改执行人"
:visible.sync="executorEditVisible"
width="20%">
<el-select v-model="executor" placeholder="请选择活动区域">
<el-option v-for="item in executorOptions" :key="item.id"
:label="item.name" :value="item.name"></el-option>
</el-select>
<template v-slot:footer>
<ms-dialog-footer
@cancel="executorEditVisible = false"
@confirm="saveExecutor"/>
</template>
</el-dialog>
</template>
<script>
import {WORKSPACE_ID} from '../../../../../common/js/constants'
import MsDialogFooter from '../../../common/components/MsDialogFooter'
export default {
name: "executorEdit",
components: {MsDialogFooter},
data() {
return {
executorEditVisible: false,
executor: '',
executorOptions: []
}
},
props: {
selectIds: {
type: Set
}
},
methods: {
setMaintainerOptions() {
let workspaceId = localStorage.getItem(WORKSPACE_ID);
this.$post('/user/ws/member/list/all', {workspaceId:workspaceId}, response => {
this.executorOptions = response.data;
});
},
saveExecutor() {
let param = {};
param.executor = this.executor;
if (this.executor === '') {
this.$message('请选择执行人!');
return;
}
param.ids = [...this.selectIds];
this.$post('/test/plan/case/batch/edit' , param, () => {
this.executor = '';
this.selectIds.clear();
this.$message.success("保存成功");
this.executorEditVisible = false;
this.$emit('refresh');
});
},
openExecutorEdit() {
this.executorEditVisible = true;
this.setMaintainerOptions();
}
}
}
</script>
<style scoped>
</style>
<template>
<el-dialog title="更改执行结果"
:visible.sync="statusEditVisible"
width="30%">
<test-plan-test-case-status-button
@statusChange="statusChange"
:status="status"/>
<template v-slot:footer>
<ms-dialog-footer
@cancel="statusEditVisible = false"
@confirm="saveStatus"/>
</template>
</el-dialog>
</template>
<script>
import TestPlanTestCaseStatusButton from '../common/TestPlanTestCaseStatusButton';
import MsDialogFooter from '../../../common/components/MsDialogFooter'
export default {
name: "statusEdit",
components: {TestPlanTestCaseStatusButton, MsDialogFooter},
data() {
return {
statusEditVisible: false,
status: ''
}
},
props: {
selectIds: {
type: Set
}
},
methods: {
statusChange(status) {
this.status = status;
},
saveStatus() {
let param = {};
if (this.status === '') {
this.$message('请选择执行结果!');
return;
}
param.status = this.status;
param.ids = [...this.selectIds];
this.$post('/test/plan/case/batch/edit' , param, () => {
this.selectIds.clear();
this.status = '';
this.$message.success("保存成功");
this.statusEditVisible = false;
this.$emit('refresh');
});
},
openStatusEdit() {
this.statusEditVisible = true;
}
}
}
</script>
<style scoped>
</style>
......@@ -143,7 +143,6 @@
}
},
close() {
console.log("clear");
this.selectIds.clear();
}
}
......
......@@ -16,7 +16,7 @@
<el-input type="text" size="small" :placeholder="$t('load_test.search_by_name')"
prefix-icon="el-icon-search"
maxlength="60"
v-model="condition" @change="search" clearable/>
v-model="condition" @change="initTableData" clearable/>
</span>
</el-col>
</el-row>
......@@ -88,23 +88,25 @@
</el-table-column>
</el-table>
<div>
<el-row>
<el-col :span="22" :offset="1">
<div class="table-page">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="[5, 10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</el-col>
</el-row>
</div>
<!--<div>-->
<!--<el-row>-->
<!--<el-col :span="22" :offset="1">-->
<!--<div class="table-page">-->
<!--<el-pagination-->
<!--@size-change="handleSizeChange"-->
<!--@current-change="handleCurrentChange"-->
<!--:current-page.sync="currentPage"-->
<!--:page-sizes="[5, 10, 20, 50, 100]"-->
<!--:page-size="pageSize"-->
<!--layout="total, sizes, prev, pager, next, jumper"-->
<!--:total="total">-->
<!--</el-pagination>-->
<!--</div>-->
<!--</el-col>-->
<!--</el-row>-->
<!--</div>-->
<ms-table-pagination :change="initTableData" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-card>
</el-main>
......@@ -113,22 +115,21 @@
<script>
import MsCreateBox from '../../../settings/CreateBox';
import MsTablePagination from '../../../../components/common/pagination/TablePagination';
export default {
name: "TestPlanList",
components: {MsCreateBox},
components: {MsCreateBox, MsTablePagination},
data() {
return {
result: {},
queryPath: "/test/plan/list",
deletePath: "/test/plan/delete",
condition: "",
tableData: [],
multipleSelection: [],
currentPage: 1,
pageSize: 5,
total: 0,
testId: null,
tableData: [],
}
},
created() {
......@@ -146,26 +147,12 @@
this.tableData = data.listObject;
});
},
search() {
this.initTableData();
},
buildPagePath(path) {
return path + "/" + this.currentPage + "/" + this.pageSize;
},
testPlanCreate() {
this.$emit('openTestPlanEditDialog');
},
handleSizeChange(size) {
this.pageSize = size;
this.initTableData();
},
handleCurrentChange(current) {
this.currentPage = current;
this.initTableData();
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleEdit(testPlan) {
this.$emit('testPlanEdit', testPlan);
},
......
......@@ -82,7 +82,7 @@
</el-col>
</el-row>
<el-row >
<el-row>
<el-col :span="15" :offset="1">
<div style="margin-bottom: 5px;">
<span class="cast_label">{{$t('commons.remark')}}:</span>
......@@ -98,28 +98,9 @@
</el-col>
</el-row>
<el-row>
<el-col :offset="1" :span="2">
<el-button type="success" round
:icon="testCase.status == 'Pass' ? 'el-icon-check' : ''"
@click="setTestCaseStatus('Pass')"> {{$t('test_track.pass')}}</el-button>
</el-col>
<el-col :span="2">
<el-button type="danger" round
:icon="testCase.status == 'Failure' ? 'el-icon-check' : ''"
@click="setTestCaseStatus('Failure')"> {{$t('test_track.failure')}}</el-button>
</el-col>
<el-col :span="2">
<el-button type="warning" round
:icon="testCase.status == 'Blocking' ? 'el-icon-check' : ''"
@click="setTestCaseStatus('Blocking')"> {{$t('test_track.blocking')}}</el-button>
</el-col>
<el-col :span="2">
<el-button type="info" round
:icon="testCase.status == 'Skip' ? 'el-icon-check' : ''"
@click="setTestCaseStatus('Skip')"> {{$t('test_track.skip')}}</el-button>
</el-col>
</el-row>
<test-plan-test-case-status-button class="status-button"
@statusChange="statusChange"
:status="testCase.status"/>
<el-row type="flex" justify="end">
<el-col :span="5">
......@@ -136,12 +117,15 @@
</template>
<script>
export default {
import TestPlanTestCaseStatusButton from '../common/TestPlanTestCaseStatusButton';
export default {
name: "TestPlanTestCaseEdit",
data() {
components: {TestPlanTestCaseStatusButton},
data() {
return {
dialog: false,
testCase: {}
testCase: {TestPlanTestCaseStatusButton}
};
},
methods: {
......@@ -151,7 +135,7 @@
cancel() {
this.dialog = false;
},
setTestCaseStatus(status) {
statusChange(status) {
this.testCase.status = status;
},
saveCase() {
......@@ -197,4 +181,8 @@
color: dimgray;
}
.status-button {
padding-left: 4%;
}
</style>
......@@ -4,22 +4,47 @@
<el-card v-loading="result.loading">
<template v-slot:header>
<div>
<el-row type="flex" justify="space-between" align="middle">
<el-col :span="5">
<span class="title">{{$t('test_track.test_case')}}</span>
<el-row type="flex" justify="end">
<el-col>
<span class="title">{{$t('test_track.test_case')}} </span>
<ms-tip-button v-if="!showMyTestCase"
:tip="'我的用例'"
icon="el-icon-s-custom" @click="searchMyTestCase"/>
<ms-tip-button v-if="showMyTestCase"
:tip="'全部用例'"
icon="el-icon-files" @click="searchMyTestCase"/>
</el-col>
<el-col :span="2" :offset="8">
<el-col :offset="8">
<el-button icon="el-icon-connection" size="small" round
@click="$emit('openTestCaseRelevanceDialog')" >{{$t('test_track.relevance_test_case')}}</el-button>
</el-col>
<el-col :span="5">
<el-col>
<el-button icon="el-icon-edit-outline" size="small" round
@click="handleBatch('status')" >更改执行结果</el-button>
</el-col>
<el-col>
<el-button icon="el-icon-user" size="small" round
@click="handleBatch('executor')" >更改执行人</el-button>
</el-col>
<executor-edit
ref="executorEdit"
:select-ids="selectIds"
@refresh="initTableData"/>
<status-edit
ref="statusEdit"
:select-ids="selectIds"
@refresh="initTableData"/>
<el-col>
<span class="search">
<el-input type="text" size="small" :placeholder="$t('load_test.search_by_name')"
prefix-icon="el-icon-search"
maxlength="60"
v-model="condition" @change="search" clearable/>
v-model="condition.name" @change="search" clearable/>
</span>
</el-col>
</el-row>
......@@ -27,8 +52,14 @@
</template>
<el-table
@select-all="handleSelectAll"
@select="handleSelectionChange"
row-key="id"
:data="tableData">
<el-table-column
type="selection"></el-table-column>
<el-table-column
prop="name"
:label="$t('commons.name')">
......@@ -106,23 +137,8 @@
</el-table-column>
</el-table>
<div>
<el-row>
<el-col :span="22" :offset="1">
<div class="table-page">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="currentPage"
:page-sizes="[5, 10, 20, 50, 100]"
:page-size="pageSize"
layout="total, sizes, prev, pager, next, jumper"
:total="total">
</el-pagination>
</div>
</el-col>
</el-row>
</div>
<ms-table-pagination :change="search" :current-page.sync="currentPage" :page-size.sync="pageSize"
:total="total"/>
</el-card>
</el-main>
......@@ -130,22 +146,26 @@
<script>
import PlanNodeTree from './PlanNodeTree';
import ExecutorEdit from './ExecutorEdit';
import StatusEdit from './StatusEdit';
import MsTipButton from '../../../../components/common/components/MsTipButton';
import MsTablePagination from '../../../../components/common/pagination/TablePagination';
import {TokenKey} from '../../../../../common/js/constants';
export default {
name: "TestPlanTestCaseList",
components: {PlanNodeTree},
components: {PlanNodeTree, StatusEdit, ExecutorEdit, MsTipButton, MsTablePagination},
data() {
return {
result: {},
deletePath: "/test/case/delete",
condition: "",
condition: {},
showMyTestCase: false,
tableData: [],
multipleSelection: [],
currentPage: 1,
pageSize: 5,
total: 0,
loadingRequire: {project: true, testCase: true},
testId: null
selectIds: new Set(),
}
},
props:{
......@@ -164,13 +184,11 @@
methods: {
initTableData(nodeIds) {
if (this.planId) {
let param = {
name: this.condition,
};
let param = {};
Object.assign(param, this.condition);
param.nodeIds = nodeIds;
param.planId = this.planId;
this.result = this.$post(this.buildPagePath('/test/plan/case/list'), param, response => {
this.loadingRequire.testCase = false;
let data = response.data;
this.total = data.itemCount;
this.tableData = data.listObject;
......@@ -183,17 +201,6 @@
buildPagePath(path) {
return path + "/" + this.currentPage + "/" + this.pageSize;
},
handleSizeChange(size) {
this.pageSize = size;
this.initTableData();
},
handleCurrentChange(current) {
this.currentPage = current;
this.initTableData();
},
handleSelectionChange(val) {
this.multipleSelection = val;
},
handleEdit(testCase) {
this.$emit('editTestPlanTestCase', testCase);
},
......@@ -217,6 +224,43 @@
type: 'success'
});
});
},
handleSelectAll(selection) {
if(selection.length > 0) {
this.tableData.forEach(item => {
this.selectIds.add(item.id);
});
} else {
this.selectIds.clear();
}
},
handleSelectionChange(selection, row) {
if(this.selectIds.has(row.id)){
this.selectIds.delete(row.id);
} else {
this.selectIds.add(row.id);
}
},
handleBatch(type){
if (this.selectIds.size < 1) {
this.$message.warning('请选择需要操作的数据');
return;
}
if (type === 'executor'){
this.$refs.executorEdit.openExecutorEdit();
} else if (type === 'status'){
this.$refs.statusEdit.openStatusEdit();
}
},
searchMyTestCase() {
this.showMyTestCase = !this.showMyTestCase;
if (this.showMyTestCase) {
let user = JSON.parse(localStorage.getItem(TokenKey));
this.condition.executor = user.id;
} else {
this.condition.executor = null;
}
this.initTableData();
}
}
}
......@@ -231,10 +275,4 @@
}
/*.main-content {*/
/*margin: 0 auto;*/
/*width: 100%;*/
/*max-width: 1200px;*/
/*}*/
</style>
......@@ -45,7 +45,8 @@ export default {
'refresh': '刷新',
'remark': '备注',
'delete': '删除',
'not_filled': '未填写'
'not_filled': '未填写',
'please_select': '请选择',
},
workspace: {
'create': '创建工作空间',
......@@ -169,25 +170,43 @@ export default {
},
api_test: {
scenario: {
input_name: "请输入场景名称",
name: "场景名称",
base_url: "基础URL",
base_url_describe: "基础URL作为所有请求的URL前缀",
base_url_description: "基础URL作为所有请求的URL前缀",
variables: "变量",
headers: "请求头"
},
request: {
input_name: "请输入请求名称",
name: "请求名称",
method: "请求方法",
url: "请求URL",
url_describe: "例如: https://fit2cloud.com",
url_description: "例如: https://fit2cloud.com",
parameters: "请求参数",
parameters_desc: "参数追加到URL,例如https://fit2cloud.com/entries?key1=Value1&Key2=Value2",
headers: "请求头",
body: "请求内容",
body_kv: "键值对",
body_text: "文本",
assertions: "断言",
extract: "提取"
assertions: {
label: "断言",
text: "文本",
regex: "正则",
response_time: "响应时间",
select_type: "请选择类型",
select_subject: "请选择对象",
select_contains: "请选择条件",
contains: "包含",
not_contains: "不包含",
equals: "等于",
start_with: "以...开始",
end_with: "以...结束",
value: "",
expression: "正则表达式",
response_in_time: "响应时间在...毫秒以内",
},
extract: "提取",
}
},
test_track: {
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册