-
Notifications
You must be signed in to change notification settings - Fork 755
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1796 from dequelabs/virtualnode-serial
feat(SerialVirtualNode): Allow serialised nodes in runNodesVirtual (experimental) [#1783]
- Loading branch information
Showing
10 changed files
with
540 additions
and
213 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}, {}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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')); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.