1<?php
2
3namespace Facebook\WebDriver;
4
5use Facebook\WebDriver\Exception\NoSuchElementException;
6use Facebook\WebDriver\Exception\UnexpectedTagNameException;
7use Facebook\WebDriver\Exception\WebDriverException;
8use Facebook\WebDriver\Support\XPathEscaper;
9
10/**
11 * Provides helper methods for checkboxes and radio buttons.
12 */
13abstract 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