src/screenplay/interactions/Close.ts

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

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

/**
 * @desc
 *  Instructs the {@link @serenity-js/core/lib/screenplay/actor~Actor} to
 *  close browser tabs or windows.
 *
 * @example <caption>Closing a browser tab or window</caption>
 *  import { actorCalled } from '@serenity-js/core';
 *  import { BrowseTheWeb, Click, Close, Switch } from '@serenity-js/protractor';
 *  import { protractor } from 'protractor';
 *
 *  actorCalled('Caleb')
 *      .whoCan(BrowseTheWeb.using(protractor.browser))
 *      .attemptsTo(
 *          Click.on(someLinkThatOpensANewWindow),
 *
 *          Switch.toNewWindow().and(
 *              // perform activities in the context of the new window
 *              Close.currentWindow(),
 *          ),
 *      );
 *
 * @example <caption>Closing any new windows after a Jasmine test</caption>
 *  import 'jasmine';
 *
 *  import { actorInTheSpotlight } from '@serenity-js/core';
 *  import { Close } from '@serenity-js/protractor';
 *
 *  after(() =>
 *      actorInTheSpotlight().attemptsTo(
 *          Close.anyNewWindows(),
 *      ));
 *
 * @example <caption>Closing any new windows after a Mocha test</caption>
 *  import 'mocha';
 *
 *  import { actorInTheSpotlight } from '@serenity-js/core';
 *  import { Close } from '@serenity-js/protractor';
 *
 *  after(() =>
 *      actorInTheSpotlight().attemptsTo(
 *          Close.anyNewWindows(),
 *      ));
 *
 * @example <caption>Closing any new windows after a    Cucumber scenario</caption>
 *  import { actorInTheSpotlight } from '@serenity-js/core';
 *  import { Close } from '@serenity-js/protractor';
 *  import { After } from 'cucumber';
 *
 *  After(() =>
 *      actorInTheSpotlight().attemptsTo(
 *          Close.anyNewWindows(),
 *      ));
 *
 * @see {@link Switch}
 */
export class Close {

    /**
     * @desc
     *  Closes any windows other than the original one that
     *  the {@link @serenity-js/core/lib/screenplay/actor~Actor}
     *  has {@link Navigate}d to.
     *
     *  When the windows are closed, it switches the context
     *  back to the original window.
     *
     * @static
     * @returns {@link @serenity-js/core/lib/screenplay~Interaction}
     *
     * @see {@link Switch}
     */
    static anyNewWindows(): Interaction {
        return new CloseWindowsOtherThan(actor => BrowseTheWeb.as(actor).getOriginalWindowHandle(), `#actor closes any new windows`);
    }

    /**
     * @desc
     *  Closes the currently focused browser window.
     *
     *  **Please note** that this interaction should be used to close
     *  pop-up windows or any new windows/tabs opened during the test
     *  rather than the _main_ window, which is managed by Protractor.
     *
     *  See tests for usage examples.
     *
     * @static
     * @returns {@link @serenity-js/core/lib/screenplay~Interaction}
     *
     * @see {@link Switch}
     */
    static currentWindow(): Interaction {
        return new CloseCurrentWindow();
    }
}

/**
 * @package
 */
class CloseWindowsOtherThan extends Interaction {
    constructor(
        private readonly windowToKeepBy: (actor: UsesAbilities & AnswersQuestions) => Promise<string>,
        private readonly description: string = `#actor closes several windows`,
    ) {
        super();
    }

    performAs(actor: UsesAbilities & AnswersQuestions): PromiseLike<void> {
        return this.windowToKeepBy(actor)
            .then(windowToKeep => this.windowsOtherThan(windowToKeep, actor)
                .then(windowsToClose => this.closeAll(windowsToClose, actor))
                .then(() => this.switchTo(windowToKeep, actor)),
            );
    }

    toString(): string {
        return this.description;
    }

    /**
     * @param {string} windowToKeep
     * @param {UsesAbilities & AnswersQuestions} actor
     * @private
     */
    private windowsOtherThan(windowToKeep: string, actor: UsesAbilities & AnswersQuestions) {
        return BrowseTheWeb.as(actor).getAllWindowHandles()
            .then(allWindows =>
                this.isDefined(windowToKeep) && allWindows.length > 1
                    ? allWindows.filter(handle => handle !== windowToKeep)
                    : []
            )
    }

    /**
     * @param {string[]} windows
     * @param {UsesAbilities & AnswersQuestions} actor
     * @private
     */
    private closeAll(windows: string[], actor: UsesAbilities & AnswersQuestions) {
        return windows.reduce(
            (previous, handle) => {
                return previous
                    .then(() => BrowseTheWeb.as(actor).switchToWindow(handle))
                    .then(() => BrowseTheWeb.as(actor).closeCurrentWindow());
            },
            Promise.resolve()
        );
    }

    /**
     * @param {string} window
     * @param {UsesAbilities & AnswersQuestions} actor
     * @private
     */
    private switchTo(window: string, actor: UsesAbilities & AnswersQuestions): Promise<void> {
        return this.isDefined(window)
            ? BrowseTheWeb.as(actor).switchToWindow(window)
            : Promise.resolve();
    }

    /**
     * @param {any} value
     * @private
     */
    private isDefined(value: any) {
        return value !== undefined && value !== null;
    }
}

/**
 * @package
 */
class CloseCurrentWindow extends Interaction {
    performAs(actor: UsesAbilities & AnswersQuestions): PromiseLike<void> {
        return BrowseTheWeb.as(actor).closeCurrentWindow();
    }

    toString(): string {
        return `#actor closes current browser window`;
    }
}