From 9d47a690945f9048cad815fefee8a2304d25be5a Mon Sep 17 00:00:00 2001 From: Andrew Nicols Date: Tue, 14 Jan 2020 12:06:10 +0800 Subject: [PATCH] MDL-67687 behat: Add axe web accessibility tool integration --- lib/tests/behat/behat_accessibility.php | 219 ++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 lib/tests/behat/behat_accessibility.php diff --git a/lib/tests/behat/behat_accessibility.php b/lib/tests/behat/behat_accessibility.php new file mode 100644 index 00000000000..32ff5fa5890 --- /dev/null +++ b/lib/tests/behat/behat_accessibility.php @@ -0,0 +1,219 @@ +. + +/** + * 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}; + +// 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 + * @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 (!$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 = << { + 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 = << { + 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)); + if ($results) { + break; + } + } + + if (empty($results)) { + throw new \Exception('No data'); + } + + if ($results->exception !== null) { + throw new ExpectationException($results->exception, $this->session); + } + + $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), + ], + ]); + } +}