package net.thucydides.core.steps;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import com.google.inject.Injector;
import net.thucydides.core.PendingStepException;
import net.thucydides.core.annotations.TestAnnotations;
import net.thucydides.core.guice.Injectors;
import net.thucydides.core.model.*;
import net.thucydides.core.pages.Pages;
import net.thucydides.core.pages.SystemClock;
import net.thucydides.core.screenshots.*;
import net.thucydides.core.webdriver.Configuration;
import net.thucydides.core.webdriver.WebDriverFacade;
import net.thucydides.core.webdriver.WebdriverManager;
import net.thucydides.core.webdriver.WebdriverProxyFactory;
import org.apache.commons.lang3.StringUtils;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.SessionId;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.util.*;
import static net.thucydides.core.model.Stories.findStoryFrom;
import static net.thucydides.core.model.TestResult.*;
import static net.thucydides.core.steps.BaseStepListener.ScreenshotType.MANDATORY_SCREENSHOT;
import static net.thucydides.core.steps.BaseStepListener.ScreenshotType.OPTIONAL_SCREENSHOT;
/**
* Observes the test run and stores test run details for later reporting.
* Observations are recorded in an TestOutcome object. This includes
* recording the names and results of each test, and taking and storing
* screenshots at strategic points during the tests.
*/
public class BaseStepListener implements StepListener, StepPublisher {
/**
* Used to build the test outcome structure as the test step results come in.
*/
private final List<TestOutcome> testOutcomes;
/**
* Keeps track of what steps have been started but not finished, in order to structure nested steps.
*/
private final Stack<TestStep> currentStepStack;
/**
* Keeps track of the current step group, if any.
*/
private final Stack<TestStep> currentGroupStack;
private StepEventBus eventBus;
/**
* Clock used to pause test execution.
*/
private final SystemClock clock;
private ScreenshotPermission screenshots;
/**
* The Java class (if any) containing the tests.
*/
private Class<?> testSuite;
private static final Logger LOGGER = LoggerFactory.getLogger(BaseStepListener.class);
private WebDriver driver;
private WebdriverManager webdriverManager;
private File outputDirectory;
private WebdriverProxyFactory proxyFactory;
private Story testedStory;
private Configuration configuration;
ScreenshotProcessor screenshotProcessor;
private boolean inFluentStepSequence;
private List<String> storywideIssues;
private List<TestTag> storywideTags;
public void setEventBus(StepEventBus eventBus) {
this.eventBus = eventBus;
}
public StepEventBus getEventBus() {
if (eventBus == null) {
eventBus = StepEventBus.getEventBus();
}
return eventBus;
}
public Optional<TestStep> cloneCurrentStep() {
return (Optional<TestStep>) ((currentStepExists()) ? getCurrentStep().clone() : Optional.absent());
}
public void setAllStepsTo(TestResult result) {
getCurrentTestOutcome().setAnnotatedResult(result);
getCurrentTestOutcome().setAllStepsTo(result);
}
protected enum ScreenshotType {
OPTIONAL_SCREENSHOT,
MANDATORY_SCREENSHOT
}
public BaseStepListener(final File outputDirectory) {
this(outputDirectory, Injectors.getInjector());
}
public BaseStepListener(final File outputDirectory, Injector injector) {
this.proxyFactory = WebdriverProxyFactory.getFactory();
this.testOutcomes = Lists.newArrayList();
this.currentStepStack = new Stack<TestStep>();
this.currentGroupStack = new Stack<TestStep>();
this.outputDirectory = outputDirectory;
this.inFluentStepSequence = false;
this.storywideIssues = Lists.newArrayList();
this.storywideTags = Lists.newArrayList();
this.webdriverManager = injector.getInstance(WebdriverManager.class);
this.clock = injector.getInstance(SystemClock.class);
this.configuration = injector.getInstance(Configuration.class);
this.screenshotProcessor = injector.getInstance(ScreenshotProcessor.class);
}
/**
* Create a step listener with a given web driver type.
*
* @param driverClass a driver of this type will be used
* @param outputDirectory reports and screenshots are generated here
*/
public BaseStepListener(final Class<? extends WebDriver> driverClass, final File outputDirectory) {
this(outputDirectory);
this.driver = getProxyFactory().proxyFor(driverClass);
}
public BaseStepListener(final Class<? extends WebDriver> driverClass,
final File outputDirectory,
final Configuration configuration) {
this(outputDirectory);
this.driver = getProxyFactory().proxyFor(driverClass);
this.configuration = configuration;
}
public BaseStepListener(final File outputDirectory,
final WebdriverManager webdriverManager) {
this(outputDirectory);
this.webdriverManager = webdriverManager;
}
/**
* Create a step listener using the driver from a given page factory.
* If the pages factory is null, a new driver will be created based on the default system values.
*
* @param outputDirectory reports and screenshots are generated here
* @param pages a pages factory.
*/
public BaseStepListener(final File outputDirectory, final Pages pages) {
this(outputDirectory);
if (pages != null) {
setDriverUsingPagesDriverIfDefined(pages);
} else {
createNewDriver();
}
}
protected ScreenshotPermission screenshots() {
if (screenshots == null) {
screenshots = new ScreenshotPermission(configuration);
}
return screenshots;
}
private void createNewDriver() {
setDriver(getProxyFactory().proxyDriver());
}
private void setDriverUsingPagesDriverIfDefined(final Pages pages) {
if (pages.getDriver() != null) {
setDriver(pages.getDriver());
} else {
createNewDriver();
pages.setDriver(getDriver());
}
}
protected WebdriverProxyFactory getProxyFactory() {
return proxyFactory;
}
protected TestOutcome getCurrentTestOutcome() {
Preconditions.checkState(!testOutcomes.isEmpty());
return testOutcomes.get(testOutcomes.size() - 1);
}
protected SystemClock getClock() {
return clock;
}
/**
* A test suite (containing a series of tests) starts.
*
* @param startedTestSuite the class implementing the test suite (e.g. a JUnit test case)
*/
public void testSuiteStarted(final Class<?> startedTestSuite) {
testSuite = startedTestSuite;
testedStory = findStoryFrom(startedTestSuite);
clearStorywideTagsAndIssues();
}
private void clearStorywideTagsAndIssues() {
storywideIssues.clear();
storywideTags.clear();
}
private boolean suiteStarted = false;
public void testSuiteStarted(final Story story) {
testSuite = null;
testedStory = story;
suiteStarted = true;
clearStorywideTagsAndIssues();
}
public boolean testSuiteRunning() {
return suiteStarted;
}
public void addIssuesToCurrentStory(List<String> issues) {
storywideIssues.addAll(issues);
}
public void addTagsToCurrentStory(List<TestTag> tags) {
storywideTags.addAll(tags);
}
public void testSuiteFinished() {
screenshotProcessor.waitUntilDone();
clearStorywideTagsAndIssues();
suiteStarted = false;
}
/**
* An individual test starts.
*
* @param testMethod the name of the test method in the test suite class.
*/
public void testStarted(final String testMethod) {
TestOutcome newTestOutcome = TestOutcome.forTestInStory(testMethod, testSuite, testedStory);
testOutcomes.add(newTestOutcome);
updateSessionIdIfKnown();
setAnnotatedResult(testMethod);
}
private void updateSessionIdIfKnown() {
SessionId sessionId = webdriverManager.getSessionId();
if (sessionId != null) {
getCurrentTestOutcome().setSessionId(sessionId.toString());
}
}
public void updateCurrentStepTitle(String updatedStepTitle) {
if (currentStepExists()) {
getCurrentStep().setDescription(updatedStepTitle);
} else {
stepStarted(ExecutedStepDescription.withTitle(updatedStepTitle));
}
}
private void setAnnotatedResult(String testMethod) {
if (TestAnnotations.forClass(testSuite).isIgnored(testMethod)) {
getCurrentTestOutcome().setAnnotatedResult(IGNORED);
}
if (TestAnnotations.forClass(testSuite).isPending(testMethod)) {
getCurrentTestOutcome().setAnnotatedResult(PENDING);
}
}
/**
* A test has finished.
*
* @param outcome the result of the test that just finished.
*/
public void testFinished(final TestOutcome outcome) {
recordTestDuration();
getCurrentTestOutcome().addIssues(storywideIssues);
// TODO: Disable when run from an IDE
getCurrentTestOutcome().addTags(storywideTags);
currentStepStack.clear();
}
public void testRetried() {
currentStepStack.clear();
testOutcomes.remove(getCurrentTestOutcome());
}
private void recordTestDuration() {
if (!testOutcomes.isEmpty()) {
getCurrentTestOutcome().recordDuration();
}
}
/**
* A step within a test is called.
* This step might be nested in another step, in which case the original step becomes a group of steps.
*
* @param description the description of the test that is about to be run
*/
public void stepStarted(final ExecutedStepDescription description) {
recordStep(description);
takeInitialScreenshot();
updateSessionIdIfKnown();
}
public void skippedStepStarted(final ExecutedStepDescription description) {
recordStep(description);
}
private void recordStep(ExecutedStepDescription description) {
String stepName = AnnotatedStepDescription.from(description).getName();
updateFluentStepStatus(description, stepName);
if (justStartedAFluentSequenceFor(description) || notInAFluentSequence()) {
TestStep step = new TestStep(stepName);
startNewGroupIfNested();
setDefaultResultFromAnnotations(step, description);
currentStepStack.push(step);
recordStepToCurrentTestOutcome(step);
}
inFluentStepSequence = AnnotatedStepDescription.from(description).isFluent();
}
private void recordStepToCurrentTestOutcome(TestStep step) {
getCurrentTestOutcome().recordStep(step);
}
private void updateFluentStepStatus(ExecutedStepDescription description, String stepName) {
if (currentlyInAFluentSequenceFor(description) || justFinishedAFluentSequenceFor(description)) {
addToFluentStepName(stepName);
}
}
private void addToFluentStepName(String stepName) {
String updatedStepName = getCurrentStep().getDescription() + " " + StringUtils.uncapitalize(stepName);
getCurrentStep().setDescription(updatedStepName);
}
private boolean notInAFluentSequence() {
return !inFluentStepSequence;
}
private boolean justFinishedAFluentSequenceFor(ExecutedStepDescription description) {
boolean thisStepIsFluent = AnnotatedStepDescription.from(description).isFluent();
return (inFluentStepSequence && !thisStepIsFluent);
}
private boolean justStartedAFluentSequenceFor(ExecutedStepDescription description) {
boolean thisStepIsFluent = AnnotatedStepDescription.from(description).isFluent();
return (!inFluentStepSequence && thisStepIsFluent);
}
private boolean currentlyInAFluentSequenceFor(ExecutedStepDescription description) {
boolean thisStepIsFluent = AnnotatedStepDescription.from(description).isFluent();
return (inFluentStepSequence && thisStepIsFluent);
}
private void setDefaultResultFromAnnotations(final TestStep step, final ExecutedStepDescription description) {
if (TestAnnotations.isPending(description.getTestMethod())) {
step.setResult(TestResult.PENDING);
}
if (TestAnnotations.isIgnored(description.getTestMethod())) {
step.setResult(TestResult.IGNORED);
}
}
private void startNewGroupIfNested() {
if (thereAreUnfinishedSteps()) {
if (getCurrentStep() != getCurrentGroup()) {
startNewGroup();
}
}
}
private void startNewGroup() {
getCurrentTestOutcome().startGroup();
currentGroupStack.push(getCurrentStep());
}
private TestStep getCurrentStep() {
return currentStepStack.peek();
}
private Optional<TestStep> getPreviousStep() {
if (getCurrentTestOutcome().getTestSteps().size() > 1) {
List<TestStep> currentTestSteps = getCurrentTestOutcome().getTestSteps();
return Optional.of(currentTestSteps.get(currentTestSteps.size() - 2));
} else {
return Optional.absent();
}
}
private TestStep getCurrentGroup() {
if (currentGroupStack.isEmpty()) {
return null;
} else {
return currentGroupStack.peek();// findLastChildIn(currentGroupStack.peek());
}
}
private boolean thereAreUnfinishedSteps() {
return !currentStepStack.isEmpty();
}
public void stepFinished() {
updateSessionIdIfKnown();
takeEndOfStepScreenshotFor(SUCCESS);
currentStepDone(SUCCESS);
// markCurrentStepAs(SUCCESS);
pauseIfRequired();
}
private void updateExampleTableIfNecessary(TestResult result) {
if (getCurrentTestOutcome().isDataDriven()) {
getCurrentTestOutcome().updateCurrentRowResult(result);
}
}
private void finishGroup() {
currentGroupStack.pop();
getCurrentTestOutcome().endGroup();
}
private void pauseIfRequired() {
int delay = configuration.getStepDelay();
if (delay > 0) {
getClock().pauseFor(delay);
}
}
private void markCurrentStepAs(final TestResult result) {
getCurrentTestOutcome().currentStep().setResult(result);
updateExampleTableIfNecessary(result);
}
FailureAnalysis failureAnalysis = new FailureAnalysis();
public void stepFailed(StepFailure failure) {
takeEndOfStepScreenshotFor(FAILURE);
getCurrentTestOutcome().determineTestFailureCause(failure.getException());
// markCurrentStepAs(failureAnalysis.resultFor(failure));
recordFailureDetailsInFailingTestStep(failure);
currentStepDone(failureAnalysis.resultFor(failure));
}
public void lastStepFailed(StepFailure failure) {
takeEndOfStepScreenshotFor(FAILURE);
getCurrentTestOutcome().lastStepFailedWith(failure);
}
private void recordFailureDetailsInFailingTestStep(final StepFailure failure) {
if (currentStepExists()) {
getCurrentStep().failedWith(new StepFailureException(failure.getMessage(), failure.getException()));
}
}
public void stepIgnored() {
if (aStepHasFailed()) {
markCurrentStepAs(SKIPPED);
currentStepDone(SKIPPED);
} else {
// markCurrentStepAs(IGNORED);
currentStepDone(IGNORED);
}
}
public void stepPending() {
// markCurrentStepAs(PENDING);
currentStepDone(PENDING);
}
public void stepPending(String message) {
getCurrentStep().testAborted(new PendingStepException(message));
stepPending();
}
public void assumptionViolated(String message) {
if (thereAreUnfinishedSteps()) {
getCurrentStep().testAborted(new PendingStepException(message));
stepIgnored();
}
testIgnored();
}
private void currentStepDone(TestResult result) {
if ((!inFluentStepSequence) && currentStepExists()) {
TestStep finishedStep = currentStepStack.pop();
finishedStep.recordDuration();
if (result != null) {
finishedStep.setResult(result);
}
if ((finishedStep == getCurrentGroup())) {
finishGroup();
}
}
updateExampleTableIfNecessary(result);
}
private boolean currentStepExists() {
return !currentStepStack.isEmpty();
}
private void takeEndOfStepScreenshotFor(final TestResult result) {
if (shouldTakeEndOfStepScreenshotFor(result)) {
take(OPTIONAL_SCREENSHOT);
}
}
private void take(final ScreenshotType screenshotType) {
if (currentStepExists() && browserIsOpen()) {
try {
Optional<ScreenshotAndHtmlSource> screenshotAndHtmlSource = grabScreenshot();
if (screenshotAndHtmlSource.isPresent()) {
takeScreenshotIfRequired(screenshotType, screenshotAndHtmlSource.get());
}
removeDuplicatedInitalScreenshotsIfPresent();
} catch (ScreenshotException e) {
LOGGER.warn("Failed to take screenshot", e);
}
}
}
private void removeDuplicatedInitalScreenshotsIfPresent() {
if (currentStepHasMoreThanOneScreenshot() && getPreviousStep().isPresent() && getPreviousStep().get().hasScreenshots()) {
ScreenshotAndHtmlSource lastScreenshotOfPreviousStep = lastScreenshotOf(getPreviousStep().get());
ScreenshotAndHtmlSource firstScreenshotOfThisStep = getCurrentStep().getFirstScreenshot();
if (firstScreenshotOfThisStep.hasIdenticalScreenshotsAs(lastScreenshotOfPreviousStep)) {
removeFirstScreenshotOfCurrentStep();
}
}
}
private void removeFirstScreenshotOfCurrentStep() {
getCurrentStep().removeScreenshot(0);
}
private boolean currentStepHasMoreThanOneScreenshot() {
return getCurrentStep().getScreenshotCount() > 1;
}
private ScreenshotAndHtmlSource lastScreenshotOf(TestStep testStep) {
return testStep.getScreenshots().get(testStep.getScreenshots().size() - 1);
}
private void takeScreenshotIfRequired(ScreenshotType screenshotType, ScreenshotAndHtmlSource screenshotAndHtmlSource) {
if (shouldTakeScreenshot(screenshotType, screenshotAndHtmlSource) && screenshotWasTaken(screenshotAndHtmlSource)) {
getCurrentStep().addScreenshot(screenshotAndHtmlSource);
}
}
private boolean screenshotWasTaken(ScreenshotAndHtmlSource screenshotAndHtmlSource) {
return screenshotAndHtmlSource.getScreenshotFile() != null;
}
private boolean shouldTakeScreenshot(ScreenshotType screenshotType,
ScreenshotAndHtmlSource screenshotAndHtmlSource) {
return (screenshotType == MANDATORY_SCREENSHOT)
|| getCurrentStep().getScreenshots().isEmpty()
|| shouldTakeOptionalScreenshot(screenshotAndHtmlSource);
}
private boolean shouldTakeOptionalScreenshot(ScreenshotAndHtmlSource screenshotAndHtmlSource) {
return (screenshotAndHtmlSource.wasTaken() && previousScreenshot().isPresent()
&& (!screenshotAndHtmlSource.hasIdenticalScreenshotsAs(previousScreenshot().get())));
}
private Optional<ScreenshotAndHtmlSource> previousScreenshot() {
List<ScreenshotAndHtmlSource> screenshotsToDate = getCurrentTestOutcome().getScreenshotAndHtmlSources();
if (screenshotsToDate.isEmpty()) {
return Optional.absent();
} else {
return Optional.of(screenshotsToDate.get(screenshotsToDate.size() - 1));
}
}
private boolean browserIsOpen() {
if (driver == null) {
return false;
}
if (driver instanceof WebDriverFacade) {
return (((WebDriverFacade) driver).isInstantiated());
} else {
return (driver.getCurrentUrl() != null);
}
}
private void takeInitialScreenshot() {
if ((currentStepExists()) && (screenshots().areAllowed(TakeScreenshots.BEFORE_AND_AFTER_EACH_STEP))) {
take(OPTIONAL_SCREENSHOT);
}
}
private Optional<ScreenshotAndHtmlSource> grabScreenshot() {
Optional<File> screenshot = getPhotographer().takeScreenshot();
if (screenshot.isPresent()) {
if (shouldStoreSourcecode()) {
File sourcecodeFile = sourcecodeForScreenshot(screenshot.get(), getPageSource());
return Optional.of(new ScreenshotAndHtmlSource(screenshot.get(), sourcecodeFile));
} else {
return Optional.of(new ScreenshotAndHtmlSource(screenshot.get()));
}
}
return Optional.absent();
}
public String getPageSource() {
return getPhotographer().getPageSource();
}
private File sourcecodeForScreenshot(File screenshotFile, String pageSource) {
File pageSourceFile = new File(screenshotFile.getAbsolutePath() + ".html");
try {
Files.write(pageSourceFile.toPath(), pageSource.getBytes());
} catch (IOException e) {
LOGGER.warn("Failed to write screen source code",e);
}
return pageSourceFile;
}
private boolean shouldStoreSourcecode() {
return configuration.storeHtmlSourceCode();
}
public Photographer getPhotographer() {
ScreenshotBlurCheck blurCheck = new ScreenshotBlurCheck();
if (blurCheck.blurLevel().isPresent()) {
return new Photographer(driver, outputDirectory, blurCheck.blurLevel().get());
} else {
return new Photographer(driver, outputDirectory);
}
}
private boolean shouldTakeEndOfStepScreenshotFor(final TestResult result) {
if (result == FAILURE) {
return screenshots().areAllowed(TakeScreenshots.FOR_FAILURES);
} else {
return screenshots().areAllowed(TakeScreenshots.AFTER_EACH_STEP);
}
}
public List<TestOutcome> getTestOutcomes() {
List<TestOutcome> sortedOutcomes = Lists.newArrayList(testOutcomes);
Collections.sort(sortedOutcomes, byStartTimeAndName());
return ImmutableList.copyOf(sortedOutcomes);
}
private Comparator<? super TestOutcome> byStartTimeAndName() {
return new Comparator<TestOutcome>() {
public int compare(TestOutcome testOutcome1, TestOutcome testOutcome2) {
String creationTimeAndName1 = testOutcome1.getStartTime().getMillis() + "_" + testOutcome1.getMethodName();
String creationTimeAndName2 = testOutcome2.getStartTime().getMillis() + "_" + testOutcome2.getMethodName();
return creationTimeAndName1.compareTo(creationTimeAndName2);
}
};
}
public void setDriver(final WebDriver driver) {
this.driver = driver;
}
public WebDriver getDriver() {
return driver;
}
@SuppressWarnings("ThrowableResultOfMethodCallIgnored")
public boolean aStepHasFailed() {
return ((!getTestOutcomes().isEmpty()) &&
(getCurrentTestOutcome().getResult() == TestResult.FAILURE || getCurrentTestOutcome().getResult() == TestResult.ERROR));
}
public FailureCause getTestFailureCause() {
return getCurrentTestOutcome().getTestFailureCause();
}
public void testFailed(TestOutcome testOutcome, final Throwable cause) {
getCurrentTestOutcome().determineTestFailureCause(cause);
}
public void testIgnored() {
getCurrentTestOutcome().setAnnotatedResult(IGNORED);
}
public void testSkipped() {
getCurrentTestOutcome().setAnnotatedResult(SKIPPED);
}
public void testPending() {
getCurrentTestOutcome().setAnnotatedResult(PENDING);
}
public void notifyScreenChange() {
if (screenshots().areAllowed(TakeScreenshots.FOR_EACH_ACTION)) {
take(OPTIONAL_SCREENSHOT);
}
}
/**
* Take a screenshot now.
*/
public void takeScreenshot() {
take(MANDATORY_SCREENSHOT);
}
int currentExample = 0;
/**
* The current scenario is a data-driven scenario using test data from the specified table.
*/
public void useExamplesFrom(DataTable table) {
getCurrentTestOutcome().useExamplesFrom(table);
currentExample = 0;
}
public void exampleStarted(Map<String, String> data) {
if (getCurrentTestOutcome().isDataDriven() && !getCurrentTestOutcome().dataIsPredefined()) {
getCurrentTestOutcome().addRow(data);
}
currentExample++;
getEventBus().stepStarted(ExecutedStepDescription.withTitle(exampleTitle(currentExample, data)));
}
private String exampleTitle(int exampleNumber, Map<String, String> data) {
return String.format("[%s] %s", exampleNumber, data);
}
public void exampleFinished() {
currentStepDone(null);
getCurrentTestOutcome().moveToNextRow();
}
}