View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.myfaces.tobago.example.demo.qunit;
21  
22  import org.apache.commons.lang3.time.DurationFormatUtils;
23  import org.junit.jupiter.api.AfterAll;
24  import org.junit.jupiter.api.Assertions;
25  import org.junit.jupiter.api.BeforeAll;
26  import org.junit.jupiter.params.provider.Arguments;
27  import org.openqa.selenium.By;
28  import org.openqa.selenium.NoSuchElementException;
29  import org.openqa.selenium.WebDriver;
30  import org.openqa.selenium.WebElement;
31  import org.openqa.selenium.chrome.ChromeOptions;
32  import org.openqa.selenium.remote.RemoteWebDriver;
33  import org.openqa.selenium.support.ui.ExpectedConditions;
34  import org.openqa.selenium.support.ui.FluentWait;
35  import org.slf4j.Logger;
36  import org.slf4j.LoggerFactory;
37  
38  import java.io.IOException;
39  import java.io.UnsupportedEncodingException;
40  import java.lang.invoke.MethodHandles;
41  import java.net.HttpURLConnection;
42  import java.net.InetAddress;
43  import java.net.MalformedURLException;
44  import java.net.URL;
45  import java.net.URLEncoder;
46  import java.net.UnknownHostException;
47  import java.nio.file.Files;
48  import java.nio.file.Path;
49  import java.nio.file.Paths;
50  import java.time.Duration;
51  import java.time.LocalTime;
52  import java.util.ArrayList;
53  import java.util.HashMap;
54  import java.util.LinkedList;
55  import java.util.List;
56  import java.util.Map;
57  import java.util.stream.Collectors;
58  import java.util.stream.Stream;
59  
60  abstract class SeleniumBase {
61  
62    private static final Logger LOG = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
63  
64    private static WebDriver chromeDriver;
65    private static List<String> serverUrls = new ArrayList<>();
66    private static Map<String, String> ignores = new HashMap<>();
67  
68    @BeforeAll
69    static void setUp() {
70      ignores.put(":8083/tobago-example-demo-myfaces-2.3",
71          "MyFaces 2.3 don't work with Tomcat 8.5 and openjdk10");
72      ignores.put("tobago-example-demo-mojarra-2.0",
73          "Ajax events don't work with Mojarra 2.0: https://issues.apache.org/jira/browse/TOBAGO-1589");
74      ignores.put("tobago-example-demo-mojarra-2.3",
75          "Currently Tobago demo don't run with Mojarra 2.3 on Tomcat 8.5");
76  
77      ignores.put("content/40-test/6000-event/event.xhtml",
78          "Focus/blur event can only be fired if the browser window is in foreground."
79              + " This cannot be guaranteed in selenium tests."
80              + " event.test.js contain focus/blur events");
81  
82      final String tobago1910 = "TreeSelect: Single selection nodes are not deselected correctly with mojarra: "
83          + "https://issues.apache.org/jira/browse/TOBAGO-1910";
84      ignores.put("tobago-example-demo-mojarra-2.1/content/20-component/090-tree/01-select/tree-select.xhtml",
85          tobago1910);
86      ignores.put("tobago-example-demo-mojarra-2.2/content/20-component/090-tree/01-select/tree-select.xhtml",
87          tobago1910);
88    }
89  
90    @AfterAll
91    static void tearDown() {
92      if (chromeDriver != null) {
93        chromeDriver.quit();
94      }
95    }
96  
97    enum Browser {
98      chrome
99      //, firefox // TODO implement firefox
100   }
101 
102   static List<String> getServerUrls() throws UnknownHostException, MalformedURLException {
103     if (serverUrls.size() <= 0) {
104       final String hostAddress = InetAddress.getLocalHost().getHostAddress();
105 
106       List<String> ports = new ArrayList<>();
107       ports.add("8082"); // Tomcat JRE 8
108       ports.add("8083"); // Tomcat JRE 10
109 
110       List<String> contextPaths = new ArrayList<>();
111       contextPaths.add("tobago-example-demo"); // MyFaces 2.0
112       contextPaths.add("tobago-example-demo-myfaces-2.1");
113       contextPaths.add("tobago-example-demo-myfaces-2.2");
114       contextPaths.add("tobago-example-demo-myfaces-2.3");
115       contextPaths.add("tobago-example-demo-mojarra-2.0");
116       contextPaths.add("tobago-example-demo-mojarra-2.1");
117       contextPaths.add("tobago-example-demo-mojarra-2.2");
118       contextPaths.add("tobago-example-demo-mojarra-2.3");
119 
120       for (String port : ports) {
121         for (String contextPath : contextPaths) {
122           String url = "http://" + hostAddress + ":" + port + "/" + contextPath;
123           final int status = getStatus(url);
124           if (status == 200) {
125             serverUrls.add(url);
126           } else {
127             LOG.warn("\n⚠️ IGNORED: Tests for " + url + ":\n Server status: " + status);
128           }
129         }
130       }
131     }
132 
133     return serverUrls;
134   }
135 
136   private static int getStatus(String url) throws MalformedURLException {
137     URL siteURL = new URL(url);
138     try {
139       HttpURLConnection connection = (HttpURLConnection) siteURL.openConnection();
140       connection.setRequestMethod("GET");
141       connection.connect();
142       return connection.getResponseCode();
143     } catch (IOException e) {
144       return -1;
145     }
146   }
147 
148   static List<String> getStandardTestPaths() throws IOException {
149     return Files.walk(Paths.get("src/main/webapp/content/"))
150         .filter(Files::isRegularFile)
151         .map(Path::toString)
152         .filter(s -> s.endsWith(".test.js"))
153         .map(s -> s.substring("src/main/webapp/".length()))
154         .sorted()
155         .map(s -> s.substring(0, s.length() - 8) + ".xhtml")
156         .collect(Collectors.toList());
157   }
158 
159   boolean isIgnored(final String serverUrl, final String path) {
160     final String url = serverUrl + "/" + path;
161     for (String key : ignores.keySet()) {
162       if (url.contains(key)) {
163         return true;
164       }
165     }
166     return false;
167   }
168 
169   void logIgnoreMessage(final String serverUrl, final String path) {
170     final String url = serverUrl + "/" + path;
171     for (final Map.Entry<String, String> ignore : ignores.entrySet()) {
172       if (url.contains(ignore.getKey())) {
173         final String message = ignore.getValue();
174         LOG.info("\n⚠️ IGNORED: Test for " + url + ":\n" + message);
175         return;
176       }
177     }
178   }
179 
180   void setupWebDriver(final Browser browser, final String serverUrl, final String path, final boolean accessTest)
181       throws MalformedURLException, UnsupportedEncodingException {
182     if (Browser.chrome.equals(browser)
183         && (chromeDriver == null) || ((RemoteWebDriver) chromeDriver).getSessionId() == null) {
184       chromeDriver = new RemoteWebDriver(new URL("http://localhost:4444/wd/hub"), new ChromeOptions());
185     }
186 
187     final String base = path.substring(0, path.length() - 6);
188     final String url = serverUrl + "/test.xhtml?base="
189         + URLEncoder.encode(base, "UTF-8") + (accessTest ? "&accessTest=true" : "");
190     getWebDriver(browser).get(url);
191   }
192 
193   WebDriver getWebDriver(Browser browser) {
194     if (Browser.chrome.equals(browser)) {
195       return chromeDriver;
196     } else {
197       return null;
198     }
199   }
200 
201   static Stream<Arguments> getArguments(final List<String> paths) throws MalformedURLException, UnknownHostException {
202     final LocalTime startTime = LocalTime.now();
203     final int testSize = Browser.values().length * getServerUrls().size() * paths.size();
204 
205     List<Arguments> arguments = new LinkedList<>();
206 
207     int testNo = 1;
208     for (String serverUrl : getServerUrls()) {
209       for (String path : paths) {
210         for (Browser browser : Browser.values()) {
211           arguments.add(Arguments.of(browser, serverUrl, path, startTime, testSize, testNo));
212           testNo++;
213         }
214       }
215     }
216 
217     return arguments.stream();
218   }
219 
220   String getTimeLeft(final LocalTime startTime, final int testSize, final int testNo) {
221     final LocalTime now = LocalTime.now();
222     final Duration completeWaitTime = Duration.between(startTime, now).dividedBy(testNo).multipliedBy(testSize);
223     final LocalTime endTime = LocalTime.from(startTime).plus(completeWaitTime);
224     final Duration timeLeft = Duration.between(LocalTime.now(), endTime);
225 
226     if (timeLeft.toHours() > 0) {
227       return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "H'h' m'm' s's'");
228     } else if (timeLeft.toMinutes() > 0) {
229       return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "m'm' s's'");
230     } else if (timeLeft.toMillis() >= 0) {
231       return DurationFormatUtils.formatDuration(timeLeft.toMillis(), "s's'");
232     } else {
233       return "---";
234     }
235   }
236 
237   /**
238    * Wait for the qunit-banner web element and return it.
239    * If the web element is available, the execution of qunit test should be done and it is safe to parse the results.
240    *
241    * @return qunit-banner web element
242    */
243   WebElement waitForQUnitBanner(final WebDriver webDriver) {
244     final FluentWait<WebDriver> fluentWait = new FluentWait<>(webDriver)
245         .withTimeout(Duration.ofSeconds(90))
246         .pollingEvery(Duration.ofSeconds(1))
247         .ignoring(NoSuchElementException.class);
248 
249     WebElement qunitBanner = fluentWait.until(driver -> driver.findElement(By.id("qunit-banner")));
250     fluentWait.until(ExpectedConditions.attributeToBeNotEmpty(qunitBanner, "class"));
251 
252     return qunitBanner;
253   }
254 
255   void parseQUnitResults(final Browser browser, final String serverUrl, final String path) {
256     final WebDriver webDriver = getWebDriver(browser);
257     WebElement qunitBanner;
258     try {
259       qunitBanner = waitForQUnitBanner(webDriver);
260     } catch (Exception e) {
261       qunitBanner = webDriver.findElement(By.id("qunit-banner"));
262     }
263 
264     WebElement qunitTestResult = webDriver.findElement(By.id("qunit-testresult"));
265     WebElement qunitTests = webDriver.findElement(By.id("qunit-tests"));
266 
267     final List<WebElement> testCases = qunitTests.findElements(By.xpath("li"));
268     Assertions.assertTrue(testCases.size() > 0, "There must be at least one test case.");
269 
270     final boolean testFailed = !qunitBanner.getAttribute("class").equals("qunit-pass");
271 
272     int testCaseCount = 1;
273     final StringBuilder stringBuilder = new StringBuilder();
274     stringBuilder.append(qunitTestResult.getAttribute("textContent"));
275     stringBuilder.append("\n");
276 
277     if (testFailed) {
278       for (final WebElement testCase : testCases) {
279         final String testName = getText(testCase, "test-name");
280         final String testStatus = testCase.getAttribute("class").toUpperCase();
281 
282         stringBuilder.append(testCaseCount++);
283         stringBuilder.append(". ");
284         stringBuilder.append(testStatus);
285         stringBuilder.append(": ");
286         stringBuilder.append(testName);
287         stringBuilder.append(" (");
288         stringBuilder.append(getText(testCase, "runtime"));
289         stringBuilder.append(")\n");
290 
291         final WebElement assertList = testCase.findElement(By.className("qunit-assert-list"));
292         final List<WebElement> asserts = assertList.findElements(By.tagName("li"));
293         int assertCount = 1;
294         for (final WebElement assertion : asserts) {
295           final String assertStatus = assertion.getAttribute("class");
296 
297           stringBuilder.append("- ");
298           if (assertCount <= 9) {
299             stringBuilder.append("0");
300           }
301           stringBuilder.append(assertCount++);
302           stringBuilder.append(". ");
303           stringBuilder.append(assertStatus);
304           stringBuilder.append(": ");
305           stringBuilder.append(getText(assertion, "test-message"));
306           stringBuilder.append(getText(assertion, "runtime"));
307           stringBuilder.append("\n");
308 
309           final String assertExpected = getText(assertion, "test-expected");
310           if (!"null".equals(assertExpected)) {
311             stringBuilder.append("-- ");
312             stringBuilder.append(assertExpected);
313             stringBuilder.append("\n");
314           }
315           final String assertResult = getText(assertion, "test-actual");
316           if (!"null".equals(assertResult)) {
317             stringBuilder.append("-- ");
318             stringBuilder.append(assertResult);
319             stringBuilder.append("\n");
320           }
321           final String assertSource = getText(assertion, "test-source");
322           if (!"null".equals(assertSource)) {
323             stringBuilder.append("-- ");
324             stringBuilder.append(assertSource);
325             stringBuilder.append("\n");
326           }
327         }
328 
329         stringBuilder.append(getText(testCase, "qunit-source"));
330         stringBuilder.append("\n\n");
331       }
332     }
333 
334     final String url = serverUrl + "/" + path;
335     if (testFailed) {
336       final String message = "\n❌ FAILED: Test with '" + browser + "' for " + url + "\n" + stringBuilder.toString();
337       LOG.warn(message);
338       Assertions.fail(message);
339     } else {
340       final String message = "\n✅ PASSED: Test with '" + browser + "' for " + url + "\n" + stringBuilder.toString();
341       LOG.info(message);
342       Assertions.assertTrue(true, message);
343     }
344   }
345 
346   private String getText(final WebElement webElement, final String className) {
347     final List<WebElement> elements = webElement.findElements(By.className(className));
348     if (elements.size() > 0) {
349       return elements.get(0).getAttribute("textContent");
350     } else {
351       return "null";
352     }
353   }
354 }