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.
This commit is contained in:
committed by
David Monllao
parent
28b3ddbdbe
commit
33fa8e7dba
@@ -38,52 +38,192 @@ 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.
|
||||
*
|
||||
* @param string $value
|
||||
r * 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 different way.
|
||||
*
|
||||
* @param string $value plain value or comma separated values if multiple. Commas in values escaped with backslash.
|
||||
* @return void
|
||||
*/
|
||||
public function set_value($value) {
|
||||
$this->field->selectOption($value);
|
||||
|
||||
// Adding a click as Selenium requires it to fire some JS events.
|
||||
// In some browsers we select an option and it triggers all the
|
||||
// autosubmits and works as expected but not in all of them, so we
|
||||
// try to catch all the possibilities to make this function work as
|
||||
// expected.
|
||||
|
||||
// Get the internal id of the element we are going to click.
|
||||
// This kind of internal IDs are only available in the selenium wire
|
||||
// protocol, so only available using selenium drivers, phantomjs and family.
|
||||
if ($this->running_javascript()) {
|
||||
$currentelementid = $this->get_internal_field_id();
|
||||
}
|
||||
|
||||
// In some browsers the selectOption actions can perform a page reload
|
||||
// so we need to ensure the element is still available to continue interacting
|
||||
// with it. We don't wait here.
|
||||
if (!$this->session->getDriver()->find($this->field->getXpath())) {
|
||||
// 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('/(?<!\\\),/', $value));
|
||||
// This is a multiple select, let's pass the multiple flag after first option.
|
||||
$afterfirstoption = false;
|
||||
foreach ($options as $option) {
|
||||
$this->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()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// With JS enabled we add more clicks as some selenium
|
||||
// drivers requires it to fire JS events.
|
||||
|
||||
// In some browsers the selectOption actions can perform a form submit or reload page
|
||||
// so we need to ensure the element is still available to continue interacting
|
||||
// with it. We don't wait here.
|
||||
$selectxpath = $this->field->getXpath();
|
||||
if (!$this->session->getDriver()->find($selectxpath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We also check the selenium internal element id, if it have changed
|
||||
// we are dealing with an autosubmit that was already executed, and we don't to
|
||||
// execute anything else as the action we wanted was already performed.
|
||||
if ($currentelementid != $this->get_internal_field_id()) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Single select sometimes needs an extra click in the option.
|
||||
if (!$multiple) {
|
||||
|
||||
// Using the driver direcly because Element methods are messy when dealing
|
||||
// with elements inside containers.
|
||||
$optionnodes = $this->session->getDriver()->find($optionxpath);
|
||||
if ($optionnodes) {
|
||||
// Wrapped in a try & catch as we can fall into race conditions
|
||||
// and the element may not be there.
|
||||
try {
|
||||
|
||||
// Wait for all the possible AJAX requests that have been
|
||||
// already triggered by selectOption() to be finished.
|
||||
$this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
|
||||
|
||||
current($optionnodes)->click();
|
||||
} catch (Exception $e) {
|
||||
// We continue and return as this means that the element is not there or it is not the same.
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
// Wait for all the possible AJAX requests that have been
|
||||
// already triggered by selectOption() to be finished.
|
||||
$this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
|
||||
|
||||
// Wrapped in a try & catch as we can fall into race conditions
|
||||
// and the element may not be there.
|
||||
try {
|
||||
// Multiple ones needs the click in the select.
|
||||
$this->field->click();
|
||||
} catch (Exception $e) {
|
||||
// We continue and return as this means that the element is not there or it is not the same.
|
||||
return;
|
||||
}
|
||||
|
||||
// Single select needs an extra click in the option.
|
||||
if (!$this->field->hasAttribute('multiple')) {
|
||||
// We ensure that the option is still there.
|
||||
if (!$this->session->getDriver()->find($optionxpath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$value = $this->session->getSelectorsHandler()->xpathLiteral($value);
|
||||
// Wait for all the possible AJAX requests that have been
|
||||
// already triggered by selectOption() to be finished.
|
||||
$this->session->wait(behat_base::TIMEOUT * 1000, behat_base::PAGE_READY_JS);
|
||||
|
||||
// Using the driver direcly because Element methods are messy when dealing
|
||||
// with elements inside containers.
|
||||
$optionxpath = $this->field->getXpath() .
|
||||
"/descendant::option[(./@value=$value or normalize-space(.)=$value)]";
|
||||
$optionnodes = $this->session->getDriver()->find($optionxpath);
|
||||
if ($optionnodes) {
|
||||
current($optionnodes)->click();
|
||||
// Wrapped in a try & catch as we can fall into race conditions
|
||||
// and the element may not be there.
|
||||
try {
|
||||
// Repeating the select(s) as some drivers (chrome that I know) are moving
|
||||
// to another option after the general select field click above.
|
||||
foreach ($options as $option) {
|
||||
$this->field->selectOption(trim($option), true);
|
||||
}
|
||||
|
||||
} else {
|
||||
// Multiple ones needs the click in the select.
|
||||
$this->field->click();
|
||||
} catch (Exception $e) {
|
||||
// We continue and return as this means that the element is not there or it is not the same.
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
+168
-14
@@ -245,6 +245,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_string>(?:[^"]|\\")*)" field should match "(?P<value_string>(?:[^"]|\\")*)" value$/
|
||||
* @throws ExpectationException
|
||||
* @throws ElementNotFoundException Thrown by behat_base::find
|
||||
@@ -297,20 +300,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('/(?<!\\\),/', $option)) as $opt) {
|
||||
$optionsarr[] = trim($opt);
|
||||
}
|
||||
} else {
|
||||
// Only one option has been passed.
|
||||
$optionsarr[] = trim($option);
|
||||
}
|
||||
|
||||
// Now get all the values and texts in the select.
|
||||
$options = $selectnode->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()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -320,18 +344,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('/(?<!\\\),/', $option)) as $opt) {
|
||||
$optionsarr[] = trim($opt);
|
||||
}
|
||||
} else {
|
||||
// Only one option has been passed.
|
||||
$optionsarr[] = trim($option);
|
||||
}
|
||||
|
||||
// Now get all the values and texts in the select.
|
||||
$options = $selectnode->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_string>(?:[^"]|\\")*)" select box should contain "(?P<option_string>(?:[^"]|\\")*)" 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('/(?<!\\\),/', $option)) as $opt) {
|
||||
$optionsarr[] = trim($opt);
|
||||
}
|
||||
} else {
|
||||
// Only one option has been passed.
|
||||
$optionsarr[] = trim($option);
|
||||
}
|
||||
|
||||
// Get currently selected texts.
|
||||
$field = behat_field_manager::get_form_field($selectnode, $this->getSession());
|
||||
$value = $field->get_value();
|
||||
|
||||
if ($multiple) {
|
||||
// Can be multiple comma separated, with valuable commas escaped with backslash.
|
||||
foreach (preg_replace('/\\\,/', ',', preg_split('/(?<!\\\),/', $value)) as $val) {
|
||||
$selectedarr[] = trim($val);
|
||||
}
|
||||
} else {
|
||||
// Only one text can be selected.
|
||||
$selectedarr[] = trim($value);
|
||||
}
|
||||
|
||||
// Everything normalized, Verify every option is a selected one.
|
||||
foreach ($optionsarr as $opt) {
|
||||
if (!in_array($opt, $selectedarr)) {
|
||||
throw new ExpectationException(
|
||||
'The select box "' . $select . '" does not contain the option "' . $opt . '"' . ' selected',
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks, that given select box contains the specified option not selected.
|
||||
*
|
||||
* @Then /^the "(?P<select_string>(?:[^"]|\\")*)" select box should contain "(?P<option_string>(?:[^"]|\\")*)" 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('/(?<!\\\),/', $option)) as $opt) {
|
||||
$optionsarr[] = trim($opt);
|
||||
}
|
||||
} else {
|
||||
// Only one option has been passed.
|
||||
$optionsarr[] = trim($option);
|
||||
}
|
||||
|
||||
// Get currently selected texts.
|
||||
$field = behat_field_manager::get_form_field($selectnode, $this->getSession());
|
||||
$value = $field->get_value();
|
||||
|
||||
if ($multiple) {
|
||||
// Can be multiple comma separated, with valuable commas escaped with backslash.
|
||||
foreach (preg_replace('/\\\,/', ',', preg_split('/(?<!\\\),/', $value)) as $val) {
|
||||
$selectedarr[] = trim($val);
|
||||
}
|
||||
} else {
|
||||
// Only one text can be selected.
|
||||
$selectedarr[] = trim($value);
|
||||
}
|
||||
|
||||
// Everything normalized, Verify every option is not a selected one.
|
||||
foreach ($optionsarr as $opt) {
|
||||
// Now, verify it's not selected.
|
||||
if (in_array($opt, $selectedarr)) {
|
||||
throw new ExpectationException(
|
||||
'The select box "' . $select . '" contains the option "' . $opt . '"' . ' selected',
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user