From c43b7393f85899983d75a0202bb40be598e44dd6 Mon Sep 17 00:00:00 2001 From: Daniel Myers Date: Mon, 15 Jul 2013 13:52:57 +1200 Subject: [PATCH 01/43] MDL-40260 block_course_overview: prevent MNET courses from being dimmed --- blocks/course_overview/renderer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blocks/course_overview/renderer.php b/blocks/course_overview/renderer.php index bb2956908ad..85b5fa8dd6d 100644 --- a/blocks/course_overview/renderer.php +++ b/blocks/course_overview/renderer.php @@ -103,7 +103,7 @@ class block_course_overview_renderer extends plugin_renderer_base { // No need to pass title through s() here as it will be done automatically by html_writer. $attributes = array('title' => $course->fullname); - if (empty($course->visible)) { + if (empty($course->visible) && ($course->id > 0)) { $attributes['class'] = 'dimmed'; } if ($course->id > 0) { From 619fc0cd3c01af69d6f9ccf25a932b992f15e3cb Mon Sep 17 00:00:00 2001 From: Russell Smith Date: Thu, 11 Jul 2013 17:14:46 +1000 Subject: [PATCH 02/43] MDL-40585 backup: cache XML parent paths For $this->groupedpaths, using a key is faster as there is no need to do in_array searches. Parent cache allows dirname calls to be substantially reduced. 2048 was chosen as a cache size as this class operates on chunks of the restore at a time, 8k questions produces a parent cache of about 500 paths and uses 300K of memory. Scaling up to 2048 will use about 1.2M of RAM for really large restores. This is acceptable for the 48% function call reduction and the 10% runtime improvement seen. --- .../grouped_parser_processor.class.php | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/backup/util/xml/parser/processors/grouped_parser_processor.class.php b/backup/util/xml/parser/processors/grouped_parser_processor.class.php index 48782b7540b..6cba6a997e3 100644 --- a/backup/util/xml/parser/processors/grouped_parser_processor.class.php +++ b/backup/util/xml/parser/processors/grouped_parser_processor.class.php @@ -42,6 +42,18 @@ abstract class grouped_parser_processor extends simplified_parser_processor { protected $groupedpaths; // Paths we are requesting grouped protected $currentdata; // Where we'll be acummulating data + + /** + * Keep cache of parent directory paths for XML parsing. + * @var array + */ + protected $parentcache = array(); + + /** + * Remaining space for parent directory paths. + * @var integer + */ + protected $parentcacheavailablesize = 2048; public function __construct(array $paths = array()) { $this->groupedpaths = array(); @@ -65,7 +77,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor { $a->child = $found; throw new progressive_parser_exception('xml_grouped_child_found', $a); } - $this->groupedpaths[] = $path; + $this->groupedpaths[$path] = true; } parent::add_path($path); } @@ -141,7 +153,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor { } protected function path_is_grouped($path) { - return in_array($path, $this->groupedpaths); + return isset($this->groupedpaths[$path]); } /** @@ -150,16 +162,41 @@ abstract class grouped_parser_processor extends simplified_parser_processor { * false if not */ protected function grouped_parent_exists($path) { - $parentpath = progressive_parser::dirname($path); + $parentpath = $this->get_parent_path($path); + while ($parentpath != '/') { if ($this->path_is_grouped($parentpath)) { return $parentpath; } - $parentpath = progressive_parser::dirname($parentpath); + $parentpath = $this->get_parent_path($parentpath); } return false; } + /** + * Get the parent path using a local cache for performance. + * + * @param $path string The pathname you wish to obtain the parent name for. + * @return string The parent pathname. + */ + protected function get_parent_path($path) { + if (!isset($this->parentcache[$path])) { + $this->parentcache[$path] = progressive_parser::dirname($path); + $this->parentcacheavailablesize--; + if ($this->parentcacheavailablesize < 0) { + // Older first is cheaper than LRU. We use 10% as items are grouped together and the large quiz + // restore from MDL-40585 used only 600 parent paths. This is an XML heirarchy, so common paths + // are grouped near each other. eg; /question_bank/question_category/question/element. After keeping + // question_bank paths in the cache when we move to another area and the question_bank cache is not + // useful any longer. + $this->parentcache = array_slice($this->parentcache, 200, null, true); + $this->parentcacheavailablesize += 200; + } + } + return $this->parentcache[$path]; + } + + /** * Function that will look for any grouped * child for the given path, returning it if found, @@ -167,7 +204,7 @@ abstract class grouped_parser_processor extends simplified_parser_processor { */ protected function grouped_child_exists($path) { $childpath = $path . '/'; - foreach ($this->groupedpaths as $groupedpath) { + foreach ($this->groupedpaths as $groupedpath => $set) { if (strpos($groupedpath, $childpath) === 0) { return $groupedpath; } From fb50800b161c997769a89534d84148184151650d Mon Sep 17 00:00:00 2001 From: David Monllao Date: Fri, 12 Jul 2013 21:42:04 +0200 Subject: [PATCH 03/43] MDL-39441 behat: Restrict config.php allowed settings --- admin/tool/behat/cli/util.php | 3 +++ lib/behat/lib.php | 26 ++++++++++++++++++++++++++ lib/setup.php | 4 ++++ 3 files changed, 33 insertions(+) diff --git a/admin/tool/behat/cli/util.php b/admin/tool/behat/cli/util.php index b28f4fcb6b0..5c24cf8c05d 100644 --- a/admin/tool/behat/cli/util.php +++ b/admin/tool/behat/cli/util.php @@ -142,6 +142,9 @@ foreach ($vars as $var) { $CFG->{$var} = $CFG->{'behat_' . $var}; } +// Clean $CFG extra values before performing any action. +behat_clean_init_config(); + $CFG->noemailever = true; $CFG->passwordsaltmain = 'moodle'; diff --git a/lib/behat/lib.php b/lib/behat/lib.php index e35b0e6192c..42ed4418ffd 100644 --- a/lib/behat/lib.php +++ b/lib/behat/lib.php @@ -141,3 +141,29 @@ function behat_error_handler($errno, $errstr, $errfile, $errline, $errcontext) { // Also use the internal error handler so we keep the usual behaviour. return false; } + +/** + * Restrict the config.php settings allowed. + * + * When running the behat features the config.php + * settings should not affect the results. + * + * @return void + */ +function behat_clean_init_config() { + global $CFG; + + $allowed = array_flip(array( + 'wwwroot', 'dataroot', 'dirroot', 'admin', 'directorypermissions', 'filepermissions', + 'dbtype', 'dblibrary', 'dbhost', 'dbname', 'dbuser', 'dbpass', 'prefix', 'dboptions', + 'proxyhost', 'proxyport', 'proxytype', 'proxyuser', 'proxypassword', 'proxybypass' + )); + + // Also allowing behat_ prefixed attributes. + foreach ($CFG as $key => $value) { + if (!isset($allowed[$key]) && strpos($key, 'behat_') !== 0) { + unset($CFG->{$key}); + } + } + +} diff --git a/lib/setup.php b/lib/setup.php index 0b9cbd2f40a..c776a891623 100644 --- a/lib/setup.php +++ b/lib/setup.php @@ -131,6 +131,10 @@ if (!defined('BEHAT_SITE_RUNNING') && !empty($CFG->behat_dataroot) && // actions like reset the site or use data generators. define('BEHAT_SITE_RUNNING', true); + // Clean extra config.php settings. + require_once(__DIR__ . '/../lib/behat/lib.php'); + behat_clean_init_config(); + $CFG->wwwroot = $CFG->behat_wwwroot; $CFG->passwordsaltmain = 'moodle'; $CFG->prefix = $CFG->behat_prefix; From d51121efbe11b8bf1d8092a023cc2216f00ed13f Mon Sep 17 00:00:00 2001 From: Russell Smith Date: Tue, 9 Jul 2013 20:04:09 +1000 Subject: [PATCH 04/43] MDL-40584 backup: Query db once per category in precheck The cache is function local and testing against a large database indicates 10k questions is a large category. Restore already uses MEMORY_EXTRA and that will have enough space for the couple of megabtyes a local sql hash will introduce. --- backup/util/dbops/restore_dbops.class.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/backup/util/dbops/restore_dbops.class.php b/backup/util/dbops/restore_dbops.class.php index 518d7bac1d6..a3dfe1e6161 100644 --- a/backup/util/dbops/restore_dbops.class.php +++ b/backup/util/dbops/restore_dbops.class.php @@ -614,13 +614,20 @@ abstract class restore_dbops { } else { self::set_backup_ids_record($restoreid, 'question_category', $category->id, $matchcat->id, $targetcontext->id); $questions = self::restore_get_questions($restoreid, $category->id); + + // Collect all the questions for this category into memory so we only talk to the DB once. + $questioncache = $DB->get_records_sql_menu("SELECT ".$DB->sql_concat('stamp', "' '", 'version').", id + FROM {question} + WHERE category = ?", array($matchcat->id)); + foreach ($questions as $question) { - $matchq = $DB->get_record('question', array( - 'category' => $matchcat->id, - 'stamp' => $question->stamp, - 'version' => $question->version)); + if (isset($questioncache[$question->stamp." ".$question->version])) { + $matchqid = $questioncache[$question->stamp." ".$question->version]; + } else { + $matchqid = false; + } // 5a) No match, check if user can add q - if (!$matchq) { + if (!$matchqid) { // 6a) User can, mark the q to be created if ($canadd) { // Nothing to mark, newitemid means create @@ -645,7 +652,7 @@ abstract class restore_dbops { // 5b) Match, mark q to be mapped } else { - self::set_backup_ids_record($restoreid, 'question', $question->id, $matchq->id); + self::set_backup_ids_record($restoreid, 'question', $question->id, $matchqid); } } } From 82b32059b52ba81c40696cfe1917e76471c47a25 Mon Sep 17 00:00:00 2001 From: Dan Poltawski Date: Tue, 16 Jul 2013 09:36:21 +0800 Subject: [PATCH 05/43] MDL-40680 store_memcached: fix serialiser options * Use correct lang string for SERIALIZER_IGBINARY * Use PARAM_INT for the parameter --- cache/stores/memcached/addinstanceform.php | 4 ++-- cache/stores/memcached/lib.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cache/stores/memcached/addinstanceform.php b/cache/stores/memcached/addinstanceform.php index a8e98bf0f48..293b1988153 100644 --- a/cache/stores/memcached/addinstanceform.php +++ b/cache/stores/memcached/addinstanceform.php @@ -57,7 +57,7 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form $form->addElement('select', 'serialiser', get_string('useserialiser', 'cachestore_memcached'), $serialiseroptions); $form->addHelpButton('serialiser', 'useserialiser', 'cachestore_memcached'); $form->setDefault('serialiser', Memcached::SERIALIZER_PHP); - $form->setType('serialiser', PARAM_NUMBER); + $form->setType('serialiser', PARAM_INT); $form->addElement('text', 'prefix', get_string('prefix', 'cachestore_memcached'), array('size' => 16)); $form->setType('prefix', PARAM_ALPHANUM); @@ -74,4 +74,4 @@ class cachestore_memcached_addinstance_form extends cachestore_addinstance_form $form->setDefault('bufferwrites', 0); $form->setType('bufferwrites', PARAM_BOOL); } -} \ No newline at end of file +} diff --git a/cache/stores/memcached/lib.php b/cache/stores/memcached/lib.php index 6e3834cce0f..4f96fda5f14 100644 --- a/cache/stores/memcached/lib.php +++ b/cache/stores/memcached/lib.php @@ -339,7 +339,7 @@ class cachestore_memcached extends cache_store implements cache_is_configurable $options[Memcached::SERIALIZER_JSON] = get_string('serialiser_json', 'cachestore_memcached'); } if (Memcached::HAVE_IGBINARY) { - $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_php', 'cachestore_memcached'); + $options[Memcached::SERIALIZER_IGBINARY] = get_string('serialiser_igbinary', 'cachestore_memcached'); } return $options; } From ffadd1242ae21dde713a54d5e39ecb3deb18fa49 Mon Sep 17 00:00:00 2001 From: Gareth J Barnard Date: Wed, 17 Jul 2013 17:39:33 +0100 Subject: [PATCH 06/43] MDL-37333 theme: Fix 'Clear theme caches' redirect. --- theme/index.php | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/theme/index.php b/theme/index.php index 7f6eb61a032..f1ad3a85497 100644 --- a/theme/index.php +++ b/theme/index.php @@ -97,11 +97,11 @@ if (!empty($CFG->enabledevicedetection) && empty($device)) { $table->head = array(get_string('devicetype', 'admin'), get_string('currenttheme', 'admin'), get_string('info')); $devices = get_device_type_list(); - foreach ($devices as $device) { + foreach ($devices as $thedevice) { $headingthemename = ''; // To output the picked theme name when needed - $themename = get_selected_theme_for_device_type($device); - if (!$themename && $device == 'default') { + $themename = get_selected_theme_for_device_type($thedevice); + if (!$themename && $thedevice == 'default') { $themename = theme_config::DEFAULT_THEME; } @@ -112,7 +112,7 @@ if (!empty($CFG->enabledevicedetection) && empty($device)) { $themename = clean_param($themename, PARAM_THEME); if (empty($themename)) { // Likely the theme has been deleted - unset_config(get_device_cfg_var_name($device)); + unset_config(get_device_cfg_var_name($thedevice)); } else { $strthemename = get_string('pluginname', 'theme_'.$themename); // link to the screenshot, now mandatory - the image path is hardcoded because we need image from other themes, not the current one @@ -123,19 +123,19 @@ if (!empty($CFG->enabledevicedetection) && empty($device)) { $headingthemename = $OUTPUT->heading($strthemename, 3); } // If not default device then show option to unset theme. - if ($device != 'default') { + if ($thedevice != 'default') { $unsetthemestr = get_string('unsettheme', 'admin'); - $unsetthemeurl = new moodle_url('/theme/index.php', array('device' => $device, 'sesskey' => sesskey(), 'unsettheme' => true)); + $unsetthemeurl = new moodle_url('/theme/index.php', array('device' => $thedevice, 'sesskey' => sesskey(), 'unsettheme' => true)); $unsetthemebutton = new single_button($unsetthemeurl, $unsetthemestr, 'get'); $unsetthemebutton = $OUTPUT->render($unsetthemebutton); } } - $deviceurl = new moodle_url('/theme/index.php', array('device' => $device, 'sesskey' => sesskey())); + $deviceurl = new moodle_url('/theme/index.php', array('device' => $thedevice, 'sesskey' => sesskey())); $select = new single_button($deviceurl, $strthemeselect, 'get'); $table->data[] = array( - $OUTPUT->heading(ucfirst($device), 3), + $OUTPUT->heading(ucfirst($thedevice), 3), $screenshotcell, $headingthemename . $OUTPUT->render($select) . $unsetthemebutton ); @@ -217,7 +217,11 @@ if (!empty($CFG->enabledevicedetection) && empty($device)) { echo $OUTPUT->header('themeselector'); echo $OUTPUT->heading($heading); -echo $OUTPUT->single_button(new moodle_url('index.php', array('sesskey' => sesskey(), 'reset' => 1, 'device' => $device)), get_string('themeresetcaches', 'admin')); +$params = array('sesskey' => sesskey(), 'reset' => 1); +if (!empty($device)) { + $params['device'] = $device; +} +echo $OUTPUT->single_button(new moodle_url('index.php', $params), get_string('themeresetcaches', 'admin')); echo html_writer::table($table); From 2e64d85668c3dfaea320b3221613dd3669d4f556 Mon Sep 17 00:00:00 2001 From: Yuliya Bozhko Date: Tue, 16 Jul 2013 15:57:07 +1200 Subject: [PATCH 07/43] MDL-38837 badges: Check for http or https in badges backpack support --- badges/backpack.js | 15 ++++++++++++++- badges/badge.php | 6 ++---- badges/mybadges.php | 7 ++----- badges/renderer.php | 11 +++++++---- lang/en/badges.php | 1 + lib/badgeslib.php | 16 ++++++++++++++++ 6 files changed, 42 insertions(+), 14 deletions(-) diff --git a/badges/backpack.js b/badges/backpack.js index 68e736bee08..98678e8f29e 100644 --- a/badges/backpack.js +++ b/badges/backpack.js @@ -2,7 +2,20 @@ * Push badges to backpack. */ function addtobackpack(event, args) { - OpenBadges.issue([args.assertion], function(errors, successes) { }); + var badgetable = Y.one('#issued-badge-table'); + var errordiv = Y.one('#addtobackpack-error'); + var errortext = M.util.get_string('error:backpackproblem', 'badges'); + var errorhtml = '
' + errortext + '
'; + + if (typeof OpenBadges !== 'undefined') { + OpenBadges.issue([args.assertion], function(errors, successes) { }); + } else { + // Add error div if it doesn't exist yet. + if (!errordiv) { + var badgerror = Y.Node.create(errorhtml); + badgetable.insert(badgerror, 'before'); + } + } } /** diff --git a/badges/badge.php b/badges/badge.php index cd90239502c..339b147313e 100644 --- a/badges/badge.php +++ b/badges/badge.php @@ -56,10 +56,8 @@ if (isloggedin()) { navigation_node::override_active_url($url); } -// TODO: Better way of pushing badges to Mozilla backpack? -if (!empty($CFG->badges_allowexternalbackpack)) { - $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true); -} +// Include JS files for backpack support. +badges_setup_backpack_js(); echo $OUTPUT->header(); diff --git a/badges/mybadges.php b/badges/mybadges.php index 46f4c1dce2d..d944e736beb 100644 --- a/badges/mybadges.php +++ b/badges/mybadges.php @@ -90,11 +90,8 @@ $PAGE->set_title($title); $PAGE->set_heading($title); $PAGE->set_pagelayout('mydashboard'); -// TODO: Better way of pushing badges to Mozilla backpack? -if (!empty($CFG->badges_allowexternalbackpack)) { - $PAGE->requires->js(new moodle_url('http://backpack.openbadges.org/issuer.js'), true); - $PAGE->requires->js('/badges/backpack.js', true); -} +// Include JS files for backpack support. +badges_setup_backpack_js(); $output = $PAGE->get_renderer('core', 'badges'); $badges = badges_get_user_badges($USER->id); diff --git a/badges/renderer.php b/badges/renderer.php index 171bdb3bb9b..0100410eb0a 100644 --- a/badges/renderer.php +++ b/badges/renderer.php @@ -282,6 +282,7 @@ class core_badges_renderer extends plugin_renderer_base { $today = strtotime($today_date); $table = new html_table(); + $table->id = 'issued-badge-table'; $imagetable = new html_table(); $imagetable->attributes = array('class' => 'clearfix badgeissuedimage'); @@ -294,11 +295,13 @@ class core_badges_renderer extends plugin_renderer_base { $expiration = isset($issued['expires']) ? strtotime($issued['expires']) : $today + 1; if (!empty($CFG->badges_allowexternalbackpack) && ($expiration > $today) && badges_user_has_backpack($USER->id)) { $assertion = new moodle_url('/badges/assertion.php', array('b' => $ibadge->hash)); + $action = new component_action('click', 'addtobackpack', array('assertion' => $assertion->out(false))); $attributes = array( - 'type' => 'button', - 'value' => get_string('addtobackpack', 'badges'), - 'onclick' => 'OpenBadges.issue(["' . $assertion->out(false) . '"], function(errors, successes) { })'); + 'type' => 'button', + 'id' => 'addbutton', + 'value' => get_string('addtobackpack', 'badges')); $tobackpack = html_writer::tag('input', '', $attributes); + $this->output->add_action_handler($action, 'addbutton'); $imagetable->data[] = array($tobackpack); } } @@ -459,7 +462,7 @@ class core_badges_renderer extends plugin_renderer_base { get_string('downloadall'), 'POST', array('class' => 'activatebadge')); // Local badges. - $localhtml = html_writer::start_tag('fieldset', array('class' => 'generalbox')); + $localhtml = html_writer::start_tag('fieldset', array('id' => 'issued-badge-table', 'class' => 'generalbox')); $localhtml .= html_writer::tag('legend', $this->output->heading_with_help(get_string('localbadges', 'badges', $SITE->fullname), 'localbadgesh', 'badges')); if ($badges->badges) { diff --git a/lang/en/badges.php b/lang/en/badges.php index 437c5dd1ecf..9e5b147dabe 100644 --- a/lang/en/badges.php +++ b/lang/en/badges.php @@ -198,6 +198,7 @@ $string['donotaward'] = 'Currently, this badge is not active, so it cannot be aw $string['editsettings'] = 'Edit settings'; $string['enablebadges'] = 'Enable badges'; $string['error:backpacknotavailable'] = 'Your site is not accessible from the Internet, so any badges issued from this site cannot be verified by external backpack services.'; +$string['error:backpackproblem'] = 'There was a problem connecting to your backpack service provider. Please try again later.'; $string['error:cannotact'] = 'Cannot activate the badge. '; $string['error:cannotawardbadge'] = 'Cannot award badge to a user.'; $string['error:clone'] = 'Cannot clone the badge.'; diff --git a/lib/badgeslib.php b/lib/badgeslib.php index daf2d44f0ce..f4d38cbb467 100644 --- a/lib/badgeslib.php +++ b/lib/badgeslib.php @@ -1348,3 +1348,19 @@ function badges_handle_course_deletion($courseid) { $DB->update_record('badge', $toupdate); } } + +/** + * Loads JS files required for backpack support. + * + * @uses $CFG, $PAGE + * @return void + */ +function badges_setup_backpack_js() { + global $CFG, $PAGE; + if (!empty($CFG->badges_allowexternalbackpack)) { + $PAGE->requires->string_for_js('error:backpackproblem', 'badges'); + $protocol = (strpos($CFG->wwwroot, 'https://') === 0) ? 'https://' : 'http://'; + $PAGE->requires->js(new moodle_url($protocol . 'backpack.openbadges.org/issuer.js'), true); + $PAGE->requires->js('/badges/backpack.js', true); + } +} From 00c9d81bb7b5beb1205bee5c67cec810c170f482 Mon Sep 17 00:00:00 2001 From: Rossiani Wijaya Date: Thu, 18 Jul 2013 10:57:11 +0800 Subject: [PATCH 08/43] MDL-40295 wiki mod: fix set_url() error for printer friendly page. Additionally, fix wiki pagelayout for printing --- mod/wiki/pagelib.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/mod/wiki/pagelib.php b/mod/wiki/pagelib.php index 558117028d0..e4b331d383e 100644 --- a/mod/wiki/pagelib.php +++ b/mod/wiki/pagelib.php @@ -2169,11 +2169,17 @@ class page_wiki_confirmrestore extends page_wiki_save { class page_wiki_prettyview extends page_wiki { - function print_header() { - global $CFG, $PAGE, $OUTPUT; + function __construct($wiki, $subwiki, $cm) { + global $PAGE; $PAGE->set_pagelayout('embedded'); - echo $OUTPUT->header(); + parent::__construct($wiki, $subwiki, $cm); + } + function print_header() { + global $OUTPUT; + $this->set_url(); + + echo $OUTPUT->header(); echo '

' . format_string($this->title) . '

'; } From c5a0598bf9289f006c476962e2eb46cbeacec253 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Tue, 9 Jul 2013 16:37:17 +0200 Subject: [PATCH 09/43] MDL-40315 behat: Performance improvement We look for exceptions after each step, this patch includes a pre-checking query to avoid multiple queries for each step when most of the time they are not necessary. --- lib/tests/behat/behat_hooks.php | 27 +++++++++++++++++++++++---- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/lib/tests/behat/behat_hooks.php b/lib/tests/behat/behat_hooks.php index 94f946335ea..6c5efe43029 100644 --- a/lib/tests/behat/behat_hooks.php +++ b/lib/tests/behat/behat_hooks.php @@ -244,7 +244,26 @@ class behat_hooks extends behat_base { try { // Exceptions. - if ($errormsg = $this->getSession()->getPage()->find('css', '.errorbox p.errormessage')) { + $exceptionsxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' errorbox ')]" . + "/descendant::p[contains(concat(' ', normalize-space(@class), ' '), ' errormessage ')]"; + // Debugging messages. + $debuggingxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' debuggingmessage ')]"; + // PHP debug messages. + $phperrorxpath = "//*[contains(concat(' ', normalize-space(@class), ' '), ' phpdebugmessage ')]"; + // Any other backtrace. + $othersxpath = "(//*[contains(., ': call to ')])[1]"; + + $xpaths = array($exceptionsxpath, $debuggingxpath, $phperrorxpath, $othersxpath); + $joinedxpath = implode(' | ', $xpaths); + + // Joined xpath expression. Most of the time there will be no exceptions, so this pre-check + // is faster than to send the 4 xpath queries for each step. + if (!$this->getSession()->getDriver()->find($joinedxpath)) { + return; + } + + // Exceptions. + if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) { // Getting the debugging info and the backtrace. $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny'); @@ -256,7 +275,7 @@ class behat_hooks extends behat_base { } // Debugging messages. - if ($debuggingmessages = $this->getSession()->getPage()->findAll('css', '.debuggingmessage')) { + if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) { $msgs = array(); foreach ($debuggingmessages as $debuggingmessage) { $msgs[] = $this->get_debug_text($debuggingmessage->getHtml()); @@ -266,7 +285,7 @@ class behat_hooks extends behat_base { } // PHP debug messages. - if ($phpmessages = $this->getSession()->getPage()->findAll('css', '.phpdebugmessage')) { + if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) { $msgs = array(); foreach ($phpmessages as $phpmessage) { @@ -279,7 +298,7 @@ class behat_hooks extends behat_base { // Any other backtrace. // First looking through xpath as it is faster than get and parse the whole page contents, // we get the contents and look for matches once we found something to suspect that there is a backtrace. - if ($this->getSession()->getDriver()->find("(//html/descendant::*[contains(., ': call to ')])[1]")) { + if ($this->getSession()->getDriver()->find($othersxpath)) { $backtracespattern = '/(line [0-9]* of [^:]*: call to [\->&;:a-zA-Z_\x7f-\xff][\->&;:a-zA-Z0-9_\x7f-\xff]*)/'; if (preg_match_all($backtracespattern, $this->getSession()->getPage()->getContent(), $backtraces)) { $msgs = array(); From 448fc8ab1a2ab269f54d91eff2c8d648cad60c51 Mon Sep 17 00:00:00 2001 From: Tim Lock Date: Thu, 11 Jul 2013 18:03:34 +0930 Subject: [PATCH 10/43] MDL-40626 gradebook: Prevent incorrect override with multiline feedback The quick grading option doesn't allow mutliline editing. --- grade/report/grader/lib.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/grade/report/grader/lib.php b/grade/report/grader/lib.php index c99f6727d5d..51eddc26a2a 100644 --- a/grade/report/grader/lib.php +++ b/grade/report/grader/lib.php @@ -226,6 +226,10 @@ class grade_report_grader extends grade_report { $changedgrades = true; } else if ($datatype === 'feedback') { + // If quick grading is on, feedback needs to be compared without line breaks. + if ($this->get_pref('quickgrading')) { + $oldvalue->feedback = preg_replace("/\r\n|\r|\n/", "", $oldvalue->feedback); + } if (($oldvalue->feedback === $postedvalue) or ($oldvalue->feedback === NULL and empty($postedvalue))) { continue; } From f2fe655aeb1ab59db041eedab44fde556f39d59c Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 19 Jul 2013 15:07:03 +1000 Subject: [PATCH 11/43] MDL-33563 Grading methods allow decimal grades --- grade/grading/form/guide/lib.php | 11 ++++++---- grade/grading/form/lib.php | 36 ++++++++++++++++++++++++++----- grade/grading/form/rubric/lib.php | 9 +++++--- grade/grading/form/upgrade.txt | 9 ++++++++ mod/assign/locallib.php | 7 +++--- 5 files changed, 57 insertions(+), 15 deletions(-) create mode 100644 grade/grading/form/upgrade.txt diff --git a/grade/grading/form/guide/lib.php b/grade/grading/form/guide/lib.php index c5292ba8eb0..942b19b37fb 100644 --- a/grade/grading/form/guide/lib.php +++ b/grade/grading/form/guide/lib.php @@ -820,10 +820,9 @@ class gradingform_guide_instance extends gradingform_instance { /** * Calculates the grade to be pushed to the gradebook * - * @return int the valid grade from $this->get_controller()->get_grade_range() + * @return float|int the valid grade from $this->get_controller()->get_grade_range() */ public function get_grade() { - global $DB, $USER; $grade = $this->get_guide_filling(); if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) { @@ -842,8 +841,12 @@ class gradingform_guide_instance extends gradingform_instance { foreach ($grade['criteria'] as $record) { $curscore += $record['score']; } - return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])* - ($maxgrade-$mingrade), 0) + $mingrade; + $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])* + ($maxgrade-$mingrade); + if ($this->get_controller()->get_allow_grade_decimals()) { + return $gradeoffset + $mingrade; + } + return round($gradeoffset, 0) + $mingrade; } /** diff --git a/grade/grading/form/lib.php b/grade/grading/form/lib.php index 6c8c8acc5da..574d087d71c 100644 --- a/grade/grading/form/lib.php +++ b/grade/grading/form/lib.php @@ -73,6 +73,9 @@ abstract class gradingform_controller { /** @var array graderange array of valid grades for this area. Use set_grade_range and get_grade_range to access this */ private $graderange = null; + /** @var bool if decimal values are allowed as grades. */ + private $allowgradedecimals = false; + /** @var boolean|null cached result of function has_active_instances() */ protected $hasactiveinstances = null; @@ -622,13 +625,23 @@ abstract class gradingform_controller { /** * Sets the range of grades used in this area. This is usually either range like 0-100 - * or the scale where keys start from 1. Typical use: - * $controller->set_grade_range(make_grades_menu($gradingtype)); + * or the scale where keys start from 1. * - * @param array $graderange + * Typically modules will call it: + * $controller->set_grade_range(make_grades_menu($gradingtype), $gradingtype > 0); + * Negative $gradingtype means that scale is used and the grade must be rounded + * to the nearest int. Positive $gradingtype means that range 0..$gradingtype + * is used for the grades and in this case grade does not have to be rounded. + * + * Sometimes modules always expect grade to be rounded (like mod_assignment does). + * + * @param array $graderange array where first _key_ is the minimum grade and the + * last key is the maximum grade. + * @param bool $allowgradedecimals if decimal values are allowed as grades. */ - public final function set_grade_range(array $graderange) { + public final function set_grade_range(array $graderange, $allowgradedecimals = false) { $this->graderange = $graderange; + $this->allowgradedecimals = $allowgradedecimals; } /** @@ -643,6 +656,15 @@ abstract class gradingform_controller { return $this->graderange; } + /** + * Returns if decimal values are allowed as grades + * + * @return bool + */ + public final function get_allow_grade_decimals() { + return $this->allowgradedecimals; + } + /** * Overridden by sub classes that wish to make definition details available to web services. * When not overridden, only definition data common to all grading methods is made available. @@ -866,7 +888,11 @@ abstract class gradingform_instance { /** * Calculates the grade to be pushed to the gradebook * - * @return int the valid grade from $this->get_controller()->get_grade_range() + * Returned grade must be in range $this->get_controller()->get_grade_range() + * Plugins must returned grade converted to int unless + * $this->get_controller()->get_allow_grade_decimals() is true. + * + * @return float|int */ abstract public function get_grade(); diff --git a/grade/grading/form/rubric/lib.php b/grade/grading/form/rubric/lib.php index ae4f5d507b1..39e6adcbe15 100644 --- a/grade/grading/form/rubric/lib.php +++ b/grade/grading/form/rubric/lib.php @@ -814,10 +814,9 @@ class gradingform_rubric_instance extends gradingform_instance { /** * Calculates the grade to be pushed to the gradebook * - * @return int the valid grade from $this->get_controller()->get_grade_range() + * @return float|int the valid grade from $this->get_controller()->get_grade_range() */ public function get_grade() { - global $DB, $USER; $grade = $this->get_rubric_filling(); if (!($scores = $this->get_controller()->get_min_max_score()) || $scores['maxscore'] <= $scores['minscore']) { @@ -836,7 +835,11 @@ class gradingform_rubric_instance extends gradingform_instance { foreach ($grade['criteria'] as $id => $record) { $curscore += $this->get_controller()->get_definition()->rubric_criteria[$id]['levels'][$record['levelid']]['score']; } - return round(($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade), 0) + $mingrade; + $gradeoffset = ($curscore-$scores['minscore'])/($scores['maxscore']-$scores['minscore'])*($maxgrade-$mingrade); + if ($this->get_controller()->get_allow_grade_decimals()) { + return $gradeoffset + $mingrade; + } + return round($gradeoffset, 0) + $mingrade; } /** diff --git a/grade/grading/form/upgrade.txt b/grade/grading/form/upgrade.txt new file mode 100644 index 00000000000..21cc6316240 --- /dev/null +++ b/grade/grading/form/upgrade.txt @@ -0,0 +1,9 @@ +This files describes API changes in /grade/grading/form/* - Advanced grading methods +information provided here is intended especially for developers. + +=== 2.5.2 === + +* Grading methods now can return grade with decimals. See API functions + gradingform_controller::set_grade_range() and + gradingform_controller::get_allow_grade_decimals(), and also examples + in gradingform_rubric_instance::get_grade(). diff --git a/mod/assign/locallib.php b/mod/assign/locallib.php index 68edb736f00..9977d6b1bae 100644 --- a/mod/assign/locallib.php +++ b/mod/assign/locallib.php @@ -3394,7 +3394,7 @@ class assign { // Only show the grade if it is not hidden in gradebook. if (!empty($gradebookgrade->grade) && ($cangrade || !$gradebookgrade->hidden)) { if ($controller = $gradingmanager->get_active_controller()) { - $controller->set_grade_range(make_grades_menu($this->get_instance()->grade)); + $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0); $gradefordisplay = $controller->render_grade($PAGE, $grade->id, $gradingitem, @@ -3491,7 +3491,7 @@ class assign { // Now get the gradefordisplay. if ($controller) { - $controller->set_grade_range(make_grades_menu($this->get_instance()->grade)); + $controller->set_grade_range(make_grades_menu($this->get_instance()->grade), $this->get_instance()->grade > 0); $grade->gradefordisplay = $controller->render_grade($PAGE, $grade->id, $gradingitem, @@ -4880,6 +4880,7 @@ class assign { global $CFG, $USER; $grademenu = make_grades_menu($this->get_instance()->grade); + $allowgradedecimals = $this->get_instance()->grade > 0; $advancedgradingwarning = false; $gradingmanager = get_grading_manager($this->context, 'mod_assign', 'submissions'); @@ -4904,7 +4905,7 @@ class assign { } } if ($gradinginstance) { - $gradinginstance->get_controller()->set_grade_range($grademenu); + $gradinginstance->get_controller()->set_grade_range($grademenu, $allowgradedecimals); } return $gradinginstance; } From 16307c1428da2338c4e476c0c92d7099cf71a1c3 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 19 Jul 2013 15:15:21 +1000 Subject: [PATCH 12/43] MDL-33563 Version bump for grading methods API change --- grade/grading/form/guide/version.php | 4 ++-- grade/grading/form/rubric/version.php | 4 ++-- version.php | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/grade/grading/form/guide/version.php b/grade/grading/form/guide/version.php index 6d92a4fa0cf..48ba85c0ba4 100644 --- a/grade/grading/form/guide/version.php +++ b/grade/grading/form/guide/version.php @@ -25,6 +25,6 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'gradingform_guide'; -$plugin->version = 2013050100; -$plugin->requires = 2013050100; +$plugin->version = 2013051401.05; +$plugin->requires = 2013051401.05; $plugin->maturity = MATURITY_STABLE; \ No newline at end of file diff --git a/grade/grading/form/rubric/version.php b/grade/grading/form/rubric/version.php index d69563909bc..020b4ae0d65 100644 --- a/grade/grading/form/rubric/version.php +++ b/grade/grading/form/rubric/version.php @@ -25,8 +25,8 @@ defined('MOODLE_INTERNAL') || die(); $plugin->component = 'gradingform_rubric'; -$plugin->version = 2013050100; +$plugin->version = 2013051401.05; -$plugin->requires = 2013050100; +$plugin->requires = 2013051401.05; $plugin->maturity = MATURITY_STABLE; diff --git a/version.php b/version.php index 6f1808759de..c77ae318720 100644 --- a/version.php +++ b/version.php @@ -29,7 +29,7 @@ defined('MOODLE_INTERNAL') || die(); -$version = 2013051401.04; // 20130514 = branching date YYYYMMDD - do not modify! +$version = 2013051401.05; // 20130514 = branching date YYYYMMDD - do not modify! // RR = release increments - 00 in DEV branches // .XX = incremental changes From 63e1765c916990a7deaddf847e03047bd04d0755 Mon Sep 17 00:00:00 2001 From: Marina Glancy Date: Fri, 19 Jul 2013 15:52:23 +1000 Subject: [PATCH 13/43] MDL-38707 Display module name when the folder is displayed inline --- mod/folder/lang/en/folder.php | 2 +- mod/folder/renderer.php | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/mod/folder/lang/en/folder.php b/mod/folder/lang/en/folder.php index 5f114197662..a3af3ee46c8 100644 --- a/mod/folder/lang/en/folder.php +++ b/mod/folder/lang/en/folder.php @@ -45,7 +45,7 @@ $string['page-mod-folder-view'] = 'Folder module main page'; $string['pluginadministration'] = 'Folder administration'; $string['pluginname'] = 'Folder'; $string['display'] = 'Display folder contents'; -$string['display_help'] = 'If you choose to display the folder contents on a course page, there will be no link to a separate page and the title will not be displayed. +$string['display_help'] = 'If you choose to display the folder contents on a course page, there will be no link to a separate page. The description will be displayed only if "Display description on course page" is checked.
Also note that participants view actions can not be logged in this case.'; $string['displaypage'] = 'On a separate page'; diff --git a/mod/folder/renderer.php b/mod/folder/renderer.php index a755acf7431..07c0f63ed7e 100644 --- a/mod/folder/renderer.php +++ b/mod/folder/renderer.php @@ -58,7 +58,12 @@ class mod_folder_renderer extends plugin_renderer_base { } } - $output .= $this->output->box($this->render(new folder_tree($folder, $cm)), + $foldertree = new folder_tree($folder, $cm); + if ($folder->display == FOLDER_DISPLAY_INLINE) { + // Display module name as the name of the root directory. + $foldertree->dir['dirname'] = $cm->get_formatted_name(); + } + $output .= $this->output->box($this->render($foldertree), 'generalbox foldertree'); // Do not append the edit button on the course page. From 6139b462d7ac47fa42e3219d6d3c8db1b94d2f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20S=CC=8Ckoda?= Date: Wed, 17 Jul 2013 21:10:16 +0200 Subject: [PATCH 14/43] MDL-38867 remove Google spell checking option --- .../tinymce/plugins/spellchecker/config.php | 15 ++++----------- lib/editor/tinymce/plugins/spellchecker/lib.php | 6 ++++++ .../tinymce/plugins/spellchecker/settings.php | 4 ++-- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/lib/editor/tinymce/plugins/spellchecker/config.php b/lib/editor/tinymce/plugins/spellchecker/config.php index 4f1ac23e6e4..98adf18e4cd 100644 --- a/lib/editor/tinymce/plugins/spellchecker/config.php +++ b/lib/editor/tinymce/plugins/spellchecker/config.php @@ -28,25 +28,18 @@ require('../../../../../config.php'); // General settings $engine = get_config('tinymce_spellchecker', 'spellengine'); -if (!$engine) { - $engine = 'GoogleSpell'; +if (!$engine or $engine === 'GoogleSpell') { + $engine = 'PSpell'; } $config['general.engine'] = $engine; -// GoogleSpell settings -$config['GoogleSpell.proxyhost'] = isset($CFG->proxyhost) ? $CFG->proxyhost : ''; -$config['GoogleSpell.proxyport'] = isset($CFG->proxyport) ? $CFG->proxyport : ''; -$config['GoogleSpell.proxytype'] = isset($CFG->proxytype) ? $CFG->proxytype : 'HTML'; -$config['GoogleSpell.proxyuser'] = isset($CFG->proxyuser) ? $CFG->proxyuser : ''; -$config['GoogleSpell.proxypassword'] = isset($CFG->proxypassword) ? $CFG->proxypassword : ''; - -if ($config['general.engine'] === 'PSpell' || $config['general.engine'] === 'PSpellShell') { +if ($config['general.engine'] === 'PSpell') { // PSpell settings $config['PSpell.mode'] = PSPELL_FAST; $config['PSpell.spelling'] = ""; $config['PSpell.jargon'] = ""; $config['PSpell.encoding'] = ""; - +} else if ($config['general.engine'] === 'PSpellShell') { // PSpellShell settings $config['PSpellShell.mode'] = PSPELL_FAST; $config['PSpellShell.aspell'] = $CFG->aspellpath; diff --git a/lib/editor/tinymce/plugins/spellchecker/lib.php b/lib/editor/tinymce/plugins/spellchecker/lib.php index 3f856656693..8c14e08546d 100644 --- a/lib/editor/tinymce/plugins/spellchecker/lib.php +++ b/lib/editor/tinymce/plugins/spellchecker/lib.php @@ -36,6 +36,12 @@ class tinymce_spellchecker extends editor_tinymce_plugin { return; } + // Check some speller is configured. + $engine = $this->get_config('spellengine', ''); + if (!$engine or $engine === 'GoogleSpell') { + return; + } + // Check at least one language is supported. $spelllanguagelist = $this->get_config('spelllanguagelist', ''); if ($spelllanguagelist !== '') { diff --git a/lib/editor/tinymce/plugins/spellchecker/settings.php b/lib/editor/tinymce/plugins/spellchecker/settings.php index c4081e3a2ea..2be8fde4dd4 100644 --- a/lib/editor/tinymce/plugins/spellchecker/settings.php +++ b/lib/editor/tinymce/plugins/spellchecker/settings.php @@ -26,11 +26,11 @@ defined('MOODLE_INTERNAL') || die; if ($ADMIN->fulltree) { $options = array( + ''=>get_string('none'), 'PSpell'=>'PSpell', - 'GoogleSpell'=>'Google Spell', 'PSpellShell'=>'PSpellShell'); $settings->add(new admin_setting_configselect('tinymce_spellchecker/spellengine', - get_string('spellengine', 'admin'), '', 'GoogleSpell', $options)); + get_string('spellengine', 'admin'), '', '', $options)); $settings->add(new admin_setting_configtextarea('tinymce_spellchecker/spelllanguagelist', get_string('spelllanguagelist', 'admin'), '', '+English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr,German=de,Italian=it,Polish=pl,' . From a4fad25c530e1dedadeab94ab3d85b4ea33dc044 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Tue, 18 Jun 2013 16:28:21 +0800 Subject: [PATCH 15/43] MDL-39635 behat: Fixing line breaks Not supported by Windows OSs. --- lib/behat/behat_files.php | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/lib/behat/behat_files.php b/lib/behat/behat_files.php index 4e8b7d171cd..5f15c3aad83 100644 --- a/lib/behat/behat_files.php +++ b/lib/behat/behat_files.php @@ -66,8 +66,9 @@ class behat_files extends behat_base { // Gets the ffilemanager node specified by the locator which contains the filepicker container. $filepickercontainer = $this->find( 'xpath', - "//input[./@id = //label[contains(normalize-space(string(.)), '" . $filepickerelement . "')]/@for] -//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ') or contains(concat(' ', normalize-space(@class), ' '), ' ffilepicker ')]", + "//input[./@id = //label[contains(normalize-space(string(.)), '" . $filepickerelement . "')]/@for]" . + "//ancestor::div[contains(concat(' ', normalize-space(@class), ' '), ' ffilemanager ') or " . + "contains(concat(' ', normalize-space(@class), ' '), ' ffilepicker ')]", $exception ); @@ -123,11 +124,10 @@ class behat_files extends behat_base { // First we look at the folder as we need to click on the contextual menu otherwise it would be opened. $node = $this->find( '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 . "')] -//descendant::a[contains(concat(' ', normalize-space(@class), ' '), ' fp-contextmenu ')] -", + "//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 . "')]" . + "//descendant::a[contains(concat(' ', normalize-space(@class), ' '), ' fp-contextmenu ')]", $exception, $containernode ); @@ -137,10 +137,10 @@ class behat_files extends behat_base { // Here the contextual menu is hidden, we click on the thumbnail. $node = $this->find( 'xpath', - "//div[@class='fp-content'] -//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')][contains(normalize-space(string(.)), '" . $name . "')] -//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')] -", + "//div[@class='fp-content']" . + "//descendant::*[self::div | self::a][contains(concat(' ', normalize-space(@class), ' '), ' fp-file ')]" . + "[contains(normalize-space(string(.)), '" . $name . "')]" . + "//descendant::div[contains(concat(' ', normalize-space(@class), ' '), ' fp-thumbnail ')]", false, $containernode ); @@ -179,9 +179,9 @@ class behat_files extends behat_base { // 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 . "')]", + "//div[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-area ')]" . + "//descendant::span[contains(concat(' ', normalize-space(@class), ' '), ' fp-repo-name ')]" . + "[contains(normalize-space(string(.)), '" . $repositoryname . "')]", $repoexception ); From 15c461d34d4c548882c345edfabf27149b731bcd Mon Sep 17 00:00:00 2001 From: David Monllao Date: Tue, 18 Jun 2013 16:29:23 +0800 Subject: [PATCH 16/43] MDL-39635 behat: Replacing hardcoded language strings Using get_string() instead. --- admin/tests/behat/behat_admin.php | 4 +- auth/tests/behat/behat_auth.php | 10 ++--- backup/util/ui/tests/behat/behat_backup.php | 44 ++++++++++--------- .../tests/behat/behat_block_comments.php | 2 +- blocks/tests/behat/behat_blocks.php | 2 +- cohort/tests/behat/behat_cohort.php | 18 ++++---- completion/tests/behat/behat_completion.php | 6 +-- course/tests/behat/behat_course.php | 20 ++++----- enrol/tests/behat/behat_enrol.php | 8 ++-- group/tests/behat/behat_groups.php | 2 +- lib/tests/behat/behat_permissions.php | 20 ++++----- message/tests/behat/behat_message.php | 4 +- mod/choice/tests/behat/behat_mod_choice.php | 2 +- mod/forum/tests/behat/behat_mod_forum.php | 8 ++-- .../tests/behat/behat_mod_glossary.php | 4 +- question/tests/behat/behat_question.php | 2 +- .../tests/behat/behat_repository_recent.php | 2 +- repository/tests/behat/behat_filepicker.php | 2 +- 18 files changed, 81 insertions(+), 79 deletions(-) diff --git a/admin/tests/behat/behat_admin.php b/admin/tests/behat/behat_admin.php index 0cd23064ad4..f29ebd185d0 100644 --- a/admin/tests/behat/behat_admin.php +++ b/admin/tests/behat/behat_admin.php @@ -62,7 +62,7 @@ class behat_admin extends behat_base { } // Search by label. - $searchbox = $this->find_field('Search in settings'); + $searchbox = $this->find_field(get_string('searchinsettings', 'admin')); $searchbox->setValue($label); $submitsearch = $this->find('css', 'form.adminsearchform input[type=submit]'); $submitsearch->press(); @@ -90,7 +90,7 @@ class behat_admin extends behat_base { $field = behat_field_manager::get_field_instance($type, $fieldnode, $this->getSession()); $field->set_value($value); - $this->find_button('Save changes')->press(); + $this->find_button(get_string('savechanges'))->press(); } } diff --git a/auth/tests/behat/behat_auth.php b/auth/tests/behat/behat_auth.php index c6d47aa4958..b484b4f876f 100644 --- a/auth/tests/behat/behat_auth.php +++ b/auth/tests/behat/behat_auth.php @@ -49,10 +49,10 @@ class behat_auth extends behat_base { public function i_log_in_as($username) { return array(new Given('I am on homepage'), - new Given('I follow "Login"'), - new Given('I fill in "Username" with "'.$username.'"'), - new Given('I fill in "Password" with "'.$username.'"'), - new Given('I press "Login"') + 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 press "' . get_string('login') . '"') ); } @@ -62,7 +62,7 @@ class behat_auth extends behat_base { * @Given /^I log out$/ */ public function i_log_out() { - return new When('I follow "Logout"'); + return new When('I follow "' . get_string('logout') . '"'); } } diff --git a/backup/util/ui/tests/behat/behat_backup.php b/backup/util/ui/tests/behat/behat_backup.php index b0d99986caf..0b22d74e95f 100644 --- a/backup/util/ui/tests/behat/behat_backup.php +++ b/backup/util/ui/tests/behat/behat_backup.php @@ -61,25 +61,25 @@ class behat_backup extends behat_base { $this->find_link($backupcourse)->click(); // Click the backup link. - $this->find_link('Backup')->click(); + $this->find_link(get_string('backup'))->click(); // Initial settings. $this->fill_backup_restore_form($options); - $this->find_button('Next')->press(); + $this->find_button(get_string('backupstage1action', 'backup'))->press(); // Schema settings. $this->fill_backup_restore_form($options); - $this->find_button('Next')->press(); + $this->find_button(get_string('backupstage2action', 'backup'))->press(); // Confirmation and review, backup filename can also be specified. $this->fill_backup_restore_form($options); - $this->find_button('Perform backup')->press(); + $this->find_button(get_string('backupstage4action', 'backup'))->press(); // Waiting for it to finish. $this->wait(10); // Last backup continue button. - $this->find_button('Continue')->press(); + $this->find_button(get_string('backupstage16action', 'backup'))->press(); } /** @@ -106,7 +106,7 @@ class behat_backup extends behat_base { $this->find_link($tocourse)->click(); // Click the backup link. - $this->find_link('Import')->click(); + $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()); @@ -119,22 +119,22 @@ class behat_backup extends behat_base { $radionode->check(); $radionode->click(); - $this->find_button('Continue')->press(); + $this->find_button(get_string('continue'))->press(); // Initial settings. $this->fill_backup_restore_form($options); - $this->find_button('Next')->press(); + $this->find_button(get_string('importbackupstage1action', 'backup'))->press(); // Schema settings. $this->fill_backup_restore_form($options); - $this->find_button('Next')->press(); + $this->find_button(get_string('importbackupstage2action', 'backup'))->press(); // Run it. - $this->find_button('Perform import')->press(); + $this->find_button(get_string('importbackupstage4action', 'backup'))->press(); $this->wait(); // Continue and redirect to 'to' course. - $this->find_button('Continue')->press(); + $this->find_button(get_string('continue'))->press(); } /** @@ -160,7 +160,8 @@ class behat_backup extends behat_base { $radionode->click(); // Pressing the continue button of the restore into an existing course section. - $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]/descendant::input[@type='submit'][@value='Continue']"); + $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-existing-course')]" . + "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"); $continuenode->click(); $this->wait(); @@ -188,7 +189,8 @@ class behat_backup extends behat_base { $radionode->click(); // Pressing the continue button of the restore into an existing course section. - $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]/descendant::input[@type='submit'][@value='Continue']"); + $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-new-course')]" . + "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"); $continuenode->click(); $this->wait(); @@ -216,7 +218,7 @@ class behat_backup extends behat_base { // Pressing the continue button of the restore merging section. $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" . - "/descendant::input[@type='submit'][@value='Continue']"); + "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"); $continuenode->click(); $this->wait(); @@ -244,7 +246,7 @@ class behat_backup extends behat_base { // Pressing the continue button of the restore merging section. $continuenode = $this->find('xpath', "//div[contains(@class, 'bcs-current-course')]" . - "/descendant::input[@type='submit'][@value='Continue']"); + "/descendant::input[@type='submit'][@value='" . get_string('continue') . "']"); $continuenode->click(); $this->wait(); @@ -263,12 +265,12 @@ 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(., 'Restore')]"; + $xpath = "//tr[contains(., '" . $backupfilename . "')]/descendant::a[contains(., '" . get_string('restore') . "')]"; $restorelink = $this->find('xpath', $xpath, $exception); $restorelink->click(); // Confirm the backup contents. - $restore = $this->find_button('Continue')->press(); + $restore = $this->find_button(get_string('continue'))->press(); } /** @@ -284,18 +286,18 @@ class behat_backup extends behat_base { // Settings. $this->fill_backup_restore_form($options); - $this->find_button('Next')->press(); + $this->find_button(get_string('restorestage4action', 'backup'))->press(); // Schema. $this->fill_backup_restore_form($options); - $this->find_button('Next')->press(); + $this->find_button(get_string('restorestage8action', 'backup'))->press(); // Review, no options here. - $this->find_button('Perform restore')->press(); + $this->find_button(get_string('restorestage16action', 'backup'))->press(); $this->wait(10); // Last restore continue button, redirected to restore course after this. - $this->find_button('Continue')->press(); + $this->find_button(get_string('restorestage32action', 'backup'))->press(); } /** diff --git a/blocks/comments/tests/behat/behat_block_comments.php b/blocks/comments/tests/behat/behat_block_comments.php index 548246062a3..8854d3b33f2 100644 --- a/blocks/comments/tests/behat/behat_block_comments.php +++ b/blocks/comments/tests/behat/behat_block_comments.php @@ -63,7 +63,7 @@ class behat_block_comments extends behat_base { $commentstextarea = $this->find('css', '.comment-area textarea', $exception); $commentstextarea->setValue($comment); - $this->find_link('Save comment')->click(); + $this->find_link(get_string('savecomment'))->click(); // Wait for the AJAX request. $this->getSession()->wait(4 * 1000, false); diff --git a/blocks/tests/behat/behat_blocks.php b/blocks/tests/behat/behat_blocks.php index 1c8f70d9aa1..881880b6a92 100644 --- a/blocks/tests/behat/behat_blocks.php +++ b/blocks/tests/behat/behat_blocks.php @@ -52,7 +52,7 @@ class behat_blocks extends behat_base { if (!$this->running_javascript()) { $steps = array( $steps, - new Given('I click on "Go" "button" in the "#add_block" "css_element"') + new Given('I click on "' . get_string('go') . '" "button" in the "#add_block" "css_element"') ); } return $steps; diff --git a/cohort/tests/behat/behat_cohort.php b/cohort/tests/behat/behat_cohort.php index 68ff6073fcd..fcce6928e0e 100644 --- a/cohort/tests/behat/behat_cohort.php +++ b/cohort/tests/behat/behat_cohort.php @@ -54,10 +54,10 @@ class behat_cohort extends behat_base { $userid = $DB->get_field('user', 'id', array('username' => $username)); $steps = array( - new Given('I click on "Assign" "link" in the "//table[@id=\'cohorts\']//tr[contains(., \'' . $cohortidnumber . '\')]" "xpath_element"'), - new Given('I select "' . $userid . '" from "Potential users"'), - new Given('I press "Add"'), - new Given('I press "Back to cohorts"') + new Given('I click on "' . get_string('assign', 'cohort') . '" "link" in the "//table[@id=\'cohorts\']//tr[contains(., \'' . $cohortidnumber . '\')]" "xpath_element"'), + 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') . '"') ); // If we are not in the cohorts management we should move there before anything else. @@ -65,11 +65,11 @@ class behat_cohort extends behat_base { $steps = array_merge( array( new Given('I am on homepage'), - new Given('I collapse "Front page settings" node'), - new Given('I expand "Site administration" node'), - new Given('I expand "Users" node'), - new Given('I expand "Accounts" node'), - new Given('I follow "Cohorts"') + new Given('I collapse "' . get_string('frontpagesettings', 'admin') . '" node'), + new Given('I expand "' . get_string('administrationsite') . '" node'), + new Given('I expand "' . get_string('users', 'admin') . '" node'), + new Given('I expand "' . get_string('accounts', 'admin') . '" node'), + new Given('I follow "' . get_string('cohorts', 'cohort') . '"') ), $steps ); diff --git a/completion/tests/behat/behat_completion.php b/completion/tests/behat/behat_completion.php index ef05aeb328d..fe325406a0b 100644 --- a/completion/tests/behat/behat_completion.php +++ b/completion/tests/behat/behat_completion.php @@ -92,12 +92,12 @@ class behat_completion extends behat_base { $this->find('xpath', "//*[@id='settingsnav']" . "/descendant::li" . "/descendant::li[not(contains(@class,'collapsed'))]" . - "/descendant::p[contains(., 'Activity completion')]"); + "/descendant::p[contains(., '" . get_string('pluginname', 'report_progress') . "')]"); } catch (ElementNotFoundException $e) { - $steps[] = new Given('I expand "Reports" node'); + $steps[] = new Given('I expand "' . get_string('reports') . '" node'); } - $steps[] = new Given('I follow "Activity completion"'); + $steps[] = new Given('I follow "' . get_string('pluginname', 'report_progress') . '"'); return $steps; } diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php index c12a9381941..17f6d387c24 100644 --- a/course/tests/behat/behat_course.php +++ b/course/tests/behat/behat_course.php @@ -48,7 +48,7 @@ class behat_course extends behat_base { * @Given /^I turn editing mode on$/ */ public function i_turn_editing_mode_on() { - return new Given('I press "Turn editing on"'); + return new Given('I press "' . get_string('turneditingon') . '"'); } /** @@ -56,7 +56,7 @@ class behat_course extends behat_base { * @Given /^I turn editing mode off$/ */ public function i_turn_editing_mode_off() { - return new Given('I press "Turn editing off"'); + return new Given('I press "' . get_string('turneditingoff') . '"'); } /** @@ -68,9 +68,9 @@ class behat_course extends behat_base { public function i_create_a_course_with(TableNode $table) { return array( new Given('I go to the courses management page'), - new Given('I press "Add a new course"'), + new Given('I press "' . get_string('addnewcourse') . '"'), new Given('I fill the moodle form with:', $table), - new Given('I press "Save changes"') + new Given('I press "' . get_string('savechanges') . '"') ); } @@ -83,9 +83,9 @@ class behat_course extends behat_base { return array( new Given('I am on homepage'), - new Given('I expand "Site administration" node'), - new Given('I expand "Courses" node'), - new Given('I follow "Add/edit courses"'), + new Given('I expand "' . get_string('administrationsite') . '" node'), + new Given('I expand "' . get_string('courses', 'admin') . '" node'), + new Given('I follow "' . get_string('coursemgmt', 'admin') . '"'), ); } @@ -102,7 +102,7 @@ class behat_course extends behat_base { return array( new Given('I add a "'.$activity.'" to section "'.$section.'"'), new Given('I fill the moodle form with:', $data), - new Given('I press "Save and return to course"') + new Given('I press "' . get_string('savechangesandreturntocourse') . '"') ); } @@ -765,8 +765,8 @@ class behat_course extends behat_base { protected function is_course_editor() { // We don't need to behat_base::spin() here as all is already loaded. - if (!$this->getSession()->getPage()->findButton('Turn editing off') && - !$this->getSession()->getPage()->findButton('Turn editing on')) { + if (!$this->getSession()->getPage()->findButton(get_string('turneditingoff')) && + !$this->getSession()->getPage()->findButton(get_string('turneditingon'))) { return false; } diff --git a/enrol/tests/behat/behat_enrol.php b/enrol/tests/behat/behat_enrol.php index 0b18d78b202..54a0dcec752 100644 --- a/enrol/tests/behat/behat_enrol.php +++ b/enrol/tests/behat/behat_enrol.php @@ -50,11 +50,11 @@ class behat_enrol extends behat_base { public function i_add_enrolment_method_with($enrolmethod, TableNode $table) { return array( - new Given('I expand "Users" node'), - new Given('I follow "Enrolment methods"'), - new Given('I select "' . $enrolmethod . '" from "Add method"'), + 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 fill the moodle form with:', $table), - new Given('I press "Add method"') + 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 847727d3b78..3411ece3ee9 100644 --- a/group/tests/behat/behat_groups.php +++ b/group/tests/behat/behat_groups.php @@ -80,7 +80,7 @@ class behat_groups extends behat_base { $this->getSession()->wait(self::TIMEOUT, '(document.readyState === "complete")'); // Returning to the main groups page. - $this->find_button('Back to groups')->click(); + $this->find_button(get_string('backtogroups', 'group'))->click(); } } diff --git a/lib/tests/behat/behat_permissions.php b/lib/tests/behat/behat_permissions.php index 84c8f5aa961..a687446a835 100644 --- a/lib/tests/behat/behat_permissions.php +++ b/lib/tests/behat/behat_permissions.php @@ -52,14 +52,14 @@ class behat_permissions extends behat_base { return array( new Given('I am on homepage'), - new Given('I collapse "Front page settings" node'), - new Given('I expand "Site administration" node'), - new Given('I expand "Users" node'), - new Given('I expand "Permissions" node'), - new Given('I follow "Define roles"'), + new Given('I collapse "' . get_string('frontpagesettings', 'admin') . '" node'), + new Given('I expand "' . get_string('administrationsite') . '" node'), + new Given('I expand "' . get_string('users', 'admin') . '" node'), + new Given('I expand "' . get_string('permissions', 'role') . '" node'), + new Given('I follow "' . get_string('defineroles', 'role') . '"'), new Given('I follow "Edit ' . $this->escape($rolename) . ' role"'), new Given('I fill the capabilities form with the following permissions:', $table), - new Given('I press "Save changes"') + new Given('I press "' . get_string('savechanges') . '"') ); } @@ -76,9 +76,9 @@ class behat_permissions extends behat_base { $roleoption = $this->find('xpath', '//select[@name="roleid"]/option[contains(text(),"' . $this->escape($rolename) . '")]'); return array( - new Given('I select "' . $this->escape($roleoption->getText()) . '" from "Advanced role override"'), + new Given('I select "' . $this->escape($roleoption->getText()) . '" from "' . get_string('advancedoverride', 'role') . '"'), new Given('I fill the capabilities form with the following permissions:', $table), - new Given('I press "Save changes"') + new Given('I press "' . get_string('savechanges') . '"') ); } @@ -93,9 +93,9 @@ class behat_permissions extends behat_base { // Ensure we are using the advanced view. // Wrapped in a try/catch to capture the exception and continue execution, we don't know if advanced mode was already enabled. try { - $advancedtoggle = $this->find_button('Show advanced'); + $advancedtoggle = $this->find_button(get_string('showadvanced', 'form')); if ($advancedtoggle) { - $this->getSession()->getPage()->pressButton('Show advanced'); + $this->getSession()->getPage()->pressButton(get_string('showadvanced', 'form')); } } catch (Exception $e) { // We already are in advanced mode. diff --git a/message/tests/behat/behat_message.php b/message/tests/behat/behat_message.php index 3b0a07edd67..12c7d5cfa56 100644 --- a/message/tests/behat/behat_message.php +++ b/message/tests/behat/behat_message.php @@ -63,10 +63,10 @@ class behat_message extends behat_base { $steps[] = new Given('I am on homepage'); if ($this->running_javascript()) { - $steps[] = new Given('I expand "My profile" node'); + $steps[] = new Given('I expand "' . get_string('myprofile') . '" node'); } - $steps[] = new Given('I follow "Messages"'); + $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 press "' . get_string('searchcombined', 'message') . '"'); $steps[] = new Given('I follow "' . get_string('sendmessageto', 'message', $tofullname) . '"'); diff --git a/mod/choice/tests/behat/behat_mod_choice.php b/mod/choice/tests/behat/behat_mod_choice.php index 286e41e92c7..1cc714c72e2 100644 --- a/mod/choice/tests/behat/behat_mod_choice.php +++ b/mod/choice/tests/behat/behat_mod_choice.php @@ -52,7 +52,7 @@ class behat_mod_choice extends behat_base { return array( new Given('I follow "' . $this->escape($choiceactivity) . '"'), new Given('I select "' . $this->escape($option) . '" radio button'), - new Given('I press "Save my choice"') + new Given('I press "' . get_string('savemychoice', 'choice') . '"') ); } diff --git a/mod/forum/tests/behat/behat_mod_forum.php b/mod/forum/tests/behat/behat_mod_forum.php index e5afd877d92..55b3f61bb89 100644 --- a/mod/forum/tests/behat/behat_mod_forum.php +++ b/mod/forum/tests/behat/behat_mod_forum.php @@ -51,9 +51,9 @@ class behat_mod_forum extends behat_base { // Escaping $forumname as it has been stripped automatically by the transformer. return array( new Given('I follow "' . $this->escape($forumname) . '"'), - new Given('I press "' . get_string('addanewdiscussion', 'mod_forum') . '"'), + new Given('I press "' . get_string('addanewdiscussion', 'forum') . '"'), new Given('I fill the moodle form with:', $table), - new Given('I press "' . get_string('posttoforum', 'mod_forum') . '"'), + new Given('I press "' . get_string('posttoforum', 'forum') . '"'), new Given('I wait to be redirected') ); } @@ -71,9 +71,9 @@ class behat_mod_forum extends behat_base { return array( new Given('I follow "' . $this->escape($forumname) . '"'), new Given('I follow "' . $this->escape($postsubject) . '"'), - new Given('I follow "' . get_string('reply', 'mod_forum') . '"'), + new Given('I follow "' . get_string('reply', 'forum') . '"'), new Given('I fill the moodle form with:', $table), - new Given('I press "' . get_string('posttoforum', 'mod_forum') . '"'), + new Given('I press "' . get_string('posttoforum', 'forum') . '"'), new Given('I wait to be redirected') ); diff --git a/mod/glossary/tests/behat/behat_mod_glossary.php b/mod/glossary/tests/behat/behat_mod_glossary.php index a692634dfd4..da77534cabe 100644 --- a/mod/glossary/tests/behat/behat_mod_glossary.php +++ b/mod/glossary/tests/behat/behat_mod_glossary.php @@ -50,7 +50,7 @@ class behat_mod_glossary extends behat_base { return array( new Given('I press "' . get_string('addentry', 'mod_glossary') . '"'), new Given('I fill the moodle form with:', $data), - new Given('I press "Save changes"') + new Given('I press "' . get_string('savechanges') . '"') ); } @@ -65,7 +65,7 @@ class behat_mod_glossary extends behat_base { return array( new Given('I follow "' . get_string('categoryview', 'mod_glossary') . '"'), new Given('I press "' . get_string('editcategories', 'mod_glossary') . '"'), - new Given('I press "Add Category"'), + new Given('I press "' . get_string('add').' '.get_string('category', 'glossary') . '"'), new Given('I fill in "name" with "' . $categoryname . '"'), new Given('I press "' . get_string('savechanges') . '"'), new Given('I press "' . get_string('back', 'mod_glossary') . '"') diff --git a/question/tests/behat/behat_question.php b/question/tests/behat/behat_question.php index 79fe0ea34ae..f022eb8bc34 100644 --- a/question/tests/behat/behat_question.php +++ b/question/tests/behat/behat_question.php @@ -60,7 +60,7 @@ class behat_question extends behat_base { new Given('I click on "' . $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 "Save changes"') + new Given('I press "' . get_string('savechanges') . '"') ); } diff --git a/repository/recent/tests/behat/behat_repository_recent.php b/repository/recent/tests/behat/behat_repository_recent.php index f5f3462f629..27dcaf53f75 100644 --- a/repository/recent/tests/behat/behat_repository_recent.php +++ b/repository/recent/tests/behat/behat_repository_recent.php @@ -56,7 +56,7 @@ class behat_repository_recent extends behat_files { // Opening the specified file contextual menu from the modal window. $this->open_element_contextual_menu($filename); - $this->find_button('Select this file')->click(); + $this->find_button(get_string('getfile', 'repository'))->click(); // Ensure the file has been selected and we returned to the form page. $this->wait_until_return_to_form(); diff --git a/repository/tests/behat/behat_filepicker.php b/repository/tests/behat/behat_filepicker.php index e06e3864c41..bcc076dd0dc 100644 --- a/repository/tests/behat/behat_filepicker.php +++ b/repository/tests/behat/behat_filepicker.php @@ -63,7 +63,7 @@ class behat_filepicker extends behat_files { $dialoginput = $this->find('css', '.fp-mkdir-dlg-text input'); $dialoginput->setValue($foldername); - $this->getSession()->getPage()->pressButton('Create folder'); + $this->getSession()->getPage()->pressButton(get_string('makeafolder')); // Wait until the process finished and modal windows are hidden. $this->wait_until_return_to_form(); From 55e9a99c1813a865cf12e29157777418a3903db1 Mon Sep 17 00:00:00 2001 From: David Monllao Date: Fri, 21 Jun 2013 13:48:36 +0800 Subject: [PATCH 17/43] 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 58c724ff642..37282738edb 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 ')]/descendant::a[contains(@href, 'print.php')]" "xpath_element" should exists + And "//span[contains(concat(' ', normalize-space(@class), ' '), ' printicon ')]/descendant::a[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 ')]/descendant::a[contains(@href, 'print.php')]" "xpath_element" should not exists + And "//span[contains(concat(' ', normalize-space(@class), ' '), ' printicon ')]/descendant::a[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 ); From 2ab6b4d43d3026966a3440aa90c34c0470641a4f Mon Sep 17 00:00:00 2001 From: David Monllao Date: Tue, 11 Jun 2013 11:27:34 +0800 Subject: [PATCH 18/43] MDL-40123 behat: Show all of form's elements changes. * Avoid waiting until Show more... links appears if we already waited. * Use collapse/expand button rather than clicking on each of the form's fieldsets. --- lib/tests/behat/behat_forms.php | 35 +++++++++++++++------------------ 1 file changed, 16 insertions(+), 19 deletions(-) diff --git a/lib/tests/behat/behat_forms.php b/lib/tests/behat/behat_forms.php index e6efb67825a..b33931fcc98 100644 --- a/lib/tests/behat/behat_forms.php +++ b/lib/tests/behat/behat_forms.php @@ -110,34 +110,31 @@ class behat_forms extends behat_base { // behat_base::find() throws an exception if there are no elements, we should not fail a test because of this. try { - // Expand fieldsets. - $fieldsets = $this->find_all('css', 'fieldset.collapsed a.fheader'); - - // We are supposed to have fieldsets here, otherwise exception. - - // Funny thing about this, with find_all() we specify a pattern and each element matching the pattern is added to the array - // with of xpaths with a [0], [1]... sufix, but when we click on an element it does not matches the specified xpath - // anymore (is not collapsed) so [1] becomes [0], that's why we always click on the first XPath match, will be always the next one. - $iterations = count($fieldsets); - for ($i = 0; $i < $iterations; $i++) { - $fieldsets[0]->click(); - } + // Expand fieldsets link. + $collapseexpandlink = $this->find('xpath', "//div[@class='collapsible-actions']" . + "/descendant::a[contains(concat(' ', @class, ' '), ' collapseexpand ')]" . + "[not(contains(concat(' ', @class, ' '), ' collapse-all '))]" + ); + $collapseexpandlink->click(); } catch (ElementNotFoundException $e) { - // We continue if there are not expanded fields. + // We continue if there are not expandable fields. } // Different try & catch as we can have expanded fieldsets with advanced fields on them. try { - // Show all fields. - $showmorestr = get_string('showmore', 'form'); - $showmores = $this->find_all('xpath', "//a[normalize-space(.)='" . $showmorestr . "']" . - "[contains(concat(' ', normalize-space(@class), ' '), ' moreless-toggler ')]"); + // Expand all fields xpath. + $showmorexpath = "//a[normalize-space(.)='" . get_string('showmore', 'form') . "']" . + "[contains(concat(' ', normalize-space(@class), ' '), ' moreless-toggler')]"; - // We are supposed to have 'show more's here, otherwise exception. + // We don't wait here as we already waited when getting the expand fieldsets links. + $showmores = $this->getSession()->getPage()->findAll('xpath', $showmorexpath); - // Same funny case, after clicking on the element the [1] showmore link becomes the [0]. + // Funny thing about this, with findAll() we specify a pattern and each element matching the pattern is added to the array + // with of xpaths with a [0], [1]... sufix, but when we click on an element it does not matches the specified xpath + // anymore (now is a "Show less..." link) so [1] becomes [0], that's why we always click on the first XPath match, + // will be always the next one. $iterations = count($showmores); for ($i = 0; $i < $iterations; $i++) { $showmores[0]->click(); From 58c44e5c20139380f3933749eb212a5a0dc79986 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 18 Jul 2013 19:26:02 +0100 Subject: [PATCH 19/43] MDL-20609 quiz tables: improve layout for long cell contents. By allowing more wrapping. --- mod/quiz/report/statistics/statistics_question_table.php | 2 +- mod/quiz/styles.css | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/mod/quiz/report/statistics/statistics_question_table.php b/mod/quiz/report/statistics/statistics_question_table.php index a1223fa10b2..d50b0d40f05 100644 --- a/mod/quiz/report/statistics/statistics_question_table.php +++ b/mod/quiz/report/statistics/statistics_question_table.php @@ -66,7 +66,7 @@ class quiz_statistics_question_table extends flexible_table { $this->define_baseurl($reporturl->out()); $this->collapsible(false); - $this->set_attribute('class', 'generaltable generalbox boxaligncenter'); + $this->set_attribute('class', 'generaltable generalbox boxaligncenter quizresponseanalysis'); // Define the table columns. $columns = array(); diff --git a/mod/quiz/styles.css b/mod/quiz/styles.css index 7d4fb1c3611..54a09145213 100644 --- a/mod/quiz/styles.css +++ b/mod/quiz/styles.css @@ -416,6 +416,14 @@ table.quizreviewsummary td.cell { #page-mod-quiz-report fieldset.felement.fgroup { margin: 0; } +#page-mod-quiz-report table th { + white-space: normal; +} +#page-mod-quiz-report table#attempts td, +#page-mod-quiz-report table.quizresponseanalysis td { + word-wrap: break-word; + max-width: 20em; +} #page-mod-quiz-report table.titlesleft td.c0 { font-weight: bold; } From 9a62c01edcff699345d5d5c6b997bdb981a125a7 Mon Sep 17 00:00:00 2001 From: Tim Hunt Date: Thu, 18 Jul 2013 18:55:56 +0100 Subject: [PATCH 20/43] MDL-40480 tablelib: specify charset in XHMTL downloads. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Thanks to Jürgen Zimmer for the suggestion. --- lib/tablelib.php | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tablelib.php b/lib/tablelib.php index ebd4886b0d9..b14231007d0 100644 --- a/lib/tablelib.php +++ b/lib/tablelib.php @@ -1686,6 +1686,7 @@ class table_xhtml_export_format extends table_default_export_format_parent { +