GaleniumReportUtil.java

/*
 * #%L
 * wcm.io
 * %%
 * Copyright (C) 2014 - 2016 wcm.io
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */
/* Copyright (c) wcm.io. All rights reserved. */
package io.wcm.qa.glnm.reporting;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.function.Consumer;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.galenframework.config.GalenConfig;
import com.galenframework.config.GalenProperty;
import com.galenframework.reports.GalenTestInfo;
import com.galenframework.reports.HtmlReportBuilder;
import com.galenframework.utils.GalenUtils;
import com.google.common.html.HtmlEscapers;

import io.qameta.allure.Allure;
import io.qameta.allure.AllureLifecycle;
import io.qameta.allure.model.Attachment;
import io.qameta.allure.model.Status;
import io.qameta.allure.model.StepResult;
import io.wcm.qa.glnm.configuration.GaleniumConfiguration;
import io.wcm.qa.glnm.context.GaleniumContext;
import io.wcm.qa.glnm.exceptions.GaleniumException;

/**
 * Utility class containing methods handling reporting.
 *
 * @since 1.0.0
 */
public final class GaleniumReportUtil {

  private static final List<GalenTestInfo> GLOBAL_GALEN_RESULTS = new ArrayList<GalenTestInfo>();

  private static final Logger LOG = LoggerFactory.getLogger(GaleniumReportUtil.class);
  private static final String PATH_GALEN_REPORT = GaleniumConfiguration.getReportDirectory() + "/galen";
  private static final String PATH_SCREENSHOTS_ROOT = GaleniumConfiguration.getReportDirectory() + "/screenshots";

  private GaleniumReportUtil() {
    // do not instantiate
  }

  /**
   * Add GalenTestInfo to global list for generating reports.
   *
   * @param galenTestInfo Galen test info to add to result set
   * @since 3.0.0
   */
  public static void addGalenResult(GalenTestInfo galenTestInfo) {
    if (isAddResult(galenTestInfo)) {
      GLOBAL_GALEN_RESULTS.add(galenTestInfo);
    }
  }

  /**
   * <p>
   * Adds image comparison result for use with Allure's screen-diff-plugin.
   * </p>
   *
   * @param actual a {@link java.io.File} object.
   * @param expected a {@link java.io.File} object.
   * @param diff a {@link java.io.File} object.
   */
  public static void addImageComparisonResult(File actual, File expected, File diff) {
    if (LOG.isDebugEnabled()) {
      LOG.debug("attaching screendiff results.");
    }
    if (LOG.isTraceEnabled()) {
      LOG.trace("actual: " + actual);
      LOG.trace("expected: " + actual);
      LOG.trace("diff: " + actual);
    }

    // Add label [key='testType', value='screenshotDiff'] to testcase
    addLabel("testType", "screenshotDiff");

    // Attach to testcase three screenshots:
    //  * diff.png
    addPngAttachment("diff", diff, true);
    //  * actual.png
    addPngAttachment("actual", actual, true);
    //  * expected.png
    addPngAttachment("expected", expected, true);

  }

  /**
   * Add label to test case.
   *
   * @param key label key
   * @param value label value
   */
  public static void addLabel(String key, String value) {
    Allure.label(key, value);
  }

  /**
   * Add attachment with "image/png" and ".png".
   *
   * @param name base name
   * @param file file to create input stream from
   */
  public static void addPngAttachment(String name, File file) {
    addPngAttachment(name, file, false);
  }

  /**
   * Add attachment with "image/png" and ".png".
   *
   * @param name base name
   * @param file file to create input stream from
   * @param attachToTestCase whether to attach at test case level instead of inside step
   */
  public static void addPngAttachment(String name, File file, boolean attachToTestCase) {
    FileInputStream inputStream;
    try {
      inputStream = new FileInputStream(file);
      addAttachment(
          name,
          "image/png",
          inputStream,
          ".png", attachToTestCase);
      inputStream.close();
    }
    catch (IOException ex) {
      throw new GaleniumException("When adding PNG attachment from: " + file, ex);
    }
  }

  /**
   * Write all test results to Galen report.
   *
   * @param testInfos list to persist test information
   * @since 3.0.0
   */
  @SuppressWarnings("PMD.AvoidCatchingNPE")
  public static void createGalenHtmlReport(List<GalenTestInfo> testInfos) {
    try {
      new HtmlReportBuilder().build(testInfos, PATH_GALEN_REPORT);
    }
    catch (IOException | NullPointerException ex) {
      LOG.error("could not generate Galen report.", ex);
    }
  }

  /**
   * Create reports from global list of GalenTestInfos.
   *
   * @since 3.0.0
   */
  public static void createGalenReports() {
    createGalenHtmlReport(GLOBAL_GALEN_RESULTS);
  }

  /**
   * Uses {@link com.google.common.html.HtmlEscapers} to escape text for use in reporting.
   *
   * @param string potentially includes unescaped HTML
   * @return best effort HTML escaping
   * @since 3.0.0
   */
  public static String escapeHtml(String string) {
    String escapedString = HtmlEscapers.htmlEscaper().escape(StringUtils.stripToEmpty(string));
    return StringUtils.replace(escapedString, "\n", "</br>");
  }

  /**
   * <p>failStep.</p>
   *
   * @param step a {@link java.lang.String} object.
   * @since 5.0.0
   */
  public static void failStep(String step) {
    if (LOG.isTraceEnabled()) {
      LOG.trace("fail step: " + step);
    }
    Allure.getLifecycle().updateStep(step, new FailStep());
  }

  /**
   * <p>passStep.</p>
   *
   * @param step a {@link java.lang.String} object.
   * @since 5.0.0
   */
  public static void passStep(String step) {
    if (LOG.isTraceEnabled()) {
      LOG.trace("pass step: " + step);
    }
    Allure.getLifecycle().updateStep(step, new PassStep());
  }

  /**
   * <p>startStep.</p>
   *
   * @param name a {@link java.lang.String} object.
   * @return a {@link java.lang.String} object.
   * @since 5.0.0
   */
  public static String startStep(String name) {
    if (LOG.isTraceEnabled()) {
      LOG.trace("start step: " + name);
    }
    String uuid = UUID.randomUUID().toString();
    StepResult result = new StepResult().setName(name);
    Allure.getLifecycle().startStep(uuid, result);
    return uuid;
  }

  /**
   * <p>startStep.</p>
   *
   * @param parentStep a {@link java.lang.String} object.
   * @param stepName a {@link java.lang.String} object.
   * @return a {@link java.lang.String} object.
   * @since 5.0.0
   */
  public static String startStep(String parentStep, String stepName) {
    if (LOG.isTraceEnabled()) {
      LOG.trace("start step: " + stepName);
    }
    String uuid = UUID.randomUUID().toString();
    StepResult result = new StepResult().setName(stepName);
    Allure.getLifecycle().startStep(parentStep, uuid, result);
    return uuid;
  }

  /**
   * Adds a step for current test case to the report.
   *
   * @param stepName to use in report
   * @since 5.0.0
   */
  public static void step(String stepName) {
    if (LOG.isInfoEnabled()) {
      LOG.info("STEP(" + stepName + ")");
    }
    Allure.step(stepName);
  }

  /**
   * Adds a failed step for current test case to the report.
   *
   * @param stepName to use in report
   * @since 5.0.0
   */
  public static void stepFailed(String stepName) {
    if (LOG.isInfoEnabled()) {
      LOG.info("FAILED_STEP(" + stepName + ")");
    }
    Allure.step(stepName, Status.FAILED);
  }

  /**
   * <p>stopStep.</p>
   *
   * @since 5.0.0
   */
  public static void stopStep() {
    if (LOG.isTraceEnabled()) {
      LOG.trace("stop step");
    }
    Allure.getLifecycle().stopStep();
  }

  /**
   * uses Galen functionality to get a full page screenshot by scrolling and
   * stitching.
   *
   * @since 5.0.0
   */
  public static void takeFullScreenshot() {
    String step = startStep("taking full page screenshot");
    GalenConfig galenConfig = GalenConfig.getConfig();
    boolean fullPageScreenshotActivatedInGalen = galenConfig.getBooleanProperty(GalenProperty.SCREENSHOT_FULLPAGE);
    if (!fullPageScreenshotActivatedInGalen) {
      if (LOG.isTraceEnabled()) {
        LOG.trace("activate full page screenshot in Galen before screenshot");
      }
      galenConfig.setProperty(GalenProperty.SCREENSHOT_FULLPAGE, "true");
    }
    try {
      File screenshotFile = GalenUtils.makeFullScreenshot(GaleniumContext.getDriver());
      attachScreenshotFile(screenshotFile);
      passStep(step);
    }
    catch (IOException | InterruptedException ex) {
      LOG.error("Could not take full screenshot.", ex);
    }
    finally {
      if (!fullPageScreenshotActivatedInGalen) {
        if (LOG.isTraceEnabled()) {
          LOG.trace("deactivate full page screenshot in Galen after screenshot");
        }
        galenConfig.setProperty(GalenProperty.SCREENSHOT_FULLPAGE, "false");
      }
      stopStep();
    }
  }

  /**
   * Take screenshot of current browser window and add to reports. Uses random filename.
   *
   * @since 4.0.0
   */
  public static void takeScreenshot() {
    String randomAlphanumeric = RandomStringUtils.randomAlphanumeric(12);
    takeScreenshot(randomAlphanumeric, getTakesScreenshot());
  }

  /**
   * Take screenshot of current browser window and add to report in a dedicated step.
   *
   * @param resultName to use in filename
   * @param takesScreenshot to take screenshot from
   * @since 4.0.0
   */
  public static void takeScreenshot(String resultName, TakesScreenshot takesScreenshot) {
    takeScreenshot(resultName, takesScreenshot, true);
  }

  /**
   * Take screenshot of current browser window and add to report in a dedicated step.
   *
   * @param resultName to use in filename
   * @param takesScreenshot to take screenshot from
   * @param dedicatedStep whether to create dedicated step for attaching screenshot
   * @since 5.0.0
   */
  public static void takeScreenshot(String resultName, TakesScreenshot takesScreenshot, boolean dedicatedStep) {
    String step = null;
    if (dedicatedStep) {
      step = startStep("taking screenshot: " + takesScreenshot);
    }
    File screenshotFile = takesScreenshot.getScreenshotAs(OutputType.FILE);
    attachScreenshotFile(resultName, screenshotFile);
    if (dedicatedStep) {
      passStep(step);
      stopStep();
    }
  }

  /**
   * Captures image from screenshot capable instance which can be the driver or a single element in page.
   *
   * @param takesScreenshot to capture
   * @since 4.0.0
   */
  public static void takeScreenshot(TakesScreenshot takesScreenshot) {
    String randomAlphanumeric = RandomStringUtils.randomAlphanumeric(12);
    takeScreenshot(randomAlphanumeric, takesScreenshot);
  }

  /**
   * <p>
   * updateStep.
   * </p>
   *
   * @param step step UUID
   * @param update step result consumer to do the update
   * @since 5.0.0
   */
  public static void updateStep(String step, Consumer<StepResult> update) {
    Allure.getLifecycle().updateStep(step, update);
  }

  /**
   * Sets a new name for a step.
   *
   * @param step step UUID
   * @param updatedStepName name to set on step
   * @since 5.0.0
   */
  public static void updateStepName(String step, String updatedStepName) {
    if (LOG.isTraceEnabled()) {
      LOG.trace("update step name: " + step + " -> " + updatedStepName);
    }
    updateStep(step, new Consumer<StepResult>() {

      @Override
      public void accept(StepResult result) {
        result.setName(updatedStepName);
      }
    });
  }

  private static void addAttachment(String name, String type, FileInputStream inputStream, String extension, boolean attachToTestCase) {
    if (attachToTestCase) {
      attachToTestCase(name, type, inputStream, extension);
      return;
    }
    attachToCurrentStep(name, type, inputStream, extension);
  }

  private static void attachScreenshotFile(File screenshotFile) {
    attachScreenshotFile(screenshotFile.getName(), screenshotFile);
  }

  private static void attachScreenshotFile(String resultName, File screenshotFile) {
    if (screenshotFile != null) {
      if (LOG.isTraceEnabled()) {
        LOG.trace("screenshot taken: " + screenshotFile.getPath());
      }
      try {
        String destFilename = System.currentTimeMillis() + "_" + resultName + ".png";
        File destFile = new File(PATH_SCREENSHOTS_ROOT, destFilename);
        FileUtils.copyFile(screenshotFile, destFile);
        if (LOG.isTraceEnabled()) {
          LOG.trace("copied screenshot: " + destFile.getPath());
        }
        addPngAttachment("Screenshot: " + resultName, destFile);
        if (FileUtils.deleteQuietly(screenshotFile)) {
          if (LOG.isTraceEnabled()) {
            LOG.trace("deleted screenshot file: " + screenshotFile.getPath());
          }
        }
        else if (LOG.isTraceEnabled()) {
          LOG.trace("could not delete screenshot file: " + screenshotFile.getPath());
        }
      }
      catch (IOException ex) {
        LOG.error("Cannot copy screenshot.", ex);
      }
    }
    else if (LOG.isDebugEnabled()) {
      LOG.debug("screenshot file is null.");
    }
  }

  private static void attachToCurrentStep(String name, String type, FileInputStream inputStream, String extension) {
    Allure.addAttachment(name, type, inputStream, extension);
  }

  @SuppressWarnings("deprecation")
  private static void attachToTestCase(String name, String type, FileInputStream inputStream, String extension) {
    AllureLifecycle lifecycle = Allure.getLifecycle();
    String source = lifecycle.prepareAttachment(name, type, extension);
    lifecycle.writeAttachment(source, inputStream);
    Attachment attachment = new Attachment();
    attachment.setName(name);
    attachment.setSource(source);
    lifecycle.updateTestCase(result -> result.getAttachments().add(attachment));
  }

  private static TakesScreenshot getTakesScreenshot() {
    WebDriver driver = GaleniumContext.getDriver();
    TakesScreenshot takesScreenshot = getTakesScreenshot(driver);
    return takesScreenshot;
  }

  private static TakesScreenshot getTakesScreenshot(WebDriver driver) {
    TakesScreenshot takesScreenshot;
    if (driver instanceof TakesScreenshot) {
      takesScreenshot = (TakesScreenshot)driver;
    }
    else {
      throw new GaleniumException("driver cannot take screenshot");
    }
    return takesScreenshot;
  }

  private static boolean isAddResult(GalenTestInfo galenTestInfo) {
    if (galenTestInfo == null) {
      return false;
    }
    if (GaleniumConfiguration.isOnlyReportGalenErrors()) {
      if ((!galenTestInfo.isFailed()) && (galenTestInfo.getReport().fetchStatistic().getWarnings() == 0)) {
        return false;
      }
    }
    return true;
  }

  private static final class FailStep implements Consumer<StepResult> {

    @Override
    public void accept(StepResult t) {
      t.setStatus(Status.FAILED);
    }
  }

  private static final class PassStep implements Consumer<StepResult> {

    @SuppressWarnings("deprecation")
    @Override
    public void accept(StepResult t) {
      if (t.getStatus() == null) {
        t.setStatus(Status.PASSED);
      }
    }
  }
}