2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Single select form field class.
22 * @copyright 2012 David Monllaó
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26 // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
28 require_once(__DIR__ . '/behat_form_field.php');
31 * Single select form field.
35 * @copyright 2012 David Monllaó
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38 class behat_form_select extends behat_form_field {
41 * Sets the value(s) of a select element.
43 * Seems an easy select, but there are lots of combinations
44 * of browsers and operative systems and each one manages the
45 * autosubmits and the multiple option selects in a different way.
47 * @param string $value plain value or comma separated values if multiple. Commas in values escaped with backslash.
50 public function set_value($value) {
52 // In some browsers we select an option and it triggers all the
53 // autosubmits and works as expected but not in all of them, so we
54 // try to catch all the possibilities to make this function work as
57 // Get the internal id of the element we are going to click.
58 // This kind of internal IDs are only available in the selenium wire
59 // protocol, so only available using selenium drivers, phantomjs and family.
60 if ($this->running_javascript()) {
61 $currentelementid = $this->get_internal_field_id();
64 // Is the select multiple?
65 $multiple = $this->field->hasAttribute('multiple');
67 // By default, assume the passed value is a non-multiple option.
68 $options = array(trim($value));
70 // Here we select the option(s).
72 // Split and decode values. Comma separated list of values allowed. With valuable commas escaped with backslash.
73 $options = preg_replace('/\\\,/', ',', preg_split('/(?<!\\\),/', $value));
74 // This is a multiple select, let's pass the multiple flag after first option.
75 $afterfirstoption = false;
76 foreach ($options as $option) {
77 $this->field->selectOption(trim($option), $afterfirstoption);
78 $afterfirstoption = true;
81 // This is a single select, let's pass the last one specified.
82 $this->field->selectOption(end($options));
85 // With JS disabled this is enough and we finish here.
86 if (!$this->running_javascript()) {
90 // With JS enabled we add more clicks as some selenium
91 // drivers requires it to fire JS events.
93 // In some browsers the selectOption actions can perform a form submit or reload page
94 // so we need to ensure the element is still available to continue interacting
95 // with it. We don't wait here.
96 $selectxpath = $this->field->getXpath();
97 if (!$this->session->getDriver()->find($selectxpath)) {
101 // We also check the selenium internal element id, if it have changed
102 // we are dealing with an autosubmit that was already executed, and we don't to
103 // execute anything else as the action we wanted was already performed.
104 if ($currentelementid != $this->get_internal_field_id()) {
108 // We also check that the option(s) are still there. We neither wait.
109 foreach ($options as $option) {
110 $valueliteral = $this->session->getSelectorsHandler()->xpathLiteral(trim($option));
111 $optionxpath = $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]";
112 if (!$this->session->getDriver()->find($optionxpath)) {
117 // Wrapped in try & catch as the element may disappear if an AJAX request was submitted.
119 $multiple = $this->field->hasAttribute('multiple');
120 } catch (Exception $e) {
121 // We do not specify any specific Exception type as there are
122 // different exceptions that can be thrown by the driver and
123 // we can not control them all, also depending on the selenium
124 // version the exception type can change.
128 // Wait for all the possible AJAX requests that have been
129 // already triggered by selectOption() to be finished.
130 $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
132 // Single select sometimes needs an extra click in the option.
135 // Using the driver direcly because Element methods are messy when dealing
136 // with elements inside containers.
137 $optionnodes = $this->session->getDriver()->find($optionxpath);
139 // Wrapped in a try & catch as we can fall into race conditions
140 // and the element may not be there.
142 current($optionnodes)->click();
143 } catch (Exception $e) {
144 // We continue and return as this means that the element is not there or it is not the same.
151 // Wrapped in a try & catch as we can fall into race conditions
152 // and the element may not be there.
154 // Multiple ones needs the click in the select.
155 $this->field->click();
156 } catch (Exception $e) {
157 // We continue and return as this means that the element is not there or it is not the same.
161 // We ensure that the option is still there.
162 if (!$this->session->getDriver()->find($optionxpath)) {
166 // Wait for all the possible AJAX requests that have been
167 // already triggered by selectOption() to be finished.
168 $this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
170 // Wrapped in a try & catch as we can fall into race conditions
171 // and the element may not be there.
173 // Repeating the select(s) as some drivers (chrome that I know) are moving
174 // to another option after the general select field click above.
175 foreach ($options as $option) {
176 $this->field->selectOption(trim($option), true);
178 } catch (Exception $e) {
179 // We continue and return as this means that the element is not there or it is not the same.
186 * Returns the text of the currently selected options.
188 * @return string Comma separated if multiple options are selected. Commas in option texts escaped with backslash.
190 public function get_value() {
192 // Is the select multiple?
193 $multiple = $this->field->hasAttribute('multiple');
195 $selectedoptions = array(); // To accumulate found selected options.
197 // Selenium getValue() implementation breaks - separates - values having
198 // commas within them, so we'll be looking for options with the 'selected' attribute instead.
199 if ($this->running_javascript()) {
200 // Get all the options in the select and extract their value/text pairs.
201 $alloptions = $this->field->findAll('xpath', '//option');
202 foreach ($alloptions as $option) {
204 if ($option->hasAttribute('selected')) {
206 // If the select is multiple, text commas must be encoded.
207 $selectedoptions[] = trim(str_replace(',', '\,', $option->getText()));
209 $selectedoptions[] = trim($option->getText());
214 // Goutte does not keep the 'selected' attribute updated, but its getValue() returns
215 // the selected elements correctly, also those having commas within them.
217 $values = $this->field->getValue();
218 // Get all the options in the select and extract their value/text pairs.
219 $alloptions = $this->field->findAll('xpath', '//option');
220 foreach ($alloptions as $option) {
222 if (in_array($option->getValue(), $values)) {
224 // If the select is multiple, text commas must be encoded.
225 $selectedoptions[] = trim(str_replace(',', '\,', $option->getText()));
227 $selectedoptions[] = trim($option->getText());
233 return implode(', ', $selectedoptions);