diff --git a/lib/amd/build/icon_system_fontawesome.min.js b/lib/amd/build/icon_system_fontawesome.min.js
index b4461657089..47c4244203d 100644
--- a/lib/amd/build/icon_system_fontawesome.min.js
+++ b/lib/amd/build/icon_system_fontawesome.min.js
@@ -1,2 +1,2 @@
-define ("core/icon_system_fontawesome",["core/icon_system","jquery","core/ajax","core/mustache","core/localstorage","core/url"],function(a,b,c,d,e,f){var g=null,h=null,i=function(){a.apply(this,arguments)};i.prototype=Object.create(a.prototype);i.prototype.init=function(){if(g){return b.when(this)}var a=e.get("core/iconmap-fontawesome");if(a){a=JSON.parse(a)}if(a){g=a;return b.when(this)}if(null===h){h=c.call([{methodname:"core_output_load_fontawesome_icon_map",args:[]}],!0,!1,!1,0,M.cfg.themerev)[0]}return h.then(function(a){g={};b.each(a,function(a,b){g[b.component+"/"+b.pix]=b.to});e.set("core/iconmap-fontawesome",JSON.stringify(g));return this}.bind(this))};i.prototype.renderIcon=function(a,b,c,e){var h=g[b+"/"+a],i=!1;if("undefined"==typeof h){var j=f.imageUrl(a,b);i={attributes:[{name:"src",value:j},{name:"alt",value:c},{name:"title",value:c}]}}var k={key:h,title:c,alt:c,unmappedIcon:i};if("undefined"==typeof c||""===c){k["aria-hidden"]=!0}var l=d.render(e,k);return l.trim()};i.prototype.getTemplateName=function(){return"core/pix_icon_fontawesome"};return i});
+define ("core/icon_system_fontawesome",["core/icon_system","jquery","core/ajax","core/mustache","core/localstorage","core/url"],function(a,b,c,d,e,f){var g=null,h=null,i=function(){a.apply(this,arguments)};i.prototype=Object.create(a.prototype);i.prototype.init=function(){if(g){return b.when(this)}var a=e.get("core/iconmap-fontawesome");if(a){a=JSON.parse(a)}if(a){g=a;return b.when(this)}if(null===h){h=c.call([{methodname:"core_output_load_fontawesome_icon_system_map",args:{themename:M.cfg.theme}}],!0,!1,!1,0,M.cfg.themerev)[0]}return h.then(function(a){g={};b.each(a,function(a,b){g[b.component+"/"+b.pix]=b.to});e.set("core/iconmap-fontawesome",JSON.stringify(g));return this}.bind(this))};i.prototype.renderIcon=function(a,b,c,e){var h=g[b+"/"+a],i=!1;if("undefined"==typeof h){var j=f.imageUrl(a,b);i={attributes:[{name:"src",value:j},{name:"alt",value:c},{name:"title",value:c}]}}var k={key:h,title:c,alt:c,unmappedIcon:i};if("undefined"==typeof c||""===c){k["aria-hidden"]=!0}var l=d.render(e,k);return l.trim()};i.prototype.getTemplateName=function(){return"core/pix_icon_fontawesome"};return i});
//# sourceMappingURL=icon_system_fontawesome.min.js.map
diff --git a/lib/amd/build/icon_system_fontawesome.min.js.map b/lib/amd/build/icon_system_fontawesome.min.js.map
index 179a881ed17..1bb4e1d699e 100644
--- a/lib/amd/build/icon_system_fontawesome.min.js.map
+++ b/lib/amd/build/icon_system_fontawesome.min.js.map
@@ -1 +1 @@
-{"version":3,"sources":["../src/icon_system_fontawesome.js"],"names":["define","IconSystem","$","Ajax","Mustache","LocalStorage","Url","staticMap","fetchMap","IconSystemFontawesome","apply","arguments","prototype","Object","create","init","when","map","get","JSON","parse","call","methodname","args","M","cfg","themerev","then","each","index","value","component","pix","to","set","stringify","bind","renderIcon","key","title","template","mappedIcon","unmappedIcon","url","imageUrl","attributes","name","context","alt","result","render","trim","getTemplateName"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Competency rule points module.\n *\n * @package core\n * @copyright 2017 Damyon Wiese\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['core/icon_system', 'jquery', 'core/ajax', 'core/mustache', 'core/localstorage', 'core/url'],\n function(IconSystem, $, Ajax, Mustache, LocalStorage, Url) {\n\n var staticMap = null;\n var fetchMap = null;\n\n /**\n * IconSystemFontawesome\n */\n var IconSystemFontawesome = function() {\n IconSystem.apply(this, arguments);\n };\n IconSystemFontawesome.prototype = Object.create(IconSystem.prototype);\n\n /**\n * Prefetch resources so later calls to renderIcon can be resolved synchronously.\n *\n * @method init\n * @return {Promise}\n */\n IconSystemFontawesome.prototype.init = function() {\n if (staticMap) {\n return $.when(this);\n }\n\n var map = LocalStorage.get('core/iconmap-fontawesome');\n if (map) {\n map = JSON.parse(map);\n }\n\n if (map) {\n staticMap = map;\n return $.when(this);\n }\n\n if (fetchMap === null) {\n fetchMap = Ajax.call([{\n methodname: 'core_output_load_fontawesome_icon_map',\n args: []\n }], true, false, false, 0, M.cfg.themerev)[0];\n }\n\n return fetchMap.then(function(map) {\n staticMap = {};\n $.each(map, function(index, value) {\n staticMap[value.component + '/' + value.pix] = value.to;\n });\n LocalStorage.set('core/iconmap-fontawesome', JSON.stringify(staticMap));\n return this;\n }.bind(this));\n };\n\n /**\n * Render an icon.\n *\n * @param {String} key\n * @param {String} component\n * @param {String} title\n * @param {String} template\n * @return {String}\n * @method renderIcon\n */\n IconSystemFontawesome.prototype.renderIcon = function(key, component, title, template) {\n var mappedIcon = staticMap[component + '/' + key];\n var unmappedIcon = false;\n if (typeof mappedIcon === \"undefined\") {\n var url = Url.imageUrl(key, component);\n\n unmappedIcon = {\n attributes: [\n {name: 'src', value: url},\n {name: 'alt', value: title},\n {name: 'title', value: title}\n ]\n };\n }\n\n var context = {\n key: mappedIcon,\n title: title,\n alt: title,\n unmappedIcon: unmappedIcon\n };\n\n if (typeof title === \"undefined\" || title === '') {\n context['aria-hidden'] = true;\n }\n\n var result = Mustache.render(template, context);\n return result.trim();\n };\n\n /**\n * Get the name of the template to pre-cache for this icon system.\n *\n * @return {String}\n * @method getTemplateName\n */\n IconSystemFontawesome.prototype.getTemplateName = function() {\n return 'core/pix_icon_fontawesome';\n };\n\n return /** @alias module:core/icon_system_fontawesome */ IconSystemFontawesome;\n\n});\n"],"file":"icon_system_fontawesome.min.js"}
\ No newline at end of file
+{"version":3,"sources":["../src/icon_system_fontawesome.js"],"names":["define","IconSystem","$","Ajax","Mustache","LocalStorage","Url","staticMap","fetchMap","IconSystemFontawesome","apply","arguments","prototype","Object","create","init","when","map","get","JSON","parse","call","methodname","args","themename","M","cfg","theme","themerev","then","each","index","value","component","pix","to","set","stringify","bind","renderIcon","key","title","template","mappedIcon","unmappedIcon","url","imageUrl","attributes","name","context","alt","result","render","trim","getTemplateName"],"mappings":"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","sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Competency rule points module.\n *\n * @package core\n * @copyright 2017 Damyon Wiese\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\ndefine(['core/icon_system', 'jquery', 'core/ajax', 'core/mustache', 'core/localstorage', 'core/url'],\n function(IconSystem, $, Ajax, Mustache, LocalStorage, Url) {\n\n var staticMap = null;\n var fetchMap = null;\n\n /**\n * IconSystemFontawesome\n */\n var IconSystemFontawesome = function() {\n IconSystem.apply(this, arguments);\n };\n IconSystemFontawesome.prototype = Object.create(IconSystem.prototype);\n\n /**\n * Prefetch resources so later calls to renderIcon can be resolved synchronously.\n *\n * @method init\n * @return {Promise}\n */\n IconSystemFontawesome.prototype.init = function() {\n if (staticMap) {\n return $.when(this);\n }\n\n var map = LocalStorage.get('core/iconmap-fontawesome');\n if (map) {\n map = JSON.parse(map);\n }\n\n if (map) {\n staticMap = map;\n return $.when(this);\n }\n\n if (fetchMap === null) {\n fetchMap = Ajax.call([{\n methodname: 'core_output_load_fontawesome_icon_system_map',\n args: {\n themename: M.cfg.theme,\n },\n }], true, false, false, 0, M.cfg.themerev)[0];\n }\n\n return fetchMap.then(function(map) {\n staticMap = {};\n $.each(map, function(index, value) {\n staticMap[value.component + '/' + value.pix] = value.to;\n });\n LocalStorage.set('core/iconmap-fontawesome', JSON.stringify(staticMap));\n return this;\n }.bind(this));\n };\n\n /**\n * Render an icon.\n *\n * @param {String} key\n * @param {String} component\n * @param {String} title\n * @param {String} template\n * @return {String}\n * @method renderIcon\n */\n IconSystemFontawesome.prototype.renderIcon = function(key, component, title, template) {\n var mappedIcon = staticMap[component + '/' + key];\n var unmappedIcon = false;\n if (typeof mappedIcon === \"undefined\") {\n var url = Url.imageUrl(key, component);\n\n unmappedIcon = {\n attributes: [\n {name: 'src', value: url},\n {name: 'alt', value: title},\n {name: 'title', value: title}\n ]\n };\n }\n\n var context = {\n key: mappedIcon,\n title: title,\n alt: title,\n unmappedIcon: unmappedIcon\n };\n\n if (typeof title === \"undefined\" || title === '') {\n context['aria-hidden'] = true;\n }\n\n var result = Mustache.render(template, context);\n return result.trim();\n };\n\n /**\n * Get the name of the template to pre-cache for this icon system.\n *\n * @return {String}\n * @method getTemplateName\n */\n IconSystemFontawesome.prototype.getTemplateName = function() {\n return 'core/pix_icon_fontawesome';\n };\n\n return /** @alias module:core/icon_system_fontawesome */ IconSystemFontawesome;\n\n});\n"],"file":"icon_system_fontawesome.min.js"}
\ No newline at end of file
diff --git a/lib/amd/src/icon_system_fontawesome.js b/lib/amd/src/icon_system_fontawesome.js
index b2f8ceeb133..25c749c7428 100644
--- a/lib/amd/src/icon_system_fontawesome.js
+++ b/lib/amd/src/icon_system_fontawesome.js
@@ -58,8 +58,10 @@ define(['core/icon_system', 'jquery', 'core/ajax', 'core/mustache', 'core/locals
if (fetchMap === null) {
fetchMap = Ajax.call([{
- methodname: 'core_output_load_fontawesome_icon_map',
- args: []
+ methodname: 'core_output_load_fontawesome_icon_system_map',
+ args: {
+ themename: M.cfg.theme,
+ },
}], true, false, false, 0, M.cfg.themerev)[0];
}
diff --git a/lib/classes/external/output/icon_system/load_fontawesome_map.php b/lib/classes/external/output/icon_system/load_fontawesome_map.php
new file mode 100644
index 00000000000..aafaa4dd7e0
--- /dev/null
+++ b/lib/classes/external/output/icon_system/load_fontawesome_map.php
@@ -0,0 +1,98 @@
+.
+
+/**
+ * A web service to load the mapping of moodle pix names to fontawesome icon names.
+ *
+ * @package core
+ * @category external
+ * @copyright 2020 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\external\output\icon_system;
+
+use external_api;
+use external_function_parameters;
+use external_multiple_structure;
+use external_single_structure;
+use external_value;
+use core\output\icon_system_fontawesome;
+use theme_config;
+
+/**
+ * Web service to load font awesome icon maps.
+ *
+ * @package core
+ * @category external
+ * @copyright 2020 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+class load_fontawesome_map extends external_api {
+
+ /**
+ * Description of the parameters suitable for the `execute` function.
+ *
+ * @return external_function_parameters
+ */
+ public static function execute_parameters() {
+ return new external_function_parameters([
+ 'themename' => new external_value(PARAM_ALPHANUMEXT, 'The theme to fetch the map for'),
+ ]);
+ }
+
+ /**
+ * Return a mapping of icon names to icons.
+ *
+ * @param string $themename The theme to fetch icons for
+ * @return array the mapping
+ */
+ public static function execute(string $themename) {
+ [
+ 'themename' => $themename,
+ ] = self::validate_parameters(self::execute_parameters(), [
+ 'themename' => $themename,
+ ]);
+
+ $theme = theme_config::load($themename);
+ $instance = icon_system_fontawesome::instance($theme->get_icon_system());
+
+ $result = [];
+ foreach ($instance->get_icon_name_map() as $from => $to) {
+ [$component, $pix] = explode(':', $from);
+ $result[] = [
+ 'component' => $component,
+ 'pix' => $pix,
+ 'to' => $to,
+ ];
+ }
+
+ return $result;
+ }
+
+ /**
+ * Description of the return value for the `execute` function.
+ *
+ * @return external_description
+ */
+ public static function execute_returns() {
+ return new external_multiple_structure(new external_single_structure([
+ 'component' => new external_value(PARAM_COMPONENT, 'The component for the icon.'),
+ 'pix' => new external_value(PARAM_RAW, 'Value to map the icon from.'),
+ 'to' => new external_value(PARAM_RAW, 'Value to map the icon to.'),
+ ]));
+ }
+}
diff --git a/lib/classes/output/external.php b/lib/classes/output/external.php
index a8f2d6a5f18..82b7b567573 100644
--- a/lib/classes/output/external.php
+++ b/lib/classes/output/external.php
@@ -34,6 +34,7 @@ use core_component;
use moodle_exception;
use context_system;
use theme_config;
+use core\external\output\icon_system\load_fontawesome_map;
/**
* This class contains a list of webservice functions related to output.
@@ -205,21 +206,9 @@ class external extends external_api {
* @return array the mapping
*/
public static function load_fontawesome_icon_map() {
- $instance = icon_system::instance(icon_system::FONTAWESOME);
+ global $PAGE;
- $map = $instance->get_icon_name_map();
-
- $result = [];
-
- foreach ($map as $from => $to) {
- list($component, $pix) = explode(':', $from);
- $one = [];
- $one['component'] = $component;
- $one['pix'] = $pix;
- $one['to'] = $to;
- $result[] = $one;
- }
- return $result;
+ return load_fontawesome_map::execute($PAGE->theme->name);
}
/**
@@ -228,13 +217,6 @@ class external extends external_api {
* @return external_description
*/
public static function load_fontawesome_icon_map_returns() {
- return new external_multiple_structure(new external_single_structure(
- array(
- 'component' => new external_value(PARAM_COMPONENT, 'The component for the icon.'),
- 'pix' => new external_value(PARAM_RAW, 'Value to map the icon from.'),
- 'to' => new external_value(PARAM_RAW, 'Value to map the icon to.')
- )
- ));
+ return load_fontawesome_map::execute_returns();
}
}
-
diff --git a/lib/classes/output/icon_system.php b/lib/classes/output/icon_system.php
index 14e1075c2d7..eff29912cd9 100644
--- a/lib/classes/output/icon_system.php
+++ b/lib/classes/output/icon_system.php
@@ -79,15 +79,24 @@ abstract class icon_system {
global $PAGE;
if (empty(self::$instance)) {
- $icontype = $PAGE->theme->get_icon_system();
- self::$instance = new $icontype();
+ $iconsystem = $PAGE->theme->get_icon_system();
+ self::$instance = new $iconsystem();
}
- // If $type is specified we need to make sure that the theme icon system supports this type,
- // if not, we will return a generic new instance of the $type.
- if ($type === null || is_a(self::$instance, $type)) {
+ if ($type === null) {
+ // No type specified. Return the icon system for the current theme.
+ return self::$instance;
+ }
+
+ if (!static::is_valid_system($type)) {
+ throw new \coding_exception("Invalid icon system requested '{$type}'");
+ }
+
+ if (is_a(self::$instance, $type) && is_a($type, get_class(self::$instance), true)) {
+ // The requested type is an exact match for the current icon system.
return self::$instance;
} else {
+ // Return the requested icon system.
return new $type();
}
}
@@ -99,7 +108,7 @@ abstract class icon_system {
* @return boolean
*/
public final static function is_valid_system($system) {
- return class_exists($system) && is_subclass_of($system, self::class);
+ return class_exists($system) && is_a($system, static::class, true);
}
/**
@@ -153,4 +162,3 @@ abstract class icon_system {
self::$instance = null;
}
}
-
diff --git a/lib/classes/output/icon_system_standard.php b/lib/classes/output/icon_system_standard.php
index 8119a2d6812..c9a2f77e18d 100644
--- a/lib/classes/output/icon_system_standard.php
+++ b/lib/classes/output/icon_system_standard.php
@@ -38,7 +38,7 @@ defined('MOODLE_INTERNAL') || die();
* @copyright 2016 Damyon Wiese
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
-class icon_system_standard {
+class icon_system_standard extends icon_system {
public function render_pix_icon(renderer_base $output, pix_icon $icon) {
$data = $icon->export_for_template($output);
diff --git a/lib/db/services.php b/lib/db/services.php
index 57038ef70c4..0be2bf7c5e9 100644
--- a/lib/db/services.php
+++ b/lib/db/services.php
@@ -1646,6 +1646,14 @@ $functions = array(
'loginrequired' => false,
'ajax' => true,
),
+ 'core_output_load_fontawesome_icon_system_map' => array(
+ 'classname' => 'core\external\output\icon_system\load_fontawesome_map',
+ 'methodname' => 'execute',
+ 'description' => 'Load the mapping of moodle pix names to fontawesome icon names',
+ 'type' => 'read',
+ 'loginrequired' => false,
+ 'ajax' => true,
+ ),
// Question related functions.
'core_question_update_flag' => array(
'classname' => 'core_question_external',
diff --git a/lib/tests/external/output/icon_system/load_fontawesome_map_test.php b/lib/tests/external/output/icon_system/load_fontawesome_map_test.php
new file mode 100644
index 00000000000..752ac6c385d
--- /dev/null
+++ b/lib/tests/external/output/icon_system/load_fontawesome_map_test.php
@@ -0,0 +1,98 @@
+.
+
+/**
+ * External functions test for record_feedback_action.
+ *
+ * @package core
+ * @category test
+ * @copyright 2020 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\external\output\icon_system;
+
+use externallib_advanced_testcase;
+
+defined('MOODLE_INTERNAL') || die();
+
+global $CFG;
+
+require_once($CFG->dirroot . '/webservice/tests/helpers.php');
+
+/**
+ * Class record_userfeedback_action_testcase
+ *
+ * @copyright 2020 Andrew Nicols
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ * @coversDefaultClass core\external\output\icon_system\load_fontawesome_map
+ */
+class load_fontawesome_map_testcase extends externallib_advanced_testcase {
+
+ /**
+ * Perform setup before these tests are run.
+ */
+ public static function setUpBeforeClass(): void {
+ global $CFG;
+
+ // In normal operation the external_api classes will have been loaded by the caller.
+ // The load_fontawesome_map class should not need to supplement our lack of autoloading of these classes.
+ require_once($CFG->libdir . '/externallib.php');
+ }
+
+ /**
+ * Ensure that a valid theme which uses fontawesome returns a map.
+ *
+ * @covers ::execute_parameters
+ * @covers ::execute
+ * @covers ::execute_returns
+ * @dataProvider valid_fontawesome_theme_provider
+ * @param string $themename
+ */
+ public function test_execute(string $themename): void {
+ $result = load_fontawesome_map::execute($themename);
+ $this->assertIsArray($result);
+
+ foreach ($result as $value) {
+ $this->assertArrayHasKey('component', $value);
+ $this->assertArrayHasKey('pix', $value);
+ $this->assertArrayHasKey('to', $value);
+ }
+ }
+
+ /**
+ * Ensure that an invalid theme cannot be loaded.
+ */
+ public function test_execute_invalid_themename(): void {
+ $result = load_fontawesome_map::execute('invalidtheme');
+ $this->assertDebuggingCalled(
+ 'This page should be using theme invalidtheme which cannot be initialised. Falling back to the site theme boost'
+ );
+ $this->assertIsArray($result);
+ }
+
+ /**
+ * Data provider for valid themes to use with the execute function.
+ *
+ * @return array
+ */
+ public function valid_fontawesome_theme_provider(): array {
+ return [
+ 'Boost theme' => ['boost'],
+ 'Classic theme (extends boost)' => ['classic'],
+ ];
+ }
+}
diff --git a/lib/tests/output/icon_system_test.php b/lib/tests/output/icon_system_test.php
new file mode 100644
index 00000000000..d018ed6b1c7
--- /dev/null
+++ b/lib/tests/output/icon_system_test.php
@@ -0,0 +1,248 @@
+.
+
+/**
+ * Unit tests for lib/outputcomponents.php.
+ *
+ * @package core
+ * @category test
+ * @copyright 2011 David Mudrak
+ * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
+ */
+
+namespace core\output;
+
+use advanced_testcase;
+use coding_exception;
+
+/**
+ * Unit tests for the `icon_system` class.
+ *
+ * @coversDefaultClass core\output\icon_system
+ */
+class icon_system_test extends advanced_testcase {
+ /**
+ * Check whether the supplied classes are valid icon subsystems of the supplied one.
+ *
+ * @covers ::is_valid_system
+ * @dataProvider is_valid_subsystem_provider
+ * @param string $parent The class to call ::is_valid_system() on
+ * @param string $system The class to request
+ * @param bool $expected Whether the supplied relationship is valid
+ */
+ public function test_is_valid_subsystem(string $parent, string $system, bool $expected): void {
+ $this->assertEquals($expected, $parent::is_valid_system($system));
+ }
+
+ /**
+ * Ensure that the ::instance() function throws an appropriate Exception when an inappropriate relationship is
+ * specified.
+ *
+ * @covers ::instance
+ * @dataProvider invalid_instance_provider
+ * @param string $parent The class to call ::instance() on
+ * @param string $system The class to request
+ */
+ public function test_invalid_instance(string $parent, string $system): void {
+ $this->expectException(coding_exception::class);
+ $this->expectExceptionMessage("Invalid icon system requested '{$system}'");
+
+ $parent::instance($system);
+ }
+
+ /**
+ * Ensure that the ::instance() function returns an instance of the supplied system for a valid icon system
+ * relationship.
+ *
+ * @covers ::instance
+ * @dataProvider valid_instance_provider
+ * @param string $parent The class to call ::instance() on
+ * @param string $system The class to request
+ */
+ public function test_valid_instance(string $parent, string $system): void {
+ $instance = $parent::instance($system);
+ $this->assertInstanceOf($parent, $instance);
+ $this->assertInstanceOf($system, $instance);
+ }
+
+ /**
+ * Ensure that subsequent calls without arguments to ::instance() return the exact same instance.
+ *
+ * @covers ::instance
+ */
+ public function test_instance_singleton(): void {
+ $singleton = icon_system::instance();
+
+ // Calling instance() again returns the same singleton.
+ $this->assertSame($singleton, icon_system::instance());
+ }
+
+ /**
+ * Ensure thaat subsequent calls with an argument to ::instance() return the exact same instance.
+ *
+ * @covers ::instance
+ */
+ public function test_instance_singleton_named_default(): void {
+ global $PAGE;
+ $singleton = icon_system::instance();
+
+ $defaultsystem = $PAGE->theme->get_icon_system();
+ $this->assertSame($singleton, icon_system::instance($defaultsystem));
+ }
+
+ /**
+ * Ensure that ::instance() returns an instance of the correct icon system when requested on the core icon_system
+ * class.
+ *
+ * @covers ::instance
+ * @dataProvider valid_instance_provider
+ * @param string $parent The class to call ::instance() on
+ * @param string $child The class to request
+ */
+ public function test_instance_singleton_named(string $parent, string $child): void {
+ $iconsystem = icon_system::instance($child);
+ $this->assertInstanceOf($child, $iconsystem);
+ }
+
+ /**
+ * Ensure that ::instance() returns an instance of the correct icon system when called on a named parent class.
+ *
+ * @covers ::instance
+ * @dataProvider valid_instance_provider
+ * @param string $parent The class to call ::instance() on
+ * @param string $child The class to request
+ */
+ public function test_instance_singleton_named_child(string $parent, string $child): void {
+ $iconsystem = $parent::instance($child);
+ $this->assertInstanceOf($parent, $iconsystem);
+ $this->assertInstanceOf($child, $iconsystem);
+ }
+
+ /**
+ * Ensure that the ::reset_caches() function resets the stored instance such that ::instance() returns a new
+ * instance in subsequent calls.
+ *
+ * @covers ::instance
+ * @covers ::reset_caches
+ */
+ public function test_instance_singleton_reset(): void {
+ $singleton = icon_system::instance();
+
+ // Reset the cache.
+ icon_system::reset_caches();
+
+ // Calling instance() again returns a new singleton.
+ $newsingleton = icon_system::instance();
+ $this->assertNotSame($singleton, $newsingleton);
+
+ // Calling it again gets the new singleton.
+ $this->assertSame($newsingleton, icon_system::instance());
+ }
+
+ /**
+ * Returns data for data providers containing:
+ * - parent icon system
+ * - child icon system
+ * - whether it is a valid child
+ *
+ * @return array
+ */
+ public function icon_system_provider(): array {
+ return [
+ 'icon_system => icon_system_standard' => [
+ icon_system::class,
+ icon_system_standard::class,
+ true,
+ ],
+ 'icon_system => icon_system_fontawesome' => [
+ icon_system::class,
+ icon_system_fontawesome::class,
+ true,
+ ],
+ 'icon_system => \theme_classic\output\icon_system_fontawesome' => [
+ icon_system::class,
+ \theme_classic\output\icon_system_fontawesome::class,
+ true,
+ ],
+ 'icon_system => notification' => [
+ icon_system::class,
+ notification::class,
+ false,
+ ],
+
+ 'icon_system_standard => icon_system_standard' => [
+ icon_system_standard::class,
+ icon_system_standard::class,
+ true,
+ ],
+ 'icon_system_standard => icon_system_fontawesome' => [
+ icon_system_standard::class,
+ icon_system_fontawesome::class,
+ false,
+ ],
+ 'icon_system_standard => \theme_classic\output\icon_system_fontawesome' => [
+ icon_system_standard::class,
+ \theme_classic\output\icon_system_fontawesome::class,
+ false,
+ ],
+ 'icon_system_fontawesome => icon_system_standard' => [
+ icon_system_fontawesome::class,
+ icon_system_standard::class,
+ false,
+ ],
+ ];
+ }
+
+ /**
+ * Data provider for tests of `is_valid`.
+ *
+ * @return array
+ */
+ public function is_valid_subsystem_provider(): array {
+ return $this->icon_system_provider();
+ }
+
+ /**
+ * Data provider for tests of `instance` containing only invalid tests.
+ *
+ * @return array
+ */
+ public function invalid_instance_provider(): array {
+ return array_filter(
+ $this->icon_system_provider(),
+ function($data) {
+ return !$data[2];
+ },
+ ARRAY_FILTER_USE_BOTH
+ );
+ }
+
+ /**
+ * Data provider for tests of `instance` containing only valid tests.
+ *
+ * @return array
+ */
+ public function valid_instance_provider(): array {
+ return array_filter(
+ $this->icon_system_provider(),
+ function($data) {
+ return $data[2];
+ },
+ ARRAY_FILTER_USE_BOTH
+ );
+ }
+
+}
diff --git a/lib/upgrade.txt b/lib/upgrade.txt
index 523e395d28f..790def00b78 100644
--- a/lib/upgrade.txt
+++ b/lib/upgrade.txt
@@ -1,6 +1,10 @@
This files describes API changes in core libraries and APIs,
information provided here is intended especially for developers.
+=== 3.9.2 ===
+* A new web service `core_output_load_fontawesome_icon_system_map` has been created.
+ This replaces the existing `core_output_load_fontawesome_icon_map` web service, which will be deprecated in Moodle 3.10.
+
=== 3.9.1 ===
* The `$CFG->behat_retart_browser_after` configuration setting has been removed.
The browser session is now restarted between all tests.