diff --git a/src/js/calculate.js b/src/js/calculate.js index ff6ca8d4f..7955451f7 100644 --- a/src/js/calculate.js +++ b/src/js/calculate.js @@ -204,14 +204,38 @@ export default { const nodes = this._getNodesForAction(action, event); nodes.forEach((actionControl) => { - const name = this.form.input.getName(actionControl); - const dataNodesObj = this.form.model.node(name); + let name = this.form.input.getName(actionControl); + + let affectedControl; + + if ( + name == null || + name === '' || + actionControl.matches( + `[name="${CSS.escape(name)}"] ~ [name="${CSS.escape( + name + )}"]` + ) + ) { + affectedControl = actionControl + .closest('.question') + ?.querySelector( + ':is([data-name], [name]):not([data-name=""], [data-event], [name=""])' + ); + + name = this.form.input.getName(affectedControl); + } else { + affectedControl = actionControl; + } + + // TODO: should this pass `{ onlyLeaf: true }` option? + const dataNodesObj = this.form.model.node(name, null); const dataNodes = dataNodesObj.getElements(); const props = { name, - dataType: this.form.input.getXmlType(actionControl), - relevantExpr: this.form.input.getRelevant(actionControl), + dataType: this.form.input.getXmlType(affectedControl), + relevantExpr: this.form.input.getRelevant(affectedControl), index: event.detail && typeof event.detail.repeatIndex !== 'undefined' @@ -222,7 +246,7 @@ export default { }; if (action === 'setvalue') { - props.expr = actionControl.dataset.setvalue; + props.expr = affectedControl.dataset.setvalue; } if ( @@ -238,7 +262,7 @@ export default { */ dataNodes.forEach((el, index) => { const obj = Object.create(props); - const control = actionControl; + const control = affectedControl; obj.index = index; this._updateCalc(control, obj); }); @@ -253,7 +277,7 @@ export default { } this._updateCalc(control, props); } else if (dataNodes[props.index]) { - const control = actionControl; + const control = affectedControl; this._updateCalc(control, props); } else { console.error( diff --git a/src/widget/geo/geopicker.js b/src/widget/geo/geopicker.js index ea0baec7b..35b1dfc46 100644 --- a/src/widget/geo/geopicker.js +++ b/src/widget/geo/geopicker.js @@ -77,7 +77,7 @@ class Geopicker extends Widget { * @type {string} */ static get selector() { - return ':is(.question input[data-type-xml="geopoint"], .question input[data-type-xml="geotrace"], .question input[data-type-xml="geoshape"]):not([data-setvalue], [data-setgeopoint])'; + return '.question [name]:not([name=""], [type="hidden"]):is(input[data-type-xml="geopoint"], input[data-type-xml="geotrace"], input[data-type-xml="geoshape"])'; } /** diff --git a/test/forms/setgeopoint.xml b/test/forms/setgeopoint.xml index 04201d7f0..077cfa8af 100644 --- a/test/forms/setgeopoint.xml +++ b/test/forms/setgeopoint.xml @@ -12,7 +12,10 @@ - + + + + @@ -22,18 +25,36 @@ - + + + + - + + - - - + + + + + + + + + + + + + + + + + diff --git a/test/spec/setgeopoint.spec.js b/test/spec/setgeopoint.spec.js index 5374c8ef7..3d4c638aa 100644 --- a/test/spec/setgeopoint.spec.js +++ b/test/spec/setgeopoint.spec.js @@ -12,6 +12,18 @@ import events from '../../src/js/event'; */ describe('setgeopoint action', () => { + const actionInBodyNodes = { + visible_first_load_adjacent_action: 'action defined adjacent to input', + visible_first_load_nested_explicit_ref: + 'action nested in input with explicit ref', + visible_first_load_nested_implied_ref: + 'action nested in input with ref determined by parent', + }; + const firstLoadeNodes = { + ...actionInBodyNodes, + visible_first_load_model_action: 'action defined in model', + }; + describe('first load', () => { const mock = mockGetCurrentPosition( createTestCoordinates({ @@ -37,94 +49,96 @@ describe('setgeopoint action', () => { .then(done, done); }); - it('works for questions with odk-instance-first-load inside of the XForms body', (done) => { - const form1 = loadForm('setgeopoint.xml'); - form1.init(); - - mock.lookup - .then(({ geopoint }) => { - expect( - form1.model.xml.querySelector('visible_first_load') - .textContent - ).to.equal(geopoint); - }) - .then(done, done); - }); + for (const [nodeName, description] of Object.entries( + actionInBodyNodes + )) { + it(`works for questions with odk-instance-first-load inside of the XForms body (${description})`, (done) => { + const form1 = loadForm('setgeopoint.xml'); + form1.init(); + + mock.lookup + .then(({ geopoint }) => { + expect( + form1.model.xml.querySelector(nodeName).textContent + ).to.equal(geopoint); + }) + .then(done, done); + }); + } }); - describe('null `accuracy`', () => { - const mock = mockGetCurrentPosition( - createTestCoordinates({ - latitude: 48.66, - longitude: -120.5, - altitude: 123, - }), - { expectLookup: true } - ); - - it('substitutes a null `accuracy` value with 0.0', (done) => { - const form1 = loadForm('setgeopoint.xml'); - form1.init(); - - mock.lookup - .then(({ geopoint }) => { - expect( - form1.model.xml.querySelector('visible_first_load') - .textContent - ).to.equal(geopoint); - expect(geopoint).to.equal('48.66 -120.5 123 0.0'); - }) - .then(done, done); + for (const [nodeName, description] of Object.entries(firstLoadeNodes)) { + describe(`null 'accuracy' (${description})`, () => { + const mock = mockGetCurrentPosition( + createTestCoordinates({ + latitude: 48.66, + longitude: -120.5, + altitude: 123, + }), + { expectLookup: true } + ); + + it('substitutes a null `accuracy` value with 0.0', (done) => { + const form1 = loadForm('setgeopoint.xml'); + form1.init(); + + mock.lookup + .then(({ geopoint }) => { + expect( + form1.model.xml.querySelector(nodeName).textContent + ).to.equal(geopoint); + expect(geopoint).to.equal('48.66 -120.5 123 0.0'); + }) + .then(done, done); + }); }); - }); - describe('null `altitude`', () => { - const mock = mockGetCurrentPosition( - createTestCoordinates({ - latitude: 48.66, - longitude: -120.5, - accuracy: 2500.12, - }), - { expectLookup: true } - ); - - it('substitutes a null `altitude` value with 0.0', (done) => { - const form1 = loadForm('setgeopoint.xml'); - form1.init(); - - mock.lookup - .then(({ geopoint }) => { - expect( - form1.model.xml.querySelector('visible_first_load') - .textContent - ).to.equal(geopoint); - expect(geopoint).to.include('48.66 -120.5 0.0 2500.12'); - }) - .then(done, done); + describe(`null 'altitude' (${description})`, () => { + const mock = mockGetCurrentPosition( + createTestCoordinates({ + latitude: 48.66, + longitude: -120.5, + accuracy: 2500.12, + }), + { expectLookup: true } + ); + + it('substitutes a null `altitude` value with 0.0', (done) => { + const form1 = loadForm('setgeopoint.xml'); + form1.init(); + + mock.lookup + .then(({ geopoint }) => { + expect( + form1.model.xml.querySelector(nodeName).textContent + ).to.equal(geopoint); + expect(geopoint).to.include('48.66 -120.5 0.0 2500.12'); + }) + .then(done, done); + }); }); - }); - - describe('lookup failure', () => { - const mock = mockGetCurrentPosition( - createGeolocationLookupError('PERMISSION_DENIED'), - { expectLookup: true } - ); - - it('sets an empty string when lookup fails', (done) => { - const form1 = loadForm('setgeopoint.xml'); - form1.init(); - mock.lookup - .then(({ geopoint }) => { - expect( - form1.model.xml.querySelector('visible_first_load') - .textContent - ).to.equal(geopoint); - expect(geopoint).to.include(''); - }) - .then(done, done); + describe(`lookup failure (${description})`, () => { + const mock = mockGetCurrentPosition( + createGeolocationLookupError('PERMISSION_DENIED'), + { expectLookup: true } + ); + + it('sets an empty string when lookup fails', (done) => { + const form1 = loadForm('setgeopoint.xml'); + form1.init(); + + mock.lookup + .then(({ geopoint }) => { + expect( + form1.model.xml.querySelector(nodeName).textContent + ).to.equal(geopoint); + expect(geopoint).to.include(''); + }) + .then(done, done); + }); }); - }); + } }); describe('setgeopoint actions to populate a value if another value changes', () => { diff --git a/test/spec/widget.geo.spec.js b/test/spec/widget.geo.spec.js index f1f1a69e3..8014a2dba 100644 --- a/test/spec/widget.geo.spec.js +++ b/test/spec/widget.geo.spec.js @@ -132,4 +132,28 @@ describe('geoshape widget', () => { expect(geopickers.length).to.equal(0); }); + + it('loads the widget when a setgeopoint odk-instance-first-load action is defined adjacent to the input', () => { + const form = loadForm('setgeopoint.xml'); + + form.init(); + + const widget = form.view.html.querySelector( + '[name="/data/visible_first_load_adjacent_action"] ~ .geopicker' + ); + + expect(widget).not.to.be.null; + }); + + it('loads the widget when a setgeopoint odk-instance-first-load action is defined in the model', () => { + const form = loadForm('setgeopoint.xml'); + + form.init(); + + const widget = form.view.html.querySelector( + '[name="/data/visible_first_load_model_action"] ~ .geopicker' + ); + + expect(widget).not.to.be.null; + }); });