From e8e17e42c3594518ee60838749a507504a839c69 Mon Sep 17 00:00:00 2001 From: Steven Lambert <2433219+straker@users.noreply.github.com> Date: Fri, 17 Jul 2020 08:31:41 -0600 Subject: [PATCH] feat(object-alt,accessible-text): object-alt rule and accessible text to work with serial virtual nodes with children * feat(object-alt,accessible-text,native-element-type): object-alt rule and accessible text to work with serial virtual nodes with children. deprecate native-element-type * remove #text from html-elm * nodeValue * fix tests * fix test * remove file * rename --- lib/commons/aria/get-owned-virtual.js | 20 +-- lib/commons/aria/lookup-table.js | 5 +- lib/commons/standards/get-element-spec.js | 5 + lib/commons/text/accessible-text-virtual.js | 21 ++- lib/commons/text/form-control-value.js | 2 +- lib/commons/text/native-text-alternative.js | 13 +- lib/commons/text/subtree-text.js | 82 ++++----- .../virtual-node/abstract-virtual-node.js | 1 - .../base/virtual-node/serial-virtual-node.js | 4 +- lib/core/base/virtual-node/virtual-node.js | 5 +- lib/core/public/run-virtual-rule.js | 5 +- lib/standards/html-elms.js | 19 +-- .../checks/aria/no-implicit-explicit-label.js | 4 +- test/checks/shared/button-has-visible-text.js | 43 ++++- test/checks/shared/has-visible-text.js | 29 +++- test/commons/standards/get-element-spec.js | 5 + .../base/virtual-node/serial-virtual-node.js | 12 +- test/integration/virtual-rules/index.html | 2 +- test/integration/virtual-rules/object-alt.js | 156 ++++++++++++++++++ 19 files changed, 335 insertions(+), 98 deletions(-) create mode 100644 test/integration/virtual-rules/object-alt.js diff --git a/lib/commons/aria/get-owned-virtual.js b/lib/commons/aria/get-owned-virtual.js index 8c27f0d2eb..53c471b7a9 100644 --- a/lib/commons/aria/get-owned-virtual.js +++ b/lib/commons/aria/get-owned-virtual.js @@ -6,22 +6,22 @@ import idrefs from '../dom/idrefs'; * @param {VirtualNode} element * @return {VirtualNode[]} Owned elements */ -function getOwnedVirtual({ actualNode, children }) { - if (!actualNode || !children) { +function getOwnedVirtual(virtualNode) { + const { actualNode, children } = virtualNode; + if (!children) { throw new Error('getOwnedVirtual requires a virtual node'); } // TODO: Check that the element has a role // TODO: Descend into children with role=presentation|none // TODO: Exclude descendents owned by other elements + if (virtualNode.hasAttr('aria-owns')) { + const owns = idrefs(actualNode, 'aria-owns') + .filter(element => !!element) + .map(element => axe.utils.getNodeFromTree(element)); + return [...children, ...owns]; + } - return idrefs(actualNode, 'aria-owns').reduce((ownedElms, element) => { - if (element) { - // TODO: es-module-utils.getNodeFromTree - const virtualNode = axe.utils.getNodeFromTree(element); - ownedElms.push(virtualNode); - } - return ownedElms; - }, children); + return [...children]; } export default getOwnedVirtual; diff --git a/lib/commons/aria/lookup-table.js b/lib/commons/aria/lookup-table.js index d6b99ac968..1f703ced36 100644 --- a/lib/commons/aria/lookup-table.js +++ b/lib/commons/aria/lookup-table.js @@ -2178,10 +2178,7 @@ lookupTable.elementsAllowedNoRole = [ { nodeName: 'select', condition: vNode => { - // TODO: this is a ridiculous hack since webpack is making these two - // separate functions - // TODO: es-module-AbstractVirtualNode - if (!axe._isAbstractNode(vNode)) { + if (!(vNode instanceof axe.AbstractVirtualNode)) { vNode = axe.utils.getNodeFromTree(vNode); } diff --git a/lib/commons/standards/get-element-spec.js b/lib/commons/standards/get-element-spec.js index 13e6bb2022..4f578fe1ba 100644 --- a/lib/commons/standards/get-element-spec.js +++ b/lib/commons/standards/get-element-spec.js @@ -9,6 +9,11 @@ import matchesFn from '../../commons/matches'; function getElementSpec(vNode) { const standard = standards.htmlElms[vNode.props.nodeName]; + // invalid element name (could be an svg or custom element name) + if (!standard) { + return {}; + } + if (!standard.variant) { return standard; } diff --git a/lib/commons/text/accessible-text-virtual.js b/lib/commons/text/accessible-text-virtual.js index bc742eb01f..7efa692535 100644 --- a/lib/commons/text/accessible-text-virtual.js +++ b/lib/commons/text/accessible-text-virtual.js @@ -32,7 +32,7 @@ function accessibleTextVirtual(virtualNode, context = {}) { nativeTextAlternative, // Step 2D formControlValue, // Step 2E subtreeText, // Step 2F + Step 2H - textNodeContent, // Step 2G (order with 2H does not matter) + textNodeValue, // Step 2G (order with 2H does not matter) titleText // Step 2I ]; @@ -56,15 +56,15 @@ function accessibleTextVirtual(virtualNode, context = {}) { } /** - * Return the textContent of a node + * Return the nodeValue of a node * @param {VirtualNode} element - * @return {String} textContent value + * @return {String} nodeValue value */ -function textNodeContent({ actualNode }) { - if (actualNode.nodeType !== 3) { +function textNodeValue(virtualNode) { + if (virtualNode.props.nodeType !== 3) { return ''; } - return actualNode.textContent; + return virtualNode.props.nodeValue; } /** @@ -75,6 +75,10 @@ function textNodeContent({ actualNode }) { * @return {Boolean} */ function shouldIgnoreHidden({ actualNode }, context) { + if (!actualNode) { + return false; + } + if ( // If the parent isn't ignored, the text node should not be either actualNode.nodeType !== 1 || @@ -98,6 +102,11 @@ function prepareContext(virtualNode, context) { if (!context.startNode) { context = { startNode: virtualNode, ...context }; } + + if (!actualNode) { + return context; + } + /** * When `aria-labelledby` directly references a `hidden` element * the element needs to be included in the accessible name. diff --git a/lib/commons/text/form-control-value.js b/lib/commons/text/form-control-value.js index c8ac64c436..a931933e70 100644 --- a/lib/commons/text/form-control-value.js +++ b/lib/commons/text/form-control-value.js @@ -43,7 +43,7 @@ export const formControlValueMethods = { function formControlValue(virtualNode, context = {}) { const { actualNode } = virtualNode; const unsupportedRoles = unsupported.accessibleNameFromFieldValue || []; - const role = getRole(actualNode); + const role = getRole(virtualNode); if ( // For the targeted node, the accessible name is never the value: diff --git a/lib/commons/text/native-text-alternative.js b/lib/commons/text/native-text-alternative.js index 56102f4ce7..eb5f7e464c 100644 --- a/lib/commons/text/native-text-alternative.js +++ b/lib/commons/text/native-text-alternative.js @@ -1,6 +1,5 @@ import getRole from '../aria/get-role'; -import matches from '../matches/matches'; -import nativeElementType from './native-element-type'; +import getElementSpec from '../standards/get-element-spec'; import nativeTextMethods from './native-text-methods'; /** @@ -13,7 +12,7 @@ import nativeTextMethods from './native-text-methods'; function nativeTextAlternative(virtualNode, context = {}) { const { actualNode } = virtualNode; if ( - actualNode.nodeType !== 1 || + virtualNode.props.nodeType !== 1 || ['presentation', 'none'].includes(getRole(virtualNode)) ) { return ''; @@ -38,12 +37,8 @@ function nativeTextAlternative(virtualNode, context = {}) { * @return {Function[]} Array of native accessible name computation methods */ function findTextMethods(virtualNode) { - const nativeType = nativeElementType.find(type => { - return matches(virtualNode, type.matches); - }); - - // Use concat because namingMethods can be a string or an array of strings - const methods = nativeType ? [].concat(nativeType.namingMethods) : []; + const elmSpec = getElementSpec(virtualNode); + const methods = elmSpec.namingMethods || []; return methods.map(methodName => nativeTextMethods[methodName]); } diff --git a/lib/commons/text/subtree-text.js b/lib/commons/text/subtree-text.js index 793c9ab637..971ef51365 100644 --- a/lib/commons/text/subtree-text.js +++ b/lib/commons/text/subtree-text.js @@ -29,50 +29,50 @@ function subtreeText(virtualNode, context = {}) { // TODO: Could do with an "HTML" lookup table, similar to ARIA, // where this sort of stuff can live. const phrasingElements = [ - 'A', - 'EM', - 'STRONG', - 'SMALL', - 'MARK', - 'ABBR', - 'DFN', - 'I', - 'B', - 'S', - 'U', - 'CODE', - 'VAR', - 'SAMP', - 'KBD', - 'SUP', - 'SUB', - 'Q', - 'CITE', - 'SPAN', - 'BDO', - 'BDI', - 'WBR', - 'INS', - 'DEL', - 'MAP', - 'AREA', - 'NOSCRIPT', - 'RUBY', - 'BUTTON', - 'LABEL', - 'OUTPUT', - 'DATALIST', - 'KEYGEN', - 'PROGRESS', - 'COMMAND', - 'CANVAS', - 'TIME', - 'METER', - '#TEXT' + '#text', + 'a', + 'abbr', + 'area', + 'b', + 'bdi', + 'bdo', + 'button', + 'canvas', + 'cite', + 'code', + 'command', + 'datalist', + 'del', + 'dfn', + 'em', + 'i', + 'ins', + 'kbd', + 'keygen', + 'label', + 'map', + 'mark', + 'meter', + 'noscript', + 'output', + 'progress', + 'q', + 'ruby', + 's', + 'samp', + 'small', + 'span', + 'strong', + 'sub', + 'sup', + 'time', + 'u', + 'var', + 'wbr' ]; function appendAccessibleText(contentText, virtualNode, context) { - const nodeName = virtualNode.actualNode.nodeName.toUpperCase(); + const nodeName = virtualNode.props.nodeName; let contentTextAdd = accessibleTextVirtual(virtualNode, context); if (!contentTextAdd) { return contentText; diff --git a/lib/core/base/virtual-node/abstract-virtual-node.js b/lib/core/base/virtual-node/abstract-virtual-node.js index a661b3b5a3..63697a25a4 100644 --- a/lib/core/base/virtual-node/abstract-virtual-node.js +++ b/lib/core/base/virtual-node/abstract-virtual-node.js @@ -2,7 +2,6 @@ const whitespaceRegex = /[\t\r\n\f]/g; class AbstractVirtualNode { constructor() { - this.children = []; this.parent = null; } diff --git a/lib/core/base/virtual-node/serial-virtual-node.js b/lib/core/base/virtual-node/serial-virtual-node.js index a94b8c9f7f..d28d915eb3 100644 --- a/lib/core/base/virtual-node/serial-virtual-node.js +++ b/lib/core/base/virtual-node/serial-virtual-node.js @@ -44,8 +44,8 @@ class SerialVirtualNode extends AbstractVirtualNode { function normaliseProps(serialNode) { let { nodeName, nodeType = 1 } = serialNode; assert( - nodeType === 1, - `nodeType has to be undefined or 1, got '${nodeType}'` + typeof nodeType === 'number', + `nodeType has to be a number, got '${nodeType}'` ); assert( typeof nodeName === 'string', diff --git a/lib/core/base/virtual-node/virtual-node.js b/lib/core/base/virtual-node/virtual-node.js index 7445efd08e..90473d273a 100644 --- a/lib/core/base/virtual-node/virtual-node.js +++ b/lib/core/base/virtual-node/virtual-node.js @@ -49,14 +49,15 @@ class VirtualNode extends AbstractVirtualNode { // abstract Node properties so we can run axe in DOM-less environments. // add to the prototype so memory is shared across all virtual nodes get props() { - const { nodeType, nodeName, id, multiple } = this.actualNode; + const { nodeType, nodeName, id, multiple, nodeValue } = this.actualNode; return { nodeType, nodeName: this._isXHTML ? nodeName : nodeName.toLowerCase(), id, type: this._type, - multiple + multiple, + nodeValue }; } diff --git a/lib/core/public/run-virtual-rule.js b/lib/core/public/run-virtual-rule.js index 19d16a4e8c..1a3c292b16 100644 --- a/lib/core/public/run-virtual-rule.js +++ b/lib/core/public/run-virtual-rule.js @@ -14,10 +14,7 @@ function runVirtualRule(ruleId, vNode, options = {}) { options.reporter = options.reporter || axe._audit.reporter || 'v1'; axe._selectorData = {}; - // TODO: this is a ridiculous hack since webpack is making these two - // separate functions - // TODO: es-module-AbstractVirtualNode - if (!axe._isAbstractNode(vNode)) { + if (!(vNode instanceof axe.AbstractVirtualNode)) { vNode = new axe.SerialVirtualNode(vNode); } diff --git a/lib/standards/html-elms.js b/lib/standards/html-elms.js index 3e6a0c2457..57d16f60d3 100644 --- a/lib/standards/html-elms.js +++ b/lib/standards/html-elms.js @@ -137,7 +137,7 @@ const htmlElms = { 'tab' ], // 5.4 button Element - namingMethods: 'subtreeText' + namingMethods: ['subtreeText'] }, canvas: { allowedRoles: true, @@ -220,7 +220,7 @@ const htmlElms = { contentTypes: ['flow'], allowedRoles: ['none', 'presentation', 'radiogroup'], // 5.5 fieldset and legend Elements - namingMethods: 'fieldsetLegendText' + namingMethods: ['fieldsetLegendText'] }, figcaption: { allowedRoles: ['group', 'none', 'presentation'] @@ -359,7 +359,7 @@ const htmlElms = { } }, // 5.10 img Element - namingMethods: 'altText' + namingMethods: ['altText'] }, input: { variant: { @@ -504,7 +504,7 @@ const htmlElms = { }, // 5.1 input type="text", input type="password", input type="search", input type="tel", input type="url" and textarea Element // 5.7 Other Form Elements - namingMethods: 'labelText' + namingMethods: ['labelText'] } } }, @@ -512,7 +512,7 @@ const htmlElms = { contentTypes: ['phrasing', 'flow'], allowedRoles: true }, - kdb: { + kbd: { contentTypes: ['phrasing', 'flow'], allowedRoles: true }, @@ -647,10 +647,10 @@ const htmlElms = { contentTypes: ['phrasing', 'flow'], allowedRoles: true, // 5.6 output Element - namingMethods: 'subtreeText' + namingMethods: ['subtreeText'] }, p: { - contentTypes: ['phrasing', 'flow'], + contentTypes: ['flow'], allowedRoles: true, shadowRoot: true }, @@ -811,10 +811,9 @@ const htmlElms = { allowedRoles: true }, summary: { - contentTypes: ['phrasing', 'flow'], allowedRoles: false, // 5.8 summary Element - namingMethods: 'subtreeText' + namingMethods: ['subtreeText'] }, sup: { contentTypes: ['phrasing', 'flow'], @@ -841,7 +840,7 @@ const htmlElms = { 'aria-valuenow': '', 'aria-multiline': 'true' }, - namingMethods: 'labelText' + namingMethods: ['labelText'] }, tfoot: { allowedRoles: true diff --git a/test/checks/aria/no-implicit-explicit-label.js b/test/checks/aria/no-implicit-explicit-label.js index 88b8de3809..4873fc4a5b 100644 --- a/test/checks/aria/no-implicit-explicit-label.js +++ b/test/checks/aria/no-implicit-explicit-label.js @@ -44,7 +44,7 @@ describe('no-implicit-explicit-label', function() { }); describe('SerialVirtualNode', function() { - it('should return undefined', function() { + it('should return false', function() { var serialNode = new axe.SerialVirtualNode({ nodeName: 'div', attributes: { @@ -54,7 +54,7 @@ describe('no-implicit-explicit-label', function() { }); var actual = check.evaluate.call(checkContext, null, {}, serialNode); - assert.isUndefined(actual); + assert.isFalse(actual); }); }); }); diff --git a/test/checks/shared/button-has-visible-text.js b/test/checks/shared/button-has-visible-text.js index 9c64c249fe..24d971cde7 100644 --- a/test/checks/shared/button-has-visible-text.js +++ b/test/checks/shared/button-has-visible-text.js @@ -54,13 +54,52 @@ describe('button-has-visible-text', function() { }); describe('SerialVirtualNode', function() { - it('should return undefined if no other attributes are provided', function() { + it('should return incomplete if no children are passed', function() { var node = new axe.SerialVirtualNode({ nodeName: 'button' }); assert.isUndefined( - axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node) + axe.testUtils.getCheckEvaluate('button-has-visible-text')( + null, + {}, + node + ) + ); + }); + + it('should return false if button element is empty', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'button' + }); + node.children = []; + + assert.isFalse( + axe.testUtils.getCheckEvaluate('button-has-visible-text')( + null, + {}, + node + ) + ); + }); + + it('should return true if a button element has text', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'button' + }); + var child = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'Text' + }); + node.children = [child]; + + assert.isTrue( + axe.testUtils.getCheckEvaluate('button-has-visible-text')( + null, + {}, + node + ) ); }); }); diff --git a/test/checks/shared/has-visible-text.js b/test/checks/shared/has-visible-text.js index 342f77addb..174ea8fdfc 100644 --- a/test/checks/shared/has-visible-text.js +++ b/test/checks/shared/has-visible-text.js @@ -52,7 +52,7 @@ describe('has-visible-text', function() { ); }); - it('should return undefined if element is named from contents', function() { + it('should return incomplete if no other properties are set', function() { var node = new axe.SerialVirtualNode({ nodeName: 'button' }); @@ -61,5 +61,32 @@ describe('has-visible-text', function() { axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node) ); }); + + it('should return false if there is no visible text', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'button' + }); + node.children = []; + + assert.isFalse( + axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node) + ); + }); + + it('should return true if there is visible text', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'object' + }); + var child = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'hello!' + }); + node.children = [child]; + + assert.isTrue( + axe.testUtils.getCheckEvaluate('has-visible-text')(null, {}, node) + ); + }); }); }); diff --git a/test/commons/standards/get-element-spec.js b/test/commons/standards/get-element-spec.js index ba326a7d28..92dcc1fc7d 100644 --- a/test/commons/standards/get-element-spec.js +++ b/test/commons/standards/get-element-spec.js @@ -34,6 +34,11 @@ describe('standards.getElementSpec', function() { }); }); + it('should return empty object if passed an invalid element', function() { + var vNode = queryFixture(''); + assert.deepEqual(getElementSpec(vNode), {}); + }); + describe('variants', function() { before(function() { axe.configure({ diff --git a/test/core/base/virtual-node/serial-virtual-node.js b/test/core/base/virtual-node/serial-virtual-node.js index 67abbf034e..11a5d8def4 100644 --- a/test/core/base/virtual-node/serial-virtual-node.js +++ b/test/core/base/virtual-node/serial-virtual-node.js @@ -33,13 +33,21 @@ describe('SerialVirtualNode', function() { assert.equal(vNode.props.nodeType, 1); }); + it('takes 3 as its nodeType', function() { + var vNode = new SerialVirtualNode({ + nodeType: 3, + nodeName: '#text' + }); + assert.equal(vNode.props.nodeType, 3); + }); + it('has a default nodeType of 1', function() { var vNode = new SerialVirtualNode({ nodeName: 'div' }); assert.equal(vNode.props.nodeType, 1); }); - it('throws if nodeType anything else', function() { - [2, 3, true, 'one', '1', null, { foo: 'bar' }].forEach(function( + it('throws if nodeType is a not a number', function() { + [true, 'one', '1', null, { foo: 'bar' }].forEach(function( throwingNodeType ) { assert.throws(function() { diff --git a/test/integration/virtual-rules/index.html b/test/integration/virtual-rules/index.html index 7ac9c872b1..f4907ddc1b 100644 --- a/test/integration/virtual-rules/index.html +++ b/test/integration/virtual-rules/index.html @@ -18,7 +18,6 @@ }); var assert = chai.assert; -
@@ -26,6 +25,7 @@ + diff --git a/test/integration/virtual-rules/object-alt.js b/test/integration/virtual-rules/object-alt.js new file mode 100644 index 0000000000..5782c85c55 --- /dev/null +++ b/test/integration/virtual-rules/object-alt.js @@ -0,0 +1,156 @@ +describe('object-alt', function() { + it('should pass for aria-label', function() { + var results = axe.runVirtualRule('object-alt', { + nodeName: 'object', + attributes: { + 'aria-label': 'foobar' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should incomplete for aria-labelledby', function() { + var results = axe.runVirtualRule('object-alt', { + nodeName: 'object', + attributes: { + 'aria-labelledby': 'foobar' + } + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); + + it('should pass for title', function() { + var results = axe.runVirtualRule('object-alt', { + nodeName: 'object', + attributes: { + title: 'foobar' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass for role=presentation', function() { + var results = axe.runVirtualRule('object-alt', { + nodeName: 'object', + attributes: { + role: 'presentation' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass for role=none', function() { + var results = axe.runVirtualRule('object-alt', { + nodeName: 'object', + attributes: { + role: 'none' + } + }); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should pass for visible text content', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'object' + }); + var child = new axe.SerialVirtualNode({ + nodeName: '#text', + nodeType: 3, + nodeValue: 'foobar' + }); + node.children = [child]; + + var results = axe.runVirtualRule('object-alt', node); + + assert.lengthOf(results.passes, 1); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 0); + }); + + it('should incomplete when alt and children are missing', function() { + var results = axe.runVirtualRule('object-alt', { + nodeName: 'object', + attributes: {} + }); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 0); + assert.lengthOf(results.incomplete, 1); + }); + + it('should fail children contain no visible text', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'object' + }); + node.children = []; + + var results = axe.runVirtualRule('object-alt', node); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail when alt contains only whitespace', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'object', + attributes: { + alt: ' \t \n ' + } + }); + node.children = []; + + var results = axe.runVirtualRule('object-alt', node); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail when aria-label is empty', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'object', + attributes: { + alt: '' + } + }); + node.children = []; + + var results = axe.runVirtualRule('object-alt', node); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); + + it('should fail when title is empty', function() { + var node = new axe.SerialVirtualNode({ + nodeName: 'object', + attributes: { + title: '' + } + }); + node.children = []; + + var results = axe.runVirtualRule('object-alt', node); + + assert.lengthOf(results.passes, 0); + assert.lengthOf(results.violations, 1); + assert.lengthOf(results.incomplete, 0); + }); +});