Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 and 403]

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * Steps definitions to open and close action menus.
  19   *
  20   * @package    core
  21   * @category   test
  22   * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
  23   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24   */
  25  
  26  use Behat\Mink\Exception\{DriverException, ExpectationException};
  27  
  28  // NOTE: no MOODLE_INTERNAL test here, this file may be required by behat before including /config.php.
  29  
  30  require_once (__DIR__ . '/../../behat/behat_base.php');
  31  
  32  /**
  33   * Steps definitions to assist with accessibility testing.
  34   *
  35   * @package    core
  36   * @category   test
  37   * @copyright  2020 Andrew Nicols <andrew@nicols.co.uk>
  38   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  39   */
  40  class behat_accessibility extends behat_base {
  41  
  42      /**
  43       * Run the axe-core accessibility tests.
  44       *
  45       * There are standard tags to ensure WCAG 2.1 A, WCAG 2.1 AA, and Section 508 compliance.
  46       * It is also possible to specify any desired optional tags.
  47       *
  48       * The list of available tags can be found at
  49       * https://github.com/dequelabs/axe-core/blob/v3.5.5/doc/rule-descriptions.md.
  50       *
  51       * @Then the page should meet accessibility standards
  52       * @Then the page should meet accessibility standards with :extratags extra tests
  53       * @Then the page should meet :standardtags accessibility standards
  54       * @param   string $standardtags Comma-separated list of standard tags to run
  55       * @param   string $extratags Comma-separated list of tags to run in addition to the standard tags
  56       */
  57      public function run_axe_validation_for_tags(string $standardtags = '', string $extratags = ''): void {
  58          $this->run_axe_for_tags(
  59              // Turn the comma-separated string into an array of trimmed values, filtering out empty values.
  60              array_filter(array_map('trim', explode(',', $standardtags))),
  61              array_filter(array_map('trim', explode(',', $extratags)))
  62          );
  63      }
  64  
  65      /**
  66       * Run the Axe tests.
  67       *
  68       * See https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md for details of the supported
  69       * tags.
  70       *
  71       * @param   array $standardtags The list of standard tags to run
  72       * @param   array $extratags The list of tags, in addition to the standard tags, to run
  73       */
  74      protected function run_axe_for_tags(array $standardtags = [], array $extratags = []): void {
  75          if (!behat_config_manager::get_behat_run_config_value('axe')) {
  76              return;
  77          }
  78  
  79          if (!$this->has_tag('accessibility')) {
  80              throw new DriverException(
  81                  'Accessibility tests using Axe must have the @accessibility tag on either the scenario or feature.'
  82              );
  83          }
  84  
  85          $this->require_javascript();
  86  
  87          $axeurl = (new \moodle_url('/lib/behat/axe/axe.min.js'))->out(false);
  88          $axeconfig = $this->get_axe_config_for_tags($standardtags, $extratags);
  89          $runaxe = <<<EOF
  90  (axeurl => {
  91      const runTests = () => {
  92          const axeTag = document.querySelector('script[data-purpose="axe"]');
  93          axeTag.dataset.results = null;
  94  
  95          axe.run({$axeconfig})
  96          .then(results => {
  97              axeTag.dataset.results = JSON.stringify({
  98                  violations: results.violations,
  99                  exception: null,
 100              });
 101          })
 102          .catch(exception => {
 103              axeTag.dataset.results = JSON.stringify({
 104                  violations: [],
 105                  exception: exception,
 106              });
 107          });
 108      };
 109  
 110      if (document.querySelector('script[data-purpose="axe"]')) {
 111          runTests();
 112      } else {
 113          // Inject the axe content.
 114          const axeTag = document.createElement('script');
 115          axeTag.src = axeurl,
 116          axeTag.dataset.purpose = 'axe';
 117  
 118          axeTag.onload = () => runTests();
 119          document.head.append(axeTag);
 120      }
 121  })('{$axeurl}');
 122  EOF;
 123  
 124          $this->execute_script($runaxe);
 125  
 126          $getresults = <<<EOF
 127  return (() => {
 128      const axeTag = document.querySelector('script[data-purpose="axe"]');
 129      return axeTag.dataset.results;
 130  })()
 131  EOF;
 132  
 133          for ($i = 0; $i < self::get_extended_timeout() * 10; $i++) {
 134              $results = json_decode($this->evaluate_script($getresults) ?? '');
 135              if ($results) {
 136                  break;
 137              }
 138          }
 139  
 140          if (empty($results)) {
 141              throw new \Exception('No data');
 142          }
 143  
 144          if ($results->exception !== null) {
 145              throw new ExpectationException($results->exception, $this->session);
 146          }
 147  
 148          $violations = $results->violations;
 149          if (!count($violations)) {
 150              return;
 151          }
 152  
 153          $violationdata = "Accessibility violations found:\n";
 154          foreach ($violations as $violation) {
 155              $nodedata = '';
 156              foreach ($violation->nodes as $node) {
 157                  $failedchecks = [];
 158                  foreach (array_merge($node->any, $node->all, $node->none) as $check) {
 159                      $failedchecks[$check->id] = $check->message;
 160                  }
 161  
 162                  $nodedata .= sprintf(
 163                      "    - %s:\n      %s\n\n",
 164                      implode(', ', $failedchecks),
 165                      implode("\n      ", $node->target)
 166                  );
 167              }
 168  
 169              $violationdata .= sprintf(
 170                  "  %.03d violations of '%s' (severity: %s)\n%s\n",
 171                  count($violation->nodes),
 172                  $violation->description,
 173                  $violation->impact,
 174                  $nodedata
 175              );
 176          }
 177  
 178          throw new ExpectationException($violationdata, $this->getSession());
 179      }
 180  
 181      /**
 182       * Get the configuration to use with Axe.
 183       *
 184       * See https://github.com/dequelabs/axe-core/blob/develop/doc/rule-descriptions.md for details of the rules.
 185       *
 186       * @param   array|null $standardtags The list of standard tags to run
 187       * @param   array|null $extratags The list of tags, in addition to the standard tags, to run
 188       * @return  string The JSON-encoded configuration.
 189       */
 190      protected function get_axe_config_for_tags(?array $standardtags = null, ?array $extratags = null): string {
 191          if (empty($standardtags)) {
 192              $standardtags = [
 193                  // Meet WCAG 2.1 A requirements.
 194                  'wcag2a',
 195  
 196                  // Meet WCAG 2.1 AA requirements.
 197                  'wcag2aa',
 198  
 199                  // Meet Section 508 requirements.
 200                  // See https://www.epa.gov/accessibility/what-section-508 for detail.
 201                  'section508',
 202  
 203                  // Ensure that ARIA attributes are correctly defined.
 204                  'cat.aria',
 205  
 206                  // Requiremetns for sensory and visual cues.
 207                  // These largely related to viewport scale and zoom functionality.
 208                  'cat.sensory-and-visual-cues',
 209  
 210                  // Meet WCAG 1.3.4 requirements for orientation.
 211                  // See https://www.w3.org/WAI/WCAG21/Understanding/orientation.html for detail.
 212                  'wcag134',
 213              ];
 214          }
 215  
 216          return json_encode([
 217              'runOnly' => [
 218                  'type' > 'tag',
 219                  'values' => array_merge($standardtags, $extratags),
 220              ],
 221          ]);
 222      }
 223  }