Skip to content

Commit

Permalink
Merged in exp (pull request #2)
Browse files Browse the repository at this point in the history
Implement first version of a plugin system
  • Loading branch information
iandotkelly committed Dec 21, 2015
2 parents a4fda58 + d221944 commit 7f06fbf
Show file tree
Hide file tree
Showing 42 changed files with 1,214 additions and 757 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ axe.a11yCheck(document, function (results) {
The [aXe API](doc/API.md) supports the following browsers:

* Internet Explorer v9, 10, 11
* Google Chrome v35 and above
* Mozilla Firefox v24 and above
* Google Chrome v42 and above
* Mozilla Firefox v38 and above
* Apple Safari v7 and above


Expand Down
8 changes: 8 additions & 0 deletions doc/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
1. [API Name: axe.configure](#api-name-axeconfigure)
1. [API Name: axe.a11yCheck](#api-name-axea11ycheck)
1. [Results Object](#results-object)
1. [API Name: axe.registerPlugin](#api-name-axeregisterplugin)
1. [Section 3: Example Reference](#section-3-example-reference)

## Section 1: Introduction
Expand Down Expand Up @@ -370,6 +371,13 @@ axe.a11yCheck(document, {
console.log(results);
});
```
### API Name: axe.registerPlugin

Register a plugin with the aXe plugin system. See [implementing a plugin](plugins.md) for more information on the plugin system

### API Name: axe.cleanup

Call the plugin system's cleanup function. See [implementing a plugin](plugins.md).


## Section 3: Example Reference
Expand Down
108 changes: 108 additions & 0 deletions doc/plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
# Plugins

aXe implements a general purpose plugin system that takes advantage of the cross-domain iframe capabilities of aXe and allows for adding functionality that extends the aXe library outside of its core automated accessibility auditing realm.

The plugin system was initially designed to support functionality like highlighting of elements but has also been utilized for a variety of tasks including implementing functionality that aids with manual accessibility auditing.

## What is a plugin?

Plugins can be viewed as a registry of tools. The plugins themselves are registered with aXe and then allow themselves for the registration of plugin instances.

Lets walk through a plugin implementation as an example to illustrate how plugins and plugin instances work.

### A simple "act" plugin

The act plugin will simply perform an action of some sort inside every iframe on the page. An example of how such a plugin might be used is to implement an instance of this plugin that performs highlighting of all of the elements of a particular type on the page.

Plugins currently support two functions, a "run" function and a "collect" function. Together these functions can be combined to implement complex behaviors on top of the aXe system.

In order to create such a plugin, we need to implement the "run" function for the plugin, and the command that registers and executes the "run" function within each iframe on the page that contains aXe. Lets look at what a noop implementation of this run function would look like:

#### Basic plugin

```
axe.registerPlugin({
id: 'doStuff',
run: function (id, action, options, callback) {
var frames;
var q = axe.utils.queue();
var that = this;
frames = axe.utils.toArray(document.querySelectorAll('iframe, frame'));
if (frames.length) {
frames.forEach(function (frame) {
q.defer(function (done) {
axe.utils.sendCommandToFrame(frame, {
options: options,
command: 'run-doStuff',
parameter: id,
action: action
}, function () {
done();
});
});
});
}
if (!options.context.length) {
q.defer(function (done) {
that._registry[id][action].call(that._registry[id], document, options, done);
});
}
q.then(function () {
callback();
});
},
commands: [{
id: 'run-doStuff',
callback: function (data, callback) {
return axe.plugins.doStuff.run(data.parameter, data.action, data.options, callback);
}
}]
});
```

Looking at the code, you will see the following things:

1. The plugin contains an id. This id is then used to access the plugin and its implementations.
2. The plugin is registered with aXe (in each iframe) using the `axe.registerPlugin()` function.
3. The plugin registers the "run" function and the "commands" with the aXe system. This allows plugin implementations to be registered with the plugin, and to be executed. It also registers handlers for eachof the commands within each of the iframes, so that the plugin can coordinate with itself accross the iframe boundaries.

When the caller wants to call a plugin instance, it does so by callin the plugin's "run" function in the top level document and passing the id of the plugin instance it would like to call, which plugin instance action it would like to call, the options and a callback function.

The plugin takes this information and sends the same instructions to its implementation in each iframe by communicating to its own command(s) using the aXe utility function `axe.utils.sendCommandToFrame()`.

The plugin waits for the commands in the iframes to complete and then executes its instances' action function within the current document.

In the above implementation, the aXe promise utility `axe.utils.queue()` is used to coordinate the asynchronous handling of communication accross iframes.

The command handler callback runs the plugin's run function within each iframe. This essentially operates like a recursive call to the run function for the plugin within each iframe.

Once all the iframes' run functions have been executed, the callback is called. This essentially operates as a recursive "return" up the iframe heirarchy until at the top document, the actual callback function is executed. This can be leveraged to pass data back up the iframe hierarchy back to the caller (but this is a more advanced topic).

#### Basic plugin instance

Lets implement a basic plugin instance to see how this works. This instance will implement a "highlight" function (to place a basic frame around the bounding box of an element on each iframe on a page)

```
var highlight = {
id: 'highlight',
highlighter: new Highlighter(),
run: function (contextNode, options, done) {
var that = this;
Array.prototype.slice.call(contextNode.querySelectorAll(options.selector)).forEach(function (node) {
that.highlighter.highlight(node, options);
});
done();
},
cleanup: function (done) {
this.highlighter.clear();
done();
}
};
axe.plugins.doStuff.add(highlight);
```

Above you can see the implementation of a `doStuff` "highlight" instance (the actual highlighting code is not included so as to simplify the example and is left as an exercise for the reader). Plugin instances have an id (which is used to address them), a cleanup function and any number of private or action members. The doStuff `add()` function is called to register this instance with the plugin (notice that we did not have to implement this add function, aXe did that for us). In this case, the action is called "run", so after registration, this instance can be called by calling `axe.plugins.doStuff.run('highlight', 'run', options, callback);` in the top-level iframe on the page.

The cleanup functions for all plugin instances are called when the `axe.cleanup()` function is called. Note that this cleanup function will automatically call all the cleanup functions for all the plugin instances in all iframes on the page.
2 changes: 1 addition & 1 deletion doc/projects.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Add your project/integration to this file and submit a pull request.
1. [aXe Chrome plugin](https://chrome.google.com/webstore/detail/axe/lhdoppojpmngadmnindnejefpokejbdd)
2. [axe-webdriverjs](https://www.npmjs.com/package/axe-webdriverjs)
3. [ember-axe](https://www.npmjs.com/package/ember-axe)
4. [axe-firefox-devtools](https://github.com/dequelabs/axe-firefox-devtools) and on the [Firefox extension page](https://addons.mozilla.org/en-US/firefox/addon/axe-firefox-devtools/)
4. [axe-firefox-devtools](https://github.com/dequelabs/axe-firefox-devtools) and on the [Firefox extension page](https://addons.mozilla.org/en-US/firefox/addon/axe-devtools/)
5. [axe-selenium-java](https://github.com/dequelabs/axe-selenium-java)
6. [a11yChromePlugin - not the official Chrome plugin source code](https://github.com/ptrstpp950/a11yChromePlugin)
7. [grunt-axe-webdriver](https://www.npmjs.com/package/grunt-axe-webdriver)
Expand Down
2 changes: 1 addition & 1 deletion lib/.jshintrc
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"strict": true,
"trailing": true,

"maxparams": 5,
"maxparams": 6,
"maxdepth": 5,
"maxstatements": 15,
"maxcomplexity": 10,
Expand Down
28 changes: 13 additions & 15 deletions lib/core/base/audit.js
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/*global Rule, Tool, Check, injectStyle, commons: true */
/*global Rule, Check, commons: true */

function setDefaultConfiguration(audit) {
'use strict';

var config = audit || {};
config.rules = config.rules || [];
config.tools = config.tools || [];
config.checks = config.checks || [];
config.data = config.data || {
checks: {},
Expand All @@ -28,27 +27,35 @@ function unpackToObject(collection, audit, method) {
* Constructor which holds configured rules and information about the document under test
*/
function Audit(audit) {
/*jshint maxstatements:16 */
'use strict';
audit = setDefaultConfiguration(audit);

axe.commons = commons = audit.commons;

this.reporter = audit.reporter;
this.commands = {};
this.rules = [];
this.tools = {};
this.checks = {};

unpackToObject(audit.rules, this, 'addRule');
unpackToObject(audit.tools, this, 'addTool');
unpackToObject(audit.checks, this, 'addCheck');
this.data = audit.data || {
checks: {},
rules: {}
};

injectStyle(audit.style);
}


/**
* Adds a new command to the audit
*/

Audit.prototype.registerCommand = function (command) {
'use strict';
this.commands[command.id] = command.callback;
};

/**
* Adds a new rule to the Audit. If a rule with specified ID already exists, it will be overridden
* @param {Object} spec Rule specification object
Expand All @@ -72,15 +79,6 @@ Audit.prototype.addRule = function (spec) {
this.rules.push(new Rule(spec, this));
};

/**
* Adds a new tool to the Audit. If a tool with specified ID already exists, it will be overridden
* @param {Object} spec Tool specification object
*/
Audit.prototype.addTool = function (spec) {
'use strict';
this.tools[spec.id] = new Tool(spec);
};

/**
* Adds a new check to the Audit. If a Check with specified ID already exists, it will be overridden
* @param {Object} spec Check specification object
Expand Down
32 changes: 15 additions & 17 deletions lib/core/base/context.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,21 @@
/*exported Context */

/*global isNodeInContext */
/**
* Pushes a unique frame onto `frames` array, filtering any hidden iframes
* @private
* @param {Context} context The context object to operate on and assign to
* @param {Array} collection The array of unique frames that is being operated on
* @param {HTMLElement} frame The frame to push onto Context
*/
function pushUniqueFrame(context, frame) {
function pushUniqueFrame(collection, frame) {
'use strict';
if (utils.isHidden(frame)) {
return;
}

var fr = utils.findBy(context.frames, 'node', frame);
var fr = utils.findBy(collection, 'node', frame);

if (!fr) {
context.frames.push({
collection.push({
node: frame,
include: [],
exclude: []
Expand Down Expand Up @@ -140,18 +140,9 @@ function parseSelectorArray(context, type) {
}
}

return result.filter(function (element) {

if (element) {
if ((element.nodeName === 'IFRAME' || element.nodeName === 'FRAME')) {
pushUniqueFrame(context, element);
return false;
}
utils.toArray(element.querySelectorAll('iframe, frame')).forEach(function (frame) {
pushUniqueFrame(context, frame);
});
}
return element;
// filter nulls
return result.filter(function (r) {
return r;
});
}

Expand All @@ -177,6 +168,7 @@ function parseSelectorArray(context, type) {
*/
function Context(spec) {
'use strict';
var self = this;

this.frames = [];
this.initiator = (spec && typeof spec.initiator === 'boolean') ? spec.initiator : true;
Expand All @@ -189,6 +181,12 @@ function Context(spec) {
this.include = parseSelectorArray(this, 'include');
this.exclude = parseSelectorArray(this, 'exclude');

utils.select('frame, iframe', this).forEach(function (frame) {
if (isNodeInContext(frame, self)) {
pushUniqueFrame(self.frames, frame);
}
});

if (this.include.length === 1 && this.include[0] === document) {
this.page = true;
}
Expand Down
28 changes: 0 additions & 28 deletions lib/core/base/tool.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@

function cleanupTools(callback) {
function cleanupPlugins(callback) {
'use strict';

if (!axe._audit) {
Expand All @@ -8,23 +8,20 @@ function cleanupTools(callback) {

var q = utils.queue();

Object.keys(axe._audit.tools).forEach(function (key) {
var tool = axe._audit.tools[key];
if (tool.active) {
q.defer(function (done) {
tool.cleanup(done);
});
}
Object.keys(axe.plugins).forEach(function (key) {
q.defer(function (done) {
axe.plugins[key].cleanup(done);
});
});

utils.toArray(document.querySelectorAll('frame, iframe')).forEach(function (frame) {
q.defer(function (done) {
return utils.sendCommandToFrame(frame, {
command: 'cleanup-tool'
command: 'cleanup-plugin'
}, done);
});
});

q.then(callback);
}
axe.cleanup = cleanupTools;
axe.cleanup = cleanupPlugins;
7 changes: 0 additions & 7 deletions lib/core/public/configure.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,4 @@ axe.configure = function (spec) {
audit.addRule(rule);
});
}

if (spec.tools) {
spec.tools.forEach(function (tool) {
audit.addTool(tool);
});
}

};
Loading

0 comments on commit 7f06fbf

Please sign in to comment.