diff --git a/test-common/src/main/java/com/test/common/utils/SeleniumUtils.java b/test-common/src/main/java/com/test/common/utils/SeleniumUtils.java index 0a0ecf8..d29ef3f 100644 --- a/test-common/src/main/java/com/test/common/utils/SeleniumUtils.java +++ b/test-common/src/main/java/com/test/common/utils/SeleniumUtils.java @@ -1,5 +1,6 @@ package com.test.common.utils; +import lombok.extern.slf4j.Slf4j; import org.openqa.selenium.*; import org.openqa.selenium.interactions.Actions; import org.openqa.selenium.support.ui.ExpectedConditions; @@ -13,6 +14,7 @@ import java.util.Set; /** * Selenium 工具类 */ +@Slf4j public class SeleniumUtils { private WebDriver driver; @@ -238,9 +240,9 @@ public class SeleniumUtils { } // 获取alert文本 - public String getAlertText() { - return switchToAlert().getText(); - } +// public String getAlertText() { +// return switchToAlert().getText(); +// } // 在alert中输入文本 public void sendKeysToAlert(String text) { @@ -377,4 +379,480 @@ public class SeleniumUtils { driver.quit(); } } + + + + //Add SeleniumUtils ----- + + + /** + * 根据句柄ID切换到指定窗口 + * @param handleId 窗口句柄ID + */ + public void switchToWindowByHandle(String handleId) { + driver.switchTo().window(handleId); + log.info("已切换到句柄ID为 {} 的窗口", handleId); + } + + /** + * 根据网页索引号切换到指定窗口 + * @param index 窗口索引(从0开始) + */ + public void switchToWindowByIndex(int index) { + Set handles = driver.getWindowHandles(); + if (index >= 0 && index < handles.size()) { + String handle = handles.toArray(new String[0])[index]; + driver.switchTo().window(handle); + log.info("已切换到索引为 {} 的窗口", index); + } else { + throw new NoSuchWindowException("无效的窗口索引: " + index); + } + } + + /** + * 切换到初始窗口(第一个打开的窗口) + */ + public void switchToDefaultWindow() { + Set handles = driver.getWindowHandles(); + if (!handles.isEmpty()) { + String firstHandle = handles.iterator().next(); + driver.switchTo().window(firstHandle); + log.info("已切换回初始窗口"); + } + } + + + /** + * 根据索引切换到指定frame + * @param index frame索引(从0开始) + */ + public void switchToFrameByIndex(int index) { + try { + driver.switchTo().frame(index); + log.info("已切换到索引为 {} 的frame", index); + } catch (NoSuchFrameException e) { + log.error("按索引切换frame失败: {}", e.getMessage()); + throw new RuntimeException("找不到索引为 " + index + " 的frame", e); + } + } + + /** + * 根据定位方式切换到frame + * @param locatorType 定位类型(id/name/xpath/css等) + * @param locatorValue 定位值 + */ + public void switchToFrameByLocator(String locatorType, String locatorValue) { + By locator = getLocator(locatorType, locatorValue); + try { + WebElement frameElement = wait.until(ExpectedConditions.presenceOfElementLocated(locator)); + driver.switchTo().frame(frameElement); + log.info("已切换到 {} = '{}' 的frame", locatorType, locatorValue); + } catch (TimeoutException e) { + log.error("等待frame元素超时: {} = '{}'", locatorType, locatorValue); + throw new RuntimeException("找不到 " + locatorType + " = '" + locatorValue + "' 的frame元素", e); + } + } + + + /** + * 根据定位类型和值获取Selenium定位器 + * @param locatorType 定位类型(不区分大小写): + * id/name/className/tagName/linkText/partialLinkText/css/xpath/label/value/index + * @param locatorValue 定位值 + * @return By 定位器对象 + * @throws IllegalArgumentException 如果定位类型不支持 + */ + public By getLocator(String locatorType, String locatorValue) { + if (locatorValue == null || locatorValue.trim().isEmpty()) { + throw new IllegalArgumentException("定位值不能为空"); + } + + switch (locatorType.toLowerCase()) { + case "id": + return By.id(locatorValue); + case "name": + return By.name(locatorValue); + case "classname": + case "class": + return By.className(locatorValue); + case "tagname": + case "tag": + return By.tagName(locatorValue); + case "linktext": + case "link": + return By.linkText(locatorValue); + case "partiallinktext": + case "partiallink": + return By.partialLinkText(locatorValue); + case "css": + case "cssselector": + return By.cssSelector(locatorValue); + case "xpath": + return By.xpath(locatorValue); + case "label": + // 通过label文本定位对应的元素 + return By.xpath(String.format("//label[contains(text(), '%s')]", locatorValue)); + case "value": + // 通过value属性定位 + return By.xpath(String.format("//*[@value='%s']", locatorValue)); + case "index": + // 索引定位需要特殊处理,通常结合其他定位方式使用 + try { + int index = Integer.parseInt(locatorValue); + return By.xpath(String.format("(//*)[%d]", index + 1)); // XPath索引从1开始 + } catch (NumberFormatException e) { + throw new IllegalArgumentException("索引必须是数字: " + locatorValue); + } + default: + throw new IllegalArgumentException("不支持的定位类型: " + locatorType + + "。支持的定位类型: id,name,className,tagName,linkText," + + "partialLinkText,css,xpath,label,value,index"); + } + } + + + /** + * 处理弹窗 + * @param operationType 操作类型 (1=确定, 2=取消) + * @param inputText 需要输入的文本 (可为null表示不输入) + */ + public void handleAlert(int operationType, String inputText) { + try { + Alert alert = wait.until(ExpectedConditions.alertIsPresent()); + + // 如果有输入文本 + if (inputText != null && !inputText.isEmpty()) { + alert.sendKeys(inputText); + log.info("已在弹窗中输入文本: {}", inputText); + } + + // 执行确认或取消操作 + if (operationType == 1) { + alert.accept(); + log.info("已确认弹窗"); + } else if (operationType == 2) { + alert.dismiss(); + log.info("已取消弹窗"); + } else { + throw new IllegalArgumentException("无效的弹窗操作类型: " + operationType); + } + + } catch (NoAlertPresentException e) { + log.error("没有找到弹窗", e); + throw new RuntimeException("当前没有可操作的弹窗", e); + } catch (TimeoutException e) { + log.error("等待弹窗超时", e); + throw new RuntimeException("等待弹窗出现超时", e); + } + } + + /** + * 获取弹窗文本 + * @return 弹窗显示的文本内容 + */ + public String getAlertText() { + try { + Alert alert = wait.until(ExpectedConditions.alertIsPresent()); + String text = alert.getText(); + log.info("获取到弹窗文本: {}", text); + return text; + } catch (Exception e) { + log.error("获取弹窗文本失败", e); + throw new RuntimeException("无法获取弹窗文本", e); + } + } + + /** + * 根据定位方式和定位值查找元素 + * @param locType 定位方式 (如 "id", "xpath", "cssSelector" 等) + * @param locValue 定位值 + * @return 找到的WebElement + */ + public WebElement findElement(String locType, String locValue) { + try { + By by = getLocator(locType, locValue); + WebElement element = wait.until(ExpectedConditions.presenceOfElementLocated(by)); + return element; + } catch (Exception e) { + throw new RuntimeException("无法定位元素,定位方式:" + locType + ",定位值:" + locValue, e); + } + } + + + /** + * 操作下拉框 + * @param element 下拉框元素 + * @param optionType 选项类型 (1=选项文本, 2=索引, 3=值) + * @param optionValue 选项值 + */ + public void selectDropdownOption(WebElement element, String optionType, String optionValue) { + Select select = new Select(element); + try { + switch (optionType) { + case "1": // 选项文本 + select.selectByVisibleText(optionValue); + log.info("通过选项文本选择下拉框选项: {}", optionValue); + break; + case "2": // 索引 + int index = Integer.parseInt(optionValue); + select.selectByIndex(index); + log.info("通过索引选择下拉框选项: {}", index); + break; + case "3": // 值 + select.selectByValue(optionValue); + log.info("通过值选择下拉框选项: {}", optionValue); + break; + default: + throw new IllegalArgumentException("不支持的选项类型: " + optionType); + } + } catch (Exception e) { + log.error("操作下拉框失败", e); + throw new RuntimeException("操作下拉框失败", e); + } + } + + /** + * 等待元素满足指定条件 + * @param locator 元素定位器 + * @param operate 操作类型: + * 1=等待元素文本等于给定值 + * 2=等待元素存在 + * 3=等待元素显示 + * 4=等待元素不显示 + * 5=等待元素不存在 + * 6=等待元素可编辑 + * 7=等待元素不可编辑 + * @param awaitValue 等待的值(用于文本比较或自定义超时时间) + * @param timeoutInMillis 默认超时时间(毫秒) + */ + public void waitForElement(By locator, int operate, String awaitValue, long timeoutInMillis) { + WebDriverWait customWait = new WebDriverWait(driver, Duration.ofMillis(timeoutInMillis)); + + try { + switch (operate) { + case 1: // 等待元素文本等于给定值 + customWait.until(ExpectedConditions.textToBe(locator, awaitValue)); + log.info("元素文本已等于: {}", awaitValue); + break; + case 2: // 等待元素存在 + customWait.until(ExpectedConditions.presenceOfElementLocated(locator)); + log.info("元素已存在"); + break; + case 3: // 等待元素显示 + customWait.until(ExpectedConditions.visibilityOfElementLocated(locator)); + log.info("元素已显示"); + break; + case 4: // 等待元素不显示 + customWait.until(ExpectedConditions.invisibilityOfElementLocated(locator)); + log.info("元素已不显示"); + break; + case 5: // 等待元素不存在 + boolean isAbsent = customWait.until(ExpectedConditions.not( + ExpectedConditions.presenceOfElementLocated(locator) + )); + log.info("元素已不存在"); + break; + case 6: // 等待元素可编辑 + customWait.until(driver -> { + WebElement element = driver.findElement(locator); + return element.isEnabled() && !element.getAttribute("readonly").equals("true"); + }); + log.info("元素已可编辑"); + break; + case 7: // 等待元素不可编辑 + customWait.until(driver -> { + WebElement element = driver.findElement(locator); + return !element.isEnabled() || element.getAttribute("readonly").equals("true"); + }); + log.info("元素已不可编辑"); + break; + default: + throw new IllegalArgumentException("不支持的等待操作类型: " + operate); + } + } catch (TimeoutException e) { + log.error("等待元素超时,操作类型: {}, 定位器: {}", operate, locator); + throw new RuntimeException("等待元素条件未满足: " + getOperationDescription(operate), e); + } + } + + // 获取操作类型描述 + private String getOperationDescription(int operate) { + switch (operate) { + case 1: return "文本等于给定值"; + case 2: return "元素存在"; + case 3: return "元素显示"; + case 4: return "元素不显示"; + case 5: return "元素不存在"; + case 6: return "元素可编辑"; + case 7: return "元素不可编辑"; + default: return "未知操作"; + } + } + + /** + * 执行鼠标操作 + * @param locator 元素定位器 + * @param clickMethod 点击方式: + * 1=单击(左键) + * 2=单击(右键) + * 3=双击 + * 4=按下 + * 5=弹起 + */ + public void performMouseAction(By locator, int clickMethod) { + try { + WebElement element = wait.until(ExpectedConditions.elementToBeClickable(locator)); + + switch (clickMethod) { + case 1: // 左键单击 + actions.click(element).perform(); + log.info("已执行左键单击操作"); + break; + case 2: // 右键单击 + actions.contextClick(element).perform(); + log.info("已执行右键单击操作"); + break; + case 3: // 双击 + actions.doubleClick(element).perform(); + log.info("已执行双击操作"); + break; + case 4: // 按下 + actions.clickAndHold(element).perform(); + log.info("已执行鼠标按下操作"); + break; + case 5: // 弹起 + actions.release(element).perform(); + log.info("已执行鼠标弹起操作"); + break; + default: + throw new IllegalArgumentException("不支持的鼠标操作类型: " + clickMethod); + } + } catch (TimeoutException e) { + log.error("等待元素可点击超时,定位器: {}", locator); + throw new RuntimeException("元素不可点击或未找到: " + locator, e); + } catch (Exception e) { + log.error("鼠标操作执行失败", e); + throw new RuntimeException("鼠标操作失败: " + e.getMessage(), e); + } + } + + /** + * 执行鼠标移动操作 + * @param locator 元素定位器 + * @param moveType 移动类型: + * 1=鼠标移出元素 + * 2=鼠标移入元素 + */ + public void performMouseMove(By locator, int moveType) { + try { + WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); + + switch (moveType) { + case 1: // 鼠标移出元素 + // 先移动到元素上,再移动到页面左上角 + actions.moveToElement(element) + .moveByOffset(-element.getSize().getWidth(), -element.getSize().getHeight()) + .perform(); + log.info("已将鼠标从元素移出"); + break; + case 2: // 鼠标移入元素(悬停) + actions.moveToElement(element).perform(); + log.info("已将鼠标悬停在元素上"); + break; + default: + throw new IllegalArgumentException("不支持的鼠标移动类型: " + moveType); + } + } catch (TimeoutException e) { + log.error("等待元素可见超时,定位器: {}", locator); + throw new RuntimeException("元素不可见或未找到: " + locator, e); + } catch (Exception e) { + log.error("鼠标移动操作执行失败", e); + throw new RuntimeException("鼠标移动操作失败: " + e.getMessage(), e); + } + } + /** + * 在指定元素中输入文本 + * @param locator 元素定位器 + * @param text 要输入的文本 + * @param clearFirst 是否先清空原有内容 + * @param inputMethod 输入方式: + * 1=普通输入(sendKeys) + * 2=通过JavaScript设置值 + * 3=模拟人工输入(带延迟) + */ + public void inputText(By locator, String text, boolean clearFirst, int inputMethod) { + try { + WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); + + // 先清空内容(如果需要) + if (clearFirst) { + element.clear(); + log.info("已清空输入框内容"); + } + + switch (inputMethod) { + case 1: // 普通输入 + element.sendKeys(text); + log.info("已通过sendKeys输入文本: {}", text); + break; + case 2: // JavaScript输入 + JavascriptExecutor js = (JavascriptExecutor) driver; + js.executeScript("arguments[0].value = arguments[1]", element, text); + log.info("已通过JavaScript设置文本: {}", text); + break; + case 3: // 模拟人工输入 + for (char c : text.toCharArray()) { + element.sendKeys(String.valueOf(c)); + Thread.sleep(50); // 每个字符间隔50ms + } + log.info("已模拟人工输入文本: {}", text); + break; + default: + throw new IllegalArgumentException("不支持的输入方式: " + inputMethod); + } + } catch (TimeoutException e) { + log.error("等待输入元素可见超时,定位器: {}", locator); + throw new RuntimeException("输入元素不可见或未找到: " + locator, e); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + log.error("模拟人工输入被中断"); + throw new RuntimeException("输入操作被中断", e); + } catch (Exception e) { + log.error("输入操作执行失败", e); + throw new RuntimeException("输入操作失败: " + e.getMessage(), e); + } + } + + /** + * 在可编辑段落中输入文本(如富文本编辑器) + * @param locator 元素定位器 + * @param text 要输入的文本 + */ + public void inputToEditableDiv(By locator, String text) { + try { + WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); + element.click(); // 先点击激活 + element.sendKeys(text); + log.info("已在可编辑段落中输入文本: {}", text); + } catch (Exception e) { + log.error("可编辑段落输入失败", e); + throw new RuntimeException("可编辑段落输入失败: " + e.getMessage(), e); + } + } + + /** + * 清空输入框内容 + * @param locator 元素定位器 + */ + public void clearInput(By locator) { + try { + WebElement element = wait.until(ExpectedConditions.visibilityOfElementLocated(locator)); + element.clear(); + log.info("已清空输入框内容"); + } catch (Exception e) { + log.error("清空输入框失败", e); + throw new RuntimeException("清空输入框失败: " + e.getMessage(), e); + } + } + } diff --git a/test-test/src/main/java/com/test/test/domain/UiSceneSteps.java b/test-test/src/main/java/com/test/test/domain/UiSceneSteps.java index 04f8600..6ab9b83 100644 --- a/test-test/src/main/java/com/test/test/domain/UiSceneSteps.java +++ b/test-test/src/main/java/com/test/test/domain/UiSceneSteps.java @@ -52,11 +52,11 @@ public class UiSceneSteps extends BaseEntity /** 元素库名称 */ @Excel(name = "元素库名称") - private Integer operateGroupId; + private Long operateGroupId; /** 元素id */ @Excel(name = "元素id") - private Integer operateElementId; + private Long operateElementId; /** 元素定位类型 */ @Excel(name = "元素定位类型") @@ -76,7 +76,7 @@ public class UiSceneSteps extends BaseEntity /** 窗口尺寸 */ @Excel(name = "窗口尺寸") - private Integer windowSize; + private String windowSize; /** 子选项 */ @Excel(name = "子选项") @@ -201,21 +201,21 @@ public class UiSceneSteps extends BaseEntity { return operateObject; } - public void setOperateGroupId(Integer operateGroupId) + public void setOperateGroupId(Long operateGroupId) { this.operateGroupId = operateGroupId; } - public Integer getOperateGroupId() + public Long getOperateGroupId() { return operateGroupId; } - public void setOperateElementId(Integer operateElementId) + public void setOperateElementId(Long operateElementId) { this.operateElementId = operateElementId; } - public Integer getOperateElementId() + public Long getOperateElementId() { return operateElementId; } @@ -255,12 +255,12 @@ public class UiSceneSteps extends BaseEntity { return inputValue; } - public void setWindowSize(Integer windowSize) + public void setWindowSize(String windowSize) { this.windowSize = windowSize; } - public Integer getWindowSize() + public String getWindowSize() { return windowSize; } diff --git a/test-test/src/main/java/com/test/test/domain/qo/UiSceneStepsQO.java b/test-test/src/main/java/com/test/test/domain/qo/UiSceneStepsQO.java index 8b14995..dbc0e74 100644 --- a/test-test/src/main/java/com/test/test/domain/qo/UiSceneStepsQO.java +++ b/test-test/src/main/java/com/test/test/domain/qo/UiSceneStepsQO.java @@ -68,7 +68,7 @@ public class UiSceneStepsQO extends BaseEntity private String inputValue; /** 窗口尺寸 */ - private Integer windowSize; + private String windowSize; /** 子选项 */ private String subOptions; diff --git a/test-test/src/main/java/com/test/test/service/impl/UiAutomationServiceImpl.java b/test-test/src/main/java/com/test/test/service/impl/UiAutomationServiceImpl.java index 835426d..11a5cc5 100644 --- a/test-test/src/main/java/com/test/test/service/impl/UiAutomationServiceImpl.java +++ b/test-test/src/main/java/com/test/test/service/impl/UiAutomationServiceImpl.java @@ -190,6 +190,7 @@ public class UiAutomationServiceImpl implements IUiAutomationService { uiAutomation.setStatus("1"); //未开始 uiAutomation.setDutyBy(SecurityUtils.getUsername()); uiAutomation.setDelFlag(0); + uiAutomation.setCrontabStatus(0); //未开启 uiAutomationMapper.insertUiAutomation(uiAutomation); Long automationId = uiAutomation.getId(); //新增场景步骤 diff --git a/test-test/src/main/java/com/test/test/service/impl/UiSceneStepsServiceImpl.java b/test-test/src/main/java/com/test/test/service/impl/UiSceneStepsServiceImpl.java index 771da27..4aa1dbc 100644 --- a/test-test/src/main/java/com/test/test/service/impl/UiSceneStepsServiceImpl.java +++ b/test-test/src/main/java/com/test/test/service/impl/UiSceneStepsServiceImpl.java @@ -1,32 +1,26 @@ package com.test.test.service.impl; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; +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.UiAutomation; -import com.test.test.domain.UiReport; -import com.test.test.domain.UiSceneSteps; -import com.test.test.domain.UiSceneStepsReport; +import com.test.test.domain.*; import com.test.test.domain.qo.OtherSettingsQO; import com.test.test.domain.vo.UiHighSettingVO; import com.test.test.mapper.UiAutomationMapper; import com.test.test.mapper.UiSceneStepsMapper; import com.test.test.mapper.UiSceneStepsReportMapper; -import com.test.test.service.IUiAutomationService; -import com.test.test.service.IUiReportService; -import com.test.test.service.IUiSceneStepsService; -import com.test.test.service.StepExecution; +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.chrome.ChromeDriver; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -46,18 +40,12 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { private UiSceneStepsReportMapper uiSceneStepsReportMapper; @Resource private IUiAutomationService uiAutomationService; - //场景成功数 - private Integer scenesSucceedNumber = 0; - //场景失败数 - private Integer scenesErrorNumber = 0; - //场景未执行数 - private Integer scenesNotNumber = 0; - //步骤成功数 - private Integer stepsSucceedNumber = 0; - //步骤失败数 - private Integer stepsErrorNumber = 0; - //步骤未执行数 - private Integer stepsNotNumber = 0; + @Resource + private IUiElementService uiElementService; + @Resource + private UiAutomationMapper uiAutomationMapper; + + /** @@ -72,7 +60,14 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { result.put("id", 0L); // 1. 验证并获取步骤 List steps = validateAndGetSteps(automationId); - stepsNotNumber = steps.size(); + + //修改ui_automation 状态为进行中 执行结果为running + UiAutomation uiAutomation = new UiAutomation(); + uiAutomation.setStatus("2"); + uiAutomation.setExecutionResult("1"); + uiAutomation.setUpdateBy(DateUtils.getTime()); + uiAutomationMapper.updateUiAutomation(uiAutomation); + Date startTime = DateUtils.getNowDate(); // 2. 创建执行报告 @@ -91,22 +86,48 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { // 3. 执行所有步骤 executeAllSteps(steps, reportId); log.info("场景执行成功: {}", automationId); - scenesSucceedNumber++; uiReport.setConsoleStr("OK"); uiReport.setStatus(3); } catch (IllegalArgumentException e) { result.put("msg", e.getMessage()); log.error("场景执行失败: {}", e.getMessage()); - scenesErrorNumber++; uiReport.setConsoleStr(e.toString()); uiReport.setStatus(2); + uiAutomation.setExecutionResult("2"); } finally { endTime = DateUtils.getNowDate(); uiReport.setTake(DateUtils.calculateTimeDifference(startTime, endTime)); uiReport.setUpdateTime(endTime); + //查询report + UiSceneStepsReport uiStepsReport = new UiSceneStepsReport(); + uiStepsReport.setReportId(reportId); + List uiSceneStepsReports = uiSceneStepsReportMapper.selectUiSceneStepsReportList(uiStepsReport); + int scenesSucceedNumber = 0; + int scenesErrorNumber = 0; + int stepsSucceedNumber = 0; + int stepsErrorNumber = 0; + int stepsNotNumber = 0; + if (uiSceneStepsReports != null && !uiSceneStepsReports.isEmpty()) { + boolean allSuccess = true; + for (UiSceneStepsReport report : uiSceneStepsReports) { + if (report.getExecutionFlag().equals("1")) { + stepsSucceedNumber++; + } else if (report.getExecutionFlag().equals("2")) { + stepsErrorNumber++; + allSuccess = false; + } else if (report.getExecutionFlag().equals("0")) { + stepsNotNumber++; + } + } + if (allSuccess) { + scenesSucceedNumber = 1; + } else { + scenesErrorNumber = 1; + } + } uiReport.setScenesSucceedNumber(scenesSucceedNumber); uiReport.setScenesErrorNumber(scenesErrorNumber); - uiReport.setScenesNotNumber(scenesNotNumber); + uiReport.setScenesNotNumber(0); uiReport.setStepsSucceedNumber(stepsSucceedNumber); uiReport.setStepsErrorNumber(stepsErrorNumber); uiReport.setStepsNotNumber(stepsNotNumber); @@ -116,6 +137,16 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { ); uiReportService.updateUiReport(uiReport); + //修改ui_automation + uiAutomation = new UiAutomation(); + if (!uiAutomation.getExecutionResult().equals("2")){ + uiAutomation.setExecutionResult("3"); + uiAutomation.setStatus("3"); + } + uiAutomation.setUpdateBy(DateUtils.getTime()); + uiAutomation.setPassRate(stepsSucceedNumber == 0 ? "0%" : + String.format("%.2f%%", (double)stepsSucceedNumber / steps.size() * 100)); + uiAutomationMapper.updateUiAutomation(uiAutomation); } return result; } @@ -128,6 +159,7 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { */ private List validateAndGetSteps(Long automationId) { List steps = uiSceneStepsMapper.selectUiSceneStepsById(automationId); + steps = steps.stream().filter(e->e.getIsDisabled() == 0).collect(Collectors.toList()); if (CollectionUtils.isEmpty(steps)) { throw new IllegalArgumentException("步骤不能为空"); } @@ -186,30 +218,29 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { // 定义所有操作类型的执行方法映射 Map> operationStrategies = Map.of( "1", Map.of( // 浏览器操作 - "1", this::openWebPage -// "2", this::closeBrowser, -// "3", this::switchWindow, -// "4", this::resizeWindow, -// "5", this::selectEmbeddedPage + "1", this::openWebPage, + "2", this::closeBrowser, + "3", this::switchWindow, + "4", this::resizeWindow, + "5", this::selectEmbeddedPage + ), + "2", Map.of( // 弹窗操作 + "1", this::handlePopup + ), + "3", Map.of( // 元素操作 + "1", this::submitForm, + "2", this::handleDropdown, + "3", this::setOption, + "4", this::waitForElement + ), + "4", Map.of( // 鼠标操作 + "1", this::mouseClick, + "2", this::mouseMove + ), + "5", Map.of( // 输入操作 + "1", this::inputText, + "2", this::uploadFile ) -// "2", Map.of( // 弹窗操作 -// "1", this::handlePopup -// ), -// "3", Map.of( // 元素操作 -// "1", this::submitForm, -// "2", this::handleDropdown, -// "3", this::setOption, -// "4", this::waitForElement -// ), -// "4", Map.of( // 鼠标操作 -// "1", this::mouseClick, -// "2", this::mouseMove, -// "3", this::mouseDrag -// ), -// "5", Map.of( // 输入操作 -// "1", this::inputText, -// "2", this::uploadFile -// ) ); // 获取对应的操作类型映射 @@ -253,7 +284,16 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { UiHighSettingVO otherSetting = extractOtherSetting(uiHighSettingVOList); OtherSettingsQO otherSettingsQO = otherSetting.getOtherSettingsQO(); Integer screenshotConfiguration = otherSettingsQO.getScreenshotConfiguration(); //是否截图 1当前步骤截图 2异常截图 3 不截图 - + //等待元素超时时间 + Integer waitElementTime = otherSettingsQO.getWaitElementTime()==null?0:otherSettingsQO.getWaitElementTime(); + //前置操作设置 + List beforeSettingList = extractbeforeSetting(uiHighSettingVOList); + //后置操作设置 + List afterSettingList = extractafterSetting(uiHighSettingVOList); + //前置等待时间 + int beforeAwaitTime = getAwaitTime(beforeSettingList); + //后置等待时间 + int afterAwaitTime = getAwaitTime(afterSettingList); // 2. 准备报告对象 UiSceneStepsReport report = new UiSceneStepsReport(); report.setId(sceneStepsReportId); @@ -261,28 +301,42 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { Date endTime = null; boolean continueExecution = true; // 默认继续执行 try { - stepsSucceedNumber++; - if (screenshotConfiguration == 1) { + // 执行前置等待 + if (beforeAwaitTime > 0) { + log.info("执行前置等待: {}ms", beforeAwaitTime); + Thread.sleep(beforeAwaitTime); + } + if (screenshotConfiguration == 1) { + report.setScreenshot(seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile())); + log.info("截图成功,路径:{}",seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile())); } // 3. 执行具体步骤 stepExecution.execute(step, seleniumUtils); + + // 执行后置等待 + if (afterAwaitTime > 0) { + log.info("执行后置等待: {}ms", afterAwaitTime); + Thread.sleep(afterAwaitTime); + } + report.setLogInfo("OK"); report.setExecutionFlag("1"); } catch (Exception e) { // 4. 错误处理 - stepsErrorNumber++; - stepsSucceedNumber--; report.setLogInfo(e.toString()); report.setExecutionFlag("2"); // 根据错误处理设置决定是否继续 if ("1".equals(errorSetting.getErrorHandling())) { continueExecution = false; // 不继续执行后续步骤 } + if (screenshotConfiguration == 2) { + report.setScreenshot(seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile())); + log.info("截图成功,路径:{}",seleniumUtils.takeScreenshotAsFile(TestConfig.getProfile())); + } log.error("步骤执行失败: {}", e.getMessage()); } finally { endTime = DateUtils.getNowDate(); - stepsNotNumber--; // 更新报告 report.setCreateTime(startTime); report.setUpdateTime(endTime); @@ -293,6 +347,52 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { return continueExecution; } + + /** + * 提取前置操作 + * @param settings + * @return + */ + private List extractbeforeSetting(List settings) { + return settings.stream() + .filter(e -> "1".equals(e.getSettingType()) && e.getIsDisabled() == 0) + .collect(Collectors.toList()); // 返回默认设置 + } + + /** + * 获取(前置//后置)等待时间 + * @param settingList + * @return + */ + private int getAwaitTime(List settingList){ + //获取前置等待时间 + int totalWaitTime = settingList.stream() + .map(UiHighSettingVO::getAwateTime) + .filter(Objects::nonNull) + .filter(s -> !s.isEmpty()) + .mapToInt(s -> { + try { + return Integer.parseInt(s); + } catch (NumberFormatException e) { + return 0; + } + }) + .sum(); + return totalWaitTime; + } + + + /** + * 提取后置操作 + * @param settings + * @return + */ + private List extractafterSetting(List settings) { + return settings.stream() + .filter(e -> "2".equals(e.getSettingType()) && e.getIsDisabled() == 0) + .collect(Collectors.toList()); // 返回默认设置 + } + /** * 提取错误处理设置 * @param settings @@ -305,6 +405,9 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { .orElse(new UiHighSettingVO()); // 返回默认设置 } + + + /** * 提取错误处理设置 * @param settings @@ -317,6 +420,14 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { .orElse(new UiHighSettingVO()); // 返回默认设置 } + + + + + + + //---------------------浏览器操作--------------------------- + /** * 打开网页 * @@ -324,8 +435,465 @@ public class UiSceneStepsServiceImpl implements IUiSceneStepsService { * @param seleniumUtils Selenium工具类 */ private void openWebPage(UiSceneSteps step, SeleniumUtils seleniumUtils) { + log.info("打开网页:{}",step.getUrl()); seleniumUtils.openUrl(step.getUrl()); } + /** + * 关闭网页 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void closeBrowser(UiSceneSteps step, SeleniumUtils seleniumUtils) { + log.info("关闭网页"); + seleniumUtils.quit(); + } + + /** + * 切换窗口 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void switchWindow(UiSceneSteps step, SeleniumUtils seleniumUtils) { + if (step.getOperate()==1){ + log.info("根据句柄 ID 切换到指定窗口"); + //句柄id + String handleId = step.getHandleId(); + seleniumUtils.switchToWindowByHandle(handleId); + }else if (step.getOperate()==2){ + log.info("根据网页索引号切换到指定窗口"); + //网页索引号 + Integer frameIndex = step.getFrameIndex(); + seleniumUtils.switchToWindowByIndex(frameIndex); + }else if (step.getOperate()==3){ + log.info("切换到初始窗口"); + seleniumUtils.switchToDefaultWindow(); + }else{ + log.info("无效的窗口切换操作类型"); + throw new IllegalArgumentException("无效的窗口切换操作类型"); + } + } + + /** + * 设置窗口大小 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void resizeWindow(UiSceneSteps step, SeleniumUtils seleniumUtils) { + if (step.getOperate()==1){ + log.info("窗口最大化"); + //窗口最大化 + seleniumUtils.maximizeWindow(); + }else if (step.getOperate()==2){ + log.info("设置窗口大小"); + String windowSize = step.getWindowSize(); + String[] dimensions = windowSize.split("\\*"); + int width = Integer.parseInt(dimensions[0]); + int height = Integer.parseInt(dimensions[1]); + seleniumUtils.setWindowSize(width, height); + }else{ + log.info("无效的设置窗口大小"); + throw new IllegalArgumentException("无效的设置窗口大小"); + } + } + + + /** + * 选择内嵌网页 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void selectEmbeddedPage(UiSceneSteps step, SeleniumUtils seleniumUtils) { + if (step.getOperate()==1){ + log.info("退出当前 frame(回到主页面)"); + seleniumUtils.switchToDefaultContent(); + }else if (step.getOperate()==2){ + log.info("根据 frame 索引号切换到指定 frame"); + //网页索引号 + seleniumUtils.switchToFrameByIndex(step.getFrameIndex()); + }else if (step.getOperate()==3){ + log.info("根据定位方式切换 frame"); + if ("2".equals(step.getOperateObject())) { + //元素对象 + String operateLocType = step.getOperateLocType(); //属性 + String operateLocValue = step.getOperateLocValue(); //值 + seleniumUtils.switchToFrameByLocator( + operateLocType, + operateLocValue + ); + }else{ + //元素定位 + UiElement uiElement = uiElementService.selectUiElementById(step.getOperateElementId()); + if (uiElement == null) { + throw new IllegalArgumentException("未找到ID为 " + step.getOperateElementId() + " 的元素"); + } + String locType = uiElement.getLocType(); //属性 + String elementLoc = uiElement.getElementLoc();//值 + seleniumUtils.switchToFrameByLocator( + locType, + elementLoc + ); + } + } else{ + log.info("无效的选择内嵌网页"); + throw new IllegalArgumentException("无效的选择内嵌网页"); + } + } + + + //--------------------弹窗操作-------------------- + + /** + * 弹窗操作 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void handlePopup(UiSceneSteps step, SeleniumUtils seleniumUtils) { + if (step.getOperate()==1){ + log.info("弹窗操作"); + // 获取输入内容(如果需要) + String inputValue = "1".equals(step.getIsEnter()) ? step.getInputValue() : null; + String operateWay = step.getOperateWay(); //操作方式 1确定 2取消 + // 执行弹窗操作 + seleniumUtils.handleAlert( + Integer.parseInt(operateWay), // 操作方式 (1=确定, 2=取消) + inputValue // 输入内容 + ); + }else{ + log.info("无效的弹窗操作"); + throw new IllegalArgumentException("无效的弹窗操作"); + } + } + + //---------------------------元素操作---------------------------------- + + /** + * 提交表单 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void submitForm(UiSceneSteps step, SeleniumUtils seleniumUtils) { + if ("2".equals(step.getOperateObject())) { + //元素对象 + String operateLocType = step.getOperateLocType(); //属性 + String operateLocValue = step.getOperateLocValue(); //值 + // 根据属性和值定位元素并提交表单 + seleniumUtils.findElement(operateLocType, operateLocValue).submit(); + log.info("通过元素对象提交表单,定位方式:{},定位值:{}", operateLocType, operateLocValue); + }else{ + //元素定位 + UiElement uiElement = uiElementService.selectUiElementById(step.getOperateElementId()); + if (uiElement == null) { + throw new IllegalArgumentException("未找到ID为 " + step.getOperateElementId() + " 的元素"); + } + String locType = uiElement.getLocType(); //属性 + String elementLoc = uiElement.getElementLoc();//值 + seleniumUtils.findElement(locType, elementLoc).submit(); + log.info("通过元素定位提交表单,定位方式:{},定位值:{}", locType, elementLoc); + } + } + + + /** + * 下拉框操作 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void handleDropdown(UiSceneSteps step, SeleniumUtils seleniumUtils) { + log.info("下拉框操作"); + Integer operate = step.getOperate(); //操作 1选择 2取消 + WebElement dropdownElement; + if ("2".equals(step.getOperateObject())) { + //元素对象 + String operateLocType = step.getOperateLocType(); //属性 + String operateLocValue = step.getOperateLocValue(); //值 + dropdownElement = seleniumUtils.findElement(operateLocType, operateLocValue); + log.info("通过元素对象定位下拉框,定位方式:{},定位值:{}", operateLocType, operateLocValue); + }else{ + //元素定位 + UiElement uiElement = uiElementService.selectUiElementById(step.getOperateElementId()); + if (uiElement == null) { + throw new IllegalArgumentException("未找到ID为 " + step.getOperateElementId() + " 的元素"); + } + String locType = uiElement.getLocType(); //属性 + String elementLoc = uiElement.getElementLoc();//值 + dropdownElement = seleniumUtils.findElement(locType, elementLoc); + log.info("通过元素定位定位下拉框,定位方式:{},定位值:{}", locType, elementLoc); + } + //子选项 + String subOptions = step.getSubOptions(); //1 选项 2索引 3值 + String subOptionsValue = step.getSubOptionsValue(); //值 + + if (operate == 1) { + // 选择操作 + seleniumUtils.selectDropdownOption(dropdownElement, subOptions, subOptionsValue); + } else if (operate == 2) { + // 取消操作 + // 如果需要取消下拉框的选择,可以使用默认选项或其他逻辑 + log.info("取消下拉框选择"); + // 示例:取消选择(假设下拉框的第一个选项是默认选项) + seleniumUtils.selectDropdownOption(dropdownElement, "2", "0"); + } else { + throw new IllegalArgumentException("无效的操作类型: " + operate); + } + } + + /** + * 设置选项 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void setOption(UiSceneSteps step, SeleniumUtils seleniumUtils) { + log.info("设置选项"); + Integer operate = step.getOperate(); // 操作 1选择 2取消 + WebElement element; + + if ("2".equals(step.getOperateObject())) { + // 元素对象 + String operateLocType = step.getOperateLocType(); // 属性 + String operateLocValue = step.getOperateLocValue(); // 值 + element = seleniumUtils.findElement(operateLocType, operateLocValue); + log.info("通过元素对象定位选项,定位方式:{},定位值:{}", operateLocType, operateLocValue); + } else { + // 元素定位 + UiElement uiElement = uiElementService.selectUiElementById(step.getOperateElementId()); + if (uiElement == null) { + log.error("未找到指定的元素,元素ID:{}", step.getOperateElementId()); + throw new IllegalArgumentException("未找到指定的元素"); + } + String locType = uiElement.getLocType(); // 属性 + String elementLoc = uiElement.getElementLoc(); // 值 + element = seleniumUtils.findElement(locType, elementLoc); + log.info("通过元素定位定位选项,定位方式:{},定位值:{}", locType, elementLoc); + } + + if (operate == 1) { + // 选择操作 + if (!element.isSelected()) { + element.click(); + log.info("设置选项为选中状态"); + } + } else if (operate == 2) { + // 取消操作 + if (element.isSelected()) { + element.click(); + log.info("设置选项为未选中状态"); + } + } else { + throw new IllegalArgumentException("无效的操作类型: " + operate); + } + } + + + /** + * 等待元素 + * + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void waitForElement(UiSceneSteps step, SeleniumUtils seleniumUtils) { + log.info("等待元素"); + //操作 1等待元素等于给定的定值(Text) 2 等待元素存在 3等待元素显示 4等待元素不显示 5等待元素不存在 6等待元素可编辑 7等待元素不可编辑 + By locator; + if ("2".equals(step.getOperateObject())) { + //元素对象 + String operateLocType = step.getOperateLocType(); //属性 + String operateLocValue = step.getOperateLocValue(); //值 + // 使用直接指定的定位方式 + locator = seleniumUtils.getLocator( + operateLocType, + operateLocValue + ); + }else{ + //元素定位 + UiElement uiElement = uiElementService.selectUiElementById(step.getOperateElementId()); + String locType = uiElement.getLocType(); //属性 + String elementLoc = uiElement.getElementLoc();//值 + locator = seleniumUtils.getLocator( + locType, + elementLoc + ); + } + // 解析等待时间(默认15000ms) + long timeout = 15000; + if (step.getOperate() != 1 && step.getAwaitValue() != null) { + try { + timeout = Long.parseLong(step.getAwaitValue()); + } catch (NumberFormatException e) { + log.warn("等待时间格式错误,使用默认值15000ms"); + } + } + // 执行等待操作 + seleniumUtils.waitForElement( + locator, + step.getOperate(), + step.getOperate() == 1 ? step.getAwaitValue() : null, + timeout + ); + } + + + //--------------------------鼠标操作------------------------------ + + + /** + * 鼠标点击操作 + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void mouseClick(UiSceneSteps step, SeleniumUtils seleniumUtils) { + try { + log.info("执行鼠标操作,方式: {}", step.getClickMethod()); + // 获取定位器 + By locator; + if ("2".equals(step.getOperateObject())) { + // 使用直接指定的定位方式 + locator = seleniumUtils.getLocator( + step.getOperateLocType(), + step.getOperateLocValue() + ); + } else { + // 使用元素ID从数据库获取定位信息 + UiElement uiElement = uiElementService.selectUiElementById(step.getOperateElementId()); + if (uiElement == null) { + throw new IllegalArgumentException("未找到ID为 " + step.getOperateElementId() + " 的元素"); + } + locator = seleniumUtils.getLocator( + uiElement.getLocType(), + uiElement.getElementLoc() + ); + } + // 执行鼠标操作 + seleniumUtils.performMouseAction( + locator, + Integer.parseInt(step.getClickMethod()) + ); + } catch (NumberFormatException e) { + log.error("鼠标操作类型格式错误: {}", step.getClickMethod()); + throw new RuntimeException("鼠标操作类型必须是数字(1-5)", e); + } catch (Exception e) { + log.error("鼠标操作执行失败", e); + throw new RuntimeException("鼠标操作失败: " + e.getMessage(), e); + } + } + + /** + * 鼠标移动操作 + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + private void mouseMove(UiSceneSteps step, SeleniumUtils seleniumUtils) { + try { + log.info("执行鼠标移动操作,方式: {}", step.getClickMethod()); + // 获取定位器 + By locator; + if ("2".equals(step.getOperateObject())) { + // 使用直接指定的定位方式 + locator = seleniumUtils.getLocator( + step.getOperateLocType(), + step.getOperateLocValue() + ); + } else { + // 使用元素ID从数据库获取定位信息 + UiElement uiElement = uiElementService.selectUiElementById(step.getOperateElementId()); + if (uiElement == null) { + throw new IllegalArgumentException("未找到ID为 " + step.getOperateElementId() + " 的元素"); + } + locator = seleniumUtils.getLocator( + uiElement.getLocType(), + uiElement.getElementLoc() + ); + } + // 执行鼠标移动操作 + seleniumUtils.performMouseMove( + locator, + Integer.parseInt(step.getClickMethod()) // 1=移出,2=移入 + ); + + + } catch (NumberFormatException e) { + log.error("鼠标移动类型格式错误: {}", step.getClickMethod()); + throw new RuntimeException("鼠标移动类型必须是数字(1-2)", e); + } catch (Exception e) { + log.error("鼠标移动操作执行失败", e); + throw new RuntimeException("鼠标移动操作失败: " + e.getMessage(), e); + } + } + + /** + * 输入操作 + * @param step 步骤对象 + * @param seleniumUtils Selenium工具类 + */ + public void inputText(UiSceneSteps step, SeleniumUtils seleniumUtils) { + try { + log.info("执行输入操作,类型: {}", step.getOperate()); + + // 获取定位器 + By locator; + if ("2".equals(step.getOperateObject())) { + // 使用直接指定的定位方式 + locator = seleniumUtils.getLocator( + step.getOperateLocType(), + step.getOperateLocValue() + ); + } else { + // 使用元素ID从数据库获取定位信息 + UiElement uiElement = uiElementService.selectUiElementById(step.getOperateElementId()); + if (uiElement == null) { + throw new IllegalArgumentException("未找到ID为 " + step.getOperateElementId() + " 的元素"); + } + locator = seleniumUtils.getLocator( + uiElement.getLocType(), + uiElement.getElementLoc() + ); + } + + // 获取输入内容 + String inputValue = step.getInputValue(); + if (inputValue == null || inputValue.trim().isEmpty()) { + log.warn("输入内容为空,将执行清空操作"); + seleniumUtils.clearInput(locator); + return; + } + // 执行输入操作 + if (step.getOperate() == 1) { // 普通输入框 + seleniumUtils.inputText( + locator, + inputValue, + true, // 默认先清空 + 1 // 默认使用普通输入方式 + ); + } else if (step.getOperate() == 2) { // 可编辑段落 + seleniumUtils.inputToEditableDiv( + locator, + inputValue + ); + } else { + throw new IllegalArgumentException("不支持的输入操作类型: " + step.getOperate()); + } + + } catch (Exception e) { + log.error("输入操作执行失败", e); + throw new RuntimeException("输入操作失败: " + e.getMessage(), e); + } + } + + /** + * 文件上传 + * @param step + * @param seleniumUtils + */ + public void uploadFile(UiSceneSteps step, SeleniumUtils seleniumUtils){ + + } } \ No newline at end of file diff --git a/test-test/src/main/resources/mapper/test/UiAutomationMapper.xml b/test-test/src/main/resources/mapper/test/UiAutomationMapper.xml index 85373ab..037908c 100644 --- a/test-test/src/main/resources/mapper/test/UiAutomationMapper.xml +++ b/test-test/src/main/resources/mapper/test/UiAutomationMapper.xml @@ -22,10 +22,12 @@ + + - 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,description 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 from ui_automation