diff --git a/lib/core/base/virtual-node/serial-virtual-node.js b/lib/core/base/virtual-node/serial-virtual-node.js new file mode 100644 index 0000000000..e075d40e48 --- /dev/null +++ b/lib/core/base/virtual-node/serial-virtual-node.js @@ -0,0 +1,80 @@ +// 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 !== undefined || nodeType !== 1, + `SerialVirtualNode expects nodeType of undefined or 1, got '${nodeType}'`) + axe.utils.assert(typeof nodeName === 'string', + `SerialVirtualNode expects nodeName 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, + `SerialVirtualNode 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 + }, {}) +} diff --git a/test/core/base/virtual-node/serial-virtual-node.js b/test/core/base/virtual-node/serial-virtual-node.js new file mode 100644 index 0000000000..cd21f2d674 --- /dev/null +++ b/test/core/base/virtual-node/serial-virtual-node.js @@ -0,0 +1,173 @@ +/*global axe, SerialVirtualNode */ +describe('SerialVirtualNode', function() { + it('extends AbstractVirtualNode', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div' + }); + assert.instanceOf(vNode, axe.AbstractVirtualNode); + }); + + describe('props', function () { + it('assigns any properties to .props', function () { + var props = { + nodeType: 1, + nodeName: 'div', + someType: 'bar', + somethingElse: 'baz' + } + var vNode = new SerialVirtualNode(props); + assert.deepEqual(vNode.props, props); + }) + + it('returns a frozen object', function () { + var vNode = new SerialVirtualNode({ nodeName: 'div' }); + assert.isTrue(Object.isFrozen(vNode.props), 'Expect object to be frozen'); + }) + + it('has a default nodeType of 1', function () { + var vNode = new SerialVirtualNode({ nodeName: 'div' }); + assert.equal(vNode.props.nodeType, 1) + }) + + it('converts nodeNames to lower case', function () { + var htmlNodes = [ + 'DIV', + 'SPAN', + 'INPUT', + 'HeAdEr', + 'TABLE', + 'TITLE', + 'BUTTON', + 'Foo' + ] + htmlNodes.forEach(function (nodeName) { + var vNode = new SerialVirtualNode({ nodeName: nodeName }); + assert.equal(vNode.props.nodeName, nodeName.toLowerCase()) + }) + }) + + it('ignores the `attributes` property', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'foo': 'foo', + 'bar': 'bar', + 'baz': 'baz' + } + }); + assert.isUndefined(vNode.props.attributes) + }) + }) + + describe('attr', function () { + it('returns a string value for the attribute', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'foo': 'foo', + 'bar': 123, + 'baz': true + } + }); + assert.equal(vNode.attr('foo'), 'foo'); + assert.equal(vNode.attr('bar'), '123'); + assert.equal(vNode.attr('baz'), 'true'); + }) + + it('returns null if the attribute is null', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div', + attributes: { 'foo': null } + }); + assert.isNull(vNode.attr('foo')); + }) + + it('returns null if the attribute is not set', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div' + }); + assert.isNull(vNode.attr('foo')); + }) + + it('converts `className` to `class`', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div', + attributes: { + className: 'foo bar baz' + } + }); + assert.equal(vNode.attr('class'), 'foo bar baz'); + }) + + it('converts `htmlFor` to `for`', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div', + attributes: { + htmlFor: 'foo' + } + }); + assert.equal(vNode.attr('for'), 'foo'); + }) + }) + + describe('hasAttr', function () { + it('returns true if the attribute has a value', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div', + attributes: { + 'foo': '', + 'bar': 0, + 'baz': false + } + }); + assert.isTrue(vNode.hasAttr('foo')); + assert.isTrue(vNode.hasAttr('bar')); + assert.isTrue(vNode.hasAttr('baz')); + }) + + it('returns true if the attribute is null', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div', + attributes: { 'foo': null } + }); + assert.isTrue(vNode.hasAttr('foo')); + }) + + it('returns false if the attribute is undefined', function () { + var vNode = new SerialVirtualNode({ + nodeName: 'div', + attributes: { 'foo': undefined } + }); + assert.isFalse(vNode.hasAttr('foo')); + assert.isFalse(vNode.hasAttr('bar')); + }) + + it('converts `htmlFor` to `for`', function () { + var nodeWithoutFor = new SerialVirtualNode({ + nodeName: 'div', + attributes: {} + }); + var nodeWithFor = new SerialVirtualNode({ + nodeName: 'div', + attributes: { 'htmlFor': 'foo' } + }); + + assert.isFalse(nodeWithoutFor.hasAttr('for')); + assert.isTrue(nodeWithFor.hasAttr('for')); + }) + + it('converts `className` to `class`', function () { + var nodeWithoutClass = new SerialVirtualNode({ + nodeName: 'div', + attributes: {} + }); + var nodeWithClass = new SerialVirtualNode({ + nodeName: 'div', + attributes: { 'className': 'foo bar baz' } + }); + + assert.isFalse(nodeWithoutClass.hasAttr('class')); + assert.isTrue(nodeWithClass.hasAttr('class')); + }) + }) +}); \ No newline at end of file