- ${b.fullDisplayName}
+ ${b.fullDisplayName}
diff --git a/plugin/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly b/plugin/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly
index c99d2774d..5e74fc38a 100644
--- a/plugin/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly
+++ b/plugin/src/main/resources/hudson/tasks/junit/ClassResult/body.jelly
@@ -23,20 +23,22 @@ THE SOFTWARE.
-->
-
+
${%All Tests}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
diff --git a/plugin/src/main/resources/hudson/tasks/junit/ClassResult/list.jelly b/plugin/src/main/resources/hudson/tasks/junit/ClassResult/list.jelly
index 661524be6..5f0a56d21 100644
--- a/plugin/src/main/resources/hudson/tasks/junit/ClassResult/list.jelly
+++ b/plugin/src/main/resources/hudson/tasks/junit/ClassResult/list.jelly
@@ -23,24 +23,25 @@ THE SOFTWARE.
-->
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
- ${b.fullDisplayName}
+ ${b.fullDisplayName}
diff --git a/plugin/src/main/resources/hudson/tasks/junit/History/index.jelly b/plugin/src/main/resources/hudson/tasks/junit/History/index.jelly
index 1728246c7..b46dcce49 100644
--- a/plugin/src/main/resources/hudson/tasks/junit/History/index.jelly
+++ b/plugin/src/main/resources/hudson/tasks/junit/History/index.jelly
@@ -70,23 +70,26 @@ THE SOFTWARE.
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
|
- ${item.fullDisplayName}
+ ${item.fullDisplayName}
|
|
diff --git a/plugin/src/main/resources/hudson/tasks/test/AggregatedTestResultPublisher/TestResultAction/index.jelly b/plugin/src/main/resources/hudson/tasks/test/AggregatedTestResultPublisher/TestResultAction/index.jelly
index db10c49c2..7e1faad7c 100644
--- a/plugin/src/main/resources/hudson/tasks/test/AggregatedTestResultPublisher/TestResultAction/index.jelly
+++ b/plugin/src/main/resources/hudson/tasks/test/AggregatedTestResultPublisher/TestResultAction/index.jelly
@@ -35,12 +35,14 @@ THE SOFTWARE.
${%Drill Down}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
@@ -61,7 +63,7 @@ THE SOFTWARE.
|
- ${i.fullDisplayName}
+ ${i.fullDisplayName}
(${%test result not available})
|
@@ -75,7 +77,7 @@ THE SOFTWARE.
|
- ${i.fullDisplayName}
+ ${i.fullDisplayName}
(${%last successful job is not fingerprinted})
|
@@ -91,4 +93,4 @@ THE SOFTWARE.
-
\ No newline at end of file
+
diff --git a/plugin/src/main/resources/hudson/tasks/test/MetaTabulatedResult/body.jelly b/plugin/src/main/resources/hudson/tasks/test/MetaTabulatedResult/body.jelly
index b99f42b29..91dfd81cd 100644
--- a/plugin/src/main/resources/hudson/tasks/test/MetaTabulatedResult/body.jelly
+++ b/plugin/src/main/resources/hudson/tasks/test/MetaTabulatedResult/body.jelly
@@ -24,15 +24,17 @@ THE SOFTWARE.
-->
-
+
${%All Failed Tests}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
|
@@ -40,7 +42,7 @@ THE SOFTWARE.
${f.durationString}
- ${f.age}
+ ${f.age}
|
@@ -49,26 +51,28 @@ THE SOFTWARE.
${%All Tests}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
diff --git a/plugin/src/main/resources/hudson/tasks/test/MetaTabulatedResult/list.jelly b/plugin/src/main/resources/hudson/tasks/test/MetaTabulatedResult/list.jelly
index 76f58239c..848b584ce 100644
--- a/plugin/src/main/resources/hudson/tasks/test/MetaTabulatedResult/list.jelly
+++ b/plugin/src/main/resources/hudson/tasks/test/MetaTabulatedResult/list.jelly
@@ -23,24 +23,25 @@ THE SOFTWARE.
-->
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
- ${b.fullDisplayName}
+ ${b.fullDisplayName}
diff --git a/plugin/src/main/resources/lib/hudson/test/aggregated-failed-tests.jelly b/plugin/src/main/resources/lib/hudson/test/aggregated-failed-tests.jelly
index c1e1d23ee..4e73ae6ff 100644
--- a/plugin/src/main/resources/lib/hudson/test/aggregated-failed-tests.jelly
+++ b/plugin/src/main/resources/lib/hudson/test/aggregated-failed-tests.jelly
@@ -23,7 +23,7 @@ THE SOFTWARE.
-->
-
+
Display links to failed test from all child reports.
@since 1.538
@@ -41,15 +41,17 @@ THE SOFTWARE.
-
-
-
-
-
-
+
+
+
+
+
+
+
+
|
diff --git a/plugin/src/main/resources/lib/hudson/test/failed-test.jelly b/plugin/src/main/resources/lib/hudson/test/failed-test.jelly
index 78e95215e..862866e30 100644
--- a/plugin/src/main/resources/lib/hudson/test/failed-test.jelly
+++ b/plugin/src/main/resources/lib/hudson/test/failed-test.jelly
@@ -91,7 +91,7 @@ THE SOFTWARE.
-
+
diff --git a/plugin/src/test/java/hudson/tasks/junit/pipeline/JUnitResultsStepTest.java b/plugin/src/test/java/hudson/tasks/junit/pipeline/JUnitResultsStepTest.java
index 146d0e5bb..8f09bf970 100644
--- a/plugin/src/test/java/hudson/tasks/junit/pipeline/JUnitResultsStepTest.java
+++ b/plugin/src/test/java/hudson/tasks/junit/pipeline/JUnitResultsStepTest.java
@@ -41,7 +41,7 @@
import org.jvnet.hudson.test.TestExtension;
import org.kohsuke.stapler.DataBoundConstructor;
-import javax.annotation.Nullable;
+import edu.umd.cs.findbugs.annotations.Nullable;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
diff --git a/plugin/src/test/resources/hudson/tasks/test/TrivialTestResult/body.jelly b/plugin/src/test/resources/hudson/tasks/test/TrivialTestResult/body.jelly
index 437283d5b..0b1e20c58 100644
--- a/plugin/src/test/resources/hudson/tasks/test/TrivialTestResult/body.jelly
+++ b/plugin/src/test/resources/hudson/tasks/test/TrivialTestResult/body.jelly
@@ -23,16 +23,16 @@ THE SOFTWARE.
-->
-
+
TrivialTestResult body.jelly
${%All Failed Tests}
-
+
-
-
-
+
+
+
@@ -65,19 +65,21 @@ THE SOFTWARE.
${%All Tests}
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index 926a29a44..262451cdf 100644
--- a/pom.xml
+++ b/pom.xml
@@ -46,4 +46,4 @@
-
\ No newline at end of file
+
diff --git a/ui-tests/pom.xml b/ui-tests/pom.xml
index 2ad3d425d..f2167781f 100644
--- a/ui-tests/pom.xml
+++ b/ui-tests/pom.xml
@@ -5,7 +5,7 @@
edu.hm.hafner
codingstyle-pom
- 2.13.1
+ 2.16.0
@@ -13,10 +13,10 @@
junit-ui-tests
jar
UNVERSIONED
- UI Tests of Jenkins Plugin
+ UI Tests of JUnit Plugin
- 2.303.3
+ 2.333
2.3
2.28.0
@@ -26,18 +26,16 @@
org.jenkins-ci.main
jenkins-core
- 2.324
+ ${jenkins.version}
-
-
org.jenkins-ci
acceptance-test-harness
- 1.106
+ 1.108
org.apache.httpcomponents
@@ -149,13 +147,13 @@
assertj-assertions-generator-maven-plugin
- io.jenkins.plugins.analysis.warnings
+ io.jenkins.plugins.analysis.junit
.*Test
.*Table
- io.jenkins.plugins.analysis.warnings
+ io.jenkins.plugins.analysis.junit
@@ -174,7 +172,7 @@
../plugin/target/
- warnings-ng.hpi
+ junit.hpi
false
@@ -214,6 +212,17 @@
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+
+
+
+ junit.ui.tests
+
+
+
+
-
\ No newline at end of file
+
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/BuildChartEntry.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/BuildChartEntry.java
new file mode 100644
index 000000000..66739d39c
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/BuildChartEntry.java
@@ -0,0 +1,65 @@
+package io.jenkins.plugins.analysis.junit;
+
+/**
+ * The entry of the build chart, located on the project overview.
+ *
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+public class BuildChartEntry {
+
+ final int buildId;
+
+ final int numberOfSkippedTests;
+
+ final int numberOfFailedTests;
+
+ final int numberOfPassedTests;
+
+ /**
+ * Custom constructor. Creates object.
+ * @param buildId the build id
+ * @param numberOfSkippedTests the number of skipped tests
+ * @param numberOfFailedTests the number of failed tests
+ * @param numberOfPassedTests the number of passed tests
+ */
+ public BuildChartEntry(final int buildId, final int numberOfSkippedTests, final int numberOfFailedTests,
+ final int numberOfPassedTests) {
+ this.buildId = buildId;
+ this.numberOfSkippedTests = numberOfSkippedTests;
+ this.numberOfFailedTests = numberOfFailedTests;
+ this.numberOfPassedTests = numberOfPassedTests;
+ }
+
+ /**
+ * Gets the build id.
+ * @return the build id
+ */
+ public int getBuildId() {
+ return buildId;
+ }
+
+ /**
+ * Gets the number of skipped tests.
+ * @return the number of skipped tests
+ */
+ public int getNumberOfSkippedTests() {
+ return numberOfSkippedTests;
+ }
+
+ /**
+ * Gets the number of failed tests.
+ * @return the number of failed tests
+ */
+ public int getNumberOfFailedTests() {
+ return numberOfFailedTests;
+ }
+
+ /**
+ * Gets the number of passed tests.
+ * @return the number of passed tests
+ */
+ public int getNumberOfPassedTests() {
+ return numberOfPassedTests;
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitBuildSummary.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitBuildSummary.java
new file mode 100644
index 000000000..e15162b70
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitBuildSummary.java
@@ -0,0 +1,143 @@
+package io.jenkins.plugins.analysis.junit;
+
+import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import org.jenkinsci.test.acceptance.po.Build;
+import org.jenkinsci.test.acceptance.po.PageObject;
+
+import io.jenkins.plugins.analysis.junit.testresults.BuildTestResults;
+
+/**
+ * {@link PageObject} representing the JUnit summary on the build page of a job.
+ *
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+public class JUnitBuildSummary extends PageObject {
+
+ private final WebElement summaryIcon;
+ private final WebElement summaryContent;
+
+ private final WebElement titleLink;
+ private final List failedTestLinks;
+
+ /**
+ * Creates a new page object representing the JUnit summary on the build page of a job.
+ *
+ * @param parent
+ * a finished build configured with a static analysis tool
+ */
+ public JUnitBuildSummary(final Build parent) {
+ super(parent, parent.url("testReport"));
+
+ WebElement table = getElement(By.cssSelector("#main-panel table"));
+ List tableEntries = table.findElements(By.cssSelector("tbody tr"));
+
+ WebElement junitBuildSummaryTableEntry = tableEntries.stream()
+ .filter(trElement -> findIconInTableEntry(trElement).isPresent())
+ .filter(trElement -> findContentInTableEntry(trElement).isPresent())
+ .findAny()
+ .orElseThrow(() -> new NoSuchElementException("junit build summary table"));
+
+ summaryIcon = findIconInTableEntry(junitBuildSummaryTableEntry).get();
+ summaryContent = findContentInTableEntry(junitBuildSummaryTableEntry).get();
+
+ titleLink = summaryContent.findElement(By.cssSelector("a"));
+ failedTestLinks = summaryContent.findElements(By.cssSelector("ul li a"));
+ }
+
+ private Optional findIconInTableEntry(final WebElement tableEntry) {
+ return findOptionalElement(tableEntry, By.cssSelector("td img.icon-clipboard.icon-xlg"));
+ }
+
+ private Optional findContentInTableEntry(final WebElement tableEntry) {
+ List foundElements = tableEntry.findElements(By.cssSelector("td"));
+ return foundElements.stream()
+ .filter(foundElement -> findOptionalElement(foundElement, By.cssSelector("a")).isPresent()
+ && findOptionalElement(foundElement, By.cssSelector("a")).get().getText().equals("Test Result"))
+ .findFirst();
+ }
+
+ private Optional findOptionalElement(final WebElement webElement, final By byArgument) {
+ List foundElements = webElement.findElements(byArgument);
+ return foundElements.isEmpty() ? Optional.empty() : Optional.of(foundElements.get(0));
+ }
+
+ /**
+ * Returns the title text of the summary.
+ *
+ * @return the title text
+ */
+ public String getTitleText() {
+ return summaryContent.getText();
+ }
+
+ /**
+ * Returns the number of failures of this junit run.
+ *
+ * @return the number of failures
+ */
+ public int getNumberOfFailures() {
+ return failedTestLinks.size();
+ }
+
+ /**
+ * Returns the failures' names, in appearance order.
+ *
+ * @return the failures' names
+ */
+ public List getFailureNames() {
+ return failedTestLinks.stream()
+ .map(WebElement::getText)
+ .collect(Collectors.toList());
+ }
+
+ /**
+ * Returns the failures' target links, accessible by its name.
+ * Method {@link #getFailureNames()} could help to retrieve the origin order.
+ *
+ * @return the failures' target links
+ */
+ public Map getFailureTargetLinksByName() {
+ return failedTestLinks.stream()
+ .collect(Collectors.toMap(WebElement::getText, failedTestLink -> failedTestLink.getAttribute("href")));
+ }
+
+ /**
+ * Opens the build test results page.
+ *
+ * @return build test results page object.
+ */
+ public BuildTestResults openBuildTestResults() {
+ return openPage(titleLink, BuildTestResults.class);
+ }
+
+ /**
+ * Opens the detail view of a test.
+ *
+ * @param testName name of a test.
+ * @return test detail page object.
+ */
+ public TestDetail openTestDetailView(final String testName) {
+ WebElement link = failedTestLinks.stream()
+ .filter(failedTestLink -> failedTestLink.getText().equals(testName))
+ .findFirst()
+ .orElseThrow(() -> new NoSuchElementException(testName));
+
+ return openPage(link, TestDetail.class);
+ }
+
+ private T openPage(final WebElement link, final Class type) {
+ String href = link.getAttribute("href");
+
+ link.click();
+ return newInstance(type, injector, url(href));
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitJobConfiguration.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitJobConfiguration.java
new file mode 100644
index 000000000..02ffc8ef7
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitJobConfiguration.java
@@ -0,0 +1,82 @@
+package io.jenkins.plugins.analysis.junit;
+
+import org.jenkinsci.test.acceptance.po.AbstractStep;
+import org.jenkinsci.test.acceptance.po.Control;
+import org.jenkinsci.test.acceptance.po.Describable;
+import org.jenkinsci.test.acceptance.po.Job;
+import org.jenkinsci.test.acceptance.po.PageObject;
+import org.jenkinsci.test.acceptance.po.PostBuildStep;
+
+/**
+ * {@link PageObject} representing the publish junit post build action in the freestyle job configuration.
+ *
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+@Describable("Publish JUnit test result report")
+public class JUnitJobConfiguration extends AbstractStep implements PostBuildStep {
+ private final Control retainLogStandardOutputError = control("/keepLongStdio");
+ private final Control allowEmptyResults = control("/allowEmptyResults");
+ private final Control skipPublishingChecks = control("/skipPublishingChecks");
+ private final Control skipMarkingBuildAsUnstableOnTestFailure = control("/skipMarkingBuildUnstable");
+ private final Control healthScaleFactor = control("/healthScaleFactor");
+ private final Control testResults = control("testResults");
+
+ /**
+ * Creates a new page object representing the junit summary on the build page of a job.
+ *
+ * @param parent a created job
+ * @param path path of the
+ */
+ public JUnitJobConfiguration(Job parent, String path) {
+ super(parent, path);
+ }
+
+ /**
+ * Set test results file.
+ * @param value to set test results
+ */
+ public void setTestResults(String value) {
+ this.testResults.set(value);
+ }
+
+ /**
+ * Set checkbox to retain standard log output error.
+ * @param shouldRetainLogStandardOutputError Check or uncheck checkbox
+ */
+ public void setRetainLogStandardOutputError(boolean shouldRetainLogStandardOutputError) {
+ this.retainLogStandardOutputError.check(shouldRetainLogStandardOutputError);
+ }
+
+ /**
+ * Set checkbox to allow empty results.
+ * @param shouldAllowEmptyResults Check or uncheck checkbox
+ */
+ public void setAllowEmptyResults(boolean shouldAllowEmptyResults) {
+ this.allowEmptyResults.check(shouldAllowEmptyResults);
+ }
+
+ /**
+ * Set checkbox to skip publishing checks.
+ * @param shouldSkipPublishingChecks Check or uncheck checkbox
+ */
+ public void setSkipPublishingChecks(boolean shouldSkipPublishingChecks) {
+ this.retainLogStandardOutputError.check(shouldSkipPublishingChecks);
+ }
+
+ /**
+ * Set checkbox to skip mark build as unstable on test failure.
+ * @param shouldSkipMarkingBuildAsUnstableOnTestFailure Check or uncheck checkbox
+ */
+ public void setSkipMarkingBuildAsUnstableOnTestFailure(boolean shouldSkipMarkingBuildAsUnstableOnTestFailure) {
+ this.skipMarkingBuildAsUnstableOnTestFailure.check(shouldSkipMarkingBuildAsUnstableOnTestFailure);
+ }
+
+ /**
+ * Set input value of health scale factor.
+ * @param value value to set the health scale factor
+ */
+ public void setHealthScaleFactor(String value) {
+ this.healthScaleFactor.set(value);
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitProjectSummary.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitProjectSummary.java
new file mode 100644
index 000000000..db1d5cef4
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/JUnitProjectSummary.java
@@ -0,0 +1,183 @@
+package io.jenkins.plugins.analysis.junit;
+
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.stream.Collectors;
+import java.util.stream.IntStream;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.gargoylesoftware.htmlunit.ScriptResult;
+
+import org.jenkinsci.test.acceptance.po.Build;
+import org.jenkinsci.test.acceptance.po.PageObject;
+
+/**
+ * {@link PageObject} representing the JUnit summary on the job page.
+ *
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+public class JUnitProjectSummary extends PageObject {
+
+ private final WebElement summaryIcon;
+ private final WebElement summaryContent;
+
+ private final WebElement titleLink;
+
+ private final List buildChartEntries;
+
+ /**
+ * Creates a new page object representing the JUnit summary on the job page.
+ *
+ * @param parent
+ * a finished build configured with a static analysis tool
+ */
+ public JUnitProjectSummary(final Build parent) throws JSONException {
+ super(parent, parent.url(""));
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ WebElement junitJobSummaryTableEntry = getJunitJobSummaryTableEntry(mainPanel);
+
+ summaryIcon = findIconInTableEntry(junitJobSummaryTableEntry).get();
+ summaryContent = findContentInTableEntry(junitJobSummaryTableEntry).get();
+ titleLink = summaryContent.findElement(By.cssSelector("a"));
+ buildChartEntries = initializeBuildChartEntries();
+ }
+
+ /**
+ * Gets the title text of the summary.
+ *
+ * @return the title text
+ */
+ public String getTitleText() {
+ return summaryContent.getText();
+ }
+
+ /**
+ * Gets the number of failures of this JUnit run.
+ *
+ * @return the number of failures
+ */
+ public int getNumberOfFailures() {
+ String summaryContentText = summaryContent.getText().trim();
+ int fromIndex = summaryContentText.indexOf('(') + 1;
+ int toIndex = summaryContentText.indexOf(" ", fromIndex);
+ return Integer.parseInt(summaryContentText.substring(fromIndex, toIndex));
+ }
+
+ /**
+ * Gets the failure difference.
+ *
+ * @return the failure difference
+ */
+ public int getFailureDifference() {
+ String summaryContentText = summaryContent.getText().trim();
+ int fromIndex = summaryContentText.indexOf('/') + 2;
+ int toIndex = summaryContentText.length() - 1;
+ return Integer.parseInt(summaryContentText.substring(fromIndex, toIndex));
+ }
+
+ /**
+ * Gets the entries of the build chart.
+ *
+ * @return the entries of the build chart
+ */
+ public List getBuildChartEntries() {
+ return buildChartEntries;
+ }
+
+ private WebElement getJunitJobSummaryTableEntry(final WebElement mainPanel) {
+ List tables = mainPanel.findElements(By.cssSelector("table tbody tr"));
+ return tables.stream()
+ .filter(trElement -> findIconInTableEntry(trElement).isPresent())
+ .filter(trElement -> findContentInTableEntry(trElement).isPresent())
+ .findAny()
+ .orElseThrow(() -> new NoSuchElementException("junit job summary table"));
+ }
+
+ private List initializeBuildChartEntries() throws JSONException {
+ String canvasJson = getJUnitChart();
+ return canvasJsonToBuildChartEntries(canvasJson);
+ }
+
+ private String getJUnitChart() {
+ Object result = executeScript(String.format(
+ "delete(window.Array.prototype.toJSON) %n"
+ + "return JSON.stringify(echarts.getInstanceByDom(document.getElementsByClassName(\"echarts-trend\")[0]).getOption())"));
+ ScriptResult scriptResult = new ScriptResult(result);
+
+ return scriptResult.getJavaScriptResult().toString();
+ }
+
+ private Optional findIconInTableEntry(final WebElement tableEntry) {
+ return findOptionalElement(tableEntry, By.cssSelector("td img.icon-clipboard.icon-xlg"));
+ }
+
+ private Optional findContentInTableEntry(final WebElement tableEntry) {
+ List foundElements = tableEntry.findElements(By.cssSelector("td"));
+ return foundElements.stream()
+ .filter(foundElement -> findOptionalElement(foundElement, By.cssSelector("a")).isPresent() &&
+ findOptionalElement(foundElement, By.cssSelector("a")).get()
+ .getText()
+ .equals("Latest Test Result"))
+ .findFirst();
+ }
+
+ private Optional findOptionalElement(final WebElement webElement, final By byArgument) {
+ List foundElements = webElement.findElements(byArgument);
+ return foundElements.isEmpty() ? Optional.empty() : Optional.of(foundElements.get(0));
+ }
+
+ private List canvasJsonToBuildChartEntries(final String canvasJson) throws JSONException {
+ JSONObject jsonObject = new JSONObject(canvasJson);
+ JSONArray buildIds = jsonObject.getJSONArray("xAxis").getJSONObject(0).getJSONArray("data");
+ JSONArray series = jsonObject.getJSONArray("series");
+
+ JSONArray failedTestNumbers = null;
+ JSONArray skippedTestNumbers = null;
+ JSONArray passedTestNumbers = null;
+
+ int seriesLength = series.length();
+ for (int i = 0; i < seriesLength; i++) {
+ JSONObject currentObject = series.getJSONObject(i);
+ String seriesName = currentObject.getString("name");
+ switch (seriesName) {
+ case "Failed":
+ failedTestNumbers = currentObject.getJSONArray("data");
+ break;
+ case "Skipped":
+ skippedTestNumbers = currentObject.getJSONArray("data");
+ break;
+ case "Passed":
+ passedTestNumbers = currentObject.getJSONArray("data");
+ break;
+ }
+ }
+
+ JSONArray finalFailedTestNumbers = failedTestNumbers;
+ JSONArray finalSkippedTestNumbers = skippedTestNumbers;
+ JSONArray finalPassedTestNumbers = passedTestNumbers;
+ return IntStream.range(0, buildIds.length())
+ .boxed()
+ .map(index -> {
+ try {
+ String buildIdString = buildIds.getString(index);
+ int buildId = Integer.parseInt(buildIdString.trim().substring(1));
+ int failedTests = Integer.parseInt(finalFailedTestNumbers.getString(index));
+ int skippedTests = Integer.parseInt(finalSkippedTestNumbers.getString(index));
+ int passedTests = Integer.parseInt(finalPassedTestNumbers.getString(index));
+ return new BuildChartEntry(buildId, skippedTests, failedTests, passedTests);
+ }
+ catch (JSONException e) {
+ throw new NoSuchElementException();
+ }
+ })
+ .collect(Collectors.toList());
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/README.md b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/README.md
deleted file mode 100644
index 3091f8e3d..000000000
--- a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/README.md
+++ /dev/null
@@ -1,2 +0,0 @@
-Please delete me after you added some files here.
-The files to be added here are PageObjects for UI testing
\ No newline at end of file
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/TestDetail.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/TestDetail.java
new file mode 100644
index 000000000..f15d6d59d
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/TestDetail.java
@@ -0,0 +1,159 @@
+package io.jenkins.plugins.analysis.junit;
+
+import java.net.URL;
+import java.util.List;
+import java.util.Optional;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.google.inject.Injector;
+
+import org.jenkinsci.test.acceptance.po.Build;
+import org.jenkinsci.test.acceptance.po.PageObject;
+
+/**
+ * {@link PageObject} representing the detail view of a failed JUnit test.
+ *
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+public class TestDetail extends PageObject {
+ private final WebElement title;
+ private final WebElement subTitle;
+
+ private final Optional errorMessage;
+ private final Optional stackTrace;
+ private final Optional standardOutput;
+
+ /**
+ * Creates a new page object representing the junit detail view of a failed JUnit test.
+ *
+ * @param parent
+ * a finished build configured with a static analysis tool
+ */
+ public TestDetail(final Build parent) {
+ super(parent, parent.url("testReport"));
+
+ WebElement pageContent = getElement(By.cssSelector("#main-panel"));
+
+ title = pageContent.findElement(By.cssSelector("h1"));
+ subTitle = pageContent.findElement(By.cssSelector("p"));
+
+ int errorMessageHeaderIndex = -1;
+ int stackTraceHeaderIndex = -1;
+ int standardOutputHeaderIndex = -1;
+ List pageContentChildren = pageContent.findElements(By.cssSelector("*"));
+
+ int counter = 0;
+ for (WebElement element : pageContentChildren) {
+ if (element.getTagName().equals("h3")) {
+ if (element.getText().equals("Error Message")) {
+ errorMessageHeaderIndex = counter;
+ }
+ else if (element.getText().equals("Stacktrace")) {
+ stackTraceHeaderIndex = counter;
+ }
+ else if (element.getText().equals("Standard Output")) {
+ standardOutputHeaderIndex = counter;
+ }
+ }
+ ++counter;
+ }
+
+ errorMessage = errorMessageHeaderIndex >= 0 ? Optional.of(pageContentChildren.get(errorMessageHeaderIndex + 1)) : Optional.empty();
+ stackTrace = stackTraceHeaderIndex >= 0 ? Optional.of(pageContentChildren.get(stackTraceHeaderIndex + 1)) : Optional.empty();
+ standardOutput = standardOutputHeaderIndex >= 0 ? Optional.of(pageContentChildren.get(standardOutputHeaderIndex + 1)) : Optional.empty();
+
+ }
+
+ /**
+ * Creates an instance of the page displaying the details of the issues. This constructor is used for injecting a
+ * filtered instance of the page (e.g. by clicking on links which open a filtered instance of a AnalysisResult.
+ *
+ * @param injector
+ * the injector of the page
+ * @param url
+ * the url of the page
+ */
+ @SuppressWarnings("unused") // Required to dynamically create page object using reflection
+ public TestDetail(final Injector injector, final URL url) {
+ super(injector, url);
+
+ WebElement pageContent = getElement(By.cssSelector("#main-panel"));
+
+ title = pageContent.findElement(By.cssSelector("h1"));
+ subTitle = pageContent.findElement(By.cssSelector("p"));
+
+ int errorMessageHeaderIndex = -1;
+ int stackTraceHeaderIndex = -1;
+ int standardOutputHeaderIndex = -1;
+ List pageContentChildren = pageContent.findElements(By.cssSelector("*"));
+
+ int counter = 0;
+ for (WebElement element : pageContentChildren) {
+ if (element.getTagName().equals("h3")) {
+ if (element.getText().equals("Error Message")) {
+ errorMessageHeaderIndex = counter;
+ }
+ else if (element.getText().equals("Stacktrace")) {
+ stackTraceHeaderIndex = counter;
+ }
+ else if (element.getText().equals("Standard Output")) {
+ standardOutputHeaderIndex = counter;
+ }
+ }
+ ++counter;
+ }
+
+ errorMessage = errorMessageHeaderIndex >= 0 ? Optional.of(pageContentChildren.get(errorMessageHeaderIndex + 1)) : Optional.empty();
+ stackTrace = stackTraceHeaderIndex >= 0 ? Optional.of(pageContentChildren.get(stackTraceHeaderIndex + 1)) : Optional.empty();
+ standardOutput = standardOutputHeaderIndex >= 0 ? Optional.of(pageContentChildren.get(standardOutputHeaderIndex + 1)) : Optional.empty();
+ }
+
+ /**
+ * Returns the title of the detail view.
+ *
+ * @return the title of the detail view
+ */
+ public String getTitle() {
+ return title.getText();
+ }
+
+ /**
+ * Returns the subtitle of the detail view, which is the test.
+ *
+ * @return the subtitle of the detail view
+ */
+ public String getSubTitle() {
+ return subTitle.findElement(By.cssSelector("span")).getText() + subTitle.getText();
+ }
+
+ /**
+ * Returns the error message telling the user why the test has failed.
+ *
+ * @return the error message
+ */
+ public Optional getErrorMessage() {
+ return errorMessage.map(WebElement::getText);
+ }
+
+ /**
+ * Returns the stack trace providing more information about the failed test.
+ *
+ * @return the stack trace of the failed test
+ */
+ public Optional getStackTrace() {
+ return stackTrace.map(WebElement::getText);
+ }
+
+ /**
+ * Returns the standard output providing more information about the test.
+ *
+ * @return the standard output of the test
+ */
+ public Optional getStandardOutput() {
+ return standardOutput.map(WebElement::getText);
+ }
+
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResults.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResults.java
new file mode 100644
index 000000000..d24b3b9fe
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResults.java
@@ -0,0 +1,114 @@
+package io.jenkins.plugins.analysis.junit.testresults;
+
+import java.net.URL;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.google.inject.Injector;
+
+import org.jenkinsci.test.acceptance.po.Build;
+import org.jenkinsci.test.acceptance.po.PageObject;
+
+import io.jenkins.plugins.analysis.junit.testresults.tableentry.PackageTableEntry;
+
+/**
+ * {@link PageObject} representing the first page of the test results of a build.
+ *
+ * @author Nikolas Paripovic
+ * @author Michael Müller
+ */
+public class BuildTestResults extends TestResultsWithFailedTestTable {
+
+ private final List packageLinks;
+ private final List packageTableEntries;
+
+ /**
+ * Creates a new page object representing the first page of the test results of a build.
+ *
+ * @param parent
+ * a finished build configured with a static analysis tool
+ */
+ public BuildTestResults(final Build parent) {
+ super(parent);
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ packageLinks = TestResultsTableUtil.getLinksOfTableItems(mainPanel);
+ packageTableEntries = initializePackageTableEntries(mainPanel);
+ }
+
+ /**
+ * Creates an instance of the page. This constructor is used for injecting a
+ * filtered instance of the page (e.g. by clicking on links which open a filtered instance of a AnalysisResult.
+ *
+ * @param injector
+ * the injector of the page
+ * @param url
+ * the url of the page
+ */
+ public BuildTestResults(final Injector injector, final URL url) {
+ super(injector, url);
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ packageLinks = TestResultsTableUtil.getLinksOfTableItems(mainPanel);
+ packageTableEntries = initializePackageTableEntries(mainPanel);
+ }
+
+ /**
+ * Gets the entries of the package table.
+ * @return the package table entries
+ */
+ public List getPackageTableEntries() {
+ return packageTableEntries;
+ }
+
+ /**
+ * Open the test results page, filtered by package.
+ * @param packageName the package to filter
+ * @return the opened page
+ */
+ public BuildTestResultsByPackage openTestResultsByPackage(final String packageName) {
+ WebElement link = packageLinks.stream()
+ .filter(packageLink -> packageLink.getText().equals(packageName))
+ .findFirst()
+ .orElseThrow(() -> new NoSuchElementException(packageName));
+ return openPage(link, BuildTestResultsByPackage.class);
+ }
+
+ private List initializePackageTableEntries(final WebElement mainPanel) {
+ return TestResultsTableUtil.getTableItemsWithoutHeader(mainPanel).stream()
+ .map(this::webElementToPackageTableEntry)
+ .collect(Collectors.toList());
+ }
+
+ private PackageTableEntry webElementToPackageTableEntry(final WebElement trElement) {
+ List columns = trElement.findElements(By.cssSelector("td"));
+
+ WebElement linkElement = columns.get(0).findElement(TestResultsTableUtil.aLink());
+ String packageName = linkElement.getText();
+ String packageLink = linkElement.getAttribute("href");
+ String durationString = columns.get(1).getText().trim();
+ int duration = Integer.parseInt(durationString.substring(0, durationString.length() - " ms".length()));
+ int fail = Integer.parseInt(columns.get(2).getText());
+ String failDiffString = columns.get(3).getText();
+ Optional failDiff = failDiffString.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(failDiffString));
+ int skip = Integer.parseInt(columns.get(4).getText());
+ String skipDiffString = columns.get(5).getText();
+ Optional skipDiff = skipDiffString.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(skipDiffString));
+ int pass = Integer.parseInt(columns.get(6).getText());
+ String passDiffString = columns.get(7).getText();
+ Optional passDiff = passDiffString.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(passDiffString));
+ int total = Integer.parseInt(columns.get(8).getText());
+ String totalDiffString = columns.get(9).getText();
+ Optional totalDiff = totalDiffString.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(totalDiffString));
+
+ return new PackageTableEntry(packageName, packageLink, duration, fail, failDiff, skip, skipDiff,
+ pass, passDiff, total, totalDiff);
+ }
+
+
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResultsByClass.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResultsByClass.java
new file mode 100644
index 000000000..ccea47da0
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResultsByClass.java
@@ -0,0 +1,101 @@
+package io.jenkins.plugins.analysis.junit.testresults;
+
+import java.net.URL;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.google.inject.Injector;
+
+import org.jenkinsci.test.acceptance.po.Build;
+import org.jenkinsci.test.acceptance.po.PageObject;
+
+import io.jenkins.plugins.analysis.junit.TestDetail;
+import io.jenkins.plugins.analysis.junit.testresults.tableentry.TestTableEntry;
+
+/**
+ * {@link PageObject} representing the third page of the test results of a build.
+ * This page contains tests filtered by a specific package and a specific class.
+ *
+ * @author Nikolas Paripovic
+ * @author Michael Müller
+ */
+public class BuildTestResultsByClass extends TestResults {
+
+ private final List testLinks;
+ private final List testTableEntries;
+
+ /**
+ * Creates a new page object representing the third page of the test results of a build.
+ *
+ * @param parent
+ * a finished build configured with a static analysis tool
+ */
+ public BuildTestResultsByClass(final Build parent) {
+ super(parent);
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ testLinks = TestResultsTableUtil.getLinksOfTableItems(mainPanel);
+ testTableEntries = initializeTestTableEntries(mainPanel);
+ }
+
+ /**
+ * Creates an instance of the page. This constructor is used for injecting a
+ * filtered instance of the page (e.g. by clicking on links which open a filtered instance of a AnalysisResult.
+ *
+ * @param injector
+ * the injector of the page
+ * @param url
+ * the url of the page
+ */
+ public BuildTestResultsByClass(final Injector injector, final URL url) {
+ super(injector, url);
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ testLinks = TestResultsTableUtil.getLinksOfTableItems(mainPanel);
+ testTableEntries = initializeTestTableEntries(mainPanel);
+ }
+
+ /**
+ * Gets the entries of the test table.
+ * @return the test table entries
+ */
+ public List getTestTableEntries() {
+ return testTableEntries;
+ }
+
+ /**
+ * Open the test results page, filtered by test name.
+ * @param testName the test to filter
+ * @return the opened page
+ */
+ public TestDetail openTestDetail(final String testName) {
+ WebElement link = testLinks.stream()
+ .filter(classLink -> classLink.getText().equals(testName))
+ .findFirst()
+ .orElseThrow(() -> new NoSuchElementException(testName));
+ return openPage(link, TestDetail.class);
+ }
+
+ private List initializeTestTableEntries(final WebElement mainPanel) {
+ return TestResultsTableUtil.getTableItemsWithoutHeader(mainPanel).stream()
+ .map(this::webElementToTestTableEntry)
+ .collect(Collectors.toList());
+ }
+
+ private TestTableEntry webElementToTestTableEntry(final WebElement trElement) {
+ List columns = trElement.findElements(By.cssSelector("td"));
+
+ WebElement linkElement = columns.get(0).findElement(TestResultsTableUtil.aLink());
+ String testName = linkElement.getText();
+ String testLink = linkElement.getAttribute("href");
+ String durationString = columns.get(1).getText().trim();
+ int duration = Integer.parseInt(durationString.substring(0, durationString.length() - " ms".length()));
+ String status = columns.get(2).getText();
+
+ return new TestTableEntry(testName, testLink, duration, status);
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResultsByPackage.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResultsByPackage.java
new file mode 100644
index 000000000..39661174b
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/BuildTestResultsByPackage.java
@@ -0,0 +1,113 @@
+package io.jenkins.plugins.analysis.junit.testresults;
+
+import java.net.URL;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.google.inject.Injector;
+
+import org.jenkinsci.test.acceptance.po.Build;
+import org.jenkinsci.test.acceptance.po.PageObject;
+
+import io.jenkins.plugins.analysis.junit.testresults.tableentry.ClassTableEntry;
+
+/**
+ * {@link PageObject} representing the second page of the test results of a build.
+ * This page contains tests filtered by a specific package.
+ *
+ * @author Nikolas Paripovic
+ * @author Michael Müller
+ */
+public class BuildTestResultsByPackage extends TestResultsWithFailedTestTable {
+
+ private final List classLinks;
+ private final List classTableEntries;
+
+ /**
+ * Creates a new page object representing the second page of the test results of a build.
+ *
+ * @param parent
+ * a finished build configured with a static analysis tool
+ */
+ public BuildTestResultsByPackage(final Build parent) {
+ super(parent);
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ classLinks = TestResultsTableUtil.getLinksOfTableItems(mainPanel);
+ classTableEntries = initializeClassTableEntries(mainPanel);
+ }
+
+ /**
+ * Creates an instance of the page. This constructor is used for injecting a
+ * filtered instance of the page (e.g. by clicking on links which open a filtered instance of a AnalysisResult.
+ *
+ * @param injector
+ * the injector of the page
+ * @param url
+ * the url of the page
+ */
+ public BuildTestResultsByPackage(final Injector injector, final URL url) {
+ super(injector, url);
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ classLinks = TestResultsTableUtil.getLinksOfTableItems(mainPanel);
+ classTableEntries = initializeClassTableEntries(mainPanel);
+ }
+
+ /**
+ * Gets the entries of the class table.
+ * @return the class table entries
+ */
+ public List getClassTableEntries() {
+ return classTableEntries;
+ }
+
+ /**
+ * Open the test results page, filtered by class name.
+ * @param className the class to filter
+ * @return the opened page
+ */
+ public BuildTestResultsByClass openTestResultsByClass(final String className) {
+ WebElement link = classLinks.stream()
+ .filter(classLink -> classLink.getText().equals(className))
+ .findFirst()
+ .orElseThrow(() -> new NoSuchElementException(className));
+ return openPage(link, BuildTestResultsByClass.class);
+ }
+
+ private List initializeClassTableEntries(final WebElement mainPanel) {
+ return TestResultsTableUtil.getTableItemsWithoutHeader(mainPanel).stream()
+ .map(this::webElementToClassTableEntry)
+ .collect(Collectors.toList());
+ }
+
+ private ClassTableEntry webElementToClassTableEntry(final WebElement trElement) {
+ List columns = trElement.findElements(By.cssSelector("td"));
+
+ WebElement linkElement = columns.get(0).findElement(TestResultsTableUtil.aLink());
+ String className = linkElement.getText();
+ String classLink = linkElement.getAttribute("href");
+ String durationString = columns.get(1).getText().trim();
+ int duration = Integer.parseInt(durationString.substring(0, durationString.length() - " ms".length()));
+ int fail = Integer.parseInt(columns.get(2).getText());
+ String failDiffString = columns.get(3).getText();
+ Optional failDiff = failDiffString.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(failDiffString));
+ int skip = Integer.parseInt(columns.get(4).getText());
+ String skipDiffString = columns.get(5).getText();
+ Optional skipDiff = skipDiffString.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(skipDiffString));
+ int pass = Integer.parseInt(columns.get(6).getText());
+ String passDiffString = columns.get(7).getText();
+ Optional passDiff = passDiffString.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(passDiffString));
+ int total = Integer.parseInt(columns.get(8).getText());
+ String totalDiffString = columns.get(9).getText();
+ Optional totalDiff = totalDiffString.isEmpty() ? Optional.empty() : Optional.of(Integer.parseInt(totalDiffString));
+
+ return new ClassTableEntry(className, classLink, duration, fail, failDiff, skip, skipDiff,
+ pass, passDiff, total, totalDiff);
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResults.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResults.java
new file mode 100644
index 000000000..abb6cabf1
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResults.java
@@ -0,0 +1,91 @@
+package io.jenkins.plugins.analysis.junit.testresults;
+
+import java.net.URL;
+import java.util.List;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.google.inject.Injector;
+
+import org.jenkinsci.test.acceptance.po.Build;
+import org.jenkinsci.test.acceptance.po.PageObject;
+
+/**
+ * Abstract {@link PageObject} base class for test results pages.
+ *
+ * @author Nikolas Paripovic
+ * @author Michael Müller
+ */
+public abstract class TestResults extends PageObject {
+
+ private final WebElement numberOfFailuresElement;
+ private final WebElement numberOfTestsElement;
+
+ /**
+ * Creates a new abstract page object as a base class for test results pages.
+ *
+ * @param parent
+ * a finished build configured with a static analysis tool
+ */
+ public TestResults(final Build parent) {
+ super(parent, parent.url("testReport"));
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ numberOfFailuresElement = initializeNumberOfFailuresElement(mainPanel);
+ numberOfTestsElement = initializeNumberOfTestsElement(mainPanel);
+ }
+
+ /**
+ * Creates an instance of the page. This constructor is used for injecting a
+ * filtered instance of the page (e.g. by clicking on links which open a filtered instance of a AnalysisResult.
+ *
+ * @param injector
+ * the injector of the page
+ * @param url
+ * the url of the page
+ */
+ public TestResults(final Injector injector, final URL url) {
+ super(injector, url);
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ numberOfFailuresElement = initializeNumberOfFailuresElement(mainPanel);
+ numberOfTestsElement = initializeNumberOfTestsElement(mainPanel);
+ }
+
+ /**
+ * Gets the number of failed tests in this build.
+ * @return the number of failures
+ */
+ public int getNumberOfFailures() {
+ String text = numberOfFailuresElement.getText().trim();
+ return Integer.parseInt(text.substring(0, text.indexOf(' ')));
+ }
+
+ /**
+ * Gets the number of processed tests in this build.
+ * @return the number of tests
+ */
+ public int getNumberOfTests() {
+ String text = numberOfTestsElement.getText().trim();
+ return Integer.parseInt(text.substring(0, text.indexOf(' ')));
+ }
+
+ protected T openPage(final WebElement link, final Class type) {
+ String href = link.getAttribute("href");
+ link.click();
+ return newInstance(type, injector, url(href));
+ }
+
+ private WebElement initializeNumberOfTestsElement(final WebElement mainPanel) {
+ List testsNumberElements = getTestsNumberElements(mainPanel);
+ return testsNumberElements.get(testsNumberElements.size() - 1);
+ }
+
+ private WebElement initializeNumberOfFailuresElement(final WebElement mainPanel) {
+ List testsNumberElements = getTestsNumberElements(mainPanel);
+ return testsNumberElements.get(0);
+ }
+
+ private List getTestsNumberElements(final WebElement mainPanel) {
+ return mainPanel.findElements(By.cssSelector("h1 + div div"));
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResultsTableUtil.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResultsTableUtil.java
new file mode 100644
index 000000000..9c7094b1a
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResultsTableUtil.java
@@ -0,0 +1,43 @@
+package io.jenkins.plugins.analysis.junit.testresults;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+/**
+ * Util class for repeating parsing tasks.
+ *
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+public class TestResultsTableUtil {
+ static By aLink() {
+ return By.xpath("a[not(@id)]");
+ }
+
+ /**
+ * Gets the test result table items, omitting the header table item.
+ * @param mainPanelElement the main panel element
+ * @return the test result table items
+ */
+ public static List getTableItemsWithoutHeader(final WebElement mainPanelElement) {
+ WebElement testResultTable = mainPanelElement.findElement(By.cssSelector("#testresult"));
+
+ List testResultTableBodies = testResultTable.findElements(By.cssSelector("tbody"));
+ WebElement testResultTableBodyWithoutHeader = testResultTableBodies.get(0);
+ return testResultTableBodyWithoutHeader.findElements(By.cssSelector("tr"));
+ }
+
+ /**
+ * Gets the links of the test result table items.
+ * @param mainPanelElement the main panel element
+ * @return the links of the test result table items
+ */
+ public static List getLinksOfTableItems(final WebElement mainPanelElement) {
+ return getTableItemsWithoutHeader(mainPanelElement).stream()
+ .map(trElement -> trElement.findElements(By.cssSelector("td")).get(0).findElement(aLink()))
+ .collect(Collectors.toList());
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResultsWithFailedTestTable.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResultsWithFailedTestTable.java
new file mode 100644
index 000000000..06e89c714
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/TestResultsWithFailedTestTable.java
@@ -0,0 +1,204 @@
+package io.jenkins.plugins.analysis.junit.testresults;
+
+import java.net.URL;
+import java.util.Collections;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebElement;
+
+import com.google.inject.Injector;
+
+import org.jenkinsci.test.acceptance.po.Build;
+import org.jenkinsci.test.acceptance.po.PageObject;
+
+import io.jenkins.plugins.analysis.junit.TestDetail;
+import io.jenkins.plugins.analysis.junit.testresults.tableentry.FailedTestTableEntry;
+
+/**
+ * Abstract {@link PageObject} base class for test results pages including a failed test table.
+ *
+ * @author Nikolas Paripovic
+ * @author Michael Müller
+ */
+public abstract class TestResultsWithFailedTestTable extends TestResults {
+
+ private final Optional failedTestsTable;
+
+ private final List failedTestLinks;
+ private final List failedTestTableEntries;
+
+ /**
+ * Creates a new abstract page object as a base class for test results pages.
+ *
+ * @param parent
+ * a finished build configured with a static analysis tool
+ */
+ public TestResultsWithFailedTestTable(final Build parent) {
+ super(parent);
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ failedTestsTable = initialFailedTestsTable(mainPanel);
+
+ if (failedTestsTable.isPresent()) {
+ failedTestLinks = initializeFailedTestLinks(failedTestsTable.get());
+ failedTestTableEntries = initializeFailedTestTableEntries(failedTestsTable.get());
+ }
+ else {
+ failedTestLinks = Collections.emptyList();
+ failedTestTableEntries = Collections.emptyList();
+ }
+ }
+
+ /**
+ * Creates an instance of the page. This constructor is used for injecting a
+ * filtered instance of the page (e.g. by clicking on links which open a filtered instance of a AnalysisResult.
+ *
+ * @param injector
+ * the injector of the page
+ * @param url
+ * the url of the page
+ */
+ public TestResultsWithFailedTestTable(final Injector injector, final URL url) {
+ super(injector, url);
+
+ WebElement mainPanel = getElement(By.cssSelector("#main-panel"));
+ failedTestsTable = initialFailedTestsTable(mainPanel);
+
+ if (failedTestsTable.isPresent()) {
+ failedTestLinks = initializeFailedTestLinks(failedTestsTable.get());
+ failedTestTableEntries = initializeFailedTestTableEntries(failedTestsTable.get());
+ }
+ else {
+ failedTestLinks = Collections.emptyList();
+ failedTestTableEntries = Collections.emptyList();
+ }
+
+ }
+
+ /**
+ * Checks whether the failed test table exists or not.
+ * With this knowledge, you can call {@link #getFailedTestTableEntries()}
+ * @return whether the failed test table exists or not
+ */
+ public boolean failedTestTableExists() {
+ return failedTestsTable.isPresent();
+ }
+
+ /**
+ * Gets the table entries of the failed tests.
+ * @return the failed test table entries.
+ */
+ public List getFailedTestTableEntries() {
+ return failedTestTableEntries;
+ }
+
+ /**
+ * Opens the test detail page.
+ * @param testName the test to open
+ * @return the opened page
+ */
+ public TestDetail openTestDetail(final String testName) {
+ WebElement link = failedTestLinks.stream()
+ .filter(failedTestLink -> failedTestLink.getText().equals(testName))
+ .findFirst()
+ .orElseThrow(() -> new NoSuchElementException(testName));
+ return openPage(link, TestDetail.class);
+ }
+
+ private List getFailedTestsTableItemsWithoutHeader(final WebElement failedTestsTableElement) {
+ return failedTestsTableElement.findElements(By.cssSelector("tbody tr"));
+ }
+
+ private List initializeFailedTestLinks(final WebElement failedTestsTableElement) {
+ List failedTestsTableItemsWithoutHeader = getFailedTestsTableItemsWithoutHeader(failedTestsTableElement);
+ return failedTestsTableItemsWithoutHeader.stream()
+ .map(trElement -> trElement.findElements(By.cssSelector("td"))
+ .get(0)
+ .findElement(TestResultsTableUtil.aLink()))
+ .collect(Collectors.toList());
+ }
+
+ private List initializeFailedTestTableEntries(final WebElement failedTestsTableElement) {
+ List failedTestsTableItemsWithoutHeader = getFailedTestsTableItemsWithoutHeader(failedTestsTableElement);
+ return failedTestsTableItemsWithoutHeader.stream()
+ .map(this::webElementToFailedTestTableEntry)
+ .collect(Collectors.toList());
+ }
+
+ private Optional initialFailedTestsTable(final WebElement mainPanel) {
+ int failedTestsTableIndex = -1;
+ List pageContentChildren = mainPanel.findElements(By.cssSelector("*"));
+
+ int counter = 0;
+ for (WebElement element : pageContentChildren) {
+ if (element.getTagName().equals("h2")) {
+ if (element.getText().equals("All Failed Tests")) {
+ failedTestsTableIndex = counter;
+ }
+ }
+ ++counter;
+ }
+ return failedTestsTableIndex >= 0 ? Optional.of(pageContentChildren.get(failedTestsTableIndex + 1)) : Optional.empty();
+ }
+
+ private FailedTestTableEntry webElementToFailedTestTableEntry(final WebElement trElement) {
+ List columns = trElement.findElements(By.cssSelector("td"));
+
+ WebElement linkElement = columns.get(0).findElement(TestResultsTableUtil.aLink());
+ String testName = linkElement.getText();
+ String testLink = linkElement.getAttribute("href");
+ String durationString = columns.get(1).getText().trim();
+ int duration = Integer.parseInt(durationString.substring(0, durationString.length() - " ms".length()));
+ int age = Integer.parseInt(columns.get(2).findElement(By.cssSelector("a")).getText());
+
+ WebElement expandLink = columns.get(0).findElement(By.cssSelector("a[title=\"Show details\"]"));
+ expandLink.click();
+
+ WebElement failureSummary = columns.get(0).findElement(By.cssSelector("div.failure-summary"));
+
+ List showErrorDetailsLinks = failureSummary.findElements(By.cssSelector("a[title=\"Show Error Details\"]"));
+ if (showErrorDetailsLinks.size() > 0) {
+ WebElement showErrorDetailsLink = showErrorDetailsLinks.get(0);
+ if (!showErrorDetailsLink.getAttribute("style").contains("display: none;")) {
+ showErrorDetailsLink.click();
+ }
+ }
+
+ List showStackTraceLinks = failureSummary.findElements(By.cssSelector("a[title=\"Show Stack Trace\"]"));
+ if (showStackTraceLinks.size() > 0) {
+ WebElement showStackTraceLink = showStackTraceLinks.get(0);
+ if (!showStackTraceLink.getAttribute("style").contains("display: none;")) {
+ showStackTraceLink.click();
+ }
+ }
+
+ List failureSummaryChildren = failureSummary.findElements(By.cssSelector("*"));
+ int counter = 0;
+ int errorDetailsHeaderIndex = -1;
+ int stackTraceHeaderIndex = -1;
+ for (WebElement element : failureSummaryChildren) {
+ if (element.getTagName().equals("h4")) {
+ if (element.findElements(By.cssSelector("a[title=\"Show Error Details\"]")).size() > 0) {
+ errorDetailsHeaderIndex = counter;
+ }
+ else if (element.findElements(By.cssSelector("a[title=\"Show Stack Trace\"]")).size() > 0) {
+ stackTraceHeaderIndex = counter;
+ }
+ }
+ ++counter;
+ }
+
+ Optional errorDetailsPreElement = errorDetailsHeaderIndex == -1 ? Optional.empty() : Optional.of(failureSummaryChildren.get(errorDetailsHeaderIndex + 1));
+ Optional stackTracePreElement = stackTraceHeaderIndex == -1 ? Optional.empty() : Optional.of(failureSummaryChildren.get(stackTraceHeaderIndex + 1));
+
+ Optional errorDetails = errorDetailsPreElement.map(WebElement::getText);
+ Optional stackTrace = stackTracePreElement.map(WebElement::getText);
+
+ return new FailedTestTableEntry(testName, testLink, duration, age, errorDetails, stackTrace);
+ }
+
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/ClassTableEntry.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/ClassTableEntry.java
new file mode 100644
index 000000000..3656f32f3
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/ClassTableEntry.java
@@ -0,0 +1,152 @@
+package io.jenkins.plugins.analysis.junit.testresults.tableentry;
+
+import java.util.Optional;
+
+/**
+ * The entry of a class table listing test results of current and previous builds.
+ *
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+public class ClassTableEntry {
+
+ private final String className;
+
+ private final String classLink;
+
+ private final int duration;
+
+ private final int fail;
+
+ private final Optional failDiff;
+
+ private final int skip;
+
+ private final Optional skipDiff;
+
+ private final int pass;
+
+ private final Optional passDiff;
+
+ private final int total;
+
+ private final Optional totalDiff;
+
+ /**
+ * Custom constructor. Creates object.
+ * @param className the class name property
+ * @param classLink the target location class link
+ * @param duration the duration property
+ * @param fail the fail property
+ * @param failDiff the fail diff property
+ * @param skip the skip property
+ * @param skipDiff the skip diff property
+ * @param pass the pass property
+ * @param passDiff the pass diff property
+ * @param total the total property
+ * @param totalDiff the total diff property
+ */
+ public ClassTableEntry(final String className, final String classLink, final int duration, final int fail,
+ final Optional failDiff, final int skip,
+ final Optional skipDiff, final int pass, final Optional passDiff, final int total, final Optional totalDiff) {
+ this.className = className;
+ this.classLink = classLink;
+ this.duration = duration;
+ this.fail = fail;
+ this.failDiff = failDiff;
+ this.skip = skip;
+ this.skipDiff = skipDiff;
+ this.pass = pass;
+ this.passDiff = passDiff;
+ this.total = total;
+ this.totalDiff = totalDiff;
+ }
+
+ /**
+ * Gets the property class name.
+ * @return the class name property
+ */
+ public String getClassName() {
+ return className;
+ }
+
+ /**
+ * Gets the target location when clicking on the class name.
+ * @return the class link
+ */
+ public String getClassLink() {
+ return classLink;
+ }
+
+ /**
+ * Gets the property duration.
+ * @return the duration property
+ */
+ public int getDuration() {
+ return duration;
+ }
+
+ /**
+ * Gets the property fail.
+ * @return the fail property
+ */
+ public int getFail() {
+ return fail;
+ }
+
+ /**
+ * Gets the optional property fail diff.
+ * @return the fail diff property
+ */
+ public Optional getFailDiff() {
+ return failDiff;
+ }
+
+ /**
+ * Gets the property skip.
+ * @return the skip property
+ */
+ public int getSkip() {
+ return skip;
+ }
+
+ /**
+ * Gets the optional property skip diff.
+ * @return the skip diff property
+ */
+ public Optional getSkipDiff() {
+ return skipDiff;
+ }
+
+ /**
+ * Gets the property pass.
+ * @return the pass property
+ */
+ public int getPass() {
+ return pass;
+ }
+
+ /**
+ * Gets the optional property pass diff.
+ * @return the pass diff property
+ */
+ public Optional getPassDiff() {
+ return passDiff;
+ }
+
+ /**
+ * Gets the property total.
+ * @return the total property
+ */
+ public int getTotal() {
+ return total;
+ }
+
+ /**
+ * Gets the optional property total diff.
+ * @return the total diff property
+ */
+ public Optional getTotalDiff() {
+ return totalDiff;
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/FailedTestTableEntry.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/FailedTestTableEntry.java
new file mode 100644
index 000000000..10ec274a9
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/FailedTestTableEntry.java
@@ -0,0 +1,92 @@
+package io.jenkins.plugins.analysis.junit.testresults.tableentry;
+
+import java.util.Optional;
+
+/**
+ * The entry of a failed test table listing failed tests of the current build.
+ *
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+public class FailedTestTableEntry {
+
+ private final String testName;
+
+ private final String testLink;
+
+ private final int duration;
+
+ private final int age;
+
+ private final Optional errorDetails;
+
+ private final Optional stackTrace;
+
+ /**
+ * Costum constructor. Creates object.
+ * @param testName the test name property
+ * @param testLink the target location test link
+ * @param duration the duration property
+ * @param age the age property
+ * @param errorDetails the error details property
+ * @param stackTrace the stack trace property
+ */
+ public FailedTestTableEntry(final String testName, final String testLink, final int duration, final int age,
+ final Optional errorDetails,
+ final Optional stackTrace) {
+ this.testName = testName;
+ this.testLink = testLink;
+ this.duration = duration;
+ this.age = age;
+ this.errorDetails = errorDetails;
+ this.stackTrace = stackTrace;
+ }
+
+ /**
+ * Gets the test name property.
+ * @return the test name property
+ */
+ public String getTestName() {
+ return testName;
+ }
+
+ /**
+ * Gets the target location when clicking on the test name.
+ * @return the test link
+ */
+ public String getTestLink() {
+ return testLink;
+ }
+
+ /**
+ * Gets the duration property.
+ * @return the duration property
+ */
+ public int getDuration() {
+ return duration;
+ }
+
+ /**
+ * Gets the age property.
+ * @return the age property
+ */
+ public int getAge() {
+ return age;
+ }
+
+ /**
+ * Gets the optional error details.
+ * @return the error details
+ */
+ public Optional getErrorDetails() {
+ return errorDetails;
+ }
+
+ /**
+ * Gets the optional stack trace.
+ * @return the stack trace
+ */
+ public Optional getStackTrace() {
+ return stackTrace;
+ }
+}
diff --git a/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/PackageTableEntry.java b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/PackageTableEntry.java
new file mode 100644
index 000000000..0db3fe75c
--- /dev/null
+++ b/ui-tests/src/main/java/io/jenkins/plugins/analysis/junit/testresults/tableentry/PackageTableEntry.java
@@ -0,0 +1,151 @@
+package io.jenkins.plugins.analysis.junit.testresults.tableentry;
+
+import java.util.Optional;
+
+/**
+ * The entry of a package table listing test results of current and previous builds.
+ * @author Michael Müller
+ * @author Nikolas Paripovic
+ */
+public class PackageTableEntry {
+
+ private final String packageName;
+
+ private final String packageLink;
+
+ private final int duration;
+
+ private final int fail;
+
+ private final Optional failDiff;
+
+ private final int skip;
+
+ private final Optional skipDiff;
+
+ private final int pass;
+
+ private final Optional passDiff;
+
+ private final int total;
+
+ private final Optional totalDiff;
+
+ /**
+ * Custom constructor. Creates object.
+ * @param packageName the package name property
+ * @param packageLink the target location package link
+ * @param duration the duration property
+ * @param fail the fail property
+ * @param failDiff the fail diff property
+ * @param skip the skip property
+ * @param skipDiff the skip diff property
+ * @param pass the pass property
+ * @param passDiff the pass diff property
+ * @param total the total property
+ * @param totalDiff the total diff property
+ */
+ public PackageTableEntry(final String packageName, final String packageLink, final int duration, final int fail,
+ final Optional failDiff, final int skip,
+ final Optional skipDiff, final int pass, final Optional passDiff, final int total, final Optional totalDiff) {
+ this.packageName = packageName;
+ this.packageLink = packageLink;
+ this.duration = duration;
+ this.fail = fail;
+ this.failDiff = failDiff;
+ this.skip = skip;
+ this.skipDiff = skipDiff;
+ this.pass = pass;
+ this.passDiff = passDiff;
+ this.total = total;
+ this.totalDiff = totalDiff;
+ }
+
+ /**
+ * Gets the package name property.
+ * @return the package name property
+ */
+ public String getPackageName() {
+ return packageName;
+ }
+
+ /**
+ * Gets the target location when clicking on the package name.
+ * @return the package link
+ */
+ public String getPackageLink() {
+ return packageLink;
+ }
+
+ /**
+ * Gets the duration property.
+ * @return the duration property
+ */
+ public int getDuration() {
+ return duration;
+ }
+
+ /**
+ * Gets the fail property.
+ * @return the fail property
+ */
+ public int getFail() {
+ return fail;
+ }
+
+ /**
+ * Gets the optional fail diff property.
+ * @return the fail diff property
+ */
+ public Optional getFailDiff() {
+ return failDiff;
+ }
+
+ /**
+ * Gets the skip property.
+ * @return the skip property
+ */
+ public int getSkip() {
+ return skip;
+ }
+
+ /**
+ * Gets the optional skip diff property.
+ * @return the skip diff property
+ */
+ public Optional getSkipDiff() {
+ return skipDiff;
+ }
+
+ /**
+ * Gets the pass property.
+ * @return the pass property
+ */
+ public int getPass() {
+ return pass;
+ }
+
+ /**
+ * Gets the optional pass diff property.
+ * @return the pass diff property
+ */
+ public Optional | | | | | | |