diff --git a/admin/tool/behat/amd/build/steps.min.js b/admin/tool/behat/amd/build/steps.min.js new file mode 100644 index 00000000000..8521d39c1f6 --- /dev/null +++ b/admin/tool/behat/amd/build/steps.min.js @@ -0,0 +1,12 @@ +define("tool_behat/steps",["exports","core/ajax","core/templates","core/pending"],(function(_exports,_ajax,_templates,_pending){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}function asyncGeneratorStep(gen,resolve,reject,_next,_throw,key,arg){try{var info=gen[key](arg),value=info.value}catch(error){return void reject(error)}info.done?resolve(value):Promise.resolve(value).then(_next,_throw)}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_templates=_interopRequireDefault(_templates),_pending=_interopRequireDefault(_pending); +/** + * Enhancements for the step definitions page. + * + * @module tool_behat/steps + * @copyright 2022 Catalyst IT EU + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +var getRequiredFieldsContent=function(entityData){var _entityData$required;return null!==(_entityData$required=entityData.required)&&void 0!==_entityData$required&&_entityData$required.length?_templates.default.renderForPromise("tool_behat/steprequiredfields",{fields:entityData.required}):Promise.resolve({html:"",js:""})};_exports.init=function(){var fn,_ref;document.addEventListener("change",(fn=regeneratorRuntime.mark((function _callee(e){var entityElement,stepElement,pendingPromise,entityData,_yield$getRequiredFie,html,js,stepRequiredFields;return regeneratorRuntime.wrap((function(_context){for(;;)switch(_context.prev=_context.next){case 0:if(entityElement=e.target.closest(".entities"),stepElement=e.target.closest(".stepcontent"),entityElement&&stepElement){_context.next=4;break}return _context.abrupt("return");case 4:return pendingPromise=new _pending.default("tool_behat/steps:change"),_context.next=7,entityType=e.target.value,_ajax.default.call([{methodname:"tool_behat_get_entity_generator",args:{entitytype:entityType}}])[0];case 7:return entityData=_context.sent,_context.next=10,getRequiredFieldsContent(entityData);case 10:if(_yield$getRequiredFie=_context.sent,html=_yield$getRequiredFie.html,js=_yield$getRequiredFie.js,!(stepRequiredFields=stepElement.querySelector(".steprequiredfields"))){_context.next=19;break}return _context.next=17,_templates.default.replaceNode(stepRequiredFields,html,js);case 17:_context.next=21;break;case 19:return _context.next=21,_templates.default.appendNodeContents(stepElement,html,js);case 21:pendingPromise.resolve();case 22:case"end":return _context.stop()}var entityType}),_callee)})),_ref=function(){var self=this,args=arguments;return new Promise((function(resolve,reject){var gen=fn.apply(self,args);function _next(value){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"next",value)}function _throw(err){asyncGeneratorStep(gen,resolve,reject,_next,_throw,"throw",err)}_next(void 0)}))},function(_x){return _ref.apply(this,arguments)}))}})); + +//# sourceMappingURL=steps.min.js.map \ No newline at end of file diff --git a/admin/tool/behat/amd/build/steps.min.js.map b/admin/tool/behat/amd/build/steps.min.js.map new file mode 100644 index 00000000000..6fb204a7630 --- /dev/null +++ b/admin/tool/behat/amd/build/steps.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"steps.min.js","sources":["../src/steps.js"],"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\nimport Ajax from 'core/ajax';\nimport Templates from 'core/templates';\nimport PendingJS from 'core/pending';\n\n/**\n * Enhancements for the step definitions page.\n *\n * @module tool_behat/steps\n * @copyright 2022 Catalyst IT EU\n * @author Mark Johnson \n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\n/**\n * Call the get_entity_generator web service function\n *\n * Takes the name of an entity generator and returns an object containing a list of the required fields.\n *\n * @param {String} entityType\n * @returns {Promise}\n */\nconst getGeneratorEntities = (entityType) => Ajax.call([{\n methodname: 'tool_behat_get_entity_generator',\n args: {entitytype: entityType}\n}])[0];\n\n/**\n * Render HTML for required fields\n *\n * Takes the entity data returned from getGeneratorEntities and renders the HTML to display the required fields.\n *\n * @param {String} entityData\n * @return {Promise}\n */\nconst getRequiredFieldsContent = (entityData) => {\n if (!entityData.required?.length) {\n return Promise.resolve({\n html: '',\n js: ''\n });\n }\n return Templates.renderForPromise('tool_behat/steprequiredfields', {fields: entityData.required});\n};\n\nexport const init = () => {\n // When an entity is selected in the \"the following exist\" step, fetch and display the required fields.\n document.addEventListener('change', async(e) => {\n const entityElement = e.target.closest('.entities');\n const stepElement = e.target.closest('.stepcontent');\n if (!entityElement || !stepElement) {\n return;\n }\n\n const pendingPromise = new PendingJS('tool_behat/steps:change');\n\n const entityData = await getGeneratorEntities(e.target.value);\n const {html, js} = await getRequiredFieldsContent(entityData);\n\n const stepRequiredFields = stepElement.querySelector('.steprequiredfields');\n if (stepRequiredFields) {\n await Templates.replaceNode(stepRequiredFields, html, js);\n } else {\n await Templates.appendNodeContents(stepElement, html, js);\n }\n pendingPromise.resolve();\n });\n};\n"],"names":["getRequiredFieldsContent","entityData","required","_entityData$required","length","Templates","renderForPromise","fields","Promise","resolve","html","js","document","addEventListener","e","entityElement","target","closest","stepElement","pendingPromise","PendingJS","entityType","value","Ajax","call","methodname","args","entitytype","stepRequiredFields","querySelector","replaceNode","appendNodeContents"],"mappings":";;;;;;;;;IAiDMA,yBAA2B,SAACC,yEACzBA,WAAWC,0CAAXC,qBAAqBC,OAMnBC,mBAAUC,iBAAiB,gCAAiC,CAACC,OAAQN,WAAWC,WAL5EM,QAAQC,QAAQ,CACnBC,KAAM,GACNC,GAAI,oBAMI,uBAEhBC,SAASC,iBAAiB,sCAAU,iBAAMC,oNAChCC,cAAgBD,EAAEE,OAAOC,QAAQ,aACjCC,YAAcJ,EAAEE,OAAOC,QAAQ,gBAChCF,eAAkBG,kFAIjBC,eAAiB,IAAIC,iBAAU,2CAhCfC,WAkCwBP,EAAEE,OAAOM,MAlClBC,cAAKC,KAAK,CAAC,CACpDC,WAAY,kCACZC,KAAM,CAACC,WAAYN,eACnB,iBA+BUpB,0CACmBD,yBAAyBC,2DAA3CS,2BAAAA,KAAMC,yBAAAA,KAEPiB,mBAAqBV,YAAYW,cAAc,wEAE3CxB,mBAAUyB,YAAYF,mBAAoBlB,KAAMC,mEAEhDN,mBAAU0B,mBAAmBb,YAAaR,KAAMC,YAE1DQ,eAAeV,mDA3CM,IAACY"} \ No newline at end of file diff --git a/admin/tool/behat/amd/src/steps.js b/admin/tool/behat/amd/src/steps.js new file mode 100644 index 00000000000..1334f1d26ee --- /dev/null +++ b/admin/tool/behat/amd/src/steps.js @@ -0,0 +1,82 @@ +// 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 . + +import Ajax from 'core/ajax'; +import Templates from 'core/templates'; +import PendingJS from 'core/pending'; + +/** + * Enhancements for the step definitions page. + * + * @module tool_behat/steps + * @copyright 2022 Catalyst IT EU + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +/** + * Call the get_entity_generator web service function + * + * Takes the name of an entity generator and returns an object containing a list of the required fields. + * + * @param {String} entityType + * @returns {Promise} + */ +const getGeneratorEntities = (entityType) => Ajax.call([{ + methodname: 'tool_behat_get_entity_generator', + args: {entitytype: entityType} +}])[0]; + +/** + * Render HTML for required fields + * + * Takes the entity data returned from getGeneratorEntities and renders the HTML to display the required fields. + * + * @param {String} entityData + * @return {Promise} + */ +const getRequiredFieldsContent = (entityData) => { + if (!entityData.required?.length) { + return Promise.resolve({ + html: '', + js: '' + }); + } + return Templates.renderForPromise('tool_behat/steprequiredfields', {fields: entityData.required}); +}; + +export const init = () => { + // When an entity is selected in the "the following exist" step, fetch and display the required fields. + document.addEventListener('change', async(e) => { + const entityElement = e.target.closest('.entities'); + const stepElement = e.target.closest('.stepcontent'); + if (!entityElement || !stepElement) { + return; + } + + const pendingPromise = new PendingJS('tool_behat/steps:change'); + + const entityData = await getGeneratorEntities(e.target.value); + const {html, js} = await getRequiredFieldsContent(entityData); + + const stepRequiredFields = stepElement.querySelector('.steprequiredfields'); + if (stepRequiredFields) { + await Templates.replaceNode(stepRequiredFields, html, js); + } else { + await Templates.appendNodeContents(stepElement, html, js); + } + pendingPromise.resolve(); + }); +}; diff --git a/admin/tool/behat/classes/external/get_entity_generator.php b/admin/tool/behat/classes/external/get_entity_generator.php new file mode 100644 index 00000000000..a3851f9c99e --- /dev/null +++ b/admin/tool/behat/classes/external/get_entity_generator.php @@ -0,0 +1,88 @@ +. + +/** + * Return data about an entity generator. + * + * @package tool_behat + * @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_behat\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/externallib.php'); +require_once($CFG->dirroot . '/vendor/autoload.php'); // Ensure we can load Behat and Facebook namespaces in behat libraries. +require_once($CFG->libdir . '/tests/behat/behat_data_generators.php'); + +/** + * External function for getting properties of entity generators. + */ +class get_entity_generator extends \external_api { + + /** + * Define parameters for external function. + * + * The parameter is either in the format 'entity' or 'component_name > entity'. There is no appropriate param type for a + * string like this containing angle brackets, so we will do PARAM_RAW. The value will be parsed by + * behat_data_generators::parse_entity_type, which validates the format of the parameter and throws an exception if it is not + * correct. + * + * @return \external_function_parameters + */ + public static function execute_parameters(): \external_function_parameters { + return new \external_function_parameters([ + 'entitytype' => new \external_value(PARAM_RAW, 'Entity type that can be created by a generator.'), + ]); + } + + /** + * Return a list of the required fields for a given entity type. + * + * @param string $entitytype + * @return array + */ + public static function execute(string $entitytype): array { + $params = self::validate_parameters(self::execute_parameters(), ['entitytype' => $entitytype]); + $context = \context_system::instance(); + self::validate_context($context); + require_capability('moodle/site:config', $context); + + $generators = new \behat_data_generators(); + $entity = $generators->get_entity($params['entitytype']); + return ['required' => $entity['required']]; + } + + /** + * Define return values. + * + * Return required fields + * + * @return \external_single_structure + */ + public static function execute_returns(): \external_single_structure { + return new \external_single_structure([ + 'required' => new \external_multiple_structure( + new \external_value(PARAM_TEXT, 'Required field'), + 'Required fields', + VALUE_OPTIONAL + ), + ]); + } +} diff --git a/admin/tool/behat/db/services.php b/admin/tool/behat/db/services.php new file mode 100644 index 00000000000..bddfab8d7dd --- /dev/null +++ b/admin/tool/behat/db/services.php @@ -0,0 +1,37 @@ +. + +/** + * Define web service functions for tool_behat + * + * @package tool_behat + * @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'tool_behat_get_entity_generator' => [ + 'classname' => 'tool_behat\\external\\get_entity_generator', + 'methodname' => 'execute', + 'description' => 'Get the generator details for an entity', + 'type' => 'read', + 'ajax' => true, + 'capabilities' => 'moodle/site:config' + ] +]; diff --git a/admin/tool/behat/index.php b/admin/tool/behat/index.php index 46d57066f6f..dda151ab88a 100644 --- a/admin/tool/behat/index.php +++ b/admin/tool/behat/index.php @@ -56,6 +56,7 @@ if ($components) { $form = new steps_definitions_form(null, array('components' => $componentswithsteps)); // Output contents. +$PAGE->requires->js_call_amd('tool_behat/steps', 'init'); $renderer = $PAGE->get_renderer('tool_behat'); echo $renderer->render_stepsdefinitions($steps, $form); diff --git a/admin/tool/behat/renderer.php b/admin/tool/behat/renderer.php index 864dc53fc3d..db85f0f063e 100644 --- a/admin/tool/behat/renderer.php +++ b/admin/tool/behat/renderer.php @@ -24,6 +24,8 @@ defined('MOODLE_INTERNAL') || die(); +require_once($CFG->libdir . '/behat/classes/behat_generator_base.php'); + /** * Renderer for behat tool web features * @@ -106,6 +108,32 @@ class tool_behat_renderer extends plugin_renderer_base { }, $stepsdefinitions ); + + $elementstrings = []; + $count = 1; + $stepsdefinitions = preg_replace_callback('/(the following ")ELEMENT\d?_STRING(" exist:)/', + function($matches) use (&$elementstrings, &$count) { + // Replace element type arguments with a user-friendly select. + if (empty($elementstrings)) { + $behatgenerators = new behat_data_generators(); + $componententities = $behatgenerators->get_all_entities(); + ksort($componententities); + $elementstrings = []; + foreach ($componententities as $component => $entities) { + asort($entities); + foreach ($entities as $entity) { + $string = ($component === 'core') ? $entity : $component . ' > ' . $entity; + $elementstrings[$string] = $string; + } + } + } + $select = html_writer::select($elementstrings, 'entities' . $count, '', ['' => 'choosedots'], + ['class' => 'entities']); + $count++; + return $matches[1] . $select . $matches[2]; + }, + $stepsdefinitions + ); } // Steps definitions. diff --git a/admin/tool/behat/styles.css b/admin/tool/behat/styles.css index c4fdb63ded9..bf96240c7bf 100644 --- a/admin/tool/behat/styles.css +++ b/admin/tool/behat/styles.css @@ -29,3 +29,9 @@ #page-admin-tool-behat-index .steps-definitions .stepregex { color: #060; } + +#page-admin-tool-behat-index .steprequiredfields { + font-weight: bold; + font-size: 1em; + margin-top: 1em; +} diff --git a/admin/tool/behat/templates/steprequiredfields.mustache b/admin/tool/behat/templates/steprequiredfields.mustache new file mode 100644 index 00000000000..b6704437a95 --- /dev/null +++ b/admin/tool/behat/templates/steprequiredfields.mustache @@ -0,0 +1,33 @@ +{{! + This file is part of Moodle - https://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 . +}} +{{! + @template tool_behat/steprequiredfields + + Display a Gherkin-style table row showing required columns for a step. + + Example context (json): + { + "fields": [ + "user", + "role", + "context" + ] + } +}} +
+    |{{#fields}} {{{.}}} |{{/fields}}
+
diff --git a/admin/tool/behat/tests/behat/list_steps.feature b/admin/tool/behat/tests/behat/list_steps.feature index 8e59cb8f50d..c8584788c14 100644 --- a/admin/tool/behat/tests/behat/list_steps.feature +++ b/admin/tool/behat/tests/behat/list_steps.feature @@ -13,6 +13,10 @@ Feature: List the system steps definitions Scenario: Accessing the list Then I should see "Step definitions" And I should not see "There aren't steps definitions matching this filter" + And I should not see "the following \"ELEMENT_STRING\" exist:" + And "entities1" "select" should exist + And "users" "option" should exist in the "entities1" "select" + And "mod_assign > submissions" "option" should exist in the "entities1" "select" @javascript Scenario: Filtering by type @@ -34,3 +38,11 @@ Feature: List the system steps definitions Then I should not see "There aren't steps definitions matching this filter" And I should see "Checks the provided element and selector type exists in the current page." And I should see "Checks that an element and selector type exists in another element and selector type on the current page." + + @javascript + Scenario: Get required fields + Given I set the field "entities1" to "users" + And I should see "| username |" + When I set the field "entities1" to "mod_quiz > user overrides" + Then I should not see "| username |" + And I should see "| quiz | user |" diff --git a/admin/tool/behat/tests/external/get_entity_generator_test.php b/admin/tool/behat/tests/external/get_entity_generator_test.php new file mode 100644 index 00000000000..716442ff6e4 --- /dev/null +++ b/admin/tool/behat/tests/external/get_entity_generator_test.php @@ -0,0 +1,104 @@ +. + +/** + * Unit tests for get_entity_generator web service + * + * @package tool_behat + * @copyright 2022 onwards Catalyst IT EU {@link https://catalyst-eu.net} + * @author Mark Johnson + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace tool_behat\external; + +/** + * Tests for get_entity_generator web service + * + * @covers \tool_behat\external\get_entity_generator + */ +class get_entity_generator_test extends \advanced_testcase { + + /** + * Log in as admin + * + * @return void + */ + public function setUp(): void { + $this->resetAfterTest(); + $this->setAdminUser(); + } + + /** + * Get the generator for a core entity. + * + * @return void + */ + public function test_execute_core_entity() : void { + $generator = get_entity_generator::execute('users'); + $this->assertEquals(['required' => ['username']], $generator); + } + + /** + * Get the generator for the plugin entity. + * + * @return void + */ + public function test_execute_plugin_entity() : void { + $generator = get_entity_generator::execute('mod_book > chapters'); + $this->assertEquals(['required' => ['book', 'title', 'content']], $generator); + } + + /** + * Get the generator for an entity with no required fields. + * + * @return void + */ + public function test_execute_no_requried() : void { + $generator = get_entity_generator::execute('mod_forum > posts'); + $this->assertEquals(['required' => []], $generator); + } + + /** + * Attempt to get the generator for a core entity that does not exist. + * + * @return void + */ + public function test_execute_invalid_entity() : void { + $this->expectException('coding_exception'); + get_entity_generator::execute('foo'); + } + + /** + * Attempt to get a generator form a plugin that does not exist. + * + * @return void + */ + public function test_execute_invalid_plugin() : void { + $this->expectException('coding_exception'); + get_entity_generator::execute('foo > bar'); + } + + /** + * Attempt to get a generator for an entity that does not exist, from a plugin that does. + * + * @return void + */ + public function test_execute_invalid_plugin_entity() : void { + $this->expectException('coding_exception'); + get_entity_generator::execute('mod_book > bar'); + } +} diff --git a/admin/tool/behat/version.php b/admin/tool/behat/version.php index daee2639387..dd7fe5655d2 100644 --- a/admin/tool/behat/version.php +++ b/admin/tool/behat/version.php @@ -24,6 +24,6 @@ defined('MOODLE_INTERNAL') || die(); -$plugin->version = 2020061500; // The current plugin version (Date: YYYYMMDDXX) +$plugin->version = 2020061501; // The current plugin version (Date: YYYYMMDDXX). $plugin->requires = 2020060900; // Requires this Moodle version $plugin->component = 'tool_behat'; // Full name of the plugin (used for diagnostics) diff --git a/lib/behat/classes/behat_generator_base.php b/lib/behat/classes/behat_generator_base.php index ffeb22b6d99..9b9661f7895 100644 --- a/lib/behat/classes/behat_generator_base.php +++ b/lib/behat/classes/behat_generator_base.php @@ -165,6 +165,15 @@ abstract class behat_generator_base { */ protected abstract function get_creatable_entities(): array; + /** + * Get the list of available generators for this class. + * + * @return array + */ + final public function get_available_generators(): array { + return $this->get_creatable_entities(); + } + /** * Do the work to generate an entity. * diff --git a/lib/tests/behat/behat_data_generators.php b/lib/tests/behat/behat_data_generators.php index eb39c0353e6..9c44aad388f 100644 --- a/lib/tests/behat/behat_data_generators.php +++ b/lib/tests/behat/behat_data_generators.php @@ -170,4 +170,51 @@ class behat_data_generators extends behat_base { $instance = new $componentclass($component); return $instance; } + + /** + * Get all entities that can be created in all components using the_following_entities_exist() + * + * @return array + * @throws coding_exception + */ + public function get_all_entities(): array { + global $CFG; + // Ensure the generator class is loaded. + require_once($CFG->libdir . '/behat/classes/behat_generator_base.php'); + $componenttypes = core_component::get_component_list(); + $coregenerator = $this->get_instance_for_component('core'); + $pluginswithentities = ['core' => array_keys($coregenerator->get_available_generators())]; + foreach ($componenttypes as $components) { + foreach ($components as $component => $componentdir) { + try { + $plugingenerator = $this->get_instance_for_component($component); + $entities = array_keys($plugingenerator->get_available_generators()); + if (!empty($entities)) { + $pluginswithentities[$component] = $entities; + } + } catch (Exception $e) { + // The component has no generator, skip it. + continue; + } + } + } + return $pluginswithentities; + } + + /** + * Get the required fields for a specific creatable entity. + * + * @param string $entitytype + * @return mixed + * @throws coding_exception + */ + public function get_entity(string $entitytype): array { + [$component, $entity] = $this->parse_entity_type($entitytype); + $generator = $this->get_instance_for_component($component); + $entities = $generator->get_available_generators(); + if (!array_key_exists($entity, $entities)) { + throw new coding_exception('No generator for ' . $entity . ' in component ' . $component); + } + return $entities[$entity]; + } }