diff --git a/mutual_methods/src/main/java/helper/selfheal/OcrAndVisualHelper.java b/mutual_methods/src/main/java/helper/selfheal/OcrAndVisualHelper.java deleted file mode 100644 index 8cb0fd8..0000000 --- a/mutual_methods/src/main/java/helper/selfheal/OcrAndVisualHelper.java +++ /dev/null @@ -1,24 +0,0 @@ -package helper.selfheal; - -import configuration.Configuration; - -/** - * Placeholder stub for OCR/visual alignment. Behind a config flag to avoid deps for now. - */ -public class OcrAndVisualHelper { - - public static boolean isEnabled() { - String v = Configuration.getInstance().getStringValueOfProp("selfheal.ocr.enabled"); - return v != null && Boolean.parseBoolean(v); - } - - /** - * Returns a small boost factor based on hypothetical visual alignment. For MVP, returns 0. - */ - public static double visualBoost(String candidateText) { - if (!isEnabled()) return 0.0; - return 0.0; - } -} - - diff --git a/mutual_methods/src/main/java/helper/selfheal/SelfHealingEngine.java b/mutual_methods/src/main/java/helper/selfheal/SelfHealingEngine.java index 97fbb16..37abb70 100644 --- a/mutual_methods/src/main/java/helper/selfheal/SelfHealingEngine.java +++ b/mutual_methods/src/main/java/helper/selfheal/SelfHealingEngine.java @@ -1,5 +1,7 @@ package helper.selfheal; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; import com.openai.models.ChatModel; import configuration.Configuration; import helper.selfheal.ai.AiLocatorClient; @@ -7,6 +9,7 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.openqa.selenium.*; +import org.openqa.selenium.NoSuchElementException; import org.openqa.selenium.io.FileHandler; import java.io.File; @@ -20,8 +23,8 @@ import java.util.*; /** - * Minimal self-healing MVP: on first locator failure, capture screenshot and a compact DOM snapshot, - * generate a few heuristic candidates and optionally act in shadow-mode (observe only). + * Minimal self-healing MVP: on locator failure, capture screenshot & DOM snapshot, + * generate candidates (AI + heuristics), and try to recover. */ public class SelfHealingEngine { @@ -45,114 +48,164 @@ public HealingResult onFailure(WebDriver driver, By original, String action, Str return new HealingResult(Optional.empty(), List.of("self-heal disabled")); } - List trace = new ArrayList<>(); - String ts = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss_SSS").format(LocalDateTime.now()); - Path outDir = Path.of("reports", "self-heal", ts); - try { - Files.createDirectories(outDir); - } catch (IOException ignored) {} + Path outDir = prepareOutDir(trace); // --- Fast path reuse --- + Optional fastPath = tryReuseWinner(driver, original, shadowMode, trace); + if (fastPath.isPresent()) { + return new HealingResult(fastPath, trace); + } + + // --- capture evidence --- + File screenshotBase64 = null; try { - String url = driver.getCurrentUrl(); - var fast = ModelStore.getWinner(url, original); - if (fast.isPresent()) { - try { - driver.findElement(fast.get()); - trace.add("fast-path winner reused: " + fast.get()); - if (shadowMode) return new HealingResult(Optional.empty(), trace); - return new HealingResult(fast, trace); - } catch (org.openqa.selenium.NoSuchElementException ignore) { - trace.add("fast-path winner stale; falling back"); - } + if (driver instanceof TakesScreenshot ts) { + screenshotBase64 = ts.getScreenshotAs(OutputType.FILE); } } catch (Exception e) { - trace.add("fast-path error: " + e.getMessage()); + trace.add("screenshot capture failed: " + e.getMessage()); } - - // --- capture evidence --- - captureScreenshot(driver, outDir, trace); String dom = captureDomSnapshot(driver, trace); writeText(outDir.resolve("dom.txt"), dom); + // --- Generate candidates (AI + heuristics) --- + List candidates = generateCandidates(original, stepText, dom, aiEnabled, driver, trace, screenshotBase64); + + // --- Try all candidates --- + Optional winner = tryCandidates(driver, original, candidates, trace); + + // --- log decision --- + logHealingResult(outDir, original, winner, candidates, trace); + + if (winner.isPresent()) { + if (shadowMode) { + log.info("Self-heal (shadow-mode) would use: {} for action {}", winner.get(), action); + return new HealingResult(Optional.empty(), trace); + } else { + log.info("Self-heal will use: {} for action {}", winner.get(), action); + return new HealingResult(winner, trace); + } + } + return new HealingResult(Optional.empty(), trace); + } + + // ----------------- Candidate generation ----------------- + + private List generateCandidates( + By original, + String stepText, + String dom, + boolean aiEnabled, + WebDriver driver, + List trace, + File screenshotBase64 + ) { Set candidateSet = new LinkedHashSet<>(); - // --- AI candidates --- + // AI candidates if (aiEnabled) { try { String domSnippet = dom.length() > 20_000 ? dom.substring(0, 20_000) : dom; AiLocatorClient aiClient = new AiLocatorClient(ChatModel.GPT_4_1); - aiClient.suggest(original.toString(), stepText, domSnippet, driver.getCurrentUrl()) + aiClient.suggest(original.toString(), stepText, domSnippet, driver.getCurrentUrl(), screenshotBase64) .ifPresent(aiRes -> { var aiCands = AiLocatorSuggester.toByCandidates(aiRes, original.toString()); trace.add("AI candidates: " + aiCands); - candidateSet.addAll(aiCands); // ✅ add to set + candidateSet.addAll(aiCands); }); } catch (Exception e) { trace.add("AI suggest error: " + e.getMessage()); } + } else { + trace.add("AI not enabled — using heuristic candidates only"); } - // --- Heuristic candidates --- - List candidates = new ArrayList<>(candidateSet); + // Heuristic candidates + candidateSet.addAll(generateHeuristicCandidates(original, stepText, trace)); - if (OcrAndVisualHelper.isEnabled()) { - trace.add("OCR enabled (stub) - visual alignment would run here"); - } + return new ArrayList<>(candidateSet); + } - // --- Try all candidates --- - Optional winner = Optional.empty(); - for (By by : candidates) { - var currentImplicitWait = driver.manage().timeouts().getImplicitWaitTimeout(); - try { - driver.manage().timeouts().implicitlyWait(Duration.ofMillis(10)); - driver.findElement(by); - winner = Optional.of(by); - trace.add("candidate found: " + by); - // Persist winner - try { - ModelStore.saveWinner(driver.getCurrentUrl(), original, by); - } catch (Exception ignored) {} - driver.manage().timeouts().implicitlyWait(currentImplicitWait); - break; - } catch (org.openqa.selenium.NoSuchElementException ignore) { - trace.add("candidate not found: " + by); - driver.manage().timeouts().implicitlyWait(currentImplicitWait); + private List generateHeuristicCandidates(By original, String stepText, List trace) { + List list = new ArrayList<>(); + trace.add("heuristic: from original=" + original); + + if (stepText != null && !stepText.isBlank()) { + String txt = stepText.trim(); + list.add(By.xpath("//*[normalize-space(.)='" + escapeXPath(txt) + "']")); + list.add(By.xpath("//*[contains(normalize-space(.), '" + escapeXPath(txt) + "')]")); + + String[] attrs = {"data-testid", "data-qa", "aria-label", "title", "name"}; + for (String a : attrs) { + list.add(By.cssSelector("*[" + a + "~='" + cssEscape(stepText) + "']")); + list.add(By.cssSelector("*[" + a + "*='" + cssEscape(stepText) + "']")); } } - // --- log decision --- - writeText(outDir.resolve("decision.log"), String.join(System.lineSeparator(), trace)); + list.add(original); // always last fallback + return new ArrayList<>(new LinkedHashSet<>(list)); + } - if (winner.isPresent()) { - if (shadowMode) { - log.info("Self-heal (shadow-mode) would use: {} for action {}", winner.get(), action); - return new HealingResult(Optional.empty(), trace); - } else { - log.info("Self-heal will use: {} for action {}", winner.get(), action); - return new HealingResult(winner, trace); + // ----------------- Candidate validation ----------------- + + private Optional tryCandidates(WebDriver driver, By original, List candidates, List trace) { + Duration originalWait = driver.manage().timeouts().getImplicitWaitTimeout(); + + try { + for (By by : candidates) { + try { + driver.manage().timeouts().implicitlyWait(Duration.ofMillis(10)); + driver.findElement(by); + trace.add("candidate found: " + by); + try { + ModelStore.saveWinner(driver.getCurrentUrl(), original, by); + } catch (Exception ignored) {} + return Optional.of(by); + } catch (NoSuchElementException ignore) { + trace.add("candidate not found: " + by); + } } + } finally { + driver.manage().timeouts().implicitlyWait(originalWait); } - - return new HealingResult(Optional.empty(), trace); + return Optional.empty(); } + private Optional tryReuseWinner(WebDriver driver, By original, boolean shadowMode, List trace) { + Duration originalWait = driver.manage().timeouts().getImplicitWaitTimeout(); - private void captureScreenshot(WebDriver driver, Path outDir, List trace) { try { - if (driver instanceof TakesScreenshot ts) { - File src = ts.getScreenshotAs(OutputType.FILE); - File dest = outDir.resolve("page.png").toFile(); - FileHandler.createDir(dest.getParentFile()); - FileHandler.copy(src, dest); - trace.add("screenshot saved: " + dest.getAbsolutePath()); - } else { - trace.add("driver not screenshot-capable"); + String url = driver.getCurrentUrl(); + var fast = ModelStore.getWinner(url, original); + if (fast.isPresent()) { + try { + driver.manage().timeouts().implicitlyWait(Duration.ofMillis(10)); + driver.findElement(fast.get()); + trace.add("fast-path winner reused: " + fast.get()); + if (shadowMode) return Optional.empty(); + return fast; + } catch (NoSuchElementException ignore) { + trace.add("fast-path winner stale; falling back"); + } } } catch (Exception e) { - trace.add("screenshot error: " + e.getMessage()); + trace.add("fast-path error: " + e.getMessage()); + } finally { + driver.manage().timeouts().implicitlyWait(originalWait); } + return Optional.empty(); + } + + // ----------------- Evidence helpers ----------------- + + private Path prepareOutDir(List trace) { + String ts = DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss_SSS").format(LocalDateTime.now()); + Path outDir = Path.of("reports", "self-heal", ts); + try { + Files.createDirectories(outDir); + } catch (IOException ignored) {} + return outDir; } private String captureDomSnapshot(WebDriver driver, List trace) { @@ -168,33 +221,6 @@ private String captureDomSnapshot(WebDriver driver, List trace) { return ""; } - private List generateHeuristicCandidates(By original, String stepText, List trace) { - List list = new ArrayList<>(); - trace.add("original: " + original); - // Keep original as last retry - // Simple text-based contains if stepText exists - if (stepText != null && !stepText.isBlank()) { - String txt = stepText.trim(); - list.add(By.xpath("//*[normalize-space(.)='" + escapeXPath(txt) + "']")); - list.add(By.xpath("//*[contains(normalize-space(.), '" + escapeXPath(txt) + "')]")); - } - // Common data-* and aria attributes - String[] attrs = {"data-testid", "data-qa", "aria-label", "title", "name"}; - for (String a : attrs) { - list.add(By.cssSelector("*[" + a + "~='" + cssEscape(stepText) + "']")); - list.add(By.cssSelector("*[" + a + "*='" + cssEscape(stepText) + "']")); - } - // Fallback: original at the end - list.add(original); - // De-duplicate - LinkedHashSet set = new LinkedHashSet<>(list); - List deduped = new ArrayList<>(set); - if (OcrAndVisualHelper.isEnabled() && stepText != null && !stepText.isBlank()) { - deduped.sort((a, b) -> Double.compare(score(b, stepText), score(a, stepText))); - } - return deduped; - } - private void writeText(Path path, String content) { try { Files.createDirectories(path.getParent()); @@ -202,6 +228,8 @@ private void writeText(Path path, String content) { } catch (IOException ignored) {} } + // ----------------- Utility ----------------- + private String escapeXPath(String s) { return s.replace("'", "\"\""); } @@ -211,15 +239,26 @@ private String cssEscape(String s) { return s.replace("'", "\\'").replace("\"", "\\\""); } - private double score(By by, String stepText) { - double base = 0.0; - String s = by.toString().toLowerCase(Locale.ROOT); - if (s.contains("contains(normalize-space") || s.contains("normalize-space(.)='")) { - base += 0.1; + private void logHealingResult(Path outDir, By original, Optional winner, + List candidates, List trace) { + try { + // 1. Human-readable decision log + writeText(outDir.resolve("decision.log"), String.join(System.lineSeparator(), trace)); + + // 2. Structured JSON log + Map data = new LinkedHashMap<>(); + data.put("originalLocator", original.toString()); + data.put("winner", winner.map(Object::toString).orElse("none")); + data.put("candidates", candidates.stream().map(Object::toString).toList()); + data.put("trace", trace); + data.put("timestamp", LocalDateTime.now().toString()); + ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT); + String json = mapper.writeValueAsString(data); + + TelemetryLogger.writeJson(outDir,"healing.json", json); + + } catch (Exception e) { + log.warn("Failed to log healing result", e); } - base += OcrAndVisualHelper.visualBoost(stepText); - return base; } } - - diff --git a/mutual_methods/src/main/java/helper/selfheal/ai/AiLocatorClient.java b/mutual_methods/src/main/java/helper/selfheal/ai/AiLocatorClient.java index ff1454d..f37691f 100644 --- a/mutual_methods/src/main/java/helper/selfheal/ai/AiLocatorClient.java +++ b/mutual_methods/src/main/java/helper/selfheal/ai/AiLocatorClient.java @@ -6,6 +6,7 @@ import com.openai.models.chat.completions.StructuredChatCompletion; import com.openai.models.chat.completions.StructuredChatCompletionCreateParams; +import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -49,37 +50,73 @@ public boolean isEnabled() { /** * Ask the model for corrected texts and best-guess locators. */ - public Optional suggest(String originalLocator, String stepText, String domSnippet, String url) { + public Optional suggest( + String originalLocator, + String stepText, + String domSnippet, + String url, + File screenshotBase64 + ) { if (!enabled) { // skip AI entirely if no API key return emptyResult(); } var sys = """ - You are a locator-fixing assistant for Selenium/Appium tests. - Given: (1) a failing locator, (2) optional human step text, (3) a DOM snippet. - Tasks: - - Detect likely typos in the target text and propose corrected variants (keep punctuation/case realistic). - - Output robust XPaths and optional CSS selectors that would match the corrected text, preferring text-safe forms: - //tag[normalize-space(.)="..."] and //*[normalize-space(.)="..."] - Use contains(...) only as a fallback. - - Keep suggestions short, valid, and de-duplicated. - - Do NOT include any explanation in fields; only the values themselves. + You are a locator-fixing assistant for Selenium/Appium automated tests. + + Inputs you will receive: + 1. The original failing locator (may be incomplete, truncated, or slightly wrong). + 2. Optional human-readable step text describing the element. + 3. A DOM snippet (may be partial or truncated). + 4. (Optional) A screenshot of the page for additional context. + + Your tasks: + - Analyze the failing locator and compare it against all available DOM attributes (id, name, class, text, aria-*, data-*). + - If the locator is incomplete (e.g., "hide" instead of "hide-textbox"), detect likely matches by: + • Expanding substrings + • Checking case-insensitive and trimmed matches + • Matching step text to nearby labels or innerText + • Considering common attribute variations (id, name, class, text, aria-label, title, etc.) + - Propose corrected locators across **all supported strategies**, not just the original type. Supported strategies: + • ID → By.id(value) + • NAME → By.name(value) + • CLASS_NAME → By.className(value) + • CSS_SELECTOR → By.cssSelector(value) + • XPATH → By.xpath(value) + • LINK_TEXT → By.linkText(value) + • PARTIAL_LINK_TEXT → By.partialLinkText(value) + • TAG_NAME → By.tagName(value) + • TEXT → By.text(value) (custom framework extension) + + Output requirements: + - Return a list of corrected texts (if any typos/variants were found). + - Return a list of candidate locators (XPath, CSS, ID, etc.), deduplicated and minimal. + - Use robust, text-safe XPath patterns when necessary: + //tag[normalize-space(.)="..."] + //*[normalize-space(.)="..."] + Only use contains(...) as a fallback. + - Prefer simple strategies (id, name, text) over complex XPath if available. + - Do NOT include any explanation or reasoning in the fields — only the raw values. """; var user = String.format(""" + Page context: URL: %s Original locator: %s - Step text (may be noisy): %s - - DOM snippet (may be truncated): + Step text: %s + + DOM snippet (truncated if large): %s + + Screenshot: (optional, base64 or reference) %s """, - Objects.toString(url, ""), - Objects.toString(originalLocator, ""), - Objects.toString(stepText, ""), - Objects.toString(domSnippet, "") - ); + Objects.toString(url, ""), + Objects.toString(originalLocator, ""), + Objects.toString(stepText, ""), + Objects.toString(domSnippet, ""), + screenshotBase64 + ); StructuredChatCompletionCreateParams params = StructuredChatCompletionCreateParams.builder() diff --git a/mutual_methods/src/main/java/imp/CompareImp.java b/mutual_methods/src/main/java/imp/CompareImp.java index 219d86c..a3608f7 100644 --- a/mutual_methods/src/main/java/imp/CompareImp.java +++ b/mutual_methods/src/main/java/imp/CompareImp.java @@ -56,7 +56,12 @@ public void dataCompareContainsFromResponse(String selector, String value) throw public void compareListCount(String storeKey, int count){ var list = (List) Utils.getFromStoreData(storeKey); assertEquals(count,list.size()); - //th[contains("Who Initiated Transfer")]/following-sibling::td/li/span + } + + @Step("Get from scenario store and then, compare with , Are they equals?") + public void compareString(String storeKey, int count){ + var list = (List) Utils.getFromStoreData(storeKey); + assertEquals(count,list.size()); } } diff --git a/mutual_methods/src/main/resources/config.properties b/mutual_methods/src/main/resources/config.properties index d3a4a0d..5d2750b 100644 --- a/mutual_methods/src/main/resources/config.properties +++ b/mutual_methods/src/main/resources/config.properties @@ -5,7 +5,6 @@ selfheal.threshold=0.80 selfheal.max_candidates=20 selfheal.embedding.enabled=false selfheal.visual.enabled=true -selfheal.ocr.enabled=true slack_token="" webhook="" connectionString="" diff --git a/web_testing/src/main/java/elements/LocatorFactory.java b/web_testing/src/main/java/elements/LocatorFactory.java index 0bc2740..b1c6fd9 100644 --- a/web_testing/src/main/java/elements/LocatorFactory.java +++ b/web_testing/src/main/java/elements/LocatorFactory.java @@ -7,6 +7,7 @@ import org.openqa.selenium.By; import java.util.HashMap; +import java.util.Locale; import java.util.Map; /** @@ -100,7 +101,7 @@ public static By createLocator(Map jsonMap, String jsonKey, Elem } private static LocatorType getLocatorType(Map locator, String jsonKey) { - var locatorType = locator.get("locatorType"); + var locatorType = locator.get("locatorType").toUpperCase(Locale.ENGLISH); if (locatorType != null) { return LocatorType.valueOf(locatorType); } else { diff --git a/web_testing/src/main/java/elements/WebDriverElementFinder.java b/web_testing/src/main/java/elements/WebDriverElementFinder.java index 2fc8897..8e9da25 100644 --- a/web_testing/src/main/java/elements/WebDriverElementFinder.java +++ b/web_testing/src/main/java/elements/WebDriverElementFinder.java @@ -3,6 +3,7 @@ import driver.DriverManager; import helper.selfheal.SelfHealingEngine; import org.openqa.selenium.By; +import org.openqa.selenium.InvalidSelectorException; import org.openqa.selenium.WebElement; import org.openqa.selenium.NoSuchElementException; @@ -18,7 +19,7 @@ public class WebDriverElementFinder implements ElementFinder { public WebElement findElement(By by) { try { return DriverManager.getInstance().getDriver().findElement(by); - } catch (NoSuchElementException e) { + } catch (NoSuchElementException | InvalidSelectorException e) { var driver = DriverManager.getInstance().getDriver(); SelfHealingEngine engine = new SelfHealingEngine(); var result = engine.onFailure(driver, by, "findElement", by.toString()); diff --git a/web_testing/src/main/java/helpers/WaitHelper.java b/web_testing/src/main/java/helpers/WaitHelper.java index 44db0bc..a78a38d 100644 --- a/web_testing/src/main/java/helpers/WaitHelper.java +++ b/web_testing/src/main/java/helpers/WaitHelper.java @@ -2,46 +2,120 @@ import driver.DriverManager; import elements.GetBy; +import helper.selfheal.SelfHealingEngine; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.openqa.selenium.By; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.support.ui.ExpectedCondition; import org.openqa.selenium.support.ui.WebDriverWait; +import utils.ReuseStoreData; import java.time.Duration; +import java.util.Optional; +import java.util.function.Function; /** - * Helper class for waiting operations in web applications. - * Extends GetBy to inherit locator functionality and provides WebDriverWait utilities. + * Base helper for waiting operations. + * Extends GetBy to inherit locator utilities (getByValue, etc.) + * and provides WebDriverWait instances with optional self-healing. * - * @author Web Testing Framework - * @version 2.0.0 + * Subclasses like PresenceHelper / VisibilityHelper should keep + * using getWait() — healing is transparently applied here. */ public class WaitHelper extends GetBy { + private static final Logger log = LogManager.getLogger(WaitHelper.class); + + protected static final long DEFAULT_WAIT = 15; + protected static final long DEFAULT_SLEEP_IN_MILLIS = 500; + + protected WebDriver driver() { + return DriverManager.getInstance().getDriver(); + } + /** * Gets a WebDriverWait instance with custom timeout and sleep interval. - * - * @param timeout the timeout in seconds - * @param sleepInMillis the sleep interval in milliseconds - * @return WebDriverWait instance */ protected WebDriverWait getWait(long timeout, long sleepInMillis) { - return new WebDriverWait(DriverManager.getInstance().getDriver(), Duration.ofSeconds(timeout), Duration.ofMillis(sleepInMillis)); + return new HealingWebDriverWait(driver(), Duration.ofSeconds(timeout), Duration.ofMillis(sleepInMillis)); } /** * Gets a WebDriverWait instance with custom timeout and default sleep interval. - * - * @param timeout the timeout in seconds - * @return WebDriverWait instance */ protected WebDriverWait getWait(long timeout) { - return new WebDriverWait(DriverManager.getInstance().getDriver(), Duration.ofSeconds(timeout), Duration.ofMillis(DEFAULT_SLEEP_IN_MILLIS)); + return new HealingWebDriverWait(driver(), Duration.ofSeconds(timeout), Duration.ofMillis(DEFAULT_SLEEP_IN_MILLIS)); } /** * Gets a WebDriverWait instance with default timeout and sleep interval. - * - * @return WebDriverWait instance */ protected WebDriverWait getWait() { - return new WebDriverWait(DriverManager.getInstance().getDriver(), Duration.ofSeconds(DEFAULT_WAIT), Duration.ofMillis(DEFAULT_SLEEP_IN_MILLIS)); + return new HealingWebDriverWait(driver(), Duration.ofSeconds(DEFAULT_WAIT), Duration.ofMillis(DEFAULT_SLEEP_IN_MILLIS)); + } + + // ---------------------------------------------------------------- + // Inner class: HealingWebDriverWait + // ---------------------------------------------------------------- + private static class HealingWebDriverWait extends WebDriverWait { + private final WebDriver driver; + private static final ThreadLocal healingInProgress = ThreadLocal.withInitial(() -> false); + HealingWebDriverWait(WebDriver driver, Duration timeout, Duration sleep) { + super(driver, timeout, sleep); + this.driver = driver; + } + + @Override + public V until(Function condition) { + String locatorStr = condition.toString(); + + // 🔹 Check cache first + Object cached = ReuseStoreData.get(locatorStr); + if (cached instanceof By healed) { + log.info("Using cached healed locator for {}", locatorStr); + return super.until((ExpectedCondition) + org.openqa.selenium.support.ui.ExpectedConditions.presenceOfElementLocated(healed)); + } + try { + // 🔹 Normal attempt + return super.until(condition); + } catch (org.openqa.selenium.TimeoutException tex) { + // 🔹 Only heal if timed out + By by = extractBy(locatorStr); + if (by == null) throw tex; + + log.warn("Wait timed out for locator {} — attempting self-healing", by); + SelfHealingEngine engine = new SelfHealingEngine(); + var result = engine.onFailure(driver, by, "wait", locatorStr); + + if (result.winner.isPresent()) { + By healed = result.winner.get(); + log.info("Retrying wait with healed locator: {}", healed); + + // cache healed locator + ReuseStoreData.put(locatorStr, healed); + + return super.until((ExpectedCondition) + org.openqa.selenium.support.ui.ExpectedConditions.presenceOfElementLocated(healed)); + } + + throw tex; + } + } + /** + * Tries to parse a By locator from ExpectedCondition.toString(). + * This is a heuristic and may not always succeed. + */ + private By extractBy(String conditionStr) { + if (conditionStr == null) return null; + if (conditionStr.contains("By.")) { + try { + String byPart = conditionStr.substring(conditionStr.indexOf("By.")); + return By.xpath(byPart); // minimal fallback — adapt parser as needed + } catch (Exception ignore) {} + } + return null; + } } -} \ No newline at end of file +} diff --git a/web_testing/src/main/java/imp/GetAttributeImp.java b/web_testing/src/main/java/imp/GetAttributeImp.java index be48065..686001c 100644 --- a/web_testing/src/main/java/imp/GetAttributeImp.java +++ b/web_testing/src/main/java/imp/GetAttributeImp.java @@ -43,7 +43,7 @@ public void getAttributeOfElmWithoutWait(String attribute, String jsonKey, Strin ScenarioDataStore.put(key, getAttributeWithoutWait(jsonKey, attribute)); } - @Step({"Get size of and store it scenario store with ", "sadsada"}) + @Step({"Get size of and store it scenario store with "}) @And("Get size of {string} and store it scenario store with {string}") public void getSizeOfElement(String jsonKey, String key) { ScenarioDataStore.put(key, getElementSize(jsonKey)); diff --git a/web_testing/src/test/resources/features/examples.feature b/web_testing/src/test/resources/features/examples.feature index dbffc5f..87ba83f 100644 --- a/web_testing/src/test/resources/features/examples.feature +++ b/web_testing/src/test/resources/features/examples.feature @@ -11,6 +11,8 @@ Feature: And Multiple select below indexes of "multiple_select" option | 0 | | 1 | + And Get "style" attribute of "show_hide" and store it in scenario store with "hide_example_style" and default wait + And Click on "hide_button" Examples: | Browsers | | Chrome | @@ -19,5 +21,5 @@ Feature: Scenario: default url Given Open "chrome" and get base url - Scenario: default url and browser - Given Open browser and get base url \ No newline at end of file + Scenario: default url and browser + Given Open browser and get base url \ No newline at end of file diff --git a/web_testing/src/test/resources/locators/examples.json b/web_testing/src/test/resources/locators/examples.json index 4d3a8ba..d5e033f 100644 --- a/web_testing/src/test/resources/locators/examples.json +++ b/web_testing/src/test/resources/locators/examples.json @@ -92,13 +92,20 @@ "locators": { "firstLocator": { "locatorType": "XPATH", - "locatorValue": "//legend[text()='Multiple Select Examplee']" + "locatorValue": "//legend[text()='Multiple Select Example']" }, "secondLocator": { "locatorType": "XPATH", "locatorValue": "//Select" } } - + }, + "hide_button": { + "locatorType": "id", + "locatorValue": "hide-textbox" + }, + "show_hide": { + "locatorType": "name", + "locatorValue": "show" } } \ No newline at end of file