Merge branch 'v1.7'

# Conflicts:
#	backend/src/main/java/io/metersphere/api/service/ApiDefinitionService.java
#	backend/src/main/java/io/metersphere/track/service/TestPlanService.java
#	frontend/src/business/components/api/automation/scenario/common/ApiBaseComponent.vue
#	frontend/src/business/components/api/automation/scenario/variable/VariableList.vue
#	frontend/src/business/components/api/definition/components/case/ApiCaseItem.vue
This commit is contained in:
song.tianyang 2021-02-03 17:11:04 +08:00
commit 683c533db0
78 changed files with 2181 additions and 1408 deletions

View File

@ -314,11 +314,7 @@ public class APITestController {
apiCountResult.setThisWeekExecutedCount(executedInThisWeekCountNumber);
//统计 失败 成功 以及总数
// List<ApiDataCountResult> api_allExecuteResult = apiReportService.countByProjectIdGroupByExecuteResult(projectId);
List<ApiDataCountResult> allExecuteResult = apiScenarioReportService.countByProjectIdGroupByExecuteResult(projectId);
// List<ApiDataCountResult> allExecuteResult = new ArrayList<>();
// allExecuteResult.addAll(api_allExecuteResult);
// allExecuteResult.addAll(scene_allExecuteResult);
apiCountResult.countScheduleExecute(allExecuteResult);
long allCount = apiCountResult.getExecutedCount();

View File

@ -216,4 +216,6 @@ public class ApiDefinitionController {
public String preview(@RequestBody String jsonSchema) {
return JSONSchemaGenerator.getJson(jsonSchema);
}
}

View File

@ -33,4 +33,6 @@ public class ApiBatchRequest extends ApiDefinitionWithBLOBs {
private List<String> unSelectIds;
private String moduleId;
}

View File

@ -98,6 +98,8 @@ public abstract class MsTestElement {
private String refType;
@JSONField(ordinal = 10)
private LinkedList<MsTestElement> hashTree;
@JSONField(ordinal = 11)
private boolean customizeReq;
private MsTestElement parent;
@ -188,15 +190,14 @@ public abstract class MsTestElement {
csvDataSet.setEnabled(true);
csvDataSet.setProperty(TestElement.TEST_CLASS, CSVDataSet.class.getName());
csvDataSet.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestBeanGUI"));
csvDataSet.setName(item.getName());
csvDataSet.setProperty("fileEncoding", item.getEncoding());
csvDataSet.setProperty("variableNames", item.getName());
csvDataSet.setName(StringUtils.isEmpty(item.getName()) ? "CSVDataSet" : item.getName());
csvDataSet.setProperty("fileEncoding", StringUtils.isEmpty(item.getEncoding()) ? "UTF-8" : item.getEncoding());
if (CollectionUtils.isNotEmpty(item.getFiles())) {
csvDataSet.setProperty("filename", BODY_FILE_DIR + "/" + item.getFiles().get(0).getId() + "_" + item.getFiles().get(0).getName());
}
csvDataSet.setIgnoreFirstLine(false);
csvDataSet.setProperty("delimiter", item.getDelimiter());
csvDataSet.setComment(item.getDescription());
csvDataSet.setComment(StringUtils.isEmpty(item.getDescription()) ? "" : item.getDescription());
tree.add(csvDataSet);
});
}
@ -218,7 +219,7 @@ public abstract class MsTestElement {
counterConfig.setVarName(item.getName());
counterConfig.setIncrement(item.getIncrement());
counterConfig.setFormat(item.getValue());
counterConfig.setComment(item.getDescription());
counterConfig.setComment(StringUtils.isEmpty(item.getDescription()) ? "" : item.getDescription());
tree.add(counterConfig);
});
}

View File

@ -6,6 +6,7 @@ import io.metersphere.api.dto.definition.request.ParameterConfig;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.assertions.*;
import org.apache.jmeter.save.SaveService;
import org.apache.jmeter.testelement.TestElement;
@ -89,7 +90,7 @@ public class MsAssertions extends MsTestElement {
private JSONPathAssertion jsonPathAssertion(MsAssertionJsonPath assertionJsonPath) {
JSONPathAssertion assertion = new JSONPathAssertion();
assertion.setEnabled(true);
assertion.setName(assertionJsonPath.getDescription());
assertion.setName(StringUtils.isEmpty(assertionJsonPath.getDescription()) ? "JSONPathAssertion" : assertionJsonPath.getDescription());
assertion.setProperty(TestElement.TEST_CLASS, JSONPathAssertion.class.getName());
assertion.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("JSONPathAssertionGui"));
assertion.setJsonPath(assertionJsonPath.getExpression());
@ -104,7 +105,7 @@ public class MsAssertions extends MsTestElement {
private XPath2Assertion xPath2Assertion(MsAssertionXPath2 assertionXPath2) {
XPath2Assertion assertion = new XPath2Assertion();
assertion.setEnabled(true);
assertion.setName(assertionXPath2.getExpression());
assertion.setName(StringUtils.isEmpty(assertionXPath2.getExpression()) ? "XPath2Assertion" : assertionXPath2.getExpression());
assertion.setProperty(TestElement.TEST_CLASS, XPath2Assertion.class.getName());
assertion.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("XPath2AssertionGui"));
assertion.setXPathString(assertionXPath2.getExpression());
@ -125,7 +126,7 @@ public class MsAssertions extends MsTestElement {
private JSR223Assertion jsr223Assertion(MsAssertionJSR223 assertionJSR223) {
JSR223Assertion assertion = new JSR223Assertion();
assertion.setEnabled(true);
assertion.setName(assertionJSR223.getDesc());
assertion.setName(StringUtils.isEmpty(assertionJSR223.getDesc()) ? "JSR223Assertion" : assertionJSR223.getDesc());
assertion.setProperty(TestElement.TEST_CLASS, JSR223Assertion.class.getName());
assertion.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestBeanGUI"));
assertion.setProperty("cacheKey", "true");

View File

@ -42,10 +42,10 @@ public class MsIfController extends MsTestElement {
private IfController ifController() {
IfController ifController = new IfController();
ifController.setEnabled(true);
ifController.setName(this.getLabelName());
ifController.setCondition(this.getCondition());
ifController.setName(StringUtils.isEmpty(this.getName()) ? "IfController" : this.getName());
ifController.setProperty(TestElement.TEST_CLASS, IfController.class.getName());
ifController.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("IfControllerPanel"));
ifController.setCondition(this.getCondition());
ifController.setEvaluateAll(false);
ifController.setUseExpression(true);
return ifController;
@ -79,13 +79,13 @@ public class MsIfController extends MsTestElement {
}
if (StringUtils.equals(operator, "is empty")) {
variable = "empty(" + variable + ")";
variable = "!empty(" + variable + ")";
operator = "";
value = "";
}
if (StringUtils.equals(operator, "is not empty")) {
variable = "!empty(" + variable + ")";
variable = "empty(" + variable + ")";
operator = "";
value = "";
}

View File

@ -123,13 +123,13 @@ public class MsLoopController extends MsTestElement {
}
if (StringUtils.equals(operator, "is empty")) {
variable = "empty(" + variable + ")";
variable = "!empty(" + variable + ")";
operator = "";
value = "";
}
if (StringUtils.equals(operator, "is not empty")) {
variable = "!empty(" + variable + ")";
variable = "empty(" + variable + ")";
operator = "";
value = "";
}

View File

@ -57,7 +57,7 @@ public class MsExtract extends MsTestElement {
if (Optional.ofNullable(extract).orElse(extract).length() > 0) {
JSR223PostProcessor shell = new JSR223PostProcessor();
shell.setEnabled(true);
shell.setName(this.getName());
shell.setName(StringUtils.isEmpty(this.getName()) ? "JSR223PostProcessor" : this.getName());
shell.setProperty(TestElement.TEST_CLASS, JSR223PostProcessor.class.getName());
shell.setProperty(TestElement.GUI_CLASS, SaveService.aliasToClass("TestBeanGUI"));
shell.setProperty("script", "io.metersphere.api.jmeter.JMeterVars.addVars(prev.hashCode(),vars," + "\"" + extract.toString() + "\"" + ");");

View File

@ -207,14 +207,17 @@ public class MsHTTPSamplerProxy extends MsTestElement {
}
final HashTree httpSamplerTree = tree.add(sampler);
// 注意顺序放在config前面会优先于环境的请求头生效
if (CollectionUtils.isNotEmpty(this.headers)) {
setHeader(httpSamplerTree, this.headers);
}
// 通用请求Headers
if (config != null && config.getConfig() != null && config.getConfig().getHttpConfig() != null
&& CollectionUtils.isNotEmpty(config.getConfig().getHttpConfig().getHeaders())) {
setHeader(httpSamplerTree, config.getConfig().getHttpConfig().getHeaders());
}
if (CollectionUtils.isNotEmpty(this.headers)) {
setHeader(httpSamplerTree, this.headers);
}
//判断是否要开启DNS
if (config != null && config.getConfig() != null && config.getConfig().getCommonConfig() != null

View File

@ -96,7 +96,7 @@ public class MsJDBCSampler extends MsTestElement {
private Arguments arguments(String name, List<KeyValue> variables) {
Arguments arguments = new Arguments();
if (!variables.isEmpty()) {
if (CollectionUtils.isNotEmpty(variables)) {
arguments.setEnabled(true);
arguments.setName(name);
arguments.setProperty(TestElement.TEST_CLASS, Arguments.class.getName());

View File

@ -12,6 +12,7 @@ import lombok.Data;
import lombok.EqualsAndHashCode;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.jmeter.config.Arguments;
import org.apache.jmeter.config.ConfigTestElement;
import org.apache.jmeter.modifiers.UserParameters;
import org.apache.jmeter.protocol.tcp.sampler.TCPSampler;
@ -79,6 +80,13 @@ public class MsTCPSampler extends MsTestElement {
}
config.setConfig(getEnvironmentConfig(useEnvironment));
parseEnvironment(config.getConfig());
// 添加环境中的公共变量
Arguments arguments = this.addArguments(config);
if (arguments != null) {
tree.add(this.addArguments(config));
}
final HashTree samplerHashTree = new ListedHashTree();
samplerHashTree.add(tcpConfig());
tree.set(tcpSampler(config), samplerHashTree);
@ -94,7 +102,7 @@ public class MsTCPSampler extends MsTestElement {
}
private void parseEnvironment(EnvironmentConfig config) {
if (config != null && config.getTcpConfig() != null) {
if (!isCustomizeReq() && config != null && config.getTcpConfig() != null) {
this.server = config.getTcpConfig().getServer();
this.port = config.getTcpConfig().getPort();
}

View File

@ -2,6 +2,7 @@ package io.metersphere.api.jmeter;
import io.metersphere.api.dto.scenario.request.RequestType;
import io.metersphere.api.service.*;
import io.metersphere.base.domain.ApiDefinitionExecResult;
import io.metersphere.base.domain.ApiScenarioReport;
import io.metersphere.base.domain.ApiTestReport;
import io.metersphere.commons.constants.*;
@ -177,10 +178,17 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
} else if (StringUtils.equals(this.runMode, ApiRunMode.JENKINS.name())) {
apiDefinitionService.addResult(testResult);
apiDefinitionExecResultService.saveApiResult(testResult, ApiRunMode.DEFINITION.name());
apiTestService.changeStatus(testId, APITestStatus.Completed);
report = apiReportService.getRunningReport(testResult.getTestId());
apiReportService.complete(testResult, report);
} else if (StringUtils.equals(this.runMode, ApiRunMode.JENKINS_API_PLAN.name())) {
apiDefinitionService.addResult(testResult);
apiDefinitionExecResultService.saveApiResult(testResult, ApiRunMode.API_PLAN.name());
ApiDefinitionExecResult result = new ApiDefinitionExecResult();
result = apiDefinitionService.getResultByJenkins(debugReportId, ApiRunMode.API_PLAN.name());
report = new ApiTestReport();
report.setStatus(result.getStatus());
report.setId(result.getId());
report.setTriggerMode(ApiRunMode.API.name());
report.setName(apiDefinitionService.getApiCaseInfo(testId).getName());
} else if (StringUtils.equalsAny(this.runMode, ApiRunMode.API_PLAN.name(), ApiRunMode.SCHEDULE_API_PLAN.name())) {
apiDefinitionService.addResult(testResult);
@ -264,9 +272,15 @@ public class APIBackendListenerClient extends AbstractBackendListenerClient impl
if (StringUtils.equals("Success", report.getStatus())) {
event = NoticeConstants.Event.EXECUTE_SUCCESSFUL;
}
if (StringUtils.equals("success", report.getStatus())) {
event = NoticeConstants.Event.EXECUTE_SUCCESSFUL;
}
if (StringUtils.equals("Error", report.getStatus())) {
event = NoticeConstants.Event.EXECUTE_FAILED;
}
if (StringUtils.equals("error", report.getStatus())) {
event = NoticeConstants.Event.EXECUTE_FAILED;
}
Map<String, Object> paramMap = new HashMap<>();
paramMap.put("testName", report.getName());
paramMap.put("id", report.getId());

View File

@ -60,6 +60,18 @@ public class TestResult {
item.getSubRequestResults().forEach(subItem -> {
subItem.setName(item.getName());
});
} else {
if (requestResultMap.containsKey(result.getName())) {
requestResultMap.get(result.getName()).add(item);
} else {
List<RequestResult> requestResults = new LinkedList<>();
requestResults.add(item);
requestResultMap.put(result.getName(), requestResults);
}
item.getSubRequestResults().forEach(subItem -> {
subItem.setName(item.getName());
});
}
});
}

View File

@ -86,23 +86,33 @@ public class ApiAutomationService {
private TestPlanScenarioCaseService testPlanScenarioCaseService;
public List<ApiScenarioDTO> list(ApiScenarioRequest request) {
request = this.initRequest(request,true,true);
request = this.initRequest(request, true, true);
List<ApiScenarioDTO> list = extApiScenarioMapper.list(request);
return list;
}
/**
* 初始化部分参数
*
* @param request
* @param setDefultOrders
* @param checkThisWeekData
* @return
*/
private ApiScenarioRequest initRequest(ApiScenarioRequest request,boolean setDefultOrders, boolean checkThisWeekData) {
if(setDefultOrders){
private ApiScenarioRequest initRequest(ApiScenarioRequest request, boolean setDefultOrders, boolean checkThisWeekData) {
if (setDefultOrders) {
request.setOrders(ServiceUtils.getDefaultOrder(request.getOrders()));
}
if(checkThisWeekData){
if(StringUtils.isNotEmpty(request.getExecuteStatus())){
Map<String,List<String>> statusFilter = new HashMap<>();
List<String> list = new ArrayList<>();
list.add("Prepare");
list.add("Underway");
list.add("Completed");
statusFilter.put("status",list);
request.setFilters(statusFilter);
}
if (checkThisWeekData) {
if (request.isSelectThisWeedData()) {
Map<String, Date> weekFirstTimeAndLastTime = DateUtils.getWeedFirstTimeAndLastTime(new Date());
Date weekFirstTime = weekFirstTimeAndLastTime.get("firstTime");
@ -217,7 +227,7 @@ public class ApiAutomationService {
ids.add(scenarioId);
deleteApiScenarioReport(ids);
scheduleService.deleteScheduleAndJobByResourceId(scenarioId,ScheduleGroup.API_SCENARIO_TEST.name());
scheduleService.deleteScheduleAndJobByResourceId(scenarioId, ScheduleGroup.API_SCENARIO_TEST.name());
TestPlanApiScenarioExample example = new TestPlanApiScenarioExample();
example.createCriteria().andApiScenarioIdEqualTo(scenarioId);
List<TestPlanApiScenario> testPlanApiScenarioList = testPlanApiScenarioMapper.selectByExample(example);
@ -284,7 +294,7 @@ public class ApiAutomationService {
extApiScenarioMapper.removeToGc(apiIds);
//将这些场景的定时任务删除掉
for (String id : apiIds) {
scheduleService.deleteScheduleAndJobByResourceId(id,ScheduleGroup.API_SCENARIO_TEST.name());
scheduleService.deleteScheduleAndJobByResourceId(id, ScheduleGroup.API_SCENARIO_TEST.name());
}
}
@ -372,7 +382,7 @@ public class ApiAutomationService {
try {
boolean isFirst = true;
for (ApiScenarioWithBLOBs item : apiScenarios) {
if (item.getStepTotal() == 0) {
if (item.getStepTotal() == null || item.getStepTotal() == 0) {
// 只有一个场景且没有测试步骤则提示
if (apiScenarios.size() == 1) {
MSException.throwException((item.getName() + "" + Translator.get("automation_exec_info")));
@ -412,14 +422,14 @@ public class ApiAutomationService {
// 创建场景报告
if (reportIds != null) {
//如果是测试计划页面触发的执行方式生成报告时createScenarioReport第二个参数需要特殊处理
if(StringUtils.equals(request.getRunMode(),ApiRunMode.SCENARIO_PLAN.name())){
if (StringUtils.equals(request.getRunMode(), ApiRunMode.SCENARIO_PLAN.name())) {
String testPlanScenarioId = item.getId();
if(request.getScenarioTestPlanIdMap()!=null&&request.getScenarioTestPlanIdMap().containsKey(item.getId())){
if (request.getScenarioTestPlanIdMap() != null && request.getScenarioTestPlanIdMap().containsKey(item.getId())) {
testPlanScenarioId = request.getScenarioTestPlanIdMap().get(item.getId());
}
createScenarioReport(group.getName(), testPlanScenarioId, item.getName(), request.getTriggerMode() == null ? ReportTriggerMode.MANUAL.name() : request.getTriggerMode(),
request.getExecuteType(), item.getProjectId(), request.getReportUserID());
}else{
} else {
createScenarioReport(group.getName(), item.getId(), item.getName(), request.getTriggerMode() == null ? ReportTriggerMode.MANUAL.name() : request.getTriggerMode(),
request.getExecuteType(), item.getProjectId(), request.getReportUserID());
}
@ -449,6 +459,8 @@ public class ApiAutomationService {
ids = this.getAllScenarioIdsByFontedSelect(
request.getModuleIds(), request.getName(), request.getProjectId(), request.getFilters(), request.getUnSelectIds());
}
//检查是否有正在执行中的情景
this.checkScenarioIsRunnng(ids);
List<ApiScenarioWithBLOBs> apiScenarios = extApiScenarioMapper.selectIds(ids);
String runMode = ApiRunMode.SCENARIO.name();
@ -465,6 +477,15 @@ public class ApiAutomationService {
return request.getId();
}
public void checkScenarioIsRunnng(List<String> ids) {
List<ApiScenarioReport> lastReportStatusByIds = apiReportService.selectLastReportByIds(ids);
for (ApiScenarioReport report : lastReportStatusByIds) {
if(StringUtils.equals(report.getStatus(),APITestStatus.Running.name())){
MSException.throwException(report.getName()+" Is Running!");
}
}
}
/**
* 获取前台查询条件查询的所有(未经分页筛选)数据ID
*
@ -506,7 +527,6 @@ public class ApiAutomationService {
ParameterConfig config = new ParameterConfig();
config.setConfig(envConfig);
HashTree hashTree = request.getTestElement().generateHashTree(config);
// 调用执行方法
createScenarioReport(request.getId(), request.getScenarioId(), request.getScenarioName(), ReportTriggerMode.MANUAL.name(), request.getExecuteType(), request.getProjectId(),
SessionUtils.getUserId());
@ -635,10 +655,10 @@ public class ApiAutomationService {
}
private void addOrUpdateApiScenarioCronJob(Schedule request) {
if(StringUtils.equals(request.getGroup(),ScheduleGroup.TEST_PLAN_TEST.name())){
if (StringUtils.equals(request.getGroup(), ScheduleGroup.TEST_PLAN_TEST.name())) {
scheduleService.addOrUpdateCronJob(
request, TestPlanTestJob.getJobKey(request.getResourceId()), TestPlanTestJob.getTriggerKey(request.getResourceId()), TestPlanTestJob.class);
}else{
} else {
scheduleService.addOrUpdateCronJob(
request, ApiScenarioTestJob.getJobKey(request.getResourceId()), ApiScenarioTestJob.getTriggerKey(request.getResourceId()), ApiScenarioTestJob.class);
}
@ -707,7 +727,9 @@ public class ApiAutomationService {
apiScenarios.forEach(item -> {
JSONObject object = JSONObject.parseObject(item.getScenarioDefinition());
object.put("environmentId", request.getEnvironmentId());
item.setScenarioDefinition(JSONObject.toJSONString(object));
if (object != null) {
item.setScenarioDefinition(JSONObject.toJSONString(object));
}
apiScenarioMapper.updateByPrimaryKeySelective(item);
});
}

View File

@ -82,6 +82,8 @@ public class ApiDefinitionService {
private ExtSwaggerUrlScheduleMapper extSwaggerUrlScheduleMapper;
@Resource
private ScheduleMapper scheduleMapper;
@Resource
private ApiTestCaseMapper apiTestCaseMapper;
private static Cache cache = Cache.newHardMemoryCache(0, 3600 * 24);
@ -348,7 +350,8 @@ public class ApiDefinitionService {
private String setImportHashTree(ApiDefinitionWithBLOBs apiDefinition) {
String request = apiDefinition.getRequest();
MsHTTPSamplerProxy msHTTPSamplerProxy = JSONObject.parseObject(request, MsHTTPSamplerProxy.class);
msHTTPSamplerProxy.setHashTree(null);
msHTTPSamplerProxy.setId(apiDefinition.getId());
msHTTPSamplerProxy.setHashTree(new LinkedList<>());
apiDefinition.setRequest(JSONObject.toJSONString(msHTTPSamplerProxy));
return request;
}
@ -468,6 +471,14 @@ public class ApiDefinitionService {
return buildAPIReportResult(result);
}
public ApiDefinitionExecResult getResultByJenkins(String testId, String type) {
return extApiDefinitionExecResultMapper.selectMaxResultByResourceIdAndType(testId, type);
}
public ApiTestCaseWithBLOBs getApiCaseInfo(String apiCaseId) {
return apiTestCaseMapper.selectByPrimaryKey(apiCaseId);
}
public ApiDefinitionImport apiTestImport(MultipartFile file, ApiTestImportRequest request) {
ApiImportParser apiImportParser = ApiImportParserFactory.getApiImportParser(request.getPlatform());

View File

@ -415,4 +415,12 @@ public class ApiScenarioReportService {
public List<ApiDataCountResult> countByProjectIdGroupByExecuteResult(String projectId) {
return extApiScenarioReportMapper.countByProjectIdGroupByExecuteResult(projectId);
}
public List<ApiScenarioReport> selectLastReportByIds(List<String> ids) {
if(!ids.isEmpty()){
return extApiScenarioReportMapper.selectLastReportByIds(ids);
}else {
return new ArrayList<>(0);
}
}
}

View File

@ -473,7 +473,7 @@ public class ApiTestCaseService {
String runMode = ApiRunMode.JENKINS.name();
*/
// 调用执行方法
jMeterService.runDefinition(request.getReportId(), jmeterHashTree, request.getReportId(), request.getRunMode());
jMeterService.runDefinition(request.getCaseId(), jmeterHashTree, request.getReportId(), request.getRunMode());
} catch (Exception ex) {
LogUtil.error(ex.getMessage());

View File

@ -304,7 +304,7 @@
<select id="countRunResultByProjectID" resultType="io.metersphere.api.dto.datacount.ApiDataCountResult">
SELECT count(id) AS countNumber, if(last_result is null,"notRun",last_result) AS groupField FROM api_scenario
WHERE project_id = #{0}
WHERE project_id = #{0} AND status != 'Trash'
GROUP BY groupField
</select>

View File

@ -3,6 +3,7 @@ package io.metersphere.base.mapper.ext;
import io.metersphere.api.dto.QueryAPIReportRequest;
import io.metersphere.api.dto.automation.APIScenarioReportResult;
import io.metersphere.api.dto.datacount.ApiDataCountResult;
import io.metersphere.base.domain.ApiScenarioReport;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@ -20,4 +21,6 @@ public interface ExtApiScenarioReportMapper {
long countByProjectIdAndCreateAndByScheduleInThisWeek(@Param("projectId") String projectId, @Param("firstDayTimestamp") long firstDayTimestamp, @Param("lastDayTimestamp") long lastDayTimestamp);
List<ApiDataCountResult> countByProjectIdGroupByExecuteResult(String projectId);
List<ApiScenarioReport> selectLastReportByIds(@Param("scenarioIdList") List<String> ids);
}

View File

@ -194,4 +194,26 @@
WHERE acr.project_id = #{projectId} AND ar.trigger_mode = 'SCHEDULE'
GROUP BY groupField;
</select>
<select id="selectLastReportByIds" resultType="io.metersphere.base.domain.ApiScenarioReport">
SELECT report.* FROM api_scenario_report report
INNER JOIN (
SELECT a.id,a.createTime,a.scenario_id FROM
(
SELECT id AS id,create_time AS createTime, scenario_id AS scenario_id FROM api_scenario_report
WHERE scenario_id in
<foreach collection="scenarioIdList" item="value" separator="," open="(" close=")">
#{value}
</foreach>
UNION
SELECT report.id AS id,report.create_time AS createTime,planScenario.api_scenario_id AS scenario_id FROM api_scenario_report report
INNER JOIN test_plan_api_scenario planScenario ON report.scenario_id = planScenario.id
WHERE planScenario.api_scenario_id in
<foreach collection="scenarioIdList" item="value" separator="," open="(" close=")">
#{value}
</foreach>
ORDER BY createTime DESC
) a GROUP BY a.scenario_id
) orderData ON orderData.id = report.id;
</select>
</mapper>

View File

@ -0,0 +1,15 @@
package io.metersphere.base.mapper.ext;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.io.InputStream;
public interface ExtFileContentMapper {
@Select(value = {
"SELECT file ",
"FROM file_content ",
"WHERE file_id = #{id, jdbcType=VARCHAR}"
})
InputStream selectZipBytes(@Param("id") String id);
}

View File

@ -4,10 +4,12 @@
<select id="list" resultType="io.metersphere.track.dto.TestPlanReportDTO"
parameterType="io.metersphere.track.request.report.QueryTestPlanReportRequest">
SELECT tpr.id AS id, tpr.`name` AS `name`, tp.`name` AS testPlanName, u.name AS creator, tpr.create_time AS createTime,tpr.trigger_Mode AS triggerMode,tpr.status AS status
SELECT tpr.id AS id, tpr.`name` AS `name`, tp.`name` AS testPlanName,
IF(u.name is null,tpr.creator,u.name)AS creator,
tpr.create_time AS createTime,tpr.trigger_Mode AS triggerMode,tpr.status AS status
FROM test_plan tp
INNER JOIN test_plan_report tpr on tp.id = tpr.test_plan_id
INNER JOIN user u on tpr.creator = u.id
LEFT JOIN user u on tpr.creator = u.id
<where>
<if test="name != null">
and tpr.name like CONCAT('%', #{name},'%')

View File

@ -339,7 +339,8 @@
where plan_id = #{planId}
</select>
<select id="listByPlanId" resultType="io.metersphere.track.dto.TestPlanCaseDTO">
SELECT test_plan_api_case.api_case_id as id,"definition" as type,api_test_case.name,test_plan_api_case.status
SELECT test_plan_api_case.id as reportId,test_plan_api_case.api_case_id as id,"definition" as
type,api_test_case.name,test_plan_api_case.status
from test_plan_api_case left join api_test_case on test_plan_api_case.api_case_id=api_test_case.id
inner join
api_definition a
@ -352,7 +353,7 @@
</if>
</where>
UNION ALL
SELECT test_plan_api_scenario.api_scenario_id as id,"scenario" as
SELECT test_plan_api_scenario.id as reportId,test_plan_api_scenario.api_scenario_id as id,"scenario" as
type,api_scenario.name,test_plan_api_scenario.status
from test_plan_api_scenario
left join
@ -366,7 +367,8 @@
</if>
</where>
UNION ALL
select load_test.id as id,"perform" as type,load_test.name as name,test_plan_load_case.status from test_plan_load_case inner join
select test_plan_load_case.id as reportId,load_test.id as id,"perform" as type,load_test.name as
name,test_plan_load_case.status from test_plan_load_case inner join
load_test on
test_plan_load_case.load_case_id =load_test.id
<where>
@ -375,7 +377,8 @@
</if>
</where>
UNION ALL
SELECT test_case.test_id as id,test_case.type as type,test_case.name,test_plan_test_case.status
SELECT test_case.id as reportId,test_case.test_id as id,test_case.type as
type,test_case.name,test_plan_test_case.status
from test_plan_test_case left join test_case on test_plan_test_case.case_id =test_case.id
<where>
<if test="request.planId != null">

View File

@ -1,5 +1,5 @@
package io.metersphere.commons.constants;
public enum ApiRunMode {
RUN, DEBUG, DEFINITION, SCENARIO, API_PLAN, JENKINS, SCENARIO_PLAN, API, SCHEDULE_API_PLAN, SCHEDULE_SCENARIO_PLAN, SCHEDULE_PERFORMANCE_TEST
RUN, DEBUG, DEFINITION, SCENARIO, API_PLAN, JENKINS_API_PLAN, JENKINS, SCENARIO_PLAN, API, SCHEDULE_API_PLAN, SCHEDULE_SCENARIO_PLAN, SCHEDULE_PERFORMANCE_TEST
}

View File

@ -16,9 +16,6 @@ import io.metersphere.performance.controller.request.ReportRequest;
import io.metersphere.performance.service.ReportService;
import org.apache.shiro.authz.annotation.Logical;
import org.apache.shiro.authz.annotation.RequiresRoles;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
@ -137,11 +134,7 @@ public class PerformanceReportController {
}
@GetMapping("/jtl/download/{reportId}")
public ResponseEntity<byte[]> downloadJtl(@PathVariable String reportId) {
byte[] bytes = reportService.downloadJtl(reportId);
return ResponseEntity.ok()
.contentType(MediaType.parseMediaType("application/octet-stream"))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + reportId + ".jtl\"")
.body(bytes);
public void downloadJtlZip(@PathVariable String reportId, HttpServletResponse response) {
reportService.downloadJtlZip(reportId, response);
}
}

View File

@ -4,6 +4,7 @@ import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.metersphere.base.domain.*;
import io.metersphere.base.mapper.*;
import io.metersphere.base.mapper.ext.ExtFileContentMapper;
import io.metersphere.base.mapper.ext.ExtLoadTestReportMapper;
import io.metersphere.commons.constants.PerformanceTestStatus;
import io.metersphere.commons.constants.ReportKeys;
@ -23,11 +24,15 @@ import io.metersphere.performance.engine.EngineFactory;
import io.metersphere.service.FileService;
import io.metersphere.service.TestResourceService;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
@ -52,6 +57,8 @@ public class ReportService {
private LoadTestReportDetailMapper loadTestReportDetailMapper;
@Resource
private FileService fileService;
@Resource
private SqlSessionFactory sqlSessionFactory;
public List<ReportDTO> getRecentReportList(ReportRequest request) {
List<OrderRequest> orders = new ArrayList<>();
@ -284,11 +291,28 @@ public class ReportService {
return JSON.parseArray(content, ChartsData.class);
}
public byte[] downloadJtl(String reportId) {
/**
* 流下载 jtl zip
*/
public void downloadJtlZip(String reportId, HttpServletResponse response) {
LoadTestReportWithBLOBs report = getReport(reportId);
if (StringUtils.isBlank(report.getFileId())) {
throw new RuntimeException(Translator.get("load_test_report_file_not_exist"));
}
return fileService.loadFileAsBytes(report.getFileId());
response.setHeader("Content-Disposition", "attachment;fileName=" + reportId + ".zip");
try (SqlSession sqlSession = sqlSessionFactory.openSession()) {
ExtFileContentMapper mapper = sqlSession.getMapper(ExtFileContentMapper.class);
try (InputStream inputStream = mapper.selectZipBytes(report.getFileId())) {
ServletOutputStream outputStream = response.getOutputStream();
byte[] buffer = new byte[1024 * 4];
int read;
while ((read = inputStream.read(buffer)) > -1) {
outputStream.write(buffer, 0, read);
}
} catch (Exception e) {
LogUtil.error(e);
MSException.throwException(e);
}
}
}
}

View File

@ -39,8 +39,14 @@ public class TestReviewTestCaseController {
@PostMapping("/batch/delete")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public void deleteTestCaseBath(@RequestBody TestReviewCaseBatchRequest request) {
testReviewTestCaseService.deleteTestCaseBath(request);
public void deleteTestCaseBatch(@RequestBody TestReviewCaseBatchRequest request) {
testReviewTestCaseService.deleteTestCaseBatch(request);
}
@PostMapping("/batch/edit/status")
@RequiresRoles(value = {RoleConstants.TEST_USER, RoleConstants.TEST_MANAGER}, logical = Logical.OR)
public void editTestCaseBatch(@RequestBody TestReviewCaseBatchRequest request) {
testReviewTestCaseService.editTestCaseBatchStatus(request);
}
@PostMapping("/list/all")

View File

@ -39,7 +39,6 @@ import io.metersphere.track.request.testplan.LoadCaseRequest;
import io.metersphere.track.request.testplan.RunTestPlanRequest;
import io.metersphere.track.request.testplancase.QueryTestPlanCaseRequest;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
@ -179,8 +178,7 @@ public class TestPlanService {
else if (res.getStatus().equals(TestPlanStatus.Completed.name())) {
testPlan.setActualEndTime(null);
} // 已完成->进行中结束时间置空
}
else if (!res.getStatus().equals(TestPlanStatus.Prepare.name()) &&
} else if (!res.getStatus().equals(TestPlanStatus.Prepare.name()) &&
testPlan.getStatus().equals(TestPlanStatus.Prepare.name())) {
testPlan.setActualStartTime(null);
testPlan.setActualEndTime(null);
@ -338,7 +336,7 @@ public class TestPlanService {
testPlanScenarioCaseService.deleteByPlanId(planId);
//删除定时任务
scheduleService.deleteScheduleAndJobByResourceId(planId,ScheduleGroup.TEST_PLAN_TEST.name());
scheduleService.deleteScheduleAndJobByResourceId(planId, ScheduleGroup.TEST_PLAN_TEST.name());
int num = testPlanMapper.deleteByPrimaryKey(planId);
List<String> relatedUsers = new ArrayList<>();
@ -437,7 +435,7 @@ public class TestPlanService {
}
public List<TestPlanDTOWithMetric> listTestPlanByProject(QueryTestPlanRequest request) {
List<TestPlanDTOWithMetric> testPlans=extTestPlanMapper.list(request);
List<TestPlanDTOWithMetric> testPlans = extTestPlanMapper.list(request);
return testPlans;
}
@ -784,12 +782,13 @@ public class TestPlanService {
}
return issues;
}
public List<TestPlanDTO> selectTestPlanByRelevancy(QueryTestPlanRequest params){
return extTestPlanMapper.selectTestPlanByRelevancy(params);
public List<TestPlanDTO> selectTestPlanByRelevancy(QueryTestPlanRequest params) {
return extTestPlanMapper.selectTestPlanByRelevancy(params);
}
public String findTestProjectNameByTestPlanID(String testPlanId) {
return extTestPlanMapper.findTestProjectNameByTestPlanID(testPlanId);
return extTestPlanMapper.findTestProjectNameByTestPlanID(testPlanId);
}
public String findScheduleCreateUserById(String testPlanId) {
@ -877,23 +876,23 @@ public class TestPlanService {
return request.getId();
}
public void run(String testPlanID,String projectID,String userId,String triggerMode){
Map<String,String> planScenarioIdMap;
Map<String,String> apiTestCaseIdMap;
Map<String,String> performanceIdMap;
public void run(String testPlanID, String projectID, String userId, String triggerMode) {
Map<String, String> planScenarioIdMap;
Map<String, String> apiTestCaseIdMap;
Map<String, String> performanceIdMap;
planScenarioIdMap = new LinkedHashMap<>();
apiTestCaseIdMap = new LinkedHashMap<>();
performanceIdMap = new LinkedHashMap<>();
List<TestPlanApiScenario> testPlanApiScenarioList = testPlanScenarioCaseService.getCasesByPlanId(testPlanID);
for (TestPlanApiScenario model :testPlanApiScenarioList) {
planScenarioIdMap.put(model.getApiScenarioId(),model.getId());
for (TestPlanApiScenario model : testPlanApiScenarioList) {
planScenarioIdMap.put(model.getApiScenarioId(), model.getId());
}
List<TestPlanApiCase> testPlanApiCaseList = testPlanApiCaseService.getCasesByPlanId(testPlanID);
for (TestPlanApiCase model :
testPlanApiCaseList) {
apiTestCaseIdMap.put(model.getApiCaseId(),model.getId());
apiTestCaseIdMap.put(model.getApiCaseId(), model.getId());
}
LoadCaseRequest loadCaseRequest = new LoadCaseRequest();
@ -901,24 +900,24 @@ public class TestPlanService {
loadCaseRequest.setProjectId(projectID);
List<TestPlanLoadCaseDTO> testPlanLoadCaseDTOList = testPlanLoadCaseService.list(loadCaseRequest);
for (TestPlanLoadCaseDTO dto : testPlanLoadCaseDTOList) {
performanceIdMap.put(dto.getId(),dto.getLoadCaseId());
performanceIdMap.put(dto.getId(), dto.getLoadCaseId());
}
LogUtil.info("-------------- start testplan schedule ----------");
TestPlanReportService testPlanReportService = CommonBeanFactory.getBean(TestPlanReportService.class);
//首先创建testPlanReport然后返回的ID重新赋值为resourceID作为后续的参数
TestPlanReport testPlanReport = testPlanReportService.genTestPlanReport(testPlanID,userId,triggerMode);
TestPlanReport testPlanReport = testPlanReportService.genTestPlanReport(testPlanID, userId, triggerMode);
//执行接口案例任务
for (Map.Entry<String,String> entry: apiTestCaseIdMap.entrySet()) {
for (Map.Entry<String, String> entry : apiTestCaseIdMap.entrySet()) {
String apiCaseID = entry.getKey();
String planCaseID = entry.getValue();
ApiTestCaseWithBLOBs blobs = apiTestCaseService.get(apiCaseID);
//需要更新这里来保证PlanCase的状态能正常更改
apiTestCaseService.run(blobs,UUID.randomUUID().toString(),testPlanReport.getId(),testPlanID,ApiRunMode.SCHEDULE_API_PLAN.name());
apiTestCaseService.run(blobs, UUID.randomUUID().toString(), testPlanReport.getId(), testPlanID, ApiRunMode.SCHEDULE_API_PLAN.name());
}
//执行场景执行任务
if(!planScenarioIdMap.isEmpty()){
if (!planScenarioIdMap.isEmpty()) {
LogUtil.info("-------------- testplan schedule ---------- api case over -----------------");
SchedulePlanScenarioExecuteRequest scenarioRequest = new SchedulePlanScenarioExecuteRequest();
String senarionReportID = UUID.randomUUID().toString();
@ -927,7 +926,7 @@ public class TestPlanService {
scenarioRequest.setProjectId(projectID);
scenarioRequest.setTriggerMode(ReportTriggerMode.SCHEDULE.name());
scenarioRequest.setExecuteType(ExecuteType.Saved.name());
Map<String, Map<String,String>> testPlanScenarioIdMap = new HashMap<>();
Map<String, Map<String, String>> testPlanScenarioIdMap = new HashMap<>();
testPlanScenarioIdMap.put(testPlanID, planScenarioIdMap);
scenarioRequest.setTestPlanScenarioIDMap(testPlanScenarioIdMap);
scenarioRequest.setReportUserID(userId);
@ -939,7 +938,7 @@ public class TestPlanService {
}
//执行性能测试任务
List<String> performaneReportIDList = new ArrayList<>();
for (Map.Entry<String,String> entry: performanceIdMap.entrySet()) {
for (Map.Entry<String, String> entry : performanceIdMap.entrySet()) {
String id = entry.getKey();
String caseID = entry.getValue();
RunTestPlanRequest performanceRequest = new RunTestPlanRequest();
@ -949,7 +948,7 @@ public class TestPlanService {
String reportId = null;
try {
reportId = performanceTestService.run(performanceRequest);
if(reportId!=null){
if (reportId != null) {
performaneReportIDList.add(reportId);
TestPlanLoadCase testPlanLoadCase = new TestPlanLoadCase();
@ -957,7 +956,7 @@ public class TestPlanService {
testPlanLoadCase.setLoadReportId(reportId);
testPlanLoadCaseService.update(testPlanLoadCase);
}
}catch (Exception e){
} catch (Exception e) {
e.printStackTrace();
}
//更新关联处的报告
@ -967,9 +966,9 @@ public class TestPlanService {
testPlanLoadCaseService.update(loadCase);
}
if(!performaneReportIDList.isEmpty()){
if (!performaneReportIDList.isEmpty()) {
//性能测试时保存性能测试报告ID在结果返回时用于捕捉并进行
testPlanReportService.updatePerformanceInfo(testPlanReport,performaneReportIDList,ReportTriggerMode.SCHEDULE.name());
testPlanReportService.updatePerformanceInfo(testPlanReport, performaneReportIDList, ReportTriggerMode.SCHEDULE.name());
}
}
}

View File

@ -18,6 +18,7 @@ import io.metersphere.track.request.testreview.QueryCaseReviewRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.ArrayList;
@ -97,7 +98,7 @@ public class TestReviewTestCaseService {
}
}
public void deleteTestCaseBath(TestReviewCaseBatchRequest request) {
public void deleteTestCaseBatch(TestReviewCaseBatchRequest request) {
checkReviewer(request.getReviewId());
TestCaseReviewTestCaseExample example = new TestCaseReviewTestCaseExample();
example.createCriteria().andIdIn(request.getIds());
@ -105,15 +106,7 @@ public class TestReviewTestCaseService {
}
public void editTestCase(TestCaseReviewTestCase testCaseReviewTestCase) {
String currentUserId = SessionUtils.getUser().getId();
String reviewId = testCaseReviewTestCase.getReviewId();
TestCaseReviewUsersExample testCaseReviewUsersExample = new TestCaseReviewUsersExample();
testCaseReviewUsersExample.createCriteria().andReviewIdEqualTo(reviewId);
List<TestCaseReviewUsers> testCaseReviewUsers = testCaseReviewUsersMapper.selectByExample(testCaseReviewUsersExample);
List<String> reviewIds = testCaseReviewUsers.stream().map(TestCaseReviewUsers::getUserId).collect(Collectors.toList());
if (!reviewIds.contains(currentUserId)) {
MSException.throwException("非此用例的评审人员!");
}
checkReviewCase(testCaseReviewTestCase.getReviewId());
// 记录测试用例评审状态变更
testCaseReviewTestCase.setStatus(testCaseReviewTestCase.getStatus());
@ -137,4 +130,37 @@ public class TestReviewTestCaseService {
public TestReviewCaseDTO get(String reviewId) {
return extTestReviewCaseMapper.get(reviewId);
}
public void editTestCaseBatchStatus(TestReviewCaseBatchRequest request) {
List<String> ids = request.getIds();
if (CollectionUtils.isEmpty(ids)) {
return;
}
if (StringUtils.isBlank(request.getReviewId())) {
return;
} else {
checkReviewCase(request.getReviewId());
}
// 更新状态
if (StringUtils.isNotBlank(request.getStatus())) {
TestCaseExample example = new TestCaseExample();
example.createCriteria().andIdIn(ids);
TestCaseWithBLOBs testCase = new TestCaseWithBLOBs();
testCase.setReviewStatus(request.getStatus());
testCaseMapper.updateByExampleSelective(testCase, example);
}
}
private void checkReviewCase(String reviewId) {
String currentUserId = SessionUtils.getUser().getId();
TestCaseReviewUsersExample testCaseReviewUsersExample = new TestCaseReviewUsersExample();
testCaseReviewUsersExample.createCriteria().andReviewIdEqualTo(reviewId);
List<TestCaseReviewUsers> testCaseReviewUsers = testCaseReviewUsersMapper.selectByExample(testCaseReviewUsersExample);
List<String> reviewIds = testCaseReviewUsers.stream().map(TestCaseReviewUsers::getUserId).collect(Collectors.toList());
if (!reviewIds.contains(currentUserId)) {
MSException.throwException("非此用例的评审人员!");
}
}
}

View File

@ -273,5 +273,7 @@
</script>
<style scoped>
/deep/ .el-tabs__header {
margin: 0 0 0px;
}
</style>

View File

@ -74,7 +74,7 @@
rule: {
name: [
{required: true, message: this.$t('test_track.case.input_name'), trigger: 'blur'},
{max: 50, message: this.$t('test_track.length_less_than') + '50', trigger: 'blur'}
{max: 100, message: this.$t('test_track.length_less_than') + '100', trigger: 'blur'}
],
principal: [{
required: true,

View File

@ -67,6 +67,19 @@
});
}
}
if (item && item.files) {
item.files.forEach(fileItem => {
if (fileItem.file) {
if (!fileItem.id) {
let fileId = getUUID().substring(0, 12);
fileItem.name = fileItem.file.name;
fileItem.id = fileId;
}
obj.bodyUploadIds.push(fileItem.id);
bodyUploadFiles.push(fileItem.file);
}
});
}
},
recursiveFile(arr, bodyUploadFiles, obj) {
arr.forEach(item => {
@ -86,6 +99,11 @@
this.recursiveFile(item.hashTree, bodyUploadFiles, obj);
}
})
if (request.variables) {
request.variables.forEach(item => {
this.setFiles(item, bodyUploadFiles, obj);
})
}
return bodyUploadFiles;
},
run() {

View File

@ -250,7 +250,7 @@
rules: {
name: [
{required: true, message: this.$t('test_track.case.input_name'), trigger: 'blur'},
{max: 50, message: this.$t('test_track.length_less_than') + '50', trigger: 'blur'}
{max: 100, message: this.$t('test_track.length_less_than') + '100', trigger: 'blur'}
],
userId: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],
apiScenarioModuleId: [{required: true, message: this.$t('test_track.case.input_module'), trigger: 'change'}],
@ -567,6 +567,9 @@
this.$error("不能引用或复制自身!");
return;
}
if (!item.hashTree) {
item.hashTree = [];
}
item.enable === undefined ? item.enable = true : item.enable;
this.scenarioDefinition.push(item);
})

View File

@ -68,7 +68,7 @@
if (response.data) {
response.data.forEach(item => {
let scenarioDefinition = JSON.parse(item.scenarioDefinition);
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'Copy', resourceId: getUUID(), hashTree: scenarioDefinition.hashTree};
let obj = {id: item.id, name: item.name, type: "scenario", referenced: 'Copy', resourceId: getUUID(), hashTree: scenarioDefinition && scenarioDefinition.hashTree ? scenarioDefinition.hashTree : []};
scenarios.push(obj);
})
this.$emit('addScenario', scenarios);

View File

@ -1,5 +1,5 @@
<template>
<relevance-dialog :title="'接口导入'" ref="relevanceDialog">
<relevance-dialog :title="$t('api_test.definition.api_import')" ref="relevanceDialog">
<template v-slot:aside>
<ms-api-module
@ -28,32 +28,36 @@
ref="apiCaseList"/>
<template v-slot:footer>
<el-button type="primary" @click="copy" @keydown.enter.native.prevent>复制</el-button>
<el-button v-if="!isApiListEnable" type="primary" @click="reference" @keydown.enter.native.prevent>引用</el-button>
<el-button type="primary" @click="copy" @keydown.enter.native.prevent>{{ $t('commons.copy') }}</el-button>
<el-button v-if="!isApiListEnable" type="primary" @click="reference" @keydown.enter.native.prevent>
{{ $t('api_test.scenario.reference') }}
</el-button>
</template>
</relevance-dialog>
</template>
<script>
import ScenarioRelevanceCaseList from "./RelevanceCaseList";
import MsApiModule from "../../../definition/components/module/ApiModule";
import MsContainer from "../../../../common/components/MsContainer";
import MsAsideContainer from "../../../../common/components/MsAsideContainer";
import MsMainContainer from "../../../../common/components/MsMainContainer";
import ScenarioRelevanceApiList from "./RelevanceApiList";
import RelevanceDialog from "../../../../track/plan/view/comonents/base/RelevanceDialog";
export default {
name: "ApiRelevance",
components: {
RelevanceDialog,
ScenarioRelevanceApiList,
MsMainContainer, MsAsideContainer, MsContainer, MsApiModule, ScenarioRelevanceCaseList},
data() {
return {
result: {},
currentProtocol: null,
selectNodeIds: [],
import ScenarioRelevanceCaseList from "./RelevanceCaseList";
import MsApiModule from "../../../definition/components/module/ApiModule";
import MsContainer from "../../../../common/components/MsContainer";
import MsAsideContainer from "../../../../common/components/MsAsideContainer";
import MsMainContainer from "../../../../common/components/MsMainContainer";
import ScenarioRelevanceApiList from "./RelevanceApiList";
import RelevanceDialog from "../../../../track/plan/view/comonents/base/RelevanceDialog";
export default {
name: "ApiRelevance",
components: {
RelevanceDialog,
ScenarioRelevanceApiList,
MsMainContainer, MsAsideContainer, MsContainer, MsApiModule, ScenarioRelevanceCaseList
},
data() {
return {
result: {},
currentProtocol: null,
selectNodeIds: [],
moduleOptions: {},
isApiListEnable: true,
}
@ -118,4 +122,7 @@
</script>
<style scoped>
/deep/ .filter-input {
width: 140px !important;
}
</style>

View File

@ -6,7 +6,8 @@
<ms-environment-select :project-id="projectId" v-if="isTestPlan" :is-read-only="isReadOnly" @setEnvironment="setEnvironment"/>
<el-input placeholder="搜索" @blur="initTable" @keyup.enter.native="initTable" class="search-input" size="small" v-model="condition.name"/>
<el-input :placeholder="$t('api_test.definition.request.select_case')" @blur="initTable"
@keyup.enter.native="initTable" class="search-input" size="small" v-model="condition.name"/>
<el-table v-loading="result.loading"
border
@ -65,34 +66,33 @@
<script>
import MsTableOperator from "../../../../common/components/MsTableOperator";
import MsTableOperatorButton from "../../../../common/components/MsTableOperatorButton";
import {LIST_CHANGE, TrackEvent} from "@/business/components/common/head/ListEvent";
import MsTablePagination from "../../../../common/pagination/TablePagination";
import MsTag from "../../../../common/components/MsTag";
import MsBottomContainer from "../../../definition/components/BottomContainer";
import ShowMoreBtn from "../../../../track/case/components/ShowMoreBtn";
import MsBatchEdit from "../../../definition/components/basis/BatchEdit";
import {API_METHOD_COLOUR, CASE_PRIORITY} from "../../../definition/model/JsonData";
import {getCurrentProjectID} from "@/common/js/utils";
import ApiListContainer from "../../../definition/components/list/ApiListContainer";
import PriorityTableItem from "../../../../track/common/tableItems/planview/PriorityTableItem";
import {_filter, _sort} from "../../../../../../common/js/utils";
import {_handleSelect, _handleSelectAll} from "../../../../../../common/js/tableUtils";
import MsEnvironmentSelect from "../../../definition/components/case/MsEnvironmentSelect";
import TableSelectCountBar from "./TableSelectCountBar";
import MsTableOperator from "../../../../common/components/MsTableOperator";
import MsTableOperatorButton from "../../../../common/components/MsTableOperatorButton";
import MsTablePagination from "../../../../common/pagination/TablePagination";
import MsTag from "../../../../common/components/MsTag";
import MsBottomContainer from "../../../definition/components/BottomContainer";
import ShowMoreBtn from "../../../../track/case/components/ShowMoreBtn";
import MsBatchEdit from "../../../definition/components/basis/BatchEdit";
import {API_METHOD_COLOUR, CASE_PRIORITY} from "../../../definition/model/JsonData";
import {getCurrentProjectID} from "@/common/js/utils";
import ApiListContainer from "../../../definition/components/list/ApiListContainer";
import PriorityTableItem from "../../../../track/common/tableItems/planview/PriorityTableItem";
import {_filter, _sort} from "../../../../../../common/js/utils";
import {_handleSelect, _handleSelectAll} from "../../../../../../common/js/tableUtils";
import MsEnvironmentSelect from "../../../definition/components/case/MsEnvironmentSelect";
import TableSelectCountBar from "./TableSelectCountBar";
export default {
name: "RelevanceCaseList",
components: {
TableSelectCountBar,
MsEnvironmentSelect,
PriorityTableItem,
ApiListContainer,
MsTableOperatorButton,
MsTableOperator,
MsTablePagination,
MsTag,
export default {
name: "RelevanceCaseList",
components: {
TableSelectCountBar,
MsEnvironmentSelect,
PriorityTableItem,
ApiListContainer,
MsTableOperatorButton,
MsTableOperator,
MsTablePagination,
MsTag,
MsBottomContainer,
ShowMoreBtn,
MsBatchEdit

View File

@ -1,23 +1,21 @@
<template>
<el-card class="api-component">
<div class="header" @click="active(data)">
<div class="header" @click="active(data)">
<fieldset :disabled="data.disabled" class="ms-fieldset">
<slot name="beforeHeaderLeft">
<div v-if="data.index" class="el-step__icon is-text" style="margin-right: 10px;" :style="{'color': color, 'background-color': backgroundColor}">
<div class="el-step__icon-inner">{{data.index}}</div>
</div>
<el-button class="ms-left-buttion" size="small" :style="{'color': color, 'background-color': backgroundColor}">{{title}}</el-button>
</slot>
</fieldset>
<slot name="beforeHeaderLeft">
<div v-if="data.index" class="el-step__icon is-text" style="margin-right: 10px;" :style="{'color': color, 'background-color': backgroundColor}">
<div class="el-step__icon-inner">{{data.index}}</div>
</div>
<el-button class="ms-left-buttion" size="small" :style="{'color': color, 'background-color': backgroundColor}">{{title}}</el-button>
</slot>
<span @click.stop>
<span @click.stop>
<slot name="headerLeft">
<i class="icon el-icon-arrow-right" :class="{'is-active': data.active}"
@click="active(data)" v-if="data.type!='scenario'"/>
<el-input :draggable="draggable" v-if="isShowInput && isShowNameInput" size="small" v-model="data.name" class="name-input"
@blur="isShowInput = false" :placeholder="$t('commons.input_name')" ref="nameEdit"/>
@blur="isShowInput = false" :placeholder="$t('commons.input_name')" ref="nameEdit" :disabled="data.disabled"/>
<span v-else>
{{data.name}}
<i class="el-icon-edit" style="cursor:pointer" @click="editName" v-tester v-if="data.referenced!='REF' && !data.disabled"/>
@ -26,31 +24,31 @@
<slot name="behindHeaderLeft"></slot>
</span>
<div class="header-right" @click.stop>
<slot name="message"></slot>
<el-tooltip :content="$t('test_resource_pool.enable_disable')" placement="top">
<el-switch v-model="data.enable" class="enable-switch"/>
</el-tooltip>
<slot name="button"></slot>
<el-tooltip content="Copy" placement="top">
<el-button size="mini" icon="el-icon-copy-document" circle @click="copyRow"/>
</el-tooltip>
<el-tooltip :content="$t('commons.remove')" placement="top">
<el-button size="mini" icon="el-icon-delete" type="danger" circle @click="remove"/>
</el-tooltip>
</div>
<div class="header-right" @click.stop>
<slot name="message"></slot>
<el-tooltip :content="$t('test_resource_pool.enable_disable')" placement="top">
<el-switch v-model="data.enable" class="enable-switch"/>
</el-tooltip>
<slot name="button"></slot>
<el-tooltip content="Copy" placement="top">
<el-button size="mini" icon="el-icon-copy-document" circle @click="copyRow"/>
</el-tooltip>
<el-tooltip :content="$t('commons.remove')" placement="top">
<el-button size="mini" icon="el-icon-delete" type="danger" circle @click="remove"/>
</el-tooltip>
</div>
<fieldset :disabled="data.disabled" class="ms-fieldset">
<div class="header">
<el-collapse-transition>
<div v-if="data.active && showCollapse" :draggable="draggable">
<el-divider></el-divider>
<slot></slot>
</div>
</el-collapse-transition>
</div>
</fieldset>
</div>
<div class="header">
<fieldset :disabled="data.disabled" class="ms-fieldset">
<el-collapse-transition>
<div v-if="data.active && showCollapse" :draggable="draggable">
<el-divider></el-divider>
<slot></slot>
</div>
</el-collapse-transition>
</fieldset>
</div>
</el-card>
</template>
@ -163,8 +161,4 @@
border: 0px;
}
.ms-fieldset {
display: contents;
}
</style>

View File

@ -0,0 +1,53 @@
<template>
<div>
<div v-if="request.protocol === 'HTTP'">
<el-input :placeholder="$t('api_test.definition.request.path_all_info')" v-if="request.url" v-model="request.url" style="width: 85%;margin-top: 10px" size="small">
<el-select v-model="request.method" slot="prepend" style="width: 100px" size="small">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
<el-input :placeholder="$t('api_test.definition.request.path_all_info')" v-else v-model="request.path" style="width: 85%;margin-top: 10px" size="small">
<el-select v-model="request.method" slot="prepend" style="width: 100px" size="small">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
</div>
<div v-if="request.protocol === 'TCP' && isCustomizeReq">
<el-form>
<el-row>
<el-col :span="8">
<el-form-item :label="$t('api_test.request.tcp.server')" prop="server">
<el-input class="server-input" v-model="request.server" maxlength="300" show-word-limit size="small"/>
</el-form-item>
</el-col>
<el-col :span="12">
<el-form-item :label="$t('api_test.request.tcp.port')" prop="port" label-width="60px">
<el-input-number v-model="request.port" controls-position="right" :min="0" :max="65535" size="small"/>
</el-form-item>
</el-col>
</el-row>
</el-form>
</div>
</div>
</template>
<script>
import {REQ_METHOD} from "@/business/components/api/definition/model/JsonData";
export default {
name: "CustomizeReqInfo",
props: ['request', 'isCustomizeReq'],
data() {
return {
reqOptions: REQ_METHOD,
}
}
}
</script>
<style scoped>
.server-input {
width: 50%;
}
</style>

View File

@ -25,18 +25,8 @@
</el-tooltip>
</template>
<div v-if="request.protocol === 'HTTP'">
<el-input :placeholder="$t('api_test.definition.request.path_all_info')" v-if="request.url" v-model="request.url" style="width: 85%;margin-top: 10px" size="small">
<el-select v-model="request.method" slot="prepend" style="width: 100px" size="small">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
<el-input :placeholder="$t('api_test.definition.request.path_all_info')" v-else v-model="request.path" style="width: 85%;margin-top: 10px" size="small">
<el-select v-model="request.method" slot="prepend" style="width: 100px" size="small">
<el-option v-for="item in reqOptions" :key="item.id" :label="item.label" :value="item.id"/>
</el-select>
</el-input>
</div>
<customize-req-info :is-customize-req="isCustomizeReq" :request="request"/>
<p class="tip">{{$t('api_test.definition.request.req_param')}} </p>
<ms-api-request-form :isShowEnable="true" :referenced="true" :headers="request.headers " :request="request" v-if="request.protocol==='HTTP' || request.type==='HTTPSamplerProxy'"/>
<ms-tcp-basis-parameters :request="request" v-if="request.protocol==='TCP'|| request.type==='TCPSampler'"/>
@ -65,6 +55,7 @@
import {getUUID} from "@/common/js/utils";
import ApiBaseComponent from "../common/ApiBaseComponent";
import ApiResponseComponent from "./ApiResponseComponent";
import CustomizeReqInfo from "@/business/components/api/automation/scenario/common/CustomizeReqInfo";
export default {
name: "MsApiComponent",
@ -79,13 +70,13 @@
currentEnvironmentId: String,
},
components: {
CustomizeReqInfo,
ApiBaseComponent, ApiResponseComponent,
MsSqlBasisParameters, MsTcpBasisParameters, MsDubboBasisParameters, MsApiRequestForm, MsRequestResultTail, MsRun
},
data() {
return {
loading: false,
reqOptions: REQ_METHOD,
reportId: "",
runData: [],
isShowInput: false,
@ -98,15 +89,8 @@
//
this.getApiInfo();
if (this.request.protocol === 'HTTP') {
try {
let urlObject = new URL(this.request.url);
let url = urlObject.protocol + "//" + urlObject.host + "/";
} catch (e) {
if (this.request.url) {
this.request.path = this.request.url;
this.request.url = undefined;
}
}
this.setUrl(this.request.url);
this.setUrl(this.request.path);
// auth
if (this.request.hashTree) {
for (let index in this.request.hashTree) {
@ -180,6 +164,17 @@
copyRow() {
this.$emit('copyRow', this.request, this.node);
},
setUrl(url) {
try {
new URL(url);
this.request.url = url;
} catch (e) {
if (url) {
this.request.path = url;
this.request.url = undefined;
}
}
},
getApiInfo() {
if (this.request.id && this.request.referenced === 'REF') {
let requestResult = this.request.requestResult;
@ -192,7 +187,8 @@
this.request.enable = enable;
if (response.data.path && response.data.path != null) {
this.request.path = response.data.path;
this.request.url = response.data.path;
this.request.url = response.data.url;
this.setUrl(this.request.path);
}
if (response.data.method && response.data.method != null) {
this.request.method = response.data.method;
@ -239,6 +235,7 @@
this.loading = true;
this.runData = [];
this.request.useEnvironment = this.currentEnvironmentId;
this.request.customizeReq = this.isCustomizeReq;
let debugData = {
id: this.currentScenario.id, name: this.currentScenario.name, type: "scenario",
variables: this.currentScenario.variables, referenced: 'Created', enableCookieShare: this.enableCookieShare,

View File

@ -16,7 +16,6 @@
import MsApiComponent from "./ApiComponent";
import MsLoopController from "./LoopController";
import MsApiScenarioComponent from "./ApiScenarioComponent";
import {getUUID} from "@/common/js/utils";
export default {
name: "ComponentConfig",

View File

@ -127,7 +127,7 @@
</ms-delete-confirm>
<ms-dialog-footer style="float: right;margin: 20px"
@confirm="confirm">
@confirm="confirm" @cancel="cancel">
</ms-dialog-footer>
</el-card>
@ -203,8 +203,12 @@
confirm() {
if (this.selection.length==0) {
this.$warning(this.$t("api_test.definition.request.test_plan_select"));
}else{
this.$emit('addTestPlan', this.selection);
}
this.$emit('addTestPlan', this.selection);
},
cancel(){
this.$emit('cancel');
},
select(selection) {
this.selection = selection.map(s => s.id);

View File

@ -1,14 +1,22 @@
<template>
<el-dialog :title="$t('api_test.scenario.variables')" :close-on-click-modal="false"
:visible.sync="visible" class="environment-dialog" width="60%"
@close="close">
:visible.sync="visible" class="visible-dialog" width="60%"
@close="close" v-loading="loading">
<div>
<el-input placeholder="变量名称搜索" style="width: 50%;margin: 0px 0px 10px" v-model="selectVariable" size="small" @change="filter" @keyup.enter="filter">
<el-select v-model="searchType" slot="prepend" placeholder="类型" style="width: 90px" @change="filter">
<el-option value="CONSTANT" label="常量"></el-option>
<el-option value="LIST" label="列表"></el-option>
<el-option value="CSV" label="CSV"></el-option>
<el-option value="COUNTER" label="计数器"></el-option>
<el-option value="RANDOM" label="随机数"></el-option>
</el-select>
</el-input>
<el-row>
<el-col :span="12">
<div style="border:1px #DCDFE6 solid; min-height: 400px;border-radius: 4px ;width: 100% ;">
<el-table ref="table" border :data="variables" class="adjust-table" @select-all="select" @select="select"
v-loading="loading" @row-click="edit">
v-loading="loading" @row-click="edit" height="400px" :row-class-name="tableRowClassName">
<el-table-column type="selection" width="38"/>
<el-table-column prop="num" label="ID" sortable/>
<el-table-column prop="name" :label="$t('api_test.variable_name')" sortable show-overflow-tooltip/>
@ -28,15 +36,12 @@
<ms-edit-list-value v-if="editData.type=='LIST'" ref="listValue" :editData="editData"/>
<ms-edit-csv v-if="editData.type=='CSV'" ref="csv" :editData.sync="editData"/>
</el-col>
</el-row>
</div>
<template v-slot:footer>
<div style="margin:20px">
<div>
<el-button style="margin-right:10px" @click="deleteVariable">{{$t('commons.delete')}}</el-button>
<el-dropdown split-button type="primary" @command="handleClick" @click="handleClick('CONSTANT')" placement="top-end">
{{$t('commons.add')}}
<el-dropdown-menu slot="dropdown">
@ -49,8 +54,6 @@
</el-dropdown>
</div>
</template>
</el-dialog>
</template>
@ -63,7 +66,7 @@
import MsEditRandom from "./EditRandom";
import MsEditListValue from "./EditListValue";
import MsEditCsv from "./EditCsv";
import {getUUID} from "@/common/js/utils";
import {_filter, getUUID} from "@/common/js/utils";
export default {
name: "MsVariableList",
@ -80,6 +83,9 @@
data() {
return {
variables: [],
searchType: "",
selectVariable: "",
condition: {},
types: new Map([
['CONSTANT', '常量'],
['LIST', '列表'],
@ -105,6 +111,12 @@
edit(row) {
this.editData = row;
},
tableRowClassName(row) {
if (row.row.hidden) {
return 'ms-variable-hidden-row';
}
return '';
},
addParameters(v) {
v.id = getUUID();
if (v.type === 'CSV') {
@ -133,10 +145,13 @@
this.visible = false;
let saveVariables = [];
this.variables.forEach(item => {
item.hidden = undefined;
if (item.name && item.name != "") {
saveVariables.push(item);
}
})
this.selectVariable = "";
this.searchType = "";
this.$emit('setVariables', saveVariables);
},
deleteVariable() {
@ -150,11 +165,50 @@
this.variables.splice(index, 1);
})
this.selection = [];
}
},
filter() {
let datas = [];
this.variables.forEach(item => {
if (this.searchType && this.searchType != "" && this.selectVariable && this.selectVariable != "") {
if ((item.type && item.type.toLowerCase().indexOf(this.searchType.toLowerCase()) == -1) || (item.name && item.name.toLowerCase().indexOf(this.selectVariable.toLowerCase()) == -1)) {
item.hidden = true;
} else {
item.hidden = undefined;
}
}
else if (this.selectVariable && this.selectVariable != "") {
if (item.name && item.name.toLowerCase().indexOf(this.selectVariable.toLowerCase()) == -1) {
item.hidden = true;
} else {
item.hidden = undefined;
}
}
else if (this.searchType && this.searchType != "") {
if (item.type && item.type.toLowerCase().indexOf(this.searchType.toLowerCase()) == -1) {
item.hidden = true;
} else {
item.hidden = undefined;
}
}
datas.push(item);
})
this.variables = datas;
},
createFilter(queryString) {
return item => {
return (item.type && item.type.toLowerCase().indexOf(queryString.toLowerCase()) !== -1);
};
},
}
}
</script>
<style scoped>
.ms-variable-hidden-row {
display: none;
}
/deep/ .el-dialog__body {
padding: 10px 10px;
}
</style>

View File

@ -1,4 +1,5 @@
<template>
<div>
<ms-container v-if="renderComponent">
<ms-aside-container>
<ms-api-module
@ -9,6 +10,7 @@
@debug="debug"
@saveAsEdit="editApi"
@setModuleOptions="setModuleOptions"
@setNodeTree="setNodeTree"
@enableTrash="enableTrash"
:type="'edit'"
ref="nodeTree"/>
@ -25,6 +27,8 @@
<!-- 列表集合 -->
<ms-api-list
v-if="item.type === 'list' && isApiListEnable"
:module-tree="nodeTree"
:module-options="moduleOptions"
:current-protocol="currentProtocol"
:visible="visible"
:currentRow="currentRow"
@ -102,8 +106,12 @@
</template>
</el-tab-pane>
</el-tabs>
</ms-main-container>
</ms-container>
</div>
</template>
<script>
import MsApiList from './components/list/ApiList';
@ -177,7 +185,7 @@
currentModule: null,
selectNodeIds: [],
currentApi: {},
moduleOptions: {},
moduleOptions: [],
trashEnable: false,
apiTabs: [{
title: this.$t('api_test.definition.api_title'),
@ -187,7 +195,8 @@
}],
isApiListEnable: true,
syncTabs: [],
projectId: ""
projectId: "",
nodeTree: []
}
},
mounted() {
@ -215,6 +224,7 @@
}
},
methods: {
changeRedirectParam(redirectIDParam) {
this.redirectID = redirectIDParam;
},
@ -394,6 +404,9 @@
setModuleOptions(data) {
this.moduleOptions = data;
},
setNodeTree(data) {
this.nodeTree = data;
},
changeSelectDataRangeAll(tableType) {
this.$route.params.dataSelectRange = 'all';
},

View File

@ -127,6 +127,9 @@
this.request = createComponent("JDBCSampler");
this.currentApi.request = this.request;
}
if (!this.currentApi.request.variables) {
this.currentApi.request.variables = [];
}
},
initDubbo() {
if (!this.setRequest()) {

View File

@ -86,7 +86,7 @@
rule: {
name: [
{required: true, message: this.$t('test_track.case.input_name'), trigger: 'blur'},
{max: 50, message: this.$t('test_track.length_less_than') + '50', trigger: 'blur'}
{max: 100, message: this.$t('test_track.length_less_than') + '100', trigger: 'blur'}
],
path: [{required: true, message: this.$t('api_test.definition.request.path_info'), trigger: 'blur'}, {validator: validateURL, trigger: 'blur'}],
userId: [{required: true, message: this.$t('test_track.case.input_maintainer'), trigger: 'change'}],

View File

@ -0,0 +1,127 @@
<template>
<div v-if="dialogVisible" class="batch-move" v-loading="result.loading">
<el-dialog :title="this.$t('test_track.case.select_catalog')"
:visible.sync="dialogVisible"
:before-close="close"
:destroy-on-close="true"
width="20%"
>
<div>
<el-input :placeholder="$t('test_track.module.search')" v-model="filterText" size="small"/>
<el-tree
class="filter-tree node-tree"
:data="treeNodes"
node-key="id"
:filter-node-method="filterNode"
:expand-on-click-node="false"
highlight-current
style="overflow: auto"
@node-click="nodeClick"
ref="tree"
>
<template v-slot:default="{node}">
<span>
<span class="node-icon">
<i class="el-icon-folder"/>
</span>
<span class="node-title">{{node.label}}</span>
</span>
</template>
</el-tree>
</div>
<template v-slot:footer>
<ms-dialog-footer
@cancel="close"
@confirm="save"/>
</template>
</el-dialog>
</div>
</template>
<script>
import MsDialogFooter from "@/business/components/common/components/MsDialogFooter";
export default {
name: "CaseBatchMove",
components: {
MsDialogFooter
},
data() {
return {
treeNodes: [],
selectIds: [],
selectNode: {},
dialogVisible: false,
currentKey: "",
moduleOptions: [],
filterText: "",
result: {},
}
},
watch: {
filterText(val) {
this.$refs.tree.filter(val);
console.log( this.treeNodes)
}
},
methods: {
open(treeNodes, selectIds, moduleOptions) {
this.dialogVisible = true;
this.treeNodes = treeNodes;
this.selectIds = selectIds;
this.moduleOptions = moduleOptions;
},
save() {
if (!this.currentKey) {
this.$warning(this.$t('test_track.case.input_module'));
return;
}
let param = {};
param.nodeId = this.currentKey;
if (this.moduleOptions) {
this.moduleOptions.forEach(item => {
if (item.id === this.currentKey) {
param.nodePath = item.path;
}
});
}
param.ids = this.selectIds;
this.$emit('moveSave', param);
},
refresh() {
this.$emit("refresh");
},
close() {
this.filterText = "";
this.dialogVisible = false;
this.selectNode = {};
},
filterNode(value, data) {
if (!value) return true;
return data.label.indexOf(value) !== -1;
},
nodeClick() {
this.currentKey = this.$refs.tree.getCurrentKey();
}
}
}
</script>
<style scoped>
.node-title {
width: 0;
text-overflow: ellipsis;
white-space: nowrap;
flex: 1 1 auto;
padding: 0 5px;
overflow: hidden;
}
.batch-move {
height: 500px;
}
</style>

View File

@ -288,6 +288,9 @@
url = "/api/testcase/update";
} else {
tmp.request.path = this.api.path;
if (tmp.request.protocol != "dubbo://" && tmp.request.protocol != "DUBBO") {
tmp.request.method = this.api.method;
}
}
if (tmp.tags instanceof Array) {
tmp.tags = JSON.stringify(tmp.tags);

View File

@ -109,29 +109,29 @@
<script>
import MsApiRequestForm from "../request/http/ApiHttpRequestForm";
import MsResponseText from "../response/ResponseText";
import {WORKSPACE_ID} from '../../../../../../common/js/constants';
import {API_STATUS, REQ_METHOD} from "../../model/JsonData";
import {KeyValue} from "../../model/ApiTestModel";
import MsInputTag from "@/business/components/api/automation/scenario/MsInputTag";
import MsJsr233Processor from "../../../automation/scenario/component/Jsr233Processor";
import MsApiRequestForm from "../request/http/ApiHttpRequestForm";
import MsResponseText from "../response/ResponseText";
import {WORKSPACE_ID} from '../../../../../../common/js/constants';
import {API_STATUS, REQ_METHOD} from "../../model/JsonData";
import {KeyValue} from "../../model/ApiTestModel";
import MsInputTag from "@/business/components/api/automation/scenario/MsInputTag";
import MsJsr233Processor from "../../../automation/scenario/component/Jsr233Processor";
export default {
name: "MsAddCompleteHttpApi",
components: {MsJsr233Processor, MsResponseText, MsApiRequestForm, MsInputTag},
data() {
let validateURL = (rule, value, callback) => {
if (!this.httpForm.path.startsWith("/") || this.httpForm.path.match(/\s/) != null) {
callback(this.$t('api_test.definition.request.path_valid_info'));
}
callback();
};
return {
export default {
name: "MsAddCompleteHttpApi",
components: {MsJsr233Processor, MsResponseText, MsApiRequestForm, MsInputTag},
data() {
let validateURL = (rule, value, callback) => {
if (!this.httpForm.path.startsWith("/") || this.httpForm.path.match(/\s/) != null) {
callback(this.$t('api_test.definition.request.path_valid_info'));
}
callback();
};
return {
rule: {
name: [
{required: true, message: this.$t('test_track.case.input_name'), trigger: 'blur'},
{max: 50, message: this.$t('test_track.length_less_than') + '50', trigger: 'blur'}
{max: 100, message: this.$t('test_track.length_less_than') + '100', trigger: 'blur'}
],
path: [{required: true, message: this.$t('api_test.definition.request.path_info'), trigger: 'blur'}, {
validator: validateURL,

View File

@ -147,8 +147,6 @@
},
saveAs() {
let obj = {request: this.request};
obj.request.server = this.debugForm.server;
obj.request.port = this.debugForm.port;
obj.server = this.debugForm.server;
obj.port = this.debugForm.port;
obj.request.id = getUUID();

View File

@ -15,6 +15,7 @@
@select-all="handleSelectAll"
@filter-change="filter"
@sort-change="sort"
@header-dragend="headerDragend"
@select="handleSelect" :height="screenHeight">
<el-table-column type="selection" width="50"/>
@ -25,25 +26,28 @@
@selectPageAll="isSelectDataAll(false)"
@selectAll="isSelectDataAll(true)"/>
<el-table-column width="30" :resizable="false" align="center">
<el-table-column width="30" :resizable="false" min-width="30px" align="center">
<template v-slot:default="scope">
<show-more-btn :is-show="scope.row.showMore" :buttons="buttons" :size="selectDataCounts"/>
</template>
</el-table-column>
<el-table-column prop="num" label="ID" show-overflow-tooltip>
<el-table-column prop="num" label="ID" min-width="120px" show-overflow-tooltip>
<template slot-scope="scope">
<el-tooltip content="编辑">
<a style="cursor:pointer" @click="handleTestCase(scope.row)"> {{ scope.row.num }} </a>
</el-tooltip>
</template>
</el-table-column>
<el-table-column prop="name" :label="$t('test_track.case.name')" show-overflow-tooltip/>
<el-table-column prop="name" min-width="160px" :label="$t('test_track.case.name')" show-overflow-tooltip/>
<el-table-column
prop="priority"
:filters="priorityFilters"
column-key="priority"
min-width="120px"
:label="$t('test_track.case.priority')"
show-overflow-tooltip>
<template v-slot:default="scope">
@ -54,10 +58,12 @@
<el-table-column
sortable="custom"
prop="path"
min-width="180px"
:label="$t('api_test.definition.api_path')"
show-overflow-tooltip/>
<el-table-column prop="tags" :label="$t('commons.tag')">
<el-table-column prop="tags" min-width="120px" :label="$t('commons.tag')">
<template v-slot:default="scope">
<div v-for="(itemName,index) in scope.row.tags" :key="index">
<ms-tag type="success" effect="plain" :content="itemName"/>
@ -68,12 +74,14 @@
<el-table-column
prop="createUser"
:label="'创建人'"
show-overflow-tooltip/>
<el-table-column
sortable="custom"
width="160"
min-width="160"
:label="$t('api_test.definition.api_last_time')"
prop="updateTime">
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
@ -82,11 +90,6 @@
<el-table-column fixed="right" v-if="!isReadOnly" :label="$t('commons.operating')" min-width="130" align="center">
<template v-slot:default="scope">
<!--<el-button type="text" @click="reductionApi(scope.row)" v-if="trashEnable">{{$t('commons.reduction')}}</el-button>-->
<!-- <el-button type="text" @click="handleTestCase(scope.row)" v-if="!trashEnable">{{ $t('commons.edit') }}-->
<!-- </el-button>-->
<!-- <el-button type="text" @click="handleDelete(scope.row)" style="color: #F56C6C">{{ $t('commons.delete') }}-->
<!-- </el-button>-->
<ms-table-operator-button :tip="$t('commons.edit')" icon="el-icon-edit" @exec="handleTestCase(scope.row)" v-tester/>
<ms-table-operator-button :tip="$t('commons.delete')" icon="el-icon-delete" @exec="handleDelete(scope.row)" type="danger" v-tester/>
<ms-api-case-table-extend-btns @showCaseRef="showCaseRef" @showEnvironment="showEnvironment" @createPerformance="createPerformance" :row="scope.row" v-tester/>
@ -505,6 +508,14 @@ export default {
this.clickRow = row;
this.$refs.setEnvironment.open(row);
},
headerDragend(newWidth,oldWidth,column,event){
let finalWidth = newWidth;
if(column.minWidth>finalWidth){
finalWidth = column.minWidth;
}
column.width = finalWidth;
column.realWidth = finalWidth;
},
createPerformance(row, environment) {
/**
* 思路调用后台创建性能测试的方法把当前案例的hashTree在后台转化为jmx并文件创建性能测试

View File

@ -126,6 +126,7 @@
buildNodePath(node, {path: ''}, moduleOptions);
});
this.$emit('setModuleOptions', moduleOptions);
this.$emit('setNodeTree', this.data);
if (this.$refs.nodeTree) {
this.$refs.nodeTree.filter(this.condition.filterText);
}

View File

@ -145,7 +145,7 @@ export default {
}
.filter-input {
width: 175px;
width: 174px;
padding-left: 3px;
}

View File

@ -1,5 +1,5 @@
<template>
<div>
<div v-loading="isReloadData">
<el-row>
<el-col :span="21" style="padding-bottom: 20px">
<div style="border:1px #DCDFE6 solid; height: 100%;border-radius: 4px ;width: 100% ;margin: 20px">
@ -28,7 +28,7 @@
</el-col>
<el-col :span="8">
<el-form-item :label="$t('api_test.request.sql.dataSource')" prop="dataSourceId" style="margin-left: 10px">
<el-select v-model="request.dataSourceId" size="small">
<el-select v-model="request.dataSourceId" size="small" @change="reload">
<el-option v-for="(item, index) in databaseConfigsOptions" :key="index" :value="item.id" :label="item.name"/>
</el-select>
</el-form-item>
@ -143,7 +143,7 @@
activeName: "variables",
rules: {
environmentId: [{required: true, message: this.$t('api_test.definition.request.run_env'), trigger: 'change'}],
dataSourceId: [{required: true, message: this.$t('api_test.request.sql.dataSource'), trigger: 'change'}],
dataSourceId: [{required: true, message: this.$t('api_test.request.sql.dataSource'), trigger: 'blur'}],
},
}
},

View File

@ -63,23 +63,23 @@
</template>
<script>
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "../MsCodeEdit";
import MsDropdown from "../../../../common/components/MsDropdown";
import {BODY_FORMAT} from "../../model/ApiTestModel";
import MsSqlResultTable from "./SqlResultTable";
import MsAssertionResults from "./AssertionResults";
import MsCodeEdit from "../MsCodeEdit";
import MsDropdown from "../../../../common/components/MsDropdown";
import {BODY_FORMAT} from "../../model/ApiTestModel";
import MsSqlResultTable from "./SqlResultTable";
export default {
name: "MsResponseResult",
export default {
name: "MsResponseResult",
components: {
MsDropdown,
MsCodeEdit,
MsAssertionResults,
MsSqlResultTable
},
components: {
MsDropdown,
MsCodeEdit,
MsAssertionResults,
MsSqlResultTable
},
props: {
props: {
response: Object,
currentProtocol: String,
},
@ -107,7 +107,7 @@
this.mode = mode;
},
setBodyType() {
if (!this.response.responseResult.headers) {
if (!this.response.responseResult || !this.response.responseResult.headers) {
return;
}
if (this.response.responseResult.headers.indexOf("Content-Type: application/json") > 0) {

View File

@ -1,6 +1,6 @@
<template>
<el-dialog
title="选则模块"
:title="$t('commons.module.select_module')"
:visible.sync="oneClickOperationVisible"
width="600px"
left
@ -11,7 +11,7 @@
<ms-node-tree
v-loading="result.loading"
:tree-nodes="data"
allLabel="默认模块"
:allLabel="$t('commons.module.default_module')"
@add="add"
:type="'edit'"
@edit="edit"

View File

@ -74,4 +74,9 @@
<style scoped>
.jar-config-list {
max-height: 600px;
overflow: scroll;
}
</style>

View File

@ -172,6 +172,22 @@ export const API_CASE_RESULT = {
}
}
export const API_SCENARIO_RESULT = {
key: "status",
name: 'MsTableSearchSelect',
label: 'test_track.plan_view.execute_result',
operator: {
options: [OPERATORS.IN, OPERATORS.NOT_IN]
},
options: [
{value: 'Success', label: 'api_test.automation.success'},
{value: 'Fail', label: 'api_test.automation.fail'}
],
props: { // 尾部控件的props一般为element ui控件的props
multiple: true
}
}
export const API_METHOD = {
key: "method",
name: 'MsTableSearchSelect',
@ -442,4 +458,6 @@ export const API_DEFINITION_CONFIGS = [NAME, API_METHOD, API_PATH, API_STATUS, A
export const API_CASE_CONFIGS = [NAME, API_CASE_PRIORITY, API_TAGS, API_CASE_RESULT, UPDATE_TIME, CREATE_TIME, CREATOR];
export const API_SCENARIO_CONFIGS = [NAME, API_CASE_PRIORITY, API_TAGS, API_SCENARIO_RESULT, UPDATE_TIME, CREATE_TIME, CREATOR];
export const TEST_PLAN_REPORT_CONFIGS = [NAME, TEST_PLAN_NAME,CREATOR, CREATE_TIME, TEST_PLAN_TRIGGER_MODE, TEST_PLAN_STATUS];

View File

@ -2,25 +2,41 @@
<el-menu :unique-opened="true" mode="horizontal" router
class="header-user-menu align-right"
background-color="#2c2a48"
active-text-color="#fff"
default-active="1"
text-color="#fff">
<el-submenu index="1" popper-class="submenu"
<el-menu-item index="1" v-show="false">Placeholder</el-menu-item>
<el-submenu index="1" popper-class="org-ws-submenu"
v-roles="['org_admin', 'test_manager', 'test_user', 'test_viewer']">
<template v-slot:title>{{$t('commons.organization')}}: {{currentOrganizationName}}</template>
<label v-for="(item,index) in organizationList" :key="index">
<el-menu-item @click="changeOrg(item)">{{item.name}}
<template v-slot:title>{{ $t('commons.organization') }}: {{ currentOrganizationName }}</template>
<el-input :placeholder="$t('project.search_by_name')"
prefix-icon="el-icon-search"
v-model="searchOrg"
clearable
class="search-input"
size="small"/>
<div class="org-ws-menu">
<el-menu-item @click="changeOrg(item)" v-for="(item,index) in organizationList" :key="index">
{{ item.name }}
<i class="el-icon-check"
v-if="item.id === currentUserInfo.lastOrganizationId"></i>
</el-menu-item>
</label>
</div>
</el-submenu>
<el-submenu index="2" popper-class="submenu" v-roles="['test_manager', 'test_user', 'test_viewer']">
<template v-slot:title>{{$t('commons.workspace')}}: {{currentWorkspaceName}}</template>
<label v-for="(item,index) in workspaceList" :key="index">
<el-menu-item @click="changeWs(item)">
{{item.name}}
<template v-slot:title>{{ $t('commons.workspace') }}: {{ currentWorkspaceName }}</template>
<el-input :placeholder="$t('project.search_by_name')"
prefix-icon="el-icon-search"
v-model="searchWs"
clearable
class="search-input"
size="small"/>
<div class="org-ws-menu">
<el-menu-item @click="changeWs(item)" v-for="(item,index) in workspaceList" :key="index">
{{ item.name }}
<i class="el-icon-check" v-if="item.id === currentUserInfo.lastWorkspaceId"></i>
</el-menu-item>
</label>
</div>
</el-submenu>
</el-menu>
</template>
@ -34,107 +50,170 @@ import {
ROLE_TEST_VIEWER,
WORKSPACE_ID
} from '../../../../common/js/constants';
import {getCurrentUser, hasRoles, saveLocalStorage} from "../../../../common/js/utils";
import {getCurrentUser, hasRoles, saveLocalStorage} from "../../../../common/js/utils";
export default {
name: "MsHeaderOrgWs",
created() {
this.initMenuData();
this.getCurrentUserInfo();
},
data() {
return {
organizationList: [
{name: this.$t('organization.none')},
],
workspaceList: [
{name: this.$t('workspace.none')},
],
currentUserInfo: {},
currentUserId: getCurrentUser().id,
workspaceIds: [],
currentOrganizationName: '',
currentWorkspaceName: ''
}
},
computed: {
currentUser: () => {
return getCurrentUser();
}
},
methods: {
initMenuData() {
if (hasRoles(ROLE_ORG_ADMIN, ROLE_TEST_VIEWER, ROLE_TEST_USER, ROLE_TEST_MANAGER)) {
this.$get("/organization/list/userorg/" + encodeURIComponent(this.currentUserId), response => {
let data = response.data;
this.organizationList = data;
let org = data.filter(r => r.id === this.currentUser.lastOrganizationId);
if (org.length > 0) {
this.currentOrganizationName = org[0].name;
}
});
}
if (hasRoles(ROLE_TEST_VIEWER, ROLE_TEST_USER, ROLE_TEST_MANAGER)) {
if (!this.currentUser.lastOrganizationId) {
return false;
}
this.$get("/workspace/list/orgworkspace/", response => {
let data = response.data;
if (data.length === 0) {
this.workspaceList = [{name: this.$t('workspace.none')}]
} else {
this.workspaceList = data;
let workspace = data.filter(r => r.id === this.currentUser.lastWorkspaceId);
if (workspace.length > 0) {
this.currentWorkspaceName = workspace[0].name;
localStorage.setItem(WORKSPACE_ID, workspace[0].id);
}
}
})
}
},
getCurrentUserInfo() {
this.$get("/user/info/" + encodeURIComponent(this.currentUserId), response => {
this.currentUserInfo = response.data;
})
},
changeOrg(data) {
let orgId = data.id;
if (!orgId) {
return false;
}
this.$post("/user/switch/source/org/" + orgId, {}, response => {
saveLocalStorage(response);
if (response.data.workspaceId) {
localStorage.setItem("workspace_id", response.data.workspaceId);
}
localStorage.removeItem(PROJECT_ID);
this.$router.push('/').then(() => {
window.location.reload();
}).catch(err => err);
});
},
changeWs(data) {
let workspaceId = data.id;
if (!workspaceId) {
return false;
}
this.$post("/user/switch/source/ws/" + workspaceId, {}, response => {
saveLocalStorage(response);
localStorage.setItem("workspace_id", workspaceId);
localStorage.removeItem(PROJECT_ID);
this.$router.push('/').then(() => {
window.location.reload();
}).catch(err => err);
})
}
export default {
name: "MsHeaderOrgWs",
created() {
this.initMenuData();
this.getCurrentUserInfo();
},
data() {
return {
organizationList: [
{name: this.$t('organization.none')},
],
workspaceList: [
{name: this.$t('workspace.none')},
],
currentUserInfo: {},
currentUserId: getCurrentUser().id,
workspaceIds: [],
currentOrganizationName: '',
currentWorkspaceName: '',
searchOrg: '',
searchWs: '',
orgListCopy: [{name: this.$t('organization.none')}],
wsListCopy: [{name: this.$t('workspace.none')}]
}
},
computed: {
currentUser: () => {
return getCurrentUser();
}
},
watch: {
searchOrg(val) {
this.query('org', val);
},
searchWs(val) {
this.query('ws', val);
}
},
methods: {
initMenuData() {
if (hasRoles(ROLE_ORG_ADMIN, ROLE_TEST_VIEWER, ROLE_TEST_USER, ROLE_TEST_MANAGER)) {
this.$get("/organization/list/userorg/" + encodeURIComponent(this.currentUserId), response => {
let data = response.data;
this.organizationList = data;
this.orgListCopy = data;
let org = data.filter(r => r.id === this.currentUser.lastOrganizationId);
if (org.length > 0) {
this.currentOrganizationName = org[0].name;
}
});
}
if (hasRoles(ROLE_TEST_VIEWER, ROLE_TEST_USER, ROLE_TEST_MANAGER)) {
if (!this.currentUser.lastOrganizationId) {
return false;
}
this.$get("/workspace/list/orgworkspace/", response => {
let data = response.data;
if (data.length === 0) {
this.workspaceList = [{name: this.$t('workspace.none')}]
} else {
this.workspaceList = data;
this.wsListCopy = data;
let workspace = data.filter(r => r.id === this.currentUser.lastWorkspaceId);
if (workspace.length > 0) {
this.currentWorkspaceName = workspace[0].name;
localStorage.setItem(WORKSPACE_ID, workspace[0].id);
}
}
})
}
},
getCurrentUserInfo() {
this.$get("/user/info/" + encodeURIComponent(this.currentUserId), response => {
this.currentUserInfo = response.data;
})
},
changeOrg(data) {
let orgId = data.id;
if (!orgId) {
return false;
}
this.$post("/user/switch/source/org/" + orgId, {}, response => {
saveLocalStorage(response);
if (response.data.workspaceId) {
localStorage.setItem("workspace_id", response.data.workspaceId);
}
localStorage.removeItem(PROJECT_ID);
this.$router.push('/').then(() => {
window.location.reload();
}).catch(err => err);
});
},
changeWs(data) {
let workspaceId = data.id;
if (!workspaceId) {
return false;
}
this.$post("/user/switch/source/ws/" + workspaceId, {}, response => {
saveLocalStorage(response);
localStorage.setItem("workspace_id", workspaceId);
localStorage.removeItem(PROJECT_ID);
this.$router.push('/').then(() => {
window.location.reload();
}).catch(err => err);
})
},
query(sign, queryString) {
if (sign === 'org') {
this.organizationList = queryString ? this.orgListCopy.filter(this.createFilter(queryString)) : this.orgListCopy;
}
if (sign === 'ws') {
this.workspaceList = queryString ? this.wsListCopy.filter(this.createFilter(queryString)) : this.wsListCopy;
}
},
createFilter(queryString) {
return item => {
return (item.name.toLowerCase().indexOf(queryString.toLowerCase()) !== -1);
};
},
}
}
</script>
<style scoped>
.el-icon-check {
color: #44b349;
margin-left: 10px;
}
.el-icon-check {
color: #44b349;
margin-left: 10px;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
position: fixed;
}
::-webkit-scrollbar-thumb {
border-radius: 1em;
background-color: #595591;
position: fixed;
}
::-webkit-scrollbar-track {
border-radius: 1em;
background-color: transparent;
position: fixed;
}
.org-ws-menu {
height: 180px;
overflow: auto;
}
.search-input {
padding: 0;
margin-top: -4px;
background-color: #595591;
}
.search-input >>> .el-input__inner {
border-radius: 0;
background-color: #2d2a49;
color: #d2ced8;
border-color: #b4aebe;
}
</style>

View File

@ -3,8 +3,8 @@
<el-row class="row" :gutter="20">
<el-col :span="8" class="ms-col-name">
<div :style="{marginLeft:`${10*deep}px`}" class="ms-col-name-c"/>
<span v-if="pickValue.type==='object'" :class="hidden? 'el-tree-node__expand-icon el-icon-caret-right':
'expanded el-tree-node__expand-icon el-icon-caret-right'" @click="hidden = !hidden"/>
<span v-if="pickValue.type==='object'" :class="hidden? 'el-icon-caret-left ms-transform':
'el-icon-caret-bottom'" @click="hidden = !hidden"/>
<span v-else style="width:10px;display:inline-block"></span>
<input class="el-input el-input__inner" style="height: 32px" :disabled="disabled || root" :value="pickKey" @blur="onInputName" size="small"/>
@ -393,4 +393,8 @@
border-left: 4px solid #783887;
margin: 0px 0px 10px;
}
.ms-transform {
transform: rotate(-180deg);
transition: 0ms;
}
</style>

View File

@ -293,12 +293,12 @@ export default {
};
this.result = this.$request(config).then(response => {
const content = response.data;
const blob = new Blob([content]);
const blob = new Blob([content], {type: "application/octet-stream"});
if ("download" in document.createElement("a")) {
// IE
// chrome/firefox
let aTag = document.createElement('a');
aTag.download = this.reportId + ".jtl";
aTag.download = this.reportId + ".zip";
aTag.href = URL.createObjectURL(blob);
aTag.click();
URL.revokeObjectURL(aTag.href)

View File

@ -10,8 +10,9 @@
<el-table-column prop="description" :label="$t('commons.description')"/>
<el-table-column :label="$t('commons.member')">
<template v-slot:default="scope">
<el-button type="text" class="member-size" @click="cellClick(scope.row)">{{scope.row.memberSize}}
</el-button>
<el-link type="primary" class="member-size" @click="cellClick(scope.row)">
{{ scope.row.memberSize }}
</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('commons.operating')">
@ -506,7 +507,6 @@
.member-size {
text-decoration: underline;
cursor: pointer;
}
.select-width {

View File

@ -114,9 +114,7 @@ export default {
}
],
phone: [
{required: true, message: this.$t('user.input_phone'), trigger: 'blur'},
{
required: false,
pattern: PHONE_REGEX,
message: this.$t('member.mobile_number_format_is_incorrect'),
trigger: 'blur'

View File

@ -399,9 +399,7 @@ export default {
}
],
phone: [
{required: true, message: this.$t('user.input_phone'), trigger: 'blur'},
{
required: true,
pattern: PHONE_REGEX,
message: this.$t('user.mobile_number_format_is_incorrect'),
trigger: 'blur'

View File

@ -29,6 +29,7 @@
@filter-change="filter"
@select-all="handleSelectAll"
@select="handleSelect"
@header-dragend="headerDragend"
@cell-mouse-enter="showPopover"
row-key="id"
class="test-content adjust-table ms-select-all-fixed"
@ -562,6 +563,14 @@ export default {
this.selectDataCounts = this.selectRows.size;
}
},
headerDragend(newWidth,oldWidth,column,event){
let finalWidth = newWidth;
if(column.minWidth>finalWidth){
finalWidth = column.minWidth;
}
column.width = finalWidth;
column.realWidth = finalWidth;
},
moveSave(param) {
param.condition = this.condition;
this.result = this.$post('/test/case/batch/edit', param, () => {

View File

@ -35,6 +35,7 @@
@select="handleSelectionChange"
row-key="id"
@row-click="showDetail"
@header-dragend="headerDragend"
:data="tableData">
<el-table-column
@ -48,17 +49,23 @@
prop="num"
sortable="custom"
:label="$t('commons.id')"
min-width="120px"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="name"
:label="$t('commons.name')"
min-width="120px"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="priority"
:filters="priorityFilters"
column-key="priority"
min-width="100px"
:label="$t('test_track.case.priority')">
<template v-slot:default="scope">
<priority-table-item :value="scope.row.priority" ref="priority"/>
@ -70,13 +77,16 @@
:filters="typeFilters"
column-key="type"
:label="$t('test_track.case.type')"
min-width="80px"
show-overflow-tooltip>
<template v-slot:default="scope">
<type-table-item :value="scope.row.type"/>
</template>
</el-table-column>
<el-table-column prop="tags" :label="$t('commons.tag')">
<el-table-column prop="tags" :label="$t('commons.tag')" min-width="120px"
>
<template v-slot:default="scope">
<div v-for="(tag, index) in scope.row.showTags" :key="tag + '_' + index">
<ms-tag type="success" effect="plain" :content="tag"/>
@ -89,6 +99,8 @@
:filters="methodFilters"
column-key="method"
:label="$t('test_track.case.method')"
min-width="100px"
show-overflow-tooltip>
<template v-slot:default="scope">
<method-table-item :value="scope.row.method"/>
@ -98,17 +110,23 @@
<el-table-column
prop="nodePath"
:label="$t('test_track.case.module')"
min-width="120px"
show-overflow-tooltip>
</el-table-column>
<el-table-column
prop="projectName"
:label="$t('test_track.plan.plan_project')"
min-width="120px"
show-overflow-tooltip>
</el-table-column>
<el-table-column
:label="$t('test_track.issue.issue')"
min-width="80px"
show-overflow-tooltip>
<template v-slot:default="scope">
<el-popover
@ -141,6 +159,8 @@
<el-table-column
prop="executorName"
:filters="executorFilters"
min-width="100px"
column-key="executor"
:label="$t('test_track.plan_view.executor')">
</el-table-column>
@ -149,6 +169,8 @@
prop="status"
:filters="statusFilters"
column-key="status"
min-width="100px"
:label="$t('test_track.plan_view.execute_result')">
<template v-slot:default="scope">
<span @click.stop="clickt = 'stop'">
@ -181,6 +203,8 @@
sortable
prop="updateTime"
:label="$t('commons.update_time')"
min-width="120px"
show-overflow-tooltip>
<template v-slot:default="scope">
<span>{{ scope.row.updateTime | timestampFormatDate }}</span>
@ -572,6 +596,14 @@ export default {
_sort(column, this.condition);
this.initTableData();
},
headerDragend(newWidth,oldWidth,column,event){
let finalWidth = newWidth;
if(column.minWidth>finalWidth){
finalWidth = column.minWidth;
}
column.width = finalWidth;
column.realWidth = finalWidth;
},
batchEdit(form) {
let param = {};
param[form.type] = form.value;

View File

@ -57,11 +57,9 @@
'redirectCharType',
'clickType'
],
// activated() {
// this.search();
// this.checkTipsType();
// },
// mounted() {
mounted() {
this.initData();
},
activated(){
this.initData();
this.openTestCaseEdit(this.$route.path);

View File

@ -1,7 +1,7 @@
<template>
<div class="failure-cases-list">
<div class="failure-cases-list-header">
接口测试用例
{{$t('test_track.plan.api_case')}}
</div>
<el-table
@ -28,7 +28,7 @@
<el-table-column
prop="createUser"
:label="'创建人'"
:label="$t('api_test.automation.creator')"
show-overflow-tooltip/>
<el-table-column prop="lastResult" :label="$t('api_test.automation.last_result')">
@ -60,9 +60,23 @@
name: "ApiFailureCasesList",
components: {MsTag, PriorityTableItem, TypeTableItem, MethodTableItem, StatusTableItem},
props: ['apiTestCases'],
watch: {
apiTestCases() {
this.parseTags();
}
},
methods: {
goFailureTestCase(row) {
this.$emit("openFailureTestCase", row);
},
parseTags() {
if (this.apiTestCases) {
this.apiTestCases.forEach(item => {
if (item.tags && item.tags.length > 0) {
item.tags = JSON.parse(item.tags);
}
});
}
}
}
}

View File

@ -1,7 +1,7 @@
<template>
<div class="failure-cases-list">
<div class="failure-cases-list-header">
场景测试用例
{{$t('test_track.plan.scenario_case')}}
</div>
<el-table
@ -64,9 +64,21 @@
name: "ScenarioFailureCasesList",
components: {MsTag, PriorityTableItem, TypeTableItem, MethodTableItem, StatusTableItem},
props: ['scenarioTestCases'],
watch: {
scenarioTestCases() {
this.parseTags();
}
},
methods: {
goFailureTestCase(row) {
this.$emit("openFailureTestCase", row);
},
parseTags() {
this.scenarioTestCases.forEach(item => {
if (item.tags && item.tags.length > 0) {
item.tags = JSON.parse(item.tags);
}
});
}
}
}

View File

@ -146,6 +146,9 @@
@refreshTable="search"/>
</el-card>
<batch-edit ref="batchEdit" @batchEdit="batchEdit"
:type-arr="typeArr" :value-arr="valueArr" :dialog-title="$t('test_track.case.batch_edit_case')"/>
</div>
</template>
@ -214,21 +217,21 @@ export default {
],
showMore: false,
buttons: [
{
name: this.$t('test_track.case.batch_edit_case'), handleClick: this.handleEditBatch
},
{
name: this.$t('test_track.case.batch_unlink'), handleClick: this.handleDeleteBatch
}
],
typeArr: [
{id: 'status', name: this.$t('test_track.plan_view.execute_result')},
{id: 'executor', name: this.$t('test_track.plan_view.executor')},
{id: 'status', name: this.$t('test_track.review_view.execute_result')},
],
valueArr: {
executor: [],
status: [
{name: this.$t('test_track.plan_view.pass'), id: 'Pass'},
{name: this.$t('test_track.plan_view.failure'), id: 'Failure'},
{name: this.$t('test_track.plan_view.blocking'), id: 'Blocking'},
{name: this.$t('test_track.plan_view.skip'), id: 'Skip'}
{name: this.$t('test_track.case.status_prepare'), id: 'Prepare'},
{name: this.$t('test_track.case.status_pass'), id: 'Pass'},
{name: this.$t('test_track.case.status_un_pass'), id: 'UnPass'},
]
},
}
@ -339,6 +342,23 @@ export default {
this.$success(this.$t('test_track.cancel_relevance_success'));
});
},
handleEditBatch() {
this.$refs.batchEdit.open(this.selectRows.size);
},
batchEdit(form) {
let reviewId = this.reviewId;
let param = {};
param[form.type] = form.value;
param.ids = Array.from(this.selectRows).map(row => row.caseId);
param.reviewId = reviewId;
this.$post('/test/review/case/batch/edit/status', param, () => {
this.selectRows.clear();
this.status = '';
this.$post('/test/case/review/edit/status/' + reviewId);
this.$success(this.$t('commons.save_success'));
this.$emit('refresh');
});
},
handleSelectAll(selection) {
if (selection.length > 0) {
this.tableData.forEach(item => {

@ -1 +1 @@
Subproject commit 53bd821bee65be3374f1ccc9a299cef9ac44b985
Subproject commit a3d469fd18f663d11e5c1c49f71ced6e3e4b292f

View File

@ -122,8 +122,8 @@ html,body {
/* 滚动条样式 */
::-webkit-scrollbar{
width: 5px;
height: 5px;
width: 10px;
height: 10px;
position: fixed;
}
::-webkit-scrollbar-thumb{

View File

@ -322,21 +322,23 @@ export function _getBodyUploadFiles(request, bodyUploadFiles, obj) {
body = request.body;
}
if (body) {
body.kvs.forEach(param => {
if (param.files) {
param.files.forEach(item => {
if (item.file) {
if (!item.id) {
let fileId = getUUID().substring(0, 12);
item.name = item.file.name;
item.id = fileId;
if (body.kvs) {
body.kvs.forEach(param => {
if (param.files) {
param.files.forEach(item => {
if (item.file) {
if (!item.id) {
let fileId = getUUID().substring(0, 12);
item.name = item.file.name;
item.id = fileId;
}
obj.bodyUploadIds.push(item.id);
bodyUploadFiles.push(item.file);
}
obj.bodyUploadIds.push(item.id);
bodyUploadFiles.push(item.file);
}
});
}
});
});
}
});
}
if (body.binary) {
body.binary.forEach(param => {
if (param.files) {

View File

@ -187,7 +187,11 @@ export default {
review: "all"
},
image: 'Image',
tag: 'Tag'
tag: 'Tag',
module: {
select_module: "Select module",
default_module: "Default module",
}
},
license: {
title: 'Authorization management',
@ -208,8 +212,8 @@ export default {
display: {
title: 'Theme',
logo: 'System LOGO',
loginLogo: 'Picture on the right side of the login page',
loginImage: 'Login page upper left corner LOGO',
loginLogo: 'Login page upper left corner LOGO',
loginImage: 'Picture on the right side of the login page',
loginTitle: 'Login page prompt information',
pageTitle: 'Page Title',
},
@ -528,6 +532,7 @@ export default {
api_case_status: "Ise case status",
api_case_passing_rate: "Use case pass rate",
create_tip: "Note: Detailed interface information can be filled out on the edit page",
api_import: "Api Import",
select_comp: {
no_data: "No Data",
add_data: "Add Data"
@ -586,6 +591,7 @@ export default {
create_info: 'Create',
update_info: 'Update',
batch_edit: "Batch edit",
batch_move:"Batch move",
path_valid_info: "The request path is invalid",
other_config: "Other Config",
message_template: "Message Template",
@ -1131,6 +1137,8 @@ export default {
plan_delete_confirm: "All use cases under this plan will be deleted,confirm delete test plan: ",
plan_delete_tip: "The test plan is under way, please confirm and delete it!",
plan_delete: "Delete test plan",
api_case: "Api case",
scenario_case: "Scenario case",
load_case: {
case: "Load Case",
execution_status: "Execution status",

View File

@ -188,7 +188,11 @@ export default {
review: "全部评审"
},
image: '镜像',
tag: '标签'
tag: '标签',
module: {
select_module: "选择模块",
default_module: "默认模块",
}
},
license: {
title: '授权管理',
@ -209,8 +213,8 @@ export default {
display: {
title: '显示设置',
logo: '系统 LOGO',
loginLogo: '登陆页面右侧图片',
loginImage: '登录页左上角 LOGO',
loginLogo: '登录页左上角 LOGO',
loginImage: '登陆页面右侧图片',
loginTitle: '登陆页面提示信息',
pageTitle: '页面 Title',
},
@ -528,6 +532,7 @@ export default {
api_case_status: "用例状态",
api_case_passing_rate: "用例通过率",
create_tip: "注: 详细的接口信息可以在编辑页面填写",
api_import: "接口导入",
select_comp: {
no_data: "无数据",
add_data: "去添加"
@ -587,6 +592,7 @@ export default {
create_info: '创建',
update_info: '更新',
batch_edit: "批量编辑",
batch_move:"批量移动",
path_valid_info: "请求路径无效",
other_config: "其他设置",
message_template: "报文模版",
@ -1135,6 +1141,8 @@ export default {
plan_delete_confirm: "将删除该测试计划下所有用例,确认删除测试计划: ",
plan_delete_tip: "该测试计划正在进行中,请确认再删除!",
plan_delete: "删除计划",
api_case: "接口测试用例",
scenario_case: "场景测试用例",
load_case: {
case: "性能用例",
execution_status: "执行状态",

View File

@ -188,7 +188,11 @@ export default {
review: "全部評審"
},
image: '鏡像',
tag: '標簽'
tag: '標簽',
module: {
select_module: "選擇模塊",
default_module: "默認模塊",
}
},
license: {
title: '授權管理',
@ -209,8 +213,8 @@ export default {
display: {
title: '顯示設置',
logo: '系統 LOGO',
loginLogo: '登陸頁面右側圖片',
loginImage: '登錄頁左上角 LOGO',
loginLogo: '登錄頁左上角 LOGO',
loginImage: '登陸頁面右側圖片',
loginTitle: '登陸頁面提示信息',
pageTitle: '頁面 Title',
},
@ -527,6 +531,7 @@ export default {
api_case_status: "用例狀態",
api_case_passing_rate: "用例通過率",
create_tip: "註: 詳細的接口信息可以在編輯頁面填寫",
api_import: "接口導入",
select_comp: {
no_data: "無數據",
add_data: "去添加"
@ -586,6 +591,7 @@ export default {
create_info: '創建',
update_info: '更新',
batch_edit: "批量編輯",
batch_move:"批量移動",
path_valid_info: "請求路徑無效",
other_config: "其他設置",
message_template: "報文模版",
@ -1133,6 +1139,8 @@ export default {
plan_delete_confirm: "將刪除該測試計劃下所有用例,確認刪除測試計劃: ",
plan_delete_tip: "該測試計劃正在進行中,請確認再刪除!",
plan_delete: "刪除計劃",
api_case: "接口測試用例",
scenario_case: "場景測試用例",
load_case: {
case: "性能用例",
execution_status: "執行狀態",

View File

@ -8,7 +8,7 @@
<img :src="'/display/file/loginLogo'" alt="">
</div>
<div class="welcome">
<span>{{ $t('commons.welcome') }}</span>
<span>{{ loginTitle }}</span>
</div>
</div>
@ -46,7 +46,7 @@
<div class="divider"/>
<el-col :span="12">
<img class="login-image" :src="'/display/file/loginImage'">
<img class="login-image" :src="'/display/file/loginImage'" alt="">
</el-col>
</el-row>
@ -85,7 +85,7 @@ export default {
msg: '',
ready: false,
openLdap: false,
loginTitle: this.$t("commons.login") + " MeterSphere",
loginTitle: this.$t("commons.welcome"),
authSources: [],
loginUrl: 'signin',
}
@ -200,6 +200,7 @@ export default {
.title img {
width: 293px;
max-height: 60px;
margin-top: 165px;
}