AbstractLoginElement.java

package org.vaadin.addons.dramafinder.element;

import com.microsoft.playwright.Locator;
import org.vaadin.addons.dramafinder.element.shared.HasEnabledElement;
import org.vaadin.addons.dramafinder.element.shared.HasStyleElement;
import org.vaadin.addons.dramafinder.element.shared.HasThemeElement;

import static com.microsoft.playwright.assertions.PlaywrightAssertions.assertThat;

/**
 * Base abstraction shared by {@code <vaadin-login-form>} and
 * {@code <vaadin-login-overlay>}.
 * <p>
 * Both components render the very same login form: the username and password
 * fields, the submit button and the forgot password button live in the light DOM
 * of the host element, while the title, the error message and the footer are
 * parts rendered inside the internal {@code <vaadin-login-form-wrapper>}. Every
 * visible string comes from the component's i18n object, so the assertion helpers
 * take the expected text as a parameter instead of hard-coding the English
 * defaults.
 */
public abstract class AbstractLoginElement extends VaadinElement
        implements HasEnabledElement, HasStyleElement, HasThemeElement {

    /** Id of the username field rendered by the login component. */
    public static final String USERNAME_FIELD_ID = "vaadinLoginUsername";

    /** Id of the password field rendered by the login component. */
    public static final String PASSWORD_FIELD_ID = "vaadinLoginPassword";

    /** Tag name of the internal wrapper holding the title, error and footer parts. */
    public static final String FORM_WRAPPER_TAG_NAME = "vaadin-login-form-wrapper";

    /** Shadow part holding the login form title. */
    protected static final String FORM_TITLE_PART = "form-title";

    /** Shadow part holding the error message, hidden while there is no error. */
    protected static final String ERROR_MESSAGE_PART = "error-message";

    /** Shadow part holding the error message title. */
    protected static final String ERROR_MESSAGE_TITLE_PART = "error-message-title";

    /** Shadow part holding the error message description. */
    protected static final String ERROR_MESSAGE_DESCRIPTION_PART = "error-message-description";

    /** Shadow part holding the footer (additional information). */
    protected static final String FOOTER_PART = "footer";

    /**
     * Creates a new {@code AbstractLoginElement}.
     *
     * @param locator the locator pointing at the component root element
     */
    public AbstractLoginElement(Locator locator) {
        super(locator);
    }

    /**
     * Get the username field of the login form.
     *
     * @return the {@code TextFieldElement} for the username
     */
    public TextFieldElement getUsernameField() {
        return new TextFieldElement(getLocator().locator("#" + USERNAME_FIELD_ID));
    }

    /**
     * Get the password field of the login form.
     *
     * @return the {@code PasswordFieldElement} for the password
     */
    public PasswordFieldElement getPasswordField() {
        return new PasswordFieldElement(getLocator().locator("#" + PASSWORD_FIELD_ID));
    }

    /**
     * Get the submit button of the login form.
     * <p>
     * Resolved through the {@code submit} slot rather than its text, because the
     * caption is i18n driven. The {@code xpath} lookup is deliberate: it does not
     * pierce the shadow root, where a {@code <slot name="submit" slot="submit">}
     * would otherwise match as well.
     *
     * @return the {@code ButtonElement} submitting the form
     */
    public ButtonElement getSubmitButton() {
        return new ButtonElement(
                getLocator().locator("xpath=./" + ButtonElement.FIELD_TAG_NAME + "[@slot='submit']"));
    }

    /**
     * Get the forgot password button of the login form.
     * <p>
     * The button is present but hidden when the component sets
     * {@code no-forgot-password}.
     *
     * @return the {@code ButtonElement} for the forgot password action
     */
    public ButtonElement getForgotPasswordButton() {
        return new ButtonElement(
                getLocator().locator("xpath=./" + ButtonElement.FIELD_TAG_NAME + "[@slot='forgot-password']"));
    }

    /**
     * Fill in the credentials and submit the form.
     *
     * @param username the username to type into the username field
     * @param password the password to type into the password field
     */
    public void login(String username, String password) {
        getUsernameField().setValue(username);
        getPasswordField().setValue(password);
        getSubmitButton().click();
    }

    /**
     * Locator for the login form title.
     *
     * @return the locator for the {@code form-title} part
     */
    public Locator getTitleLocator() {
        return getPartLocator(FORM_TITLE_PART);
    }

    /**
     * Assert that the login form title matches the expected text.
     *
     * @param title the expected title
     */
    public void assertTitle(String title) {
        assertThat(getTitleLocator()).hasText(title);
    }

    /**
     * Locator for the error message container.
     * <p>
     * The container is always in the DOM and is hidden while the component's
     * {@code error} property is {@code false}.
     *
     * @return the locator for the {@code error-message} part
     */
    public Locator getErrorLocator() {
        return getPartLocator(ERROR_MESSAGE_PART);
    }

    /**
     * Locator for the error message title.
     *
     * @return the locator for the {@code error-message-title} part
     */
    public Locator getErrorTitleLocator() {
        return getPartLocator(ERROR_MESSAGE_TITLE_PART);
    }

    /**
     * Locator for the error message description.
     *
     * @return the locator for the {@code error-message-description} part
     */
    public Locator getErrorMessageLocator() {
        return getPartLocator(ERROR_MESSAGE_DESCRIPTION_PART);
    }

    /**
     * Whether the error message is currently shown.
     *
     * @return {@code true} when the error message is visible
     */
    public boolean isErrorVisible() {
        return getErrorLocator().isVisible();
    }

    /** Assert that the error message is shown. */
    public void assertErrorVisible() {
        assertThat(getErrorLocator()).isVisible();
    }

    /** Assert that no error message is shown. */
    public void assertNoError() {
        assertThat(getErrorLocator()).isHidden();
    }

    /**
     * Assert that the error message title matches the expected text.
     *
     * @param title the expected error title
     */
    public void assertErrorTitle(String title) {
        assertThat(getErrorTitleLocator()).hasText(title);
    }

    /**
     * Assert that the error message description matches the expected text.
     *
     * @param message the expected error message
     */
    public void assertErrorMessage(String message) {
        assertThat(getErrorMessageLocator()).hasText(message);
    }

    /**
     * Locator for the footer, holding the additional information text.
     *
     * @return the locator for the {@code footer} part
     */
    public Locator getFooterLocator() {
        return getPartLocator(FOOTER_PART);
    }

    /**
     * Assert that the footer contains the expected additional information.
     *
     * @param additionalInformation the expected footer text
     */
    public void assertAdditionalInformation(String additionalInformation) {
        assertThat(getFooterLocator()).hasText(additionalInformation);
    }

    /**
     * Whether the login form is disabled.
     * <p>
     * The component disables itself while a login is in flight and re-enables
     * itself once an error is reported.
     *
     * @return {@code true} when the {@code disabled} property is set
     */
    public boolean isDisabled() {
        return Boolean.TRUE.equals(getProperty("disabled"));
    }

    /**
     * {@inheritDoc}
     * <p>
     * The {@code disabled} property is not reflected to an attribute on the host,
     * so the enabled state is asserted on the submit button, which mirrors it.
     */
    @Override
    public Locator getEnabledLocator() {
        return getSubmitButton().getLocator();
    }

    /**
     * Locator for the internal {@code <vaadin-login-form-wrapper>} rendering the
     * title, the error message and the footer.
     *
     * @return the locator for the form wrapper
     */
    protected Locator getFormWrapperLocator() {
        return getLocator().locator(FORM_WRAPPER_TAG_NAME);
    }

    /**
     * Locator for a shadow part of the form wrapper.
     * <p>
     * The lookup is scoped to the wrapper on purpose: the username and password
     * fields are slotted into it from the light DOM and expose parts of their own
     * (an {@code error-message} among them), but they are not DOM descendants of
     * the wrapper, so scoping keeps the lookup unambiguous.
     *
     * @param part the part name
     * @return the locator for the given part
     */
    protected Locator getPartLocator(String part) {
        return getFormWrapperLocator().locator("[part='" + part + "']");
    }
}