View Javadoc
1   /*
2    * #%L
3    * wcm.io
4    * %%
5    * Copyright (C) 2014 - 2016 wcm.io
6    * %%
7    * Licensed under the Apache License, Version 2.0 (the "License");
8    * you may not use this file except in compliance with the License.
9    * You may obtain a copy of the License at
10   *
11   *      http://www.apache.org/licenses/LICENSE-2.0
12   *
13   * Unless required by applicable law or agreed to in writing, software
14   * distributed under the License is distributed on an "AS IS" BASIS,
15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   * See the License for the specific language governing permissions and
17   * limitations under the License.
18   * #L%
19   */
20  /* Copyright (c) wcm.io. All rights reserved. */
21  package io.wcm.qa.glnm.reporting;
22  
23  import java.io.File;
24  import java.io.FileInputStream;
25  import java.io.IOException;
26  import java.util.ArrayList;
27  import java.util.List;
28  import java.util.UUID;
29  import java.util.function.Consumer;
30  
31  import org.apache.commons.io.FileUtils;
32  import org.apache.commons.lang3.RandomStringUtils;
33  import org.apache.commons.lang3.StringUtils;
34  import org.openqa.selenium.OutputType;
35  import org.openqa.selenium.TakesScreenshot;
36  import org.openqa.selenium.WebDriver;
37  import org.slf4j.Logger;
38  import org.slf4j.LoggerFactory;
39  
40  import com.galenframework.config.GalenConfig;
41  import com.galenframework.config.GalenProperty;
42  import com.galenframework.reports.GalenTestInfo;
43  import com.galenframework.reports.HtmlReportBuilder;
44  import com.galenframework.utils.GalenUtils;
45  import com.google.common.html.HtmlEscapers;
46  
47  import io.qameta.allure.Allure;
48  import io.qameta.allure.AllureLifecycle;
49  import io.qameta.allure.model.Attachment;
50  import io.qameta.allure.model.Status;
51  import io.qameta.allure.model.StepResult;
52  import io.wcm.qa.glnm.configuration.GaleniumConfiguration;
53  import io.wcm.qa.glnm.context.GaleniumContext;
54  import io.wcm.qa.glnm.exceptions.GaleniumException;
55  
56  /**
57   * Utility class containing methods handling reporting.
58   *
59   * @since 1.0.0
60   */
61  public final class GaleniumReportUtil {
62  
63    private static final List<GalenTestInfo> GLOBAL_GALEN_RESULTS = new ArrayList<GalenTestInfo>();
64  
65    private static final Logger LOG = LoggerFactory.getLogger(GaleniumReportUtil.class);
66    private static final String PATH_GALEN_REPORT = GaleniumConfiguration.getReportDirectory() + "/galen";
67    private static final String PATH_SCREENSHOTS_ROOT = GaleniumConfiguration.getReportDirectory() + "/screenshots";
68  
69    private GaleniumReportUtil() {
70      // do not instantiate
71    }
72  
73    /**
74     * Add GalenTestInfo to global list for generating reports.
75     *
76     * @param galenTestInfo Galen test info to add to result set
77     * @since 3.0.0
78     */
79    public static void addGalenResult(GalenTestInfo galenTestInfo) {
80      if (isAddResult(galenTestInfo)) {
81        GLOBAL_GALEN_RESULTS.add(galenTestInfo);
82      }
83    }
84  
85    /**
86     * <p>
87     * Adds image comparison result for use with Allure's screen-diff-plugin.
88     * </p>
89     *
90     * @param actual a {@link java.io.File} object.
91     * @param expected a {@link java.io.File} object.
92     * @param diff a {@link java.io.File} object.
93     */
94    public static void addImageComparisonResult(File actual, File expected, File diff) {
95      if (LOG.isDebugEnabled()) {
96        LOG.debug("attaching screendiff results.");
97      }
98      if (LOG.isTraceEnabled()) {
99        LOG.trace("actual: " + actual);
100       LOG.trace("expected: " + actual);
101       LOG.trace("diff: " + actual);
102     }
103 
104     // Add label [key='testType', value='screenshotDiff'] to testcase
105     addLabel("testType", "screenshotDiff");
106 
107     // Attach to testcase three screenshots:
108     //  * diff.png
109     addPngAttachment("diff", diff, true);
110     //  * actual.png
111     addPngAttachment("actual", actual, true);
112     //  * expected.png
113     addPngAttachment("expected", expected, true);
114 
115   }
116 
117   /**
118    * Add label to test case.
119    *
120    * @param key label key
121    * @param value label value
122    */
123   public static void addLabel(String key, String value) {
124     Allure.label(key, value);
125   }
126 
127   /**
128    * Add attachment with "image/png" and ".png".
129    *
130    * @param name base name
131    * @param file file to create input stream from
132    */
133   public static void addPngAttachment(String name, File file) {
134     addPngAttachment(name, file, false);
135   }
136 
137   /**
138    * Add attachment with "image/png" and ".png".
139    *
140    * @param name base name
141    * @param file file to create input stream from
142    * @param attachToTestCase whether to attach at test case level instead of inside step
143    */
144   public static void addPngAttachment(String name, File file, boolean attachToTestCase) {
145     FileInputStream inputStream;
146     try {
147       inputStream = new FileInputStream(file);
148       addAttachment(
149           name,
150           "image/png",
151           inputStream,
152           ".png", attachToTestCase);
153       inputStream.close();
154     }
155     catch (IOException ex) {
156       throw new GaleniumException("When adding PNG attachment from: " + file, ex);
157     }
158   }
159 
160   /**
161    * Write all test results to Galen report.
162    *
163    * @param testInfos list to persist test information
164    * @since 3.0.0
165    */
166   @SuppressWarnings("PMD.AvoidCatchingNPE")
167   public static void createGalenHtmlReport(List<GalenTestInfo> testInfos) {
168     try {
169       new HtmlReportBuilder().build(testInfos, PATH_GALEN_REPORT);
170     }
171     catch (IOException | NullPointerException ex) {
172       LOG.error("could not generate Galen report.", ex);
173     }
174   }
175 
176   /**
177    * Create reports from global list of GalenTestInfos.
178    *
179    * @since 3.0.0
180    */
181   public static void createGalenReports() {
182     createGalenHtmlReport(GLOBAL_GALEN_RESULTS);
183   }
184 
185   /**
186    * Uses {@link com.google.common.html.HtmlEscapers} to escape text for use in reporting.
187    *
188    * @param string potentially includes unescaped HTML
189    * @return best effort HTML escaping
190    * @since 3.0.0
191    */
192   public static String escapeHtml(String string) {
193     String escapedString = HtmlEscapers.htmlEscaper().escape(StringUtils.stripToEmpty(string));
194     return StringUtils.replace(escapedString, "\n", "</br>");
195   }
196 
197   /**
198    * <p>failStep.</p>
199    *
200    * @param step a {@link java.lang.String} object.
201    * @since 5.0.0
202    */
203   public static void failStep(String step) {
204     if (LOG.isTraceEnabled()) {
205       LOG.trace("fail step: " + step);
206     }
207     Allure.getLifecycle().updateStep(step, new FailStep());
208   }
209 
210   /**
211    * <p>passStep.</p>
212    *
213    * @param step a {@link java.lang.String} object.
214    * @since 5.0.0
215    */
216   public static void passStep(String step) {
217     if (LOG.isTraceEnabled()) {
218       LOG.trace("pass step: " + step);
219     }
220     Allure.getLifecycle().updateStep(step, new PassStep());
221   }
222 
223   /**
224    * <p>startStep.</p>
225    *
226    * @param name a {@link java.lang.String} object.
227    * @return a {@link java.lang.String} object.
228    * @since 5.0.0
229    */
230   public static String startStep(String name) {
231     if (LOG.isTraceEnabled()) {
232       LOG.trace("start step: " + name);
233     }
234     String uuid = UUID.randomUUID().toString();
235     StepResult result = new StepResult().setName(name);
236     Allure.getLifecycle().startStep(uuid, result);
237     return uuid;
238   }
239 
240   /**
241    * <p>startStep.</p>
242    *
243    * @param parentStep a {@link java.lang.String} object.
244    * @param stepName a {@link java.lang.String} object.
245    * @return a {@link java.lang.String} object.
246    * @since 5.0.0
247    */
248   public static String startStep(String parentStep, String stepName) {
249     if (LOG.isTraceEnabled()) {
250       LOG.trace("start step: " + stepName);
251     }
252     String uuid = UUID.randomUUID().toString();
253     StepResult result = new StepResult().setName(stepName);
254     Allure.getLifecycle().startStep(parentStep, uuid, result);
255     return uuid;
256   }
257 
258   /**
259    * Adds a step for current test case to the report.
260    *
261    * @param stepName to use in report
262    * @since 5.0.0
263    */
264   public static void step(String stepName) {
265     if (LOG.isInfoEnabled()) {
266       LOG.info("STEP(" + stepName + ")");
267     }
268     Allure.step(stepName);
269   }
270 
271   /**
272    * Adds a failed step for current test case to the report.
273    *
274    * @param stepName to use in report
275    * @since 5.0.0
276    */
277   public static void stepFailed(String stepName) {
278     if (LOG.isInfoEnabled()) {
279       LOG.info("FAILED_STEP(" + stepName + ")");
280     }
281     Allure.step(stepName, Status.FAILED);
282   }
283 
284   /**
285    * <p>stopStep.</p>
286    *
287    * @since 5.0.0
288    */
289   public static void stopStep() {
290     if (LOG.isTraceEnabled()) {
291       LOG.trace("stop step");
292     }
293     Allure.getLifecycle().stopStep();
294   }
295 
296   /**
297    * uses Galen functionality to get a full page screenshot by scrolling and
298    * stitching.
299    *
300    * @since 5.0.0
301    */
302   public static void takeFullScreenshot() {
303     String step = startStep("taking full page screenshot");
304     GalenConfig galenConfig = GalenConfig.getConfig();
305     boolean fullPageScreenshotActivatedInGalen = galenConfig.getBooleanProperty(GalenProperty.SCREENSHOT_FULLPAGE);
306     if (!fullPageScreenshotActivatedInGalen) {
307       if (LOG.isTraceEnabled()) {
308         LOG.trace("activate full page screenshot in Galen before screenshot");
309       }
310       galenConfig.setProperty(GalenProperty.SCREENSHOT_FULLPAGE, "true");
311     }
312     try {
313       File screenshotFile = GalenUtils.makeFullScreenshot(GaleniumContext.getDriver());
314       attachScreenshotFile(screenshotFile);
315       passStep(step);
316     }
317     catch (IOException | InterruptedException ex) {
318       LOG.error("Could not take full screenshot.", ex);
319     }
320     finally {
321       if (!fullPageScreenshotActivatedInGalen) {
322         if (LOG.isTraceEnabled()) {
323           LOG.trace("deactivate full page screenshot in Galen after screenshot");
324         }
325         galenConfig.setProperty(GalenProperty.SCREENSHOT_FULLPAGE, "false");
326       }
327       stopStep();
328     }
329   }
330 
331   /**
332    * Take screenshot of current browser window and add to reports. Uses random filename.
333    *
334    * @since 4.0.0
335    */
336   public static void takeScreenshot() {
337     String randomAlphanumeric = RandomStringUtils.randomAlphanumeric(12);
338     takeScreenshot(randomAlphanumeric, getTakesScreenshot());
339   }
340 
341   /**
342    * Take screenshot of current browser window and add to report in a dedicated step.
343    *
344    * @param resultName to use in filename
345    * @param takesScreenshot to take screenshot from
346    * @since 4.0.0
347    */
348   public static void takeScreenshot(String resultName, TakesScreenshot takesScreenshot) {
349     takeScreenshot(resultName, takesScreenshot, true);
350   }
351 
352   /**
353    * Take screenshot of current browser window and add to report in a dedicated step.
354    *
355    * @param resultName to use in filename
356    * @param takesScreenshot to take screenshot from
357    * @param dedicatedStep whether to create dedicated step for attaching screenshot
358    * @since 5.0.0
359    */
360   public static void takeScreenshot(String resultName, TakesScreenshot takesScreenshot, boolean dedicatedStep) {
361     String step = null;
362     if (dedicatedStep) {
363       step = startStep("taking screenshot: " + takesScreenshot);
364     }
365     File screenshotFile = takesScreenshot.getScreenshotAs(OutputType.FILE);
366     attachScreenshotFile(resultName, screenshotFile);
367     if (dedicatedStep) {
368       passStep(step);
369       stopStep();
370     }
371   }
372 
373   /**
374    * Captures image from screenshot capable instance which can be the driver or a single element in page.
375    *
376    * @param takesScreenshot to capture
377    * @since 4.0.0
378    */
379   public static void takeScreenshot(TakesScreenshot takesScreenshot) {
380     String randomAlphanumeric = RandomStringUtils.randomAlphanumeric(12);
381     takeScreenshot(randomAlphanumeric, takesScreenshot);
382   }
383 
384   /**
385    * <p>
386    * updateStep.
387    * </p>
388    *
389    * @param step step UUID
390    * @param update step result consumer to do the update
391    * @since 5.0.0
392    */
393   public static void updateStep(String step, Consumer<StepResult> update) {
394     Allure.getLifecycle().updateStep(step, update);
395   }
396 
397   /**
398    * Sets a new name for a step.
399    *
400    * @param step step UUID
401    * @param updatedStepName name to set on step
402    * @since 5.0.0
403    */
404   public static void updateStepName(String step, String updatedStepName) {
405     if (LOG.isTraceEnabled()) {
406       LOG.trace("update step name: " + step + " -> " + updatedStepName);
407     }
408     updateStep(step, new Consumer<StepResult>() {
409 
410       @Override
411       public void accept(StepResult result) {
412         result.setName(updatedStepName);
413       }
414     });
415   }
416 
417   private static void addAttachment(String name, String type, FileInputStream inputStream, String extension, boolean attachToTestCase) {
418     if (attachToTestCase) {
419       attachToTestCase(name, type, inputStream, extension);
420       return;
421     }
422     attachToCurrentStep(name, type, inputStream, extension);
423   }
424 
425   private static void attachScreenshotFile(File screenshotFile) {
426     attachScreenshotFile(screenshotFile.getName(), screenshotFile);
427   }
428 
429   private static void attachScreenshotFile(String resultName, File screenshotFile) {
430     if (screenshotFile != null) {
431       if (LOG.isTraceEnabled()) {
432         LOG.trace("screenshot taken: " + screenshotFile.getPath());
433       }
434       try {
435         String destFilename = System.currentTimeMillis() + "_" + resultName + ".png";
436         File destFile = new File(PATH_SCREENSHOTS_ROOT, destFilename);
437         FileUtils.copyFile(screenshotFile, destFile);
438         if (LOG.isTraceEnabled()) {
439           LOG.trace("copied screenshot: " + destFile.getPath());
440         }
441         addPngAttachment("Screenshot: " + resultName, destFile);
442         if (FileUtils.deleteQuietly(screenshotFile)) {
443           if (LOG.isTraceEnabled()) {
444             LOG.trace("deleted screenshot file: " + screenshotFile.getPath());
445           }
446         }
447         else if (LOG.isTraceEnabled()) {
448           LOG.trace("could not delete screenshot file: " + screenshotFile.getPath());
449         }
450       }
451       catch (IOException ex) {
452         LOG.error("Cannot copy screenshot.", ex);
453       }
454     }
455     else if (LOG.isDebugEnabled()) {
456       LOG.debug("screenshot file is null.");
457     }
458   }
459 
460   private static void attachToCurrentStep(String name, String type, FileInputStream inputStream, String extension) {
461     Allure.addAttachment(name, type, inputStream, extension);
462   }
463 
464   @SuppressWarnings("deprecation")
465   private static void attachToTestCase(String name, String type, FileInputStream inputStream, String extension) {
466     AllureLifecycle lifecycle = Allure.getLifecycle();
467     String source = lifecycle.prepareAttachment(name, type, extension);
468     lifecycle.writeAttachment(source, inputStream);
469     Attachment attachment = new Attachment();
470     attachment.setName(name);
471     attachment.setSource(source);
472     lifecycle.updateTestCase(result -> result.getAttachments().add(attachment));
473   }
474 
475   private static TakesScreenshot getTakesScreenshot() {
476     WebDriver driver = GaleniumContext.getDriver();
477     TakesScreenshot takesScreenshot = getTakesScreenshot(driver);
478     return takesScreenshot;
479   }
480 
481   private static TakesScreenshot getTakesScreenshot(WebDriver driver) {
482     TakesScreenshot takesScreenshot;
483     if (driver instanceof TakesScreenshot) {
484       takesScreenshot = (TakesScreenshot)driver;
485     }
486     else {
487       throw new GaleniumException("driver cannot take screenshot");
488     }
489     return takesScreenshot;
490   }
491 
492   private static boolean isAddResult(GalenTestInfo galenTestInfo) {
493     if (galenTestInfo == null) {
494       return false;
495     }
496     if (GaleniumConfiguration.isOnlyReportGalenErrors()) {
497       if ((!galenTestInfo.isFailed()) && (galenTestInfo.getReport().fetchStatistic().getWarnings() == 0)) {
498         return false;
499       }
500     }
501     return true;
502   }
503 
504   private static final class FailStep implements Consumer<StepResult> {
505 
506     @Override
507     public void accept(StepResult t) {
508       t.setStatus(Status.FAILED);
509     }
510   }
511 
512   private static final class PassStep implements Consumer<StepResult> {
513 
514     @SuppressWarnings("deprecation")
515     @Override
516     public void accept(StepResult t) {
517       if (t.getStatus() == null) {
518         t.setStatus(Status.PASSED);
519       }
520     }
521   }
522 }