Skip to content

Commit

Permalink
MDL-43713 behat: improve multi-select support
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
stronk7 authored and David Monllao committed Feb 27, 2014
1 parent b71419a commit 5f66d46
Show file tree
Hide file tree
Showing 2 changed files with 247 additions and 30 deletions.
95 changes: 79 additions & 16 deletions lib/behat/form_field/behat_form_select.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@
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) {
Expand All @@ -61,8 +61,26 @@ public function set_value($value) {
$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('/(?<!\\\),/', $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()) {
Expand All @@ -87,11 +105,13 @@ public function set_value($value) {
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.
Expand Down Expand Up @@ -150,9 +170,11 @@ public function set_value($value) {
// 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;
Expand All @@ -161,12 +183,53 @@ public function set_value($value) {
}

/**
* 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);
}
}
182 changes: 168 additions & 14 deletions lib/tests/behat/behat_forms.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,9 @@ public function uncheck_option($option) {
/**
* 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
Expand Down Expand Up @@ -288,20 +291,41 @@ public function assert_checkbox_not_checked($checkbox) {
* @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()
);
}
}
}

/**
Expand All @@ -311,18 +335,148 @@ public function the_select_box_should_contain($select, $option) {
* @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()
);
}
}
}

Expand Down

0 comments on commit 5f66d46

Please sign in to comment.