enketo-validate
Advanced tools
Comparing version 2.0.1 to 2.1.0
@@ -5,2 +5,9 @@ ## Change Log | ||
[2.1.0] - 2023-02-03 | ||
-------------------------- | ||
##### Added | ||
- Check for missing value elements in choice options. | ||
- Check that only one of OpenClinica's _external signature_ questions are present. | ||
- Check that value of OpenClinica's _external signature_ question is "1". | ||
[2.0.1] - 2022-11-07 | ||
@@ -7,0 +14,0 @@ -------------------------- |
{ | ||
"name": "enketo-validate", | ||
"version": "2.0.1", | ||
"version": "2.1.0", | ||
"description": "An XForm validator around Enketo's form engine", | ||
@@ -31,4 +31,4 @@ "main": "src/validator.js", | ||
"dependencies": { | ||
"commander": "^9.4.1", | ||
"enketo-transformer": "^2.1.6", | ||
"commander": "^10.0.0", | ||
"enketo-transformer": "^2.1.7", | ||
"enketo-xpath-extensions-oc": "git+https://github.com/OpenClinica/enketo-xpath-extensions-oc.git#727803c", | ||
@@ -35,0 +35,0 @@ "jsdom": "^20.0.2", |
@@ -247,3 +247,3 @@ const utils = require( '../build/utils-cjs-bundle' ); | ||
} ); | ||
*/ | ||
*/ | ||
@@ -363,13 +363,21 @@ return page.evaluateHandle( ( modelStr, externalArr, ocExtensions ) => { | ||
// These are the elements we expect to have a label though we're going slightly beyond spec requirement here. | ||
this.formControls.concat( this.items ) | ||
.forEach( control => { | ||
// The selector ":scope > label" fails with namespaced elements such as odk:rank | ||
// TODO: after https://github.com/XLSForm/pyxform/issues/439 has been implemented remove "|| el.nodeName === 'hint'". | ||
if ( ![ ...control.childNodes ].some( el => el.nodeName === 'label' || el.nodeName === 'hint' ) ) { | ||
const type = control.nodeName === 'item' || control.nodeName === 'itemset' ? 'Select option for question' : 'Question'; | ||
const nodeName = this._nodeName( control,'ref' ) || this._nodeName( control.parentElement, 'ref' ) || '?'; | ||
errors.push( `${type} "${nodeName}" has no label.` ); | ||
} | ||
} ); | ||
this.formControls.forEach( control => { | ||
// The selector ":scope > label" fails with namespaced elements such as odk:rank | ||
if ( ![ ...control.childNodes ].some( el => el.nodeName === 'label' ) ) { | ||
const nodeName = this._nodeName( control,'ref' ) || '?'; | ||
errors.push( `Question "${nodeName}" has no label.` ); | ||
} | ||
} ); | ||
this.items.forEach( item => { | ||
if ( ![ ...item.childNodes ].some( el => el.nodeName === 'label' ) ){ | ||
const nodeName = this._nodeName( item.parentElement, 'ref' ) || '?'; | ||
errors.push( `Select option for question "${nodeName}" has no label.` ); | ||
} | ||
if ( ![ ...item.childNodes ].some( el => el.nodeName === 'value' ) ){ | ||
const nodeName = this._nodeName( item.parentElement, 'ref' ) || '?'; | ||
errors.push( `Select option for question "${nodeName}" has no value.` ); | ||
} | ||
} ); | ||
let modelEl; | ||
@@ -650,5 +658,11 @@ if ( headEl ) { | ||
this.binds | ||
.filter( bind => bind.getAttributeNS( this.NAMESPACES.oc, 'external' ) === 'signature' ) | ||
.filter( bind => { | ||
const externalSignatureQuestions = this.binds | ||
.filter( bind => bind.getAttributeNS( this.NAMESPACES.oc, 'external' ) === 'signature' ); | ||
if ( externalSignatureQuestions.length > 1 ){ | ||
errors.push( 'Consent forms can only include one signature item.' ); | ||
} | ||
externalSignatureQuestions | ||
.forEach( bind => { | ||
const path = bind.getAttribute( 'nodeset' ); | ||
@@ -658,8 +672,11 @@ const select = this.doc.querySelector( `select[ref="${path}"]` ); | ||
const options = select ? select.querySelectorAll( 'item' ) : []; | ||
const valueEl = options[0] ? options[0].querySelector( 'value' ) : null; | ||
return !select || options.length !== 1 || | ||
( appearanceVal && appearanceVal.trim().split( ' ' ).includes( 'minimal' ) ); | ||
} ) | ||
.map( bind => this._nodeName( bind ) ) | ||
.forEach( () => errors.push( 'Signature items must be of type "select_multiple" with one option.' ) ); | ||
if( !select || options.length !== 1 || | ||
( appearanceVal && appearanceVal.trim().split( ' ' ).includes( 'minimal' ) ) ){ | ||
errors.push( 'Signature items must be of type "select_multiple" with one option.' ); | ||
} else if ( valueEl && valueEl.textContent !== '1' ){ | ||
errors.push( 'Signature items must have choice name set to "1"' ); | ||
} | ||
} ); | ||
@@ -666,0 +683,0 @@ this.binds |
@@ -18,2 +18,8 @@ const XForm = require( '../../src/xform' ).XForm; | ||
} ); | ||
it( 'returns no errors and no warnings', async() => { | ||
const result = await validator.validate( xf ); | ||
expect( result.errors.length ).to.equal( 0 ); | ||
expect( result.warnings.length ).to.equal( 0 ); | ||
} ); | ||
} ); | ||
@@ -154,3 +160,3 @@ | ||
it( 'outputs errors', async() => { | ||
it( 'returns errors', async() => { | ||
const result = await validation; | ||
@@ -168,3 +174,3 @@ expect( arrContains( result.errors, /"a" has a calculation that is not set to readonly/i ) ).to.equal( true ); | ||
it( 'outputs errors', async() => { | ||
it( 'returns errors', async() => { | ||
const result = await validation; | ||
@@ -174,3 +180,3 @@ expect( result.errors.length ).to.equal( 6 ); | ||
it( 'outputs errors for calculations without form control that refer to external ' + | ||
it( 'returns errors for calculations without form control that refer to external ' + | ||
'clinicaldata instance but do not have the oc:external="clinicaldata" bind', async() => { | ||
@@ -183,3 +189,3 @@ const result = await validation; | ||
it( 'outputs errors for binds with oc:external="clinicaldata" that do not ' + | ||
it( 'returns errors for binds with oc:external="clinicaldata" that do not ' + | ||
'do not have a calculation that refers to instance(\'clinicaldata\')', async() => { | ||
@@ -194,13 +200,24 @@ const result = await validation; | ||
describe( 'forms with the special signature extensions', ()=>{ | ||
const validation = validator.validate( loadXForm( 'openclinica-external-signature.xml' ), { | ||
describe( 'forms with the special signature extensions ', ()=>{ | ||
const validation1 = validator.validate( loadXForm( 'openclinica-external-signature-invalid.xml' ), { | ||
openclinica: true | ||
} ); | ||
const validation2 = validator.validate( loadXForm( 'openclinica-external-signature-valid.xml' ), { | ||
openclinica: true | ||
} ); | ||
it( 'outputs warnings for non-checkbox questions or questions with more than 1 checkbox', async()=>{ | ||
const result = await validation; | ||
it( 'passes without errors and warnings when defined correctly', async()=>{ | ||
const result = await validation2; | ||
expect( result.warnings.length ).to.equal( 0 ); | ||
expect( result.errors.length ).to.equal( 9 ); | ||
expect( result.errors.every( error => error.includes( 'Signature' ) ) ).to.equal( true ); | ||
expect( result.errors.length ).to.equal( 0 ); | ||
} ); | ||
it( 'returns errors when defined incorrectly', async()=>{ | ||
const result = await validation1; | ||
expect( result.warnings.length ).to.equal( 0 ); | ||
expect( result.errors.length ).to.equal( 11 ); | ||
expect ( arrContains( result.errors, /Signature .* choice name set to "1"/ ) ).to.equal( true ); | ||
expect ( arrContains( result.errors, /only include one signature item/ ) ).to.equal( true ); | ||
expect ( arrContains( result.errors, /Signature .* must be of type "select_multiple" with one option/ ) ).to.equal( true ); | ||
} ); | ||
} ); | ||
@@ -213,3 +230,3 @@ | ||
it( 'outputs errors', async() => { | ||
it( 'returns errors', async() => { | ||
const result = await validation; | ||
@@ -234,3 +251,3 @@ expect( result.errors.length ).to.equal( 9 ); | ||
it( 'outputs an error', async() => { | ||
it( 'returns an error', async() => { | ||
const result = await validation; | ||
@@ -248,3 +265,3 @@ expect( result.errors.length ).to.equal( 1 ); | ||
it( 'does not output an error', async() => { | ||
it( 'does not return an error', async() => { | ||
const result = await validation; | ||
@@ -264,3 +281,3 @@ expect( result.errors.length ).to.equal( 0 ); | ||
it( 'outputs warnings', async() => { | ||
it( 'returns warnings', async() => { | ||
const result = await validation; | ||
@@ -285,3 +302,3 @@ | ||
it( 'outputs 1 error', async() => { | ||
it( 'returns 1 error', async() => { | ||
const result = await validation; | ||
@@ -292,3 +309,3 @@ expect( result.errors.length ).to.equal( ERRORS ); | ||
it( 'outputs 1 error with --oc flag', async() => { | ||
it( 'returns 1 error with --oc flag', async() => { | ||
const resultOc = await validationOc; | ||
@@ -299,3 +316,3 @@ expect( resultOc.errors.length ).to.equal( ERRORS ); | ||
it( 'outputs warnings with --oc flag too', async() => { | ||
it( 'returns warnings with --oc flag too', async() => { | ||
const resultOc = await validationOc; | ||
@@ -306,3 +323,3 @@ //expect( arrContains( result.warnings, /deprecated/ ) ).to.equal( false ); | ||
it( 'including the special case "horizontal" output warnings', async() => { | ||
it( 'including the special case "horizontal" return warnings', async() => { | ||
const result = await validator.validate( loadXForm( 'appearance-horizontal.xml' ) ); | ||
@@ -331,3 +348,3 @@ | ||
it( 'outputs warnings', async() => { | ||
it( 'returns warnings', async() => { | ||
const result = await validation; | ||
@@ -346,3 +363,3 @@ | ||
it( 'outputs warnings', async() => { | ||
it( 'returns warnings', async() => { | ||
const result = await validation; | ||
@@ -370,3 +387,3 @@ | ||
it( 'outputs warnings', async() => { | ||
it( 'returns warnings', async() => { | ||
const result = await validation; | ||
@@ -383,3 +400,3 @@ | ||
it( 'outputs errors', async() => { | ||
it( 'returns errors', async() => { | ||
const result = await validator.validate( loadXForm( 'missing-labels.xml' ) ); | ||
@@ -396,3 +413,3 @@ const ISSUES = 6; | ||
it( 'does not output errors for setvalue actions without a label', async() => { | ||
it( 'does not return errors for setvalue actions without a label', async() => { | ||
const result = await validator.validate( loadXForm( 'setvalue.xml' ) ); | ||
@@ -404,5 +421,20 @@ expect( result.errors.length ).to.equal( 0 ); | ||
describe( 'with missing <value> elements', () => { | ||
it( 'returns errors', async() => { | ||
const result = await validator.validate( loadXForm( 'missing-values.xml' ) ); | ||
expect( result.errors.length ).to.equal( 1 ); | ||
expect( arrContains( result.errors, /option for question "k" has no value/i ) ).to.equal( true ); | ||
} ); | ||
it( 'does not return errors for setvalue actions without a label', async() => { | ||
const result = await validator.validate( loadXForm( 'setvalue.xml' ) ); | ||
expect( result.errors.length ).to.equal( 0 ); | ||
} ); | ||
} ); | ||
describe( 'with duplicate nodenames', () => { | ||
it( 'outputs warnings', async() => { | ||
it( 'returns warnings', async() => { | ||
const result = await validator.validate( loadXForm( 'duplicate-nodename.xml' ) ); | ||
@@ -419,3 +451,3 @@ expect( result.warnings.length ).to.equal( 2 ); | ||
it( 'outputs warnings', async() => { | ||
it( 'returns warnings', async() => { | ||
const result = await validator.validate( loadXForm( 'nodename-underscore.xml' ) ); | ||
@@ -430,3 +462,3 @@ expect( result.warnings.length ).to.equal( 0 ); | ||
it( 'outputs warnings', async() => { | ||
it( 'returns warnings', async() => { | ||
const result = await validator.validate( loadXForm( 'nested-repeats.xml' ) ); | ||
@@ -442,3 +474,3 @@ expect( result.warnings.length ).to.equal( 2 ); | ||
it( 'outputs errors for disallowed self-referencing', async() => { | ||
it( 'returns errors for disallowed self-referencing', async() => { | ||
// Unit tests are in xpath.spec.js | ||
@@ -445,0 +477,0 @@ const result = await validator.validate( loadXForm( 'self-reference.xml' ) ); |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
4110470
105
71347
+ Addedcommander@10.0.1(transitive)
- Removedcommander@9.5.0(transitive)
Updatedcommander@^10.0.0
Updatedenketo-transformer@^2.1.7