From 5f66d46e220d54660ce5aa78a366ed0e8c103b9b Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Thu, 16 Jan 2014 02:38:24 +0100 Subject: [PATCH] MDL-43713 behat: improve multi-select support This patch implements: 1) Normalization of options. Before the patch options in a select were being returned as "op1 op2 op3" by selenium and "op1 op2 op3" by goutte. With the patch, those lists are always returned like "op1, op2, op3". If real commas are needed when handling multiple selects they should be escaped with backslash in feature files. 2) Support for selecting multiple options. Before the patch only one option was selected and a new selection was cleaning the previous one. With the patch it's possible to pass "op1, op2" in these steps: - I fill the moodle form with (table) - I select "OPTION_STRING" from "SELECT_STRING" 3) Ability to match multiple options in this steps. Before the patch matching of multiple was really random, now every every passed option ("opt1, opt2") is individually verified. It applies to these 2 steps: - the "ELEMENT" select box should contain "OPTIONS" - the "ELEMENT" select box should not contain "OPTIONS" 4) Two new steps able to verify if a form have some options selected or no: - the "ELEMENT" select box should contain "OPTIONS" selected - the "ELEMENT" select box should contain "OPTIONS" not selected 5) Change get_value from xpath search to Mink's getValue() that is immediate (does not need form submission) and works for all browsers but Safari, that fails because of the extra ->click() issued. Note all the changes 1-4 only affect to multi-select fields. Single selects should continue working 100% the same. The change 5) causes Safari to fail. The problem has been traced down to the extra ->click() present there. Anyway there are not test cases requiring that "immediate" evaluation right now. Only the special feature file attached verifies it. --- lib/behat/form_field/behat_form_select.php | 95 +++++++++-- lib/tests/behat/behat_forms.php | 182 +++++++++++++++++++-- 2 files changed, 247 insertions(+), 30 deletions(-) diff --git a/lib/behat/form_field/behat_form_select.php b/lib/behat/form_field/behat_form_select.php index 208b95a6ac7..b0e94ee09a7 100644 --- a/lib/behat/form_field/behat_form_select.php +++ b/lib/behat/form_field/behat_form_select.php @@ -38,13 +38,13 @@ require_once(__DIR__ . '/behat_form_field.php'); class behat_form_select extends behat_form_field { /** - * Sets the value of a single select. + * Sets the value(s) of a select element. * * Seems an easy select, but there are lots of combinations * of browsers and operative systems and each one manages the - * autosubmits and the multiple option selects in a diferent way. + * autosubmits and the multiple option selects in a different way. * - * @param string $value + * @param string $value plain value or comma separated values if multiple. Commas in values escaped with backslash. * @return void */ public function set_value($value) { @@ -61,8 +61,26 @@ class behat_form_select extends behat_form_field { $currentelementid = $this->get_internal_field_id(); } - // Here we select an option. - $this->field->selectOption($value); + // Is the select multiple? + $multiple = $this->field->hasAttribute('multiple'); + + // By default, assume the passed value is a non-multiple option. + $options = array(trim($value)); + + // Here we select the option(s). + if ($multiple) { + // Split and decode values. Comma separated list of values allowed. With valuable commas escaped with backslash. + $options = preg_replace('/\\\,/', ',', preg_split('/(?field->selectOption(trim($option), $afterfirstoption); + $afterfirstoption = true; + } + } else { + // This is a single select, let's pass the last one specified. + $this->field->selectOption(end($options)); + } // With JS disabled this is enough and we finish here. if (!$this->running_javascript()) { @@ -87,11 +105,13 @@ class behat_form_select extends behat_form_field { return; } - // We also check that the option is still there. We neither wait. - $valueliteral = $this->session->getSelectorsHandler()->xpathLiteral($value); - $optionxpath = $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]"; - if (!$this->session->getDriver()->find($optionxpath)) { - return; + // We also check that the option(s) are still there. We neither wait. + foreach ($options as $option) { + $valueliteral = $this->session->getSelectorsHandler()->xpathLiteral(trim($option)); + $optionxpath = $selectxpath . "/descendant::option[(./@value=$valueliteral or normalize-space(.)=$valueliteral)]"; + if (!$this->session->getDriver()->find($optionxpath)) { + return; + } } // Wrapped in try & catch as the element may disappear if an AJAX request was submitted. @@ -150,9 +170,11 @@ class behat_form_select extends behat_form_field { // Wrapped in a try & catch as we can fall into race conditions // and the element may not be there. try { - // Repeating the select as some drivers (chrome that I know) are moving + // Repeating the select(s) as some drivers (chrome that I know) are moving // to another option after the general select field click above. - $this->field->selectOption($value); + foreach ($options as $option) { + $this->field->selectOption(trim($option), true); + } } catch (Exception $e) { // We continue and return as this means that the element is not there or it is not the same. return; @@ -161,12 +183,53 @@ class behat_form_select extends behat_form_field { } /** - * Returns the text of the current value. + * Returns the text of the currently selected options. * - * @return string + * @return string Comma separated if multiple options are selected. Commas in option texts escaped with backslash. */ public function get_value() { - $selectedoption = $this->field->find('xpath', '//option[@selected="selected"]'); - return $selectedoption->getText(); + + // Is the select multiple? + $multiple = $this->field->hasAttribute('multiple'); + + $selectedoptions = array(); // To accumulate found selected options. + + // Selenium getValue() implementation breaks - separates - values having + // commas within them, so we'll be looking for options with the 'selected' attribute instead. + if ($this->running_javascript()) { + // Get all the options in the select and extract their value/text pairs. + $alloptions = $this->field->findAll('xpath', '//option'); + foreach ($alloptions as $option) { + // Is it selected? + if ($option->hasAttribute('selected')) { + if ($multiple) { + // If the select is multiple, text commas must be encoded. + $selectedoptions[] = trim(str_replace(',', '\,', $option->getText())); + } else { + $selectedoptions[] = trim($option->getText()); + } + } + } + + // Goutte does not keep the 'selected' attribute updated, but its getValue() returns + // the selected elements correctly, also those having commas within them. + } else { + $values = $this->field->getValue(); + // Get all the options in the select and extract their value/text pairs. + $alloptions = $this->field->findAll('xpath', '//option'); + foreach ($alloptions as $option) { + // Is it selected? + if (in_array($option->getValue(), $values)) { + if ($multiple) { + // If the select is multiple, text commas must be encoded. + $selectedoptions[] = trim(str_replace(',', '\,', $option->getText())); + } else { + $selectedoptions[] = trim($option->getText()); + } + } + } + } + + return implode(', ', $selectedoptions); } } diff --git a/lib/tests/behat/behat_forms.php b/lib/tests/behat/behat_forms.php index f3fdf3757a5..8659e0a5822 100644 --- a/lib/tests/behat/behat_forms.php +++ b/lib/tests/behat/behat_forms.php @@ -236,6 +236,9 @@ class behat_forms extends behat_base { /** * Checks that the form element field have the specified value. * + * NOTE: This method/step does not support all fields. Namely, multi-select ones aren't supported. + * @todo: MDL-43738 would try to put some better support here for that multi-select and others. + * * @Then /^the "(?P(?:[^"]|\\")*)" field should match "(?P(?:[^"]|\\")*)" value$/ * @throws ExpectationException * @throws ElementNotFoundException Thrown by behat_base::find @@ -288,20 +291,41 @@ class behat_forms extends behat_base { * @throws ExpectationException * @throws ElementNotFoundException Thrown by behat_base::find * @param string $select The select element name - * @param string $option The option text/value + * @param string $option The option text/value. Plain value or comma separated + * values if multiple. Commas in multiple values escaped with backslash. */ public function the_select_box_should_contain($select, $option) { $selectnode = $this->find_field($select); + $multiple = $selectnode->hasAttribute('multiple'); + $optionsarr = array(); // Array of passed value/text options to test. - $regex = '/' . preg_quote($option, '/') . '/ui'; - if (!preg_match($regex, $selectnode->getText())) { - throw new ExpectationException( - 'The select box "' . $select . '" does not contains the option "' . $option . '"', - $this->getSession() - ); + if ($multiple) { + // Can pass multiple comma separated, with valuable commas escaped with backslash. + foreach (preg_replace('/\\\,/', ',', preg_split('/(?findAll('xpath', '//option'); + $values = array(); + foreach ($options as $opt) { + $values[trim($opt->getValue())] = trim($opt->getText()); } + foreach ($optionsarr as $opt) { + // Verify every option is a valid text or value. + if (!in_array($opt, $values) && !array_key_exists($opt, $values)) { + throw new ExpectationException( + 'The select box "' . $select . '" does not contain the option "' . $opt . '"', + $this->getSession() + ); + } + } } /** @@ -311,18 +335,148 @@ class behat_forms extends behat_base { * @throws ExpectationException * @throws ElementNotFoundException Thrown by behat_base::find * @param string $select The select element name - * @param string $option The option text/value + * @param string $option The option text/value. Plain value or comma separated + * values if multiple. Commas in multiple values escaped with backslash. */ public function the_select_box_should_not_contain($select, $option) { $selectnode = $this->find_field($select); + $multiple = $selectnode->hasAttribute('multiple'); + $optionsarr = array(); // Array of passed value/text options to test. - $regex = '/' . preg_quote($option, '/') . '/ui'; - if (preg_match($regex, $selectnode->getText())) { - throw new ExpectationException( - 'The select box "' . $select . '" contains the option "' . $option . '"', - $this->getSession() - ); + if ($multiple) { + // Can pass multiple comma separated, with valuable commas escaped with backslash. + foreach (preg_replace('/\\\,/', ',', preg_split('/(?findAll('xpath', '//option'); + $values = array(); + foreach ($options as $opt) { + $values[trim($opt->getValue())] = trim($opt->getText()); + } + + foreach ($optionsarr as $opt) { + // Verify every option is not a valid text or value. + if (in_array($opt, $values) || array_key_exists($opt, $values)) { + throw new ExpectationException( + 'The select box "' . $select . '" contains the option "' . $opt . '"', + $this->getSession() + ); + } + } + } + + /** + * Checks, that given select box contains the specified option selected. + * + * @Then /^the "(?P(?:[^"]|\\")*)" select box should contain "(?P(?:[^"]|\\")*)" selected$/ + * @throws ExpectationException + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $select The select element name + * @param string $option The option text. Plain value or comma separated + * values if multiple. Commas in multiple values escaped with backslash. + */ + public function the_select_box_should_contain_selected($select, $option) { + + $selectnode = $this->find_field($select); + $multiple = $selectnode->hasAttribute('multiple'); + $optionsarr = array(); // Array of passed text options to test. + $selectedarr = array(); // Array of selected text options. + + if ($multiple) { + // Can pass multiple comma separated, with valuable commas escaped with backslash. + foreach (preg_replace('/\\\,/', ',', preg_split('/(?getSession()); + $value = $field->get_value(); + + if ($multiple) { + // Can be multiple comma separated, with valuable commas escaped with backslash. + foreach (preg_replace('/\\\,/', ',', preg_split('/(?getSession() + ); + } + } + } + + /** + * Checks, that given select box contains the specified option not selected. + * + * @Then /^the "(?P(?:[^"]|\\")*)" select box should contain "(?P(?:[^"]|\\")*)" not selected$/ + * @throws ExpectationException + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $select The select element name + * @param string $option The option text. Plain value or comma separated + * values if multiple. Commas in multiple values escaped with backslash. + */ + public function the_select_box_should_contain_not_selected($select, $option) { + + $selectnode = $this->find_field($select); + $multiple = $selectnode->hasAttribute('multiple'); + $optionsarr = array(); // Array of passed text options to test. + $selectedarr = array(); // Array of selected text options. + + // First of all, the option(s) must exist, delegate it. Plain and raw. + $this->the_select_box_should_contain($select, $option); + + if ($multiple) { + // Can pass multiple comma separated, with valuable commas escaped with backslash. + foreach (preg_replace('/\\\,/', ',', preg_split('/(?getSession()); + $value = $field->get_value(); + + if ($multiple) { + // Can be multiple comma separated, with valuable commas escaped with backslash. + foreach (preg_replace('/\\\,/', ',', preg_split('/(?getSession() + ); + } } } -- 2.43.0