Serenity-js: Login usecase - How can I enrich the actor (so it have more than a name)?

Created on 8 Mar 2018  路  6Comments  路  Source: serenity-js/serenity-js

Hi,

I'm currently automating a website. This website has a subscribe form. The subscribe form require : firstname, lastname, email. I'd thus like a way for my actor not only to have a "name", but more details to it. A way for me to be able to call stage.theActorInTheSpotlight.lastname(); for ex.

I tried adding methods in the class that implements cast, without much success.

Today I either hardcode the lastname in my scenario (not elegant) or define, on my cast implementation, a switch case, which exports a lastname based on actor.toString. But I don't really like using global variable. There must be a better way to code that, isn't it?

spec/customerJourney.js

import { serenity } from 'serenity-js';
import { Open, Click, Is, Wait } from 'serenity-js/lib/serenity-protractor'
import { Persona } from '../src/screenplay/actors/persona'
import { Subscribe } from "../src/screenplay/tasks/subscribe";

const ssoUrl = 'https://sso.mydomain/';
const stage = serenity.callToStageFor(new Persona());
const han = stage.theActorCalled("Han");
//[...]
it(" - Han's subscription", () => {han.attemptsTo(
            Open.browserOn(ssoUrl),
            Subscribe.as("Solo",han.toString()),
//[...]

src/screenplay/actors/persona.ts

import { protractor } from 'protractor';
import { Actor, BrowseTheWeb, Cast } from 'serenity-js/protractor';
import { AuthenticateViaUI } from '../abilities/authenticateViaUI'

export class Persona implements Cast {
    actor(firstNameActor: string): Actor {

        let email = "test+" + firstNameActor + "@mydomain";

        switch (firstNameActor) {
            case 'Pharell':
                return Actor.named(firstNameActor).whoCan(
                    // AuthenticateViaAPI.using('some-authentication-token')
                );

            case 'Han':
                //const lastname = "Solo" ; module.exports = { lastName, email };
                return Actor.named(firstNameActor)
                    .whoCan(AuthenticateViaUI.using(email, firstNameActor))
                    .whoCan(BrowseTheWeb.using(protractor.browser))
                    ;

            default:
                return Actor.named(firstNameActor)
                    .whoCan(BrowseTheWeb.using(protractor.browser)
                );
        }
}

Could you advise me on something elegant I could code to make it work?

@serenitcore documentation question

All 6 comments

I can think of two ways to achieve that:

1. Extend the Actor

Since Cast is a factory of Actors, the simplest way to accomplish your goal would be to extend the Actor class to decorate it with the necessary properties.

class Persona extends Actor {
    constructor(public readonly email: string, name: string, stage_manager: StageManager) {
       super(name, stage_manager);
    }

    // any additional methods
}

2. Encapsulate the properties within an Ability

That's the approach you're using to define Mr. Han Solo:

  return Actor.named(firstNameActor)
                    .whoCan(AuthenticateViaUI.using(email, firstNameActor))
                    .whoCan(BrowseTheWeb.using(protractor.browser))
                    ;

If AuthenticateViaUI stored the email and the firstName, then you could have a corresponding Interaction that retrieves this data:

const LogIn = () => Interaction.where(`#actor logs in with their credentials`, (actor: Actor) => {
  const email = AuthenticateViaUI.as(actor).email;
 // etc.

  return actor.attemptsTo(
    Enter.theValue(email).into(LoginForm.email),
    // etc.
  );
});

I describe this design in more details in this article (code samples in Java, but approach itself still holds).

3. Externalise the properties

The third way I've been experimenting with recently could be to externalise the properties so that defining a property of the persona is yet another Activity of an actor.

For example:

Actor.named('Bruce').attemptsTo(
  Define(new FullName('Bruce Wayne')),
);

and then to retrieve the value:

Bruce.attemptsTo(
  Enter.theValue(FullName.of(Bruce)).into(RegistrationForm.Full_Name),
)

Where FullName is a Tiny Type that also acts as a data holder:

class FullName extends TinyType {
    static of = (actor: Actor) => FullName.all[actor.toString()];  // we could also throw here, if needed
    static all: { [_: string]: string } = {};

    constructor(public readonly value: string) {
    }
}

and Define is an Interaction that sets the value on FullName, which could be more or less implemented as follows:

const Define = (fact: TinyType) => Interaction.where(
    `#actor defines their ${ fact.constructor.name }...`,
    actor => ((fact as any).all[actor.toString()] = fact, Promise.resolve()),
);

Would any of the above work for you?

Jan

Clearly some of the above can fit. I'm thinking using 1 or 2 for the identity (ex. lastname, email) and 3 for user choices (ex. password). Maybe not the easiest, but I think that's what makes sense.
Thanks for your kind and clear feedback, as usual :+1: .

Hi @jan-molak

Sorry to bother you again (I'm quite a user aren't I :D ?) but I tried method 3 on a field (an option choice that a user can make). I get Option.of(sandra) = undefined.

I basically retrieve your code, with define.ts being an interaction, option.ts containing class Option extends TinyType {...} block (to the difference that the constructor has a "super()" call in it).

const stage = serenity.callToStageFor(new Persona());
const sandra = stage.theActorCalled("Sandra");
(...)

it("First option ", () => sandra.attemptsTo(
        Define(new Option('opt01')),
),
        console.log(stage.theActorInTheSpotlight()),                              //gets Actor { name: 'Sandra', ...}
        console.log(Option.of(stage.theActorInTheSpotlight())),             //gets undefined
        console.log(Option.of(sandra)),                                                   //also gets undefined

I also have a task (and a scenario calling it) ready to exploit that, but truth is, if I got undefined in the basic scenario above, there is no way it works when called within a more evolved context. Sad as the task uses the lately added Check interaction ;)!

Check.whether(Offer.of(this.actor) == 'opt01').andIfSo(
    Enter.theValue(faker.name).into(UserForm.opt),
),

Sorry for not finding that out myself. Maybe I'm missing the obvious here, maybe not :/.

@jan-molak No idea :( ?

Trying to upgrade test to the pattern i was using from java Serenity but falling at first hurdle..
If I add Authenticate ability with:

 import { Ability,UsesAbilities } from 'serenity-js/protractor';

export class Authenticate implements Ability{
    username:string;
    password:string;

    static using(
      username :string,
      password: string
    ) {
        return new Authenticate(username,password);
    }
    static as(actor:UsesAbilities): Authenticate {
        return actor.abilityTo(Authenticate);
    }
    constructor ( username:string, password:string) {
       this.username = username;
       this.password = password; 
    }
}

I thought I should be able to use in a task

export class Login implements Task {


    @step('{0} logs into the application')
    performAs(actor: PerformsTasks): PromiseLike<void> {    // required by the Task interface
        return actor.attemptsTo(                            // delegates the work to lower-level tasks
          Open.browserOn(''),
            Click.on(HomePage.Login_Menu_Item),
            Enter.theValue(Authenticate.as(actor).username)
                .into(HomePage.User_Field),
            Enter.theValue(Authenticate.as(actor).password)
                 .into(HomePage.Pwd_Field),
            Click.on(HomePage.Login_Btn)
        );
    }


}

(works if I just insert the username password).
But, not actor is not same type. This has to be extremely common approach (I have cast of no. of actors so ability for authenticate seems good way to set their credentials up). I am also new to typescript so sorry about beginner questions.

okay, answering my own question by looking at the logic instead of looking for magic. In case anyone else is novice enough to run into this:
My task looks like:

import {HomePage} from '../components/homePage';
import { PerformsTasks, step, Task } from 'serenity-js/protractor';
import { Open,Click,Enter } from 'serenity-js/protractor';
import {Authenticate} from '../abilities/Authenticate'


export class Login implements Task {
    static withCredentials(username:string, password:string) {       // static method to improve the readability
        return new Login(username,password);
    }


    @step('{0} logs into the application')
    performAs(actor: PerformsTasks): PromiseLike<void> {    // required by the Task interface
        return actor.attemptsTo(                            // delegates the work to lower-level tasks
          Open.browserOn('index.html'),
            Click.on(HomePage.Login_Menu_Item),
            Enter.theValue(this.username)
                .into(HomePage.User_Field),
            Enter.theValue(this.password)
                 .into(HomePage.Pwd_Field),
            Click.on(HomePage.Login_Btn)
        );
    }

    constructor (private username:string, private password:string){
    }

}

and the step looks like:

       this.Given(/^that (.*) has authenticated$/, function (actorName: string) {
        let actor = this.stage.theActorCalled(actorName);
        return actor.attemptsTo(
            Login.withCredentials(Authenticate.as(actor).username,Authenticate.as(actor).password)
            )
       });

I hope noone wasted time on this.

Was this page helpful?
0 / 5 - 0 ratings