src/screenplay/interactions/StartLocalServer.ts

import { Answerable, AnswersQuestions, CollectsArtifacts, Interaction, UsesAbilities } from '@serenity-js/core';

import { ManageALocalServer } from '../abilities';

/**
 * @desc
 *  Starts local server so that a test can interact with it.
 */
export class StartLocalServer {

    /**
     * @desc
     *  Starts local test server on a random available ports.
     *
     * @see {@link LocalServer.url}
     *
     * @returns {@serenity-js/core/lib/screenplay~Interaction}
     */
    static onRandomPort(): Interaction {
        return new StartLocalServerOnRandomPort();
    }

    /**
     * @desc
     *  Instructs the {@link @serenity-js/core/lib/screenplay/actor~Actor}
     *  to start a local test server on one of the preferred ports.
     *
     *  Please note: this method is kept for backwards compatibility. However, its behaviour has changed
     *  and is currently identical to calling `StartLocalServer.onPort` with the first of `preferredPorts`
     *  passed as an argument.
     *
     * @param {@serenity-js/core/lib/screenplay~Answerable<number[]>} preferredPorts
     *  A list of preferred ports. Please note that only the first one will be used!
     *
     * @see {@link LocalServer.url}
     * @see {@link StartLocalServer.onPort}
     * @see {@link StartLocalServer.onRandomPortBetween}
     *
     * @deprecated Use `StartLocalServer.onPort` or `StartLocalServer.onRandomPortBetween`
     *
     * @returns {@serenity-js/core/lib/screenplay~Interaction}
     */
    static onOneOfThePreferredPorts(preferredPorts: Answerable<number[]>): Interaction {
        return new StartLocalServerOnFirstOf(preferredPorts);
    }

    /**
     * @desc
     *  Instructs the {@link @serenity-js/core/lib/screenplay/actor~Actor}
     *  to start a local test server on a `preferredPort`, or a random one if that's not available.
     *
     * @param {@serenity-js/core/lib/screenplay~Answerable<number>} preferredPort
     *  preferred port
     *
     * @see {@link LocalServer.url}
     * @see {@link @serenity-js/core/lib/screenplay~Answerable}
     *
     * @returns {@serenity-js/core/lib/screenplay~Interaction}
     */
    static onPort(preferredPort: Answerable<number>): Interaction {
        return new StartLocalServerOnPort(preferredPort);
    }

    /**
     * @desc
     *  Instructs the {@link @serenity-js/core/lib/screenplay/actor~Actor}
     *  to start a local test server on a random port between `lowestPort` and `highestPort`.
     *
     * @param {@serenity-js/core/lib/screenplay~Answerable<number>} lowestPort
     *  Lower bound of the preferred port range
     *
     * @param {@serenity-js/core/lib/screenplay~Answerable<number>} highestPort
     *  Upper bound of the preferred port range
     *
     * @see {@link LocalServer.url}
     *
     * @returns {@serenity-js/core/lib/screenplay~Interaction}
     */
    static onRandomPortBetween(lowestPort: Answerable<number>, highestPort: Answerable<number>): Interaction {
        return new StartLocalServerOnRandomPortBetween(lowestPort, highestPort);
    }
}

/**
 * @package
 */
class StartLocalServerOnRandomPort extends Interaction {

    performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): Promise<void> {
        return ManageALocalServer.as(actor).listen();
    }

    toString(): string {
        return `#actor starts local server on a random port`;
    }
}

/**
 * @package
 */
class StartLocalServerOnPort extends Interaction {

    constructor(private readonly preferredPort: Answerable<number>) {
        super();
    }

    performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): Promise<void> {
        return actor.answer(this.preferredPort)
            .then(port => ManageALocalServer.as(actor).listen(port));
    }

    toString(): string {
        return `#actor starts local server on port ${ this.preferredPort }`;
    }
}

/**
 * @package
 */
class StartLocalServerOnRandomPortBetween extends Interaction {

    constructor(
        private readonly lowestPort: Answerable<number>,
        private readonly highestPort: Answerable<number>,
    ) {
        super();
    }

    performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): Promise<void> {
        return Promise.all([
            actor.answer(this.lowestPort),
            actor.answer(this.highestPort),
        ]).
        then(([lowestPort, highestPort]) => ManageALocalServer.as(actor).listen(lowestPort, highestPort));
    }

    toString(): string {
        return `#actor starts local server on port between ${ this.lowestPort } and ${ this.highestPort }`;
    }
}

/**
 * @package
 */
class StartLocalServerOnFirstOf extends Interaction {

    constructor(private readonly preferredPorts: Answerable<number[]>) {
        super();
    }

    performAs(actor: UsesAbilities & CollectsArtifacts & AnswersQuestions): Promise<void> {
        return actor.answer(this.preferredPorts)
            .then(preferredPorts => ManageALocalServer.as(actor).listen(preferredPorts[0]));
    }

    toString(): string {
        return `#actor starts local server on first port of ${ this.preferredPorts }`;
    }
}