1 <?php
2 
3 namespace Facebook\WebDriver;
4 
5 use Facebook\WebDriver\Exception\NoSuchElementException;
6 use Facebook\WebDriver\Exception\UnexpectedTagNameException;
7 use Facebook\WebDriver\Exception\WebDriverException;
8 use Facebook\WebDriver\Support\XPathEscaper;
9 
10 /**
11  * Provides helper methods for checkboxes and radio buttons.
12  */
13 abstract class AbstractWebDriverCheckboxOrRadio implements WebDriverSelectInterface
14 {
15     /** @var WebDriverElement */
16     protected $element;
17 
18     /** @var string */
19     protected $type;
20 
21     /** @var string */
22     protected $name;
23 
24     public function __construct(WebDriverElement $element)
25     {
26         $tagName = $element->getTagName();
27         if ($tagName !== 'input') {
28             throw new UnexpectedTagNameException('input', $tagName);
29         }
30 
31         $this->name = $element->getAttribute('name');
32         if ($this->name === null) {
33             throw new WebDriverException('The input does not have a "name" attribute.');
34         }
35 
36         $this->element = $element;
37     }
38 
39     public function getOptions()
40     {
41         return $this->getRelatedElements();
42     }
43 
44     public function getAllSelectedOptions()
45     {
46         $selectedElement = [];
47         foreach ($this->getRelatedElements() as $element) {
48             if ($element->isSelected()) {
49                 $selectedElement[] = $element;
50 
51                 if (!$this->isMultiple()) {
52                     return $selectedElement;
53                 }
54             }
55         }
56 
57         return $selectedElement;
58     }
59 
60     public function getFirstSelectedOption()
61     {
62         foreach ($this->getRelatedElements() as $element) {
63             if ($element->isSelected()) {
64                 return $element;
65             }
66         }
67 
68         throw new NoSuchElementException(
69             sprintf('No %s are selected', $this->type === 'radio' ? 'radio buttons' : 'checkboxes')
70         );
71     }
72 
73     public function selectByIndex($index)
74     {
75         $this->byIndex($index);
76     }
77 
78     public function selectByValue($value)
79     {
80         $this->byValue($value);
81     }
82 
83     public function selectByVisibleText($text)
84     {
85         $this->byVisibleText($text);
86     }
87 
88     public function selectByVisiblePartialText($text)
89     {
90         $this->byVisibleText($text, true);
91     }
92 
93     /**
94      * Selects or deselects a checkbox or a radio button by its value.
95      *
96      * @param string $value
97      * @param bool $select
98      * @throws NoSuchElementException
99      */
100     protected function byValue($value, $select = true)
101     {
102         $matched = false;
103         foreach ($this->getRelatedElements($value) as $element) {
104             $select ? $this->selectOption($element) : $this->deselectOption($element);
105             if (!$this->isMultiple()) {
106                 return;
107             }
108 
109             $matched = true;
110         }
111 
112         if (!$matched) {
113             throw new NoSuchElementException(
114                 sprintf('Cannot locate %s with value: %s', $this->type, $value)
115             );
116         }
117     }
118 
119     /**
120      * Selects or deselects a checkbox or a radio button by its index.
121      *
122      * @param int $index
123      * @param bool $select
124      * @throws NoSuchElementException
125      */
126     protected function byIndex($index, $select = true)
127     {
128         $elements = $this->getRelatedElements();
129         if (!isset($elements[$index])) {
130             throw new NoSuchElementException(sprintf('Cannot locate %s with index: %d', $this->type, $index));
131         }
132 
133         $select ? $this->selectOption($elements[$index]) : $this->deselectOption($elements[$index]);
134     }
135 
136     /**
137      * Selects or deselects a checkbox or a radio button by its visible text.
138      *
139      * @param string $text
140      * @param bool $partial
141      * @param bool $select
142      */
143     protected function byVisibleText($text, $partial = false, $select = true)
144     {
145         foreach ($this->getRelatedElements() as $element) {
146             $normalizeFilter = sprintf(
147                 $partial ? 'contains(normalize-space(.), %s)' : 'normalize-space(.) = %s',
148                 XPathEscaper::escapeQuotes($text)
149             );
150 
151             $xpath = 'ancestor::label';
152             $xpathNormalize = sprintf('%s[%s]', $xpath, $normalizeFilter);
153 
154             $id = $element->getAttribute('id');
155             if ($id !== null) {
156                 $idFilter = sprintf('@for = %s', XPathEscaper::escapeQuotes($id));
157 
158                 $xpath .= sprintf(' | //label[%s]', $idFilter);
159                 $xpathNormalize .= sprintf(' | //label[%s and %s]', $idFilter, $normalizeFilter);
160             }
161 
162             try {
163                 $element->findElement(WebDriverBy::xpath($xpathNormalize));
164             } catch (NoSuchElementException $e) {
165                 if ($partial) {
166                     continue;
167                 }
168 
169                 try {
170                     // Since the mechanism of getting the text in xpath is not the same as
171                     // webdriver, use the expensive getText() to check if nothing is matched.
172                     if ($text !== $element->findElement(WebDriverBy::xpath($xpath))->getText()) {
173                         continue;
174                     }
175                 } catch (NoSuchElementException $e) {
176                     continue;
177                 }
178             }
179 
180             $select ? $this->selectOption($element) : $this->deselectOption($element);
181             if (!$this->isMultiple()) {
182                 return;
183             }
184         }
185     }
186 
187     /**
188      * Gets checkboxes or radio buttons with the same name.
189      *
190      * @param string|null $value
191      * @return WebDriverElement[]
192      */
193     protected function getRelatedElements($value = null)
194     {
195         $valueSelector = $value ? sprintf(' and @value = %s', XPathEscaper::escapeQuotes($value)) : '';
196         $formId = $this->element->getAttribute('form');
197         if ($formId === null) {
198             $form = $this->element->findElement(WebDriverBy::xpath('ancestor::form'));
199 
200             $formId = $form->getAttribute('id');
201             if ($formId === '' || $formId === null) {
202                 return $form->findElements(WebDriverBy::xpath(
203                     sprintf('.//input[@name = %s%s]', XPathEscaper::escapeQuotes($this->name), $valueSelector)
204                 ));
205             }
206         }
207 
208         // https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input#form
209         return $this->element->findElements(
210             WebDriverBy::xpath(sprintf(
211                 '//form[@id = %1$s]//input[@name = %2$s%3$s'
212                 . ' and ((boolean(@form) = true() and @form = %1$s) or boolean(@form) = false())]'
213                 . ' | //input[@form = %1$s and @name = %2$s%3$s]',
214                 XPathEscaper::escapeQuotes($formId),
215                 XPathEscaper::escapeQuotes($this->name),
216                 $valueSelector
217             ))
218         );
219     }
220 
221     /**
222      * Selects a checkbox or a radio button.
223      */
224     protected function selectOption(WebDriverElement $element)
225     {
226         if (!$element->isSelected()) {
227             $element->click();
228         }
229     }
230 
231     /**
232      * Deselects a checkbox or a radio button.
233      */
234     protected function deselectOption(WebDriverElement $element)
235     {
236         if ($element->isSelected()) {
237             $element->click();
238         }
239     }
240 }
241