Search code examples
seleniumjsfprimefacesjsf-2graphene2

How to check a p:selectBooleanCheckbox and wait for it to be checked in Graphene/Selenium?


Similar to Why can I click an input with type=radio of a h:selectOneRadio, but not one of a p:selectOneRadio with Graphene/Selenium? I'd like to know how I can check a p:selectBooleanCheckbox in a functional tests with Arquillian and Selenium/Graphene and wait for it to be checked.

I already figured out

  • that in plain HTML the checked state is determined not by the value of the checked attribute, but by its presence and that in XHTML (where the attribute needs to be always present) by the value checked (not true or false, but why make a new standard intuitive...), see https://www.w3schools.com/tags/att_input_checked.asp for details.
  • that the Selenium documentation can be misleading in some cases (I'd say even wrong) since it doesn't distinguish between "" and null for example in some cases, like ExpectedConditions.attributeToBe(WebElement, String, String) and doesn't mention it - and then the request to improve it get closed with a hint that the Javadoc is accepted to be wrong because everything is well documented in the W3C web driver spec.
  • that WebElement.click (where the WebElement instance refers to the id of p:selectBooleanCheckbox works in a minimal environment, but not in a complex one.

The checkbox is checked when I investigate visually with Firefox after the checkbox is clicked with WebElement.click on the @FindBy(id="mainForm:mainCheckbox") element declared in JSF:

<h:form id="mainForm">
    <p:selectBooleanCheckbox id="mainCheckbox"
                             value="#{managedBeanView.property0}"/>
</h:form>

I tried:

  • to investigate the checked attribute of the generated div which simply always is present (or maybe not, see trouble with Selenium documentation above)
  • wait for the checked attribute of the nested HTML _input which never the attribute checked apparently (or maybe not, see above).

Solution

  • There's the pretty awesome arquillian-primefaces project now which provides a one-liner. In case you can't use this:

    /**
     * Checks a {@code p:selectBooleanCheckbox} and waits for it to be checked.
     * The method works for XHTML elements only and does not check whether the
     * passed {@code checkbox} is declared in XHTML.
     *
     * The handling of Primefaces in functional testing with Selenium/Graphene
     * is extremely difficult because the behaviour of elements is not
     * documented in a usable way (either extremely complex for Primefaces
     * developers or not at all), so that the following behaviour has been
     * extracted:<ul>
     * <li>The checked state of HTML input element which is a checkbox is
     * determined by the presence of the {@code checked} attribute and not its
     * value. In XHTML where the attribute has to be always present its the
     * value {@code checked} of the attribute .See
     * {@link https://www.w3schools.com/tags/att_input_checked.asp} for details.
     * </li>
     * <li>The {@code checked} attribute on {@code checkbox} is always
     * {@code null} and
     * {@code ExpectedConditions.attributeToBe(checkbox, "checked", "")} is
     * always {@code true} (Selenium doesn't document whether it distinguishes
     * between {@code ""} and {@code null} and refused all suggestions on
     * improving the documentation (see e.g.
     * https://github.com/SeleniumHQ/selenium/issues/5649)).</li>
     * <li>{@link WebElement#click() } works in a minimal environment (the
     * checked state appears visually) whereas it doesn't work in most real
     * world conditions.</li>
     * </ul>
     *
     * Asked
     * https://stackoverflow.com/questions/46765542/how-to-access-primefaces-components-through-graphene-in-the-most-portable-way
     * for a general solution.
     *
     * @param browser the web driver to use
     * @param checkbox the web element representing the {@code p:selectBooleanCheckbox}
     * @param checked whether to check or uncheck the checkbox
     */
    public void checkCheckbox(WebDriver browser,
            WebElement checkbox,
            boolean checked) {
        assert checkbox.getAttribute("id") != null
                && !checkbox.getAttribute("id").isEmpty();
        String checkboxInputId = String.format("%s_input",
                checkbox.getAttribute("id"));
        final WebElement checkboxInputElement = checkbox.findElements(By.xpath(String.format("id('%s')/div[2]/span",
                        checkbox.getAttribute("id"))))
                .stream().collect(MoreCollectors.onlyElement());
        if(checked && !isCheckboxChecked(browser, checkboxInputId)
                || !checked && isCheckboxChecked(browser, checkboxInputId)) {
            LOGGER.trace("checkCheckbox: need to click");
            LOGGER.trace(String.format("checkboxInputElement.selected: %s",
                    isCheckboxChecked(browser, checkboxInputId)));
            checkboxInputElement.click();
            LOGGER.trace(String.format("checkboxInputElement.selected: %s",
                    isCheckboxChecked(browser, checkboxInputId)));
            //- cannot invoke `checkboxInputElement.click` because the element
            //is not visible
            //- cannot invoke `checkboxInputElement.isSelected` or
            //`checkbox.isSelected` (causes
            //org.openqa.selenium.ElementNotSelectableException)
        }
        new WebDriverWait(browser, 5).until(
            (WebDriver t) -> {
                boolean retValue0 = isCheckboxChecked(browser,
                        checkboxInputId) == checked;
                return retValue0;
            });
            //waiting for the checked attribute is no guarantee that the check
            //will be visible on the screenshot
    }
    
    private boolean isCheckboxChecked(WebDriver browser,
            String checkboxInputId) {
        boolean retValue = (Boolean) ((JavascriptExecutor)browser).executeScript(String.format("return document.getElementById('%s').checked;",
                        checkboxInputId));
        return retValue;
    }
    

    I figured out the right elements using the Selenium IDE which will probably help in all other similar situations.