From 275dc3f8d48579fc3515803b01b82234769b3d79 Mon Sep 17 00:00:00 2001 From: Noel De Martin Date: Wed, 4 May 2022 17:09:29 +0200 Subject: [PATCH] MDL-74655 competency: Implement behat generators --- admin/tool/lp/tests/behat/behat_tool_lp.php | 63 +++++ .../lp/tests/behat/view_competencies.feature | 187 +++++++++++++ .../behat_core_competency_generator.php | 261 ++++++++++++++++++ competency/tests/generator/lib.php | 7 +- .../tests/behat/behat_report_competency.php | 79 ++++++ 5 files changed, 595 insertions(+), 2 deletions(-) create mode 100644 admin/tool/lp/tests/behat/view_competencies.feature create mode 100644 competency/tests/generator/behat_core_competency_generator.php create mode 100644 report/competency/tests/behat/behat_report_competency.php diff --git a/admin/tool/lp/tests/behat/behat_tool_lp.php b/admin/tool/lp/tests/behat/behat_tool_lp.php index acd0b19d2a328..91d43738095d5 100644 --- a/admin/tool/lp/tests/behat/behat_tool_lp.php +++ b/admin/tool/lp/tests/behat/behat_tool_lp.php @@ -63,4 +63,67 @@ public function select_of_the_competency_tree($competencyname) { $this->execute('behat_general::i_click_on', [$xpathtarget, 'xpath_element']); } + /** + * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. + * + * Recognised page names are: + * | pagetype | name meaning | description | + * | Course competencies | Course name | The course competencies page | + * + * @param string $page identifies which type of page this is, e.g. 'Course competencies'. + * @param string $identifier identifies the particular page, e.g. 'C1'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_instance_url(string $page, string $identifier): moodle_url { + switch (strtolower($page)) { + case 'course competencies': + $courseid = $this->get_course_id($identifier); + return new moodle_url('/admin/tool/lp/coursecompetencies.php', [ + 'courseid' => $courseid, + ]); + default: + throw new Exception("Unrecognised page type '{$page}'"); + } + } + + /** + * Return a list of the exact named selectors for the component. + * + * @return behat_component_named_selector[] + */ + public static function get_exact_named_selectors(): array { + return [ + new behat_component_named_selector('competency', [ + "//*[@data-region='coursecompetencies']//table[contains(@class,'managecompetencies')]". + "//tr[contains(., //a[@title='View details'][contains(., %locator%)])]", + ]), + new behat_component_named_selector('learning plan', [ + "//*[@data-region='plan-competencies']//table[contains(@class,'managecompetencies')]". + "//tr[@data-node='user-competency'][contains(., //a[@data-usercompetency='true'][contains(., %locator%)])]", + ]), + new behat_component_named_selector('competency description', [ + "//td/p[contains(., %locator%)]", + ]), + new behat_component_named_selector('competency grade', [ + "//span[contains(concat(' ', normalize-space(@class), ' '), ' badge ')][contains(., %locator%)]", + ]), + new behat_component_named_selector('learning plan rating', [ + "//td[position()=2][contains(., %locator%)]", + ]), + new behat_component_named_selector('learning plan proficiency', [ + "//td[position()=3][contains(., %locator%)]", + ]), + new behat_component_named_selector('competency page proficiency', [ + "//dt[contains(., 'Proficient')]/following-sibling::dd[1][contains(., %locator%)]", + ]), + new behat_component_named_selector('competency page rating', [ + "//dt[contains(., 'Rating')]/following-sibling::dd[1][contains(., %locator%)]", + ]), + new behat_component_named_selector('competency page related competency', [ + "//*[@data-region='relatedcompetencies']//a[contains(., %locator%)]", + ]), + ]; + } + } diff --git a/admin/tool/lp/tests/behat/view_competencies.feature b/admin/tool/lp/tests/behat/view_competencies.feature new file mode 100644 index 0000000000000..addbff173206f --- /dev/null +++ b/admin/tool/lp/tests/behat/view_competencies.feature @@ -0,0 +1,187 @@ +@tool @tool_lp @javascript +Feature: View competencies + In order to access competency information + As a user + I need to view user competencies + + Background: + Given the following "users" exist: + | username | firstname | lastname | + | student1 | Student | first | + | teacher1 | Teacher | first | + And the following "system role assigns" exist: + | user | role | contextlevel | + | teacher1 | editingteacher | System | + And the following "permission overrides" exist: + | capability | permission | role | contextlevel | reference | + | moodle/competency:planview | Allow | editingteacher | System | | + And the following "courses" exist: + | fullname | shortname | + | Course 1 | C1 | + And the following "course enrolments" exist: + | user | course | role | + | student1 | C1 | student | + | teacher1 | C1 | editingteacher | + And the following "scales" exist: + | name | scale | + | Test Scale | Bad, Good, Great | + And the following "core_competency > frameworks" exist: + | shortname | idnumber | scale | + | Cookery | cookery | Test Scale | + | Literacy | literacy | Test Scale | + And the following "core_competency > competencies" exist: + | shortname | idnumber | description | competencyframework | + | Salads | salads | Salads are important | cookery | + | Desserts | desserts | Desserts are important | cookery | + | Cakes | cakes | Cakes are important | cookery | + | Reading | reading | Reading is important | literacy | + | Writing | writing | Writing is important | literacy | + And the following "core_competency > related_competencies" exist: + | competency | relatedcompetency | + | desserts | cakes | + And the following "core_competency > plans" exist: + | name | description | competencies | user | + | Cookery | Cookery is important | salads, desserts, cakes | student1 | + | Literacy | Literacy is important | reading, writing | student1 | + And the following "core_competency > course_competencies" exist: + | course | competency | + | C1 | salads | + | C1 | desserts | + | C1 | cakes | + | C1 | reading | + | C1 | writing | + And the following "core_competency > user_competency" exist: + | competency | user | grade | + | salads | student1 | Good | + | desserts | student1 | Great | + | cakes | student1 | Great | + And the following "core_competency > user_competency_courses" exist: + | course | competency | user | grade | + | C1 | salads | student1 | Good | + | C1 | desserts | student1 | Great | + | C1 | cakes | student1 | Great | + + Scenario: Student view + # Course competencies + Given I am on the "C1" "tool_lp > course competencies" page logged in as "student1" + Then I should see "You are proficient in 3 out of 5 competencies in this course" + + And "Salads are important" "tool_lp > competency description" should exist in the "Salads" "tool_lp > competency" + And "Good" "tool_lp > competency grade" should exist in the "Salads" "tool_lp > competency" + + And "Desserts are important" "tool_lp > competency description" should exist in the "Desserts" "tool_lp > competency" + And "Great" "tool_lp > competency grade" should exist in the "Desserts" "tool_lp > competency" + + And "Cakes are important" "tool_lp > competency description" should exist in the "Cakes" "tool_lp > competency" + And "Great" "tool_lp > competency grade" should exist in the "Cakes" "tool_lp > competency" + + And "Reading is important" "tool_lp > competency description" should exist in the "Reading" "tool_lp > competency" + And "Good" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency" + And "Great" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency" + And "Bad" "tool_lp > competency grade" should not exist in the "Reading" "tool_lp > competency" + + And "Writing is important" "tool_lp > competency description" should exist in the "Writing" "tool_lp > competency" + And "Good" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency" + And "Great" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency" + And "Bad" "tool_lp > competency grade" should not exist in the "Writing" "tool_lp > competency" + + # Course competencies details + And I click on "Desserts" "link" in the "Desserts" "tool_lp > competency" + And I should see "Desserts are important" + And "Yes" "tool_lp > competency page proficiency" should exist + And "Great" "tool_lp > competency page rating" should exist + + # Course competencies summary + And I click on "Cakes" "link" + And I should see "Cakes are important" + + # Learning plans + And I click on "Close" "button" in the "Cakes" "dialogue" + And I follow "Profile" in the user menu + And I click on "Learning plans" "link" + And I should see "Cookery" + And I should see "Literacy" + + # Learning plans details + And I click on "Cookery" "link" + And I should see "Cookery is important" + And I should see "3 out of 3 competencies are proficient" + + And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan" + And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan" + + And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan" + And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan" + + And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan" + And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan" + + And I should not see "Literacy" + And I should not see "Reading" + And I should not see "Writing" + + # Learning plans competency details + And I click on "Desserts" "link" in the "Desserts" "tool_lp > learning plan" + And I should see "Desserts are important" + And "Yes" "tool_lp > competency page proficiency" should exist + And "Great" "tool_lp > competency page rating" should exist + + # Learning plans competency summary + And I click on "Cakes cakes" "link" + And I should see "Cakes are important" + + Scenario: Teacher view + # Participant competencies + Given I am on the "C1" "report_competency > breakdown" page logged in as "teacher1" + Then I should see "Student first" + And "Good" "report_competency > breakdown rating" should exist in the "Salads" "report_competency > breakdown" + And "Great" "report_competency > breakdown rating" should exist in the "Desserts" "report_competency > breakdown" + And "Great" "report_competency > breakdown rating" should exist in the "Cakes" "report_competency > breakdown" + And "Not rated" "report_competency > breakdown rating" should exist in the "Reading" "report_competency > breakdown" + And "Not rated" "report_competency > breakdown rating" should exist in the "Writing" "report_competency > breakdown" + + # Participant competencies details + And I click on "Great" "report_competency > breakdown rating" in the "Desserts" "report_competency > breakdown" + And "Yes" "tool_lp > competency page proficiency" should exist + And "Great" "tool_lp > competency page rating" should exist + + # Participant competencies summary + And I click on "Cakes" "tool_lp > competency page related competency" + And I should see "Cakes are important" + + # Participant learning plans + And I click on "Close" "button" in the "Cakes" "dialogue" + And I click on "Close" "button" in the "User competency summary" "dialogue" + And I navigate to course participants + And I click on "Student first" "link" + And I click on "Learning plans" "link" + And I should see "Cookery" + And I should see "Literacy" + + # Participant learning plans details + And I click on "Cookery" "link" + And I should see "Cookery is important" + And I should see "3 out of 3 competencies are proficient" + + And "Good" "tool_lp > learning plan rating" should exist in the "Salads" "tool_lp > learning plan" + And "Yes" "tool_lp > learning plan proficiency" should exist in the "Salads" "tool_lp > learning plan" + + And "Great" "tool_lp > learning plan rating" should exist in the "Desserts" "tool_lp > learning plan" + And "Yes" "tool_lp > learning plan proficiency" should exist in the "Desserts" "tool_lp > learning plan" + + And "Great" "tool_lp > learning plan rating" should exist in the "Cakes" "tool_lp > learning plan" + And "Yes" "tool_lp > learning plan proficiency" should exist in the "Cakes" "tool_lp > learning plan" + + And I should not see "Literacy" + And I should not see "Reading" + And I should not see "Writing" + + # Learning plans competency details + And I click on "Desserts" "link" + And I should see "Desserts are important" + And "Yes" "tool_lp > competency page proficiency" should exist + And "Great" "tool_lp > competency page rating" should exist + + # Learning plans competency summary + And I click on "Cakes" "tool_lp > competency page related competency" + And I should see "Cakes are important" diff --git a/competency/tests/generator/behat_core_competency_generator.php b/competency/tests/generator/behat_core_competency_generator.php new file mode 100644 index 0000000000000..687a35abad738 --- /dev/null +++ b/competency/tests/generator/behat_core_competency_generator.php @@ -0,0 +1,261 @@ +. + +use core_competency\competency; +use core_competency\competency_framework; +use core_competency\plan; + +/** + * Behat data generator for core_competency. + * + * @package core_competency + * @category test + * @copyright 2022 Noel De Martin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_core_competency_generator extends behat_generator_base { + + /** + * Get a list of the entities that Behat can create using the generator step. + * + * @return array + */ + protected function get_creatable_entities(): array { + return [ + 'competencies' => [ + 'singular' => 'competency', + 'datagenerator' => 'competency', + 'required' => ['shortname', 'competencyframework'], + 'switchids' => ['competencyframework' => 'competencyframeworkid'], + ], + 'course_competencies' => [ + 'singular' => 'course_competency', + 'datagenerator' => 'course_competency', + 'required' => ['course', 'competency'], + 'switchids' => ['course' => 'courseid', 'competency' => 'competencyid'], + ], + 'frameworks' => [ + 'singular' => 'framework', + 'datagenerator' => 'framework', + 'required' => ['shortname'], + 'switchids' => ['scale' => 'scaleid'], + ], + 'plans' => [ + 'singular' => 'plan', + 'datagenerator' => 'plan', + 'required' => ['name'], + 'switchids' => ['user' => 'userid'], + ], + 'related_competencies' => [ + 'singular' => 'related_competency', + 'datagenerator' => 'related_competency', + 'required' => ['competency', 'relatedcompetency'], + 'switchids' => ['competency' => 'competencyid', 'relatedcompetency' => 'relatedcompetencyid'], + ], + 'user_competency' => [ + 'singular' => 'user_competency', + 'datagenerator' => 'user_competency', + 'required' => ['competency', 'user'], + 'switchids' => ['competency' => 'competencyid', 'user' => 'userid'], + ], + 'user_competency_courses' => [ + 'singular' => 'user_competency_course', + 'datagenerator' => 'user_competency_course', + 'required' => ['course', 'competency', 'user'], + 'switchids' => ['course' => 'courseid', 'competency' => 'competencyid', 'user' => 'userid'], + ], + 'user_competency_plans' => [ + 'singular' => 'user_competency_plan', + 'datagenerator' => 'user_competency_plan', + 'required' => ['plan', 'competency', 'user'], + 'switchids' => ['plan' => 'planid', 'competency' => 'competencyid', 'user' => 'userid'], + ], + ]; + } + + /** + * Get the competecy framework id using an idnumber. + * + * @param string $idnumber + * @return int The competecy framework id + */ + protected function get_competencyframework_id(string $idnumber): int { + global $DB; + + if (!$id = $DB->get_field('competency_framework', 'id', ['idnumber' => $idnumber])) { + throw new Exception('The specified competency framework with idnumber "' . $idnumber . '" could not be found.'); + } + + return $id; + } + + /** + * Get the competecy id using an idnumber. + * + * @param string $idnumber + * @return int The competecy id + */ + protected function get_competency_id(string $idnumber): int { + global $DB; + + if (!$id = $DB->get_field('competency', 'id', ['idnumber' => $idnumber])) { + throw new Exception('The specified competency with idnumber "' . $idnumber . '" could not be found.'); + } + + return $id; + } + + /** + * Get the learning plan id using a name. + * + * @param string $name + * @return int The learning plan id + */ + protected function get_plan_id(string $name): int { + global $DB; + + if (!$id = $DB->get_field('competency_plan', 'id', ['name' => $name])) { + throw new Exception('The specified learning plan with name "' . $name . '" could not be found.'); + } + + return $id; + } + + /** + * Get the related competecy id using an idnumber. + * + * @param string $idnumber + * @return int The related competecy id + */ + protected function get_relatedcompetency_id(string $idnumber): int { + return $this->get_competency_id($idnumber); + } + + /** + * Add a plan. + * + * @param array $data Plan data. + */ + public function process_plan(array $data): void { + $generator = $this->get_data_generator(); + $competencyids = $data['competencyids'] ?? []; + + unset($data['competencyids']); + + $plan = $generator->create_plan($data); + + foreach ($competencyids as $competencyid) { + $generator->create_plan_competency([ + 'planid' => $plan->get('id'), + 'competencyid' => $competencyid, + ]); + } + } + + /** + * Preprocess user competency data. + * + * @param array $data Raw data. + * @return array Processed data. + */ + protected function preprocess_user_competency(array $data): array { + $this->prepare_grading($data); + + return $data; + } + + /** + * Preprocess user course competency data. + * + * @param array $data Raw data. + * @return array Processed data. + */ + protected function preprocess_user_competency_course(array $data): array { + $this->prepare_grading($data); + + return $data; + } + + /** + * Preprocess user learning plan competency data. + * + * @param array $data Raw data. + * @return array Processed data. + */ + protected function preprocess_user_competency_plan(array $data): array { + $this->prepare_grading($data); + + return $data; + } + + /** + * Preprocess plan data. + * + * @param array $data Raw data. + * @return array Processed data. + */ + protected function preprocess_plan(array $data): array { + if (isset($data['competencies'])) { + $competencies = array_map('trim', str_getcsv($data['competencies'])); + $data['competencyids'] = array_map([$this, 'get_competency_id'], $competencies); + + unset($data['competencies']); + } + + global $USER; + + return $data + [ + 'userid' => $USER->id, + 'status' => plan::STATUS_ACTIVE, + ]; + } + + /** + * Prepare grading attributes for record data. + * + * @param array $data Record data. + */ + protected function prepare_grading(array &$data): void { + if (!isset($data['grade'])) { + return; + } + + global $DB; + + $competency = competency::get_record(['id' => $data['competencyid']]); + $competencyframework = competency_framework::get_record(['id' => $competency->get('competencyframeworkid')]); + $scale = $DB->get_field('scale', 'scale', ['id' => $competencyframework->get('scaleid')]); + $grades = array_map('trim', explode(',', $scale)); + $grade = array_search($data['grade'], $grades); + + if ($grade === false) { + throw new Exception('The grade "'.$data['grade'].'" was not found in the "'. + $competencyframework->get('shortname').'" competency framework.'); + } + + $data['proficiency'] = true; + $data['grade'] = $grade + 1; + } + + /** + * Get the module data generator. + * + * @return core_competency_generator Competency data generator. + */ + protected function get_data_generator(): core_competency_generator { + return $this->componentdatagenerator; + } +} diff --git a/competency/tests/generator/lib.php b/competency/tests/generator/lib.php index 15a4473c6ca44..e73957cf417d6 100644 --- a/competency/tests/generator/lib.php +++ b/competency/tests/generator/lib.php @@ -136,7 +136,11 @@ public function create_competency($record = null) { * @return competency_framework */ public function create_framework($record = null) { - $generator = phpunit_util::get_data_generator(); + if (defined('BEHAT_TEST') && BEHAT_TEST) { + $generator = behat_util::get_data_generator(); + } else { + $generator = phpunit_util::get_data_generator(); + } $this->frameworkcount++; $i = $this->frameworkcount; $record = (object) $record; @@ -584,4 +588,3 @@ protected function make_default_scale_configuration($scaleid) { } } - diff --git a/report/competency/tests/behat/behat_report_competency.php b/report/competency/tests/behat/behat_report_competency.php new file mode 100644 index 0000000000000..6d5df5350a8ed --- /dev/null +++ b/report/competency/tests/behat/behat_report_competency.php @@ -0,0 +1,79 @@ +. + +/** + * Behat competency report definitions. + * + * @package report_competency + * @category test + * @copyright 2022 Noel De Martin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +require_once(__DIR__ . '/../../../../lib/behat/behat_base.php'); + +/** + * Competency report definitions. + * + * @package report_competency + * @category test + * @copyright 2022 Noel De Martin + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ +class behat_report_competency extends behat_base { + + /** + * Convert page names to URLs for steps like 'When I am on the "[identifier]" "[page type]" page'. + * + * Recognised page names are: + * | pagetype | name meaning | description | + * | Breakdown | Course name | The course competencies breakdown page | + * + * @param string $page identifies which type of page this is, e.g. 'Breakdown'. + * @param string $identifier identifies the particular page, e.g. 'C1'. + * @return moodle_url the corresponding URL. + * @throws Exception with a meaningful error message if the specified page cannot be found. + */ + protected function resolve_page_instance_url(string $page, string $identifier): moodle_url { + switch (strtolower($page)) { + case 'breakdown': + $courseid = $this->get_course_id($identifier); + return new moodle_url('/report/competency/index.php', [ + 'id' => $courseid, + ]); + default: + throw new Exception("Unrecognised page type '{$page}'"); + } + } + + /** + * Return a list of the exact named selectors for the component. + * + * @return behat_component_named_selector[] + */ + public static function get_exact_named_selectors(): array { + return [ + new behat_component_named_selector('breakdown', [ + "//*[@data-region='competency-breakdown-report']//table". + "//tr[contains(., //a[@data-action='competency-dialogue'][contains(., %locator%)])]", + ]), + new behat_component_named_selector('breakdown rating', [ + "//td[position()=2][contains(., //a[@title='User competency summary'][contains(., %locator%)])]", + ]), + ]; + } + +}