View Javadoc

1   /*
2    * Copyright 2007 united internet (unitedinternet.com) Robert Zimmermann
3    *
4    *  Licensed under the Apache License, Version 2.0 (the "License");
5    *  you may not use this file except in compliance with the License.
6    *  You may obtain a copy of the License at
7    *
8    *      http://www.apache.org/licenses/LICENSE-2.0
9    *
10   *  Unless required by applicable law or agreed to in writing, software
11   *  distributed under the License is distributed on an "AS IS" BASIS,
12   *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   *  See the License for the specific language governing permissions and
14   *  limitations under the License.
15   *
16   */
17  package com.unitedinternet.portal.selenium.utils.logging;
18  
19  import java.io.File;
20  import java.io.IOException;
21  import java.io.Writer;
22  import java.text.MessageFormat;
23  import java.text.SimpleDateFormat;
24  import java.util.Date;
25  
26  import org.apache.commons.lang.ArrayUtils;
27  import org.apache.commons.lang.StringUtils;
28  
29  /**
30   * Formats all logging events as HTML.
31   * Trying to bring the Selenium TestRunner result look-and-feel to Junit.
32   *
33   * Default formatter for logging. Be sure to pass an encoding-aware writer
34   * together with encoding name to the 2 parameters constuctor.
35   *
36   * @author Robert Zimmermann
37   *
38   * $Id: HtmlResultFormatter.java 135 2009-01-15 20:37:36Z bobbyde $
39   */
40  public class HtmlResultFormatter implements LoggingResultsFormatter {
41      String resultFileEncoding = "ISO-8859-1";
42  
43      static final int HTML_MAX_COLUMNS = 7;
44  
45      static final int SCREENSHOT_PREVIEW_HEIGHT = 200;
46  
47      static final int SCREENSHOT_PREVIEW_WIDHT = 200;
48  
49      static final String URL_PATH_SEPARATOR = "/";
50  
51      static final String CSS_CLASS_FAILED = "status_failed";
52  
53      static final String CSS_CLASS_PASSED = "status_passed";
54  
55      static final String CSS_CLASS_UNKNOWN = "status_maybefailed";
56  
57      static final String CSS_CLASS_DONE = "status_done";
58  
59      static final String CSS_CLASS_TITLE = "title";
60  
61      static final String TOOL_TIPP_MESSAGE_TIME_DELTA = "time delta reporting is alpha and subject to change";
62  
63      static final SimpleDateFormat LOGGING_DATETIME_FORMAT = new SimpleDateFormat("HH:mm:ss dd-MM-yyyy");
64  
65      static SimpleDateFormat FILENAME_DATETIME_FORMAT = new SimpleDateFormat("yyyy-MM-dd_HH-mm");
66  
67      // design for testability: for easier mocking of eg windows paths on unix systems
68      String localFsPathSeparator = File.separator;
69  
70      String screenShotBaseUri = "";
71  
72      String automaticScreenshotPath = ".";
73  
74      Writer resultsWriter = null;
75  
76      // customized copy from org.openqa.selenium.server.htmlrunner.HTMLTestResults
77      static final String HTML_HEADER = "<html>\n"
78              + "<head>"
79              + "<meta content=\"text/html; charset={0}\" http-equiv=\"content-type\">"
80              + "<meta content=\"cache-control\" http-equiv=\"no-cache\">"
81              + "<meta content=\"pragma\" http-equiv=\"no-cache\">"
82              + "<style type=\"text/css\">\n"
83              + "body, table '{'\n"
84              + "    font-family: Verdana, Arial, sans-serif;\n"
85              + "    font-size: 12;\n"
86              + "'}'\n"
87              + "\n"
88              + "table '{'\n"
89              + "    border-collapse: collapse;\n"
90              + "    border: 1px solid #ccc;\n"
91              + "'}'\n"
92              + "\n"
93              + "th, td '{'\n"
94              + "    padding-left: 0.3em;\n"
95              + "    padding-right: 0.3em;\n"
96              + "'}'\n"
97              + "\n"
98              + "a '{'\n"
99              + "    text-decoration: none;\n"
100             + "'}'\n"
101             + "\n"
102             + "."
103             + CSS_CLASS_TITLE
104             + " '{'\n"
105             + "    font-style: italic;\n"
106             + "'}'\n"
107             + "\n"
108             + ".selected '{'\n"
109             + "    background-color: #ffffcc;\n"
110             + "'}'\n"
111             + "\n"
112             + "."
113             + CSS_CLASS_DONE
114             + " '{'\n"
115             + "    background-color: #eeffee;\n"
116             + "'}'\n"
117             + "\n"
118             + "."
119             + CSS_CLASS_PASSED
120             + " '{'\n"
121             + "    background-color: #ccffcc;\n"
122             + "'}'\n"
123             + "\n"
124             + "."
125             + CSS_CLASS_FAILED
126             + " '{'\n"
127             + "    background-color: #ffcccc;\n"
128             + "'}'\n"
129             + "\n"
130             + "."
131             + CSS_CLASS_UNKNOWN
132             + " '{'\n"
133             + "    background-color: #ffffcc;\n"
134             + "'}'\n"
135             + "\n"
136             + ".breakpoint '{'\n"
137             + "    background-color: #cccccc;\n"
138             + "    border: 1px solid black;\n"
139             + "'}'\n"
140             + "</style>\n"
141             + "<title>Test results</title></head>\n"
142             + "<body>\n"
143             + " <span style=\"font-size:9px;font-family:arial,verdana,sans-serif;\">"
144             + "HTML Logging of Junit driven Selenium-RC Tests. Contributed by Robert Zimmermann (unitedinternet.com)"
145             + "</span>"
146             + "<h1>Test results </h1>";
147 
148     static final String HTML_TABLE_HEADER = "<tr>"
149             + "<td><b>Selenium-Command</b></td>"
150             + "<td><b>Parameter-1</b></td>"
151             + "<td><b>Parameter-2</b></td>"
152             + "<td><b>Res.RC</b></td>"
153             + "<td><b>Res.Selenium</b></td>"
154             + "<td><b>Time [ms]</b></td>"
155             + "<td><b>Calling-Class with Linenumber</b></td>"
156             + "</tr>\n";
157 
158     static final String HTML_METRICS = "<table>\n"
159             + "<tr><td>user-agent:</td><td>{0}</td></tr>\n"
160             + "<tr><td>selenium-rc:</td><td> v{1} [{2}]</td></tr>\n"
161             + "<tr><td>selenium-core:</td><td> v{3} [{4}]</td></tr>\n"
162             + "<tr><td>LoggingSelenium:</td><td> revision [{5}]</td></tr>\n"
163             + "<tr><td>test-started:</td><td>{6}</td></tr>\n"
164             + "<tr><td>test-finished:</td><td>{7}</td></tr>\n"
165             + "<tr><td>test-duration [ms]:</td><td>{8}</td></tr>\n"
166             + "<tr><td>commands processed:</td><td>{9}</td></tr>\n"
167             + "<tr><td>verifications processed:</td><td>{10}</td></tr>\n"
168             + "{11}\n"
169             + "<tr><td>commands not logged:</td><td>{12}</td></tr>\n"
170             + "</table>\n";
171 
172     static final String HTML_COMMENT = "<tr class=\"{0}\"><td colspan=\"{1}\">{2}</td></tr>\n";
173 
174     static final String HTML_FOOTER = "</tbody></table></body></html>";
175 
176     static final String HTML_SPECIAL = "<span style=\"font-size:9px;font-family:arial,verdana,sans-serif;\">" + "{0}</span>";
177 
178     static final String HTML_SCREENSHOT_ROW = "<tr class=\"{0}\">"
179             + "<td colspan=\"{1}\" valign=\"center\" align=\"center\" halign=\"center\">{2}</td>"
180             + "<td>{3}</td>"
181             + "<td>{4}</td>"
182             + "</tr>\n";
183 
184     static final String HTML_SCREENSHOT_IMG = "<a href=\"{0}\">"
185             + "<img src=\"{1}\" width=\"{2}\" height=\"{3}\""
186             + " alt=\"Selenium Screenshot\""
187             + " title=\"Selenium Screenshot\"/>"
188             + "<br/>{4}</a>";
189 
190     static final String HTML_EMPTY_COLUMN = "<td>&nbsp;</td>";
191 
192     /**
193      * Write results to the specified writer.
194      * Note: encoding ISO-8859-1 is assumed
195      *
196      * It is recommended to use the 2-parameter constructor.
197      *
198      * @param myResultsWriter where results will be written in "ISO-8859-1" encoding
199      */
200     public HtmlResultFormatter(Writer myResultsWriter) {
201         this.resultsWriter = myResultsWriter;
202     }
203 
204     /**
205      * Write results with an arbitrary encoding.
206      * Be sure to create the writer with the correct encoding
207      * Note: resultFileEncoding is only used to set a corresponding meta-tag in the resulting HTML-file
208      *
209      * For Example:
210      * <code>new BufferedWriter(new OutputStreamWriter(new FileOutputStream("myResultFile.html"),
211      *              "UTF-8")</code>
212      *
213      * @param myResultsWriter writer with resultFileEncoding set. See also Example above
214      * @param myResultFileEncoding any encoding supported by the running jvm
215      */
216     public HtmlResultFormatter(Writer myResultsWriter, String myResultFileEncoding) {
217         this.resultsWriter = myResultsWriter;
218         this.resultFileEncoding = myResultFileEncoding;
219     }
220 
221     /**
222      * {@inheritDoc}
223      */
224     public void commentLogEvent(LoggingBean loggingBean) {
225         String[] loggingBeanArgs = LoggingUtils.getCorrectedArgsArray(loggingBean, 2, "");
226         String commentToBeLogged = loggingBeanArgs[0];
227         String additionalInformation = loggingBeanArgs[1];
228         logToWriter(MessageFormat.format(HTML_COMMENT, CSS_CLASS_TITLE, HTML_MAX_COLUMNS, commentToBeLogged
229                 + extraInformationLogEvent(additionalInformation)));
230     }
231 
232     String formatMetrics(TestMetricsBean metrics) {
233         String failedCommandsRow = "";
234         if (metrics.getFailedCommands() > 0) {
235             failedCommandsRow = "<tr class=\""
236                     + CSS_CLASS_FAILED
237                     + "\"><td>failed commands:</td><td>"
238                     + metrics.getFailedCommands()
239                     + "</td></tr>\n";
240             if (StringUtils.isNotBlank(metrics.getLastFailedCommandMessage())) {
241                 failedCommandsRow = failedCommandsRow
242                         + "<tr class=\""
243                         + CSS_CLASS_FAILED
244                         + "\"><td>last failed message:</td><td>"
245                         + metrics.getLastFailedCommandMessage()
246                         + "</td></tr>\n";
247             } else {
248                 System.err.println("WARNING: NO LastFailedCommandMessage");
249             }
250         }
251         return MessageFormat.format(HTML_METRICS, metrics.getUserAgent(), metrics.getSeleniumRcVersion(), metrics
252                 .getSeleniumRcRevision(), metrics.getSeleniumCoreVersion(), metrics.getSeleniumCoreRevision(), metrics
253                 .getLoggingSeleniumRevision(), LOGGING_DATETIME_FORMAT.format(metrics.getStartTimeStamp()),
254                 LOGGING_DATETIME_FORMAT.format(metrics.getEndTimeStamp()), metrics.getTestDuration(), metrics
255                         .getCommandsProcessed(), metrics.getVerificationsProcessed(), failedCommandsRow, ArrayUtils
256                         .toString(metrics.getCommandsExcludedFromLogging()));
257     }
258 
259     /**
260      * {@inheritDoc}
261      */
262     public void headerLogEvent(TestMetricsBean metrics) {
263         logToWriter(formatHeader(metrics));
264     }
265 
266     String formatHeader(TestMetricsBean metrics) {
267         String header = MessageFormat.format(HTML_HEADER, resultFileEncoding)
268                 + "\n"
269                 + formatMetrics(metrics)
270                 + "<table border=\"1\"><tbody>"
271                 + HTML_TABLE_HEADER;
272         return header;
273     }
274 
275     /**
276      * {@inheritDoc}
277      */
278     public void footerLogEvent() {
279         logToWriter(HTML_FOOTER);
280     }
281 
282     String extraInformationLogEvent(String extraInformation) {
283         String result = "";
284         if (null != extraInformation) {
285             result = MessageFormat.format(HTML_SPECIAL, extraInformation);
286         }
287         return result;
288     }
289 
290     /**
291      * {@inheritDoc}
292      */
293     public void commandLogEvent(LoggingBean loggingBean) {
294         if (!loggingBean.isExcludeFromLogging()) {
295             String resultClass = loggingBean.isCommandSuccessful() ? CSS_CLASS_DONE : CSS_CLASS_FAILED;
296             if ("captureScreenshot".equals(loggingBean.getCommandName())) {
297                 logToWriter(formatScreenshot(loggingBean, resultClass));
298             } else {
299                 logToWriter(formatCommandAsHtml(loggingBean, resultClass, ""));
300             }
301         }
302     }
303 
304     /**
305      * {@inheritDoc}
306      */
307     public void booleanCommandLogEvent(LoggingBean loggingBean) {
308         String toolTippMessage = "";
309         String resultClass = "";
310         if (loggingBean.isCommandSuccessful()) {
311             resultClass = CSS_CLASS_PASSED;
312         } else {
313             resultClass = CSS_CLASS_UNKNOWN;
314             toolTippMessage = "How this &quot;false&quot; result from Selenium"
315                     + " is treated by the test cannot be determined here.";
316         }
317         logToWriter(formatCommandAsHtml(loggingBean, resultClass, toolTippMessage));
318     }
319 
320     /**
321      * {@inheritDoc}
322      */
323     public String getScreenShotBaseUri() {
324         return screenShotBaseUri;
325     }
326 
327     /**
328      * {@inheritDoc}
329      */
330     public void setScreenShotBaseUri(String screenShotBaseUri) {
331         this.screenShotBaseUri = screenShotBaseUri == null ? "" : screenShotBaseUri;
332     }
333 
334     /**
335      * {@inheritDoc}
336      */
337     public String generateFilenameForAutomaticScreenshot(String baseName) {
338         final String constWaitTimeoutScreenshotFileName = "automatic" + baseName + "Screenshot" + timeStampForFileName() + ".png";
339         return this.automaticScreenshotPath + localFsPathSeparator + constWaitTimeoutScreenshotFileName;
340 
341     }
342 
343     /**
344      * {@inheritDoc}
345      */
346     public String getAutomaticScreenshotPath() {
347         return this.automaticScreenshotPath;
348     }
349 
350     /**
351      * Automatic screenshots are taken if a Wait-timeout is detected.
352      * Default location is current working dir (".") If another
353      * (filesystem-)location is desired use this setter.
354      *
355      * {@inheritDoc}
356      */
357     public void setAutomaticScreenshotPath(String automaticScreenshotPath) {
358         this.automaticScreenshotPath = new File(automaticScreenshotPath).getAbsolutePath();
359     }
360 
361     String formatScreenshot(LoggingBean loggingBean, String resultClass) {
362         // if screenshot could not be written there should be something like a SeleniumException
363         return MessageFormat.format(HTML_SCREENSHOT_ROW, resultClass, HTML_MAX_COLUMNS - 2,
364                 formatScreenshotFileImgTag(loggingBean.getArgs()[0]), +loggingBean.getDeltaMillis(), loggingBean
365                         .getCallingClass());
366     }
367 
368     /**
369      * Format img HTML tag.
370      * Link has to be relative on any system
371      *
372      * @param absFsPathToScreenshot Absolute path to saved screenshot on the local filesystem
373      * @return formatted HTML img tag
374      */
375     String formatScreenshotFileImgTag(String absFsPathToScreenshot) {
376         String screenshotRelativeUrl;
377         String screenshotPathNormalized = absFsPathToScreenshot.replace(localFsPathSeparator, URL_PATH_SEPARATOR);
378         String screenShotName = screenshotPathNormalized.substring(screenshotPathNormalized.lastIndexOf(URL_PATH_SEPARATOR)
379                 + URL_PATH_SEPARATOR.length());
380 
381         if ("".equals(this.screenShotBaseUri)) {
382             screenshotRelativeUrl = screenShotName;
383         } else {
384             screenshotRelativeUrl = this.screenShotBaseUri.endsWith("/") ? this.screenShotBaseUri + screenShotName
385                     : this.screenShotBaseUri + URL_PATH_SEPARATOR + screenShotName;
386         }
387         return MessageFormat.format(HTML_SCREENSHOT_IMG, screenshotRelativeUrl, screenshotRelativeUrl, SCREENSHOT_PREVIEW_WIDHT,
388                 SCREENSHOT_PREVIEW_HEIGHT, screenShotName);
389     }
390 
391     String formatCommandAsHtml(LoggingBean loggingBean, String resultClass, String toolTippMessage) {
392         StringBuilder htmlWrappedCommand = new StringBuilder();
393         htmlWrappedCommand.append("<tr class=\""
394                 + resultClass
395                 + "\" title=\""
396                 + toolTippMessage
397                 + "\" alt=\""
398                 + toolTippMessage
399                 + "\">"
400                 + "<td>"
401                 + quoteHtml(loggingBean.getCommandName())
402                 + "</td>");
403         int writtenColumns = 0;
404         if (loggingBean.getArgs() != null) {
405             for (int i = 0; i < loggingBean.getArgs().length; i++) {
406                 writtenColumns++;
407                 htmlWrappedCommand.append("<td>" + quoteHtml(loggingBean.getArgs()[i]) + "</td>");
408             }
409         }
410         // put empty columns if parameters are missing
411         htmlWrappedCommand.append(generateEmptyColumns(HTML_MAX_COLUMNS - writtenColumns - (HTML_MAX_COLUMNS - 2)));
412 
413         htmlWrappedCommand.append("<td>"
414                 + quoteHtml(loggingBean.getSrcResult())
415                 + "</td><td>"
416                 + quoteHtml(loggingBean.getSelResult())
417                 + "</td><td title=\""
418                 + TOOL_TIPP_MESSAGE_TIME_DELTA
419                 + "\" alt=\""
420                 + TOOL_TIPP_MESSAGE_TIME_DELTA
421                 + "\">"
422                 + loggingBean.getDeltaMillis()
423                 + "</td><td>"
424                 + loggingBean.getCallingClass()
425                 + "</td></tr>\n");
426         return htmlWrappedCommand.toString();
427     }
428 
429     void logToWriter(final String formattedLogEvent) {
430         try {
431             this.resultsWriter.write(formattedLogEvent);
432         } catch (IOException e) {
433             throw new RuntimeException(e);
434         }
435     }
436 
437     /**
438      * Generate empty HTML columns.
439      *
440      * @param numColsToGenerate num of columns to be generated
441      * @return generated, empty columns in one string
442      */
443     public static final String generateEmptyColumns(final int numColsToGenerate) {
444         StringBuilder result = new StringBuilder();
445         for (int i = 0; i < numColsToGenerate; i++) {
446             result.append(HTML_EMPTY_COLUMN);
447         }
448         return result.toString();
449     }
450 
451     /**
452      * Generates a Date-Time String based on the current Time.
453      * To be used for and in filenames.
454      *
455      * TODO: maybe place this in LoggingUtils
456      *
457      * @return current date-time as string
458      */
459     public static final String timeStampForFileName() {
460         Date currentDateTime = new Date(System.currentTimeMillis());
461         return FILENAME_DATETIME_FORMAT.format(currentDateTime);
462     }
463 
464     /**
465      * {@inheritDoc}
466      */
467     public void methodLogEvent(LoggingBean loggingBean) {
468     }
469 
470     public static final String quoteHtml(String unquoted) {
471        String quoted = unquoted == null ? "" : unquoted;
472        quoted = quoted.replace("&", "&amp;");
473        quoted = quoted.replace("<", "&lt;");
474        quoted = quoted.replace(">", "&gt;");
475        return quoted;
476     }
477 
478     public static SimpleDateFormat getFILENAME_DATETIME_FORMAT() {
479         return FILENAME_DATETIME_FORMAT;
480     }
481 
482     public static void setFILENAME_DATETIME_FORMAT(SimpleDateFormat newFormat) {
483         FILENAME_DATETIME_FORMAT = newFormat;
484     }
485 }