Dapangma
Dapangma

Reputation: 127

How do I make a Task with two parameters?

In the Serenity-js book we have one example of a Task with just one parameter :

// spec/screenplay/tasks/add_a_todo_item.ts
import { PerformsTasks, Task } from 'serenity-js/protractor';

export class AddATodoItem implements Task {

static called(itemName: string) {                       // static method to improve the readability
    return new AddATodoItem(itemName);
}

performAs(actor: PerformsTasks): PromiseLike<void> {    // required by the Task interface
    return actor.attemptsTo(                            // delegates the work to lower-level tasks
        // todo: interact with the UI
    );
}

constructor(private itemName: string) {                 // constructor assigning the name of the item
                                                        // to a private field
}

Imagine you can add a date the TodoItem should be done. We would receive a date parameter, say 'deadline'. I cannot figure out how to do it.

First thoughts:

constructor:

constructor(private itemName: string, private deadline: Date) {
}

performAs: just add the interaction to type the deadline

We would have a second static method. And possibly the called method return would be changed.

Thanks for your explanations.

Upvotes: 1

Views: 795

Answers (1)

Jan Molak
Jan Molak

Reputation: 4536

There are several ways to do it, depending on which parameters are mandatory, and which are optional, and how many of them you'd like the task to have.

No parameters

If you have a task with no parameters, the easier way to define it is using the Task.where factory function:

import { Task } from '@serenity-js/core';

const Login = () => Task.where(`#actor logs in`,
    Click.on(SubmitButton),
);

This is almost the same as using a class-style definition below, but with much less code:

class Login extends Task {
    performAs(actor: PerformsTasks) {
        return actor.attemptsTo( 
            Click.on(SubmitButton),
        );
    }

    toString() {
        return `#actor logs in`;
    }
}

One parameter

You can use the above approach with tasks that should receive one parameter:

const LoginAs = (username: string) => Task.where(`#actor logs in as ${ username }`, 
    Enter.theValue(username).into(UsernameField),
    Click.on(SubmitButton),
);

Which, alternatively, you could also implement as follows:

const Login = {
  as: (username: string) => Task.where(`#actor logs in as ${ username }`, 
      Enter.theValue(username).into(UsernameField),
      Click.on(SubmitButton),
  ),
}

I find this second version a bit more elegant and more consistent with the built-in interactions like Click.on, Enter.theValue, etc. since you'd be calling Login.as rather than LoginAs in your actor flow.

N parameters

If there are more than 1 parameters, but all of them are required and you're simply after an elegant DSL, you could extend the above pattern as follows:

const Login = {
    as: (username: string) => ({
        identifiedBy: (password: string) => Task.where(`#actor logs in as ${ username }`, 
            Enter.theValue(username).into(...),
            Enter.theValue(password).into(...),
            Click.on(SubmitButton),
    }),
}

You'd then invoke the above task:

actor.attemptsTo(
    Login.as(username).identifiedBy(password),
);

This design is not particularly flexible, as it doesn't allow you to change the order of parameters (i.e. you can't say Login.identifiedBy(password).as(username)) or make some of the parameters optional, but gives you a good-looking DSL with relatively little implementation effort.

More flexibility

If you require more flexibility, for example in a scenario where some parameters are optional, you might opt for the class-style definition and a quasi-builder pattern. (I say "quasi" because it doesn't mutate the object, but instead produces new objects).

For example, let's assume that while the system required the username to be provided, the password might be optional.

class Login extends Task { 
    static as(username: string) {
        return new Login(username);
    }

    identifiedBy(password: string {
        return new Login(this.username, password);
    }

    constructor(
        private readonly username: string,
        private readonly password: string = '',
    ) {
        super();
    }

    performAs(actor: PerformsTasks) {
        return actor.attemptsTo(
            Enter.theValue(username).into(...),
            Enter.theValue(password).into(...),
            Click.on(SubmitButton),
        );
    }

    toString() {
        return `#actor logs in as ${ this.username }`;
    }
}

You can, of course, take it even further and decouple the act of instantiating the task from the task itself, which is useful if the different tasks are different enough to justify separate implementations:

export class Login { 
    static as(username: string) {
        return new LoginWithUsernameOnly(username);
    }
}

class LoginWithUsernameOnly extends Task {

    constructor(
        private readonly username: string,
    ) {
        super();
    }

    identifiedBy(password: string {
        return new LoginWithUsernameAndPassword(this.username, password);
    }


    performAs(actor: PerformsTasks) {
        return actor.attemptsTo(
            Enter.theValue(username).into(...),
            Click.on(SubmitButton),
        );
    }

    toString() {
        return `#actor logs in as ${ this.username }`;
    }
}

class LoginWithUsernameAndPassword extends Task {

    constructor(
        private readonly username: string,
        private readonly username: string,
    ) {
        super();
    }


    performAs(actor: PerformsTasks) {
        return actor.attemptsTo(
            Enter.theValue(this.username).into(...),
            Enter.theValue(this.password).into(...),
            Click.on(SubmitButton),
        );
    }

    toString() {
        return `#actor logs in as ${ this.username }`;
    }
}

Both the above implementations allow you to call the task as Login.as(username) and Login.as(username).identifiedBy(password), but while the first implementation uses a default value of an empty string for the password, the second implementation doesn't even touch the password field.

I hope this helps!

Jan

Upvotes: 1

Related Questions