Skip to content

Commit

Permalink
Merge pull request #1796 from dequelabs/virtualnode-serial
Browse files Browse the repository at this point in the history
feat(SerialVirtualNode): Allow serialised nodes in runNodesVirtual (experimental) [#1783]
  • Loading branch information
WilcoFiers committed Sep 26, 2019
2 parents 5e53d7b + 0fa9ef4 commit 64c59ab
Show file tree
Hide file tree
Showing 10 changed files with 540 additions and 213 deletions.
40 changes: 40 additions & 0 deletions lib/core/base/virtual-node/abstract-virtual-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
const whitespaceRegex = /[\t\r\n\f]/g;

class AbstractVirtualNode {
constructor() {
this.children = [];
this.parent = null;
}

get props() {
throw new Error(
'VirtualNode class must have a "props" object consisting ' +
'of "nodeType" and "nodeName" properties'
);
}

attr() {
throw new Error('VirtualNode class must have a "attr" function');
}

hasAttr() {
throw new Error('VirtualNode class must have a "hasAttr" function');
}

hasClass(className) {
// get the value of the class attribute as svgs return a SVGAnimatedString
// if you access the className property
let classAttr = this.attr('class');
if (!classAttr) {
return false;
}

let selector = ' ' + className + ' ';
return (
(' ' + classAttr + ' ').replace(whitespaceRegex, ' ').indexOf(selector) >=
0
);
}
}

axe.AbstractVirtualNode = AbstractVirtualNode;
86 changes: 86 additions & 0 deletions lib/core/base/virtual-node/serial-virtual-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// eslint-disable-next-line no-unused-vars
class SerialVirtualNode extends axe.AbstractVirtualNode {
/**
* Convert a serialised node into a VirtualNode object
* @param {Object} node Serialised node
*/
constructor(serialNode) {
super();
this._props = normaliseProps(serialNode);
this._attrs = normaliseAttrs(serialNode);
}

// Accessof for DOM node properties
get props() {
return this._props;
}

/**
* Get the value of the given attribute name.
* @param {String} attrName The name of the attribute.
* @return {String|null} The value of the attribute or null if the attribute does not exist
*/
attr(attrName) {
return this._attrs[attrName] || null;
}

/**
* Determine if the element has the given attribute.
* @param {String} attrName The name of the attribute
* @return {Boolean} True if the element has the attribute, false otherwise.
*/
hasAttr(attrName) {
return this._attrs[attrName] !== undefined;
}
}

/**
* Convert between serialised props and DOM-like properties
* @param {Object} serialNode
* @return {Object} normalProperties
*/
function normaliseProps(serialNode) {
let { nodeName, nodeType = 1 } = serialNode;
axe.utils.assert(
nodeType === 1,
`nodeType has to be undefined or 1, got '${nodeType}'`
);
axe.utils.assert(
typeof nodeName === 'string',
`nodeName has to be a string, got '${nodeName}'`
);

const props = {
...serialNode,
nodeType,
nodeName: nodeName.toLowerCase()
};
delete props.attributes;
return Object.freeze(props);
}

/**
* Convert between serialised attributes and DOM-like attributes
* @param {Object} serialNode
* @return {Object} normalAttributes
*/
function normaliseAttrs({ attributes = {} }) {
const attrMap = {
htmlFor: 'for',
className: 'class'
};

return Object.keys(attributes).reduce((attrs, attrName) => {
const value = attributes[attrName];
axe.utils.assert(
typeof value !== 'object' || value === null,
`expects attributes not to be an object, '${attrName}' was`
);

if (value !== undefined) {
const mappedName = attrMap[attrName] || attrName;
attrs[mappedName] = value !== null ? String(value) : null;
}
return attrs;
}, {});
}
Original file line number Diff line number Diff line change
@@ -1,34 +1,6 @@
const whitespaceRegex = /[\t\r\n\f]/g;

class AbstractVirtualNode {
constructor() {
this.children = [];
this.parent = null;
}

get props() {
throw new Error(
'VirtualNode class must have a "props" object consisting ' +
'of "nodeType" and "nodeName" properties'
);
}

hasClass() {
throw new Error('VirtualNode class must have a "hasClass" function');
}

attr() {
throw new Error('VirtualNode class must have a "attr" function');
}

hasAttr() {
throw new Error('VirtualNode class must have a "hasAttr" function');
}
}

// class is unused in the file...
// eslint-disable-next-line no-unused-vars
class VirtualNode extends AbstractVirtualNode {
class VirtualNode extends axe.AbstractVirtualNode {
/**
* Wrap the real node and provide list of the flattened children
* @param {Node} node the node in question
Expand Down Expand Up @@ -63,27 +35,6 @@ class VirtualNode extends AbstractVirtualNode {
};
}

/**
* Determine if the actualNode has the given class name.
* @see https://j11y.io/jquery/#v=2.0.3&fn=jQuery.fn.hasClass
* @param {String} className The class to check for.
* @return {Boolean} True if the actualNode has the given class, false otherwise.
*/
hasClass(className) {
// get the value of the class attribute as svgs return a SVGAnimatedString
// if you access the className property
let classAttr = this.attr('class');
if (!classAttr) {
return false;
}

let selector = ' ' + className + ' ';
return (
(' ' + classAttr + ' ').replace(whitespaceRegex, ' ').indexOf(selector) >=
0
);
}

/**
* Get the value of the given attribute name.
* @param {String} attrName The name of the attribute.
Expand Down Expand Up @@ -132,5 +83,3 @@ class VirtualNode extends AbstractVirtualNode {
return this._cache.tabbableElements;
}
}

axe.AbstractVirtualNode = AbstractVirtualNode;
6 changes: 5 additions & 1 deletion lib/core/public/run-virtual-rule.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/* global helpers */
/* global helpers, SerialVirtualNode */

/**
* Run a rule in a non-browser environment
Expand All @@ -11,6 +11,10 @@ axe.runVirtualRule = function(ruleId, vNode, options = {}) {
options.reporter = options.reporter || axe._audit.reporter || 'v1';
axe._selectorData = {};

if (vNode instanceof axe.AbstractVirtualNode === false) {
vNode = new SerialVirtualNode(vNode);
}

let rule = axe._audit.rules.find(rule => rule.id === ruleId);

if (!rule) {
Expand Down
12 changes: 12 additions & 0 deletions lib/core/utils/assert.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/* global axe*/

/**
* If the first argument is falsey, throw an error using the second argument as a message.
* @param {boolean} bool
* @param {string} message
*/
axe.utils.assert = function assert(bool, message) {
if (!bool) {
throw new Error(message);
}
};
110 changes: 110 additions & 0 deletions test/core/base/virtual-node/abstract-virtual-node.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
describe('AbstractVirtualNode', function() {
it('should be a function', function() {
assert.isFunction(axe.AbstractVirtualNode);
});

it('should throw an error when accessing props', function() {
function fn() {
var abstractNode = new axe.AbstractVirtualNode();
if (abstractNode.props.nodeType === 1) {
return;
}
}

assert.throws(fn);
});

it('should throw an error when accessing hasClass', function() {
function fn() {
var abstractNode = new axe.AbstractVirtualNode();
if (abstractNode.hasClass('foo')) {
return;
}
}

assert.throws(fn);
});

it('should throw an error when accessing attr', function() {
function fn() {
var abstractNode = new axe.AbstractVirtualNode();
if (abstractNode.attr('foo') === 'bar') {
return;
}
}

assert.throws(fn);
});

it('should throw an error when accessing hasAttr', function() {
function fn() {
var abstractNode = new axe.AbstractVirtualNode();
if (abstractNode.hasAttr('foo')) {
return;
}
}

assert.throws(fn);
});

describe('hasClass, when attr is set', function() {
it('should return true when the element has the class', function() {
var vNode = new axe.AbstractVirtualNode();
vNode.attr = function() {
return 'my-class';
};

assert.isTrue(vNode.hasClass('my-class'));
});

it('should return true when the element contains more than one class', function() {
var vNode = new axe.AbstractVirtualNode();
vNode.attr = function() {
return 'my-class a11y-focus visually-hidden';
};

assert.isTrue(vNode.hasClass('my-class'));
assert.isTrue(vNode.hasClass('a11y-focus'));
assert.isTrue(vNode.hasClass('visually-hidden'));
});

it('should return false when the element does not contain the class', function() {
var vNode = new axe.AbstractVirtualNode();
vNode.attr = function() {
return undefined;
};

assert.isFalse(vNode.hasClass('my-class'));
});

it('should return false when the element contains only part of the class', function() {
var vNode = new axe.AbstractVirtualNode();
vNode.attr = function() {
return 'my-class';
};
assert.isFalse(vNode.hasClass('class'));
});

it('should return false if className is not of type string', function() {
var vNode = new axe.AbstractVirtualNode();
vNode.attr = function() {
return null;
};

assert.isFalse(vNode.hasClass('my-class'));
});

it('should return true for whitespace characters', function() {
var vNode = new axe.AbstractVirtualNode();
vNode.attr = function() {
return 'my-class\ta11y-focus\rvisually-hidden\ngrid\fcontainer';
};

assert.isTrue(vNode.hasClass('my-class'));
assert.isTrue(vNode.hasClass('a11y-focus'));
assert.isTrue(vNode.hasClass('visually-hidden'));
assert.isTrue(vNode.hasClass('grid'));
assert.isTrue(vNode.hasClass('container'));
});
});
});
Loading

0 comments on commit 64c59ab

Please sign in to comment.