Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.
<?php
// 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 <http://www.gnu.org/licenses/>.

/**
 * Steps definitions to open and close action menus.
 *
 * @package    core
 * @category   test
 * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 */

use Behat\Mink\Exception\{DriverException, ExpectationException};

// NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.

require_once(__DIR__ . '/../../behat/behat_base.php');

/**
 * Steps definitions to assist with accessibility testing.
 *
 * @package    core
 * @category   test
 * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
 * @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.
     *
     * 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.
     *
     * The list of available tags can be found at
     * https://github.com/dequelabs/axe-core/blob/v3.5.5/doc/rule-descriptions.md.
     *
     * @Then the page should meet accessibility standards
     * @Then the page should meet accessibility standards with :extratags extra tests
     * @Then the page should meet :standardtags accessibility standards
     * @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(string $standardtags = '', string $extratags = ''): void {
        $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)))
        );
    }

    /**
     * Run the Axe tests.
     *
     * See https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md for details of the supported
     * tags.
     *
     * @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
     */
    protected function run_axe_for_tags(array $standardtags = [], array $extratags = []): void {
        if (!behat_config_manager::get_behat_run_config_value('axe')) {
            return;
        }

        if (!$this->has_tag('accessibility')) {
            throw new DriverException(
                'Accessibility tests using Axe must have the @accessibility tag on either the scenario or feature.'
            );
        }

        $this->require_javascript();

        $axeurl = (new \moodle_url('/lib/behat/axe/axe.min.js'))->out(false);
        $axeconfig = $this->get_axe_config_for_tags($standardtags, $extratags);
        $runaxe = <<<EOF
(axeurl => {
    const runTests = () => {
        const axeTag = document.querySelector('script[data-purpose="axe"]');
        axeTag.dataset.results = null;

        axe.run({$axeconfig})
        .then(results => {
            axeTag.dataset.results = JSON.stringify({
                violations: results.violations,
                exception: null,
            });
        })
        .catch(exception => {
            axeTag.dataset.results = JSON.stringify({
                violations: [],
                exception: exception,
            });
        });
    };

    if (document.querySelector('script[data-purpose="axe"]')) {
        runTests();
    } else {
        // Inject the axe content.
        const axeTag = document.createElement('script');
        axeTag.src = axeurl,
        axeTag.dataset.purpose = 'axe';

        axeTag.onload = () => runTests();
        document.head.append(axeTag);
    }
})('{$axeurl}');
EOF;

        $this->execute_script($runaxe);

        $getresults = <<<EOF
return (() => {
    const axeTag = document.querySelector('script[data-purpose="axe"]');
    return axeTag.dataset.results;
})()
EOF;

        for ($i = 0; $i < self::get_extended_timeout() * 10; $i++) {
< $results = json_decode($this->evaluate_script($getresults));
> $results = json_decode($this->evaluate_script($getresults) ?? '');
if ($results) { break; } } if (empty($results)) { throw new \Exception('No data'); } if ($results->exception !== null) {
< throw new ExpectationException($results->exception, $this->session);
> throw new ExpectationException($results->exception, $this->getSession());
} $violations = $results->violations; if (!count($violations)) { return; } $violationdata = "Accessibility violations found:\n"; foreach ($violations as $violation) { $nodedata = ''; foreach ($violation->nodes as $node) { $failedchecks = []; foreach (array_merge($node->any, $node->all, $node->none) as $check) { $failedchecks[$check->id] = $check->message; } $nodedata .= sprintf( " - %s:\n %s\n\n", implode(', ', $failedchecks), implode("\n ", $node->target) ); } $violationdata .= sprintf( " %.03d violations of '%s' (severity: %s)\n%s\n", count($violation->nodes), $violation->description, $violation->impact, $nodedata ); } throw new ExpectationException($violationdata, $this->getSession()); } /** * Get the configuration to use with Axe. * * See https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md for details of the rules. * * @param array|null $standardtags The list of standard tags to run * @param array|null $extratags The list of tags, in addition to the standard tags, to run * @return string The JSON-encoded configuration. */ protected function get_axe_config_for_tags(?array $standardtags = null, ?array $extratags = null): string { if (empty($standardtags)) { $standardtags = [ // Meet WCAG 2.1 A requirements. 'wcag2a', // Meet WCAG 2.1 AA requirements. 'wcag2aa', // Meet Section 508 requirements. // See https://www.epa.gov/accessibility/what-section-508 for detail. 'section508', // Ensure that ARIA attributes are correctly defined. 'cat.aria', // Requiremetns for sensory and visual cues. // These largely related to viewport scale and zoom functionality. 'cat.sensory-and-visual-cues', // Meet WCAG 1.3.4 requirements for orientation. // See https://www.w3.org/WAI/WCAG21/Understanding/orientation.html for detail. 'wcag134', ]; } return json_encode([ 'runOnly' => [ 'type' > 'tag', 'values' => array_merge($standardtags, $extratags), ], ]); } }