Skip to content

Commit f6863b8

Browse files
dkayiwaCopilot
andcommitted
LUI-208: Fix Node.appendChild error in search results with newer DWR util.js
The cellCreator function in OpenmrsSearch.js returned options.data directly when it was already a TD element. The newer DWR util.js (from org.openmrs.contrib) then calls cell.appendChild(data) on the returned cell, but since cell === data, this throws 'Node.appendChild: The new child is an ancestor of the parent' on pages like Find Duplicate Patients and concept search popups. Fix: Create a new TD element in cellCreator and transfer all attributes and child nodes from the original, so the returned cell is always a different object from the data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 0f65e29 commit f6863b8

File tree

4 files changed

+263
-2
lines changed

4 files changed

+263
-2
lines changed

omod/src/main/webapp/resources/scripts/dojo/src/widget/openmrs/OpenmrsSearch.js

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,8 +573,33 @@ dojo.widget.defineWidget(
573573
},
574574

575575
cellCreator: function(options) {
576-
if (dwr.util._isHTMLElement(options.data, "td") == true)
577-
return options.data;
576+
if (dwr.util._isHTMLElement(options.data, "td") == true) {
577+
// LUI-208: Create a new td and transfer content from the original.
578+
// The newer DWR util.js (from org.openmrs.contrib) always tries to
579+
// appendChild(data) on the cell returned by cellCreator. When the
580+
// cell IS the data (same object), this causes "Node.appendChild:
581+
// The new child is an ancestor of the parent". By returning a
582+
// different td object with the same content, we avoid this error.
583+
var original = options.data;
584+
var td = document.createElement("td");
585+
td.className = original.className;
586+
td.id = original.id;
587+
if (original.colSpan > 1) td.colSpan = original.colSpan;
588+
td.onclick = original.onclick;
589+
td.onmouseover = original.onmouseover;
590+
td.onmouseout = original.onmouseout;
591+
while (original.firstChild) {
592+
td.appendChild(original.firstChild);
593+
}
594+
// Clear the original to avoid duplicate ids and stale state
595+
// in case DWR appends it as a nested element
596+
original.className = '';
597+
original.id = '';
598+
original.onclick = null;
599+
original.onmouseover = null;
600+
original.onmouseout = null;
601+
return td;
602+
}
578603

579604
return document.createElement("td");
580605
},

omod/src/test/java/org/openmrs/web/dwr/DWRConceptServiceTest.java

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -431,4 +431,85 @@ public void findBatchOfConcepts_shouldNotReturnDuplicatesWhenSearchingByConceptU
431431
Assert.assertEquals(1, result.size());
432432
Assert.assertTrue(isConceptFound(expected, result));
433433
}
434+
435+
/**
436+
* LUI-208: Tests that findConcepts returns ConceptListItem objects with all the fields
437+
* needed by ConceptSearch.js getCellContent() method. The JS rendering code creates
438+
* DOM elements (anchor, span) from these fields and the newer DWR util.js from
439+
* org.openmrs.contrib fails with "Node.appendChild: The new child is an ancestor of
440+
* the parent" when cellCreator returns options.data directly as a TD.
441+
* This test exercises the code path triggered by the concept search popup in
442+
* programForm.jsp (Manage Programs > Add new program > Select concept).
443+
*
444+
* @see DWRConceptService#findConcepts(String,boolean,List,List,List,List,boolean)
445+
*/
446+
@Test
447+
public void findConcepts_shouldReturnConceptListItemsWithFieldsRequiredByJsRendering() throws Exception {
448+
List<Object> results = dwrConceptService.findConcepts("CD4", false, null, null, null, null, false);
449+
450+
// If results are found, verify each has the fields the JS rendering code needs
451+
for (Object result : results) {
452+
if (result instanceof ConceptListItem) {
453+
ConceptListItem item = (ConceptListItem) result;
454+
Assert.assertNotNull("conceptId is needed by ConceptSearch.js getCellContent() "
455+
+ "to create anchor elements that trigger the appendChild bug",
456+
item.getConceptId());
457+
Assert.assertNotNull("name is needed by getCellContent() to set span.innerHTML",
458+
item.getName());
459+
}
460+
}
461+
}
462+
463+
/**
464+
* LUI-208: Tests the concept search path used by programForm.jsp when searching
465+
* for concepts with class filter "Program". The ConceptSearch widget calls
466+
* DWRConceptService.findConcepts() with includeClassNames=["Program"] and the results
467+
* are rendered via dwr.util.addRows which fails with the newer util.js.
468+
*
469+
* @see DWRConceptService#findConcepts(String,boolean,List,List,List,List,boolean)
470+
*/
471+
@Test
472+
public void findConcepts_shouldReturnResultsForProgramClassConceptSearch() throws Exception {
473+
// This simulates the programForm.jsp concept search:
474+
// <div dojoType="ConceptSearch" widgetId="cSearch" conceptClasses="Program">
475+
// which triggers DWRConceptService.findConcepts(text, false, ["Program"], [], [], [], false)
476+
List<String> includeClassNames = new ArrayList<String>();
477+
includeClassNames.add("Program");
478+
479+
// Search for any text - even if no results, verify no error is thrown
480+
List<Object> results = dwrConceptService.findConcepts("test", false, includeClassNames, null, null, null, false);
481+
Assert.assertNotNull("findConcepts should return a non-null list even with no matches", results);
482+
483+
// All returned items should be ConceptListItem (not error strings)
484+
for (Object result : results) {
485+
if (result instanceof String) {
486+
// Strings are allowed as informational messages (e.g., "no results")
487+
continue;
488+
}
489+
Assert.assertTrue("Each non-string result should be ConceptListItem",
490+
result instanceof ConceptListItem);
491+
}
492+
}
493+
494+
/**
495+
* LUI-208: Tests that findConceptAnswers returns ConceptListItem objects with fields
496+
* required by the JS rendering pipeline. This path is triggered when using concept
497+
* search popups with showAnswers attribute (e.g., in observation forms).
498+
*
499+
* @see DWRConceptService#findConceptAnswers(String,Integer,boolean,boolean)
500+
*/
501+
@Test
502+
public void findConceptAnswers_shouldReturnResultsWithFieldsRequiredByJsRendering() throws Exception {
503+
List<Object> results = dwrConceptService.findConceptAnswers("", 21, false, true);
504+
505+
for (Object result : results) {
506+
Assert.assertTrue("Each answer should be a ConceptListItem with fields needed "
507+
+ "by getCellContent() which creates DOM elements that trigger the "
508+
+ "DWR util.js appendChild bug",
509+
result instanceof ConceptListItem);
510+
ConceptListItem item = (ConceptListItem) result;
511+
Assert.assertNotNull("conceptId is required by JS rendering", item.getConceptId());
512+
Assert.assertNotNull("name is required by JS rendering", item.getName());
513+
}
514+
}
434515
}

omod/src/test/java/org/openmrs/web/dwr/DWRPatientServiceTest.java

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.Collection;
1313
import java.util.List;
1414
import java.util.Map;
15+
import java.util.Vector;
1516

1617
import org.junit.Assert;
1718
import org.junit.Ignore;
@@ -134,4 +135,117 @@ public void findPatientsByIdentifier_shouldGetResultsForPatientsForGivenIdentifi
134135
Assert.assertEquals(2, resultObjects.size());
135136
}
136137

138+
/**
139+
* LUI-208: Tests the findDuplicatePatients DWR method that is called from
140+
* findDuplicatePatients.jsp. On legacyUI >=2.x this triggers a JavaScript error
141+
* "Node.appendChild: The new child is an ancestor of the parent" because the newer
142+
* DWR util.js from org.openmrs.contrib handles addRows differently than the old
143+
* org.openmrs.directwebremoting version.
144+
*
145+
* @see DWRPatientService#findDuplicatePatients(String[])
146+
*/
147+
@Test
148+
public void findDuplicatePatients_shouldReturnPatientListItemsForDuplicateGivenNames() throws Exception {
149+
executeDataSet("org/openmrs/web/dwr/include/DWRPatientService-duplicatePatients.xml");
150+
DWRPatientService dwrService = new DWRPatientService();
151+
Vector<Object> results = dwrService.findDuplicatePatients(new String[] { "givenName" });
152+
153+
// Should find the two patients with the same given name "DuplicateFirst"
154+
Assert.assertFalse("Should find duplicate patients by givenName", results.isEmpty());
155+
156+
// All results should be PatientListItem instances (not error strings)
157+
for (Object result : results) {
158+
Assert.assertTrue("Each result should be a PatientListItem, not a String. "
159+
+ "The JS cellCreator in OpenmrsSearch.js returns options.data directly when it is "
160+
+ "a TD element, and the newer DWR util.js then fails with appendChild error.",
161+
result instanceof PatientListItem);
162+
}
163+
}
164+
165+
/**
166+
* LUI-208: Tests that findDuplicatePatients returns results with all the fields needed
167+
* by the JavaScript getCellFunctions() in PatientSearch.js. These functions (getNumber,
168+
* getId, getGiven, getMiddle, getFamily, getAge, getGender, getBirthday) create TD
169+
* elements from PatientListItem properties. The newer DWR util.js fails when cellCreator
170+
* returns these TD elements directly (instead of creating new ones).
171+
*
172+
* @see DWRPatientService#findDuplicatePatients(String[])
173+
*/
174+
@Test
175+
public void findDuplicatePatients_shouldReturnResultsWithFieldsRequiredByJsRendering() throws Exception {
176+
executeDataSet("org/openmrs/web/dwr/include/DWRPatientService-duplicatePatients.xml");
177+
DWRPatientService dwrService = new DWRPatientService();
178+
Vector<Object> results = dwrService.findDuplicatePatients(new String[] { "givenName" });
179+
180+
Assert.assertFalse("Should return results for duplicate patients", results.isEmpty());
181+
182+
// Verify each result has the fields the JS rendering code needs.
183+
// The JS functions getId(), getGiven(), getFamily(), getGender(), getAge()
184+
// create TD elements and return them. When the newer DWR util.js processes
185+
// these via cellCreator -> addRows, it tries to appendChild(data) to the cell
186+
// that IS the data, causing "The new child is an ancestor of the parent".
187+
for (Object result : results) {
188+
PatientListItem item = (PatientListItem) result;
189+
Assert.assertNotNull("patientId is needed by getId() in PatientSearch.js", item.getPatientId());
190+
Assert.assertNotNull("identifier is needed by getId() which creates a TD with an anchor",
191+
item.getIdentifier());
192+
Assert.assertNotNull("givenName is needed by getGiven() in PersonSearch.js", item.getGivenName());
193+
Assert.assertNotNull("familyName is needed by getFamily() in PersonSearch.js", item.getFamilyName());
194+
Assert.assertNotNull("gender is needed by getGender() which creates a TD with an img element",
195+
item.getGender());
196+
}
197+
}
198+
199+
/**
200+
* LUI-208: Tests findDuplicatePatients with multiple search attributes.
201+
* This simulates the findDuplicatePatients.jsp flow where a user selects
202+
* multiple checkboxes (e.g., givenName + gender) before clicking Search.
203+
*
204+
* @see DWRPatientService#findDuplicatePatients(String[])
205+
*/
206+
@Test
207+
public void findDuplicatePatients_shouldReturnResultsWhenSearchingOnMultipleAttributes() throws Exception {
208+
executeDataSet("org/openmrs/web/dwr/include/DWRPatientService-duplicatePatients.xml");
209+
DWRPatientService dwrService = new DWRPatientService();
210+
211+
// Search on givenName + gender (both test patients have givenName="DuplicateFirst", gender="F")
212+
Vector<Object> results = dwrService.findDuplicatePatients(new String[] { "givenName", "gender" });
213+
214+
Assert.assertFalse("Should find duplicates when searching on multiple attributes", results.isEmpty());
215+
for (Object result : results) {
216+
Assert.assertTrue("Result should be PatientListItem not error string",
217+
result instanceof PatientListItem);
218+
}
219+
}
220+
221+
/**
222+
* LUI-208: Tests findPatientsByIdentifier returns PatientListItem objects with all
223+
* properties needed by the JavaScript rendering pipeline. This method is called from
224+
* findDuplicatePatients.jsp's "search by identifiers" feature, and the results are
225+
* rendered using the same dwr.util.addRows code that triggers the appendChild bug.
226+
*
227+
* @see DWRPatientService#findPatientsByIdentifier(String[])
228+
*/
229+
@Test
230+
public void findPatientsByIdentifier_shouldReturnResultsWithFieldsRequiredByJsRendering() throws Exception {
231+
executeDataSet("org/openmrs/web/dwr/include/DWRPatientService-findByIdentifier.xml");
232+
DWRPatientService dwrService = new DWRPatientService();
233+
Collection<Object> results = dwrService
234+
.findPatientsByIdentifier(new String[] { "Identifier1", "Identifier2" });
235+
236+
Assert.assertEquals(2, results.size());
237+
238+
// Verify results have all fields needed by the JS getCellFunctions pipeline
239+
// that triggers the DWR util.js appendChild bug
240+
for (Object result : results) {
241+
Assert.assertTrue("Each result must be PatientListItem", result instanceof PatientListItem);
242+
PatientListItem item = (PatientListItem) result;
243+
Assert.assertNotNull("patientId required by JS getNumber/getId", item.getPatientId());
244+
Assert.assertNotNull("identifier required by JS getId() TD creation", item.getIdentifier());
245+
Assert.assertFalse("identifier should not be empty", item.getIdentifier().isEmpty());
246+
Assert.assertNotNull("givenName required by JS getGiven() cell rendering", item.getGivenName());
247+
Assert.assertNotNull("familyName required by JS getFamily() cell rendering", item.getFamilyName());
248+
}
249+
}
250+
137251
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?xml version='1.0' encoding='UTF-8'?>
2+
<!--
3+
4+
This Source Code Form is subject to the terms of the Mozilla Public License,
5+
v. 2.0. If a copy of the MPL was not distributed with this file, You can
6+
obtain one at http://mozilla.org/MPL/2.0/. OpenMRS is also distributed under
7+
the terms of the Healthcare Disclaimer located at http://openmrs.org/license.
8+
9+
Copyright (C) OpenMRS Inc. OpenMRS is a registered trademark and the OpenMRS
10+
graphic logo is a trademark of OpenMRS Inc.
11+
12+
-->
13+
<!--
14+
Test data for LUI-208: duplicate patients with shared attributes.
15+
Two patients share the same given name so findDuplicatePatients(["givenName"]) returns results.
16+
-->
17+
<dataset>
18+
<person person_id="30" gender="F" dead="false" creator="1" birthdate="1990-01-01" birthdate_estimated="0"
19+
date_created="2008-08-15 15:57:09.0" voided="false" void_reason=""
20+
uuid="85e04d42-3ca8-11e3-bf2b-ab87271c1b75"/>
21+
<patient patient_id="30" creator="1" date_created="2005-09-21 00:00:00.0" changed_by="1"
22+
date_changed="2008-08-18 12:29:59.0" voided="false" void_reason=""/>
23+
<person_name person_name_id="30" preferred="true" person_id="30" prefix="" given_name="DuplicateFirst" middle_name=""
24+
family_name="Smith" family_name_suffix="" creator="1" date_created="2005-09-21 00:00:00.0"
25+
voided="false" void_reason="" uuid="509eef34-6482-487d-94ce-c07bb3ca3cca"/>
26+
<patient_identifier date_created="2005-09-21 00:00:00" patient_identifier_id="30" patient_id="30"
27+
identifier="DupId1" identifier_type="1" creator="1"
28+
uuid="8db74fd1-128d-ef12-842f-81d3535da3a6" preferred="1" voided="0"/>
29+
30+
<person person_id="31" gender="F" dead="false" creator="1" birthdate="1990-01-01" birthdate_estimated="0"
31+
date_created="2008-08-15 15:57:09.0" voided="false" void_reason=""
32+
uuid="86e04d42-3ca8-11e3-bf2b-ab87271c1b75"/>
33+
<patient patient_id="31" creator="1" date_created="2005-09-22 00:00:00.0" changed_by="1"
34+
date_changed="2008-08-18 12:29:59.0" voided="false" void_reason=""/>
35+
<person_name person_name_id="31" preferred="true" person_id="31" prefix="" given_name="DuplicateFirst" middle_name=""
36+
family_name="Jones" family_name_suffix="" creator="1" date_created="2005-09-22 00:00:00.0"
37+
voided="false" void_reason="" uuid="519eef34-6482-487d-94ce-c07bb3ca3cca"/>
38+
<patient_identifier date_created="2005-09-22 00:00:00" patient_identifier_id="31" patient_id="31"
39+
identifier="DupId2" identifier_type="1" creator="1"
40+
uuid="8cb74fd1-128d-ef12-842f-35da3a681d35" preferred="1" voided="0"/>
41+
</dataset>

0 commit comments

Comments
 (0)