4

A h:selectOneRadio results in <input type="radio"> in a table and p:selectOneRadio in <input type="radio"> in a table with some divs around the input. The id for both is [form id]:[selectOneRadio id]:[option number] which I can use successfully for the plain JSF in a Graphene functional test when accessing it with @FindBy(id="[...]") whereas the PrimeFaces variant fails due to org.openqa.selenium.ElementNotInteractableException. Investigating the generated HTML I don't see the difference

<html xmlns="http://www.w3.org/1999/xhtml">

<head>
  <link type="text/css" rel="stylesheet" href="/34696ceb-eeaa-4b35-88dd-f3c8fc5901bf/javax.faces.resource/theme.css.xhtml?ln=primefaces-aristo">
  <link type="text/css" rel="stylesheet" href="/34696ceb-eeaa-4b35-88dd-f3c8fc5901bf/javax.faces.resource/primefaces.css.xhtml;jsessionid=48ca919d0b7e89661f92149ac321?ln=primefaces&amp;v=5.0">
  <script type="text/javascript" src="/34696ceb-eeaa-4b35-88dd-f3c8fc5901bf/javax.faces.resource/jquery/jquery.js.xhtml;jsessionid=48ca919d0b7e89661f92149ac321?ln=primefaces&amp;v=5.0"></script>
  <script type="text/javascript" src="/34696ceb-eeaa-4b35-88dd-f3c8fc5901bf/javax.faces.resource/primefaces.js.xhtml;jsessionid=48ca919d0b7e89661f92149ac321?ln=primefaces&amp;v=5.0"></script>
  <title>Facelet Title</title>
</head>

<body>
  <form id="mainForm" name="mainForm" method="post" action="/34696ceb-eeaa-4b35-88dd-f3c8fc5901bf/index.xhtml;jsessionid=48ca919d0b7e89661f92149ac321" enctype="application/x-www-form-urlencoded">
    <input name="mainForm" value="mainForm" type="hidden">
    <table id="mainForm:mainSelectOneRadio">
      <tbody>
        <tr>
          <td>
            <input name="mainForm:mainSelectOneRadio" id="mainForm:mainSelectOneRadio:0" value="a" type="radio">
            <label for="mainForm:mainSelectOneRadio:0"> a</label>
          </td>
          <td>
            <input name="mainForm:mainSelectOneRadio" id="mainForm:mainSelectOneRadio:1" value="b" type="radio">
            <label for="mainForm:mainSelectOneRadio:1"> b</label>
          </td>
          <td>
            <input name="mainForm:mainSelectOneRadio" id="mainForm:mainSelectOneRadio:2" value="c" type="radio">
            <label for="mainForm:mainSelectOneRadio:2"> c</label>
          </td>
        </tr>
      </tbody>
    </table>
    <table id="mainForm:mainSelectOneRadioPrime" class="ui-selectoneradio ui-widget">
      <tbody>
        <tr>
          <td>
            <div class="ui-radiobutton ui-widget">
              <div class="ui-helper-hidden-accessible">
                <input id="mainForm:mainSelectOneRadioPrime:0" name="mainForm:mainSelectOneRadioPrime" value="aPrime" type="radio">
              </div>
              <div class="ui-radiobutton-box ui-widget ui-corner-all ui-state-default"><span class="ui-radiobutton-icon ui-icon ui-icon-blank"></span>
              </div>
            </div>
          </td>
          <td>
            <label for="mainForm:mainSelectOneRadioPrime:0">aPrime</label>
          </td>
          <td>
            <div class="ui-radiobutton ui-widget">
              <div class="ui-helper-hidden-accessible">
                <input id="mainForm:mainSelectOneRadioPrime:1" name="mainForm:mainSelectOneRadioPrime" value="bPrime" type="radio">
              </div>
              <div class="ui-radiobutton-box ui-widget ui-corner-all ui-state-default"><span class="ui-radiobutton-icon ui-icon ui-icon-blank"></span>
              </div>
            </div>
          </td>
          <td>
            <label for="mainForm:mainSelectOneRadioPrime:1">bPrime</label>
          </td>
          <td>
            <div class="ui-radiobutton ui-widget">
              <div class="ui-helper-hidden-accessible">
                <input id="mainForm:mainSelectOneRadioPrime:2" name="mainForm:mainSelectOneRadioPrime" value="cPrime" type="radio">
              </div>
              <div class="ui-radiobutton-box ui-widget ui-corner-all ui-state-default"><span class="ui-radiobutton-icon ui-icon ui-icon-blank"></span>
              </div>
            </div>
          </td>
          <td>
            <label for="mainForm:mainSelectOneRadioPrime:2">cPrime</label>
          </td>
        </tr>
      </tbody>
    </table>
    <input name="javax.faces.ViewState" id="j_id1:javax.faces.ViewState:0" value="-485558793831512050:990657069126697889" autocomplete="off" type="hidden">
  </form>
</body>

</html>

nor do I if I deploy the application on Payara 4.1.2 or any other reason for the ElementNotInteractableException.

The access is done with

@RunWith(Arquillian.class)
public class MyManagedBeanTest {
    private static final String WEBAPP_SRC = "src/main/webapp";
    private final static Logger LOGGER = LoggerFactory.getLogger(MyManagedBeanTest.class);

    @Deployment(testable = false)
    public static Archive<?> createDeployment0() throws TransformerException, XPathExpressionException, ParserConfigurationException, SAXException, IOException {
        WebArchive retValue = ShrinkWrap.create(WebArchive.class)
                .add(EmptyAsset.INSTANCE, "beans.xml")
                .addClasses(MyManagedBean.class)
                .addAsWebInfResource(
                        new StringAsset("<faces-config version=\"2.0\"/>"),
                        "faces-config.xml");
        Maven.configureResolver().workOffline().resolve("richtercloud:graphene-click-input-radio:war:1.0-SNAPSHOT").withoutTransitivity().asList(JavaArchive.class).forEach(dependency -> retValue.addAsLibrary(dependency));
        //add all webapp resources
        retValue.merge(ShrinkWrap.create(GenericArchive.class)
                .as(ExplodedImporter.class)
                .importDirectory(WEBAPP_SRC)
                .as(GenericArchive.class), "/", Filters.include(".*\\.(xhtml|css|js|png)$"));

        ByteArrayOutputStream archiveContentOutputStream = new ByteArrayOutputStream();
        retValue.writeTo(archiveContentOutputStream, Formatters.VERBOSE);
        LOGGER.info(archiveContentOutputStream.toString());
        return retValue;
    }

    @Drone
    private WebDriver browser;
    @ArquillianResource
    private URL deploymentUrl;
    @FindBy(id = "mainForm:mainSelectOneRadio:0")
    private WebElement mainSelectOneRadioOption0;
    @FindBy(id = "mainForm:mainSelectOneRadioPrime:0")
    private WebElement mainSelectOneRadioPrimeOption0;

    @Test
    public void testAll() {
        browser.get(deploymentUrl.toExternalForm()+"index.xhtml");
        LOGGER.debug(browser.getPageSource());
        mainSelectOneRadioOption0.click();
        mainSelectOneRadioPrimeOption0.click();
    }
}

I'm searching for a solution which triggers JSF action methods and AJAX listeners!

I'd be interested in a generic approach as well, e.g. p:selectOneButton produces

<div id="mainForm:mainSelectOneButtonPrime" class="ui-selectonebutton ui-buttonset ui-widget ui-corner-all">
    <div class="ui-button ui-widget ui-state-default ui-button-text-only ui-corner-left">
        <input id="mainForm:mainSelectOneButtonPrime:0" name="mainForm:mainSelectOneButtonPrime" value="aPrime" class="ui-helper-hidden" type="radio">
        <span class="ui-button-text ui-c">aPrime</span>
    </div>
    <div class="ui-button ui-widget ui-state-default ui-button-text-only">
        <input id="mainForm:mainSelectOneButtonPrime:1" name="mainForm:mainSelectOneButtonPrime" value="bPrime" class="ui-helper-hidden" type="radio">
        <span class="ui-button-text ui-c">bPrime</span>
    </div>
    <div class="ui-button ui-widget ui-state-default ui-button-text-only ui-corner-right">
        <input id="mainForm:mainSelectOneButtonPrime:2" name="mainForm:mainSelectOneButtonPrime" value="cPrime" class="ui-helper-hidden" type="radio">
        <span class="ui-button-text ui-c">cPrime</span>
    </div>
</div>
<input name="javax.faces.ViewState" id="j_id1:javax.faces.ViewState:0" value="-5130093024933213812:2291815208147638618" autocomplete="off" type="hidden">

which doesn't seem to have anything in common with the HTML generated for p:selectOneRadio at first sight. Maybe there's a trick.

I'm using PrimeFaces 6.1.

Kalle Richter
  • 8,008
  • 26
  • 77
  • 177
  • 1
    Sure you click on the right elements? With PF ypu need to click om different things than with plain jsf since the orininal inputs are wrapped... – Kukeltje Nov 04 '17 at 07:56
  • There're only `div`s and `input`s involved and only clicking on `input` makes sense. – Kalle Richter Nov 04 '17 at 16:09
  • Do you mean the element with `id = "mainForm:mainSelectOneRadioPrime:0"`? – Guy Mar 28 '18 at 12:27
  • @Guy the generated HTML element with the id `mainForm:mainSelectOneRadioPrime:0` is the one I want to click on programmatically in the functional test. It's represented by `mainSelectOneRadioPrimeOption0` in the code above afaik. – Kalle Richter Mar 28 '18 at 12:38
  • If you have thought on improving the testability of PrimeFaces, don't hesitate to contribute at https://stackoverflow.com/questions/46765542/how-to-access-primefaces-components-through-graphene-in-the-most-portable-way. – Kalle Richter Apr 02 '18 at 10:12

2 Answers2

4

ui-helper-hidden-accessible is a JQuery layout helper to hide items visually. The radio button you see is actually the parent element, <div class="ui-radiobutton ui-widget">.

The problem can be resolved by clicking on the radio button label. The 'for` attribute put the focus on the input label associated with it

@FindBy(css = "[for='mainForm:mainSelectOneRadioPrime:0']")
private WebElement mainSelectOneRadioPrimeOption0;
Kalle Richter
  • 8,008
  • 26
  • 77
  • 177
Guy
  • 46,488
  • 10
  • 44
  • 88
  • That works, thank you. Any change you could give me a hint how click on an option produced by `f:selectItems` inside a `p:selectOneButton`. The generated HTML doesn't contain `label`s. – Kalle Richter Mar 28 '18 at 14:16
  • I figured it out based on the answer by FlorentB.: `@FindBy(xpath = "id('mainForm:mainSelectOneButtonPrime:0')/../span")`. Thanks for your support. – Kalle Richter Mar 28 '18 at 16:18
  • I'll probably give the bounty to you and the correct answer to Florent B. since his explanation helped me a lot as well - your answer is correct as well, it's only about the points I have to give. – Kalle Richter Mar 28 '18 at 16:21
  • @KarlRichter `@FindBy(css = "[id='mainForm:mainSelectOneButtonPrime:1'] + span")` should also work – Guy Mar 28 '18 at 16:24
3

@Guy has the right answer.

The issue here is that PrimeFaces is applying its own styling on top of the HTML which is covering the element you are trying to click.

Selenium checks that the targeted element mainForm:mainSelectOneRadio:0 receives the events when the element on top is clicked. But in this case the overlay is done with a sibling container which is not an descendant of targeted element. Thus Selenium assumes that the element will not receive the events and raises an ElementNotInteractableException (see event bubbling and propagation).

You can clearly see the issue by visiting oneRadio.xhtml and by inspecting the radio button with a right click. You'll see that the selected DOM element and the <input> are located in two different branches of the DOM tree.

To overcome this issue, either click the label since it has no overlay (see solution from @Guy). The label has the for attribute which mean that all the events are forwarded to the element assigned to for which is the targeted <input>.

You could also directly click the overlay. Though, you'll have to use an XPath to express the relationship.

Parent of parent of the targeted <input> :

    @FindBy(xpath = "id('mainForm:mainSelectOneRadioPrime:2')/../..")  
    private WebElement mainSelectOneRadioPrimeOption0;

Or first <td> having the targeted <input> :

    @FindBy(xpath = "//td[.//input[@id='mainForm:mainSelectOneRadioPrime:2']]")
    private WebElement mainSelectOneRadioPrimeOption0;
Florent B.
  • 41,537
  • 7
  • 86
  • 101
  • 1
    Thanks for this good answer. It'll improve my understanding of Selenium beyond the scope of this question. It's unfortunate that there's no library/extension wrapping all this logic and allowing to do `PrimeFacesSelectOneRadio (extends WebElement).getSelectedOption()`. See comment on Guy's answer about the distribution of points. – Kalle Richter Mar 28 '18 at 16:35