diff --git a/lib/behat/classes/behat_session_trait.php b/lib/behat/classes/behat_session_trait.php index 830b7b46454..9374443c0e7 100644 --- a/lib/behat/classes/behat_session_trait.php +++ b/lib/behat/classes/behat_session_trait.php @@ -1744,4 +1744,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 4f53755cb76..42e241b49cb 100644 --- a/lib/tests/behat/behat_accessibility.php +++ b/lib/tests/behat/behat_accessibility.php @@ -14,7 +14,9 @@ // You should have received a copy of the GNU General Public License // along with Moodle. If not, see . -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. @@ -29,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. * @@ -52,6 +53,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. * @@ -60,8 +92,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; } @@ -76,13 +113,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 f7e5e929d3d..303b01daa30 100644 --- a/lib/tests/behat/behat_general.php +++ b/lib/tests/behat/behat_general.php @@ -997,13 +997,12 @@ class behat_general extends behat_base { [$preselector, $prelocator] = $this->transform_selector($preselectortype, $preelement); [$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