--- /dev/null
+@tool_behat
+Feature: Verify that keyboard steps work as expected
+ In order to use behat step definitions
+ As a test writer
+ I need to verify that the keyboard steps work as expected
+
+ @javascript
+ Scenario: Typing keys into a field causes them to be input
+ Given the following "users" exist:
+ | username | email | firstname | lastname | password |
+ | saffronr | saffron.rutledge@example.com | Saffron | Rutledge | flowerpower |
+ Given I click on "Log in" "link"
+ And I click on "Username" "field"
+ When I type "saffronr"
+ And I press the tab key
+ And I type "flowerpower"
+ And I press enter
+ Then I should see "You are logged in as Saffron Rutledge"
+
+ @javascript
+ Scenario: Using tab changes focus to the next or previous field
+ Given I click on "Log in" "link"
+ And I click on "Username" "field"
+ And the focused element is "Username" "field"
+ When I press the tab key
+ Then the focused element is "Password" "field"
+
+ And I press the shift tab key
+ And the focused element is "Username" "field"
+
+ @javascript
+ Scenario: Using the arrow keys allows me to navigate through menus
+ Given the following "users" exist:
+ | username | email | firstname | lastname |
+ | saffronr | saffron.rutledge@example.com | Saffron | Rutledge |
+ And I log in as "saffronr"
+ And I click on "Saffron Rutledge" "link" in the ".usermenu" "css_element"
+ When I press the up key
+ Then the focused element is "Log out" "link"
+
+ @javascript
+ Scenario: The escape key can be used to close a dialogue
+ Given the following "course" exists:
+ | fullname | C1|
+ | shortname | C1 |
+ And I log in as "admin"
+ And I am on "C1" course homepage
+ And I navigate to course participants
+ And I press "Enrol users"
+ And "Enrol users" "dialogue" should be visible
+ When I press the escape key
+ Then "Enrol users" "dialogue" should not be visible
And I open the action menu in "John Doe" "table_row"
And I should see "View the request"
But I should not see "Mark as complete"
- And I press key "27" in ".moodle-actionmenu" "css_element"
+ And I press the escape key
And I open the action menu in "Jane Doe" "table_row"
And I choose "Mark as complete" in the open action menu
And I should see "Do you really want to mark this user enquiry as complete?"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
And I click on "Contract (GDPR Art. 6.1(b))" "list_item"
And I click on "Legal obligation (GDPR Art 6.1(c))" "list_item"
- And I press key "27" in the field "Lawful bases"
+ And I press the escape key
And I click on ".form-autocomplete-downarrow" "css_element" in the "Sensitive personal data processing reasons" "form_row"
And I click on "Explicit consent (GDPR Art. 9.2(a))" "list_item"
- And I press key "27" in the field "Sensitive personal data processing reasons"
+ And I press the escape key
And I set the field "retentionperiodnumber" to "2"
When I press "Save"
Then I should see "Purpose 1" in the "List of data purposes" "table"
And I click on "Legal obligation (GDPR Art 6.1(c))" "text" in the ".form-autocomplete-selection" "css_element"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Lawful bases" "form_row"
And I click on "Vital interests (GDPR Art. 6.1(d))" "list_item"
- And I press key "27" in the field "Lawful bases"
+ And I press the escape key
And I set the field "retentionperiodnumber" to "3"
And I click on "protected" "checkbox"
When I press "Save changes"
Then I should see "Test-Comp1"
And I should see "Test-Comp2"
And I set the field "Filter competencies by resource or activity" to "PageName1"
- And I press key "13" in the field "Filter competencies by resource or activity"
+ And I press the enter key
And I should see "Test-Comp1"
And I should not see "Test-Comp2"
And I set the field "Filter competencies by resource or activity" to "PageName2"
- And I press key "13" in the field "Filter competencies by resource or activity"
+ And I press the enter key
And I should not see "Test-Comp1"
And I should not see "Test-Comp2"
And I should see "No competencies have been linked to this activity or resource."
And I click on ".template-userplans" "css_element" in the "Science template" "table_row"
And I open the autocomplete suggestions list
And I click on "Admin User" item in the autocomplete list
- And I press key "27" in the field "Select users to create learning plans for"
+ And I press the escape key
When I click on "Create learning plans" "button"
Then I should see "A learning plan was created"
And I should see "Admin User" in the "Science template" "table_row"
And I click on ".template-cohorts" "css_element" in the "Science template cohort" "table_row"
And I click on ".form-autocomplete-downarrow" "css_element"
And I click on "cohort plan" item in the autocomplete list
- And I press key "27" in the field "Select cohorts to sync"
+ And I press the escape key
When I click on "Add cohorts" "button"
Then I should see "2 learning plans were created."
And I follow "Learning plan templates"
// Action requires session key.
require_sesskey();
+\core\session\manager::write_close();
+
// Prepare to handle output via mtrace.
echo html_writer::start_tag('pre');
$CFG->mtrace_wrapper = 'tool_task_mtrace_wrapper';
use templatable;
use renderer_base;
use stdClass;
+use core_collator;
+use core_component;
use core_plugin_manager;
use tool_templatelibrary\api;
* @return stdClass
*/
public function export_for_template(renderer_base $output) {
- $data = new stdClass();
- $data->allcomponents = array();
$fulltemplatenames = api::list_templates();
$pluginmanager = core_plugin_manager::instance();
- $components = array();
+ $components = [];
foreach ($fulltemplatenames as $templatename) {
- list($component, $templatename) = explode('/', $templatename, 2);
- $components[$component] = 1;
- }
+ [$component, ] = explode('/', $templatename, 2);
+ [$type, ] = core_component::normalize_component($component);
+
+ // Core sub-systems are grouped together and are denoted by a distinct lang string.
+ $coresubsystem = (strpos($component, 'core') === 0);
+
+ if (!array_key_exists($type, $components)) {
+ $typename = $coresubsystem
+ ? get_string('core', 'tool_templatelibrary')
+ : $pluginmanager->plugintype_name_plural($type);
- $components = array_keys($components);
- foreach ($components as $component) {
- $info = new stdClass();
- $info->component = $component;
- if (strpos($component, 'core') === 0) {
- $info->name = get_string('coresubsystem', 'tool_templatelibrary', $component);
- } else {
- $info->name = $pluginmanager->plugin_name($component);
+ $components[$type] = (object) [
+ 'type' => $typename,
+ 'plugins' => [],
+ ];
}
- $data->allcomponents[] = $info;
+
+ $pluginname = $coresubsystem
+ ? get_string('coresubsystem', 'tool_templatelibrary', $component)
+ : $pluginmanager->plugin_name($component);
+
+ $components[$type]->plugins[$component] = (object) [
+ 'name' => $pluginname,
+ 'component' => $component,
+ ];
}
- return $data;
+ // Sort returned components according to their type, followed by name.
+ core_collator::asort_objects_by_property($components, 'type');
+ array_walk($components, function(stdClass $component) {
+ core_collator::asort_objects_by_property($component->plugins, 'name');
+ $component->plugins = array_values($component->plugins);
+ });
+
+ return (object) [
+ 'allcomponents' => array_values($components),
+ ];
}
}
$string['all'] = 'All components';
$string['component'] = 'Component';
+$string['core'] = 'Core';
$string['coresubsystem'] = 'Subsystem ({$a})';
$string['documentation'] = 'Documentation';
$string['example'] = 'Example';
<select id="selectcomponent" class="form-control" data-field="component">
<option value="">{{#str}}all, tool_templatelibrary{{/str}}</option>
{{#allcomponents}}
- <option value="{{component}}">{{name}}</option>
+ <optgroup label="{{type}}">
+ {{#plugins}}
+ <option value="{{component}}">{{name}}</option>
+ {{/plugins}}
+ </optgroup>
{{/allcomponents}}
</select>
{{/element}}
$select = "instanceid $csql AND fieldid = :fieldid";
$params['fieldid'] = $fieldid;
$distinctablevalue = $DB->sql_compare_text('value');
- $values = $DB->get_records_select_menu('customfield_data', $select, $params, $DB->sql_order_by_text('value'),
+ $values = $DB->get_records_select_menu('customfield_data', $select, $params, '',
"DISTINCT $distinctablevalue, $distinctablevalue AS value2");
+ \core_collator::asort($values, \core_collator::SORT_NATURAL);
$values = array_filter($values);
if (!$values) {
return [];
| 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 key "13" in the field "New name for activity My forum name"
+ And I press the enter key
Then I should not see "My forum name"
And I should see "New forum name"
And I follow "New 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 key "13" in the field "New name for activity My forum name"
+ And I press the enter key
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"
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 key "13" in the field "New name for cohort Test cohort name"
+ And I press the enter key
Then I should not see "Test cohort name"
And I should see "Students cohort"
And I follow "Cohorts"
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 key "13" in the field "New name for topic Topic 1"
+ And I press the enter key
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"
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 key "13" in the field "New name for week 1 May - 7 May"
+ And I press the enter key
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"
# 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 key "13" in the field "New name for activity Test forum name"
+ And I press the enter key
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"
# 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 press key "27" in the field "New name for activity Good news"
+ And I press the escape key
And "New name for activity Good news" "field" should not exist
And I should see "Good news"
And I should not see "Terrible news"
And I am on "Course 1" course homepage
And I should see "Good news"
And I should not see "Terrible news"
- And I log out
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 key "13" in the field "New value for Category for test"
+ And I press the enter key
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"
*/
public function edit_instance_validation($data, $files, $instance, $context) {
global $DB;
+
$errors = array();
$thiscourseid = $context->instanceid;
- $c = false;
if (!empty($data['customint1'])) {
- $courses = is_array($data['customint1']) ? $data['customint1'] : [$data['customint1']];
- foreach ($courses as $courseid) {
- $c = $DB->get_record('course', array('id' => $courseid), '*', MUST_EXIST);
- $coursecontext = context_course::instance($c->id);
-
- $sqlexists = 'enrol = :meta AND courseid = :currentcourseid AND customint1 = :courseid AND id != :id';
- $existing = $DB->record_exists_select('enrol', $sqlexists, [
+ $coursesidarr = is_array($data['customint1']) ? $data['customint1'] : [$data['customint1']];
+ list($coursesinsql, $coursesinparams) = $DB->get_in_or_equal($coursesidarr, SQL_PARAMS_NAMED, 'metacourseid');
+ if ($coursesrecords = $DB->get_records_select('course', "id {$coursesinsql}",
+ $coursesinparams, '', 'id,visible')) {
+ // Cast NULL to 0 to avoid possible mess with the SQL.
+ $instanceid = $instance->id ?? 0;
+
+ $existssql = "enrol = :meta AND courseid = :currentcourseid AND id != :id AND customint1 {$coursesinsql}";
+ $existsparams = [
'meta' => 'meta',
'currentcourseid' => $thiscourseid,
- 'courseid' => $c->id,
- 'id' => $instance->id
- ]);
-
- if (!$c->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
- $errors['customint1'] = get_string('error');
- } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
- $errors['customint1'] = get_string('error');
- } else if ($c->id == SITEID or $c->id == $thiscourseid or $existing) {
- $errors['customint1'] = get_string('error');
+ 'id' => $instanceid
+ ];
+ $existsparams += $coursesinparams;
+ if ($DB->record_exists_select('enrol', $existssql, $existsparams)) {
+ // We may leave right here as further checks do not make sense in case we have existing enrol records
+ // with the parameters from above.
+ $errors['customint1'] = get_string('invalidcourseid', 'error');
+ } else {
+ foreach ($coursesrecords as $coursesrecord) {
+ $coursecontext = context_course::instance($coursesrecord->id);
+ if (!$coursesrecord->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext)) {
+ $errors['customint1'] = get_string('nopermissions', 'error',
+ 'moodle/course:viewhiddencourses');
+ } else if (!has_capability('enrol/meta:selectaslinked', $coursecontext)) {
+ $errors['customint1'] = get_string('nopermissions', 'error',
+ 'enrol/meta:selectaslinked');
+ } else if ($coursesrecord->id == SITEID or $coursesrecord->id == $thiscourseid) {
+ $errors['customint1'] = get_string('invalidcourseid', 'error');
+ }
+ }
}
+ } else {
+ $errors['customint1'] = get_string('invalidcourseid', 'error');
}
} else {
$errors['customint1'] = get_string('required');
// Meta-link enrolment has enrol actions for suspended students -- unenrol.
$this->assertCount(1, $actions);
}
+
+ /**
+ * Test how data for instance editing is validated.
+ */
+ public function test_edit_instance_validation() {
+ global $DB;
+
+ $this->resetAfterTest();
+
+ $metaplugin = enrol_get_plugin('meta');
+
+ // A course with meta enrolment.
+ $course = $this->getDataGenerator()->create_course();
+ $coursecontext = context_course::instance($course->id);
+
+ // Create a meta enrolment instance.
+ $instance = (object)$metaplugin->get_instance_defaults();
+ $instance->id = null;
+ $instance->courseid = $course->id;
+ $instance->status = ENROL_INSTANCE_ENABLED;
+ // Emulate the form data.
+ $data = [
+ 'customint1' => 0,
+ 'customint2' => 0
+ ];
+ // Test when no valid 'customint1' field (meta courses links) is provided.
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ // We're going to check the string contents of the errors returned as this is the only way
+ // to differentiate the errors produced by the 'edit_instance_validation()' method somehow.
+ // The method always returns what the edit instance form expects and this is an array of form fields
+ // with the corresponding errors messages.
+ $this->assertEquals('Required', $errors['customint1']);
+
+ // Test when 'customint1' contains an unknown course.
+ // Fetch the max course id from the courses table and increment it to get
+ // the course id which surely doesn't exist.
+ $maxid = $DB->get_field_sql('SELECT MAX(id) FROM {course}');
+ // Use the same instance as before but set another data.
+ $data = [
+ 'customint1' => [$maxid + 1],
+ 'customint2' => 0
+ ];
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ $this->assertEquals('You are trying to use an invalid course ID', $errors['customint1']);
+
+ // Test when 'customint1' field already contains courses meta linked with the current one.
+ $metacourse1 = $this->getDataGenerator()->create_course();
+ $metaplugin->add_instance($course, array('customint1' => $metacourse1->id));
+ // Use the same instance as before but set another data.
+ $data = [
+ 'customint1' => [$metacourse1->id],
+ 'customint2' => 0
+ ];
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ $this->assertEquals('You are trying to use an invalid course ID', $errors['customint1']);
+
+ // Test when a course is set as a not visible and a user doesn't have the capability to use it here.
+ $metacourse2record = new stdClass();
+ $metacourse2record->visible = 0;
+ $metacourse2 = $this->getDataGenerator()->create_course($metacourse2record);
+ $metacourse2context = context_course::instance($metacourse2->id);
+
+ $user = $this->getDataGenerator()->create_user();
+ $teacherrole = $DB->get_record('role', array('shortname' => 'teacher'));
+ role_assign($teacherrole->id, $user->id, $metacourse2context->id);
+ unassign_capability('moodle/course:viewhiddencourses', $teacherrole->id);
+ $this->setUser($user);
+
+ // Use the same instance as before but set another data.
+ $data = [
+ 'customint1' => [$metacourse2->id],
+ 'customint2' => 0
+ ];
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ $this->assertEquals('Sorry, but you do not currently have permissions to do that (moodle/course:viewhiddencourses).',
+ $errors['customint1']);
+
+ // Revert some changes from the last assertion to reuse the course.
+ $metacourse2->visible = 1;
+ $DB->update_record('course', $metacourse2);
+ assign_capability('moodle/course:viewhiddencourses', CAP_ALLOW,
+ $teacherrole->id, context_course::instance($metacourse2->id));
+
+ // Test with no 'enrol/meta:selectaslinked' capability.
+ unassign_capability('enrol/meta:selectaslinked', $teacherrole->id);
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ $this->assertEquals('Sorry, but you do not currently have permissions to do that (enrol/meta:selectaslinked).',
+ $errors['customint1']);
+
+ // Back to admin user to regain the capabilities quickly.
+ $this->setAdminUser();
+
+ // Test when meta course id is the site id.
+ $site = $DB->get_record('course', ['id' => SITEID]);
+ // Use the same instance as before but set another data.
+ $data = [
+ 'customint1' => [$site->id],
+ 'customint2' => 0
+ ];
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ $this->assertEquals('You are trying to use an invalid course ID', $errors['customint1']);
+
+ // Test when meta course id is id of the current course.
+ // Use the same instance as before but set another data.
+ $data = [
+ 'customint1' => [$course->id],
+ 'customint2' => 0
+ ];
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ $this->assertEquals('You are trying to use an invalid course ID', $errors['customint1']);
+
+ // Test with the 'customint2' field set (which is groups).
+ // Prepare some groups data.
+ $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+ $this->getDataGenerator()->create_group(array('courseid' => $course->id));
+ $groups = [];
+ foreach (groups_get_all_groups($course->id) as $group) {
+ $groups[$group->id] = format_string($group->name, true, array('context' => $coursecontext));
+ }
+
+ // Use the same instance as before but set another data.
+ // Use a non-existing group id.
+ if (!$maxid = $DB->get_field_sql('SELECT MAX(id) FROM {groups}')) {
+ $maxid = 0;
+ }
+ $data = [
+ 'customint1' => [$metacourse2->id],
+ 'customint2' => [$maxid + 1]
+ ];
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ $this->assertArrayHasKey('customint2', $errors);
+
+ // Test with valid data.
+ $validgroup = reset($groups);
+ $data = [
+ 'customint1' => [$metacourse2->id],
+ 'customint2' => $validgroup
+ ];
+ $errors = $metaplugin->edit_instance_validation($data, [], $instance, $coursecontext);
+ $this->assertArrayNotHasKey('customint1', $errors);
+ $this->assertArrayNotHasKey('customint2', $errors);
+ }
}
And I click on "Group 1" item in the autocomplete list
And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
And I click on "Group 2" item in the autocomplete list
- And I press key "27" in the field "Edit groups for \"Student 1\""
+ And I press the escape key
And I click on "Save changes" "link" in the "student1" "table_row"
Then I should see "Group 1, Group 2"
$graderreportsifirst = optional_param('sifirst', null, PARAM_NOTAGS);
$graderreportsilast = optional_param('silast', null, PARAM_NOTAGS);
-// The report object is recreated each time, save search information to SESSION object for future use.
-if (isset($graderreportsifirst)) {
- $SESSION->gradereport['filterfirstname'] = $graderreportsifirst;
-}
-if (isset($graderreportsilast)) {
- $SESSION->gradereport['filtersurname'] = $graderreportsilast;
-}
-
$PAGE->set_url(new moodle_url('/grade/report/grader/index.php', array('id'=>$courseid)));
$PAGE->requires->yui_module('moodle-gradereport_grader-gradereporttable', 'Y.M.gradereport_grader.init', null, null, true);
require_login($course);
$context = context_course::instance($course->id);
+// The report object is recreated each time, save search information to SESSION object for future use.
+if (isset($graderreportsifirst)) {
+ $SESSION->gradereport["filterfirstname-{$context->id}"] = $graderreportsifirst;
+}
+if (isset($graderreportsilast)) {
+ $SESSION->gradereport["filtersurname-{$context->id}"] = $graderreportsilast;
+}
+
require_capability('gradereport/grader:view', $context);
require_capability('moodle/grade:viewall', $context);
// User search
$url = new moodle_url('/grade/report/grader/index.php', array('id' => $course->id));
-$firstinitial = isset($SESSION->gradereport['filterfirstname']) ? $SESSION->gradereport['filterfirstname'] : '';
-$lastinitial = isset($SESSION->gradereport['filtersurname']) ? $SESSION->gradereport['filtersurname'] : '';
+$firstinitial = $SESSION->gradereport["filterfirstname-{$context->id}"] ?? '';
+$lastinitial = $SESSION->gradereport["filtersurname-{$context->id}"] ?? '';
$totalusers = $report->get_numusers(true, false);
$renderer = $PAGE->get_renderer('core_user');
echo $renderer->user_search($url, $firstinitial, $lastinitial, $numusers, $totalusers, $report->currentgroupname);
Then I should see a grade field for "Student 2" and grade item "Item VU"
And I should not see a feedback field for "Student 2" and grade item "Item VU"
And I set the field "ajaxgrade" to "33"
- And I press key "13" in the field "ajaxgrade"
+ And I press the enter key
And I should not see a grade field for "Student 2" and grade item "Item VU"
And I should not see a feedback field for "Student 2" and grade item "Item VU"
+ And I click on student "Student 3" for grade item "Item SU"
+ And I set the field "ajaxgrade" to "Very good"
+ And I press the shift tab key
And I click on student "Student 3" for grade item "Item VU"
And I set the field "ajaxgrade" to "50"
- And I press key "13" in the field "ajaxgrade"
+ And I press the enter key
And I click on student "Student 3" for grade item "Item 1"
And I set the field "ajaxgrade" to "80"
- And I press key "13" in the field "ajaxgrade"
- And I click on student "Student 3" for grade item "Item SU"
- And I set the field "ajaxgrade" to "Very good"
- And I press key "13" in the field "ajaxgrade"
+ And I press the enter key
And the following should exist in the "user-grades" table:
| -1- | -6- | -7- | -13- | -16- |
| Student 2 | - | 33.00 | - | 33.00 |
And I should see a grade field for "Student 1" and grade item "Course total"
And I should not see a feedback field for "Student 1" and grade item "Course total"
And I set the field "ajaxgrade" to "90"
- And I press key "13" in the field "ajaxgrade"
+ And I press the enter key
And the following should exist in the "user-grades" table:
| -1- | -16- |
| Student 1 | 90.00 |
And I should see a feedback field for "Student 2" and grade item "Item VU"
And I set the field "ajaxgrade" to "33"
And I set the field "ajaxfeedback" to "Student 2 VU feedback"
- And I press key "13" in the field "ajaxfeedback"
+ And I press the enter key
And I click on student "Student 3" for grade item "Item VL"
And I should not see a grade field for "Student 3" and grade item "Item VL"
And I should not see a feedback field for "Student 3" and grade item "Item VL"
And I should not see a grade field for "Student 3" and grade item "Item TU"
And I should see a feedback field for "Student 3" and grade item "Item TU"
And I set the field "ajaxfeedback" to "Student 3 TU feedback"
- And I press key "13" in the field "ajaxfeedback"
+ And I press the enter key
And I click on student "Student 2" for grade item "Item SU"
And I set the field "ajaxgrade" to "Very good"
And I set the field "ajaxfeedback" to "Student 2 SU feedback"
- And I press key "13" in the field "ajaxfeedback"
+ And I press the enter key
# Reload grader report:
And I navigate to "View > User report" in the course gradebook
And I navigate to "View > Grader report" in the course gradebook
Then I should see a grade field for "Student 2" and grade item "Item VU"
And I should see a feedback field for "Student 2" and grade item "Item VU"
And I set the field "ajaxgrade" to "33"
- And I press key "13" in the field "ajaxgrade"
+ And I press the enter key
And I click on student "Student 2" for grade item "Course total"
And I should not see a grade field for "Student 3" and grade item "Course total"
And I should not see a feedback field for "Student 3" and grade item "Course total"
public function setup_users() {
global $SESSION, $DB;
+ $filterfirstnamekey = "filterfirstname-{$this->context->id}";
+ $filtersurnamekey = "filtersurname-{$this->context->id}";
+
$this->userwheresql = "";
$this->userwheresql_params = array();
- if (isset($SESSION->gradereport['filterfirstname']) && !empty($SESSION->gradereport['filterfirstname'])) {
+ if (!empty($SESSION->gradereport[$filterfirstnamekey])) {
$this->userwheresql .= ' AND '.$DB->sql_like('u.firstname', ':firstname', false, false);
- $this->userwheresql_params['firstname'] = $SESSION->gradereport['filterfirstname'].'%';
+ $this->userwheresql_params['firstname'] = $SESSION->gradereport[$filterfirstnamekey] . '%';
}
- if (isset($SESSION->gradereport['filtersurname']) && !empty($SESSION->gradereport['filtersurname'])) {
+ if (!empty($SESSION->gradereport[$filtersurnamekey])) {
$this->userwheresql .= ' AND '.$DB->sql_like('u.lastname', ':lastname', false, false);
- $this->userwheresql_params['lastname'] = $SESSION->gradereport['filtersurname'].'%';
+ $this->userwheresql_params['lastname'] = $SESSION->gradereport[$filtersurnamekey] . '%';
}
}
--- /dev/null
+// This file is part of Moodle - http://moodle.org/
+//
+// Moodle is free software: you can redistribute it and/or modify
+// it under the terms of the GNU General Public License as published by
+// the Free Software Foundation, either version 3 of the License, or
+// (at your option) any later version.
+//
+// Moodle is distributed in the hope that it will be useful,
+// but WITHOUT ANY WARRANTY; without even the implied warranty of
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// GNU General Public License for more details.
+//
+// You should have received a copy of the GNU General Public License
+// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
+
+/**
+ * Detects if an element is fullscreen.
+ *
+ * @module core/fullscreen
+ * @class fullscreen
+ * @package core
+ * @copyright 2020 University of Nottingham
+ * @author Neill Magill <neill.magill@nottingham.ac.uk>
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+/**
+ * Gets the element that is fullscreen or null if no element is fullscreen.
+ *
+ * @returns {HTMLElement}
+ */
+export const getElement = () => {
+ let element = null;
+ if (document.fullscreenElement) {
+ element = document.fullscreenElement;
+ } else if (document.mozFullscreenElement) {
+ // Fallback for older Firefox.
+ element = document.mozFullscreenElement;
+ } else if (document.msFullscreenElement) {
+ // Fallback for Edge and IE.
+ element = document.msFullscreenElement;
+ } else if (document.webkitFullscreenElement) {
+ // Fallback for Chrome, Edge and Safari.
+ element = document.webkitFullscreenElement;
+ }
+
+ return element;
+};
'core/local/aria/focuslock',
'core/pending',
'core/aria',
-], function($, Templates, Notification, KeyCodes, CustomEvents, ModalBackdrop, Event, ModalEvents, FocusLock, Pending, Aria) {
+ 'core/fullscreen'
+], function(
+ $,
+ Templates,
+ Notification,
+ KeyCodes,
+ CustomEvents,
+ ModalBackdrop,
+ Event,
+ ModalEvents,
+ FocusLock,
+ Pending,
+ Aria,
+ Fullscreen
+) {
var SELECTORS = {
CONTAINER: '[data-region="modal-container"]',
};
/**
- * Add the modal to the page, if it hasn't already been added. This includes running any
+ * Attach the modal to the correct part of the page.
+ *
+ * If it hasn't already been added it runs any
* javascript that has been cached until now.
*
* @method attachToDOM
*/
Modal.prototype.attachToDOM = function() {
+ this.getAttachmentPoint().append(this.root);
+
if (this.isAttached) {
return;
}
- $('body').append(this.root);
FocusLock.trapFocus(this.root[0]);
// If we'd cached any JS then we can run it how that the modal is
return this.getRoot().hasClass('fade');
};
+ /**
+ * Gets the jQuery wrapped node that the Modal should be attached to.
+ *
+ * @returns {jQuery}
+ */
+ Modal.prototype.getAttachmentPoint = function() {
+ return $(Fullscreen.getElement() || document.body);
+ };
+
/**
* Display this modal. The modal will be attached to the DOM if it hasn't
* already been.
this.hideFooter();
}
- if (!this.isAttached) {
- this.attachToDOM();
- }
+ this.attachToDOM();
return this.getBackdrop()
.then(function(backdrop) {
Modal.prototype.hide = function() {
this.getBackdrop().done(function(backdrop) {
FocusLock.untrapFocus();
+
if (!this.countOtherVisibleModals()) {
// Hide the backdrop if we're the last open modal.
backdrop.hide();
this.getRoot().removeClass('show').addClass('hide');
}
+ // Ensure the modal is moved onto the body node if it is still attached to the DOM.
+ if ($(document.body).find(this.getRoot()).length) {
+ $(document.body).append(this.getRoot());
+ }
+
this.root.trigger(ModalEvents.hidden, this);
}.bind(this));
};
* @copyright 2016 Ryan Wyllie <ryan@moodle.com>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-define(['jquery', 'core/templates', 'core/notification'],
- function($, Templates, Notification) {
+define(['jquery', 'core/templates', 'core/notification', 'core/fullscreen'],
+ function($, Templates, Notification, Fullscreen) {
var SELECTORS = {
ROOT: '[data-region="modal-backdrop"]',
return this.root;
};
+ /**
+ * Gets the jQuery wrapped node that the Modal should be attached to.
+ *
+ * @returns {jQuery}
+ */
+ ModalBackdrop.prototype.getAttachmentPoint = function() {
+ return $(Fullscreen.getElement() || document.body);
+ };
+
/**
* Add the modal backdrop to the page, if it hasn't already been added.
*
* @method attachToDOM
*/
ModalBackdrop.prototype.attachToDOM = function() {
+ this.getAttachmentPoint().append(this.root);
+
if (this.isAttached) {
return;
}
- $('body').append(this.root);
this.isAttached = true;
};
return;
}
- if (!this.isAttached) {
- this.attachToDOM();
- }
+ this.attachToDOM();
this.root.removeClass('hide').addClass('show');
};
} else {
this.getRoot().removeClass('show').addClass('hide');
}
+
+ // Ensure the modal is moved onto the body node if it is still attached to the DOM.
+ if ($(document.body).find(this.getRoot()).length) {
+ $(document.body).append(this.getRoot());
+ }
};
/**
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');
+
/**
* Steps definitions base class.
*
];
}
+ /**
+ * 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.
*
$suggestion->click();
} else {
// Press the return key to create a new tag.
- // Note: We cannot use $this->key_press() because the keyPress action, in combination with the keyDown
- // submits the form.
- $this->field->keyDown(13);
- $this->field->keyUp(13);
+ behat_base::type_keys($this->session, [behat_keys::ENTER]);
}
-
$this->wait_for_pending_js();
- $this->key_press(27);
+
+ // Press the escape to close the autocomplete suggestions list.
+ behat_base::type_keys($this->session, [behat_keys::ESCAPE]);
$this->wait_for_pending_js();
}
}
upgrade_main_savepoint(true, 2021052500.36);
}
+ if ($oldversion < 2021052500.42) {
+ // Get all lessons that are set with a completion criteria of 'requires grade' but with no grade type set.
+ $sql = "SELECT cm.id
+ FROM {course_modules} cm
+ JOIN {lesson} l ON l.id = cm.instance
+ JOIN {modules} m ON m.id = cm.module
+ WHERE m.name = :name AND cm.completiongradeitemnumber IS NOT NULL AND l.grade = :grade";
+
+ do {
+ if ($invalidconfigrations = $DB->get_records_sql($sql, ['name' => 'lesson', 'grade' => 0], 0, 1000)) {
+ list($insql, $inparams) = $DB->get_in_or_equal(array_keys($invalidconfigrations), SQL_PARAMS_NAMED);
+ $DB->set_field_select('course_modules', 'completiongradeitemnumber', null, "id $insql", $inparams);
+ }
+ } while ($invalidconfigrations);
+
+ upgrade_main_savepoint(true, 2021052500.42);
+ }
+
return true;
}
*
* NOTE: this function is called from lib/db/upgrade.php
*
- * @static string|false $siteidentifier The site identifier is not cached. We use this static cache so
- * that we need only fetch it once per request.
* @param string $plugin full component name
* @param string $name default null
* @return mixed hash-like object or single value, return false no config found
function get_config($plugin, $name = null) {
global $CFG, $DB;
- static $siteidentifier = null;
-
if ($plugin === 'moodle' || $plugin === 'core' || empty($plugin)) {
$forced =& $CFG->config_php_settings;
- $iscore = true;
+ $table = 'config';
+ $filter = [];
$plugin = 'core';
} else {
if (array_key_exists($plugin, $CFG->forced_plugin_settings)) {
$forced =& $CFG->forced_plugin_settings[$plugin];
} else {
- $forced = array();
+ $forced = [];
}
- $iscore = false;
+ $table = 'config_plugins';
+ $filter = ['plugin' => $plugin];
}
- if ($siteidentifier === null) {
- try {
- // This may fail during installation.
- // If you have a look at {@link initialise_cfg()} you will see that this is how we detect the need to
- // install the database.
- $siteidentifier = $DB->get_field('config', 'value', array('name' => 'siteidentifier'));
- } catch (dml_exception $ex) {
- // Set siteidentifier to false. We don't want to trip this continually.
- $siteidentifier = false;
- throw $ex;
- }
- }
-
- if (!empty($name)) {
- if (array_key_exists($name, $forced)) {
- return (string)$forced[$name];
- } else if ($name === 'siteidentifier' && $plugin == 'core') {
- return $siteidentifier;
- }
+ if (!empty($name) && array_key_exists($name, $forced)) {
+ return (string)$forced[$name];
}
$cache = cache::make('core', 'config');
$result = $cache->get($plugin);
if ($result === false) {
- // The user is after a recordset.
- if (!$iscore) {
- $result = $DB->get_records_menu('config_plugins', array('plugin' => $plugin), '', 'name,value');
- } else {
- // This part is not really used any more, but anyway...
- $result = $DB->get_records_menu('config', array(), '', 'name,value');;
- }
+ $result = $DB->get_records_menu($table, $filter, '', 'name,value');
$cache->set($plugin, $result);
}
return false;
}
- if ($plugin === 'core') {
- $result['siteidentifier'] = $siteidentifier;
- }
-
foreach ($forced as $key => $value) {
if (is_null($value) or is_array($value) or is_object($value)) {
// We do not want any extra mess here, just real settings that could be saved in db.
function print_headers() {
global $CFG, $OUTPUT;
+ // Set the primary sort column/order where possible, so that sort links/icons are correct.
+ [
+ 'sortby' => $primarysortcolumn,
+ 'sortorder' => $primarysortorder,
+ ] = $this->get_primary_sort_order();
+
echo html_writer::start_tag('thead');
echo html_writer::start_tag('tr');
foreach ($this->columns as $column => $index) {
if ($this->is_collapsible) {
$icon_hide = $this->show_hide_link($column, $index);
}
-
- $primarysortcolumn = '';
- $primarysortorder = '';
- if (reset($this->prefs['sortby'])) {
- $primarysortcolumn = key($this->prefs['sortby']);
- $primarysortorder = current($this->prefs['sortby']);
- }
-
switch ($column) {
case 'fullname':
]) . ' ' . $this->sort_icon($isprimary, $order);
}
+ /**
+ * Return primary sorting column/order, either the first preferred "sortby" value or defaults defined for the table
+ *
+ * @return array
+ */
+ protected function get_primary_sort_order(): array {
+ if (reset($this->prefs['sortby'])) {
+ return $this->get_sort_order();
+ }
+
+ return [
+ 'sortby' => $this->sort_default_column,
+ 'sortorder' => $this->sort_default_order,
+ ];
+ }
+
/**
* Return sorting attributes values.
*
# The menu should now be visible.
Then ".usermenu [role='menu']" "css_element" should be visible
# Press down arrow.
- And I press key "40" in "#actionmenuaction-1" "css_element"
+ And I press the down key
# The menu should still be visible.
And ".usermenu [role='menu']" "css_element" should be visible
require_once(__DIR__ . '/../../behat/behat_base.php');
-use Behat\Mink\Exception\ExpectationException as ExpectationException,
- Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException,
- Behat\Mink\Exception\DriverException as DriverException,
- WebDriver\Exception\NoSuchElement as NoSuchElement,
- WebDriver\Exception\StaleElementReference as StaleElementReference,
- Behat\Gherkin\Node\TableNode as TableNode;
+use Behat\Gherkin\Node\TableNode as TableNode;
+use Behat\Mink\Exception\DriverException as DriverException;
+use Behat\Mink\Exception\ElementNotFoundException as ElementNotFoundException;
+use Behat\Mink\Exception\ExpectationException as ExpectationException;
+use WebDriver\Exception\NoSuchElement as NoSuchElement;
+use WebDriver\Exception\StaleElementReference as StaleElementReference;
/**
* Cross component steps definitions.
}
}
+ /**
+ * Send key presses to the browser without first changing focusing, or applying the key presses to a specific
+ * element.
+ *
+ * Example usage of this step:
+ * When I type "Penguin"
+ *
+ * @When I type :keys
+ * @param string $keys The key, or list of keys, to type
+ */
+ public function i_type(string $keys): void {
+ behat_base::type_keys($this->getSession(), str_split($keys));
+ }
+
+ /**
+ * Press a named key with an optional set of modifiers.
+ *
+ * Supported named keys are:
+ * - up
+ * - down
+ * - left
+ * - right
+ * - pageup|page_up
+ * - pagedown|page_down
+ * - home
+ * - end
+ * - insert
+ * - delete
+ * - backspace
+ * - escape
+ * - enter
+ * - tab
+ *
+ * Supported moderators are:
+ * - shift
+ * - ctrl
+ * - alt
+ * - meta
+ *
+ * Example usage of this new step:
+ * When I press the up key
+ * When I press the space key
+ * When I press the shift tab key
+ *
+ * Multiple moderator keys can be combined using the '+' operator, for example:
+ * When I press the ctrl+shift enter key
+ * When I press the ctrl + shift enter key
+ *
+ * @When /^I press the (?P<modifiers_string>.* )?(?P<key_string>.*) key$/
+ * @param string $modifiers A list of keyboard modifiers, separated by the `+` character
+ * @param string $key The name of the key to press
+ */
+ public function i_press_named_key(string $modifiers, string $key): void {
+ behat_base::require_javascript_in_session($this->getSession());
+
+ $keys = [];
+
+ foreach (explode('+', $modifiers) as $modifier) {
+ switch (strtoupper(trim($modifier))) {
+ case '':
+ break;
+ case 'SHIFT':
+ $keys[] = behat_keys::SHIFT;
+ break;
+ case 'CTRL':
+ $keys[] = behat_keys::CONTROL;
+ break;
+ case 'ALT':
+ $keys[] = behat_keys::ALT;
+ break;
+ case 'META':
+ $keys[] = behat_keys::META;
+ break;
+ default:
+ throw new \coding_exception("Unknown modifier key '$modifier'}");
+ }
+ }
+
+ $modifier = trim($key);
+ switch (strtoupper($key)) {
+ case 'UP':
+ $keys[] = behat_keys::UP_ARROW;
+ break;
+ case 'DOWN':
+ $keys[] = behat_keys::DOWN_ARROW;
+ break;
+ case 'LEFT':
+ $keys[] = behat_keys::LEFT_ARROW;
+ break;
+ case 'RIGHT':
+ $keys[] = behat_keys::RIGHT_ARROW;
+ break;
+ case 'HOME':
+ $keys[] = behat_keys::HOME;
+ break;
+ case 'END':
+ $keys[] = behat_keys::END;
+ break;
+ case 'INSERT':
+ $keys[] = behat_keys::INSERT;
+ break;
+ case 'BACKSPACE':
+ $keys[] = behat_keys::BACKSPACE;
+ break;
+ case 'DELETE':
+ $keys[] = behat_keys::DELETE;
+ break;
+ case 'PAGEUP':
+ case 'PAGE_UP':
+ $keys[] = behat_keys::PAGE_UP;
+ break;
+ case 'PAGEDOWN':
+ case 'PAGE_DOWN':
+ $keys[] = behat_keys::PAGE_DOWN;
+ break;
+ case 'ESCAPE':
+ $keys[] = behat_keys::ESCAPE;
+ break;
+ case 'ENTER':
+ $keys[] = behat_keys::ENTER;
+ break;
+ case 'TAB':
+ $keys[] = behat_keys::TAB;
+ break;
+ case 'SPACE':
+ $keys[] = behat_keys::SPACE;
+ break;
+ default:
+ throw new \coding_exception("Unknown key '$key'}");
+ }
+
+ // Always send the NULL key as the last key.
+ $keys[] = behat_keys::NULL_KEY;
+
+ behat_base::type_keys($this->getSession(), $keys);
+ }
+
/**
* Trigger a keydown event for a key on a specific element.
*
}
// Gets the node based on the requested selector type and locator.
$node = $this->get_selected_node($selectortype, $element);
- $driver = $this->getSession()->getDriver();
- if ($driver instanceof \Moodle\BehatExtension\Driver\MoodleSelenium2Driver) {
- $driver->post_key("\xEE\x80\x84", $node->getXpath());
- } else {
- $driver->keyDown($node->getXpath(), "\t");
- }
+ $this->execute('behat_general::i_click_on', [$node, 'NodeElement']);
+ $this->execute('behat_general::i_press_named_key', ['', 'tab']);
}
/**
* @throws DriverException
*/
public function i_manually_press_tab($shift = '') {
- if (!$this->running_javascript()) {
- throw new DriverException($shift . ' Tab press step is not available with Javascript disabled');
+ if (empty($shift)) {
+ $this->execute('behat_general::i_press_named_key', ['', 'tab']);
+ } else {
+ $this->execute('behat_general::i_press_named_key', ['shift', 'tab']);
}
-
- $value = ($shift == ' shift') ? [\WebDriver\Key::SHIFT . \WebDriver\Key::TAB] : [\WebDriver\Key::TAB];
- $this->getSession()->getDriver()->getWebDriverSession()->activeElement()->postValue(['value' => $value]);
}
/**
* @throws DriverException
*/
public function i_manually_press_enter() {
- if (!$this->running_javascript()) {
- throw new DriverException('Enter press step is not available with Javascript disabled');
- }
-
- $value = [\WebDriver\Key::ENTER];
- $this->getSession()->getDriver()->getWebDriverSession()->activeElement()->postValue(['value' => $value]);
+ $this->execute('behat_general::i_press_named_key', ['', 'enter']);
}
/**
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
+=== 3.11 ===
+* New optional parameter $extracontent for print_collapsible_region_start(). This allows developers to add interactive HTML elements
+ (e.g. a help icon) after the collapsible region's toggle link.
+
=== 3.10 ===
* PHPUnit has been upgraded to 8.5. That comes with a few changes:
- Breaking change: All the "template methods" (setUp(), tearDown()...) now require to return void. This implies
* (May be blank if you do not wish the state to be persisted.
* @param boolean $default Initial collapsed state to use if the user_preference it not set.
* @param boolean $return if true, return the HTML as a string, rather than printing it.
+ * @param string $extracontent the extra content will show next to caption, eg.Help icon.
* @return string|void if $return is false, returns nothing, otherwise returns a string of HTML.
*/
-function print_collapsible_region_start($classes, $id, $caption, $userpref = '', $default = false, $return = false) {
+function print_collapsible_region_start($classes, $id, $caption, $userpref = '', $default = false, $return = false,
+ $extracontent = null) {
global $PAGE;
// Work out the initial state.
$output .= '<div id="' . $id . '" class="collapsibleregion ' . $classes . '">';
$output .= '<div id="' . $id . '_sizer">';
$output .= '<div id="' . $id . '_caption" class="collapsibleregioncaption">';
- $output .= $caption . ' ';
- $output .= '</div><div id="' . $id . '_inner" class="collapsibleregioninner">';
+ $output .= $caption . ' </div>';
+ if ($extracontent) {
+ $output .= html_writer::div($extracontent, 'collapsibleregionextracontent');
+ }
+ $output .= '<div id="' . $id . '_inner" class="collapsibleregioninner">';
$PAGE->requires->js_init_call('M.util.init_collapsible_region', array($id, $userpref, get_string('clicktohideshow')));
if ($return) {
And I open messaging
And I select "Student 2" user in messaging
And I set the field with xpath "//textarea[@data-region='send-message-txt']" to "Hi!"
- And I press key "13" in "//textarea[@data-region='send-message-txt']" "xpath_element"
+ And I press the enter key
Then I should see "Hi!" in the "//*[@data-region='message-drawer']//div[@data-region='content-message-container']" "xpath_element"
Scenario: Sending a message after 'Use enter to send' is disabled
And I go back in "view-settings" message drawer
Then I select "Student 2" user in messaging
And I set the field with xpath "//textarea[@data-region='send-message-txt']" to "Hi!"
- And I press key "13" in "//textarea[@data-region='send-message-txt']" "xpath_element"
+ And I press the enter key
And I should not see "Hi!" in the "//*[@data-region='message-drawer']//div[@data-region='content-message-container']" "xpath_element"
And I press "Send message"
And I should see "Hi!" in the "//*[@data-region='message-drawer']//div[@data-region='content-message-container']" "xpath_element"
$quickgrading,
$downloadfilename = null) {
global $CFG, $PAGE, $DB, $USER;
- parent::__construct('mod_assign_grading');
+
+ parent::__construct('mod_assign_grading-' . $assignment->get_context()->id);
+
$this->is_persistent(true);
$this->assignment = $assignment;
$document = new DOMDocument();
@$document->loadHTML($output);
$xpath = new DOMXPath($document);
- $this->assertEquals('', $xpath->evaluate('string(//td[@id="mod_assign_grading_r0_c8"])'));
+ $this->assertEmpty($xpath->evaluate('string(//td[@id="mod_assign_grading-' . $assign->get_context()->id. '_r0_c8"])'));
}
/**
@$document->loadHTML($output);
$xpath = new DOMXPath($document);
+ // The XPath expression is based on the unique ID of the table.
+ $xpathuniqueidroot = 'mod_assign_grading-' . $assign->get_context()->id;
+
// Check status.
- $this->assertSame(get_string('submissionstatus_submitted', 'assign'), $xpath->evaluate('string(//td[@id="mod_assign_grading_r0_c4"]/div[@class="submissionstatussubmitted"])'));
- $this->assertSame(get_string('submissionstatus_submitted', 'assign'), $xpath->evaluate('string(//td[@id="mod_assign_grading_r3_c4"]/div[@class="submissionstatussubmitted"])'));
+ $this->assertSame(get_string('submissionstatus_submitted', 'assign'),
+ $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c4"]/div[@class="submissionstatussubmitted"])'));
+ $this->assertSame(get_string('submissionstatus_submitted', 'assign'),
+ $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c4"]/div[@class="submissionstatussubmitted"])'));
// Check submission last modified date
- $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="mod_assign_grading_r0_c8"])')));
- $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="mod_assign_grading_r3_c8"])')));
+ $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c8"])')));
+ $this->assertGreaterThan(0, strtotime($xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c8"])')));
// Check group.
- $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="mod_assign_grading_r0_c5"])'));
- $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="mod_assign_grading_r3_c5"])'));
+ $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c5"])'));
+ $this->assertSame($group->name, $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c5"])'));
// Check submission text.
- $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="mod_assign_grading_r0_c9"]/div/div)'));
- $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="mod_assign_grading_r3_c9"]/div/div)'));
+ $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r0_c9"]/div/div)'));
+ $this->assertSame('Submission text', $xpath->evaluate('string(//td[@id="' . $xpathuniqueidroot . '_r3_c9"]/div/div)'));
// Check comments can be made.
- $this->assertSame(1, (int)$xpath->evaluate('count(//td[@id="mod_assign_grading_r0_c10"]//textarea)'));
- $this->assertSame(1, (int)$xpath->evaluate('count(//td[@id="mod_assign_grading_r3_c10"]//textarea)'));
+ $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r0_c10"]//textarea)'));
+ $this->assertEquals(1, $xpath->evaluate('count(//td[@id="' . $xpathuniqueidroot . '_r3_c10"]//textarea)'));
}
public function test_show_intro() {
'display' => new external_value(PARAM_INT, 'Display type of folder contents on a separate page or inline'),
'showexpanded' => new external_value(PARAM_INT, '1 = expanded, 0 = collapsed for sub-folders'),
'showdownloadfolder' => new external_value(PARAM_INT, 'Whether to show the download folder button'),
+ 'forcedownload' => new external_value(PARAM_INT, 'Whether file download is forced'),
'section' => new external_value(PARAM_INT, 'Course section id'),
'visible' => new external_value(PARAM_INT, 'Module visibility'),
'groupmode' => new external_value(PARAM_INT, 'Group mode'),
// First folder.
$record = new stdClass();
$record->course = $course1->id;
+ $record->forcedownload = 1;
$folder1 = self::getDataGenerator()->create_module('folder', $record);
// Second folder.
$record = new stdClass();
$record->course = $course2->id;
+ $record->forcedownload = 0;
$folder2 = self::getDataGenerator()->create_module('folder', $record);
// Execute real Moodle enrolment as we'll call unenrol() method on the instance later.
// Create what we expect to be returned when querying the two courses.
$expectedfields = array('id', 'coursemodule', 'course', 'name', 'intro', 'introformat', 'introfiles', 'revision',
'timemodified', 'display', 'showexpanded', 'showdownloadfolder', 'section', 'visible',
- 'groupmode', 'groupingid');
+ 'forcedownload', 'groupmode', 'groupingid');
// Add expected coursemodule and data.
$folder1->coursemodule = $folder1->cmid;
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 key "27" in the field "Is tagged with"
+ And I press the escape key
When I press "Search forums"
Then I should see "My subject"
And I should not see "Your subjective"
--- /dev/null
+@mod @mod_forum @javascript
+Feature: View discussion while grading in a forum
+ In order to grade efficiently
+ As a teacher
+ I want to be able to see the full discussion the student was taking part in.
+
+ Background:
+ # Student 1 needs to be created first or they will not be the first user on the grading screen.
+ Given the following "users" exist:
+ | username | firstname | lastname | email |
+ | student1 | Student | 1 | student.1@example.com |
+ | student2 | Student | 2 | student.2@example.com |
+ | teacher | Teacher | Tom | teacher@example.com |
+ And the following "courses" exist:
+ | fullname | shortname | category |
+ | Course 1 | C1 | 0 |
+ And the following "course enrolments" exist:
+ | user | course | role |
+ | student1 | C1 | student |
+ | student2 | C1 | student |
+ | teacher | C1 | editingteacher |
+ And the following "activity" exists:
+ | activity | forum |
+ | name | Gradable forum |
+ | intro | Standard forum description |
+ | course | C1 |
+ | idnumber | forum1 |
+ | grade_forum | 100 |
+ | scale | 100 |
+ # If there is more than one pots for Student 1 the test will not be able to select the
+ # correct View discussion link, as there is no selector for thier container.
+ And the following forum discussions exist in course "Course 1":
+ | forum | user | name | message |
+ | Gradable forum | student1 | My topic | This is the thing I posted about |
+ And the following forum replies exist in course "Course 1":
+ | forum | user | discussion | message |
+ | Gradable forum | student2 | My topic | I disagree |
+
+ Scenario: Viewing a discussion
+ Given I log in as "teacher"
+ And I am on "Course 1" course homepage
+ And I follow "Gradable forum"
+ And I press "Grade users"
+ When I press "View discussion"
+ Then I should see "I disagree" in the "My topic" "dialogue"
+ And I click on "Cancel" "button" in the "My topic" "dialogue"
+ And I should not see "I disagree"
+
+ Scenario: Viewing a discussion while grading is fullscreen
+ Given I log in as "teacher"
+ And I am on "Course 1" course homepage
+ And I follow "Gradable forum"
+ And I press "Grade users"
+ # Uses the aria-label for the menu in in the grading interface.
+ And I press "Actions for the grader interface"
+ And I press "Toggle full screen"
+ When I press "View discussion"
+ Then I should see "I disagree" in the "My topic" "dialogue"
+ And I click on "Cancel" "button" in the "My topic" "dialogue"
+ And I should not see "I disagree"
| 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 key "13" in the field "New name for activity Test tool activity 1"
+ And I press the enter key
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"
And I should see "bar" in the "question 02 name" "table_row"
And I should see "qidnum" in the "question 02 name" "table_row"
And I set the field "Filter by tags..." to "foo"
- And I press key "13" in the field "Filter by tags..."
+ And I press the enter key
And I should see "question 01 name" in the "categoryquestions" "table"
And I should not see "question 02 name" in the "categoryquestions" "table"
And I should see "Total of marks: 10.00"
When I follow "Edit maximum mark"
- And I wait until "li input[name=maxmark]" "css_element" exists
- And I take focus off "li input[name=maxmark]" "css_element"
+ And I press the escape key
Then I should see "7.00"
And I should see "3.00"
And I should see "Total of marks: 10.00"
And I should see "Total of marks: 10.00"
When I follow "Edit maximum mark"
- And I wait until "li input[name=maxmark]" "css_element" exists
- And I take focus off "li input[name=maxmark]" "css_element"
+ And I press the escape key
Then I should see "7.00"
And I should see "3.00"
And I should see "Total of marks: 10.00"
if (!window_name && node.title != null) {
obj.setAttribute('src', url_prefix + node.title);
}
+ // Attach unload observers to the iframe. The scorm package may be observing these unload events
+ // and trying to save progress when they occur. We need to ensure we use the Beacon API in those
+ // situations.
+ if (typeof mod_scorm_monitorForBeaconRequirement !== 'undefined') {
+ mod_scorm_monitorForBeaconRequirement(obj);
+ }
if (window_name) {
var mine = window.open('','','width=1,height=1,left=0,top=0,scrollbars=no');
if(! mine) {
};
- // Handle AJAX Request
+ /**
+ * @deprecated as it is now unused.
+ * @param {string} url
+ * @param {string} datastring
+ * @returns {string|*|boolean}
+ */
var scorm_ajax_request = function(url, datastring) {
var myRequest = NewHttpReq();
var result = DoRequest(myRequest, url + datastring);
return null;
};
+ /**
+ * Sends a request to the sequencing handler script on the server.
+ * @param {string} datastring
+ * @returns {string|boolean|*}
+ */
+ var scorm_dorequest_sequencing = function(datastring) {
+ var myRequest = NewHttpReq();
+ var result = DoRequest(
+ myRequest,
+ M.cfg.wwwroot + '/mod/scorm/datamodels/sequencinghandler.php?' + datastring,
+ '',
+ false
+ );
+ return result;
+ };
+
// Launch prev sco
var scorm_launch_prev_sco = function() {
var result = null;
if (scoes_nav[launch_sco].flow === 1) {
var datastring = scoes_nav[launch_sco].url + '&function=scorm_seq_flow&request=backward';
- result = scorm_ajax_request(M.cfg.wwwroot + '/mod/scorm/datamodels/sequencinghandler.php?', datastring);
+ result = scorm_dorequest_sequencing(datastring);
+ // Check the scorm_ajax_result, it may be false.
if (result === false) {
// Either the outcome was a failure, or we are unloading and simply just don't know
// what the outcome actually was.
var result = null;
if (scoes_nav[launch_sco].flow === 1) {
var datastring = scoes_nav[launch_sco].url + '&function=scorm_seq_flow&request=forward';
- result = scorm_ajax_request(M.cfg.wwwroot + '/mod/scorm/datamodels/sequencinghandler.php?', datastring);
+ result = scorm_dorequest_sequencing(datastring);
+ // Check the scorm_ajax_result, it may be false.
if (result === false) {
// Either the outcome was a failure, or we are unloading and simply just don't know
// what the outcome actually was.
return httpReq;
}
-function DoRequest(httpReq,url,param) {
+/**
+ *
+ * @param {XMLHttpRequest} httpReq
+ * @param {string} url
+ * @param {string} param
+ * @param {boolean} allowBeaconAPI Should the BeaconAPI be used if required? Defaults to true
+ * If True, and we can use the Beacon API and are should use the beacon API then we will.
+ * If False, we will not use the Beacon API, even if we expect a synchronous XHR request to fail.
+ * @returns {string|boolean|*}
+ * @constructor
+ */
+function DoRequest(httpReq, url, param, allowBeaconAPI) {
- // If we are unloading, and we can use sendBeacon then do that, Chrome does not permit synchronous XHR requests on unload.
- if (window.mod_scorm_is_window_closing && navigator && navigator.sendBeacon && FormData) {
+ // Default allowBeaconAPI to true. This argument was added to the function late.
+ if (typeof allowBeaconAPI === 'undefined') {
+ allowBeaconAPI = true;
+ }
+
+ /**
+ * Returns true if we are able to use the Beacon API in this browser.
+ * @returns boolean
+ */
+ var canUseBeaconAPI = function() {
+ return (allowBeaconAPI && navigator && navigator.sendBeacon && FormData);
+ };
+
+ /**
+ * Returns true if we should use the Beacon API.
+ * We don't use the Beacon API unless we have to as it stiffles our ability to return data on the request.
+ * @returns {boolean}
+ */
+ var useBeaconAPI = function() {
+ if (typeof window.mod_scorm_useBeaconAPI === 'undefined' || window.mod_scorm_useBeaconAPI === false) {
+ // Last ditch effort, the SCORM package may have introduced its own listeners before our listeners.
+ // This is OLD API, window.event is not reliable and is not recommended API.
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/event
+ if (window.event && ['beforeunload', 'unload', 'pagehide'].indexOf(window.event.type)) {
+ window.mod_scorm_useBeaconAPI = true;
+ }
+ }
+ return (window.mod_scorm_useBeaconAPI && canUseBeaconAPI());
+ };
+
+ /**
+ * Uses the Beacon API to communicate this request to the server.
+ * This function always returns a successful result, because we don't get the actual result, the page doesn't wait for it.
+ * @param {string} url
+ * @param {string} param
+ * @returns {string}
+ */
+ var useSendBeacon = function(url, param) {
// Ok, old API alert, the param is a URI encoded string. We need to split it and convert it to a supported format.
// I've chosen FormData and FormData.append as they are compatible with our supported browsers:
// - https://developer.mozilla.org/en-US/docs/Web/API/FormData/FormData
// We'll also inform it that we are unloading, potentially useful in the future.
formData.append('unloading', '1');
+ // We're going to add a token to the URL that will identify this request as going to the beacon API.
+ // In the future this would allow our server side scripts to respond differently when the beacon API
+ // is being used, as the response will be discarded.
+ if (url.indexOf('?') === -1) {
+ // First param
+ url += '?api=beacon';
+ } else {
+ url += '&api=beacon';
+ }
+
// The results is true or false, we don't get the response from the server. Make it look like it was a success.
- navigator.sendBeacon(url, formData);
+ var outcome = navigator.sendBeacon(url, formData);
+ if (!outcome) {
+ if (console && console.log) {
+ console.log('mod_scorm: Failed to queue navigator.sendBeacon request');
+ }
+ return "false\n101";
+ }
// This is what a success looks like when it comes back from the server.
return "true\n0";
+ };
+
+ // If we are unloading, and we can use sendBeacon then do that, Chrome does not permit synchronous XHR requests on unload.
+ if (useBeaconAPI()) {
+ return useSendBeacon(url, param);
}
// httpReq.open (Method("get","post"), URL(string), Asyncronous(true,false))
try {
httpReq.send(param);
} catch (e) {
+ if (console && console.log) {
+ // This may be frivolous as during a shutdown the console log will most likely be lost. But it may help someone.
+ var message = 'XHR request from mod_scorm::DoRequest failed';
+ if (canUseBeaconAPI()) {
+ message += '; attempting to use Beacon API.';
+ }
+ console.log(message);
+ }
+ // The HTTP request failed. We don't know why, but as a last ditch effort, in case we are unloading and haven't detected it
+ // we will attempt to send the request one more time using the Beacon API. This will result in a successful result regardless
+ // of the actual outcome.
+ if (canUseBeaconAPI()) {
+ return useSendBeacon(url, param);
+ }
return false;
}
if (httpReq.status == 200) {
op.document.close();
}
+/**
+ * Global variable to track whether we should use the Beacon API instead of synchronous XHR.
+ * This gets set to true in situations where we expect synchronoush XHR requests to fail.
+ */
+window.mod_scorm_useBeaconAPI = false;
+
/**
* We wire up a small marker for the unload events triggered when the user is navigating away or closing the tab.
* This is done because Chrome does not allow synchronous XHR requests on the following unload events:
* - pagehide
* - visibilitychange
*/
-(function() {
- // Set up a global var. Sorry about this, old code ... old ways.
- window.mod_scorm_is_window_closing = false;
- var toggle = function() {
- window.mod_scorm_is_window_closing = true;
+function mod_scorm_monitorForBeaconRequirement(target) {
+
+ if (typeof target.mod_scorm_monitoring_for_beacon_requirement !== 'undefined') {
+ // We're already observing unload events on this target.
+ console.log('mod_scorm: unload event handlers already attached');
+ return;
+ }
+ target.mod_scorm_monitoring_for_beacon_requirement = true;
+
+ // The navigator.sendBeacon API is available in all browsers EXCEPT Internet Explorer (IE)
+ // Internet explorer should never get past this check.
+ if (!navigator || !navigator.sendBeacon) {
+ // We can't use the BeaconAPI. There is no point in proceeding to observe unload events.
+ // This is done after adding the flag to target, and establishing the window variable.
+ return;
+ }
+
+ /**
+ * Turns on the use of the Beacon API.
+ */
+ var toggleOn = function() {
+ window.mod_scorm_useBeaconAPI = true;
};
- // Listen to the four events known to represent an unload operation.
- window.addEventListener('beforeunload', toggle);
- window.addEventListener('unload', toggle);
- window.addEventListener('pagehide', toggle);
- window.addEventListener('visibilitychange', toggle);
-})();
\ No newline at end of file
+
+ /**
+ * Turns off the use of the Beacon API.
+ */
+ var toggleOff = function() {
+ window.mod_scorm_useBeaconAPI = false;
+ };
+
+ /**
+ * Observes an event.
+ * Required because this patch will be backported.
+ * @param {string} on
+ * @param {CallableFunction} callback
+ */
+ var observe = function(on, callback) {
+ if (!target.addEventListener) {
+ console.log('Unable to attach page dismissal event listeners');
+ return null;
+ }
+ return target.addEventListener(on, callback);
+ };
+
+ // Listen to the three events known to represent an unload operation.
+ observe('beforeunload', toggleOn);
+ observe('unload', toggleOn);
+ observe('pagehide', toggleOn);
+
+ // Listen to the event fired when navigating to a page and ensure we toggle useBeaconAPI off.
+ // This shouldn't be needed (page should be uncached) but just in case!
+ observe('pageshow', toggleOff);
+
+ // Finally listen to the visibility change event, and respond to it.
+ // This unfortunately is not ideal, but is required as a SCORM package may also be listening to this and
+ // trying to save content when the user hides the page. As this can occur as part of the page dismissal lifecycle
+ // we also need to ensure we use the Beacon API here.
+ observe('visibilitychange', function() {
+ // Visible means synchronous XHR permitted, use XHR.
+ // Hidden means synchronous XHR not permitted, use Beacon API.
+ if (document.visibilityState === 'visible' || document.visibilityState === 'prerender') {
+ toggleOff();
+ } else if (document.visibilityState === 'hidden') {
+ toggleOn();
+ }
+ });
+}
+// Begin monitoring on the main window immediately.
+mod_scorm_monitorForBeaconRequirement(window);
\ No newline at end of file
echo html_writer::end_tag('form');
// Output the technical info.
-print_collapsible_region_start('', 'techinfo', get_string('technicalinfo', 'question') .
- $OUTPUT->help_icon('technicalinfo', 'question'),
- 'core_question_preview_techinfo_collapsed', true);
+print_collapsible_region_start('', 'techinfo', get_string('technicalinfo', 'question'),
+ 'core_question_preview_techinfo_collapsed', true, false, $OUTPUT->help_icon('technicalinfo', 'question'));
foreach ($technical as $info) {
echo html_writer::tag('p', $info, array('class' => 'notifytiny'));
}
@javascript
Scenario: The questions can be filtered by tag
When I set the field "Filter by tags..." to "foo"
- And I press key "13" in the field "Filter by tags..."
+ And I press the enter key
Then I should see "question 1 name" in the "categoryquestions" "table"
And I should not see "question 2 name" in the "categoryquestions" "table"
public function i_type_on_place_in_the_drag_and_drop_onto_image_question($keys, $placenumber) {
$node = $this->get_selected_node('xpath_element', $this->drop_xpath($placenumber));
$this->ensure_node_is_visible($node);
+
+ $node->focus();
foreach (str_split($keys) as $key) {
- $node->keyDown($key);
- $node->keyPress($key);
- $node->keyUp($key);
+ behat_base::type_keys($this->getSession(), [$key]);
$this->wait_for_pending_js();
}
}
* @Given /^I type "(?P<direction>up|down|left|right)" "(?P<repeats>\d+)" times on marker "(?P<marker>[^"]*)" in the drag and drop markers question$/
*/
public function i_type_on_marker_in_the_drag_and_drop_markers_question($direction, $repeats, $marker) {
- $keycodes = array(
- 'up' => chr(38),
- 'down' => chr(40),
- 'left' => chr(37),
- 'right' => chr(39),
- );
$node = $this->get_selected_node('xpath_element', $this->marker_xpath($marker, true));
$this->ensure_node_is_visible($node);
+ $node->focus();
for ($i = 0; $i < $repeats; $i++) {
- $node->keyDown($keycodes[$direction]);
- $node->keyPress($keycodes[$direction]);
- $node->keyUp($keycodes[$direction]);
+ $this->execute('behat_general::i_press_named_key', ['', $direction]);
}
}
}
# Keep window large else drag will scroll the window to find element.
And I change window size to "medium"
And I wait "2" seconds
- And I type "up" "44" times on marker "Railway station" in the drag and drop markers question
- And I type "right" "13" times on marker "Railway station" in the drag and drop markers question
+ And I type "up" "88" times on marker "Railway station" in the drag and drop markers question
+ And I type "right" "26" times on marker "Railway station" in the drag and drop markers question
And I press "Submit and finish"
Then the state of "Please place the markers on the map of Milton Keynes" question is shown as "Partially correct"
And I should see "Mark 0.25 out of 1.00"
public function i_type_into_space_in_the_drag_and_drop_into_text_question($keys, $spacenumber) {
$node = $this->get_selected_node('xpath_element', $this->drop_xpath($spacenumber));
$this->ensure_node_is_visible($node);
+ $node->focus();
foreach (str_split($keys) as $key) {
- $node->keyDown($key);
- $node->keyPress($key);
- $node->keyUp($key);
+ behat_base::type_keys($this->getSession(), [$key]);
$this->wait_for_pending_js();
}
}
Scenario: Go to the competency breakdown report
When I navigate to "Reports > Competency breakdown" in current page administration
And I set the field "Filter competencies by resource or activity" to "PageName1"
- And I press key "13" in the field "Filter competencies by resource or activity"
+ And I press the enter key
Then I should see "Test-Comp1"
And I should not see "Test-Comp2"
And I click on "Not rated" "link"
And I click on "Rate" "button" in the ".competency-grader" "css_element"
And I click on "Close" "button"
And I set the field "Filter competencies by resource or activity" to "No filters applied"
- And I press key "13" in the field "Filter competencies by resource or activity"
+ And I press the enter key
And I should see "Test-Comp1"
And I should see "Test-Comp2"
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 key "13" in the field "New name for tag collection Hobbies"
+ And I press the enter key
Then I should not see "Hobbies"
And I should see "Newname"
And I log out
And I should see "Dog" in the ".tag_list" "css_element"
And I should see "Turtle" in the ".tag_list" "css_element"
And I should see "Fish" in the ".tag_list" "css_element"
- And I log out
@javascript
Scenario: Manager can change tag description, related tags and rename the tag from tag view page
And I should see "Turtle" in the ".tag_list" "css_element"
And I should see "Fish" in the ".tag_list" "css_element"
And I should not see "Dog"
- And I log out
Scenario: Renaming the tag from tag view page
When I log in as "manager1"
| Tag name | KITTEN |
And I press "Update"
And "KITTEN" "text" should exist in the ".breadcrumb" "css_element"
- And I log out
@javascript
Scenario: Manager can change tag description and rename the tag from tag manage page
And I should see "Dog" in the ".tag_list" "css_element"
And I should see "Turtle" in the ".tag_list" "css_element"
And I should see "Fish" in the ".tag_list" "css_element"
- And I log out
Scenario: Renaming the tag in edit tag form from tag manage page
When I log in as "manager1"
And "Default collection" "text" should exist in the ".breadcrumb" "css_element"
And I should see "KITTEN"
And I should not see "Kitten"
- And I log out
@javascript
Scenario: Renaming the tag using quick edit field on tag manage page
# 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 key "13" in the field "New name for tag Cat"
+ And I press the enter key
Then I should not see "Cat"
And "New name for tag" "field" should not exist
And I wait until "Kitten" "link" exists
# 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 key "13" in the field "New name for tag Turtle"
+ And I press the enter key
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
# 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 press key "27" in the field "New name for tag Dog"
+ And I press the escape key
And "New name for tag" "field" should not exist
And I should see "Turtle"
And I should not see "Penguin"
And I follow "Default collection"
And I should see "Turtle"
And I should not see "Penguin"
- And I log out
@javascript
Scenario: Combining tags when renaming
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 key "13" in the field "New name for tag Turtle"
+ And I press the enter key
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"
And I should not see "DOG"
And I should see "Dog"
- And I log out
@javascript
Scenario: Combining multiple tags
And I should see "Turtle"
# Even though Turtle was not standard but at least one of combined tags was (Neverusedtag). Now Turtle is also standard.
And "Remove from standard tags" "link" should exist in the "Turtle" "table_row"
- And I log out
Scenario: Filtering tags
When I log in as "manager1"
And I should see "Dog"
And I should see "Cat"
And I should see "Turtle"
- And I log out
margin: 1em 0;
}
+#page-question-preview .collapsibleregion {
+ .collapsibleregioncaption,
+ .collapsibleregionextracontent {
+ display: inline-block;
+ }
+}
+
// imported from quiz.css
#page-mod-quiz-edit ul.slots .activityinstance {
#page-question-preview #techinfo {
margin: 1em 0; }
+#page-question-preview .collapsibleregion .collapsibleregioncaption,
+#page-question-preview .collapsibleregion .collapsibleregionextracontent {
+ display: inline-block; }
+
#page-mod-quiz-edit ul.slots .activityinstance > a {
display: flex;
max-width: 100%;
#page-question-preview #techinfo {
margin: 1em 0; }
+#page-question-preview .collapsibleregion .collapsibleregioncaption,
+#page-question-preview .collapsibleregion .collapsibleregionextracontent {
+ display: inline-block; }
+
#page-mod-quiz-edit ul.slots .activityinstance > a {
display: flex;
max-width: 100%;
.then(tableRoot => {
// Always update the toggle state.
// This ensures that the bulk actions are disabled after changing the page size.
- CheckboxToggleAll.setGroupState(tableRoot, 'participants-table', checkCountButtonClicked);
+ CheckboxToggleAll.setGroupState(root, 'participants-table', checkCountButtonClicked);
return tableRoot;
})
const currentPageSize = parseInt(tableRoot.dataset.tablePageSize, 10);
const totalRowCount = parseInt(tableRoot.dataset.tableTotalRows, 10);
- CheckboxToggleAll.updateSlavesFromMasterState(tableRoot, 'participants-table');
+ CheckboxToggleAll.updateSlavesFromMasterState(root, 'participants-table');
const pageCountStrings = [
{
And I click on "Student 1's role assignments" "link"
And I click on ".form-autocomplete-downarrow" "css_element" in the "student1" "table_row"
And I click on "Non-editing teacher" item in the autocomplete list
- And I press key "27" in the field "Student 1's role assignments"
+ And I press the escape key
When I click on "Save changes" "link"
Then I should see "Student, Non-editing teacher" in the "Student 1" "table_row"
And I navigate to course participants
And I click on "Student 1's role assignments" "link"
And I click on ".form-autocomplete-selection [aria-selected=true]" "css_element"
- And I press key "27" in the field "Student 1's role assignments"
+ And I press the escape key
When I click on "Save changes" "link"
Then I should see "No roles" in the "Student 1" "table_row"
And I navigate to course participants
And I click on "Student 1's role assignments" "link"
And I click on ".form-autocomplete-selection [aria-selected=true]" "css_element"
- And I press key "27" in the field "Student 1's role assignments"
+ And I press the escape key
And I click on "Save changes" "link"
And I set the field "type" in the "Filter 1" "fieldset" to "Roles"
And I click on ".form-autocomplete-downarrow" "css_element" in the "Filter 1" "fieldset"
And I set the field "Match" in the "Filter 3" "fieldset" to "Any"
And I set the field "type" in the "Filter 3" "fieldset" to "Keyword"
And I set the field "Type..." to "teacher1"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 4" in the "participants" "table"
And I set the field "Match" in the "Filter 3" "fieldset" to "Any"
And I set the field "type" in the "Filter 3" "fieldset" to "Keyword"
And I set the field "Type..." to "3@"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I set the field "Type..." to "1@example"
- And I press key "13" in the field "Type..."
+ And I press the enter key
When I click on "Apply filters" "button"
Then I should see "Student 1" in the "participants" "table"
And I should see "Teacher 1" in the "participants" "table"
And I should not see "Teacher 1" in the "participants" "table"
# Add a second keyword filter value
And I set the field "Type..." to "moodle"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Student 2" in the "participants" "table"
And I should see "Student 3" in the "participants" "table"
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
# Search by email (only).
And I set the field "Type..." to "student1@example.com"
- And I press key "13" in the field "Type..."
+ And I press the enter key
When I click on "Apply filters" "button"
Then I should see "Student 1" in the "participants" "table"
And I should not see "Student 2" in the "participants" "table"
# Search by idnumber (only).
And I click on "student1@example.com" "autocomplete_selection"
And I set the field "Type..." to "SID"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
# Search by city (only).
And I click on "SID" "autocomplete_selection"
And I set the field "Type..." to "SCITY"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
# Search by country text (only) - should not match.
And I click on "SCITY" "autocomplete_selection"
And I set the field "Type..." to "GB"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Nothing to display"
# Check no match.
And I click on "GB" "autocomplete_selection"
And I set the field "Type..." to "NOTHING"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Nothing to display"
# Search by email (only) - should only see visible email + own.
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I set the field "Type..." to "@example."
- And I press key "13" in the field "Type..."
+ And I press the enter key
When I click on "Apply filters" "button"
Then I should not see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
# Search for other fields - should only see own results.
And I click on "@example." "autocomplete_selection"
And I set the field "Type..." to "SID"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Nothing to display"
And I click on "SID" "autocomplete_selection"
And I set the field "Type..." to "TID"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Teacher 1" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
And I click on "TID" "autocomplete_selection"
And I set the field "Type..." to "CITY"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Teacher 1" in the "participants" "table"
And I should not see "Student 1" in the "participants" "table"
# Check no match.
And I click on "CITY" "autocomplete_selection"
And I set the field "Type..." to "NOTHING"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Nothing to display"
And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
And I set the field "type" in the "Filter 2" "fieldset" to "Keyword"
And I set the field "Type..." to "@example"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I set the field "Match" in the "Filter 2" "fieldset" to "Any"
And I set the field "type" in the "Filter 2" "fieldset" to "Keyword"
And I set the field "Type..." to "@example"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Apply filters" "button"
And I should see "Student 1" in the "participants" "table"
And I should see "Student 2" in the "participants" "table"
And I set the field "Match" in the "Filter 1" "fieldset" to "Any"
And I set the field "type" in the "Filter 1" "fieldset" to "Keyword"
And I set the field "Type..." to "@example.com"
- And I press key "13" in the field "Type..."
+ And I press the enter key
And I click on "Add condition" "button"
# Set filterset to match none.
And I set the field "Match" to "None"
And the field "Select 'Student 18x'" matches value "0"
And the field "Select 'Student 19x'" matches value "0"
+ # Pressing the "Select all X users" button should select all including the 21st user (Student 9x).
And I press "Select all 21 users"
And I should see "Student 9x"
And the field "Select 'Teacher 1x'" matches value "1"
And the field "Select 'Student 17x'" matches value "1"
And the field "Select 'Student 18x'" matches value "1"
And the field "Select 'Student 19x'" matches value "1"
+ And the "With selected users..." "select" should be enabled
And I click on "Deselect all" "checkbox"
And the field "Select 'Teacher 1x'" matches value "0"