From 3c89ebaed10fb57db276578caecbfcae63400ea6 Mon Sep 17 00:00:00 2001 From: helen_yrofeeva Date: Fri, 10 Jul 2026 12:12:21 +0300 Subject: [PATCH 1/5] [EPMHLM-546] db automigrarion --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index d4f1f24..0772e67 100644 --- a/build.gradle +++ b/build.gradle @@ -38,7 +38,7 @@ dependencies { implementation 'org.springframework.retry:spring-retry:2.0.12' implementation 'org.springframework.boot:spring-boot-starter-aspectj' - implementation 'org.liquibase:liquibase-core:4.33.0' + implementation 'org.springframework.boot:spring-boot-starter-liquibase' implementation 'org.postgresql:postgresql:42.7.7' implementation 'com.zaxxer:HikariCP:3.3.1' implementation 'com.vladmihalcea:hibernate-types-52:2.7.0' From 3344934c74f27c74e42b9fade8435f328d0fcba9 Mon Sep 17 00:00:00 2001 From: helen_yrofeeva Date: Fri, 10 Jul 2026 12:13:10 +0300 Subject: [PATCH 2/5] [EPMHLM-546] used 2 type --- .../com/epam/healenium/controller/HealingController.java | 2 +- src/main/java/com/epam/healenium/model/dto/Locator.java | 2 +- .../epam/healenium/service/impl/HealingServiceImpl.java | 8 -------- src/main/resources/application.yml | 1 + 4 files changed, 3 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/epam/healenium/controller/HealingController.java b/src/main/java/com/epam/healenium/controller/HealingController.java index e11b640..0f860dc 100644 --- a/src/main/java/com/epam/healenium/controller/HealingController.java +++ b/src/main/java/com/epam/healenium/controller/HealingController.java @@ -210,7 +210,7 @@ public ResponseEntity> getCandidate(@RequestBody RequestDto dto) { ReferenceElementsDto referenceElements = selectorService.getReferenceElements(dto); if (healingService.validateReference(referenceElements)) { List candidates = healingService.getCandidates(dto, referenceElements).stream() - .map(c -> new Locator().setUsing("css selector") + .map(c -> new Locator().setType("css selector") .setValue((String) ((By.ByCssSelector) c).getRemoteParameters().value())) .collect(Collectors.toList()); log.info("[Get Candidate] Candidates: {})", candidates); diff --git a/src/main/java/com/epam/healenium/model/dto/Locator.java b/src/main/java/com/epam/healenium/model/dto/Locator.java index 54926ec..0a59727 100644 --- a/src/main/java/com/epam/healenium/model/dto/Locator.java +++ b/src/main/java/com/epam/healenium/model/dto/Locator.java @@ -6,6 +6,6 @@ @Data @Accessors(chain = true) public class Locator { - private String using; private String value; + private String type; } diff --git a/src/main/java/com/epam/healenium/service/impl/HealingServiceImpl.java b/src/main/java/com/epam/healenium/service/impl/HealingServiceImpl.java index 7093f47..90f88b1 100644 --- a/src/main/java/com/epam/healenium/service/impl/HealingServiceImpl.java +++ b/src/main/java/com/epam/healenium/service/impl/HealingServiceImpl.java @@ -236,13 +236,6 @@ public List findNewLocations(List paths, Node destination, ReferenceEl public By toLocator(Scored node, ReferenceElementsDto referenceElements) { By locator = null; -// if (useXPath(engine)) { -// String xpath = createXPathFromElement(node.getValue(), engine); -// locator = By.xpath(xpath); -// if (isUnsuccessLocator(locator, context, engine)) { -// return null; -// } -// } for (Set detailLevel : selectorDetailLevels) { locator = construct(node.getValue(), detailLevel); @@ -260,7 +253,6 @@ public By construct(Node node, Set detailLevel) { .collect(Collectors.joining())); } - private boolean isUnsuccessLocator(By locator, ReferenceElementsDto referenceElements) { List unsuccessfulLocators = referenceElements.getUnsuccessfulLocators(); Locator candidate = new Locator(((By.ByCssSelector) locator).getRemoteParameters().using(), diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 4ff3fd2..70b177a 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -7,6 +7,7 @@ server: spring: profiles: + # pro, free default: pro group: devlocal: From 2dfa14c18bc44376a75cbb50ec31bcb5a6dd5de0 Mon Sep 17 00:00:00 2001 From: helen_yrofeeva Date: Fri, 10 Jul 2026 13:02:40 +0300 Subject: [PATCH 3/5] [EPMHLM-546] add 2 Dynamic settings --- .../healenium/config/DynamicSettings.java | 7 +++ .../healenium/service/SettingService.java | 4 ++ .../service/impl/HealingServiceImpl.java | 3 +- .../service/impl/SettingServiceImpl.java | 50 +++++++++++++++++++ 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/epam/healenium/config/DynamicSettings.java b/src/main/java/com/epam/healenium/config/DynamicSettings.java index 161f899..82b0ec1 100644 --- a/src/main/java/com/epam/healenium/config/DynamicSettings.java +++ b/src/main/java/com/epam/healenium/config/DynamicSettings.java @@ -21,5 +21,12 @@ public class DynamicSettings { @Value("${app.healing.elements:false}") private boolean findElementsAutoHealing; + + @Value("${healenium.score-cap:.6}") + private double scoreCap; + + // Selector type must be 'cssSelector' or 'xpath' + @Value("${healenium.selector-type:cssSelector}") + private String selectorType; } diff --git a/src/main/java/com/epam/healenium/service/SettingService.java b/src/main/java/com/epam/healenium/service/SettingService.java index e9d5ab3..13ffbc2 100644 --- a/src/main/java/com/epam/healenium/service/SettingService.java +++ b/src/main/java/com/epam/healenium/service/SettingService.java @@ -10,6 +10,10 @@ public interface SettingService { boolean setCollectMetrics(boolean enabled); boolean setFindElementsAutoHealing(boolean enabled); + + boolean setScoreCap(double scoreCap); + + boolean setSelectorType(String selectorType); Map updateSetting(String key, String value); diff --git a/src/main/java/com/epam/healenium/service/impl/HealingServiceImpl.java b/src/main/java/com/epam/healenium/service/impl/HealingServiceImpl.java index 90f88b1..c0156bc 100644 --- a/src/main/java/com/epam/healenium/service/impl/HealingServiceImpl.java +++ b/src/main/java/com/epam/healenium/service/impl/HealingServiceImpl.java @@ -224,8 +224,7 @@ public List findNewLocations(List paths, Node destination, ReferenceEl PathFinder pathFinder = new PathFinder(new LCSPathDistance(), new HeuristicNodeDistance()); AbstractMap.SimpleImmutableEntry>>> scoresToNodes = pathFinder.findScoresToNodes(new Path(paths.toArray(new Node[0])), destination); - // TODO get guessCap from settings - List> scoreds = pathFinder.getSortedNodes(scoresToNodes.getValue(), 1000, 0.6); + List> scoreds = pathFinder.getSortedNodes(scoresToNodes.getValue(), 1000, dynamicSettings.getScoreCap()); List healedElements = scoreds.stream() .map(node -> toLocator(node, referenceElements)) diff --git a/src/main/java/com/epam/healenium/service/impl/SettingServiceImpl.java b/src/main/java/com/epam/healenium/service/impl/SettingServiceImpl.java index d6d3e3e..c3e81d2 100644 --- a/src/main/java/com/epam/healenium/service/impl/SettingServiceImpl.java +++ b/src/main/java/com/epam/healenium/service/impl/SettingServiceImpl.java @@ -8,6 +8,7 @@ import lombok.extern.slf4j.Slf4j; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; +import org.springframework.util.StringUtils; import java.util.Arrays; import java.util.HashMap; @@ -27,6 +28,8 @@ public class SettingServiceImpl implements SettingService { private static final String KEY_SELECTOR_URL = "KEY_SELECTOR_URL"; private static final String COLLECT_METRICS = "COLLECT_METRICS"; private static final String FIND_ELEMENTS_AUTO_HEALING = "FIND_ELEMENTS_AUTO_HEALING"; + private static final String SCORE_CAP = "SCORE_CAP"; + private static final String SELECTOR_TYPE = "SELECTOR_TYPE"; private static final String LOG_LEVEL = "LOG_LEVEL"; /** @@ -89,6 +92,38 @@ public boolean setFindElementsAutoHealing(boolean enabled) { return false; } } + + @Override + public boolean setScoreCap(double scoreCap) { + if (scoreCap < 0.0 || scoreCap > 1.0) { + log.error("Invalid score cap: {}. Value must be between 0.0 and 1.0", scoreCap); + return false; + } + + try { + dynamicSettings.setScoreCap(scoreCap); + return true; + } catch (Exception e) { + log.error("Error setting SCORE_CAP", e); + return false; + } + } + + @Override + public boolean setSelectorType(String selectorType) { + if (!isValidSelectorType(selectorType)) { + log.error("Invalid selector type: {}. Value must be 'cssSelector' or 'xpath'", selectorType); + return false; + } + + try { + dynamicSettings.setSelectorType(selectorType); + return true; + } catch (Exception e) { + log.error("Error setting SELECTOR_TYPE", e); + return false; + } + } @Override public Map updateSetting(String key, String value) { @@ -115,6 +150,14 @@ public Map updateSetting(String key, String value) { boolean findElementsAutoHealingSuccess = setFindElementsAutoHealing(Boolean.parseBoolean(value)); result.put("success", findElementsAutoHealingSuccess); break; + case SCORE_CAP: + boolean scoreCapSuccess = setScoreCap(Double.parseDouble(value)); + result.put("success", scoreCapSuccess); + break; + case SELECTOR_TYPE: + boolean selectorTypeSuccess = setSelectorType(value); + result.put("success", selectorTypeSuccess); + break; default: log.warn("Unknown setting key: {}", key); result.put("message", "Unknown setting key"); @@ -138,9 +181,16 @@ public Map getAllSettings() { settings.put(KEY_SELECTOR_URL, dynamicSettings.isKeySelectorUrl()); settings.put(COLLECT_METRICS, dynamicSettings.isCollectMetrics()); settings.put(FIND_ELEMENTS_AUTO_HEALING, dynamicSettings.isFindElementsAutoHealing()); + settings.put(SCORE_CAP, dynamicSettings.getScoreCap()); + settings.put(SELECTOR_TYPE, dynamicSettings.getSelectorType()); return settings; } + private boolean isValidSelectorType(String selectorType) { + return StringUtils.hasText(selectorType) + && ("cssSelector".equals(selectorType) || "xpath".equals(selectorType)); + } + /** * Check if the provided log level is valid * From 6d0b975e7f59f78cce32eddd35e21e31d6e903d7 Mon Sep 17 00:00:00 2001 From: helen_yrofeeva Date: Fri, 10 Jul 2026 17:00:06 +0300 Subject: [PATCH 4/5] [EPMHLM-546] one more Dynamic setting --- .../healenium/config/DynamicSettings.java | 3 +++ .../healenium/service/SettingService.java | 2 ++ .../service/impl/SettingServiceImpl.java | 22 +++++++++++++++++++ 3 files changed, 27 insertions(+) diff --git a/src/main/java/com/epam/healenium/config/DynamicSettings.java b/src/main/java/com/epam/healenium/config/DynamicSettings.java index 82b0ec1..8ff2734 100644 --- a/src/main/java/com/epam/healenium/config/DynamicSettings.java +++ b/src/main/java/com/epam/healenium/config/DynamicSettings.java @@ -28,5 +28,8 @@ public class DynamicSettings { // Selector type must be 'cssSelector' or 'xpath' @Value("${healenium.selector-type:cssSelector}") private String selectorType; + + @Value("${healenium.recovery-tries:1}") + private int recoveryTries; } diff --git a/src/main/java/com/epam/healenium/service/SettingService.java b/src/main/java/com/epam/healenium/service/SettingService.java index 13ffbc2..a465340 100644 --- a/src/main/java/com/epam/healenium/service/SettingService.java +++ b/src/main/java/com/epam/healenium/service/SettingService.java @@ -14,6 +14,8 @@ public interface SettingService { boolean setScoreCap(double scoreCap); boolean setSelectorType(String selectorType); + + boolean setRecoveryTries(int recoveryTries); Map updateSetting(String key, String value); diff --git a/src/main/java/com/epam/healenium/service/impl/SettingServiceImpl.java b/src/main/java/com/epam/healenium/service/impl/SettingServiceImpl.java index c3e81d2..5ce4325 100644 --- a/src/main/java/com/epam/healenium/service/impl/SettingServiceImpl.java +++ b/src/main/java/com/epam/healenium/service/impl/SettingServiceImpl.java @@ -30,6 +30,7 @@ public class SettingServiceImpl implements SettingService { private static final String FIND_ELEMENTS_AUTO_HEALING = "FIND_ELEMENTS_AUTO_HEALING"; private static final String SCORE_CAP = "SCORE_CAP"; private static final String SELECTOR_TYPE = "SELECTOR_TYPE"; + private static final String RECOVERY_TRIES = "RECOVERY_TRIES"; private static final String LOG_LEVEL = "LOG_LEVEL"; /** @@ -124,6 +125,22 @@ public boolean setSelectorType(String selectorType) { return false; } } + + @Override + public boolean setRecoveryTries(int recoveryTries) { + if (recoveryTries < 0) { + log.error("Invalid recovery tries: {}. Value must be a non-negative integer", recoveryTries); + return false; + } + + try { + dynamicSettings.setRecoveryTries(recoveryTries); + return true; + } catch (Exception e) { + log.error("Error setting RECOVERY_TRIES", e); + return false; + } + } @Override public Map updateSetting(String key, String value) { @@ -158,6 +175,10 @@ public Map updateSetting(String key, String value) { boolean selectorTypeSuccess = setSelectorType(value); result.put("success", selectorTypeSuccess); break; + case RECOVERY_TRIES: + boolean recoveryTriesSuccess = setRecoveryTries(Integer.parseInt(value)); + result.put("success", recoveryTriesSuccess); + break; default: log.warn("Unknown setting key: {}", key); result.put("message", "Unknown setting key"); @@ -183,6 +204,7 @@ public Map getAllSettings() { settings.put(FIND_ELEMENTS_AUTO_HEALING, dynamicSettings.isFindElementsAutoHealing()); settings.put(SCORE_CAP, dynamicSettings.getScoreCap()); settings.put(SELECTOR_TYPE, dynamicSettings.getSelectorType()); + settings.put(RECOVERY_TRIES, dynamicSettings.getRecoveryTries()); return settings; } From 4c8384991f184ea1559ae71056365e9986693ce9 Mon Sep 17 00:00:00 2001 From: helen_yrofeeva Date: Fri, 10 Jul 2026 17:01:39 +0300 Subject: [PATCH 5/5] [EPMHLM-546] intermedate state with separate endpoint for rich candidates --- .../config/JacksonConfiguration.java | 7 +- .../controller/HealingController.java | 22 ++- .../converter/ScoredDeserializer.java | 24 +++ .../healenium/converter/ScoredSerializer.java | 22 +++ .../healenium/model/dto/ElementCandidate.java | 18 ++ .../model/dto/HealingCandidateMetaData.java | 21 +++ .../epam/healenium/model/dto/RequestDto.java | 1 + .../model/dto/SelectorCandidate.java | 25 +++ .../service/SelectorCandidateService.java | 15 ++ .../impl/SelectorCandidateServiceImpl.java | 156 ++++++++++++++++++ 10 files changed, 309 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/epam/healenium/converter/ScoredDeserializer.java create mode 100644 src/main/java/com/epam/healenium/converter/ScoredSerializer.java create mode 100644 src/main/java/com/epam/healenium/model/dto/ElementCandidate.java create mode 100644 src/main/java/com/epam/healenium/model/dto/HealingCandidateMetaData.java create mode 100644 src/main/java/com/epam/healenium/model/dto/SelectorCandidate.java create mode 100644 src/main/java/com/epam/healenium/service/SelectorCandidateService.java create mode 100644 src/main/java/com/epam/healenium/service/impl/SelectorCandidateServiceImpl.java diff --git a/src/main/java/com/epam/healenium/config/JacksonConfiguration.java b/src/main/java/com/epam/healenium/config/JacksonConfiguration.java index cde31ea..a10b28c 100644 --- a/src/main/java/com/epam/healenium/config/JacksonConfiguration.java +++ b/src/main/java/com/epam/healenium/config/JacksonConfiguration.java @@ -2,7 +2,10 @@ import com.epam.healenium.converter.NodeDeserializer; import com.epam.healenium.converter.NodeSerializer; +import com.epam.healenium.converter.ScoredDeserializer; +import com.epam.healenium.converter.ScoredSerializer; import com.epam.healenium.treecomparing.Node; +import com.epam.healenium.treecomparing.Scored; import org.springframework.boot.jackson.autoconfigure.JsonMapperBuilderCustomizer; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; @@ -21,7 +24,9 @@ public class JacksonConfiguration implements JsonMapperBuilderCustomizer { public void customize(JsonMapper.Builder builder) { SimpleModule nodeModule = new SimpleModule("node") .addSerializer(Node.class, new NodeSerializer()) - .addDeserializer(Node.class, new NodeDeserializer()); + .addDeserializer(Node.class, new NodeDeserializer()) + .addSerializer(Scored.class, new ScoredSerializer()) + .addDeserializer(Scored.class, new ScoredDeserializer()); builder.addModule(nodeModule) .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) .configure(DateTimeFeature.WRITE_DATES_AS_TIMESTAMPS, false) diff --git a/src/main/java/com/epam/healenium/controller/HealingController.java b/src/main/java/com/epam/healenium/controller/HealingController.java index 0f860dc..1662881 100644 --- a/src/main/java/com/epam/healenium/controller/HealingController.java +++ b/src/main/java/com/epam/healenium/controller/HealingController.java @@ -8,10 +8,12 @@ import com.epam.healenium.model.dto.RecordDto; import com.epam.healenium.model.dto.ReferenceElementsDto; import com.epam.healenium.model.dto.RequestDto; +import com.epam.healenium.model.dto.SelectorCandidate; import com.epam.healenium.model.dto.SelectorDto; import com.epam.healenium.model.dto.SelectorRequestDto; import com.epam.healenium.model.dto.SessionDto; import com.epam.healenium.service.HealingService; +import com.epam.healenium.service.SelectorCandidateService; import com.epam.healenium.service.SelectorService; import com.epam.healenium.util.Utils; import com.epam.healenium.tenant.TenantTxFacade; @@ -45,11 +47,13 @@ public class HealingController { private final TenantTxFacade tenantTx; private final HealingService healingService; private final SelectorService selectorService; + private final SelectorCandidateService selectorCandidateService; - public HealingController(TenantTxFacade tenantTx, HealingService healingService, SelectorService selectorService) { + public HealingController(TenantTxFacade tenantTx, HealingService healingService, SelectorService selectorService, SelectorCandidateService selectorCandidateService) { this.tenantTx = tenantTx; this.healingService = healingService; this.selectorService = selectorService; + this.selectorCandidateService = selectorCandidateService; } /** @@ -221,6 +225,22 @@ public ResponseEntity> getCandidate(@RequestBody RequestDto dto) { }); } + @PostMapping("/selector-candidates") + public ResponseEntity> getElementCandidates(@RequestBody RequestDto dto) { + return tenantTx.required(() -> { + log.debug("[Get Selector Candidates] Request: {})", dto); + + ReferenceElementsDto referenceElements = selectorService.getReferenceElements(dto); + if (selectorCandidateService.validateReference(referenceElements)) { + List candidates = selectorCandidateService.getCandidates(dto, referenceElements); + log.info("[Get Selector Candidates] Candidates: {})", candidates); + return ResponseEntity.ok(candidates); + } else { + return ResponseEntity.notFound().build(); + } + }); + } + @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity handleValidationExceptions(MethodArgumentNotValidException ex) { Map errors = new HashMap<>(); diff --git a/src/main/java/com/epam/healenium/converter/ScoredDeserializer.java b/src/main/java/com/epam/healenium/converter/ScoredDeserializer.java new file mode 100644 index 0000000..98118af --- /dev/null +++ b/src/main/java/com/epam/healenium/converter/ScoredDeserializer.java @@ -0,0 +1,24 @@ +package com.epam.healenium.converter; + +import com.epam.healenium.treecomparing.Node; +import com.epam.healenium.treecomparing.Scored; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.deser.std.StdDeserializer; + +public class ScoredDeserializer extends StdDeserializer { + + public ScoredDeserializer() { + super(Scored.class); + } + + @Override + public Scored deserialize(JsonParser parser, DeserializationContext ctxt) throws JacksonException { + JsonNode tree = ctxt.readTree(parser); + double score = ctxt.readTreeAsValue(tree.path("score"), Double.class); + Node value = ctxt.readTreeAsValue(tree.path("value"), Node.class); + return new Scored<>(score, value); + } +} diff --git a/src/main/java/com/epam/healenium/converter/ScoredSerializer.java b/src/main/java/com/epam/healenium/converter/ScoredSerializer.java new file mode 100644 index 0000000..a0dd304 --- /dev/null +++ b/src/main/java/com/epam/healenium/converter/ScoredSerializer.java @@ -0,0 +1,22 @@ +package com.epam.healenium.converter; + +import com.epam.healenium.treecomparing.Scored; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonGenerator; +import tools.jackson.databind.SerializationContext; +import tools.jackson.databind.ser.std.StdSerializer; + +public class ScoredSerializer extends StdSerializer { + + public ScoredSerializer() { + super(Scored.class); + } + + @Override + public void serialize(Scored value, JsonGenerator gen, SerializationContext serializers) throws JacksonException { + gen.writeStartObject(); + gen.writeNumberProperty("score", value.getScore()); + gen.writePOJOProperty("value", value.getValue()); + gen.writeEndObject(); + } +} diff --git a/src/main/java/com/epam/healenium/model/dto/ElementCandidate.java b/src/main/java/com/epam/healenium/model/dto/ElementCandidate.java new file mode 100644 index 0000000..7a0af02 --- /dev/null +++ b/src/main/java/com/epam/healenium/model/dto/ElementCandidate.java @@ -0,0 +1,18 @@ +package com.epam.healenium.model.dto; + +import com.epam.healenium.model.Locator; +import com.epam.healenium.treecomparing.Node; +import com.epam.healenium.treecomparing.Scored; +import lombok.Data; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +@Data +@Accessors(chain = true) +public class ElementCandidate { + private List locators; + @ToString.Exclude + private Scored nodeScored; +} diff --git a/src/main/java/com/epam/healenium/model/dto/HealingCandidateMetaData.java b/src/main/java/com/epam/healenium/model/dto/HealingCandidateMetaData.java new file mode 100644 index 0000000..a260104 --- /dev/null +++ b/src/main/java/com/epam/healenium/model/dto/HealingCandidateMetaData.java @@ -0,0 +1,21 @@ +package com.epam.healenium.model.dto; + +import com.epam.healenium.treecomparing.Node; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * healenium-web :: HealingCandidateDto + */ +@Data +@Accessors(chain = true) +@AllArgsConstructor +public class HealingCandidateMetaData { + + private Double score; + private Integer LCSDistance; + private Integer curPathHeight; + private Node node; + +} diff --git a/src/main/java/com/epam/healenium/model/dto/RequestDto.java b/src/main/java/com/epam/healenium/model/dto/RequestDto.java index d1febd0..c20edd6 100644 --- a/src/main/java/com/epam/healenium/model/dto/RequestDto.java +++ b/src/main/java/com/epam/healenium/model/dto/RequestDto.java @@ -11,6 +11,7 @@ public class RequestDto { private String locator; + private String locatorType; private String className; private String methodName; private String command; diff --git a/src/main/java/com/epam/healenium/model/dto/SelectorCandidate.java b/src/main/java/com/epam/healenium/model/dto/SelectorCandidate.java new file mode 100644 index 0000000..bf83f05 --- /dev/null +++ b/src/main/java/com/epam/healenium/model/dto/SelectorCandidate.java @@ -0,0 +1,25 @@ +package com.epam.healenium.model.dto; + +import com.epam.healenium.treecomparing.Node; +import com.epam.healenium.treecomparing.Scored; +import lombok.Data; +import lombok.ToString; +import lombok.experimental.Accessors; + +import java.util.List; + +@Data +@Accessors(chain = true) +public class SelectorCandidate { + List elementCandidates; + + // -- to save healing result -- + // one List from ReferenceElementsDto::List> paths + // in playwright usually there is only one List + @ToString.Exclude + List path; + @ToString.Exclude + private List> scores; + @ToString.Exclude + List candidateMetaData; +} diff --git a/src/main/java/com/epam/healenium/service/SelectorCandidateService.java b/src/main/java/com/epam/healenium/service/SelectorCandidateService.java new file mode 100644 index 0000000..4354c1a --- /dev/null +++ b/src/main/java/com/epam/healenium/service/SelectorCandidateService.java @@ -0,0 +1,15 @@ +package com.epam.healenium.service; + +import com.epam.healenium.model.dto.ReferenceElementsDto; +import com.epam.healenium.model.dto.RequestDto; +import com.epam.healenium.model.dto.SelectorCandidate; + +import java.util.List; + +public interface SelectorCandidateService { + + boolean validateReference(ReferenceElementsDto referenceElements); + + List getCandidates(RequestDto dto, ReferenceElementsDto referenceElements); + +} diff --git a/src/main/java/com/epam/healenium/service/impl/SelectorCandidateServiceImpl.java b/src/main/java/com/epam/healenium/service/impl/SelectorCandidateServiceImpl.java new file mode 100644 index 0000000..d4aed03 --- /dev/null +++ b/src/main/java/com/epam/healenium/service/impl/SelectorCandidateServiceImpl.java @@ -0,0 +1,156 @@ +package com.epam.healenium.service.impl; + +import com.epam.healenium.config.DynamicSettings; +import com.epam.healenium.elementcreators.SelectorComponent; +import com.epam.healenium.model.dto.ElementCandidate; +import com.epam.healenium.model.dto.HealingCandidateMetaData; +import com.epam.healenium.model.dto.ReferenceElementsDto; +import com.epam.healenium.model.dto.RequestDto; +import com.epam.healenium.model.dto.SelectorCandidate; +import com.epam.healenium.service.SelectorCandidateService; +import com.epam.healenium.tenant.TenantTransactional; +import com.epam.healenium.treecomparing.HeuristicNodeDistance; +import com.epam.healenium.treecomparing.JsoupHTMLParser; +import com.epam.healenium.treecomparing.LCSPathDistance; +import com.epam.healenium.treecomparing.Node; +import com.epam.healenium.treecomparing.Path; +import com.epam.healenium.treecomparing.PathFinder; +import com.epam.healenium.treecomparing.Scored; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.openqa.selenium.By; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.ByteArrayInputStream; +import java.nio.charset.StandardCharsets; +import java.util.AbstractMap; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +@TenantTransactional +public class SelectorCandidateServiceImpl implements SelectorCandidateService { + private final DynamicSettings dynamicSettings; + + public static final String PLAYWRIGHT_LOCATOR_TYPE = "locator"; + + private final List> selectorDetailLevels = Collections.unmodifiableList(TEMP); + + private final static List> TEMP = new ArrayList>() {{ + add(EnumSet.of(SelectorComponent.TAG, SelectorComponent.ID)); + add(EnumSet.of(SelectorComponent.TAG, SelectorComponent.CLASS)); + add(EnumSet.of(SelectorComponent.PARENT, SelectorComponent.TAG, SelectorComponent.ID, SelectorComponent.CLASS)); + add(EnumSet.of(SelectorComponent.PARENT, SelectorComponent.TAG, SelectorComponent.CLASS, SelectorComponent.POSITION)); + add(EnumSet.of(SelectorComponent.PARENT, SelectorComponent.TAG, SelectorComponent.ID, SelectorComponent.CLASS, SelectorComponent.ATTRIBUTES)); + add(EnumSet.of(SelectorComponent.PATH)); + }}; + + @Override + public boolean validateReference(ReferenceElementsDto referenceElements) { + log.debug("[Get Reference] Response: {})", referenceElements); + if (referenceElements.getPaths().isEmpty()) { + log.warn("New element locator have not been found. There is no reference data to selector in the database." + + "\nMake sure that: " + + "\n- There is selector on the page /selectors/ and type: single, if not then you have to run successful tests." + + "\n- Your locator was changed on the page and not in code."); + return false; + } + return true; + } + + @Override + public List getCandidates(RequestDto dto, ReferenceElementsDto ref) { + Node destination = parseTree(dto.getPageContent()); + + return ref.getPaths().stream() + .map(path -> findSelectorCandidate(path, destination, ref, dto.getLocatorType())) + .collect(Collectors.toCollection(ArrayList::new)); + } + + private static Node parseTree(String tree) { + return new JsoupHTMLParser().parse(new ByteArrayInputStream(tree.getBytes(StandardCharsets.UTF_8))); + } + + private SelectorCandidate findSelectorCandidate(List path, Node destination, ReferenceElementsDto ref, + String locatorType) { + PathFinder pathFinder = new PathFinder(new LCSPathDistance(), new HeuristicNodeDistance()); + AbstractMap.SimpleImmutableEntry>>> scoresToNodes = + pathFinder.findScoresToNodes(new Path(path.toArray(new Node[0])), destination); + List> scores = pathFinder.getSortedNodes(scoresToNodes.getValue(), 1000, dynamicSettings.getScoreCap()); + log.debug("List> scores size: {}", scores.size()); + + List candidateMetaData = getAllHealingCandidateMetaData(scoresToNodes); + + List candidates = scores.stream() + .map(scored -> toElementCandidate(scored, ref, locatorType)) + .filter(Objects::nonNull) + .limit(dynamicSettings.getRecoveryTries()) + .collect(Collectors.toCollection(ArrayList::new)); + return new SelectorCandidate() + .setElementCandidates(candidates) + .setPath(path) + .setScores(scores) + .setCandidateMetaData(candidateMetaData); + } + + /** + * @param curPathHeightToScores - all PathToNode candidate collection + * @return list healingCandidateDto for metrics + */ + private List getAllHealingCandidateMetaData(AbstractMap.SimpleImmutableEntry>>> curPathHeightToScores) { + Integer curPathHeight = curPathHeightToScores.getKey(); + Map>> scoresToNodes = curPathHeightToScores.getValue(); + return scoresToNodes.keySet().stream() + .sorted(Comparator.reverseOrder()) + .flatMap(score -> scoresToNodes.get(score).stream() + .map(it -> new HealingCandidateMetaData(score, it.getValue(), curPathHeight, it.getKey()))) + .limit(10) + .toList(); + } + + protected ElementCandidate toElementCandidate(Scored node, ReferenceElementsDto ref, String locatorType) { + List locators = new ArrayList<>(); + if (dynamicSettings.getSelectorType().equals("xpath")) { + // todo get candidate by xpath from AI service + } + for (Set detailLevel : selectorDetailLevels) { + locators.add(construct(node.getValue(), detailLevel, locatorType)); + } + log.debug("[Get Selector Candidates] raw candidates before filter: {}", locators); + @SuppressWarnings("java:S6204") + List filtered = locators.stream() + .filter(locator -> !ref.getUnsuccessfulLocators().contains(locator)) + .collect(Collectors.toList()); + + log.debug("[Get Selector Candidates] raw candidates after filter: {}", filtered); + return new ElementCandidate().setNodeScored(node).setLocators(filtered); + } + + protected com.epam.healenium.model.Locator construct(Node node, Set detailLevel, String locatorType) { + StringBuilder selectorBuilder = new StringBuilder(); + + for (SelectorComponent component : detailLevel) { + selectorBuilder.append(component.createComponent(node)); + } + if (PLAYWRIGHT_LOCATOR_TYPE.equals(locatorType)) { + return new com.epam.healenium.model.Locator(selectorBuilder.toString(), PLAYWRIGHT_LOCATOR_TYPE); + } else { + return new com.epam.healenium.model.Locator(selectorBuilder.toString(), "By.cssSelector"); + } + + } + +}