MDL-80907 behat: be more precise in selecting table rows

This commit is contained in:
Marina Glancy 2024-02-12 22:21:26 +00:00
parent f88dbfcafc
commit 569d9dfe39

View File

@ -1423,31 +1423,8 @@ EOF;
$rowliteral = behat_context_helper::escape($row);
$valueliteral = behat_context_helper::escape($value);
$columnliteral = behat_context_helper::escape($column);
if (preg_match('/^-?(\d+)-?$/', $column, $columnasnumber)) {
// Column indicated as a number, just use it as position of the column.
$columnpositionxpath = "/child::*[position() = {$columnasnumber[1]}]";
} else {
// Header can be in thead or tbody (first row), following xpath should work.
$theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
$columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]";
$tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)=" . $columnliteral . " or a[normalize-space(text())=" .
$columnliteral . "] or div[normalize-space(text())=" . $columnliteral . "])]";
// Check if column exists.
$columnheaderxpath = $tablexpath . "[" . $theadheaderxpath . " | " . $tbodyheaderxpath . "]";
$columnheader = $this->getSession()->getDriver()->find($columnheaderxpath);
if (empty($columnheader)) {
$columnexceptionmsg = $column . '" in table "' . $table . '"';
throw new ElementNotFoundException($this->getSession(), "\n$columnheaderxpath\n\n".'Column', null, $columnexceptionmsg);
}
// Following conditions were considered before finding column count.
// 1. Table header can be in thead/tr/th or tbody/tr/td[1].
// 2. First column can have th (Gradebook -> user report), so having lenient sibling check.
$columnpositionxpath = "/child::*[position() = count(" . $tablexpath . "/" . $theadheaderxpath .
"/preceding-sibling::*) + 1]";
}
$columnpositionxpath = $this->get_table_column_xpath($table, $column);
// Check if value exists in specific row/column.
// Get row xpath.
@ -1489,6 +1466,99 @@ EOF;
);
}
/**
* Get xpath for a row child that corresponds to the specified column header
*
* @param string $table table identifier that can be used with 'table' node selector (i.e. table title or CSS class)
* @param string $column either text in the column header or the column number, such as -1-, -2-, etc
* When matching the column header it has to be either exact match of the whole header or an exact
* match of a text inside a link in the header.
* For example, to match "<a>First name</a> / <a>Last name</a>" you need to specify either "First name" or "Last name"
* @return string
*/
protected function get_table_column_xpath(string $table, string $column): string {
$tablenode = $this->get_selected_node('table', $table);
$tablexpath = $tablenode->getXpath();
$columnliteral = behat_context_helper::escape($column);
if (preg_match('/^-?(\d+)-?$/', $column, $columnasnumber)) {
// Column indicated as a number, just use it as position of the column.
$columnpositionxpath = "/child::*[position() = {$columnasnumber[1]}]";
} else {
// Header can be in thead or tbody (first row), following xpath should work.
$theadheaderxpath = "thead/tr[1]/th[(normalize-space(.)={$columnliteral} or a[normalize-space(text())=" .
$columnliteral . "] or div[normalize-space(text())={$columnliteral}])]";
$tbodyheaderxpath = "tbody/tr[1]/td[(normalize-space(.)={$columnliteral} or a[normalize-space(text())=" .
$columnliteral . "] or div[normalize-space(text())={$columnliteral}])]";
// Check if column exists.
$columnheaderxpath = "{$tablexpath}[{$theadheaderxpath} | {$tbodyheaderxpath}]";
$columnheader = $this->getSession()->getDriver()->find($columnheaderxpath);
if (empty($columnheader)) {
if (strpos($column, '/') !== false) {
// We are not able to match headers consisting of several links, such as "First name / Last name".
// Instead we can match "First name" or "Last name" or "-1-" (column number).
throw new Exception("Column matching locator \"$column\" not found. ".
"If the column header contains multiple links, specify only one of the link texts. ".
"Otherwise, use the column number as the locator");
}
$columnexceptionmsg = $column . '" in table "' . $table . '"';
throw new ElementNotFoundException($this->getSession(), "\n$columnheaderxpath\n\n".'Column',
null, $columnexceptionmsg);
}
// Following conditions were considered before finding column count.
// 1. Table header can be in thead/tr/th or tbody/tr/td[1].
// 2. First column can have th (Gradebook -> user report), so having lenient sibling check.
$columnpositionxpath = "/child::*[position() = count({$tablexpath}/{$theadheaderxpath}" .
"/preceding-sibling::*) + 1]";
}
return $columnpositionxpath;
}
/**
* Find a table row where each of the specified columns matches and throw exception if not found
*
* @param string $table table locator
* @param array $cells key is the column locator (name or index such as '-1-') and value is the text contents of the table cell
*/
protected function ensure_table_row_exists(string $table, array $cells): void {
$tablenode = $this->get_selected_node('table', $table);
$tablexpath = $tablenode->getXpath();
$columnconditions = [];
foreach ($cells as $columnname => $value) {
$valueliteral = behat_context_helper::escape($value);
$columnpositionxpath = $this->get_table_column_xpath($table, $columnname);
$columnconditions[] = '.' . $columnpositionxpath . "[contains(normalize-space(.)," . $valueliteral . ")]";
}
$rowxpath = $tablexpath . "/tbody/tr[" . join(' and ', $columnconditions) . ']';
$rownode = $this->getSession()->getDriver()->find($rowxpath);
if (empty($rownode)) {
$rowlocator = array_map(fn($k) => "{$k} => {$cells[$k]}", array_keys($cells));
throw new ElementNotFoundException($this->getSession(), "\n$rowxpath\n\n".'Table row', null, join(', ', $rowlocator));
}
}
/**
* Find a table row where each of the specified columns matches and throw exception if found
*
* @param string $table table locator
* @param array $cells key is the column locator (name or index such as '-1-') and value is the text contents of the table cell
*/
protected function ensure_table_row_does_not_exist(string $table, array $cells): void {
try {
$this->ensure_table_row_exists($table, $cells);
// Throw exception if found.
} catch (ElementNotFoundException $e) {
// Table row/column doesn't contain this value. Nothing to do.
return;
}
$rowlocator = array_map(fn($k) => "{$k} => {$cells[$k]}", array_keys($cells));
throw new ExpectationException('Table row "' . join(', ', $rowlocator) .
'" is present in the table "' . $table . '"', $this->getSession()
);
}
/**
* Checks that the provided value exist in table.
*
@ -1505,29 +1575,22 @@ EOF;
*/
public function following_should_exist_in_the_table($table, TableNode $data) {
$datahash = $data->getHash();
if ($datahash && count($data->getRow(0)) != count($datahash[0])) {
// Check that the number of columns in the hash is the same as the number of the columns in the first row.
throw new coding_exception('Table contains duplicate column headers');
}
foreach ($datahash as $row) {
// Row contains only a single column, just assert it's present in the table.
if (count($row) === 1) {
$this->execute('behat_general::assert_element_contains_text', [reset($row), $table, 'table']);
} else {
// Iterate over all columns.
$firstcell = null;
foreach ($row as $column => $value) {
if ($firstcell === null) {
$firstcell = $value;
} else {
$this->row_column_of_table_should_contain($firstcell, $column, $table, $value);
}
}
}
$this->ensure_table_row_exists($table, $row);
}
}
/**
* Checks that the provided values do not exist in a table.
*
* If there are more than two columns, we check that NEITHER of the columns 2..n match
* in the row where the first column matches
*
* @Then /^the following should not exist in the "(?P<table_string>[^"]*)" table:$/
* @throws ExpectationException
* @param string $table name of table
@ -1537,27 +1600,24 @@ EOF;
*/
public function following_should_not_exist_in_the_table($table, TableNode $data) {
$datahash = $data->getHash();
if ($datahash && count($data->getRow(0)) != count($datahash[0])) {
// Check that the number of columns in the hash is the same as the number of the columns in the first row.
throw new coding_exception('Table contains duplicate column headers');
}
foreach ($datahash as $value) {
// Row contains only a single column, just assert it's not present in the table.
if (count($value) === 1) {
$this->execute('behat_general::assert_element_not_contains_text', [reset($value), $table, 'table']);
} else {
// Iterate over all columns.
$row = array_shift($value);
foreach ($value as $column => $value) {
try {
$this->row_column_of_table_should_contain($row, $column, $table, $value);
// Throw exception if found.
} catch (ElementNotFoundException $e) {
// Table row/column doesn't contain this value. Nothing to do.
continue;
}
throw new ExpectationException('"' . $column . '" with value "' . $value . '" is present in "' .
$row . '" row for table "' . $table . '"', $this->getSession()
);
if (count($value) > 2) {
// When there are more than two columns, what we really want to check is that for the rows
// where the first column matches, NEITHER of the other columns match.
$columns = array_keys($value);
for ($i = 1; $i < count($columns); $i++) {
$this->ensure_table_row_does_not_exist($table, [
$columns[0] => $value[$columns[0]],
$columns[$i] => $value[$columns[$i]],
]);
}
} else {
$this->ensure_table_row_does_not_exist($table, $value);
}
}
}