MDL-43713 behat: improve multi-select support
[moodle.git] / lib / behat / form_field / behat_form_select.php
CommitLineData
23ebc481
DM
1<?php
2// This file is part of Moodle - http://moodle.org/
3//
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.
8//
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.
13//
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/>.
16
17/**
18 * Single select form field class.
19 *
20 * @package core_form
21 * @category test
22 * @copyright 2012 David MonllaĆ³
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24 */
25
26// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
27
28require_once(__DIR__ . '/behat_form_field.php');
29
30/**
31 * Single select form field.
32 *
33 * @package core_form
34 * @category test
35 * @copyright 2012 David MonllaĆ³
36 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
37 */
38class behat_form_select extends behat_form_field {
39
40 /**
5f66d46e 41 * Sets the value(s) of a select element.
23ebc481 42 *
d1e55a47
DM
43 * Seems an easy select, but there are lots of combinations
44 * of browsers and operative systems and each one manages the
5f66d46e 45 * autosubmits and the multiple option selects in a different way.
d1e55a47 46 *
5f66d46e 47 * @param string $value plain value or comma separated values if multiple. Commas in values escaped with backslash.
23ebc481
DM
48 * @return void
49 */
50 public function set_value($value) {
28abad1a 51
d1e55a47
DM
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
55 // expected.
56
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.
28abad1a 60 if ($this->running_javascript()) {
d1e55a47
DM
61 $currentelementid = $this->get_internal_field_id();
62 }
28abad1a 63
5f66d46e
EL
64 // Is the select multiple?
65 $multiple = $this->field->hasAttribute('multiple');
66
67 // By default, assume the passed value is a non-multiple option.
68 $options = array(trim($value));
69
70 // Here we select the option(s).
71 if ($multiple) {
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;
79 }
80 } else {
81 // This is a single select, let's pass the last one specified.
82 $this->field->selectOption(end($options));
83 }
1fb97157 84
d1e55a47
DM
85 // With JS disabled this is enough and we finish here.
86 if (!$this->running_javascript()) {
87 return;
88 }
38976081 89
d1e55a47
DM
90 // With JS enabled we add more clicks as some selenium
91 // drivers requires it to fire JS events.
38976081 92
d1e55a47
DM
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)) {
98 return;
99 }
28abad1a 100
d1e55a47
DM
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()) {
105 return;
106 }
107
5f66d46e
EL
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)) {
113 return;
114 }
d1e55a47
DM
115 }
116
405cdd04
DM
117 // Wrapped in try & catch as the element may disappear if an AJAX request was submitted.
118 try {
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.
125 return;
126 }
127
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);
131
d1e55a47 132 // Single select sometimes needs an extra click in the option.
405cdd04 133 if (!$multiple) {
d1e55a47
DM
134
135 // Using the driver direcly because Element methods are messy when dealing
136 // with elements inside containers.
137 $optionnodes = $this->session->getDriver()->find($optionxpath);
138 if ($optionnodes) {
fff500c7
DM
139 // Wrapped in a try & catch as we can fall into race conditions
140 // and the element may not be there.
141 try {
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.
145 return;
146 }
28abad1a 147 }
d1e55a47
DM
148
149 } else {
afe9f42a 150
fff500c7
DM
151 // Wrapped in a try & catch as we can fall into race conditions
152 // and the element may not be there.
153 try {
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.
158 return;
159 }
d1e55a47
DM
160
161 // We ensure that the option is still there.
162 if (!$this->session->getDriver()->find($optionxpath)) {
163 return;
164 }
165
afe9f42a
DM
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);
169
fff500c7
DM
170 // Wrapped in a try & catch as we can fall into race conditions
171 // and the element may not be there.
172 try {
5f66d46e 173 // Repeating the select(s) as some drivers (chrome that I know) are moving
fff500c7 174 // to another option after the general select field click above.
5f66d46e
EL
175 foreach ($options as $option) {
176 $this->field->selectOption(trim($option), true);
177 }
fff500c7
DM
178 } catch (Exception $e) {
179 // We continue and return as this means that the element is not there or it is not the same.
180 return;
181 }
28abad1a 182 }
23ebc481
DM
183 }
184
185 /**
5f66d46e 186 * Returns the text of the currently selected options.
23ebc481 187 *
5f66d46e 188 * @return string Comma separated if multiple options are selected. Commas in option texts escaped with backslash.
23ebc481
DM
189 */
190 public function get_value() {
5f66d46e
EL
191
192 // Is the select multiple?
193 $multiple = $this->field->hasAttribute('multiple');
194
195 $selectedoptions = array(); // To accumulate found selected options.
196
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) {
203 // Is it selected?
204 if ($option->hasAttribute('selected')) {
205 if ($multiple) {
206 // If the select is multiple, text commas must be encoded.
207 $selectedoptions[] = trim(str_replace(',', '\,', $option->getText()));
208 } else {
209 $selectedoptions[] = trim($option->getText());
210 }
211 }
212 }
213
214 // Goutte does not keep the 'selected' attribute updated, but its getValue() returns
215 // the selected elements correctly, also those having commas within them.
216 } else {
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) {
221 // Is it selected?
222 if (in_array($option->getValue(), $values)) {
223 if ($multiple) {
224 // If the select is multiple, text commas must be encoded.
225 $selectedoptions[] = trim(str_replace(',', '\,', $option->getText()));
226 } else {
227 $selectedoptions[] = trim($option->getText());
228 }
229 }
230 }
231 }
232
233 return implode(', ', $selectedoptions);
23ebc481
DM
234 }
235}