From 389760815259f19c1d70ddf0f31a06c970181030 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Fri, 21 Jun 2013 13:48:36 +0800 Subject: [PATCH] MDL-39635 behat: XPath cleanups - Escaping steps arguments redirected to other steps - Adding normalized-space() in all contains() assertions - General xpaths review - Convering provided xpath text strings to xpath literals to avoid problems with arguments containing both single quotes and double quotes --- admin/tests/behat/behat_admin.php | 6 +- admin/tests/behat/display_short_names.feature | 2 +- .../behat/tests/behat/basic_actions.feature | 4 +- .../behat/tests/behat/data_generators.feature | 16 ++--- auth/tests/behat/behat_auth.php | 4 +- .../ui/tests/behat/backup_courses.feature | 4 +- backup/util/ui/tests/behat/behat_backup.php | 41 +++++++---- .../tests/behat/behat_block_comments.php | 9 ++- blocks/tests/behat/behat_blocks.php | 2 +- .../configure_block_throughout_site.feature | 2 +- cohort/tests/behat/behat_cohort.php | 2 +- .../tests/behat/upload_cohort_users.feature | 4 +- completion/tests/behat/behat_completion.php | 15 ++-- .../behat/activities_group_icons.feature | 12 ++-- .../behat/activities_indentation.feature | 4 +- course/tests/behat/behat_course.php | 68 ++++++++++--------- course/tests/behat/force_group_mode.feature | 30 ++++---- enrol/tests/behat/behat_enrol.php | 2 +- group/tests/behat/behat_groups.php | 13 ++-- lib/behat/behat_files.php | 22 ++++-- lib/behat/form_field/behat_form_select.php | 5 +- lib/tests/behat/behat_forms.php | 11 +-- lib/tests/behat/behat_general.php | 8 +-- lib/tests/behat/behat_navigation.php | 28 +++++--- lib/tests/behat/behat_permissions.php | 2 +- message/tests/behat/behat_message.php | 6 +- ...etion_condition_number_discussions.feature | 4 +- .../tests/behat/behat_mod_glossary.php | 2 +- .../behat/print_friendly_version.feature | 4 +- .../tests/behat/lesson_navigation.feature | 2 +- question/tests/behat/behat_question.php | 16 +++-- repository/tests/behat/behat_filepicker.php | 8 ++- .../tests/behat/cancel_add_file.feature | 2 +- .../tests/behat/behat_repository_upload.php | 6 +- 34 files changed, 211 insertions(+), 155 deletions(-) diff --git a/admin/tests/behat/behat_admin.php b/admin/tests/behat/behat_admin.php index f29ebd185d0..6b4f88f69ae 100644 --- a/admin/tests/behat/behat_admin.php +++ b/admin/tests/behat/behat_admin.php @@ -72,8 +72,12 @@ class behat_admin extends behat_base { // Admin settings does not use the same DOM structure than other moodle forms // but we also need to use lib/behat/form_field/* to deal with the different moodle form elements. $exception = new ElementNotFoundException($this->getSession(), '"' . $label . '" administration setting '); + + // The argument should be converted to an xpath literal. + $label = $this->getSession()->getSelectorsHandler()->xpathLiteral($label); + $fieldxpath = "//*[self::input | self::textarea | self::select][not(./@type = 'submit' or ./@type = 'image' or ./@type = 'hidden')]" . - "[@id=//label[contains(normalize-space(string(.)), '" . $label . "')]/@for]"; + "[@id=//label[contains(normalize-space(.), $label)]/@for]"; $fieldnode = $this->find('xpath', $fieldxpath, $exception); $formfieldtypenode = $this->find('xpath', $fieldxpath . "/ancestor::div[@class='form-setting']" . "/child::div[contains(concat(' ', @class, ' '), ' form-')]/child::*/parent::div"); diff --git a/admin/tests/behat/display_short_names.feature b/admin/tests/behat/display_short_names.feature index b6c825812e8..f17f092191f 100644 --- a/admin/tests/behat/display_short_names.feature +++ b/admin/tests/behat/display_short_names.feature @@ -15,7 +15,7 @@ Feature: Display extended course names And I should not see "C_shortname Course fullname" Scenario: Courses list with extended course names - Given I click on "Courses" "link" in the "//div[@id='settingsnav']//descendant::li[contains(concat(' ', @class, ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element" + Given I click on "Courses" "link" in the "//div[@id='settingsnav']/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' type_setting ')][not(contains(., 'Site administration'))][contains(., 'Appearance')]" "xpath_element" And I check "Display extended course names" When I press "Save changes" And I am on homepage diff --git a/admin/tool/behat/tests/behat/basic_actions.feature b/admin/tool/behat/tests/behat/basic_actions.feature index c2f829bd659..637086f20a8 100644 --- a/admin/tool/behat/tests/behat/basic_actions.feature +++ b/admin/tool/behat/tests/behat/basic_actions.feature @@ -37,7 +37,7 @@ Feature: Page contents assertions And I follow "Course 1" When I click on "Move this to the dock" "button" in the ".block_settings" "css_element" Then I should not see "Question bank" - And I click on "//div[@id='dock']/descendant::*[contains(., 'Administration')]/h2" "xpath_element" + And I click on "//div[@id='dock']/descendant::h2[normalize-space(.)='Administration']" "xpath_element" @javascript Scenario: Locators inside specific DOM nodes using XPath @@ -45,5 +45,5 @@ Feature: Page contents assertions | fullname | shortname | category | | Course 1 | C1 | 0 | And I log in as "admin" - When I click on "Move this to the dock" "button" in the "//*[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element" + When I click on "Move this to the dock" "button" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_settings ')]" "xpath_element" Then I should not see "Turn editing on" diff --git a/admin/tool/behat/tests/behat/data_generators.feature b/admin/tool/behat/tests/behat/data_generators.feature index a98adcf9f1a..b19db483ed7 100644 --- a/admin/tool/behat/tests/behat/data_generators.feature +++ b/admin/tool/behat/tests/behat/data_generators.feature @@ -30,17 +30,17 @@ Feature: Set up contextual data for tests Then I should see "Course 1" And I should see "Course 2" And I should see "Course 3" - When I go to the courses management page + And I go to the courses management page And I follow "Cat 1" - Then I should see "Cat 2" + And I should see "Cat 2" And I should see "Cat 3" - When I follow "Cat 3" - Then I should see "Course 1" + And I follow "Cat 3" + And I should see "Course 1" And I should see "Course 2" - When I select "Cat 2" from "Course categories:" - Then I should see "No courses in this category" - When I select "Miscellaneous" from "Course categories:" - Then I should see "Course 3" + And I select "Cat 1 / Cat 2" from "Course categories:" + And I should see "No courses in this category" + And I select "Miscellaneous" from "Course categories:" + And I should see "Course 3" @javascript Scenario: Add a bunch of groups and groupings diff --git a/auth/tests/behat/behat_auth.php b/auth/tests/behat/behat_auth.php index b484b4f876f..4ced1542330 100644 --- a/auth/tests/behat/behat_auth.php +++ b/auth/tests/behat/behat_auth.php @@ -50,8 +50,8 @@ class behat_auth extends behat_base { return array(new Given('I am on homepage'), new Given('I follow "' . get_string('login') . '"'), - new Given('I fill in "' . get_string('username') . '" with "'.$username.'"'), - new Given('I fill in "' . get_string('password') . '" with "'.$username.'"'), + new Given('I fill in "' . get_string('username') . '" with "' . $this->escape($username) . '"'), + new Given('I fill in "' . get_string('password') . '" with "'. $this->escape($username) . '"'), new Given('I press "' . get_string('login') . '"') ); } diff --git a/backup/util/ui/tests/behat/backup_courses.feature b/backup/util/ui/tests/behat/backup_courses.feature index 488ab6013cb..f8b85d75f0b 100644 --- a/backup/util/ui/tests/behat/backup_courses.feature +++ b/backup/util/ui/tests/behat/backup_courses.feature @@ -31,7 +31,7 @@ Feature: Backup Moodle courses And I should not see "Section 3" And I press "Continue" And I click on "Continue" "button" in the ".bcs-current-course" "css_element" - And I click on "//div[contains(concat(' ', @class, ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element" - And I click on "setting_root_logs" "checkbox" in the "//div[contains(@class, 'fitem')][contains(., 'Include course logs')]" "xpath_element" + And "//div[contains(concat(' ', normalize-space(@class), ' '), ' fitem ')][contains(., 'Include calendar events')]/descendant::img" "xpath_element" should exists + And I check "Include course logs" And I press "Cancel" And I click on "Cancel" "button" in the ".confirmation-dialogue" "css_element" diff --git a/backup/util/ui/tests/behat/behat_backup.php b/backup/util/ui/tests/behat/behat_backup.php index 0b22d74e95f..d06185ed304 100644 --- a/backup/util/ui/tests/behat/behat_backup.php +++ b/backup/util/ui/tests/behat/behat_backup.php @@ -105,15 +105,16 @@ class behat_backup extends behat_base { // Click the course link. $this->find_link($tocourse)->click(); - // Click the backup link. + // Click the import link. $this->find_link(get_string('import'))->click(); // Select the course. $exception = new ExpectationException('"' . $fromcourse . '" course not found in the list of courses to import from', $this->getSession()); - $fromcourse = str_replace("'", "\'", $fromcourse); - $xpath = "//div[contains(concat(' ', @class, ' '), ' ics-results ')]" . - "/descendant::tr[contains(., '" . $fromcourse . "')]" . + // The argument should be converted to an xpath literal. + $fromcourse = $this->getSession()->getSelectorsHandler()->xpathLiteral($fromcourse); + $xpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' ics-results ')]" . + "/descendant::tr[contains(., $fromcourse)]" . "/descendant::input[@type='radio']"; $radionode = $this->find('xpath', $xpath, $exception); $radionode->check(); @@ -150,17 +151,19 @@ class behat_backup extends behat_base { // Confirm restore. $this->select_backup($backupfilename); + // The argument should be converted to an xpath literal. + $existingcourse = $this->getSession()->getSelectorsHandler()->xpathLiteral($existingcourse); + // Selecting the specified course (we can not call behat_forms::select_radio here as is in another behat subcontext). - $existingcourse = str_replace("'", "\'", $existingcourse); - $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]" . + $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" . "/descendant::div[@class='restore-course-search']" . - "/descendant::tr[contains(., '" . $existingcourse . "')]" . + "/descendant::tr[contains(., $existingcourse)]" . "/descendant::input[@type='radio']"); $radionode->check(); $radionode->click(); // Pressing the continue button of the restore into an existing course section. - $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]" . + $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-existing-course ')]" . "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"); $continuenode->click(); $this->wait(); @@ -182,14 +185,14 @@ class behat_backup extends behat_base { $this->select_backup($backupfilename); // The first category in the list. - $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]" . + $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" . "/descendant::div[@class='restore-course-search']" . "/descendant::input[@type='radio']"); $radionode->check(); $radionode->click(); // Pressing the continue button of the restore into an existing course section. - $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]" . + $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' bcs-new-course ')]" . "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"); $continuenode->click(); $this->wait(); @@ -211,13 +214,13 @@ class behat_backup extends behat_base { $this->select_backup($backupfilename); // Merge without deleting radio option. - $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" . + $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" . "/descendant::input[@type='radio'][@name='target'][@value='1']"); $radionode->check(); $radionode->click(); // Pressing the continue button of the restore merging section. - $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" . + $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" . "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"); $continuenode->click(); $this->wait(); @@ -239,13 +242,13 @@ class behat_backup extends behat_base { $this->select_backup($backupfilename); // Delete contents radio option. - $radionode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" . + $radionode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" . "/descendant::input[@type='radio'][@name='target'][@value='0']"); $radionode->check(); $radionode->click(); // Pressing the continue button of the restore merging section. - $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" . + $continuenode = $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), 'bcs-current-course')]" . "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"); $continuenode->click(); $this->wait(); @@ -265,7 +268,11 @@ class behat_backup extends behat_base { // Using xpath as there are other restore links before this one. $exception = new ExpectationException('The "' . $backupfilename . '" backup file can not be found in this page', $this->getSession()); - $xpath = "//tr[contains(., '" . $backupfilename . "')]/descendant::a[contains(., '" . get_string('restore') . "')]"; + + // The argument should be converted to an xpath literal. + $backupfilename = $this->getSession()->getSelectorsHandler()->xpathLiteral($backupfilename); + + $xpath = "//tr[contains(., $backupfilename)]/descendant::a[contains(., '" . get_string('restore') . "')]"; $restorelink = $this->find('xpath', $xpath, $exception); $restorelink->click(); @@ -341,6 +348,10 @@ class behat_backup extends behat_base { */ protected function wait($timeout = false) { + if (!$this->running_javascript()) { + return; + } + if (!$timeout) { $timeout = self::TIMEOUT; } diff --git a/blocks/comments/tests/behat/behat_block_comments.php b/blocks/comments/tests/behat/behat_block_comments.php index 8854d3b33f2..3cac07a0d8f 100644 --- a/blocks/comments/tests/behat/behat_block_comments.php +++ b/blocks/comments/tests/behat/behat_block_comments.php @@ -91,8 +91,11 @@ class behat_block_comments extends behat_base { $exception = new ElementNotFoundException($this->getSession(), '"' . $comment . '" comment '); - $commentxpath = "//div[contains(concat(' ', @class, ' '), ' block_comments ')]" . - "/descendant::div[@class='comment-message'][contains(., '" . $comment . "')]"; + // Using xpath liternal to avoid possible problems with comments containing quotes. + $commentliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($comment); + + $commentxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' block_comments ')]" . + "/descendant::div[@class='comment-message'][contains(., $commentliteral)]"; $commentnode = $this->find('xpath', $commentxpath, $exception); // Click on delete icon. @@ -101,7 +104,7 @@ class behat_block_comments extends behat_base { $deleteicon->click(); // Yes confirm. - $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., 'Yes')]"); + $confirmnode = $this->find('xpath', "//div[@class='comment-delete-confirm']/descendant::a[contains(., '" . get_string('yes') . "')]"); $confirmnode->click(); // Wait for the AJAX request. diff --git a/blocks/tests/behat/behat_blocks.php b/blocks/tests/behat/behat_blocks.php index 881880b6a92..42fe9a117c7 100644 --- a/blocks/tests/behat/behat_blocks.php +++ b/blocks/tests/behat/behat_blocks.php @@ -46,7 +46,7 @@ class behat_blocks extends behat_base { * @param string $blockname */ public function i_add_the_block($blockname) { - $steps = new Given('I select "' . $blockname . '" from "bui_addblock"'); + $steps = new Given('I select "' . $this->escape($blockname) . '" from "bui_addblock"'); // If we are running without javascript we need to submit the form. if (!$this->running_javascript()) { diff --git a/blocks/tests/behat/configure_block_throughout_site.feature b/blocks/tests/behat/configure_block_throughout_site.feature index fb97f0483d1..9f74446e7ca 100644 --- a/blocks/tests/behat/configure_block_throughout_site.feature +++ b/blocks/tests/behat/configure_block_throughout_site.feature @@ -32,4 +32,4 @@ Feature: Add and configure blocks throughout the site And I press "Save changes" And I follow "Course 1" # The first block matching the pattern should be top-left block - And I should see "Comments" in the "//*[@id='region-pre']/descendant::div[contains(concat(' ', @class, ' '), ' block ')]" "xpath_element" + And I should see "Comments" in the "//*[@id='region-pre']/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' block ')]" "xpath_element" diff --git a/cohort/tests/behat/behat_cohort.php b/cohort/tests/behat/behat_cohort.php index fcce6928e0e..5349c53e751 100644 --- a/cohort/tests/behat/behat_cohort.php +++ b/cohort/tests/behat/behat_cohort.php @@ -54,7 +54,7 @@ class behat_cohort extends behat_base { $userid = $DB->get_field('user', 'id', array('username' => $username)); $steps = array( - new Given('I click on "' . get_string('assign', 'cohort') . '" "link" in the "//table[@id=\'cohorts\']//tr[contains(., \'' . $cohortidnumber . '\')]" "xpath_element"'), + new Given('I click on "' . get_string('assign', 'cohort') . '" "link" in the "' . $this->escape($cohortidnumber) . '" table row'), new Given('I select "' . $userid . '" from "' . get_string('potusers', 'cohort') . '"'), new Given('I press "' . get_string('add') . '"'), new Given('I press "' . get_string('backtocohorts', 'cohort') . '"') diff --git a/cohort/tests/behat/upload_cohort_users.feature b/cohort/tests/behat/upload_cohort_users.feature index c40095f16db..47b169a29d7 100644 --- a/cohort/tests/behat/upload_cohort_users.feature +++ b/cohort/tests/behat/upload_cohort_users.feature @@ -32,11 +32,11 @@ Feature: Upload users to a cohort And I press "Upload users" And I press "Continue" And I follow "Cohorts" - And I click on "Assign" "link" in the "//table[@id='cohorts']//tr[contains(., 'Cohort 1')]" "xpath_element" + And I click on "Assign" "link" in the "Cohort 1" table row Then the "Current users" select box should contain "Tom Jones (tomjones@example.com)" And the "Current users" select box should contain "Bob Jones (bobjones@example.com)" And I press "Back to cohorts" - And I click on "Assign" "link" in the "//table[@id='cohorts']//tr[contains(., 'Cohort 2')]" "xpath_element" + And I click on "Assign" "link" in the "Cohort 2" table row And the "Current users" select box should contain "Mary Smith (marysmith@example.com)" And the "Current users" select box should contain "Alice Smith (alicesmith@example.com)" And I am on homepage diff --git a/completion/tests/behat/behat_completion.php b/completion/tests/behat/behat_completion.php index fe325406a0b..89b6344b85f 100644 --- a/completion/tests/behat/behat_completion.php +++ b/completion/tests/behat/behat_completion.php @@ -50,12 +50,13 @@ class behat_completion extends behat_base { public function user_has_completed_activity($userfullname, $activityname) { // Will throw an exception if the element can not be hovered. + $titleliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($userfullname . ", " . $activityname . ": Completed"); $xpath = "//table[@id='completion-progress']" . - "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Completed')]"; + "/descendant::img[contains(@title, $titleliteral)]"; return array( new Given('I go to the current course activity completion report'), - new Given('I hover "' . $xpath . '" "xpath_element"') + new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"') ); } @@ -68,11 +69,13 @@ class behat_completion extends behat_base { */ public function user_has_not_completed_activity($userfullname, $activityname) { + // Will throw an exception if the element can not be hovered. + $titleliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($userfullname . ", " . $activityname . ": Not completed"); $xpath = "//table[@id='completion-progress']" . - "/descendant::img[contains(@title, '" . $userfullname . ", " . $activityname . ": Not completed')]"; + "/descendant::img[contains(@title, $titleliteral)]"; return array( new Given('I go to the current course activity completion report'), - new Given('I hover "' . $xpath . '" "xpath_element"') + new Given('I hover "' . $this->escape($xpath) . '" "xpath_element"') ); return $steps; @@ -89,9 +92,9 @@ class behat_completion extends behat_base { // Expand reports node if we can't see the link. try { - $this->find('xpath', "//*[@id='settingsnav']" . + $this->find('xpath', "//div[@id='settingsnav']" . "/descendant::li" . - "/descendant::li[not(contains(@class,'collapsed'))]" . + "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]" . "/descendant::p[contains(., '" . get_string('pluginname', 'report_progress') . "')]"); } catch (ElementNotFoundException $e) { $steps[] = new Given('I expand "' . get_string('reports') . '" node'); diff --git a/course/tests/behat/activities_group_icons.feature b/course/tests/behat/activities_group_icons.feature index 78c342aa7cb..65abea5d721 100644 --- a/course/tests/behat/activities_group_icons.feature +++ b/course/tests/behat/activities_group_icons.feature @@ -27,21 +27,21 @@ Feature: Toggle activities groups mode from the course page | Force group mode | No | When I press "Save changes" Then "No groups (Click to change)" "link" should exists - And ".//a//img[contains(@src, 'groupn')]" "xpath_element" should exists + And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists And I click on "No groups (Click to change)" "link" in the "Test forum name" activity And I wait "3" seconds And "Separate groups (Click to change)" "link" should exists - And ".//a//img[contains(@src, 'groups')]" "xpath_element" should exists + And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists And I reload the page And "Separate groups (Click to change)" "link" should exists - And ".//a//img[contains(@src, 'groups')]" "xpath_element" should exists + And "//a/child::img[contains(@src, 'groups')]" "xpath_element" should exists And I click on "Separate groups (Click to change)" "link" in the "Test forum name" activity And I wait "3" seconds And "Visible groups (Click to change)" "link" should exists - And ".//a//img[contains(@src, 'groupv')]" "xpath_element" should exists + And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists And I reload the page And "Visible groups (Click to change)" "link" should exists - And ".//a//img[contains(@src, 'groupv')]" "xpath_element" should exists + And "//a/child::img[contains(@src, 'groupv')]" "xpath_element" should exists And I click on "Visible groups (Click to change)" "link" in the "Test forum name" activity And "No groups (Click to change)" "link" should exists - And ".//a//img[contains(@src, 'groupn')]" "xpath_element" should exists + And "//a/child::img[contains(@src, 'groupn')]" "xpath_element" should exists diff --git a/course/tests/behat/activities_indentation.feature b/course/tests/behat/activities_indentation.feature index 5621e49aae9..7cd00794680 100644 --- a/course/tests/behat/activities_indentation.feature +++ b/course/tests/behat/activities_indentation.feature @@ -26,7 +26,7 @@ Feature: Indent items on the course page When I indent right "Test glossary name" activity Then "#section-1 li.glossary div.mod-indent-1" "css_element" should exists And I indent right "Test glossary name" activity - And "//*[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists + And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should exists And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists And I reload the page And "#section-1 li.glossary div.mod-indent-2" "css_element" should exists @@ -34,4 +34,4 @@ Feature: Indent items on the course page And I indent left "Test glossary name" activity And "#section-1 li.glossary div.mod-indent-2" "css_element" should not exists And "#section-1 li.glossary div.mod-indent-1" "css_element" should not exists - And "//*[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists + And "//li[@id='section-1']/descendant::li[contains(concat(' ', @class, ' '), ' glossary ')]/descendant::a[@title='Move left']" "xpath_element" should not exists diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php index 17f6d387c24..49145f29859 100644 --- a/course/tests/behat/behat_course.php +++ b/course/tests/behat/behat_course.php @@ -100,7 +100,7 @@ class behat_course extends behat_base { public function i_add_to_section_and_i_fill_the_form_with($activity, $section, TableNode $data) { return array( - new Given('I add a "'.$activity.'" to section "'.$section.'"'), + new Given('I add a "' . $this->escape($activity) . '" to section "' . $this->escape($section) . '"'), new Given('I fill the moodle form with:', $data), new Given('I press "' . get_string('savechangesandreturntocourse') . '"') ); @@ -116,7 +116,9 @@ class behat_course extends behat_base { */ public function i_add_to_section($activity, $section) { - $sectionxpath = "//*[@id='section-" . $section . "']"; + $sectionxpath = "//li[@id='section-" . $section . "']"; + + $activityliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral(ucfirst($activity)); if ($this->running_javascript()) { @@ -126,9 +128,9 @@ class behat_course extends behat_base { $sectionnode->click(); // Clicks the selected activity if it exists. - $activity = ucfirst($activity); $activityxpath = "//div[@id='chooseform']/descendant::label" . - "/descendant::span[contains(concat(' ', @class, ' '), ' typename ')][contains(.,'" . $activity . "')]" . + "/descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' typename ')]" . + "[contains(., $activityliteral)]" . "/parent::label/child::input"; $activitynode = $this->find('xpath', $activityxpath); $activitynode->doubleClick(); @@ -137,8 +139,8 @@ class behat_course extends behat_base { // Without Javascript. // Selecting the option from the select box which contains the option. - $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', @class, ' '), ' section_add_menus ')]" . - "/descendant::select[contains(., '" . $activity . "')]"; + $selectxpath = $sectionxpath . "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' section_add_menus ')]" . + "/descendant::select[contains(., $activityliteral)]"; $selectnode = $this->find('xpath', $selectxpath); $selectnode->selectOption($activity); @@ -162,7 +164,7 @@ class behat_course extends behat_base { $xpath = $this->section_exists($sectionnumber); return array( - new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'), + new Given('I click on "' . get_string('markthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'), new Given('I wait "2" seconds') ); } @@ -179,7 +181,7 @@ class behat_course extends behat_base { $xpath = $this->section_exists($sectionnumber); return array( - new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $xpath . '" "xpath_element"'), + new Given('I click on "' . get_string('markedthistopic') . '" "link" in the "' . $this->escape($xpath) . '" "xpath_element"'), new Given('I wait "2" seconds') ); } @@ -268,7 +270,7 @@ class behat_course extends behat_base { // Section should be hidden. $exception = new ExpectationException('The section is not hidden', $this->getSession()); - $this->find('xpath', $sectionxpath . "[contains(concat(' ', @class, ' '), ' hidden ')]", $exception); + $this->find('xpath', $sectionxpath . "[contains(concat(' ', normalize-space(@class), ' '), ' hidden ')]", $exception); // The checking are different depending on user permissions. if ($this->is_course_editor()) { @@ -284,8 +286,8 @@ class behat_course extends behat_base { foreach ($activities as $activity) { // Dimmed. - $this->find('xpath', "//div[contains(concat(' ', @class, ' '), ' activityinstance ')]" . - "/a[contains(concat(' ', @class, ' '), ' dimmed ')]", $dimmedexception, $activity); + $this->find('xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' activityinstance ')]" . + "/a[contains(concat(' ', normalize-space(@class), ' '), ' dimmed ')]", $dimmedexception, $activity); // Non-JS browsers can not click on img elements. if ($this->running_javascript()) { @@ -319,7 +321,8 @@ class behat_course extends behat_base { $sectionxpath = $this->section_exists($sectionnumber); // Section should not be hidden. - if (!$this->getSession()->getPage()->find('xpath', $sectionxpath . "[not(contains(concat(' ', @class, ' '), ' hidden '))]")) { + $xpath = $sectionxpath . "[not(contains(concat(' ', normalize-space(@class), ' '), ' hidden '))]"; + if (!$this->getSession()->getPage()->find('xpath', $xpath)) { throw new ExpectationException('The section is hidden', $this->getSession()); } @@ -449,10 +452,11 @@ class behat_course extends behat_base { // JS enabled. if ($this->running_javascript()) { - $destinationxpath = $sectionxpath . "/descendant::ul[contains(@class, 'yui3-dd-drop')]"; + $destinationxpath = $sectionxpath . "/descendant::ul[contains(concat(' ', normalize-space(@class), ' '), ' yui3-dd-drop ')]"; return array( - new Given('I drag "' . $activitynode->getXpath() . '" "xpath_element" and I drop it in "' . $destinationxpath . '" "xpath_element"'), + new Given('I drag "' . $this->escape($activitynode->getXpath()) . '" "xpath_element" ' . + 'and I drop it in "' . $this->escape($destinationxpath) . '" "xpath_element"'), ); } else { @@ -460,8 +464,8 @@ class behat_course extends behat_base { // Moving to the fist spot of the section (before all other section's activities). return array( - new Given('I click on "a.editing_move" "css_element" in the "' . $activityname . '" activity'), - new Given('I click on "li.movehere a" "css_element" in the "' . $sectionxpath . '" "xpath_element"'), + new Given('I click on "a.editing_move" "css_element" in the "' . $this->escape($activityname) . '" activity'), + new Given('I click on "li.movehere a" "css_element" in the "' . $this->escape($sectionxpath) . '" "xpath_element"'), ); } } @@ -482,8 +486,8 @@ class behat_course extends behat_base { // Adding chr(10) to save changes. return array( - new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $activityname .'" activity'), - new Given('I fill in "title" with "' . $newactivityname . chr(10) . '"'), + new Given('I click on "' . get_string('edittitle') . '" "link" in the "' . $this->escape($activityname) .'" activity'), + new Given('I fill in "title" with "' . $this->escape($newactivityname) . chr(10) . '"'), new Given('I wait "2" seconds') ); } @@ -497,7 +501,7 @@ class behat_course extends behat_base { public function i_indent_right_activity($activityname) { $steps = array( - new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $activityname . '" activity') + new Given('I click on "' . get_string('moveright') . '" "link" in the "' . $this->escape($activityname) . '" activity') ); if ($this->running_javascript()) { @@ -516,7 +520,7 @@ class behat_course extends behat_base { public function i_indent_left_activity($activityname) { $steps = array( - new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $activityname . '" activity') + new Given('I click on "' . get_string('moveleft') . '" "link" in the "' . $this->escape($activityname) . '" activity') ); if ($this->running_javascript()) { @@ -553,7 +557,7 @@ class behat_course extends behat_base { // With JS disabled. $steps = array( - new Given('I click on "' . $deletestring . '" "link" in the "' . $activityname . '" activity'), + new Given('I click on "' . $this->escape($deletestring) . '" "link" in the "' . $this->escape($activityname) . '" activity'), new Given('I press "' . get_string('yes') . '"') ); @@ -569,7 +573,7 @@ class behat_course extends behat_base { */ public function i_duplicate_activity($activityname) { return array( - new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'), + new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'), new Given('I press "' . get_string('continue') .'"'), new Given('I press "' . get_string('duplicatecontcourse') .'"') ); @@ -584,7 +588,7 @@ class behat_course extends behat_base { */ public function i_duplicate_activity_editing_the_new_copy_with($activityname, TableNode $data) { return array( - new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $activityname . '" activity'), + new Given('I click on "' . get_string('duplicate') . '" "link" in the "' . $this->escape($activityname) . '" activity'), new Given('I press "' . get_string('continue') .'"'), new Given('I press "' . get_string('duplicatecontedit') . '"'), new Given('I fill the moodle form with:', $data), @@ -657,9 +661,9 @@ class behat_course extends behat_base { $courseformat = $this->get_course_format(); // Checking the show button alt text and show icon. - $showtext = get_string('showfromothers', $courseformat); - $linkxpath = $xpath . "/descendant::a[@title='". $showtext ."']"; - $imgxpath = $linkxpath . "/descendant::img[@alt='". $showtext ."'][contains(@src, 'show')]"; + $showtext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('showfromothers', $courseformat)); + $linkxpath = $xpath . "/descendant::a[@title=$showtext]"; + $imgxpath = $linkxpath . "/descendant::img[@alt=$showtext][contains(@src, 'show')]"; $exception = new ElementNotFoundException($this->getSession(), 'Show section icon '); $this->find('xpath', $imgxpath, $exception); @@ -684,9 +688,9 @@ class behat_course extends behat_base { $courseformat = $this->get_course_format(); // Checking the hide button alt text and hide icon. - $hidetext = get_string('hidefromothers', $courseformat); - $linkxpath = $xpath . "/descendant::a[@title='" . $hidetext . "']"; - $imgxpath = $linkxpath . "/descendant::img[@alt='" . $hidetext ."'][contains(@src, 'hide')]"; + $hidetext = $this->getSession()->getSelectorsHandler()->xpathLiteral(get_string('hidefromothers', $courseformat)); + $linkxpath = $xpath . "/descendant::a[@title=$hidetext]"; + $imgxpath = $linkxpath . "/descendant::img[@alt=$hidetext][contains(@src, 'hide')]"; $exception = new ElementNotFoundException($this->getSession(), 'Hide section icon '); $this->find('xpath', $imgxpath, $exception); @@ -730,7 +734,7 @@ class behat_course extends behat_base { */ protected function get_section_activities($sectionxpath) { - $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', @class, ' '), ' activity ')]"; + $xpath = $sectionxpath . "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')]"; // We spin here, as activities usually require a lot of time to load. try { @@ -751,8 +755,8 @@ class behat_course extends behat_base { */ protected function get_activity_node($activityname) { - $activityname = str_replace("'", "\'", $activityname); - $xpath = "//li[contains(concat(' ', @class, ' '), ' activity ')][contains(., '" .$activityname. "')]"; + $activityname = $this->getSession()->getSelectorsHandler()->xpathLiteral($activityname); + $xpath = "//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][contains(., $activityname)]"; return $this->find('xpath', $xpath); } diff --git a/course/tests/behat/force_group_mode.feature b/course/tests/behat/force_group_mode.feature index 1d88c8d4a54..59cc3219878 100644 --- a/course/tests/behat/force_group_mode.feature +++ b/course/tests/behat/force_group_mode.feature @@ -28,11 +28,11 @@ Feature: Force group mode in a course | Group mode | Separate groups | | Force group mode | Yes | When I press "Save changes" - Then ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists - And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists - And I click on "//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element" - And ".//a//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists - And ".//img[contains(./@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists + Then "//a/child::img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists + And "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists + And I click on "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element" + And "//a/child::img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should not exists + And "//img[contains(@alt, 'Separate groups (forced mode)')]" "xpath_element" should exists @javascript Scenario: Forced group mode using visible groups @@ -40,11 +40,11 @@ Feature: Force group mode in a course | Group mode | Visible groups | | Force group mode | Yes | And I press "Save changes" - Then ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists - And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists - And I click on "//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element" - And ".//a//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists - And ".//img[contains(./@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists + Then "//a/child::img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists + And "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists + And I click on "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element" + And "//a/child::img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should not exists + And "//img[contains(@alt, 'Visible groups (forced mode)')]" "xpath_element" should exists @javascript Scenario: Forced group mode without groups @@ -52,9 +52,9 @@ Feature: Force group mode in a course | Group mode | No groups | | Force group mode | Yes | And I press "Save changes" - Then ".//a//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should not exists - And ".//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should exists - And I click on "//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element" - And ".//a//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should not exists - And ".//img[contains(./@alt, 'No groups (forced mode)')]" "xpath_element" should exists + Then "//a/child::img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should not exists + And "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should exists + And I click on "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" in the "li.activity.chat" "css_element" + And "//a/child::img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should not exists + And "//img[contains(@alt, 'No groups (forced mode)')]" "xpath_element" should exists diff --git a/enrol/tests/behat/behat_enrol.php b/enrol/tests/behat/behat_enrol.php index 54a0dcec752..7e751d517aa 100644 --- a/enrol/tests/behat/behat_enrol.php +++ b/enrol/tests/behat/behat_enrol.php @@ -52,7 +52,7 @@ class behat_enrol extends behat_base { return array( new Given('I expand "' . get_string('users', 'admin') . '" node'), new Given('I follow "' . get_string('type_enrol_plural', 'plugin') . '"'), - new Given('I select "' . $enrolmethod . '" from "' . get_string('addinstance', 'enrol') . '"'), + new Given('I select "' . $this->escape($enrolmethod) . '" from "' . get_string('addinstance', 'enrol') . '"'), new Given('I fill the moodle form with:', $table), new Given('I press "' . get_string('addinstance', 'enrol') . '"') ); diff --git a/group/tests/behat/behat_groups.php b/group/tests/behat/behat_groups.php index 3411ece3ee9..2afb6f383b8 100644 --- a/group/tests/behat/behat_groups.php +++ b/group/tests/behat/behat_groups.php @@ -51,30 +51,33 @@ class behat_groups extends behat_base { global $DB; $user = $DB->get_record('user', array('username' => $username)); - $userfullname = fullname($user); + $userfullname = $this->getSession()->getSelectorsHandler()->xpathLiteral(fullname($user)); + + // Using a xpath liternal to avoid problems with quotes and double quotes. + $groupname = $this->getSession()->getSelectorsHandler()->xpathLiteral($groupname); // We don't know the option text as it contains the number of users in the group. $select = $this->find_field('groups'); - $xpath = "//select[@id='groups']/descendant::option[contains(., '" . $groupname . "')]"; + $xpath = "//select[@id='groups']/descendant::option[contains(., $groupname)]"; $groupoption = $this->find('xpath', $xpath); $fulloption = $groupoption->getText(); $select->selectOption($fulloption); // Here we don't need to wait for the AJAX response. - $this->find_button('Add/remove users')->click(); + $this->find_button(get_string('adduserstogroup', 'group'))->click(); // Wait for add/remove members page to be loaded. $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")'); // Getting the option and selecting it. $select = $this->find_field('addselect'); - $xpath = "//select[@id='addselect']/descendant::option[contains(., '" . $userfullname . "')]"; + $xpath = "//select[@id='addselect']/descendant::option[contains(., $userfullname)]"; $memberoption = $this->find('xpath', $xpath); $fulloption = $memberoption->getText(); $select->selectOption($fulloption); // Click add button. - $this->find_button('Add')->click(); + $this->find_button(get_string('add'))->click(); // Wait for the page to load. $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")'); diff --git a/lib/behat/behat_files.php b/lib/behat/behat_files.php index 5f15c3aad83..9d12a3a783d 100644 --- a/lib/behat/behat_files.php +++ b/lib/behat/behat_files.php @@ -64,9 +64,10 @@ class behat_files extends behat_base { $exception = new ExpectationException('"' . $filepickerelement . '" filepicker can not be found', $this->getSession()); // Gets the ffilemanager node specified by the locator which contains the filepicker container. + $filepickerelement = $this->getSession()->getSelectorsHandler()->xpathLiteral($filepickerelement); $filepickercontainer = $this->find( 'xpath', - "//input[./@id = //label[contains(normalize-space(string(.)), '" . $filepickerelement . "')]/@for]" . + "//input[./@id = //label[normalize-space(.)=$filepickerelement]/@for]" . "//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ') or " . "contains(concat(' ', normalize-space(@class), ' '), ' ffilepicker ')]", $exception @@ -118,6 +119,9 @@ class behat_files extends behat_base { $exception = new ExpectationException($exceptionmsg, $this->getSession()); + // Avoid quote-related problems. + $name = $this->getSession()->getSelectorsHandler()->xpathLiteral($name); + // Get a filepicker element (folder or file). try { @@ -126,7 +130,8 @@ class behat_files extends behat_base { 'xpath', "//div[@class='fp-content']" . "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" . - "[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')][contains(normalize-space(string(.)), '" . $name . "')]" . + "[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')]" . + "[normalize-space(.)=$name]" . "//descendant::a[contains(concat(' ', normalize-space(@class), ' '), ' fp-contextmenu ')]", $exception, $containernode @@ -139,7 +144,7 @@ class behat_files extends behat_base { 'xpath', "//div[@class='fp-content']" . "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" . - "[contains(normalize-space(string(.)), '" . $name . "')]" . + "[normalize-space(.)=$name]" . "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]", false, $containernode @@ -176,12 +181,15 @@ class behat_files extends behat_base { // Getting the repository link and opening it. $repoexception = new ExpectationException('The "' . $repositoryname . '" repository has not been found', $this->getSession()); + // Avoid problems with both double and single quotes in the same string. + $repositoryname = $this->getSession()->getSelectorsHandler()->xpathLiteral($repositoryname); + // Here we don't need to look inside the selected filepicker because there can only be one modal window. $repositorylink = $this->find( 'xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" . "//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]" . - "[contains(normalize-space(string(.)), '" . $repositoryname . "')]", + "[normalize-space(.)=$repositoryname]", $repoexception ); @@ -226,10 +234,10 @@ class behat_files extends behat_base { // only used when accessing the filepicker, there is no filemanager-loading after selecting the file. $this->find( 'xpath', - "//div[contains(concat(' ', @class, ' '), ' filemanager ')]" . - "[not(contains(concat(' ', @class, ' '), ' fm-updating '))]" . + "//div[contains(concat(' ', normalize-space(@class), ' '), ' filemanager ')]" . + "[not(contains(concat(' ', normalize-space(@class), ' '), ' fm-updating '))]" . "|" . - "//div[contains(concat(' ', @class, ' '), ' filemanager-loading ')]" . + "//div[contains(concat(' ', normalize-space(@class), ' '), ' filemanager-loading ')]" . "[contains(@style, 'display: none;')]", $exception, $filepickernode diff --git a/lib/behat/form_field/behat_form_select.php b/lib/behat/form_field/behat_form_select.php index fbde5719266..5be8135633e 100644 --- a/lib/behat/form_field/behat_form_select.php +++ b/lib/behat/form_field/behat_form_select.php @@ -58,10 +58,13 @@ class behat_form_select extends behat_form_field { // Single select needs an extra click in the option. if (!$this->field->hasAttribute('multiple')) { + + $value = $this->session->getSelectorsHandler()->xpathLiteral($value); + // Using the driver direcly because Element methods are messy when dealing // with elements inside containers. $optionxpath = $this->field->getXpath() . - "/descendant::option[(./@value = '" . $value . "' or contains(normalize-space(string(.)), '" . $value . "'))]"; + "/descendant::option[(./@value=$value or normalize-space(.)=$value)]"; $optionnodes = $this->session->getDriver()->find($optionxpath); if ($optionnodes) { current($optionnodes)->click(); diff --git a/lib/tests/behat/behat_forms.php b/lib/tests/behat/behat_forms.php index bbd8ffb0794..e6efb67825a 100644 --- a/lib/tests/behat/behat_forms.php +++ b/lib/tests/behat/behat_forms.php @@ -132,8 +132,8 @@ class behat_forms extends behat_base { // Show all fields. $showmorestr = get_string('showmore', 'form'); - $showmores = $this->find_all('xpath', "//a[contains(concat(' ', normalize-space(.), ' '), '" . $showmorestr . "')]" . - "[contains(concat(' ', normalize-space(@class), ' '), ' moreless-toggler')]"); + $showmores = $this->find_all('xpath', "//a[normalize-space(.)='" . $showmorestr . "']" . + "[contains(concat(' ', normalize-space(@class), ' '), ' moreless-toggler ')]"); // We are supposed to have 'show more's here, otherwise exception. @@ -186,9 +186,12 @@ class behat_forms extends behat_base { return; } + // Single select needs an extra click in the option. if (!$selectnode->hasAttribute('multiple')) { - // Single select needs an extra click in the option. - $xpath = ".//option[(./@value = '" . $option . "' or contains(normalize-space(string(.)), '" . $option . "'))]"; + + // Avoid quotes problems. + $option = $this->getSession()->getSelectorsHandler()->xpathLiteral($option); + $xpath = "//option[(./@value=$option or normalize-space(.)=$option)]"; $optionnode = $this->find('xpath', $xpath, false, $selectnode); $optionnode->click(); } else { diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index d7e0ffbde9d..f898c2618ee 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -242,8 +242,8 @@ class behat_general extends behat_base { // The table row container. $nocontainerexception = new ElementNotFoundException($this->getSession(), '"' . $tablerowtext . '" row text '); - $tablerowtext = str_replace("'", "\'", $tablerowtext); - $rownode = $this->find('xpath', "//tr[contains(., '" . $tablerowtext . "')]", $nocontainerexception); + $tablerowtext = $this->getSession()->getSelectorsHandler()->xpathLiteral($tablerowtext); + $rownode = $this->find('xpath', "//tr[contains(., $tablerowtext)]", $nocontainerexception); // Looking for the element DOM node inside the specified row. list($selector, $locator) = $this->transform_selector($selectortype, $element); @@ -285,7 +285,7 @@ class behat_general extends behat_base { public function assert_page_contains_text($text) { $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text); - $xpath = "/descendant::*[contains(., " . $xpathliteral. ")]"; + $xpath = "/descendant::*[contains(., $xpathliteral)]"; // Wait until it finds the text, otherwise custom exception. try { @@ -305,7 +305,7 @@ class behat_general extends behat_base { public function assert_page_not_contains_text($text) { $xpathliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($text); - $xpath = "/descendant::*[not(contains(., " . $xpathliteral. "))]"; + $xpath = "/descendant::*[not(contains(., $xpathliteral))]"; // Wait until it finds the text, otherwise custom exception. try { diff --git a/lib/tests/behat/behat_navigation.php b/lib/tests/behat/behat_navigation.php index f676146ed06..90d68a5764c 100644 --- a/lib/tests/behat/behat_navigation.php +++ b/lib/tests/behat/behat_navigation.php @@ -56,16 +56,19 @@ class behat_navigation extends behat_base { return false; } + // Avoid problems with quotes. + $nodetextliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($nodetext); + $xpath = "//ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" . "/child::li[contains(concat(' ', normalize-space(@class), ' '), ' collapsed ')]" . - "/child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch')]" . - "/child::span[contains(concat(' ', normalize-space(.), ' '), '" . $nodetext . "')]" . + "/child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" . + "/child::span[normalize-space(.)=$nodetextliteral]" . "|" . "//ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" . - "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed'))]" . - "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' collapsed')]" . - "/child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch')]" . - "/child::span[contains(concat(' ', normalize-space(.), ' '), '" . $nodetext . "')]"; + "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]" . + "/descendant::li[contains(concat(' ', normalize-space(@class), ' '), ' collapsed ')]" . + "/child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" . + "/child::span[normalize-space(.)=$nodetextliteral]"; $exception = new ExpectationException('The "' . $nodetext . '" node can not be expanded', $this->getSession()); $node = $this->find('xpath', $xpath, $exception); @@ -86,15 +89,18 @@ class behat_navigation extends behat_base { return false; } + // Avoid problems with quotes. + $nodetextliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($nodetext); + $xpath = "//ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" . "/child::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]" . - "/child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch')]" . - "/child::span[contains(concat(' ', normalize-space(.), ' '), '" . $nodetext . "')]" . + "/child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" . + "/child::span[normalize-space(.)=$nodetextliteral]" . "|" . "//ul[contains(concat(' ', normalize-space(@class), ' '), ' block_tree ')]" . - "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed'))]" . - "/child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch')]" . - "/child::span[contains(concat(' ', normalize-space(.), ' '), '" . $nodetext . "')]"; + "/descendant::li[not(contains(concat(' ', normalize-space(@class), ' '), ' collapsed '))]" . + "/child::p[contains(concat(' ', normalize-space(@class), ' '), ' branch ')]" . + "/child::span[normalize-space(.)=$nodetextliteral]"; $exception = new ExpectationException('The "' . $nodetext . '" node can not be collapsed', $this->getSession()); $node = $this->find('xpath', $xpath, $exception); diff --git a/lib/tests/behat/behat_permissions.php b/lib/tests/behat/behat_permissions.php index a687446a835..2d5f2f04fa6 100644 --- a/lib/tests/behat/behat_permissions.php +++ b/lib/tests/behat/behat_permissions.php @@ -73,7 +73,7 @@ class behat_permissions extends behat_base { public function i_override_the_system_permissions_of_role_with($rolename, $table) { // We don't know the number of overrides so we have to get it to match the option contents. - $roleoption = $this->find('xpath', '//select[@name="roleid"]/option[contains(text(),"' . $this->escape($rolename) . '")]'); + $roleoption = $this->find('xpath', '//select[@name="roleid"]/option[contains(.,"' . $this->escape($rolename) . '")]'); return array( new Given('I select "' . $this->escape($roleoption->getText()) . '" from "' . get_string('advancedoverride', 'role') . '"'), diff --git a/message/tests/behat/behat_message.php b/message/tests/behat/behat_message.php index 12c7d5cfa56..ba5f6a52afd 100644 --- a/message/tests/behat/behat_message.php +++ b/message/tests/behat/behat_message.php @@ -67,10 +67,10 @@ class behat_message extends behat_base { } $steps[] = new Given('I follow "' . get_string('messages', 'message') . '"'); - $steps[] = new Given('I fill in "' . get_string('searchcombined', 'message') . '" with "' . $tofullname . '"'); + $steps[] = new Given('I fill in "' . get_string('searchcombined', 'message') . '" with "' . $this->escape($tofullname) . '"'); $steps[] = new Given('I press "' . get_string('searchcombined', 'message') . '"'); - $steps[] = new Given('I follow "' . get_string('sendmessageto', 'message', $tofullname) . '"'); - $steps[] = new Given('I fill in "id_message" with "' . $messagecontent . '"'); + $steps[] = new Given('I follow "' . $this->escape(get_string('sendmessageto', 'message', $tofullname)) . '"'); + $steps[] = new Given('I fill in "id_message" with "' . $this->escape($messagecontent) . '"'); $steps[] = new Given('I press "' . get_string('sendmessage', 'message') . '"'); return $steps; diff --git a/mod/forum/tests/behat/completion_condition_number_discussions.feature b/mod/forum/tests/behat/completion_condition_number_discussions.feature index 0bb30f440cc..06729835189 100644 --- a/mod/forum/tests/behat/completion_condition_number_discussions.feature +++ b/mod/forum/tests/behat/completion_condition_number_discussions.feature @@ -38,7 +38,7 @@ Feature: Set a certain number of discussions as a completion condition for a for And I log out And I log in as "student1" And I follow "Course 1" - Then I hover "//li[contains(concat(' ', @class, ' '), ' modtype_forum ')]/descendant::img[@alt='Not completed: Test forum name']" "xpath_element" + Then I hover "//li[contains(concat(' ', normalize-space(@class), ' '), ' modtype_forum ')]/descendant::img[@alt='Not completed: Test forum name']" "xpath_element" And I add a new discussion to "Test forum name" forum with: | Subject | Post 1 subject | | Message | Body 1 content | @@ -46,7 +46,7 @@ Feature: Set a certain number of discussions as a completion condition for a for | Subject | Post 2 subject | | Message | Body 2 content | And I follow "Course 1" - And I hover "//li[contains(concat(' ', @class, ' '), ' modtype_forum ')]/descendant::img[contains(@alt, 'Completed: Test forum name')]" "xpath_element" + And I hover "//li[contains(concat(' ', normalize-space(@class), ' '), ' modtype_forum ')]/descendant::img[contains(@alt, 'Completed: Test forum name')]" "xpath_element" And I log out And I log in as "teacher1" And I follow "Course 1" diff --git a/mod/glossary/tests/behat/behat_mod_glossary.php b/mod/glossary/tests/behat/behat_mod_glossary.php index da77534cabe..b1c9de89b00 100644 --- a/mod/glossary/tests/behat/behat_mod_glossary.php +++ b/mod/glossary/tests/behat/behat_mod_glossary.php @@ -66,7 +66,7 @@ class behat_mod_glossary extends behat_base { new Given('I follow "' . get_string('categoryview', 'mod_glossary') . '"'), new Given('I press "' . get_string('editcategories', 'mod_glossary') . '"'), new Given('I press "' . get_string('add').' '.get_string('category', 'glossary') . '"'), - new Given('I fill in "name" with "' . $categoryname . '"'), + new Given('I fill in "name" with "' . $this->escape($categoryname) . '"'), new Given('I press "' . get_string('savechanges') . '"'), new Given('I press "' . get_string('back', 'mod_glossary') . '"') ); diff --git a/mod/glossary/tests/behat/print_friendly_version.feature b/mod/glossary/tests/behat/print_friendly_version.feature index 9e4bcf77c49..b65fd306697 100644 --- a/mod/glossary/tests/behat/print_friendly_version.feature +++ b/mod/glossary/tests/behat/print_friendly_version.feature @@ -34,7 +34,7 @@ Feature: A teacher can choose whether to provide a printer-friendly glossary ent | Concept | Just a test concept | | Definition | Concept definition | Then "Printer-friendly version" "link" should exists - And "//*[contains(concat(' ', @class, ' '), ' printicon ')][contains(@href, 'print.php')]" "xpath_element" should exists + And "//*[contains(concat(' ', normalize-space(@class), ' '), ' printicon ')][contains(@href, 'print.php')]" "xpath_element" should exists And I follow "Printer-friendly version" And I should see "Just a test concept" @@ -51,4 +51,4 @@ Feature: A teacher can choose whether to provide a printer-friendly glossary ent | Concept | Just a test concept | | Definition | Concept definition | Then "Printer-friendly version" "link" should not exists - And "//*[contains(concat(' ', @class, ' '), ' printicon ')][contains(@href, 'print.php')]" "xpath_element" should not exists + And "//*[contains(concat(' ', normalize-space(@class), ' '), ' printicon ')][contains(@href, 'print.php')]" "xpath_element" should not exists diff --git a/mod/lesson/tests/behat/lesson_navigation.feature b/mod/lesson/tests/behat/lesson_navigation.feature index 353eac587c6..0630bfe03b4 100644 --- a/mod/lesson/tests/behat/lesson_navigation.feature +++ b/mod/lesson/tests/behat/lesson_navigation.feature @@ -40,7 +40,7 @@ Feature: In a lesson activity, students can navigate through a series of pages i | id_jumpto_1 | Next page | And I press "Save page" And I follow "Expanded" - And I click on "Add a question page here" "link" in the "//div[contains(concat(' ', @class, ' '), ' addlinks ')][3]" "xpath_element" + And I click on "Add a question page here" "link" in the "//div[contains(concat(' ', normalize-space(@class), ' '), ' addlinks ')][3]" "xpath_element" And I select "Numerical" from "Select a question type" And I press "Add a question page" And I fill the moodle form with: diff --git a/question/tests/behat/behat_question.php b/question/tests/behat/behat_question.php index f022eb8bc34..7f6435e1f57 100644 --- a/question/tests/behat/behat_question.php +++ b/question/tests/behat/behat_question.php @@ -51,13 +51,15 @@ class behat_question extends behat_base { */ public function i_add_a_question_filling_the_form_with($questiontypename, TableNode $questiondata) { - $questiontypexpath = "//span[@class='qtypename'][.='" . $questiontypename . "']" . + // Using xpath literal to avoid quotes problems. + $questiontypename = $this->getSession()->getSelectorsHandler()->xpathLiteral($questiontypename); + $questiontypexpath = "//span[@class='qtypename'][normalize-space(.)=$questiontypename]" . "/ancestor::div[@class='qtypeoption']/descendant::input"; return array( new Given('I follow "' . get_string('questionbank', 'question') . '"'), new Given('I press "' . get_string('createnewquestion', 'question') . '"'), - new Given('I click on "' . $questiontypexpath . '" "xpath_element"'), + new Given('I click on "' . $this->escape($questiontypexpath) . '" "xpath_element"'), new Given('I click on "Next" "button" in the "#qtypechoicecontainer" "css_element"'), new Given('I fill the moodle form with:', $questiondata), new Given('I press "' . get_string('savechanges') . '"') @@ -75,14 +77,18 @@ class behat_question extends behat_base { */ public function the_state_of_question_is_shown_as($questiondescription, $state) { + // Using xpath literal to avoid quotes problems. + $questiondescriptionliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($questiondescription); + $stateliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($state); + // Split in two checkings to give more feedback in case of exception. $exception = new ElementNotFoundException($this->getSession(), 'Question "' . $questiondescription . '" '); - $questionxpath = "//div[contains(concat(' ', @class, ' '), ' qtext ')][contains(., '" . $questiondescription . "')]"; + $questionxpath = "//div[contains(concat(' ', normalize-space(@class), ' '), ' qtext ')][contains(., $questiondescriptionliteral)]"; $this->find('xpath', $questionxpath, $exception); $exception = new ExpectationException('Question "' . $questiondescription . '" state is not "' . $state . '"', $this->getSession()); - $xpath = $questionxpath . "/ancestor::div[contains(concat(' ', @class, ' '), ' que ')]" . - "/descendant::div[@class='state'][contains(., '" . $state . "')]"; + $xpath = $questionxpath . "/ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' que ')]" . + "/descendant::div[@class='state'][contains(., $stateliteral)]"; $this->find('xpath', $xpath, $exception); } diff --git a/repository/tests/behat/behat_filepicker.php b/repository/tests/behat/behat_filepicker.php index bcc076dd0dc..c8afbf138c2 100644 --- a/repository/tests/behat/behat_filepicker.php +++ b/repository/tests/behat/behat_filepicker.php @@ -92,6 +92,8 @@ class behat_filepicker extends behat_files { // Just in case there is any contents refresh in progress. $this->wait_until_contents_are_updated($fieldnode); + $folderliteral = $this->getSession()->getSelectorsHandler()->xpathLiteral($foldername); + // We look both in the pathbar and in the contents. try { @@ -99,8 +101,8 @@ class behat_filepicker extends behat_files { $folder = $this->find( 'xpath', "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-folder ')]" . - "/descendant::div[contains(concat(' ', @class, ' '), ' fp-filename ')]" . - "[normalize-space(.)='" . $foldername . "']", + "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-filename ')]" . + "[normalize-space(.)=$folderliteral]", $exception, $fieldnode ); @@ -110,7 +112,7 @@ class behat_filepicker extends behat_files { $folder = $this->find( 'xpath', "//a[contains(concat(' ', normalize-space(@class), ' '), ' fp-path-folder-name ')]" . - "[normalize-space(.)='" . $foldername . "']", + "[normalize-space(.)=$folderliteral]", $exception, $fieldnode ); diff --git a/repository/tests/behat/cancel_add_file.feature b/repository/tests/behat/cancel_add_file.feature index c0b37d2671f..f995bd8e270 100644 --- a/repository/tests/behat/cancel_add_file.feature +++ b/repository/tests/behat/cancel_add_file.feature @@ -24,7 +24,7 @@ Feature: A selected file can be cancelled And I upload "lib/tests/fixtures/upload_users.csv" file to "Files" filepicker And I click on "#fitem_id_files .fp-btn-add a" "css_element" And I click on "Recent files" "link" in the ".fp-repo-area" "css_element" - And I click on "//a[contains(concat(' ', @class, ' '), ' fp-file ')][contains(., 'empty.txt')]" "xpath_element" + And I click on "//a[contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')][normalize-space(.)='empty.txt']" "xpath_element" And I click on ".yui3-panel-focused .fp-select .fp-select-cancel" "css_element" And I click on ".yui3-panel-focused .file-picker button.yui3-button-close" "css_element" And I press "Save and display" diff --git a/repository/upload/tests/behat/behat_repository_upload.php b/repository/upload/tests/behat/behat_repository_upload.php index 5378fc02740..bf090acb4b9 100644 --- a/repository/upload/tests/behat/behat_repository_upload.php +++ b/repository/upload/tests/behat/behat_repository_upload.php @@ -65,10 +65,10 @@ class behat_repository_upload extends behat_files { $noformexception = new ExpectationException('The upload file form is not ready', $this->getSession()); $this->find( 'xpath', - "//div[contains(concat(' ', @class, ' '), ' file-picker ')]" . - "[contains(concat(' ', @class, ' '), ' repository_upload ')]" . + "//div[contains(concat(' ', normalize-space(@class), ' '), ' file-picker ')]" . + "[contains(concat(' ', normalize-space(@class), ' '), ' repository_upload ')]" . "/descendant::div[@class='fp-content']" . - "/descendant::div[contains(concat(' ', @class, ' '), ' fp-upload-form ')]" . + "/descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-upload-form ')]" . "/descendant::form", $noformexception );