diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..6b472b8 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,16 @@ +version: 2 +updates: +- package-ecosystem: "maven" + directory: "/" + schedule: + interval: "weekly" + time: "09:00" + open-pull-requests-limit: 10 + +- package-ecosystem: "github-actions" + # Workflow files stored in the + # default location of `.github/workflows` + directory: "/" + schedule: + interval: "weekly" + time: "09:00" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0daa3d2 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,26 @@ +name: CI + +on: [push, pull_request] + +jobs: + build: + name: Build on Java ${{ matrix.java }} + runs-on: ubuntu-latest + strategy: + matrix: + java: [ 8, 17 ] + steps: + - uses: actions/checkout@v2.3.4 + - uses: actions/cache@v2.1.6 + with: + path: ~/.m2/repository + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- + - name: Set up Java ${{ matrix.java }} + uses: actions/setup-java@v2 + with: + java-version: ${{ matrix.java }} + distribution: 'zulu' + - name: Build with Java ${{ matrix.java }} + run: mvn clean verify diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..ea34d20 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,20 @@ +name: Publish package to GitHub Packages + +on: [workflow_dispatch] + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v2.3.4 + - uses: actions/setup-java@v2 + with: + java-version: '8' + distribution: 'zulu' + - name: Publish package + run: mvn deploy + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2c36dea --- /dev/null +++ b/.gitignore @@ -0,0 +1,27 @@ +# ignore all idea files +.idea/* +!.idea/codeStyleSettings.xml + + +### Intellij Patch ### +# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 + +*.iml +modules.xml +.idea/misc.xml +*.ipr + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Exclude maven wrapper +!/.mvn/wrapper/maven-wrapper.jar + diff --git a/README.md b/README.md index 3f15ed6..cfb6c40 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,8 @@ # jbehave-support-web-tables Web table steps for jbehave-support-core - legacy unsupported version + +Provided only for potential backwards compatibility workarounds for legacy applications that used them in jbehave-support-core 1.x.x. +Not intended for any use in new projects. + +**WARNING**: Not supported, please **use at your own risk**. If you want/plan to use this please **contact us** and let us know. +(so that we might potentially continue development if there is enough interest) diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..14b5e15 --- /dev/null +++ b/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + org.jbehavesupport + jbehave-support-web-tables + 1.0-SNAPSHOT + + + 8 + 8 + UTF-8 + + 1.18.22 + 1.3.2 + + + + + github + GitHub Packages + https://maven.pkg.github.com/EmbedITCZ/jbehave-support-web-tables + + + + + + + src/test/java + + **/*.story + + + + + + + + org.jbehavesupport + jbehave-support-core + ${jbehave-support.version} + + + org.projectlombok + lombok + ${lombok.version} + + + + \ No newline at end of file diff --git a/src/main/java/org/jbehavesupport/web/tables/LegacyWebTableSteps.java b/src/main/java/org/jbehavesupport/web/tables/LegacyWebTableSteps.java new file mode 100644 index 0000000..622c7b8 --- /dev/null +++ b/src/main/java/org/jbehavesupport/web/tables/LegacyWebTableSteps.java @@ -0,0 +1,376 @@ +package org.jbehavesupport.web.tables; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import junit.framework.AssertionFailedError; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.jbehave.core.annotations.Given; +import org.jbehave.core.annotations.Then; +import org.jbehave.core.model.ExamplesTable; +import org.jbehave.core.steps.Row; +import org.jbehavesupport.core.web.WebElementRegistry; +import org.jbehavesupport.core.web.WebSetting; +import org.jbehavesupport.core.web.WebSteps; +import org.openqa.selenium.By; +import org.openqa.selenium.JavascriptExecutor; +import org.openqa.selenium.NoSuchElementException; +import org.openqa.selenium.WebDriver; +import org.openqa.selenium.WebElement; +import org.openqa.selenium.support.ui.ExpectedCondition; +import org.openqa.selenium.support.ui.WebDriverWait; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class LegacyWebTableSteps { + + private static final String TEXT_CONTENT = "textContent"; + private static final int MAXIMUM_LOCATE_ATTEMPTS = 2; + + public enum HtmlRenderer { + SIMPLE, + WICKET + } + + private final WebDriver driver; + private final WebElementRegistry elementRegistry; + + @Autowired(required = false) + private List webSettings; + + @Given("on [$page] page in table [$table] row $rowNumber, column $columnNumber is clicked") + public void clickCell(String page, String tableName, int rowNumber, int columnNumber) { + WebElement table = findElementByPageAndName(page, tableName); + List tableRows = + table.findElement(By.className("imxt-body")).findElements(By.className("imxt-grid-row")); + List row = tableRows.get(rowNumber - 1).findElements(By.tagName("td")); + WebElement td = row.get(columnNumber - 1); + td.findElement(By.tagName("a")).click(); + } + + @Then("on [$page] page the table [$tableName] contains exactly the following data:$expectedData") + public void tableContainsExactlyFollowingData(String page, String tableName, ExamplesTable data) { + List> expectedTableData = data.getRowsAsParameters().stream() + .map(Row::values) + .collect(Collectors.toList()); + verifyTableContainsExactlyFollowingData( + convertHtmlRenderer(WebSteps.getCurrentSetting().getHtmlRenderer()), + page, + tableName, + expectedTableData, + true); + } + + @Then("on [$page] page the table [$tableName] contains in row $rowNumber the following data:$expectedData") + public void tableContainsFollowingDataInRow(String page, String tableName, int rowNumber, ExamplesTable expectedData) { + List> expectedTableData = expectedData.getRowsAsParameters().stream() + .map(Row::values) + .collect(Collectors.toList()); + verifyTableContainsFollowingDataInSpecifiedRow( + convertHtmlRenderer(WebSteps.getCurrentSetting().getHtmlRenderer()), + page, + tableName, + expectedTableData.get(0), + rowNumber - 1); + } + + @Then("on [$page] page the table [$tableName] contains in rows $startRow to $endRow the following data:$expectedData") + public void tableContainsFollowingDataInRow(String page, String tableName, int startRow, int endRow, ExamplesTable expectedData) { + List> expectedTableData = expectedData.getRowsAsParameters().stream() + .map(Row::values) + .collect(Collectors.toList()); + verifyTableContainsFollowingDataInSpecifiedRows( + convertHtmlRenderer(WebSteps.getCurrentSetting().getHtmlRenderer()), + page, + tableName, + expectedTableData, + startRow - 1, + endRow - 1); + } + + @Then("on [$page] page the table [$tableName] contains the following data:$expectedData") + public void tableContainsFollowingData(String page, String tableName, ExamplesTable expectedData) { + List> expectedTableData = expectedData.getRowsAsParameters().stream() + .map(Row::values) + .collect(Collectors.toList()); + verifyTableContainsAtLeastFollowingData( + convertHtmlRenderer(WebSteps.getCurrentSetting().getHtmlRenderer()), + page, + tableName, + expectedTableData); + } + + @Then("on [$page] page the table [$tableName] contains all of the following data regardless of order:$expectedData") + public void tableContainsFollowingDataWithNoOrder(String page, String tableName, ExamplesTable expectedData) { + List> expectedTableData = expectedData.getRowsAsParameters().stream() + .map(Row::values) + .collect(Collectors.toList()); + verifyTableContainsExactlyFollowingData( + convertHtmlRenderer(WebSteps.getCurrentSetting().getHtmlRenderer()), + page, + tableName, + expectedTableData, + false); + } + + @SuppressWarnings("squid:S1166") + private WebElement findElementByPageAndName(String page, String elementName, int maxLocateAttempts) { + int currentAttempt = 1; + do { + waitForLoad(); + try { + return driver.findElement(elementRegistry.getLocator(page, elementName)); + } catch (NoSuchElementException e) { + log.error("Missing reference for element: {}, attempt: {} of {}", elementName, currentAttempt++, maxLocateAttempts); + } + } while (currentAttempt <= maxLocateAttempts); + throw new NoSuchElementException(page + ": " + elementName); + } + + private WebElement findElementByPageAndName(String page, String elementName) { + return findElementByPageAndName(page, elementName, MAXIMUM_LOCATE_ATTEMPTS); + } + + private List> convertSimpleTableToListOfMaps(WebElement table, Collection relevantHeaders, int startRow, int endRow) { + HashMap headers = new HashMap<>(); + List tableHeaders = table.findElement(By.tagName("thead")).findElement(By.tagName("tr")).findElements(By.tagName("th")); + int index = 0; + for (WebElement th : tableHeaders) { + headers.put(index++, th.getAttribute(TEXT_CONTENT).trim()); + } + + for (String relevantHeader : relevantHeaders) { + assertThat(headers.containsValue(relevantHeader)).as("Column '%s' was not found in table.", relevantHeader).isTrue(); + } + + List> tableData = new ArrayList<>(); + List tableRows = table.findElements(By.tagName("tbody")).stream() + .flatMap(e -> e.findElements(By.tagName("tr")).stream()) + .collect(Collectors.toList()); + + startRow = (startRow == -1) ? 0 : startRow; + endRow = (endRow == -1) ? tableRows.size() - 1 : endRow; + + for (int rowNumber = startRow; rowNumber <= endRow - startRow; rowNumber++) { + WebElement tr = tableRows.get(rowNumber); + List row = tr.findElements(By.tagName("td")); + HashMap rowData = new HashMap<>(); + for (Map.Entry entry : headers.entrySet()) { + if (relevantHeaders == null || relevantHeaders.contains(entry.getValue())) { + WebElement td = row.get(entry.getKey()); + rowData.put(entry.getValue(), td.getText()); + } + } + + tableData.add(rowData); + } + + return tableData; + } + + private List> convertWicketTableToListOfMaps(WebElement table, Collection relevantHeaders, int startRow, int endRow) { + ArrayList headers = new ArrayList<>(); + List tableHeaders = table.findElement(By.className("imxt-head")).findElement(By.tagName("tr")).findElements(By.tagName("th")); + for (WebElement th : tableHeaders) { + List headingElement = th.findElements(By.xpath("div/div/a/div/div")); + if (headingElement.size() == 1) { + String headerText = headingElement.get(0).getAttribute(TEXT_CONTENT); + headers.add(headerText); + } else { + break; + } + } + + List> tableData = new ArrayList<>(); + List tableRows = table.findElement(By.className("imxt-body")).findElements(By.className("imxt-grid-row")); + startRow = (startRow == -1) ? 0 : startRow; + if (endRow == -1) { + endRow = tableRows.size() - 1; + } + + for (int rowNumber = startRow; rowNumber <= endRow - startRow; rowNumber++) { + WebElement tr = tableRows.get(rowNumber); + List row = tr.findElements(By.tagName("td")); + HashMap rowData = new HashMap<>(); + for (int i = 0; i < headers.size(); i++) { + if (relevantHeaders == null || relevantHeaders.contains(headers.get(i))) { + WebElement td = row.get(i); + rowData.put(headers.get(i), td.findElement(By.tagName("div")).getAttribute(TEXT_CONTENT)); + } + } + tableData.add(rowData); + } + + return tableData; + } + + private void verifyTableContainsFollowingDataInSpecifiedRow( + HtmlRenderer htmlRenderer, + String page, + String tableName, + Map expectedTableData, + int rowNumber) { + verifyTableContainsFollowingDataInSpecifiedRows( + htmlRenderer, + page, + tableName, + Collections.singletonList(expectedTableData), + rowNumber, + rowNumber); + } + + private void verifyTableContainsFollowingDataInSpecifiedRows( + HtmlRenderer htmlRenderer, + String page, + String tableName, + List> expectedTableData, + int startRow, + int endRow) { + Collection relevantHeaders = expectedTableData.get(0).keySet(); + WebElement table = findElementByPageAndName(page, tableName); + List> htmlTableData = convertHtmlTableToListOfMaps(htmlRenderer, table, relevantHeaders, startRow, endRow); + for (int rowNumber = startRow; rowNumber <= endRow - startRow; rowNumber++) { + Map expectedRow = expectedTableData.get(rowNumber); + for (String header : expectedRow.keySet()) { + assertThat(htmlTableData.get(rowNumber).get(header)) + .as("table cell on row '" + (rowNumber + 1) + "' and column '" + header + "' should contain '" + expectedRow.get(header) + + "'. It contains '" + htmlTableData.get(rowNumber).get(header) + "'.") + .isEqualTo(expectedRow.get(header)); + } + } + } + + /** + * returns the row which matches the item in the list + */ + private int listContainsItem(final List> list, Map item, Set skipRows) { + boolean verificationResult = false; + for (int i = 0; i < list.size(); i++) { + if (!skipRows.contains(i)) { + Map actualRow = list.get(i); + for (String header : actualRow.keySet()) { + verificationResult = item.get(header).equals(actualRow.get(header)); + if (!verificationResult) { + break; + } + } + if (verificationResult) { + return i; + } + } + } + return -1; + } + + private void compareListOfMaps(final List> expectedTableData, final List> htmlTableData, boolean strictOrder) { + if (strictOrder) { + for (int i = 0; i < expectedTableData.size(); i++) { + Map row = expectedTableData.get(i); + for (String header : row.keySet()) { + boolean verificationResult = expectedTableData.get(i).get(header).equals(htmlTableData.get(i).get(header)); + assertThat(verificationResult). + as("table cell on row '" + (i + 1) + "' and column '" + header + "' should contain '" + expectedTableData.get(i).get(header) + + "'. It contains '" + htmlTableData.get(i).get(header) + "'.") + .isTrue(); + } + } + } else { + HashSet foundRows = new HashSet<>(); + for (int i = 0; i < expectedTableData.size(); i++) { + Map row = expectedTableData.get(i); + int foundIndex = listContainsItem(htmlTableData, row, foundRows); + if (foundIndex != -1) { + foundRows.add(foundIndex); + } else { + throw new AssertionFailedError("Row " + (i + 1) + " not found"); + } + } + } + } + + private void verifyTableContainsExactlyFollowingData( + HtmlRenderer htmlRenderer, + String page, + String tableName, + List> expectedTableData, + boolean strictOrder) { + Collection relevantHeaders = expectedTableData.get(0).keySet(); + WebElement table = findElementByPageAndName(page, tableName); + List> htmlTableData = convertHtmlTableToListOfMaps(htmlRenderer, table, relevantHeaders, -1, -1); + + assertThat(htmlTableData.size()) + .as("expected number of rows doesn't match") + .isEqualTo(expectedTableData.size()); + + compareListOfMaps(expectedTableData, htmlTableData, strictOrder); + } + + private void verifyTableContainsAtLeastFollowingData( + HtmlRenderer htmlRenderer, + String page, + String tableName, + List> expectedTableData) { + Collection relevantHeaders = expectedTableData.get(0).keySet(); + WebElement table = findElementByPageAndName(page, tableName); + List> foundTableData = convertHtmlTableToListOfMaps(htmlRenderer, table, relevantHeaders, -1, -1); + verifyFoundDataContainExpectedData(expectedTableData, foundTableData); + } + + private void verifyFoundDataContainExpectedData(final List> expectedData, final List> foundData) { + HashSet foundRows = new HashSet<>(); + for (int i = 0; i < expectedData.size(); i++) { + Map row = expectedData.get(i); + int foundIndex = listContainsItem(foundData, row, foundRows); + if (foundIndex != -1) { + foundRows.add(foundIndex); + } else { + throw new AssertionFailedError("row " + (i + 1) + " not found"); + } + } + } + + private List> convertHtmlTableToListOfMaps( + final HtmlRenderer htmlRenderer, + final WebElement table, + final Collection relevantHeaders, + int startRow, + int endRow) { + List> htmlTableData; + switch (htmlRenderer) { + case SIMPLE: + htmlTableData = convertSimpleTableToListOfMaps(table, relevantHeaders, startRow, endRow); + break; + case WICKET: + htmlTableData = convertWicketTableToListOfMaps(table, relevantHeaders, startRow, endRow); + break; + default: + throw new IllegalArgumentException("unknown renderer"); + } + return htmlTableData; + } + + private void waitForLoad() { + new WebDriverWait(driver, 10).until((ExpectedCondition) wd -> + ((JavascriptExecutor) wd).executeScript("return document.readyState").equals("complete")); + webSettings.stream() + .forEach(s -> s.getWaitForLoad().accept(driver)); + } + + private HtmlRenderer convertHtmlRenderer(org.jbehavesupport.core.web.WebTableSteps.HtmlRenderer htmlRenderer) { + return HtmlRenderer.valueOf(htmlRenderer.name()); + } + +}