如何使用两个参数创建任务?

How do I make a Task with two parameters?

在 Serenity-js 一书中,我们有一个只有一个参数的任务示例:

// 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
}

假设您可以添加 TodoItem 应该完成的日期。我们会收到一个日期参数,比如 'deadline'。我不知道该怎么做。

第一想法:

构造函数:

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

performAs:只需添加交互以输入截止日期

我们会有第二个静态方法。 并且可能调用的方法 return 会被更改。

感谢您的解释。

有几种方法可以做到这一点,具体取决于哪些参数是必需的,哪些是可选的,以及您希望任务拥有多少参数。

无参数

如果您有一个没有参数的任务,定义它的更简单方法是使用 Task.where factory function:

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

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

几乎 与使用下面的 class 样式定义相同,但代码少得多:

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

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

一个参数

您可以将上述方法用于应该接收一个参数的任务:

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

或者,您也可以按如下方式实现:

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

我发现第二个版本更加优雅,并且与 Click.onEnter.theValue 等内置交互更加一致,因为您会调用 Login.as 而不是比你的演员流程中的 LoginAs

N个参数

如果有超过 1 个参数,但所有参数都是必需的,并且您只是追求一个优雅的 DSL,您可以按如下方式扩展上述模式:

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),
    }),
}

然后您将调用上述任务:

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

这种设计不是特别灵活,因为它不允许您更改参数的顺序(即您不能说 Login.identifiedBy(password).as(username))或使某些参数可选,但为您提供了一个漂亮的 DSL,实现工作相对较少。

更大的灵活性

如果您需要更大的灵活性,例如在某些参数是可选的情况下,您可以选择 class 样式定义和准 builder pattern。 (我说 "quasi" 因为它不会改变对象,而是产生新的对象)。

例如,假设系统要求提供用户名,而密码可能是可选的。

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 }`;
    }
}

当然,您可以更进一步,将实例化任务的行为与任务本身分离,如果不同的任务差异足以证明单独的实现是合理的,这将很有用:

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 }`;
    }
}

以上两种实现都允许您将任务称为 Login.as(username)Login.as(username).identifiedBy(password),但是第一个实现使用空字符串的默认值作为密码,第二个实现则不甚至不要触摸密码字段。

希望对您有所帮助!

一月