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