diff --git a/src/lib/validation/form/note-countdown-timer.js b/src/lib/validation/form/note-countdown-timer.js new file mode 100644 index 00000000..bfd1dc27 --- /dev/null +++ b/src/lib/validation/form/note-countdown-timer.js @@ -0,0 +1,37 @@ +const semver = require('semver'); +const { getNodes, getBindNodes } = require('../../forms-utils'); + +const validateNoteCountdownTimer = async ({ xformPath, xmlDoc, apiVersion }) => { + if (!apiVersion) { + return { errors: [], warnings: [] }; + } + if (semver.lt(apiVersion, '4.7.0')) { + return { errors: [], warnings: [] }; + } + + const bindNodes = getBindNodes(xmlDoc); + + const warnings = getNodes(xmlDoc, '/h:html/h:body//*[@appearance]') + .filter(node => node.getAttribute('appearance').match(/(?:^|\s)countdown-timer(?:$|\s)/)) + .map(node => node.getAttribute('ref')) + .filter(ref => { + const bind = bindNodes.find(b => b.getAttribute('nodeset') === ref); + return bind?.hasAttribute('readonly'); + }) + .map(ref => ` - ${ref}`); + + if (warnings.length) { + warnings.unshift( + `Form at ${xformPath} contains fields with the deprecated countdown-timer note appearance. ` + + 'Please update the following fields to use trigger fields instead:' + ); + } + + return { errors: [], warnings }; +}; + +module.exports = { + requiresInstance: true, + skipFurtherValidation: false, + execute: validateNoteCountdownTimer +}; diff --git a/test/lib/validate-forms.spec.js b/test/lib/validate-forms.spec.js index b720487d..5a754751 100644 --- a/test/lib/validate-forms.spec.js +++ b/test/lib/validate-forms.spec.js @@ -77,6 +77,11 @@ describe('validate-forms', () => { expect(noRequiredNotes.requiresInstance).to.equal(false); expect(noRequiredNotes.skipFurtherValidation).to.equal(false); + const noteCountdownTimer = validations.shift(); + expect(noteCountdownTimer.name).to.equal('note-countdown-timer.js'); + expect(noteCountdownTimer.requiresInstance).to.equal(true); + expect(noteCountdownTimer.skipFurtherValidation).to.equal(false); + expect(validations, 'Update this test if you have added a new form validation.').to.be.empty; }); diff --git a/test/lib/validation/form/note-countdown-timer.spec.js b/test/lib/validation/form/note-countdown-timer.spec.js new file mode 100644 index 00000000..12347220 --- /dev/null +++ b/test/lib/validation/form/note-countdown-timer.spec.js @@ -0,0 +1,106 @@ +const { expect } = require('chai'); +const { DOMParser } = require('@xmldom/xmldom'); +const noteCountdownTimer = require('../../../../src/lib/validation/form/note-countdown-timer'); + +const domParser = new DOMParser(); +const xformPath = 'test/form/path.xml'; + +const getXml = ({ deprecated = false, newStyle = false } = {}) => ` + + + + + + + ${deprecated ? '15' : ''} + ${newStyle ? '' : ''} + + + ${deprecated ? '' : ''} + ${newStyle ? '' : ''} + + + + ${deprecated + ? '' : ''} + ${newStyle + ? '' : ''} + +`; + +const getXmlDoc = (opts) => domParser.parseFromString(getXml(opts), 'text/xml'); + +const LATEST_VERSION = '999.99.99'; +const ERROR_HEADER = `Form at ${xformPath} contains fields with the deprecated countdown-timer note appearance. ` + + 'Please update the following fields to use trigger fields instead:'; + +describe('note-countdown-timer', () => { + it('resolves OK when no countdown-timer fields present', () => { + return noteCountdownTimer + .execute({ xformPath, xmlDoc: getXmlDoc(), apiVersion: LATEST_VERSION }) + .then(({ errors, warnings }) => { + expect(errors).is.empty; + expect(warnings).is.empty; + }); + }); + + it('returns warning for deprecated countdown-timer note field', () => { + return noteCountdownTimer + .execute({ xformPath, xmlDoc: getXmlDoc({ deprecated: true }), apiVersion: LATEST_VERSION }) + .then(({ errors, warnings }) => { + expect(errors).is.empty; + expect(warnings).to.have.length(2); + expect(warnings[0]).to.equal(ERROR_HEADER); + expect(warnings[1]).to.equal(' - /data/deprecated_timer'); + }); + }); + + it('resolves OK for new trigger style countdown-timer', () => { + return noteCountdownTimer + .execute({ xformPath, xmlDoc: getXmlDoc({ newStyle: true }), apiVersion: LATEST_VERSION }) + .then(({ errors, warnings }) => { + expect(errors).is.empty; + expect(warnings).is.empty; + }); + }); + + it('returns warning only for deprecated when both deprecated and new style present', () => { + return noteCountdownTimer + .execute({ xformPath, xmlDoc: getXmlDoc({ deprecated: true, newStyle: true }), apiVersion: LATEST_VERSION }) + .then(({ errors, warnings }) => { + expect(errors).is.empty; + expect(warnings).to.have.length(2); + expect(warnings[0]).to.equal(ERROR_HEADER); + expect(warnings[1]).to.equal(' - /data/deprecated_timer'); + }); + }); + + it('resolves OK when apiVersion is below 4.7.0', () => { + return noteCountdownTimer + .execute({ xformPath, xmlDoc: getXmlDoc({ deprecated: true }), apiVersion: '4.6.0' }) + .then(({ errors, warnings }) => { + expect(errors).is.empty; + expect(warnings).is.empty; + }); + }); + + it('resolves OK when no apiVersion provided', () => { + return noteCountdownTimer + .execute({ xformPath, xmlDoc: getXmlDoc({ deprecated: true }) }) + .then(({ errors, warnings }) => { + expect(errors).is.empty; + expect(warnings).is.empty; + }); + }); + + it('returns warning when apiVersion is exactly 4.7.0', () => { + return noteCountdownTimer + .execute({ xformPath, xmlDoc: getXmlDoc({ deprecated: true }), apiVersion: '4.7.0' }) + .then(({ errors, warnings }) => { + expect(errors).is.empty; + expect(warnings).to.have.length(2); + expect(warnings[0]).to.equal(ERROR_HEADER); + expect(warnings[1]).to.equal(' - /data/deprecated_timer'); + }); + }); +});