Compare commits

...

63 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
fa30bb8bc5 执行高级设置 断言和数据提取 2025-05-07 16:41:21 +08:00
d1b00e3ebf 解决场景详情显示bug 2025-05-07 16:41:20 +08:00
106 changed files with 5810 additions and 633 deletions

View File

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

View File

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

View File

@@ -8,6 +8,7 @@ import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Date;
import org.apache.commons.lang3.time.DateFormatUtils;
@@ -232,4 +233,14 @@ public class DateUtils extends org.apache.commons.lang3.time.DateUtils
}
return endTime.getTime() - startTime.getTime();
}
/**
* 格式化日期为 yyyy-MM-dd HH:mm:ss
*/
public static String formatDateTime(Date date) {
return date.toInstant()
.atZone(ZoneId.systemDefault())
.toLocalDateTime()
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
}
}

View File

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

View File

@@ -5,6 +5,8 @@ import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @author liangdaliang
@@ -66,6 +68,32 @@ public class MySQLExecutor {
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) {
// String url = "jdbc:mysql://47.103.142.5:3306/cmcf-test";
// List<String> columnNameList = new ArrayList<>();

View File

@@ -1,13 +1,17 @@
package com.test.common.utils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.openqa.selenium.*;
import org.openqa.selenium.interactions.Actions;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.WebDriverWait;
import java.io.File;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.List;
import java.util.Set;
@@ -368,10 +372,52 @@ public class SeleniumUtils {
return ((TakesScreenshot) driver).getScreenshotAs(OutputType.BYTES);
}
// 截屏并保存为文件
/**
* 截屏并保存为文件
* @param filePath 保存路径的基础目录
* @return 保存后的文件完整路径
*/
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() {
@@ -867,4 +913,41 @@ public class SeleniumUtils {
driver.get(url);
}
/**
* 数据提取提取窗口信息
* @param windowType
* @return
*/
public String extractWindowInformation(String windowType) {
switch (windowType) {
case "1": // 窗口 Handle
return driver.getWindowHandle();
case "2": // 网页标题
return driver.getTitle();
default:
throw new IllegalArgumentException("Invalid windowType: " + windowType);
}
}
/**
* 查找多个元素
* @param locType
* @param locValue
* @return
*/
public List<WebElement> findElements(String locType, String locValue) {
return driver.findElements(getLocator(locType, locValue));
}
/**
* 获取网页标题
* @return
*/
public String getTitle(){
return driver.getTitle();
}
}

View File

@@ -1,7 +1,12 @@
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.TestCase;
import com.test.test.domain.qo.PerformanceTestQO;
@@ -12,16 +17,12 @@ import com.test.test.service.ITestCaseService;
import com.test.test.task.DynamicTaskManager;
import jakarta.servlet.http.HttpServletResponse;
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.Value;
import org.springframework.web.bind.annotation.*;
import com.test.common.annotation.Log;
import com.test.common.core.controller.BaseController;
import com.test.common.core.domain.AjaxResult;
import com.test.common.enums.BusinessType;
import com.test.common.utils.poi.ExcelUtil;
import com.test.common.core.page.TableDataInfo;
import java.util.List;
import java.util.concurrent.CompletableFuture;
/**
* 性能测试Controller
@@ -119,6 +120,7 @@ public class PerformanceTestController extends BaseController {
@PostMapping("/addAndExecute")
public AjaxResult addAndExecute(@RequestBody PerformanceTestQO performanceTestQO) {
try {
String createBy = getLoginUser().getUsername();
Long l = performanceTestService.insertPerformanceTest(performanceTestQO);
// 获取新增的任务完整信息
PerformanceTestVO newTaskVo = performanceTestService.selectPerformanceTestById(l);
@@ -133,8 +135,10 @@ public class PerformanceTestController extends BaseController {
}
// 执行性能测试
try {
Long l1 = performanceTestCaseReportService.executePerformanceTestAndReport(l, jmeterHomePath, 2);
return success(l1);
CompletableFuture.runAsync(() -> {
performanceTestCaseReportService.executePerformanceTestAndReport(l, jmeterHomePath, 2, createBy);
});
return toAjax(true);
} catch (Exception e) {
log.error("执行失败!", e);
return error("执行失败!");
@@ -157,7 +161,8 @@ public class PerformanceTestController extends BaseController {
Long l = performanceTestService.updatePerformanceTest(performanceTestQO);
PerformanceTestVO updatedTask = performanceTestService.selectPerformanceTestById(performanceTestQO.getId());
// 如果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()) {
PerformanceTest entity = new PerformanceTest();
entity.setId(updatedTask.getId());
@@ -178,6 +183,7 @@ public class PerformanceTestController extends BaseController {
@Log(title = "性能测试", businessType = BusinessType.UPDATE)
@PutMapping("/editAndExecute")
public AjaxResult editAndExecute(@RequestBody PerformanceTestQO performanceTestQO) {
String createBy = getLoginUser().getUsername();
try {
PerformanceTestVO originalTask = performanceTestService.selectPerformanceTestById(performanceTestQO.getId());
Long l = performanceTestService.updatePerformanceTest(performanceTestQO);
@@ -193,11 +199,13 @@ public class PerformanceTestController extends BaseController {
dynamicTaskManager.updateTask(entity);
}
try {
Long l1 = performanceTestCaseReportService.executePerformanceTestAndReport(l, jmeterHomePath, 2);
return success(l1);
CompletableFuture.runAsync(() -> {
performanceTestCaseReportService.executePerformanceTestAndReport(l, jmeterHomePath, 2, createBy);
});
return toAjax(true);
} catch (Exception e) {
log.error("执行失败!", e);
return error("执行失败!");
return error("执行失败!"+e);
}
} catch (Exception e) {
log.error("修改并执行失败!", e);
@@ -222,7 +230,10 @@ public class PerformanceTestController extends BaseController {
// @PreAuthorize("@ss.hasPermi('system:test:remove')")
@GetMapping("/executeNow")
public AjaxResult executeNow(@RequestParam Long id) {
Long l1 = performanceTestCaseReportService.executePerformanceTestAndReport(id, jmeterHomePath, 2);
return success(l1);
String createBy = getLoginUser().getUsername();
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.enums.BusinessType;
import com.test.common.utils.DateUtils;
import com.test.common.utils.SecurityUtils;
import com.test.common.utils.uuid.IdUtils;
import com.test.test.domain.TestCase;
import com.test.test.domain.TestPlanCase;
@@ -127,6 +128,7 @@ public class TestCaseController extends BaseController {
@PostMapping("/runTestPlanCase")
public AjaxResult runTestPlanCase(@RequestBody TestPlanCase testPlanCase) {
String caseSid = IdUtils.simpleUUID();
testPlanCase.setExecuteBy(SecurityUtils.getUsername());
// 异步执行任务
CompletableFuture.runAsync(() -> {
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.TestPlanListQO;
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 jakarta.annotation.Resource;
import java.util.List;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
@@ -79,4 +82,20 @@ public class TestPlanController extends BaseController {
public AjaxResult edit(@RequestBody TestPlan 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.enums.BusinessType;
import com.test.common.utils.poi.ExcelUtil;
import com.test.test.domain.TestPlan;
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.TestPlanDefectRelQO;
import com.test.test.domain.vo.TestPlanDefectVo;
@@ -92,4 +95,26 @@ public class TestPlanDefectController extends BaseController
{
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.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.vo.TestPlanProjectVo;
import com.test.test.service.ITestPlanProjectService;
@@ -36,4 +38,34 @@ public class TestPlanProjectController extends BaseController {
List<TestPlanProjectVo> list = testPlanProjectService.selectTestPlanProjectList(qo.getId());
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.page.TableDataInfo;
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.TestReportAddQO;
import com.test.test.domain.vo.TestReportVo;
@@ -12,7 +13,7 @@ import com.test.test.service.ITestReportService;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
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.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -37,7 +38,7 @@ public class TestReportController extends BaseController {
* @return
*/
@PostMapping("/reportList")
public TableDataInfo list(@RequestBody IDQO qo) {
public TableDataInfo list(@Validated IDQO qo) {
startPage();
List<TestReportVo> list = testReportService.selectTestReportList(qo.getId());
return getDataTable(list);
@@ -53,4 +54,36 @@ public class TestReportController extends BaseController {
public AjaxResult addTestReport(@RequestBody TestReportAddQO 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.ITestTaskResultService;
import com.test.test.service.ITestTaskService;
import com.test.test.task.TestTaskManager;
import jakarta.annotation.Resource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
@@ -42,6 +43,9 @@ public class TestTaskController extends BaseController {
@Resource
private ITestTaskLogService taskLogService;
@Resource
private TestTaskManager testTaskManager;
/**
* 查询定时任务列表
*/
@@ -90,9 +94,24 @@ public class TestTaskController extends BaseController {
@Log(title = "定时任务", businessType = BusinessType.INSERT)
@PostMapping("/add")
public AjaxResult add(@RequestBody TestTask testTask) {
try {
testTask.setCreateBy(getLoginUser().getUsername());
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)
@PostMapping("/edit")
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)
@PostMapping("/del")
public AjaxResult remove(@RequestBody IDQO qo) {
try {
// 先从定时任务管理器中移除
testTaskManager.removeTask(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);
}
}

View File

@@ -1,14 +1,20 @@
package com.test.test.controller;
import java.util.List;
import java.util.Map;
import com.test.common.utils.SeleniumUtils;
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.UiAutomationQO;
import com.test.test.service.IUiAutomationService;
import com.test.test.service.IUiSceneStepsService;
import lombok.extern.slf4j.Slf4j;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import com.test.common.annotation.Log;
import com.test.common.core.controller.BaseController;
@@ -110,11 +116,12 @@ public class UiAutomationController extends BaseController
public AjaxResult executeStep(@RequestParam Long id)
{
try{
String triggerMode = "1"; //手动执行
log.info("执行完成!");
return success(uiSceneStepsService.executeStep(id));
return success(uiSceneStepsService.executeStep(id,triggerMode));
} catch (Exception e) {
log.error("执行完成!",e);
return error("执行完成!");
log.error("执行错误!",e);
return error("执行错误!"+e.getMessage());
}
}
}

View File

@@ -1,5 +1,9 @@
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 com.test.test.domain.UiReport;
@@ -7,14 +11,7 @@ import com.test.test.service.IUiReportService;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
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 org.springframework.web.bind.annotation.*;
import com.test.common.annotation.Log;
import com.test.common.core.controller.BaseController;
import com.test.common.core.domain.AjaxResult;
@@ -70,4 +67,28 @@ public class UiReportController extends BaseController
{
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;
/** 开始时间 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "开始时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date startTime;
/** 结束时间 */
@JsonFormat(pattern = "yyyy-MM-dd")
@Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
private Date endTime;
/** 耗时(毫秒) */

View File

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

View File

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

View File

@@ -0,0 +1,21 @@
package com.test.test.domain.qo;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class AssertionConfigQO {
/** 弹窗文本数组 */
private List<AssertionQO> popupTexts = new ArrayList<>();
/** 元素断言数组 */
private List<AssertionQO> elementAssertions = new ArrayList<>();
/** 下拉框数组 */
private List<AssertionQO> dropdownBoxes = new ArrayList<>();
/** 网页标题数组 */
private List<AssertionQO> webTitles = new ArrayList<>();
}

View File

@@ -18,10 +18,10 @@ public class AssertionQO {
private String operateObject;
/** 元素库名称 */
private Integer operateGroupId;
private Long operateGroupId;
/** 元素id */
private Integer operateElementId;
private Long operateElementId;
/** 元素定位类型 id css ……*/
private String operateLocType;
@@ -52,5 +52,5 @@ public class AssertionQO {
private String isFailedAbort;
/**是否禁用 0否 1是*/
private String isDisabled;
private Integer isDisabled;
}

View File

@@ -0,0 +1,16 @@
package com.test.test.domain.qo;
import lombok.Data;
import java.util.List;
/**
* 数据提取配置(新版嵌套结构)
*/
@Data
public class DataExtractionConfigQO {
/** 窗口信息提取配置 */
private List<DataExtractionQO> windowExtractions;
/** 元素信息提取配置 */
private List<DataExtractionQO> elementExtractions;
}

View File

@@ -8,14 +8,14 @@ import lombok.Data;
@Data
public class DataExtractionQO {
/** 1提取元素信息 2提取窗口信息 */
/** 1提取窗口信息 2提取元素信息 */
private String informationType;
/** 1普通对象(store) 2元素文本(storeText) 3元素值(storeValue) 4元素属性(storeAttribute)
/** informationType为2时 1普通对象(store) 2元素文本(storeText) 3元素值(storeValue) 4元素属性(storeAttribute)
* 5CSS属性(storeCssAttribute) 6匹配 xpath 的元素数量(storeXpathCount) */
private String elementType;
/** 1窗口 Handle(storeWindowHandle) 2网页标题(storeTitle) */
/** informationType为1时 1窗口 Handle(storeWindowHandle) 2网页标题(storeTitle) */
private String windowType;
/**值 */
@@ -26,10 +26,10 @@ public class DataExtractionQO {
private String operateObject;
/** 元素库名称 */
private Integer operateGroupId;
private Long operateGroupId;
/** 元素id */
private Integer operateElementId;
private Long operateElementId;
/** 元素定位类型 id css ……*/
private String operateLocType;
@@ -37,11 +37,11 @@ public class DataExtractionQO {
/** 元素定位值 */
private String operateLocValue;
/**元素属性*/
/** 普通对象 元素属性 元素css属性*/
private String elementAttribute;
/**是否禁用 0否 1是 */
private String isDisabled;
private Integer isDisabled;
}

View File

@@ -3,6 +3,6 @@ package com.test.test.domain.qo;
import lombok.Data;
@Data
public class IDQO {
public class IDQO extends InheritQO{
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

@@ -56,16 +56,13 @@ public class UiHighSettingQO extends BaseEntity
/** 是否删除 */
private Integer delFlag;
/** 等待元素超时时间 */
private Integer waitElementTime;
/** 截图配置 */
private Integer screenshotConfiguration;
/**其他设置*/
OtherSettingsQO otherSettingsQO = new OtherSettingsQO();
/**数据提取*/
private List<DataExtractionQO> dataExtractionQOList = new ArrayList<>();
private DataExtractionConfigQO dataExtractionQOList = new DataExtractionConfigQO();
/**断言*/
private List<AssertionQO> assertionQOList = new ArrayList<>();
private AssertionConfigQO assertionQOList = new AssertionConfigQO();
}

View File

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

View File

@@ -0,0 +1,25 @@
package com.test.test.domain.vo;
import lombok.Data;
/**
* 断言报告结果
*/
@Data
public class AssertionReportVO {
/**
* 断言名称
*/
private String name;
/**
* 错误信息
*/
private String errorInfo;
/**
* 是否成功 0否 1是
*/
private String isSuccess;
}

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

View File

@@ -1,13 +1,10 @@
package com.test.test.domain.vo;
import com.test.common.annotation.Excel;
import com.test.common.core.domain.BaseEntity;
import com.test.test.domain.qo.AssertionConfigQO;
import com.test.test.domain.qo.AssertionQO;
import com.test.test.domain.qo.DataExtractionQO;
import com.test.test.domain.qo.DataExtractionConfigQO;
import com.test.test.domain.qo.OtherSettingsQO;
import lombok.Data;
import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import java.util.ArrayList;
import java.util.List;
@@ -48,10 +45,10 @@ public class UiHighSettingVO
private Integer isDisabled;
/**断言*/
private List<AssertionQO> assertionQOList = new ArrayList<>();
private AssertionConfigQO assertionQOList = new AssertionConfigQO();
/**数据提取*/
private List<DataExtractionQO> dataExtractionQOList = new ArrayList<>();
private DataExtractionConfigQO dataExtractionQOList =new DataExtractionConfigQO();
/**其他设置*/
private OtherSettingsQO otherSettingsQO;

View File

@@ -44,10 +44,10 @@ public class UiSceneStepsVO
private String operateObject;
/** 元素库名称 */
private Integer operateGroupId;
private Long operateGroupId;
/** 元素id */
private Integer operateElementId;
private Long operateElementId;
/** 元素定位类型 */
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.qo.TestReportAddQO;
import java.util.List;
/**
@@ -28,6 +29,14 @@ public interface TestCaseLogMapper
*/
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;
import com.test.test.domain.TestCase;
import com.test.test.domain.TestDefect;
import com.test.test.domain.qo.TestCaseListQO;
import java.util.List;
@@ -49,4 +50,9 @@ public interface TestCaseMapper
* 批量删除用例
*/
int deleteTestCaseByIds(Long[] ids);
/**
* 查询关联的用例列表
*/
List<TestCase> selectRelateCaseList(Long projectId);
}

View File

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

View File

@@ -1,5 +1,6 @@
package com.test.test.mapper;
import com.test.test.domain.TestPlan;
import com.test.test.domain.TestPlanDefect;
import com.test.test.domain.qo.TestPlanDefectQO;
import com.test.test.domain.vo.TestPlanDefectVo;
@@ -67,4 +68,11 @@ public interface TestPlanDefectMapper
* @return
*/
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.qo.TestPlanListQO;
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;
public interface TestPlanMapper {
/**
@@ -41,4 +42,18 @@ public interface TestPlanMapper {
* @return
*/
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
*/
int updateTestProject(TestProject testProject);
/**
* 查询缺陷关联的测试计划
* @param defectId
* @return
*/
List<TestProject> selectRelateProjectList(Long defectId);
}

View File

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

View File

@@ -20,4 +20,18 @@ public interface TestReportMapper {
* @return
*/
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 jmeterHomePath
* @param triggerType 触发方式1-定时任务2-手动
* @param createBy 创建人
* @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;
import com.test.test.domain.TestPlan;
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.TestPlanDefectRelQO;
import com.test.test.domain.vo.TestPlanDefectVo;
@@ -62,4 +65,18 @@ public interface ITestPlanDefectService
* @return 结果
*/
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;
import com.test.test.domain.TestCase;
import com.test.test.domain.TestDefect;
import com.test.test.domain.vo.TestPlanProjectVo;
import java.util.List;
@@ -14,4 +16,25 @@ public interface ITestPlanProjectService {
* @return
*/
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.TestPlanListQO;
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;
/**
@@ -41,4 +43,18 @@ public interface ITestPlanService {
* @return
*/
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
*/
public int addTestReport(TestReportAddQO testReportAddQO);
/**
* 更新执行用例测试报告
* @param testReport
* @return
*/
public int updateExecuteCaseReport(TestReport testReport);
/**
* 查询测试报告详情
* @param id
* @return
*/
public TestReport selectCaseTestReportById(Long id);
}

View File

@@ -19,6 +19,6 @@ public interface IUiSceneStepsService
* @param automationId 场景id
* @return
*/
Map<String,Object> executeStep(Long automationId);
Map<String,Object> executeStep(Long automationId,String triggerMode);
}

View File

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

View File

@@ -229,13 +229,15 @@ public class TestCaseServiceImpl implements ITestCaseService
for (TestPlanCase input : testPlanCaseList) {
boolean isSuccess = true;
input.setExecuteTime(DateUtils.getNowDate());
input.setExecuteBy(SecurityUtils.getUsername());
input.setExecuteBy(testPlanCase.getExecuteBy());
TestCase testCase = this.selectTestCaseById(input.getCaseId());
Long id = input.getCaseId();
Map<String, String> contextResultMap = new HashMap<>();
testCase.setContextResultMap(contextResultMap);
TestCaseLog testCaseLog = new TestCaseLog();
testCaseLog.setCaseId(id);
testCaseLog.setPlanId(testPlanCase.getPlanId());
testCaseLog.setType(testPlanCase.getType());
testCaseLog.setOperTime(DateUtils.getNowDate());
testCaseLog.setOperType("执行");
testCaseLog.setCaseSid(caseSid);
@@ -290,13 +292,14 @@ public class TestCaseServiceImpl implements ITestCaseService
}
if (isSuccess) {
testCaseLog.setOperDetail("成功");
input.setExecuteResult("成功");
input.setExecuteResult("1");
} else {
testCaseLog.setOperDetail("失败");
input.setExecuteResult("失败");
input.setExecuteResult("2");
}
testCaseLogMapper.insertTestCaseLog(testCaseLog);
testPlanCaseMapper.updateTestPlanCase(input);
System.out.println("111111111111111111111111");
}
testPlan.setUpdateTime(DateUtils.getNowDate());
testPlan.setStatus("2");
@@ -339,7 +342,8 @@ public class TestCaseServiceImpl implements ITestCaseService
useTime = Long.valueOf(resultMap.get("costMiliseconds"));
}
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("失败");
testCaseResultMapper.insertTestCaseResult(testCaseResult);
return false;
@@ -393,7 +397,10 @@ public class TestCaseServiceImpl implements ITestCaseService
// sql查询特有字段结果存储
Map<String, String> sqlSpecialResultMap = new HashMap<>();
// 获取所有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)) {
SqlResult sqlResult = new SqlResult();
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) {
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);
Type ruleListType = new TypeToken<List<ExtractionRule>>() {}.getType();
// 将 JSON 字符串解析为 List<ExtractionRule>

View File

@@ -15,6 +15,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -216,14 +217,23 @@ public class TestCaseStepServiceImpl implements ITestCaseStepService {
JmeterRequest jmeterRequest = new JmeterRequest();
jmeterRequest.setId(id);
jmeterRequest.setUrl(url);
jmeterRequest.setPort(testCaseStep.getApiPort());
jmeterRequest.setPort(testCaseStep.getApiPort() == null ? 80 : testCaseStep.getApiPort());
jmeterRequest.setMethod(testCaseStep.getRequestMethod());
jmeterRequest.setRequestBody(testCaseStep.getRequestBody());
jmeterRequest.setRequestParams(testCaseStep.getRequestParams());
jmeterRequest.setRequestHeader(testCaseStep.getRequestHeader());
jmeterRequest.setJmeterHomePath(jmeterHomePath);
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;
}

View File

@@ -1,11 +1,15 @@
package com.test.test.service.impl;
import com.test.common.utils.DateUtils;
import com.test.test.domain.TestPlan;
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.TestPlanDefectRelQO;
import com.test.test.domain.vo.TestPlanDefectVo;
import com.test.test.mapper.TestPlanDefectMapper;
import com.test.test.mapper.TestProjectMapper;
import com.test.test.service.ITestPlanDefectService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
@@ -25,6 +29,9 @@ public class TestPlanDefectServiceImpl implements ITestPlanDefectService
@Resource
private TestPlanDefectMapper testPlanDefectMapper;
@Resource
private TestProjectMapper testProjectMapper;
/**
* 查询测试计划测试缺陷关联
*
@@ -118,4 +125,24 @@ public class TestPlanDefectServiceImpl implements ITestPlanDefectService
{
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;
import com.test.test.domain.TestCase;
import com.test.test.domain.TestDefect;
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.service.ITestPlanProjectService;
import jakarta.annotation.Resource;
@@ -18,6 +22,12 @@ public class TestPlanProjectServiceImpl implements ITestPlanProjectService {
@Resource
private TestProjectPlanMapper testProjectPlanMapper;
@Resource
private TestCaseMapper testCaseMapper;
@Resource
private TestDefectMapper testDefectMapper;
/**
* 查询测试计划需求关联列表
* @param planId
@@ -27,4 +37,34 @@ public class TestPlanProjectServiceImpl implements ITestPlanProjectService {
public List<TestPlanProjectVo> selectTestPlanProjectList(Long 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.TestPlanListQO;
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.TestProjectPlanMapper;
import com.test.test.service.ITestPlanService;
import jakarta.annotation.Resource;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -82,4 +87,39 @@ public class TestPlanServiceImpl implements ITestPlanService {
testPlan.setUpdateTime(DateUtils.getNowDate());
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;
import com.alibaba.fastjson2.JSONObject;
import com.test.common.utils.DateUtils;
import com.test.common.utils.SecurityUtils;
import com.test.common.utils.StringUtils;
import com.test.common.utils.bean.BeanUtils;
import com.test.test.domain.TestCaseResult;
import com.test.test.domain.TestReport;
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.mapper.TestCaseLogMapper;
import com.test.test.mapper.TestCaseResultMapper;
import com.test.test.mapper.TestPlanReportMapper;
import com.test.test.mapper.TestReportMapper;
import com.test.test.service.ITestReportService;
@@ -29,6 +35,12 @@ public class TestReportServiceImpl implements ITestReportService {
@Resource
private TestPlanReportMapper testPlanReportMapper;
@Resource
private TestCaseLogMapper testCaseLogMapper;
@Resource
private TestCaseResultMapper testCaseResultMapper;
/**
* 查询测试报告列表
* @param planId
@@ -46,11 +58,33 @@ public class TestReportServiceImpl implements ITestReportService {
*/
@Override
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();
if (count == passNum) {
testReport.setResult("1");
}
BeanUtils.copyProperties(testReportAddQO,testReport);
testReport.setSerialNumber(generateSerialNumber());
testReport.setResult("0");
testReport.setStatus("0");
testReport.setReport(reportJson);
testReport.setCreateTime(DateUtils.getNowDate());
testReport.setDelFlag("0");
testReport.setUpdateBy(SecurityUtils.getUsername());
@@ -61,6 +95,27 @@ public class TestReportServiceImpl implements ITestReportService {
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

View File

@@ -70,7 +70,16 @@ public class TestTaskServiceImpl implements ITestTaskService {
*/
@Override
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
public int updateTestTask(TestTask testTask) {
String operUser = testTask.getCreateBy();
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("定时任务已删除,不能执行!");
return false;
}
if (triggerType == 2) {
TestTaskLog testTaskLog = new TestTaskLog();
testTaskLog.setTaskId(id);
testTaskLog.setOperType("执行");
@@ -121,6 +140,7 @@ public class TestTaskServiceImpl implements ITestTaskService {
testTaskLog.setOperUser(operUser);
testTaskLog.setOperTime(DateUtils.getNowDate());
testTaskLogMapper.insertTestTaskLog(testTaskLog);
}
TestTaskResult testTaskResult = new TestTaskResult();
testTaskResult.setTaskId(id);
testTaskResult.setTriggerTime(DateUtils.getNowDate());
@@ -148,7 +168,8 @@ public class TestTaskServiceImpl implements ITestTaskService {
// 开始执行定时任务逻辑。。。
if (triggerType == 1) {
// 添加定时任务定时执行
taskManagerService.addTask(testTask, jmeterHomePath, testTaskCaseList, testTaskResult);
// taskManagerService.addTask(testTask, jmeterHomePath, testTaskCaseList, testTaskResult);
taskManagerService.executeTaskWithTestCases(testTask, jmeterHomePath, testTaskCaseList, testTaskResult);
} else {
// 手动立即执行
taskManagerService.executeTaskWithTestCases(testTask, jmeterHomePath, testTaskCaseList, testTaskResult);

View File

@@ -76,16 +76,44 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
new TypeReference<List<AssertionQO>>() {
}
);
uiHighSettingVO.setAssertionQOList(assertionList);
List<AssertionQO> popupTexts = new ArrayList<>();
List<AssertionQO> elementAssertions = new ArrayList<>();
List<AssertionQO> dropdownBoxes = new ArrayList<>();
List<AssertionQO> webTitles = new ArrayList<>();
for (AssertionQO extractionQO : assertionList) {
if ("1".equals(extractionQO.getAssertionType())) {
popupTexts.add(extractionQO);
} else if ("2".equals(extractionQO.getAssertionType())) {
elementAssertions.add(extractionQO);
} else if ("3".equals(extractionQO.getAssertionType())) {
dropdownBoxes.add(extractionQO);
} else if ("4".equals(extractionQO.getAssertionType())) {
webTitles.add(extractionQO);
}
}
uiHighSettingVO.getAssertionQOList().setPopupTexts(popupTexts);
uiHighSettingVO.getAssertionQOList().setElementAssertions(elementAssertions);
uiHighSettingVO.getAssertionQOList().setDropdownBoxes(dropdownBoxes);
uiHighSettingVO.getAssertionQOList().setWebTitles(webTitles);
// 转换extractionDataJson 数据提取
if (StringUtils.isNotBlank(uiHighSetting.getExtractionDataJson())) {
List<DataExtractionQO> extractionData = null;
extractionData = objectMapper.readValue(
uiHighSetting.getExtractionDataJson(),
new TypeReference<List<DataExtractionQO>>(){
new TypeReference<List<DataExtractionQO>>() {
}
);
uiHighSettingVO.setDataExtractionQOList(extractionData);
List<DataExtractionQO> windowExtractions = new ArrayList<>();
List<DataExtractionQO> elementExtractions = new ArrayList<>();
for (DataExtractionQO extractionQO : extractionData) {
if ("1".equals(extractionQO.getInformationType())) {
windowExtractions.add(extractionQO);
} else if ("2".equals(extractionQO.getInformationType())) {
elementExtractions.add(extractionQO);
}
}
uiHighSettingVO.getDataExtractionQOList().setWindowExtractions(windowExtractions);
uiHighSettingVO.getDataExtractionQOList().setElementExtractions(elementExtractions);
}
// 转换otherSetting 其他设置
if (StringUtils.isNotBlank(uiHighSetting.getOtherSetting())) {
@@ -122,7 +150,25 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
new TypeReference<List<AssertionQO>>() {
}
);
uiHighSettingVO.setAssertionQOList(assertionList);
List<AssertionQO> popupTexts = new ArrayList<>();
List<AssertionQO> elementAssertions = new ArrayList<>();
List<AssertionQO> dropdownBoxes = new ArrayList<>();
List<AssertionQO> webTitles = new ArrayList<>();
for (AssertionQO extractionQO : assertionList) {
if ("1".equals(extractionQO.getAssertionType())) {
popupTexts.add(extractionQO);
} else if ("2".equals(extractionQO.getAssertionType())) {
elementAssertions.add(extractionQO);
} else if ("3".equals(extractionQO.getAssertionType())) {
dropdownBoxes.add(extractionQO);
} else if ("4".equals(extractionQO.getAssertionType())) {
webTitles.add(extractionQO);
}
}
uiHighSettingVO.getAssertionQOList().setPopupTexts(popupTexts);
uiHighSettingVO.getAssertionQOList().setElementAssertions(elementAssertions);
uiHighSettingVO.getAssertionQOList().setDropdownBoxes(dropdownBoxes);
uiHighSettingVO.getAssertionQOList().setWebTitles(webTitles);
// 转换extractionDataJson 数据提取
if (StringUtils.isNotBlank(uiHighSetting.getExtractionDataJson())) {
List<DataExtractionQO> extractionData = null;
@@ -131,7 +177,17 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
new TypeReference<List<DataExtractionQO>>() {
}
);
uiHighSettingVO.setDataExtractionQOList(extractionData);
List<DataExtractionQO> windowExtractions = new ArrayList<>();
List<DataExtractionQO> elementExtractions = new ArrayList<>();
for (DataExtractionQO extractionQO : extractionData) {
if ("1".equals(extractionQO.getInformationType())) {
windowExtractions.add(extractionQO);
} else if ("2".equals(extractionQO.getInformationType())) {
elementExtractions.add(extractionQO);
}
}
uiHighSettingVO.getDataExtractionQOList().setWindowExtractions(windowExtractions);
uiHighSettingVO.getDataExtractionQOList().setElementExtractions(elementExtractions);
}
// 转换otherSetting 其他设置
if (StringUtils.isNotBlank(uiHighSetting.getOtherSetting())) {
@@ -150,6 +206,7 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
}
return uiHighSettingVOList;
}
/**
* 查询ui自动化列表
*
@@ -187,7 +244,6 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
uiAutomation.setUpdateTime(DateUtils.getNowDate());
uiAutomation.setCreateBy(SecurityUtils.getUsername());
uiAutomation.setUpdateBy(SecurityUtils.getUsername());
// uiAutomation.setStatus("1"); //未开始
uiAutomation.setDutyBy(SecurityUtils.getUsername());
uiAutomation.setDelFlag(0);
uiAutomation.setCrontabStatus(0); //未开启
@@ -218,22 +274,50 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
uiHighSetting.setCreateBy(SecurityUtils.getUsername());
uiHighSetting.setCreateTime(DateUtils.getNowDate());
try {
List<DataExtractionQO> combinedList1 = new ArrayList<>();
// 安全添加 windowExtractions
if (uiHighSettingQO.getDataExtractionQOList() != null &&
uiHighSettingQO.getDataExtractionQOList().getWindowExtractions() != null) {
combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getWindowExtractions());
}
// 安全添加 elementExtractions
if (uiHighSettingQO.getDataExtractionQOList() != null &&
uiHighSettingQO.getDataExtractionQOList().getElementExtractions() != null) {
combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getElementExtractions());
}
//数据提取
String jsonStr = objectMapper.writeValueAsString(uiHighSettingQO.getDataExtractionQOList());
String jsonStr = objectMapper.writeValueAsString(combinedList1);
if (jsonStr != null) {
uiHighSetting.setExtractionDataJson(jsonStr);
}
//断言
jsonStr = objectMapper.writeValueAsString(uiHighSettingQO.getAssertionQOList());
List<AssertionQO> combinedList2 = new ArrayList<>();
// 安全添加 popupTexts
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getPopupTexts() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getPopupTexts());
}
// 安全添加 elementAssertions
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getElementAssertions() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getElementAssertions());
}
// 安全添加 dropdownBoxes
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getDropdownBoxes() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getDropdownBoxes());
}
// 安全添加 webTitles
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getWebTitles() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getWebTitles());
}
jsonStr = objectMapper.writeValueAsString(combinedList2);
if (jsonStr != null) {
uiHighSetting.setAssertionJson(jsonStr);
}
//其他设置
if (uiHighSettingQO.getWaitElementTime() != null || uiHighSettingQO.getScreenshotConfiguration() != null) {
OtherSettingsQO settings = new OtherSettingsQO();
settings.setWaitElementTime(uiHighSettingQO.getWaitElementTime());
settings.setScreenshotConfiguration(uiHighSettingQO.getScreenshotConfiguration());
jsonStr = objectMapper.writeValueAsString(settings);
if (uiHighSettingQO.getOtherSettingsQO() != null) {
jsonStr = objectMapper.writeValueAsString(uiHighSettingQO.getOtherSettingsQO());
uiHighSetting.setOtherSetting(jsonStr);
}
} catch (JsonProcessingException e) {
@@ -297,22 +381,51 @@ public class UiAutomationServiceImpl implements IUiAutomationService {
uiHighSetting.setCreateBy(SecurityUtils.getUsername());
uiHighSetting.setCreateTime(DateUtils.getNowDate());
try {
List<DataExtractionQO> combinedList1 = new ArrayList<>();
// 安全添加 windowExtractions
if (uiHighSettingQO.getDataExtractionQOList() != null &&
uiHighSettingQO.getDataExtractionQOList().getWindowExtractions() != null) {
combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getWindowExtractions());
}
// 安全添加 elementExtractions
if (uiHighSettingQO.getDataExtractionQOList() != null &&
uiHighSettingQO.getDataExtractionQOList().getElementExtractions() != null) {
combinedList1.addAll(uiHighSettingQO.getDataExtractionQOList().getElementExtractions());
}
//数据提取
String jsonStr = objectMapper.writeValueAsString(uiHighSettingQO.getDataExtractionQOList());
String jsonStr = objectMapper.writeValueAsString(combinedList1);
if (jsonStr != null) {
uiHighSetting.setExtractionDataJson(jsonStr);
}
//断言
jsonStr = objectMapper.writeValueAsString(uiHighSettingQO.getAssertionQOList());
// 断言
List<AssertionQO> combinedList2 = new ArrayList<>();
// 安全添加 popupTexts
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getPopupTexts() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getPopupTexts());
}
// 安全添加 elementAssertions
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getElementAssertions() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getElementAssertions());
}
// 安全添加 dropdownBoxes
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getDropdownBoxes() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getDropdownBoxes());
}
// 安全添加 webTitles
if (uiHighSettingQO.getAssertionQOList() != null &&
uiHighSettingQO.getAssertionQOList().getWebTitles() != null) {
combinedList2.addAll(uiHighSettingQO.getAssertionQOList().getWebTitles());
}
jsonStr = objectMapper.writeValueAsString(combinedList2);
if (jsonStr != null) {
uiHighSetting.setAssertionJson(jsonStr);
}
//其他设置
if (uiHighSettingQO.getWaitElementTime() != null || uiHighSettingQO.getScreenshotConfiguration() != null) {
OtherSettingsQO settings = new OtherSettingsQO();
settings.setWaitElementTime(uiHighSettingQO.getWaitElementTime());
settings.setScreenshotConfiguration(uiHighSettingQO.getScreenshotConfiguration());
jsonStr = objectMapper.writeValueAsString(settings);
if (uiHighSettingQO.getOtherSettingsQO() != null) {
jsonStr = objectMapper.writeValueAsString(uiHighSettingQO.getOtherSettingsQO());
uiHighSetting.setOtherSetting(jsonStr);
}
} catch (JsonProcessingException e) {

View File

@@ -95,7 +95,7 @@ public class UiReportServiceImpl implements IUiReportService
UiAutomation automation = uiAutomationMapper.selectUiAutomationById(automationId);
String name = automation.getName();
UiReport uiReport = new UiReport();
uiReport.setName(name + "-" + DateUtils.getTime());
uiReport.setName(name + "-" + DateUtils.formatDateTime(startTime));
uiReport.setReportType("场景");
uiReport.setCreateTime(startTime);
uiReport.setCreateBy(SecurityUtils.getUsername());

View File

@@ -1,15 +1,16 @@
package com.test.test.service.impl;
import java.util.*;
import java.util.stream.Collectors;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import com.test.common.config.TestConfig;
import com.test.common.utils.DateUtils;
import com.test.common.utils.SecurityUtils;
import com.test.common.utils.SeleniumUtils;
import com.test.common.utils.StringUtils;
import com.test.test.domain.*;
import com.test.test.domain.qo.OtherSettingsQO;
import com.test.test.domain.qo.*;
import com.test.test.domain.vo.AssertionReportVO;
import com.test.test.domain.vo.UiHighSettingVO;
import com.test.test.mapper.UiAutomationMapper;
import com.test.test.mapper.UiSceneStepsMapper;
@@ -17,22 +18,28 @@ import com.test.test.mapper.UiSceneStepsReportMapper;
import com.test.test.service.*;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import org.checkerframework.checker.units.qual.C;
import org.openqa.selenium.By;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.*;
import org.openqa.selenium.chrome.ChromeDriver;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.openqa.selenium.chrome.ChromeOptions;
import org.openqa.selenium.support.ui.Select;
import org.openqa.selenium.support.ui.UnexpectedTagNameException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.NoSuchElementException;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
@Service
@Slf4j
public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
// 添加静态Map用于存储提取的数据
private static final Map<String, Object> extractedDataMap = new HashMap<>();
@Resource
private UiSceneStepsMapper uiSceneStepsMapper;
@Resource
@@ -56,10 +63,13 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
* @param automationId
* @return
*/
public Map<String, Object> executeStep(Long automationId) {
public Map<String, Object> executeStep(Long automationId, String triggerMode) {
Map<String, Object> result = new HashMap<>();
result.put("msg", "");
result.put("id", 0L);
AtomicInteger totalAssertions = new AtomicInteger(0);
AtomicInteger successAssertions = new AtomicInteger(0);
Gson gson = new Gson();
// 1. 验证并获取步骤
List<UiSceneSteps> steps = validateAndGetSteps(automationId);
@@ -73,7 +83,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
Date startTime = DateUtils.getNowDate();
// 2. 创建执行报告
Long reportId = uiReportService.insertUiReport(startTime, automationId, "1", steps.size());
Long reportId = uiReportService.insertUiReport(startTime, automationId, triggerMode, steps.size());
UiReport uiReport = new UiReport();
result.put("id", reportId);
uiReport.setId(reportId);
@@ -84,9 +94,48 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
//创建报告步骤表
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 {
// 3. 执行所有步骤
executeAllSteps(steps, reportId);
executeAllSteps(steps, reportId, seleniumUtils);
log.info("场景执行成功: {}", automationId);
uiReport.setConsoleStr("OK");
uiReport.setStatus(3);
@@ -120,6 +169,25 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
} else if (report.getExecutionFlag().equals("0")) {
stepsNotNumber++;
}
//成功断言数
String assertionJson = report.getAssertionJson();
if (assertionJson != null && !assertionJson.trim().isEmpty()) {
try {
List<AssertionReportVO> assertions = gson.fromJson(
assertionJson,
new TypeToken<List<AssertionReportVO>>() {
}.getType()
);
int stepAssertionCount = assertions.size();
int stepSuccessCount = (int) assertions.stream()
.filter(a -> "1".equals(a.getIsSuccess()))
.count();
totalAssertions.addAndGet(stepAssertionCount);
successAssertions.addAndGet(stepSuccessCount);
} catch (Exception e) {
log.error("步骤[{}]断言数据解析失败: {}", report.getId(), assertionJson, e);
}
}
}
if (allSuccess) {
scenesSucceedNumber = 1;
@@ -127,6 +195,10 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
scenesErrorNumber = 1;
}
}
uiReport.setSuccessAssertion("0/0");
String globalAssertionResult = successAssertions.get() + "/" + totalAssertions.get();
uiReport.setSuccessAssertion(globalAssertionResult);
uiReport.setScenesSucceedNumber(scenesSucceedNumber);
uiReport.setScenesErrorNumber(scenesErrorNumber);
uiReport.setScenesNotNumber(0);
@@ -137,19 +209,41 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
stepsErrorNumber == 0 ? "0%" :
String.format("%.2f%%", (double) stepsErrorNumber / steps.size() * 100)
);
if (1 == uiReport.getStatus()){
uiReport.setStatus(4);
}
if (scenesErrorNumber > 0){
uiReport.setStatus(2);
}
uiReportService.updateUiReport(uiReport);
//修改ui_automation
UiAutomation uiAutomation1 = new UiAutomation();
uiAutomation1.setId(automationId);
uiAutomation1.setExecutionResult("2");
if (!uiAutomation.getExecutionResult().equals("2")) {
if (scenesErrorNumber == 0) {
uiAutomation1.setExecutionResult("3");
}
uiAutomation1.setUpdateTime(DateUtils.getNowDate());
uiAutomation1.setPassRate(stepsSucceedNumber == 0 ? "0%" :
String.format("%.2f%%", (double) stepsSucceedNumber / steps.size() * 100));
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;
}
@@ -162,7 +256,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/
private List<UiSceneSteps> validateAndGetSteps(Long 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)) {
throw new IllegalArgumentException("步骤不能为空");
}
@@ -196,10 +290,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*
* @param steps
*/
private void executeAllSteps(List<UiSceneSteps> steps, Long reportId) {
System.setProperty("webdriver.chrome.driver", chromeDriverPath);
WebDriver driver = new ChromeDriver();
SeleniumUtils seleniumUtils = new SeleniumUtils(driver);
private void executeAllSteps(List<UiSceneSteps> steps, Long reportId,SeleniumUtils seleniumUtils) {
Integer orderNumber = 0;
UiSceneStepsReport uiSceneStepsReport = new UiSceneStepsReport();
uiSceneStepsReport.setReportId(reportId);
@@ -214,6 +305,8 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
List<UiHighSettingVO> uiHighSettingVOList = uiAutomationService.getUiHighSettingVOList(step.getId());
executeSingleStep(step, seleniumUtils, uiSceneStepsReports.get(0).getId(), uiHighSettingVOList);
}
//关闭浏览器
// seleniumUtils.quit();
}
private void executeSingleStep(UiSceneSteps step, SeleniumUtils seleniumUtils,
@@ -263,6 +356,8 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
boolean shouldContinue = executeStepWithLog(step, seleniumUtils, sceneStepsReportId,
execution, uiHighSettingVOList);
if (!shouldContinue) {
//关闭浏览器
// seleniumUtils.quit();
throw new IllegalArgumentException("步骤执行失败,根据设置终止场景执行");
}
}
@@ -281,24 +376,35 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
private boolean executeStepWithLog(UiSceneSteps step, SeleniumUtils seleniumUtils,
Long sceneStepsReportId, StepExecution stepExecution,
List<UiHighSettingVO> uiHighSettingVOList) {
log.info("开始执行步骤:{}",step.getName());
// 1. 初始化设置对象
UiHighSettingVO errorSetting = extractErrorSetting(uiHighSettingVOList);
UiHighSettingVO otherSetting = extractOtherSetting(uiHighSettingVOList);
OtherSettingsQO otherSettingsQO = otherSetting.getOtherSettingsQO();
Integer screenshotConfiguration = otherSettingsQO.getScreenshotConfiguration(); //是否截图 1当前步骤截图 2异常截图 3 不截图
Integer screenshotConfiguration = 0;
if (otherSettingsQO != null) {
screenshotConfiguration = otherSettingsQO.getScreenshotConfiguration() == null ? 0 : otherSettingsQO.getScreenshotConfiguration();
//等待元素超时时间
Integer waitElementTime = otherSettingsQO.getWaitElementTime() == null ? 0 : otherSettingsQO.getWaitElementTime();
Integer waitElementTime = otherSettingsQO.getWaitElementTime() == null ? -1 : otherSettingsQO.getWaitElementTime();
if (waitElementTime > 0) {
// 转换为秒至少1秒
long timeoutInSeconds = Math.max(1, waitElementTime / 1000);
log.info("元素等待时间{}秒", timeoutInSeconds);
seleniumUtils.setWaitTimeout(timeoutInSeconds);
}
}
//前置操作设置
List<UiHighSettingVO> beforeSettingList = extractbeforeSetting(uiHighSettingVOList);
// 获取前置操作数据集合(判空)
Map<String, Object> beforeData = CollectionUtils.isEmpty(beforeSettingList)
? new HashMap<>()
: filterBydataExtractionQOList(beforeSettingList, seleniumUtils);
// 保存前置数据到Map中使用步骤ID作为key的一部分
extractedDataMap.put("beforeData_" + step.getId(), beforeData);
//后置操作设置
List<UiHighSettingVO> afterSettingList = extractafterSetting(uiHighSettingVOList);
//前置等待时间
int beforeAwaitTime = getAwaitTime(beforeSettingList);
//后置等待时间
@@ -316,10 +422,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
Thread.sleep(beforeAwaitTime);
}
if (screenshotConfiguration == 1) {
report.setScreenshot(seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile()));
log.info("截图成功,路径:{}", seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile()));
}
// 3. 执行具体步骤
stepExecution.execute(step, seleniumUtils);
@@ -335,7 +438,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
// 4. 错误处理
report.setLogInfo(e.toString());
report.setExecutionFlag("2");
// 根据错误处理设置决定是否继续
// 根据错误处理设置决定是否继续哦了
if ("1".equals(errorSetting.getErrorHandling())) {
continueExecution = false; // 不继续执行后续步骤
}
@@ -345,12 +448,54 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
}
log.error("步骤执行失败: {}", e.getMessage());
} finally {
if (screenshotConfiguration == 1) {
report.setScreenshot(seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile()));
log.info("截图成功,路径:{}", seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile()));
}
//获取后置操作数据集合
Map<String, Object> afterData = CollectionUtils.isEmpty(afterSettingList)
? new HashMap<>()
: filterBydataExtractionQOList(afterSettingList, seleniumUtils);
// 保存后置数据到Map中使用步骤ID作为key的一部分
extractedDataMap.put("afterData_" + step.getId(), afterData);
// 合并当前步骤的前置和后置数据
Map<String, Object> mergedData = new HashMap<>();
if (!beforeData.isEmpty()) {
mergedData.put("前置数据提取", beforeData);
}
if (!afterData.isEmpty()) {
mergedData.put("后置数据提取", afterData);
}
// 设置到报告
try {
String json = mergedData.isEmpty() ? null : new ObjectMapper().writeValueAsString(mergedData);
report.setExtractionDataJson(json);
} catch (JsonProcessingException e) {
report.setExtractionDataJson("JsonProcessingException error");
}
Map<String, Object> stringObjectMap = filterByAssertionQOList(afterSettingList, seleniumUtils);
//断言失败是否终止
String continueExecution1 = (String) stringObjectMap.get("continueExecution");
String isSuccess = (String) stringObjectMap.get("isSuccess");
if (continueExecution1.equals("1")) {
continueExecution = false;
report.setLogInfo("");
report.setExecutionFlag("2");
}
if("0".equals(isSuccess)){
report.setExecutionFlag("2");
}
List<AssertionReportVO> assertionReportVOS = (List<AssertionReportVO>) stringObjectMap.get("assertionReportVOS");
Gson gson = new Gson();
report.setAssertionJson(assertionReportVOS.isEmpty() ? null : gson.toJson(assertionReportVOS));
endTime = DateUtils.getNowDate();
// 更新报告
report.setCreateTime(startTime);
report.setUpdateTime(endTime);
report.setTake(DateUtils.differenceInMilliseconds(startTime, endTime) + "ms");
uiSceneStepsReportMapper.updateUiSceneStepsReport(report);
}
return continueExecution;
@@ -365,10 +510,523 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/
private List<UiHighSettingVO> extractbeforeSetting(List<UiHighSettingVO> settings) {
return settings.stream()
.filter(e -> "1".equals(e.getSettingType()) && e.getIsDisabled() == 0)
.filter(e -> "1".equals(e.getSettingType()) && 1 == e.getIsDisabled() )
.collect(Collectors.toList()); // 返回默认设置
}
/**
* 断言
*
* @param settingList
* @param seleniumUtils
* @return
*/
public Map<String, Object> filterByAssertionQOList(List<UiHighSettingVO> settingList, SeleniumUtils seleniumUtils) {
Map<String, Object> result = new HashMap<>();
String continueExecution = "2";
List<AssertionReportVO> assertionReports = new ArrayList<>();
String isSuccess = "1"; // 默认所有断言成功
result.put("continueExecution", continueExecution);
result.put("assertionReportVOS", assertionReports);
result.put("isSuccess", isSuccess); // 添加整体成功标志
List<UiHighSettingVO> uiHighSettingVOS = settingList.stream()
.filter(setting -> setting.getOperateType().equals("2") && 1 == setting.getIsDisabled())
.toList();
if (CollectionUtils.isEmpty(uiHighSettingVOS)) {
return result;
}
for (UiHighSettingVO uiHighSettingVO : uiHighSettingVOS) {
AssertionConfigQO assertion = uiHighSettingVO.getAssertionQOList();
List<AssertionQO> assertionQOList = new ArrayList<>();
assertionQOList.addAll(assertion.getPopupTexts());
assertionQOList.addAll(assertion.getElementAssertions());
assertionQOList.addAll(assertion.getDropdownBoxes());
assertionQOList.addAll(assertion.getWebTitles());
if (!CollectionUtils.isEmpty(assertionQOList)) {
for (AssertionQO assertionQO : assertionQOList) {
if ("0".equals(assertionQO.getIsDisabled())) {
continue;
}
AssertionReportVO report = new AssertionReportVO();
report.setName("1".equals(assertionQO.getIsFailedAbort()) ? "assert" : "verify");
report.setIsSuccess("1"); // 默认成功
try {
boolean assertionResult = false;
String errorDetail = "";
switch (assertionQO.getAssertionType()) {
case "1": // 弹窗文本断言
assertionResult = handleAlertAssertion(assertionQO, seleniumUtils, report);
break;
case "2": // 元素断言
assertionResult = handleElementAssertion(assertionQO, seleniumUtils, report);
break;
case "3": // 下拉框断言
assertionResult = handleDropdownAssertion(assertionQO, seleniumUtils, report);
break;
case "4": // 网页标题断言
assertionResult = handleTitleAssertion(assertionQO, seleniumUtils, report);
break;
default:
errorDetail = "不支持的断言类型: " + assertionQO.getAssertionType();
report.setErrorInfo(errorDetail);
report.setIsSuccess("0");
isSuccess = "0"; // 设置整体失败标志
}
if (!assertionResult) {
isSuccess = "0"; // 任何断言失败都设置整体失败标志
if ("1".equals(assertionQO.getIsFailedAbort())) {
continueExecution = "1";
result.put("continueExecution", continueExecution);
}
}
} catch (Exception e) {
report.setErrorInfo("断言执行异常: " + e.getMessage());
report.setIsSuccess("0");
isSuccess = "0"; // 异常也设置整体失败标志
log.error("断言执行异常", e);
}
assertionReports.add(report);
}
}
}
result.put("assertionReportVOS", assertionReports);
result.put("isSuccess", isSuccess); // 更新最终的整体成功标志
return result;
}
private boolean handleAlertAssertion(AssertionQO assertionQO,
SeleniumUtils seleniumUtils,
AssertionReportVO report) {
try {
// 1. 获取弹窗文本带10秒等待
String actualText = seleniumUtils.getAlertText();
String expectedText = assertionQO.getExpectations();
report.setErrorInfo("");
// 3. 文本比较
if (!actualText.equals(expectedText)) {
report.setErrorInfo(String.format(
"弹窗文本不匹配,实际值:'%s',期望值:'%s'",
actualText, expectedText
));
report.setIsSuccess("0");
return false;
}
// 4. 根据配置处理弹窗
if ("1".equals(assertionQO.getIsPopText())) {
seleniumUtils.acceptAlert(); // 点击确认
log.info("弹窗断言成功,已点击确认");
} else {
log.info("弹窗断言成功,未执行点击操作");
}
report.setIsSuccess("1");
return true;
} catch (NoAlertPresentException e) {
report.setErrorInfo("弹窗未出现:" + e.getMessage());
report.setIsSuccess("0");
return false;
} catch (Exception e) {
report.setErrorInfo("弹窗处理异常:" + e.getMessage());
report.setIsSuccess("0");
return false;
}
}
/**
* 处理元素断言
*
* @param assertionQO 断言配置
* @param seleniumUtils Selenium工具类
* @param report 断言报告
* @return 断言是否成功
*/
private boolean handleElementAssertion(AssertionQO assertionQO,
SeleniumUtils seleniumUtils,
AssertionReportVO report) {
WebElement element = null;
String locatorDescription = "";
report.setErrorInfo("");
try {
// 1. 获取元素定位信息
if ("1".equals(assertionQO.getOperateObject())) {
// 从数据库查询元素信息
//元素定位
UiElement uiElement = uiElementService.selectUiElementById(assertionQO.getOperateElementId());
if (uiElement == null) {
report.setErrorInfo("元素ID " + assertionQO.getOperateElementId() + " 不存在");
report.setIsSuccess("0");
return false;
}
element = seleniumUtils.findElement(uiElement.getLocType(), uiElement.getElementLoc());
locatorDescription = String.format("元素ID:%d (%s=%s)",
assertionQO.getOperateElementId(),
uiElement.getLocType(),
uiElement.getElementLoc());
} else if ("2".equals(assertionQO.getOperateObject())) {
// 直接使用配置的定位信息
element = seleniumUtils.findElement(
assertionQO.getOperateLocType(),
assertionQO.getOperateLocValue()
);
locatorDescription = String.format("直接定位 (%s=%s)",
assertionQO.getOperateLocType(),
assertionQO.getOperateLocValue());
}
// 3. 执行断言
boolean result = false;
String actualValue = "";
String expectedValue = assertionQO.getExpectations();
switch (assertionQO.getAssertionMode()) {
case "1": // 元素被选中(Checked)
actualValue = String.valueOf(element.isSelected());
result = element.isSelected();
if (!result) {
report.setErrorInfo(locatorDescription + " 未被选中");
}
break;
case "2": // 元素可编辑(Editable)
actualValue = String.valueOf(element.isEnabled());
result = element.isEnabled();
if (!result) {
report.setErrorInfo(locatorDescription + " 不可编辑");
}
break;
case "3": // 元素存在(ElementPresent)
result = true; // 如果能找到元素,说明存在
break;
case "4": // 元素不存在(ElementNotPresent)
report.setErrorInfo("元素存在性断言应使用其他方式实现");
return false;
case "5": // 元素未被选中(NotChecked)
actualValue = String.valueOf(element.isSelected());
result = !element.isSelected();
if (!result) {
report.setErrorInfo(locatorDescription + " 已被选中");
}
break;
case "6": // 元素不可编辑(NotEditable)
actualValue = String.valueOf(element.isEnabled());
result = !element.isEnabled();
if (!result) {
report.setErrorInfo(locatorDescription + " 可编辑");
}
break;
case "7": // 元素文本不等于期望(NotText)
actualValue = element.getText();
result = !actualValue.equals(expectedValue);
if (!result) {
report.setErrorInfo(String.format(
"%s 文本等于期望值(实际:'%s',期望不等于:'%s'",
locatorDescription, actualValue, expectedValue
));
}
break;
case "8": // 元素文本等于期望(Text)
actualValue = element.getText();
result = actualValue.equals(expectedValue);
if (!result) {
report.setErrorInfo(String.format(
"%s 文本不等于期望值(实际:'%s',期望:'%s'",
locatorDescription, actualValue, expectedValue
));
}
break;
case "9": // 元素值等于期望(Value)
actualValue = element.getAttribute("value");
result = actualValue.equals(expectedValue);
if (!result) {
report.setErrorInfo(String.format(
"%s 值不等于期望值(实际:'%s',期望:'%s'",
locatorDescription, actualValue, expectedValue
));
}
break;
case "10": // 元素文本包含期望(InText)
actualValue = element.getText();
result = actualValue.contains(expectedValue);
if (!result) {
report.setErrorInfo(String.format(
"%s 文本不包含期望内容(实际:'%s',期望包含:'%s'",
locatorDescription, actualValue, expectedValue
));
}
break;
case "11": // 元素文本不包含期望(NotlnText)
actualValue = element.getText();
result = !actualValue.contains(expectedValue);
if (!result) {
report.setErrorInfo(String.format(
"%s 文本包含不应出现的内容(实际:'%s',期望不包含:'%s'",
locatorDescription, actualValue, expectedValue
));
}
break;
default:
report.setErrorInfo("不支持的断言方式: " + assertionQO.getAssertionMode());
return false;
}
report.setIsSuccess(result ? "1" : "0");
return result;
} catch (NoSuchElementException e) {
if ("3".equals(assertionQO.getAssertionMode())) {
// 元素存在断言时找不到元素应该返回false
report.setErrorInfo(locatorDescription + " 不存在");
report.setIsSuccess("0");
return false;
}
report.setErrorInfo("元素定位失败: " + e.getMessage());
report.setIsSuccess("0");
return false;
} catch (Exception e) {
report.setErrorInfo("元素断言异常: " + e.getMessage());
report.setIsSuccess("0");
return false;
}
}
/**
* 处理下拉框断言
*
* @param assertionQO 断言配置
* @param seleniumUtils Selenium工具类
* @param report 断言报告
* @return 断言是否成功
*/
private boolean handleDropdownAssertion(AssertionQO assertionQO,
SeleniumUtils seleniumUtils,
AssertionReportVO report) {
WebElement dropdown = null;
String locatorDescription = "";
report.setErrorInfo("");
try {
// 1. 获取下拉框元素
if ("1".equals(assertionQO.getOperateObject())) {
UiElement uiElement = uiElementService.selectUiElementById(assertionQO.getOperateElementId());
if (uiElement == null) {
report.setErrorInfo("元素ID " + assertionQO.getOperateElementId() + " 不存在");
report.setIsSuccess("0");
return false;
}
dropdown = seleniumUtils.findElement(uiElement.getLocType(), uiElement.getElementLoc());
locatorDescription = String.format("下拉框ID:%d (%s=%s)",
assertionQO.getOperateElementId(),
uiElement.getLocType(),
uiElement.getElementLoc());
} else {
dropdown = seleniumUtils.findElement(
assertionQO.getOperateLocType(),
assertionQO.getOperateLocValue()
);
locatorDescription = String.format("下拉框 (%s=%s)",
assertionQO.getOperateLocType(),
assertionQO.getOperateLocValue());
}
// 2. 转换为Select对象
Select select = new Select(dropdown);
WebElement selectedOption = select.getFirstSelectedOption();
String actualValue = selectedOption.getAttribute("value");
String actualText = selectedOption.getText();
String expectedValue = assertionQO.getExpectations();
// 4. 执行断言
boolean result = false;
switch (assertionQO.getAssertionMode()) {
case "1": // 所选元素的值等于期望(SelectedValue)
result = actualValue.equals(expectedValue);
if (!result) {
report.setErrorInfo(String.format(
"下拉框选中值不匹配(实际:'%s',期望:'%s'",
actualValue, expectedValue
));
}
break;
case "2": // 下拉框选项显示的文本等于期望(SelectedLabel)
result = actualText.equals(expectedValue);
if (!result) {
report.setErrorInfo(String.format(
"下拉框选中文本不匹配(实际:'%s',期望:'%s'",
actualText, expectedValue
));
}
break;
case "3": // 所选元素的值不等于期望(NotSelectedValue)
result = !actualValue.equals(expectedValue);
if (!result) {
report.setErrorInfo(String.format(
"下拉框选中值不应匹配(实际:'%s',期望不等于:'%s'",
actualValue, expectedValue
));
}
break;
default:
report.setErrorInfo("不支持的断言方式: " + assertionQO.getAssertionMode());
return false;
}
report.setIsSuccess(result ? "1" : "0");
return result;
} catch (NoSuchElementException e) {
report.setErrorInfo("下拉框元素不存在: " + e.getMessage());
report.setIsSuccess("0");
return false;
} catch (UnexpectedTagNameException e) {
report.setErrorInfo("元素不是下拉框: " + e.getMessage());
report.setIsSuccess("0");
return false;
} catch (Exception e) {
report.setErrorInfo("下拉框断言异常: " + e.getMessage());
report.setIsSuccess("0");
return false;
}
}
/**
* 处理网页标题断言
*
* @param assertionQO 断言配置
* @param seleniumUtils Selenium工具类
* @param report 断言报告
* @return 断言是否成功
*/
private boolean handleTitleAssertion(AssertionQO assertionQO,
SeleniumUtils seleniumUtils,
AssertionReportVO report) {
try {
// 1. 获取实际标题和期望标题
String actualTitle = seleniumUtils.getTitle();
String expectedTitle = assertionQO.getExpectations();
report.setErrorInfo("");
// 3. 执行完全匹配比较
boolean result = actualTitle.equals(expectedTitle);
if (!result) {
report.setErrorInfo(String.format(
"网页标题不匹配(实际:'%s',期望:'%s'",
actualTitle, expectedTitle
));
}
report.setIsSuccess(result ? "1" : "0");
return result;
} catch (Exception e) {
report.setErrorInfo("获取网页标题失败: " + e.getMessage());
report.setIsSuccess("0");
return false;
}
}
/**
* 获取数据提取集合
*
* @param settingList
* @return
*/
public Map<String, Object> filterBydataExtractionQOList(List<UiHighSettingVO> settingList, SeleniumUtils seleniumUtils) {
List<UiHighSettingVO> uiHighSettingVOS = settingList.stream()
.filter(setting -> setting.getOperateType().equals("3") && setting.getIsDisabled() == 1)
.toList();
Map<String, Object> variableStorage = new HashMap<>();
if (CollectionUtils.isEmpty(uiHighSettingVOS)) {
return variableStorage;
}
for (UiHighSettingVO uiHighSettingVO : uiHighSettingVOS) {
DataExtractionConfigQO dataExtractionQO = uiHighSettingVO.getDataExtractionQOList();
List<DataExtractionQO> dataExtractionQOList = new ArrayList<>();
dataExtractionQOList.addAll(dataExtractionQO.getElementExtractions());
dataExtractionQOList.addAll(dataExtractionQO.getWindowExtractions());
if (!CollectionUtils.isEmpty(dataExtractionQOList)) {
for (DataExtractionQO extraction : dataExtractionQOList) {
if ("0".equals(extraction.getIsDisabled())) {
continue;
}
try {
String variableName = extraction.getValue();
String extractedValue = extractData(extraction, seleniumUtils);
variableStorage.put(variableName, extractedValue);
log.info("Extracted '{}' = {}", variableName, extractedValue);
} catch (NoSuchElementException e) {
log.info("Extracted '" + extraction.getValue() + "' = NoSuchElementException");
variableStorage.put(extraction.getValue(), "NoSuchElementException");
} catch (Exception e) {
log.error("Error extracting '" + extraction.getValue() + "': " + e.getMessage());
}
}
}
}
return variableStorage;
}
/**
* 获取数据提取集合具体实现
*
* @param extraction
* @param seleniumUtils
* @return
*/
private String extractData(DataExtractionQO extraction, SeleniumUtils seleniumUtils) {
if ("1".equals(extraction.getInformationType())) {
return seleniumUtils.extractWindowInformation(extraction.getWindowType());
} else if ("2".equals(extraction.getInformationType())) {
WebElement element = null;
String locType = "";
String elementLoc = "";
if ("1".equals(extraction.getOperateObject())) {
//元素定位
UiElement uiElement = uiElementService.selectUiElementById(extraction.getOperateElementId());
if (uiElement == null) {
return "";
}
locType = uiElement.getLocType(); //属性
elementLoc = uiElement.getElementLoc();//值
} else if ("2".equals(extraction.getOperateObject())) {
locType = extraction.getOperateLocType(); //属性
elementLoc = extraction.getOperateLocValue();//值
}
element = seleniumUtils.findElement(locType, elementLoc);
switch (extraction.getElementType()) {
case "1": // 普通对象
return extraction.getElementAttribute(); // 直接返回属性值
case "2": // 元素文本
return element.getText();
case "3": // 元素值
return element.getAttribute("value");
case "4": // 元素属性
return element.getAttribute(extraction.getElementAttribute());
case "5": // CSS属性
return element.getCssValue(extraction.getElementAttribute());
case "6": // 匹配 xpath 的元素数量(storeXpathCount)
return String.valueOf(seleniumUtils.findElements(locType, elementLoc).size());
default:
return "";
}
}
return "";
}
/**
* 获取(前置//后置)等待时间
*
@@ -401,7 +1059,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/
private List<UiHighSettingVO> extractafterSetting(List<UiHighSettingVO> settings) {
return settings.stream()
.filter(e -> "2".equals(e.getSettingType()) && e.getIsDisabled() == 0)
.filter(e -> "2".equals(e.getSettingType()) && 1 == e.getIsDisabled())
.collect(Collectors.toList()); // 返回默认设置
}
@@ -413,7 +1071,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/
private UiHighSettingVO extractErrorSetting(List<UiHighSettingVO> settings) {
return settings.stream()
.filter(e -> "3".equals(e.getSettingType()) && e.getIsDisabled() == 0)
.filter(e -> "3".equals(e.getSettingType()))
.findFirst()
.orElse(new UiHighSettingVO()); // 返回默认设置
}
@@ -427,7 +1085,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/
private UiHighSettingVO extractOtherSetting(List<UiHighSettingVO> settings) {
return settings.stream()
.filter(e -> "4".equals(e.getSettingType()) && e.getIsDisabled() == 0)
.filter(e -> "4".equals(e.getSettingType()))
.findFirst()
.orElse(new UiHighSettingVO()); // 返回默认设置
}
@@ -443,7 +1101,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
*/
private void openWebPage(UiSceneSteps step, SeleniumUtils seleniumUtils) {
//追加页面在新的页面打开url不勾选覆盖当前url 0不追加 1追加
Integer isAppendPage = step.getIsAppendPage(); // 0=当前页1=新标签页
Integer isAppendPage = step.getIsAppendPage() == null ? 0 : step.getIsAppendPage(); // 0=当前页1=新标签页
String url = step.getUrl();
log.info("打开网页:{}, 模式:{}", url, isAppendPage == 1 ? "新标签页" : "当前页");
if (isAppendPage != null && isAppendPage == 1) {
@@ -883,6 +1541,31 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
seleniumUtils.clearInput(locator);
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) { // 普通输入框
seleniumUtils.inputText(
@@ -916,4 +1599,5 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService {
}
}

View File

@@ -123,7 +123,7 @@ public class DynamicTaskManager {
// 执行业务逻辑
log.info("执行任务: " + task.getId());
try{
performanceTestCaseReportService.executePerformanceTestAndReport(task.getId(),jmeterHomePath,1);
performanceTestCaseReportService.executePerformanceTestAndReport(task.getId(),jmeterHomePath,1,"system");
} catch (Exception 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="status != null and status != ''"> and a.status = #{status}</if>
</where>
order by a.id desc
</select>
<select id="selectPerformanceTestCaseReportById" parameterType="Long" resultMap="PerformanceTestCaseReportResult">

View File

@@ -36,10 +36,21 @@
where id = #{id}
</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 into test_case_log
<trim prefix="(" suffix=")" suffixOverrides=",">
<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="operDetail != null">oper_detail,</if>
<if test="operUser != null">oper_user,</if>
@@ -48,6 +59,8 @@
</trim>
<trim prefix="values (" suffix=")" suffixOverrides=",">
<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="operDetail != null">#{operDetail},</if>
<if test="operUser != null">#{operUser},</if>

View File

@@ -138,4 +138,24 @@
#{id}
</foreach>
</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>

View File

@@ -148,4 +148,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id}
</foreach>
</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>

View File

@@ -124,4 +124,21 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
#{id}
</foreach>
</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>

View File

@@ -149,19 +149,23 @@
(SELECT COUNT(1) FROM test_plan_case tpc
WHERE tpc.plan_id = tp.id
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
WHERE tpc.plan_id = tp.id
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
WHERE tpc.plan_id = tp.id
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
WHERE tpc.plan_id = tp.id
AND tpc.del_flag = '0'
AND tpc.type = '4') AS productionTestPassNum,
AND tpc.type = '4'
AND tpc.execute_result = '1') AS productionTestPassNum,
tp.version,
(
SELECT COUNT(1) FROM test_plan_defect tpd
@@ -203,4 +207,56 @@
<select id="selectPlanId" resultType="Long">
select id from test_plan where serial_number = #{serialNumber} and del_flag = '0'
</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>

View File

@@ -155,4 +155,28 @@
<include refid="selectTestProjectVo"/>
WHERE id = #{id}
</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>

View File

@@ -45,4 +45,21 @@
WHERE tpp.plan_id = #{planId}
AND tpp.del_flag = '0'
</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>

View File

@@ -31,17 +31,72 @@
</trim>
</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
SELECT tr.id AS id,
tr.name AS name,
tr.result AS result,
tr.report AS report,
tr.status AS status,
tpr.type AS type,
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
LEFT JOIN test_report tr ON tr.id = tpr.report_id
WHERE tpr.plan_id = #{planId}
AND tpr.del_flag = '0'
AND tr.del_flag = '0'
ORDER BY tr.create_time DESC
</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>

View File

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

View File

@@ -27,7 +27,7 @@
</resultMap>
<sql id="selectUiAutomationVo">
select id, group_id, name, case_level, status, label, create_by, duty_by, run_environment, update_time, steps_number, execution_result, pass_rate, crontab, crontab_status, del_flag,create_time,update_by from ui_automation
select id, group_id, name, case_level, status, label, create_by, duty_by, run_environment, update_time, steps_number, execution_result, pass_rate, crontab, crontab_status, del_flag,create_time,update_by,description from ui_automation
</sql>
<select id="selectUiAutomationList" parameterType="UiAutomation" resultMap="UiAutomationResult">

View File

@@ -43,7 +43,7 @@
<select id="selectUiHighSettingById" parameterType="Long" resultMap="UiHighSettingResult">
<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>
<insert id="insertUiHighSetting" parameterType="UiHighSetting" useGeneratedKeys="true" keyProperty="id">

View File

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

View File

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

View File

@@ -5,7 +5,9 @@ const api = {
addBug: 'test/defect/addBug',
delBug: 'test/defect/delBug',
getBugDetail: 'test/defect/bugDetail',
updateBug: 'test/defect/editBug'
updateBug: 'test/defect/editBug',
getDefectPlanList: 'testPlan/defect/defectPlanList',
getDefectProjectList: 'testPlan/defect/defectProjectList',
}
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',
delProject: 'test/project/delProject',
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) {
@@ -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',
testPlanProjectList: '/test/testPlanProject/list',
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) {
@@ -67,10 +71,42 @@ export function addTestReport(data) {
})
}
export function getTestReportList(id) {
export function getTestReportList(data) {
return request({
url: api.getTestReportList,
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}
})
}

View File

@@ -24,3 +24,13 @@ export function getTestReportDetail(query) {
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,
hidden: true,
children: [
@@ -173,7 +173,7 @@ export const constantRoutes = [
]
},
{
path: '/performance/report/detail',
path: '/performance/report/detail/:id',
component: Layout,
hidden: true,
children: [
@@ -187,7 +187,7 @@ export const constantRoutes = [
]
},
{
path: '/ui-test/report/detail',
path: '/ui-test/report/detail/:id',
component: Layout,
hidden: true,
children: [
@@ -215,7 +215,7 @@ export const constantRoutes = [
]
},
{
path: '/ui-test/automation/edit',
path: '/ui-test/automation/edit/:id',
component: Layout,
hidden: true,
children: [
@@ -223,11 +223,23 @@ export const constantRoutes = [
path: '',
component: () => import('@/views/test/uiTest/editScene'),
name: 'EditScene',
noCache: true,
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',
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-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 prop="serialNumber" label="ID" 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 label="操作" align="left" fixed="right">
<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>
</template>
@@ -237,6 +237,25 @@
<el-button type="primary" @click="editSubmitForm"> </el-button>
</span>
</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>
</template>
@@ -245,10 +264,13 @@ import {managerList} from "@/api/test/project";
import {addBug, delBug, getBugDetail, getBugList, updateBug} from "@/api/test/bug";
import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue";
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 {
name: 'defect',
components: {SimpleOptions},
components: {DefectProject, DefectPlan, BugDetail, SimpleOptions},
dicts: ['severity_level', 'bug_type', 'bug_status'],
data() {
return {
@@ -294,6 +316,10 @@ export default {
addOpen: false,
//编辑弹窗
editOpen: false,
detailOpen: false,
activeIndex: '1',
defectId: 0,
managerName: '',
selectedRows: [],
selectedData: [],
managerList: [],
@@ -336,6 +362,16 @@ export default {
},
},
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) {
this.selectedRows = selection;
},
@@ -437,6 +473,8 @@ export default {
getBugDetail(id).then((res) => {
this.editForm = res.data
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
})
},

View File

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

View File

@@ -14,10 +14,10 @@
<div class="date-top">
<i class="el-icon-date"></i>
<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 class="date-bottom">
<span>下次执行时间</span>
<div class="date-bottom" v-if="addForm.crontabStatus === 1">
<span>下次执行时间{{ nextExecutionTime }}</span>
</div>
</div>
</div>
@@ -31,7 +31,7 @@
<el-table-column prop="name" label="场景名称" />
<el-table-column prop="status" label="Enable/Disable">
<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>
</el-table-column>
<el-table-column prop="action" label="操作">
@@ -44,7 +44,7 @@
</el-tab-pane>
<el-tab-pane label="压力配置" name="second">
<div class="pressure-header">
<div class="title">速兑通接口</div>
<div class="title">{{ addForm.performanceName }}</div>
<div class="pressure-title">并发用户数{{ addForm.concurrentThreads }}压测时长{{ addForm.pressureHour }}{{
addForm.pressureMinute }}{{ addForm.pressureSecond }}</div>
</div>
@@ -124,8 +124,6 @@
<div class="crontab-wrap">
<div class="title">Crontab表达式</div>
<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 class="near-time">
<div class="title">最近5次运行时间</div>
@@ -135,8 +133,8 @@
</div>
</el-tabs>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisibleTime = false, switchOpen = false"> </el-button>
<el-button type="primary" @click="dialogVisibleTime = false"> </el-button>
<el-button @click="dialogVisibleTime = false, addForm.crontabStatus = false"> </el-button>
<el-button type="primary" @click="handleDialogConfirm"> </el-button>
</span>
</el-dialog>
</div>
@@ -144,6 +142,7 @@
<script>
import { addTest, addAndExecuteTest, getTestCaseList } from '../../../api/performance';
import parser from 'cron-parser'; // 添加 cron-parser 依赖
export default {
name: "PerformanceAdd",
@@ -162,7 +161,7 @@ export default {
rpsStatus: '0', // rps状态0关闭1开启默认0
rpsLimit: '0', // 每分钟rps上限数默认0
crontab: '', // crontab表达式
crontabStatus: '0', // 定时任务状态0关闭1开启默认0
crontabStatus: 0, // 定时任务状态0关闭1开启默认0
loopCount: '0', // 迭代次数默认0
},
activeName: 'first',
@@ -177,26 +176,10 @@ export default {
searchScene: '',
changeList: [],
multipleSelection: [],
switchOpen: false,
dialogVisibleTime: false,
activeTime: 'first',
executionTimeList: [
{
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",
},
],
executionTimeList: [], // 修改为空数组
nextExecutionTime: '', // 添加下次执行时间
validation: false, // 校验
}
},
@@ -227,6 +210,29 @@ export default {
handleWithSave() {
this.validationForm()
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 => {
var par = {
testCaseId: item.id,
@@ -313,37 +319,130 @@ export default {
})
this.dialogVisible = false
},
switchChange(val) {
this.dialogVisibleTime = val
},
handleClose() {
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() {
const crontab = this.addForm.crontab.trim();
if (!crontab) {
this.executionTimeList = [];
this.nextExecutionTime = '';
return;
}
// 首先验证 crontab 表达式格式
if (!this.validateCrontab(crontab)) {
this.$message.error('无效的 crontab 表达式格式');
this.executionTimeList = [];
this.nextExecutionTime = '';
return;
}
try {
// 解析crontab表达式
const interval = cronParser.parseExpression(crontab);
const interval = parser.parseExpression(crontab);
const times = [];
// 获取最近次执行时间
for (let i = 0; i < 3; i++) {
const nextTime = interval.next().toDate(); // 获取 Date 对象
const formattedTime = this.formatDate(nextTime); // 格式化时间
times.push({ time: formattedTime });
// 获取最近5次执行时间
for (let i = 0; i < 5; i++) {
const nextTime = interval.next().toDate();
times.push({
time: this.formatDate(nextTime)
});
}
this.executionTimeList = times;
this.nextExecutionTime = times[0].time; // 设置下次执行时间
} catch (error) {
console.error('无效的crontab表达式:', error);
this.executionTimeList = [{ time: '无效的crontab表达式' }];
this.$message.error('无效的 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>
<el-input v-model="addForm.performanceName" placeholder="请输入名称" maxlength="255" show-word-limit></el-input>
</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="handleWithSaveAndExecute">保存并执行</el-button>
<el-button type="primary" plain @click="handleWithExecute">立即执行</el-button>
@@ -15,10 +15,10 @@
<div class="date-top">
<i class="el-icon-date"></i>
<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 class="date-bottom">
<span>下次执行时间</span>
<div class="date-bottom" v-if="addForm.crontabStatus === 1">
<span>下次执行时间{{ nextExecutionTime }}</span>
</div>
</div>
</div>
@@ -32,7 +32,7 @@
<el-table-column prop="testCaseName" label="场景名称" />
<el-table-column prop="status" label="Enable/Disable">
<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>
</el-table-column>
<el-table-column prop="action" label="操作">
@@ -45,7 +45,7 @@
</el-tab-pane>
<el-tab-pane label="压力配置" name="second">
<div class="pressure-header">
<div class="title">速兑通接口</div>
<div class="title">{{ addForm.performanceName }}</div>
<div class="pressure-title">并发用户数{{ addForm.concurrentThreads }}压测时长{{ addForm.pressureHour }}{{
addForm.pressureMinute }}{{ addForm.pressureSecond }}</div>
</div>
@@ -125,8 +125,6 @@
<div class="crontab-wrap">
<div class="title">Crontab表达式</div>
<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 class="near-time">
<div class="title">最近5次运行时间</div>
@@ -136,8 +134,8 @@
</div>
</el-tabs>
<span slot="footer" class="dialog-footer">
<el-button @click="dialogVisibleTime = false, switchOpen = false"> </el-button>
<el-button type="primary" @click="dialogVisibleTime = false"> </el-button>
<el-button @click="dialogVisibleTime = false, addForm.crontabStatus = 0"> </el-button>
<el-button type="primary" @click="handleDialogConfirm"> </el-button>
</span>
</el-dialog>
</div>
@@ -145,6 +143,8 @@
<script>
import { editTest, editAndExecuteTest, getTestCaseList, getTestDetail, executeTest } from '../../../api/performance';
import {runTestPlanCase} from "../../../api/test/planCase";
import parser from 'cron-parser'; // 添加 cron-parser 依赖
export default {
name: "PerformanceEdit",
@@ -163,9 +163,10 @@ export default {
rpsStatus: '0', // rps状态0关闭1开启默认0
rpsLimit: '0', // 每分钟rps上限数默认0
crontab: '', // crontab表达式
crontabStatus: '0', // 定时任务状态0关闭1开启默认0
crontabStatus: 0, // 定时任务状态0关闭1开启默认0
loopCount: '0', // 迭代次数默认0
},
loading: false,
activeName: 'first',
sceneList: [], // 场景列表
dialogVisible: false, // 场景列表
@@ -178,26 +179,10 @@ export default {
searchScene: '',
changeList: [],
multipleSelection: [],
switchOpen: false,
dialogVisibleTime: false,
activeTime: 'first',
executionTimeList: [
{
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",
},
],
executionTimeList: [], // 修改为空数组
nextExecutionTime: '', // 添加下次执行时间
validation: false, // 校验
}
},
@@ -205,6 +190,25 @@ export default {
this.getTestCaseData()
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: {
// 校验
validationForm() {
@@ -229,6 +233,29 @@ export default {
handleWithSave() {
this.validationForm()
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
editTest(this.addForm).then(res => {
if (res.code === 200) {
@@ -247,6 +274,7 @@ export default {
this.$message({ message: '请输入Crontab表达式', type: 'warning' })
return
}
this.loading = true;
editAndExecuteTest(this.addForm).then(res => {
if (res.code === 200) {
this.$message({ message: '编辑成功', type: 'success' })
@@ -254,18 +282,24 @@ export default {
} else {
this.$message({ message: '编辑失败', type: 'error' })
}
}).finally(() => {
this.loading = false;
})
},
// 立即执行
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) {
this.$message({ message: '执行成功', type: 'success' })
this.$tab.closeOpenPage({ path: "/performance/performance" });
this.$modal.msgSuccess("提交执行成功");
} else {
this.$message({ message: '执行失败', type: 'error' })
this.$message({ message: '提交执行失败', type: 'error' })
}
})
}).finally(() => {
this.loading = false;
});
},
// 取消
handleWithCancel() {
@@ -314,18 +348,11 @@ export default {
getTestDetail(this.$route.query.id).then(res => {
if (res.code === 200) {
this.addForm = res.data
res.data.performanceTestCaseVOList.forEach(item => {
item.status = String(item.status)
})
this.changeList = res.data.performanceTestCaseVOList
this.sceneList.forEach(row => {
this.changeList.forEach(item => {
if (row.id === item.testCaseId) {
this.multipleSelection.push(row)
// 如果有定时任务,更新下次执行时间
if (this.addForm.crontabStatus === 1 && this.addForm.crontab) {
this.updateExecutionTimes();
}
})
})
this.addForm.rpsStatus = String(res.data.rpsStatus)
}
})
},
@@ -357,37 +384,130 @@ export default {
this.dialogVisible = false
},
switchChange(val) {
this.dialogVisibleTime = val
},
handleClose() {
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() {
const crontab = this.form.crontab.trim();
const crontab = this.addForm.crontab.trim();
if (!crontab) {
this.executionTimeList = [];
this.nextExecutionTime = '';
return;
}
// 首先验证 crontab 表达式格式
if (!this.validateCrontab(crontab)) {
this.$message.error('无效的 crontab 表达式格式');
this.executionTimeList = [];
this.nextExecutionTime = '';
return;
}
try {
// 解析crontab表达式
const interval = cronParser.parseExpression(crontab);
const interval = parser.parseExpression(crontab);
const times = [];
// 获取最近次执行时间
for (let i = 0; i < 3; i++) {
const nextTime = interval.next().toDate(); // 获取 Date 对象
const formattedTime = this.formatDate(nextTime); // 格式化时间
times.push({ time: formattedTime });
// 获取最近5次执行时间
for (let i = 0; i < 5; i++) {
const nextTime = interval.next().toDate();
times.push({
time: this.formatDate(nextTime)
});
}
this.executionTimeList = times;
this.nextExecutionTime = times[0].time; // 设置下次执行时间
} catch (error) {
console.error('无效的crontab表达式:', error);
this.executionTimeList = [{ time: '无效的crontab表达式' }];
this.$message.error('无效的 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="triggerType" label="触发方式" align="center" sortable>
<template slot-scope="scope">
<div v-if="scope.row.triggerType === 1">按持续时间</div>
<div v-else>按迭代次数</div>
<div v-if="scope.row.triggerType === 1">定时任务</div>
<div v-else>手动</div>
</template>
</el-table-column>
<el-table-column prop="status" label="状态" align="center" width="150" sortable>
@@ -65,7 +65,7 @@ export default {
methods: {
// 查看
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) {
@@ -86,8 +86,14 @@ export default {
})
},
// 分页
handleSizeChange() { },
handleCurrentChange() { },
handleSizeChange(val) {
this.serachForm.pageSize = val;
this.getReportListData();
},
handleCurrentChange(val) {
this.serachForm.pageNum = val;
this.getReportListData();
},
serachList() {
this.getReportListData()
},

View File

@@ -71,7 +71,7 @@
<el-tab-pane :label="'已完成(' + statusCounts.completed + ')'" name="2"></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 prop="serialNumber" label="ID" 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 label="操作" align="left" fixed="right">
<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>
</template>
@@ -234,6 +234,32 @@
<el-button type="primary" @click="editSubmitForm"> </el-button>
</span>
</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>
</template>
@@ -241,10 +267,14 @@
import {addProject, delProject, getProjectDetail, getProjectList, managerList, updateProject} from "@/api/test/project";
import SimpleOptions from "@/components/FormItem/option/SimpleOptions.vue";
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 {
name: 'project',
components: {SimpleOptions},
components: {RelatePlan, ProjectDetail, SimpleOptions , RelateCaseTable , RelateDefectTable},
dicts: ['priority_level', 'project_source', 'project_type', 'status'],
data() {
return {
@@ -281,6 +311,8 @@ export default {
total: 0,
list: [],
title: '',
projectId: 0,
managerName: '',
// 遮罩层
loading: false,
editSubmitLoading: false,
@@ -289,9 +321,11 @@ export default {
addOpen: false,
//编辑弹窗
editOpen: false,
detailOpen: false,
selectedRows: [],
selectedData: [],
managerList: [],
activeIndex: '1',
queryParams: {
serialNumber: '',
outline: '',
@@ -327,6 +361,16 @@ export default {
},
},
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) {
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">
<el-table :data="tableData" border style="width: 100%">
<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>
<el-table-column prop="resultDesc" label="情况描述"> </el-table-column>

View File

@@ -16,7 +16,7 @@
</el-button>
</div>
<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="result" label="测试结果" align="center">
<template #default="{ row }">
@@ -35,6 +35,11 @@
</el-table-column>
<el-table-column prop="updateBy" 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-tabs>
<pagination
@@ -65,7 +70,7 @@
</template>
<script>
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 {
name: 'caseReport',
@@ -91,7 +96,7 @@ export default {
planId: '',
},
queryParams: {
planId: '',
id: '',
pageNum: 1,
pageSize: 10,
},
@@ -101,14 +106,38 @@ export default {
}
}
},
mounted() {
activated() {
this.$nextTick(() => {
console.log('getlist')
this.getList()
})
},
mounted() {
},
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() {
this.loading = true
this.queryParams.planId = this.planId
getTestReportList(this.queryParams.planId).then(res => {
this.queryParams.id = Number(this.planId)
getTestReportList(this.queryParams).then(res => {
this.list = res.rows
this.total = res.total
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-column type="selection"/>
<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="executeBy" label="最近执行人" align="center"/>
<el-table-column prop="executeTime" label="最近执行时间" align="center"/>
@@ -176,7 +176,9 @@ export default {
return runTestPlanCase(queryParams);
}).then((res) => {
this.$modal.msgSuccess("提交执行成功");
this.open = true;
// this.open = true;
this.loading = false;
}).finally(() => {
this.loading = false;
});
},

View File

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

View File

@@ -30,7 +30,7 @@
</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-report :planId="planId" v-show="activeIndex === '3'"></case-report>
<test-defects :planId="planId" :name="testTitle" v-show="activeIndex === '4'"></test-defects>
@@ -66,6 +66,7 @@ export default {
'3': '已终止'
},
planId: '',
overViewData: {},
testStatus: '',
testTitle: '',
activeTab: 'overview',// 默认选中的标签页
@@ -114,6 +115,11 @@ export default {
this.planId = this.$route.query.id;
this.testTitle = this.$route.query.name
this.testStatus = this.$route.query.testStatus
this.overViewData = {
startTime: this.$route.query.startPlanTime,
endTime: this.$route.query.endPlanTime,
manager: this.$route.query.manager
}
}
};
</script>

View File

@@ -17,14 +17,13 @@
</el-row>
<el-row :gutter="10">
<el-col :span="5">
<!-- <span>起止时间{{ startToEndTime }}</span>-->
<span>起止时间2020-04-01 ~ 2020-04-30</span>
<span>起止时间{{ overViewData.startTime }} ~ {{ overViewData.endTime }}</span>
</el-col>
<el-col :span="7">
<span>当前测试计划已经开始 3 ,距离截止时间还有 52 </span>
<span>当前测试计划已经开始 {{ daysElapsed }} ,距离截止时间还有 {{ daysRemaining }} </span>
</el-col>
<el-col :span="5">
<span>负责人{{ }}</span>
<span>负责人{{ overViewData.manager }}</span>
</el-col>
</el-row>
</div>
@@ -34,10 +33,10 @@
<div class="progress-content">
<div id="executionProgressChart" style="width: 70%; height: 200px;"></div>
<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>用例总数 1</p>
<p><span style="color: #409EFF"></span> 已执行用例 1</p>
<p>用例总数 {{ viewData.caseCount }}</p>
<p><span style="color: #6e92ef"></span> 已执行用例 {{ viewData.executedCaseCount }}</p>
</div>
</div>
</el-card>
@@ -47,13 +46,13 @@
<div class="progress-content">
<el-row :gutter="10">
<el-col class="rate-content" :span="12">
<p>100%</p>
<el-progress :percentage="100" status="success"></el-progress>
<p>{{ executionPassRate || 0 }}%</p>
<el-progress :percentage="(viewData.passedCaseCount / viewData.executedCaseCount).toFixed(2) * 100 || 0" status="success"></el-progress>
<p>执行用例通过率</p>
</el-col>
<el-col class="rate-content" :span="12">
<p>100%</p>
<el-progress :percentage="100" status="success"></el-progress>
<p>{{ overallPassRate || 0 }}%</p>
<el-progress :percentage="(viewData.passedCaseCount / viewData.caseCount).toFixed(2) * 100 || 0" status="success"></el-progress>
<p>总体用例通过率</p>
</el-col>
</el-row>
@@ -64,14 +63,14 @@
<el-card shadow="hover">
<div class="progress-content">
<div class="defect-content">
<p>0</p>
<p>{{ viewData.defectCount }}</p>
<p>缺陷数</p>
<el-row>
<el-col :span="4">0</el-col>
<el-col :span="4">0</el-col>
<el-col :span="4">0</el-col>
<el-col :span="4">0</el-col>
<el-col :span="4">0</el-col>
<el-col :span="4">{{ viewData.unconfirmedDefectCount }}</el-col>
<el-col :span="4">{{ viewData.fixingDefectCount }}</el-col>
<el-col :span="4">{{ viewData.unverifiedDefectCount }}</el-col>
<el-col :span="4">{{ viewData.invalidDefectCount }}</el-col>
<el-col :span="4">{{ viewData.suspendedDefectCount }}</el-col>
</el-row>
<el-row>
<el-col :span="4">待确认</el-col>
@@ -98,13 +97,13 @@
</div>
<el-card shadow="hover">
<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">
<p><span style="background-color: #67C23A; display: inline-block; width: 10px; height: 10px;"></span> 通过 1</p>
<p><span style="background-color: #F56C6C; display: inline-block; width: 10px; height: 10px;"></span> 失败 0</p>
<p><span style="background-color: #E6A23C; display: inline-block; width: 10px; height: 10px;"></span> 阻塞 0</p>
<p><span style="background-color: #F56C6C; display: inline-block; width: 10px; height: 10px;"></span> 跳过 0</p>
<p><span style="background-color: #909399; display: inline-block; width: 10px; height: 10px;"></span> 未执行 0</p>
<p><span style="background-color: #9cdab9; display: inline-block; width: 10px; height: 10px;"></span> 通过 {{ viewData.passedCaseCount }}</p>
<p><span style="background-color: #d4836a; display: inline-block; width: 10px; height: 10px;"></span> 失败 {{ viewData.failedCaseCount }}</p>
<p><span style="background-color: #e9c456; display: inline-block; width: 10px; height: 10px;"></span> 阻塞 {{ viewData.blockedCaseCount }}</p>
<p><span style="background-color: #79849e; display: inline-block; width: 10px; height: 10px;"></span> 跳过 {{ viewData.skippedCaseCount }}</p>
<p><span style="background-color: #cdcdcd; display: inline-block; width: 10px; height: 10px;"></span> 未执行 {{ viewData.notExecutedCaseCount }}</p>
</div>
</div>
</el-card>
@@ -117,12 +116,34 @@
</div>
<el-card shadow="hover">
<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>
</el-card>
</el-card>
</el-col>
</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">
<div>
关联需求信息
@@ -160,25 +181,29 @@
</template>
<script>
import LineChart from '@/views/dashboard/LineChart';
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 {
name: 'probablyView',
components: {
LineChart,
},
props: {
planId: {
type: String,
default: '',
},
overViewData: {
type: Object,
default: () => {
return {
startTime: '',
endTime: '',
manager: '',
}
}
},
},
dicts: ['priority_level', 'project_source', 'project_type', 'status'],
mounted() {
this.initExecutionProgressChart();
},
data() {
return {
priorityColor: {
@@ -192,42 +217,84 @@ export default {
list: [],
loading: false,
myChart: null,
startToEndTime: '',
distributionChart: null,
lineChart: null,
viewData: [],
caseTrendData: [],
// 执行进度
executionProgress: 0,
// 执行用例通过率
executionPassRate: 0,
// 总体用例通过率
overallPassRate: 0,
defectSlider: 0,
queryParams: {
planId: '',
pageNum: 1,
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() {
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: {
getList() {
@@ -239,11 +306,37 @@ export default {
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() {
const chartDom = document.getElementById('executionProgressChart');
const myChart = echarts.init(chartDom);
const option = {
tooltip: {
trigger: 'item'
@@ -254,36 +347,230 @@ export default {
},
series: [
{
name: 'Access From',
name: '执行进度',
type: 'pie',
radius: ['40%', '70%'],
avoidLabelOverlap: false,
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 40,
fontWeight: 'bold'
}
},
labelLine: {
show: false
formatter: '{b}: {c}%',
position: 'inside'
},
data: [
{value: 1},
{ value: this.executionProgress, name: '执行进度' , itemStyle: { color: '#6e92ef'}},
{ value: 100 - this.executionProgress, name: '剩余进度' , itemStyle: { color: '#f5f5f5'}}
]
}
]
};
myChart.setOption(option);
// 监听窗口大小变化,自动调整图表大小
window.addEventListener('resize', () => {
myChart.resize();
});
option && this.myChart.setOption(option);
},
// 初始化执行分布饼图
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>
</div>
<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 class="add-btn">
@@ -80,7 +81,7 @@
<script>
import AdvancedSetting from './advancedSetting.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"
export default {
@@ -109,7 +110,7 @@ export default {
},
stepList: [],
defaultActive: 0, // 当前激活菜单的 index
changeStep: null, // 选中的步骤
changeStep: { operateType: null }, // 选中的步骤
}
},
mounted() {
@@ -125,6 +126,10 @@ export default {
}
})
},
// 修改高级设置
changeSetting(val) {
this.informationForm.uiSceneStepsVOList[this.defaultActive - 1].uiHighSettingVOList = val
},
// 删除步骤
deleteStep(index) {
@@ -175,17 +180,21 @@ export default {
handleClickSave() {
if (this.informationForm.name === null || this.informationForm.name === '') {
this.$modal.msgWarning("请输入名称!");
return
}
if (this.informationForm.groupId === null || this.informationForm.groupId === '') {
this.$modal.msgWarning("请选择模块!");
return
}
if (this.informationForm.status === null || this.informationForm.status === '') {
this.$modal.msgWarning("请选择状态");
return
}
if (this.informationForm.dutyBy === null || this.informationForm.dutyBy === '') {
this.$modal.msgWarning("请输入责任人");
return
}
updateAutomation(this.informationForm).then(res => {
addAutomation(this.informationForm).then(res => {
if (res.code === 200) {
this.$modal.msgSuccess("编辑成功")
this.$tab.closeOpenPage({ path: "/ui-test/automation-test" });

Some files were not shown because too many files have changed in this diff Show More