1<?php
2
3namespace Facebook\WebDriver;
4
5use Facebook\WebDriver\Exception\NoSuchElementException;
6use Facebook\WebDriver\Exception\UnexpectedTagNameException;
7use Facebook\WebDriver\Exception\UnsupportedOperationException;
8use Facebook\WebDriver\Support\XPathEscaper;
9
10/**
11 * Models a default HTML `<select>` tag, providing helper methods to select and deselect options.
12 */
13class WebDriverSelect implements WebDriverSelectInterface
14{
15    /** @var WebDriverElement */
16    private $element;
17    /** @var bool */
18    private $isMulti;
19
20    public function __construct(WebDriverElement $element)
21    {
22        $tag_name = $element->getTagName();
23
24        if ($tag_name !== 'select') {
25            throw new UnexpectedTagNameException('select', $tag_name);
26        }
27        $this->element = $element;
28        $value = $element->getAttribute('multiple');
29        $this->isMulti = $value === 'true';
30    }
31
32    public function isMultiple()
33    {
34        return $this->isMulti;
35    }
36
37    public function getOptions()
38    {
39        return $this->element->findElements(WebDriverBy::tagName('option'));
40    }
41
42    public function getAllSelectedOptions()
43    {
44        $selected_options = [];
45        foreach ($this->getOptions() as $option) {
46            if ($option->isSelected()) {
47                $selected_options[] = $option;
48
49                if (!$this->isMultiple()) {
50                    return $selected_options;
51                }
52            }
53        }
54
55        return $selected_options;
56    }
57
58    public function getFirstSelectedOption()
59    {
60        foreach ($this->getOptions() as $option) {
61            if ($option->isSelected()) {
62                return $option;
63            }
64        }
65
66        throw new NoSuchElementException('No options are selected');
67    }
68
69    public function selectByIndex($index)
70    {
71        foreach ($this->getOptions() as $option) {
72            if ($option->getAttribute('index') === (string) $index) {
73                $this->selectOption($option);
74
75                return;
76            }
77        }
78
79        throw new NoSuchElementException(sprintf('Cannot locate option with index: %d', $index));
80    }
81
82    public function selectByValue($value)
83    {
84        $matched = false;
85        $xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']';
86        $options = $this->element->findElements(WebDriverBy::xpath($xpath));
87
88        foreach ($options as $option) {
89            $this->selectOption($option);
90            if (!$this->isMultiple()) {
91                return;
92            }
93            $matched = true;
94        }
95
96        if (!$matched) {
97            throw new NoSuchElementException(
98                sprintf('Cannot locate option with value: %s', $value)
99            );
100        }
101    }
102
103    public function selectByVisibleText($text)
104    {
105        $matched = false;
106        $xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']';
107        $options = $this->element->findElements(WebDriverBy::xpath($xpath));
108
109        foreach ($options as $option) {
110            $this->selectOption($option);
111            if (!$this->isMultiple()) {
112                return;
113            }
114            $matched = true;
115        }
116
117        // Since the mechanism of getting the text in xpath is not the same as
118        // webdriver, use the expensive getText() to check if nothing is matched.
119        if (!$matched) {
120            foreach ($this->getOptions() as $option) {
121                if ($option->getText() === $text) {
122                    $this->selectOption($option);
123                    if (!$this->isMultiple()) {
124                        return;
125                    }
126                    $matched = true;
127                }
128            }
129        }
130
131        if (!$matched) {
132            throw new NoSuchElementException(
133                sprintf('Cannot locate option with text: %s', $text)
134            );
135        }
136    }
137
138    public function selectByVisiblePartialText($text)
139    {
140        $matched = false;
141        $xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]';
142        $options = $this->element->findElements(WebDriverBy::xpath($xpath));
143
144        foreach ($options as $option) {
145            $this->selectOption($option);
146            if (!$this->isMultiple()) {
147                return;
148            }
149            $matched = true;
150        }
151
152        if (!$matched) {
153            throw new NoSuchElementException(
154                sprintf('Cannot locate option with text: %s', $text)
155            );
156        }
157    }
158
159    public function deselectAll()
160    {
161        if (!$this->isMultiple()) {
162            throw new UnsupportedOperationException('You may only deselect all options of a multi-select');
163        }
164
165        foreach ($this->getOptions() as $option) {
166            $this->deselectOption($option);
167        }
168    }
169
170    public function deselectByIndex($index)
171    {
172        if (!$this->isMultiple()) {
173            throw new UnsupportedOperationException('You may only deselect options of a multi-select');
174        }
175
176        foreach ($this->getOptions() as $option) {
177            if ($option->getAttribute('index') === (string) $index) {
178                $this->deselectOption($option);
179
180                return;
181            }
182        }
183    }
184
185    public function deselectByValue($value)
186    {
187        if (!$this->isMultiple()) {
188            throw new UnsupportedOperationException('You may only deselect options of a multi-select');
189        }
190
191        $xpath = './/option[@value = ' . XPathEscaper::escapeQuotes($value) . ']';
192        $options = $this->element->findElements(WebDriverBy::xpath($xpath));
193        foreach ($options as $option) {
194            $this->deselectOption($option);
195        }
196    }
197
198    public function deselectByVisibleText($text)
199    {
200        if (!$this->isMultiple()) {
201            throw new UnsupportedOperationException('You may only deselect options of a multi-select');
202        }
203
204        $xpath = './/option[normalize-space(.) = ' . XPathEscaper::escapeQuotes($text) . ']';
205        $options = $this->element->findElements(WebDriverBy::xpath($xpath));
206        foreach ($options as $option) {
207            $this->deselectOption($option);
208        }
209    }
210
211    public function deselectByVisiblePartialText($text)
212    {
213        if (!$this->isMultiple()) {
214            throw new UnsupportedOperationException('You may only deselect options of a multi-select');
215        }
216
217        $xpath = './/option[contains(normalize-space(.), ' . XPathEscaper::escapeQuotes($text) . ')]';
218        $options = $this->element->findElements(WebDriverBy::xpath($xpath));
219        foreach ($options as $option) {
220            $this->deselectOption($option);
221        }
222    }
223
224    /**
225     * Mark option selected
226     * @param WebDriverElement $option
227     */
228    protected function selectOption(WebDriverElement $option)
229    {
230        if (!$option->isSelected()) {
231            $option->click();
232        }
233    }
234
235    /**
236     * Mark option not selected
237     * @param WebDriverElement $option
238     */
239    protected function deselectOption(WebDriverElement $option)
240    {
241        if ($option->isSelected()) {
242            $option->click();
243        }
244    }
245}
246