From 86565e4a364ca7e3cc0aeaabe72d462340e3e7c6 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Mon, 16 Nov 2020 13:52:07 +0100 Subject: [PATCH 01/26] MDL-42382 admin: Add replace filters button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Luca Bösch Co-authored-by: Andrei Bautu --- lang/en/filters.php | 1 + user/filters/lib.php | 28 +++++++++++++++++----------- user/filters/user_filter_forms.php | 13 +++++++++++-- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/lang/en/filters.php b/lang/en/filters.php index f178887b4d6..e7fec9177cb 100644 --- a/lang/en/filters.php +++ b/lang/en/filters.php @@ -80,6 +80,7 @@ $string['profilelabel'] = '{$a->label}: {$a->profile} {$a->operator} {$a->value} $string['profilelabelnovalue'] = '{$a->label}: {$a->profile} {$a->operator}'; $string['removeall'] = 'Remove all filters'; $string['removeselected'] = 'Remove selected'; +$string['replacefilters'] = 'Replace filters'; $string['selectlabel'] = '{$a->label} {$a->operator} {$a->value}'; $string['startswith'] = 'starts with'; $string['tablenosave'] = 'Changes in table above are saved automatically.'; diff --git a/user/filters/lib.php b/user/filters/lib.php index a6fd73d4541..0bb6c893a4b 100644 --- a/user/filters/lib.php +++ b/user/filters/lib.php @@ -101,6 +101,12 @@ class user_filtering { // Fist the new filter form. $this->_addform = new user_add_filter_form($baseurl, array('fields' => $this->_fields, 'extraparams' => $extraparams)); if ($adddata = $this->_addform->get_data()) { + // Clear previous filters. + if (!empty($adddata->replacefilters)) { + $SESSION->user_filtering = []; + } + + // Add new filters. foreach ($this->_fields as $fname => $field) { $data = $field->check_data($adddata); if ($data === false) { @@ -111,19 +117,16 @@ class user_filtering { } $SESSION->user_filtering[$fname][] = $data; } - // Clear the form. - $_POST = array(); - $this->_addform = new user_add_filter_form($baseurl, array('fields' => $this->_fields, 'extraparams' => $extraparams)); } // Now the active filters. $this->_activeform = new user_active_filter_form($baseurl, array('fields' => $this->_fields, 'extraparams' => $extraparams)); - if ($adddata = $this->_activeform->get_data()) { - if (!empty($adddata->removeall)) { + if ($activedata = $this->_activeform->get_data()) { + if (!empty($activedata->removeall)) { $SESSION->user_filtering = array(); - } else if (!empty($adddata->removeselected) and !empty($adddata->filter)) { - foreach ($adddata->filter as $fname => $instances) { + } else if (!empty($activedata->removeselected) and !empty($activedata->filter)) { + foreach ($activedata->filter as $fname => $instances) { foreach ($instances as $i => $val) { if (empty($val)) { continue; @@ -135,11 +138,14 @@ class user_filtering { } } } - // Clear+reload the form. - $_POST = array(); - $this->_activeform = new user_active_filter_form($baseurl, array('fields' => $this->_fields, 'extraparams' => $extraparams)); } - // Now the active filters. + + // Rebuild the forms if filters data was processed. + if ($adddata || $activedata) { + $_POST = []; // Reset submitted data. + $this->_addform = new user_add_filter_form($baseurl, ['fields' => $this->_fields, 'extraparams' => $extraparams]); + $this->_activeform = new user_active_filter_form($baseurl, ['fields' => $this->_fields, 'extraparams' => $extraparams]); + } } /** diff --git a/user/filters/user_filter_forms.php b/user/filters/user_filter_forms.php index c08413598d5..b72ca1cab09 100644 --- a/user/filters/user_filter_forms.php +++ b/user/filters/user_filter_forms.php @@ -36,6 +36,8 @@ class user_add_filter_form extends moodleform { * Form definition. */ public function definition() { + global $SESSION; + $mform =& $this->_form; $fields = $this->_customdata['fields']; $extraparams = $this->_customdata['extraparams']; @@ -54,8 +56,15 @@ class user_add_filter_form extends moodleform { } } - // Add button. - $mform->addElement('submit', 'addfilter', get_string('addfilter', 'filters')); + // Add buttons. + $replacefiltersbutton = $mform->createElement('submit', 'replacefilters', get_string('replacefilters', 'filters')); + $addfilterbutton = $mform->createElement('submit', 'addfilter', get_string('addfilter', 'filters')); + $buttons = array_filter([ + empty($SESSION->user_filtering) ? null : $replacefiltersbutton, + $addfilterbutton, + ]); + + $mform->addGroup($buttons); } } From 4657c380a6593c320fd7116a0660c7d3a432ac6d Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Mon, 16 Nov 2020 15:20:34 +0100 Subject: [PATCH 02/26] MDL-70153 qtype_essay: Fix max size displayed for attachments --- question/type/essay/renderer.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/question/type/essay/renderer.php b/question/type/essay/renderer.php index a0cb56969eb..da1303552c7 100644 --- a/question/type/essay/renderer.php +++ b/question/type/essay/renderer.php @@ -106,7 +106,7 @@ class qtype_essay_renderer extends qtype_renderer { */ public function files_input(question_attempt $qa, $numallowed, question_display_options $options) { - global $CFG; + global $CFG, $COURSE; require_once($CFG->dirroot . '/lib/form/filemanager.php'); $pickeroptions = new stdClass(); @@ -122,7 +122,8 @@ class qtype_essay_renderer extends qtype_renderer { $pickeroptions->accepted_types = $qa->get_question()->filetypeslist; $fm = new form_filemanager($pickeroptions); - $fm->options->maxbytes = $qa->get_question()->maxbytes;; + $fm->options->maxbytes = get_user_max_upload_file_size( + $this->page->context, $CFG->maxbytes, $COURSE->maxbytes, $qa->get_question()->maxbytes); $filesrenderer = $this->page->get_renderer('core', 'files'); $text = ''; From 5047eac57eab1fef8e18942074c2f4893b7640d9 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Tue, 17 Nov 2020 10:31:32 +0100 Subject: [PATCH 03/26] MDL-70153 qtype_essay: Add behat test for attachments max size --- .../essay/tests/behat/max_file_size.feature | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/question/type/essay/tests/behat/max_file_size.feature b/question/type/essay/tests/behat/max_file_size.feature index 943bae11195..0ac3052fc9a 100644 --- a/question/type/essay/tests/behat/max_file_size.feature +++ b/question/type/essay/tests/behat/max_file_size.feature @@ -8,8 +8,8 @@ I need to choose the appropriate maxbytes for attachments | username | firstname | lastname | email | | teacher1 | T1 | Teacher1 | teacher1@moodle.com | And the following "courses" exist: - | fullname | shortname | category | - | Course 1 | C1 | 0 | + | fullname | shortname | category | maxbytes | + | Course 1 | C1 | 0 | 1048576 | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | @@ -18,15 +18,24 @@ I need to choose the appropriate maxbytes for attachments | Course | C1 | Test questions | And the following "questions" exist: | questioncategory | qtype | name | template | attachments | maxbytes | - | Test questions | essay | essay-1-20MB | editor | 1 | 20971520 | + | Test questions | essay | essay-1-512KB | editor | 1 | 524288 | + | Test questions | essay | essay-1-max | editor | 1 | 0 | Given I log in as "teacher1" And I am on "Course 1" course homepage And I navigate to "Question bank" in current page administration @javascript @_switch_window Scenario: Preview an Essay question and see the allowed maximum file sizes and number of attachments. - When I choose "Preview" action for "essay-1-20MB" in the question bank + When I choose "Preview" action for "essay-1-512KB" in the question bank And I switch to "questionpreview" window And I should see "Please write a story about a frog." - And I should see "Maximum file size: 20MB, maximum number of files: 1" + And I should see "Maximum file size: 512KB, maximum number of files: 1" + And I switch to the main window + + @javascript @_switch_window + Scenario: Preview an Essay question with Course upload limit and see the allowed maximum file size. + When I choose "Preview" action for "essay-1-max" in the question bank + And I switch to "questionpreview" window + And I should see "Please write a story about a frog." + And I should see "Maximum file size: 1MB, maximum number of files: 1" And I switch to the main window From 118c309a8718f4cc4d54440346bdaded989a8b6d Mon Sep 17 00:00:00 2001 From: Shamim Rezaie Date: Mon, 16 Nov 2020 21:37:45 +1100 Subject: [PATCH 04/26] MDL-70237 payment: Allow html tags in gateway description --- payment/classes/external/get_available_gateways.php | 2 +- payment/gateway/paypal/db/install.php | 2 ++ payment/templates/gateway.mustache | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/payment/classes/external/get_available_gateways.php b/payment/classes/external/get_available_gateways.php index b8a1abf0f66..b926a3fe73f 100644 --- a/payment/classes/external/get_available_gateways.php +++ b/payment/classes/external/get_available_gateways.php @@ -96,7 +96,7 @@ class get_available_gateways extends external_api { new external_single_structure([ 'shortname' => new external_value(PARAM_PLUGIN, 'Name of the plugin'), 'name' => new external_value(PARAM_TEXT, 'Human readable name of the gateway'), - 'description' => new external_value(PARAM_TEXT, 'description of the gateway'), + 'description' => new external_value(PARAM_RAW, 'description of the gateway'), 'surcharge' => new external_value(PARAM_INT, 'percentage of surcharge when using the gateway'), 'cost' => new external_value(PARAM_TEXT, 'Cost in human-readable form (amount plus surcharge with currency sign)'), diff --git a/payment/gateway/paypal/db/install.php b/payment/gateway/paypal/db/install.php index f9ca2d6eb8f..59db98939b4 100644 --- a/payment/gateway/paypal/db/install.php +++ b/payment/gateway/paypal/db/install.php @@ -23,6 +23,8 @@ */ function xmldb_paygw_paypal_install() { + global $CFG; + // Enable the Paypal payment gateway on installation. It still needs to be configured and enabled for accounts. $order = (!empty($CFG->paygw_plugins_sortorder)) ? explode(',', $CFG->paygw_plugins_sortorder) : []; set_config('paygw_plugins_sortorder', join(',', array_merge($order, ['paypal']))); diff --git a/payment/templates/gateway.mustache b/payment/templates/gateway.mustache index 4189f9ffef0..fed351fd5ae 100644 --- a/payment/templates/gateway.mustache +++ b/payment/templates/gateway.mustache @@ -45,7 +45,7 @@ \ No newline at end of file From b505b893fc04715a0895a12c77cdb7cbcfd1518d Mon Sep 17 00:00:00 2001 From: Peter Dias Date: Mon, 16 Nov 2020 23:11:17 +0800 Subject: [PATCH 05/26] MDL-63266 core: Final deprecation enrol/cli/sync.php --- enrol/database/cli/sync.php | 95 ------------------------------------- enrol/database/upgrade.txt | 3 ++ 2 files changed, 3 insertions(+), 95 deletions(-) delete mode 100644 enrol/database/cli/sync.php diff --git a/enrol/database/cli/sync.php b/enrol/database/cli/sync.php deleted file mode 100644 index 9a786911ad1..00000000000 --- a/enrol/database/cli/sync.php +++ /dev/null @@ -1,95 +0,0 @@ -. - -/** - * CLI sync for full external database synchronisation. - * - * Sample cron entry: - * # 5 minutes past 4am - * 5 4 * * * $sudo -u www-data /usr/bin/php /var/www/moodle/enrol/database/cli/sync.php - * - * Notes: - * - it is required to use the web server account when executing PHP CLI scripts - * - you need to change the "www-data" to match the apache user account - * - use "su" if "sudo" not available - * - * @deprecated since Moodle 3.7 MDL-59986 - please do not use this CLI script any more, use scheduled task instead. - * @todo MDL-63266 This will be deleted in Moodle 3.11. - * @package enrol_database - * @copyright 2010 Petr Skoda {@link http://skodak.org} - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -define('CLI_SCRIPT', true); - -require(__DIR__.'/../../../config.php'); -require_once("$CFG->libdir/clilib.php"); - -// Now get cli options. -list($options, $unrecognized) = cli_get_params(array('verbose'=>false, 'help'=>false), array('v'=>'verbose', 'h'=>'help')); - -if ($unrecognized) { - $unrecognized = implode("\n ", $unrecognized); - cli_error(get_string('cliunknowoption', 'admin', $unrecognized)); -} - -if ($options['help']) { - $help = -"Execute enrol sync with external database. -The enrol_database plugin must be enabled and properly configured. - -Options: --v, --verbose Print verbose progress information --h, --help Print out this help - -Example: -\$ sudo -u www-data /usr/bin/php enrol/database/cli/sync.php - -Sample cron entry: -# 5 minutes past 4am -5 4 * * * sudo -u www-data /usr/bin/php /var/www/moodle/enrol/database/cli/sync.php -"; - - echo $help; - die; -} - -cli_problem('[ENROL DATABASE] The sync enrolments cron script has been deprecated. Please use the scheduled task instead.'); - -// Abort execution of the CLI script if the enrol_database\task\sync_enrolments is enabled. -$task = \core\task\manager::get_scheduled_task('enrol_database\task\sync_enrolments'); -if (!$task->get_disabled()) { - cli_error('[ENROL DATABASE] The scheduled task sync_enrolments is enabled, the cron execution has been aborted.'); -} - -if (!enrol_is_enabled('database')) { - cli_error('enrol_database plugin is disabled, synchronisation stopped', 2); -} - -if (empty($options['verbose'])) { - $trace = new null_progress_trace(); -} else { - $trace = new text_progress_trace(); -} - -/** @var enrol_database_plugin $enrol */ -$enrol = enrol_get_plugin('database'); -$result = 0; - -$result = $result | $enrol->sync_courses($trace); -$result = $result | $enrol->sync_enrolments($trace); - -exit($result); diff --git a/enrol/database/upgrade.txt b/enrol/database/upgrade.txt index 53c0931a3c2..fb5d57c641e 100644 --- a/enrol/database/upgrade.txt +++ b/enrol/database/upgrade.txt @@ -1,5 +1,8 @@ This files describes API changes in the enrol_database code. +=== 3.11 === +* Final deprecation enrol/database/cli/sync.php. Refer below for substitute. + === 3.9 === * Class enrol_database_admin_setting_category has been removed. This class was only used by the database enrolment plugin settings and it was replaced by admin_settings_coursecat_select. From e339a551d2528a69e01cc71b2fc66e3bc8a294a7 Mon Sep 17 00:00:00 2001 From: Peter Dias Date: Thu, 12 Nov 2020 15:32:52 +0800 Subject: [PATCH 06/26] MDL-64776 book: Final deprecation booktool_print_get_toc --- mod/book/tool/print/locallib.php | 82 ++------------------------------ mod/book/upgrade.txt | 3 ++ 2 files changed, 6 insertions(+), 79 deletions(-) diff --git a/mod/book/tool/print/locallib.php b/mod/book/tool/print/locallib.php index 169caf3dc11..549de3d9c1e 100644 --- a/mod/book/tool/print/locallib.php +++ b/mod/book/tool/print/locallib.php @@ -28,85 +28,9 @@ require_once(__DIR__.'/lib.php'); require_once($CFG->dirroot.'/mod/book/locallib.php'); /** - * Generate toc structure and titles - * * @deprecated since Moodle 3.7 - * @param array $chapters - * @param stdClass $book - * @param stdClass $cm - * @return array */ -function booktool_print_get_toc($chapters, $book, $cm) { - debugging('booktool_print_get_toc() is deprecated. Please use booktool_print renderer - function render_print_book_toc().', DEBUG_DEVELOPER); - - $first = true; - $titles = array(); - - $context = context_module::instance($cm->id); - - $toc = ''; // Representation of toc (HTML). - - switch ($book->numbering) { - case BOOK_NUM_NONE: - $toc .= html_writer::start_tag('div', array('class' => 'book_toc_none')); - break; - case BOOK_NUM_NUMBERS: - $toc .= html_writer::start_tag('div', array('class' => 'book_toc_numbered')); - break; - case BOOK_NUM_BULLETS: - $toc .= html_writer::start_tag('div', array('class' => 'book_toc_bullets')); - break; - case BOOK_NUM_INDENTED: - $toc .= html_writer::start_tag('div', array('class' => 'book_toc_indented')); - break; - } - - $toc .= html_writer::tag('a', '', array('name' => 'toc')); // Representation of toc (HTML). - - $toc .= html_writer::tag('h2', get_string('toc', 'mod_book')); - $toc .= html_writer::start_tag('ul'); - foreach ($chapters as $ch) { - if (!$ch->hidden) { - $title = book_get_chapter_title($ch->id, $chapters, $book, $context); - if (!$ch->subchapter) { - - if ($first) { - $toc .= html_writer::start_tag('li'); - } else { - $toc .= html_writer::end_tag('ul'); - $toc .= html_writer::end_tag('li'); - $toc .= html_writer::start_tag('li'); - } - - } else { - - if ($first) { - $toc .= html_writer::start_tag('li'); - $toc .= html_writer::start_tag('ul'); - $toc .= html_writer::start_tag('li'); - } else { - $toc .= html_writer::start_tag('li'); - } - - } - $titles[$ch->id] = $title; - $toc .= html_writer::link(new moodle_url('#ch'.$ch->id), $title, array('title' => s($title))); - if (!$ch->subchapter) { - $toc .= html_writer::start_tag('ul'); - } else { - $toc .= html_writer::end_tag('li'); - } - $first = false; - } - } - - $toc .= html_writer::end_tag('ul'); - $toc .= html_writer::end_tag('li'); - $toc .= html_writer::end_tag('ul'); - $toc .= html_writer::end_tag('div'); - - $toc = str_replace('
    ', '', $toc); // Cleanup of invalid structures. - - return array($toc, $titles); +function booktool_print_get_toc() { + throw new coding_exception(__FUNCTION__ . ' can not be used any more. Please use booktool_print renderer + function render_print_book_toc().'); } diff --git a/mod/book/upgrade.txt b/mod/book/upgrade.txt index f58e5dc285c..1e563ca3e5c 100644 --- a/mod/book/upgrade.txt +++ b/mod/book/upgrade.txt @@ -1,5 +1,8 @@ This files describes API changes in the book code. +=== 3.11 === +* Final deprecation - booktool_print_get_toc(). Please use render_print_book_toc() instead. + === 3.8 === * The following functions have been finally deprecated and can not be used anymore: From d921cbb41f4b29af7250f38f1821b743ccad5fd2 Mon Sep 17 00:00:00 2001 From: Huong Nguyen Date: Wed, 18 Nov 2020 16:09:05 +0700 Subject: [PATCH 07/26] MDL-70248 qtype_ddimageortext: Drop zones have UI issue in Editing form --- question/type/ddimageortext/amd/build/form.min.js | 2 +- question/type/ddimageortext/amd/build/form.min.js.map | 2 +- question/type/ddimageortext/amd/src/form.js | 7 +++---- question/type/ddimageortext/styles.css | 1 + 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/question/type/ddimageortext/amd/build/form.min.js b/question/type/ddimageortext/amd/build/form.min.js index f963ee65b66..4606e6813c5 100644 --- a/question/type/ddimageortext/amd/build/form.min.js +++ b/question/type/ddimageortext/amd/build/form.min.js @@ -1,2 +1,2 @@ -define ("qtype_ddimageortext/form",["jquery","core/dragdrop"],function(a,b){"use strict";var c={maxBgImageSize:null,maxDragImageSize:null,fp:null,init:function init(){c.fp=c.filePickers();a("#id_previewareaheader").append("
    ");c.updateVisibilityOfFilePickers();c.setOptionsForDragItemSelectors();c.setupEventHandlers();c.waitForFilePickerToInitialise()},waitForFilePickerToInitialise:function waitForFilePickerToInitialise(){if(null===c.fp.file("bgimage").href){setTimeout(c.waitForFilePickerToInitialise,1e3);return}M.util.js_pending("dragDropToImageForm");a("form.mform[data-qtype=\"ddimageortext\"]").on("change",".filepickerhidden",function(){M.util.js_pending("dragDropToImageForm");c.loadPreviewImage()});c.loadPreviewImage()},loadPreviewImage:function loadPreviewImage(){a("fieldset#id_previewareaheader .dropbackground").one("load",c.afterPreviewImageLoaded).attr("src",c.fp.file("bgimage").href)},afterPreviewImageLoaded:function afterPreviewImageLoaded(){c.createDropZones();M.util.js_complete("dragDropToImageForm")},createDropZones:function createDropZones(){var b=a(".dropzones");b.empty();var d=c.fp.file("bgimage").href;if(null===d){return}for(var e=c.form.getFormValue("nodropzone",[]),f=0,g;f")}else if(""!==i){b.append("
    "+i+"
    ")}}c.waitForAllDropImagesToBeLoaded()},waitForAllDropImagesToBeLoaded:function waitForAllDropImagesToBeLoaded(){var b=a(".dropzones img").not(function(a,b){return c.imageIsLoaded(b)});if(0"+b[l]+"");var m=j.find("option[value=\""+l+"\"]");if(parseInt(l)===parseInt(k)){m.attr("selected",!0)}else if(c.isItemUsed(parseInt(l))){m.attr("disabled",!0)}}}},isItemUsed:function isItemUsed(b){if(0===b){return!1}if(c.form.getFormValue("drags",[b-1,"infinite"])){return!1}return 0!==a("fieldset#id_dropzoneheader select").filter(function(c,d){return parseInt(a(d).val())===b}).length},dragStart:function dragStart(d){var e=a(d.target).closest(".droppreview"),f=b.prepare(d);if(!f.start){return}b.start(d,e,function(a,b,d){c.dragMove(d)},function(){c.dragEnd()})},dragMove:function dragMove(b){var d=a("fieldset#id_previewareaheader .dropbackground"),e=d.offset(),f=b.data("dropNo"),g=b.offset(),h=Math.round(g.left-e.left),i=Math.round(g.top-e.top);h=Math.max(0,Math.min(h,d.width()-b.width()-10));i=Math.max(0,Math.min(i,d.height()-b.height()-10));c.form.setFormValue("drops",[f,"xleft"],h);c.form.setFormValue("drops",[f,"ytop"],i)},dragEnd:function dragEnd(){c.updateDropZones()},form:{toNameWithIndex:function toNameWithIndex(a,b){for(var c=a,d=0;d
    ");c.updateVisibilityOfFilePickers();c.setOptionsForDragItemSelectors();c.setupEventHandlers();c.waitForFilePickerToInitialise()},waitForFilePickerToInitialise:function waitForFilePickerToInitialise(){if(null===c.fp.file("bgimage").href){setTimeout(c.waitForFilePickerToInitialise,1e3);return}M.util.js_pending("dragDropToImageForm");a("form.mform[data-qtype=\"ddimageortext\"]").on("change",".filepickerhidden",function(){M.util.js_pending("dragDropToImageForm");c.loadPreviewImage()});c.loadPreviewImage()},loadPreviewImage:function loadPreviewImage(){a("fieldset#id_previewareaheader .dropbackground").one("load",c.afterPreviewImageLoaded).attr("src",c.fp.file("bgimage").href)},afterPreviewImageLoaded:function afterPreviewImageLoaded(){c.createDropZones();M.util.js_complete("dragDropToImageForm")},createDropZones:function createDropZones(){var b=a(".dropzones");b.empty();var d=c.fp.file("bgimage").href;if(null===d){return}for(var e=c.form.getFormValue("nodropzone",[]),f=0,g;f")}else if(""!==i){b.append("
    "+i+"
    ")}}c.waitForAllDropImagesToBeLoaded()},waitForAllDropImagesToBeLoaded:function waitForAllDropImagesToBeLoaded(){var b=a(".dropzones img").not(function(a,b){return c.imageIsLoaded(b)});if(0"+b[l]+"");var m=j.find("option[value=\""+l+"\"]");if(parseInt(l)===parseInt(k)){m.attr("selected",!0)}else if(c.isItemUsed(parseInt(l))){m.attr("disabled",!0)}}}},isItemUsed:function isItemUsed(b){if(0===b){return!1}if(c.form.getFormValue("drags",[b-1,"infinite"])){return!1}return 0!==a("fieldset#id_dropzoneheader select").filter(function(c,d){return parseInt(a(d).val())===b}).length},dragStart:function dragStart(d){var e=a(d.target).closest(".droppreview"),f=b.prepare(d);if(!f.start){return}b.start(d,e,function(a,b,d){c.dragMove(d)},function(){c.dragEnd()})},dragMove:function dragMove(b){var d=a("fieldset#id_previewareaheader .dropbackground"),e=d.offset(),f=b.data("dropNo"),g=b.offset(),h=Math.round(g.left-e.left),i=Math.round(g.top-e.top);h=Math.round(Math.max(0,Math.min(h,d.outerWidth()-b.outerWidth())));i=Math.round(Math.max(0,Math.min(i,d.outerHeight()-b.outerHeight())));c.form.setFormValue("drops",[f,"xleft"],h);c.form.setFormValue("drops",[f,"ytop"],i)},dragEnd:function dragEnd(){c.updateDropZones()},form:{toNameWithIndex:function toNameWithIndex(a,b){for(var c=a,d=0;d.\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/form\n * @package qtype_ddimageortext\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/dragdrop'], function($, dragDrop) {\n\n \"use strict\";\n\n /**\n * Singleton object to handle progressive enhancement of the\n * drag-drop onto image question editing form.\n * @type {Object}\n */\n var dragDropToImageForm = {\n /**\n * @var {Object} with properties width and height.\n */\n maxBgImageSize: null,\n\n /**\n * @var {Object} with properties width and height.\n */\n maxDragImageSize: null,\n\n /**\n * @var {object} for interacting with the file pickers.\n */\n fp: null, // Object containing functions associated with the file picker.\n\n /**\n * Initialise the form javascript features.\n */\n init: function() {\n dragDropToImageForm.fp = dragDropToImageForm.filePickers();\n\n $('#id_previewareaheader').append(\n '
    ' +\n '
    ' +\n ' ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ');\n\n dragDropToImageForm.updateVisibilityOfFilePickers();\n dragDropToImageForm.setOptionsForDragItemSelectors();\n dragDropToImageForm.setupEventHandlers();\n dragDropToImageForm.waitForFilePickerToInitialise();\n },\n\n /**\n * Waits for the file-pickers to be sufficiently ready before initialising the preview.\n */\n waitForFilePickerToInitialise: function() {\n if (dragDropToImageForm.fp.file('bgimage').href === null) {\n // It would be better to use an onload or onchange event rather than this timeout.\n // Unfortunately attempts to do this early are overwritten by filepicker during its loading.\n setTimeout(dragDropToImageForm.waitForFilePickerToInitialise, 1000);\n return;\n }\n M.util.js_pending('dragDropToImageForm');\n\n // From now on, when a new file gets loaded into the filepicker, update the preview.\n // This is not in the setupEventHandlers section as it needs to be delayed until\n // after filepicker's javascript has finished.\n $('form.mform[data-qtype=\"ddimageortext\"]').on('change', '.filepickerhidden', function() {\n M.util.js_pending('dragDropToImageForm');\n dragDropToImageForm.loadPreviewImage();\n });\n\n dragDropToImageForm.loadPreviewImage();\n },\n\n /**\n * Loads the preview background image.\n */\n loadPreviewImage: function() {\n $('fieldset#id_previewareaheader .dropbackground')\n .one('load', dragDropToImageForm.afterPreviewImageLoaded)\n .attr('src', dragDropToImageForm.fp.file('bgimage').href);\n },\n\n /**\n * After the background image is loaded, continue setting up the preview.\n */\n afterPreviewImageLoaded: function() {\n dragDropToImageForm.createDropZones();\n M.util.js_complete('dragDropToImageForm');\n },\n\n /**\n * Create, or recreate all the drop zones.\n */\n createDropZones: function() {\n var dropZoneHolder = $('.dropzones');\n dropZoneHolder.empty();\n\n var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;\n if (bgimageurl === null) {\n return; // There is not currently a valid preview to update.\n }\n\n var numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']);\n if (dragNo === '0') {\n continue;\n }\n dragNo = dragNo - 1;\n var group = dragDropToImageForm.form.getFormValue('drags', [dragNo, 'draggroup']),\n label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {\n var imgUrl = dragDropToImageForm.fp.file('dragitem[' + dragNo + ']').href;\n if (imgUrl === null) {\n continue;\n }\n // Althoug these are previews of drops, we also add the class name 'drag',\n dropZoneHolder.append('\"'');\n\n } else if (label !== '') {\n dropZoneHolder.append('
    ' + label + '
    ');\n }\n }\n\n dragDropToImageForm.waitForAllDropImagesToBeLoaded();\n },\n\n /**\n * This polls until all the drop-zone images have loaded, and then calls updateDropZones().\n */\n waitForAllDropImagesToBeLoaded: function() {\n var notYetLoadedImages = $('.dropzones img').not(function(i, imgNode) {\n return dragDropToImageForm.imageIsLoaded(imgNode);\n });\n\n if (notYetLoadedImages.length > 0) {\n setTimeout(function() {\n dragDropToImageForm.waitForAllDropImagesToBeLoaded();\n }, 100);\n return;\n }\n\n dragDropToImageForm.updateDropZones();\n },\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n imageIsLoaded: function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n },\n\n /**\n * Set the size and position of all the drop zones.\n */\n updateDropZones: function() {\n var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;\n if (bgimageurl === null) {\n return; // There is not currently a valid preview to update.\n }\n\n var dropBackgroundPosition = $('fieldset#id_previewareaheader .dropbackground').offset(),\n numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n\n // Move each drop to the right position and update the text.\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var drop = $('.dropzones .drop' + dropNo);\n if (drop.length === 0) {\n continue;\n }\n var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']) - 1;\n\n drop.offset({\n left: dropBackgroundPosition.left +\n parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'xleft'])),\n top: dropBackgroundPosition.top +\n parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'ytop']))\n });\n\n var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n if (drop.is('img')) {\n drop.attr('alt', label);\n } else {\n drop.html(label);\n }\n }\n\n // Resize them to the same size.\n $('.dropzones .droppreview').css('padding', '0');\n var numGroups = $('select.draggroup').first().find('option').length;\n for (var group = 1; group <= numGroups; group++) {\n dragDropToImageForm.resizeAllDragsAndDropsInGroup(group);\n }\n },\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n resizeAllDragsAndDropsInGroup: function(group) {\n var drops = $('.dropzones .droppreview.group' + group),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n drops.each(function(i, drop) {\n maxWidth = Math.max(maxWidth, Math.ceil(drop.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drop.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n drops.each(function(i, drop) {\n var left = Math.round((maxWidth - drop.offsetWidth) / 2),\n top = Math.floor((maxHeight - drop.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drop).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drop.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drop.offsetHeight - top) + 'px'\n });\n });\n },\n\n /**\n * Events linked to form actions.\n */\n setupEventHandlers: function() {\n // Changes to settings in the draggable items section.\n $('fieldset#id_draggableitemheader')\n .on('change input', 'input, select', function(e) {\n var input = $(e.target).closest('select, input');\n if (input.hasClass('dragitemtype')) {\n dragDropToImageForm.updateVisibilityOfFilePickers();\n }\n\n dragDropToImageForm.setOptionsForDragItemSelectors();\n\n if (input.is('.dragitemtype, .draggroup')) {\n dragDropToImageForm.createDropZones();\n } else if (input.is('.draglabel')) {\n dragDropToImageForm.updateDropZones();\n }\n });\n\n // Changes to Drop zones section: left, top and drag item.\n $('fieldset#id_dropzoneheader').on('change input', 'input, select', function(e) {\n var input = $(e.target).closest('select, input');\n if (input.is('select')) {\n dragDropToImageForm.createDropZones();\n } else {\n dragDropToImageForm.updateDropZones();\n }\n });\n\n // Moving drop zones in the preview.\n $('fieldset#id_previewareaheader').on('mousedown touchstart', '.droppreview', function(e) {\n dragDropToImageForm.dragStart(e);\n });\n\n $(window).on('resize', function() {\n dragDropToImageForm.updateDropZones();\n });\n },\n\n /**\n * Update all the drag item filepickers, so they are only shown for\n */\n updateVisibilityOfFilePickers: function() {\n var numDrags = dragDropToImageForm.form.getFormValue('noitems', []);\n for (var dragNo = 0; dragNo < numDrags; dragNo++) {\n var picker = $('input#id_dragitem_' + dragNo).closest('.fitem_ffilepicker');\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {\n picker.show();\n } else {\n picker.hide();\n }\n }\n },\n\n\n setOptionsForDragItemSelectors: function() {\n var dragItemOptions = {'0': ''},\n numDrags = dragDropToImageForm.form.getFormValue('noitems', []),\n numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n\n // Work out the list of options.\n for (var dragNo = 0; dragNo < numDrags; dragNo++) {\n var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n var file = dragDropToImageForm.fp.file(dragDropToImageForm.form.toNameWithIndex('dragitem', [dragNo]));\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype']) && file.name !== null) {\n dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label + ' (' + file.name + ')';\n } else if (label !== '') {\n dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label;\n }\n }\n\n // Initialise each select.\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var selector = $('#id_drops_' + dropNo + '_choice');\n\n var selectedvalue = selector.val();\n selector.find('option').remove();\n for (var value in dragItemOptions) {\n if (!dragItemOptions.hasOwnProperty(value)) {\n continue;\n }\n selector.append('');\n var optionnode = selector.find('option[value=\"' + value + '\"]');\n if (parseInt(value) === parseInt(selectedvalue)) {\n optionnode.attr('selected', true);\n } else if (dragDropToImageForm.isItemUsed(parseInt(value))) {\n optionnode.attr('disabled', true);\n }\n }\n }\n },\n\n /**\n * Checks if the specified drag option is already used somewhere.\n *\n * @param {Number} value of the drag item to check\n * @return {Boolean} true if item is allocated to dropzone\n */\n isItemUsed: function(value) {\n if (value === 0) {\n return false; // None option can always be selected.\n }\n\n if (dragDropToImageForm.form.getFormValue('drags', [value - 1, 'infinite'])) {\n return false; // Infinite, so can't be used up.\n }\n\n return $('fieldset#id_dropzoneheader select').filter(function(i, selectNode) {\n return parseInt($(selectNode).val()) === value;\n }).length !== 0;\n },\n\n /**\n * Handles when a dropzone in dragged in the preview.\n * @param {Object} e Event object\n */\n dragStart: function(e) {\n var drop = $(e.target).closest('.droppreview');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragDrop.start(e, drop, function(x, y, drop) {\n dragDropToImageForm.dragMove(drop);\n }, function() {\n dragDropToImageForm.dragEnd();\n });\n },\n\n /**\n * Handles update while a drop is being dragged.\n *\n * @param {jQuery} drop the drop preview being moved.\n */\n dragMove: function(drop) {\n var backgroundImage = $('fieldset#id_previewareaheader .dropbackground'),\n backgroundPosition = backgroundImage.offset(),\n dropNo = drop.data('dropNo'),\n dropPosition = drop.offset(),\n left = Math.round(dropPosition.left - backgroundPosition.left),\n top = Math.round(dropPosition.top - backgroundPosition.top);\n\n // Constrain coordinates to be inside the background.\n // The -10 here matches the +10 in resizeAllDragsAndDropsInGroup().\n left = Math.max(0, Math.min(left, backgroundImage.width() - drop.width() - 10));\n top = Math.max(0, Math.min(top, backgroundImage.height() - drop.height() - 10));\n\n // Update the form.\n dragDropToImageForm.form.setFormValue('drops', [dropNo, 'xleft'], left);\n dragDropToImageForm.form.setFormValue('drops', [dropNo, 'ytop'], top);\n },\n\n /**\n * Handles when the drag ends.\n */\n dragEnd: function() {\n // Redraw, in case the position was constrained.\n dragDropToImageForm.updateDropZones();\n },\n\n /**\n * Low level operations on form.\n */\n form: {\n toNameWithIndex: function(name, indexes) {\n var indexString = name;\n for (var i = 0; i < indexes.length; i++) {\n indexString = indexString + '[' + indexes[i] + ']';\n }\n return indexString;\n },\n\n getEl: function(name, indexes) {\n var form = $('form.mform[data-qtype=\"ddimageortext\"]')[0];\n return form.elements[this.toNameWithIndex(name, indexes)];\n },\n\n /**\n * Helper to get the value of a form elements with name like \"drops[0][xleft]\".\n *\n * @param {String} name the base name, e.g. 'drops'.\n * @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].\n * @return {String} the value of that field.\n */\n getFormValue: function(name, indexes) {\n var el = this.getEl(name, indexes);\n if (!el.type) {\n el = el[el.length - 1];\n }\n if (el.type === 'checkbox') {\n return el.checked;\n } else {\n return el.value;\n }\n },\n\n /**\n * Helper to get the value of a form elements with name like \"drops[0][xleft]\".\n *\n * @param {String} name the base name, e.g. 'drops'.\n * @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].\n * @param {String|Number} value the value to set.\n */\n setFormValue: function(name, indexes, value) {\n var el = this.getEl(name, indexes);\n if (el.type === 'checkbox') {\n el.checked = value;\n } else {\n el.value = value;\n }\n }\n },\n\n /**\n * Utility to get the file name and url from the filepicker.\n * @returns {Object} object containing functions {file, name}\n */\n filePickers: function() {\n var draftItemIdsToName;\n var nameToParentNode;\n\n if (draftItemIdsToName === undefined) {\n draftItemIdsToName = {};\n nameToParentNode = {};\n var fp = $('form.mform[data-qtype=\"ddimageortext\"] input.filepickerhidden');\n fp.each(function(index, filepicker) {\n draftItemIdsToName[filepicker.value] = filepicker.name;\n nameToParentNode[filepicker.name] = filepicker.parentNode;\n });\n }\n\n return {\n file: function(name) {\n var parentNode = $(nameToParentNode[name]);\n var fileAnchor = parentNode.find('div.filepicker-filelist a');\n if (fileAnchor.length) {\n return {href: fileAnchor.get(0).href, name: fileAnchor.get(0).innerHTML};\n } else {\n return {href: null, name: null};\n }\n },\n\n name: function(draftitemid) {\n return draftItemIdsToName[draftitemid];\n }\n };\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/form\n */\n return {\n /**\n * Initialise the form JavaScript features.\n */\n init: dragDropToImageForm.init\n };\n});\n"],"file":"form.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/form.js"],"names":["define","$","dragDrop","dragDropToImageForm","maxBgImageSize","maxDragImageSize","fp","init","filePickers","append","updateVisibilityOfFilePickers","setOptionsForDragItemSelectors","setupEventHandlers","waitForFilePickerToInitialise","file","href","setTimeout","M","util","js_pending","on","loadPreviewImage","one","afterPreviewImageLoaded","attr","createDropZones","js_complete","dropZoneHolder","empty","bgimageurl","numDrops","form","getFormValue","dropNo","dragNo","group","label","imgUrl","waitForAllDropImagesToBeLoaded","notYetLoadedImages","not","i","imgNode","imageIsLoaded","length","updateDropZones","imgElement","complete","naturalHeight","dropBackgroundPosition","offset","drop","left","parseInt","top","is","html","css","numGroups","first","find","resizeAllDragsAndDropsInGroup","drops","maxWidth","maxHeight","each","Math","max","ceil","offsetWidth","offsetHeight","round","floor","e","input","target","closest","hasClass","dragStart","window","numDrags","picker","show","hide","dragItemOptions","toNameWithIndex","name","selector","selectedvalue","val","remove","value","hasOwnProperty","optionnode","isItemUsed","filter","selectNode","info","prepare","start","x","y","dragMove","dragEnd","backgroundImage","backgroundPosition","data","dropPosition","min","outerWidth","outerHeight","setFormValue","indexes","indexString","getEl","elements","el","type","checked","draftItemIdsToName","nameToParentNode","index","filepicker","parentNode","fileAnchor","get","innerHTML","draftitemid"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/*\n * JavaScript to allow dragging options to slots (using mouse down or touch) or tab through slots using keyboard.\n *\n * @module qtype_ddimageortext/form\n * @package qtype_ddimageortext\n * @copyright 2018 The Open University\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(['jquery', 'core/dragdrop'], function($, dragDrop) {\n\n \"use strict\";\n\n /**\n * Singleton object to handle progressive enhancement of the\n * drag-drop onto image question editing form.\n * @type {Object}\n */\n var dragDropToImageForm = {\n /**\n * @var {Object} with properties width and height.\n */\n maxBgImageSize: null,\n\n /**\n * @var {Object} with properties width and height.\n */\n maxDragImageSize: null,\n\n /**\n * @var {object} for interacting with the file pickers.\n */\n fp: null, // Object containing functions associated with the file picker.\n\n /**\n * Initialise the form javascript features.\n */\n init: function() {\n dragDropToImageForm.fp = dragDropToImageForm.filePickers();\n\n $('#id_previewareaheader').append(\n '
    ' +\n '
    ' +\n ' ' +\n '
    ' +\n '
    ' +\n '
    ' +\n '
    ');\n\n dragDropToImageForm.updateVisibilityOfFilePickers();\n dragDropToImageForm.setOptionsForDragItemSelectors();\n dragDropToImageForm.setupEventHandlers();\n dragDropToImageForm.waitForFilePickerToInitialise();\n },\n\n /**\n * Waits for the file-pickers to be sufficiently ready before initialising the preview.\n */\n waitForFilePickerToInitialise: function() {\n if (dragDropToImageForm.fp.file('bgimage').href === null) {\n // It would be better to use an onload or onchange event rather than this timeout.\n // Unfortunately attempts to do this early are overwritten by filepicker during its loading.\n setTimeout(dragDropToImageForm.waitForFilePickerToInitialise, 1000);\n return;\n }\n M.util.js_pending('dragDropToImageForm');\n\n // From now on, when a new file gets loaded into the filepicker, update the preview.\n // This is not in the setupEventHandlers section as it needs to be delayed until\n // after filepicker's javascript has finished.\n $('form.mform[data-qtype=\"ddimageortext\"]').on('change', '.filepickerhidden', function() {\n M.util.js_pending('dragDropToImageForm');\n dragDropToImageForm.loadPreviewImage();\n });\n\n dragDropToImageForm.loadPreviewImage();\n },\n\n /**\n * Loads the preview background image.\n */\n loadPreviewImage: function() {\n $('fieldset#id_previewareaheader .dropbackground')\n .one('load', dragDropToImageForm.afterPreviewImageLoaded)\n .attr('src', dragDropToImageForm.fp.file('bgimage').href);\n },\n\n /**\n * After the background image is loaded, continue setting up the preview.\n */\n afterPreviewImageLoaded: function() {\n dragDropToImageForm.createDropZones();\n M.util.js_complete('dragDropToImageForm');\n },\n\n /**\n * Create, or recreate all the drop zones.\n */\n createDropZones: function() {\n var dropZoneHolder = $('.dropzones');\n dropZoneHolder.empty();\n\n var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;\n if (bgimageurl === null) {\n return; // There is not currently a valid preview to update.\n }\n\n var numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']);\n if (dragNo === '0') {\n continue;\n }\n dragNo = dragNo - 1;\n var group = dragDropToImageForm.form.getFormValue('drags', [dragNo, 'draggroup']),\n label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {\n var imgUrl = dragDropToImageForm.fp.file('dragitem[' + dragNo + ']').href;\n if (imgUrl === null) {\n continue;\n }\n // Althoug these are previews of drops, we also add the class name 'drag',\n dropZoneHolder.append('\"'');\n\n } else if (label !== '') {\n dropZoneHolder.append('
    ' + label + '
    ');\n }\n }\n\n dragDropToImageForm.waitForAllDropImagesToBeLoaded();\n },\n\n /**\n * This polls until all the drop-zone images have loaded, and then calls updateDropZones().\n */\n waitForAllDropImagesToBeLoaded: function() {\n var notYetLoadedImages = $('.dropzones img').not(function(i, imgNode) {\n return dragDropToImageForm.imageIsLoaded(imgNode);\n });\n\n if (notYetLoadedImages.length > 0) {\n setTimeout(function() {\n dragDropToImageForm.waitForAllDropImagesToBeLoaded();\n }, 100);\n return;\n }\n\n dragDropToImageForm.updateDropZones();\n },\n\n /**\n * Check if an image has loaded without errors.\n *\n * @param {HTMLImageElement} imgElement an image.\n * @returns {boolean} true if this image has loaded without errors.\n */\n imageIsLoaded: function(imgElement) {\n return imgElement.complete && imgElement.naturalHeight !== 0;\n },\n\n /**\n * Set the size and position of all the drop zones.\n */\n updateDropZones: function() {\n var bgimageurl = dragDropToImageForm.fp.file('bgimage').href;\n if (bgimageurl === null) {\n return; // There is not currently a valid preview to update.\n }\n\n var dropBackgroundPosition = $('fieldset#id_previewareaheader .dropbackground').offset(),\n numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n\n // Move each drop to the right position and update the text.\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var drop = $('.dropzones .drop' + dropNo);\n if (drop.length === 0) {\n continue;\n }\n var dragNo = dragDropToImageForm.form.getFormValue('drops', [dropNo, 'choice']) - 1;\n\n drop.offset({\n left: dropBackgroundPosition.left +\n parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'xleft'])),\n top: dropBackgroundPosition.top +\n parseInt(dragDropToImageForm.form.getFormValue('drops', [dropNo, 'ytop']))\n });\n\n var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n if (drop.is('img')) {\n drop.attr('alt', label);\n } else {\n drop.html(label);\n }\n }\n\n // Resize them to the same size.\n $('.dropzones .droppreview').css('padding', '0');\n var numGroups = $('.draggroup select').first().find('option').length;\n for (var group = 1; group <= numGroups; group++) {\n dragDropToImageForm.resizeAllDragsAndDropsInGroup(group);\n }\n },\n\n /**\n * In a given group, set all the drags and drops to be the same size.\n *\n * @param {int} group the group number.\n */\n resizeAllDragsAndDropsInGroup: function(group) {\n var drops = $('.dropzones .droppreview.group' + group),\n maxWidth = 0,\n maxHeight = 0;\n\n // Find the maximum size of any drag in this groups.\n drops.each(function(i, drop) {\n maxWidth = Math.max(maxWidth, Math.ceil(drop.offsetWidth));\n maxHeight = Math.max(maxHeight, Math.ceil(drop.offsetHeight));\n });\n\n // The size we will want to set is a bit bigger than this.\n maxWidth += 10;\n maxHeight += 10;\n\n // Set each drag home to that size.\n drops.each(function(i, drop) {\n var left = Math.round((maxWidth - drop.offsetWidth) / 2),\n top = Math.floor((maxHeight - drop.offsetHeight) / 2);\n // Set top and left padding so the item is centred.\n $(drop).css({\n 'padding-left': left + 'px',\n 'padding-right': (maxWidth - drop.offsetWidth - left) + 'px',\n 'padding-top': top + 'px',\n 'padding-bottom': (maxHeight - drop.offsetHeight - top) + 'px'\n });\n });\n },\n\n /**\n * Events linked to form actions.\n */\n setupEventHandlers: function() {\n // Changes to settings in the draggable items section.\n $('fieldset#id_draggableitemheader')\n .on('change input', 'input, select', function(e) {\n var input = $(e.target).closest('select, input');\n if (input.hasClass('dragitemtype')) {\n dragDropToImageForm.updateVisibilityOfFilePickers();\n }\n\n dragDropToImageForm.setOptionsForDragItemSelectors();\n\n if (input.is('.dragitemtype, .draggroup')) {\n dragDropToImageForm.createDropZones();\n } else if (input.is('.draglabel')) {\n dragDropToImageForm.updateDropZones();\n }\n });\n\n // Changes to Drop zones section: left, top and drag item.\n $('fieldset#id_dropzoneheader').on('change input', 'input, select', function(e) {\n var input = $(e.target).closest('select, input');\n if (input.is('select')) {\n dragDropToImageForm.createDropZones();\n } else {\n dragDropToImageForm.updateDropZones();\n }\n });\n\n // Moving drop zones in the preview.\n $('fieldset#id_previewareaheader').on('mousedown touchstart', '.droppreview', function(e) {\n dragDropToImageForm.dragStart(e);\n });\n\n $(window).on('resize', function() {\n dragDropToImageForm.updateDropZones();\n });\n },\n\n /**\n * Update all the drag item filepickers, so they are only shown for\n */\n updateVisibilityOfFilePickers: function() {\n var numDrags = dragDropToImageForm.form.getFormValue('noitems', []);\n for (var dragNo = 0; dragNo < numDrags; dragNo++) {\n var picker = $('input#id_dragitem_' + dragNo).closest('.fitem_ffilepicker');\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype'])) {\n picker.show();\n } else {\n picker.hide();\n }\n }\n },\n\n\n setOptionsForDragItemSelectors: function() {\n var dragItemOptions = {'0': ''},\n numDrags = dragDropToImageForm.form.getFormValue('noitems', []),\n numDrops = dragDropToImageForm.form.getFormValue('nodropzone', []);\n\n // Work out the list of options.\n for (var dragNo = 0; dragNo < numDrags; dragNo++) {\n var label = dragDropToImageForm.form.getFormValue('draglabel', [dragNo]);\n var file = dragDropToImageForm.fp.file(dragDropToImageForm.form.toNameWithIndex('dragitem', [dragNo]));\n if ('image' === dragDropToImageForm.form.getFormValue('drags', [dragNo, 'dragitemtype']) && file.name !== null) {\n dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label + ' (' + file.name + ')';\n } else if (label !== '') {\n dragItemOptions[dragNo + 1] = (dragNo + 1) + '. ' + label;\n }\n }\n\n // Initialise each select.\n for (var dropNo = 0; dropNo < numDrops; dropNo++) {\n var selector = $('#id_drops_' + dropNo + '_choice');\n\n var selectedvalue = selector.val();\n selector.find('option').remove();\n for (var value in dragItemOptions) {\n if (!dragItemOptions.hasOwnProperty(value)) {\n continue;\n }\n selector.append('');\n var optionnode = selector.find('option[value=\"' + value + '\"]');\n if (parseInt(value) === parseInt(selectedvalue)) {\n optionnode.attr('selected', true);\n } else if (dragDropToImageForm.isItemUsed(parseInt(value))) {\n optionnode.attr('disabled', true);\n }\n }\n }\n },\n\n /**\n * Checks if the specified drag option is already used somewhere.\n *\n * @param {Number} value of the drag item to check\n * @return {Boolean} true if item is allocated to dropzone\n */\n isItemUsed: function(value) {\n if (value === 0) {\n return false; // None option can always be selected.\n }\n\n if (dragDropToImageForm.form.getFormValue('drags', [value - 1, 'infinite'])) {\n return false; // Infinite, so can't be used up.\n }\n\n return $('fieldset#id_dropzoneheader select').filter(function(i, selectNode) {\n return parseInt($(selectNode).val()) === value;\n }).length !== 0;\n },\n\n /**\n * Handles when a dropzone in dragged in the preview.\n * @param {Object} e Event object\n */\n dragStart: function(e) {\n var drop = $(e.target).closest('.droppreview');\n\n var info = dragDrop.prepare(e);\n if (!info.start) {\n return;\n }\n\n dragDrop.start(e, drop, function(x, y, drop) {\n dragDropToImageForm.dragMove(drop);\n }, function() {\n dragDropToImageForm.dragEnd();\n });\n },\n\n /**\n * Handles update while a drop is being dragged.\n *\n * @param {jQuery} drop the drop preview being moved.\n */\n dragMove: function(drop) {\n var backgroundImage = $('fieldset#id_previewareaheader .dropbackground'),\n backgroundPosition = backgroundImage.offset(),\n dropNo = drop.data('dropNo'),\n dropPosition = drop.offset(),\n left = Math.round(dropPosition.left - backgroundPosition.left),\n top = Math.round(dropPosition.top - backgroundPosition.top);\n\n // Constrain coordinates to be inside the background.\n left = Math.round(Math.max(0, Math.min(left, backgroundImage.outerWidth() - drop.outerWidth())));\n top = Math.round(Math.max(0, Math.min(top, backgroundImage.outerHeight() - drop.outerHeight())));\n\n // Update the form.\n dragDropToImageForm.form.setFormValue('drops', [dropNo, 'xleft'], left);\n dragDropToImageForm.form.setFormValue('drops', [dropNo, 'ytop'], top);\n },\n\n /**\n * Handles when the drag ends.\n */\n dragEnd: function() {\n // Redraw, in case the position was constrained.\n dragDropToImageForm.updateDropZones();\n },\n\n /**\n * Low level operations on form.\n */\n form: {\n toNameWithIndex: function(name, indexes) {\n var indexString = name;\n for (var i = 0; i < indexes.length; i++) {\n indexString = indexString + '[' + indexes[i] + ']';\n }\n return indexString;\n },\n\n getEl: function(name, indexes) {\n var form = $('form.mform[data-qtype=\"ddimageortext\"]')[0];\n return form.elements[this.toNameWithIndex(name, indexes)];\n },\n\n /**\n * Helper to get the value of a form elements with name like \"drops[0][xleft]\".\n *\n * @param {String} name the base name, e.g. 'drops'.\n * @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].\n * @return {String} the value of that field.\n */\n getFormValue: function(name, indexes) {\n var el = this.getEl(name, indexes);\n if (!el.type) {\n el = el[el.length - 1];\n }\n if (el.type === 'checkbox') {\n return el.checked;\n } else {\n return el.value;\n }\n },\n\n /**\n * Helper to get the value of a form elements with name like \"drops[0][xleft]\".\n *\n * @param {String} name the base name, e.g. 'drops'.\n * @param {String[]} indexes the indexes, e.g. ['0', 'xleft'].\n * @param {String|Number} value the value to set.\n */\n setFormValue: function(name, indexes, value) {\n var el = this.getEl(name, indexes);\n if (el.type === 'checkbox') {\n el.checked = value;\n } else {\n el.value = value;\n }\n }\n },\n\n /**\n * Utility to get the file name and url from the filepicker.\n * @returns {Object} object containing functions {file, name}\n */\n filePickers: function() {\n var draftItemIdsToName;\n var nameToParentNode;\n\n if (draftItemIdsToName === undefined) {\n draftItemIdsToName = {};\n nameToParentNode = {};\n var fp = $('form.mform[data-qtype=\"ddimageortext\"] input.filepickerhidden');\n fp.each(function(index, filepicker) {\n draftItemIdsToName[filepicker.value] = filepicker.name;\n nameToParentNode[filepicker.name] = filepicker.parentNode;\n });\n }\n\n return {\n file: function(name) {\n var parentNode = $(nameToParentNode[name]);\n var fileAnchor = parentNode.find('div.filepicker-filelist a');\n if (fileAnchor.length) {\n return {href: fileAnchor.get(0).href, name: fileAnchor.get(0).innerHTML};\n } else {\n return {href: null, name: null};\n }\n },\n\n name: function(draftitemid) {\n return draftItemIdsToName[draftitemid];\n }\n };\n }\n };\n\n /**\n * @alias module:qtype_ddimageortext/form\n */\n return {\n /**\n * Initialise the form JavaScript features.\n */\n init: dragDropToImageForm.init\n };\n});\n"],"file":"form.min.js"} \ No newline at end of file diff --git a/question/type/ddimageortext/amd/src/form.js b/question/type/ddimageortext/amd/src/form.js index 1e0c80fe767..2e5dc965bbd 100644 --- a/question/type/ddimageortext/amd/src/form.js +++ b/question/type/ddimageortext/amd/src/form.js @@ -211,7 +211,7 @@ define(['jquery', 'core/dragdrop'], function($, dragDrop) { // Resize them to the same size. $('.dropzones .droppreview').css('padding', '0'); - var numGroups = $('select.draggroup').first().find('option').length; + var numGroups = $('.draggroup select').first().find('option').length; for (var group = 1; group <= numGroups; group++) { dragDropToImageForm.resizeAllDragsAndDropsInGroup(group); } @@ -398,9 +398,8 @@ define(['jquery', 'core/dragdrop'], function($, dragDrop) { top = Math.round(dropPosition.top - backgroundPosition.top); // Constrain coordinates to be inside the background. - // The -10 here matches the +10 in resizeAllDragsAndDropsInGroup(). - left = Math.max(0, Math.min(left, backgroundImage.width() - drop.width() - 10)); - top = Math.max(0, Math.min(top, backgroundImage.height() - drop.height() - 10)); + left = Math.round(Math.max(0, Math.min(left, backgroundImage.outerWidth() - drop.outerWidth()))); + top = Math.round(Math.max(0, Math.min(top, backgroundImage.outerHeight() - drop.outerHeight()))); // Update the form. dragDropToImageForm.form.setFormValue('drops', [dropNo, 'xleft'], left); diff --git a/question/type/ddimageortext/styles.css b/question/type/ddimageortext/styles.css index 16c75e81246..8334a9d00bc 100644 --- a/question/type/ddimageortext/styles.css +++ b/question/type/ddimageortext/styles.css @@ -99,6 +99,7 @@ form.mform fieldset#id_previewareaheader .dragitems { form.mform fieldset#id_previewareaheader .droppreview { position: absolute; cursor: move; + white-space: nowrap; } .que.ddimageortext .dragitems.readonly .drag { From 788a74ad05c7925fa766a0dc98703dace5d9465e Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Mon, 16 Nov 2020 14:19:27 +0100 Subject: [PATCH 08/26] MDL-70192 composer: bump to moodle-behat-extension 3.311.0 Generated following the instructions @: https://docs.moodle.org/dev/Composer#How_to_prepare_and_submit_composer_changes (using php72) --- composer.json | 2 +- composer.lock | 490 ++++++++++++++++---------------------------------- 2 files changed, 152 insertions(+), 340 deletions(-) diff --git a/composer.json b/composer.json index 4e345aa7e84..aafccbc5269 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ ], "require-dev": { "phpunit/phpunit": "8.5.*", - "moodlehq/behat-extension": "3.310.0", + "moodlehq/behat-extension": "3.311.0", "mikey179/vfsstream": "^1.6", "instaclick/php-webdriver": "dev-local as 1.x-dev" } diff --git a/composer.lock b/composer.lock index 6ee34b7cc17..273da677350 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "104dc538011bde60b0cd2e7980b750b6", + "content-hash": "dc5ad6aef22a668ce774b8d0914059b2", "packages": [], "packages-dev": [ { @@ -518,36 +518,31 @@ }, { "name": "doctrine/instantiator", - "version": "1.3.1", + "version": "1.4.0", "source": { "type": "git", "url": "https://github.com/doctrine/instantiator.git", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea" + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/f350df0268e904597e3bd9c4685c53e0e333feea", - "reference": "f350df0268e904597e3bd9c4685c53e0e333feea", + "url": "https://api.github.com/repos/doctrine/instantiator/zipball/d56bf6102915de5702778fe20f2de3b2fe570b5b", + "reference": "d56bf6102915de5702778fe20f2de3b2fe570b5b", "shasum": "" }, "require": { "php": "^7.1 || ^8.0" }, "require-dev": { - "doctrine/coding-standard": "^6.0", + "doctrine/coding-standard": "^8.0", "ext-pdo": "*", "ext-phar": "*", - "phpbench/phpbench": "^0.13", - "phpstan/phpstan-phpunit": "^0.11", - "phpstan/phpstan-shim": "^0.11", - "phpunit/phpunit": "^7.0" + "phpbench/phpbench": "^0.13 || 1.0.0-alpha2", + "phpstan/phpstan": "^0.12", + "phpstan/phpstan-phpunit": "^0.12", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, "autoload": { "psr-4": { "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" @@ -561,7 +556,7 @@ { "name": "Marco Pivetta", "email": "ocramius@gmail.com", - "homepage": "http://ocramius.github.com/" + "homepage": "https://ocramius.github.io/" } ], "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", @@ -584,25 +579,25 @@ "type": "tidelift" } ], - "time": "2020-05-29T17:27:14+00:00" + "time": "2020-11-10T18:47:58+00:00" }, { "name": "fabpot/goutte", - "version": "v3.3.0", + "version": "v3.3.1", "source": { "type": "git", "url": "https://github.com/FriendsOfPHP/Goutte.git", - "reference": "4ab5199e3ec0ffde0ee0b5ecf568a4fb8398dbae" + "reference": "80a23b64f44d54dd571d114c473d9d7e9ed84ca5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/4ab5199e3ec0ffde0ee0b5ecf568a4fb8398dbae", - "reference": "4ab5199e3ec0ffde0ee0b5ecf568a4fb8398dbae", + "url": "https://api.github.com/repos/FriendsOfPHP/Goutte/zipball/80a23b64f44d54dd571d114c473d9d7e9ed84ca5", + "reference": "80a23b64f44d54dd571d114c473d9d7e9ed84ca5", "shasum": "" }, "require": { "guzzlehttp/guzzle": "^6.0", - "php": "^7.1.3", + "php": ">=7.1.3", "symfony/browser-kit": "^4.4|^5.0", "symfony/css-selector": "^4.4|^5.0", "symfony/dom-crawler": "^4.4|^5.0" @@ -639,7 +634,7 @@ "keywords": [ "scraper" ], - "time": "2019-12-06T13:11:18+00:00" + "time": "2020-11-01T09:30:18+00:00" }, { "name": "guzzlehttp/guzzle", @@ -939,7 +934,7 @@ }, { "name": "moodlehq/behat-extension", - "version": "v3.310.0", + "version": "v3.311.0", "source": { "type": "git", "url": "https://github.com/moodlehq/moodle-behat-extension.git", @@ -988,16 +983,16 @@ }, { "name": "myclabs/deep-copy", - "version": "1.10.1", + "version": "1.10.2", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5" + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", - "reference": "969b211f9a51aa1f6c01d1d2aef56d3bd91598e5", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/776f831124e9c62e1a2c601ecc52e776d8bb7220", + "reference": "776f831124e9c62e1a2c601ecc52e776d8bb7220", "shasum": "" }, "require": { @@ -1038,52 +1033,7 @@ "type": "tidelift" } ], - "time": "2020-06-29T13:22:24+00:00" - }, - { - "name": "paragonie/random_compat", - "version": "v9.99.99", - "source": { - "type": "git", - "url": "https://github.com/paragonie/random_compat.git", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/paragonie/random_compat/zipball/84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "reference": "84b4dfb120c6f9b4ff7b3685f9b8f1aa365a0c95", - "shasum": "" - }, - "require": { - "php": "^7" - }, - "require-dev": { - "phpunit/phpunit": "4.*|5.*", - "vimeo/psalm": "^1" - }, - "suggest": { - "ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes." - }, - "type": "library", - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Paragon Initiative Enterprises", - "email": "security@paragonie.com", - "homepage": "https://paragonie.com" - } - ], - "description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7", - "keywords": [ - "csprng", - "polyfill", - "pseudorandom", - "random" - ], - "time": "2018-07-02T15:55:56+00:00" + "time": "2020-11-13T09:40:50+00:00" }, { "name": "phar-io/manifest", @@ -1651,39 +1601,39 @@ }, { "name": "phpunit/phpunit", - "version": "8.5.8", + "version": "8.5.9", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997" + "reference": "f5c8a5dd5e7e8d68d7562bfb48d47287d33937d6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/34c18baa6a44f1d1fbf0338907139e9dce95b997", - "reference": "34c18baa6a44f1d1fbf0338907139e9dce95b997", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f5c8a5dd5e7e8d68d7562bfb48d47287d33937d6", + "reference": "f5c8a5dd5e7e8d68d7562bfb48d47287d33937d6", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.2.0", + "doctrine/instantiator": "^1.3.1", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.9.1", + "myclabs/deep-copy": "^1.10.0", "phar-io/manifest": "^1.0.3", "phar-io/version": "^2.0.1", "php": "^7.2", - "phpspec/prophecy": "^1.8.1", - "phpunit/php-code-coverage": "^7.0.7", + "phpspec/prophecy": "^1.10.3", + "phpunit/php-code-coverage": "^7.0.10", "phpunit/php-file-iterator": "^2.0.2", "phpunit/php-text-template": "^1.2.1", "phpunit/php-timer": "^2.1.2", "sebastian/comparator": "^3.0.2", "sebastian/diff": "^3.0.2", - "sebastian/environment": "^4.2.2", - "sebastian/exporter": "^3.1.1", + "sebastian/environment": "^4.2.3", + "sebastian/exporter": "^3.1.2", "sebastian/global-state": "^3.0.0", "sebastian/object-enumerator": "^3.0.3", "sebastian/resource-operations": "^2.0.1", @@ -1740,7 +1690,7 @@ "type": "github" } ], - "time": "2020-06-22T07:06:58+00:00" + "time": "2020-11-10T12:51:38+00:00" }, { "name": "psr/container", @@ -2544,16 +2494,16 @@ }, { "name": "symfony/browser-kit", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/browser-kit.git", - "reference": "9a1786e5020783605a30cff2ceed9aca030e8d80" + "reference": "99b640fd5d06877e3242ba0393b40a7877dfe534" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/browser-kit/zipball/9a1786e5020783605a30cff2ceed9aca030e8d80", - "reference": "9a1786e5020783605a30cff2ceed9aca030e8d80", + "url": "https://api.github.com/repos/symfony/browser-kit/zipball/99b640fd5d06877e3242ba0393b40a7877dfe534", + "reference": "99b640fd5d06877e3242ba0393b40a7877dfe534", "shasum": "" }, "require": { @@ -2570,11 +2520,6 @@ "symfony/process": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\BrowserKit\\": "" @@ -2613,20 +2558,20 @@ "type": "tidelift" } ], - "time": "2020-10-02T08:38:15+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/config", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "7c5a1002178a612787c291a4f515f87b19176b61" + "reference": "e85481cf359a7b28a44ac91f7d83441b70d76192" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/7c5a1002178a612787c291a4f515f87b19176b61", - "reference": "7c5a1002178a612787c291a4f515f87b19176b61", + "url": "https://api.github.com/repos/symfony/config/zipball/e85481cf359a7b28a44ac91f7d83441b70d76192", + "reference": "e85481cf359a7b28a44ac91f7d83441b70d76192", "shasum": "" }, "require": { @@ -2648,11 +2593,6 @@ "symfony/yaml": "To use the yaml reference dumper" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Config\\": "" @@ -2691,20 +2631,20 @@ "type": "tidelift" } ], - "time": "2020-10-02T07:34:48+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/console", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8" + "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/ae789a8a2ad189ce7e8216942cdb9b77319f5eb8", - "reference": "ae789a8a2ad189ce7e8216942cdb9b77319f5eb8", + "url": "https://api.github.com/repos/symfony/console/zipball/e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", + "reference": "e0b2c29c0fa6a69089209bbe8fcff4df2a313d0e", "shasum": "" }, "require": { @@ -2741,11 +2681,6 @@ "symfony/process": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Console\\": "" @@ -2784,31 +2719,26 @@ "type": "tidelift" } ], - "time": "2020-10-07T15:23:00+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/css-selector", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/css-selector.git", - "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9" + "reference": "6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/css-selector/zipball/e544e24472d4c97b2d11ade7caacd446727c6bf9", - "reference": "e544e24472d4c97b2d11ade7caacd446727c6bf9", + "url": "https://api.github.com/repos/symfony/css-selector/zipball/6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0", + "reference": "6cbebda22ffc0d4bb8fea0c1311c2ca54c4c8fa0", "shasum": "" }, "require": { "php": ">=7.2.5" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\CssSelector\\": "" @@ -2851,20 +2781,20 @@ "type": "tidelift" } ], - "time": "2020-05-20T17:43:50+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/dependency-injection", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "89274c8847dff2ed703e481843eb9159ca25cc6e" + "reference": "4c41ad68924fd8f9e55e1cd77fd6bc28daa3fe89" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/89274c8847dff2ed703e481843eb9159ca25cc6e", - "reference": "89274c8847dff2ed703e481843eb9159ca25cc6e", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/4c41ad68924fd8f9e55e1cd77fd6bc28daa3fe89", + "reference": "4c41ad68924fd8f9e55e1cd77fd6bc28daa3fe89", "shasum": "" }, "require": { @@ -2895,11 +2825,6 @@ "symfony/yaml": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\DependencyInjection\\": "" @@ -2938,7 +2863,7 @@ "type": "tidelift" } ], - "time": "2020-09-10T10:08:39+00:00" + "time": "2020-10-27T10:05:40+00:00" }, { "name": "symfony/deprecation-contracts", @@ -3006,16 +2931,16 @@ }, { "name": "symfony/dom-crawler", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/dom-crawler.git", - "reference": "bdcb7633a501770a0daefbf81d2e6b28c3864f2b" + "reference": "30ad9ac96a01913195bf0328d48e29d54fa53e6e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/bdcb7633a501770a0daefbf81d2e6b28c3864f2b", - "reference": "bdcb7633a501770a0daefbf81d2e6b28c3864f2b", + "url": "https://api.github.com/repos/symfony/dom-crawler/zipball/30ad9ac96a01913195bf0328d48e29d54fa53e6e", + "reference": "30ad9ac96a01913195bf0328d48e29d54fa53e6e", "shasum": "" }, "require": { @@ -3034,11 +2959,6 @@ "symfony/css-selector": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\DomCrawler\\": "" @@ -3077,20 +2997,20 @@ "type": "tidelift" } ], - "time": "2020-10-02T07:34:48+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "d5de97d6af175a9e8131c546db054ca32842dd0f" + "reference": "26f4edae48c913fc183a3da0553fe63bdfbd361a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/d5de97d6af175a9e8131c546db054ca32842dd0f", - "reference": "d5de97d6af175a9e8131c546db054ca32842dd0f", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/26f4edae48c913fc183a3da0553fe63bdfbd361a", + "reference": "26f4edae48c913fc183a3da0553fe63bdfbd361a", "shasum": "" }, "require": { @@ -3121,11 +3041,6 @@ "symfony/http-kernel": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\EventDispatcher\\": "" @@ -3164,7 +3079,7 @@ "type": "tidelift" } ], - "time": "2020-09-18T14:27:32+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -3244,16 +3159,16 @@ }, { "name": "symfony/filesystem", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae" + "reference": "df08650ea7aee2d925380069c131a66124d79177" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/1a8697545a8d87b9f2f6b1d32414199cc5e20aae", - "reference": "1a8697545a8d87b9f2f6b1d32414199cc5e20aae", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/df08650ea7aee2d925380069c131a66124d79177", + "reference": "df08650ea7aee2d925380069c131a66124d79177", "shasum": "" }, "require": { @@ -3261,11 +3176,6 @@ "symfony/polyfill-ctype": "~1.8" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Filesystem\\": "" @@ -3304,24 +3214,24 @@ "type": "tidelift" } ], - "time": "2020-09-27T14:02:37+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454" + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/1c302646f6efc070cd46856e600e5e0684d6b454", - "reference": "1c302646f6efc070cd46856e600e5e0684d6b454", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/f4ba089a5b6366e453971d3aad5fe8e897b37f41", + "reference": "f4ba089a5b6366e453971d3aad5fe8e897b37f41", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-ctype": "For best performance" @@ -3329,7 +3239,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3380,24 +3290,24 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5" + "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/b740103edbdcc39602239ee8860f0f45a8eb9aa5", - "reference": "b740103edbdcc39602239ee8860f0f45a8eb9aa5", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", + "reference": "c7cf3f858ec7d70b89559d6e6eb1f7c2517d479c", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -3405,7 +3315,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3458,26 +3368,25 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-idn", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-idn.git", - "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251" + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/5dcab1bc7146cf8c1beaa4502a3d9be344334251", - "reference": "5dcab1bc7146cf8c1beaa4502a3d9be344334251", + "url": "https://api.github.com/repos/symfony/polyfill-intl-idn/zipball/3b75acd829741c768bc8b1f84eb33265e7cc5117", + "reference": "3b75acd829741c768bc8b1f84eb33265e7cc5117", "shasum": "" }, "require": { - "php": ">=5.3.3", + "php": ">=7.1", "symfony/polyfill-intl-normalizer": "^1.10", - "symfony/polyfill-php70": "^1.10", "symfony/polyfill-php72": "^1.10" }, "suggest": { @@ -3486,7 +3395,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3543,24 +3452,24 @@ "type": "tidelift" } ], - "time": "2020-08-04T06:02:08+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e" + "reference": "727d1096295d807c309fb01a851577302394c897" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", - "reference": "37078a8dd4a2a1e9ab0231af7c6cb671b2ed5a7e", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/727d1096295d807c309fb01a851577302394c897", + "reference": "727d1096295d807c309fb01a851577302394c897", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-intl": "For best performance" @@ -3568,7 +3477,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3624,24 +3533,24 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a" + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/a6977d63bf9a0ad4c65cd352709e230876f9904a", - "reference": "a6977d63bf9a0ad4c65cd352709e230876f9904a", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/39d483bdf39be819deabf04ec872eb0b2410b531", + "reference": "39d483bdf39be819deabf04ec872eb0b2410b531", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "suggest": { "ext-mbstring": "For best performance" @@ -3649,7 +3558,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3701,106 +3610,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" - }, - { - "name": "symfony/polyfill-php70", - "version": "v1.18.1", - "source": { - "type": "git", - "url": "https://github.com/symfony/polyfill-php70.git", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php70/zipball/0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", - "reference": "0dd93f2c578bdc9c72697eaa5f1dd25644e618d3", - "shasum": "" - }, - "require": { - "paragonie/random_compat": "~1.0|~2.0|~9.99", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.18-dev" - }, - "thanks": { - "name": "symfony/polyfill", - "url": "https://github.com/symfony/polyfill" - } - }, - "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php70\\": "" - }, - "files": [ - "bootstrap.php" - ], - "classmap": [ - "Resources/stubs" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Nicolas Grekas", - "email": "p@tchwork.com" - }, - { - "name": "Symfony Community", - "homepage": "https://symfony.com/contributors" - } - ], - "description": "Symfony polyfill backporting some PHP 7.0+ features to lower PHP versions", - "homepage": "https://symfony.com", - "keywords": [ - "compatibility", - "polyfill", - "portable", - "shim" - ], - "funding": [ - { - "url": "https://symfony.com/sponsor", - "type": "custom" - }, - { - "url": "https://github.com/fabpot", - "type": "github" - }, - { - "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", - "type": "tidelift" - } - ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php72", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php72.git", - "reference": "639447d008615574653fb3bc60d1986d7172eaae" + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/639447d008615574653fb3bc60d1986d7172eaae", - "reference": "639447d008615574653fb3bc60d1986d7172eaae", + "url": "https://api.github.com/repos/symfony/polyfill-php72/zipball/cede45fcdfabdd6043b3592e83678e42ec69e930", + "reference": "cede45fcdfabdd6043b3592e83678e42ec69e930", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3851,29 +3683,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php73", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca" + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/fffa1a52a023e782cdcc221d781fe1ec8f87fcca", - "reference": "fffa1a52a023e782cdcc221d781fe1ec8f87fcca", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/8ff431c517be11c78c48a39a66d37431e26a6bed", + "reference": "8ff431c517be11c78c48a39a66d37431e26a6bed", "shasum": "" }, "require": { - "php": ">=5.3.3" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -3927,29 +3759,29 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.18.1", + "version": "v1.20.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981" + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/d87d5766cbf48d72388a9f6b85f280c8ad51f981", - "reference": "d87d5766cbf48d72388a9f6b85f280c8ad51f981", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/e70aa8b064c5b72d3df2abd5ab1e90464ad009de", + "reference": "e70aa8b064c5b72d3df2abd5ab1e90464ad009de", "shasum": "" }, "require": { - "php": ">=7.0.8" + "php": ">=7.1" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.18-dev" + "dev-main": "1.20-dev" }, "thanks": { "name": "symfony/polyfill", @@ -4007,20 +3839,20 @@ "type": "tidelift" } ], - "time": "2020-07-14T12:35:20+00:00" + "time": "2020-10-23T14:02:19+00:00" }, { "name": "symfony/process", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/process.git", - "reference": "d3a2e64866169586502f0cd9cab69135ad12cee9" + "reference": "f00872c3f6804150d6a0f73b4151daab96248101" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/process/zipball/d3a2e64866169586502f0cd9cab69135ad12cee9", - "reference": "d3a2e64866169586502f0cd9cab69135ad12cee9", + "url": "https://api.github.com/repos/symfony/process/zipball/f00872c3f6804150d6a0f73b4151daab96248101", + "reference": "f00872c3f6804150d6a0f73b4151daab96248101", "shasum": "" }, "require": { @@ -4028,11 +3860,6 @@ "symfony/polyfill-php80": "^1.15" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Process\\": "" @@ -4071,7 +3898,7 @@ "type": "tidelift" } ], - "time": "2020-09-02T16:23:27+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/service-contracts", @@ -4151,16 +3978,16 @@ }, { "name": "symfony/string", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "4a9afe9d07bac506f75bcee8ed3ce76da5a9343e" + "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/4a9afe9d07bac506f75bcee8ed3ce76da5a9343e", - "reference": "4a9afe9d07bac506f75bcee8ed3ce76da5a9343e", + "url": "https://api.github.com/repos/symfony/string/zipball/a97573e960303db71be0dd8fda9be3bca5e0feea", + "reference": "a97573e960303db71be0dd8fda9be3bca5e0feea", "shasum": "" }, "require": { @@ -4178,11 +4005,6 @@ "symfony/var-exporter": "^4.4|^5.0" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\String\\": "" @@ -4232,20 +4054,20 @@ "type": "tidelift" } ], - "time": "2020-09-15T12:23:47+00:00" + "time": "2020-10-24T12:01:57+00:00" }, { "name": "symfony/translation", - "version": "v4.4.15", + "version": "v4.4.16", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "8494fa1bbf9d77fe1e7d50ac8ccfb80a858a98bd" + "reference": "73095716af79f610f3b6338b911357393fdd10ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/8494fa1bbf9d77fe1e7d50ac8ccfb80a858a98bd", - "reference": "8494fa1bbf9d77fe1e7d50ac8ccfb80a858a98bd", + "url": "https://api.github.com/repos/symfony/translation/zipball/73095716af79f610f3b6338b911357393fdd10ab", + "reference": "73095716af79f610f3b6338b911357393fdd10ab", "shasum": "" }, "require": { @@ -4279,11 +4101,6 @@ "symfony/yaml": "" }, "type": "library", - "extra": { - "branch-alias": { - "dev-master": "4.4-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Translation\\": "" @@ -4322,7 +4139,7 @@ "type": "tidelift" } ], - "time": "2020-10-02T07:34:48+00:00" + "time": "2020-10-24T11:50:19+00:00" }, { "name": "symfony/translation-contracts", @@ -4401,16 +4218,16 @@ }, { "name": "symfony/yaml", - "version": "v5.1.7", + "version": "v5.1.8", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a" + "reference": "f284e032c3cefefb9943792132251b79a6127ca6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", - "reference": "e147a68cb66a8b510f4b7481fe4da5b2ab65ec6a", + "url": "https://api.github.com/repos/symfony/yaml/zipball/f284e032c3cefefb9943792132251b79a6127ca6", + "reference": "f284e032c3cefefb9943792132251b79a6127ca6", "shasum": "" }, "require": { @@ -4431,11 +4248,6 @@ "Resources/bin/yaml-lint" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "5.1-dev" - } - }, "autoload": { "psr-4": { "Symfony\\Component\\Yaml\\": "" @@ -4474,7 +4286,7 @@ "type": "tidelift" } ], - "time": "2020-09-27T03:44:28+00:00" + "time": "2020-10-24T12:03:25+00:00" }, { "name": "theseer/tokenizer", From 89a9b87c959eaf9ed190dd3b72b1dbdeacbc1648 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Fri, 20 Nov 2020 16:20:18 +0100 Subject: [PATCH 09/26] MDL-70265 travis: Only run highest phpunit if configured via env By default only lowest php version will be executed, and only pgsql. This default behavior can be changed with a new variable: MOODLE_PHP = [all] MOODLE_DATABASE = [pgsql | mysqli | all] --- .travis.yml | 71 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7354b3d9d1f..5c58e679a44 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,31 +16,9 @@ services: - mysql - docker -php: - # We only run the highest and lowest supported versions to reduce the load on travis-ci.org. - - 7.4 - - 7.2 - addons: postgresql: "9.6" -env: - # Although we want to run these jobs and see failures as quickly as possible, we also want to get the slowest job to - # start first so that the total run time is not too high. - # - # We only run MySQL on PHP 7.2, so run that first. - # CI Tests should be second-highest in priority as these only take <= 60 seconds to run under normal circumstances. - # Postgres is significantly is pretty reasonable in its run-time. - - # Run CI Tests without running PHPUnit. - - DB=none TASK=CITEST - - # Run unit tests on Postgres - - DB=pgsql TASK=PHPUNIT - - # Perform an upgrade test too. - - DB=pgsql TASK=UPGRADE - jobs: # Enable fast finish. # This will fail the build if a single job fails (except those in allow_failures). @@ -48,12 +26,51 @@ jobs: fast_finish: true include: - # Run mysql only on highest - it's just too slow - - php: 7.4 + # First all the lowest php ones (7.2) + - php: 7.2 + env: DB=none TASK=CITEST + - php: 7.2 + env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon' + + - if: env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present + php: 7.2 + env: DB=pgsql TASK=PHPUNIT + + - if: env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present + php: 7.2 + env: DB=pgsql TASK=UPGRADE + + - if: env(MOODLE_DATABASE) = "mysqli" OR env(MOODLE_DATABASE) = "all" + php: 7.2 env: DB=mysqli TASK=PHPUNIT - # Run grunt/npm install on highest version too ('node' is an alias for the latest node.js version.) - - php: 7.4 - env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon' + + - if: env(MOODLE_DATABASE) = "mysqli" OR env(MOODLE_DATABASE) = "all" + php: 7.2 + env: DB=mysqli TASK=UPGRADE + + # Then, conditionally, all the highest php ones (7.4) + - if: env(MOODLE_PHP) = "all" + php: 7.4 + env: DB=none TASK=CITEST + - if: env(MOODLE_PHP) = "all" + php: 7.4 + env: DB=none TASK=GRUNT NVM_VERSION='lts/carbon' + + - if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present) + php: 7.4 + env: DB=pgsql TASK=PHPUNIT + + - if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present) + php: 7.4 + env: DB=pgsql TASK=UPGRADE + + - if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "mysqli" OR env(MOODLE_DATABASE) = "all") + php: 7.4 + env: DB=mysqli TASK=PHPUNIT + + - if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "mysqli" OR env(MOODLE_DATABASE) = "all") + php: 7.4 + env: DB=mysqli TASK=UPGRADE cache: directories: From 621199b331e134db90bce0a019ea1d8b47ab3112 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 12 Nov 2020 08:15:55 +0800 Subject: [PATCH 10/26] MDL-67668 behat: Support NodeElement when fetching node in container This is similar to change made in MDL-69136 to allow an already-fetched NodeElement to be provided to the get_node_in_container() function and makes it easier to be deterministic when writing steps. --- lib/behat/behat_base.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index 57688c8d671..5298e50e6ab 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -467,10 +467,16 @@ class behat_base extends Behat\MinkExtension\Context\RawMinkContext { * @return NodeElement */ protected function get_node_in_container($selectortype, $element, $containerselectortype, $containerelement) { - // Gets the container, it will always be text based. - $containernode = $this->get_text_selector_node($containerselectortype, $containerelement); + if ($containerselectortype === 'NodeElement' && is_a($containerelement, NodeElement::class)) { + // Support a NodeElement being passed in for use in step chaining. + $containernode = $containerelement; + $locatorexceptionmsg = $element; + } else { + // Gets the container, it will always be text based. + $containernode = $this->get_text_selector_node($containerselectortype, $containerelement); + $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"'; + } - $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"'; $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg); return $this->find($selectortype, $element, $exception, $containernode); From a53c5b847e58f19e1f42973bff31c446216688f8 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 12 Nov 2020 08:17:34 +0800 Subject: [PATCH 11/26] MDL-67668 behat: Correct js pending check Pending checks should only run when JS is running, but some uses were not apply this check. --- lib/behat/behat_base.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index 5298e50e6ab..8dc95d3dd3f 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -848,11 +848,6 @@ EOF; * @return bool Whether any JS is still pending completion. */ public function wait_for_pending_js() { - if (!$this->running_javascript()) { - // JS is not available therefore there is nothing to wait for. - return false; - } - return static::wait_for_pending_js_in_session($this->getSession()); } @@ -863,6 +858,11 @@ EOF; * @return bool Whether any JS is still pending completion. */ public static function wait_for_pending_js_in_session(Session $session) { + if (!self::running_javascript_in_session($session)) { + // JS is not available therefore there is nothing to wait for. + return false; + } + // We don't use behat_base::spin() here as we don't want to end up with an exception // if the page & JSs don't finish loading properly. for ($i = 0; $i < self::get_extended_timeout() * 10; $i++) { From 75801895aa90decebb3ced39bcd0187fd8b7afa6 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 10 Nov 2020 15:10:30 +0800 Subject: [PATCH 12/26] MDL-67668 behat: Share behat_behat functionality with forms Behat form fields are implemented in a way completely isolated from the rest of the Behat Context system. Whereas regular step definitions have access to execute steps, to call `find`, check for JS running, and other related functionality, the Moodle implementation of a field type does not have any access to this. By moving the core functionality of behat_base to a new trait, and the constants to a new interface, the functionality can also be used in behat form fields in the same way as elsewhere. --- lib/behat/behat_base.php | 1314 +---------------- lib/behat/classes/behat_session_interface.php | 87 ++ lib/behat/classes/behat_session_trait.php | 1312 ++++++++++++++++ 3 files changed, 1404 insertions(+), 1309 deletions(-) create mode 100644 lib/behat/classes/behat_session_interface.php create mode 100644 lib/behat/classes/behat_session_trait.php diff --git a/lib/behat/behat_base.php b/lib/behat/behat_base.php index 8dc95d3dd3f..0fe5c09bc79 100644 --- a/lib/behat/behat_base.php +++ b/lib/behat/behat_base.php @@ -28,19 +28,8 @@ // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. -use Behat\Mink\Exception\DriverException; -use Behat\Mink\Exception\ExpectationException; -use Behat\Mink\Exception\ElementNotFoundException; -use Behat\Mink\Element\NodeElement; -use Behat\Mink\Element\Element; -use Behat\Mink\Session; - -require_once(__DIR__ . '/classes/component_named_selector.php'); -require_once(__DIR__ . '/classes/component_named_replacement.php'); - -// Alias the WebDriver\Key class to behat_keys to make future transition to a different WebDriver implementation -// easier. -class_alias('WebDriver\\Key', 'behat_keys'); +require_once(__DIR__ . '/classes/behat_session_interface.php'); +require_once(__DIR__ . '/classes/behat_session_trait.php'); /** * Steps definitions base class. @@ -60,1301 +49,8 @@ class_alias('WebDriver\\Key', 'behat_keys'); * @copyright 2012 David MonllaĂł * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ -class behat_base extends Behat\MinkExtension\Context\RawMinkContext { +class behat_base extends Behat\MinkExtension\Context\RawMinkContext implements behat_session_interface { - /** - * Small timeout. - * - * A reduced timeout for cases where self::TIMEOUT is too much - * and a simple $this->getSession()->getPage()->find() could not - * be enough. - * - * @deprecated since Moodle 3.7 MDL-64979 - please use get_reduced_timeout() instead - * @todo MDL-64982 This will be deleted in Moodle 3.11 - * @see behat_base::get_reduced_timeout() - */ - const REDUCED_TIMEOUT = 2; - - /** - * The timeout for each Behat step (load page, wait for an element to load...). - * - * @deprecated since Moodle 3.7 MDL-64979 - please use get_timeout() instead - * @todo MDL-64982 This will be deleted in Moodle 3.11 - * @see behat_base::get_timeout() - */ - const TIMEOUT = 6; - - /** - * And extended timeout for specific cases. - * - * @deprecated since Moodle 3.7 MDL-64979 - please use get_extended_timeout() instead - * @todo MDL-64982 This will be deleted in Moodle 3.11 - * @see behat_base::get_extended_timeout() - */ - const EXTENDED_TIMEOUT = 10; - - /** - * The JS code to check that the page is ready. - * - * The document must be complete and either M.util.pending_js must be empty, or it must not be defined at all. - */ - const PAGE_READY_JS = "document.readyState === 'complete' && " . - "(typeof M !== 'object' || typeof M.util !== 'object' || " . - "typeof M.util.pending_js === 'undefined' || M.util.pending_js.length === 0)"; - - /** - * Locates url, based on provided path. - * Override to provide custom routing mechanism. - * - * @see Behat\MinkExtension\Context\MinkContext - * @param string $path - * @return string - */ - protected function locate_path($path) { - $starturl = rtrim($this->getMinkParameter('base_url'), '/') . '/'; - return 0 !== strpos($path, 'http') ? $starturl . ltrim($path, '/') : $path; - } - - /** - * Returns the first matching element. - * - * @link http://mink.behat.org/#traverse-the-page-selectors - * @param string $selector The selector type (css, xpath, named...) - * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator... - * @param Exception $exception Otherwise we throw exception with generic info - * @param NodeElement $node Spins around certain DOM node instead of the whole page - * @param int $timeout Forces a specific time out (in seconds). - * @return NodeElement - */ - protected function find($selector, $locator, $exception = false, $node = false, $timeout = false) { - if ($selector === 'NodeElement' && is_a($locator, NodeElement::class)) { - // Support a NodeElement being passed in for use in step chaining. - return $locator; - } - - // Returns the first match. - $items = $this->find_all($selector, $locator, $exception, $node, $timeout); - return count($items) ? reset($items) : null; - } - - /** - * Returns all matching elements. - * - * Adapter to Behat\Mink\Element\Element::findAll() using the spin() method. - * - * @link http://mink.behat.org/#traverse-the-page-selectors - * @param string $selector The selector type (css, xpath, named...) - * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator... - * @param Exception $exception Otherwise we throw expcetion with generic info - * @param NodeElement $container Restrict the search to just children of the specified container - * @param int $timeout Forces a specific time out (in seconds). If 0 is provided the default timeout will be applied. - * @return array NodeElements list - */ - protected function find_all($selector, $locator, $exception = false, $container = false, $timeout = false) { - // Throw exception, so dev knows it is not supported. - if ($selector === 'named') { - $exception = 'Using the "named" selector is deprecated as of 3.1. ' - .' Use the "named_partial" or use the "named_exact" selector instead.'; - throw new ExpectationException($exception, $this->getSession()); - } - - // Generic info. - if (!$exception) { - // With named selectors we can be more specific. - if (($selector == 'named_exact') || ($selector == 'named_partial')) { - $exceptiontype = $locator[0]; - $exceptionlocator = $locator[1]; - - // If we are in a @javascript session all contents would be displayed as HTML characters. - if ($this->running_javascript()) { - $locator[1] = html_entity_decode($locator[1], ENT_NOQUOTES); - } - - } else { - $exceptiontype = $selector; - $exceptionlocator = $locator; - } - - $exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator); - } - - // How much we will be waiting for the element to appear. - if (!$timeout) { - $timeout = self::get_timeout(); - $microsleep = false; - } else { - // Spinning each 0.1 seconds if the timeout was forced as we understand - // that is a special case and is good to refine the performance as much - // as possible. - $microsleep = true; - } - - // Normalise the values in order to perform the search. - [ - 'selector' => $selector, - 'locator' => $locator, - 'container' => $container, - ] = $this->normalise_selector($selector, $locator, $container ?: $this->getSession()->getPage()); - - // Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception. - return $this->spin( - function() use ($selector, $locator, $container) { - return $container->findAll($selector, $locator); - }, [], $timeout, $exception, $microsleep - ); - } - - /** - * Normalise the locator and selector. - * - * @param string $selector The type of thing to search - * @param mixed $locator The locator value. Can be an array, but is more likely a string. - * @param Element $container An optional container to search within - * @return array The selector, locator, and container to search within - */ - public function normalise_selector(string $selector, $locator, Element $container): array { - // Check for specific transformations for this selector type. - $transformfunction = "transform_find_for_{$selector}"; - if (method_exists('behat_selectors', $transformfunction)) { - // A selector-specific transformation exists. - // Perform initial transformation of the selector within the current container. - [ - 'selector' => $selector, - 'locator' => $locator, - 'container' => $container, - ] = behat_selectors::{$transformfunction}($this, $locator, $container); - } - - // Normalise the css and xpath selector types. - if ('css_element' === $selector) { - $selector = 'css'; - } else if ('xpath_element' === $selector) { - $selector = 'xpath'; - } - - // Convert to a named selector where the selector type is not a known selector. - $converttonamed = !$this->getSession()->getSelectorsHandler()->isSelectorRegistered($selector); - $converttonamed = $converttonamed && 'xpath' !== $selector; - if ($converttonamed) { - if (behat_partial_named_selector::is_deprecated_selector($selector)) { - if ($replacement = behat_partial_named_selector::get_deprecated_replacement($selector)) { - error_log("The '{$selector}' selector has been replaced with {$replacement}"); - $selector = $replacement; - } - } else if (behat_exact_named_selector::is_deprecated_selector($selector)) { - if ($replacement = behat_exact_named_selector::get_deprecated_replacement($selector)) { - error_log("The '{$selector}' selector has been replaced with {$replacement}"); - $selector = $replacement; - } - } - - $allowedpartialselectors = behat_partial_named_selector::get_allowed_selectors(); - $allowedexactselectors = behat_exact_named_selector::get_allowed_selectors(); - if (isset($allowedpartialselectors[$selector])) { - $locator = behat_selectors::normalise_named_selector($allowedpartialselectors[$selector], $locator); - $selector = 'named_partial'; - } else if (isset($allowedexactselectors[$selector])) { - $locator = behat_selectors::normalise_named_selector($allowedexactselectors[$selector], $locator); - $selector = 'named_exact'; - } else { - throw new ExpectationException("The '{$selector}' selector type is not registered.", $this->getSession()->getDriver()); - } - } - - return [ - 'selector' => $selector, - 'locator' => $locator, - 'container' => $container, - ]; - } - - /** - * Send key presses straight to the currently active element. - * - * The `$keys` array contains a list of key values to send to the session as defined in the WebDriver and JsonWire - * specifications: - * - JsonWire: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#sessionsessionidkeys - * - W3C WebDriver: https://www.w3.org/TR/webdriver/#keyboard-actions - * - * This may be a combination of typable characters, modifier keys, and other supported keypoints. - * - * The NULL_KEY should be used to release modifier keys. If the NULL_KEY is not used then modifier keys will remain - * in the pressed state. - * - * Example usage: - * - * behat_base::type_keys($this->getSession(), [behat_keys::SHIFT, behat_keys::TAB, behat_keys::NULL_KEY]); - * behat_base::type_keys($this->getSession(), [behat_keys::ENTER, behat_keys::NULL_KEY]); - * behat_base::type_keys($this->getSession(), [behat_keys::ESCAPE, behat_keys::NULL_KEY]); - * - * It can also be used to send text input, for example: - * - * behat_base::type_keys( - * $this->getSession(), - * ['D', 'o', ' ', 'y', 'o', 'u', ' ', 'p', 'l', 'a' 'y', ' ', 'G', 'o', '?', behat_base::NULL_KEY] - * ); - * - * - * Please note: This function does not use the element/sendKeys variants but sends keys straight to the browser. - * - * @param Session $session - * @param string[] $keys - */ - public static function type_keys(Session $session, array $keys): void { - $session->getDriver()->getWebDriverSession()->keys([ - 'value' => $keys, - ]); - } - - /** - * Finds DOM nodes in the page using named selectors. - * - * The point of using this method instead of Mink ones is the spin - * method of behat_base::find() that looks for the element until it - * is available or it timeouts, this avoids the false failures received - * when selenium tries to execute commands on elements that are not - * ready to be used. - * - * All steps that requires elements to be available before interact with - * them should use one of the find* methods. - * - * The methods calls requires a {'find_' . $elementtype}($locator) - * format, like find_link($locator), find_select($locator), - * find_button($locator)... - * - * @link http://mink.behat.org/#named-selectors - * @throws coding_exception - * @param string $name The name of the called method - * @param mixed $arguments - * @return NodeElement - */ - public function __call($name, $arguments) { - if (substr($name, 0, 5) === 'find_') { - return call_user_func_array([$this, 'find'], array_merge( - [substr($name, 5)], - $arguments - )); - } - - throw new coding_exception("The '{$name}' method does not exist"); - } - - /** - * Escapes the double quote character. - * - * Double quote is the argument delimiter, it can be escaped - * with a backslash, but we auto-remove this backslashes - * before the step execution, this method is useful when using - * arguments as arguments for other steps. - * - * @param string $string - * @return string - */ - public function escape($string) { - return str_replace('"', '\"', $string); - } - - /** - * Executes the passed closure until returns true or time outs. - * - * In most cases the document.readyState === 'complete' will be enough, but sometimes JS - * requires more time to be completely loaded or an element to be visible or whatever is required to - * perform some action on an element; this method receives a closure which should contain the - * required statements to ensure the step definition actions and assertions have all their needs - * satisfied and executes it until they are satisfied or it timeouts. Redirects the return of the - * closure to the caller. - * - * The closures requirements to work well with this spin method are: - * - Must return false, null or '' if something goes wrong - * - Must return something != false if finishes as expected, this will be the (mixed) value - * returned by spin() - * - * The arguments of the closure are mixed, use $args depending on your needs. - * - * You can provide an exception to give more accurate feedback to tests writers, otherwise the - * closure exception will be used, but you must provide an exception if the closure does not throw - * an exception. - * - * @throws Exception If it timeouts without receiving something != false from the closure - * @param Function|array|string $lambda The function to execute or an array passed to call_user_func (maps to a class method) - * @param mixed $args Arguments to pass to the closure - * @param int $timeout Timeout in seconds - * @param Exception $exception The exception to throw in case it time outs. - * @param bool $microsleep If set to true it'll sleep micro seconds rather than seconds. - * @return mixed The value returned by the closure - */ - protected function spin($lambda, $args = false, $timeout = false, $exception = false, $microsleep = false) { - - // Using default timeout which is pretty high. - if (!$timeout) { - $timeout = self::get_timeout(); - } - - $start = microtime(true); - $end = $start + $timeout; - - do { - // We catch the exception thrown by the step definition to execute it again. - try { - // We don't check with !== because most of the time closures will return - // direct Behat methods returns and we are not sure it will be always (bool)false - // if it just runs the behat method without returning anything $return == null. - if ($return = call_user_func($lambda, $this, $args)) { - return $return; - } - } catch (Exception $e) { - // We would use the first closure exception if no exception has been provided. - if (!$exception) { - $exception = $e; - } - } - - if (!$this->running_javascript()) { - break; - } - - usleep(100000); - - } while (microtime(true) < $end); - - // Using coding_exception as is a development issue if no exception has been provided. - if (!$exception) { - $exception = new coding_exception('spin method requires an exception if the callback does not throw an exception'); - } - - // Throwing exception to the user. - throw $exception; - } - - /** - * Gets a NodeElement based on the locator and selector type received as argument from steps definitions. - * - * Use behat_base::get_text_selector_node() for text-based selectors. - * - * @throws ElementNotFoundException Thrown by behat_base::find - * @param string $selectortype - * @param string $element - * @return NodeElement - */ - protected function get_selected_node($selectortype, $element) { - return $this->find($selectortype, $element); - } - - /** - * Gets a NodeElement based on the locator and selector type received as argument from steps definitions. - * - * @throws ElementNotFoundException Thrown by behat_base::find - * @param string $selectortype - * @param string $element - * @return NodeElement - */ - protected function get_text_selector_node($selectortype, $element) { - // Getting Mink selector and locator. - list($selector, $locator) = $this->transform_text_selector($selectortype, $element); - - // Returns the NodeElement. - return $this->find($selector, $locator); - } - - /** - * Gets the requested element inside the specified container. - * - * @throws ElementNotFoundException Thrown by behat_base::find - * @param mixed $selectortype The element selector type. - * @param mixed $element The element locator. - * @param mixed $containerselectortype The container selector type. - * @param mixed $containerelement The container locator. - * @return NodeElement - */ - protected function get_node_in_container($selectortype, $element, $containerselectortype, $containerelement) { - if ($containerselectortype === 'NodeElement' && is_a($containerelement, NodeElement::class)) { - // Support a NodeElement being passed in for use in step chaining. - $containernode = $containerelement; - $locatorexceptionmsg = $element; - } else { - // Gets the container, it will always be text based. - $containernode = $this->get_text_selector_node($containerselectortype, $containerelement); - $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"'; - } - - $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg); - - return $this->find($selectortype, $element, $exception, $containernode); - } - - /** - * Transforms from step definition's argument style to Mink format. - * - * Mink has 3 different selectors css, xpath and named, where named - * selectors includes link, button, field... to simplify and group multiple - * steps in one we use the same interface, considering all link, buttons... - * at the same level as css selectors and xpath; this method makes the - * conversion from the arguments received by the steps to the selectors and locators - * required to interact with Mink. - * - * @throws ExpectationException - * @param string $selectortype It can be css, xpath or any of the named selectors. - * @param string $element The locator (or string) we are looking for. - * @return array Contains the selector and the locator expected by Mink. - */ - protected function transform_selector($selectortype, $element) { - // Here we don't know if an allowed text selector is being used. - $selectors = behat_selectors::get_allowed_selectors(); - if (!isset($selectors[$selectortype])) { - throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession()); - } - - [ - 'selector' => $selector, - 'locator' => $locator, - ] = $this->normalise_selector($selectortype, $element, $this->getSession()->getPage()); - - return [$selector, $locator]; - } - - /** - * Transforms from step definition's argument style to Mink format. - * - * Delegates all the process to behat_base::transform_selector() checking - * the provided $selectortype. - * - * @throws ExpectationException - * @param string $selectortype It can be css, xpath or any of the named selectors. - * @param string $element The locator (or string) we are looking for. - * @return array Contains the selector and the locator expected by Mink. - */ - protected function transform_text_selector($selectortype, $element) { - - $selectors = behat_selectors::get_allowed_text_selectors(); - if (empty($selectors[$selectortype])) { - throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession()); - } - - return $this->transform_selector($selectortype, $element); - } - - /** - * Whether Javascript is available in the current Session. - * - * @return boolean - */ - protected function running_javascript() { - return self::running_javascript_in_session($this->getSession()); - } - - /** - * Require that javascript be available in the current Session. - * - * @throws DriverException - */ - protected function require_javascript() { - return self::require_javascript_in_session($this->getSession()); - } - - /** - * Whether Javascript is available in the specified Session. - * - * @param Session $session - * @return boolean - */ - protected static function running_javascript_in_session(Session $session): bool { - return get_class($session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver'; - } - - /** - * Require that javascript be available for the specified Session. - * - * @param Session $session - * @throws DriverException - */ - protected static function require_javascript_in_session(Session $session): void { - if (self::running_javascript_in_session($session)) { - return; - } - - throw new DriverException('Javascript is required'); - } - - /** - * Checks if the current page is part of the mobile app. - * - * @return bool True if it's in the app - */ - protected function is_in_app() : bool { - // Cannot be in the app if there's no @app tag on scenario. - if (!$this->has_tag('app')) { - return false; - } - - // Check on page to see if it's an app page. Safest way is to look for added JavaScript. - return $this->evaluate_script('return typeof window.behat') === 'object'; - } - - /** - * Spins around an element until it exists - * - * @throws ExpectationException - * @param string $locator - * @param string $selectortype - * @return void - */ - protected function ensure_element_exists($locator, $selectortype) { - // Exception if it timesout and the element is still there. - $msg = "The '{$locator}' element does not exist and should"; - $exception = new ExpectationException($msg, $this->getSession()); - - // Normalise the values in order to perform the search. - [ - 'selector' => $selector, - 'locator' => $locator, - 'container' => $container, - ] = $this->normalise_selector($selectortype, $locator, $this->getSession()->getPage()); - - // It will stop spinning once the find() method returns true. - $this->spin( - function() use ($selector, $locator, $container) { - if ($container->find($selector, $locator)) { - return true; - } - return false; - }, - [], - self::get_extended_timeout(), - $exception, - true - ); - } - - /** - * Spins until the element does not exist - * - * @throws ExpectationException - * @param string $locator - * @param string $selectortype - * @return void - */ - protected function ensure_element_does_not_exist($locator, $selectortype) { - // Exception if it timesout and the element is still there. - $msg = "The '{$locator}' element exists and should not exist"; - $exception = new ExpectationException($msg, $this->getSession()); - - // Normalise the values in order to perform the search. - [ - 'selector' => $selector, - 'locator' => $locator, - 'container' => $container, - ] = $this->normalise_selector($selectortype, $locator, $this->getSession()->getPage()); - - // It will stop spinning once the find() method returns false. - $this->spin( - function() use ($selector, $locator, $container) { - if ($container->find($selector, $locator)) { - return false; - } - return true; - }, - // Note: We cannot use $this because the find will then be $this->find(), which leads us to a nested spin(). - // We cannot nest spins because the outer spin times out before the inner spin completes. - [], - self::get_extended_timeout(), - $exception, - true - ); - } - - /** - * Ensures that the provided node is visible and we can interact with it. - * - * @throws ExpectationException - * @param NodeElement $node - * @return void Throws an exception if it times out without the element being visible - */ - protected function ensure_node_is_visible($node) { - - if (!$this->running_javascript()) { - return; - } - - // Exception if it timesout and the element is still there. - $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible'; - $exception = new ExpectationException($msg, $this->getSession()); - - // It will stop spinning once the isVisible() method returns true. - $this->spin( - function($context, $args) { - if ($args->isVisible()) { - return true; - } - return false; - }, - $node, - self::get_extended_timeout(), - $exception, - true - ); - } - - /** - * Ensures that the provided node has a attribute value set. This step can be used to check if specific - * JS has finished modifying the node. - * - * @throws ExpectationException - * @param NodeElement $node - * @param string $attribute attribute name - * @param string $attributevalue attribute value to check. - * @return void Throws an exception if it times out without the element being visible - */ - protected function ensure_node_attribute_is_set($node, $attribute, $attributevalue) { - - if (!$this->running_javascript()) { - return; - } - - // Exception if it timesout and the element is still there. - $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible'; - $exception = new ExpectationException($msg, $this->getSession()); - - // It will stop spinning once the $args[1]) == $args[2], and method returns true. - $this->spin( - function($context, $args) { - if ($args[0]->getAttribute($args[1]) == $args[2]) { - return true; - } - return false; - }, - array($node, $attribute, $attributevalue), - self::get_extended_timeout(), - $exception, - true - ); - } - - /** - * Ensures that the provided element is visible and we can interact with it. - * - * Returns the node in case other actions are interested in using it. - * - * @throws ExpectationException - * @param string $element - * @param string $selectortype - * @return NodeElement Throws an exception if it times out without being visible - */ - protected function ensure_element_is_visible($element, $selectortype) { - - if (!$this->running_javascript()) { - return; - } - - $node = $this->get_selected_node($selectortype, $element); - $this->ensure_node_is_visible($node); - - return $node; - } - - /** - * Ensures that all the page's editors are loaded. - * - * @deprecated since Moodle 2.7 MDL-44084 - please do not use this function any more. - * @throws ElementNotFoundException - * @throws ExpectationException - * @return void - */ - protected function ensure_editors_are_loaded() { - global $CFG; - - if (empty($CFG->behat_usedeprecated)) { - debugging('Function behat_base::ensure_editors_are_loaded() is deprecated. It is no longer required.'); - } - return; - } - - /** - * Checks if the current scenario, or its feature, has a specified tag. - * - * @param string $tag Tag to check - * @return bool True if the tag exists in scenario or feature - */ - public function has_tag(string $tag) : bool { - return array_key_exists($tag, behat_hooks::get_tags_for_scenario()); - } - - /** - * Change browser window size. - * - small: 640x480 - * - medium: 1024x768 - * - large: 2560x1600 - * - * @param string $windowsize size of window. - * @param bool $viewport If true, changes viewport rather than window size - * @throws ExpectationException - */ - protected function resize_window($windowsize, $viewport = false) { - // Non JS don't support resize window. - if (!$this->running_javascript()) { - return; - } - - switch ($windowsize) { - case "small": - $width = 1024; - $height = 768; - break; - case "medium": - $width = 1366; - $height = 768; - break; - case "large": - $width = 2560; - $height = 1600; - break; - default: - preg_match('/^(\d+x\d+)$/', $windowsize, $matches); - if (empty($matches) || (count($matches) != 2)) { - throw new ExpectationException("Invalid screen size, can't resize", $this->getSession()); - } - $size = explode('x', $windowsize); - $width = (int) $size[0]; - $height = (int) $size[1]; - } - if ($viewport) { - // When setting viewport size, we set it so that the document width will be exactly - // as specified, assuming that there is a vertical scrollbar. (In cases where there is - // no scrollbar it will be slightly wider. We presume this is rare and predictable.) - // The window inner height will be as specified, which means the available viewport will - // actually be smaller if there is a horizontal scrollbar. We assume that horizontal - // scrollbars are rare so this doesn't matter. - $js = <<evaluate_script($js); - $width += $offset['x']; - $height += $offset['y']; - } - - $this->getSession()->getDriver()->resizeWindow($width, $height); - } - - /** - * Waits for all the JS to be loaded. - * - * @return bool Whether any JS is still pending completion. - */ - public function wait_for_pending_js() { - return static::wait_for_pending_js_in_session($this->getSession()); - } - - /** - * Waits for all the JS to be loaded. - * - * @param Session $session The Mink Session where JS can be run - * @return bool Whether any JS is still pending completion. - */ - public static function wait_for_pending_js_in_session(Session $session) { - if (!self::running_javascript_in_session($session)) { - // JS is not available therefore there is nothing to wait for. - return false; - } - - // We don't use behat_base::spin() here as we don't want to end up with an exception - // if the page & JSs don't finish loading properly. - for ($i = 0; $i < self::get_extended_timeout() * 10; $i++) { - $pending = ''; - try { - $jscode = trim(preg_replace('/\s+/', ' ', ' - return (function() { - if (document.readyState !== "complete") { - return "incomplete"; - } - - if (typeof M !== "object" || typeof M.util !== "object" || typeof M.util.pending_js === "undefined") { - return ""; - } - - return M.util.pending_js.join(":"); - })()')); - $pending = self::evaluate_script_in_session($session, $jscode); - } catch (NoSuchWindow $nsw) { - // We catch an exception here, in case we just closed the window we were interacting with. - // No javascript is running if there is no window right? - $pending = ''; - } catch (UnknownError $e) { - // M is not defined when the window or the frame don't exist anymore. - if (strstr($e->getMessage(), 'M is not defined') != false) { - $pending = ''; - } - } - - // If there are no pending JS we stop waiting. - if ($pending === '') { - return true; - } - - // 0.1 seconds. - usleep(100000); - } - - // Timeout waiting for JS to complete. It will be caught and forwarded to behat_hooks::i_look_for_exceptions(). - // It is unlikely that Javascript code of a page or an AJAX request needs more than get_extended_timeout() seconds - // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the - // number of JS pending code and JS completed code will not match and we will reach this point. - throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . - self::get_extended_timeout() . - ' seconds. There is a Javascript error or the code is extremely slow (' . $pending . - '). If you are using a slow machine, consider setting $CFG->behat_increasetimeout.'); - } - - /** - * Internal step definition to find exceptions, debugging() messages and PHP debug messages. - * - * Part of behat_hooks class as is part of the testing framework, is auto-executed - * after each step so no features will splicitly use it. - * - * @throws Exception Unknown type, depending on what we caught in the hook or basic \Exception. - * @see Moodle\BehatExtension\Tester\MoodleStepTester - */ - public function look_for_exceptions() { - // Wrap in try in case we were interacting with a closed window. - try { - - // Exceptions. - $exceptionsxpath = "//div[@data-rel='fatalerror']"; - // Debugging messages. - $debuggingxpath = "//div[@data-rel='debugging']"; - // PHP debug messages. - $phperrorxpath = "//div[@data-rel='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)) { - // Check if we have recorded any errors in driver process. - $phperrors = behat_get_shutdown_process_errors(); - if (!empty($phperrors)) { - foreach ($phperrors as $error) { - $errnostring = behat_get_error_string($error['type']); - $msgs[] = $errnostring . ": " .$error['message'] . " at " . $error['file'] . ": " . $error['line']; - } - $msg = "PHP errors found:\n" . implode("\n", $msgs); - throw new \Exception(htmlentities($msg)); - } - - return; - } - - // Exceptions. - if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) { - - // Getting the debugging info and the backtrace. - $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error'); - // If errorinfoboxes is empty, try find alert-danger (bootstrap4) class. - if (empty($errorinfoboxes)) { - $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-danger'); - } - // If errorinfoboxes is empty, try find notifytiny (original) class. - if (empty($errorinfoboxes)) { - $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny'); - } - - // If errorinfoboxes is empty, try find ajax/JS exception in dialogue. - if (empty($errorinfoboxes)) { - $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.moodle-exception-message'); - - // If ajax/JS exception. - if ($errorinfoboxes) { - $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()); - } - - } else { - $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" . - $this->get_debug_text($errorinfoboxes[1]->getHtml()); - } - - $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo; - throw new \Exception(html_entity_decode($msg)); - } - - // Debugging messages. - if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) { - $msgs = array(); - foreach ($debuggingmessages as $debuggingmessage) { - $msgs[] = $this->get_debug_text($debuggingmessage->getHtml()); - } - $msg = "debugging() message/s found:\n" . implode("\n", $msgs); - throw new \Exception(html_entity_decode($msg)); - } - - // PHP debug messages. - if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) { - - $msgs = array(); - foreach ($phpmessages as $phpmessage) { - $msgs[] = $this->get_debug_text($phpmessage->getHtml()); - } - $msg = "PHP debug message/s found:\n" . implode("\n", $msgs); - throw new \Exception(html_entity_decode($msg)); - } - - // 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($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(); - foreach ($backtraces[0] as $backtrace) { - $msgs[] = $backtrace . '()'; - } - $msg = "Other backtraces found:\n" . implode("\n", $msgs); - throw new \Exception(htmlentities($msg)); - } - } - - } catch (NoSuchWindow $e) { - // If we were interacting with a popup window it will not exists after closing it. - } catch (DriverException $e) { - // Same reason as above. - } - } - - /** - * Converts HTML tags to line breaks to display the info in CLI - * - * @param string $html - * @return string - */ - protected function get_debug_text($html) { - - // Replacing HTML tags for new lines and keeping only the text. - $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html); - return preg_replace("/(\n)+/s", "\n", $notags); - } - - /** - * Helper function to execute api in a given context. - * - * @param string $contextapi context in which api is defined. - * @param array $params list of params to pass. - * @throws Exception - */ - protected function execute($contextapi, $params = array()) { - if (!is_array($params)) { - $params = array($params); - } - - // Get required context and execute the api. - $contextapi = explode("::", $contextapi); - $context = behat_context_helper::get($contextapi[0]); - call_user_func_array(array($context, $contextapi[1]), $params); - - // NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results. - // Don't make them optional for performance reasons. - - // Wait for pending js. - $this->wait_for_pending_js(); - - // Look for exceptions. - $this->look_for_exceptions(); - } - - /** - * Get the actual user in the behat session (note $USER does not correspond to the behat session's user). - * @return mixed - * @throws coding_exception - */ - protected function get_session_user() { - global $DB; - - $sid = $this->getSession()->getCookie('MoodleSession'); - if (empty($sid)) { - throw new coding_exception('failed to get moodle session'); - } - $userid = $DB->get_field('sessions', 'userid', ['sid' => $sid]); - if (empty($userid)) { - throw new coding_exception('failed to get user from seession id '.$sid); - } - return $DB->get_record('user', ['id' => $userid]); - } - - /** - * Set current $USER, reset access cache. - * - * In some cases, behat will execute the code as admin but in many cases we need to set an specific user as some - * API's might rely on the logged user to take some action. - * - * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid - */ - public static function set_user($user = null) { - global $DB; - - if (is_object($user)) { - $user = clone($user); - } else if (!$user) { - // Assign valid data to admin user (some generator-related code needs a valid user). - $user = $DB->get_record('user', array('username' => 'admin')); - } else { - $user = $DB->get_record('user', array('id' => $user)); - } - unset($user->description); - unset($user->access); - unset($user->preference); - - // Ensure session is empty, as it may contain caches and user specific info. - \core\session\manager::init_empty_session(); - - \core\session\manager::set_user($user); - } - /** - * Trigger click on node via javascript instead of actually clicking on it via pointer. - * - * This function resolves the issue of nested elements with click listeners or links - in these cases clicking via - * the pointer may accidentally cause a click on the wrong element. - * Example of issue: clicking to expand navigation nodes when the config value linkadmincategories is enabled. - * @param NodeElement $node - */ - protected function js_trigger_click($node) { - if (!$this->running_javascript()) { - $node->click(); - } - $this->ensure_node_is_visible($node); // Ensures hidden elements can't be clicked. - $xpath = $node->getXpath(); - $driver = $this->getSession()->getDriver(); - if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) { - $script = "Syn.click({{ELEMENT}})"; - $driver->triggerSynScript($xpath, $script); - } else { - $driver->click($xpath); - } - } - - /** - * Convert page names to URLs for steps like 'When I am on the "[page name]" page'. - * - * You should override this as appropriate for your plugin. The method - * {@link behat_navigation::resolve_core_page_url()} is a good example. - * - * Your overridden method should document the recognised page types with - * a table like this: - * - * Recognised page names are: - * | Page | Description | - * - * @param string $page name of the page, with the component name removed e.g. 'Admin notification'. - * @return moodle_url the corresponding URL. - * @throws Exception with a meaningful error message if the specified page cannot be found. - */ - protected function resolve_page_url(string $page): moodle_url { - throw new Exception('Component "' . get_class($this) . - '" does not support the generic \'When I am on the "' . $page . - '" page\' navigation step.'); - } - - /** - * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. - * - * A typical example might be: - * When I am on the "Test quiz" "mod_quiz > Responses report" page - * which would cause this method in behat_mod_quiz to be called with - * arguments 'Responses report', 'Test quiz'. - * - * You should override this as appropriate for your plugin. The method - * {@link behat_navigation::resolve_core_page_instance_url()} is a good example. - * - * Your overridden method should document the recognised page types with - * a table like this: - * - * Recognised page names are: - * | Type | identifier meaning | Description | - * - * @param string $type identifies which type of page this is, e.g. 'Attempt review'. - * @param string $identifier identifies the particular page, e.g. 'Test quiz > student > Attempt 1'. - * @return moodle_url the corresponding URL. - * @throws Exception with a meaningful error message if the specified page cannot be found. - */ - protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { - throw new Exception('Component "' . get_class($this) . - '" does not support the generic \'When I am on the "' . $identifier . - '" "' . $type . '" page\' navigation step.'); - } - - /** - * Gets the required timeout in seconds. - * - * @param int $timeout One of the TIMEOUT constants - * @return int Actual timeout (in seconds) - */ - protected static function get_real_timeout(int $timeout) : int { - global $CFG; - if (!empty($CFG->behat_increasetimeout)) { - return $timeout * $CFG->behat_increasetimeout; - } else { - return $timeout; - } - } - - /** - * Gets the default timeout. - * - * The timeout for each Behat step (load page, wait for an element to load...). - * - * @return int Timeout in seconds - */ - public static function get_timeout() : int { - return self::get_real_timeout(6); - } - - /** - * Gets the reduced timeout. - * - * A reduced timeout for cases where self::get_timeout() is too much - * and a simple $this->getSession()->getPage()->find() could not - * be enough. - * - * @return int Timeout in seconds - */ - public static function get_reduced_timeout() : int { - return self::get_real_timeout(2); - } - - /** - * Gets the extended timeout. - * - * A longer timeout for cases where the normal timeout is not enough. - * - * @return int Timeout in seconds - */ - public static function get_extended_timeout() : int { - return self::get_real_timeout(10); - } - - /** - * Return a list of the exact named selectors for the component. - * - * Named selectors are what make Behat steps like - * Then I should see "Useful text" in the "General" "fieldset" - * work. Here, "fieldset" is the named selector, and "General" is the locator. - * - * If you override this method in your plugin (e.g. mod_mymod), to define - * new selectors specific to your plugin. For example, if you returned - * new behat_component_named_selector('Thingy', - * [".//some/xpath//img[contains(@alt, %locator%)]/.."]) - * then - * Then I should see "Useful text" in the "Whatever" "mod_mymod > Thingy" - * would work. - * - * This method should return a list of {@link behat_component_named_selector} and - * the docs on that class explain how it works. - * - * @return behat_component_named_selector[] - */ - public static function get_exact_named_selectors(): array { - return []; - } - - /** - * Return a list of the partial named selectors for the component. - * - * Like the exact named selectors above, but the locator only - * needs to match part of the text. For example, the standard - * "button" is a partial selector, so: - * When I click "Save" "button" - * will activate "Save changes". - * - * @return behat_component_named_selector[] - */ - public static function get_partial_named_selectors(): array { - return []; - } - - /** - * Return a list of the Mink named replacements for the component. - * - * Named replacements allow you to define parts of an xpath that can be reused multiple times, or in multiple - * xpaths. - * - * This method should return a list of {@link behat_component_named_replacement} and the docs on that class explain - * how it works. - * - * @return behat_component_named_replacement[] - */ - public static function get_named_replacements(): array { - return []; - } - - /** - * Evaluate the supplied script in the current session, returning the result. - * - * @param string $script - * @return mixed - */ - public function evaluate_script(string $script) { - return self::evaluate_script_in_session($this->getSession(), $script); - } - - /** - * Evaluate the supplied script in the specified session, returning the result. - * - * @param Session $session - * @param string $script - * @return mixed - */ - public static function evaluate_script_in_session(Session $session, string $script) { - self::require_javascript_in_session($session); - - return $session->evaluateScript($script); - } - - /** - * Execute the supplied script in the current session. - * - * No result will be returned. - * - * @param string $script - */ - public function execute_script(string $script): void { - self::execute_script_in_session($this->getSession(), $script); - } - - /** - * Excecute the supplied script in the specified session. - * - * No result will be returned. - * - * @param Session $session - * @param string $script - */ - public static function execute_script_in_session(Session $session, string $script): void { - self::require_javascript_in_session($session); - - $session->executeScript($script); - } - - /** - * Get the session key for the current session via Javascript. - * - * @return string - */ - public function get_sesskey(): string { - $script = <<evaluate_script($script); - } + // All of the functionality of behat_base is shared with form fields via the behat_session_trait trait. + use behat_session_trait; } diff --git a/lib/behat/classes/behat_session_interface.php b/lib/behat/classes/behat_session_interface.php new file mode 100644 index 00000000000..2746070974c --- /dev/null +++ b/lib/behat/classes/behat_session_interface.php @@ -0,0 +1,87 @@ +. + +/** + * The Interface for a behat root context. + * + * @package core + * @category test + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * The Interface for a behat root context. + * + * This interface should be implemented by the behat_base context, and behat form fields, and it should be paired with + * the behat_session_trait. + * + * It should not be necessary to implement this interface, and the behat_session_trait trait in normal circumstances. + * + * @package core + * @category test + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +interface behat_session_interface { + /** + * Small timeout. + * + * A reduced timeout for cases where self::TIMEOUT is too much + * and a simple $this->getSession()->getPage()->find() could not + * be enough. + * + * @deprecated since Moodle 3.7 MDL-64979 - please use get_reduced_timeout() instead + * @todo MDL-64982 This will be deleted in Moodle 3.11 + * @see behat_base::get_reduced_timeout() + */ + const REDUCED_TIMEOUT = 2; + + /** + * The timeout for each Behat step (load page, wait for an element to load...). + * + * @deprecated since Moodle 3.7 MDL-64979 - please use get_timeout() instead + * @todo MDL-64982 This will be deleted in Moodle 3.11 + * @see behat_base::get_timeout() + */ + const TIMEOUT = 6; + + /** + * And extended timeout for specific cases. + * + * @deprecated since Moodle 3.7 MDL-64979 - please use get_extended_timeout() instead + * @todo MDL-64982 This will be deleted in Moodle 3.11 + * @see behat_base::get_extended_timeout() + */ + const EXTENDED_TIMEOUT = 10; + + /** + * The JS code to check that the page is ready. + * + * The document must be complete and either M.util.pending_js must be empty, or it must not be defined at all. + */ + const PAGE_READY_JS = "document.readyState === 'complete' && " . + "(typeof M !== 'object' || typeof M.util !== 'object' || " . + "typeof M.util.pending_js === 'undefined' || M.util.pending_js.length === 0)"; + + /** + * Returns the Mink session. + * + * @param string|null $name name of the session OR active session will be used + * @return \Behat\Mink\Session + */ + public function getSession($name = null); +} diff --git a/lib/behat/classes/behat_session_trait.php b/lib/behat/classes/behat_session_trait.php new file mode 100644 index 00000000000..5e9d3c0d35d --- /dev/null +++ b/lib/behat/classes/behat_session_trait.php @@ -0,0 +1,1312 @@ +. + +/** + * A trait containing functionality used by the behat base context, and form fields. + * + * @package core + * @category test + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +use Behat\Mink\Exception\DriverException; +use Behat\Mink\Exception\ExpectationException; +use Behat\Mink\Exception\ElementNotFoundException; +use Behat\Mink\Element\NodeElement; +use Behat\Mink\Element\Element; +use Behat\Mink\Session; + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/component_named_replacement.php'); +require_once(__DIR__ . '/component_named_selector.php'); + +// Alias the WebDriver\Key class to behat_keys to make future transition to a different WebDriver implementation +// easier. +class_alias('WebDriver\\Key', 'behat_keys'); + +/** + * A trait containing functionality used by the behat base context, and form fields. + * + * This trait should be used by the behat_base context, and behat form fields, and it should be paired with the + * behat_session_interface interface. + * + * It should not be necessary to use this trait, and the behat_session_interface interface in normal circumstances. + * + * @package core + * @category test + * @copyright 2020 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +trait behat_session_trait { + + /** + * Locates url, based on provided path. + * Override to provide custom routing mechanism. + * + * @see Behat\MinkExtension\Context\MinkContext + * @param string $path + * @return string + */ + protected function locate_path($path) { + $starturl = rtrim($this->getMinkParameter('base_url'), '/') . '/'; + return 0 !== strpos($path, 'http') ? $starturl . ltrim($path, '/') : $path; + } + + /** + * Returns the first matching element. + * + * @link http://mink.behat.org/#traverse-the-page-selectors + * @param string $selector The selector type (css, xpath, named...) + * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator... + * @param Exception $exception Otherwise we throw exception with generic info + * @param NodeElement $node Spins around certain DOM node instead of the whole page + * @param int $timeout Forces a specific time out (in seconds). + * @return NodeElement + */ + protected function find($selector, $locator, $exception = false, $node = false, $timeout = false) { + if ($selector === 'NodeElement' && is_a($locator, NodeElement::class)) { + // Support a NodeElement being passed in for use in step chaining. + return $locator; + } + + // Returns the first match. + $items = $this->find_all($selector, $locator, $exception, $node, $timeout); + return count($items) ? reset($items) : null; + } + + /** + * Returns all matching elements. + * + * Adapter to Behat\Mink\Element\Element::findAll() using the spin() method. + * + * @link http://mink.behat.org/#traverse-the-page-selectors + * @param string $selector The selector type (css, xpath, named...) + * @param mixed $locator It depends on the $selector, can be the xpath, a name, a css locator... + * @param Exception $exception Otherwise we throw expcetion with generic info + * @param NodeElement $container Restrict the search to just children of the specified container + * @param int $timeout Forces a specific time out (in seconds). If 0 is provided the default timeout will be applied. + * @return array NodeElements list + */ + protected function find_all($selector, $locator, $exception = false, $container = false, $timeout = false) { + // Throw exception, so dev knows it is not supported. + if ($selector === 'named') { + $exception = 'Using the "named" selector is deprecated as of 3.1. ' + .' Use the "named_partial" or use the "named_exact" selector instead.'; + throw new ExpectationException($exception, $this->getSession()); + } + + // Generic info. + if (!$exception) { + // With named selectors we can be more specific. + if (($selector == 'named_exact') || ($selector == 'named_partial')) { + $exceptiontype = $locator[0]; + $exceptionlocator = $locator[1]; + + // If we are in a @javascript session all contents would be displayed as HTML characters. + if ($this->running_javascript()) { + $locator[1] = html_entity_decode($locator[1], ENT_NOQUOTES); + } + + } else { + $exceptiontype = $selector; + $exceptionlocator = $locator; + } + + $exception = new ElementNotFoundException($this->getSession(), $exceptiontype, null, $exceptionlocator); + } + + // How much we will be waiting for the element to appear. + if (!$timeout) { + $timeout = self::get_timeout(); + $microsleep = false; + } else { + // Spinning each 0.1 seconds if the timeout was forced as we understand + // that is a special case and is good to refine the performance as much + // as possible. + $microsleep = true; + } + + // Normalise the values in order to perform the search. + [ + 'selector' => $selector, + 'locator' => $locator, + 'container' => $container, + ] = $this->normalise_selector($selector, $locator, $container ?: $this->getSession()->getPage()); + + // Waits for the node to appear if it exists, otherwise will timeout and throw the provided exception. + return $this->spin( + function() use ($selector, $locator, $container) { + return $container->findAll($selector, $locator); + }, [], $timeout, $exception, $microsleep + ); + } + + /** + * Normalise the locator and selector. + * + * @param string $selector The type of thing to search + * @param mixed $locator The locator value. Can be an array, but is more likely a string. + * @param Element $container An optional container to search within + * @return array The selector, locator, and container to search within + */ + public function normalise_selector(string $selector, $locator, Element $container): array { + // Check for specific transformations for this selector type. + $transformfunction = "transform_find_for_{$selector}"; + if (method_exists('behat_selectors', $transformfunction)) { + // A selector-specific transformation exists. + // Perform initial transformation of the selector within the current container. + [ + 'selector' => $selector, + 'locator' => $locator, + 'container' => $container, + ] = behat_selectors::{$transformfunction}($this, $locator, $container); + } + + // Normalise the css and xpath selector types. + if ('css_element' === $selector) { + $selector = 'css'; + } else if ('xpath_element' === $selector) { + $selector = 'xpath'; + } + + // Convert to a named selector where the selector type is not a known selector. + $converttonamed = !$this->getSession()->getSelectorsHandler()->isSelectorRegistered($selector); + $converttonamed = $converttonamed && 'xpath' !== $selector; + if ($converttonamed) { + if (behat_partial_named_selector::is_deprecated_selector($selector)) { + if ($replacement = behat_partial_named_selector::get_deprecated_replacement($selector)) { + error_log("The '{$selector}' selector has been replaced with {$replacement}"); + $selector = $replacement; + } + } else if (behat_exact_named_selector::is_deprecated_selector($selector)) { + if ($replacement = behat_exact_named_selector::get_deprecated_replacement($selector)) { + error_log("The '{$selector}' selector has been replaced with {$replacement}"); + $selector = $replacement; + } + } + + $allowedpartialselectors = behat_partial_named_selector::get_allowed_selectors(); + $allowedexactselectors = behat_exact_named_selector::get_allowed_selectors(); + if (isset($allowedpartialselectors[$selector])) { + $locator = behat_selectors::normalise_named_selector($allowedpartialselectors[$selector], $locator); + $selector = 'named_partial'; + } else if (isset($allowedexactselectors[$selector])) { + $locator = behat_selectors::normalise_named_selector($allowedexactselectors[$selector], $locator); + $selector = 'named_exact'; + } else { + throw new ExpectationException("The '{$selector}' selector type is not registered.", $this->getSession()->getDriver()); + } + } + + return [ + 'selector' => $selector, + 'locator' => $locator, + 'container' => $container, + ]; + } + + /** + * Send key presses straight to the currently active element. + * + * The `$keys` array contains a list of key values to send to the session as defined in the WebDriver and JsonWire + * specifications: + * - JsonWire: https://github.com/SeleniumHQ/selenium/wiki/JsonWireProtocol#sessionsessionidkeys + * - W3C WebDriver: https://www.w3.org/TR/webdriver/#keyboard-actions + * + * This may be a combination of typable characters, modifier keys, and other supported keypoints. + * + * The NULL_KEY should be used to release modifier keys. If the NULL_KEY is not used then modifier keys will remain + * in the pressed state. + * + * Example usage: + * + * behat_base::type_keys($this->getSession(), [behat_keys::SHIFT, behat_keys::TAB, behat_keys::NULL_KEY]); + * behat_base::type_keys($this->getSession(), [behat_keys::ENTER, behat_keys::NULL_KEY]); + * behat_base::type_keys($this->getSession(), [behat_keys::ESCAPE, behat_keys::NULL_KEY]); + * + * It can also be used to send text input, for example: + * + * behat_base::type_keys( + * $this->getSession(), + * ['D', 'o', ' ', 'y', 'o', 'u', ' ', 'p', 'l', 'a' 'y', ' ', 'G', 'o', '?', behat_base::NULL_KEY] + * ); + * + * + * Please note: This function does not use the element/sendKeys variants but sends keys straight to the browser. + * + * @param Session $session + * @param string[] $keys + */ + public static function type_keys(Session $session, array $keys): void { + $session->getDriver()->getWebDriverSession()->keys([ + 'value' => $keys, + ]); + } + + /** + * Finds DOM nodes in the page using named selectors. + * + * The point of using this method instead of Mink ones is the spin + * method of behat_base::find() that looks for the element until it + * is available or it timeouts, this avoids the false failures received + * when selenium tries to execute commands on elements that are not + * ready to be used. + * + * All steps that requires elements to be available before interact with + * them should use one of the find* methods. + * + * The methods calls requires a {'find_' . $elementtype}($locator) + * format, like find_link($locator), find_select($locator), + * find_button($locator)... + * + * @link http://mink.behat.org/#named-selectors + * @throws coding_exception + * @param string $name The name of the called method + * @param mixed $arguments + * @return NodeElement + */ + public function __call($name, $arguments) { + if (substr($name, 0, 5) === 'find_') { + return call_user_func_array([$this, 'find'], array_merge( + [substr($name, 5)], + $arguments + )); + } + + throw new coding_exception("The '{$name}' method does not exist"); + } + + /** + * Escapes the double quote character. + * + * Double quote is the argument delimiter, it can be escaped + * with a backslash, but we auto-remove this backslashes + * before the step execution, this method is useful when using + * arguments as arguments for other steps. + * + * @param string $string + * @return string + */ + public function escape($string) { + return str_replace('"', '\"', $string); + } + + /** + * Executes the passed closure until returns true or time outs. + * + * In most cases the document.readyState === 'complete' will be enough, but sometimes JS + * requires more time to be completely loaded or an element to be visible or whatever is required to + * perform some action on an element; this method receives a closure which should contain the + * required statements to ensure the step definition actions and assertions have all their needs + * satisfied and executes it until they are satisfied or it timeouts. Redirects the return of the + * closure to the caller. + * + * The closures requirements to work well with this spin method are: + * - Must return false, null or '' if something goes wrong + * - Must return something != false if finishes as expected, this will be the (mixed) value + * returned by spin() + * + * The arguments of the closure are mixed, use $args depending on your needs. + * + * You can provide an exception to give more accurate feedback to tests writers, otherwise the + * closure exception will be used, but you must provide an exception if the closure does not throw + * an exception. + * + * @throws Exception If it timeouts without receiving something != false from the closure + * @param Function|array|string $lambda The function to execute or an array passed to call_user_func (maps to a class method) + * @param mixed $args Arguments to pass to the closure + * @param int $timeout Timeout in seconds + * @param Exception $exception The exception to throw in case it time outs. + * @param bool $microsleep If set to true it'll sleep micro seconds rather than seconds. + * @return mixed The value returned by the closure + */ + protected function spin($lambda, $args = false, $timeout = false, $exception = false, $microsleep = false) { + + // Using default timeout which is pretty high. + if (!$timeout) { + $timeout = self::get_timeout(); + } + + $start = microtime(true); + $end = $start + $timeout; + + do { + // We catch the exception thrown by the step definition to execute it again. + try { + // We don't check with !== because most of the time closures will return + // direct Behat methods returns and we are not sure it will be always (bool)false + // if it just runs the behat method without returning anything $return == null. + if ($return = call_user_func($lambda, $this, $args)) { + return $return; + } + } catch (Exception $e) { + // We would use the first closure exception if no exception has been provided. + if (!$exception) { + $exception = $e; + } + } + + if (!$this->running_javascript()) { + break; + } + + usleep(100000); + + } while (microtime(true) < $end); + + // Using coding_exception as is a development issue if no exception has been provided. + if (!$exception) { + $exception = new coding_exception('spin method requires an exception if the callback does not throw an exception'); + } + + // Throwing exception to the user. + throw $exception; + } + + /** + * Gets a NodeElement based on the locator and selector type received as argument from steps definitions. + * + * Use behat_base::get_text_selector_node() for text-based selectors. + * + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $selectortype + * @param string $element + * @return NodeElement + */ + protected function get_selected_node($selectortype, $element) { + return $this->find($selectortype, $element); + } + + /** + * Gets a NodeElement based on the locator and selector type received as argument from steps definitions. + * + * @throws ElementNotFoundException Thrown by behat_base::find + * @param string $selectortype + * @param string $element + * @return NodeElement + */ + protected function get_text_selector_node($selectortype, $element) { + // Getting Mink selector and locator. + list($selector, $locator) = $this->transform_text_selector($selectortype, $element); + + // Returns the NodeElement. + return $this->find($selector, $locator); + } + + /** + * Gets the requested element inside the specified container. + * + * @throws ElementNotFoundException Thrown by behat_base::find + * @param mixed $selectortype The element selector type. + * @param mixed $element The element locator. + * @param mixed $containerselectortype The container selector type. + * @param mixed $containerelement The container locator. + * @return NodeElement + */ + protected function get_node_in_container($selectortype, $element, $containerselectortype, $containerelement) { + if ($containerselectortype === 'NodeElement' && is_a($containerelement, NodeElement::class)) { + // Support a NodeElement being passed in for use in step chaining. + $containernode = $containerelement; + $locatorexceptionmsg = $element; + } else { + // Gets the container, it will always be text based. + $containernode = $this->get_text_selector_node($containerselectortype, $containerelement); + $locatorexceptionmsg = $element . '" in the "' . $containerelement. '" "' . $containerselectortype. '"'; + } + + $exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg); + + return $this->find($selectortype, $element, $exception, $containernode); + } + + /** + * Transforms from step definition's argument style to Mink format. + * + * Mink has 3 different selectors css, xpath and named, where named + * selectors includes link, button, field... to simplify and group multiple + * steps in one we use the same interface, considering all link, buttons... + * at the same level as css selectors and xpath; this method makes the + * conversion from the arguments received by the steps to the selectors and locators + * required to interact with Mink. + * + * @throws ExpectationException + * @param string $selectortype It can be css, xpath or any of the named selectors. + * @param string $element The locator (or string) we are looking for. + * @return array Contains the selector and the locator expected by Mink. + */ + protected function transform_selector($selectortype, $element) { + // Here we don't know if an allowed text selector is being used. + $selectors = behat_selectors::get_allowed_selectors(); + if (!isset($selectors[$selectortype])) { + throw new ExpectationException('The "' . $selectortype . '" selector type does not exist', $this->getSession()); + } + + [ + 'selector' => $selector, + 'locator' => $locator, + ] = $this->normalise_selector($selectortype, $element, $this->getSession()->getPage()); + + return [$selector, $locator]; + } + + /** + * Transforms from step definition's argument style to Mink format. + * + * Delegates all the process to behat_base::transform_selector() checking + * the provided $selectortype. + * + * @throws ExpectationException + * @param string $selectortype It can be css, xpath or any of the named selectors. + * @param string $element The locator (or string) we are looking for. + * @return array Contains the selector and the locator expected by Mink. + */ + protected function transform_text_selector($selectortype, $element) { + + $selectors = behat_selectors::get_allowed_text_selectors(); + if (empty($selectors[$selectortype])) { + throw new ExpectationException('The "' . $selectortype . '" selector can not be used to select text nodes', $this->getSession()); + } + + return $this->transform_selector($selectortype, $element); + } + + /** + * Whether Javascript is available in the current Session. + * + * @return boolean + */ + protected function running_javascript() { + return self::running_javascript_in_session($this->getSession()); + } + + /** + * Require that javascript be available in the current Session. + * + * @throws DriverException + */ + protected function require_javascript() { + return self::require_javascript_in_session($this->getSession()); + } + + /** + * Whether Javascript is available in the specified Session. + * + * @param Session $session + * @return boolean + */ + protected static function running_javascript_in_session(Session $session): bool { + return get_class($session->getDriver()) !== 'Behat\Mink\Driver\GoutteDriver'; + } + + /** + * Require that javascript be available for the specified Session. + * + * @param Session $session + * @throws DriverException + */ + protected static function require_javascript_in_session(Session $session): void { + if (self::running_javascript_in_session($session)) { + return; + } + + throw new DriverException('Javascript is required'); + } + + /** + * Checks if the current page is part of the mobile app. + * + * @return bool True if it's in the app + */ + protected function is_in_app() : bool { + // Cannot be in the app if there's no @app tag on scenario. + if (!$this->has_tag('app')) { + return false; + } + + // Check on page to see if it's an app page. Safest way is to look for added JavaScript. + return $this->evaluate_script('return typeof window.behat') === 'object'; + } + + /** + * Spins around an element until it exists + * + * @throws ExpectationException + * @param string $locator + * @param string $selectortype + * @return void + */ + protected function ensure_element_exists($locator, $selectortype) { + // Exception if it timesout and the element is still there. + $msg = "The '{$locator}' element does not exist and should"; + $exception = new ExpectationException($msg, $this->getSession()); + + // Normalise the values in order to perform the search. + [ + 'selector' => $selector, + 'locator' => $locator, + 'container' => $container, + ] = $this->normalise_selector($selectortype, $locator, $this->getSession()->getPage()); + + // It will stop spinning once the find() method returns true. + $this->spin( + function() use ($selector, $locator, $container) { + if ($container->find($selector, $locator)) { + return true; + } + return false; + }, + [], + self::get_extended_timeout(), + $exception, + true + ); + } + + /** + * Spins until the element does not exist + * + * @throws ExpectationException + * @param string $locator + * @param string $selectortype + * @return void + */ + protected function ensure_element_does_not_exist($locator, $selectortype) { + // Exception if it timesout and the element is still there. + $msg = "The '{$locator}' element exists and should not exist"; + $exception = new ExpectationException($msg, $this->getSession()); + + // Normalise the values in order to perform the search. + [ + 'selector' => $selector, + 'locator' => $locator, + 'container' => $container, + ] = $this->normalise_selector($selectortype, $locator, $this->getSession()->getPage()); + + // It will stop spinning once the find() method returns false. + $this->spin( + function() use ($selector, $locator, $container) { + if ($container->find($selector, $locator)) { + return false; + } + return true; + }, + // Note: We cannot use $this because the find will then be $this->find(), which leads us to a nested spin(). + // We cannot nest spins because the outer spin times out before the inner spin completes. + [], + self::get_extended_timeout(), + $exception, + true + ); + } + + /** + * Ensures that the provided node is visible and we can interact with it. + * + * @throws ExpectationException + * @param NodeElement $node + * @return void Throws an exception if it times out without the element being visible + */ + protected function ensure_node_is_visible($node) { + + if (!$this->running_javascript()) { + return; + } + + // Exception if it timesout and the element is still there. + $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible'; + $exception = new ExpectationException($msg, $this->getSession()); + + // It will stop spinning once the isVisible() method returns true. + $this->spin( + function($context, $args) { + if ($args->isVisible()) { + return true; + } + return false; + }, + $node, + self::get_extended_timeout(), + $exception, + true + ); + } + + /** + * Ensures that the provided node has a attribute value set. This step can be used to check if specific + * JS has finished modifying the node. + * + * @throws ExpectationException + * @param NodeElement $node + * @param string $attribute attribute name + * @param string $attributevalue attribute value to check. + * @return void Throws an exception if it times out without the element being visible + */ + protected function ensure_node_attribute_is_set($node, $attribute, $attributevalue) { + + if (!$this->running_javascript()) { + return; + } + + // Exception if it timesout and the element is still there. + $msg = 'The "' . $node->getXPath() . '" xpath node is not visible and it should be visible'; + $exception = new ExpectationException($msg, $this->getSession()); + + // It will stop spinning once the $args[1]) == $args[2], and method returns true. + $this->spin( + function($context, $args) { + if ($args[0]->getAttribute($args[1]) == $args[2]) { + return true; + } + return false; + }, + array($node, $attribute, $attributevalue), + self::get_extended_timeout(), + $exception, + true + ); + } + + /** + * Ensures that the provided element is visible and we can interact with it. + * + * Returns the node in case other actions are interested in using it. + * + * @throws ExpectationException + * @param string $element + * @param string $selectortype + * @return NodeElement Throws an exception if it times out without being visible + */ + protected function ensure_element_is_visible($element, $selectortype) { + + if (!$this->running_javascript()) { + return; + } + + $node = $this->get_selected_node($selectortype, $element); + $this->ensure_node_is_visible($node); + + return $node; + } + + /** + * Ensures that all the page's editors are loaded. + * + * @deprecated since Moodle 2.7 MDL-44084 - please do not use this function any more. + * @throws ElementNotFoundException + * @throws ExpectationException + * @return void + */ + protected function ensure_editors_are_loaded() { + global $CFG; + + if (empty($CFG->behat_usedeprecated)) { + debugging('Function behat_base::ensure_editors_are_loaded() is deprecated. It is no longer required.'); + } + return; + } + + /** + * Checks if the current scenario, or its feature, has a specified tag. + * + * @param string $tag Tag to check + * @return bool True if the tag exists in scenario or feature + */ + public function has_tag(string $tag) : bool { + return array_key_exists($tag, behat_hooks::get_tags_for_scenario()); + } + + /** + * Change browser window size. + * - small: 640x480 + * - medium: 1024x768 + * - large: 2560x1600 + * + * @param string $windowsize size of window. + * @param bool $viewport If true, changes viewport rather than window size + * @throws ExpectationException + */ + protected function resize_window($windowsize, $viewport = false) { + // Non JS don't support resize window. + if (!$this->running_javascript()) { + return; + } + + switch ($windowsize) { + case "small": + $width = 1024; + $height = 768; + break; + case "medium": + $width = 1366; + $height = 768; + break; + case "large": + $width = 2560; + $height = 1600; + break; + default: + preg_match('/^(\d+x\d+)$/', $windowsize, $matches); + if (empty($matches) || (count($matches) != 2)) { + throw new ExpectationException("Invalid screen size, can't resize", $this->getSession()); + } + $size = explode('x', $windowsize); + $width = (int) $size[0]; + $height = (int) $size[1]; + } + if ($viewport) { + // When setting viewport size, we set it so that the document width will be exactly + // as specified, assuming that there is a vertical scrollbar. (In cases where there is + // no scrollbar it will be slightly wider. We presume this is rare and predictable.) + // The window inner height will be as specified, which means the available viewport will + // actually be smaller if there is a horizontal scrollbar. We assume that horizontal + // scrollbars are rare so this doesn't matter. + $js = <<evaluate_script($js); + $width += $offset['x']; + $height += $offset['y']; + } + + $this->getSession()->getDriver()->resizeWindow($width, $height); + } + + /** + * Waits for all the JS to be loaded. + * + * @return bool Whether any JS is still pending completion. + */ + public function wait_for_pending_js() { + return static::wait_for_pending_js_in_session($this->getSession()); + } + + /** + * Waits for all the JS to be loaded. + * + * @param Session $session The Mink Session where JS can be run + * @return bool Whether any JS is still pending completion. + */ + public static function wait_for_pending_js_in_session(Session $session) { + if (!self::running_javascript_in_session($session)) { + // JS is not available therefore there is nothing to wait for. + return false; + } + + // We don't use behat_base::spin() here as we don't want to end up with an exception + // if the page & JSs don't finish loading properly. + for ($i = 0; $i < self::get_extended_timeout() * 10; $i++) { + $pending = ''; + try { + $jscode = trim(preg_replace('/\s+/', ' ', ' + return (function() { + if (document.readyState !== "complete") { + return "incomplete"; + } + + if (typeof M !== "object" || typeof M.util !== "object" || typeof M.util.pending_js === "undefined") { + return ""; + } + + return M.util.pending_js.join(":"); + })()')); + $pending = self::evaluate_script_in_session($session, $jscode); + } catch (NoSuchWindow $nsw) { + // We catch an exception here, in case we just closed the window we were interacting with. + // No javascript is running if there is no window right? + $pending = ''; + } catch (UnknownError $e) { + // M is not defined when the window or the frame don't exist anymore. + if (strstr($e->getMessage(), 'M is not defined') != false) { + $pending = ''; + } + } + + // If there are no pending JS we stop waiting. + if ($pending === '') { + return true; + } + + // 0.1 seconds. + usleep(100000); + } + + // Timeout waiting for JS to complete. It will be caught and forwarded to behat_hooks::i_look_for_exceptions(). + // It is unlikely that Javascript code of a page or an AJAX request needs more than get_extended_timeout() seconds + // to be loaded, although when pages contains Javascript errors M.util.js_complete() can not be executed, so the + // number of JS pending code and JS completed code will not match and we will reach this point. + throw new \Exception('Javascript code and/or AJAX requests are not ready after ' . + self::get_extended_timeout() . + ' seconds. There is a Javascript error or the code is extremely slow (' . $pending . + '). If you are using a slow machine, consider setting $CFG->behat_increasetimeout.'); + } + + /** + * Internal step definition to find exceptions, debugging() messages and PHP debug messages. + * + * Part of behat_hooks class as is part of the testing framework, is auto-executed + * after each step so no features will splicitly use it. + * + * @throws Exception Unknown type, depending on what we caught in the hook or basic \Exception. + * @see Moodle\BehatExtension\Tester\MoodleStepTester + */ + public function look_for_exceptions() { + // Wrap in try in case we were interacting with a closed window. + try { + + // Exceptions. + $exceptionsxpath = "//div[@data-rel='fatalerror']"; + // Debugging messages. + $debuggingxpath = "//div[@data-rel='debugging']"; + // PHP debug messages. + $phperrorxpath = "//div[@data-rel='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)) { + // Check if we have recorded any errors in driver process. + $phperrors = behat_get_shutdown_process_errors(); + if (!empty($phperrors)) { + foreach ($phperrors as $error) { + $errnostring = behat_get_error_string($error['type']); + $msgs[] = $errnostring . ": " .$error['message'] . " at " . $error['file'] . ": " . $error['line']; + } + $msg = "PHP errors found:\n" . implode("\n", $msgs); + throw new \Exception(htmlentities($msg)); + } + + return; + } + + // Exceptions. + if ($errormsg = $this->getSession()->getPage()->find('xpath', $exceptionsxpath)) { + + // Getting the debugging info and the backtrace. + $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-error'); + // If errorinfoboxes is empty, try find alert-danger (bootstrap4) class. + if (empty($errorinfoboxes)) { + $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.alert-danger'); + } + // If errorinfoboxes is empty, try find notifytiny (original) class. + if (empty($errorinfoboxes)) { + $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.notifytiny'); + } + + // If errorinfoboxes is empty, try find ajax/JS exception in dialogue. + if (empty($errorinfoboxes)) { + $errorinfoboxes = $this->getSession()->getPage()->findAll('css', 'div.moodle-exception-message'); + + // If ajax/JS exception. + if ($errorinfoboxes) { + $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()); + } + + } else { + $errorinfo = $this->get_debug_text($errorinfoboxes[0]->getHtml()) . "\n" . + $this->get_debug_text($errorinfoboxes[1]->getHtml()); + } + + $msg = "Moodle exception: " . $errormsg->getText() . "\n" . $errorinfo; + throw new \Exception(html_entity_decode($msg)); + } + + // Debugging messages. + if ($debuggingmessages = $this->getSession()->getPage()->findAll('xpath', $debuggingxpath)) { + $msgs = array(); + foreach ($debuggingmessages as $debuggingmessage) { + $msgs[] = $this->get_debug_text($debuggingmessage->getHtml()); + } + $msg = "debugging() message/s found:\n" . implode("\n", $msgs); + throw new \Exception(html_entity_decode($msg)); + } + + // PHP debug messages. + if ($phpmessages = $this->getSession()->getPage()->findAll('xpath', $phperrorxpath)) { + + $msgs = array(); + foreach ($phpmessages as $phpmessage) { + $msgs[] = $this->get_debug_text($phpmessage->getHtml()); + } + $msg = "PHP debug message/s found:\n" . implode("\n", $msgs); + throw new \Exception(html_entity_decode($msg)); + } + + // 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($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(); + foreach ($backtraces[0] as $backtrace) { + $msgs[] = $backtrace . '()'; + } + $msg = "Other backtraces found:\n" . implode("\n", $msgs); + throw new \Exception(htmlentities($msg)); + } + } + + } catch (NoSuchWindow $e) { + // If we were interacting with a popup window it will not exists after closing it. + } catch (DriverException $e) { + // Same reason as above. + } + } + + /** + * Converts HTML tags to line breaks to display the info in CLI + * + * @param string $html + * @return string + */ + protected function get_debug_text($html) { + + // Replacing HTML tags for new lines and keeping only the text. + $notags = preg_replace('/<+\s*\/*\s*([A-Z][A-Z0-9]*)\b[^>]*\/*\s*>*/i', "\n", $html); + return preg_replace("/(\n)+/s", "\n", $notags); + } + + /** + * Helper function to execute api in a given context. + * + * @param string $contextapi context in which api is defined. + * @param array $params list of params to pass. + * @throws Exception + */ + protected function execute($contextapi, $params = array()) { + if (!is_array($params)) { + $params = array($params); + } + + // Get required context and execute the api. + $contextapi = explode("::", $contextapi); + $context = behat_context_helper::get($contextapi[0]); + call_user_func_array(array($context, $contextapi[1]), $params); + + // NOTE: Wait for pending js and look for exception are not optional, as this might lead to unexpected results. + // Don't make them optional for performance reasons. + + // Wait for pending js. + $this->wait_for_pending_js(); + + // Look for exceptions. + $this->look_for_exceptions(); + } + + /** + * Get the actual user in the behat session (note $USER does not correspond to the behat session's user). + * @return mixed + * @throws coding_exception + */ + protected function get_session_user() { + global $DB; + + $sid = $this->getSession()->getCookie('MoodleSession'); + if (empty($sid)) { + throw new coding_exception('failed to get moodle session'); + } + $userid = $DB->get_field('sessions', 'userid', ['sid' => $sid]); + if (empty($userid)) { + throw new coding_exception('failed to get user from seession id '.$sid); + } + return $DB->get_record('user', ['id' => $userid]); + } + + /** + * Set current $USER, reset access cache. + * + * In some cases, behat will execute the code as admin but in many cases we need to set an specific user as some + * API's might rely on the logged user to take some action. + * + * @param null|int|stdClass $user user record, null or 0 means non-logged-in, positive integer means userid + */ + public static function set_user($user = null) { + global $DB; + + if (is_object($user)) { + $user = clone($user); + } else if (!$user) { + // Assign valid data to admin user (some generator-related code needs a valid user). + $user = $DB->get_record('user', array('username' => 'admin')); + } else { + $user = $DB->get_record('user', array('id' => $user)); + } + unset($user->description); + unset($user->access); + unset($user->preference); + + // Ensure session is empty, as it may contain caches and user specific info. + \core\session\manager::init_empty_session(); + + \core\session\manager::set_user($user); + } + /** + * Trigger click on node via javascript instead of actually clicking on it via pointer. + * + * This function resolves the issue of nested elements with click listeners or links - in these cases clicking via + * the pointer may accidentally cause a click on the wrong element. + * Example of issue: clicking to expand navigation nodes when the config value linkadmincategories is enabled. + * @param NodeElement $node + */ + protected function js_trigger_click($node) { + if (!$this->running_javascript()) { + $node->click(); + } + $this->ensure_node_is_visible($node); // Ensures hidden elements can't be clicked. + $xpath = $node->getXpath(); + $driver = $this->getSession()->getDriver(); + if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) { + $script = "Syn.click({{ELEMENT}})"; + $driver->triggerSynScript($xpath, $script); + } else { + $driver->click($xpath); + } + } + + /** + * Convert page names to URLs for steps like 'When I am on the "[page name]" page'. + * + * You should override this as appropriate for your plugin. The method + * {@link behat_navigation::resolve_core_page_url()} is a good example. + * + * Your overridden method should document the recognised page types with + * a table like this: + * + * Recognised page names are: + * | Page | Description | + * + * @param string $page name of the page, with the component name removed e.g. 'Admin notification'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_url(string $page): moodle_url { + throw new Exception('Component "' . get_class($this) . + '" does not support the generic \'When I am on the "' . $page . + '" page\' navigation step.'); + } + + /** + * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. + * + * A typical example might be: + * When I am on the "Test quiz" "mod_quiz > Responses report" page + * which would cause this method in behat_mod_quiz to be called with + * arguments 'Responses report', 'Test quiz'. + * + * You should override this as appropriate for your plugin. The method + * {@link behat_navigation::resolve_core_page_instance_url()} is a good example. + * + * Your overridden method should document the recognised page types with + * a table like this: + * + * Recognised page names are: + * | Type | identifier meaning | Description | + * + * @param string $type identifies which type of page this is, e.g. 'Attempt review'. + * @param string $identifier identifies the particular page, e.g. 'Test quiz > student > Attempt 1'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_instance_url(string $type, string $identifier): moodle_url { + throw new Exception('Component "' . get_class($this) . + '" does not support the generic \'When I am on the "' . $identifier . + '" "' . $type . '" page\' navigation step.'); + } + + /** + * Gets the required timeout in seconds. + * + * @param int $timeout One of the TIMEOUT constants + * @return int Actual timeout (in seconds) + */ + protected static function get_real_timeout(int $timeout) : int { + global $CFG; + if (!empty($CFG->behat_increasetimeout)) { + return $timeout * $CFG->behat_increasetimeout; + } else { + return $timeout; + } + } + + /** + * Gets the default timeout. + * + * The timeout for each Behat step (load page, wait for an element to load...). + * + * @return int Timeout in seconds + */ + public static function get_timeout() : int { + return self::get_real_timeout(6); + } + + /** + * Gets the reduced timeout. + * + * A reduced timeout for cases where self::get_timeout() is too much + * and a simple $this->getSession()->getPage()->find() could not + * be enough. + * + * @return int Timeout in seconds + */ + public static function get_reduced_timeout() : int { + return self::get_real_timeout(2); + } + + /** + * Gets the extended timeout. + * + * A longer timeout for cases where the normal timeout is not enough. + * + * @return int Timeout in seconds + */ + public static function get_extended_timeout() : int { + return self::get_real_timeout(10); + } + + /** + * Return a list of the exact named selectors for the component. + * + * Named selectors are what make Behat steps like + * Then I should see "Useful text" in the "General" "fieldset" + * work. Here, "fieldset" is the named selector, and "General" is the locator. + * + * If you override this method in your plugin (e.g. mod_mymod), to define + * new selectors specific to your plugin. For example, if you returned + * new behat_component_named_selector('Thingy', + * [".//some/xpath//img[contains(@alt, %locator%)]/.."]) + * then + * Then I should see "Useful text" in the "Whatever" "mod_mymod > Thingy" + * would work. + * + * This method should return a list of {@link behat_component_named_selector} and + * the docs on that class explain how it works. + * + * @return behat_component_named_selector[] + */ + public static function get_exact_named_selectors(): array { + return []; + } + + /** + * Return a list of the partial named selectors for the component. + * + * Like the exact named selectors above, but the locator only + * needs to match part of the text. For example, the standard + * "button" is a partial selector, so: + * When I click "Save" "button" + * will activate "Save changes". + * + * @return behat_component_named_selector[] + */ + public static function get_partial_named_selectors(): array { + return []; + } + + /** + * Return a list of the Mink named replacements for the component. + * + * Named replacements allow you to define parts of an xpath that can be reused multiple times, or in multiple + * xpaths. + * + * This method should return a list of {@link behat_component_named_replacement} and the docs on that class explain + * how it works. + * + * @return behat_component_named_replacement[] + */ + public static function get_named_replacements(): array { + return []; + } + + /** + * Evaluate the supplied script in the current session, returning the result. + * + * @param string $script + * @return mixed + */ + public function evaluate_script(string $script) { + return self::evaluate_script_in_session($this->getSession(), $script); + } + + /** + * Evaluate the supplied script in the specified session, returning the result. + * + * @param Session $session + * @param string $script + * @return mixed + */ + public static function evaluate_script_in_session(Session $session, string $script) { + self::require_javascript_in_session($session); + + return $session->evaluateScript($script); + } + + /** + * Execute the supplied script in the current session. + * + * No result will be returned. + * + * @param string $script + */ + public function execute_script(string $script): void { + self::execute_script_in_session($this->getSession(), $script); + } + + /** + * Excecute the supplied script in the specified session. + * + * No result will be returned. + * + * @param Session $session + * @param string $script + */ + public static function execute_script_in_session(Session $session, string $script): void { + self::require_javascript_in_session($session); + + $session->executeScript($script); + } + + /** + * Get the session key for the current session via Javascript. + * + * @return string + */ + public function get_sesskey(): string { + $script = <<evaluate_script($script); + } +} From 7a2006b499e39abd2b98b5d0ec3f9cc57a1d81a7 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 17 Jun 2020 13:45:15 +0800 Subject: [PATCH 13/26] MDL-67668 behat: Add inplace editable field type This commit promotes the Inplace Editable field to a first-class form element by introducing a new partial selector for inplace editable fields, and teaching the field manager how to recognise these, then introducing a new field type which can handle setting values for this field. --- .../behat/tests/behat/inplaceeditable.feature | 30 ++++++++ lib/behat/behat_field_manager.php | 5 +- lib/behat/classes/partial_named_selector.php | 7 +- lib/behat/form_field/behat_form_field.php | 19 ++++- .../form_field/behat_form_inplaceeditable.php | 74 +++++++++++++++++++ 5 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 admin/tool/behat/tests/behat/inplaceeditable.feature create mode 100644 lib/behat/form_field/behat_form_inplaceeditable.php diff --git a/admin/tool/behat/tests/behat/inplaceeditable.feature b/admin/tool/behat/tests/behat/inplaceeditable.feature new file mode 100644 index 00000000000..014c5f5972b --- /dev/null +++ b/admin/tool/behat/tests/behat/inplaceeditable.feature @@ -0,0 +1,30 @@ +@tool_behat +Feature: Verify that the inplace editable field works as expected + In order to use behat step definitions + As a test write + I need to ensure that the inplace editable works in forms + + Background: + Given the following "course" exists: + | fullname | Course 1 | + | shortname | C1 | + And the following "activities" exist: + | activity | course | name | idnumber | + | forum | C1 | My first forum | forum1 | + | assign | C1 | My first assignment | assign1 | + | quiz | C1 | My first quiz | quiz1 | + And I log in as "admin" + And I am on "Course 1" course homepage with editing mode on + + @javascript + Scenario: Using an inplace editable updates the name of an activity + When I set the field "Edit title" in the "My first assignment" "activity" to "Coursework submission" + Then I should see "Coursework submission" + And I should not see "My first assignment" + But I should see "My first forum" + And I should see "My first quiz" + And I set the field "Edit title" in the "Coursework submission" "activity" to "My first assignment" + And I should not see "Coursework submission" + But I should see "My first assignment" + And I should see "My first forum" + And I should see "My first quiz" diff --git a/lib/behat/behat_field_manager.php b/lib/behat/behat_field_manager.php index 6d9b4c7a7ea..1a807a20a14 100644 --- a/lib/behat/behat_field_manager.php +++ b/lib/behat/behat_field_manager.php @@ -48,7 +48,6 @@ class behat_field_manager { * @return behat_form_field */ public static function get_form_field_from_label($label, RawMinkContext $context) { - // There are moodle form elements that are not directly related with // a basic HTML form field, we should also take care of them. // The DOM node. @@ -172,6 +171,10 @@ class behat_field_manager { } else if ($tagname == 'select') { // Select tag. return 'select'; + } else if ($tagname == 'span') { + if ($fieldnode->hasAttribute('data-inplaceeditable') && $fieldnode->getAttribute('data-inplaceeditable')) { + return 'inplaceeditable'; + } } // We can not provide a closer field type. diff --git a/lib/behat/classes/partial_named_selector.php b/lib/behat/classes/partial_named_selector.php index 18c1526c55b..74054eeb4a8 100644 --- a/lib/behat/classes/partial_named_selector.php +++ b/lib/behat/classes/partial_named_selector.php @@ -135,7 +135,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele */ protected static $moodleselectors = array( 'activity' => << << <<session; + } + /** * General constructor with the node and the session to interact with. diff --git a/lib/behat/form_field/behat_form_inplaceeditable.php b/lib/behat/form_field/behat_form_inplaceeditable.php new file mode 100644 index 00000000000..57cc8b77c94 --- /dev/null +++ b/lib/behat/form_field/behat_form_inplaceeditable.php @@ -0,0 +1,74 @@ +. + +/** + * Custom interaction with inplace editable elements. + * + * @package core_form + * @category test + * @copyright 2019 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. + +require_once(__DIR__ . '/behat_form_text.php'); + +/** + * Custom interaction with inplace editable elements. + * + * @package core_form + * @category test + * @copyright 2019 Andrew Nicols + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_form_inplaceeditable extends behat_form_text { + /** + * Sets the value to a field. + * + * @param string $value + * @return void + */ + public function set_value($value) { + // Require JS to run this step. + self::require_javascript(); + + // Click to enable editing. + self::execute( + 'behat_general::i_click_on_in_the', + [ + '[data-inplaceeditablelink]', + 'css_element', + $this->field, + 'NodeElement', + ] + ); + + // Note: It is not possible to use the NodeElement->keyDown() and related functions because + // this can trigger a focusOnElement call each time. + // Instead use the behat_base::type_keys() function. + + // The inplace editable selects all existing content on focus. + // Clear the existing value. + self::type_keys($this->session, [behat_keys::BACKSPACE]); + + // Type in the new value, followed by ENTER to save the value. + self::type_keys($this->session, array_merge( + str_split($value), + [behat_keys::ENTER] + )); + } +} From 94a492a0f48c248e641eb1b5e12b876840460f04 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Wed, 17 Jun 2020 13:45:37 +0800 Subject: [PATCH 14/26] MDL-67668 behat: Update tests for inplace editable field type --- .../behat/behat_block_site_main_menu.php | 13 +++++++++++ .../tests/behat/edit_activities.feature | 13 ++++++----- .../behat/behat_block_social_activities.php | 13 +++++++++++ .../tests/behat/edit_activities.feature | 5 +--- cohort/tests/behat/add_cohort.feature | 4 +--- .../tests/behat/edit_delete_sections.feature | 4 +--- .../tests/behat/edit_delete_sections.feature | 4 +--- .../tests/behat/activities_edit_name.feature | 15 ++++++------ course/tests/behat/behat_course.php | 22 +++++------------- .../tests/behat/edit_categories.feature | 9 +------- mod/book/tests/behat/edit_tags.feature | 12 +++++----- mod/forum/tests/behat/advanced_search.feature | 3 +-- mod/lti/tests/behat/renametool.feature | 4 +--- mod/quiz/tests/behat/behat_mod_quiz.php | 7 ++++-- .../editing_set_marks_no_attempts.feature | 23 +++++++++++-------- tag/tests/behat/collections.feature | 22 ++++-------------- tag/tests/behat/edit_tag.feature | 15 ++++-------- 17 files changed, 86 insertions(+), 102 deletions(-) diff --git a/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php index b80713c60c6..f99f479b5d9 100644 --- a/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php +++ b/blocks/site_main_menu/tests/behat/behat_block_site_main_menu.php @@ -150,4 +150,17 @@ class behat_block_site_main_menu extends behat_base { $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., $activityname)]"; $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']); } + + /** + * Return the list of partial named selectors. + * + * @return array + */ + public static function get_partial_named_selectors(): array { + return [ + new behat_component_named_selector('Activity', [ + "//*[contains(concat(' ',normalize-space(@class),' '),' block_site_main_menu ')]//li[contains(., %locator%)]" + ]), + ]; + } } diff --git a/blocks/site_main_menu/tests/behat/edit_activities.feature b/blocks/site_main_menu/tests/behat/edit_activities.feature index 0f6d8c28e37..51b60ea5500 100644 --- a/blocks/site_main_menu/tests/behat/edit_activities.feature +++ b/blocks/site_main_menu/tests/behat/edit_activities.feature @@ -6,15 +6,16 @@ Feature: Edit activities in main menu block @javascript Scenario: Edit name of acitivity in-place in site main menu block - Given I log in as "admin" + Given the following "activity" exists: + | activity | forum | + | course | Acceptance test site | + | name | My forum name | + | idnumber | forum | + And I log in as "admin" And I am on site homepage And I navigate to "Turn editing on" in current page administration And I add the "Main menu" block - When I add a "Forum" to section "0" and I fill the form with: - | Forum name | My forum name | - And I click on "Edit title" "link" in the "My forum name" activity in site main menu block - And I set the field "New name for activity My forum name" to "New forum name" - And I press the enter key + When I set the field "Edit title" in the "My forum name" "block_site_main_menu > Activity" to "New forum name" Then I should not see "My forum name" And I should see "New forum name" And I follow "New forum name" diff --git a/blocks/social_activities/tests/behat/behat_block_social_activities.php b/blocks/social_activities/tests/behat/behat_block_social_activities.php index 167b24d51a2..eb285231f83 100644 --- a/blocks/social_activities/tests/behat/behat_block_social_activities.php +++ b/blocks/social_activities/tests/behat/behat_block_social_activities.php @@ -158,4 +158,17 @@ class behat_block_social_activities extends behat_base { $xpath = "//*[contains(concat(' ',normalize-space(@class),' '),' block_social_activities ')]//li[contains(., $activityname)]"; $this->execute('behat_action_menu::i_open_the_action_menu_in', [$xpath, 'xpath_element']); } + + /** + * Return the list of partial named selectors. + * + * @return array + */ + public static function get_partial_named_selectors(): array { + return [ + new behat_component_named_selector('Activity', [ + "//*[contains(concat(' ',normalize-space(@class),' '),' block_social_activities ')]//li[contains(., %locator%)]", + ]), + ]; + } } diff --git a/blocks/social_activities/tests/behat/edit_activities.feature b/blocks/social_activities/tests/behat/edit_activities.feature index 984b73173a3..a21e848de71 100644 --- a/blocks/social_activities/tests/behat/edit_activities.feature +++ b/blocks/social_activities/tests/behat/edit_activities.feature @@ -25,9 +25,7 @@ Feature: Edit activities in social activities block And I click on "Add a new Forum" "link" in the "Add an activity or resource" "dialogue" And I set the field "Forum name" to "My forum name" And I press "Save and return to course" - And I click on "Edit title" "link" in the "My forum name" activity in social activities block - And I set the field "New name for activity My forum name" to "New forum name" - And I press the enter key + When I set the field "Edit title" in the "My forum name" "block_social_activities > Activity" to "New forum name" Then I should not see "My forum name" in the "Social activities" "block" And I should see "New forum name" And I follow "New forum name" @@ -84,4 +82,3 @@ Feature: Edit activities in social activities block And I should not see "My forum name" in the "Social activities" "block" And I click on "My forum name" "link" in the "Recent activity" "block" And I should see "My forum name" in the ".breadcrumb" "css_element" - And I log out diff --git a/cohort/tests/behat/add_cohort.feature b/cohort/tests/behat/add_cohort.feature index 9bc546951fa..c422c70f662 100644 --- a/cohort/tests/behat/add_cohort.feature +++ b/cohort/tests/behat/add_cohort.feature @@ -62,9 +62,7 @@ Feature: Add cohorts of users @javascript Scenario: Edit cohort name in-place When I follow "Cohorts" - And I click on "Edit cohort name" "link" in the "Test cohort name" "table_row" - And I set the field "New name for cohort Test cohort name" to "Students cohort" - And I press the enter key + And I set the field "Edit cohort name" to "Students cohort" Then I should not see "Test cohort name" And I should see "Students cohort" And I follow "Cohorts" diff --git a/course/format/topics/tests/behat/edit_delete_sections.feature b/course/format/topics/tests/behat/edit_delete_sections.feature index a1e205b3c19..573c7e50fcd 100644 --- a/course/format/topics/tests/behat/edit_delete_sections.feature +++ b/course/format/topics/tests/behat/edit_delete_sections.feature @@ -53,9 +53,7 @@ Feature: Sections can be edited and deleted in topics format @javascript Scenario: Inline edit section name in topics format - When I click on "Edit topic name" "link" in the "li#section-1" "css_element" - And I set the field "New name for topic Topic 1" to "Midterm evaluation" - And I press the enter key + When I set the field "Edit topic name" in the "li#section-1" "css_element" to "Midterm evaluation" Then I should not see "Topic 1" in the "region-main" "region" And "New name for topic" "field" should not exist And I should see "Midterm evaluation" in the "li#section-1" "css_element" diff --git a/course/format/weeks/tests/behat/edit_delete_sections.feature b/course/format/weeks/tests/behat/edit_delete_sections.feature index 5d8ff891130..6fdc14aef26 100644 --- a/course/format/weeks/tests/behat/edit_delete_sections.feature +++ b/course/format/weeks/tests/behat/edit_delete_sections.feature @@ -54,9 +54,7 @@ Feature: Sections can be edited and deleted in weeks format @javascript Scenario: Inline edit section name in weeks format - When I click on "Edit week name" "link" in the "li#section-1" "css_element" - And I set the field "New name for week 1 May - 7 May" to "Midterm evaluation" - And I press the enter key + When I set the field "Edit week name" in the "li#section-1" "css_element" to "Midterm evaluation" Then I should not see "1 May - 7 May" in the "region-main" "region" And "New name for week" "field" should not exist And I should see "Midterm evaluation" in the "li#section-1" "css_element" diff --git a/course/tests/behat/activities_edit_name.feature b/course/tests/behat/activities_edit_name.feature index 7372c1741a1..18217384e98 100644 --- a/course/tests/behat/activities_edit_name.feature +++ b/course/tests/behat/activities_edit_name.feature @@ -15,15 +15,16 @@ Feature: Edit activity name in-place And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | + And the following "activity" exists: + | course | C1 | + | activity | forum | + | name | Test forum name | + | description | Test forum description | + | idnumber | forum1 | When I log in as "teacher1" And I am on "Course 1" course homepage with editing mode on - And I add a "Forum" to section "1" and I fill the form with: - | Forum name | Test forum name | - | Description | Test forum description | # Rename activity - And I click on "Edit title" "link" in the "//div[contains(@class,'activityinstance') and contains(.,'Test forum name')]" "xpath_element" - And I set the field "New name for activity Test forum name" to "Good news" - And I press the enter key + And I set the field "Edit title" in the "Test forum name" "activity" to "Good news" Then I should not see "Test forum name" in the ".course-content" "css_element" And "New name for activity Test forum name" "field" should not exist And I should see "Good news" @@ -32,7 +33,7 @@ Feature: Edit activity name in-place And I should not see "Test forum name" # Cancel renaming And I click on "Edit title" "link" in the "//div[contains(@class,'activityinstance') and contains(.,'Good news')]" "xpath_element" - And I set the field "New name for activity Good news" to "Terrible news" + And I type "Terrible news" And I press the escape key And "New name for activity Good news" "field" should not exist And I should see "Good news" diff --git a/course/tests/behat/behat_course.php b/course/tests/behat/behat_course.php index 26cecd130b7..d06a8bf6c0d 100644 --- a/course/tests/behat/behat_course.php +++ b/course/tests/behat/behat_course.php @@ -852,22 +852,12 @@ class behat_course extends behat_base { * @param string $newactivityname */ public function i_change_activity_name_to($activityname, $newactivityname) { - - if (!$this->running_javascript()) { - throw new DriverException('Change activity name step is not available with Javascript disabled'); - } - - $activity = $this->escape($activityname); - - $this->execute('behat_course::i_click_on_in_the_activity', - array(get_string('edittitle'), "link", $activity) - ); - - // Adding chr(10) to save changes. - $this->execute('behat_forms::i_set_the_field_to', - array('title', $this->escape($newactivityname) . chr(10)) - ); - + $this->execute('behat_forms::i_set_the_field_in_container_to', [ + get_string('edittitle'), + $activityname, + 'activity', + $newactivityname + ]); } /** diff --git a/customfield/tests/behat/edit_categories.feature b/customfield/tests/behat/edit_categories.feature index 2b88046c088..b44a66163fc 100644 --- a/customfield/tests/behat/edit_categories.feature +++ b/customfield/tests/behat/edit_categories.feature @@ -12,7 +12,6 @@ Feature: Managers can manage categories for course custom fields Then I should see "Other fields" in the "#customfield_catlist" "css_element" And I navigate to "Reports > Logs" in site administration And I press "Get these logs" - And I log out Scenario: Edit a category name for custom course fields Given the following "custom field categories" exist: @@ -20,15 +19,12 @@ Feature: Managers can manage categories for course custom fields | Category for test | core_course | course | 0 | And I log in as "admin" And I navigate to "Courses > Course custom fields" in site administration - And I click on "Edit category name" "link" in the "//div[contains(@class,'categoryinstance') and contains(.,'Category for test')]" "xpath_element" - And I set the field "New value for Category for test" to "Good fields" - And I press the enter key + And I set the field "Edit category name" in the "//div[contains(@class,'categoryinstance') and contains(.,'Category for test')]" "xpath_element" to "Good fields" Then I should not see "Category for test" in the "#customfield_catlist" "css_element" And "New value for Category for test" "field" should not exist And I should see "Good fields" in the "#customfield_catlist" "css_element" And I navigate to "Reports > Logs" in site administration And I press "Get these logs" - And I log out Scenario: Delete a category for custom course fields Given the following "custom field categories" exist: @@ -46,7 +42,6 @@ Feature: Managers can manage categories for course custom fields Then I should not see "Test category" in the "#customfield_catlist" "css_element" And I navigate to "Reports > Logs" in site administration And I press "Get these logs" - And I log out Scenario: Move field in the course custom fields to another category Given the following "custom field categories" exist: @@ -78,7 +73,6 @@ Feature: Managers can manage categories for course custom fields And I press "Move \"Field1\"" And I follow "After field Field2" And "Field1" "text" should appear after "Field2" "text" - And I log out Scenario: Reorder course custom field categories Given the following "custom field categories" exist: @@ -108,4 +102,3 @@ Feature: Managers can manage categories for course custom fields And "Field1" "text" should appear after "Category1" "text" And "Category2" "text" should appear after "Field1" "text" And "Category3" "text" should appear after "Category2" "text" - And I log out diff --git a/mod/book/tests/behat/edit_tags.feature b/mod/book/tests/behat/edit_tags.feature index 938b07f9a2d..7b2b8c70e60 100644 --- a/mod/book/tests/behat/edit_tags.feature +++ b/mod/book/tests/behat/edit_tags.feature @@ -12,16 +12,16 @@ Feature: Edited book chapters handle tags correctly And the following "courses" exist: | fullname | shortname | format | | Course 1 | C1 | topics | + And the following "activity" exists: + | activity | book | + | course | C1 | + | idnumber | book1 | + | name | Test book | + | description | A book about dreams | And the following "course enrolments" exist: | user | course | role | | teacher1 | C1 | editingteacher | | student1 | C1 | student | - And I log in as "teacher1" - And I am on "Course 1" course homepage with editing mode on - And I add a "Book" to section "1" and I fill the form with: - | Name | Test book | - | Description | A book about dreams! | - And I log out Scenario: Book chapter edition of custom tags works as expected Given I log in as "teacher1" diff --git a/mod/forum/tests/behat/advanced_search.feature b/mod/forum/tests/behat/advanced_search.feature index 90dd16298c8..dbcca3b8797 100644 --- a/mod/forum/tests/behat/advanced_search.feature +++ b/mod/forum/tests/behat/advanced_search.feature @@ -130,8 +130,7 @@ Feature: The forum search allows users to perform advanced searches for forum po And I press "Search forums" And I should see "Advanced search" And I set the field "Is tagged with" to "SearchedTag" - And I click on "[data-value='SearchedTag']" "css_element" - And I press the escape key + And I press the enter key When I press "Search forums" Then I should see "My subject" And I should not see "Your subjective" diff --git a/mod/lti/tests/behat/renametool.feature b/mod/lti/tests/behat/renametool.feature index 30eed0702d0..48eee71af4c 100644 --- a/mod/lti/tests/behat/renametool.feature +++ b/mod/lti/tests/behat/renametool.feature @@ -21,9 +21,7 @@ Feature: Rename external tools via inline editing And I am on "Course 1" course homepage with editing mode on And I add a "External tool" to section "1" and I fill the form with: | Activity name | Test tool activity 1 | - And I click on "Edit title" "link" in the "li#section-1" "css_element" - And I set the field "New name for activity Test tool activity 1" to "Test tool activity renamed" - And I press the enter key + And I set the field "Edit title" in the "li#section-1" "css_element" to "Test tool activity renamed" And I navigate to "Setup > Gradebook setup" in the course gradebook Then I should not see "Test tool activity 1" And I should see "Test tool activity renamed" diff --git a/mod/quiz/tests/behat/behat_mod_quiz.php b/mod/quiz/tests/behat/behat_mod_quiz.php index 5db5ba702e7..417b26d370a 100644 --- a/mod/quiz/tests/behat/behat_mod_quiz.php +++ b/mod/quiz/tests/behat/behat_mod_quiz.php @@ -397,7 +397,8 @@ class behat_mod_quiz extends behat_question_base { $this->execute('behat_general::assert_page_contains_text', $this->escape(get_string('edittitleinstructions'))); - $this->execute('behat_forms::i_set_the_field_to', array('maxmark', $this->escape($newmark) . chr(10))); + $this->execute('behat_general::i_type', [$newmark]); + $this->execute('behat_general::i_press_named_key', ['', 'enter']); } /** @@ -653,7 +654,9 @@ class behat_mod_quiz extends behat_question_base { $this->execute('behat_general::assert_page_contains_text', $this->escape(get_string('edittitleinstructions'))); - $this->execute('behat_forms::i_set_the_field_to', array('section', $this->escape($sectionheading) . chr(10))); + $this->execute('behat_general::i_press_named_key', ['', 'backspace']); + $this->execute('behat_general::i_type', [$sectionheading]); + $this->execute('behat_general::i_press_named_key', ['', 'enter']); } /** diff --git a/mod/quiz/tests/behat/editing_set_marks_no_attempts.feature b/mod/quiz/tests/behat/editing_set_marks_no_attempts.feature index b4bee513a0b..47a382f1762 100644 --- a/mod/quiz/tests/behat/editing_set_marks_no_attempts.feature +++ b/mod/quiz/tests/behat/editing_set_marks_no_attempts.feature @@ -17,16 +17,19 @@ Feature: Edit quiz marks with no attempts And the following "activities" exist: | activity | name | course | idnumber | grade | decimalpoints | questiondecimalpoints | | quiz | Quiz 1 | C1 | quiz1 | 20 | 2 | -1 | - And I log in as "teacher1" - And I am on "Course 1" course homepage - And I add a "True/False" question to the "Quiz 1" quiz with: - | Question name | First question | - | Question text | Answer me | - | Default mark | 2.0 | - And I add a "True/False" question to the "Quiz 1" quiz with: - | Question name | Second question | - | Question text | Answer again | - | Default mark | 3.0 | + + And the following "question categories" exist: + | contextlevel | reference | name | + | Course | C1 | Test questions | + And the following "questions" exist: + | questioncategory | qtype | name | questiontext | + | Test questions | truefalse | First question | Answer me | + | Test questions | truefalse | Second question | Answer again | + And quiz "Quiz 1" contains the following questions: + | question | page | maxmark | + | First question | 1 | 2.0 | + | Second question | 1 | 3.0 | + And I am on the "Quiz 1" "mod_quiz > Edit" page logged in as "teacher1" @javascript Scenario: Set the max mark for a question. diff --git a/tag/tests/behat/collections.feature b/tag/tests/behat/collections.feature index 6cdb568b7c4..50bdd8bf9cb 100644 --- a/tag/tests/behat/collections.feature +++ b/tag/tests/behat/collections.feature @@ -28,15 +28,11 @@ Feature: Managers can create and manage tag collections Scenario: Adding tag collections When I follow "Hobbies" Then I should see "Nothing to display" - And I log out Scenario: Editing tag collections - When I click on "Edit tag collection name" "link" in the "//table[contains(@class,'tag-collections-table')]//tr[contains(.,'Hobbies')]" "xpath_element" - And I set the field "New name for tag collection Hobbies" to "Newname" - And I press the enter key + When I set the field "Edit tag collection name" in the "//table[contains(@class,'tag-collections-table')]//tr[contains(.,'Hobbies')]" "xpath_element" to "Newname" Then I should not see "Hobbies" And I should see "Newname" - And I log out Scenario: Resorting tag collections When I follow "Add tag collection" @@ -48,41 +44,34 @@ Feature: Managers can create and manage tag collections And "Blogging" "link" should appear before "Hobbies" "link" And I click on "Move down" "link" in the "Blogging" "table_row" And "Blogging" "link" should appear after "Hobbies" "link" - And I log out Scenario: Deleting tag collections When I click on "Delete" "link" in the "Hobbies" "table_row" Then I should see "Are you sure you want to delete tag collection \"Hobbies\"?" And I press "Yes" And I should not see "Hobbies" - And I log out Scenario: Assigning tag area to tag collection And I should see "User interests" in the "//table[contains(@class,'tag-collections-table')]//tr[contains(.,'Default collection')]" "xpath_element" And I should not see "User interests" in the "//table[contains(@class,'tag-collections-table')]//tr[contains(.,'Hobbies')]" "xpath_element" - When I click on "Change tag collection" "link" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" - And I set the field "Change tag collection of area User interests" to "Hobbies" + When I set the field "Change tag collection" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" to "Hobbies" Then I should not see "User interests" in the "//table[contains(@class,'tag-collections-table')]//tr[contains(.,'Default collection')]" "xpath_element" And I should see "User interests" in the "//table[contains(@class,'tag-collections-table')]//tr[contains(.,'Hobbies')]" "xpath_element" And I should see "Hobbies" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" - And I log out Scenario: Disabling tag areas When I click on "Disable" "link" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" And I should not see "User interests" in the "table.tag-collections-table" "css_element" And I click on "Enable" "link" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" And I should see "User interests" in the "//table[contains(@class,'tag-collections-table')]//tr[contains(.,'Default collection')]" "xpath_element" - And I log out Scenario: Deleting non-empty tag collections - When I click on "Change tag collection" "link" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" - And I set the field "Change tag collection of area User interests" to "Hobbies" + When I set the field "Change tag collection" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" to "Hobbies" And I click on "Delete" "link" in the "Hobbies" "table_row" Then I should see "Are you sure you want to delete tag collection \"Hobbies\"?" And I press "Yes" And I should not see "Hobbies" And I should see "User interests" in the "//table[contains(@class,'tag-collections-table')]//tr[contains(.,'Default collection')]" "xpath_element" - And I log out Scenario: Moving tags when changing tag collections And I open my profile in edit mode @@ -90,8 +79,7 @@ Feature: Managers can create and manage tag collections And I set the field "List of interests" to "Swimming, Tag0, Tag3" And I press "Update profile" And I navigate to "Appearance > Manage tags" in site administration - When I click on "Change tag collection" "link" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" - And I set the field "Change tag collection of area User interests" to "Hobbies" + When I set the field "Change tag collection" in the "//table[contains(@class,'tag-areas-table')]//tr[contains(.,'User interests')]" "xpath_element" to "Hobbies" And I follow "Hobbies" Then I should see "Swimming" And I should see "Tag0" @@ -107,7 +95,6 @@ Feature: Managers can create and manage tag collections And I should see "Tag3" And I should see "Tag1" And I should see "Tag2" - And I log out Scenario: Creating searchable and non-searchable tag collections And I follow "Add tag collection" @@ -129,4 +116,3 @@ Feature: Managers can create and manage tag collections And I click on "Site pages" "list_item" in the "Navigation" "block" And I click on "Tags" "link" in the "Navigation" "block" And "Select tag collection" "select" should not exist - And I log out diff --git a/tag/tests/behat/edit_tag.feature b/tag/tests/behat/edit_tag.feature index 4682edb587b..c3bc35e51ec 100644 --- a/tag/tests/behat/edit_tag.feature +++ b/tag/tests/behat/edit_tag.feature @@ -158,19 +158,14 @@ Feature: Users can edit tags to add description or rename And I navigate to "Appearance > Manage tags" in site administration And I follow "Default collection" # Renaming tag to a valid name - And I click on "Edit tag name" "link" in the "Cat" "table_row" - And I set the field "New name for tag Cat" to "Kitten" - And I press the enter key + And I set the field "Edit tag name" in the "Cat" "table_row" to "Kitten" Then I should not see "Cat" And "New name for tag" "field" should not exist - And I wait until "Kitten" "link" exists And I follow "Default collection" And I should see "Kitten" And I should not see "Cat" # Renaming tag to an invalid name - And I click on "Edit tag name" "link" in the "Turtle" "table_row" - And I set the field "New name for tag Turtle" to "DOG" - And I press the enter key + And I set the field "Edit tag name" in the "Turtle" "table_row" to "DOG" And I should see "The tag name is already in use. Do you want to combine these tags?" And I click on "Cancel" "button" in the "Confirm" "dialogue" And "New name for tag" "field" should not exist @@ -183,7 +178,7 @@ Feature: Users can edit tags to add description or rename And I should not see "DOG" # Cancel tag renaming And I click on "Edit tag name" "link" in the "Dog" "table_row" - And I set the field "New name for tag Dog" to "Penguin" + And I type "Penguin" And I press the escape key And "New name for tag" "field" should not exist And I should see "Turtle" @@ -197,9 +192,7 @@ Feature: Users can edit tags to add description or rename When I log in as "manager1" And I navigate to "Appearance > Manage tags" in site administration And I follow "Default collection" - And I click on "Edit tag name" "link" in the "Turtle" "table_row" - And I set the field "New name for tag Turtle" to "DOG" - And I press the enter key + And I set the field "Edit tag name" in the "Turtle" "table_row" to "DOG" And I should see "The tag name is already in use. Do you want to combine these tags?" And I press "Yes" Then I should not see "Turtle" From 50baaebc31431f9b5c730f88a0ce360955161133 Mon Sep 17 00:00:00 2001 From: Adrian Hutchinson Date: Mon, 28 Sep 2020 10:06:56 -0700 Subject: [PATCH 15/26] MDL-67028 mod_lti: Support CourseSection.timeFrame custom parameters --- mod/lti/locallib.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/mod/lti/locallib.php b/mod/lti/locallib.php index 44acf5e80a3..e586766204a 100644 --- a/mod/lti/locallib.php +++ b/mod/lti/locallib.php @@ -2074,6 +2074,18 @@ function lti_calculate_custom_parameter($value) { return implode(",", groups_get_user_groups($COURSE->id, $USER->id)[0]); case 'Context.id.history': return implode(",", get_course_history($COURSE)); + case 'CourseSection.timeFrame.begin': + if (empty($COURSE->startdate)) { + return ""; + } + $dt = new DateTime("@$COURSE->startdate", new DateTimeZone('UTC')); + return $dt->format(DateTime::ATOM); + case 'CourseSection.timeFrame.end': + if (empty($COURSE->enddate)) { + return ""; + } + $dt = new DateTime("@$COURSE->enddate", new DateTimeZone('UTC')); + return $dt->format(DateTime::ATOM); } return null; } @@ -3739,7 +3751,8 @@ function lti_get_capabilities() { 'CourseSection.label' => 'context_label', 'CourseSection.sourcedId' => 'lis_course_section_sourcedid', 'CourseSection.longDescription' => '$COURSE->summary', - 'CourseSection.timeFrame.begin' => '$COURSE->startdate', + 'CourseSection.timeFrame.begin' => null, + 'CourseSection.timeFrame.end' => null, 'ResourceLink.id' => 'resource_link_id', 'ResourceLink.title' => 'resource_link_title', 'ResourceLink.description' => 'resource_link_description', From 08c6aad9a77290e7ff93fa0fadce50e6fba05359 Mon Sep 17 00:00:00 2001 From: Peter Burnett Date: Tue, 10 Nov 2020 11:06:17 +1000 Subject: [PATCH 16/26] MDL-70160 cache: plugin_functions checks for function_exists() --- lib/moodlelib.php | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/moodlelib.php b/lib/moodlelib.php index 7532952f55c..b0f51149c83 100644 --- a/lib/moodlelib.php +++ b/lib/moodlelib.php @@ -7801,7 +7801,14 @@ function get_plugin_list_with_function($plugintype, $function, $file = 'lib.php' $filepath = $allplugins[$pluginname] . DIRECTORY_SEPARATOR . $file; if (file_exists($filepath)) { include_once($filepath); - $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname; + + // Now that the file is loaded, we must verify the function still exists. + if (function_exists($functionname)) { + $pluginfunctions[$plugintype . '_' . $pluginname] = $functionname; + } else { + // Invalidate the cache for next run. + \cache_helper::invalidate_by_definition('core', 'plugin_functions'); + } } } } @@ -7834,6 +7841,7 @@ function get_plugins_with_function($function, $file = 'lib.php', $include = true // Clearning the filename as cache_helper::hash_key only allows a-zA-Z0-9_. $key = $function . '_' . clean_param($file, PARAM_ALPHA); $pluginfunctions = $cache->get($key); + $dirty = false; // Use the plugin manager to check that plugins are currently installed. $pluginmanager = \core_plugin_manager::instance(); @@ -7848,14 +7856,14 @@ function get_plugins_with_function($function, $file = 'lib.php', $include = true foreach ($plugins as $plugin => $function) { if (!isset($installedplugins[$plugin])) { // Plugin code is still present on disk but it is not installed. - unset($pluginfunctions[$plugintype][$plugin]); - continue; + $dirty = true; + break 2; } // Cache might be out of sync with the codebase, skip the plugin if it is not available. if (empty($allplugins[$plugin])) { - unset($pluginfunctions[$plugintype][$plugin]); - continue; + $dirty = true; + break 2; } $fileexists = file_exists($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); @@ -7864,11 +7872,22 @@ function get_plugins_with_function($function, $file = 'lib.php', $include = true include_once($allplugins[$plugin] . DIRECTORY_SEPARATOR . $file); } else if (!$fileexists) { // If the file is not available any more it should not be returned. - unset($pluginfunctions[$plugintype][$plugin]); + $dirty = true; + break 2; + } + + // Check if the function still exists in the file. + if ($include && !function_exists($function)) { + $dirty = true; + break 2; } } } - return $pluginfunctions; + + // If the cache is dirty, we should fall through and let it rebuild. + if (!$dirty) { + return $pluginfunctions; + } } $pluginfunctions = array(); From a014ebdc6ab9b6e7bfd6d62a366aab8ea953eb58 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Mon, 23 Nov 2020 13:40:52 +0100 Subject: [PATCH 17/26] MDL-70265 travis: Completely remove the UPGRADE check It was broken since ages ago, see MDL-64874, so no sense to keep it eating minutes for nothing. --- .travis.yml | 61 +---------------------------------------------------- 1 file changed, 1 insertion(+), 60 deletions(-) diff --git a/.travis.yml b/.travis.yml index 5c58e679a44..796d0452652 100644 --- a/.travis.yml +++ b/.travis.yml @@ -36,18 +36,10 @@ jobs: php: 7.2 env: DB=pgsql TASK=PHPUNIT - - if: env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present - php: 7.2 - env: DB=pgsql TASK=UPGRADE - - if: env(MOODLE_DATABASE) = "mysqli" OR env(MOODLE_DATABASE) = "all" php: 7.2 env: DB=mysqli TASK=PHPUNIT - - if: env(MOODLE_DATABASE) = "mysqli" OR env(MOODLE_DATABASE) = "all" - php: 7.2 - env: DB=mysqli TASK=UPGRADE - # Then, conditionally, all the highest php ones (7.4) - if: env(MOODLE_PHP) = "all" php: 7.4 @@ -60,18 +52,10 @@ jobs: php: 7.4 env: DB=pgsql TASK=PHPUNIT - - if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "pgsql" OR env(MOODLE_DATABASE) = "all" OR env(MOODLE_DATABASE) IS NOT present) - php: 7.4 - env: DB=pgsql TASK=UPGRADE - - if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "mysqli" OR env(MOODLE_DATABASE) = "all") php: 7.4 env: DB=mysqli TASK=PHPUNIT - - if: env(MOODLE_PHP) = "all" AND (env(MOODLE_DATABASE) = "mysqli" OR env(MOODLE_DATABASE) = "all") - php: 7.4 - env: DB=mysqli TASK=UPGRADE - cache: directories: - $HOME/.composer/cache @@ -130,7 +114,7 @@ install: before_script: - phpenv config-rm xdebug.ini - > - if [ "$TASK" = 'PHPUNIT' -o "$TASK" = 'UPGRADE' ]; + if [ "$TASK" = 'PHPUNIT' ]; then # Copy generic configuration in place. cp config-dist.php config.php ; @@ -241,32 +225,6 @@ before_script: export phpcmd=`which php`; fi - ######################################################################## - # Upgrade test - ######################################################################## - - > - if [ "$TASK" = 'UPGRADE' ]; - then - # We need the official upstream. - git remote add upstream https://github.com/moodle/moodle.git; - - # Checkout 30 STABLE branch (the first version compatible with PHP 7.x) - git fetch upstream MOODLE_30_STABLE; - git checkout MOODLE_30_STABLE; - - # Perform the upgrade - php admin/cli/install_database.php --agree-license --adminpass=Password --adminemail=admin@example.com --fullname="Upgrade test" --shortname=Upgrade; - - # Return to the previous commit - git checkout -; - - # Perform the upgrade - php admin/cli/upgrade.php --non-interactive --allow-unstable ; - - # The local_ci repository can be used to check upgrade savepoints. - git clone https://github.com/moodlehq/moodle-local_ci.git local/ci ; - fi - script: - > if [ "$TASK" = 'PHPUNIT' ]; @@ -292,23 +250,6 @@ script: git diff --cached --exit-code ; fi - ######################################################################## - # Upgrade test - ######################################################################## - - > - if [ "$TASK" = 'UPGRADE' ]; - then - cp local/ci/check_upgrade_savepoints/check_upgrade_savepoints.php ./check_upgrade_savepoints.php - result=`php check_upgrade_savepoints.php`; - # Check if there are problems - count=`echo "$result" | grep -P "ERROR|WARN" | wc -l` ; - if (($count > 0)); - then - echo "$result" - exit 1 ; - fi - fi - after_script: - > if [ "$TASK" = 'PHPUNIT' ]; From 64e7678c47c9d5cb4f3ae35ff7e0307e3baa2530 Mon Sep 17 00:00:00 2001 From: Mikhail Golenkov Date: Tue, 24 Nov 2020 11:34:38 +1100 Subject: [PATCH 18/26] MDL-69773 block_section_links: Add an option to display section name --- blocks/section_links/block_section_links.php | 9 +++- blocks/section_links/edit_form.php | 3 ++ .../lang/en/block_section_links.php | 2 + blocks/section_links/renderer.php | 9 +++- blocks/section_links/settings.php | 5 +++ .../tests/behat/show_section_name.feature | 43 +++++++++++++++++++ blocks/section_links/upgrade.txt | 6 +++ blocks/section_links/version.php | 2 +- 8 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 blocks/section_links/tests/behat/show_section_name.feature create mode 100644 blocks/section_links/upgrade.txt diff --git a/blocks/section_links/block_section_links.php b/blocks/section_links/block_section_links.php index 6c638ea47da..f7e555e5360 100644 --- a/blocks/section_links/block_section_links.php +++ b/blocks/section_links/block_section_links.php @@ -105,6 +105,9 @@ class block_section_links extends block_base { } } + // Whether or not section name should be displayed. + $showsectionname = !empty($config->showsectionname) ? true : false; + // Prepare an array of sections to create links for. $sections = array(); $canviewhidden = has_capability('moodle/course:update', $context); @@ -126,13 +129,17 @@ class block_section_links extends block_base { $sections[$i]->highlight = true; $sectiontojumpto = $section->section; } + if ($showsectionname) { + $sections[$i]->name = $courseformat->get_section_name($i); + } } } if (!empty($sections)) { // Render the sections. $renderer = $this->page->get_renderer('block_section_links'); - $this->content->text = $renderer->render_section_links($this->page->course, $sections, $sectiontojumpto); + $this->content->text = $renderer->render_section_links($this->page->course, $sections, + $sectiontojumpto, $showsectionname); } return $this->content; diff --git a/blocks/section_links/edit_form.php b/blocks/section_links/edit_form.php index 63a3593aa61..3cedaf078cb 100644 --- a/blocks/section_links/edit_form.php +++ b/blocks/section_links/edit_form.php @@ -82,5 +82,8 @@ class block_section_links_edit_form extends block_edit_form { $mform->addHelpButton('config_incby'.$i, 'incby'.$i, 'block_section_links'); } + $mform->addElement('selectyesno', 'config_showsectionname', get_string('showsectionname', 'block_section_links')); + $mform->setDefault('config_showsectionname', !empty($config->showsectionname) ? 1 : 0); + $mform->addHelpButton('config_showsectionname', 'showsectionname', 'block_section_links'); } } \ No newline at end of file diff --git a/blocks/section_links/lang/en/block_section_links.php b/blocks/section_links/lang/en/block_section_links.php index b2678987e34..950a0bbe08d 100644 --- a/blocks/section_links/lang/en/block_section_links.php +++ b/blocks/section_links/lang/en/block_section_links.php @@ -34,6 +34,8 @@ $string['numsections2'] = 'Alternative number of sections'; $string['numsections2_help'] = 'Once the number of sections in the course reaches this number then the Alternative increment by value is used.'; $string['pluginname'] = 'Section links'; $string['section_links:addinstance'] = 'Add a new section links block'; +$string['showsectionname'] = 'Display section name'; +$string['showsectionname_help'] = 'Display section name in addition to section number'; $string['topics'] = 'Topics'; $string['weeks'] = 'Weeks'; $string['privacy:metadata'] = 'The Section links block only shows data stored in other locations.'; diff --git a/blocks/section_links/renderer.php b/blocks/section_links/renderer.php index 338855b9238..a1ebe8756bb 100644 --- a/blocks/section_links/renderer.php +++ b/blocks/section_links/renderer.php @@ -38,10 +38,12 @@ class block_section_links_renderer extends plugin_renderer_base { * @param stdClass $course The course we are rendering for. * @param array $sections An array of section objects to render. * @param bool|int The section to provide a jump to link for. + * @param bool $showsectionname Whether or not section name should be displayed. * @return string The HTML to display. */ - public function render_section_links(stdClass $course, array $sections, $jumptosection = false) { - $html = html_writer::start_tag('ol', array('class' => 'inline-list')); + public function render_section_links(stdClass $course, array $sections, $jumptosection = false, $showsectionname = false) { + $olparams = $showsectionname ? ['class' => 'unlist'] : ['class' => 'inline-list']; + $html = html_writer::start_tag('ol', $olparams); foreach ($sections as $section) { $attributes = array(); if (!$section->visible) { @@ -49,6 +51,9 @@ class block_section_links_renderer extends plugin_renderer_base { } $html .= html_writer::start_tag('li'); $sectiontext = $section->section; + if ($showsectionname) { + $sectiontext .= ': ' . $section->name; + } if ($section->highlight) { $sectiontext = html_writer::tag('strong', $sectiontext); } diff --git a/blocks/section_links/settings.php b/blocks/section_links/settings.php index 2fcb4a4a729..ef18237c5d0 100644 --- a/blocks/section_links/settings.php +++ b/blocks/section_links/settings.php @@ -48,4 +48,9 @@ if ($ADMIN->fulltree) { get_string('incby'.$i.'_help', 'block_section_links'), $selected[$i][1], $increments)); } + + $settings->add(new admin_setting_configcheckbox('block_section_links/showsectionname', + get_string('showsectionname', 'block_section_links'), + get_string('showsectionname_help', 'block_section_links'), + 0)); } \ No newline at end of file diff --git a/blocks/section_links/tests/behat/show_section_name.feature b/blocks/section_links/tests/behat/show_section_name.feature new file mode 100644 index 00000000000..8c0b713baf8 --- /dev/null +++ b/blocks/section_links/tests/behat/show_section_name.feature @@ -0,0 +1,43 @@ +@block @block_section_links +Feature: The Section links block can be configured to display section name in addition to section number + + Background: + Given the following "courses" exist: + | fullname | shortname | category | numsections | coursedisplay | + | Course 1 | C1 | 0 | 10 | 1 | + And the following "activities" exist: + | activity | name | course | idnumber | section | + | assign | First assignment | C1 | assign1 | 7 | + And the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | 1 | teacher1@example.com | + | student1 | Student | 1 | student1@example.com | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + | student1 | C1 | student | + And I log in as "admin" + And I set the following administration settings values: + | showsectionname | 1 | + And I am on "Course 1" course homepage with editing mode on + And I add the "Section links" block + And I log out + + Scenario: Student can see section name under the Section links block + Given I log in as "student1" + When I am on "Course 1" course homepage + Then I should see "7: Topic 7" in the "Section links" "block" + And I follow "7: Topic 7" + And I should see "First assignment" + + Scenario: Teacher can configure existing Section links block to display section number or section name + Given I log in as "teacher1" + And I am on "Course 1" course homepage with editing mode on + When I configure the "Section links" block + And I set the following fields to these values: + | Display section name | No | + And I click on "Save changes" "button" + Then I should not see "7: Topic 7" in the "Section links" "block" + And I should see "7" in the "Section links" "block" + And I follow "7" + And I should see "First assignment" diff --git a/blocks/section_links/upgrade.txt b/blocks/section_links/upgrade.txt new file mode 100644 index 00000000000..bf4f9a4776d --- /dev/null +++ b/blocks/section_links/upgrade.txt @@ -0,0 +1,6 @@ +This file describes API changes in the section_links block code. + +=== 3.11 === + +* New optional parameter $showsectionname has been added to render_section_links(). Setting this to true will display + section name in addition to section number. diff --git a/blocks/section_links/version.php b/blocks/section_links/version.php index f2ea6f87d7f..ddda04718d0 100644 --- a/blocks/section_links/version.php +++ b/blocks/section_links/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2020110900; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2020110901; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2020110300; // Requires this Moodle version $plugin->component = 'block_section_links'; // Full name of the plugin (used for diagnostics) From b0c2b85310ae9a136c3bf9e196254b748c208fc9 Mon Sep 17 00:00:00 2001 From: Jamie Stamp Date: Mon, 9 Nov 2020 12:25:00 +0000 Subject: [PATCH 19/26] MDL-69121 core: Add ZSTD/Gzip compression options to Redis sessions --- config-dist.php | 3 ++ lib/classes/session/redis.php | 71 +++++++++++++++++++++++++++++++- lib/tests/session_redis_test.php | 24 +++++++++++ 3 files changed, 97 insertions(+), 1 deletion(-) diff --git a/config-dist.php b/config-dist.php index e35e19b7649..b8c5d51cb99 100644 --- a/config-dist.php +++ b/config-dist.php @@ -324,6 +324,9 @@ $CFG->admin = 'admin'; // Use the igbinary serializer instead of the php default one. Note that phpredis must be compiled with // igbinary support to make the setting to work. Also, if you change the serializer you have to flush the database! // $CFG->session_redis_serializer_use_igbinary = false; // Optional, default is PHP builtin serializer. +// $CFG->session_redis_compressor = 'none'; // Optional, possible values are: +// // 'gzip' - PHP GZip compression +// // 'zstd' - PHP Zstandard compression // // Please be aware that when selecting Memcached for sessions that it is advised to use a dedicated // memcache server. The memcached extension does not provide isolated environments for individual uses. diff --git a/lib/classes/session/redis.php b/lib/classes/session/redis.php index 61ee51e5aeb..1855238ecc8 100644 --- a/lib/classes/session/redis.php +++ b/lib/classes/session/redis.php @@ -40,6 +40,19 @@ defined('MOODLE_INTERNAL') || die(); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class redis extends handler { + /** + * Compressor: none. + */ + const COMPRESSION_NONE = 'none'; + /** + * Compressor: PHP GZip. + */ + const COMPRESSION_GZIP = 'gzip'; + /** + * Compressor: PHP Zstandard. + */ + const COMPRESSION_ZSTD = 'zstd'; + /** @var string $host save_path string */ protected $host = ''; /** @var int $port The port to connect to */ @@ -56,6 +69,8 @@ class redis extends handler { protected $lockretry = 100; /** @var int $serializer The serializer to use */ protected $serializer = \Redis::SERIALIZER_PHP; + /** @var int $compressor The compressor to use */ + protected $compressor = self::COMPRESSION_NONE; /** @var string $lasthash hash of the session data content */ protected $lasthash = null; @@ -122,6 +137,10 @@ class redis extends handler { if (isset($CFG->session_redis_lock_expire)) { $this->lockexpire = (int)$CFG->session_redis_lock_expire; } + + if (isset($CFG->session_redis_compressor)) { + $this->compressor = $CFG->session_redis_compressor; + } } /** @@ -268,7 +287,8 @@ class redis extends handler { if ($this->requires_write_lock()) { $this->lock_session($id); } - $sessiondata = $this->connection->get($id); + $sessiondata = $this->uncompress($this->connection->get($id)); + if ($sessiondata === false) { if ($this->requires_write_lock()) { $this->unlock_session($id); @@ -285,6 +305,53 @@ class redis extends handler { return $sessiondata; } + /** + * Compresses session data. + * + * @param mixed $value + * @return string + */ + private function compress($value) { + switch ($this->compressor) { + case self::COMPRESSION_NONE: + return $value; + case self::COMPRESSION_GZIP: + return gzencode($value); + case self::COMPRESSION_ZSTD: + return zstd_compress($value); + default: + debugging("Invalid compressor: {$this->compressor}"); + return $value; + } + } + + /** + * Uncompresses session data. + * + * @param string $value + * @return mixed + */ + private function uncompress($value) { + if ($value === false) { + return false; + } + + switch ($this->compressor) { + case self::COMPRESSION_NONE: + break; + case self::COMPRESSION_GZIP: + $value = gzdecode($value); + break; + case self::COMPRESSION_ZSTD: + $value = zstd_uncompress($value); + break; + default: + debugging("Invalid compressor: {$this->compressor}"); + } + + return $value; + } + /** * Write the serialized session data to our session store. * @@ -312,6 +379,8 @@ class redis extends handler { // There can be race conditions on new sessions racing each other but we can // address that in the future. try { + $data = $this->compress($data); + $this->connection->setex($id, $this->timeout, $data); } catch (RedisException $e) { error_log('Failed talking to redis: '.$e->getMessage()); diff --git a/lib/tests/session_redis_test.php b/lib/tests/session_redis_test.php index 028a6b6ee2b..dab879f696c 100644 --- a/lib/tests/session_redis_test.php +++ b/lib/tests/session_redis_test.php @@ -116,6 +116,30 @@ class core_session_redis_testcase extends advanced_testcase { $this->assertSessionNoLocks(); } + public function test_compression_read_and_write_works() { + global $CFG; + + $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_GZIP; + + $sess = new \core\session\redis(); + $sess->init(); + $this->assertTrue($sess->handler_write('sess1', 'DATA')); + $this->assertSame('DATA', $sess->handler_read('sess1')); + $this->assertTrue($sess->handler_close()); + + if (extension_loaded('zstd')) { + $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_ZSTD; + + $sess = new \core\session\redis(); + $sess->init(); + $this->assertTrue($sess->handler_write('sess2', 'DATA')); + $this->assertSame('DATA', $sess->handler_read('sess2')); + $this->assertTrue($sess->handler_close()); + } + + $CFG->session_redis_compressor = \core\session\redis::COMPRESSION_NONE; + } + public function test_session_blocks_with_existing_session() { $sess = new \core\session\redis(); $sess->init(); From 375af0e6c3261b6e6f4a15ec22cf6ae13f3ffea1 Mon Sep 17 00:00:00 2001 From: Ilya Tregubov Date: Fri, 13 Nov 2020 11:02:52 +0200 Subject: [PATCH 20/26] MDL-65852 user: Fix permission check to download course participants. --- user/action_redir.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/user/action_redir.php b/user/action_redir.php index 62bf3c53915..cccc8e95251 100644 --- a/user/action_redir.php +++ b/user/action_redir.php @@ -23,6 +23,7 @@ */ require_once("../config.php"); +require_once($CFG->dirroot . '/course/lib.php'); $formaction = required_param('formaction', PARAM_LOCALURL); $id = required_param('id', PARAM_INT); @@ -78,7 +79,8 @@ if ($formaction == 'bulkchange.php') { if (empty($plugin) AND $operationname == 'download_participants') { // Check permissions. - if (has_capability('moodle/course:manageactivities', $context)) { + $pagecontext = ($course->id == SITEID) ? context_system::instance() : $context; + if (course_can_view_participants($pagecontext)) { $plugins = core_plugin_manager::instance()->get_plugins_of_type('dataformat'); if (isset($plugins[$dataformat])) { if ($plugins[$dataformat]->is_enabled()) { From 83875bc586cccb21c8f5cc2975342d4b96362024 Mon Sep 17 00:00:00 2001 From: "Eloy Lafuente (stronk7)" Date: Sat, 21 Nov 2020 13:05:22 +0100 Subject: [PATCH 21/26] MDL-70276 github actions: First cut, phpunit and grunt checks First working version, supports phpunit (using build matrix): - php72 (lowest), running mysql. - php74 (highest), running postgres. Also verifies that the branch has been "gruntified" and there isn't any missing change (build js/css files). TODO: Verify the remaining checks currently in .travis.yml, namely: - CITEST - Add caching - Better health-check for DB images. - Support from the tracker (satus badges and enable check). - Support from CiBoT (status and enable check). - Consider moving both the common setup (git, composer...) and the database (mysql, postgres) to own actions for easier tweaking. --- .github/workflows/config-template.php | 71 ++++++++++++++++++ .github/workflows/push.yml | 103 ++++++++++++++++++++++++++ 2 files changed, 174 insertions(+) create mode 100644 .github/workflows/config-template.php create mode 100644 .github/workflows/push.yml diff --git a/.github/workflows/config-template.php b/.github/workflows/config-template.php new file mode 100644 index 00000000000..dc5f958eb53 --- /dev/null +++ b/.github/workflows/config-template.php @@ -0,0 +1,71 @@ +. + +/** + * Template configuraton file for github actions CI/CD. + * + * @package core + * @copyright 2020 onwards Eloy Lafuente (stronk7) {@link https://stronk7.com} + * @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +// This cannot be used out from a github actions workflow, so just exit. +getenv('GITHUB_WORKFLOW') || die; // phpcs:ignore moodle.Files.MoodleInternal.MoodleInternalGlobalState + +unset($CFG); +global $CFG; +$CFG = new stdClass(); + +$CFG->dbtype = getenv('dbtype'); +$CFG->dblibrary = 'native'; +$CFG->dbhost = '127.0.0.1'; +$CFG->dbname = 'test'; +$CFG->dbuser = 'test'; +$CFG->dbpass = 'test'; +$CFG->prefix = 'm_'; +$CFG->dboptions = ['dbcollation' => 'utf8mb4_bin']; + +$host = 'localhost'; +$CFG->wwwroot = "http://{$host}"; +$CFG->dataroot = realpath(dirname(__DIR__)) . '/moodledata'; +$CFG->admin = 'admin'; +$CFG->directorypermissions = 0777; + +// Debug options - possible to be controlled by flag in future. +$CFG->debug = (E_ALL | E_STRICT); // DEBUG_DEVELOPER. +$CFG->debugdisplay = 1; +$CFG->debugstringids = 1; // Add strings=1 to url to get string ids. +$CFG->perfdebug = 15; +$CFG->debugpageinfo = 1; +$CFG->allowthemechangeonurl = 1; +$CFG->passwordpolicy = 0; +$CFG->cronclionly = 0; +$CFG->pathtophp = getenv('pathtophp'); + +$CFG->phpunit_dataroot = realpath(dirname(__DIR__)) . '/phpunitdata'; +$CFG->phpunit_prefix = 't_'; + +define('TEST_EXTERNAL_FILES_HTTP_URL', 'http://localhost:8080'); +define('TEST_EXTERNAL_FILES_HTTPS_URL', 'http://localhost:8080'); + +define('TEST_SESSION_REDIS_HOST', 'localhost'); +define('TEST_CACHESTORE_REDIS_TESTSERVERS', 'localhost'); + +// TODO: add others (solr, mongodb, memcached, ldap...). + +// Too much for now: define('PHPUNIT_LONGTEST', true); // Only leaves a few tests out and they are run later by CI. + +require_once(__DIR__ . '/lib/setup.php'); diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml new file mode 100644 index 00000000000..f063803e4a7 --- /dev/null +++ b/.github/workflows/push.yml @@ -0,0 +1,103 @@ +name: Core + +on: [push] + +env: + php: 7.4 + +jobs: + Grunt: + runs-on: ubuntu-18.04 + + steps: + - name: Checking out code + uses: actions/checkout@v2 + + - name: Configuring node & npm + shell: bash -l {0} + run: nvm install + + - name: Installing node stuff + run: npm install + + - name: Running grunt + run: npx grunt + + - name: Looking for uncommitted changes + # Add all files to the git index and then run diff --cached to see all changes. + # This ensures that we get the status of all files, including new files. + # We ignore npm-shrinkwrap.json to make the tasks immune to npm changes. + run: | + git add . + git reset -- npm-shrinkwrap.json + git diff --cached --exit-code + + PHPUnit: + runs-on: ${{ matrix.os }} + services: + exttests: + image: moodlehq/moodle-exttests + ports: + - 8080:80 + redis: + image: redis + ports: + - 6379:6379 + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-18.04 + php: 7.2 + db: mysqli + - os: ubuntu-18.04 + php: 7.4 + db: pgsql + + steps: + - name: Setting up DB mysql + if: ${{ matrix.db == 'mysqli' }} + uses: johanmeiring/mysql-action@tmpfs-patch + with: + collation server: utf8mb4_bin + mysql version: 5.7 + mysql database: test + mysql user: test + mysql password: test + use tmpfs: true + + - name: Setting up DB pgsql + if: ${{ matrix.db == 'pgsql' }} + uses: m4nu56/postgresql-action@v1 + with: + postgresql version: 9.6 + postgresql db: test + postgresql user: test + postgresql password: test + + - name: Configuring git vars + uses: rlespinasse/github-slug-action@v3.x + + - name: Setting up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + + - name: Checking out code from ${{ env.GITHUB_REF_SLUG }} + uses: actions/checkout@v2 + + - name: Setting up PHPUnit + env: + dbtype: ${{ matrix.db }} + run: | + echo "pathtophp=$(which php)" >> $GITHUB_ENV # Inject installed pathtophp to env. The template config needs it. + cp .github/workflows/config-template.php config.php + mkdir ../moodledata + sudo locale-gen en_AU.UTF-8 + php admin/tool/phpunit/cli/init.php --no-composer-self-update + + - name: Running PHPUnit tests + env: + dbtype: ${{ matrix.db }} + run: vendor/bin/phpunit -v From 2d81fc6f65c044467a71f5303e1033095a890bea Mon Sep 17 00:00:00 2001 From: Bas Brands Date: Wed, 28 Oct 2020 15:11:57 +0100 Subject: [PATCH 22/26] MDL-69878 core_message: always show message drawer close icon --- message/amd/build/message_drawer.min.js | 2 +- message/amd/build/message_drawer.min.js.map | 2 +- message/amd/src/message_drawer.js | 6 +++++- message/templates/message_drawer.mustache | 4 ++-- .../message_drawer_view_contacts_header.mustache | 2 +- .../message_drawer_view_conversation_header.mustache | 2 +- .../message_drawer_view_overview_header.mustache | 2 +- .../templates/message_drawer_view_search_header.mustache | 2 +- .../message_drawer_view_settings_header.mustache | 2 +- theme/boost/scss/moodle/drawer.scss | 8 -------- theme/boost/style/moodle.css | 5 ----- theme/classic/style/moodle.css | 5 ----- 12 files changed, 14 insertions(+), 28 deletions(-) diff --git a/message/amd/build/message_drawer.min.js b/message/amd/build/message_drawer.min.js index bcb430f8a01..e5ea5a0c5c2 100644 --- a/message/amd/build/message_drawer.min.js +++ b/message/amd/build/message_drawer.min.js @@ -1,2 +1,2 @@ -define ("core_message/message_drawer",["jquery","core/custom_interaction_events","core/pubsub","core_message/message_drawer_view_contact","core_message/message_drawer_view_contacts","core_message/message_drawer_view_conversation","core_message/message_drawer_view_group_info","core_message/message_drawer_view_overview","core_message/message_drawer_view_search","core_message/message_drawer_view_settings","core_message/message_drawer_router","core_message/message_drawer_routes","core_message/message_drawer_events","core/pending","core/drawer"],function(a,b,c,d,e,f,g,h,i,j,k,l,m,n,o){var p={DRAWER:"[data-region=\"right-hand-drawer\"]",JUMPTO:".popover-region [data-region=\"jumpto\"]",PANEL_BODY_CONTAINER:"[data-region=\"panel-body-container\"]",PANEL_HEADER_CONTAINER:"[data-region=\"panel-header-container\"]",VIEW_CONTACT:"[data-region=\"view-contact\"]",VIEW_CONTACTS:"[data-region=\"view-contacts\"]",VIEW_CONVERSATION:"[data-region=\"view-conversation\"]",VIEW_GROUP_INFO:"[data-region=\"view-group-info\"]",VIEW_OVERVIEW:"[data-region=\"view-overview\"]",VIEW_SEARCH:"[data-region=\"view-search\"]",VIEW_SETTINGS:"[data-region=\"view-settings\"]",ROUTES:"[data-route]",ROUTES_BACK:"[data-route-back]",HEADER_CONTAINER:"[data-region=\"header-container\"]",BODY_CONTAINER:"[data-region=\"body-container\"]",FOOTER_CONTAINER:"[data-region=\"footer-container\"]",CLOSE_BUTTON:"[data-action=\"closedrawer\"]"},q=function(a,b,c){var d=b.find(p.HEADER_CONTAINER).find(c);if(!d.length){d=b.find(p.PANEL_HEADER_CONTAINER).find(c)}var e=b.find(p.BODY_CONTAINER).find(c);if(!e.length){e=b.find(p.PANEL_BODY_CONTAINER).find(c)}var f=b.find(p.FOOTER_CONTAINER).find(c);return[a,d.length?d:null,e.length?e:null,f.length?f:null]},r=[[l.VIEW_CONTACT,p.VIEW_CONTACT,d.show,d.description],[l.VIEW_CONTACTS,p.VIEW_CONTACTS,e.show,e.description],[l.VIEW_CONVERSATION,p.VIEW_CONVERSATION,f.show,f.description],[l.VIEW_GROUP_INFO,p.VIEW_GROUP_INFO,g.show,g.description],[l.VIEW_OVERVIEW,p.VIEW_OVERVIEW,h.show,h.description],[l.VIEW_SEARCH,p.VIEW_SEARCH,i.show,i.description],[l.VIEW_SETTINGS,p.VIEW_SETTINGS,j.show,j.description]],s=function(a,b){r.forEach(function(c){k.add(a,c[0],q(a,b,c[1]),c[2],c[3])})},t=function(a,b){if(!b.attr("data-shown")){k.go(a,l.VIEW_OVERVIEW);b.attr("data-shown",!0)}var c=o.getDrawerRoot(b);if(c.length){o.show(c)}},u=function(a){var b=o.getDrawerRoot(a);if(b.length){o.hide(b)}},v=function(a){var b=o.getDrawerRoot(a);if(b.length){return o.isVisible(b)}return!0},w=function(b){a(p.DRAWER).attr("data-origin",b)},x=function(d,e,f){b.define(e,[b.events.activate]);var g=/^data-route-param-?(\d*)$/;e.on(b.events.activate,p.ROUTES,function(b,c){for(var e=a(b.target).closest(p.ROUTES),f=e.attr("data-route"),h=[],j=0;j.\n\n/**\n * Controls the message drawer.\n *\n * @module core_message/message_drawer\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n 'jquery',\n 'core/custom_interaction_events',\n 'core/pubsub',\n 'core_message/message_drawer_view_contact',\n 'core_message/message_drawer_view_contacts',\n 'core_message/message_drawer_view_conversation',\n 'core_message/message_drawer_view_group_info',\n 'core_message/message_drawer_view_overview',\n 'core_message/message_drawer_view_search',\n 'core_message/message_drawer_view_settings',\n 'core_message/message_drawer_router',\n 'core_message/message_drawer_routes',\n 'core_message/message_drawer_events',\n 'core/pending',\n 'core/drawer',\n],\nfunction(\n $,\n CustomEvents,\n PubSub,\n ViewContact,\n ViewContacts,\n ViewConversation,\n ViewGroupInfo,\n ViewOverview,\n ViewSearch,\n ViewSettings,\n Router,\n Routes,\n Events,\n Pending,\n Drawer\n) {\n\n var SELECTORS = {\n DRAWER: '[data-region=\"right-hand-drawer\"]',\n JUMPTO: '.popover-region [data-region=\"jumpto\"]',\n PANEL_BODY_CONTAINER: '[data-region=\"panel-body-container\"]',\n PANEL_HEADER_CONTAINER: '[data-region=\"panel-header-container\"]',\n VIEW_CONTACT: '[data-region=\"view-contact\"]',\n VIEW_CONTACTS: '[data-region=\"view-contacts\"]',\n VIEW_CONVERSATION: '[data-region=\"view-conversation\"]',\n VIEW_GROUP_INFO: '[data-region=\"view-group-info\"]',\n VIEW_OVERVIEW: '[data-region=\"view-overview\"]',\n VIEW_SEARCH: '[data-region=\"view-search\"]',\n VIEW_SETTINGS: '[data-region=\"view-settings\"]',\n ROUTES: '[data-route]',\n ROUTES_BACK: '[data-route-back]',\n HEADER_CONTAINER: '[data-region=\"header-container\"]',\n BODY_CONTAINER: '[data-region=\"body-container\"]',\n FOOTER_CONTAINER: '[data-region=\"footer-container\"]',\n CLOSE_BUTTON: '[data-action=\"closedrawer\"]'\n };\n\n /**\n * Get elements for route.\n *\n * @param {String} namespace Unique identifier for the Routes\n * @param {Object} root The message drawer container.\n * @param {string} selector The route container.\n *\n * @return {array} elements Found route container objects.\n */\n var getParametersForRoute = function(namespace, root, selector) {\n\n var header = root.find(SELECTORS.HEADER_CONTAINER).find(selector);\n if (!header.length) {\n header = root.find(SELECTORS.PANEL_HEADER_CONTAINER).find(selector);\n }\n var body = root.find(SELECTORS.BODY_CONTAINER).find(selector);\n if (!body.length) {\n body = root.find(SELECTORS.PANEL_BODY_CONTAINER).find(selector);\n }\n var footer = root.find(SELECTORS.FOOTER_CONTAINER).find(selector);\n\n return [\n namespace,\n header.length ? header : null,\n body.length ? body : null,\n footer.length ? footer : null\n ];\n };\n\n var routes = [\n [Routes.VIEW_CONTACT, SELECTORS.VIEW_CONTACT, ViewContact.show, ViewContact.description],\n [Routes.VIEW_CONTACTS, SELECTORS.VIEW_CONTACTS, ViewContacts.show, ViewContacts.description],\n [Routes.VIEW_CONVERSATION, SELECTORS.VIEW_CONVERSATION, ViewConversation.show, ViewConversation.description],\n [Routes.VIEW_GROUP_INFO, SELECTORS.VIEW_GROUP_INFO, ViewGroupInfo.show, ViewGroupInfo.description],\n [Routes.VIEW_OVERVIEW, SELECTORS.VIEW_OVERVIEW, ViewOverview.show, ViewOverview.description],\n [Routes.VIEW_SEARCH, SELECTORS.VIEW_SEARCH, ViewSearch.show, ViewSearch.description],\n [Routes.VIEW_SETTINGS, SELECTORS.VIEW_SETTINGS, ViewSettings.show, ViewSettings.description]\n ];\n\n /**\n * Create routes.\n *\n * @param {String} namespace Unique identifier for the Routes\n * @param {Object} root The message drawer container.\n */\n var createRoutes = function(namespace, root) {\n routes.forEach(function(route) {\n Router.add(namespace, route[0], getParametersForRoute(namespace, root, route[1]), route[2], route[3]);\n });\n };\n\n /**\n * Show the message drawer.\n *\n * @param {string} namespace The route namespace.\n * @param {Object} root The message drawer container.\n */\n var show = function(namespace, root) {\n if (!root.attr('data-shown')) {\n Router.go(namespace, Routes.VIEW_OVERVIEW);\n root.attr('data-shown', true);\n }\n\n var drawerRoot = Drawer.getDrawerRoot(root);\n if (drawerRoot.length) {\n Drawer.show(drawerRoot);\n }\n };\n\n /**\n * Hide the message drawer.\n *\n * @param {Object} root The message drawer container.\n */\n var hide = function(root) {\n var drawerRoot = Drawer.getDrawerRoot(root);\n if (drawerRoot.length) {\n Drawer.hide(drawerRoot);\n }\n };\n\n /**\n * Check if the drawer is visible.\n *\n * @param {Object} root The message drawer container.\n * @return {boolean}\n */\n var isVisible = function(root) {\n var drawerRoot = Drawer.getDrawerRoot(root);\n if (drawerRoot.length) {\n return Drawer.isVisible(drawerRoot);\n }\n return true;\n };\n\n /**\n * Set Jump from button\n *\n * @param {String} buttonid The originating button id\n */\n var setJumpFrom = function(buttonid) {\n $(SELECTORS.DRAWER).attr('data-origin', buttonid);\n };\n\n /**\n * Listen to and handle events for routing, showing and hiding the message drawer.\n *\n * @param {string} namespace The route namespace.\n * @param {Object} root The message drawer container.\n * @param {bool} alwaysVisible Is this messaging app always shown?\n */\n var registerEventListeners = function(namespace, root, alwaysVisible) {\n CustomEvents.define(root, [CustomEvents.events.activate]);\n var paramRegex = /^data-route-param-?(\\d*)$/;\n\n root.on(CustomEvents.events.activate, SELECTORS.ROUTES, function(e, data) {\n var element = $(e.target).closest(SELECTORS.ROUTES);\n var route = element.attr('data-route');\n var attributes = [];\n\n for (var i = 0; i < element[0].attributes.length; i++) {\n attributes.push(element[0].attributes[i]);\n }\n\n var paramAttributes = attributes.filter(function(attribute) {\n var name = attribute.nodeName;\n var match = paramRegex.test(name);\n return match;\n });\n paramAttributes.sort(function(a, b) {\n var aParts = paramRegex.exec(a.nodeName);\n var bParts = paramRegex.exec(b.nodeName);\n var aIndex = aParts.length > 1 ? aParts[1] : 0;\n var bIndex = bParts.length > 1 ? bParts[1] : 0;\n\n if (aIndex < bIndex) {\n return -1;\n } else if (bIndex < aIndex) {\n return 1;\n } else {\n return 0;\n }\n });\n\n var params = paramAttributes.map(function(attribute) {\n return attribute.nodeValue;\n });\n\n var routeParams = [namespace, route].concat(params);\n\n Router.go.apply(null, routeParams);\n\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.activate, SELECTORS.ROUTES_BACK, function(e, data) {\n Router.back(namespace);\n\n data.originalEvent.preventDefault();\n });\n\n // These are theme-specific to help us fix random behat fails.\n // These events target those events defined in BS3 and BS4 onwards.\n root.on('hide.bs.collapse', '.collapse', function(e) {\n var pendingPromise = new Pending();\n $(e.target).one('hidden.bs.collapse', function() {\n pendingPromise.resolve();\n });\n });\n\n root.on('show.bs.collapse', '.collapse', function(e) {\n var pendingPromise = new Pending();\n $(e.target).one('shown.bs.collapse', function() {\n pendingPromise.resolve();\n });\n });\n\n $(SELECTORS.JUMPTO).focus(function() {\n var firstInput = $(SELECTORS.HEADER_CONTAINER).find('input:visible');\n if (firstInput.length) {\n firstInput.focus();\n } else {\n $(SELECTORS.HEADER_CONTAINER).find(SELECTORS.ROUTES_BACK).focus();\n }\n });\n\n $(SELECTORS.DRAWER).focus(function() {\n var button = $(this).attr('data-origin');\n if (button) {\n $('#' + button).focus();\n }\n });\n\n if (!alwaysVisible) {\n PubSub.subscribe(Events.SHOW, function() {\n show(namespace, root);\n });\n\n PubSub.subscribe(Events.HIDE, function() {\n hide(root);\n });\n\n PubSub.subscribe(Events.TOGGLE_VISIBILITY, function(buttonid) {\n if (isVisible(root)) {\n hide(root);\n $(SELECTORS.JUMPTO).attr('tabindex', -1);\n } else {\n show(namespace, root);\n setJumpFrom(buttonid);\n $(SELECTORS.JUMPTO).attr('tabindex', 0);\n }\n });\n }\n\n PubSub.subscribe(Events.SHOW_CONVERSATION, function(args) {\n setJumpFrom(args.buttonid);\n show(namespace, root);\n Router.go(namespace, Routes.VIEW_CONVERSATION, args.conversationid);\n });\n\n var closebutton = root.find(SELECTORS.CLOSE_BUTTON);\n closebutton.on(CustomEvents.events.activate, function() {\n PubSub.publish(Events.TOGGLE_VISIBILITY);\n });\n\n PubSub.subscribe(Events.CREATE_CONVERSATION_WITH_USER, function(args) {\n setJumpFrom(args.buttonid);\n show(namespace, root);\n Router.go(namespace, Routes.VIEW_CONVERSATION, null, 'create', args.userid);\n });\n\n PubSub.subscribe(Events.SHOW_SETTINGS, function() {\n show(namespace, root);\n Router.go(namespace, Routes.VIEW_SETTINGS);\n });\n\n PubSub.subscribe(Events.PREFERENCES_UPDATED, function(preferences) {\n var filteredPreferences = preferences.filter(function(preference) {\n return preference.type == 'message_entertosend';\n });\n var enterToSendPreference = filteredPreferences.length ? filteredPreferences[0] : null;\n\n if (enterToSendPreference) {\n var viewConversationFooter = root.find(SELECTORS.FOOTER_CONTAINER).find(SELECTORS.VIEW_CONVERSATION);\n viewConversationFooter.attr('data-enter-to-send', enterToSendPreference.value);\n }\n });\n };\n\n /**\n * Initialise the message drawer.\n *\n * @param {Object} root The message drawer container.\n * @param {String} uniqueId Unique identifier for the Routes\n * @param {bool} alwaysVisible Should we show the app now, or wait for the user?\n * @param {Object} route\n */\n var init = function(root, uniqueId, alwaysVisible, route) {\n root = $(root);\n createRoutes(uniqueId, root);\n registerEventListeners(uniqueId, root, alwaysVisible);\n\n if (alwaysVisible) {\n show(uniqueId, root);\n\n if (route) {\n var routeParams = route.params || [];\n routeParams = [uniqueId, route.path].concat(routeParams);\n Router.go.apply(null, routeParams);\n }\n }\n };\n\n return {\n init: init,\n };\n});\n"],"file":"message_drawer.min.js"} \ No newline at end of file +{"version":3,"sources":["../src/message_drawer.js"],"names":["define","$","CustomEvents","PubSub","ViewContact","ViewContacts","ViewConversation","ViewGroupInfo","ViewOverview","ViewSearch","ViewSettings","Router","Routes","Events","Pending","Drawer","SELECTORS","DRAWER","JUMPTO","PANEL_BODY_CONTAINER","PANEL_HEADER_CONTAINER","VIEW_CONTACT","VIEW_CONTACTS","VIEW_CONVERSATION","VIEW_GROUP_INFO","VIEW_OVERVIEW","VIEW_SEARCH","VIEW_SETTINGS","ROUTES","ROUTES_BACK","HEADER_CONTAINER","BODY_CONTAINER","FOOTER_CONTAINER","CLOSE_BUTTON","getParametersForRoute","namespace","root","selector","header","find","length","body","footer","routes","show","description","createRoutes","forEach","route","add","attr","go","drawerRoot","getDrawerRoot","hide","isVisible","setJumpFrom","buttonid","registerEventListeners","alwaysVisible","events","activate","paramRegex","on","e","data","element","target","closest","attributes","i","push","paramAttributes","filter","attribute","name","nodeName","match","test","sort","a","b","aParts","exec","bParts","aIndex","bIndex","params","map","nodeValue","routeParams","concat","apply","originalEvent","preventDefault","back","pendingPromise","one","resolve","focus","firstInput","button","subscribe","SHOW","HIDE","TOGGLE_VISIBILITY","SHOW_CONVERSATION","args","conversationid","closebutton","publish","CREATE_CONVERSATION_WITH_USER","userid","SHOW_SETTINGS","PREFERENCES_UPDATED","preferences","filteredPreferences","preference","type","enterToSendPreference","viewConversationFooter","value","init","uniqueId","path"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Controls the message drawer.\n *\n * @module core_message/message_drawer\n * @copyright 2018 Ryan Wyllie \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\ndefine(\n[\n 'jquery',\n 'core/custom_interaction_events',\n 'core/pubsub',\n 'core_message/message_drawer_view_contact',\n 'core_message/message_drawer_view_contacts',\n 'core_message/message_drawer_view_conversation',\n 'core_message/message_drawer_view_group_info',\n 'core_message/message_drawer_view_overview',\n 'core_message/message_drawer_view_search',\n 'core_message/message_drawer_view_settings',\n 'core_message/message_drawer_router',\n 'core_message/message_drawer_routes',\n 'core_message/message_drawer_events',\n 'core/pending',\n 'core/drawer',\n],\nfunction(\n $,\n CustomEvents,\n PubSub,\n ViewContact,\n ViewContacts,\n ViewConversation,\n ViewGroupInfo,\n ViewOverview,\n ViewSearch,\n ViewSettings,\n Router,\n Routes,\n Events,\n Pending,\n Drawer\n) {\n\n var SELECTORS = {\n DRAWER: '[data-region=\"right-hand-drawer\"]',\n JUMPTO: '.popover-region [data-region=\"jumpto\"]',\n PANEL_BODY_CONTAINER: '[data-region=\"panel-body-container\"]',\n PANEL_HEADER_CONTAINER: '[data-region=\"panel-header-container\"]',\n VIEW_CONTACT: '[data-region=\"view-contact\"]',\n VIEW_CONTACTS: '[data-region=\"view-contacts\"]',\n VIEW_CONVERSATION: '[data-region=\"view-conversation\"]',\n VIEW_GROUP_INFO: '[data-region=\"view-group-info\"]',\n VIEW_OVERVIEW: '[data-region=\"view-overview\"]',\n VIEW_SEARCH: '[data-region=\"view-search\"]',\n VIEW_SETTINGS: '[data-region=\"view-settings\"]',\n ROUTES: '[data-route]',\n ROUTES_BACK: '[data-route-back]',\n HEADER_CONTAINER: '[data-region=\"header-container\"]',\n BODY_CONTAINER: '[data-region=\"body-container\"]',\n FOOTER_CONTAINER: '[data-region=\"footer-container\"]',\n CLOSE_BUTTON: '[data-action=\"closedrawer\"]'\n };\n\n /**\n * Get elements for route.\n *\n * @param {String} namespace Unique identifier for the Routes\n * @param {Object} root The message drawer container.\n * @param {string} selector The route container.\n *\n * @return {array} elements Found route container objects.\n */\n var getParametersForRoute = function(namespace, root, selector) {\n\n var header = root.find(SELECTORS.HEADER_CONTAINER).find(selector);\n if (!header.length) {\n header = root.find(SELECTORS.PANEL_HEADER_CONTAINER).find(selector);\n }\n var body = root.find(SELECTORS.BODY_CONTAINER).find(selector);\n if (!body.length) {\n body = root.find(SELECTORS.PANEL_BODY_CONTAINER).find(selector);\n }\n var footer = root.find(SELECTORS.FOOTER_CONTAINER).find(selector);\n\n return [\n namespace,\n header.length ? header : null,\n body.length ? body : null,\n footer.length ? footer : null\n ];\n };\n\n var routes = [\n [Routes.VIEW_CONTACT, SELECTORS.VIEW_CONTACT, ViewContact.show, ViewContact.description],\n [Routes.VIEW_CONTACTS, SELECTORS.VIEW_CONTACTS, ViewContacts.show, ViewContacts.description],\n [Routes.VIEW_CONVERSATION, SELECTORS.VIEW_CONVERSATION, ViewConversation.show, ViewConversation.description],\n [Routes.VIEW_GROUP_INFO, SELECTORS.VIEW_GROUP_INFO, ViewGroupInfo.show, ViewGroupInfo.description],\n [Routes.VIEW_OVERVIEW, SELECTORS.VIEW_OVERVIEW, ViewOverview.show, ViewOverview.description],\n [Routes.VIEW_SEARCH, SELECTORS.VIEW_SEARCH, ViewSearch.show, ViewSearch.description],\n [Routes.VIEW_SETTINGS, SELECTORS.VIEW_SETTINGS, ViewSettings.show, ViewSettings.description]\n ];\n\n /**\n * Create routes.\n *\n * @param {String} namespace Unique identifier for the Routes\n * @param {Object} root The message drawer container.\n */\n var createRoutes = function(namespace, root) {\n routes.forEach(function(route) {\n Router.add(namespace, route[0], getParametersForRoute(namespace, root, route[1]), route[2], route[3]);\n });\n };\n\n /**\n * Show the message drawer.\n *\n * @param {string} namespace The route namespace.\n * @param {Object} root The message drawer container.\n */\n var show = function(namespace, root) {\n if (!root.attr('data-shown')) {\n Router.go(namespace, Routes.VIEW_OVERVIEW);\n root.attr('data-shown', true);\n }\n\n var drawerRoot = Drawer.getDrawerRoot(root);\n if (drawerRoot.length) {\n Drawer.show(drawerRoot);\n }\n };\n\n /**\n * Hide the message drawer.\n *\n * @param {Object} root The message drawer container.\n */\n var hide = function(root) {\n var drawerRoot = Drawer.getDrawerRoot(root);\n if (drawerRoot.length) {\n Drawer.hide(drawerRoot);\n }\n };\n\n /**\n * Check if the drawer is visible.\n *\n * @param {Object} root The message drawer container.\n * @return {boolean}\n */\n var isVisible = function(root) {\n var drawerRoot = Drawer.getDrawerRoot(root);\n if (drawerRoot.length) {\n return Drawer.isVisible(drawerRoot);\n }\n return true;\n };\n\n /**\n * Set Jump from button\n *\n * @param {String} buttonid The originating button id\n */\n var setJumpFrom = function(buttonid) {\n $(SELECTORS.DRAWER).attr('data-origin', buttonid);\n };\n\n /**\n * Listen to and handle events for routing, showing and hiding the message drawer.\n *\n * @param {string} namespace The route namespace.\n * @param {Object} root The message drawer container.\n * @param {bool} alwaysVisible Is this messaging app always shown?\n */\n var registerEventListeners = function(namespace, root, alwaysVisible) {\n CustomEvents.define(root, [CustomEvents.events.activate]);\n var paramRegex = /^data-route-param-?(\\d*)$/;\n\n root.on(CustomEvents.events.activate, SELECTORS.ROUTES, function(e, data) {\n var element = $(e.target).closest(SELECTORS.ROUTES);\n var route = element.attr('data-route');\n var attributes = [];\n\n for (var i = 0; i < element[0].attributes.length; i++) {\n attributes.push(element[0].attributes[i]);\n }\n\n var paramAttributes = attributes.filter(function(attribute) {\n var name = attribute.nodeName;\n var match = paramRegex.test(name);\n return match;\n });\n paramAttributes.sort(function(a, b) {\n var aParts = paramRegex.exec(a.nodeName);\n var bParts = paramRegex.exec(b.nodeName);\n var aIndex = aParts.length > 1 ? aParts[1] : 0;\n var bIndex = bParts.length > 1 ? bParts[1] : 0;\n\n if (aIndex < bIndex) {\n return -1;\n } else if (bIndex < aIndex) {\n return 1;\n } else {\n return 0;\n }\n });\n\n var params = paramAttributes.map(function(attribute) {\n return attribute.nodeValue;\n });\n\n var routeParams = [namespace, route].concat(params);\n\n Router.go.apply(null, routeParams);\n\n data.originalEvent.preventDefault();\n });\n\n root.on(CustomEvents.events.activate, SELECTORS.ROUTES_BACK, function(e, data) {\n Router.back(namespace);\n\n data.originalEvent.preventDefault();\n });\n\n // These are theme-specific to help us fix random behat fails.\n // These events target those events defined in BS3 and BS4 onwards.\n root.on('hide.bs.collapse', '.collapse', function(e) {\n var pendingPromise = new Pending();\n $(e.target).one('hidden.bs.collapse', function() {\n pendingPromise.resolve();\n });\n });\n\n root.on('show.bs.collapse', '.collapse', function(e) {\n var pendingPromise = new Pending();\n $(e.target).one('shown.bs.collapse', function() {\n pendingPromise.resolve();\n });\n });\n\n $(SELECTORS.JUMPTO).focus(function() {\n var firstInput = root.find(SELECTORS.CLOSE_BUTTON);\n if (firstInput.length) {\n firstInput.focus();\n } else {\n $(SELECTORS.HEADER_CONTAINER).find(SELECTORS.ROUTES_BACK).focus();\n }\n });\n\n $(SELECTORS.DRAWER).focus(function() {\n var button = $(this).attr('data-origin');\n if (button) {\n $('#' + button).focus();\n }\n });\n\n if (!alwaysVisible) {\n PubSub.subscribe(Events.SHOW, function() {\n show(namespace, root);\n });\n\n PubSub.subscribe(Events.HIDE, function() {\n hide(root);\n });\n\n PubSub.subscribe(Events.TOGGLE_VISIBILITY, function(buttonid) {\n if (isVisible(root)) {\n hide(root);\n $(SELECTORS.JUMPTO).attr('tabindex', -1);\n } else {\n show(namespace, root);\n setJumpFrom(buttonid);\n $(SELECTORS.JUMPTO).attr('tabindex', 0);\n }\n });\n }\n\n PubSub.subscribe(Events.SHOW_CONVERSATION, function(args) {\n setJumpFrom(args.buttonid);\n show(namespace, root);\n Router.go(namespace, Routes.VIEW_CONVERSATION, args.conversationid);\n });\n\n var closebutton = root.find(SELECTORS.CLOSE_BUTTON);\n closebutton.on(CustomEvents.events.activate, function() {\n var button = $(SELECTORS.DRAWER).attr('data-origin');\n if (button) {\n $('#' + button).focus();\n }\n PubSub.publish(Events.TOGGLE_VISIBILITY);\n });\n\n PubSub.subscribe(Events.CREATE_CONVERSATION_WITH_USER, function(args) {\n setJumpFrom(args.buttonid);\n show(namespace, root);\n Router.go(namespace, Routes.VIEW_CONVERSATION, null, 'create', args.userid);\n });\n\n PubSub.subscribe(Events.SHOW_SETTINGS, function() {\n show(namespace, root);\n Router.go(namespace, Routes.VIEW_SETTINGS);\n });\n\n PubSub.subscribe(Events.PREFERENCES_UPDATED, function(preferences) {\n var filteredPreferences = preferences.filter(function(preference) {\n return preference.type == 'message_entertosend';\n });\n var enterToSendPreference = filteredPreferences.length ? filteredPreferences[0] : null;\n\n if (enterToSendPreference) {\n var viewConversationFooter = root.find(SELECTORS.FOOTER_CONTAINER).find(SELECTORS.VIEW_CONVERSATION);\n viewConversationFooter.attr('data-enter-to-send', enterToSendPreference.value);\n }\n });\n };\n\n /**\n * Initialise the message drawer.\n *\n * @param {Object} root The message drawer container.\n * @param {String} uniqueId Unique identifier for the Routes\n * @param {bool} alwaysVisible Should we show the app now, or wait for the user?\n * @param {Object} route\n */\n var init = function(root, uniqueId, alwaysVisible, route) {\n root = $(root);\n createRoutes(uniqueId, root);\n registerEventListeners(uniqueId, root, alwaysVisible);\n\n if (alwaysVisible) {\n show(uniqueId, root);\n\n if (route) {\n var routeParams = route.params || [];\n routeParams = [uniqueId, route.path].concat(routeParams);\n Router.go.apply(null, routeParams);\n }\n }\n };\n\n return {\n init: init,\n };\n});\n"],"file":"message_drawer.min.js"} \ No newline at end of file diff --git a/message/amd/src/message_drawer.js b/message/amd/src/message_drawer.js index c36f8b03a9b..03a4866cc19 100644 --- a/message/amd/src/message_drawer.js +++ b/message/amd/src/message_drawer.js @@ -254,7 +254,7 @@ function( }); $(SELECTORS.JUMPTO).focus(function() { - var firstInput = $(SELECTORS.HEADER_CONTAINER).find('input:visible'); + var firstInput = root.find(SELECTORS.CLOSE_BUTTON); if (firstInput.length) { firstInput.focus(); } else { @@ -298,6 +298,10 @@ function( var closebutton = root.find(SELECTORS.CLOSE_BUTTON); closebutton.on(CustomEvents.events.activate, function() { + var button = $(SELECTORS.DRAWER).attr('data-origin'); + if (button) { + $('#' + button).focus(); + } PubSub.publish(Events.TOGGLE_VISIBILITY); }); diff --git a/message/templates/message_drawer.mustache b/message/templates/message_drawer.mustache index 215cf15d922..c29d841aa78 100644 --- a/message/templates/message_drawer.mustache +++ b/message/templates/message_drawer.mustache @@ -36,8 +36,8 @@ {{< core/drawer}} {{$drawercontent}}
    -
    - + diff --git a/message/templates/message_drawer_view_contacts_header.mustache b/message/templates/message_drawer_view_contacts_header.mustache index cef660d585e..ff09a4b124f 100644 --- a/message/templates/message_drawer_view_contacts_header.mustache +++ b/message/templates/message_drawer_view_contacts_header.mustache @@ -34,7 +34,7 @@ {} }} -