diff --git a/engine/src/main/java/com/knubisoft/testlum/testing/framework/interpreter/lib/ui/executor/DropDownExecutor.java b/engine/src/main/java/com/knubisoft/testlum/testing/framework/interpreter/lib/ui/executor/DropDownExecutor.java index 79b7f49e5..8642a2ba5 100644 --- a/engine/src/main/java/com/knubisoft/testlum/testing/framework/interpreter/lib/ui/executor/DropDownExecutor.java +++ b/engine/src/main/java/com/knubisoft/testlum/testing/framework/interpreter/lib/ui/executor/DropDownExecutor.java @@ -94,12 +94,14 @@ private void logOneValueDropDownData(final TypeForOneValue type, final OneValue private void selectSearchableOptionForCustomDropDown(final List dropDownParentElements, final String value) { + By searchableOptionLocator = By.xpath(format(CONTAINS_TEXT_PATTERN, value)); + uiUtil.waitForElementPresence(dependencies, searchableOptionLocator); Collections.reverse(dropDownParentElements); for (int i = 0; i < dropDownParentElements.size(); i++) { WebElement element = dropDownParentElements.get(i); try { - WebElement searchableOption = element.findElement(By.xpath(format(CONTAINS_TEXT_PATTERN, value))); + WebElement searchableOption = element.findElement(searchableOptionLocator); searchableOption.click(); break; } catch (NoSuchElementException e) { diff --git a/engine/src/main/java/com/knubisoft/testlum/testing/framework/scenario/ScenarioRunner.java b/engine/src/main/java/com/knubisoft/testlum/testing/framework/scenario/ScenarioRunner.java index bf42fb530..9e8c52c12 100644 --- a/engine/src/main/java/com/knubisoft/testlum/testing/framework/scenario/ScenarioRunner.java +++ b/engine/src/main/java/com/knubisoft/testlum/testing/framework/scenario/ScenarioRunner.java @@ -82,7 +82,7 @@ public ScenarioRunner(final ScenarioArguments scenarioArguments, this.interpreterScanner = ctx.getBean(InterpreterScanner.class); this.webDownloadUtil = ctx.getBean(WebDownloadUtil.class); this.stopScenarioOnFailure = ctx.getBean(GlobalTestConfiguration.class).isStopScenarioOnFailure(); - + this.scenarioDir = webDownloadUtil.resolveScenarioDir(scenarioArguments.getFile()); this.dependencies = createDependencies(); this.cmdToInterpreterMap = createClassToInterpreterMap(dependencies); } @@ -97,7 +97,6 @@ public ScenarioResult run() { private void takeFileNamesSnapshot() { this.executionStartTime = System.currentTimeMillis(); - this.scenarioDir = webDownloadUtil.resolveScenarioDir(scenarioArguments.getFile()); if (nonNull(scenarioDir)) { try { java.nio.file.Files.createDirectories(scenarioDir); diff --git a/engine/src/main/java/com/knubisoft/testlum/testing/framework/util/UiUtil.java b/engine/src/main/java/com/knubisoft/testlum/testing/framework/util/UiUtil.java index 3f9c24392..2527af741 100644 --- a/engine/src/main/java/com/knubisoft/testlum/testing/framework/util/UiUtil.java +++ b/engine/src/main/java/com/knubisoft/testlum/testing/framework/util/UiUtil.java @@ -17,6 +17,7 @@ import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.io.FileUtils; +import org.openqa.selenium.By; import org.openqa.selenium.Dimension; import org.openqa.selenium.OutputType; import org.openqa.selenium.Point; @@ -276,6 +277,12 @@ public void waitForMatSelectToOpen(final ExecutorDependencies dependencies, fina getWebDriverWait(dependencies).until(d -> "true".equalsIgnoreCase(matSelect.getAttribute("aria-expanded"))); } + public void waitForElementPresence(final ExecutorDependencies dependencies, final By locator) { + WebDriverWait wait = getWebDriverWait(dependencies); + wait.until(ExpectedConditions.presenceOfElementLocated(locator)); + } + + public String getBasePageURL(final String currentPageURL) { try { URL url = new URL(currentPageURL); diff --git a/engine/src/main/java/com/knubisoft/testlum/testing/framework/util/VariableHelperImpl.java b/engine/src/main/java/com/knubisoft/testlum/testing/framework/util/VariableHelperImpl.java index bd0b716d9..b7323fb07 100644 --- a/engine/src/main/java/com/knubisoft/testlum/testing/framework/util/VariableHelperImpl.java +++ b/engine/src/main/java/com/knubisoft/testlum/testing/framework/util/VariableHelperImpl.java @@ -51,6 +51,7 @@ import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; +import java.time.format.ResolverStyle; import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; import java.time.temporal.TemporalQueries; @@ -74,6 +75,7 @@ public class VariableHelperImpl implements VariableHelper { private static final String VAR_CONTEXT_LOG = LogFormat.table("Created from"); private static final String DEFAULT_ALIAS_VALUE = "DEFAULT"; private static final String JAVA_COMPATIBLE_FORMAT = "yyyy-MM-dd HH:mm:ss"; + private static final String ALLOWED_DATE_LETTERS = "yMdHmsSnuEaZzXx"; private final Map randomGenerateMethodMap; @@ -291,6 +293,7 @@ private String formatAndRegisterResult(final ZonedDateTime dateTime, final DateT } private DateTimeFormatter createDateTimeFormatter(final String dateFormatPattern) { + validateDateFormatPattern(dateFormatPattern); try { return new DateTimeFormatterBuilder().appendPattern(dateFormatPattern) .parseDefaulting(ChronoField.YEAR_OF_ERA, ZonedDateTime.now().getYear()) @@ -341,6 +344,39 @@ private TemporalAccessor parseToTemporalAccessor(final String valueToParse, fina } } + private void validateDateFormatPattern(final String dateFormatPattern) { + if (hasUnquotedInvalidChars(dateFormatPattern)) { + throw new DefaultFrameworkException(ExceptionMessage.INVALID_DATE_FORMAT_PATTERN, + dateFormatPattern, "Pattern contains unsupported letters or unquoted digits"); + } + verifyPatternFunctionality(dateFormatPattern); + } + + private void verifyPatternFunctionality(final String dateFormatPattern) { + try { + DateTimeFormatter formatter = DateTimeFormatter.ofPattern(dateFormatPattern) + .withResolverStyle(ResolverStyle.STRICT); + ZonedDateTime now = ZonedDateTime.now(); + String formatted = formatter.format(now); + formatter.parse(formatted); + } catch (RuntimeException e) { + throw new DefaultFrameworkException(ExceptionMessage.INVALID_DATE_FORMAT_PATTERN, + dateFormatPattern, "Invalid pattern syntax: " + e.getMessage()); + } + } + + private boolean hasUnquotedInvalidChars(String pattern) { + String cleaned = pattern.replaceAll("'[^']*'", ""); + return cleaned.chars().anyMatch(this::isInvalidChar); + } + + private boolean isInvalidChar(int c) { + if (Character.isLetter(c)) { + return ALLOWED_DATE_LETTERS.indexOf(c) == -1; + } + return Character.isDigit(c); + } + private ZonedDateTime convertToZonedDateTime(final TemporalAccessor temporalAccessor, final ZoneId zoneId, final String originalValue, final String pattern) { LocalDate localDate = temporalAccessor.query(TemporalQueries.localDate());