From beb5ba99e715e7fc90658f3c128e3e00d6bf0e40 Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Thu, 20 Feb 2025 10:58:06 +0800 Subject: [PATCH] MDL-84595 tool_behat: Allow accessibility tests to run for a single Node --- lib/behat/classes/behat_session_trait.php | 15 +++++ lib/tests/behat/behat_accessibility.php | 74 +++++++++++++++++++---- lib/tests/behat/behat_general.php | 13 ++-- 3 files changed, 82 insertions(+), 20 deletions(-) diff --git a/lib/behat/classes/behat_session_trait.php b/lib/behat/classes/behat_session_trait.php index e002eaf7f6c..4069a4797f1 100644 --- a/lib/behat/classes/behat_session_trait.php +++ b/lib/behat/classes/behat_session_trait.php @@ -1676,4 +1676,19 @@ EOF; return $result ?: null; } + + /** + * Prepare an xpath for insertion into Selenium JavaScript. + * + * @param string $xpath + * @return string + */ + protected function prepare_xpath_for_javascript(string $xpath): string { + $newlines = [ + "\r\n", + "\r", + "\n", + ]; + return str_replace($newlines, ' ', $xpath); + } } diff --git a/lib/tests/behat/behat_accessibility.php b/lib/tests/behat/behat_accessibility.php index 2179a9f5978..8fe1a8c191d 100644 --- a/lib/tests/behat/behat_accessibility.php +++ b/lib/tests/behat/behat_accessibility.php @@ -14,16 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -/** - * Steps definitions to open and close action menus. - * - * @package core - * @category test - * @copyright 2020 Andrew Nicols - * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later - */ - -use Behat\Mink\Exception\{DriverException, ExpectationException}; +use Behat\Mink\Exception\DriverException; +use Behat\Mink\Exception\ExpectationException; +use Behat\Mink\Element\NodeElement; // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php. @@ -38,7 +31,6 @@ require_once(__DIR__ . '/../../behat/behat_base.php'); * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class behat_accessibility extends behat_base { - /** * Run the axe-core accessibility tests. * @@ -62,6 +54,37 @@ class behat_accessibility extends behat_base { ); } + /** + * Run the axe-core accessibility tests for a page region. + * + * There are standard tags to ensure WCAG 2.1 A, WCAG 2.1 AA, and Section 508 compliance. + * It is also possible to specify any desired optional tags. + * + * See {@link https://github.com/dequelabs/axe-core/blob/v4.10.0/doc/rule-descriptions.md} for the list of available tags + * + * @Then the :element :selector should meet accessibility standards + * @Then the :element :selector should meet accessibility standards with :extratags extra tests + * @Then the :element :selector should meet :standardtags accessibility standards + * @param string $element The element to run the tests on + * @param string $selector The selector to use to find the element + * @param string $standardtags Comma-separated list of standard tags to run + * @param string $extratags Comma-separated list of tags to run in addition to the standard tags + */ + public function run_axe_validation_for_tags_within_element( + string $element, + string $selector, + string $standardtags = '', + string $extratags = '', + ): void { + $node = $this->get_selected_node($selector, $element); + $this->run_axe_for_tags( + // Turn the comma-separated string into an array of trimmed values, filtering out empty values. + array_filter(array_map('trim', explode(',', $standardtags))), + array_filter(array_map('trim', explode(',', $extratags))), + $node, + ); + } + /** * Run the Axe tests. * @@ -70,8 +93,13 @@ class behat_accessibility extends behat_base { * * @param array $standardtags The list of standard tags to run * @param array $extratags The list of tags, in addition to the standard tags, to run + * @param null|NodeElement $containerelement The element to run the tests on */ - protected function run_axe_for_tags(array $standardtags = [], array $extratags = []): void { + protected function run_axe_for_tags( + array $standardtags = [], + array $extratags = [], + ?NodeElement $containerelement = null, + ): void { if (!behat_config_manager::get_behat_run_config_value('axe')) { return; } @@ -86,13 +114,33 @@ class behat_accessibility extends behat_base { $axeurl = (new \moodle_url('/lib/behat/axe/axe.min.js'))->out(false); $axeconfig = $this->get_axe_config_for_tags($standardtags, $extratags); + $xpath = ''; + if ($containerelement) { + $xpath = $this->prepare_xpath_for_javascript($containerelement->getXpath()); + } $runaxe = << { const runTests = () => { const axeTag = document.querySelector('script[data-purpose="axe"]'); axeTag.dataset.results = null; - axe.run({$axeconfig}) + const getRun = () => { + const xpath = "{$xpath}"; + if (xpath.length) { + const targetElements = []; + const results = document.evaluate(xpath, document, null, XPathResult.ANY_TYPE, null); + let targetElement = results.iterateNext(); + while (targetElement) { + targetElements.push(targetElement); + targetElement = results.iterateNext(); + } + return axe.run(targetElements, {$axeconfig}); + } + + return axe.run({$axeconfig}); + }; + + getRun() .then(results => { axeTag.dataset.results = JSON.stringify({ violations: results.violations, diff --git a/lib/tests/behat/behat_general.php b/lib/tests/behat/behat_general.php index 725b818bafe..0c42370512d 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -977,13 +977,12 @@ class behat_general extends behat_base { list($preselector, $prelocator) = $this->transform_selector($preselectortype, $preelement); list($postselector, $postlocator) = $this->transform_selector($postselectortype, $postelement); - $newlines = [ - "\r\n", - "\r", - "\n", - ]; - $prexpath = str_replace($newlines, ' ', $this->find($preselector, $prelocator, false, $containernode)->getXpath()); - $postxpath = str_replace($newlines, ' ', $this->find($postselector, $postlocator, false, $containernode)->getXpath()); + $prexpath = $this->prepare_xpath_for_javascript( + $this->find($preselector, $prelocator, false, $containernode)->getXpath() + ); + $postxpath = $this->prepare_xpath_for_javascript( + $this->find($postselector, $postlocator, false, $containernode)->getXpath() + ); if ($this->running_javascript()) { // The xpath to do this was running really slowly on certain Chrome versions so we are using