Skip to content

Conversation

dunglas
Copy link
Contributor

@dunglas dunglas commented Mar 13, 2018

This PR adds an utility class to easily manipulate checkboxes and radio buttons.

Imagine the following form:

<form id="my-form" method="POST">
    <input type="checkbox" name="c" value="1">

    <label>
        Checkbox Foo

        <input type="checkbox" name="c" value="2">
    </label>

    <input type="radio" name="r" value="x">

    <label>
        Radio Foo

        <input type="radio" name="r" value="y">
    </label>

    <input type="submit" value="OK">
</form>

<label for="c1">Checkbox Bar</label>
<input type="checkbox" form="my-form" name="c" value="3" id="c1">

<label for="r1">Radio Bar</label>
<input type="radio" form="my-form" name="r" value="z" id="r1">

The proposed class allows to interact with it in a smooth way:

$checkbox = new WebDriverCheckbox($this->driver->findElement(WebDriverBy::xpath('//input[@type="checkbox"]'))); // select any checkbox
$radio = new WebDriverRadio($this->driver->findElement(WebDriverBy::xpath('//input[@type="radio"]'))); // Select any radio button

var_dump($checkbox->getOptions()); // 1, 2, 3
var_dump($radio->getOptions()); // x, y, z

$checkbox->selectByValue('1');
$checkbox->selectByVisibleText('Checkbox Foo');
$checkbox->selectByVisiblePartialText('Bar');
$checkbox->deselectByVisiblePartialText('Bar');

$radio->selectByValue('a');
$radio->selectByIndex(1);

var_dump($checkbox->getAllSelectedOptions()); // 1, 2
var_dump($radio->getAllSelectedOptions()); // b

var_dump($radio->getFirstSelectedOption()); // b

$checkbox->deselectAll());

Features:

  • Checkbox and radio buttons support
  • Ability to select an element by its label (full or partial matching)
  • Ability to select an element by its (pseudo) 0-based index

For consistency and convenience - especially when using libraries such as Symfony Form that allow to switch from checkboxes to selects in 1 line - the new class mimics the public API of WebDriverSelect and implements the WebDriverSelectInterface.

Closes #373.

}

try {
// Since the mechanism of getting the text in xpath is not the same as
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure that this case really occurs. But as the WebDriverSelect already does that, I've kept this behavior.

Copy link
Collaborator

@OndraM OndraM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @dunglas , this is such a great contribution! I've done some more in-depth code-review, hope you don't mind :-). Would be fantastic if you could have look at it.

Thanks again!

/**
* Provides helper methods for checkboxes and radio buttons.
*
* @author Kévin Dunglas <dunglas@gmail.com>
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't use these @author annotation in other parts of php-webdriver :).


public function __construct(WebDriverElement $element)
{
if ('input' !== $tagName = $element->getTagName()) {
Copy link
Collaborator

@OndraM OndraM Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please swap all of yoda conditions used (here and on all other places)? They are not used in the codebase and are IMO making the code harder to read.

}

/**
* @expectedException \Facebook\WebDriver\Exception\NoSuchElementException
Copy link
Collaborator

@OndraM OndraM Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please change this annotation (here and below) to $this->expectException(NoSuchElementException::class) and also add expectedExceptionMessage (both placed on the lines right before the exception should occur)?

The expectedException annotations are now not considered as being the best practice in PHPUnit.


$xpath = 'ancestor::label';
$xpathNormalize = sprintf('%s[%s]', $xpath, $normalizeFilter);
if (null !== $id = $element->getAttribute('id')) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This kind of conditions combined with variable assigning is really hard to read - could you please expand them (also on other places)? I know it is longer, but also much more readable IMO.

return $this->getRelatedElements();
}

public function getAllSelectedOptions()
Copy link
Collaborator

@OndraM OndraM Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, look like the interface is a bit too specific. I may change it to something like WebDriverSelectableIterface, and rename this to getAllSelected or something like that in the future - because the keyword "option" is related to <option> tags in selects :].

For now I suggest at least renaming the variable(s) to $selected or $selectedElements so that thet don't suggest you are dealing with <options>. What do you think?

Copy link
Contributor Author

@dunglas dunglas Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I definitely agree, the interface is too specific, but it's convenient to have a common one. Introducing the new one can even be done (in another PR?) without breaking changes.

}
}

throw new NoSuchElementException('No options are selected');
Copy link
Collaborator

@OndraM OndraM Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe rather No checkboxes are selected? (supposing the class is split for radios and checkboxes)

}

try {
$element->findElement(WebDriverBy::xpath($xpathNormalize));
Copy link
Collaborator

@OndraM OndraM Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add some short comment about the logic there? (Why first trying to find element this way and why in different way inside the catch block?)

Copy link
Contributor Author

@dunglas dunglas Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See #545 (comment)
It mimics the behavior of WebDriverSelect but I don't even know if it's really necessary. I propose to remove this for now, and we'll add it back if someone reports an issue.

*
* @author Kévin Dunglas <dunglas@gmail.com>
*/
class WebDriverCheckbox implements WebDriverSelectInterface
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suggest splitting the class in two (Checkbox and Radio), because:

  • the semantic is confusing ($radiobutton = new WebDriverCheckbox($radibuttonElement))
  • there are lot of $this->isMultiple() conditionals, which separates logic for checkboxes and radiobuttons

Maybe some common abstract class could be introduced to DRY the logics. What do you think?

<input type="checkbox" name="j2" value="j2a">

<label>
J2B
Copy link
Collaborator

@OndraM OndraM Mar 14, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make sure the normalize-space() selectors work as expected, there could also be a case where the visible text has multiple spaces inside, ie. not being just one word, like:

<label>
   J     2 

  B    
...
 

What are you thoughts?

}

if (null === $name = $element->getAttribute('name')) {
throw new WebDriverException('The input have a "name" attribute.');
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does not have?

@dunglas dunglas force-pushed the checkbox branch 2 times, most recently from d43ed46 to 40439f0 Compare March 14, 2018 22:20
@dunglas
Copy link
Contributor Author

dunglas commented Mar 14, 2018

@OndraM all comments fixed.
CS differences was because I followed Symfony ones.

@dunglas dunglas changed the title Add an utility class to interact with checkboxes and radio buttons Add utility classes to interact with checkboxes and radio buttons Mar 14, 2018
@OndraM
Copy link
Collaborator

OndraM commented Mar 28, 2018

@dunglas Sorry for delays, I will review this soon.

Copy link
Collaborator

@OndraM OndraM left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it on some webpages, it looks really great! Just a few remaining suggestions from me.

Thank you very much :)

$this->element = $element;
}

public function isMultiple()
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about omitting the method implementation here (should be possible, abstract class does not have to fulfill the WebDriverSelectInterface interface) and let the descendant decide (ie. return true in WebDriverCheckbox and return false in WebDriverRadio).

*/
abstract class AbstractWebDriverCheckboxOrRadio implements WebDriverSelectInterface
{
protected $element;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please add (one-line) phpdocs? /** @var WebDriverElement */ etc.

}
}

return null;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the interface (and WebDriverSelect behavior), this should return WebDriverElement or throw NoSuchElementException.

I know this is done in descendants, but in PHP 7 this would fail static analysis. And the difference in descendants is just in the error message, so maybe this could be done here using something like throw new NoSuchElementException('No ' . $this->type . ' selected.');?


if (!$matched) {
throw new NoSuchElementException(
sprintf('Cannot locate option with value: %s', $value)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The option is misleading... Maybe something like "cannot locate $this->type with value ..."?

{
$elements = $this->getRelatedElements();
if (!isset($elements[$index])) {
throw new NoSuchElementException(sprintf('Cannot locate option with index: %d', $index));
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here, I'd avoid the "option" term at least in the messages, as it belongs to domain of <select>.

@@ -0,0 +1,72 @@
<?php
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When I was using the classes I was thinking about their names - it seems they are still kinda confusing.

Consider this code:

$checkbox = new WebDriverCheckbox($driver->findElement(...));

$checkbox->deselectAll();
$checkbox->getFirstSelectedOption();

It is confusing the class is called "checkbox" (ie. singular), however it actually represents not a single checkbox, but group of checkboxes - and also the methods semantically interacts with multiple objects (deselectAll, getFirstSelected etc.).

So I have two possible suggestions how to make this more understandable:

  1. Name it in plural, like "WebDriverCheckboxes", WebDriverRadios"
  2. Or make it obvious it is group of elements: WebDriverCheckboxGroup", "WebDriverRadioGroup"

What do you think?

@dunglas
Copy link
Contributor Author

dunglas commented Apr 22, 2018

@OndraM, thanks for the review! Changes made.

@OndraM
Copy link
Collaborator

OndraM commented May 28, 2018

FYI I was investigating the reason of CI fails, and it looks like there is a bug in HtmlUnit browser which is bundled with this version of Selenium server. The tests however seems to be working properly in Firefox & Chrome, but the XPath somehow fails in HtmlUnit. I'll try to update the Selenium server to see if it will be resolved.

@dunglas
Copy link
Contributor Author

dunglas commented May 28, 2018

Thanks for investigating. For what it worth, tests run locally with the latest version of HtmlUnit.

@OndraM OndraM merged commit 6c88535 into php-webdriver:community May 29, 2018
@OndraM
Copy link
Collaborator

OndraM commented May 29, 2018

The CI tests succeeded after Selenium server upgrade, so merging now!

Thanks a lot for the effort @dunglas! (And sorry the review took so long...)

@OndraM
Copy link
Collaborator

OndraM commented May 29, 2018

BTW I prepared Wiki page to explain how to use these helper class, would you be interested to add at least some examples for the Checboxes and Radios class (Similarly os it is for the WebDriverSelect)? https://github.com/facebook/php-webdriver/wiki/Select,-checkboxes,-radio-buttons

@dunglas dunglas deleted the checkbox branch May 29, 2018 13:23
@dunglas
Copy link
Contributor Author

dunglas commented May 29, 2018

Thanks for the review @OndraM! Maybe can I start with copying this PR description to the wiki?

@OndraM
Copy link
Collaborator

OndraM commented May 29, 2018

Yeah, maybe simplify it a bit for better readability, but it looks like a good start 👍 , thanks :)

@dunglas
Copy link
Contributor Author

dunglas commented Jun 11, 2018

@OndraM the wiki is readonly, or am I missing something?

@OndraM
Copy link
Collaborator

OndraM commented Jun 11, 2018

It should be editable for contributors - this doesn't work for you? https://github.com/facebook/php-webdriver/wiki/Select%2C-checkboxes%2C-radio-buttons/_edit

@dunglas
Copy link
Contributor Author

dunglas commented Jun 11, 2018

It doesn't:

capture d ecran 2018-06-11 a 15 01 39

@OndraM
Copy link
Collaborator

OndraM commented Jun 16, 2018

@dunglas Weird, it used to work for collaborators. Perhaps the wiki permission settings has changed on GitHub, let me check this with @gfosco .

@OndraM
Copy link
Collaborator

OndraM commented Jun 18, 2018

@dunglas The wiki permission should be changed, could you please have a look to see if it works for you?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants