Compare commits
65 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f89a47eff8 | |||
| 6a16596f78 | |||
| 7e22b75c28 | |||
| 7d547f726f | |||
| 75c9e608ce | |||
| fa8591ea3e | |||
| ae8c5e16e9 | |||
| 5425233b37 | |||
| dcc561762b | |||
| 6307f8dd89 | |||
| 1a3e679835 | |||
| 6cfdd1bbae | |||
| 56a368facf | |||
| 4842ae967c | |||
| 6f62e05a96 | |||
| 6f23e028dd | |||
| ff013438d6 | |||
| 3e0941fb5b | |||
| e667c191f8 | |||
| 4566a44f14 | |||
| 8b2c0d969d | |||
| 32d337a934 | |||
| 4e1d36c8bc | |||
| 87c2aae85c | |||
| 8c6831f0fe | |||
| 5dbc672825 | |||
| b97d313c11 | |||
| ee0b0b4fc8 | |||
| 5b707aa422 | |||
| 519ba9e103 | |||
| c3a318e16a | |||
| faf63d076d | |||
| b1fe51cf0d | |||
| 651a97efc0 | |||
| cd8af2d23a | |||
| 8b75bb1772 | |||
| b672263e04 | |||
| 1b506c9639 | |||
| c463d72821 | |||
| 162053448e | |||
| 2034d440af | |||
| 70f7efe0fe | |||
| f1447db751 | |||
| 7fc01a94e2 | |||
| 568e72e7dc | |||
| 5d4db3dce3 | |||
| e5c7b50d85 | |||
| 9fda41582c | |||
| bddacfd5d7 | |||
| f3d9759375 | |||
| 1d776476b6 | |||
| fca9778023 | |||
| 482de4e370 | |||
| 91f43dd47f | |||
| fa9ba48533 | |||
| 141657a483 | |||
| c8be5a9ab3 | |||
| 507f737266 | |||
| 6d75761b94 | |||
| 52a9113c6d | |||
| 94f703e1d1 | |||
| d9ceef45de | |||
| 1ad332bbde | |||
| d23efa1ff2 | |||
| bf1ebf3b44 |
@@ -13,14 +13,14 @@ env:
|
||||
|
||||
jobs:
|
||||
Grunt:
|
||||
runs-on: ubuntu-18.04
|
||||
runs-on: ubuntu-22.04
|
||||
|
||||
steps:
|
||||
- name: Checking out code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Configuring node & npm
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
|
||||
@@ -54,10 +54,10 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-22.04
|
||||
php: 7.2
|
||||
db: mysqli
|
||||
- os: ubuntu-18.04
|
||||
- os: ubuntu-22.04
|
||||
php: 7.4
|
||||
db: pgsql
|
||||
|
||||
@@ -83,7 +83,7 @@ jobs:
|
||||
postgresql password: test
|
||||
|
||||
- name: Configuring git vars
|
||||
uses: rlespinasse/github-slug-action@v3.x
|
||||
uses: rlespinasse/github-slug-action@v4
|
||||
|
||||
- name: Setting up PHP ${{ matrix.php }}
|
||||
uses: shivammathur/setup-php@v2
|
||||
@@ -92,7 +92,7 @@ jobs:
|
||||
coverage: none
|
||||
|
||||
- name: Checking out code from ${{ env.GITHUB_REF_SLUG }}
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Setting up PHPUnit
|
||||
env:
|
||||
|
||||
@@ -3781,11 +3781,6 @@
|
||||
<ON_CHECK message="tokenizerrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="xmlrpc" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="xmlrpcrecommended" />
|
||||
</FEEDBACK>
|
||||
</PHP_EXTENSION>
|
||||
<PHP_EXTENSION name="soap" level="optional">
|
||||
<FEEDBACK>
|
||||
<ON_CHECK message="soaprecommended" />
|
||||
|
||||
@@ -77,9 +77,10 @@
|
||||
ucw.registerEvents('[data-region="user-competency-status"]');
|
||||
ucw.on('status-changed', infoReloader.reload.bind(infoReloader));
|
||||
ucw.on('error-occured', infoReloader.reload.bind(infoReloader));
|
||||
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
{{#cangrade}}
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '{{plan.id}}', '', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
{{/cangrade}}
|
||||
});
|
||||
{{/js}}
|
||||
{{/usercompetency}}
|
||||
|
||||
@@ -95,13 +95,14 @@
|
||||
{{#js}}
|
||||
require(['jquery', 'tool_lp/grade_user_competency_inline', 'tool_lp/user_competency_info'], function($, mod, info) {
|
||||
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
var competencyElement = $('[data-region-id="{{uniqid}}"]');
|
||||
|
||||
var displayuser = ('{{displayuser}}' == 'true') ? true : false;
|
||||
var infoReloader = new info(competencyElement, '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', displayuser);
|
||||
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
{{#cangrade}}
|
||||
var inlineGrader = new mod('#rate_{{uniqid}}', '{{competency.scaleid}}', '{{competency.competency.id}}', '{{user.id}}', '', '{{course.id}}', '{{#str}}chooserating, tool_lp{{/str}}');
|
||||
inlineGrader.on('competencyupdated', infoReloader.reload.bind(infoReloader));
|
||||
{{/cangrade}}
|
||||
});
|
||||
|
||||
{{/js}}
|
||||
|
||||
@@ -643,7 +643,7 @@ class backup_ui_stage_complete extends backup_ui_stage_final {
|
||||
}
|
||||
$output .= $renderer->get_samesite_notification();
|
||||
$output .= $renderer->notification(get_string('executionsuccess', 'backup'), 'notifysuccess');
|
||||
$output .= $renderer->continue_button($restorerul);
|
||||
$output .= $renderer->continue_button($restorerul, 'get');
|
||||
$output .= $renderer->box_end();
|
||||
|
||||
return $output;
|
||||
|
||||
@@ -553,7 +553,8 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a continue button
|
||||
* Displays a continue button, overriding core renderer method of the same in order
|
||||
* to override submission method of the button form
|
||||
*
|
||||
* @param string|moodle_url $url
|
||||
* @param string $method
|
||||
@@ -566,7 +567,6 @@ class core_backup_renderer extends plugin_renderer_base {
|
||||
if ($method != 'post') {
|
||||
$method = 'get';
|
||||
}
|
||||
$url->param('sesskey', sesskey());
|
||||
$button = new single_button($url, get_string('continue'), $method, true);
|
||||
$button->class = 'continuebutton';
|
||||
return $this->render($button);
|
||||
|
||||
@@ -1161,8 +1161,10 @@ class restore_ui_stage_complete extends restore_ui_stage_process {
|
||||
$html .= $renderer->notification(get_string('restorefileweremissing', 'backup'), 'notifyproblem');
|
||||
}
|
||||
$html .= $renderer->notification(get_string('restoreexecutionsuccess', 'backup'), 'notifysuccess');
|
||||
$html .= $renderer->continue_button(new moodle_url('/course/view.php', array(
|
||||
'id' => $this->get_ui()->get_controller()->get_courseid())), 'get');
|
||||
|
||||
$courseurl = course_get_url($this->get_ui()->get_controller()->get_courseid());
|
||||
$html .= $renderer->continue_button($courseurl, 'get');
|
||||
|
||||
$html .= $renderer->box_end();
|
||||
|
||||
return $html;
|
||||
|
||||
+5
-5
@@ -7,13 +7,13 @@
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "7.5.*",
|
||||
"phpunit/dbunit": "4.0.*",
|
||||
"mikey179/vfsstream": "^1.6",
|
||||
"behat/mink": "~1.8",
|
||||
"mikey179/vfsstream": "1.6.*",
|
||||
"behat/mink": "^1.10.0",
|
||||
"behat/mink-extension": "~2.3",
|
||||
"behat/mink-goutte-driver": "~1.2",
|
||||
"behat/mink-goutte-driver": "^1.3",
|
||||
"symfony/process": "^4.0 || ^5.0",
|
||||
"behat/behat": "3.10.*",
|
||||
"oleg-andreyev/mink-phpwebdriver": "^1.0"
|
||||
"behat/behat": "3.11.*",
|
||||
"oleg-andreyev/mink-phpwebdriver": "^1.0.1"
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-0": {
|
||||
|
||||
Generated
+377
-331
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,6 @@ use core_privacy\local\request\approved_contextlist;
|
||||
use core_privacy\local\request\contextlist;
|
||||
use core_privacy\local\request\writer;
|
||||
use core_privacy\manager;
|
||||
use Horde\Socket\Client\Exception;
|
||||
|
||||
/**
|
||||
* Class provider
|
||||
@@ -196,7 +195,7 @@ class provider implements
|
||||
$field = array_key_exists($record->fieldid, $fields) ? $fields[$record->fieldid] : null;
|
||||
$data = data_controller::create(0, $record, $field);
|
||||
self::export_customfield_data($data, array_merge($subcontext, [$record->id]));
|
||||
} catch (Exception $e) {
|
||||
} catch (\Exception $e) {
|
||||
// We store some data that we can not initialise controller for. We still need to export it.
|
||||
self::export_customfield_data_unknown($record, $field, array_merge($subcontext, [$record->id]));
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@customfield @customfield_textarea @javascript @_file_upload
|
||||
# This feature has Atto-specific steps. See MDL-75913 for further details.
|
||||
@customfield @customfield_textarea @javascript @_file_upload @editor_atto
|
||||
Feature: Default value for the textarea custom field can contain images
|
||||
In order to see images on custom fields
|
||||
As a manager
|
||||
|
||||
@@ -49,7 +49,8 @@ Feature: Render H5P content using filters
|
||||
And I wait until the page is ready
|
||||
Then ".h5p-iframe" "css_element" should not exist
|
||||
|
||||
@javascript
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @editor_atto
|
||||
Scenario: Render a local H5P file as admin
|
||||
Given I log in as "admin"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
@@ -83,7 +84,8 @@ Feature: Render H5P content using filters
|
||||
And I should not see "you don't have access"
|
||||
And I should see "Lorum ipsum"
|
||||
|
||||
@javascript
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @editor_atto
|
||||
Scenario: Render a local H5P file as teacher
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
@@ -107,7 +109,8 @@ Feature: Render H5P content using filters
|
||||
Then I should see "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries."
|
||||
And I should see "missing-required-library"
|
||||
|
||||
@javascript
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @editor_atto
|
||||
Scenario: Render a local H5P file with existing libraries
|
||||
Given I log in as "teacher1"
|
||||
And I am on "Course 1" course homepage with editing mode on
|
||||
|
||||
@@ -333,6 +333,34 @@ class behat_config_util {
|
||||
return $this->get_component_contexts($component);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort the list of components contexts.
|
||||
*
|
||||
* This ensures that contexts are sorted consistently.
|
||||
* Core hooks defined in the behat_hooks class _must_ be defined first.
|
||||
*
|
||||
* @param array $contexts
|
||||
* @return array The sorted context list
|
||||
*/
|
||||
protected function sort_component_contexts(array $contexts): array {
|
||||
// Ensure that the lib_tests are first as they include the root of all tests, hooks, and more.
|
||||
uksort($contexts, function($a, $b): int {
|
||||
if ($a === 'behat_hooks') {
|
||||
return -1;
|
||||
}
|
||||
if ($b === 'behat_hooks') {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if ($a == $b) {
|
||||
return 0;
|
||||
}
|
||||
return ($a < $b) ? -1 : 1;
|
||||
});
|
||||
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Behat config file specifing the main context class,
|
||||
* the required Behat extensions and Moodle test wwwroot.
|
||||
@@ -1537,8 +1565,9 @@ class behat_config_util {
|
||||
}
|
||||
|
||||
// Sort the list of contexts.
|
||||
ksort($contexts);
|
||||
$contexts = $this->sort_component_contexts($contexts);
|
||||
|
||||
// Cache it for subsequent fetches.
|
||||
$this->themecontexts[$theme] = $contexts;
|
||||
|
||||
return $contexts;
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
use Behat\Testwork\Environment\Environment;
|
||||
use Behat\Mink\Exception\DriverException;
|
||||
|
||||
/**
|
||||
* Helper to get behat contexts.
|
||||
@@ -115,6 +116,34 @@ class behat_context_helper {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all Behat contexts which match the specified context class name prefix.
|
||||
*
|
||||
* Moodle uses a consistent class naming scheme for all Behat contexts, whereby the context name is in the format:
|
||||
*
|
||||
* behat_{component}
|
||||
*
|
||||
* This method will return all contexts which match the specified prefix.
|
||||
*
|
||||
* For example, to find all editors, you would pass in 'behat_editor', and this might return:
|
||||
* - behat_editor_atto
|
||||
* - behat_editor_textarea
|
||||
*
|
||||
* @param string $prefix The prefix to search for
|
||||
* @return \Behat\Behat\Context\Context[]
|
||||
*/
|
||||
public static function get_prefixed_contexts(string $prefix): array {
|
||||
if (!is_a(self::$environment, \Behat\Behat\Context\Environment\InitializedContextEnvironment::class)) {
|
||||
throw new DriverException(
|
||||
'Cannot get prefixed contexts - the environment is not an InitializedContextEnvironment'
|
||||
);
|
||||
}
|
||||
|
||||
return array_filter(self::$environment->getContexts(), function($context) use ($prefix): bool {
|
||||
return (strpos(get_class($context), $prefix) === 0);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for any theme override of the specified class name.
|
||||
*
|
||||
|
||||
@@ -30,6 +30,7 @@ use Behat\Mink\Exception\ExpectationException;
|
||||
use Behat\Mink\Exception\ElementNotFoundException;
|
||||
use Behat\Mink\Exception\NoSuchWindowException;
|
||||
use Behat\Mink\Session;
|
||||
use Behat\Testwork\Hook\Scope\HookScope;
|
||||
use Facebook\WebDriver\Exception\ScriptTimeoutException;
|
||||
use Facebook\WebDriver\WebDriverBy;
|
||||
use Facebook\WebDriver\WebDriverElement;
|
||||
@@ -223,6 +224,22 @@ trait behat_session_trait {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a description of the selector and locator to use in an exception message.
|
||||
*
|
||||
* @param string $selector The type of locator
|
||||
* @param mixed $locator The locator text
|
||||
* @return string
|
||||
*/
|
||||
protected function get_selector_description(string $selector, $locator): string {
|
||||
if ($selector === 'NodeElement') {
|
||||
$description = $locator->getText();
|
||||
return "'{$description}' {$selector}";
|
||||
}
|
||||
|
||||
return "'{$locator}' {$selector}";
|
||||
}
|
||||
|
||||
/**
|
||||
* Send key presses straight to the currently active element.
|
||||
*
|
||||
@@ -1029,6 +1046,29 @@ EOF;
|
||||
$this->look_for_exceptions();
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a function in a specific behat context.
|
||||
*
|
||||
* For example, to call the 'set_editor_value' function for all editors, you would call:
|
||||
*
|
||||
* behat_base::execute_in_matching_contexts('editor', 'set_editor_value', ['Some value']);
|
||||
*
|
||||
* This would find all behat contexts whose class name starts with 'behat_editor_' and
|
||||
* call the 'set_editor_value' function on that context.
|
||||
*
|
||||
* @param string $prefix
|
||||
* @param string $method
|
||||
* @param array $params
|
||||
*/
|
||||
public static function execute_in_matching_contexts(string $prefix, string $method, array $params): void {
|
||||
$contexts = behat_context_helper::get_prefixed_contexts("behat_{$prefix}_");
|
||||
foreach ($contexts as $context) {
|
||||
if (method_exists($context, $method) && is_callable([$context, $method])) {
|
||||
call_user_func_array([$context, $method], $params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the actual user in the behat session (note $USER does not correspond to the behat session's user).
|
||||
* @return mixed
|
||||
@@ -1578,4 +1618,63 @@ EOF;
|
||||
|
||||
return get_fast_modinfo($course)->get_cm($result->cmid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether any of the tags availble to the current scope match using the given callable.
|
||||
*
|
||||
* This function is typically called from within a Behat Hook, such as BeforeFeature, BeforeScenario, AfterStep, etc.
|
||||
*
|
||||
* The callable is used as the second argument to `array_filter()`, and is passed a single string argument for each of the
|
||||
* tags available in the scope.
|
||||
*
|
||||
* The tags passed will include:
|
||||
* - For a FeatureScope, the Feature tags only
|
||||
* - For a ScenarioScope, the Feature and Scenario tags
|
||||
* - For a StepScope, the Feature, Scenario, and Step tags
|
||||
*
|
||||
* An example usage may be:
|
||||
*
|
||||
* // Note: phpDoc beforeStep attribution not shown.
|
||||
* public function before_step(StepScope $scope) {
|
||||
* $callback = function (string $tag): bool {
|
||||
* return $tag === 'editor_atto' || substr($tag, 0, 5) === 'atto_';
|
||||
* };
|
||||
*
|
||||
* if (!self::scope_tags_match($scope, $callback)) {
|
||||
* return;
|
||||
* }
|
||||
*
|
||||
* // Do something here.
|
||||
* }
|
||||
*
|
||||
* @param HookScope $scope The scope to check
|
||||
* @param callable $callback The callable to use to check the scope
|
||||
* @return boolean Whether any of the scope tags match
|
||||
*/
|
||||
public static function scope_tags_match(HookScope $scope, callable $callback): bool {
|
||||
$tags = [];
|
||||
|
||||
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\FeatureScope::class)) {
|
||||
$tags = $scope->getFeature()->getTags();
|
||||
}
|
||||
|
||||
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\ScenarioScope::class)) {
|
||||
$tags = array_merge(
|
||||
$scope->getFeature()->getTags(),
|
||||
$scope->getScenario()->getTags()
|
||||
);
|
||||
}
|
||||
|
||||
if (is_subclass_of($scope, \Behat\Behat\Hook\Scope\StepScope::class)) {
|
||||
$tags = array_merge(
|
||||
$scope->getFeature()->getTags(),
|
||||
$scope->getScenario()->getTags(),
|
||||
$scope->getStep()->getTags()
|
||||
);
|
||||
}
|
||||
|
||||
$matches = array_filter($tags, $callback);
|
||||
|
||||
return !empty($matches);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -86,6 +86,7 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
|
||||
*/
|
||||
protected static $allowedselectors = array(
|
||||
'activity' => 'activity',
|
||||
'actionmenu' => 'actionmenu',
|
||||
'block' => 'block',
|
||||
'button' => 'button',
|
||||
'checkbox' => 'checkbox',
|
||||
@@ -136,6 +137,17 @@ class behat_partial_named_selector extends \Behat\Mink\Selector\PartialNamedSele
|
||||
protected static $moodleselectors = array(
|
||||
'activity' => <<<XPATH
|
||||
.//li[contains(concat(' ', normalize-space(@class), ' '), ' activity ')][descendant::*[contains(normalize-space(.), %locator%)]]
|
||||
XPATH
|
||||
, 'actionmenu' => <<<XPATH
|
||||
.//*[
|
||||
contains(concat(' ', normalize-space(@class), ' '), ' action-menu ')
|
||||
and
|
||||
descendant::*[
|
||||
contains(concat(' ', normalize-space(@class), ' '), ' dropdown-toggle ')
|
||||
and
|
||||
contains(normalize-space(.), %locator%)
|
||||
]
|
||||
]
|
||||
XPATH
|
||||
, 'block' => <<<XPATH
|
||||
.//*[@data-block][contains(concat(' ', normalize-space(@class), ' '), concat(' ', %locator%, ' ')) or
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Editor interface for setting editor values.
|
||||
*
|
||||
* @package behat
|
||||
* @category test
|
||||
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
namespace core_behat;
|
||||
|
||||
interface settable_editor {
|
||||
|
||||
/**
|
||||
* Set the editor value.
|
||||
*
|
||||
* @param string $editorid The id of the editor within the page
|
||||
* @param string $value The intended content of the editor
|
||||
*/
|
||||
public function set_editor_value(string $editorid, string $value): void;
|
||||
}
|
||||
@@ -32,7 +32,6 @@ require_once(__DIR__ . '/behat_form_textarea.php');
|
||||
/**
|
||||
* Moodle editor field.
|
||||
*
|
||||
* @todo Support for multiple editors
|
||||
* @package core_form
|
||||
* @category test
|
||||
* @copyright 2012 David Monllaó
|
||||
@@ -44,31 +43,40 @@ class behat_form_editor extends behat_form_textarea {
|
||||
* Sets the value to a field.
|
||||
*
|
||||
* @param string $value
|
||||
* @return void
|
||||
*/
|
||||
public function set_value($value) {
|
||||
|
||||
public function set_value($value): void {
|
||||
$editorid = $this->field->getAttribute('id');
|
||||
if ($this->running_javascript()) {
|
||||
$value = addslashes($value);
|
||||
// This will be transported in JSON, which doesn't allow newlines in strings, so we must escape them.
|
||||
$value = str_replace("\n", "\\n", $value);
|
||||
$js = '
|
||||
(function() {
|
||||
var editor = Y.one(document.getElementById("'.$editorid.'editable"));
|
||||
if (editor) {
|
||||
editor.setHTML("' . $value . '");
|
||||
}
|
||||
editor = Y.one(document.getElementById("'.$editorid.'"));
|
||||
editor.set("value", "' . $value . '");
|
||||
})();
|
||||
';
|
||||
behat_base::execute_script_in_session($this->session, $js);
|
||||
behat_base::execute_in_matching_contexts('editor', 'set_editor_value', [
|
||||
$editorid,
|
||||
$value,
|
||||
]);
|
||||
|
||||
} else {
|
||||
parent::set_value($value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current value of the select element.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function get_value(): string {
|
||||
if ($this->running_javascript()) {
|
||||
// Give any listening editors a chance to persist the value to the textarea.
|
||||
// Some editors only do this on form submission or similar events.
|
||||
behat_base::execute_in_matching_contexts('editor', 'store_current_value', [
|
||||
$this->field->getAttribute('id'),
|
||||
]);
|
||||
}
|
||||
|
||||
return parent::get_value();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select all the text in the form field.
|
||||
*
|
||||
@@ -102,8 +110,60 @@ class behat_form_editor extends behat_form_textarea {
|
||||
* @return bool The provided value matches the field value?
|
||||
*/
|
||||
public function matches($expectedvalue) {
|
||||
// A text editor may silently wrap the content in p tags (or not). Neither is an error.
|
||||
return $this->text_matches($expectedvalue) || $this->text_matches('<p>' . $expectedvalue . '</p>');
|
||||
// Fetch the actual value to save fetching it multiple times.
|
||||
$actualvalue = $this->get_value();
|
||||
|
||||
if ($this->text_matches($expectedvalue, $actualvalue)) {
|
||||
// The text is an exact match already.
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($this->text_matches("<p>{$expectedvalue}</p>", $actualvalue)) {
|
||||
// A text editor may silently wrap the content in p tags.
|
||||
return true;
|
||||
}
|
||||
|
||||
// Standardise both the expected value and the actual field value.
|
||||
// We are likely dealing with HTML content, given this is an editor.
|
||||
$expectedvalue = $this->standardise_html($expectedvalue);
|
||||
$actualvalue = $this->standardise_html($actualvalue);
|
||||
|
||||
// Note: We don't need to worry about the floats here that we care about in text_matches.
|
||||
// That condition isn't relevant to the content of an editor.
|
||||
if ($expectedvalue === $actualvalue) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Standardises the HTML content for comparison.
|
||||
*
|
||||
* @param string $html The HTML content to standardise
|
||||
* @return string The standardised HTML content
|
||||
*/
|
||||
protected function standardise_html(string $html): string {
|
||||
$document = new DOMDocument();
|
||||
$errorstate = libxml_use_internal_errors(true);
|
||||
|
||||
// Format the whitespace nicely.
|
||||
$document->preserveWhiteSpace = false;
|
||||
$document->formatOutput = true;
|
||||
|
||||
// Wrap the content in a DIV element so that it is not parsed weirdly.
|
||||
// Note: We must remove newlines too because DOMDocument does not do so, despite preserveWhiteSpace being false.
|
||||
// Unfortunately this is slightly limited in that it will also remove newlines from <pre> content and similar.
|
||||
$document->loadHTML(str_replace("\n", "", "<div>{$html}</div>"), LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
$document->normalizeDocument();
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($errorstate);
|
||||
|
||||
// Save the content of the 'div' element, removing the <div> and </div> tags at the start and end.
|
||||
return trim(substr(
|
||||
$document->saveHTML($document->getElementsByTagName('div')->item(0)),
|
||||
5,
|
||||
-6
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -244,10 +244,14 @@ class behat_form_field implements behat_session_interface {
|
||||
* Checks if the provided text matches the field value.
|
||||
*
|
||||
* @param string $expectedvalue
|
||||
* @param string|null $actualvalue The actual value. If not specified, this will be fetched from $this->get_value().
|
||||
* @return bool
|
||||
*/
|
||||
protected function text_matches($expectedvalue) {
|
||||
if (trim($expectedvalue) != trim($this->get_value())) {
|
||||
protected function text_matches($expectedvalue, ?string $actualvalue = null): bool {
|
||||
$actualvalue = $actualvalue ?? $this->get_value();
|
||||
|
||||
// Non strict string comparison.
|
||||
if (trim($expectedvalue) != trim($actualvalue)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
|
||||
@@ -23,9 +23,12 @@
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
use Behat\Behat\Hook\Scope\BeforeScenarioScope;
|
||||
|
||||
// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
|
||||
|
||||
require_once(__DIR__ . '/../../../../behat/behat_base.php');
|
||||
require_once(__DIR__ . '/../../../../behat/classes/settable_editor.php');
|
||||
|
||||
/**
|
||||
* Steps definitions to deal with the atto text editor
|
||||
@@ -35,7 +38,30 @@ require_once(__DIR__ . '/../../../../behat/behat_base.php');
|
||||
* @copyright 2014 Damyon Wiese
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
class behat_editor_atto extends behat_base {
|
||||
class behat_editor_atto extends behat_base implements \core_behat\settable_editor {
|
||||
|
||||
/**
|
||||
* Set the value for the editor.
|
||||
*
|
||||
* @param string $editorid
|
||||
* @param string $value
|
||||
*/
|
||||
public function set_editor_value(string $editorid, string $value): void {
|
||||
$js = <<<EOF
|
||||
(function() {
|
||||
const editableEditor = document.getElementById("${editorid}editable");
|
||||
if (editableEditor && editableEditor.classList.contains('editor_atto_content')) {
|
||||
editableEditor.innerHTML = "${value}";
|
||||
}
|
||||
const editor = document.getElementById("${editorid}");
|
||||
if (editor) {
|
||||
editor.value = "${value}";
|
||||
}
|
||||
})();
|
||||
EOF;
|
||||
|
||||
$this->execute_script($js);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the text in an Atto field.
|
||||
@@ -59,6 +85,24 @@ class behat_editor_atto extends behat_base {
|
||||
$field->select_text();
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that the Atto editor is used for all tests using the editor_atto, or atto_* tags.
|
||||
*
|
||||
* This ensures, whatever the default editor, that the Atto editor is used for these tests.
|
||||
*
|
||||
* @BeforeScenario
|
||||
* @param BeforeScenarioScope $scope The Behat Scope
|
||||
*/
|
||||
public function set_default_editor_flag(BeforeScenarioScope $scope): void {
|
||||
// This only applies to a scenario which matches the editor_atto, or an atto subplugin.
|
||||
$callback = function (string $tag): bool {
|
||||
return $tag === 'editor_atto' || substr($tag, 0, 5) === 'atto_';
|
||||
};
|
||||
|
||||
if (!self::scope_tags_match($scope, $callback)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->execute('behat_general::the_default_editor_is_set_to', ['atto']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
// 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/>.
|
||||
|
||||
/**
|
||||
* Moodle textarea editor field.
|
||||
*
|
||||
* @package core_form
|
||||
* @category test
|
||||
* @copyright 2022 Andrew Lyons <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
*/
|
||||
|
||||
require_once(__DIR__ . '/../../../../behat/behat_base.php');
|
||||
require_once(__DIR__ . '/../../../../behat/classes/settable_editor.php');
|
||||
|
||||
class behat_editor_textarea extends behat_base implements \core_behat\settable_editor {
|
||||
|
||||
/**
|
||||
* Set the value for the editor.
|
||||
*
|
||||
* @param string $editorid
|
||||
* @param string $value
|
||||
*/
|
||||
public function set_editor_value(string $editorid, string $value): void {
|
||||
$js = <<<EOF
|
||||
(function() {
|
||||
const editor = document.getElementById("${editorid}");
|
||||
if (editor && editor.tagName.toLowerCase() === 'textarea') {
|
||||
editor.value = "${value}";
|
||||
}
|
||||
})();
|
||||
EOF;
|
||||
$this->execute_script($js);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -3265,7 +3265,11 @@ class curl {
|
||||
throw new coding_exception('Curl options should be defined using strings, not constant values.');
|
||||
}
|
||||
if (stripos($name, 'CURLOPT_') === false) {
|
||||
$name = strtoupper('CURLOPT_'.$name);
|
||||
// Only prefix with CURLOPT_ if the option doesn't contain CURLINFO_,
|
||||
// which is a valid prefix for at least one option CURLINFO_HEADER_OUT.
|
||||
if (stripos($name, 'CURLINFO_') === false) {
|
||||
$name = strtoupper('CURLOPT_'.$name);
|
||||
}
|
||||
} else {
|
||||
$name = strtoupper($name);
|
||||
}
|
||||
|
||||
@@ -64,6 +64,9 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab
|
||||
/** @var array values for editor */
|
||||
protected $_values = array('text'=>null, 'format'=>null, 'itemid'=>null);
|
||||
|
||||
/** @var bool if true label will be hidden */
|
||||
protected $_hiddenLabel = false;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
@@ -496,4 +499,14 @@ class MoodleQuickForm_editor extends HTML_QuickForm_element implements templatab
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets label to be hidden.
|
||||
*
|
||||
* @param bool $hiddenLabel Whether the label should be hidden or not.
|
||||
* @return void
|
||||
*/
|
||||
function setHiddenLabel($hiddenLabel) {
|
||||
$this->_hiddenLabel = $hiddenLabel;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
{{< core_form/element-template-inline }}
|
||||
{{$element}}
|
||||
{{{element.html}}}
|
||||
{{/element}}
|
||||
{{/ core_form/element-template-inline }}
|
||||
@@ -16,6 +16,8 @@ These changes can be reverted once the following pull requests have been integra
|
||||
This local changes can be reverted once it's checked that they are present upstream (note the
|
||||
LTI-Tool-Provider-Library-PHP repo has been archived so it doesn't accept pull requests anymore):
|
||||
* MDL-67034 php74 compliance fixes
|
||||
* MDL-71920: Migrated from curl_exec and friends to use our Moodle curl wrapper,
|
||||
so we can better handle site security settings
|
||||
|
||||
It is recommended by upstream to install depdencies via composer - but the composer installation is bundled
|
||||
with an autoloader so it's better to do it manually.
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
namespace IMSGlobal\LTI;
|
||||
|
||||
global $CFG;
|
||||
require_once($CFG->libdir . '/filelib.php');
|
||||
|
||||
/**
|
||||
* Class to represent an HTTP message
|
||||
*
|
||||
@@ -110,31 +113,33 @@ class HTTPMessage
|
||||
{
|
||||
|
||||
$this->ok = false;
|
||||
// Try using curl if available
|
||||
// Try using curl if available
|
||||
if (function_exists('curl_init')) {
|
||||
$resp = '';
|
||||
$ch = curl_init();
|
||||
curl_setopt($ch, CURLOPT_URL, $this->url);
|
||||
$chResp = '';
|
||||
|
||||
$curl = new \curl();
|
||||
$options = [
|
||||
'CURLOPT_HEADER' => true,
|
||||
'CURLINFO_HEADER_OUT' => true,
|
||||
];
|
||||
if (!empty($this->requestHeaders)) {
|
||||
curl_setopt($ch, CURLOPT_HTTPHEADER, $this->requestHeaders);
|
||||
$options['CURLOPT_HTTPHEADER'] = $this->requestHeaders;
|
||||
} else {
|
||||
curl_setopt($ch, CURLOPT_HEADER, 0);
|
||||
$options['CURLOPT_HEADER'] = 0;
|
||||
}
|
||||
if ($this->method === 'POST') {
|
||||
curl_setopt($ch, CURLOPT_POST, true);
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
|
||||
$chResp = $curl->post($this->url, $this->request, $options);
|
||||
} else if ($this->method !== 'GET') {
|
||||
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $this->method);
|
||||
if (!is_null($this->request)) {
|
||||
curl_setopt($ch, CURLOPT_POSTFIELDS, $this->request);
|
||||
$chResp = $curl->post($this->url, $this->request, $options);
|
||||
}
|
||||
} else {
|
||||
$chResp = $curl->get($this->url, null, $options);
|
||||
}
|
||||
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||
curl_setopt($ch, CURLINFO_HEADER_OUT, true);
|
||||
curl_setopt($ch, CURLOPT_HEADER, true);
|
||||
$chResp = curl_exec($ch);
|
||||
$this->ok = $chResp !== false;
|
||||
if ($this->ok) {
|
||||
$info = $curl->get_info();
|
||||
|
||||
if (!$curl->get_errno() && !$curl->error) {
|
||||
$chResp = str_replace("\r\n", "\n", $chResp);
|
||||
$chRespSplit = explode("\n\n", $chResp, 2);
|
||||
if ((count($chRespSplit) > 1) && (substr($chRespSplit[1], 0, 5) === 'HTTP/')) {
|
||||
@@ -142,17 +147,20 @@ class HTTPMessage
|
||||
}
|
||||
$this->responseHeaders = $chRespSplit[0];
|
||||
$resp = $chRespSplit[1];
|
||||
$this->status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
$this->status = $info['http_code'];
|
||||
$this->ok = $this->status < 400;
|
||||
if (!$this->ok) {
|
||||
$this->error = curl_error($ch);
|
||||
$this->error = $curl->error;
|
||||
}
|
||||
} else {
|
||||
$this->error = $curl->error;
|
||||
$resp = $chResp;
|
||||
}
|
||||
$this->requestHeaders = str_replace("\r\n", "\n", curl_getinfo($ch, CURLINFO_HEADER_OUT));
|
||||
curl_close($ch);
|
||||
|
||||
$this->response = $resp;
|
||||
$this->requestHeaders = !empty($info['request_header']) ? str_replace("\r\n", "\n", $info['request_header']) : '';
|
||||
} else {
|
||||
// Try using fopen if curl was not available
|
||||
// Try using fopen if curl was not available
|
||||
$opts = array('method' => $this->method,
|
||||
'content' => $this->request
|
||||
);
|
||||
|
||||
+2
-1
@@ -1063,8 +1063,9 @@ function clean_param($param, $type) {
|
||||
} else if (preg_match('/^' . preg_quote($CFG->wwwroot . '/', '/') . '/i', $param)) {
|
||||
// Absolute, and matches our wwwroot.
|
||||
} else {
|
||||
|
||||
// Relative - let's make sure there are no tricks.
|
||||
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?')) {
|
||||
if (validateUrlSyntax('/' . $param, 's-u-P-a-p-f+q?r?') && !preg_match('/javascript:/i', $param)) {
|
||||
// Looks ok.
|
||||
} else {
|
||||
$param = '';
|
||||
|
||||
@@ -14,15 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
/**
|
||||
* Unit tests for core_table\local\filter\filter.
|
||||
*
|
||||
* @package core_table
|
||||
* @category test
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU Public License
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace core_table\local\filter;
|
||||
@@ -33,11 +24,11 @@ use InvalidArgumentException;
|
||||
/**
|
||||
* Unit tests for core_table\local\filter\filter.
|
||||
*
|
||||
* @coversDefaultClass \core_table\local\filter\filter
|
||||
* @package core_table
|
||||
* @category test
|
||||
* @copyright 2020 Andrew Nicols <andrew@nicols.co.uk>
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \core_table\local\filter\filter
|
||||
*/
|
||||
class filter_test extends advanced_testcase {
|
||||
/**
|
||||
|
||||
@@ -27,8 +27,8 @@
|
||||
|
||||
require_once(__DIR__ . '/../../behat/behat_base.php');
|
||||
|
||||
use Behat\Mink\Exception\ExpectationException as ExpectationException;
|
||||
use Behat\Mink\Exception\DriverException as DriverException;
|
||||
use Behat\Mink\Element\NodeElement;
|
||||
use Behat\Mink\Exception\DriverException;
|
||||
|
||||
/**
|
||||
* Steps definitions to open and close action menus.
|
||||
@@ -78,4 +78,122 @@ class behat_action_menu extends behat_base {
|
||||
$this->ensure_node_is_visible($node);
|
||||
$node->click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific item in an action menu.
|
||||
*
|
||||
* @When /^I choose the "(?P<item_string>(?:[^"]|\\")*)" item in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
|
||||
* @param string $item The item to choose
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
*/
|
||||
public function i_choose_in_the_named_menu(string $item, string $actionmenu): void {
|
||||
$menu = $this->find('actionmenu', $actionmenu);
|
||||
$this->select_item_in_action_menu($item, $menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a specific item in an action menu within a container.
|
||||
*
|
||||
* @When /^I choose the "(?P<item_string>(?:[^"]|\\")*)" item in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
|
||||
* @param string $item The item to choose
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
* @param string|NodeElement $locator The identifer used for the container
|
||||
* @param string $selector The type of container to locate
|
||||
*/
|
||||
public function i_choose_in_the_named_menu_in_container(string $item, string $actionmenu, $locator, $selector): void {
|
||||
$container = $this->find($selector, $locator);
|
||||
$menu = $this->find('actionmenu', $actionmenu, false, $container);
|
||||
$this->select_item_in_action_menu($item, $menu);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select an item in the specified menu.
|
||||
*
|
||||
* Note: This step does work both with, and without, JavaScript.
|
||||
*
|
||||
* @param string $item Item string value
|
||||
* @param NodeElement $menu The menu NodeElement to select from
|
||||
*/
|
||||
protected function select_item_in_action_menu(string $item, NodeElement $menu): void {
|
||||
if ($this->running_javascript()) {
|
||||
// Open the menu by clicking on the trigger.
|
||||
$this->execute(
|
||||
'behat_general::i_click_on',
|
||||
[$menu, "NodeElement"]
|
||||
);
|
||||
}
|
||||
|
||||
// Select the menu item.
|
||||
$this->execute(
|
||||
'behat_general::i_click_on_in_the',
|
||||
[$item, "link", $menu, "NodeElement"]
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The action menu item should not exist.
|
||||
*
|
||||
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should not exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
|
||||
* @param string $item The item to check
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
*/
|
||||
public function item_should_not_exist(string $item, string $actionmenu): void {
|
||||
$menu = $this->find('actionmenu', $actionmenu);
|
||||
$this->execute('behat_general::should_not_exist_in_the', [
|
||||
$item, 'link',
|
||||
$menu, 'NodeElement'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The action menu item should not exist within a container.
|
||||
*
|
||||
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should not exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
|
||||
* @param string $item The item to check
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
* @param string|NodeElement $locator The identifer used for the container
|
||||
* @param string $selector The type of container to locate
|
||||
*/
|
||||
public function item_should_not_exist_in_the(string $item, string $actionmenu, $locator, $selector): void {
|
||||
$container = $this->find($selector, $locator);
|
||||
$menu = $this->find('actionmenu', $actionmenu, false, $container);
|
||||
$this->execute('behat_general::should_not_exist_in_the', [
|
||||
$item, 'link',
|
||||
$menu, 'NodeElement'
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* The action menu item should exist.
|
||||
*
|
||||
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu$/
|
||||
* @param string $item The item to check
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
*/
|
||||
public function item_should_exist(string $item, string $actionmenu): void {
|
||||
$menu = $this->find('actionmenu', $actionmenu);
|
||||
$this->execute('behat_general::should_exist_in_the', [
|
||||
$item, 'link',
|
||||
$menu, 'NodeElement'
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* The action menu item should exist within a container.
|
||||
*
|
||||
* @Then /^the "(?P<item_string>(?:[^"]|\\")*)" item should exist in the "(?P<actionmenu_string>(?:[^"]|\\")*)" action menu of the "(?P<locator_string>(?:[^"]|\\")*)" "(?P<type_string>(?:[^"]|\\")*)"$/
|
||||
* @param string $item The item to check
|
||||
* @param string $actionmenu The text used in the description of the action menu
|
||||
* @param string|NodeElement $locator The identifer used for the container
|
||||
* @param string $selector The type of container to locate
|
||||
*/
|
||||
public function item_should_exist_in_the(string $item, string $actionmenu, $locator, $selector): void {
|
||||
$container = $this->find($selector, $locator);
|
||||
$menu = $this->find('actionmenu', $actionmenu, false, $container);
|
||||
$this->execute('behat_general::should_exist_in_the', [
|
||||
$item, 'link',
|
||||
$menu, 'NodeElement'
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,22 @@ class behat_general extends behat_base {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to a second window.
|
||||
*
|
||||
* @Given /^I switch to a second window$/
|
||||
* @throws DriverException If there aren't exactly 2 windows open.
|
||||
*/
|
||||
public function switch_to_second_window() {
|
||||
$names = $this->getSession()->getWindowNames();
|
||||
|
||||
if (count($names) !== 2) {
|
||||
throw new DriverException('Expected to see 2 windows open, found ' . count($names));
|
||||
}
|
||||
|
||||
$this->getSession()->switchToWindow($names[1]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches to the main Moodle frame.
|
||||
*
|
||||
@@ -1154,7 +1170,8 @@ EOF;
|
||||
$containernode = $this->find($containerselectortype, $containerelement);
|
||||
|
||||
// Specific exception giving info about where can't we find the element.
|
||||
$locatorexceptionmsg = "{$element} in the {$containerelement} {$containerselectortype}";
|
||||
$containerdescription = $this->get_selector_description($containerselectortype, $containerelement);
|
||||
$locatorexceptionmsg = "{$element} not found in the {$containerdescription}}";
|
||||
$exception = new ElementNotFoundException($this->getSession(), $selectortype, null, $locatorexceptionmsg);
|
||||
|
||||
// Looks for the requested node inside the container node.
|
||||
@@ -1188,8 +1205,10 @@ EOF;
|
||||
}
|
||||
|
||||
// The element was found and should not have been. Throw an exception.
|
||||
$elementdescription = $this->get_selector_description($selectortype, $element);
|
||||
$containerdescription = $this->get_selector_description($containerselectortype, $containerelement);
|
||||
throw new ExpectationException(
|
||||
"The '{$element}' '{$selectortype}' exists in the '{$containerelement}' '{$containerselectortype}'",
|
||||
"The {$elementdescription} exists in the {$containerdescription}",
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
@@ -1713,7 +1732,17 @@ EOF;
|
||||
* @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));
|
||||
// Certain keys, such as the newline character, must be converted to the appropriate character code.
|
||||
// Without this, keys will behave differently depending on the browser.
|
||||
$keylist = array_map(function($key): string {
|
||||
switch ($key) {
|
||||
case "\n":
|
||||
return behat_keys::ENTER;
|
||||
default:
|
||||
return $key;
|
||||
}
|
||||
}, str_split($keys));
|
||||
behat_base::type_keys($this->getSession(), $keylist);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2099,4 +2128,33 @@ EOF;
|
||||
public function i_mark_this_test_as_long_running(int $factor = 2): void {
|
||||
$this->set_test_timeout_factor($factor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default text editor to the named text editor.
|
||||
*
|
||||
* @Given the default editor is set to :editor
|
||||
* @param string $editor
|
||||
* @throws ExpectationException If the specified editor is not available.
|
||||
*/
|
||||
public function the_default_editor_is_set_to(string $editor): void {
|
||||
global $CFG;
|
||||
|
||||
// Check if the provided editor is available.
|
||||
if (!array_key_exists($editor, editors_get_available())) {
|
||||
throw new ExpectationException(
|
||||
"Unable to set the editor to {$editor} as it is not installed. The available editors are: " .
|
||||
implode(', ', array_keys(editors_get_available())),
|
||||
$this->getSession()
|
||||
);
|
||||
}
|
||||
|
||||
// Make the provided editor the default one in $CFG->texteditors by
|
||||
// moving it to the first [editor],atto,tiny,tinymce,textarea on the list.
|
||||
$list = explode(',', $CFG->texteditors);
|
||||
array_unshift($list, $editor);
|
||||
$list = array_unique($list);
|
||||
|
||||
// Set the list new list of editors.
|
||||
set_config('texteditors', implode(',', $list));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @package core
|
||||
* @copyright 2017 Damyon Wiese
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later.
|
||||
* @coversDefaultClass \core\oauth2\api
|
||||
* @covers \core\oauth2\api
|
||||
*/
|
||||
class core_oauth2_testcase extends advanced_testcase {
|
||||
|
||||
|
||||
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -3479,6 +3479,7 @@
|
||||
_proto.createEl = function createEl$1(tagName, properties, attributes) {
|
||||
return createEl(tagName, properties, attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Localize a string given the string in english.
|
||||
*
|
||||
@@ -17820,11 +17821,16 @@
|
||||
_proto.createEl = function createEl(type, props, attrs) {
|
||||
// The control is textual, not just an icon
|
||||
this.nonIconControl = true;
|
||||
return _ClickableComponent.prototype.createEl.call(this, 'li', assign({
|
||||
var el = _ClickableComponent.prototype.createEl.call(this, 'li', assign({
|
||||
className: 'vjs-menu-item',
|
||||
innerHTML: "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label) + "</span>",
|
||||
tabIndex: -1
|
||||
}, props), attrs);
|
||||
}, props), attrs); // swap icon with menu item text.
|
||||
|
||||
el.replaceChild(createEl('span', {
|
||||
className: 'vjs-menu-item-text',
|
||||
textContent: this.localize(this.options_.label)
|
||||
}), el.querySelector('.vjs-icon-placeholder'));
|
||||
return el;
|
||||
}
|
||||
/**
|
||||
* Ignore keys which are used by the menu, but pass any other ones up. See
|
||||
@@ -18888,18 +18894,22 @@
|
||||
var _proto = SubsCapsMenuItem.prototype;
|
||||
|
||||
_proto.createEl = function createEl(type, props, attrs) {
|
||||
var innerHTML = "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label);
|
||||
var el = _TextTrackMenuItem.prototype.createEl.call(this, type, props, attrs);
|
||||
var parentSpan = el.querySelector('.vjs-menu-item-text');
|
||||
|
||||
if (this.options_.track.kind === 'captions') {
|
||||
innerHTML += "\n <span aria-hidden=\"true\" class=\"vjs-icon-placeholder\"></span>\n <span class=\"vjs-control-text\"> " + this.localize('Captions') + "</span>\n ";
|
||||
parentSpan.appendChild(createEl('span', {
|
||||
className: 'vjs-icon-placeholder'
|
||||
}, {
|
||||
'aria-hidden': true
|
||||
}));
|
||||
parentSpan.appendChild(createEl('span', {
|
||||
className: 'vjs-control-text',
|
||||
// space added as the text will visually flow with the
|
||||
// label
|
||||
textContent: " " + this.localize('Captions')
|
||||
}));
|
||||
}
|
||||
|
||||
innerHTML += '</span>';
|
||||
|
||||
var el = _TextTrackMenuItem.prototype.createEl.call(this, type, assign({
|
||||
innerHTML: innerHTML
|
||||
}, props), attrs);
|
||||
|
||||
return el;
|
||||
};
|
||||
|
||||
@@ -19049,17 +19059,20 @@
|
||||
var _proto = AudioTrackMenuItem.prototype;
|
||||
|
||||
_proto.createEl = function createEl(type, props, attrs) {
|
||||
var innerHTML = "<span class=\"vjs-menu-item-text\">" + this.localize(this.options_.label);
|
||||
var el = _MenuItem.prototype.createEl.call(this, type, props, attrs);
|
||||
var parentSpan = el.querySelector('.vjs-menu-item-text');
|
||||
|
||||
if (this.options_.track.kind === 'main-desc') {
|
||||
innerHTML += "\n <span aria-hidden=\"true\" class=\"vjs-icon-placeholder\"></span>\n <span class=\"vjs-control-text\"> " + this.localize('Descriptions') + "</span>\n ";
|
||||
}
|
||||
|
||||
innerHTML += '</span>';
|
||||
|
||||
var el = _MenuItem.prototype.createEl.call(this, type, assign({
|
||||
innerHTML: innerHTML
|
||||
}, props), attrs);
|
||||
parentSpan.appendChild(_MenuItem.prototype.createEl.call(this, 'span', {
|
||||
className: 'vjs-icon-placeholder'
|
||||
}, {
|
||||
'aria-hidden': true
|
||||
}));
|
||||
parentSpan.appendChild(_MenuItem.prototype.createEl.call(this, 'span', {
|
||||
className: 'vjs-control-text',
|
||||
textContent: this.localize('Descriptions')
|
||||
}));
|
||||
}
|
||||
|
||||
return el;
|
||||
}
|
||||
@@ -32546,7 +32559,22 @@
|
||||
return buf.push(' ', node.name, '="', node.value.replace(/[<&"]/g, _xmlEncoder), '"');
|
||||
|
||||
case TEXT_NODE:
|
||||
return buf.push(node.data.replace(/[<&]/g, _xmlEncoder));
|
||||
/**
|
||||
* The ampersand character (&) and the left angle bracket (<) must not appear in their literal form,
|
||||
* except when used as markup delimiters, or within a comment, a processing instruction, or a CDATA section.
|
||||
* If they are needed elsewhere, they must be escaped using either numeric character references or the strings
|
||||
* `&` and `<` respectively.
|
||||
* The right angle bracket (>) may be represented using the string " > ", and must, for compatibility,
|
||||
* be escaped using either `>` or a character reference when it appears in the string `]]>` in content,
|
||||
* when that string is not marking the end of a CDATA section.
|
||||
*
|
||||
* In the content of elements, character data is any string of characters
|
||||
* which does not contain the start-delimiter of any markup
|
||||
* and does not include the CDATA-section-close delimiter, `]]>`.
|
||||
*
|
||||
* @see https://www.w3.org/TR/xml/#NT-CharData
|
||||
*/
|
||||
return buf.push(node.data.replace(/[<&]/g, _xmlEncoder).replace(/]]>/g, ']]>'));
|
||||
|
||||
case CDATA_SECTION_NODE:
|
||||
return buf.push('<![CDATA[', node.data, ']]>');
|
||||
|
||||
@@ -36,4 +36,10 @@ Import plugins:
|
||||
define(['media_videojs/video-lazy']
|
||||
|
||||
3. Download https://github.com/videojs/video-js-swf/blob/master/dist/video-js.swf
|
||||
and place it into 'videojs/video-js.swf'
|
||||
and place it into 'videojs/video-js.swf'
|
||||
|
||||
Changes:
|
||||
In order to improve the validation, a couple of minor changes have been added to the video-lazy.js:
|
||||
1. Partial upgrade from VideoJS 7.17.0 for 'Subtitles' label check in 17824 - 17833
|
||||
2. Partial upgrade from VideoJS 7.17.0 for 'Captions' label check from 18897 - 18911
|
||||
3. Partial upgrade from VideoJS 7.17.0 for 'Description' label check from 19062 - 19075
|
||||
@@ -4,7 +4,8 @@ Feature: In an assignment, teachers can edit feedback for a students previous su
|
||||
As a teacher
|
||||
I need to be able to edit the feedback for a students previous submission attempt.
|
||||
|
||||
@javascript
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @editor_atto
|
||||
Scenario: Edit feedback for a students previous attempt.
|
||||
Given the following "courses" exist:
|
||||
| fullname | shortname | category | groupmode |
|
||||
|
||||
@@ -183,8 +183,10 @@ if (($mode === 'csstemplate') || ($mode === 'jstemplate')) {
|
||||
$usehtmleditor = data_get_config($data, "editor_{$mode}", true);
|
||||
}
|
||||
|
||||
$datafieldtype = '';
|
||||
if ($usehtmleditor) {
|
||||
$format = FORMAT_HTML;
|
||||
$datafieldtype = ' data-fieldtype="editor" ';
|
||||
} else {
|
||||
$format = FORMAT_PLAIN;
|
||||
}
|
||||
@@ -226,7 +228,7 @@ if ($mode == 'listtemplate'){
|
||||
$field = 'listtemplateheader';
|
||||
$editor->set_text($data->listtemplateheader);
|
||||
$editor->use_editor($field, $options);
|
||||
echo '<div><textarea id="'.$field.'" name="'.$field.'" class="form-control" rows="15" cols="80">' .
|
||||
echo "<div><textarea id='{$field}' {$datafieldtype} name='{$field}' class='form-control' rows='15' cols='80'>" .
|
||||
s($data->listtemplateheader) . '</textarea></div>';
|
||||
|
||||
echo '</td>';
|
||||
@@ -344,7 +346,7 @@ $field = 'template';
|
||||
$editor->set_text($data->{$mode});
|
||||
$editor->use_editor($field, $options);
|
||||
echo '<div>';
|
||||
echo '<textarea class="form-control" id="' . $field . '" ' .
|
||||
echo '<textarea class="form-control" id="' . $field . '" ' . $datafieldtype .
|
||||
'name="' . $field . '" rows="15" cols="80">' . s($data->{$mode}) . '</textarea>';
|
||||
echo '</div>';
|
||||
echo '</td>';
|
||||
@@ -360,7 +362,7 @@ if ($mode == 'listtemplate'){
|
||||
$editor->set_text($data->listtemplatefooter);
|
||||
$editor->use_editor($field, $options);
|
||||
echo '<div>';
|
||||
echo '<textarea id="' . $field . '" class="form-control" ' .
|
||||
echo '<textarea id="' . $field . '" class="form-control" ' . $datafieldtype .
|
||||
'name="' . $field . '" rows="15" cols="80">' . s($data->listtemplatefooter) . '</textarea>';
|
||||
echo '</div>';
|
||||
echo '</td>';
|
||||
@@ -377,7 +379,7 @@ if ($mode == 'listtemplate'){
|
||||
$editor->set_text($data->rsstitletemplate);
|
||||
$editor->use_editor($field, $options);
|
||||
echo '<div>';
|
||||
echo '<textarea id="' . $field . '" name="' . $field . '" ' .
|
||||
echo '<textarea id="' . $field . '" name="' . $field . '" ' . $datafieldtype .
|
||||
'class="form-control" rows="15" cols="80">' . s($data->rsstitletemplate) . '</textarea>';
|
||||
echo '</div>';
|
||||
echo '</td>';
|
||||
|
||||
@@ -22,6 +22,7 @@ Feature: Content bank link in the activity settings form
|
||||
And I add a "H5P" to section "1"
|
||||
Then I should see "Use the content bank (opens in new window) to manage your H5P files"
|
||||
And I click on "content bank (opens in new window)" "link" in the "General" "fieldset"
|
||||
And I switch to a second window
|
||||
And I should see "C1" in the "page-navbar" "region"
|
||||
And I close all opened windows
|
||||
|
||||
|
||||
@@ -24,26 +24,18 @@ Feature: Change grading options in an H5P activity
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
|
||||
And I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
# H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page.
|
||||
And I reload the page
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
And the following "mod_h5pactivity > attempts" exist:
|
||||
| user | h5pactivity | attempt | interactiontype | rawscore | maxscore | duration | completion | success |
|
||||
| student1 | Awesome H5P package | 1 | choice | 0 | 1 | 4 | 1 | 0 |
|
||||
| student1 | Awesome H5P package | 2 | choice | 1 | 1 | 4 | 1 | 1 |
|
||||
| student1 | Awesome H5P package | 3 | choice | 0 | 1 | 4 | 1 | 0 |
|
||||
|
||||
@javascript
|
||||
Scenario: Default grading is max attempt grade
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
|
||||
And I expand all fieldsets
|
||||
And the field "Grading method" matches value "Highest grade"
|
||||
And I click on "Save and return to course" "button"
|
||||
When I navigate to "View > User report" in the course gradebook
|
||||
And I set the field "Select all or one user" to "Student 1"
|
||||
Then the following should exist in the "user-grade" table:
|
||||
@@ -63,7 +55,7 @@ Feature: Change grading options in an H5P activity
|
||||
| Awesome H5P package | 0.00 | 0.00 % |
|
||||
|
||||
@javascript
|
||||
Scenario: Change setting to first attempt
|
||||
Scenario: Change setting to last attempt
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity editing" page logged in as teacher1
|
||||
When I set the following fields to these values:
|
||||
| Grading method | Last attempt |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Feature: Do a H5P attempt
|
||||
In order to let students do a H5P attempt
|
||||
As a teacher
|
||||
I need to list students attempts on the log report
|
||||
I need to list students attempts on various reports
|
||||
|
||||
Background:
|
||||
Given the following "users" exist:
|
||||
@@ -24,6 +24,7 @@ Feature: Do a H5P attempt
|
||||
| course | C1 |
|
||||
| name | Awesome H5P package |
|
||||
| packagefilepath | h5p/tests/fixtures/multiple-choice-2-6.h5p |
|
||||
| grademethod | 2 |
|
||||
|
||||
Scenario: View an H5P as a teacher
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as teacher1
|
||||
@@ -31,7 +32,7 @@ Feature: Do a H5P attempt
|
||||
Then I should see "This content is displayed in preview mode"
|
||||
|
||||
@javascript
|
||||
Scenario: To an attempts and check on course log report
|
||||
Scenario: Do an attempt and check on course log report
|
||||
When I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I wait until the page is ready
|
||||
And I should not see "This content is displayed in preview mode"
|
||||
@@ -46,3 +47,45 @@ Feature: Do a H5P attempt
|
||||
And I follow "Student 1"
|
||||
Then I follow "Today's logs"
|
||||
And I should see "xAPI statement received"
|
||||
|
||||
@javascript
|
||||
Scenario: Do various attempts and check them with the attempts and user grades reports
|
||||
Given I am on the "Awesome H5P package" "h5pactivity activity" page logged in as student1
|
||||
And I wait until the page is ready
|
||||
And I should not see "This content is displayed in preview mode"
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element"
|
||||
# We need to wait 1 second here because, in very quick environments, the 2nd
|
||||
# attempts happen too close to the 1st one and it's not sent properly. See MDL-76010.
|
||||
And I wait "1" seconds
|
||||
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
# H5P does not allow to Retry if the user checks the correct answer, we need to refresh the page.
|
||||
And I switch to the main frame
|
||||
And I reload the page
|
||||
And I switch to "h5p-player" class iframe
|
||||
And I switch to "h5p-iframe" class iframe
|
||||
# Because of the steps above, the 2nd and 3rd attempts are enough "separated" and we don't
|
||||
# need to add any wait here.
|
||||
And I click on "Wrong one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I click on "Retry" "button" in the ".h5p-question-buttons" "css_element"
|
||||
# Again, the wait between 3rd and 4th attempt, to separate them a little bit.
|
||||
And I wait "1" seconds
|
||||
And I click on "Correct one" "text" in the ".h5p-question-content" "css_element"
|
||||
And I click on "Check" "button" in the ".h5p-question-buttons" "css_element"
|
||||
And I switch to the main frame
|
||||
When I follow "View my attempts"
|
||||
And "1" row "Score" column of "table" table should contain "0"
|
||||
And "2" row "Score" column of "table" table should contain "1"
|
||||
And "3" row "Score" column of "table" table should contain "0"
|
||||
And "4" row "Score" column of "table" table should contain "1"
|
||||
And I am on the "Course 1" course page logged in as teacher1
|
||||
And I navigate to "View > User report" in the course gradebook
|
||||
And I set the field "Select all or one user" to "Student 1"
|
||||
Then the following should exist in the "user-grade" table:
|
||||
| Grade item | Grade | Percentage |
|
||||
| Awesome H5P package | 50.00 | 50.00 % |
|
||||
|
||||
@@ -191,6 +191,7 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
* @param array $data the attempts data array
|
||||
*/
|
||||
public function create_attempt(array $data): void {
|
||||
global $DB;
|
||||
|
||||
if (!isset($data['h5pactivityid'])) {
|
||||
throw new coding_exception('Must specify h5pactivityid when creating a H5P attempt.');
|
||||
@@ -218,6 +219,12 @@ class mod_h5pactivity_generator extends testing_module_generator {
|
||||
}
|
||||
|
||||
$this->insert_statement($data, $this->$method($data));
|
||||
|
||||
// If the activity has tracking enabled, try to recalculate grades.
|
||||
$activity = $DB->get_record('h5pactivity', ['id' => $data['h5pactivityid']]);
|
||||
if ($activity->enabletracking) {
|
||||
h5pactivity_update_grades($activity, $data['userid']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@mod @mod_lesson
|
||||
# This feature has Atto-specific steps. See MDL-75913 for further details.
|
||||
@mod @mod_lesson @editor_atto
|
||||
Feature: In a lesson activity, a teacher can duplicate a lesson page
|
||||
In order to duplicate a lesson page
|
||||
As a teacher
|
||||
|
||||
@@ -3,7 +3,8 @@ Feature: In a lesson activity, teacher can add embedded images in questions answ
|
||||
As a teacher
|
||||
I need to add questions with images in answers and responses
|
||||
|
||||
@javascript @_file_upload
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @_file_upload @editor_atto
|
||||
Scenario: questions with images in answers and responses
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
|
||||
@@ -55,7 +55,8 @@ Feature: Teachers can override the grade for any question
|
||||
# This time is same as time the window is open. So wait for it to close before proceeding.
|
||||
And I wait "2" seconds
|
||||
|
||||
@javascript @_switch_window @_file_upload @_bug_phantomjs
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@javascript @_switch_window @_file_upload @_bug_phantomjs @editor_atto
|
||||
Scenario: Comment on a response to an essay question attempt.
|
||||
When I log in as "teacher1"
|
||||
And I follow "Manage private files"
|
||||
|
||||
@@ -179,6 +179,7 @@ class mod_quiz_generator extends testing_module_generator {
|
||||
public function create_override(array $data): void {
|
||||
global $DB;
|
||||
|
||||
// Validate.
|
||||
if (!isset($data['quiz'])) {
|
||||
throw new coding_exception('Must specify quiz (id) when creating a quiz override.');
|
||||
}
|
||||
@@ -191,6 +192,10 @@ class mod_quiz_generator extends testing_module_generator {
|
||||
throw new coding_exception('Cannot specify both userid and groupid when creating a quiz override.');
|
||||
}
|
||||
|
||||
// Create the override.
|
||||
$DB->insert_record('quiz_overrides', (object) $data);
|
||||
|
||||
// Update any associated calendar events, if necessary.
|
||||
quiz_update_events($DB->get_record('quiz', ['id' => $data['quiz']], '*', MUST_EXIST));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ defined('MOODLE_INTERNAL') || die();
|
||||
* @category phpunit
|
||||
* @copyright 2012 Matt Petro
|
||||
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
||||
* @covers \mod_quiz_generator
|
||||
*/
|
||||
class mod_quiz_generator_testcase extends advanced_testcase {
|
||||
public function test_generator() {
|
||||
@@ -42,7 +43,7 @@ class mod_quiz_generator_testcase extends advanced_testcase {
|
||||
|
||||
$this->assertEquals(0, $DB->count_records('quiz'));
|
||||
|
||||
/** @var mod_quiz_generator $generator */
|
||||
/** @var \mod_quiz_generator $generator */
|
||||
$generator = $this->getDataGenerator()->get_plugin_generator('mod_quiz');
|
||||
$this->assertInstanceOf('mod_quiz_generator', $generator);
|
||||
$this->assertEquals('quiz', $generator->get_modulename());
|
||||
@@ -64,4 +65,65 @@ class mod_quiz_generator_testcase extends advanced_testcase {
|
||||
$this->assertEqualsWithDelta($createtime,
|
||||
$DB->get_field('quiz', 'timecreated', ['id' => $cm->instance]), 2);
|
||||
}
|
||||
|
||||
public function test_generating_a_user_override() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$user = $generator->create_user();
|
||||
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
|
||||
$generator->enrol_user($user->id, $course->id, 'student');
|
||||
|
||||
/** @var \mod_quiz_generator $quizgenerator */
|
||||
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
|
||||
$quizgenerator->create_override([
|
||||
'quiz' => $quiz->id,
|
||||
'userid' => $user->id,
|
||||
'timeclose' => strtotime('2022-10-20'),
|
||||
]);
|
||||
|
||||
// Check the corresponding calendar event now exists.
|
||||
$events = calendar_get_events(strtotime('2022-01-01'),
|
||||
strtotime('2022-12-31'), $user->id, false, $course->id);
|
||||
$this->assertCount(1, $events);
|
||||
$event = reset($events);
|
||||
$this->assertEquals($user->id, $event->userid);
|
||||
$this->assertEquals(0, $event->groupid);
|
||||
$this->assertEquals(0, $event->courseid);
|
||||
$this->assertEquals('quiz', $event->modulename);
|
||||
$this->assertEquals($quiz->id, $event->instance);
|
||||
$this->assertEquals('close', $event->eventtype);
|
||||
$this->assertEquals(strtotime('2022-10-20'), $event->timestart);
|
||||
}
|
||||
|
||||
public function test_generating_a_group_override() {
|
||||
$this->resetAfterTest(true);
|
||||
|
||||
$generator = $this->getDataGenerator();
|
||||
$course = $generator->create_course();
|
||||
$quiz = $generator->create_module('quiz', ['course' => $course->id]);
|
||||
$group = $generator->create_group(['courseid' => $course->id]);
|
||||
|
||||
/** @var \mod_quiz_generator $quizgenerator */
|
||||
$quizgenerator = $generator->get_plugin_generator('mod_quiz');
|
||||
$quizgenerator->create_override([
|
||||
'quiz' => $quiz->id,
|
||||
'groupid' => $group->id,
|
||||
'timeclose' => strtotime('2022-10-20'),
|
||||
]);
|
||||
|
||||
// Check the corresponding calendar event now exists.
|
||||
$events = calendar_get_events(strtotime('2022-01-01'),
|
||||
strtotime('2022-12-31'), false, $group->id, $course->id);
|
||||
$this->assertCount(1, $events);
|
||||
$event = reset($events);
|
||||
$this->assertEquals(0, $event->userid);
|
||||
$this->assertEquals($group->id, $event->groupid);
|
||||
$this->assertEquals($course->id, $event->courseid);
|
||||
$this->assertEquals('quiz', $event->modulename);
|
||||
$this->assertEquals($quiz->id, $event->instance);
|
||||
$this->assertEquals('close', $event->eventtype);
|
||||
$this->assertEquals(strtotime('2022-10-20'), $event->timestart);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,8 @@ Feature: Teachers can embed images into instructions and conclusion fields
|
||||
As a teacher
|
||||
I need to be able to embed images into the fields and they should display correctly
|
||||
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@editor_atto
|
||||
Scenario: Embedding the image into the instructions and conclusions fields
|
||||
Given the following "users" exist:
|
||||
| username | firstname | lastname | email |
|
||||
|
||||
@@ -139,8 +139,8 @@ class qbehaviour_deferredcbm_type_test extends qbehaviour_walkthrough_test_base
|
||||
}
|
||||
|
||||
public function test_calculate_bonus() {
|
||||
$this->assertEquals(0.05, $this->behaviourtype->calculate_bonus(1, 1/2));
|
||||
$this->assertEquals(-0.01, $this->behaviourtype->calculate_bonus(2, 9/10));
|
||||
$this->assertEquals(0, $this->behaviourtype->calculate_bonus(3, 1));
|
||||
$this->assertEqualsWithDelta(0.05, $this->behaviourtype->calculate_bonus(1, 1 / 2), question_testcase::GRADE_DELTA);
|
||||
$this->assertEqualsWithDelta(-0.01, $this->behaviourtype->calculate_bonus(2, 9 / 10), question_testcase::GRADE_DELTA);
|
||||
$this->assertEqualsWithDelta(0, $this->behaviourtype->calculate_bonus(3, 1), question_testcase::GRADE_DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,6 +479,11 @@ abstract class data_loading_method_test_base extends advanced_testcase {
|
||||
|
||||
abstract class question_testcase extends advanced_testcase {
|
||||
|
||||
/**
|
||||
* Tolerance accepted in some unit tests when float operations are involved.
|
||||
*/
|
||||
const GRADE_DELTA = 0.00000005;
|
||||
|
||||
public function assert($expectation, $compare, $notused = '') {
|
||||
|
||||
if (get_class($expectation) === 'question_pattern_expectation') {
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
|
||||
/**
|
||||
* Unit tests for the multianswer question definition class.
|
||||
*
|
||||
@@ -199,7 +198,7 @@ class qtype_multianswer_question_test extends advanced_testcase {
|
||||
2 => array('sub1_answer' => 'Owl', 'sub2_answer' => $right),
|
||||
);
|
||||
$finalgrade = $question->compute_final_grade($responses, 1);
|
||||
$this->assertEquals(1 / 3 * (1 - 0.2) + 2 / 3 * (1 - 2 * 0.2), $finalgrade);
|
||||
$this->assertEqualsWithDelta(1 / 3 * (1 - 0.2) + 2 / 3 * (1 - 2 * 0.2), $finalgrade, question_testcase::GRADE_DELTA);
|
||||
|
||||
// Get subquestion 1 right at 3rd try and subquestion 2 right at 2nd try.
|
||||
$responses = array(0 => array('sub1_answer' => 'Dog', 'sub2_answer' => $wrong),
|
||||
@@ -208,7 +207,7 @@ class qtype_multianswer_question_test extends advanced_testcase {
|
||||
3 => array('sub1_answer' => 'Owl', 'sub2_answer' => $right),
|
||||
);
|
||||
$finalgrade = $question->compute_final_grade($responses, 1);
|
||||
$this->assertEquals(1 / 3 * (1 - 2 * 0.2) + 2 / 3 * (1 - 0.2), $finalgrade);
|
||||
$this->assertEqualsWithDelta(1 / 3 * (1 - 2 * 0.2) + 2 / 3 * (1 - 0.2), $finalgrade, question_testcase::GRADE_DELTA);
|
||||
|
||||
// Get subquestion 1 right at 4th try and subquestion 2 right at 1st try.
|
||||
$responses = array(0 => array('sub1_answer' => 'Dog', 'sub2_answer' => $right),
|
||||
@@ -217,7 +216,7 @@ class qtype_multianswer_question_test extends advanced_testcase {
|
||||
3 => array('sub1_answer' => 'Owl', 'sub2_answer' => $right),
|
||||
);
|
||||
$finalgrade = $question->compute_final_grade($responses, 1);
|
||||
$this->assertEquals(1 / 3 * (1 - 3 * 0.2) + 2 / 3, $finalgrade);
|
||||
$this->assertEqualsWithDelta(1 / 3 * (1 - 3 * 0.2) + 2 / 3, $finalgrade, question_testcase::GRADE_DELTA);
|
||||
|
||||
// Get subquestion 1 right at 4th try and subquestion 2 right 3rd try.
|
||||
// Subquestion 2 was right at 1st try, but last change is at 3rd try.
|
||||
@@ -227,7 +226,7 @@ class qtype_multianswer_question_test extends advanced_testcase {
|
||||
3 => array('sub1_answer' => 'Owl', 'sub2_answer' => $right),
|
||||
);
|
||||
$finalgrade = $question->compute_final_grade($responses, 1);
|
||||
$this->assertEquals(1 / 3 * (1 - 3 * 0.2) + 2 / 3 * (1 - 2 * 0.2), $finalgrade);
|
||||
$this->assertEqualsWithDelta(1 / 3 * (1 - 3 * 0.2) + 2 / 3 * (1 - 2 * 0.2), $finalgrade, question_testcase::GRADE_DELTA);
|
||||
|
||||
// Incomplete responses. Subquestion 1 is right at 4th try and subquestion 2 at 3rd try.
|
||||
$responses = array(0 => array('sub1_answer' => 'Dog'),
|
||||
@@ -236,6 +235,6 @@ class qtype_multianswer_question_test extends advanced_testcase {
|
||||
3 => array('sub1_answer' => 'Owl', 'sub2_answer' => $right),
|
||||
);
|
||||
$finalgrade = $question->compute_final_grade($responses, 1);
|
||||
$this->assertEquals(1 / 3 * (1 - 3 * 0.2) + 2 / 3 * (1 - 2 * 0.2), $finalgrade);
|
||||
$this->assertEqualsWithDelta(1 / 3 * (1 - 3 * 0.2) + 2 / 3 * (1 - 2 * 0.2), $finalgrade, question_testcase::GRADE_DELTA);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ Feature: Updating a file in the content bank after using in a course
|
||||
| activity | name | intro | introformat | course | content | contentformat | idnumber |
|
||||
| page | PageName1 | PageDesc1 | 1 | C1 | H5Ptest | 1 | 1 |
|
||||
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@editor_atto
|
||||
Scenario: Referenced files is the default option and updates alias as well
|
||||
Given I am on the PageName1 "Page activity editing" page logged in as admin
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
@@ -56,6 +58,8 @@ Feature: Updating a file in the content bank after using in a course
|
||||
Then I should see "This is a new text"
|
||||
And I switch to the main frame
|
||||
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@editor_atto
|
||||
Scenario: Copied files should not be updated if the original is edited
|
||||
Given I am on the PageName1 "Page activity editing" page logged in as admin
|
||||
And I click on "Insert H5P" "button" in the "#fitem_id_page" "css_element"
|
||||
|
||||
@@ -128,6 +128,8 @@ Feature: Select content bank files using the content bank files repository
|
||||
Then I should see "1" elements in "Files" filemanager
|
||||
And I should see "filltheblanks.h5p" in the ".fp-content .fp-file" "css_element"
|
||||
|
||||
# This scenario has Atto-specific steps. See MDL-75913 for further details.
|
||||
@editor_atto
|
||||
Scenario: Non-editing teacher can not see the content bank repository
|
||||
Given I am on the Forum "Forum activity" page logged in as teacher2
|
||||
And I click on "Add a new discussion topic" "link"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
require_once(__DIR__ . '/../../../lib/behat/behat_base.php');
|
||||
|
||||
use Behat\Gherkin\Node\TableNode as TableNode;
|
||||
use Moodle\BehatExtension\Exception\SkippedException;
|
||||
|
||||
/**
|
||||
* Behat search-related step definitions.
|
||||
@@ -145,4 +146,15 @@ class behat_search extends behat_base {
|
||||
public function i_update_the_global_search_index() {
|
||||
\core_search\manager::instance()->index(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* This step looks to see if Solr is installed or skip the rest of the scenario otherwise
|
||||
*
|
||||
* @Given /^solr is installed/
|
||||
*/
|
||||
public function solr_is_installed() {
|
||||
if (!function_exists('solr_get_version')) {
|
||||
throw new SkippedException('Skipping this scenario because Solr is not installed.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,8 @@ Feature: Select users when searching for user-created content
|
||||
I need to be able to add users to the select list in the search form
|
||||
|
||||
Background:
|
||||
Given the following config values are set as admin:
|
||||
Given solr is installed
|
||||
And the following config values are set as admin:
|
||||
| enableglobalsearch | 1 |
|
||||
| searchengine | solr |
|
||||
And the following "courses" exist:
|
||||
|
||||
@@ -78,7 +78,8 @@ Feature: Use global search interface
|
||||
@javascript
|
||||
Scenario: Check that groups option in search form appears when intended
|
||||
# Switch to mocked Solr search because simpledb doesn't support groups.
|
||||
Given the following config values are set as admin:
|
||||
Given solr is installed
|
||||
And the following config values are set as admin:
|
||||
| searchengine | solr |
|
||||
And the following "groups" exist:
|
||||
| name | course | idnumber |
|
||||
|
||||
+2
-2
@@ -2,5 +2,5 @@ Contact: https://moodle.org/security/report/
|
||||
Acknowledgments: https://moodle.org/security/
|
||||
Preferred-Languages: en
|
||||
Canonical: https://moodle.org/.well-known/security.txt
|
||||
Policy: https://docs.moodle.org/dev/Moodle_security_procedures
|
||||
Expires: 2022-12-05T01:00:00.000Z
|
||||
Policy: https://moodledev.io/general/development/process/security
|
||||
Expires: 2023-05-29T01:00:00.000Z
|
||||
|
||||
+2
-2
@@ -29,9 +29,9 @@
|
||||
|
||||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$version = 2020061517.00; // 20200615 = branching date YYYYMMDD - do not modify!
|
||||
$version = 2020061518.00; // 20200615 = branching date YYYYMMDD - do not modify!
|
||||
// RR = release increments - 00 in DEV branches.
|
||||
// .XX = incremental changes.
|
||||
$release = '3.9.17 (Build: 20220912)'; // Human-friendly version name
|
||||
$release = '3.9.18 (Build: 20221114)'; // Human-friendly version name
|
||||
$branch = '39'; // This version's branch.
|
||||
$maturity = MATURITY_STABLE; // This version's maturity level.
|
||||
|
||||
Reference in New Issue
Block a user