diff --git a/Gruntfile.js b/Gruntfile.js index 0a888fe1c..47d9e0787 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -15,6 +15,7 @@ module.exports = function (grunt) { grunt.loadTasks('build/tasks'); var langs; + var wrapper = !!grunt.option('wrapper'); if (grunt.option('lang')) { langs = (grunt.option('lang') || '').split(/[,;]/g).map(function (lang) { lang = lang.trim(); @@ -85,11 +86,11 @@ module.exports = function (grunt) { files: langs.map(function (lang, i) { return { src: [ - 'lib/intro.stub', + wrapper ? 'lib/custom/intro.stub' : 'lib/intro.stub', '<%= concat.engine.coreFiles %>', // include rules / checks / commons '<%= configure.rules.files[' + i + '].dest.auto %>', - 'lib/outro.stub' + wrapper ? 'lib/custom/outro.stub' : 'lib/outro.stub' ], dest: 'axe' + lang + '.js' }; @@ -127,7 +128,8 @@ module.exports = function (grunt) { entry: 'lib/commons/aria/index.js', destFile: 'doc/aria-supported.md', options: { - langs: langs + langs: langs, + wrapper: wrapper }, listType: 'unsupported' // Possible values for listType: 'supported', 'unsupported', 'all' } diff --git a/build/tasks/aria-supported.js b/build/tasks/aria-supported.js index 257b52ed5..efe43437f 100644 --- a/build/tasks/aria-supported.js +++ b/build/tasks/aria-supported.js @@ -16,7 +16,8 @@ module.exports = function (grunt) { * hence cannot be required at the top of the file. */ const done = this.async(); - const { langs } = this.options(); + const { langs, wrapper } = this.options(); + if (wrapper) return true; const fileNameSuffix = langs && langs.length > 0 ? `${langs[0]}` : ''; const axe = require(`../../axe${fileNameSuffix}`); const listType = this.data.listType.toLowerCase(); diff --git a/lib/core/public/load.js b/lib/core/public/load.js index 124c849b1..e80d3d8f6 100644 --- a/lib/core/public/load.js +++ b/lib/core/public/load.js @@ -1,8 +1,10 @@ +/*global a11yEngine*/ import Audit from '../base/audit'; import cleanup from './cleanup'; import runRules from './run-rules'; import respondable from '../utils/respondable'; import nodeSerializer from '../utils/node-serializer'; +import mergeErrors from '../utils/merge-errors'; /** * Sets up Rules, Messages and default options for Checks, must be invoked before attempting analysis @@ -36,6 +38,21 @@ function runCommand(data, keepalive, callback) { (results, cleanupFn) => { // Serialize all DqElements results = nodeSerializer.mapRawResults(results); + + //a11y-engine iframe rules error merging logic + const errors = a11yEngine.getErrors(); + if (Object.keys(errors).length !== 0) { + if (results[results.length - 1].a11yEngineErrors) { + const error = results.pop(); + delete error.a11yEngineErrors; + const mergedErrors = mergeErrors(error, errors); + results.push({ ...mergedErrors, a11yEngineErrors: true }); + } else { + results.push({ ...errors, a11yEngineErrors: true }); + } + } + a11yEngine.clearErrors(); + resolve(results); // Cleanup AFTER resolve so that selectors can be generated cleanupFn(); diff --git a/lib/core/public/run-rules.js b/lib/core/public/run-rules.js index 5eb26d7c9..8e04c13dc 100644 --- a/lib/core/public/run-rules.js +++ b/lib/core/public/run-rules.js @@ -1,3 +1,4 @@ +/*global a11yEngine*/ import Context from '../base/context'; import teardown from './teardown'; import { @@ -36,6 +37,12 @@ export default function runRules(context, options, resolve, reject) { performanceTimer.auditStart(); } + // If advanced run for iframes is true then setup socket for iframes + if (options.a11yEngineConfig && options.a11yEngineConfig.iframesAdvancedRun) { + a11yEngine.setup(options.a11yEngineConfig); + } + + // If run for iframes is true then collect results from iframes if (context.frames.length && options.iframes !== false) { q.defer((res, rej) => { collectResultsFromFrames(context, options, 'rules', null, res, rej); @@ -59,6 +66,13 @@ export default function runRules(context, options, resolve, reject) { // after should only run once, so ensure we are in the top level window if (context.initiator) { + // Return a11y-engine errors when at top level window + if (results[results.length - 1].a11yEngineErrors) { + const error = results.pop(); + delete error.a11yEngineErrors; + a11yEngine.mergeErrors(error); + } + results = audit.after(results, options); results.forEach(publishMetaData); diff --git a/lib/core/utils/collect-results-from-frames.js b/lib/core/utils/collect-results-from-frames.js index be17f8ccf..e19e356a8 100644 --- a/lib/core/utils/collect-results-from-frames.js +++ b/lib/core/utils/collect-results-from-frames.js @@ -22,6 +22,14 @@ export default function collectResultsFromFrames( // elementRefs can't be passed across frame boundaries options = { ...options, elementRef: false }; + // check a11yengine iframe advance run flag + if ( + options.a11yEngineConfig && + options.a11yEngineConfig.iframesAdvancedRun === false + ) { + options.a11yEngineConfig.iframesAdvancedRun = true; + } + var q = queue(); var frames = parentContent.frames; diff --git a/lib/core/utils/merge-errors.js b/lib/core/utils/merge-errors.js new file mode 100644 index 000000000..7fffe01eb --- /dev/null +++ b/lib/core/utils/merge-errors.js @@ -0,0 +1,52 @@ +// Function to merge errors for a11y-engine. +// Handles errors differently for check_errors and other errors. +// It also adds the target selector to the errors for better identification. + +function mergeErrors(mergedErrors, frameErrors, frameSpec) { + for (const [key, value] of Object.entries(frameErrors)) { + if (key === 'check_errors') { + if (!mergedErrors[key]) { + mergedErrors[key] = {}; + } + + for (const [checkNameKey, checkNameValue] of Object.entries(value)) { + // Add the target if not present. If present then append parents target. + checkNameValue.forEach(checkNameValueError => { + if (!checkNameValueError.target && frameSpec) { + checkNameValueError.target = frameSpec?.selector; + } else if (checkNameValueError.target && frameSpec) { + checkNameValueError.target = [ + ...frameSpec.selector, + ...checkNameValueError.target + ]; + } + }); + if (mergedErrors[key][checkNameKey]) { + mergedErrors[key][checkNameKey].push(...checkNameValue); + } else { + mergedErrors[key][checkNameKey] = Array.isArray(checkNameValue) + ? [...checkNameValue] + : [checkNameValue]; + } + } + } else { + // Add the target if not present. If present then append parents target. + value.forEach(errorValue => { + if (!errorValue.target && frameSpec) { + errorValue.target = frameSpec?.selector; + } else if (errorValue.target && frameSpec) { + errorValue.target = [...frameSpec.selector, ...errorValue.target]; + } + }); + if (mergedErrors[key]) { + mergedErrors[key] = [...mergedErrors[key], ...value]; + } else { + mergedErrors[key] = value; + } + } + } + + return mergedErrors; +} + +export default mergeErrors; diff --git a/lib/core/utils/merge-results.js b/lib/core/utils/merge-results.js index e5c9547ad..0e6277132 100644 --- a/lib/core/utils/merge-results.js +++ b/lib/core/utils/merge-results.js @@ -1,6 +1,7 @@ import nodeSerializer from './node-serializer'; import getAllChecks from './get-all-checks'; import findBy from './find-by'; +import mergeErrors from './merge-errors'; /** * Adds the owning frame's CSS selector onto each instance of DqElement @@ -75,6 +76,8 @@ function normalizeResult(result) { */ function mergeResults(frameResults, options) { const mergedResult = []; + // A11yEngine merged errors + let mergedErrors = {}; frameResults.forEach(frameResult => { const results = normalizeResult(frameResult); if (!results || !results.length) { @@ -82,6 +85,13 @@ function mergeResults(frameResults, options) { } const frameSpec = getFrameSpec(frameResult); + // Extract existing errors and merge with new ones + if (results[results.length - 1].a11yEngineErrors) { + const error = results.pop(); + delete error.a11yEngineErrors; + mergedErrors = mergeErrors(mergedErrors, error, frameSpec); + } + results.forEach(ruleResult => { if (ruleResult.nodes && frameSpec) { pushFrame(ruleResult.nodes, options, frameSpec); @@ -106,7 +116,11 @@ function mergeResults(frameResults, options) { }); } }); - return mergedResult; + + if (Object.keys(mergedErrors).length === 0) { + return mergedResult; + } + return [...mergedResult, { ...mergedErrors, a11yEngineErrors: true }]; } function nodeIndexSort(nodeIndexesA = [], nodeIndexesB = []) { diff --git a/lib/custom/intro.stub b/lib/custom/intro.stub new file mode 100644 index 000000000..9ed8b3c4b --- /dev/null +++ b/lib/custom/intro.stub @@ -0,0 +1,15 @@ +/*! axe v<%= pkg.version %> + * Copyright (c) 2015 - <%= grunt.template.today("yyyy") %> Deque Systems, Inc. + * + * Your use of this Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + * + * This entire copyright notice must appear in every copy of this file you + * distribute or in any file that contains substantial portions of this source + * code. + */ +const createAxe = () => (function axeFunction (window) { + // A window reference is required to access the axe object in a "global". + var global = window; + var document = window.document; diff --git a/lib/custom/outro.stub b/lib/custom/outro.stub new file mode 100644 index 000000000..19fd22a6e --- /dev/null +++ b/lib/custom/outro.stub @@ -0,0 +1,2 @@ + +}( typeof window === 'object' ? window : this )); diff --git a/package.json b/package.json index 6c9a3e1fa..5d5beb356 100644 --- a/package.json +++ b/package.json @@ -71,6 +71,7 @@ "develop": "grunt dev --force", "api-docs": "jsdoc --configure .jsdoc.json", "build": "grunt", + "build:wrapper": "grunt --wrapper=true", "eslint": "eslint --color --format stylish '{lib,test,build,doc}/**/*.js' 'Gruntfile.js'", "test": "npm run test:tsc && run-s \"test:unit:* -- {@}\" --", "test:tsc": "tsc",