src/screenplay/questions/Attribute.ts

import { Answerable, AnswersQuestions, Question, UsesAbilities } from '@serenity-js/core';
import { formatted } from '@serenity-js/core/lib/io';
import { ElementFinder } from 'protractor';

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

// todo: it might be better to swap the order of arguments
//  - Attribute.called('href').of(link) to make it work with ArrayListFilter

export class Attribute extends Question<Promise<string>> {
    /**
     * @param {Question<ElementFinder> | ElementFinder} target
     * @returns {AttributeBuilder}
     */
    static of(target: Question<ElementFinder> | ElementFinder): AttributeBuilder {
        return {
            called: (name: Answerable<string>) => new Attribute(target, name),
        };
    }

    constructor(
        private readonly target: Question<ElementFinder> | ElementFinder,
        private readonly name: Answerable<string>,
    ) {
        super(formatted `the value of the ${ name } attribute of ${ target}`);
    }

    /**
     * @desc
     *  Makes the provided {@link @serenity-js/core/lib/screenplay/actor~Actor}
     *  answer this {@link @serenity-js/core/lib/screenplay~Question}.
     *
     * @param {AnswersQuestions & UsesAbilities} actor
     * @returns {Promise<void>}
     *
     * @see {@link @serenity-js/core/lib/screenplay/actor~Actor}
     * @see {@link @serenity-js/core/lib/screenplay/actor~AnswersQuestions}
     * @see {@link @serenity-js/core/lib/screenplay/actor~UsesAbilities}
     */
    answeredBy(actor: AnswersQuestions & UsesAbilities): Promise<string> {
        return actor.answer(this.name)
            .then(name => withAnswerOf(actor, this.target, (elf: ElementFinder) =>
                elf.getAttribute(name).then(value => {
                    return value !== null               // workaround for bug in Chromium 91 - https://bugs.chromium.org/p/chromium/issues/detail?id=1205107&start=300
                        ? value
                        : BrowseTheWeb.as(actor).executeFunction(
                            /* istanbul ignore next */
                            function getAttribute(webElement, attributeName: string) {
                                // eslint-disable-next-line no-var
                                var value = (webElement[attributeName] || webElement.getAttribute(attributeName));
                                if (value !== null && value !== undefined) {
                                    return '' + value;
                                }
                                return value;
                            },
                            elf.getWebElement(),
                            name
                        );
                })
            ));
    }
}

interface AttributeBuilder {
    called(name: Answerable<string>): Attribute;
}