View Javadoc
1   /*
2    * Copyright (c) 2002-2025 Gargoyle Software Inc.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    * https://www.apache.org/licenses/LICENSE-2.0
8    *
9    * Unless required by applicable law or agreed to in writing, software
10   * distributed under the License is distributed on an "AS IS" BASIS,
11   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12   * See the License for the specific language governing permissions and
13   * limitations under the License.
14   */
15  package org.htmlunit.javascript.host.dom;
16  
17  import java.net.URL;
18  import java.util.Arrays;
19  
20  import org.htmlunit.WebDriverTestCase;
21  import org.htmlunit.junit.annotation.Alerts;
22  import org.htmlunit.junit.annotation.BuggyWebDriver;
23  import org.junit.jupiter.api.Test;
24  import org.openqa.selenium.By;
25  import org.openqa.selenium.WebDriver;
26  
27  /**
28   * Tests for {@link MutationObserver}.
29   *
30   * @author Ahmed Ashour
31   * @author Ronald Brill
32   * @author Atsushi Nakagawa
33   */
34  public class MutationObserverTest extends WebDriverTestCase {
35  
36      /**
37       * @throws Exception if the test fails
38       */
39      @Test
40      @Alerts("ReferenceError")
41      public void observeNullNode() throws Exception {
42          final String html = DOCTYPE_HTML
43              + "<html><head><script>\n"
44              + LOG_TITLE_FUNCTION
45              + "function test() {\n"
46              + "  var observer = new MutationObserver(function(mutations) {});\n"
47              + "\n"
48              + "  try {\n"
49              + "    observer.observe(div, {});\n"
50              + "  } catch(e) { logEx(e); }\n"
51              + "}\n"
52              + "</script></head>\n"
53              + "<body onload='test()'>\n"
54              + "  <div id='myDiv'>old</div>\n"
55              + "</body></html>";
56  
57          loadPageVerifyTitle2(html);
58      }
59  
60      /**
61       * @throws Exception if the test fails
62       */
63      @Test
64      @Alerts("TypeError")
65      public void observeNullInit() throws Exception {
66          final String html = DOCTYPE_HTML
67              + "<html><head><script>\n"
68              + LOG_TITLE_FUNCTION
69              + "function test() {\n"
70              + "  var div = document.getElementById('myDiv');\n"
71              + "  var observer = new MutationObserver(function(mutations) {});\n"
72              + "\n"
73              + "  try {\n"
74              + "    observer.observe(div);\n"
75              + "  } catch(e) { logEx(e); }\n"
76              + "}\n"
77              + "</script></head>\n"
78              + "<body onload='test()'>\n"
79              + "  <div id='myDiv'>old</div>\n"
80              + "</body></html>";
81  
82          loadPageVerifyTitle2(html);
83      }
84  
85      /**
86       * @throws Exception if the test fails
87       */
88      @Test
89      @Alerts("TypeError")
90      public void observeEmptyInit() throws Exception {
91          final String html = DOCTYPE_HTML
92              + "<html><head><script>\n"
93              + LOG_TITLE_FUNCTION
94              + "function test() {\n"
95              + "  var div = document.getElementById('myDiv');\n"
96              + "  var observer = new MutationObserver(function(mutations) {});\n"
97              + "\n"
98              + "  try {\n"
99              + "    observer.observe(div, {});\n"
100             + "  } catch(e) { logEx(e); }\n"
101             + "}\n"
102             + "</script></head>\n"
103             + "<body onload='test()'>\n"
104             + "  <div id='myDiv'>old</div>\n"
105             + "</body></html>";
106 
107         loadPageVerifyTitle2(html);
108     }
109 
110     /**
111      * @throws Exception if the test fails
112      */
113     @Test
114     @Alerts({"TypeError", "childList", "attributes", "characterData"})
115     public void observeRequiredMissingInit() throws Exception {
116         final String html = DOCTYPE_HTML
117             + "<html><head><script>\n"
118             + LOG_TITLE_FUNCTION
119             + "function test() {\n"
120             + "  var div = document.getElementById('myDiv');\n"
121             + "  var observer = new MutationObserver(function(mutations) {});\n"
122             + "\n"
123             + "  try {\n"
124             + "    observer.observe(div, {subtree: true});\n"
125             + "  } catch(e) { logEx(e); }\n"
126             + "  try {\n"
127             + "    observer.observe(div, {childList: true});\n"
128             + "    log('childList');\n"
129             + "  } catch(e) { logEx(e); }\n"
130             + "  try {\n"
131             + "    observer.observe(div, {attributes: true});\n"
132             + "    log('attributes');\n"
133             + "  } catch(e) { logEx(e); }\n"
134             + "  try {\n"
135             + "    observer.observe(div, {characterData: true});\n"
136             + "    log('characterData');\n"
137             + "  } catch(e) { logEx(e); }\n"
138             + "}\n"
139             + "</script></head>\n"
140             + "<body onload='test()'>\n"
141             + "  <div id='myDiv'>old</div>\n"
142             + "</body></html>";
143 
144         loadPageVerifyTitle2(html);
145     }
146 
147     /**
148      * @throws Exception if the test fails
149      */
150     @Test
151     @Alerts({"old", "new"})
152     public void characterData() throws Exception {
153         final String html = DOCTYPE_HTML
154             + "<html><head><script>\n"
155             + LOG_TITLE_FUNCTION
156             + "function test() {\n"
157             + "  var div = document.getElementById('myDiv');\n"
158             + "  var observer = new MutationObserver(function(mutations) {\n"
159             + "    mutations.forEach(function(mutation) {\n"
160             + "      log(mutation.oldValue);\n"
161             + "      log(mutation.target.textContent);\n"
162             + "    });\n"
163             + "  });\n"
164             + "\n"
165             + "  observer.observe(div, {\n"
166             + "    characterData: true,\n"
167             + "    characterDataOldValue: true,\n"
168             + "    subtree: true\n"
169             + "  });\n"
170             + "\n"
171             + "  div.firstChild.textContent = 'new';\n"
172             + "}\n"
173             + "</script></head>\n"
174             + "<body onload='test()'>\n"
175             + "  <div id='myDiv'>old</div>\n"
176             + "</body></html>";
177 
178         loadPageVerifyTitle2(html);
179     }
180 
181     /**
182      * @throws Exception if the test fails
183      */
184     @Test
185     @Alerts({"null", "new"})
186     public void characterDataNoOldValue() throws Exception {
187         final String html = DOCTYPE_HTML
188             + "<html><head><script>\n"
189             + LOG_TITLE_FUNCTION
190             + "function test() {\n"
191             + "  var div = document.getElementById('myDiv');\n"
192             + "  var observer = new MutationObserver(function(mutations) {\n"
193             + "    mutations.forEach(function(mutation) {\n"
194             + "      log(mutation.oldValue);\n"
195             + "      log(mutation.target.textContent);\n"
196             + "    });\n"
197             + "  });\n"
198             + "\n"
199             + "  observer.observe(div, {\n"
200             + "    characterData: true,\n"
201             + "    subtree: true\n"
202             + "  });\n"
203             + "\n"
204             + "  div.firstChild.textContent = 'new';\n"
205             + "}\n"
206             + "</script></head>\n"
207             + "<body onload='test()'>\n"
208             + "  <div id='myDiv'>old</div>\n"
209             + "</body></html>";
210 
211         loadPageVerifyTitle2(html);
212     }
213 
214     /**
215      * @throws Exception if the test fails
216      */
217     @Test
218     public void characterDataNoSubtree() throws Exception {
219         final String html = DOCTYPE_HTML
220             + "<html><head><script>\n"
221             + LOG_TITLE_FUNCTION
222             + "function test() {\n"
223             + "  var div = document.getElementById('myDiv');\n"
224             + "  var observer = new MutationObserver(function(mutations) {\n"
225             + "    mutations.forEach(function(mutation) {\n"
226             + "      log(mutation.oldValue);\n"
227             + "      log(mutation.target.textContent);\n"
228             + "    });\n"
229             + "  });\n"
230             + "\n"
231             + "  observer.observe(div, {\n"
232             + "    characterData: true,\n"
233             + "    characterDataOldValue: true\n"
234             + "  });\n"
235             + "\n"
236             + "  div.firstChild.textContent = 'new';\n"
237             + "}\n"
238             + "</script></head>\n"
239             + "<body onload='test()'>\n"
240             + "  <div id='myDiv'>old</div>\n"
241             + "</body></html>";
242         loadPageVerifyTitle2(html);
243     }
244 
245     /**
246      * @throws Exception if the test fails
247      */
248     @Test
249     @Alerts({"attributes", "ltr"})
250     public void attributes() throws Exception {
251         final String html = DOCTYPE_HTML
252             + "<html><head><script>\n"
253             + LOG_TITLE_FUNCTION
254             + "function test() {\n"
255             + "  var div = document.getElementById('myDiv');\n"
256             + "  var observer = new MutationObserver(function(mutations) {\n"
257             + "    mutations.forEach(function(mutation) {\n"
258             + "      log(mutation.type);\n"
259             + "      log(mutation.oldValue);\n"
260             + "    });\n"
261             + "  });\n"
262             + "\n"
263             + "  observer.observe(div, {\n"
264             + "    attributes: true,\n"
265             + "    attributeFilter: ['dir'],\n"
266             + "    attributeOldValue: true\n"
267             + "  });\n"
268             + "\n"
269             + "  div.dir = 'rtl';\n"
270             + "}\n"
271             + "</script></head>\n"
272             + "<body onload='test()'>\n"
273             + "  <div id='myDiv' dir='ltr'>old</div>\n"
274             + "</body></html>";
275 
276         loadPageVerifyTitle2(html);
277     }
278 
279     /**
280      * Test case for issue #1811.
281      * @throws Exception if the test fails
282      */
283     @Test
284     @Alerts({"heho", "attributes", "value", "null", "x", "abc",
285              "heho", "attributes", "value", "null", "y", "abc"})
286     public void attributeValue() throws Exception {
287         final String html = DOCTYPE_HTML
288             + "<html>\n"
289             + "<head><script>\n"
290             + LOG_TITLE_FUNCTION
291             + "  function test() {\n"
292             + "    var config = { attributes: true, childList: true, characterData: true, subtree: true };\n"
293             + "    var observer = new MutationObserver(function(mutations) {\n"
294             + "      mutations.forEach(function(mutation) {\n"
295             + "        log(mutation.type);\n"
296             + "        log(mutation.attributeName);\n"
297             + "        log(mutation.oldValue);\n"
298             + "        log(mutation.target.getAttribute(\"value\"));\n"
299             + "        log(mutation.target.value);\n"
300             + "      });\n"
301             + "    });\n"
302             + "    observer.observe(document.getElementById('tester'), config);\n"
303             + "  }\n"
304             + "</script></head>\n"
305             + "<body onload='test()'>\n"
306             + "  <input id='tester' value=''>\n"
307             + "  <button id='doAlert' onclick='log(\"heho\");'>DoAlert</button>\n"
308             + "  <button id='doIt' "
309                         + "onclick='document.getElementById(\"tester\").setAttribute(\"value\", \"x\")'>"
310                         + "DoIt</button>\n"
311             + "  <button id='doItAgain' "
312                         + " onclick='document.getElementById(\"tester\").setAttribute(\"value\", \"y\")'>"
313                         + "DoItAgain</button>\n"
314             + "</body></html>";
315         final WebDriver driver = loadPage2(html);
316         driver.findElement(By.id("tester")).sendKeys("abc");
317         verifyTitle2(driver, new String[] {});
318 
319         driver.findElement(By.id("doAlert")).click();
320         verifyTitle2(driver, new String[] {"heho"});
321 
322         final String[] expected = getExpectedAlerts();
323         driver.findElement(By.id("doIt")).click();
324         verifyTitle2(driver, Arrays.copyOfRange(expected, 0, 6));
325 
326         driver.findElement(By.id("doAlert")).click();
327         verifyTitle2(driver, Arrays.copyOfRange(expected, 0, 7));
328 
329         driver.findElement(By.id("doItAgain")).click();
330         verifyTitle2(driver, expected);
331     }
332 
333     /**
334      * Test case for issue #1811.
335      * @throws Exception if the test fails
336      */
337     @Test
338     @Alerts({"heho", "attributes", "value", "null", "x", "abc", "0", "0",
339              "heho", "attributes", "value", "null", "null", "abc", "0", "0"})
340     public void attributeValueAddRemove() throws Exception {
341         final String html = DOCTYPE_HTML
342             + "<html>\n"
343             + "<head><script>\n"
344             + LOG_TITLE_FUNCTION
345             + "  function test() {\n"
346             + "    var config = { attributes: true, childList: true, characterData: true, subtree: true };\n"
347             + "    var observer = new MutationObserver(function(mutations) {\n"
348             + "      mutations.forEach(function(mutation) {\n"
349             + "        log(mutation.type);\n"
350             + "        log(mutation.attributeName);\n"
351             + "        log(mutation.oldValue);\n"
352             + "        log(mutation.target.getAttribute(\"value\"));\n"
353             + "        log(mutation.target.value);\n"
354             + "        log(mutation.addedNodes.length);\n"
355             + "        log(mutation.removedNodes.length);\n"
356             + "      });\n"
357             + "    });\n"
358             + "    observer.observe(document.getElementById('tester'), config);\n"
359             + "  }\n"
360             + "</script></head>\n"
361             + "<body onload='test()'>\n"
362             + "  <input id='tester'>\n"
363             + "  <button id='doAlert' onclick='log(\"heho\");'>DoAlert</button>\n"
364             + "  <button id='doIt' "
365                         + "onclick='document.getElementById(\"tester\").setAttribute(\"value\", \"x\")'>"
366                         + "DoIt</button>\n"
367             + "  <button id='doItAgain' "
368                         + " onclick='document.getElementById(\"tester\").removeAttribute(\"value\")'>"
369                         + "DoItAgain</button>\n"
370             + "</body></html>";
371         final WebDriver driver = loadPage2(html);
372         driver.findElement(By.id("tester")).sendKeys("abc");
373         verifyTitle2(driver, new String[] {});
374 
375         driver.findElement(By.id("doAlert")).click();
376         verifyTitle2(driver, new String[] {"heho"});
377 
378         final String[] expected = getExpectedAlerts();
379         driver.findElement(By.id("doIt")).click();
380         verifyTitle2(driver, Arrays.copyOfRange(expected, 0, 8));
381 
382         driver.findElement(By.id("doAlert")).click();
383         verifyTitle2(driver, Arrays.copyOfRange(expected, 0, 9));
384 
385         driver.findElement(By.id("doItAgain")).click();
386         verifyTitle2(driver, expected);
387     }
388 
389     /**
390      * @throws Exception if an error occurs
391      */
392     @Test
393     @Alerts("[object HTMLHeadingElement]-attributes")
394     @BuggyWebDriver(
395             FF = {"[object HTMLInputElement]-attributesn",
396                   "[object HTMLInputElement]-attributes",
397                   "[object HTMLInputElement]-attributes",
398                   "[object HTMLInputElement]-attributes",
399                   "[object HTMLHeadingElement]-attributes"},
400             FF_ESR = {"[object HTMLInputElement]-attributes",
401                       "[object HTMLInputElement]-attributes",
402                       "[object HTMLInputElement]-attributes",
403                       "[object HTMLInputElement]-attributes",
404                       "[object HTMLHeadingElement]-attributes"})
405     public void attributeValue2() throws Exception {
406         final String html = DOCTYPE_HTML
407             + "<html><head><script>\n"
408             + LOG_TEXTAREA_FUNCTION
409             + "  function makeRed() {\n"
410             + "    document.getElementById('headline').setAttribute('style', 'color: red');\n"
411             + "  }\n"
412 
413             + "  function print(mutation) {\n"
414             + "    log(mutation.target + '-' + mutation.type);\n"
415             + "  }\n"
416 
417             + "  function test() {\n"
418             + "    var mobs = new MutationObserver(function(mutations) {\n"
419             + "      mutations.forEach(print)\n"
420             + "    });\n"
421 
422             + "    mobs.observe(document.getElementById('container'), {\n"
423             + "      attributes: true,\n"
424             + "      childList: true,\n"
425             + "      characterData: true,\n"
426             + "      subtree: true\n"
427             + "    });\n"
428 
429             + "    document.addEventListener('beforeunload', function() {\n"
430             + "      mobs.disconnect();\n"
431             + "    });\n"
432             + "  }\n"
433             + "</script></head><body onload='test()'>\n"
434             + "  <div id='container'>\n"
435             + "    <h1 id='headline' style='font-style: italic'>Some headline</h1>\n"
436             + "    <input id='id1' type='button' onclick='makeRed()' value='Make Red'>\n"
437             + "  </div>\n"
438             + LOG_TEXTAREA
439             + "</body></html>\n";
440         final WebDriver driver = loadPage2(html);
441         driver.findElement(By.id("id1")).click();
442 
443         verifyTextArea2(driver, getExpectedAlerts());
444     }
445 
446     /**
447      * @throws Exception if the test fails
448      */
449     @Test
450     @Alerts({"before", "after div", "after text", "div observed", "text observed"})
451     public void callbackOrder() throws Exception {
452         final String html = DOCTYPE_HTML
453             + "<html><head><script>\n"
454             + LOG_TITLE_FUNCTION
455             + "function test() {\n"
456             + "  var div = document.getElementById('myDiv');\n"
457             + "  var divObserver = new MutationObserver(function() {\n"
458             + "      log('div observed');\n"
459             + "    });\n"
460             + "\n"
461             + "  divObserver.observe(div, { attributes: true });\n"
462             + "\n"
463             + "  var text = document.createTextNode('')\n"
464             + "  var txtObserver = new MutationObserver(function() {\n"
465             + "        log('text observed');\n"
466             + "    });\n"
467             + "  txtObserver.observe(text, { characterData: true });"
468             + "\n"
469             + "  log('before');\n"
470             + "  div.style = 'background-color: red';\n"
471             + "  log('after div');\n"
472             + "  text.data = 42;\n"
473             + "  log('after text');\n"
474             + "}\n"
475             + "</script></head>\n"
476             + "<body onload='test()'>\n"
477             + "  <div id='myDiv' style='color: green'>old</div>\n"
478             + "</body></html>";
479 
480         loadPageVerifyTitle2(html);
481     }
482 
483     /**
484      * @throws Exception if the test fails
485      */
486     @Test
487     @Alerts("Content")
488     public void callbackRequiresStackSetup() throws Exception {
489         final String content = DOCTYPE_HTML
490             + "<html><head><title>Content</title></head><body><p>content</p></body></html>";
491 
492         getMockWebConnection().setResponse(new URL(URL_FIRST, "content.html"), content);
493 
494         final String html = DOCTYPE_HTML
495             + "<html><head><script>\n"
496             + "function test() {\n"
497             + "\n"
498             + "  var text = document.createTextNode('')\n"
499             + "  var txtObserver = new MutationObserver(function() {\n"
500             + "        window.location.href = 'content.html'"
501             + "    });\n"
502             + "  txtObserver.observe(text, { characterData: true });"
503             + "\n"
504             + "  text.data = 42\n"
505             + "}\n"
506             + "</script></head>\n"
507             + "<body onload='test()'>\n"
508             + "  <div id='myDiv' dir='ltr'>old</div>\n"
509             + "</body></html>";
510 
511         final WebDriver driver = loadPage2(html);
512         assertTitle(driver, getExpectedAlerts()[0]);
513     }
514 
515     /**
516      * @throws Exception if the test fails
517      */
518     @Test
519     @Alerts(DEFAULT = {"[object MutationObserver]", "", "false"},
520             CHROME = {"[object MutationObserver]", "[object MutationObserver]", "true"},
521             EDGE = {"[object MutationObserver]", "[object MutationObserver]", "true"})
522     public void webKitMutationObserver() throws Exception {
523         final String html = DOCTYPE_HTML
524             + "<html><head>\n"
525             + "<script>\n"
526             + LOG_TITLE_FUNCTION
527             + "function test() {\n"
528             + "  var observer = new MutationObserver(function() {});\n"
529             + "  var wkObserver = '';\n"
530             + "  if (typeof(WebKitMutationObserver) == 'function') {\n"
531             + "    wkObserver = new WebKitMutationObserver(function() {});\n"
532             + "  }\n"
533             + "  log(observer);\n"
534             + "  log(wkObserver);\n"
535             + "  log(Object.getPrototypeOf(observer) == Object.getPrototypeOf(wkObserver));\n"
536             + "}\n"
537             + "</script></head>\n"
538             + "<body onload='test()'>\n"
539             + "</body></html>";
540 
541         loadPageVerifyTitle2(html);
542     }
543 }