Compare commits

..

61 Commits

Author SHA1 Message Date
liangdaliang
59d0031f75 Merge remote-tracking branch 'origin/master' 2025-07-01 16:37:12 +08:00
liangdaliang
d960cb6553 修改前端新打开tab不能刷新的问题及适配性能测试http端口null的问题 2025-07-01 16:36:23 +08:00
pfl
af54b475a2 fix:测试计划报告最后更新时间显示问题修复 2025-07-01 10:06:33 +08:00
pfl
c12be119a3 add:测试计划关联测试报告编辑API 2025-06-27 18:25:56 +08:00
pfl
924f5a935f 测试计划关联测试报告界面调整 2025-06-26 16:19:57 +08:00
pfl
963e9a8162 fix:测试计划进度显示sql修改 2025-06-25 18:12:55 +08:00
pfl
5296b22534 add:测试计划执行用例报告删除api 2025-06-25 16:11:54 +08:00
pfl
48b20390d4 Merge remote-tracking branch 'origin/master' 2025-06-25 13:51:17 +08:00
pfl
ef741fcb64 fix:测试计划新建报告及报告详情显示 2025-06-25 13:50:21 +08:00
liangdaliang
c170f9e5ec 解决Jmeter异常日志不显示问题 2025-06-24 10:49:09 +08:00
f9774b7eef fix 修改测试路径修改 2025-06-23 17:44:30 +08:00
pfl
ef4d51876f Merge remote-tracking branch 'origin/master' 2025-06-23 17:36:05 +08:00
pfl
737aaf4482 fix:测试计划界面列表显示问题修复 2025-06-23 17:35:27 +08:00
c0f0b96487 fix 路由修改 2025-06-23 17:21:59 +08:00
liangdaliang
ca1e8182db 解决迭代次数为0时问题 2025-06-23 16:55:11 +08:00
pfl
f8b935378f 测试计划关联用例
测试报告界面
2025-06-23 15:51:34 +08:00
660871f873 fix 修改截图路径 2025-06-19 18:04:31 +08:00
cfdc292c1d fix 修改截图路径 2025-06-19 17:13:30 +08:00
liangdaliang
49d63847ee 性能测试增加执行人 2025-06-17 10:05:48 +08:00
liangdaliang
efa41a7217 数据库sql查询支持上下文变量 2025-06-16 17:04:18 +08:00
0f25043d8c fix 压力测试bug修改 2025-06-16 15:04:04 +08:00
c9e2aa7c57 fix 数据提取第二步提取的参数,在第五步里用不了 2025-06-13 16:28:57 +08:00
liangdaliang
e0910ea033 redis配置及jmeter持续时长默认值 2025-06-13 16:16:34 +08:00
ffed752170 fix 打开一个测试报告后,再打开另外一个测试报告,显示的还是之前打开的测试报告 2025-06-13 15:45:52 +08:00
614aa05b42 fix 场景步骤删除后,右边的页面没有清空 2025-06-13 15:09:59 +08:00
liangdaliang
7161354894 适配定时任务调度改造 2025-06-09 14:42:18 +08:00
liangdaliang
c6380b6bde 修改数据库连接 2025-06-09 10:58:10 +08:00
liangdaliang
f394dc4590 删除无效的引入包 2025-06-09 10:21:55 +08:00
pfl
5cc2cdfc34 测试计划概览前端及后端代码完善 2025-06-06 17:19:18 +08:00
eee51078b5 fix 截图动作放到后置操作结束后截图 2025-06-06 11:22:44 +08:00
74457d2c81 fix 解决任务进去场景列表里都变成失效了,打开保存后重新进去也还是失效bug 2025-06-06 09:33:39 +08:00
a790ffbcba feat 前面步骤中提取的参数,需要支持在后续步骤中引用 2025-06-03 14:14:41 +08:00
pfl
eb58fd40c7 测试报告倒序及分页修复 2025-06-03 11:12:08 +08:00
pfl
c6a73afb76 1、需求详情前后端代码完善
2、缺陷管理详情前后端代码完善
2025-05-30 17:58:46 +08:00
606c3c15ea fix ui场景列表前端执行结果显示问题修复 2025-05-30 17:54:55 +08:00
d54b4523bf fix 压力测试添加更严格的cron表达式验证 2025-05-29 11:44:54 +08:00
3fad8f3a5a fix 断言支持配置失败后是否继续执行和 定时任务配置完成后,有下次执行时间,保存重新进来后没有下次执行时间 2025-05-29 11:27:08 +08:00
liangdaliang
3209f1e35c 时间格式化 2025-05-29 10:33:23 +08:00
liangdaliang
75fcd51dea 解决异步执行线程卡住问题 2025-05-28 19:02:53 +08:00
a9d0f68f52 ui自动化报表截图查看 2025-05-28 18:20:06 +08:00
e5dc3bab05 ui自动化编辑回到列表不刷新列表 2025-05-28 18:02:10 +08:00
080a80d53d ui自动化和压力测试bug修改 2025-05-28 16:31:27 +08:00
liangdaliang
66498fd750 解决排序问题 2025-05-28 15:04:18 +08:00
8636842735 ui自动化执行问题处理 2025-05-28 13:55:16 +08:00
liangdaliang
66c627d33a 解决测试计划性能测试相关bug 2025-05-28 13:54:43 +08:00
liangdaliang
c71b9d8c63 解决测试计划性能测试相关bug 2025-05-28 13:24:19 +08:00
426ee6aae9 自动化测试定时任务 2025-05-28 10:45:35 +08:00
0960370562 ui测试前后端问题修复 2025-05-27 16:09:35 +08:00
a3c140db47 ui测试前后端问题修复 2025-05-27 15:14:40 +08:00
64b57fc3ec ui测试前后端问题修复 2025-05-27 14:32:09 +08:00
2965095148 ui测试前后端问题修复 2025-05-27 09:18:47 +08:00
e7248e613a fix:断言 2025-05-26 11:18:23 +08:00
441f7d041d 场景执行bug修改 2025-05-12 13:24:50 +08:00
85decf2a60 场景执行bug修改 2025-05-12 11:07:50 +08:00
c07742651d 场景执行bug修改 2025-05-12 10:44:24 +08:00
a3e13b3972 场景执行bug修改 2025-05-12 10:19:21 +08:00
78cb16fd19 场景执行bug修改 2025-05-12 10:11:05 +08:00
c99f639976 高级设置显示排序 2025-05-08 15:55:08 +08:00
56d4a24e81 fix:修复场景新增编辑,数据提取问题 2025-05-08 15:22:15 +08:00
09c9c4eaec 场景新增bug修改 2025-05-08 13:12:49 +08:00
898fc59b50 add:ui测试,编辑和新增api 2025-05-07 17:31:17 +08:00
97 changed files with 5083 additions and 667 deletions

View File

@@ -6,9 +6,9 @@ spring:
druid: druid:
# 主库数据源 # 主库数据源
master: master:
url: jdbc:mysql://47.103.142.5:3306/cmcf-test?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8 url: jdbc:mysql://120.27.225.67:9609/riskmanage?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=true&serverTimezone=GMT%2B8
username: test username: riskmanage
password: Test123@ password: riskmanage@Farben2023
# 从库数据源 # 从库数据源
slave: slave:
# 从数据源开关/默认关闭 # 从数据源开关/默认关闭

View File

@@ -71,13 +71,13 @@ spring:
# redis 配置 # redis 配置
redis: redis:
# 地址 # 地址
host: ah.qyyh.net host: 47.99.93.74
# 端口默认为6379 # 端口默认为6379
port: 15129 port: 6379
# 数据库索引 # 数据库索引
database: 1 database: 1
# 密码 # 密码
password: password: foobaredDkhsb
# 连接超时时间 # 连接超时时间
timeout: 10s timeout: 10s
lettuce: lettuce:

View File

@@ -283,7 +283,11 @@ public class JMeterGroupUtil {
threadGroup.setDelay(0); threadGroup.setDelay(0);
if (executeType == 1) { if (executeType == 1) {
threadGroup.setDuration(pressureSecond); threadGroup.setDuration(pressureSecond);
threadGroup.setProperty(new TestElementProperty(ThreadGroup.MAIN_CONTROLLER, createLoopController(-1)));
threadGroup.setProperty(new BooleanProperty(ThreadGroup.IS_SAME_USER_ON_NEXT_ITERATION, true));
threadGroup.setScheduler(true);
} else { } else {
threadGroup.setScheduler(false);
threadGroup.setProperty(new TestElementProperty(ThreadGroup.MAIN_CONTROLLER, createLoopController(loops))); threadGroup.setProperty(new TestElementProperty(ThreadGroup.MAIN_CONTROLLER, createLoopController(loops)));
} }
if (errorOperType == 1) { if (errorOperType == 1) {
@@ -297,7 +301,6 @@ public class JMeterGroupUtil {
} else if (errorOperType == 5) { } else if (errorOperType == 5) {
threadGroup.setProperty(new StringProperty(ThreadGroup.ON_SAMPLE_ERROR, "stoptestnow")); threadGroup.setProperty(new StringProperty(ThreadGroup.ON_SAMPLE_ERROR, "stoptestnow"));
} }
threadGroup.setScheduler(false);
threadGroup.setProperty(TestElement.TEST_CLASS, ThreadGroup.class.getName()); threadGroup.setProperty(TestElement.TEST_CLASS, ThreadGroup.class.getName());
threadGroup.setProperty(TestElement.GUI_CLASS, ThreadGroupGui.class.getName()); threadGroup.setProperty(TestElement.GUI_CLASS, ThreadGroupGui.class.getName());
threadGroup.setProperty(new BooleanProperty(TestElement.ENABLED, true)); threadGroup.setProperty(new BooleanProperty(TestElement.ENABLED, true));
@@ -315,6 +318,9 @@ public class JMeterGroupUtil {
loopController.setProperty(new StringProperty(TestElement.TEST_CLASS, LoopController.class.getName())); loopController.setProperty(new StringProperty(TestElement.TEST_CLASS, LoopController.class.getName()));
loopController.setProperty(new StringProperty(TestElement.NAME, "循环控制器")); loopController.setProperty(new StringProperty(TestElement.NAME, "循环控制器"));
loopController.setProperty(new BooleanProperty(TestElement.ENABLED, true)); loopController.setProperty(new BooleanProperty(TestElement.ENABLED, true));
if (loops == 0) {
loops = 1;
}
loopController.setProperty(new IntegerProperty(LoopController.LOOPS, loops)); loopController.setProperty(new IntegerProperty(LoopController.LOOPS, loops));
return loopController; return loopController;
} }

View File

@@ -5,6 +5,8 @@ import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/** /**
* @author liangdaliang * @author liangdaliang
@@ -66,6 +68,32 @@ public class MySQLExecutor {
return result; return result;
} }
public static String replaceSqlVariables(String sql, Map<String, String> params) {
if (sql == null || params == null) {
return sql;
}
// 正则匹配 ${variable_name}
Pattern pattern = Pattern.compile("\\$\\{([^}]*)\\}");
Matcher matcher = pattern.matcher(sql);
StringBuilder sb = new StringBuilder();
while (matcher.find()) {
String key = matcher.group(1).trim(); // 获取变量名
String replacement = params.get(key); // 从 map 中获取对应值
if (replacement != null) {
// 如果值存在,替换并加上单引号(适用于字符串)
// 如果你需要支持数字不加引号,请自行判断类型
replacement = "'" + replacement.replace("'", "''") + "'";
matcher.appendReplacement(sb, replacement);
}
}
matcher.appendTail(sb);
return sb.toString();
}
// public static void main(String[] args) { // public static void main(String[] args) {
// String url = "jdbc:mysql://47.103.142.5:3306/cmcf-test"; // String url = "jdbc:mysql://47.103.142.5:3306/cmcf-test";
// List<String> columnNameList = new ArrayList<>(); // List<String> columnNameList = new ArrayList<>();

View File

@@ -1,13 +1,17 @@
package com.test.common.utils; package com.test.common.utils;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.*; import org.openqa.selenium.*;
import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions; import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select; import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait; import org.openqa.selenium.support.ui.WebDriverWait;
import java.io.File;
import java.text.SimpleDateFormat;
import java.time.Duration; import java.time.Duration;
import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
@@ -368,10 +372,52 @@ public class SeleniumUtils {
return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES); return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
} }
// 截屏并保存为文件 /**
* 截屏并保存为文件
* @param filePath 保存路径的基础目录
* @return 保存后的文件完整路径
*/
public String takeScreenshotAsFile(String filePath) { public String takeScreenshotAsFile(String filePath) {
return ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE).getAbsolutePath(); try {
// 生成文件名: screenshot_年月日_时分秒.png
String timestamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
String fileName = "screenshot_" + timestamp + ".png";
// 确保目录存在
File directory = new File(filePath);
if (!directory.exists()) {
boolean created = directory.mkdirs();
if (!created) {
log.error("无法创建目录: {}", filePath);
return null;
} }
}
// 检查目录是否可写
if (!directory.canWrite()) {
log.error("目录不可写: {}", filePath);
return null;
}
// 获取截图临时文件
File tempFile = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
// 构建目标文件路径
String fullPath = filePath + File.separator + fileName;
File targetFile = new File(fullPath);
// 复制文件到目标路径
FileUtils.copyFile(tempFile, targetFile);
log.info("截图已保存到: {}", targetFile.getAbsolutePath());
return targetFile.getAbsolutePath();
} catch (Exception e) {
log.error("截图保存失败: {}", e.getMessage());
return null;
}
}
// 关闭浏览器 // 关闭浏览器
public void quit() { public void quit() {

View File

@@ -1,7 +1,12 @@
package com.test.test.controller; package com.test.test.controller;
import java.util.List; import com.test.common.annotation.Log;
import com.test.common.core.controller.BaseController;
import com.test.common.core.domain.AjaxResult;
import com.test.common.core.page.TableDataInfo;
import com.test.common.enums.BusinessType;
import com.test.common.utils.StringUtils;
import com.test.common.utils.poi.ExcelUtil;
import com.test.test.domain.PerformanceTest; import com.test.test.domain.PerformanceTest;
import com.test.test.domain.TestCase; import com.test.test.domain.TestCase;
import com.test.test.domain.qo.PerformanceTestQO; import com.test.test.domain.qo.PerformanceTestQO;
@@ -12,16 +17,12 @@ import com.test.test.service.ITestCaseService;
import com.test.test.task.DynamicTaskManager; import com.test.test.task.DynamicTaskManager;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import com.test.common.annotation.Log;
import com.test.common.core.controller.BaseController; import java.util.List;
import com.test.common.core.domain.AjaxResult; import java.util.concurrent.CompletableFuture;
import com.test.common.enums.BusinessType;
import com.test.common.utils.poi.ExcelUtil;
import com.test.common.core.page.TableDataInfo;
/** /**
* 性能测试Controller * 性能测试Controller
@@ -119,6 +120,7 @@ public class PerformanceTestController extends BaseController {
@PostMapping("/addAndExecute") @PostMapping("/addAndExecute")
public AjaxResult addAndExecute(@RequestBody PerformanceTestQO performanceTestQO) { public AjaxResult addAndExecute(@RequestBody PerformanceTestQO performanceTestQO) {
try { try {
String createBy = getLoginUser().getUsername();
Long l = performanceTestService.insertPerformanceTest(performanceTestQO); Long l = performanceTestService.insertPerformanceTest(performanceTestQO);
// 获取新增的任务完整信息 // 获取新增的任务完整信息
PerformanceTestVO newTaskVo = performanceTestService.selectPerformanceTestById(l); PerformanceTestVO newTaskVo = performanceTestService.selectPerformanceTestById(l);
@@ -133,8 +135,10 @@ public class PerformanceTestController extends BaseController {
} }
// 执行性能测试 // 执行性能测试
try { try {
Long l1 = performanceTestCaseReportService.executePerformanceTestAndReport(l, jmeterHomePath, 2); CompletableFuture.runAsync(() -> {
return success(l1); performanceTestCaseReportService.executePerformanceTestAndReport(l, jmeterHomePath, 2, createBy);
});
return toAjax(true);
} catch (Exception e) { } catch (Exception e) {
log.error("执行失败!", e); log.error("执行失败!", e);
return error("执行失败!"); return error("执行失败!");
@@ -157,7 +161,8 @@ public class PerformanceTestController extends BaseController {
Long l = performanceTestService.updatePerformanceTest(performanceTestQO); Long l = performanceTestService.updatePerformanceTest(performanceTestQO);
PerformanceTestVO updatedTask = performanceTestService.selectPerformanceTestById(performanceTestQO.getId()); PerformanceTestVO updatedTask = performanceTestService.selectPerformanceTestById(performanceTestQO.getId());
// 如果crontab或crontab_status被修改了则更新定时任务 // 如果crontab或crontab_status被修改了则更新定时任务
if (!updatedTask.getCrontab().equals(originalTask.getCrontab()) if ((!StringUtils.isEmpty(updatedTask.getCrontab()) &&
!StringUtils.isEmpty(originalTask.getCrontab()) && !updatedTask.getCrontab().equals(originalTask.getCrontab()))
|| updatedTask.getCrontabStatus() != originalTask.getCrontabStatus()) { || updatedTask.getCrontabStatus() != originalTask.getCrontabStatus()) {
PerformanceTest entity = new PerformanceTest(); PerformanceTest entity = new PerformanceTest();
entity.setId(updatedTask.getId()); entity.setId(updatedTask.getId());
@@ -178,6 +183,7 @@ public class PerformanceTestController extends BaseController {
@Log(title = "性能测试", businessType = BusinessType.UPDATE) @Log(title = "性能测试", businessType = BusinessType.UPDATE)
@PutMapping("/editAndExecute") @PutMapping("/editAndExecute")
public AjaxResult editAndExecute(@RequestBody PerformanceTestQO performanceTestQO) { public AjaxResult editAndExecute(@RequestBody PerformanceTestQO performanceTestQO) {
String createBy = getLoginUser().getUsername();
try { try {
PerformanceTestVO originalTask = performanceTestService.selectPerformanceTestById(performanceTestQO.getId()); PerformanceTestVO originalTask = performanceTestService.selectPerformanceTestById(performanceTestQO.getId());
Long l = performanceTestService.updatePerformanceTest(performanceTestQO); Long l = performanceTestService.updatePerformanceTest(performanceTestQO);
@@ -193,11 +199,13 @@ public class PerformanceTestController extends BaseController {
dynamicTaskManager.updateTask(entity); dynamicTaskManager.updateTask(entity);
} }
try { try {
Long l1 = performanceTestCaseReportService.executePerformanceTestAndReport(l, jmeterHomePath, 2); CompletableFuture.runAsync(() -> {
return success(l1); performanceTestCaseReportService.executePerformanceTestAndReport(l, jmeterHomePath, 2, createBy);
});
return toAjax(true);
} catch (Exception e) { } catch (Exception e) {
log.error("执行失败!", e); log.error("执行失败!", e);
return error("执行失败!"); return error("执行失败!"+e);
} }
} catch (Exception e) { } catch (Exception e) {
log.error("修改并执行失败!", e); log.error("修改并执行失败!", e);
@@ -222,7 +230,10 @@ public class PerformanceTestController extends BaseController {
// @PreAuthorize("@ss.hasPermi('system:test:remove')") // @PreAuthorize("@ss.hasPermi('system:test:remove')")
@GetMapping("/executeNow") @GetMapping("/executeNow")
public AjaxResult executeNow(@RequestParam Long id) { public AjaxResult executeNow(@RequestParam Long id) {
Long l1 = performanceTestCaseReportService.executePerformanceTestAndReport(id, jmeterHomePath, 2); String createBy = getLoginUser().getUsername();
return success(l1); CompletableFuture.runAsync(() -> {
performanceTestCaseReportService.executePerformanceTestAndReport(id, jmeterHomePath, 2, createBy);
});
return toAjax(true);
} }
} }

View File

@@ -6,6 +6,7 @@ import com.test.common.core.domain.AjaxResult;
import com.test.common.core.page.TableDataInfo; import com.test.common.core.page.TableDataInfo;
import com.test.common.enums.BusinessType; import com.test.common.enums.BusinessType;
import com.test.common.utils.DateUtils; import com.test.common.utils.DateUtils;
import com.test.common.utils.SecurityUtils;
import com.test.common.utils.uuid.IdUtils; import com.test.common.utils.uuid.IdUtils;
import com.test.test.domain.TestCase; import com.test.test.domain.TestCase;
import com.test.test.domain.TestPlanCase; import com.test.test.domain.TestPlanCase;
@@ -127,6 +128,7 @@ public class TestCaseController extends BaseController {
@PostMapping("/runTestPlanCase") @PostMapping("/runTestPlanCase")
public AjaxResult runTestPlanCase(@RequestBody TestPlanCase testPlanCase) { public AjaxResult runTestPlanCase(@RequestBody TestPlanCase testPlanCase) {
String caseSid = IdUtils.simpleUUID(); String caseSid = IdUtils.simpleUUID();
testPlanCase.setExecuteBy(SecurityUtils.getUsername());
// 异步执行任务 // 异步执行任务
CompletableFuture.runAsync(() -> { CompletableFuture.runAsync(() -> {
testCaseService.executeTestCaseByPlanCase(testPlanCase, jmeterHomePath, caseSid); testCaseService.executeTestCaseByPlanCase(testPlanCase, jmeterHomePath, caseSid);

View File

@@ -10,9 +10,12 @@ import com.test.test.domain.qo.IDQO;
import com.test.test.domain.qo.TestPlanAddQO; import com.test.test.domain.qo.TestPlanAddQO;
import com.test.test.domain.qo.TestPlanListQO; import com.test.test.domain.qo.TestPlanListQO;
import com.test.test.domain.vo.TestPlanListVO; import com.test.test.domain.vo.TestPlanListVO;
import com.test.test.domain.vo.TestPlanOverviewTrendDataVO;
import com.test.test.domain.vo.TestPlanOverviewVO;
import com.test.test.service.ITestPlanService; import com.test.test.service.ITestPlanService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import java.util.List; import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -79,4 +82,20 @@ public class TestPlanController extends BaseController {
public AjaxResult edit(@RequestBody TestPlan testPlan) { public AjaxResult edit(@RequestBody TestPlan testPlan) {
return toAjax(testPlanService.updateTestPlan(testPlan)); return toAjax(testPlanService.updateTestPlan(testPlan));
} }
/**
* 获取测试计划概览信息
*/
@PostMapping("/planOverview")
public TestPlanOverviewVO selectPlanOverview(@RequestBody IDQO qo) {
return testPlanService.selectPlanOverview(qo.getId());
}
/**
* 获取测试计划用例执行趋势数据
*/
@PostMapping("/planCaseTrendData")
public List<TestPlanOverviewTrendDataVO> getPlanCaseTrendData(@RequestBody IDQO qo) {
return testPlanService.getCaseTrendData(qo.getId());
}
} }

View File

@@ -6,7 +6,10 @@ import com.test.common.core.domain.AjaxResult;
import com.test.common.core.page.TableDataInfo; import com.test.common.core.page.TableDataInfo;
import com.test.common.enums.BusinessType; import com.test.common.enums.BusinessType;
import com.test.common.utils.poi.ExcelUtil; import com.test.common.utils.poi.ExcelUtil;
import com.test.test.domain.TestPlan;
import com.test.test.domain.TestPlanDefect; import com.test.test.domain.TestPlanDefect;
import com.test.test.domain.TestProject;
import com.test.test.domain.qo.IDQO;
import com.test.test.domain.qo.TestPlanDefectQO; import com.test.test.domain.qo.TestPlanDefectQO;
import com.test.test.domain.qo.TestPlanDefectRelQO; import com.test.test.domain.qo.TestPlanDefectRelQO;
import com.test.test.domain.vo.TestPlanDefectVo; import com.test.test.domain.vo.TestPlanDefectVo;
@@ -92,4 +95,26 @@ public class TestPlanDefectController extends BaseController
{ {
return toAjax(testPlanDefectService.deleteTestPlanDefectByIds(ids)); return toAjax(testPlanDefectService.deleteTestPlanDefectByIds(ids));
} }
/**
* 查询缺陷关联测试计划列表
*/
@PostMapping("/defectPlanList")
public TableDataInfo defectPlanList(@RequestBody IDQO qo)
{
startPage();
List<TestPlan> list = testPlanDefectService.selectDefectPlanList(qo.getId());
return getDataTable(list);
}
/**
* 查询缺陷关联需求列表
*/
@PostMapping("/defectProjectList")
public TableDataInfo defectProjectList(@RequestBody IDQO qo)
{
startPage();
List<TestProject> list = testPlanDefectService.selectDefectProjectList(qo.getId());
return getDataTable(list);
}
} }

View File

@@ -2,6 +2,8 @@ package com.test.test.controller;
import com.test.common.core.controller.BaseController; import com.test.common.core.controller.BaseController;
import com.test.common.core.page.TableDataInfo; import com.test.common.core.page.TableDataInfo;
import com.test.test.domain.TestCase;
import com.test.test.domain.TestDefect;
import com.test.test.domain.qo.IDQO; import com.test.test.domain.qo.IDQO;
import com.test.test.domain.vo.TestPlanProjectVo; import com.test.test.domain.vo.TestPlanProjectVo;
import com.test.test.service.ITestPlanProjectService; import com.test.test.service.ITestPlanProjectService;
@@ -36,4 +38,34 @@ public class TestPlanProjectController extends BaseController {
List<TestPlanProjectVo> list = testPlanProjectService.selectTestPlanProjectList(qo.getId()); List<TestPlanProjectVo> list = testPlanProjectService.selectTestPlanProjectList(qo.getId());
return getDataTable(list); return getDataTable(list);
} }
/**
* 查询需求关联测试计划列表
*/
@PostMapping("/relatePlanList")
public TableDataInfo relatePlanList(@RequestBody IDQO qo) {
startPage();
List<TestPlanProjectVo> list = testPlanProjectService.selectRelatePlanList(qo.getId());
return getDataTable(list);
}
/**
* 查询需求关联用例列表
*/
@PostMapping("/relateCaseList")
public TableDataInfo relateCaseList(@RequestBody IDQO qo) {
startPage();
List<TestCase> list = testPlanProjectService.selectRelateCaseList(qo.getId());
return getDataTable(list);
}
/**
* 查询需求关联缺陷列表
*/
@PostMapping("/relateDefectList")
public TableDataInfo relateDefectList(@RequestBody IDQO qo) {
startPage();
List<TestDefect> list = testPlanProjectService.selectRelateDefectList(qo.getId());
return getDataTable(list);
}
} }

View File

@@ -5,6 +5,7 @@ import com.test.common.core.controller.BaseController;
import com.test.common.core.domain.AjaxResult; import com.test.common.core.domain.AjaxResult;
import com.test.common.core.page.TableDataInfo; import com.test.common.core.page.TableDataInfo;
import com.test.common.enums.BusinessType; import com.test.common.enums.BusinessType;
import com.test.test.domain.TestReport;
import com.test.test.domain.qo.IDQO; import com.test.test.domain.qo.IDQO;
import com.test.test.domain.qo.TestReportAddQO; import com.test.test.domain.qo.TestReportAddQO;
import com.test.test.domain.vo.TestReportVo; import com.test.test.domain.vo.TestReportVo;
@@ -12,7 +13,7 @@ import com.test.test.service.ITestReportService;
import java.util.List; import java.util.List;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -37,7 +38,7 @@ public class TestReportController extends BaseController {
* @return * @return
*/ */
@PostMapping("/reportList") @PostMapping("/reportList")
public TableDataInfo list(@RequestBody IDQO qo) { public TableDataInfo list(@Validated IDQO qo) {
startPage(); startPage();
List<TestReportVo> list = testReportService.selectTestReportList(qo.getId()); List<TestReportVo> list = testReportService.selectTestReportList(qo.getId());
return getDataTable(list); return getDataTable(list);
@@ -53,4 +54,36 @@ public class TestReportController extends BaseController {
public AjaxResult addTestReport(@RequestBody TestReportAddQO testReportAddQO) { public AjaxResult addTestReport(@RequestBody TestReportAddQO testReportAddQO) {
return toAjax(testReportService.addTestReport(testReportAddQO)); return toAjax(testReportService.addTestReport(testReportAddQO));
} }
/**
* 查询测试计划关联测试报告详情
*/
@PostMapping("/caseExecuteDetail")
public AjaxResult caseExecuteDetail(@RequestBody IDQO id) {
return success(testReportService.selectCaseTestReportById(id.getId()));
}
/**
* 删除测试报告
* @param id
* @return
*/
@Log(title = "测试报告", businessType = BusinessType.DELETE)
@PostMapping("/delExecuteCaseReport")
public AjaxResult delExecuteCaseReport(@RequestBody IDQO id) {
TestReport testReport = testReportService.selectCaseTestReportById(id.getId());
testReport.setDelFlag("1");
return toAjax(testReportService.updateExecuteCaseReport(testReport));
}
/**
* 修改测试计划关联测试报告
* @param testReport
*/
@Log(title = "测试报告", businessType = BusinessType.UPDATE)
@PostMapping("/updateExecuteCaseReport")
public AjaxResult updateExecuteCaseReport(@RequestBody TestReport testReport) {
return toAjax(testReportService.updateExecuteCaseReport(testReport));
}
} }

View File

@@ -15,6 +15,7 @@ import com.test.test.domain.qo.IDQO;
import com.test.test.service.ITestTaskLogService; import com.test.test.service.ITestTaskLogService;
import com.test.test.service.ITestTaskResultService; import com.test.test.service.ITestTaskResultService;
import com.test.test.service.ITestTaskService; import com.test.test.service.ITestTaskService;
import com.test.test.task.TestTaskManager;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
@@ -42,6 +43,9 @@ public class TestTaskController extends BaseController {
@Resource @Resource
private ITestTaskLogService taskLogService; private ITestTaskLogService taskLogService;
@Resource
private TestTaskManager testTaskManager;
/** /**
* 查询定时任务列表 * 查询定时任务列表
*/ */
@@ -90,9 +94,24 @@ public class TestTaskController extends BaseController {
@Log(title = "定时任务", businessType = BusinessType.INSERT) @Log(title = "定时任务", businessType = BusinessType.INSERT)
@PostMapping("/add") @PostMapping("/add")
public AjaxResult add(@RequestBody TestTask testTask) { public AjaxResult add(@RequestBody TestTask testTask) {
try {
testTask.setCreateBy(getLoginUser().getUsername()); testTask.setCreateBy(getLoginUser().getUsername());
testTask.setCreateTime(DateUtils.getNowDate()); testTask.setCreateTime(DateUtils.getNowDate());
return toAjax(testTaskService.insertTestTask(testTask)); if (testTask.getStatus() == null) {
testTask.setStatus(2);
}
int result = testTaskService.insertTestTask(testTask);
// 如果任务状态是启用且没有被删除,则添加到定时任务管理器
if (testTask.getStatus() == 0 && "0".equals(testTask.getDelFlag())) {
testTaskManager.addNewTask(testTask);
}
return toAjax(result);
} catch (Exception e) {
logger.error("新增失败:", e);
return error("新增失败:" + e.getMessage());
}
} }
/** /**
@@ -101,7 +120,25 @@ public class TestTaskController extends BaseController {
@Log(title = "定时任务", businessType = BusinessType.UPDATE) @Log(title = "定时任务", businessType = BusinessType.UPDATE)
@PostMapping("/edit") @PostMapping("/edit")
public AjaxResult edit(@RequestBody TestTask testTask) { public AjaxResult edit(@RequestBody TestTask testTask) {
return toAjax(testTaskService.updateTestTask(testTask)); try {
testTask.setUpdateBy(getLoginUser().getUsername());
testTask.setUpdateTime(DateUtils.getNowDate());
// 获取原任务信息
TestTask originalTask = testTaskService.selectTestTaskById(testTask.getId()).getTask();
// 更新任务
int result = testTaskService.updateTestTask(testTask);
// 如果状态或crontab发生变化更新定时任务
if (testTask.getStatus() != originalTask.getStatus() ||
!testTask.getCrontab().equals(originalTask.getCrontab())) {
testTaskManager.updateTask(testTask);
}
return toAjax(result);
} catch (Exception e) {
return error("修改失败:" + e.getMessage());
}
} }
/** /**
@@ -110,7 +147,14 @@ public class TestTaskController extends BaseController {
@Log(title = "定时任务", businessType = BusinessType.DELETE) @Log(title = "定时任务", businessType = BusinessType.DELETE)
@PostMapping("/del") @PostMapping("/del")
public AjaxResult remove(@RequestBody IDQO qo) { public AjaxResult remove(@RequestBody IDQO qo) {
try {
// 先从定时任务管理器中移除
testTaskManager.removeTask(qo.getId());
// 再删除数据库记录
return toAjax(testTaskService.deleteTestTaskById(qo.getId())); return toAjax(testTaskService.deleteTestTaskById(qo.getId()));
} catch (Exception e) {
return error("删除失败:" + e.getMessage());
}
} }
/** /**
@@ -126,6 +170,4 @@ public class TestTaskController extends BaseController {
}); });
return toAjax(true); return toAjax(true);
} }
} }

View File

@@ -1,9 +1,11 @@
package com.test.test.controller; package com.test.test.controller;
import java.util.List; import java.util.List;
import java.util.Map;
import com.test.common.utils.SeleniumUtils; import com.test.common.utils.SeleniumUtils;
import com.test.test.domain.UiAutomation; import com.test.test.domain.UiAutomation;
import com.test.test.domain.UiReport;
import com.test.test.domain.qo.UiAutomationDataQO; import com.test.test.domain.qo.UiAutomationDataQO;
import com.test.test.domain.qo.UiAutomationQO; import com.test.test.domain.qo.UiAutomationQO;
import com.test.test.service.IUiAutomationService; import com.test.test.service.IUiAutomationService;
@@ -36,8 +38,6 @@ public class UiAutomationController extends BaseController
private IUiAutomationService uiAutomationService; private IUiAutomationService uiAutomationService;
@Autowired @Autowired
private IUiSceneStepsService uiSceneStepsService; private IUiSceneStepsService uiSceneStepsService;
@Value("${test.selenium.chrome-driver-path}")
private String chromeDriverPath;
/** /**
* 查询ui自动化列表 * 查询ui自动化列表
@@ -120,11 +120,6 @@ public class UiAutomationController extends BaseController
log.info("执行完成!"); log.info("执行完成!");
return success(uiSceneStepsService.executeStep(id,triggerMode)); return success(uiSceneStepsService.executeStep(id,triggerMode));
} catch (Exception e) { } catch (Exception e) {
System.setProperty("webdriver.chrome.driver", chromeDriverPath);
WebDriver driver = new ChromeDriver();
SeleniumUtils seleniumUtils = new SeleniumUtils(driver);
//关闭浏览器
seleniumUtils.quit();
log.error("执行错误!",e); log.error("执行错误!",e);
return error("执行错误!"+e.getMessage()); return error("执行错误!"+e.getMessage());
} }

View File

@@ -1,5 +1,9 @@
package com.test.test.controller; package com.test.test.controller;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.List; import java.util.List;
import com.test.test.domain.UiReport; import com.test.test.domain.UiReport;
@@ -7,14 +11,7 @@ import com.test.test.service.IUiReportService;
import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.test.common.annotation.Log; import com.test.common.annotation.Log;
import com.test.common.core.controller.BaseController; import com.test.common.core.controller.BaseController;
import com.test.common.core.domain.AjaxResult; import com.test.common.core.domain.AjaxResult;
@@ -70,4 +67,28 @@ public class UiReportController extends BaseController
{ {
return toAjax(uiReportService.deleteUiReportByIds(ids)); return toAjax(uiReportService.deleteUiReportByIds(ids));
} }
@GetMapping("/screenshot")
public void getScreenshot(@RequestParam String path, HttpServletResponse response) throws IOException {
File file = new File(path);
if (!file.exists()) {
try {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
} catch (IOException e) {
throw new RuntimeException(e);
}
return;
}
response.setContentType("image/png");
try (FileInputStream in = new FileInputStream(file);
OutputStream out = response.getOutputStream()) {
byte[] buffer = new byte[4096];
int length;
while ((length = in.read(buffer)) > 0) {
out.write(buffer, 0, length);
}
}
}
} }

View File

@@ -44,13 +44,13 @@ public class PerformanceTestCaseReport extends BaseEntity
private String tps; private String tps;
/** 开始时间 */ /** 开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date startTime; private Date startTime;
/** 结束时间 */ /** 结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date endTime; private Date endTime;
/** 耗时(毫秒) */ /** 耗时(毫秒) */

View File

@@ -29,6 +29,14 @@ public class TestCaseLog extends BaseEntity
@Excel(name = "用例id") @Excel(name = "用例id")
private Long caseId; private Long caseId;
/** 测试计划id */
@Excel(name = "测试计划id")
private Long planId;
/** 测试计划关联用例类型 */
@Excel(name = "测试计划关联用例类型")
private Integer type;
/** 操作类别 */ /** 操作类别 */
@Excel(name = "操作类别") @Excel(name = "操作类别")
private String operType; private String operType;

View File

@@ -40,8 +40,8 @@ public class TestPlanCase extends BaseEntity
private String executeResult; private String executeResult;
/** 执行时间 */ /** 执行时间 */
@JsonFormat(pattern = "yyyy-MM-dd") @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "执行时间", width = 30, dateFormat = "yyyy-MM-dd") @Excel(name = "执行时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date executeTime; private Date executeTime;
/** 执行人 */ /** 执行人 */

View File

@@ -52,5 +52,5 @@ public class AssertionQO {
private String isFailedAbort; private String isFailedAbort;
/**是否禁用 0否 1是*/ /**是否禁用 0否 1是*/
private String isDisabled; private Integer isDisabled;
} }

View File

@@ -41,7 +41,7 @@ public class DataExtractionQO {
private String elementAttribute; private String elementAttribute;
/**是否禁用 0否 1是 */ /**是否禁用 0否 1是 */
private String isDisabled; private Integer isDisabled;
} }

View File

@@ -3,6 +3,6 @@ package com.test.test.domain.qo;
import lombok.Data; import lombok.Data;
@Data @Data
public class IDQO { public class IDQO extends InheritQO{
private Long id; private Long id;
} }

View File

@@ -0,0 +1,14 @@
package com.test.test.domain.qo;
import java.io.Serializable;
import lombok.Data;
@Data
public class InheritQO implements Serializable {
private static final long serialVersionUID = -6405523005491017445L;
private Integer pageNum;
private Integer pageSize;
}

View File

@@ -50,10 +50,10 @@ public class UiSceneStepsQO extends BaseEntity
private String operateObject; private String operateObject;
/** 元素库名称 */ /** 元素库名称 */
private Integer operateGroupId; private Long operateGroupId;
/** 元素id */ /** 元素id */
private Integer operateElementId; private Long operateElementId;
/** 元素定位类型 */ /** 元素定位类型 */
private String operateLocType; private String operateLocType;

View File

@@ -0,0 +1,18 @@
package com.test.test.domain.vo;
import java.io.Serializable;
import lombok.Data;
@Data
public class TestCaseReportVO implements Serializable {
private static final long serialVersionUID = 6972995415807958849L;
private String name;
private String result;
private Integer caseNum;
private Integer passNum;
}

View File

@@ -0,0 +1,22 @@
package com.test.test.domain.vo;
import java.io.Serializable;
import lombok.Data;
@Data
public class TestPlanOverviewTrendDataVO implements Serializable {
private static final long serialVersionUID = -6444876575138174061L;
private String caseTrendDates;
private Integer notExecuted;
private Integer passed;
private Integer failed;
private Integer blocked;
private Integer skipped;
}

View File

@@ -0,0 +1,146 @@
package com.test.test.domain.vo;
import java.io.Serializable;
import java.util.List;
import lombok.Data;
@Data
public class TestPlanOverviewVO implements Serializable {
private static final long serialVersionUID = -2634301979620606338L;
/**
* 用例总数
*/
private Integer caseCount;
/**
* 已执行用例数
*/
private Integer executedCaseCount;
/**
* 用例通过数
*/
private Integer passedCaseCount;
/**
* 用例失败数
*/
private Integer failedCaseCount;
/**
* 用例阻塞数
*/
private Integer blockedCaseCount;
/**
* 用例跳过数
*/
private Integer skippedCaseCount;
/**
* 用例未执行数
*/
private Integer notExecutedCaseCount;
/**
* 缺陷数
*/
private Integer defectCount;
/**
* 待确认缺陷状态数
*/
private Integer unconfirmedDefectCount;
/**
* 修复中缺陷状态数
*/
private Integer fixingDefectCount;
/**
* 待验证缺陷状态数
*/
private Integer unverifiedDefectCount;
/**
* 无效缺陷状态数
*/
private Integer invalidDefectCount;
/**
* 挂起缺陷状态数
*/
private Integer suspendedDefectCount;
/**
* 无效等级缺陷数
*/
private Integer invalidLevelDefectCount;
/**
* 轻微等级缺陷数
*/
private Integer minorLevelDefectCount;
/**
* 一般等级缺陷数
*/
private Integer normalLevelDefectCount;
/**
* 严重等级缺陷数
*/
private Integer seriousLevelDefectCount;
/**
* 致命等级缺陷数
*/
private Integer fatalLevelDefectCount;
/**
* 功能逻辑类型缺陷数
*/
private Integer logicDefectCount;
/**
* UI交互类型缺陷数
*/
private Integer uiDefectCount;
/**
* 性能问题类型缺陷数
*/
private Integer performanceDefectCount;
/**
* 兼容性问题类型缺陷数
*/
private Integer compatibilityDefectCount;
/**
* 配置错误类型缺陷数
*/
private Integer configurationDefectCount;
/**
* 安全问题类型缺陷数
*/
private Integer securityDefectCount;
/**
* 安装部署类型缺陷数
*/
private Integer installationDefectCount;
/**
* 其他类型缺陷数
*/
private Integer otherDefectCount;
/**
* 用例执行时间
*/
private List<String> caseTrendDates;
}

View File

@@ -9,6 +9,9 @@ public class TestReportVo extends BaseEntity {
private static final long serialVersionUID = -4331077290310280474L; private static final long serialVersionUID = -4331077290310280474L;
/** 测试报告id */
private Long id;
/** /**
* 测试报告名称 * 测试报告名称
*/ */
@@ -17,6 +20,9 @@ public class TestReportVo extends BaseEntity {
/** 测试结果(0,未通过,1,通过) */ /** 测试结果(0,未通过,1,通过) */
private String result; private String result;
/** 测试报告jason格式存储*/
private String report;
/** 测试用例类型(0,冒烟测试,1,功能测试,2,回归测试,3,准生产测试,4,生产验证) */ /** 测试用例类型(0,冒烟测试,1,功能测试,2,回归测试,3,准生产测试,4,生产验证) */
private Long type; private Long type;

View File

@@ -44,10 +44,10 @@ public class UiSceneStepsVO
private String operateObject; private String operateObject;
/** 元素库名称 */ /** 元素库名称 */
private Integer operateGroupId; private Long operateGroupId;
/** 元素id */ /** 元素id */
private Integer operateElementId; private Long operateElementId;
/** 元素定位类型 */ /** 元素定位类型 */
private String operateLocType; private String operateLocType;

View File

@@ -2,6 +2,7 @@ package com.test.test.mapper;
import com.test.test.domain.TestCaseLog; import com.test.test.domain.TestCaseLog;
import com.test.test.domain.qo.TestReportAddQO;
import java.util.List; import java.util.List;
/** /**
@@ -28,6 +29,14 @@ public interface TestCaseLogMapper
*/ */
public List<TestCaseLog> selectTestCaseLogList(TestCaseLog testCaseLog); public List<TestCaseLog> selectTestCaseLogList(TestCaseLog testCaseLog);
/**
* 查询用例日志列表
*
* @param testCaseLog 用例日志
* @return 用例日志集合
*/
public String selectTestCaseLogCaseSid(TestReportAddQO testCaseLog);
/** /**
* 新增用例日志 * 新增用例日志
* *

View File

@@ -1,6 +1,7 @@
package com.test.test.mapper; package com.test.test.mapper;
import com.test.test.domain.TestCase; import com.test.test.domain.TestCase;
import com.test.test.domain.TestDefect;
import com.test.test.domain.qo.TestCaseListQO; import com.test.test.domain.qo.TestCaseListQO;
import java.util.List; import java.util.List;
@@ -49,4 +50,9 @@ public interface TestCaseMapper
* 批量删除用例 * 批量删除用例
*/ */
int deleteTestCaseByIds(Long[] ids); int deleteTestCaseByIds(Long[] ids);
/**
* 查询关联的用例列表
*/
List<TestCase> selectRelateCaseList(Long projectId);
} }

View File

@@ -60,4 +60,9 @@ public interface TestDefectMapper
* @return 结果 * @return 结果
*/ */
public int deleteTestDefectByIds(Long[] ids); public int deleteTestDefectByIds(Long[] ids);
/**
* 查询关联的缺陷列表
*/
List<TestDefect> selectRelateDefectList(Long projectId);
} }

View File

@@ -1,5 +1,6 @@
package com.test.test.mapper; package com.test.test.mapper;
import com.test.test.domain.TestPlan;
import com.test.test.domain.TestPlanDefect; import com.test.test.domain.TestPlanDefect;
import com.test.test.domain.qo.TestPlanDefectQO; import com.test.test.domain.qo.TestPlanDefectQO;
import com.test.test.domain.vo.TestPlanDefectVo; import com.test.test.domain.vo.TestPlanDefectVo;
@@ -67,4 +68,11 @@ public interface TestPlanDefectMapper
* @return * @return
*/ */
List<TestPlanDefect> selectRelList(TestPlanDefect testPlanDefect); List<TestPlanDefect> selectRelList(TestPlanDefect testPlanDefect);
/**
* 查询缺陷关联计划列表
* @param defectId
* @return
*/
List<TestPlan> selectDefectPlanList(Long defectId);
} }

View File

@@ -3,8 +3,9 @@ package com.test.test.mapper;
import com.test.test.domain.TestPlan; import com.test.test.domain.TestPlan;
import com.test.test.domain.qo.TestPlanListQO; import com.test.test.domain.qo.TestPlanListQO;
import com.test.test.domain.vo.TestPlanListVO; import com.test.test.domain.vo.TestPlanListVO;
import com.test.test.domain.vo.TestPlanOverviewTrendDataVO;
import com.test.test.domain.vo.TestPlanOverviewVO;
import java.util.List; import java.util.List;
public interface TestPlanMapper { public interface TestPlanMapper {
/** /**
@@ -41,4 +42,18 @@ public interface TestPlanMapper {
* @return * @return
*/ */
Long selectPlanId(String serialNumber); Long selectPlanId(String serialNumber);
/**
* 查询测试计划概览信息
* @param id
* @return
*/
TestPlanOverviewVO selectPlanOverview(Long id);
/**
* 查询测试计划用例执行趋势数据
* @param id
* @return
*/
List<TestPlanOverviewTrendDataVO> getCaseTrendData(Long id);
} }

View File

@@ -40,5 +40,12 @@ public interface TestProjectMapper {
* @return * @return
*/ */
int updateTestProject(TestProject testProject); int updateTestProject(TestProject testProject);
/**
* 查询缺陷关联的测试计划
* @param defectId
* @return
*/
List<TestProject> selectRelateProjectList(Long defectId);
} }

View File

@@ -19,4 +19,11 @@ public interface TestProjectPlanMapper {
* @return * @return
*/ */
List<TestPlanProjectVo> selectTestPlanProjectList(Long planId); List<TestPlanProjectVo> selectTestPlanProjectList(Long planId);
/**
* 查询需求关联计划列表
* @param projectId
* @return
*/
List<TestPlanProjectVo> selectRelatePlanList(Long projectId);
} }

View File

@@ -20,4 +20,18 @@ public interface TestReportMapper {
* @return * @return
*/ */
int addTestReport(TestReport testReport); int addTestReport(TestReport testReport);
/**
* 查询用例执行测试报告详情
* @param id
* @return
*/
TestReport selectCaseTestReportById(Long id);
/**
* 修改用例执行测试报告
* @param testReport
* @return
*/
int updateExecuteCaseReport(TestReport testReport);
} }

View File

@@ -17,9 +17,10 @@ public interface IPerformanceTestCaseReportService
* @param id * @param id
* @param jmeterHomePath * @param jmeterHomePath
* @param triggerType 触发方式1-定时任务2-手动 * @param triggerType 触发方式1-定时任务2-手动
* @param createBy 创建人
* @return 返回本次性能测试的批次id * @return 返回本次性能测试的批次id
*/ */
public Long executePerformanceTestAndReport(Long id, String jmeterHomePath, Integer triggerType); public Long executePerformanceTestAndReport(Long id, String jmeterHomePath, Integer triggerType, String createBy);
/** /**
* 查询性能测试用例报告 * 查询性能测试用例报告

View File

@@ -1,7 +1,10 @@
package com.test.test.service; package com.test.test.service;
import com.test.test.domain.TestPlan;
import com.test.test.domain.TestPlanDefect; import com.test.test.domain.TestPlanDefect;
import com.test.test.domain.TestProject;
import com.test.test.domain.qo.IDQO;
import com.test.test.domain.qo.TestPlanDefectQO; import com.test.test.domain.qo.TestPlanDefectQO;
import com.test.test.domain.qo.TestPlanDefectRelQO; import com.test.test.domain.qo.TestPlanDefectRelQO;
import com.test.test.domain.vo.TestPlanDefectVo; import com.test.test.domain.vo.TestPlanDefectVo;
@@ -62,4 +65,18 @@ public interface ITestPlanDefectService
* @return 结果 * @return 结果
*/ */
public int deleteTestPlanDefectById(Long id); public int deleteTestPlanDefectById(Long id);
/**
* 查询缺陷关联的测试计划列表
* @param defectId
* @return
*/
List<TestPlan> selectDefectPlanList(Long defectId);
/**
* 查询缺陷关联的需求列表
* @param defectId
* @return
*/
List<TestProject> selectDefectProjectList(Long defectId);
} }

View File

@@ -1,5 +1,7 @@
package com.test.test.service; package com.test.test.service;
import com.test.test.domain.TestCase;
import com.test.test.domain.TestDefect;
import com.test.test.domain.vo.TestPlanProjectVo; import com.test.test.domain.vo.TestPlanProjectVo;
import java.util.List; import java.util.List;
@@ -14,4 +16,25 @@ public interface ITestPlanProjectService {
* @return * @return
*/ */
List<TestPlanProjectVo> selectTestPlanProjectList(Long planId); List<TestPlanProjectVo> selectTestPlanProjectList(Long planId);
/**
* 查询需求关联测试计划列表
* @param projectId
* @return
*/
List<TestPlanProjectVo> selectRelatePlanList(Long projectId);
/**
* 查询需求关联用例列表
* @param projectId
* @return
*/
List<TestCase> selectRelateCaseList(Long projectId);
/**
* 查询需求关联缺陷列表
* @param projectId
* @return
*/
List<TestDefect> selectRelateDefectList(Long projectId);
} }

View File

@@ -4,6 +4,8 @@ import com.test.test.domain.TestPlan;
import com.test.test.domain.qo.TestPlanAddQO; import com.test.test.domain.qo.TestPlanAddQO;
import com.test.test.domain.qo.TestPlanListQO; import com.test.test.domain.qo.TestPlanListQO;
import com.test.test.domain.vo.TestPlanListVO; import com.test.test.domain.vo.TestPlanListVO;
import com.test.test.domain.vo.TestPlanOverviewTrendDataVO;
import com.test.test.domain.vo.TestPlanOverviewVO;
import java.util.List; import java.util.List;
/** /**
@@ -41,4 +43,18 @@ public interface ITestPlanService {
* @return * @return
*/ */
public int updateTestPlan(TestPlan testPlan); public int updateTestPlan(TestPlan testPlan);
/**
* 获取测试计划概览信息
* @param id
* @return
*/
TestPlanOverviewVO selectPlanOverview(Long id);
/**
* 获取测试计划用例趋势数据
* @param id
* @return
*/
List<TestPlanOverviewTrendDataVO> getCaseTrendData(Long id);
} }

View File

@@ -23,4 +23,18 @@ public interface ITestReportService {
* @return * @return
*/ */
public int addTestReport(TestReportAddQO testReportAddQO); public int addTestReport(TestReportAddQO testReportAddQO);
/**
* 更新执行用例测试报告
* @param testReport
* @return
*/
public int updateExecuteCaseReport(TestReport testReport);
/**
* 查询测试报告详情
* @param id
* @return
*/
public TestReport selectCaseTestReportById(Long id);
} }

View File

@@ -51,7 +51,7 @@ public class PerformanceTestCaseReportServiceImpl implements IPerformanceTestCas
private ITestCaseStepService testCaseStepService; private ITestCaseStepService testCaseStepService;
@Override @Override
public Long executePerformanceTestAndReport(Long id, String jmeterHomePath, Integer triggerType) { public Long executePerformanceTestAndReport(Long id, String jmeterHomePath, Integer triggerType, String createBy) {
Long sid = System.currentTimeMillis(); Long sid = System.currentTimeMillis();
PerformanceTest performanceTest = performanceTestMapper.selectPerformanceTestById(id); PerformanceTest performanceTest = performanceTestMapper.selectPerformanceTestById(id);
PerformanceTestCase performanceTestCase = new PerformanceTestCase(); PerformanceTestCase performanceTestCase = new PerformanceTestCase();
@@ -68,6 +68,9 @@ public class PerformanceTestCaseReportServiceImpl implements IPerformanceTestCas
jmeterGroupRequest.setRampUpSeconds(performanceTest.getRampUpSeconds()); jmeterGroupRequest.setRampUpSeconds(performanceTest.getRampUpSeconds());
Long seconds = performanceTest.getPressureHour() * 3600L + performanceTest.getPressureMinute() * 60L + performanceTest.getPressureSecond() * 1L; Long seconds = performanceTest.getPressureHour() * 3600L + performanceTest.getPressureMinute() * 60L + performanceTest.getPressureSecond() * 1L;
jmeterGroupRequest.setPressureSecond(seconds); jmeterGroupRequest.setPressureSecond(seconds);
if (seconds == 0L) {
jmeterGroupRequest.setPressureSecond(1L);
}
jmeterGroupRequest.setLoopCount(performanceTest.getLoopCount()); jmeterGroupRequest.setLoopCount(performanceTest.getLoopCount());
jmeterGroupRequest.setRpsStatus(performanceTest.getRpsStatus()); jmeterGroupRequest.setRpsStatus(performanceTest.getRpsStatus());
jmeterGroupRequest.setRpsLimit(performanceTest.getRpsLimit()); jmeterGroupRequest.setRpsLimit(performanceTest.getRpsLimit());
@@ -86,6 +89,7 @@ public class PerformanceTestCaseReportServiceImpl implements IPerformanceTestCas
if (!CollectionUtils.isEmpty(jmeterResultList)) { if (!CollectionUtils.isEmpty(jmeterResultList)) {
LabelStatsEntity lastElement = jmeterResultList.get(jmeterResultList.size() - 1); LabelStatsEntity lastElement = jmeterResultList.get(jmeterResultList.size() - 1);
PerformanceTestCaseReport performanceTestCaseReport = new PerformanceTestCaseReport(); PerformanceTestCaseReport performanceTestCaseReport = new PerformanceTestCaseReport();
performanceTestCaseReport.setCreateBy(createBy);
performanceTestCaseReport.setPerformanceId(id); performanceTestCaseReport.setPerformanceId(id);
performanceTestCaseReport.setTestCaseId(relateTestCase.getTestCaseId()); performanceTestCaseReport.setTestCaseId(relateTestCase.getTestCaseId());
performanceTestCaseReport.setSid(sid); performanceTestCaseReport.setSid(sid);
@@ -163,7 +167,7 @@ public class PerformanceTestCaseReportServiceImpl implements IPerformanceTestCas
JmeterRequest jmeterRequest = new JmeterRequest(); JmeterRequest jmeterRequest = new JmeterRequest();
jmeterRequest.setTestCaseName(testCaseStep.getName()); jmeterRequest.setTestCaseName(testCaseStep.getName());
jmeterRequest.setUrl(url); jmeterRequest.setUrl(url);
jmeterRequest.setPort(testCaseStep.getApiPort()); jmeterRequest.setPort(testCaseStep.getApiPort() == null ? 80 : testCaseStep.getApiPort());
jmeterRequest.setMethod(testCaseStep.getRequestMethod()); jmeterRequest.setMethod(testCaseStep.getRequestMethod());
jmeterRequest.setRequestBody(testCaseStep.getRequestBody()); jmeterRequest.setRequestBody(testCaseStep.getRequestBody());
jmeterRequest.setRequestParams(testCaseStep.getRequestParams()); jmeterRequest.setRequestParams(testCaseStep.getRequestParams());

View File

@@ -229,13 +229,15 @@ public class TestCaseServiceImpl implements ITestCaseService
for (TestPlanCase input : testPlanCaseList) { for (TestPlanCase input : testPlanCaseList) {
boolean isSuccess = true; boolean isSuccess = true;
input.setExecuteTime(DateUtils.getNowDate()); input.setExecuteTime(DateUtils.getNowDate());
input.setExecuteBy(SecurityUtils.getUsername()); input.setExecuteBy(testPlanCase.getExecuteBy());
TestCase testCase = this.selectTestCaseById(input.getCaseId()); TestCase testCase = this.selectTestCaseById(input.getCaseId());
Long id = input.getCaseId(); Long id = input.getCaseId();
Map<String, String> contextResultMap = new HashMap<>(); Map<String, String> contextResultMap = new HashMap<>();
testCase.setContextResultMap(contextResultMap); testCase.setContextResultMap(contextResultMap);
TestCaseLog testCaseLog = new TestCaseLog(); TestCaseLog testCaseLog = new TestCaseLog();
testCaseLog.setCaseId(id); testCaseLog.setCaseId(id);
testCaseLog.setPlanId(testPlanCase.getPlanId());
testCaseLog.setType(testPlanCase.getType());
testCaseLog.setOperTime(DateUtils.getNowDate()); testCaseLog.setOperTime(DateUtils.getNowDate());
testCaseLog.setOperType("执行"); testCaseLog.setOperType("执行");
testCaseLog.setCaseSid(caseSid); testCaseLog.setCaseSid(caseSid);
@@ -290,13 +292,14 @@ public class TestCaseServiceImpl implements ITestCaseService
} }
if (isSuccess) { if (isSuccess) {
testCaseLog.setOperDetail("成功"); testCaseLog.setOperDetail("成功");
input.setExecuteResult("成功"); input.setExecuteResult("1");
} else { } else {
testCaseLog.setOperDetail("失败"); testCaseLog.setOperDetail("失败");
input.setExecuteResult("失败"); input.setExecuteResult("2");
} }
testCaseLogMapper.insertTestCaseLog(testCaseLog); testCaseLogMapper.insertTestCaseLog(testCaseLog);
testPlanCaseMapper.updateTestPlanCase(input); testPlanCaseMapper.updateTestPlanCase(input);
System.out.println("111111111111111111111111");
} }
testPlan.setUpdateTime(DateUtils.getNowDate()); testPlan.setUpdateTime(DateUtils.getNowDate());
testPlan.setStatus("2"); testPlan.setStatus("2");
@@ -339,7 +342,8 @@ public class TestCaseServiceImpl implements ITestCaseService
useTime = Long.valueOf(resultMap.get("costMiliseconds")); useTime = Long.valueOf(resultMap.get("costMiliseconds"));
} }
testCaseResult.setUseTime(useTime); testCaseResult.setUseTime(useTime);
if ("fail".equals(assignmentResultMap.get("assertionResult"))) { String responseCode = resultMap.get("responseCode");
if ("fail".equals(assignmentResultMap.get("assertionResult")) || responseCode == null || resultMap.get("responseCode").startsWith("Non HTTP response code")) {
testCaseResult.setStatus("失败"); testCaseResult.setStatus("失败");
testCaseResultMapper.insertTestCaseResult(testCaseResult); testCaseResultMapper.insertTestCaseResult(testCaseResult);
return false; return false;
@@ -393,7 +397,10 @@ public class TestCaseServiceImpl implements ITestCaseService
// sql查询特有字段结果存储 // sql查询特有字段结果存储
Map<String, String> sqlSpecialResultMap = new HashMap<>(); Map<String, String> sqlSpecialResultMap = new HashMap<>();
// 获取所有sql查询集合对象 // 获取所有sql查询集合对象
List<Map<String, Object>> resultMapList = MySQLExecutor.executeQuery(testCaseStep.getSqlCommand(), url, testDatasource.getUsername(), testDatasource.getPassword(), columnNameList, sqlSpecialResultMap); String sqlCommand = testCaseStep.getSqlCommand();
sqlCommand = MySQLExecutor.replaceSqlVariables(sqlCommand, testCase.getContextResultMap());
log.info("{}###Replace value sqlCommand:{}", testCase.getContextResultMap().keySet(), sqlCommand);
List<Map<String, Object>> resultMapList = MySQLExecutor.executeQuery(sqlCommand, url, testDatasource.getUsername(), testDatasource.getPassword(), columnNameList, sqlSpecialResultMap);
if (!CollectionUtils.isEmpty(resultMapList)) { if (!CollectionUtils.isEmpty(resultMapList)) {
SqlResult sqlResult = new SqlResult(); SqlResult sqlResult = new SqlResult();
sqlResult.setColumnNameList(columnNameList); sqlResult.setColumnNameList(columnNameList);
@@ -644,7 +651,10 @@ public class TestCaseServiceImpl implements ITestCaseService
*/ */
private String dealDataSourceTestCaseStepAssignment(List<Map<String, Object>> resultMapList, Map<String, String> assignmentResultMap, Map<String, String> sqlSpecialResultMap, String assignment) { private String dealDataSourceTestCaseStepAssignment(List<Map<String, Object>> resultMapList, Map<String, String> assignmentResultMap, Map<String, String> sqlSpecialResultMap, String assignment) {
if (!StringUtils.isEmpty(assignment) && !"[]".equals(assignment)) { if (!StringUtils.isEmpty(assignment) && !"[]".equals(assignment)) {
Gson gson = new Gson(); Gson gson = new GsonBuilder()
.registerTypeAdapter(Byte.class, new TinyIntTypeAdapter())
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeConverter())
.create();
String responseBody = gson.toJson(resultMapList); String responseBody = gson.toJson(resultMapList);
Type ruleListType = new TypeToken<List<ExtractionRule>>() {}.getType(); Type ruleListType = new TypeToken<List<ExtractionRule>>() {}.getType();
// 将 JSON 字符串解析为 List<ExtractionRule> // 将 JSON 字符串解析为 List<ExtractionRule>

View File

@@ -15,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -216,14 +217,23 @@ public class TestCaseStepServiceImpl implements ITestCaseStepService {
JmeterRequest jmeterRequest = new JmeterRequest(); JmeterRequest jmeterRequest = new JmeterRequest();
jmeterRequest.setId(id); jmeterRequest.setId(id);
jmeterRequest.setUrl(url); jmeterRequest.setUrl(url);
jmeterRequest.setPort(testCaseStep.getApiPort()); jmeterRequest.setPort(testCaseStep.getApiPort() == null ? 80 : testCaseStep.getApiPort());
jmeterRequest.setMethod(testCaseStep.getRequestMethod()); jmeterRequest.setMethod(testCaseStep.getRequestMethod());
jmeterRequest.setRequestBody(testCaseStep.getRequestBody()); jmeterRequest.setRequestBody(testCaseStep.getRequestBody());
jmeterRequest.setRequestParams(testCaseStep.getRequestParams()); jmeterRequest.setRequestParams(testCaseStep.getRequestParams());
jmeterRequest.setRequestHeader(testCaseStep.getRequestHeader()); jmeterRequest.setRequestHeader(testCaseStep.getRequestHeader());
jmeterRequest.setJmeterHomePath(jmeterHomePath); jmeterRequest.setJmeterHomePath(jmeterHomePath);
log.info("getRequestHeader:{}", jmeterRequest.getRequestHeader()); log.info("getRequestHeader:{}", jmeterRequest.getRequestHeader());
Map<String, String> resultMap = JMeterUtil.getJmeterResult(jmeterRequest); Map<String, String> resultMap = new HashMap<>();
try {
resultMap = JMeterUtil.getJmeterResult(jmeterRequest);
} catch (Exception e) {
log.error("JMeterUtil异常", e);
resultMap.put("requestHeader", jmeterRequest.getRequestHeader());
resultMap.put("requestBody", jmeterRequest.getRequestBody());
resultMap.put("responseHeader", "");
resultMap.put("responseBody", "");
}
return resultMap; return resultMap;
} }

View File

@@ -1,11 +1,15 @@
package com.test.test.service.impl; package com.test.test.service.impl;
import com.test.common.utils.DateUtils; import com.test.common.utils.DateUtils;
import com.test.test.domain.TestPlan;
import com.test.test.domain.TestPlanDefect; import com.test.test.domain.TestPlanDefect;
import com.test.test.domain.TestProject;
import com.test.test.domain.qo.IDQO;
import com.test.test.domain.qo.TestPlanDefectQO; import com.test.test.domain.qo.TestPlanDefectQO;
import com.test.test.domain.qo.TestPlanDefectRelQO; import com.test.test.domain.qo.TestPlanDefectRelQO;
import com.test.test.domain.vo.TestPlanDefectVo; import com.test.test.domain.vo.TestPlanDefectVo;
import com.test.test.mapper.TestPlanDefectMapper; import com.test.test.mapper.TestPlanDefectMapper;
import com.test.test.mapper.TestProjectMapper;
import com.test.test.service.ITestPlanDefectService; import com.test.test.service.ITestPlanDefectService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -25,6 +29,9 @@ public class TestPlanDefectServiceImpl implements ITestPlanDefectService
@Resource @Resource
private TestPlanDefectMapper testPlanDefectMapper; private TestPlanDefectMapper testPlanDefectMapper;
@Resource
private TestProjectMapper testProjectMapper;
/** /**
* 查询测试计划测试缺陷关联 * 查询测试计划测试缺陷关联
* *
@@ -118,4 +125,24 @@ public class TestPlanDefectServiceImpl implements ITestPlanDefectService
{ {
return testPlanDefectMapper.deleteTestPlanDefectById(id); return testPlanDefectMapper.deleteTestPlanDefectById(id);
} }
/**
* 查询缺陷关联计划列表
* @param defectId
* @return
*/
@Override
public List<TestPlan> selectDefectPlanList(Long defectId) {
return testPlanDefectMapper.selectDefectPlanList(defectId);
}
/**
* 查询缺陷关联需求列表
* @param defectId
* @return
*/
@Override
public List<TestProject> selectDefectProjectList(Long defectId) {
return testProjectMapper.selectRelateProjectList(defectId);
}
} }

View File

@@ -1,6 +1,10 @@
package com.test.test.service.impl; package com.test.test.service.impl;
import com.test.test.domain.TestCase;
import com.test.test.domain.TestDefect;
import com.test.test.domain.vo.TestPlanProjectVo; import com.test.test.domain.vo.TestPlanProjectVo;
import com.test.test.mapper.TestCaseMapper;
import com.test.test.mapper.TestDefectMapper;
import com.test.test.mapper.TestProjectPlanMapper; import com.test.test.mapper.TestProjectPlanMapper;
import com.test.test.service.ITestPlanProjectService; import com.test.test.service.ITestPlanProjectService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
@@ -18,6 +22,12 @@ public class TestPlanProjectServiceImpl implements ITestPlanProjectService {
@Resource @Resource
private TestProjectPlanMapper testProjectPlanMapper; private TestProjectPlanMapper testProjectPlanMapper;
@Resource
private TestCaseMapper testCaseMapper;
@Resource
private TestDefectMapper testDefectMapper;
/** /**
* 查询测试计划需求关联列表 * 查询测试计划需求关联列表
* @param planId * @param planId
@@ -27,4 +37,34 @@ public class TestPlanProjectServiceImpl implements ITestPlanProjectService {
public List<TestPlanProjectVo> selectTestPlanProjectList(Long planId) { public List<TestPlanProjectVo> selectTestPlanProjectList(Long planId) {
return testProjectPlanMapper.selectTestPlanProjectList(planId); return testProjectPlanMapper.selectTestPlanProjectList(planId);
} }
/**
* 查询需求关联计划列表
* @param projectId
* @return
*/
@Override
public List<TestPlanProjectVo> selectRelatePlanList(Long projectId) {
return testProjectPlanMapper.selectRelatePlanList(projectId);
}
/**
* 查询需求关联用例列表
* @param projectId
* @return
*/
@Override
public List<TestCase> selectRelateCaseList(Long projectId) {
return testCaseMapper.selectRelateCaseList(projectId);
}
/**
* 查询需求关联缺陷列表
* @param projectId
* @return
*/
@Override
public List<TestDefect> selectRelateDefectList(Long projectId) {
return testDefectMapper.selectRelateDefectList(projectId);
}
} }

View File

@@ -6,10 +6,15 @@ import com.test.test.domain.TestPlan;
import com.test.test.domain.qo.TestPlanAddQO; import com.test.test.domain.qo.TestPlanAddQO;
import com.test.test.domain.qo.TestPlanListQO; import com.test.test.domain.qo.TestPlanListQO;
import com.test.test.domain.vo.TestPlanListVO; import com.test.test.domain.vo.TestPlanListVO;
import com.test.test.domain.vo.TestPlanOverviewTrendDataVO;
import com.test.test.domain.vo.TestPlanOverviewVO;
import com.test.test.mapper.TestPlanMapper; import com.test.test.mapper.TestPlanMapper;
import com.test.test.mapper.TestProjectPlanMapper; import com.test.test.mapper.TestProjectPlanMapper;
import com.test.test.service.ITestPlanService; import com.test.test.service.ITestPlanService;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -82,4 +87,39 @@ public class TestPlanServiceImpl implements ITestPlanService {
testPlan.setUpdateTime(DateUtils.getNowDate()); testPlan.setUpdateTime(DateUtils.getNowDate());
return testPlanMapper.updateTestPlan(testPlan); return testPlanMapper.updateTestPlan(testPlan);
} }
/**
* 获取测试计划概览信息
* @param id
* @return
*/
@Override
public TestPlanOverviewVO selectPlanOverview(Long id) {
TestPlanOverviewVO overviewVO = testPlanMapper.selectPlanOverview(id);
overviewVO.setCaseTrendDates(getCaseTrendDates());
return overviewVO;
}
/**
* 获取测试计划用例趋势数据
* @param id
* @return
*/
@Override
public List<TestPlanOverviewTrendDataVO> getCaseTrendData(Long id) {
return testPlanMapper.getCaseTrendData(id);
}
private List<String> getCaseTrendDates() {
List<String> dates = new ArrayList<>();
LocalDate today = LocalDate.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
for (int i = 6; i >= 0; i--) {
LocalDate date = today.minusDays(i);
dates.add(date.format(formatter));
}
return dates;
}
} }

View File

@@ -1,11 +1,17 @@
package com.test.test.service.impl; package com.test.test.service.impl;
import com.alibaba.fastjson2.JSONObject;
import com.test.common.utils.DateUtils; import com.test.common.utils.DateUtils;
import com.test.common.utils.SecurityUtils; import com.test.common.utils.SecurityUtils;
import com.test.common.utils.StringUtils;
import com.test.common.utils.bean.BeanUtils; import com.test.common.utils.bean.BeanUtils;
import com.test.test.domain.TestCaseResult;
import com.test.test.domain.TestReport; import com.test.test.domain.TestReport;
import com.test.test.domain.qo.TestReportAddQO; import com.test.test.domain.qo.TestReportAddQO;
import com.test.test.domain.vo.TestCaseReportVO;
import com.test.test.domain.vo.TestReportVo; import com.test.test.domain.vo.TestReportVo;
import com.test.test.mapper.TestCaseLogMapper;
import com.test.test.mapper.TestCaseResultMapper;
import com.test.test.mapper.TestPlanReportMapper; import com.test.test.mapper.TestPlanReportMapper;
import com.test.test.mapper.TestReportMapper; import com.test.test.mapper.TestReportMapper;
import com.test.test.service.ITestReportService; import com.test.test.service.ITestReportService;
@@ -29,6 +35,12 @@ public class TestReportServiceImpl implements ITestReportService {
@Resource @Resource
private TestPlanReportMapper testPlanReportMapper; private TestPlanReportMapper testPlanReportMapper;
@Resource
private TestCaseLogMapper testCaseLogMapper;
@Resource
private TestCaseResultMapper testCaseResultMapper;
/** /**
* 查询测试报告列表 * 查询测试报告列表
* @param planId * @param planId
@@ -46,11 +58,33 @@ public class TestReportServiceImpl implements ITestReportService {
*/ */
@Override @Override
public int addTestReport(TestReportAddQO testReportAddQO) { public int addTestReport(TestReportAddQO testReportAddQO) {
String caseSid = testCaseLogMapper.selectTestCaseLogCaseSid(testReportAddQO);
if (StringUtils.isEmpty(caseSid)) {
throw new RuntimeException("未执行用例,无法生成报告");
}
TestCaseResult testCaseResult = new TestCaseResult();
testCaseResult.setCaseSid(caseSid);
List<TestCaseResult> testCaseResults = testCaseResultMapper.selectTestCaseResultList(testCaseResult);
int count = testCaseResults.size();
int passNum = (int)testCaseResults.stream().filter(result -> "成功".equals(result.getStatus())).count();
if (count == passNum) {
testReportAddQO.setResult("1");
}
TestCaseReportVO testCaseReportVO = new TestCaseReportVO();
testCaseReportVO.setName(testReportAddQO.getName());
testCaseReportVO.setResult("0");
testCaseReportVO.setCaseNum(count);
testCaseReportVO.setPassNum(passNum);
String reportJson = JSONObject.toJSONString(testCaseReportVO);
TestReport testReport = new TestReport(); TestReport testReport = new TestReport();
if (count == passNum) {
testReport.setResult("1");
}
BeanUtils.copyProperties(testReportAddQO,testReport); BeanUtils.copyProperties(testReportAddQO,testReport);
testReport.setSerialNumber(generateSerialNumber()); testReport.setSerialNumber(generateSerialNumber());
testReport.setResult("0"); testReport.setResult("0");
testReport.setStatus("0"); testReport.setStatus("0");
testReport.setReport(reportJson);
testReport.setCreateTime(DateUtils.getNowDate()); testReport.setCreateTime(DateUtils.getNowDate());
testReport.setDelFlag("0"); testReport.setDelFlag("0");
testReport.setUpdateBy(SecurityUtils.getUsername()); testReport.setUpdateBy(SecurityUtils.getUsername());
@@ -61,6 +95,27 @@ public class TestReportServiceImpl implements ITestReportService {
return testPlanReportMapper.insertTestPlanReport(testReportAddQO); return testPlanReportMapper.insertTestPlanReport(testReportAddQO);
} }
/**
* 更新执行用例报告
* @param testReport
* @return
*/
@Override
public int updateExecuteCaseReport(TestReport testReport) {
testReport.setUpdateTime(DateUtils.getNowDate());
return testReportMapper.updateExecuteCaseReport(testReport);
}
/**
* 根据id查询报告
* @param id
* @return
*/
@Override
public TestReport selectCaseTestReportById(Long id) {
return testReportMapper.selectCaseTestReportById(id);
}
/** /**
* 生成随机序列号 * 生成随机序列号
* @return * @return

View File

@@ -70,7 +70,16 @@ public class TestTaskServiceImpl implements ITestTaskService {
*/ */
@Override @Override
public int insertTestTask(TestTask testTask) { public int insertTestTask(TestTask testTask) {
return testTaskMapper.insertTestTask(testTask); String operUser = testTask.getCreateBy();
int i = testTaskMapper.insertTestTask(testTask);
TestTaskLog testTaskLog = new TestTaskLog();
testTaskLog.setTaskId(testTask.getId());
testTaskLog.setOperType("新增");
testTaskLog.setOperDetail("操作人:" + operUser + "新增了【" + testTask.getName() + "】定时任务测试用例");
testTaskLog.setOperUser(operUser);
testTaskLog.setOperTime(DateUtils.getNowDate());
testTaskLogMapper.insertTestTaskLog(testTaskLog);
return i;
} }
/** /**
@@ -78,8 +87,17 @@ public class TestTaskServiceImpl implements ITestTaskService {
*/ */
@Override @Override
public int updateTestTask(TestTask testTask) { public int updateTestTask(TestTask testTask) {
String operUser = testTask.getCreateBy();
testTask.setUpdateTime(DateUtils.getNowDate()); testTask.setUpdateTime(DateUtils.getNowDate());
return testTaskMapper.updateTestTask(testTask); int i = testTaskMapper.updateTestTask(testTask);
TestTaskLog testTaskLog = new TestTaskLog();
testTaskLog.setTaskId(testTask.getId());
testTaskLog.setOperType("修改");
testTaskLog.setOperDetail("操作人:" + operUser + "修改了【" + testTask.getName() + "】定时任务测试用例");
testTaskLog.setOperUser(operUser);
testTaskLog.setOperTime(DateUtils.getNowDate());
testTaskLogMapper.insertTestTaskLog(testTaskLog);
return i;
} }
/** /**
@@ -114,6 +132,7 @@ public class TestTaskServiceImpl implements ITestTaskService {
log.error("定时任务已删除,不能执行!"); log.error("定时任务已删除,不能执行!");
return false; return false;
} }
if (triggerType == 2) {
TestTaskLog testTaskLog = new TestTaskLog(); TestTaskLog testTaskLog = new TestTaskLog();
testTaskLog.setTaskId(id); testTaskLog.setTaskId(id);
testTaskLog.setOperType("执行"); testTaskLog.setOperType("执行");
@@ -121,6 +140,7 @@ public class TestTaskServiceImpl implements ITestTaskService {
testTaskLog.setOperUser(operUser); testTaskLog.setOperUser(operUser);
testTaskLog.setOperTime(DateUtils.getNowDate()); testTaskLog.setOperTime(DateUtils.getNowDate());
testTaskLogMapper.insertTestTaskLog(testTaskLog); testTaskLogMapper.insertTestTaskLog(testTaskLog);
}
TestTaskResult testTaskResult = new TestTaskResult(); TestTaskResult testTaskResult = new TestTaskResult();
testTaskResult.setTaskId(id); testTaskResult.setTaskId(id);
testTaskResult.setTriggerTime(DateUtils.getNowDate()); testTaskResult.setTriggerTime(DateUtils.getNowDate());
@@ -148,7 +168,8 @@ public class TestTaskServiceImpl implements ITestTaskService {
// 开始执行定时任务逻辑。。。 // 开始执行定时任务逻辑。。。
if (triggerType == 1) { if (triggerType == 1) {
// 添加定时任务定时执行 // 添加定时任务定时执行
taskManagerService.addTask(testTask, jmeterHomePath, testTaskCaseList, testTaskResult); // taskManagerService.addTask(testTask, jmeterHomePath, testTaskCaseList, testTaskResult);
taskManagerService.executeTaskWithTestCases(testTask, jmeterHomePath, testTaskCaseList, testTaskResult);
} else { } else {
// 手动立即执行 // 手动立即执行
taskManagerService.executeTaskWithTestCases(testTask, jmeterHomePath, testTaskCaseList, testTaskResult); taskManagerService.executeTaskWithTestCases(testTask, jmeterHomePath, testTaskCaseList, testTaskResult);

View File

@@ -206,6 +206,7 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
} }
return uiHighSettingVOList; return uiHighSettingVOList;
} }
/** /**
* 查询ui自动化列表 * 查询ui自动化列表
* *
@@ -274,19 +275,42 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
uiHighSetting.setCreateTime(DateUtils.getNowDate()); uiHighSetting.setCreateTime(DateUtils.getNowDate());
try { try {
List<DataExtractionQO> combinedList1 = new ArrayList<>(); List<DataExtractionQO> combinedList1 = new ArrayList<>();
// 安全添加 windowExtractions
if (uiHighSettingQO.getDataExtractionQOList() != null &&
uiHighSettingQO.getDataExtractionQOList().getWindowExtractions() != null) {
combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getWindowExtractions()); combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getWindowExtractions());
}
// 安全添加 elementExtractions
if (uiHighSettingQO.getDataExtractionQOList() != null &&
uiHighSettingQO.getDataExtractionQOList().getElementExtractions() != null) {
combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getElementExtractions()); combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getElementExtractions());
}
//数据提取 //数据提取
String jsonStr = objectMapper.writeValueAsString(combinedList1); String jsonStr = objectMapper.writeValueAsString(combinedList1);
if (jsonStr != null) { if (jsonStr != null) {
uiHighSetting.setExtractionDataJson(jsonStr); uiHighSetting.setExtractionDataJson(jsonStr);
} }
//断言
List<AssertionQO> combinedList2 = new ArrayList<>(); List<AssertionQO> combinedList2 = new ArrayList<>();
// 安全添加 popupTexts
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getPopupTexts() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getPopupTexts()); combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getPopupTexts());
}
// 安全添加 elementAssertions
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getElementAssertions() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getElementAssertions()); combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getElementAssertions());
}
// 安全添加 dropdownBoxes
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getDropdownBoxes() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getDropdownBoxes()); combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getDropdownBoxes());
}
// 安全添加 webTitles
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getWebTitles() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getWebTitles()); combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getWebTitles());
}
jsonStr = objectMapper.writeValueAsString(combinedList2); jsonStr = objectMapper.writeValueAsString(combinedList2);
if (jsonStr != null) { if (jsonStr != null) {
uiHighSetting.setAssertionJson(jsonStr); uiHighSetting.setAssertionJson(jsonStr);
@@ -358,8 +382,16 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
uiHighSetting.setCreateTime(DateUtils.getNowDate()); uiHighSetting.setCreateTime(DateUtils.getNowDate());
try { try {
List<DataExtractionQO> combinedList1 = new ArrayList<>(); List<DataExtractionQO> combinedList1 = new ArrayList<>();
// 安全添加 windowExtractions
if (uiHighSettingQO.getDataExtractionQOList() != null &&
uiHighSettingQO.getDataExtractionQOList().getWindowExtractions() != null) {
combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getWindowExtractions()); combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getWindowExtractions());
}
// 安全添加 elementExtractions
if (uiHighSettingQO.getDataExtractionQOList() != null &&
uiHighSettingQO.getDataExtractionQOList().getElementExtractions() != null) {
combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getElementExtractions()); combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getElementExtractions());
}
//数据提取 //数据提取
String jsonStr = objectMapper.writeValueAsString(combinedList1); String jsonStr = objectMapper.writeValueAsString(combinedList1);
if (jsonStr != null) { if (jsonStr != null) {
@@ -367,10 +399,26 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
} }
// 断言 // 断言
List<AssertionQO> combinedList2 = new ArrayList<>(); List<AssertionQO> combinedList2 = new ArrayList<>();
// 安全添加 popupTexts
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getPopupTexts() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getPopupTexts()); combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getPopupTexts());
}
// 安全添加 elementAssertions
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getElementAssertions() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getElementAssertions()); combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getElementAssertions());
}
// 安全添加 dropdownBoxes
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getDropdownBoxes() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getDropdownBoxes()); combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getDropdownBoxes());
}
// 安全添加 webTitles
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getWebTitles() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getWebTitles()); combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getWebTitles());
}
jsonStr = objectMapper.writeValueAsString(combinedList2); jsonStr = objectMapper.writeValueAsString(combinedList2);
if (jsonStr != null) { if (jsonStr != null) {
uiHighSetting.setAssertionJson(jsonStr); uiHighSetting.setAssertionJson(jsonStr);

View File

@@ -1,10 +1,5 @@
package com.test.test.service.impl; package com.test.test.service.impl;
import java.util.*;
import java.util.NoSuchElementException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.reflect.TypeToken; import com.google.common.reflect.TypeToken;
@@ -13,7 +8,6 @@ import com.test.common.config.TestConfig;
import com.test.common.utils.DateUtils; import com.test.common.utils.DateUtils;
import com.test.common.utils.SecurityUtils; import com.test.common.utils.SecurityUtils;
import com.test.common.utils.SeleniumUtils; import com.test.common.utils.SeleniumUtils;
import com.test.common.utils.StringUtils;
import com.test.test.domain.*; import com.test.test.domain.*;
import com.test.test.domain.qo.*; import com.test.test.domain.qo.*;
import com.test.test.domain.vo.AssertionReportVO; import com.test.test.domain.vo.AssertionReportVO;
@@ -24,22 +18,28 @@ import com.test.test.mapper.UiSceneStepsReportMapper;
import com.test.test.service.*; import com.test.test.service.*;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.C;
import org.openqa.selenium.*; import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.Select; import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.UnexpectedTagNameException; import org.openqa.selenium.support.ui.UnexpectedTagNameException;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils; import org.springframework.util.CollectionUtils;
import java.util.NoSuchElementException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service @Service
@Slf4j @Slf4j
public class UiSceneStepsServiceImpl implements IUiSceneStepsService { public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
// 添加静态Map用于存储提取的数据
private static final Map<String, Object> extractedDataMap = new HashMap<>();
@Resource @Resource
private UiSceneStepsMapper uiSceneStepsMapper; private UiSceneStepsMapper uiSceneStepsMapper;
@Resource @Resource
@@ -94,9 +94,48 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
//创建报告步骤表 //创建报告步骤表
createSceneStepsReport(orderNumber, step, reportId); createSceneStepsReport(orderNumber, step, reportId);
} }
System.setProperty("webdriver.chrome.driver", chromeDriverPath);
// 配置 ChromeOptions
ChromeOptions options = new ChromeOptions();
// Docker 环境特定配置
options.addArguments("--headless=new"); // 使用新的无头模式
options.addArguments("--no-sandbox"); // 在 Docker 中必需
options.addArguments("--disable-dev-shm-usage"); // 避免 Docker 中的内存问题
options.addArguments("--remote-allow-origins=*");
// 性能优化
options.addArguments("--disable-gpu"); // 在无头模式中禁用 GPU
options.addArguments("--disable-extensions"); // 禁用扩展
options.addArguments("--disable-plugins");
options.addArguments("--window-size=1920,1080");
// 错误和日志处理
options.addArguments("--ignore-certificate-errors");
options.addArguments("--disable-notifications");
options.addArguments("--log-level=3"); // 最小化日志
options.addArguments("--silent");
// 内存优化
options.addArguments("--aggressive-cache-discard");
options.addArguments("--disable-cache");
options.addArguments("--disable-application-cache");
options.addArguments("--disable-offline-load-stale-cache");
options.addArguments("--disk-cache-size=0");
// 自动化检测绕过
options.addArguments("--disable-blink-features=AutomationControlled");
options.setExperimentalOption("excludeSwitches", new String[]{"enable-automation"});
options.setExperimentalOption("useAutomationExtension", false);
WebDriver driver = null;
try {
driver = new ChromeDriver(options);
SeleniumUtils seleniumUtils = new SeleniumUtils(driver);
try { try {
// 3. 执行所有步骤 // 3. 执行所有步骤
executeAllSteps(steps, reportId); executeAllSteps(steps, reportId, seleniumUtils);
log.info("场景执行成功: {}", automationId); log.info("场景执行成功: {}", automationId);
uiReport.setConsoleStr("OK"); uiReport.setConsoleStr("OK");
uiReport.setStatus(3); uiReport.setStatus(3);
@@ -136,7 +175,8 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
try { try {
List<AssertionReportVO> assertions = gson.fromJson( List<AssertionReportVO> assertions = gson.fromJson(
assertionJson, assertionJson,
new TypeToken<List<AssertionReportVO>>(){}.getType() new TypeToken<List<AssertionReportVO>>() {
}.getType()
); );
int stepAssertionCount = assertions.size(); int stepAssertionCount = assertions.size();
int stepSuccessCount = (int) assertions.stream() int stepSuccessCount = (int) assertions.stream()
@@ -168,22 +208,42 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
uiReport.setFaiiRate( uiReport.setFaiiRate(
stepsErrorNumber == 0 ? "0%" : stepsErrorNumber == 0 ? "0%" :
String.format("%.2f%%", (double) stepsErrorNumber / steps.size() * 100) String.format("%.2f%%", (double) stepsErrorNumber / steps.size() * 100)
); );
if (1 == uiReport.getStatus()){
uiReport.setStatus(4);
}
if (scenesErrorNumber > 0){
uiReport.setStatus(2);
}
uiReportService.updateUiReport(uiReport); uiReportService.updateUiReport(uiReport);
//修改ui_automation //修改ui_automation
UiAutomation uiAutomation1 = new UiAutomation(); UiAutomation uiAutomation1 = new UiAutomation();
uiAutomation1.setId(automationId); uiAutomation1.setId(automationId);
uiAutomation1.setExecutionResult("2"); uiAutomation1.setExecutionResult("2");
if (!uiAutomation.getExecutionResult().equals("2")) { if (scenesErrorNumber == 0) {
uiAutomation1.setExecutionResult("3"); uiAutomation1.setExecutionResult("3");
} }
uiAutomation1.setUpdateTime(DateUtils.getNowDate()); uiAutomation1.setUpdateTime(DateUtils.getNowDate());
uiAutomation1.setPassRate(stepsSucceedNumber == 0 ? "0%" : uiAutomation1.setPassRate(stepsSucceedNumber == 0 ? "0%" :
String.format("%.2f%%", (double) stepsSucceedNumber / steps.size() * 100)); String.format("%.2f%%", (double) stepsSucceedNumber / steps.size() * 100));
uiAutomationMapper.updateUiAutomation(uiAutomation1); uiAutomationMapper.updateUiAutomation(uiAutomation1);
//关闭浏览器
if (driver != null) {
try {
driver.quit();
} catch (Exception e) {
log.error("关闭浏览器失败", e);
}
}
}
} catch (SessionNotCreatedException e) {
log.error("ChromeDriver 会话创建失败: {}", e.getMessage());
throw new RuntimeException("浏览器启动失败,请检查 Chrome 和 ChromeDriver 版本是否匹配", e);
} catch (WebDriverException e) {
log.error("WebDriver 异常: {}", e.getMessage());
throw new RuntimeException("浏览器操作异常: " + e.getMessage(), e);
} }
return result; return result;
} }
@@ -196,7 +256,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/ */
private List<UiSceneSteps> validateAndGetSteps(Long automationId) { private List<UiSceneSteps> validateAndGetSteps(Long automationId) {
List<UiSceneSteps> steps = uiSceneStepsMapper.selectUiSceneStepsById(automationId); List<UiSceneSteps> steps = uiSceneStepsMapper.selectUiSceneStepsById(automationId);
steps = steps.stream().filter(e -> e.getIsDisabled() == 0).collect(Collectors.toList()); steps = steps.stream().filter(e -> 0 == e.getIsDisabled() ).collect(Collectors.toList());
if (CollectionUtils.isEmpty(steps)) { if (CollectionUtils.isEmpty(steps)) {
throw new IllegalArgumentException("步骤不能为空"); throw new IllegalArgumentException("步骤不能为空");
} }
@@ -230,10 +290,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
* *
* @param steps * @param steps
*/ */
private void executeAllSteps(List<UiSceneSteps> steps, Long reportId) { private void executeAllSteps(List<UiSceneSteps> steps, Long reportId,SeleniumUtils seleniumUtils) {
System.setProperty("webdriver.chrome.driver", chromeDriverPath);
WebDriver driver = new ChromeDriver();
SeleniumUtils seleniumUtils = new SeleniumUtils(driver);
Integer orderNumber = 0; Integer orderNumber = 0;
UiSceneStepsReport uiSceneStepsReport = new UiSceneStepsReport(); UiSceneStepsReport uiSceneStepsReport = new UiSceneStepsReport();
uiSceneStepsReport.setReportId(reportId); uiSceneStepsReport.setReportId(reportId);
@@ -249,7 +306,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
executeSingleStep(step, seleniumUtils, uiSceneStepsReports.get(0).getId(), uiHighSettingVOList); executeSingleStep(step, seleniumUtils, uiSceneStepsReports.get(0).getId(), uiHighSettingVOList);
} }
//关闭浏览器 //关闭浏览器
seleniumUtils.quit(); // seleniumUtils.quit();
} }
private void executeSingleStep(UiSceneSteps step, SeleniumUtils seleniumUtils, private void executeSingleStep(UiSceneSteps step, SeleniumUtils seleniumUtils,
@@ -300,7 +357,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
execution, uiHighSettingVOList); execution, uiHighSettingVOList);
if (!shouldContinue) { if (!shouldContinue) {
//关闭浏览器 //关闭浏览器
seleniumUtils.quit(); // seleniumUtils.quit();
throw new IllegalArgumentException("步骤执行失败,根据设置终止场景执行"); throw new IllegalArgumentException("步骤执行失败,根据设置终止场景执行");
} }
} }
@@ -319,6 +376,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
private boolean executeStepWithLog(UiSceneSteps step, SeleniumUtils seleniumUtils, private boolean executeStepWithLog(UiSceneSteps step, SeleniumUtils seleniumUtils,
Long sceneStepsReportId, StepExecution stepExecution, Long sceneStepsReportId, StepExecution stepExecution,
List<UiHighSettingVO> uiHighSettingVOList) { List<UiHighSettingVO> uiHighSettingVOList) {
log.info("开始执行步骤:{}",step.getName());
// 1. 初始化设置对象 // 1. 初始化设置对象
UiHighSettingVO errorSetting = extractErrorSetting(uiHighSettingVOList); UiHighSettingVO errorSetting = extractErrorSetting(uiHighSettingVOList);
UiHighSettingVO otherSetting = extractOtherSetting(uiHighSettingVOList); UiHighSettingVO otherSetting = extractOtherSetting(uiHighSettingVOList);
@@ -336,13 +394,14 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
} }
} }
//前置操作设置 //前置操作设置
List<UiHighSettingVO> beforeSettingList = extractbeforeSetting(uiHighSettingVOList); List<UiHighSettingVO> beforeSettingList = extractbeforeSetting(uiHighSettingVOList);
// 获取前置操作数据集合(判空) // 获取前置操作数据集合(判空)
Map<String, Object> beforeData = CollectionUtils.isEmpty(beforeSettingList) Map<String, Object> beforeData = CollectionUtils.isEmpty(beforeSettingList)
? new HashMap<>() ? new HashMap<>()
: filterBydataExtractionQOList(beforeSettingList, seleniumUtils); : filterBydataExtractionQOList(beforeSettingList, seleniumUtils);
// 保存前置数据到Map中使用步骤ID作为key的一部分
extractedDataMap.put("beforeData_" + step.getId(), beforeData);
//后置操作设置 //后置操作设置
List<UiHighSettingVO> afterSettingList = extractafterSetting(uiHighSettingVOList); List<UiHighSettingVO> afterSettingList = extractafterSetting(uiHighSettingVOList);
@@ -363,10 +422,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
Thread.sleep(beforeAwaitTime); Thread.sleep(beforeAwaitTime);
} }
if (screenshotConfiguration == 1) {
report.setScreenshot(seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile()));
log.info("截图成功,路径:{}", seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile()));
}
// 3. 执行具体步骤 // 3. 执行具体步骤
stepExecution.execute(step, seleniumUtils); stepExecution.execute(step, seleniumUtils);
@@ -375,13 +431,14 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
log.info("执行后置等待: {}ms", afterAwaitTime); log.info("执行后置等待: {}ms", afterAwaitTime);
Thread.sleep(afterAwaitTime); Thread.sleep(afterAwaitTime);
} }
report.setLogInfo("OK"); report.setLogInfo("OK");
report.setExecutionFlag("1"); report.setExecutionFlag("1");
} catch (Exception e) { } catch (Exception e) {
// 4. 错误处理 // 4. 错误处理
report.setLogInfo(e.toString()); report.setLogInfo(e.toString());
report.setExecutionFlag("2"); report.setExecutionFlag("2");
// 根据错误处理设置决定是否继续 // 根据错误处理设置决定是否继续哦了
if ("1".equals(errorSetting.getErrorHandling())) { if ("1".equals(errorSetting.getErrorHandling())) {
continueExecution = false; // 不继续执行后续步骤 continueExecution = false; // 不继续执行后续步骤
} }
@@ -391,10 +448,18 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
} }
log.error("步骤执行失败: {}", e.getMessage()); log.error("步骤执行失败: {}", e.getMessage());
} finally { } finally {
if (screenshotConfiguration == 1) {
report.setScreenshot(seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile()));
log.info("截图成功,路径:{}", seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile()));
}
//获取后置操作数据集合 //获取后置操作数据集合
Map<String, Object> afterData = CollectionUtils.isEmpty(afterSettingList) Map<String, Object> afterData = CollectionUtils.isEmpty(afterSettingList)
? new HashMap<>() ? new HashMap<>()
: filterBydataExtractionQOList(afterSettingList, seleniumUtils); : filterBydataExtractionQOList(afterSettingList, seleniumUtils);
// 保存后置数据到Map中使用步骤ID作为key的一部分
extractedDataMap.put("afterData_" + step.getId(), afterData);
// 合并当前步骤的前置和后置数据
Map<String, Object> mergedData = new HashMap<>(); Map<String, Object> mergedData = new HashMap<>();
if (!beforeData.isEmpty()) { if (!beforeData.isEmpty()) {
mergedData.put("前置数据提取", beforeData); mergedData.put("前置数据提取", beforeData);
@@ -412,11 +477,15 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
Map<String, Object> stringObjectMap = filterByAssertionQOList(afterSettingList, seleniumUtils); Map<String, Object> stringObjectMap = filterByAssertionQOList(afterSettingList, seleniumUtils);
//断言失败是否终止 //断言失败是否终止
String continueExecution1 = (String) stringObjectMap.get("continueExecution"); String continueExecution1 = (String) stringObjectMap.get("continueExecution");
String isSuccess = (String) stringObjectMap.get("isSuccess");
if (continueExecution1.equals("1")) { if (continueExecution1.equals("1")) {
continueExecution = false; continueExecution = false;
report.setLogInfo(""); report.setLogInfo("");
report.setExecutionFlag("2"); report.setExecutionFlag("2");
} }
if("0".equals(isSuccess)){
report.setExecutionFlag("2");
}
List<AssertionReportVO> assertionReportVOS = (List<AssertionReportVO>) stringObjectMap.get("assertionReportVOS"); List<AssertionReportVO> assertionReportVOS = (List<AssertionReportVO>) stringObjectMap.get("assertionReportVOS");
Gson gson = new Gson(); Gson gson = new Gson();
report.setAssertionJson(assertionReportVOS.isEmpty() ? null : gson.toJson(assertionReportVOS)); report.setAssertionJson(assertionReportVOS.isEmpty() ? null : gson.toJson(assertionReportVOS));
@@ -433,8 +502,6 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
} }
/** /**
* 提取前置操作 * 提取前置操作
* *
@@ -443,12 +510,13 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/ */
private List<UiHighSettingVO> extractbeforeSetting(List<UiHighSettingVO> settings) { private List<UiHighSettingVO> extractbeforeSetting(List<UiHighSettingVO> settings) {
return settings.stream() return settings.stream()
.filter(e -> "1".equals(e.getSettingType()) && e.getIsDisabled() == 0) .filter(e -> "1".equals(e.getSettingType()) && 1 == e.getIsDisabled() )
.collect(Collectors.toList()); // 返回默认设置 .collect(Collectors.toList()); // 返回默认设置
} }
/** /**
* 断言 * 断言
*
* @param settingList * @param settingList
* @param seleniumUtils * @param seleniumUtils
* @return * @return
@@ -457,11 +525,14 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
Map<String, Object> result = new HashMap<>(); Map<String, Object> result = new HashMap<>();
String continueExecution = "2"; String continueExecution = "2";
List<AssertionReportVO> assertionReports = new ArrayList<>(); List<AssertionReportVO> assertionReports = new ArrayList<>();
String isSuccess = "1"; // 默认所有断言成功
result.put("continueExecution", continueExecution); result.put("continueExecution", continueExecution);
result.put("assertionReportVOS", assertionReports); result.put("assertionReportVOS", assertionReports);
result.put("isSuccess", isSuccess); // 添加整体成功标志
List<UiHighSettingVO> uiHighSettingVOS = settingList.stream() List<UiHighSettingVO> uiHighSettingVOS = settingList.stream()
.filter(setting -> setting.getOperateType().equals("2") && 0 == setting.getIsDisabled()) .filter(setting -> setting.getOperateType().equals("2") && 1 == setting.getIsDisabled())
.toList(); .toList();
if (CollectionUtils.isEmpty(uiHighSettingVOS)) { if (CollectionUtils.isEmpty(uiHighSettingVOS)) {
@@ -476,7 +547,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
assertionQOList.addAll(assertion.getWebTitles()); assertionQOList.addAll(assertion.getWebTitles());
if (!CollectionUtils.isEmpty(assertionQOList)) { if (!CollectionUtils.isEmpty(assertionQOList)) {
for (AssertionQO assertionQO : assertionQOList) { for (AssertionQO assertionQO : assertionQOList) {
if ("1".equals(assertionQO.getIsDisabled())) { if ("0".equals(assertionQO.getIsDisabled())) {
continue; continue;
} }
AssertionReportVO report = new AssertionReportVO(); AssertionReportVO report = new AssertionReportVO();
@@ -504,15 +575,20 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
errorDetail = "不支持的断言类型: " + assertionQO.getAssertionType(); errorDetail = "不支持的断言类型: " + assertionQO.getAssertionType();
report.setErrorInfo(errorDetail); report.setErrorInfo(errorDetail);
report.setIsSuccess("0"); report.setIsSuccess("0");
isSuccess = "0"; // 设置整体失败标志
} }
if (!assertionResult && "1".equals(assertionQO.getIsFailedAbort())) { if (!assertionResult) {
isSuccess = "0"; // 任何断言失败都设置整体失败标志
if ("1".equals(assertionQO.getIsFailedAbort())) {
continueExecution = "1"; continueExecution = "1";
result.put("continueExecution", continueExecution); result.put("continueExecution", continueExecution);
} }
}
} catch (Exception e) { } catch (Exception e) {
report.setErrorInfo("断言执行异常: " + e.getMessage()); report.setErrorInfo("断言执行异常: " + e.getMessage());
report.setIsSuccess("0"); report.setIsSuccess("0");
isSuccess = "0"; // 异常也设置整体失败标志
log.error("断言执行异常", e); log.error("断言执行异常", e);
} }
@@ -521,6 +597,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
} }
} }
result.put("assertionReportVOS", assertionReports); result.put("assertionReportVOS", assertionReports);
result.put("isSuccess", isSuccess); // 更新最终的整体成功标志
return result; return result;
} }
@@ -565,6 +642,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
/** /**
* 处理元素断言 * 处理元素断言
*
* @param assertionQO 断言配置 * @param assertionQO 断言配置
* @param seleniumUtils Selenium工具类 * @param seleniumUtils Selenium工具类
* @param report 断言报告 * @param report 断言报告
@@ -728,6 +806,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
/** /**
* 处理下拉框断言 * 处理下拉框断言
*
* @param assertionQO 断言配置 * @param assertionQO 断言配置
* @param seleniumUtils Selenium工具类 * @param seleniumUtils Selenium工具类
* @param report 断言报告 * @param report 断言报告
@@ -826,6 +905,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
/** /**
* 处理网页标题断言 * 处理网页标题断言
*
* @param assertionQO 断言配置 * @param assertionQO 断言配置
* @param seleniumUtils Selenium工具类 * @param seleniumUtils Selenium工具类
* @param report 断言报告 * @param report 断言报告
@@ -859,12 +939,13 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
/** /**
* 获取数据提取集合 * 获取数据提取集合
*
* @param settingList * @param settingList
* @return * @return
*/ */
public Map<String, Object> filterBydataExtractionQOList(List<UiHighSettingVO> settingList, SeleniumUtils seleniumUtils) { public Map<String, Object> filterBydataExtractionQOList(List<UiHighSettingVO> settingList, SeleniumUtils seleniumUtils) {
List<UiHighSettingVO> uiHighSettingVOS = settingList.stream() List<UiHighSettingVO> uiHighSettingVOS = settingList.stream()
.filter(setting -> setting.getOperateType().equals("3") && setting.getIsDisabled() == 0) .filter(setting -> setting.getOperateType().equals("3") && setting.getIsDisabled() == 1)
.toList(); .toList();
Map<String, Object> variableStorage = new HashMap<>(); Map<String, Object> variableStorage = new HashMap<>();
if (CollectionUtils.isEmpty(uiHighSettingVOS)) { if (CollectionUtils.isEmpty(uiHighSettingVOS)) {
@@ -877,7 +958,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
dataExtractionQOList.addAll(dataExtractionQO.getWindowExtractions()); dataExtractionQOList.addAll(dataExtractionQO.getWindowExtractions());
if (!CollectionUtils.isEmpty(dataExtractionQOList)) { if (!CollectionUtils.isEmpty(dataExtractionQOList)) {
for (DataExtractionQO extraction : dataExtractionQOList) { for (DataExtractionQO extraction : dataExtractionQOList) {
if ("1".equals(extraction.getIsDisabled())) { if ("0".equals(extraction.getIsDisabled())) {
continue; continue;
} }
try { try {
@@ -900,6 +981,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
/** /**
* 获取数据提取集合具体实现 * 获取数据提取集合具体实现
*
* @param extraction * @param extraction
* @param seleniumUtils * @param seleniumUtils
* @return * @return
@@ -945,7 +1027,6 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
} }
/** /**
* 获取(前置//后置)等待时间 * 获取(前置//后置)等待时间
* *
@@ -978,7 +1059,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/ */
private List<UiHighSettingVO> extractafterSetting(List<UiHighSettingVO> settings) { private List<UiHighSettingVO> extractafterSetting(List<UiHighSettingVO> settings) {
return settings.stream() return settings.stream()
.filter(e -> "2".equals(e.getSettingType()) && e.getIsDisabled() == 0) .filter(e -> "2".equals(e.getSettingType()) && 1 == e.getIsDisabled())
.collect(Collectors.toList()); // 返回默认设置 .collect(Collectors.toList()); // 返回默认设置
} }
@@ -990,7 +1071,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/ */
private UiHighSettingVO extractErrorSetting(List<UiHighSettingVO> settings) { private UiHighSettingVO extractErrorSetting(List<UiHighSettingVO> settings) {
return settings.stream() return settings.stream()
.filter(e -> "3".equals(e.getSettingType()) && e.getIsDisabled() == 0) .filter(e -> "3".equals(e.getSettingType()))
.findFirst() .findFirst()
.orElse(new UiHighSettingVO()); // 返回默认设置 .orElse(new UiHighSettingVO()); // 返回默认设置
} }
@@ -1004,7 +1085,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/ */
private UiHighSettingVO extractOtherSetting(List<UiHighSettingVO> settings) { private UiHighSettingVO extractOtherSetting(List<UiHighSettingVO> settings) {
return settings.stream() return settings.stream()
.filter(e -> "4".equals(e.getSettingType()) && e.getIsDisabled() == 0) .filter(e -> "4".equals(e.getSettingType()))
.findFirst() .findFirst()
.orElse(new UiHighSettingVO()); // 返回默认设置 .orElse(new UiHighSettingVO()); // 返回默认设置
} }
@@ -1020,7 +1101,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/ */
private void openWebPage(UiSceneSteps step, SeleniumUtils seleniumUtils) { private void openWebPage(UiSceneSteps step, SeleniumUtils seleniumUtils) {
//追加页面在新的页面打开url不勾选覆盖当前url 0不追加 1追加 //追加页面在新的页面打开url不勾选覆盖当前url 0不追加 1追加
Integer isAppendPage = step.getIsAppendPage(); // 0=当前页1=新标签页 Integer isAppendPage = step.getIsAppendPage() == null ? 0 : step.getIsAppendPage(); // 0=当前页1=新标签页
String url = step.getUrl(); String url = step.getUrl();
log.info("打开网页:{}, 模式:{}", url, isAppendPage == 1 ? "新标签页" : "当前页"); log.info("打开网页:{}, 模式:{}", url, isAppendPage == 1 ? "新标签页" : "当前页");
if (isAppendPage != null && isAppendPage == 1) { if (isAppendPage != null && isAppendPage == 1) {
@@ -1460,6 +1541,31 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
seleniumUtils.clearInput(locator); seleniumUtils.clearInput(locator);
return; return;
} }
// 检查输入值是否是变量引用格式 ${key}
if (inputValue.startsWith("${") && inputValue.endsWith("}")) {
Object value = null;
// 遍历所有步骤的数据,从最新的开始查找
for (Map.Entry<String, Object> entry : extractedDataMap.entrySet()) {
if (entry.getValue() instanceof Map) {
Map<String, Object> dataMap = (Map<String, Object>) entry.getValue();
// 检查是否包含完整的 ${key} 格式
if (dataMap.containsKey(inputValue)) {
value = dataMap.get(inputValue);
break;
}
}
}
if (value != null) {
inputValue = value.toString();
log.info("变量 {} 被替换为: {}", inputValue, value);
} else {
log.warn("未找到变量 {} 的值,保持原样", inputValue);
}
}
// 执行输入操作 // 执行输入操作
if (step.getOperate() == 1) { // 普通输入框 if (step.getOperate() == 1) { // 普通输入框
seleniumUtils.inputText( seleniumUtils.inputText(
@@ -1494,6 +1600,4 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
} }
} }

View File

@@ -123,7 +123,7 @@ public class DynamicTaskManager {
// 执行业务逻辑 // 执行业务逻辑
log.info("执行任务: " + task.getId()); log.info("执行任务: " + task.getId());
try{ try{
performanceTestCaseReportService.executePerformanceTestAndReport(task.getId(),jmeterHomePath,1); performanceTestCaseReportService.executePerformanceTestAndReport(task.getId(),jmeterHomePath,1,"system");
} catch (Exception e) { } catch (Exception e) {
log.error("执行失败!",e); log.error("执行失败!",e);
} }

View File

@@ -0,0 +1,149 @@
package com.test.test.task;
import com.test.test.domain.TestTask;
import com.test.test.domain.qo.GroupIdQO;
import com.test.test.service.ITestTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.TaskScheduler;
import org.springframework.scheduling.support.CronTrigger;
import org.springframework.stereotype.Component;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.Resource;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
/**
* 定时任务管理器
* 负责调度和执行定时任务
*/
@Slf4j
@Component
public class TestTaskManager {
@Resource
private ITestTaskService testTaskService;
@Resource
private TaskScheduler taskScheduler;
@Value("${test.jmeterHomePath:/opt/apache-jmeter}")
private String jmeterHomePath;
// 用于存储正在运行的任务
private final Map<Long, ScheduledFuture<?>> scheduledTasks = new ConcurrentHashMap<>();
@PostConstruct
public void init() {
loadTasks(); // 初始化时加载所有启用的定时任务
}
/**
* 初始加载所有启用的定时任务
*/
private void loadTasks() {
try {
GroupIdQO qo = new GroupIdQO();
List<TestTask> tasks = testTaskService.selectTestTaskList(qo);
if (tasks != null) {
tasks.stream()
.filter(task -> task != null
&& task.getStatus() != null
&& task.getStatus() == 0
&& "0".equals(task.getDelFlag()))
.forEach(this::scheduleTask);
}
} catch (Exception e) {
log.error("加载定时任务失败", e);
}
}
/**
* 更新任务
*/
public void updateTask(TestTask task) {
log.info("更新任务 {}", task.getId());
Long taskId = task.getId();
// 先取消现有任务
if (scheduledTasks.containsKey(taskId)) {
scheduledTasks.get(taskId).cancel(true);
scheduledTasks.remove(taskId);
}
// 如果任务状态是启用且没有被删除,则重新调度
if (task.getStatus() == 0 && "0".equals(task.getDelFlag())) {
log.info("启动任务:{}", task.getId());
scheduleTask(task);
}
}
/**
* 添加新任务到定时调度
*/
public void addNewTask(TestTask task) {
// 只有当任务状态是启用且没有被删除时才创建定时任务
if (task.getStatus() == 0 && "0".equals(task.getDelFlag())) {
log.info("加入定时任务:{}", task.getId());
scheduleTask(task);
}
}
/**
* 移除定时任务
*/
public void removeTask(Long taskId) {
if (scheduledTasks.containsKey(taskId)) {
scheduledTasks.get(taskId).cancel(true);
scheduledTasks.remove(taskId);
log.info("已移除定时任务: {}", taskId);
}
}
/**
* 批量移除定时任务
*/
public void removeTasks(Long[] taskIds) {
for (Long taskId : taskIds) {
removeTask(taskId);
}
}
/**
* 调度单个任务
*/
private void scheduleTask(TestTask task) {
try {
if (task.getCrontab() == null || task.getCrontab().trim().isEmpty()) {
log.error("任务 [{}] 的cron表达式为空", task.getName());
return;
}
CronTrigger trigger = new CronTrigger(task.getCrontab());
ScheduledFuture<?> future = taskScheduler.schedule(
() -> executeTask(task), trigger
);
scheduledTasks.put(task.getId(), future);
log.info("成功调度任务: {}", task.getName());
} catch (IllegalArgumentException e) {
log.error("无效的cron表达式: " + task.getCrontab(), e);
} catch (Exception e) {
log.error("调度任务时发生错误: " + task.getName(), e);
}
}
/**
* 执行单个任务
*/
private void executeTask(TestTask task) {
try {
log.info("开始执行任务: {}", task.getName());
testTaskService.executeTestTaskById(task.getId(), 1, null, jmeterHomePath, "system");
} catch (Exception e) {
log.error("执行任务 [{}] 时发生错误", task.getName(), e);
}
}
}

View File

@@ -47,6 +47,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
<if test="delFlag != null and delFlag != ''"> and a.del_flag = #{delFlag}</if> <if test="delFlag != null and delFlag != ''"> and a.del_flag = #{delFlag}</if>
<if test="status != null and status != ''"> and a.status = #{status}</if> <if test="status != null and status != ''"> and a.status = #{status}</if>
</where> </where>
order by a.id desc
</select> </select>
<select id="selectPerformanceTestCaseReportById" parameterType="Long" resultMap="PerformanceTestCaseReportResult"> <select id="selectPerformanceTestCaseReportById" parameterType="Long" resultMap="PerformanceTestCaseReportResult">

View File

@@ -36,10 +36,21 @@
where id = #{id} where id = #{id}
</select> </select>
<select id="selectTestCaseLogCaseSid" resultType="java.lang.String">
SELECT case_sid
FROM test_case_log
WHERE plan_id = #{planId}
AND type = #{type}
ORDER BY id
DESC limit 1
</select>
<insert id="insertTestCaseLog" parameterType="TestCaseLog" useGeneratedKeys="true" keyProperty="id"> <insert id="insertTestCaseLog" parameterType="TestCaseLog" useGeneratedKeys="true" keyProperty="id">
insert into test_case_log insert into test_case_log
<trim prefix="(" suffix=")" suffixOverrides=","> <trim prefix="(" suffix=")" suffixOverrides=",">
<if test="caseId != null">case_id,</if> <if test="caseId != null">case_id,</if>
<if test="planId != null">plan_id,</if>
<if test="type != null">type,</if>
<if test="operType != null">oper_type,</if> <if test="operType != null">oper_type,</if>
<if test="operDetail != null">oper_detail,</if> <if test="operDetail != null">oper_detail,</if>
<if test="operUser != null">oper_user,</if> <if test="operUser != null">oper_user,</if>
@@ -48,6 +59,8 @@
</trim> </trim>
<trim prefix="values (" suffix=")" suffixOverrides=","> <trim prefix="values (" suffix=")" suffixOverrides=",">
<if test="caseId != null">#{caseId},</if> <if test="caseId != null">#{caseId},</if>
<if test="planId != null">#{planId},</if>
<if test="type != null">#{type},</if>
<if test="operType != null">#{operType},</if> <if test="operType != null">#{operType},</if>
<if test="operDetail != null">#{operDetail},</if> <if test="operDetail != null">#{operDetail},</if>
<if test="operUser != null">#{operUser},</if> <if test="operUser != null">#{operUser},</if>

View File

@@ -138,4 +138,24 @@
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<select id="selectRelateCaseList" resultType="TestCase">
SELECT DISTINCT
tc.id,
tc.group_id,
tc.project_id,
tc.name,
tc.importance,
tc.status,
tc.del_flag,
tc.create_by AS createBy,
tc.create_time,
tc.update_by,
tc.update_time
FROM test_project_plan tpp
LEFT JOIN test_plan_case tpc ON tpc.plan_id = tpp.plan_id
LEFT JOIN test_case tc ON tc.id = tpc.case_id
WHERE tpp.project_id = #{projectId}
AND tpc.del_flag = '0'
</select>
</mapper> </mapper>

View File

@@ -148,4 +148,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<select id="selectRelateDefectList" resultType="TestDefect">
SELECT DISTINCT
td.id,
td.serial_number AS serialNumber,
td.name,
td.outline,
td.detail,
td.status,
su.user_name AS manager,
td.dev,
td.level,
td.type,
td.reappearance,
td.test,
td.mail,
td.version,
td.del_flag,
td.create_time AS createTime
FROM test_project_plan tpp
LEFT JOIN test_plan_defect tpd ON tpd.plan_id = tpp.plan_id
LEFT JOIN test_defect td ON td.id = tpd.defect_id
LEFT JOIN sys_user su ON su.user_id = td.manager
WHERE tpp.project_id = #{projectId}
AND tpd.del_flag = '0'
</select>
</mapper> </mapper>

View File

@@ -124,4 +124,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id} #{id}
</foreach> </foreach>
</delete> </delete>
<select id="selectDefectPlanList" resultType="TestPlan">
SELECT DISTINCT
tp.id,
tp.serial_number AS serialNumber,
tp.name ,
tp.status,
su.user_name AS manager,
tp.create_time AS createTime,
tp.version,
tp.del_flag
FROM test_plan_defect tpd
LEFT JOIN test_plan tp ON tp.id = tpd.plan_id
LEFT JOIN sys_user su ON su.user_id = tp.manager
WHERE tpd.defect_id = #{defectId}
AND tpd.del_flag = '0'
</select>
</mapper> </mapper>

View File

@@ -149,19 +149,23 @@
(SELECT COUNT(1) FROM test_plan_case tpc (SELECT COUNT(1) FROM test_plan_case tpc
WHERE tpc.plan_id = tp.id WHERE tpc.plan_id = tp.id
AND tpc.del_flag = '0' AND tpc.del_flag = '0'
AND tpc.type = '1') AS functionTestPassNum, AND tpc.type = '1'
AND tpc.execute_result = '1') AS functionTestPassNum,
(SELECT COUNT(1) FROM test_plan_case tpc (SELECT COUNT(1) FROM test_plan_case tpc
WHERE tpc.plan_id = tp.id WHERE tpc.plan_id = tp.id
AND tpc.del_flag = '0' AND tpc.del_flag = '0'
AND tpc.type = '2') AS regressionTestPassNum, AND tpc.type = '2'
AND tpc.execute_result = '1') AS regressionTestPassNum,
(SELECT COUNT(1) FROM test_plan_case tpc (SELECT COUNT(1) FROM test_plan_case tpc
WHERE tpc.plan_id = tp.id WHERE tpc.plan_id = tp.id
AND tpc.del_flag = '0' AND tpc.del_flag = '0'
AND tpc.type = '3') AS preProductionTestPassNum, AND tpc.type = '3'
AND tpc.execute_result = '1') AS preProductionTestPassNum,
(SELECT COUNT(1) FROM test_plan_case tpc (SELECT COUNT(1) FROM test_plan_case tpc
WHERE tpc.plan_id = tp.id WHERE tpc.plan_id = tp.id
AND tpc.del_flag = '0' AND tpc.del_flag = '0'
AND tpc.type = '4') AS productionTestPassNum, AND tpc.type = '4'
AND tpc.execute_result = '1') AS productionTestPassNum,
tp.version, tp.version,
( (
SELECT COUNT(1) FROM test_plan_defect tpd SELECT COUNT(1) FROM test_plan_defect tpd
@@ -203,4 +207,56 @@
<select id="selectPlanId" resultType="Long"> <select id="selectPlanId" resultType="Long">
select id from test_plan where serial_number = #{serialNumber} and del_flag = '0' select id from test_plan where serial_number = #{serialNumber} and del_flag = '0'
</select> </select>
<select id="selectPlanOverview" resultType="TestPlanOverviewVO">
SELECT
(SELECT COUNT(1) FROM test_plan_case tpc WHERE tpc.plan_id = #{id} AND tpc.del_flag = '0') AS caseCount,
(SELECT COUNT(1) FROM test_plan_case tpc WHERE tpc.plan_id = #{id} AND tpc.del_flag = '0' AND tpc.execute_result != '0') AS executedCaseCount,
(SELECT COUNT(1) FROM test_plan_case tpc WHERE tpc.plan_id = #{id} AND tpc.del_flag = '0' AND tpc.execute_result = '1') AS passedCaseCount,
(SELECT COUNT(1) FROM test_plan_case tpc WHERE tpc.plan_id = #{id} AND tpc.del_flag = '0' AND tpc.execute_result = '2') AS failedCaseCount,
(SELECT COUNT(1) FROM test_plan_case tpc WHERE tpc.plan_id = #{id} AND tpc.del_flag = '0' AND tpc.execute_result = '0') AS notExecutedCaseCount,
(SELECT COUNT(1) FROM test_plan_case tpc WHERE tpc.plan_id = #{id} AND tpc.del_flag = '0' AND tpc.execute_result = '3') AS blockedCaseCount,
(SELECT COUNT(1) FROM test_plan_case tpc WHERE tpc.plan_id = #{id} AND tpc.del_flag = '0' AND tpc.execute_result = '4') AS skippedCaseCount,
(SELECT COUNT(1) FROM test_plan_defect tpd WHERE tpd.plan_id = #{id} AND tpd.del_flag = '0') AS defectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.status = '0' AND tpd.del_flag = '0') AS unconfirmedDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.status = '1' AND tpd.del_flag = '0') AS fixingDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.status = '5' AND tpd.del_flag = '0') AS unverifiedDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.status = '3' AND tpd.del_flag = '0') AS invalidDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.status = '4' AND tpd.del_flag = '0') AS suspendedDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.level = '0' AND tpd.del_flag = '0') AS invalidLevelDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.level = '1' AND tpd.del_flag = '0') AS minorLevelDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.level = '2' AND tpd.del_flag = '0') AS normalLevelDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.level = '3' AND tpd.del_flag = '0') AS seriousLevelDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.level = '4' AND tpd.del_flag = '0') AS fatalLevelDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.type = '0' AND tpd.del_flag = '0') AS logicDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.type = '1' AND tpd.del_flag = '0') AS uiDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.type = '2' AND tpd.del_flag = '0') AS performanceDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.type = '3' AND tpd.del_flag = '0') AS compatibilityDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.type = '4' AND tpd.del_flag = '0') AS configurationDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.type = '5' AND tpd.del_flag = '0') AS securityDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.type = '6' AND tpd.del_flag = '0') AS installationDefectCount,
(SELECT COUNT(1) FROM test_plan_defect tpd LEFT JOIN test_defect td ON td.id = tpd.defect_id WHERE tpd.plan_id = #{id} AND td.type = '7' AND tpd.del_flag = '0') AS otherDefectCount
FROM test_plan tp
WHERE tp.id = #{id}
AND tp.del_flag = '0'
</select>
<select id="getCaseTrendData" resultType="TestPlanOverviewTrendDataVO">
SELECT
DATE_FORMAT(tpc.execute_time, '%Y-%m-%d') AS caseTrendDates,
SUM(CASE WHEN tpc.execute_result = '0' THEN 1 ELSE 0 END) AS notExecuted,
SUM(CASE WHEN tpc.execute_result = '1' THEN 1 ELSE 0 END) AS passed,
SUM(CASE WHEN tpc.execute_result = '2' THEN 1 ELSE 0 END) AS failed,
SUM(CASE WHEN tpc.execute_result = '3' THEN 1 ELSE 0 END) AS blocked,
SUM(CASE WHEN tpc.execute_result = '4' THEN 1 ELSE 0 END) AS skipped
FROM test_plan_case tpc
LEFT JOIN test_plan tp ON tp.id = tpc.plan_id
WHERE tpc.del_flag = '0'
AND tpc.plan_id = #{id}
AND tp.del_flag = '0'
AND tpc.execute_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY)
GROUP BY DATE_FORMAT(tpc.execute_time, '%Y-%m-%d')
ORDER BY DATE_FORMAT(tpc.execute_time, '%Y-%m-%d') ASC
</select>
</mapper> </mapper>

View File

@@ -155,4 +155,28 @@
<include refid="selectTestProjectVo"/> <include refid="selectTestProjectVo"/>
WHERE id = #{id} WHERE id = #{id}
</select> </select>
<select id="selectRelateProjectList" resultType="TestProject">
SELECT DISTINCT
tp.id,
tp.serial_number AS serialNumber,
tp.name,
tp.outline,
tp.detail,
tp.priority,
tp.estimated_time AS estimatedTime,
tp.source,
tp.type,
tp.status,
su.user_name AS manager,
tp.create_time AS createTime,
tp.version,
tp.del_flag
FROM test_plan_defect tpd
LEFT JOIN test_project_plan tpp ON tpp.plan_id = tpd.plan_id
LEFT JOIN test_project tp ON tp.id = tpp.project_id
LEFT JOIN sys_user su ON su.user_id = tp.manager
WHERE tpd.defect_id = #{defectId}
AND tpp.del_flag = '0'
</select>
</mapper> </mapper>

View File

@@ -45,4 +45,21 @@
WHERE tpp.plan_id = #{planId} WHERE tpp.plan_id = #{planId}
AND tpp.del_flag = '0' AND tpp.del_flag = '0'
</select> </select>
<select id="selectRelatePlanList" resultType="TestPlanProjectVo">
SELECT
tp.id,
tp.serial_number AS serialNumber,
tp.name ,
tp.status,
su.user_name AS manager,
tp.create_time AS createTime,
tp.version,
tp.del_flag
FROM test_project_plan tpp
LEFT JOIN test_plan tp ON tp.id = tpp.plan_id
LEFT JOIN sys_user su ON su.user_id = tp.manager
WHERE tpp.project_id = #{projectId}
AND tpp.del_flag = '0'
</select>
</mapper> </mapper>

View File

@@ -31,17 +31,72 @@
</trim> </trim>
</insert> </insert>
<resultMap type="TestReport" id="TestReportResult">
<result property="id" column="id"/>
<result property="serialNumber" column="serial_number"/>
<result property="name" column="name"/>
<result property="result" column="result"/>
<result property="status" column="status"/>
<result property="report" column="report"/>
<result property="updateBy" column="update_by"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<result property="version" column="version"/>
<result property="delFlag" column="del_flag"/>
</resultMap>
<sql id="selectTestReportVo">
SELECT id,
serial_number,
name,
result,
status,
report,
update_by,
create_time,
update_time,
version,
del_flag
FROM test_report
</sql>
<select id="selectTestReportList" parameterType="Long" resultType="TestReportVo"> <select id="selectTestReportList" parameterType="Long" resultType="TestReportVo">
SELECT SELECT tr.id AS id,
tr.name AS name, tr.name AS name,
tr.result AS result, tr.result AS result,
tr.report AS report,
tr.status AS status, tr.status AS status,
tpr.type AS type, tpr.type AS type,
tr.create_time AS createTime, tr.create_time AS createTime,
tr.update_by AS updateBy tr.update_by AS updateBy,
tr.update_time AS updateTime
FROM test_plan_report tpr FROM test_plan_report tpr
LEFT JOIN test_report tr ON tr.id = tpr.report_id LEFT JOIN test_report tr ON tr.id = tpr.report_id
WHERE tpr.plan_id = #{planId} WHERE tpr.plan_id = #{planId}
AND tpr.del_flag = '0' AND tpr.del_flag = '0'
AND tr.del_flag = '0'
ORDER BY tr.create_time DESC
</select> </select>
<select id="selectCaseTestReportById" parameterType="Long" resultMap="TestReportResult">
<include refid="selectTestReportVo"/>
where id = #{id}
</select>
<update id="updateExecuteCaseReport" parameterType="testReport">
update test_report
<trim prefix="SET" suffixOverrides=",">
<if test="serialNumber != null">serial_number = #{serialNumber},</if>
<if test="name != null">name = #{name},</if>
<if test="result != null">result = #{result},</if>
<if test="status != null">status = #{status},</if>
<if test="report != null">report = #{report},</if>
<if test="updateBy != null">update_by = #{updateBy},</if>
<if test="createTime != null">create_time = #{createTime},</if>
<if test="updateTime != null">update_time = #{updateTime},</if>
<if test="version != null">version = #{version},</if>
<if test="delFlag != null">del_flag = #{delFlag},</if>
</trim>
where id = #{id}
</update>
</mapper> </mapper>

View File

@@ -28,7 +28,9 @@
<select id="selectTestTaskList" parameterType="GroupIdQO" resultMap="TestTaskResult"> <select id="selectTestTaskList" parameterType="GroupIdQO" resultMap="TestTaskResult">
<include refid="selectTestTaskVo"/> <include refid="selectTestTaskVo"/>
<where> <where>
<if test="groupId != null">
group_id = #{groupId} group_id = #{groupId}
</if>
</where> </where>
order by create_time desc order by create_time desc
</select> </select>

View File

@@ -43,7 +43,7 @@
<select id="selectUiHighSettingById" parameterType="Long" resultMap="UiHighSettingResult"> <select id="selectUiHighSettingById" parameterType="Long" resultMap="UiHighSettingResult">
<include refid="selectUiHighSettingVo"/> <include refid="selectUiHighSettingVo"/>
where scene_steps_id = #{sceneStepsId} and del_flag = 0 order by operate_type asc where scene_steps_id = #{sceneStepsId} and del_flag = 0 order by setting_type,order_number asc
</select> </select>
<insert id="insertUiHighSetting" parameterType="UiHighSetting" useGeneratedKeys="true" keyProperty="id"> <insert id="insertUiHighSetting" parameterType="UiHighSetting" useGeneratedKeys="true" keyProperty="id">

View File

@@ -46,6 +46,7 @@
"js-cookie": "3.0.1", "js-cookie": "3.0.1",
"jsencrypt": "3.0.0-rc.1", "jsencrypt": "3.0.0-rc.1",
"json-editor-vue": "^0.17.3", "json-editor-vue": "^0.17.3",
"moment": "^2.30.1",
"nprogress": "0.2.0", "nprogress": "0.2.0",
"quill": "2.0.2", "quill": "2.0.2",
"screenfull": "5.0.2", "screenfull": "5.0.2",

View File

@@ -73,7 +73,7 @@ export function editAndExecuteTest(data) {
// 测试-编辑-立即执行 // 测试-编辑-立即执行
export function executeTest(query) { export function executeTest(query) {
return request({ return request({
url: '/test/performanceTest/executeNow?' + query, url: '/test/performanceTest/executeNow?id=' + query,
method: 'get', method: 'get'
}) })
} }

View File

@@ -5,7 +5,9 @@ const api = {
addBug: 'test/defect/addBug', addBug: 'test/defect/addBug',
delBug: 'test/defect/delBug', delBug: 'test/defect/delBug',
getBugDetail: 'test/defect/bugDetail', getBugDetail: 'test/defect/bugDetail',
updateBug: 'test/defect/editBug' updateBug: 'test/defect/editBug',
getDefectPlanList: 'testPlan/defect/defectPlanList',
getDefectProjectList: 'testPlan/defect/defectProjectList',
} }
export function getBugList(data) { export function getBugList(data) {
@@ -49,3 +51,19 @@ export function updateBug(data) {
}) })
} }
export function getDefectPlanList(id) {
return request({
url: api.getDefectPlanList,
method: 'post',
data: {id}
})
}
export function getDefectProjectList(id) {
return request({
url: api.getDefectProjectList,
method: 'post',
data: {id}
})
}

View File

@@ -6,7 +6,10 @@ const api = {
addProject: 'test/project/addProject', addProject: 'test/project/addProject',
delProject: 'test/project/delProject', delProject: 'test/project/delProject',
getProjectDetail: 'test/project/projectDetail', getProjectDetail: 'test/project/projectDetail',
updateProject: 'test/project/editProject' updateProject: 'test/project/editProject',
relatePlanList: 'test/testPlanProject/relatePlanList',
relateCaseList: 'test/testPlanProject/relateCaseList',
relateDefectList: 'test/testPlanProject/relateDefectList',
} }
export function managerList(data) { export function managerList(data) {
@@ -58,3 +61,27 @@ export function updateProject(data) {
}) })
} }
export function getRelatePlanList(id) {
return request({
url: api.relatePlanList,
method: 'post',
data: {id}
})
}
export function getRelateCaseTableList(id) {
return request({
url: api.relateCaseList,
method: 'post',
data: {id}
})
}
export function getRelateDefectList(id) {
return request({
url: api.relateDefectList,
method: 'post',
data: {id}
})
}

View File

@@ -8,7 +8,11 @@ const api = {
testPlanDetail: 'test/testPlan/planDetail', testPlanDetail: 'test/testPlan/planDetail',
testPlanProjectList: '/test/testPlanProject/list', testPlanProjectList: '/test/testPlanProject/list',
addTestReport: 'test/testReport/addTestReport', addTestReport: 'test/testReport/addTestReport',
getTestReportList: 'test/testReport/reportList' getTestReportList: 'test/testReport/reportList',
delExecuteCaseReport: 'test/testReport/delExecuteCaseReport',
updateExecuteCaseReport: 'test/testReport/updateExecuteCaseReport',
getPlanOverview:'test/testPlan/planOverview',
getPlanCaseTrendData: 'test/testPlan/planCaseTrendData',
} }
export function getTestPlanList(data) { export function getTestPlanList(data) {
@@ -67,10 +71,42 @@ export function addTestReport(data) {
}) })
} }
export function getTestReportList(id) { export function getTestReportList(data) {
return request({ return request({
url: api.getTestReportList, url: api.getTestReportList,
method: 'post', method: 'post',
params: data
})
}
export function delExecuteCaseReport(id) {
return request({
url: api.delExecuteCaseReport,
method: 'post',
data: {id}
})
}
export function updateExecuteCaseReport(data) {
return request({
url: api.updateExecuteCaseReport,
method: 'post',
data: data
})
}
export function getPlanOverview(id) {
return request({
url: api.getPlanOverview,
method: 'post',
data: {id}
})
}
export function getPlanCaseTrendData(id) {
return request({
url: api.getPlanCaseTrendData,
method: 'post',
data: {id} data: {id}
}) })
} }

View File

@@ -24,3 +24,13 @@ export function getTestReportDetail(query) {
method: 'get', method: 'get',
}) })
} }
// 测试报告 - 获取截图
export function getScreenshot(path) {
return request({
url: '/test/report/screenshot',
method: 'get',
params: { path },
responseType: 'blob'
})
}

View File

@@ -159,7 +159,7 @@ export const constantRoutes = [
] ]
}, },
{ {
path: '/performance/edit', path: '/performance/edit/:id',
component: Layout, component: Layout,
hidden: true, hidden: true,
children: [ children: [
@@ -173,7 +173,7 @@ export const constantRoutes = [
] ]
}, },
{ {
path: '/performance/report/detail', path: '/performance/report/detail/:id',
component: Layout, component: Layout,
hidden: true, hidden: true,
children: [ children: [
@@ -187,7 +187,7 @@ export const constantRoutes = [
] ]
}, },
{ {
path: '/ui-test/report/detail', path: '/ui-test/report/detail/:id',
component: Layout, component: Layout,
hidden: true, hidden: true,
children: [ children: [
@@ -215,7 +215,7 @@ export const constantRoutes = [
] ]
}, },
{ {
path: '/ui-test/automation/edit', path: '/ui-test/automation/edit/:id',
component: Layout, component: Layout,
hidden: true, hidden: true,
children: [ children: [
@@ -223,11 +223,23 @@ export const constantRoutes = [
path: '', path: '',
component: () => import('@/views/test/uiTest/editScene'), component: () => import('@/views/test/uiTest/editScene'),
name: 'EditScene', name: 'EditScene',
noCache: true,
meta: { title: '编辑场景', activeMenu: '/ui-test' } meta: { title: '编辑场景', activeMenu: '/ui-test' }
} }
] ]
}, },
{
path: '/ui-test/automation-test',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/test/uiTest/automationTest'),
name: 'AutomationTestView',
meta: { title: 'UI自动化测试', activeMenu: '/ui-test' }
}
]
},
{ {
path: '/testplan/overview', path: '/testplan/overview',
component: Layout, component: Layout,
@@ -242,6 +254,20 @@ export const constantRoutes = [
} }
] ]
}, },
{
path: '/testplan/casereport/platformReport',
component: Layout,
hidden: true,
children: [
{
path: '',
component: () => import('@/views/test/testplan/caseReport/platformReport'),
name: 'platformReport',
noCache: true,
meta: { title: '平台报告', activeMenu: '/testplan' }
}
]
},
] ]
// 动态路由,基于用户权限动态去加载 // 动态路由,基于用户权限动态去加载

View File

@@ -0,0 +1,136 @@
<template>
<div class="app-container">
<el-form ref="detailForm" :model="detailForm" label-width="110px" label-position="right">
<el-container>
<el-main>
<el-form-item label="缺陷概要" prop="outline">
{{ detailForm.outline }}
</el-form-item>
<!-- 描述 -->
<el-form-item label="描述" prop="detail">
{{ detailForm.detail }}
</el-form-item>
</el-main>
<el-aside width="450px" class="basic-information">
<h3>基础信息</h3>
<el-form-item label="缺陷ID" prop="serialNumber">
{{detailForm.serialNumber}}
</el-form-item>
<el-form-item label="缺陷名称" prop="name">
{{ detailForm.name }}
</el-form-item>
<el-form-item label="经办人" prop="manager">
{{ managerName }}
</el-form-item>
<el-form-item label="严重程度" prop="level">
{{ detailForm.level }}
</el-form-item>
<el-form-item label="缺陷类型" prop="type">
{{ detailForm.type }}
</el-form-item>
<el-form-item label="缺陷状态" prop="status">
{{ detailForm.status }}
</el-form-item>
<el-form-item label="能否复现" prop="reappearance">
{{ reappearanceLabel }}
</el-form-item>
<el-form-item label="开发人" prop="dev">
{{ detailForm.dev }}
</el-form-item>
<el-form-item label="测试人" prop="test">
{{ detailForm.test }}
</el-form-item>
<el-form-item label="版本" prop="version">
{{ detailForm.version }}
</el-form-item>
</el-aside>
</el-container>
</el-form>
</div>
</template>
<script>
import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue";
import {getBugDetail} from "@/api/test/bug";
export default {
name: 'bugDetail',
components: {SimpleOptions},
dicts: ['severity_level', 'bug_type', 'bug_status'],
props: {
defectId: {
type: Number,
default: 0,
},
managerName: {
type: String,
default: ''
},
managerList: {
type: Array,
default: () => []
}
},
data() {
return {
reappearanceOptions: [{
value: '0',
label: '必然复现'
}, {
value: '1',
label: '偶发复现'
}],
managerList: [],
detailForm: {
outline: '',
detail: '',
serialNumber: '',
name: '',
manager: '',
level: '',
type: '',
status: '',
reappearance: '',
dev: '',
test: '',
version: '',
},
}
},
computed: {
reappearanceLabel() {
const option = this.reappearanceOptions.find(
(opt) => opt.value === this.detailForm.reappearance
);
return option ? option.label : '';
},
},
watch: {
defectId(newVal){
this.getDetail()
}
},
methods: {
getDetail() {
getBugDetail(this.defectId).then(res => {
this.detailForm = res.data;
this.detailForm.level = this.dict.type.severity_level.find(e => e.value === res.data.level).label;
this.detailForm.type = this.dict.type.bug_type.find(e => e.value === res.data.type).label;
this.detailForm.status = this.dict.type.bug_status.find(e => e.value === res.data.status).label;
this.detailForm.dev = this.managerList.find(e => e.value === parseInt(res.data.dev,10)).label;
this.detailForm.test = this.managerList.find(e => e.value === parseInt(res.data.test,10)).label;
})
}
}
}
</script>
<style scoped lang="scss">
.basic-information {
background-color: rgb(248, 248, 249) !important;
}
</style>

View File

@@ -0,0 +1,65 @@
<template>
<div class="app-container">
<el-table v-loading="loading" :data="list" height="500">
<el-table-column prop="statusLabel" label="状态" width="180"></el-table-column>
<el-table-column prop="name" label="计划名称"></el-table-column>
<el-table-column prop="manager" label="负责人"></el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList"></pagination>
</div>
</template>
<script>
import {getDefectPlanList} from "@/api/test/bug";
export default {
name: 'defectPlan',
dicts: ['status'],
props: {
defectId: {
type: Number,
default: '',
},
},
data() {
return {
// 遮罩层
loading: true,
// 总条数
total: 0,
// 表格数据
list: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10
}
}
},
watch: {
defectId(newVal){
this.getList()
}
},
methods: {
getList() {
this.loading = true;
getDefectPlanList(this.defectId).then(response => {
this.list = response.rows.map(item => {
const matched = this.dict.type.status.find(e => e.value === item.status);
return {
...item,
statusLabel: matched ? matched.label : '未知状态'
};
});
this.total = response.total;
this.loading = false;
})
},
}
}
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,74 @@
<template>
<div class="app-container">
<el-Table v-loading="loading" :data="list" height="500">
<el-table-column prop="serialNumber" label="ID" align="center"/>
<el-table-column prop="version" label="版本" align="center"/>
<el-table-column prop="outline" label="概要" align="center"/>
<el-table-column prop="priority" label="优先级" align="center">
<template slot-scope="scope">
<el-tag :type="priorityColor[scope.row.priority]">{{ scope.row.priority }}</el-tag>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" align="center">
<template #default="{ row }">
<dict-tag :options="dict.type.status" :value="row.status"/>
</template>
</el-table-column>
<el-table-column prop="manager" label="负责人" align="center"/>
<el-table-column prop="createTime" label="创建时间" align="center"/>
</el-Table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList"/>
</div>
</template>
<script>
import {getDefectProjectList} from "@/api/test/bug";
export default {
name: 'defectProject',
dicts: ['priority_level', 'project_source', 'project_type', 'status'],
props: {
defectId: {
type: Number,
default: '',
},
},
data() {
return {
list: [],
loading: false,
total: 0,
priorityColor: {
'P0': 'danger',
'P1': 'warning',
'P2': 'info',
'P3': 'success',
},
queryParams: {
pageNum: 1,
pageSize: 10,
}
}
},
watch: {
defectId(newVal){
this.getList()
}
},
methods: {
getList() {
this.loading = true
getDefectProjectList(this.defectId).then(response => {
this.list = response.rows
this.total = response.total
this.loading = false
})
}
}
}
</script>
<style scoped lang="scss">
</style>

View File

@@ -62,7 +62,7 @@
</el-collapse> </el-collapse>
<!-- 标签页 --> <!-- 标签页 -->
<el-Table v-loading="loading" :data="list" @selection-change="handleSelectionChange"> <el-Table v-loading="loading" :data="list" @selection-change="handleSelectionChange" @row-click="handleRowClick">
<el-table-column type="selection"/> <el-table-column type="selection"/>
<el-table-column prop="serialNumber" label="ID" align="center"/> <el-table-column prop="serialNumber" label="ID" align="center"/>
<el-table-column prop="outline" label="概要" align="center"/> <el-table-column prop="outline" label="概要" align="center"/>
@@ -80,7 +80,7 @@
<el-table-column prop="createTime" label="创建时间" align="center"/> <el-table-column prop="createTime" label="创建时间" align="center"/>
<el-table-column label="操作" align="left" fixed="right"> <el-table-column label="操作" align="left" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row.id)">编辑</el-button> <el-button size="mini" type="text" icon="el-icon-edit" @click.native.stop="handleEdit(scope.row.id)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click.native.stop="handleDelete(scope.row.id)">删除 <el-button size="mini" type="text" icon="el-icon-delete" @click.native.stop="handleDelete(scope.row.id)">删除
</el-button> </el-button>
</template> </template>
@@ -237,6 +237,25 @@
<el-button type="primary" @click="editSubmitForm"> </el-button> <el-button type="primary" @click="editSubmitForm"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
<el-dialog :visible.sync="detailOpen" width="90%">
<template #title>
<div class="detail-title">
<el-row>
<span>查看缺陷-{{ title }}</span>
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect">
<el-menu-item index="1">缺陷详情</el-menu-item>
<el-menu-item index="2">缺陷动态</el-menu-item>
<el-menu-item index="3">已关联测试计划</el-menu-item>
<el-menu-item index="4">已关联需求</el-menu-item>
</el-menu>
</el-row>
<bug-detail :managerList="managerList" :managerName="managerName" :defectId="defectId" v-show="activeIndex === '1'"></bug-detail>
<defect-plan :defectId="defectId" v-show="activeIndex === '3'"></defect-plan>
<defect-project :defectId="defectId" v-show="activeIndex === '4'"></defect-project>
</div>
</template>
</el-dialog>
</div> </div>
</template> </template>
@@ -245,10 +264,13 @@ import {managerList} from "@/api/test/project";
import {addBug, delBug, getBugDetail, getBugList, updateBug} from "@/api/test/bug"; import {addBug, delBug, getBugDetail, getBugList, updateBug} from "@/api/test/bug";
import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue"; import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue";
import {requestDownload} from "@/utils/request"; import {requestDownload} from "@/utils/request";
import BugDetail from "@/views/test/bug/components/bugDetail.vue";
import DefectPlan from "@/views/test/bug/components/defectPlan.vue";
import DefectProject from "@/views/test/bug/components/defectProject.vue";
export default { export default {
name: 'defect', name: 'defect',
components: {SimpleOptions}, components: {DefectProject, DefectPlan, BugDetail, SimpleOptions},
dicts: ['severity_level', 'bug_type', 'bug_status'], dicts: ['severity_level', 'bug_type', 'bug_status'],
data() { data() {
return { return {
@@ -294,6 +316,10 @@ export default {
addOpen: false, addOpen: false,
//编辑弹窗 //编辑弹窗
editOpen: false, editOpen: false,
detailOpen: false,
activeIndex: '1',
defectId: 0,
managerName: '',
selectedRows: [], selectedRows: [],
selectedData: [], selectedData: [],
managerList: [], managerList: [],
@@ -336,6 +362,16 @@ export default {
}, },
}, },
methods: { methods: {
handleRowClick(row) {
this.defectId = row.id
this.activeIndex = '1'
this.managerName = row.manager
this.detailOpen = true;
this.title = row.serialNumber;
},
handleSelect(key) {
this.activeIndex = key
},
handleSelectionChange(selection) { handleSelectionChange(selection) {
this.selectedRows = selection; this.selectedRows = selection;
}, },
@@ -437,6 +473,8 @@ export default {
getBugDetail(id).then((res) => { getBugDetail(id).then((res) => {
this.editForm = res.data this.editForm = res.data
this.editForm.manager = parseInt(res.data.manager, 10); this.editForm.manager = parseInt(res.data.manager, 10);
this.editForm.dev = parseInt(res.data.dev, 10);
this.editForm.test = parseInt(res.data.test, 10);
this.editOpen = true this.editOpen = true
}) })
}, },

View File

@@ -57,6 +57,9 @@ export default {
mounted() { mounted() {
this.getPerformaceData() this.getPerformaceData()
}, },
activated() {
this.getPerformaceData()
},
methods: { methods: {
// 创建测试 // 创建测试
addTest() { addTest() {
@@ -68,7 +71,7 @@ export default {
}, },
// 编辑 // 编辑
hadleClickEdit(val) { hadleClickEdit(val) {
this.$tab.openPage("修改测试", "/performance/edit", { id: val.id }); this.$tab.openPage(`修改测试_${val.id}`, `/performance/edit/${val.id}`, { id: val.id });
}, },
// 删除 // 删除
hadleClickDelete(val) { hadleClickDelete(val) {

View File

@@ -14,10 +14,10 @@
<div class="date-top"> <div class="date-top">
<i class="el-icon-date"></i> <i class="el-icon-date"></i>
<span class="date-title">SCHEDULER</span> <span class="date-title">SCHEDULER</span>
<el-switch v-model="switchOpen" @change="switchChange"></el-switch> <el-switch v-model="addForm.crontabStatus" :active-value="1" :inactive-value="0" @change="handleSwitchChange"></el-switch>
</div> </div>
<div class="date-bottom"> <div class="date-bottom" v-if="addForm.crontabStatus === 1">
<span>下次执行时间</span> <span>下次执行时间{{ nextExecutionTime }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -31,7 +31,7 @@
<el-table-column prop="name" label="场景名称" /> <el-table-column prop="name" label="场景名称" />
<el-table-column prop="status" label="Enable/Disable"> <el-table-column prop="status" label="Enable/Disable">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch v-model="scope.row.status" active-value="1" inactive-value="0"></el-switch> <el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0"></el-switch>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="action" label="操作"> <el-table-column prop="action" label="操作">
@@ -44,7 +44,7 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="压力配置" name="second"> <el-tab-pane label="压力配置" name="second">
<div class="pressure-header"> <div class="pressure-header">
<div class="title">速兑通接口</div> <div class="title">{{ addForm.performanceName }}</div>
<div class="pressure-title">并发用户数{{ addForm.concurrentThreads }}压测时长{{ addForm.pressureHour }}{{ <div class="pressure-title">并发用户数{{ addForm.concurrentThreads }}压测时长{{ addForm.pressureHour }}{{
addForm.pressureMinute }}{{ addForm.pressureSecond }}</div> addForm.pressureMinute }}{{ addForm.pressureSecond }}</div>
</div> </div>
@@ -124,8 +124,6 @@
<div class="crontab-wrap"> <div class="crontab-wrap">
<div class="title">Crontab表达式</div> <div class="title">Crontab表达式</div>
<el-input v-model="addForm.crontab" @input="updateExecutionTimes" placeholder="请输入crontab表达式" /> <el-input v-model="addForm.crontab" @input="updateExecutionTimes" placeholder="请输入crontab表达式" />
<div class="title" style="margin-left: 50px;">定时任务开关</div>
<el-switch v-model="addForm.crontabStatus" active-value="1" inactive-value="0"></el-switch>
</div> </div>
<div class="near-time"> <div class="near-time">
<div class="title">最近5次运行时间</div> <div class="title">最近5次运行时间</div>
@@ -135,8 +133,8 @@
</div> </div>
</el-tabs> </el-tabs>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="dialogVisibleTime = false, switchOpen = false"> </el-button> <el-button @click="dialogVisibleTime = false, addForm.crontabStatus = false"> </el-button>
<el-button type="primary" @click="dialogVisibleTime = false"> </el-button> <el-button type="primary" @click="handleDialogConfirm"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
</div> </div>
@@ -144,6 +142,7 @@
<script> <script>
import { addTest, addAndExecuteTest, getTestCaseList } from '../../../api/performance'; import { addTest, addAndExecuteTest, getTestCaseList } from '../../../api/performance';
import parser from 'cron-parser'; // 添加 cron-parser 依赖
export default { export default {
name: "PerformanceAdd", name: "PerformanceAdd",
@@ -162,7 +161,7 @@ export default {
rpsStatus: '0', // rps状态0关闭1开启默认0 rpsStatus: '0', // rps状态0关闭1开启默认0
rpsLimit: '0', // 每分钟rps上限数默认0 rpsLimit: '0', // 每分钟rps上限数默认0
crontab: '', // crontab表达式 crontab: '', // crontab表达式
crontabStatus: '0', // 定时任务状态0关闭1开启默认0 crontabStatus: 0, // 定时任务状态0关闭1开启默认0
loopCount: '0', // 迭代次数默认0 loopCount: '0', // 迭代次数默认0
}, },
activeName: 'first', activeName: 'first',
@@ -177,26 +176,10 @@ export default {
searchScene: '', searchScene: '',
changeList: [], changeList: [],
multipleSelection: [], multipleSelection: [],
switchOpen: false,
dialogVisibleTime: false, dialogVisibleTime: false,
activeTime: 'first', activeTime: 'first',
executionTimeList: [ executionTimeList: [], // 修改为空数组
{ nextExecutionTime: '', // 添加下次执行时间
time: "2025-02-18 10:00:00",
},
{
time: "2025-02-19 10:00:00",
},
{
time: "2025-02-20 10:00:00",
},
{
time: "2025-02-21 10:00:00",
},
{
time: "2025-02-22 10:00:00",
},
],
validation: false, // 校验 validation: false, // 校验
} }
}, },
@@ -227,6 +210,29 @@ export default {
handleWithSave() { handleWithSave() {
this.validationForm() this.validationForm()
if (this.validation === false) { return } if (this.validation === false) { return }
// 如果开启了定时任务,验证 crontab 表达式
if (this.addForm.crontabStatus === 1) {
const crontab = this.addForm.crontab.trim();
if (!crontab) {
this.$message({ message: '请输入 Crontab 表达式', type: 'warning' })
return
}
// 验证 crontab 表达式格式
if (!this.validateCrontab(crontab)) {
this.$message.error('无效的 crontab 表达式格式,请输入正确的 crontab 表达式');
return;
}
try {
parser.parseExpression(crontab);
} catch (error) {
this.$message.error('无效的 crontab 表达式,请修正后再保存');
return;
}
}
this.changeList.forEach(item => { this.changeList.forEach(item => {
var par = { var par = {
testCaseId: item.id, testCaseId: item.id,
@@ -313,37 +319,130 @@ export default {
}) })
this.dialogVisible = false this.dialogVisible = false
}, },
switchChange(val) {
this.dialogVisibleTime = val
},
handleClose() { handleClose() {
this.dialogVisibleTime = false this.dialogVisibleTime = false
this.switchOpen = false this.addForm.crontabStatus = false
}, },
// 根据crontab表达式更新执行时间 // 添加 crontab 表达式验证函数
validateCrontab(crontab) {
// crontab 表达式必须包含 5 个或 6 个空格分隔的字段
const fields = crontab.trim().split(/\s+/);
if (fields.length < 5 || fields.length > 6) {
return false;
}
// 验证每个字段的格式
const patterns = {
seconds: /^(\*|([0-9]|[1-5][0-9])(,[0-9]|[1-5][0-9])*|\*\/[1-9][0-9]?|[0-9]|[1-5][0-9]\/[1-9][0-9]?)$/,
minutes: /^(\*|([0-9]|[1-5][0-9])(,[0-9]|[1-5][0-9])*|\*\/[1-9][0-9]?|[0-9]|[1-5][0-9]\/[1-9][0-9]?)$/,
hours: /^(\*|([0-9]|1[0-9]|2[0-3])(,[0-9]|1[0-9]|2[0-3])*|\*\/[1-9][0-9]?|[0-9]|1[0-9]|2[0-3]\/[1-9][0-9]?)$/,
dayOfMonth: /^(\*|([1-9]|[12][0-9]|3[01])(,[1-9]|[12][0-9]|3[01])*|\*\/[1-9][0-9]?|[1-9]|[12][0-9]|3[01]\/[1-9][0-9]?|\?|L|W|LW|1W|2W|3W|4W|5W|6W|7W|8W|9W|10W|11W|12W|13W|14W|15W|16W|17W|18W|19W|20W|21W|22W|23W|24W|25W|26W|27W|28W|29W|30W|31W)$/,
month: /^(\*|([1-9]|1[0-2])(,[1-9]|1[0-2])*|\*\/[1-9][0-9]?|[1-9]|1[0-2]\/[1-9][0-9]?|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)$/,
dayOfWeek: /^(\*|([0-6]|SUN|MON|TUE|WED|THU|FRI|SAT)(,[0-6]|SUN|MON|TUE|WED|THU|FRI|SAT)*|\*\/[1-9][0-9]?|[0-6]\/[1-9][0-9]?|\?|L|6L|5L|4L|3L|2L|1L|1#1|1#2|1#3|1#4|1#5|2#1|2#2|2#3|2#4|2#5|3#1|3#2|3#3|3#4|3#5|4#1|4#2|4#3|4#4|4#5|5#1|5#2|5#3|5#4|5#5|6#1|6#2|6#3|6#4|6#5|7#1|7#2|7#3|7#4|7#5)$/
};
// 根据字段数量确定验证顺序
const validationOrder = fields.length === 6
? ['seconds', 'minutes', 'hours', 'dayOfMonth', 'month', 'dayOfWeek']
: ['minutes', 'hours', 'dayOfMonth', 'month', 'dayOfWeek'];
// 验证每个字段
const isValid = fields.every((field, index) => {
const pattern = patterns[validationOrder[index]];
return pattern && pattern.test(field);
});
if (!isValid) {
return false;
}
// 检查 dayOfMonth 和 dayOfWeek 不能同时为具体值
const dayOfMonthIndex = fields.length === 6 ? 3 : 2;
const dayOfWeekIndex = fields.length === 6 ? 5 : 4;
const dayOfMonth = fields[dayOfMonthIndex];
const dayOfWeek = fields[dayOfWeekIndex];
if (dayOfMonth !== '*' && dayOfWeek !== '*' && dayOfMonth !== '?' && dayOfWeek !== '?') {
return false;
}
return true;
},
// 更新执行时间列表
updateExecutionTimes() { updateExecutionTimes() {
const crontab = this.addForm.crontab.trim(); const crontab = this.addForm.crontab.trim();
if (!crontab) { if (!crontab) {
this.executionTimeList = []; this.executionTimeList = [];
this.nextExecutionTime = '';
return;
}
// 首先验证 crontab 表达式格式
if (!this.validateCrontab(crontab)) {
this.$message.error('无效的 crontab 表达式格式');
this.executionTimeList = [];
this.nextExecutionTime = '';
return; return;
} }
try { try {
// 解析crontab表达式 // 解析crontab表达式
const interval = cronParser.parseExpression(crontab); const interval = parser.parseExpression(crontab);
const times = []; const times = [];
// 获取最近次执行时间 // 获取最近5次执行时间
for (let i = 0; i < 3; i++) { for (let i = 0; i < 5; i++) {
const nextTime = interval.next().toDate(); // 获取 Date 对象 const nextTime = interval.next().toDate();
const formattedTime = this.formatDate(nextTime); // 格式化时间 times.push({
times.push({ time: formattedTime }); time: this.formatDate(nextTime)
});
} }
this.executionTimeList = times; this.executionTimeList = times;
this.nextExecutionTime = times[0].time; // 设置下次执行时间
} catch (error) { } catch (error) {
console.error('无效的crontab表达式:', error); this.$message.error('无效的 crontab 表达式');
this.executionTimeList = [{ time: '无效的crontab表达式' }]; this.executionTimeList = [];
this.nextExecutionTime = '';
}
},
// 格式化日期
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const second = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
},
// 修改确定按钮的处理函数
handleDialogConfirm() {
const crontab = this.addForm.crontab.trim();
if (!crontab) {
this.$message.warning('请输入 Crontab 表达式');
return;
}
// 首先验证 crontab 表达式格式
if (!this.validateCrontab(crontab)) {
this.$message.error('无效的 crontab 表达式格式,请输入正确的 crontab 表达式');
return;
}
try {
parser.parseExpression(crontab);
this.updateExecutionTimes();
this.dialogVisibleTime = false;
} catch (error) {
this.$message.error('无效的 crontab 表达式,请修正后再确定');
return;
}
},
// 添加开关变化处理函数
handleSwitchChange(val) {
if (val === 1) {
this.dialogVisibleTime = true;
} }
}, },
} }

View File

@@ -5,7 +5,7 @@
<div class="name-wrap">测试名称</div> <div class="name-wrap">测试名称</div>
<el-input v-model="addForm.performanceName" placeholder="请输入名称" maxlength="255" show-word-limit></el-input> <el-input v-model="addForm.performanceName" placeholder="请输入名称" maxlength="255" show-word-limit></el-input>
</div> </div>
<div class="save"> <div class="save" v-loading="loading">
<el-button type="primary" plain @click="handleWithSave">保存</el-button> <el-button type="primary" plain @click="handleWithSave">保存</el-button>
<el-button type="primary" plain @click="handleWithSaveAndExecute">保存并执行</el-button> <el-button type="primary" plain @click="handleWithSaveAndExecute">保存并执行</el-button>
<el-button type="primary" plain @click="handleWithExecute">立即执行</el-button> <el-button type="primary" plain @click="handleWithExecute">立即执行</el-button>
@@ -15,10 +15,10 @@
<div class="date-top"> <div class="date-top">
<i class="el-icon-date"></i> <i class="el-icon-date"></i>
<span class="date-title">SCHEDULER</span> <span class="date-title">SCHEDULER</span>
<el-switch v-model="switchOpen" @change="switchChange"></el-switch> <el-switch v-model="addForm.crontabStatus" :active-value="1" :inactive-value="0" @change="handleSwitchChange"></el-switch>
</div> </div>
<div class="date-bottom"> <div class="date-bottom" v-if="addForm.crontabStatus === 1">
<span>下次执行时间</span> <span>下次执行时间{{ nextExecutionTime }}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -32,7 +32,7 @@
<el-table-column prop="testCaseName" label="场景名称" /> <el-table-column prop="testCaseName" label="场景名称" />
<el-table-column prop="status" label="Enable/Disable"> <el-table-column prop="status" label="Enable/Disable">
<template slot-scope="scope"> <template slot-scope="scope">
<el-switch v-model="scope.row.status" active-value="1" inactive-value="0"></el-switch> <el-switch v-model="scope.row.status" :active-value="1" :inactive-value="0"></el-switch>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="action" label="操作"> <el-table-column prop="action" label="操作">
@@ -45,7 +45,7 @@
</el-tab-pane> </el-tab-pane>
<el-tab-pane label="压力配置" name="second"> <el-tab-pane label="压力配置" name="second">
<div class="pressure-header"> <div class="pressure-header">
<div class="title">速兑通接口</div> <div class="title">{{ addForm.performanceName }}</div>
<div class="pressure-title">并发用户数{{ addForm.concurrentThreads }}压测时长{{ addForm.pressureHour }}{{ <div class="pressure-title">并发用户数{{ addForm.concurrentThreads }}压测时长{{ addForm.pressureHour }}{{
addForm.pressureMinute }}{{ addForm.pressureSecond }}</div> addForm.pressureMinute }}{{ addForm.pressureSecond }}</div>
</div> </div>
@@ -125,8 +125,6 @@
<div class="crontab-wrap"> <div class="crontab-wrap">
<div class="title">Crontab表达式</div> <div class="title">Crontab表达式</div>
<el-input v-model="addForm.crontab" @input="updateExecutionTimes" placeholder="请输入crontab表达式" /> <el-input v-model="addForm.crontab" @input="updateExecutionTimes" placeholder="请输入crontab表达式" />
<div class="title" style="margin-left: 50px;">定时任务开关</div>
<el-switch v-model="addForm.crontabStatus" active-value="1" inactive-value="0"></el-switch>
</div> </div>
<div class="near-time"> <div class="near-time">
<div class="title">最近5次运行时间</div> <div class="title">最近5次运行时间</div>
@@ -136,8 +134,8 @@
</div> </div>
</el-tabs> </el-tabs>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="dialogVisibleTime = false, switchOpen = false"> </el-button> <el-button @click="dialogVisibleTime = false, addForm.crontabStatus = 0"> </el-button>
<el-button type="primary" @click="dialogVisibleTime = false"> </el-button> <el-button type="primary" @click="handleDialogConfirm"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
</div> </div>
@@ -145,6 +143,8 @@
<script> <script>
import { editTest, editAndExecuteTest, getTestCaseList, getTestDetail, executeTest } from '../../../api/performance'; import { editTest, editAndExecuteTest, getTestCaseList, getTestDetail, executeTest } from '../../../api/performance';
import {runTestPlanCase} from "../../../api/test/planCase";
import parser from 'cron-parser'; // 添加 cron-parser 依赖
export default { export default {
name: "PerformanceEdit", name: "PerformanceEdit",
@@ -163,9 +163,10 @@ export default {
rpsStatus: '0', // rps状态0关闭1开启默认0 rpsStatus: '0', // rps状态0关闭1开启默认0
rpsLimit: '0', // 每分钟rps上限数默认0 rpsLimit: '0', // 每分钟rps上限数默认0
crontab: '', // crontab表达式 crontab: '', // crontab表达式
crontabStatus: '0', // 定时任务状态0关闭1开启默认0 crontabStatus: 0, // 定时任务状态0关闭1开启默认0
loopCount: '0', // 迭代次数默认0 loopCount: '0', // 迭代次数默认0
}, },
loading: false,
activeName: 'first', activeName: 'first',
sceneList: [], // 场景列表 sceneList: [], // 场景列表
dialogVisible: false, // 场景列表 dialogVisible: false, // 场景列表
@@ -178,26 +179,10 @@ export default {
searchScene: '', searchScene: '',
changeList: [], changeList: [],
multipleSelection: [], multipleSelection: [],
switchOpen: false,
dialogVisibleTime: false, dialogVisibleTime: false,
activeTime: 'first', activeTime: 'first',
executionTimeList: [ executionTimeList: [], // 修改为空数组
{ nextExecutionTime: '', // 添加下次执行时间
time: "2025-02-18 10:00:00",
},
{
time: "2025-02-19 10:00:00",
},
{
time: "2025-02-20 10:00:00",
},
{
time: "2025-02-21 10:00:00",
},
{
time: "2025-02-22 10:00:00",
},
],
validation: false, // 校验 validation: false, // 校验
} }
}, },
@@ -205,6 +190,25 @@ export default {
this.getTestCaseData() this.getTestCaseData()
this.getTestDetailData() this.getTestDetailData()
}, },
watch: {
// 监听 crontab 和 crontabStatus 的变化
'addForm.crontab': {
handler(newVal) {
if (newVal && this.addForm.crontabStatus === 1) {
this.updateExecutionTimes();
}
}
},
'addForm.crontabStatus': {
handler(newVal) {
if (newVal === 1 && this.addForm.crontab) {
this.updateExecutionTimes();
} else if (newVal === 0) {
this.nextExecutionTime = '';
}
}
}
},
methods: { methods: {
// 校验 // 校验
validationForm() { validationForm() {
@@ -229,6 +233,29 @@ export default {
handleWithSave() { handleWithSave() {
this.validationForm() this.validationForm()
if (this.validation === false) { return } if (this.validation === false) { return }
// 如果开启了定时任务,验证 crontab 表达式
if (this.addForm.crontabStatus === 1) {
const crontab = this.addForm.crontab.trim();
if (!crontab) {
this.$message({ message: '请输入 Crontab 表达式', type: 'warning' })
return
}
// 验证 crontab 表达式格式
if (!this.validateCrontab(crontab)) {
this.$message.error('无效的 crontab 表达式格式,请输入正确的 crontab 表达式');
return;
}
try {
parser.parseExpression(crontab);
} catch (error) {
this.$message.error('无效的 crontab 表达式,请修正后再保存');
return;
}
}
this.addForm.performanceTestCaseVOList = this.changeList this.addForm.performanceTestCaseVOList = this.changeList
editTest(this.addForm).then(res => { editTest(this.addForm).then(res => {
if (res.code === 200) { if (res.code === 200) {
@@ -247,6 +274,7 @@ export default {
this.$message({ message: '请输入Crontab表达式', type: 'warning' }) this.$message({ message: '请输入Crontab表达式', type: 'warning' })
return return
} }
this.loading = true;
editAndExecuteTest(this.addForm).then(res => { editAndExecuteTest(this.addForm).then(res => {
if (res.code === 200) { if (res.code === 200) {
this.$message({ message: '编辑成功', type: 'success' }) this.$message({ message: '编辑成功', type: 'success' })
@@ -254,18 +282,24 @@ export default {
} else { } else {
this.$message({ message: '编辑失败', type: 'error' }) this.$message({ message: '编辑失败', type: 'error' })
} }
}).finally(() => {
this.loading = false;
}) })
}, },
// 立即执行 // 立即执行
handleWithExecute() { handleWithExecute() {
executeTest(this.$route.query.id).then(res => { const myId = this.$route.query.id;
this.$modal.confirm('是否确认立即执行性能测试?').then(function () {
return executeTest(myId);
}).then((res) => {
if (res.code === 200) { if (res.code === 200) {
this.$message({ message: '执行成功', type: 'success' }) this.$modal.msgSuccess("提交执行成功");
this.$tab.closeOpenPage({ path: "/performance/performance" });
} else { } else {
this.$message({ message: '执行失败', type: 'error' }) this.$message({ message: '提交执行失败', type: 'error' })
} }
}) }).finally(() => {
this.loading = false;
});
}, },
// 取消 // 取消
handleWithCancel() { handleWithCancel() {
@@ -314,18 +348,11 @@ export default {
getTestDetail(this.$route.query.id).then(res => { getTestDetail(this.$route.query.id).then(res => {
if (res.code === 200) { if (res.code === 200) {
this.addForm = res.data this.addForm = res.data
res.data.performanceTestCaseVOList.forEach(item => {
item.status = String(item.status)
})
this.changeList = res.data.performanceTestCaseVOList this.changeList = res.data.performanceTestCaseVOList
this.sceneList.forEach(row => { // 如果有定时任务,更新下次执行时间
this.changeList.forEach(item => { if (this.addForm.crontabStatus === 1 && this.addForm.crontab) {
if (row.id === item.testCaseId) { this.updateExecutionTimes();
this.multipleSelection.push(row)
} }
})
})
this.addForm.rpsStatus = String(res.data.rpsStatus)
} }
}) })
}, },
@@ -357,37 +384,130 @@ export default {
this.dialogVisible = false this.dialogVisible = false
}, },
switchChange(val) {
this.dialogVisibleTime = val
},
handleClose() { handleClose() {
this.dialogVisibleTime = false this.dialogVisibleTime = false
this.switchOpen = false this.addForm.crontabStatus = 0
}, },
// 根据crontab表达式更新执行时间 // 添加 crontab 表达式验证函数
validateCrontab(crontab) {
// crontab 表达式必须包含 5 个或 6 个空格分隔的字段
const fields = crontab.trim().split(/\s+/);
if (fields.length < 5 || fields.length > 6) {
return false;
}
// 验证每个字段的格式
const patterns = {
seconds: /^(\*|([0-9]|[1-5][0-9])(,[0-9]|[1-5][0-9])*|\*\/[1-9][0-9]?|[0-9]|[1-5][0-9]\/[1-9][0-9]?)$/,
minutes: /^(\*|([0-9]|[1-5][0-9])(,[0-9]|[1-5][0-9])*|\*\/[1-9][0-9]?|[0-9]|[1-5][0-9]\/[1-9][0-9]?)$/,
hours: /^(\*|([0-9]|1[0-9]|2[0-3])(,[0-9]|1[0-9]|2[0-3])*|\*\/[1-9][0-9]?|[0-9]|1[0-9]|2[0-3]\/[1-9][0-9]?)$/,
dayOfMonth: /^(\*|([1-9]|[12][0-9]|3[01])(,[1-9]|[12][0-9]|3[01])*|\*\/[1-9][0-9]?|[1-9]|[12][0-9]|3[01]\/[1-9][0-9]?|\?|L|W|LW|1W|2W|3W|4W|5W|6W|7W|8W|9W|10W|11W|12W|13W|14W|15W|16W|17W|18W|19W|20W|21W|22W|23W|24W|25W|26W|27W|28W|29W|30W|31W)$/,
month: /^(\*|([1-9]|1[0-2])(,[1-9]|1[0-2])*|\*\/[1-9][0-9]?|[1-9]|1[0-2]\/[1-9][0-9]?|JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)$/,
dayOfWeek: /^(\*|([0-6]|SUN|MON|TUE|WED|THU|FRI|SAT)(,[0-6]|SUN|MON|TUE|WED|THU|FRI|SAT)*|\*\/[1-9][0-9]?|[0-6]\/[1-9][0-9]?|\?|L|6L|5L|4L|3L|2L|1L|1#1|1#2|1#3|1#4|1#5|2#1|2#2|2#3|2#4|2#5|3#1|3#2|3#3|3#4|3#5|4#1|4#2|4#3|4#4|4#5|5#1|5#2|5#3|5#4|5#5|6#1|6#2|6#3|6#4|6#5|7#1|7#2|7#3|7#4|7#5)$/
};
// 根据字段数量确定验证顺序
const validationOrder = fields.length === 6
? ['seconds', 'minutes', 'hours', 'dayOfMonth', 'month', 'dayOfWeek']
: ['minutes', 'hours', 'dayOfMonth', 'month', 'dayOfWeek'];
// 验证每个字段
const isValid = fields.every((field, index) => {
const pattern = patterns[validationOrder[index]];
return pattern && pattern.test(field);
});
if (!isValid) {
return false;
}
// 检查 dayOfMonth 和 dayOfWeek 不能同时为具体值
const dayOfMonthIndex = fields.length === 6 ? 3 : 2;
const dayOfWeekIndex = fields.length === 6 ? 5 : 4;
const dayOfMonth = fields[dayOfMonthIndex];
const dayOfWeek = fields[dayOfWeekIndex];
if (dayOfMonth !== '*' && dayOfWeek !== '*' && dayOfMonth !== '?' && dayOfWeek !== '?') {
return false;
}
return true;
},
// 更新执行时间列表
updateExecutionTimes() { updateExecutionTimes() {
const crontab = this.form.crontab.trim(); const crontab = this.addForm.crontab.trim();
if (!crontab) { if (!crontab) {
this.executionTimeList = []; this.executionTimeList = [];
this.nextExecutionTime = '';
return;
}
// 首先验证 crontab 表达式格式
if (!this.validateCrontab(crontab)) {
this.$message.error('无效的 crontab 表达式格式');
this.executionTimeList = [];
this.nextExecutionTime = '';
return; return;
} }
try { try {
// 解析crontab表达式 // 解析crontab表达式
const interval = cronParser.parseExpression(crontab); const interval = parser.parseExpression(crontab);
const times = []; const times = [];
// 获取最近次执行时间 // 获取最近5次执行时间
for (let i = 0; i < 3; i++) { for (let i = 0; i < 5; i++) {
const nextTime = interval.next().toDate(); // 获取 Date 对象 const nextTime = interval.next().toDate();
const formattedTime = this.formatDate(nextTime); // 格式化时间 times.push({
times.push({ time: formattedTime }); time: this.formatDate(nextTime)
});
} }
this.executionTimeList = times; this.executionTimeList = times;
this.nextExecutionTime = times[0].time; // 设置下次执行时间
} catch (error) { } catch (error) {
console.error('无效的crontab表达式:', error); this.$message.error('无效的 crontab 表达式');
this.executionTimeList = [{ time: '无效的crontab表达式' }]; this.executionTimeList = [];
this.nextExecutionTime = '';
}
},
// 格式化日期
formatDate(date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
const hour = String(date.getHours()).padStart(2, '0');
const minute = String(date.getMinutes()).padStart(2, '0');
const second = String(date.getSeconds()).padStart(2, '0');
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
},
// 修改确定按钮的处理函数
handleDialogConfirm() {
const crontab = this.addForm.crontab.trim();
if (!crontab) {
this.$message.warning('请输入 Crontab 表达式');
return;
}
// 首先验证 crontab 表达式格式
if (!this.validateCrontab(crontab)) {
this.$message.error('无效的 crontab 表达式格式,请输入正确的 crontab 表达式');
return;
}
try {
parser.parseExpression(crontab);
this.updateExecutionTimes();
this.dialogVisibleTime = false;
} catch (error) {
this.$message.error('无效的 crontab 表达式,请修正后再确定');
return;
}
},
// 添加开关变化处理函数
handleSwitchChange(val) {
if (val === 1) {
this.dialogVisibleTime = true;
} }
}, },
} }

View File

@@ -17,8 +17,8 @@
<el-table-column prop="endTime" label="结束时间" align="center" sortable /> <el-table-column prop="endTime" label="结束时间" align="center" sortable />
<el-table-column prop="triggerType" label="触发方式" align="center" sortable> <el-table-column prop="triggerType" label="触发方式" align="center" sortable>
<template slot-scope="scope"> <template slot-scope="scope">
<div v-if="scope.row.triggerType === 1">按持续时间</div> <div v-if="scope.row.triggerType === 1">定时任务</div>
<div v-else>按迭代次数</div> <div v-else>手动</div>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column prop="status" label="状态" align="center" width="150" sortable> <el-table-column prop="status" label="状态" align="center" width="150" sortable>
@@ -65,7 +65,7 @@ export default {
methods: { methods: {
// 查看 // 查看
hadleClickDetail(val) { hadleClickDetail(val) {
this.$tab.openPage("报告详情", "/performance/report/detail", { id: val.id }); this.$tab.openPage(`报告详情_${val.id}`, `/performance/report/detail/${val.id}`, { id: val.id });
}, },
// 删除 // 删除
hadleClickDelete(val) { hadleClickDelete(val) {
@@ -86,8 +86,14 @@ export default {
}) })
}, },
// 分页 // 分页
handleSizeChange() { }, handleSizeChange(val) {
handleCurrentChange() { }, this.serachForm.pageSize = val;
this.getReportListData();
},
handleCurrentChange(val) {
this.serachForm.pageNum = val;
this.getReportListData();
},
serachList() { serachList() {
this.getReportListData() this.getReportListData()
}, },

View File

@@ -71,7 +71,7 @@
<el-tab-pane :label="'已完成(' + statusCounts.completed + ')'" name="2"></el-tab-pane> <el-tab-pane :label="'已完成(' + statusCounts.completed + ')'" name="2"></el-tab-pane>
<el-tab-pane :label="'已终止(' + statusCounts.terminated + ')'" name="3"></el-tab-pane> <el-tab-pane :label="'已终止(' + statusCounts.terminated + ')'" name="3"></el-tab-pane>
<el-Table v-loading="loading" :data="list" @selection-change="handleSelectionChange"> <el-Table v-loading="loading" :data="list" @selection-change="handleSelectionChange" @row-click="handleRowClick">
<el-table-column type="selection"/> <el-table-column type="selection"/>
<el-table-column prop="serialNumber" label="ID" align="center"/> <el-table-column prop="serialNumber" label="ID" align="center"/>
<el-table-column prop="version" label="版本" align="center"/> <el-table-column prop="version" label="版本" align="center"/>
@@ -90,7 +90,7 @@
<el-table-column prop="createTime" label="创建时间" align="center"/> <el-table-column prop="createTime" label="创建时间" align="center"/>
<el-table-column label="操作" align="left" fixed="right"> <el-table-column label="操作" align="left" fixed="right">
<template slot-scope="scope"> <template slot-scope="scope">
<el-button size="mini" type="text" icon="el-icon-edit" @click="handleEdit(scope.row.id)">编辑</el-button> <el-button size="mini" type="text" icon="el-icon-edit" @click.native.stop="handleEdit(scope.row.id)">编辑</el-button>
<el-button size="mini" type="text" icon="el-icon-delete" @click.native.stop="handleDelete(scope.row.id)">删除 <el-button size="mini" type="text" icon="el-icon-delete" @click.native.stop="handleDelete(scope.row.id)">删除
</el-button> </el-button>
</template> </template>
@@ -234,6 +234,32 @@
<el-button type="primary" @click="editSubmitForm"> </el-button> <el-button type="primary" @click="editSubmitForm"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
<el-dialog :visible.sync="detailOpen" width="90%">
<template #title>
<div class="detail-title">
<el-row>
<span>查看需求-{{ title }}</span>
<el-menu :default-active="activeIndex" class="el-menu-demo" mode="horizontal" @select="handleSelect">
<el-menu-item index="1">需求详情</el-menu-item>
<el-menu-item index="2">需求动态</el-menu-item>
<el-menu-item index="3">已关联测试计划</el-menu-item>
<el-menu-item index="4">已关联用例</el-menu-item>
<el-menu-item index="5">已关联缺陷</el-menu-item>
<el-menu-item index="6">评审</el-menu-item>
</el-menu>
</el-row>
<project-detail :managerName="managerName" :projectId="projectId" v-show="activeIndex === '1'"></project-detail>
<relate-plan :projectId="projectId" v-show="activeIndex === '3'"></relate-plan>
<relateCaseTable :projectId="projectId" v-show="activeIndex === '4'"></relateCaseTable>
<relateDefectTable :projectId="projectId" v-show="activeIndex === '5'"></relateDefectTable>
</div>
</template>
<span slot="footer" class="dialog-footer">
<el-button @click="detailOpen = false"> </el-button>
</span>
</el-dialog>
</div> </div>
</template> </template>
@@ -241,10 +267,14 @@
import {addProject, delProject, getProjectDetail, getProjectList, managerList, updateProject} from "@/api/test/project"; import {addProject, delProject, getProjectDetail, getProjectList, managerList, updateProject} from "@/api/test/project";
import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue"; import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue";
import {requestDownload} from "@/utils/request"; import {requestDownload} from "@/utils/request";
import ProjectDetail from "@/views/test/project/projectDetail/projectDetail.vue";
import RelatePlan from "@/views/test/project/projectDetail/relatePlan.vue";
import RelateCaseTable from "@/views/test/project/projectDetail/relateCaseTable.vue";
import RelateDefectTable from "@/views/test/project/projectDetail/relateDefectTable.vue";
export default { export default {
name: 'project', name: 'project',
components: {SimpleOptions}, components: {RelatePlan, ProjectDetail, SimpleOptions , RelateCaseTable , RelateDefectTable},
dicts: ['priority_level', 'project_source', 'project_type', 'status'], dicts: ['priority_level', 'project_source', 'project_type', 'status'],
data() { data() {
return { return {
@@ -281,6 +311,8 @@ export default {
total: 0, total: 0,
list: [], list: [],
title: '', title: '',
projectId: 0,
managerName: '',
// 遮罩层 // 遮罩层
loading: false, loading: false,
editSubmitLoading: false, editSubmitLoading: false,
@@ -289,9 +321,11 @@ export default {
addOpen: false, addOpen: false,
//编辑弹窗 //编辑弹窗
editOpen: false, editOpen: false,
detailOpen: false,
selectedRows: [], selectedRows: [],
selectedData: [], selectedData: [],
managerList: [], managerList: [],
activeIndex: '1',
queryParams: { queryParams: {
serialNumber: '', serialNumber: '',
outline: '', outline: '',
@@ -327,6 +361,16 @@ export default {
}, },
}, },
methods: { methods: {
handleRowClick(row) {
this.projectId = row.id
this.activeIndex = '1'
this.managerName = row.manager
this.detailOpen = true;
this.title = row.serialNumber;
},
handleSelect(key) {
this.activeIndex = key
},
handleSelectionChange(selection) { handleSelectionChange(selection) {
this.selectedRows = selection; this.selectedRows = selection;
}, },

View File

@@ -0,0 +1,109 @@
<template>
<div class="project-detail">
<el-form ref="editForm" :model="detailForm" label-width="110px" label-position="right">
<el-container>
<el-main>
<el-form-item label="需求概要" prop="outline">
{{ detailForm.outline }}
</el-form-item>
<!-- 描述 -->
<el-form-item label="描述" prop="detail">
{{ detailForm.detail }}
</el-form-item>
</el-main>
<el-aside width="450px" class="basic-information">
<h3>基础信息</h3>
<el-form-item label="需求名称" prop="name">
{{ detailForm.name }}
</el-form-item>
<el-form-item label="状态" prop="status">
{{ detailForm.status }}
</el-form-item>
<el-form-item label="负责人" prop="manager">
{{ managerName }}
</el-form-item>
<el-form-item label="优先级" prop="priority">
{{ detailForm.priority }}
</el-form-item>
<el-form-item label="预期完成时间" prop="estimatedTime">
{{ detailForm.estimatedTime }}
</el-form-item>
<el-form-item label="需求来源" prop="source">
{{ detailForm.source }}
</el-form-item>
<el-form-item label="需求类型" prop="type">
{{ detailForm.type }}
</el-form-item>
<el-form-item label="版本" prop="version">
{{ detailForm.version }}
</el-form-item>
</el-aside>
</el-container>
</el-form>
</div>
</template>
<script>
import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue";
import {getProjectDetail} from "@/api/test/project";
export default {
name: 'projectDetail',
components: {SimpleOptions},
dicts: ['priority_level', 'project_source', 'project_type', 'status'],
props: {
projectId: {
type: Number,
default: 0,
},
managerName: {
type: String,
default: ''
}
},
data() {
return {
detailForm: {
outline: '',
detail: '',
name: '',
status: '',
manager: '',
priority: '',
estimatedTime: '',
source: '',
type: '',
version: ''
},
managerList: [],
editOpen: false,
};
},
created() {
},
watch: {
projectId(newVal){
this.getDetail()
}
},
methods: {
getDetail() {
getProjectDetail(this.projectId).then(res => {
this.detailForm = res.data;
this.detailForm.source = this.dict.type.project_source.find(e => e.value === res.data.source).label;
this.detailForm.type = this.dict.type.project_type.find(e => e.value === res.data.type).label;
this.detailForm.status = this.dict.type.status.find(e => e.value === res.data.status).label;
});
}
}
}
</script>
<style lang="scss" scoped>
.basic-information {
background-color: rgb(248, 248, 249) !important;
}
</style>

View File

@@ -0,0 +1,54 @@
<template>
<div class="app-container">
<el-table v-loading="loading" :data="list" height="500">
<el-table-column prop="name" label="用例名称"></el-table-column>
<el-table-column prop="status" label="用例状态" :formatter="row => ['','草稿', '通过', '不通过'][row.status]"></el-table-column>
<el-table-column prop="createBy" label="创建人"></el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList"/>
</div>
</template>
<script>
import {getRelateCaseTableList} from "@/api/test/project";
export default {
name: 'relateCaseTable',
props: {
projectId: {
type: Number,
default: '',
},
},
data() {
return {
loading: false,
list: [],
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
},
}
},
watch: {
projectId(newVal){
this.getList()
}
},
methods: {
getList() {
this.loading = true
getRelateCaseTableList(this.projectId).then(res => {
this.list = res.rows
this.total = res.total
this.loading = false
})
}
},
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -0,0 +1,73 @@
<template>
<div class="app-container">
<el-table v-loading="loading" :data="list" height="500">
<el-table-column prop="serialNumber" label="ID"></el-table-column>
<el-table-column prop="outline" label="摘要"></el-table-column>
<el-table-column prop="status" label="缺陷状态">
<template #default="{ row }">
<dict-tag :options="dict.type.bug_status" :value="row.status"/>
</template>
</el-table-column>
<el-table-column prop="manager" label="经办人"></el-table-column>
<el-table-column prop="level" label="严重程度">
<template #default="{ row }">
<dict-tag :options="dict.type.severity_level" :value="row.level"/>
</template>
</el-table-column>
<el-table-column prop="createTime" label="创建时间"></el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList"/>
</div>
</template>
<script>
import {getRelateDefectList} from "@/api/test/project";
export default {
name: 'relateCaseTable',
props: {
projectId: {
type: Number,
default: '',
},
},
dicts: ['severity_level', 'bug_status'],
data() {
return {
// 遮罩层
loading: true,
// 总条数
total: 0,
// 表格数据
list: [],
// 查询参数
queryParams: {
pageNum: 1,
pageSize: 10,
}
}
},
watch: {
projectId(newVal){
this.getList()
}
},
methods: {
getList() {
this.loading = true
getRelateDefectList(this.projectId).then(response => {
this.list = response.rows
this.total = response.total
this.loading = false
})
}
},
}
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,63 @@
<template>
<div class="app-container">
<el-table v-loading="loading" :data="list" height="500">
<el-table-column prop="statusLabel" label="状态" width="180"></el-table-column>
<el-table-column prop="name" label="计划名称"></el-table-column>
<el-table-column prop="manager" label="负责人"></el-table-column>
</el-table>
<pagination v-show="total>0" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getList"></pagination>
</div>
</template>
<script>
import {getRelatePlanList} from "@/api/test/project";
export default {
name: 'relatePlan',
dicts: ['status'],
props: {
projectId: {
type: Number,
default: '',
},
},
data() {
return {
list: [],
loading: false,
total: 0,
queryParams: {
pageNum: 1,
pageSize: 10,
},
}
},
watch: {
projectId(newVal){
this.getList()
}
},
methods: {
getList() {
this.loading = true;
getRelatePlanList(this.projectId).then(response => {
this.list = response.rows.map(item => {
const matched = this.dict.type.status.find(e => e.value === item.status);
return {
...item,
statusLabel: matched ? matched.label : '未知状态'
};
});
this.total = response.total;
this.loading = false;
})
},
}
}
</script>
<style lang="scss" scoped>
</style>

View File

@@ -15,7 +15,7 @@
<div class="table-content"> <div class="table-content">
<el-table :data="tableData" border style="width: 100%"> <el-table :data="tableData" border style="width: 100%">
<el-table-column prop="triggerTime" label="触发时间"> </el-table-column> <el-table-column prop="triggerTime" label="触发时间"> </el-table-column>
<el-table-column prop="startTime" label="开始时间"> </el-table-column> <!-- <el-table-column prop="startTime" label="开始时间"> </el-table-column>-->
<el-table-column prop="triggerType" label="触发方式" :formatter="row => ['','定时任务', '手动'][row.triggerType]"> <el-table-column prop="triggerType" label="触发方式" :formatter="row => ['','定时任务', '手动'][row.triggerType]">
</el-table-column> </el-table-column>
<el-table-column prop="resultDesc" label="情况描述"> </el-table-column> <el-table-column prop="resultDesc" label="情况描述"> </el-table-column>

View File

@@ -16,7 +16,7 @@
</el-button> </el-button>
</div> </div>
<el-tabs style="margin-top: 10px;"> <el-tabs style="margin-top: 10px;">
<el-Table v-loading="loading" :data="list"> <el-Table v-loading="loading" :data="list" @row-click="handleRowClick">
<el-table-column prop="name" label="测试报告名称" align="center"/> <el-table-column prop="name" label="测试报告名称" align="center"/>
<el-table-column prop="result" label="测试结果" align="center"> <el-table-column prop="result" label="测试结果" align="center">
<template #default="{ row }"> <template #default="{ row }">
@@ -35,6 +35,11 @@
</el-table-column> </el-table-column>
<el-table-column prop="updateBy" label="最后更新人" align="center"/> <el-table-column prop="updateBy" label="最后更新人" align="center"/>
<el-table-column prop="updateTime" label="最后更新时间" align="center"/> <el-table-column prop="updateTime" label="最后更新时间" align="center"/>
<el-table-column label="操作" align="center">
<template #default="{ row }">
<el-button type="text" size="mini" @click.native.stop="handleDel(row.id)">删除</el-button>
</template>
</el-table-column>
</el-Table> </el-Table>
</el-tabs> </el-tabs>
<pagination <pagination
@@ -65,7 +70,7 @@
</template> </template>
<script> <script>
import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue"; import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue";
import {addTestReport, getTestReportList} from "@/api/test/testPlan"; import {addTestReport, delExecuteCaseReport, getTestReportList} from "@/api/test/testPlan";
export default { export default {
name: 'caseReport', name: 'caseReport',
@@ -91,7 +96,7 @@ export default {
planId: '', planId: '',
}, },
queryParams: { queryParams: {
planId: '', id: '',
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
}, },
@@ -101,14 +106,38 @@ export default {
} }
} }
}, },
mounted() { activated() {
this.$nextTick(() => {
console.log('getlist')
this.getList() this.getList()
})
},
mounted() {
}, },
methods: { methods: {
handleRowClick(row){
this.$tab.openPage('平台报告', '/testplan/casereport/platformReport',
{id: row.id,
name: row.name,
type: row.type,
planId: row.planId,
report: row.report
}
)
},
handleDel(id) {
this.$modal.confirm('是否确认删除测试报告?').then(function () {
return delExecuteCaseReport(id);
}).then(() => {
this.getList();
this.$modal.msgSuccess("删除成功");
});
},
getList() { getList() {
this.loading = true this.loading = true
this.queryParams.planId = this.planId this.queryParams.id = Number(this.planId)
getTestReportList(this.queryParams.planId).then(res => { getTestReportList(this.queryParams).then(res => {
this.list = res.rows this.list = res.rows
this.total = res.total this.total = res.total
this.loading = false this.loading = false

View File

@@ -0,0 +1,416 @@
<template>
<div class="app-container">
<el-row :gutter="10" class="mb8">
<el-col :span="24" class="page-header">
<el-page-header :title="reportTitle" @back="goBack" ></el-page-header>
<div class="header-buttons" v-if="isEdit">
<span>测试结果</span>
<el-select v-model="selectedResult" placeholder="请选择">
<el-option
v-for="item in resultOptions"
:key="item.value"
:label="item.label"
:value="item.value">
</el-option>
</el-select>
<el-button type="primary" icon="el-icon-setting" @click="configureModule">配置模块</el-button>
<el-button type="primary" size="medium" @click="saveReport">保存报告</el-button>
<el-button type="danger" size="medium" @click="isEdit = false">取消编辑</el-button>
</div>
<div class="header-buttons" v-else>
<el-button type="primary" icon="el-icon-setting" @click="configureModule">下载报告</el-button>
<el-button type="primary" size="medium" @click="isEdit = true">编辑报告</el-button>
</div>
</el-col>
</el-row>
<div class="report-container" :class="testStatusClass">
<div class="report-header">
<div class="report-title" v-if="isEdit">
<el-input v-model="reportTitle" style="width: 300px" placeholder="请输入报告名称"/>
<div class="report-info">
<span>关联项目中移商业保理平台</span>
<span>测试阶段{{ reportType }}</span>
</div>
</div>
<div class="report-title" v-else>
<span>{{ reportTitle }}</span>
<div class="report-info">
<span>关联项目中移商业保理平台</span>
<span>测试阶段{{ reportType }}</span>
</div>
</div>
</div>
<div class="report-overview">
<div class="overview-item">
<div class="value">{{ reportData.caseNum }}</div>
<span class="label">执行用例数</span>
</div>
<div class="overview-item">
<span class="value">{{ passRate }}%</span>
<div class="progress-bar-container">
<div class="progress-bar" :style="{ width: passRate + '%' }"></div>
</div>
<div class="label">用例通过率</div>
</div>
<div class="overview-item">
<div class="value">0</div>
<span class="label">缺陷数</span>
</div>
</div>
</div>
<el-row :gutter="10">
<el-col :span="24">
<el-card>
<el-row>
<span style="font-size: 18px;color: #4f587e">用例分布</span>
</el-row>
<div class="charts">
<el-card shadow="hover" class="chart-item">
<h3>执行结果分布</h3>
<div id="result-distribution-chart"></div>
</el-card>
<el-card shadow="hover" class="chart-item">
<h3>用例类型分布</h3>
<div id="case-type-distribution-chart"></div>
</el-card>
</div>
</el-card>
</el-col>
</el-row>
</div>
</template>
<script>
import * as echarts from 'echarts';
import router from "@/router";
import {updateExecuteCaseReport} from "@/api/test/testPlan";
export default {
name: 'report',
dicts: ['test_type'],
props: {
planId: {
type: String,
default: '',
},
},
data() {
return {
isEdit: false,
reportId: '',
reportType: '',
reportTitle: '',
reportData: {},
selectedResult: '', // 选择的测试结果
resultOptions: [ // 测试结果选项
{ value: '1', label: '通过' },
{ value: '0', label: '不通过' }
],
// 执行结果分布数据
resultDistributionData: {
series: [
{ value: 0, name: '通过' ,itemStyle: {color: '#6fcdac'}},
{ value: 0, name: '失败' ,itemStyle: {color: '#e97d64'}},
{ value: 0, name: '跳过' ,itemStyle: {color: '#7484a1'}},
{ value: 0, name: '阻塞' ,itemStyle: {color: '#f5c539'}},
{ value: 0, name: '未执行' ,itemStyle: {color: '#cecece'}}
]
},
// 用例类型分布数据
caseTypeDistributionData: {
series: [
{ value: 0, name: '接口用例' ,itemStyle: {color: '#6fcdac'} }
]
}
};
},
created() {
this.reportId = this.$route.query.id;
this.reportTitle = this.$route.query.name;
this.reportType = this.$route.query.type;
this.reportData = JSON.parse(this.$route.query.report);
this.selectedResult = this.reportData.result;
// 更新饼图数据
this.resultDistributionData.series[0].value = this.reportData.passNum || 0;
this.resultDistributionData.series[1].value = this.reportData.caseNum - this.reportData.passNum || 0;
// 更新接口用例饼图数据
this.caseTypeDistributionData.series[0].value = this.reportData.caseNum || 0;
},
watch: {
'dict.type.test_type': {
handler(newVal) {
if (newVal && newVal.length > 0) {
const targetType = this.$route.query.type;
this.reportType = newVal.find(e => String(e.value) === String(targetType)).label;
}
},
immediate: true
}
},
computed: {
passRate() {
if (this.reportData.caseNum === 0) return 0;
return (this.reportData.passNum / this.reportData.caseNum) * 100;
},
testStatusClass() {
return this.selectedResult === '0' ? 'status-not-passed' : 'status-passed';
}
},
mounted() {
this.initResultDistributionChart();
this.initCaseTypeDistributionChart();
// 强制重绘图表以应用最新数据
this.$nextTick(() => {
const chartDom = document.getElementById('result-distribution-chart');
if (chartDom) {
const myChart = echarts.getInstanceByDom(chartDom);
myChart.setOption({
series: [{
type: 'pie',
radius: ['40%', '70%'],
data: this.resultDistributionData.series
}]
}, true); // true 表示合并选项
}
});
},
methods: {
initResultDistributionChart() {
const chartDom = document.getElementById('result-distribution-chart');
const myChart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
right: 'right',
textStyle: {
color: '#666'
}
},
series: [
{
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: '20',
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
data: this.resultDistributionData.series
}
]
};
myChart.setOption(option);
},
initCaseTypeDistributionChart() {
const chartDom = document.getElementById('case-type-distribution-chart');
const myChart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'item'
},
legend: {
orient: 'vertical',
right: 'right',
textStyle: {
color: '#666'
}
},
series: [
{
type: 'pie',
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
labelLine: {
show: false
},
data: this.caseTypeDistributionData.series
}
]
};
myChart.setOption(option);
},
goBack() {
this.$router.back();
// 关闭当前页面
this.$tab.closePage(router.currentRoute);
},
configureModule() {
// 配置模块的逻辑
console.log('Configure module');
},
saveReport() {
let reportData = {
caseNum: this.reportData.caseNum,
name: this.reportData.name,
passNum: this.reportData.passNum,
result: this.selectedResult // 使用外部的 result 值替换
};
// 构造要提交的数据对象
const reportDataToSave = {
id: this.reportId,
name: this.reportTitle,
result: this.selectedResult,
report: JSON.stringify(reportData)
};
updateExecuteCaseReport(reportDataToSave)
.then(response => {
this.$message.success('报告保存成功');
// 可选:更新页面状态为非编辑模式
this.isEdit = false;
})
.catch(error => {
this.$message.error('报告保存失败');
console.error('保存报告失败:', error);
});
},
},
}
</script>
<style scoped lang="scss">
.report-container {
height: 300px;
margin-bottom: 20px;
&.status-not-passed {
background: linear-gradient(180deg, #e7998b, #e8b1b0);
}
&.status-passed {
background: linear-gradient(180deg, #6fcdac, #9ed2bb);
}
.report-header {
color: white;
padding: 20px;
text-align: center;
position: relative;
margin-bottom: 50px;
.report-title {
font-size: 18px;
margin-bottom: 10px;
}
.report-info {
font-size: 14px;
}
}
.report-overview {
display: flex;
justify-content: space-around;
align-items: center;
color: white;
padding: 20px;
.overview-item {
text-align: center;
.value {
font-size: 24px;
margin-bottom: 10px;
}
.label {
font-size: 14px;
}
.progress-bar-container {
width: 100px;
height: 10px;
background-color: #e0e0e0; /* 背景颜色表示未完成部分 */
border-radius: 5px;
overflow: hidden;
margin-top: 10px;
}
.progress-bar {
height: 100%;
background-color: white; /* 进度条颜色 */
border-radius: 5px;
}
//.progress-bar {
// width: 100px;
// height: 10px;
// background-color: white;
// margin-top: 10px;
// border-radius: 5px;
//}
}
}
}
.charts {
display: flex;
justify-content: space-around;
margin-top: 20px;
.chart-item {
width: 45%;
h3 {
text-align: center;
margin-bottom: 10px;
}
#result-distribution-chart, #case-type-distribution-chart {
width: 100%;
height: 300px;
}
}
}
.page-header {
::v-deep.el-page-header__title {
font-size: 20px;
color: #000000;
font-weight: bold;
}
height: 100px;
display: flex;
align-items: center;
border-bottom: solid 1px #e6e6e6;
.header-buttons {
margin-left: auto;
display: flex;
align-items: center;
span {
margin-right: 10px;
}
.el-select, .el-button {
margin-left: 10px;
}
}
}
</style>

View File

@@ -39,7 +39,7 @@
<el-Table v-loading="loading" :data="list" @selection-change="handleSelectionChange"> <el-Table v-loading="loading" :data="list" @selection-change="handleSelectionChange">
<el-table-column type="selection"/> <el-table-column type="selection"/>
<el-table-column prop="caseName" label="测试用例名称" align="center"/> <el-table-column prop="caseName" label="测试用例名称" align="center"/>
<el-table-column prop="executeResult" label="执行结果" align="center"/> <el-table-column prop="executeResult" label="执行结果" :formatter="row => ['未执行', '成功', '失败'][row.executeResult]" align="center"/>
<el-table-column prop="createBy" label="创建人" align="center"/> <el-table-column prop="createBy" label="创建人" align="center"/>
<el-table-column prop="executeBy" label="最近执行人" align="center"/> <el-table-column prop="executeBy" label="最近执行人" align="center"/>
<el-table-column prop="executeTime" label="最近执行时间" align="center"/> <el-table-column prop="executeTime" label="最近执行时间" align="center"/>
@@ -176,7 +176,9 @@ export default {
return runTestPlanCase(queryParams); return runTestPlanCase(queryParams);
}).then((res) => { }).then((res) => {
this.$modal.msgSuccess("提交执行成功"); this.$modal.msgSuccess("提交执行成功");
this.open = true; // this.open = true;
this.loading = false;
}).finally(() => {
this.loading = false; this.loading = false;
}); });
}, },

View File

@@ -123,7 +123,7 @@
placement="top-start" placement="top-start"
trigger="hover" trigger="hover"
> >
<span v-if="scope.row.smokeTest === null">暂无关联用例</span> <span v-if="scope.row.functionTest === null">暂无关联用例</span>
<div v-else class="custom-content"> <div v-else class="custom-content">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="8"> <el-col :span="8">
@@ -152,7 +152,7 @@
placement="top-start" placement="top-start"
trigger="hover" trigger="hover"
> >
<span v-if="scope.row.smokeTest === null">暂无关联用例</span> <span v-if="scope.row.regressionTest === null">暂无关联用例</span>
<div v-else class="custom-content"> <div v-else class="custom-content">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="8"> <el-col :span="8">
@@ -181,7 +181,7 @@
placement="top-start" placement="top-start"
trigger="hover" trigger="hover"
> >
<span v-if="scope.row.smokeTest === null">暂无关联用例</span> <span v-if="scope.row.preProductionTest === null">暂无关联用例</span>
<div v-else class="custom-content"> <div v-else class="custom-content">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="8"> <el-col :span="8">
@@ -210,7 +210,7 @@
placement="top-start" placement="top-start"
trigger="hover" trigger="hover"
> >
<span v-if="scope.row.smokeTest === null">暂无关联用例</span> <span v-if="scope.row.productionTest === null">暂无关联用例</span>
<div v-else class="custom-content"> <div v-else class="custom-content">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="8"> <el-col :span="8">
@@ -375,6 +375,9 @@ export default {
{id: row.id, {id: row.id,
name: row.name, name: row.name,
testStatus: row.status, testStatus: row.status,
startPlanTime: row.startPlanTime,
endPlanTime: row.endPlanTime,
manager: row.manager,
}); });
}, },
handleTabClick(tab) { handleTabClick(tab) {

View File

@@ -30,7 +30,7 @@
</el-row> </el-row>
<!-- 测试计划界面 --> <!-- 测试计划界面 -->
<probably-view :planId="planId" v-show="activeIndex === '1'"></probably-view> <probably-view :overViewData="overViewData" :planId="planId" v-show="activeIndex === '1'"></probably-view>
<case-execute :planId="planId" :name="testTitle" v-show="activeIndex === '2'"></case-execute> <case-execute :planId="planId" :name="testTitle" v-show="activeIndex === '2'"></case-execute>
<case-report :planId="planId" v-show="activeIndex === '3'"></case-report> <case-report :planId="planId" v-show="activeIndex === '3'"></case-report>
<test-defects :planId="planId" :name="testTitle" v-show="activeIndex === '4'"></test-defects> <test-defects :planId="planId" :name="testTitle" v-show="activeIndex === '4'"></test-defects>
@@ -66,6 +66,7 @@ export default {
'3': '已终止' '3': '已终止'
}, },
planId: '', planId: '',
overViewData: {},
testStatus: '', testStatus: '',
testTitle: '', testTitle: '',
activeTab: 'overview',// 默认选中的标签页 activeTab: 'overview',// 默认选中的标签页
@@ -114,6 +115,11 @@ export default {
this.planId = this.$route.query.id; this.planId = this.$route.query.id;
this.testTitle = this.$route.query.name this.testTitle = this.$route.query.name
this.testStatus = this.$route.query.testStatus this.testStatus = this.$route.query.testStatus
this.overViewData = {
startTime: this.$route.query.startPlanTime,
endTime: this.$route.query.endPlanTime,
manager: this.$route.query.manager
}
} }
}; };
</script> </script>

View File

@@ -17,14 +17,13 @@
</el-row> </el-row>
<el-row :gutter="10"> <el-row :gutter="10">
<el-col :span="5"> <el-col :span="5">
<!-- <span>起止时间{{ startToEndTime }}</span>--> <span>起止时间{{ overViewData.startTime }} ~ {{ overViewData.endTime }}</span>
<span>起止时间2020-04-01 ~ 2020-04-30</span>
</el-col> </el-col>
<el-col :span="7"> <el-col :span="7">
<span>当前测试计划已经开始 3 ,距离截止时间还有 52 </span> <span>当前测试计划已经开始 {{ daysElapsed }} ,距离截止时间还有 {{ daysRemaining }} </span>
</el-col> </el-col>
<el-col :span="5"> <el-col :span="5">
<span>负责人{{ }}</span> <span>负责人{{ overViewData.manager }}</span>
</el-col> </el-col>
</el-row> </el-row>
</div> </div>
@@ -34,10 +33,10 @@
<div class="progress-content"> <div class="progress-content">
<div id="executionProgressChart" style="width: 70%; height: 200px;"></div> <div id="executionProgressChart" style="width: 70%; height: 200px;"></div>
<div class="progress-text"> <div class="progress-text">
<p style="font-size: 40px;margin: auto">100%</p> <p style="font-size: 40px;margin: auto">{{ executionProgress || 0 }}%</p>
<p>执行进度</p> <p>执行进度</p>
<p>用例总数 1</p> <p>用例总数 {{ viewData.caseCount }}</p>
<p><span style="color: #409EFF"></span> 已执行用例 1</p> <p><span style="color: #6e92ef"></span> 已执行用例 {{ viewData.executedCaseCount }}</p>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -47,13 +46,13 @@
<div class="progress-content"> <div class="progress-content">
<el-row :gutter="10"> <el-row :gutter="10">
<el-col class="rate-content" :span="12"> <el-col class="rate-content" :span="12">
<p>100%</p> <p>{{ executionPassRate || 0 }}%</p>
<el-progress :percentage="100" status="success"></el-progress> <el-progress :percentage="(viewData.passedCaseCount / viewData.executedCaseCount).toFixed(2) * 100 || 0" status="success"></el-progress>
<p>执行用例通过率</p> <p>执行用例通过率</p>
</el-col> </el-col>
<el-col class="rate-content" :span="12"> <el-col class="rate-content" :span="12">
<p>100%</p> <p>{{ overallPassRate || 0 }}%</p>
<el-progress :percentage="100" status="success"></el-progress> <el-progress :percentage="(viewData.passedCaseCount / viewData.caseCount).toFixed(2) * 100 || 0" status="success"></el-progress>
<p>总体用例通过率</p> <p>总体用例通过率</p>
</el-col> </el-col>
</el-row> </el-row>
@@ -64,14 +63,14 @@
<el-card shadow="hover"> <el-card shadow="hover">
<div class="progress-content"> <div class="progress-content">
<div class="defect-content"> <div class="defect-content">
<p>0</p> <p>{{ viewData.defectCount }}</p>
<p>缺陷数</p> <p>缺陷数</p>
<el-row> <el-row>
<el-col :span="4">0</el-col> <el-col :span="4">{{ viewData.unconfirmedDefectCount }}</el-col>
<el-col :span="4">0</el-col> <el-col :span="4">{{ viewData.fixingDefectCount }}</el-col>
<el-col :span="4">0</el-col> <el-col :span="4">{{ viewData.unverifiedDefectCount }}</el-col>
<el-col :span="4">0</el-col> <el-col :span="4">{{ viewData.invalidDefectCount }}</el-col>
<el-col :span="4">0</el-col> <el-col :span="4">{{ viewData.suspendedDefectCount }}</el-col>
</el-row> </el-row>
<el-row> <el-row>
<el-col :span="4">待确认</el-col> <el-col :span="4">待确认</el-col>
@@ -98,13 +97,13 @@
</div> </div>
<el-card shadow="hover"> <el-card shadow="hover">
<div class="distribution-content"> <div class="distribution-content">
<el-progress type="circle" :percentage="100" color="#67C23A"></el-progress> <div id="distributionChart" style="width: 70%; height: 200px;"></div>
<div class="distribution-text"> <div class="distribution-text">
<p><span style="background-color: #67C23A; display: inline-block; width: 10px; height: 10px;"></span> 通过 1</p> <p><span style="background-color: #9cdab9; display: inline-block; width: 10px; height: 10px;"></span> 通过 {{ viewData.passedCaseCount }}</p>
<p><span style="background-color: #F56C6C; display: inline-block; width: 10px; height: 10px;"></span> 失败 0</p> <p><span style="background-color: #d4836a; display: inline-block; width: 10px; height: 10px;"></span> 失败 {{ viewData.failedCaseCount }}</p>
<p><span style="background-color: #E6A23C; display: inline-block; width: 10px; height: 10px;"></span> 阻塞 0</p> <p><span style="background-color: #e9c456; display: inline-block; width: 10px; height: 10px;"></span> 阻塞 {{ viewData.blockedCaseCount }}</p>
<p><span style="background-color: #F56C6C; display: inline-block; width: 10px; height: 10px;"></span> 跳过 0</p> <p><span style="background-color: #79849e; display: inline-block; width: 10px; height: 10px;"></span> 跳过 {{ viewData.skippedCaseCount }}</p>
<p><span style="background-color: #909399; display: inline-block; width: 10px; height: 10px;"></span> 未执行 0</p> <p><span style="background-color: #cdcdcd; display: inline-block; width: 10px; height: 10px;"></span> 未执行 {{ viewData.notExecutedCaseCount }}</p>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -117,12 +116,34 @@
</div> </div>
<el-card shadow="hover"> <el-card shadow="hover">
<div class="distribution-text" style="height: 200px"> <div class="distribution-text" style="height: 200px">
<line-chart :data="trendData" :chart-data="trendData"></line-chart> <div id="caseTrendChart" style="width: 100%; height: 200px;"></div>
</div> </div>
</el-card> </el-card>
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="10">
<el-col :span="12">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>缺陷等级分布</span>
</div>
<el-card shadow="hover">
<div id="defectLevelChart" style="width: 100%; height: 200px;"></div>
</el-card>
</el-card>
</el-col>
<el-col :span="12">
<el-card class="box-card">
<div slot="header" class="clearfix">
<span>缺陷类型分布</span>
</div>
<el-card shadow="hover">
<div id="defectTypeChart" style="width: 100%; height: 200px;"></div>
</el-card>
</el-card>
</el-col>
</el-row>
<el-card class="box-card"> <el-card class="box-card">
<div> <div>
关联需求信息 关联需求信息
@@ -160,25 +181,29 @@
</template> </template>
<script> <script>
import LineChart from '@/views/dashboard/LineChart';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import {getTestPlanProjectList} from "@/api/test/testPlan"; import moment from 'moment';
import {getPlanCaseTrendData, getPlanOverview, getTestPlanProjectList} from "@/api/test/testPlan";
export default { export default {
name: 'probablyView', name: 'probablyView',
components: {
LineChart,
},
props: { props: {
planId: { planId: {
type: String, type: String,
default: '', default: '',
}, },
overViewData: {
type: Object,
default: () => {
return {
startTime: '',
endTime: '',
manager: '',
}
}
},
}, },
dicts: ['priority_level', 'project_source', 'project_type', 'status'], dicts: ['priority_level', 'project_source', 'project_type', 'status'],
mounted() {
this.initExecutionProgressChart();
},
data() { data() {
return { return {
priorityColor: { priorityColor: {
@@ -192,42 +217,84 @@ export default {
list: [], list: [],
loading: false, loading: false,
myChart: null, myChart: null,
startToEndTime: '', distributionChart: null,
lineChart: null,
viewData: [],
caseTrendData: [],
// 执行进度
executionProgress: 0,
// 执行用例通过率
executionPassRate: 0,
// 总体用例通过率
overallPassRate: 0,
defectSlider: 0, defectSlider: 0,
queryParams: { queryParams: {
planId: '', planId: '',
pageNum: 1, pageNum: 1,
pageSize: 10, pageSize: 10,
}, },
trendData: {
labels: ['2025-04-01', '2025-04-02', '2025-04-03'],
datasets: [
{
label: '通过',
backgroundColor: '#67C23A',
data: [0, 0.5, 1],
},
{
label: '失败',
backgroundColor: '#F56C6C',
data: [0, 0, 0],
},
{
label: '跳过',
backgroundColor: '#E6A23C',
data: [0, 0, 0],
},
{
label: '阻塞',
backgroundColor: '#909399',
data: [0, 0, 0],
},
],
},
}; };
}, },
mounted() {
// 饼图1
const chartDom = document.getElementById('executionProgressChart');
this.myChart = echarts.init(chartDom);
const Observer = new ResizeObserver(() =>{
this.myChart.resize();
})
Observer.observe(chartDom);
// 饼图2
const chartDom2 = document.getElementById('distributionChart');
this.distributionChart = echarts.init(chartDom2);
const Observer2 = new ResizeObserver(() =>{
this.distributionChart.resize();
})
Observer2.observe(chartDom2);
// 折线趋势图
const lineChartDom = document.getElementById('caseTrendChart');
this.lineChart = echarts.init(lineChartDom);
const Observer3 = new ResizeObserver(() =>{
this.lineChart.resize();
})
Observer3.observe(lineChartDom);
// 柱状图1
const barChartDom = document.getElementById('defectLevelChart');
this.barChart = echarts.init(barChartDom);
const Observer4 = new ResizeObserver(() =>{
this.barChart.resize();
})
Observer4.observe(barChartDom);
// 柱状图2
const barChartDom2 = document.getElementById('defectTypeChart');
this.barChart2 = echarts.init(barChartDom2);
const Observer5 = new ResizeObserver(() =>{
this.barChart2.resize();
})
Observer5.observe(barChartDom2);
},
created() { created() {
this.getList(); this.getList();
this.getInfoList();
this.getTrendData();
},
computed: {
daysElapsed() {
if (!this.overViewData.startTime) return 0;
const start = moment(this.overViewData.startTime);
const now = moment();
return now.diff(start, 'days');
},
daysRemaining() {
if (!this.overViewData.endTime) return 0;
const end = moment(this.overViewData.endTime);
const now = moment();
const diffDays = end.diff(now, 'days');
return diffDays > 0 ? diffDays : 0; // 确保不显示负数
},
}, },
methods: { methods: {
getList() { getList() {
@@ -239,11 +306,37 @@ export default {
this.loading = false; this.loading = false;
}) })
}, },
getInfoList() {
getPlanOverview(this.queryParams.planId).then(response => {
this.viewData = response;
this.executionProgress = (this.viewData.executedCaseCount / this.viewData.caseCount).toFixed(2) * 100 || 0;
if (this.viewData.executedCaseCount === 0) {
this.executionPassRate = 0;
} else {
this.executionPassRate = ((this.viewData.passedCaseCount / this.viewData.executedCaseCount).toFixed(2) * 100).toFixed(2) || 0 ;
}
if (this.viewData.caseCount === 0) {
this.overallPassRate = 0;
} else {
this.overallPassRate = ((this.viewData.passedCaseCount / this.viewData.caseCount).toFixed(2) * 100).toFixed(2) || 0;
}
this.initExecutionProgressChart();
this.initDistributionChart();
this.initCaseTrendChart();
this.initDefectLevelChart();
this.initDefectTypeChart();
})
},
getTrendData(){
getPlanCaseTrendData(this.queryParams.planId).then(response => {
this.caseTrendData = response;
this.initCaseTrendChart();
})
},
// 初始化执行进度饼图 // 初始化执行进度饼图
initExecutionProgressChart() { initExecutionProgressChart() {
const chartDom = document.getElementById('executionProgressChart');
const myChart = echarts.init(chartDom);
const option = { const option = {
tooltip: { tooltip: {
trigger: 'item' trigger: 'item'
@@ -254,36 +347,230 @@ export default {
}, },
series: [ series: [
{ {
name: 'Access From', name: '执行进度',
type: 'pie', type: 'pie',
radius: ['40%', '70%'], radius: ['40%', '70%'],
avoidLabelOverlap: false, avoidLabelOverlap: false,
label: { label: {
show: false, formatter: '{b}: {c}%',
position: 'center' position: 'inside'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
}, },
data: [ data: [
{value: 1}, { value: this.executionProgress, name: '执行进度' , itemStyle: { color: '#6e92ef'}},
{ value: 100 - this.executionProgress, name: '剩余进度' , itemStyle: { color: '#f5f5f5'}}
] ]
} }
] ]
}; };
myChart.setOption(option); option && this.myChart.setOption(option);
// 监听窗口大小变化,自动调整图表大小 },
window.addEventListener('resize', () => {
myChart.resize(); // 初始化执行分布饼图
}); initDistributionChart() {
const option = {
tooltip: {
trigger: 'item'
},
series: [
{
name: '用例状态',
type: 'pie',
radius: ['50%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
labelLine: {
show: false
},
data: [
{ value: this.viewData.passedCaseCount, itemStyle: { color: '#9cdab9' } },
{ value: this.viewData.failedCaseCount, itemStyle: { color: '#d4836a' } },
{ value: this.viewData.blockedCaseCount, itemStyle: { color: '#e9c456' } },
{ value: this.viewData.skippedCaseCount, itemStyle: { color: '#79849e' } },
{ value: this.viewData.notExecutedCaseCount, itemStyle: { color: '#cdcdcd' } }
]
}
]
};
option && this.distributionChart.setOption(option);
},
// 初始化折线图
initCaseTrendChart() {
const dates = this.caseTrendData.map(item => item.caseTrendDates);
const passedData = this.caseTrendData.map(item => item.passed);
const failedData = this.caseTrendData.map(item => item.failed);
const blockedData = this.caseTrendData.map(item => item.blocked);
const skippedData = this.caseTrendData.map(item => item.skipped);
const notExecutedData = this.caseTrendData.map(item => item.notExecuted);
const option = {
tooltip: {
trigger: 'axis'
},
legend: {
data: ['阻塞', '通过', '跳过', '失败', '未执行'],
left: 'left'
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
data: dates
},
yAxis: {
type: 'value'
},
series: [
{
name: '阻塞',
type: 'line',
data: blockedData
},
{
name: '通过',
type: 'line',
data: passedData
},
{
name: '跳过',
type: 'line',
data: skippedData
},
{
name: '失败',
type: 'line',
data: failedData
},
{
name: '未执行',
type: 'line',
data: notExecutedData
}
]
};
option && this.lineChart.setOption(option);
},
// 初始化柱状图
initDefectLevelChart() {
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
legend: {
data: ['缺陷数']
},
xAxis: [
{
type: 'category',
data: ['致命', '严重', '一般', '轻微', '无效'],
axisTick: {
alignWithLabel: true
}
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '缺陷数',
type: 'bar',
barWidth: '30%',
itemStyle: {
color: '#D68972FF' // 更改柱状图颜色为橙色
},
data: [
this.viewData.fatalLevelDefectCount,
this.viewData.seriousLevelDefectCount,
this.viewData.normalLevelDefectCount,
this.viewData.minorLevelDefectCount,
this.viewData.invalidLevelDefectCount
] // 更新数据以匹配图片
}
]
};
option && this.barChart.setOption(option);
},
// 初始化柱状图
initDefectTypeChart() {
const option = {
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
legend: {
data: ['缺陷数']
},
xAxis: [
{
type: 'category',
data: ['功能逻辑', 'UI交互', '性能问题', '兼容性问题', '配置错误','安全问题', '安装部署' ,'其他'],
axisTick: {
alignWithLabel: true
}
}
],
yAxis: [
{
type: 'value'
}
],
series: [
{
name: '缺陷数',
type: 'bar',
barWidth: '30%',
itemStyle: {
color: '#D68972FF' // 更改柱状图颜色为橙色
},
data: [this.viewData.logicDefectCount,
this.viewData.uiDefectCount,
this.viewData.performanceDefectCount,
this.viewData.compatibilityDefectCount,
this.viewData.configurationDefectCount,
this.viewData.securityDefectCount,
this.viewData.installationDefectCount,
this.viewData.otherDefectCount
] // 更新数据以匹配图片
}
]
};
option && this.barChart2.setOption(option);
}, },
}, },
}; };

View File

@@ -59,7 +59,8 @@
<el-button size="mini" @click="handleClickSave">保存</el-button> <el-button size="mini" @click="handleClickSave">保存</el-button>
</div> </div>
<SceneStep v-show="informationForm.uiSceneStepsVOList.length > 0" :detail="changeStep" /> <SceneStep v-show="informationForm.uiSceneStepsVOList.length > 0" :detail="changeStep" />
<AdvancedSetting v-show="informationForm.uiSceneStepsVOList.length > 0" /> <AdvancedSetting v-show="informationForm.uiSceneStepsVOList.length > 0" :detail="changeStep"
@changeSetting="changeSetting" />
</div> </div>
<!-- 添加步骤 --> <!-- 添加步骤 -->
<div class="add-btn"> <div class="add-btn">
@@ -80,7 +81,7 @@
<script> <script>
import AdvancedSetting from './advancedSetting.vue' import AdvancedSetting from './advancedSetting.vue'
import SceneStep from './sceneStep.vue' import SceneStep from './sceneStep.vue'
import { getAutomationDetail, updateAutomation } from '../../../api/uiTest/automationTest' import { getAutomationDetail, addAutomation } from '../../../api/uiTest/automationTest'
import { listGroup } from "../../../api/test/group" import { listGroup } from "../../../api/test/group"
export default { export default {
@@ -109,7 +110,7 @@ export default {
}, },
stepList: [], stepList: [],
defaultActive: 0, // 当前激活菜单的 index defaultActive: 0, // 当前激活菜单的 index
changeStep: null, // 选中的步骤 changeStep: { operateType: null }, // 选中的步骤
} }
}, },
mounted() { mounted() {
@@ -125,6 +126,10 @@ export default {
} }
}) })
}, },
// 修改高级设置
changeSetting(val) {
this.informationForm.uiSceneStepsVOList[this.defaultActive - 1].uiHighSettingVOList = val
},
// 删除步骤 // 删除步骤
deleteStep(index) { deleteStep(index) {
@@ -175,17 +180,21 @@ export default {
handleClickSave() { handleClickSave() {
if (this.informationForm.name === null || this.informationForm.name === '') { if (this.informationForm.name === null || this.informationForm.name === '') {
this.$modal.msgWarning("请输入名称!"); this.$modal.msgWarning("请输入名称!");
return
} }
if (this.informationForm.groupId === null || this.informationForm.groupId === '') { if (this.informationForm.groupId === null || this.informationForm.groupId === '') {
this.$modal.msgWarning("请选择模块!"); this.$modal.msgWarning("请选择模块!");
return
} }
if (this.informationForm.status === null || this.informationForm.status === '') { if (this.informationForm.status === null || this.informationForm.status === '') {
this.$modal.msgWarning("请选择状态"); this.$modal.msgWarning("请选择状态");
return
} }
if (this.informationForm.dutyBy === null || this.informationForm.dutyBy === '') { if (this.informationForm.dutyBy === null || this.informationForm.dutyBy === '') {
this.$modal.msgWarning("请输入责任人"); this.$modal.msgWarning("请输入责任人");
return
} }
updateAutomation(this.informationForm).then(res => { addAutomation(this.informationForm).then(res => {
if (res.code === 200) { if (res.code === 200) {
this.$modal.msgSuccess("编辑成功") this.$modal.msgSuccess("编辑成功")
this.$tab.closeOpenPage({ path: "/ui-test/automation-test" }); this.$tab.closeOpenPage({ path: "/ui-test/automation-test" });

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,6 @@
<template> <template>
<folder-page type="automation" @click="folderHandleSelected" ref="folder"> <folder-page type="automation" @click="folderHandleSelected" ref="folder">
<div v-if="searchForm.groupId">
<div class="header"> <div class="header">
<el-dropdown @command="handleCommand"> <el-dropdown @command="handleCommand">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
@@ -32,13 +33,18 @@
<div v-if="scope.row.status === '3'">已完成</div> <div v-if="scope.row.status === '3'">已完成</div>
</template> </template>
</el-table-column> </el-table-column>
</el-table-column>
<el-table-column prop="label" label="标签" min-width="150" align="center" /> <el-table-column prop="label" label="标签" min-width="150" align="center" />
<el-table-column prop="createBy" label="创建人" min-width="150" align="center" sortable /> <el-table-column prop="createBy" label="创建人" min-width="150" align="center" sortable />
<el-table-column prop="dutyBy" label="责任人" min-width="150" align="center" sortable /> <el-table-column prop="dutyBy" label="责任人" min-width="150" align="center" sortable />
<el-table-column prop="updateTime" label="最后更新时间" min-width="180" align="center" sortable /> <el-table-column prop="updateTime" label="最后更新时间" min-width="180" align="center" sortable />
<el-table-column prop="stepsNumber" label="步骤数" min-width="80" align="center" /> <el-table-column prop="stepsNumber" label="步骤数" min-width="80" align="center" />
<el-table-column prop="executionResult" label="执行结果" min-width="150" align="center" sortable /> <el-table-column prop="executionResult" label="执行结果" min-width="150" align="center" sortable>
<template slot-scope="scope">
<el-tag v-if="scope.row.executionResult === '1'" type="warning">running</el-tag>
<el-tag v-if="scope.row.executionResult === '2'" type="danger">error</el-tag>
<el-tag v-if="scope.row.executionResult === '3'" type="success">success</el-tag>
</template>
</el-table-column>
<el-table-column prop="passRate" label="通过率" min-width="150" align="center" sortable /> <el-table-column prop="passRate" label="通过率" min-width="150" align="center" sortable />
<el-table-column prop="action" label="操作" align="center" fixed="right" width="200px"> <el-table-column prop="action" label="操作" align="center" fixed="right" width="200px">
<template slot-scope="scope"> <template slot-scope="scope">
@@ -53,6 +59,8 @@
<el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange" <el-pagination background @size-change="handleSizeChange" @current-change="handleCurrentChange"
:page-size="searchForm.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total" /> :page-size="searchForm.pageSize" layout="total, sizes, prev, pager, next, jumper" :total="total" />
</div> </div>
</div>
<el-empty v-else/>
</folder-page> </folder-page>
</template> </template>
@@ -61,7 +69,7 @@ import FolderPage from "@/components/FolderPage/index.vue"
import { getAutomationList, deleteAutomation, executeAutomationData } from "../../../api/uiTest/automationTest"; import { getAutomationList, deleteAutomation, executeAutomationData } from "../../../api/uiTest/automationTest";
export default { export default {
name: "AutomationTest", name: "AutomationTestView",
components: { FolderPage }, components: { FolderPage },
data() { data() {
return { return {
@@ -80,18 +88,27 @@ export default {
groupId: null, groupId: null,
}, },
total: 0, total: 0,
isFromEdit: false, // 添加标记,用于判断是否从编辑页面返回
} }
}, },
mounted() { activated() {
// 当从编辑页面返回时,不刷新列表
if (this.isFromEdit) {
this.isFromEdit = false;
return;
}
}, },
methods: { methods: {
folderHandleSelected(id) { folderHandleSelected(id) {
if (id) { if (id) {
// 获取列表
this.searchForm.groupId = id; this.searchForm.groupId = id;
// 只有不是从编辑页面返回时才刷新列表
if (!this.isFromEdit) {
this.getAutomationListData(); this.getAutomationListData();
}
} else { } else {
this.automationList = []; this.automationList = [];
this.searchForm.groupId = null;
} }
}, },
// 获取列表 // 获取列表
@@ -113,11 +130,12 @@ export default {
}, },
// 编辑 // 编辑
hadleClickEdit(val) { hadleClickEdit(val) {
this.$tab.openPage("编辑场景", "/ui-test/automation/edit", { id: val.id }); this.isFromEdit = true; // 标记即将进入编辑页面
this.$tab.openPage(`编辑场景_${val.id}`, `/ui-test/automation/edit/${val.id}`, { id: val.id });
}, },
// 删除 // 删除
hadleClickDelete(val) { hadleClickDelete(val) {
this.$modal.confirm('确认删除元素' + '').then(() => { this.$modal.confirm('确认删除该场景' + '').then(() => {
deleteAutomation(val.id).then(res => { deleteAutomation(val.id).then(res => {
if (res.code === 200) { if (res.code === 200) {
this.$modal.msgSuccess("删除成功"); this.$modal.msgSuccess("删除成功");
@@ -130,13 +148,22 @@ export default {
}, },
// 查看 // 查看
handleClickDetail(val) { handleClickDetail(val) {
const loading = this.$loading({
lock: true,
text: '执行中...',
spinner: 'el-icon-loading',
background: 'rgba(0, 0, 0, 0.7)'
});
executeAutomationData({ id: val.id }).then(res => { executeAutomationData({ id: val.id }).then(res => {
loading.close();
if (res.code === 200) { if (res.code === 200) {
this.$modal.msgSuccess("执行成功"); this.$modal.msgSuccess("执行成功");
} else { } else {
this.$modal.msgSuccess("执行失败"); this.$modal.msgError("执行失败");
} }
}) }).catch(err => {
loading.close();
});
}, },
// 分页 // 分页
handleSizeChange(val) { handleSizeChange(val) {

View File

@@ -56,10 +56,11 @@
</div> </div>
<div class="scene-wrap"> <div class="scene-wrap">
<div class="scene-header"> <div class="scene-header">
<el-button size="mini" @click="handleClickSave">保存</el-button> <el-button size="mini" :loading="isSaving" @click="handleClickSave">保存</el-button>
</div> </div>
<SceneStep v-show="informationForm.uiSceneStepsVOList.length > 0" :detail="changeStep" /> <SceneStep v-show="informationForm.uiSceneStepsVOList.length > 0" :detail="changeStep" />
<AdvancedSetting v-show="informationForm.uiSceneStepsVOList.length > 0" /> <AdvancedSetting v-show="informationForm.uiSceneStepsVOList.length > 0" :detail="changeStep"
@changeSetting="changeSetting" />
</div> </div>
<!-- 添加步骤 --> <!-- 添加步骤 -->
<div class="add-btn"> <div class="add-btn">
@@ -89,6 +90,7 @@ export default {
data() { data() {
return { return {
activeName: 'first', activeName: 'first',
isSaving: false, // 保存按钮loading状态
groupList: [], // 分组 groupList: [], // 分组
informationForm: { informationForm: {
automationId: null, // 场景id automationId: null, // 场景id
@@ -109,7 +111,7 @@ export default {
}, },
stepList: [], stepList: [],
defaultActive: 0, // 当前激活菜单的 index defaultActive: 0, // 当前激活菜单的 index
changeStep: null, // 选中的步骤 changeStep: { operateType: null }, // 选中的步骤
} }
}, },
mounted() { mounted() {
@@ -126,6 +128,10 @@ export default {
} }
}) })
}, },
// 修改高级设置
changeSetting(val) {
this.informationForm.uiSceneStepsVOList[this.defaultActive - 1].uiHighSettingVOList = val
},
// 获取详情 // 获取详情
getSceneDetail() { getSceneDetail() {
getAutomationDetail(this.$route.query.id).then(res => { getAutomationDetail(this.$route.query.id).then(res => {
@@ -144,6 +150,31 @@ export default {
deleteStep(index) { deleteStep(index) {
this.$modal.confirm('确认删除该步骤' + '').then(() => { this.$modal.confirm('确认删除该步骤' + '').then(() => {
this.informationForm.uiSceneStepsVOList.splice(index, 1); this.informationForm.uiSceneStepsVOList.splice(index, 1);
// 如果删除后没有步骤了,清空右侧面板
if (this.informationForm.uiSceneStepsVOList.length === 0) {
this.defaultActive = 0;
this.changeStep = { operateType: null };
return;
}
// 如果删除的是当前选中的步骤
if (this.defaultActive - 1 === index) {
// 如果有下一个步骤,选择下一个
if (index < this.informationForm.uiSceneStepsVOList.length) {
this.defaultActive = index + 1;
this.changeStep = this.informationForm.uiSceneStepsVOList[index];
}
// 如果有上一个步骤,选择上一个
else if (index > 0) {
this.defaultActive = index;
this.changeStep = this.informationForm.uiSceneStepsVOList[index - 1];
}
}
// 如果删除的步骤在当前选中步骤之前需要更新defaultActive
else if (this.defaultActive - 1 > index) {
this.defaultActive--;
}
}).catch(() => { }) }).catch(() => { })
}, },
// 场景步骤 // 场景步骤
@@ -189,24 +220,30 @@ export default {
handleClickSave() { handleClickSave() {
if (this.informationForm.name === null || this.informationForm.name === '') { if (this.informationForm.name === null || this.informationForm.name === '') {
this.$modal.msgWarning("请输入名称!"); this.$modal.msgWarning("请输入名称!");
return
} }
if (this.informationForm.groupId === null || this.informationForm.groupId === '') { if (this.informationForm.groupId === null || this.informationForm.groupId === '') {
this.$modal.msgWarning("请选择模块!"); this.$modal.msgWarning("请选择模块!");
return
} }
if (this.informationForm.status === null || this.informationForm.status === '') { if (this.informationForm.status === null || this.informationForm.status === '') {
this.$modal.msgWarning("请选择状态"); this.$modal.msgWarning("请选择状态");
return
} }
if (this.informationForm.dutyBy === null || this.informationForm.dutyBy === '') { if (this.informationForm.dutyBy === null || this.informationForm.dutyBy === '') {
this.$modal.msgWarning("请输入责任人"); this.$modal.msgWarning("请输入责任人");
return
} }
this.isSaving = true;
updateAutomation(this.informationForm).then(res => { updateAutomation(this.informationForm).then(res => {
if (res.code === 200) { if (res.code === 200) {
this.$modal.msgSuccess("编辑成功") this.$modal.msgSuccess("编辑成功")
this.$tab.closeOpenPage({ path: "/ui-test/automation-test" });
} else { } else {
this.$modal.msgError("编辑失败") this.$modal.msgError("编辑失败")
} }
}).finally(() => {
this.isSaving = false;
}) })
}, },
} }

View File

@@ -13,7 +13,7 @@
</el-form-item> </el-form-item>
<!-- 判断样式 --> <!-- 判断样式 -->
<!-- 浏览器操作 --> <!-- 浏览器操作 -->
<div v-show="saveForm.operateType === '1'"> <div v-if="saveForm.operateType === '1'">
<!-- 打开网页 --> <!-- 打开网页 -->
<el-form-item prop="url" label="URL" v-show="saveForm.stepType === '1'"> <el-form-item prop="url" label="URL" v-show="saveForm.stepType === '1'">
<el-input v-model="saveForm.url" placeholder="URL或路径" class="input" /> <el-input v-model="saveForm.url" placeholder="URL或路径" class="input" />
@@ -22,7 +22,9 @@
</el-tooltip> </el-tooltip>
</el-form-item> </el-form-item>
<el-form-item v-show="saveForm.stepType === '1'"> <el-form-item v-show="saveForm.stepType === '1'">
<el-checkbox v-model="saveForm.isAppendPage">追加页面</el-checkbox> <el-checkbox
v-model="isAppendPageModel"
>追加页面</el-checkbox>
<el-tooltip class="item" effect="dark" content="追加页面在新的页面打开url不勾选覆盖当前url" placement="right"> <el-tooltip class="item" effect="dark" content="追加页面在新的页面打开url不勾选覆盖当前url" placement="right">
<i class="el-icon-info"></i> <i class="el-icon-info"></i>
</el-tooltip> </el-tooltip>
@@ -86,34 +88,42 @@
<!-- 根据定位方式切换 frame --> <!-- 根据定位方式切换 frame -->
<el-form-item prop="operateObject" label="操作对象" v-show="saveForm.operate === 3"> <el-form-item prop="operateObject" label="操作对象" v-show="saveForm.operate === 3">
<div class="operateObject-wrap"> <div class="operateObject-wrap">
<el-select v-model="saveForm.operateObject" class="select"> <el-select v-model="saveForm.operateObject" class="select" @change="handleOperateObjectChange">
<el-option key="1" label="元素对象" value="1"></el-option> <el-option key="1" label="元素对象" value="1"></el-option>
<el-option key="2" label="元素定位" value="2"></el-option> <el-option key="2" label="元素定位" value="2"></el-option>
</el-select> </el-select>
<!-- 元素对象 --> <!-- 元素对象 -->
<div v-show="saveForm.operateObject != '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '1'" class="operateObject-wrap">
<el-select v-model="saveForm.operateGroupId" class="select" @change="getOperateGroup"> <el-select v-model="saveForm.operateGroupId" class="select" @change="handleOperateGroupChange" placeholder="请选择元素组">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<el-select v-model="saveForm.action" class="select"> <el-select v-model="saveForm.operateElementId" class="select" placeholder="请选择元素">
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
</div> </div>
<!-- 元素定位 --> <!-- 元素定位 -->
<div v-show="saveForm.operateObject === '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '2'" class="operateObject-wrap">
<el-select v-model="saveForm.operateLocType" class="select" @change="getOperateLoc"> <el-select v-model="saveForm.operateLocType" class="select">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="id" value="id"></el-option>
</el-select> <el-option label="name" value="name"></el-option>
<el-select v-model="saveForm.operateLocValue" class="select"> <el-option label="className" value="className"></el-option>
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="tagName" value="tagName"></el-option>
<el-option label="linkText" value="linkText"></el-option>
<el-option label="partialLinkText" value="partialLinkText"></el-option>
<el-option label="css" value="css"></el-option>
<el-option label="xpath" value="xpath"></el-option>
<el-option label="table" value="table"></el-option>
<el-option label="value" value="value"></el-option>
<el-option label="index" value="index"></el-option>
</el-select> </el-select>
<el-input v-model="saveForm.operateLocValue" placeholder="请输入元素名称" class="select"></el-input>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
</div> </div>
</div> </div>
<!-- 弹窗操作 --> <!-- 弹窗操作 -->
<div v-show="saveForm.operateType === '2'"> <div v-if="saveForm.operateType === '2'">
<el-form-item label="是否输入" prop="isEnter"> <el-form-item label="是否输入" prop="isEnter">
<el-select v-model="saveForm.isEnter" class="select"> <el-select v-model="saveForm.isEnter" class="select">
<el-option key="1" label="否" value="1"></el-option> <el-option key="1" label="否" value="1"></el-option>
@@ -131,7 +141,7 @@
</el-form-item> </el-form-item>
</div> </div>
<!-- 元素操作 --> <!-- 元素操作 -->
<div v-show="saveForm.operateType === '3'"> <div v-if="saveForm.operateType === '3'">
<el-form-item label="操作" v-show="saveForm.stepType != '1'"> <el-form-item label="操作" v-show="saveForm.stepType != '1'">
<el-select v-model="saveForm.operate" class="select"> <el-select v-model="saveForm.operate" class="select">
<el-option v-for="item in actionList" :key="Number(item.value)" :label="item.label" <el-option v-for="item in actionList" :key="Number(item.value)" :label="item.label"
@@ -145,22 +155,30 @@
<el-option key="2" label="元素定位" value="2"></el-option> <el-option key="2" label="元素定位" value="2"></el-option>
</el-select> </el-select>
<!-- 元素对象 --> <!-- 元素对象 -->
<div v-show="saveForm.operateObject != '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '1'" class="operateObject-wrap">
<el-select v-model="saveForm.operateGroupId" class="select" @change="getOperateGroup"> <el-select v-model="saveForm.operateGroupId" class="select" @change="handleOperateGroupChange" placeholder="请选择元素组">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<el-select v-model="saveForm.action" class="select"> <el-select v-model="saveForm.operateElementId" class="select" placeholder="请选择元素">
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
</div> </div>
<!-- 元素定位 --> <!-- 元素定位 -->
<div v-show="saveForm.operateObject === '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '2'" class="operateObject-wrap">
<el-select v-model="saveForm.operateLocType" class="select" @change="getOperateLoc"> <el-select v-model="saveForm.operateLocType" class="select">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="id" value="id"></el-option>
</el-select> <el-option label="name" value="name"></el-option>
<el-select v-model="saveForm.operateLocValue" class="select"> <el-option label="className" value="className"></el-option>
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="tagName" value="tagName"></el-option>
<el-option label="linkText" value="linkText"></el-option>
<el-option label="partialLinkText" value="partialLinkText"></el-option>
<el-option label="css" value="css"></el-option>
<el-option label="xpath" value="xpath"></el-option>
<el-option label="table" value="table"></el-option>
<el-option label="value" value="value"></el-option>
<el-option label="index" value="index"></el-option>
</el-select> </el-select>
<el-input v-model="saveForm.operateLocValue" placeholder="请输入元素名称" class="select"></el-input>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
@@ -189,7 +207,7 @@
</div> </div>
</div> </div>
<!-- 鼠标操作 --> <!-- 鼠标操作 -->
<div v-show="saveForm.operateType === '4'"> <div v-if="saveForm.operateType === '4'">
<el-form-item label="点击方式" v-show="saveForm.stepType === '1'"> <el-form-item label="点击方式" v-show="saveForm.stepType === '1'">
<el-select v-model="saveForm.clickMethod" class="select"> <el-select v-model="saveForm.clickMethod" class="select">
<el-option key="1" label="单击(左击)" value="1"></el-option> <el-option key="1" label="单击(左击)" value="1"></el-option>
@@ -206,22 +224,30 @@
<el-option key="2" label="元素定位" value="2"></el-option> <el-option key="2" label="元素定位" value="2"></el-option>
</el-select> </el-select>
<!-- 元素对象 --> <!-- 元素对象 -->
<div v-show="saveForm.operateObject != '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '1'" class="operateObject-wrap">
<el-select v-model="saveForm.operateGroupId" class="select" @change="getOperateGroup"> <el-select v-model="saveForm.operateGroupId" class="select" @change="handleOperateGroupChange" placeholder="请选择元素组">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<el-select v-model="saveForm.action" class="select"> <el-select v-model="saveForm.operateElementId" class="select" placeholder="请选择元素">
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
</div> </div>
<!-- 元素定位 --> <!-- 元素定位 -->
<div v-show="saveForm.operateObject === '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '2'" class="operateObject-wrap">
<el-select v-model="saveForm.operateLocType" class="select" @change="getOperateLoc"> <el-select v-model="saveForm.operateLocType" class="select">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="id" value="id"></el-option>
</el-select> <el-option label="name" value="name"></el-option>
<el-select v-model="saveForm.operateLocValue" class="select"> <el-option label="className" value="className"></el-option>
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="tagName" value="tagName"></el-option>
<el-option label="linkText" value="linkText"></el-option>
<el-option label="partialLinkText" value="partialLinkText"></el-option>
<el-option label="css" value="css"></el-option>
<el-option label="xpath" value="xpath"></el-option>
<el-option label="table" value="table"></el-option>
<el-option label="value" value="value"></el-option>
<el-option label="index" value="index"></el-option>
</el-select> </el-select>
<el-input v-model="saveForm.operateLocValue" placeholder="请输入元素名称" class="select"></el-input>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
@@ -239,28 +265,36 @@
<el-option key="2" label="元素定位" value="2"></el-option> <el-option key="2" label="元素定位" value="2"></el-option>
</el-select> </el-select>
<!-- 元素对象 --> <!-- 元素对象 -->
<div v-show="saveForm.operateObject != '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '1'" class="operateObject-wrap">
<el-select v-model="saveForm.operateGroupId" class="select" @change="getOperateGroup"> <el-select v-model="saveForm.operateGroupId" class="select" @change="handleOperateGroupChange" placeholder="请选择元素组">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<el-select v-model="saveForm.action" class="select"> <el-select v-model="saveForm.operateElementId" class="select" placeholder="请选择元素">
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
</div> </div>
<!-- 元素定位 --> <!-- 元素定位 -->
<div v-show="saveForm.operateObject === '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '2'" class="operateObject-wrap">
<el-select v-model="saveForm.operateLocType" class="select" @change="getOperateLoc"> <el-select v-model="saveForm.operateLocType" class="select">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="id" value="id"></el-option>
</el-select> <el-option label="name" value="name"></el-option>
<el-select v-model="saveForm.operateLocValue" class="select"> <el-option label="className" value="className"></el-option>
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="tagName" value="tagName"></el-option>
<el-option label="linkText" value="linkText"></el-option>
<el-option label="partialLinkText" value="partialLinkText"></el-option>
<el-option label="css" value="css"></el-option>
<el-option label="xpath" value="xpath"></el-option>
<el-option label="table" value="table"></el-option>
<el-option label="value" value="value"></el-option>
<el-option label="index" value="index"></el-option>
</el-select> </el-select>
<el-input v-model="saveForm.operateLocValue" placeholder="请输入元素名称" class="select"></el-input>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
</div> </div>
<!-- 输入操作 --> <!-- 输入操作 -->
<div v-show="saveForm.operateType === '5'"> <div v-if="saveForm.operateType === '5'">
<el-form-item label="操作"> <el-form-item label="操作">
<el-select v-model="saveForm.operate" class="select"> <el-select v-model="saveForm.operate" class="select">
<el-option v-for="item in actionList" :key="Number(item.value)" :label="item.label" <el-option v-for="item in actionList" :key="Number(item.value)" :label="item.label"
@@ -279,22 +313,30 @@
<el-option key="2" label="元素定位" value="2"></el-option> <el-option key="2" label="元素定位" value="2"></el-option>
</el-select> </el-select>
<!-- 元素对象 --> <!-- 元素对象 -->
<div v-show="saveForm.operateObject != '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '1'" class="operateObject-wrap">
<el-select v-model="saveForm.operateGroupId" class="select" @change="getOperateGroup"> <el-select v-model="saveForm.operateGroupId" class="select" @change="handleOperateGroupChange" placeholder="请选择元素组">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
<el-select v-model="saveForm.action" class="select"> <el-select v-model="saveForm.operateElementId" class="select" placeholder="请选择元素">
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option>
</el-select> </el-select>
</div> </div>
<!-- 元素定位 --> <!-- 元素定位 -->
<div v-show="saveForm.operateObject === '2'" class="operateObject-wrap"> <div v-show="saveForm.operateObject === '2'" class="operateObject-wrap">
<el-select v-model="saveForm.operateLocType" class="select" @change="getOperateLoc"> <el-select v-model="saveForm.operateLocType" class="select">
<el-option v-for="item in groupList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="id" value="id"></el-option>
</el-select> <el-option label="name" value="name"></el-option>
<el-select v-model="saveForm.operateLocValue" class="select"> <el-option label="className" value="className"></el-option>
<el-option v-for="item in elementList" :key="item.id" :label="item.name" :value="item.id"></el-option> <el-option label="tagName" value="tagName"></el-option>
<el-option label="linkText" value="linkText"></el-option>
<el-option label="partialLinkText" value="partialLinkText"></el-option>
<el-option label="css" value="css"></el-option>
<el-option label="xpath" value="xpath"></el-option>
<el-option label="table" value="table"></el-option>
<el-option label="value" value="value"></el-option>
<el-option label="index" value="index"></el-option>
</el-select> </el-select>
<el-input v-model="saveForm.operateLocValue" placeholder="请输入元素名称" class="select"></el-input>
</div> </div>
</div> </div>
</el-form-item> </el-form-item>
@@ -308,7 +350,6 @@
</template> </template>
<script> <script>
import { number } from "echarts";
import { listGroup } from "../../../api/test/group"; import { listGroup } from "../../../api/test/group";
import { getElementList } from "../../../api/uiTest/elementLibrary"; import { getElementList } from "../../../api/uiTest/elementLibrary";
@@ -323,13 +364,14 @@ export default {
height: 0, height: 0,
width: 0, width: 0,
}, },
isAppendPageModel: false, // 追加页面的临时模型
saveForm: { saveForm: {
automationId: null, // 场景id automationId: null, // 场景id
name: null, // 名称 name: null, // 名称
operateType: null, // 操作类型 operateType: null, // 操作类型
stepType: null, // 步骤类型 stepType: null, // 步骤类型
url: null, // url url: null, // url
isAppendPage: 0, // 追加页面 isAppendPage: 0, // 追加页面 - 使用0而不是false作为默认值
operate: null, // 操作 operate: null, // 操作
handleId: null, // 句柄ID handleId: null, // 句柄ID
pageIndex: 1, // 网页索引号 pageIndex: 1, // 网页索引号
@@ -347,6 +389,11 @@ export default {
awaitValue: null, // 等待文本 awaitValue: null, // 等待文本
clickMethod: null, // 点击方式 clickMethod: null, // 点击方式
inputValue: null, // 输入内容 inputValue: null, // 输入内容
beforeList: [], // 初始化前置操作列表
afterList: [], // 初始化后置操作列表
errorHandling: null, // 错误处理
waitElementTime: null, // 等待元素时间
screenshotConfiguration: null // 截图配置
}, },
rules: { rules: {
name: [{ required: true, message: '请输入元素名称', trigger: 'blur' }], name: [{ required: true, message: '请输入元素名称', trigger: 'blur' }],
@@ -373,17 +420,33 @@ export default {
typeName: '', // 步骤类型 typeName: '', // 步骤类型
actionList: [], // 操作列表 actionList: [], // 操作列表
groupList: [], // 操作对象分组 groupList: [], // 操作对象分组
elementList: [], // 操作对象分组 elementList: [], // 元素列表
} }
}, },
mounted() { mounted() {
// 确保在组件挂载时saveForm中有正确的初始值
if (!this.detail || Object.keys(this.detail).length === 0) {
this.$set(this.saveForm, 'isAppendPage', 0);
}
this.initData()
this.getListGroupData() this.getListGroupData()
}, },
methods: { methods: {
// 初始化数据
initData() {
// 如果是编辑模式会通过watch detail来设置saveForm
if (!this.detail || Object.keys(this.detail).length === 0) {
// 创建模式下,确保有初始值
this.saveForm = {
...this.saveForm,
isAppendPage: 0
}
}
},
// 步骤类型变换 // 步骤类型变换
selectStepType() { selectStepType() {
// 置空操作 // 置空操作
this.saveForm.operate = null // this.saveForm.operate = null
}, },
// 改变尺寸 // 改变尺寸
changeWindSize() { changeWindSize() {
@@ -397,26 +460,81 @@ export default {
} }
}) })
}, },
// 元素对象 // 处理操作对象类型切换
getOperateGroup() { handleOperateObjectChange() {
this.getElementListData(this.saveForm.operateGroupId) if (this.saveForm.operateObject === '2') {
}, // 切换到元素定位时,清空元素对象相关的值
// 元素定位 this.saveForm.operateGroupId = null;
getOperateLoc() { this.saveForm.operateElementId = null;
this.getElementListData(this.saveForm.operateLocType) this.elementList = [];
} else if (this.saveForm.operateObject === '1') {
// 切换到元素对象时,如果之前有选择过分组,则重新获取元素列表
if (this.saveForm.operateGroupId) {
this.getElementListData(this.saveForm.operateGroupId);
}
}
}, },
// 获取元素列表 // 获取元素列表
getElementListData(id) { getElementListData(groupId) {
getElementList({ groupId: id }).then(res => { if (!groupId) return;
getElementList({ groupId: groupId }).then(res => {
if (res.code === 200) { if (res.code === 200) {
this.elementList = res.rows this.elementList = res.rows;
// 检查当前选中的operateElementId是否在新的列表中存在
if (this.saveForm.operateElementId) {
const elementExists = this.elementList.some(item => item.id === this.saveForm.operateElementId);
if (!elementExists) {
// 如果不存在,清空选择
this.saveForm.operateElementId = null;
}
}
} }
}) })
}, },
// 元素组变化时触发
handleOperateGroupChange() {
if (this.saveForm.operateGroupId) {
this.getElementListData(this.saveForm.operateGroupId);
} else {
this.elementList = [];
this.saveForm.operateElementId = null;
}
},
}, },
watch: { watch: {
// 监听操作对象类型的变化
'saveForm.operateObject': {
handler(newVal, oldVal) {
if (newVal === '1' && this.saveForm.operateGroupId) {
// 当切换到元素对象且已有分组ID时重新获取元素列表
this.getElementListData(this.saveForm.operateGroupId);
}
}
},
// 监听复选框模型的变化
isAppendPageModel: {
immediate: true,
handler(val) {
this.saveForm.isAppendPage = val ? 1 : 0;
console.log('isAppendPageModel changed:', val, this.saveForm.isAppendPage);
}
},
// 监听saveForm中isAppendPage的变化来更新复选框状态
'saveForm.isAppendPage': {
immediate: true,
handler(val) {
this.isAppendPageModel = val === 1;
console.log('isAppendPage changed:', val, this.isAppendPageModel);
}
},
detail(newVal, oldVal) { detail(newVal, oldVal) {
this.saveForm = newVal this.saveForm = newVal
// 如果是元素对象且有元素组ID获取元素列表
if (this.saveForm.operateObject === '1' && this.saveForm.operateGroupId) {
this.getElementListData(this.saveForm.operateGroupId)
}
}, },
saveForm: { saveForm: {
handler(newVal, oldVal) { handler(newVal, oldVal) {
@@ -464,7 +582,7 @@ export default {
// 判断步骤类型 // 判断步骤类型
switch (newVal.stepType) { switch (newVal.stepType) {
case '1': // 提交表单 case '1': // 提交表单
this.typeTitle = '针对属性type="submit的元素,用于提交表单数据' this.typeTitle = '针对属性type="submit"的元素,用于提交表单数据'
break break
case '2': // 下拉框操作 case '2': // 下拉框操作
this.typeTitle = '对下拉选项进行操作,可实现单选,多选,以及取消选择的操作' this.typeTitle = '对下拉选项进行操作,可实现单选,多选,以及取消选择的操作'
@@ -505,18 +623,56 @@ export default {
} }
break break
} }
// 尺寸 // 尺寸
if (newVal.windowSize != null) { if (newVal.windowSize != null) {
let result = newVal.windowSize.split(/\*/); let result = newVal.windowSize.split(/\*/);
if (result.length > 0) { if (result.length > 0) {
this.winSize.width = result[0] this.winSize.width = result[0]
this.winSize.width = result[1] this.winSize.height = result[1]
} }
} }
// 确保数组已初始化
const beforeList = this.saveForm.beforeList || [];
const afterList = this.saveForm.afterList || [];
var newList = [...beforeList, ...afterList];
// 错误处理
var param3 = {
settingType: '3',
errorHandling: this.saveForm.errorHandling,
}
newList.push(param3)
// 其他设置
var param4 = {
settingType: '4',
otherSettingsQO: {
waitElementTime: this.saveForm.waitElementTime,
screenshotConfiguration: this.saveForm.screenshotConfiguration
}
}
newList.push(param4)
this.$emit('changeSetting', newList)
}, },
deep: true deep: true
}, },
},
computed: {
appendPageValue: {
get() {
return this.saveForm && typeof this.saveForm.isAppendPage !== 'undefined' ? this.saveForm.isAppendPage === 1 : false;
},
set(value) {
if (!this.saveForm) {
this.saveForm = {};
} }
this.saveForm.isAppendPage = value ? 1 : 0;
console.log('appendPageValue changed:', value, this.saveForm.isAppendPage);
}
}
},
} }
</script> </script>

View File

@@ -96,7 +96,8 @@ export default {
}, },
// 查看 // 查看
handleClickDetail(val) { handleClickDetail(val) {
this.$tab.openPage("测试报告详情", "/ui-test/report/detail", { id: val.id }); // 打开新的测试报告详情,使用唯一的路径
this.$tab.openPage(`测试报告详情_${val.id}`, `/ui-test/report/detail/${val.id}`, { id: val.id });
}, },
// 分页 // 分页
handleSizeChange(val) { handleSizeChange(val) {

View File

@@ -105,7 +105,7 @@
</div> </div>
<div class="right-collapse"> <div class="right-collapse">
<div>{{ item.take }}</div> <div>{{ item.take }}</div>
<el-button size="mini" type="text" @click.native.stop="clickHandleScreenshot">截图</el-button> <el-button v-if="item.screenshot" size="mini" type="text" @click.native.stop="clickHandleScreenshot(item.screenshot)">截图</el-button>
<div class="collapse-status"> <div class="collapse-status">
<div v-if="item.executionFlag === '1'">成功</div> <div v-if="item.executionFlag === '1'">成功</div>
<div v-if="item.executionFlag === '0'">未执行</div> <div v-if="item.executionFlag === '0'">未执行</div>
@@ -148,7 +148,7 @@
</div> </div>
<div class="right-collapse"> <div class="right-collapse">
<div>{{ item.take }}</div> <div>{{ item.take }}</div>
<el-button size="mini" type="text" @click.native.stop="clickHandleScreenshot">截图</el-button> <el-button v-if="item.screenshot" size="mini" type="text" @click.native.stop="clickHandleScreenshot(item.screenshot)">截图</el-button>
<div class="collapse-status"> <div class="collapse-status">
<div v-if="item.executionFlag === '1'">成功</div> <div v-if="item.executionFlag === '1'">成功</div>
<div v-if="item.executionFlag === '0'">未执行</div> <div v-if="item.executionFlag === '0'">未执行</div>
@@ -191,7 +191,7 @@
</div> </div>
<div class="right-collapse"> <div class="right-collapse">
<div>{{ item.take }}</div> <div>{{ item.take }}</div>
<el-button size="mini" type="text" @click.native.stop="clickHandleScreenshot">截图</el-button> <el-button v-if="item.screenshot" size="mini" type="text" @click.native.stop="clickHandleScreenshot(item.screenshot)">截图</el-button>
<div class="collapse-status"> <div class="collapse-status">
<div v-if="item.executionFlag === '1'">成功</div> <div v-if="item.executionFlag === '1'">成功</div>
<div v-if="item.executionFlag === '0'">未执行</div> <div v-if="item.executionFlag === '0'">未执行</div>
@@ -219,12 +219,25 @@
</el-tab-pane> </el-tab-pane>
</el-tabs> </el-tabs>
</div> </div>
<!-- 图片预览对话框 -->
<el-dialog :visible.sync="dialogVisible" append-to-body width="80%" :close-on-click-modal="false">
<div v-loading="imageLoading" style="min-height: 200px;">
<el-image
v-if="previewUrl"
style="width: 100%"
:src="previewUrl"
:preview-src-list="[previewUrl]"
fit="contain"
/>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script> <script>
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import { getTestReportDetail } from '../../../api/uiTest/testReport'; import { getTestReportDetail, getScreenshot } from '../../../api/uiTest/testReport';
export default { export default {
name: "TestReportDetail", name: "TestReportDetail",
@@ -233,10 +246,13 @@ export default {
return { return {
activeName: 'first', activeName: 'first',
uiSceneStepsAllReportVOList: [], // 全部 uiSceneStepsAllReportVOList: [], // 全部
uiSceneStepsErrorReportVOList: [{ title: 'aaa', contentList: [{}, {}] }, { title: 'bbb', contentList: [{}, {}] }], // 失败 uiSceneStepsErrorReportVOList: [], // 失败
uiSceneStepsNotReportVOList: [{ title: 'aaa', contentList: [{}, {}] }, { title: 'bbb', contentList: [{}, {}] }], // 未执行 uiSceneStepsNotReportVOList: [], // 未执行
active: 'first', active: 'first',
detailData: {}, // 详情内容 detailData: {}, // 详情内容
dialogVisible: false, // 图片预览对话框是否显示
previewUrl: '', // 预览图片的URL
imageLoading: false, // 图片加载状态
} }
}, },
mounted() { mounted() {
@@ -246,7 +262,7 @@ export default {
handleClick() { }, handleClick() { },
// 获取详情 // 获取详情
getDetailDAta() { getDetailDAta() {
getTestReportDetail(this.$route.query.id).then(res => { getTestReportDetail(this.$route.params.id).then(res => {
if (res.code === 200) { if (res.code === 200) {
this.detailData = res.data this.detailData = res.data
this.uiSceneStepsAllReportVOList = res.data.uiSceneStepsAllReportVOList this.uiSceneStepsAllReportVOList = res.data.uiSceneStepsAllReportVOList
@@ -255,18 +271,40 @@ export default {
} }
}) })
}, },
// 截图 // 截图预览
clickHandleScreenshot() { async clickHandleScreenshot(screenshot) {
console.log('截图') if (screenshot) {
try {
this.imageLoading = true;
this.dialogVisible = true;
// 获取图片数据
const response = await getScreenshot(screenshot);
// 创建 Blob URL
const blob = new Blob([response], { type: 'image/png' });
if (this.previewUrl) {
URL.revokeObjectURL(this.previewUrl); // 释放之前的 URL
}
this.previewUrl = URL.createObjectURL(blob);
} catch (error) {
this.$message.error('图片加载失败');
this.dialogVisible = false;
} finally {
this.imageLoading = false;
}
}
}, },
},
beforeDestroy() {
// 清理 Blob URL
if (this.previewUrl) {
URL.revokeObjectURL(this.previewUrl);
}
} }
} }
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
.report-detail { .report-detail {
padding: 20px; padding: 20px;