All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.serenitybdd.screenplay.Actor Maven / Gradle / Ivy

There is a newer version: 4.2.9
Show newest version
package net.serenitybdd.screenplay;

import net.serenitybdd.model.PendingStepException;
import net.serenitybdd.core.Serenity;
import net.serenitybdd.core.SkipNested;
import net.serenitybdd.core.eventbus.Broadcaster;
import net.serenitybdd.core.exceptions.IgnoreStepException;
import net.serenitybdd.core.parallel.Agent;
import net.serenitybdd.markers.IsHidden;
import net.serenitybdd.screenplay.events.*;
import net.serenitybdd.screenplay.facts.Fact;
import net.serenitybdd.screenplay.facts.FactLifecycleListener;
import net.serenitybdd.annotations.Pending;
import net.serenitybdd.annotations.Step;
import net.thucydides.model.environment.SystemEnvironmentVariables;
import net.thucydides.model.screenshots.ScreenshotAndHtmlSource;
import net.thucydides.model.steps.ExecutedStepDescription;
import net.thucydides.core.steps.StepEventBus;
import net.thucydides.core.steps.events.StepFinishedEvent;
import net.thucydides.core.steps.events.StepPendingEvent;
import net.thucydides.core.steps.events.StepStartedEvent;
import net.thucydides.core.steps.session.TestSession;
import net.thucydides.model.util.EnvironmentVariables;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.util.*;

import static net.serenitybdd.screenplay.Actor.ErrorHandlingMode.IGNORE_EXCEPTIONS;
import static net.serenitybdd.screenplay.Actor.ErrorHandlingMode.THROW_EXCEPTION_ON_FAILURE;
import static net.serenitybdd.screenplay.SilentTasks.isNestedInSilentTask;
import static net.serenitybdd.screenplay.SilentTasks.isSilent;
import static net.thucydides.model.ThucydidesSystemProperty.MANUAL_TASK_INSTRUMENTATION;

/**
 * An actor represents the person or system using the application under test.
 * Actors can have Abilities, which allows them to perform Tasks and Interactions.
 * Actors can ask Questions about the state of the system.
 */
public class Actor implements PerformsTasks, SkipNested, Agent {

    private String id;
    private String name;
    private final PerformedTaskTally taskTally = new PerformedTaskTally();
    private final EventBusInterface eventBusInterface = new EventBusInterface();
    private final ConsequenceListener consequenceListener = new ConsequenceListener(eventBusInterface);

    private String description;
    private final Map notepad = new HashMap<>();
    private final Map abilities = new HashMap<>();

    private String preferredPronoun;

    public Actor(String name) {
        this.name = name;
    }

    /**
     * Add all the remembered items for the current actor to the other actor's memory
     *
     * @param otherActor
     */
    public void brief(Actor otherActor) {
        otherActor.notepad.putAll(this.notepad);
    }

    public String toString() {
        return getNameOrPronoun();
    }

    /**
     * Create a new actor with a given name.
     * This actor will have no abilities initially, so you will need to assign some abilities
     * using the whoCan() method.
     */
    public static Actor named(String name) {
        return new Actor(name);
    }

    public Actor describedAs(String description) {
        this.description = description;
        assignDescriptionToActor(description);
        return this;
    }

    public String getName() {
        return name;
    }

    public String getDescription() {
        return description;
    }

    public String getNameOrPronoun() {
        return (preferredPronoun != null) ? preferredPronoun : name;
    }

    public  Actor can(T doSomething) {
        if (doSomething instanceof RefersToActor) {
            ((RefersToActor) doSomething).asActor(this);
        }
        abilities.put(doSomething.getClass(), doSomething);
        eventBusInterface.assignAbilityToActor(this, doSomething.toString());
        return this;
    }

    /**
     * Assign an ability to an actor.
     */
    public  Actor whoCan(T doSomething) {
        return can(doSomething);
    }

    @SuppressWarnings("unchecked")
    public  T abilityTo(Class doSomething) {
        T ability = (T) abilities.get(doSomething);

        if (ability == null) {
            ability = this.getAbilityThatExtends(doSomething);
        }

        return ability;
    }

    /**
     * Return an ability that extends the given class. Can be a Superclass or an Interface. If there are multiple
     * candidate Abilities, the first one found will be returned.
     *
     * @param extendedClass the Interface class that we expect to find
     * @param            the matching Ability cast to extendedClass or null if none match
     */
    @SuppressWarnings("unchecked")
    public  C getAbilityThatExtends(Class extendedClass) {
        // See if any ability extends doSomething
        for (Map.Entry entry : abilities.entrySet()) {
            // Return the first matching Ability we find
            if (extendedClass.isAssignableFrom(entry.getKey())) {
                return (C) entry.getValue();
            }
        }
        return null;
    }

    /**
     * Return a list of all {@link Ability}s which implement {@link HasTeardown}
     */
    public List getTeardowns() {
        List teardowns = new ArrayList<>();
        for (Ability a : abilities.values()) {
            if (a instanceof HasTeardown) {
                teardowns.add((HasTeardown) a);
            }
        }
        return teardowns;
    }

    /**
     * A more readable way to access an actor's abilities.
     */
    public  T usingAbilityTo(Class doSomething) {
        return abilityTo(doSomething);
    }

    /**
     * A method used to declare that an actor is now the actor in the spotlight, without having them perform any tasks.
     */
    public final void entersTheScene() {
    }

    public final void has(Performable... todos) {
        attemptsTo(todos);
    }

    private final List factListeners = new ArrayList<>();

    public final void has(Fact... facts) {
        Arrays.stream(facts).forEach(
                fact -> {
                    fact.setup(this);
                    eventBusInterface.assignFactToActor(this, fact.toString());
                    FactLifecycleListener listener = new FactLifecycleListener(this, fact);
                    factListeners.add(listener);
                    StepEventBus.getParallelEventBus().registerListener(listener);
                }
        );
    }

    /**
     * A tense-neutral synonym for addFact() for use with given() clauses
     */
    public final void wasAbleTo(Performable... todos) {
        attemptsTo(todos);
    }

    public enum ErrorHandlingMode {
        THROW_EXCEPTION_ON_FAILURE, IGNORE_EXCEPTIONS
    }

    public final void attemptsTo(ErrorHandlingMode mode, Performable... tasks) {
        // Do not perform tasks if the test is being run in dry-run mode (e.g. for manual tests)
        if (StepEventBus.getParallelEventBus().isDryRun()) {
            return;
        }

        beginPerformance();
        for (Performable task : tasks) {
            if (isNestedInSilentTask()) {
                performSilently(task);
            } else if (isSilent(task)) {
                performSilently(task);
            } else if (isHidden(task) || shouldNotReport(task)) {
                performWithoutReporting(task);
            } else {
                perform(InstrumentedTask.of(task));
            }
        }
        endPerformance(mode);
    }

    public final void attemptsTo(Performable... tasks) {
        attemptsTo(THROW_EXCEPTION_ON_FAILURE, tasks);
    }

    private boolean isHidden(Performable task) {
        return task instanceof IsHidden;
    }

    private boolean shouldNotReport(Performable task) {
        if (manualTaskInstrumentation() && noStepAnnotationIsPresentIn(task)) {
            return true;
        }
        return !InstrumentedTask.isInstrumented(task) && !InstrumentedTask.shouldInstrument(task);
    }

    private boolean noStepAnnotationIsPresentIn(Performable task) {
        try {
            return task.getClass().getMethod("performAs").getAnnotation(Step.class) != null;
        } catch (NoSuchMethodException e) {
            return false;
        }
    }

    @Override
    public  ANSWER asksFor(Question question) {
        beginPerformance();
        ANSWER answer = question.answeredBy(this);
        endPerformance();

        return answer;
    }

    private  void performSilently(T todo) {
        perform(todo);
    }

    private  void performWithoutReporting(T todo) {
        perform(todo);
    }

    private  void performConditionally(T todo) {
        perform(todo);
    }

    private  void perform(T todo) {
        if (isPending(todo)) {
            stepPending();
        }

        try {
            notifyPerformanceOf(todo);
            taskTally.newTask();

            performTask(todo);

            // TODO: When and how should this work
            if (anOutOfStepErrorOccurred()) {
                eventBusInterface.mergePreviousStep();
            }
        } catch (Throwable exception) {
            if (!pendingOrIgnore(exception) && !thisIsAnExceptionBubblingUpFromAPreviousFailure()) {
                eventBusInterface.reportStepFailureFor(todo, exception);
            }
            if (Serenity.shouldThrowErrorsImmediately() || isAnAssumptionFailure(exception)) {
                throw exception;
            }
        } finally {
            eventBusInterface.updateOverallResult();
        }
    }

    private static boolean thisIsAnExceptionBubblingUpFromAPreviousFailure() {
        return StepEventBus.getParallelEventBus().getBaseStepListener().aStepHasFailed();
    }

    private  void performTask(T todo) {
        if (!StepEventBus.getParallelEventBus().currentTestIsSuspended()) {
            EventBusInterface.castActor(name);
            todo.performAs(this);
        }
    }

    private  void notifyPerformanceOf(T todo) {
        Broadcaster.getEventBus().post(new ActorPerforms(todo, getName()));
    }

    private  boolean isPending(T todo) {
        Method performAs = getPerformAsForClass(todo.getClass().getSuperclass()).orElse(getPerformAsForClass(todo.getClass()).orElse(null));

        return (performAs != null) && (performAs.getAnnotation(Pending.class) != null);
    }

    private Optional getPerformAsForClass(Class taskClass) {
        try {
            return Optional.of(taskClass.getMethod("performAs", Actor.class));
        } catch (NoSuchMethodException e) {
            return Optional.empty();
        }
    }

    private boolean pendingOrIgnore(Throwable exception) {
        return exception instanceof IgnoreStepException ||
                exception instanceof PendingStepException;
    }

    private boolean isAnAssumptionFailure(Throwable e) {
        return e.getClass().getSimpleName().contains("Assumption");
    }

    public final void can(Consequence... consequences) {
        should(consequences);
    }


    public final void should(String groupStepName, Consequence... consequences) {

        try {
            String groupTitle = injectActorInto(groupStepName);
            stepStarted(groupTitle);
            should(consequences);

        } catch (Throwable error) {
            throw error;
        } finally {
            //StepEventBus.getParallelEventBus().stepFinished();
            stepFinished();
        }
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(Actor.class);

    private void stepStarted(String groupTitle) {
        if (!TestSession.isSessionStarted()) {
            StepEventBus.getParallelEventBus().stepStarted(ExecutedStepDescription.withTitle(groupTitle));
        } else {
            TestSession.addEvent(new StepStartedEvent( ExecutedStepDescription.withTitle(groupTitle)));
        }
    }

    private void stepFinished() {
        if (TestSession.isSessionStarted()) {
            List screenshotList = TestSession.getTestSessionContext().getStepEventBus().takeScreenshots();
            TestSession.addEvent(new StepFinishedEvent(screenshotList));
        } else {
            StepEventBus.getParallelEventBus().stepFinished();
        }
    }

    private void stepPending() {
        if (TestSession.isSessionStarted()) {
            TestSession.addEvent(new StepPendingEvent());
        } else {
            StepEventBus.getParallelEventBus().stepPending();
        }
    }

    private String injectActorInto(String groupStepName) {
        return groupStepName.replaceAll("\\{0\\}", this.toString());
    }

    public final void should(List> consequences) {
        should(consequences.toArray(new Consequence[]{}));
    }

    public final void should(Consequence... consequences) {

        ErrorTally errorTally = new ErrorTally(eventBusInterface);

        startConsequenceCheck();

        for (Consequence consequence : consequences) {
            check(consequence, errorTally);
        }

        endConsequenceCheck();

        errorTally.reportAnyErrors();

    }

    private boolean anOutOfStepErrorOccurred() {
        if (!eventBusInterface.isBaseStepListenerRegistered()) {
            return false;
        }
        if (eventBusInterface.aStepHasFailedInTheCurrentExample()) {
            return (eventBusInterface.getRunningStepCount()) > taskTally.getPerformedTaskCount();
        } else {
            return false;
        }
    }

    private  void check(Consequence consequence, ErrorTally errorTally) {

        ConsequenceCheckReporter reporter = new ConsequenceCheckReporter(eventBusInterface, consequence);
        try {
            reporter.startQuestion(this);
            if (eventBusInterface.shouldIgnoreConsequences()) {
                reporter.reportStepIgnored();
            } else {
                consequence.evaluateFor(this);
                reporter.reportStepFinished();
            }
        } catch (IgnoreStepException e) {
            reporter.reportStepIgnored();
        } catch (Throwable e) {
            errorTally.recordError(consequence, e);
        }
    }

    public  void remember(String key, Question question) {
        beginPerformance();
        ANSWER answer = this.asksFor(question);
        notepad.put(key, answer);
        endPerformance();
    }

    public void remember(String key, Object value) {
        notepad.put(key, value);
    }

    @SuppressWarnings("unchecked")
    public  T recall(String key) {
        return (T) notepad.get(key);
    }

    public Map recallAll() {
        return new HashMap<>(notepad);
    }

    @SuppressWarnings("unchecked")
    public  T forget(String key) {
        return (T) notepad.remove(key);
    }

    public  T sawAsThe(String key) {
        return recall(key);
    }

    public  T gaveAsThe(String key) {
        return recall(key);
    }

    private void beginPerformance() {
        Serenity.setSessionVariable(Agent.IN_THE_CURRENT_SESSION).to(this);
        Broadcaster.getEventBus().post(new ActorBeginsPerformanceEvent(name));
    }

    private void endPerformance() {
        endPerformance(THROW_EXCEPTION_ON_FAILURE);
    }

    private void endPerformance(ErrorHandlingMode mode) {
        Broadcaster.getEventBus().post(new ActorEndsPerformanceEvent(name));
        boolean isAFixtureMethod = StepEventBus.getParallelEventBus().inFixtureMethod();
        if (mode == THROW_EXCEPTION_ON_FAILURE && !isAFixtureMethod) {
            eventBusInterface.failureCause().ifPresent(
                    cause -> {
                        StepEventBus.getParallelEventBus().notifyFailure();
                        //StepEventBus.getParallelEventBus().testFinished(StepEventBus.getParallelEventBus().currentTestOutcomeIsDataDriven());
                        if (cause.isCompromised()) {
                            throw cause.asCompromisedException();
                        } else if (cause.isAnError()) {
                            throw cause.asError();
                        } else if (cause.isAnAssertionError()) {
                            throw cause.asAssertionError();
                        } else if (cause.getOriginalCause() instanceof RuntimeException) {
                            throw (RuntimeException) cause.getOriginalCause();
                        } else {
                            throw cause.asFailure();
                        }
                    }
            );
        }
    }


    private void startConsequenceCheck() {
        beginPerformance();
        consequenceListener.beginConsequenceCheck();
        Broadcaster.getEventBus().post(new ActorBeginsConsequenceCheckEvent(name));
    }

    private void endConsequenceCheck() {
        consequenceListener.endConsequenceCheck();
        Broadcaster.getEventBus().post(new ActorEndsConsequenceCheckEvent(name));
        endPerformance(IGNORE_EXCEPTIONS);
    }


    public Actor usingPronoun(String pronoun) {
        this.preferredPronoun = pronoun;
        return this;
    }

    public Actor withNoPronoun() {
        this.preferredPronoun = null;
        return this;
    }


    public void assignDescriptionToActor(String description) {
        StepEventBus.getParallelEventBus().getBaseStepListener().latestTestOutcome().ifPresent(
                testOutcome -> testOutcome.assignDescriptionToActor(getName(), description)
        );
    }

    public void assignName(String name) {
        this.name = name;
    }

    private boolean manualTaskInstrumentation() {
        EnvironmentVariables environmentVariables = SystemEnvironmentVariables.currentEnvironmentVariables();
        return (MANUAL_TASK_INSTRUMENTATION.booleanFrom(environmentVariables, false));
    }

    public void wrapUp() {
        getTeardowns().forEach(HasTeardown::tearDown);
        factListeners.forEach(
                factLifecycleListener -> StepEventBus.getParallelEventBus().dropListener(factLifecycleListener)
        );

    }

    @Override
    public String getId() {
        return id;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy