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 org.htmlunit.WebDriverTestCase;
18  import org.htmlunit.junit.annotation.Alerts;
19  import org.htmlunit.junit.annotation.HtmlUnitNYI;
20  import org.junit.jupiter.api.Test;
21  
22  /**
23   * Unit tests for {@link Selection}.
24   *
25   * @author Daniel Gredler
26   * @author Marc Guillemot
27   * @author Ronald Brill
28   */
29  public class SelectionTest extends WebDriverTestCase {
30  
31      /**
32       * @throws Exception if an error occurs
33       */
34      @Test
35      @Alerts("true")
36      public void equality_selection() throws Exception {
37          final String html = DOCTYPE_HTML
38                  + "<html>\n"
39                  + "<body>\n"
40                  + "<script>"
41                  + LOG_TITLE_FUNCTION
42                  + "log(document.selection==document.selection);\n"
43                  + "</script>\n"
44                  + "</body></html>";
45          loadPageVerifyTitle2(html);
46      }
47  
48      /**
49       * @throws Exception if an error occurs
50       */
51      @Test
52      @Alerts("true")
53      public void equality_getSelection() throws Exception {
54          final String html = DOCTYPE_HTML
55              + "<html><body>\n"
56              + "<script>\n"
57              + LOG_TITLE_FUNCTION
58              + "try {\n"
59              + "  log(window.getSelection()==window.getSelection());\n"
60              + "} catch(e) { logEx(e) }\n"
61              + "</script>\n"
62              + "</body></html>";
63          loadPageVerifyTitle2(html);
64      }
65  
66      /**
67       * @throws Exception if an error occurs
68       */
69      @Test
70      @Alerts({"0", "0", "0", "cdefg"})
71      public void inputSelectionsAreIndependent() throws Exception {
72          final String html = DOCTYPE_HTML
73              + "<html><body onload='test()'>\n"
74              + "<input id='i' value='abcdefghi'/>\n"
75              + "<script>\n"
76              + LOG_TITLE_FUNCTION
77              + "  function test() {\n"
78              + "    var i = document.getElementById('i');\n"
79              + "    var s = window.getSelection();\n"
80              + "    log(s.rangeCount);\n"
81              + "    i.selectionStart = 2;\n"
82              + "    log(s.rangeCount);\n"
83              + "    i.selectionEnd = 7;\n"
84              + "    log(s.rangeCount);\n"
85              + "    log(i.value.substring(i.selectionStart, i.selectionEnd));\n"
86              + "  }\n"
87              + "</script>\n"
88              + "</body></html>";
89          loadPageVerifyTitle2(html);
90      }
91  
92      /**
93       * @throws Exception if an error occurs
94       */
95      @Test
96      @Alerts({"1:null/0/null/0/true/None/0/",
97               "2:s2/0/s2/1/false/Range/1/xyz[xyz"})
98      public void selectAllChildren() throws Exception {
99          final String jsSnippet = ""
100             + "    alertSelection(selection);\n"
101             + "    selection.selectAllChildren(s2);\n"
102             + "    alertSelection(selection);\n";
103 
104         tester(jsSnippet);
105     }
106 
107     /**
108      * @throws Exception if an error occurs
109      */
110     @Test
111     @Alerts({"1:s2/0/s2/1/false/Range/1/xyz[xyz",
112              "2:s2/0/s3/1/false/Range/1/xyzfoo[xyzfoo",
113              "3:s2/0/s3/2/false/Range/1/xyzfoo---[xyzfoo---",
114              "4:s2/0/s3/3/false/Range/1/xyzfoo---foo[xyzfoo---foo"})
115     public void extend() throws Exception {
116         final String jsSnippet = ""
117             + "    selection.selectAllChildren(s2);\n"
118             + "    alertSelection(selection);\n"
119             + "    if (selection.extend) {\n"
120             + "      selection.extend(s3, 1);\n"
121             + "      alertSelection(selection);\n"
122             + "      selection.extend(s3, 2);\n"
123             + "      alertSelection(selection);\n"
124             + "      selection.extend(s3, 3);\n"
125             + "      alertSelection(selection);\n"
126             + "    } else { log('selection.extend not available'); }\n";
127 
128         tester(jsSnippet);
129     }
130 
131     /**
132      * @throws Exception if an error occurs
133      */
134     @Test
135     @Alerts({"1:s2/0/s2/1/false/Range/1/xyz[xyz",
136              "2:s2/0/s2/0/true/Caret/1/["})
137     public void collapseToStart() throws Exception {
138         final String jsSnippet = ""
139             + "    selection.selectAllChildren(s2);\n"
140             + "    alertSelection(selection);\n"
141             + "    selection.collapseToStart();\n"
142             + "    alertSelection(selection);\n";
143 
144         tester(jsSnippet);
145     }
146 
147     /**
148      * @throws Exception if an error occurs
149      */
150     @Test
151     @Alerts({"1:s2/0/s2/1/false/Range/1/xyz[xyz",
152              "2:s2/1/s2/1/true/Caret/1/["})
153     public void collapseToEnd() throws Exception {
154         final String jsSnippet = ""
155             + "    selection.selectAllChildren(s2);\n"
156             + "    alertSelection(selection);\n"
157             + "    selection.collapseToEnd();\n"
158             + "    alertSelection(selection);\n";
159 
160         tester(jsSnippet);
161     }
162 
163     /**
164      * @throws Exception if an error occurs
165      */
166     @Test
167     @Alerts({"1:s2/0/s2/1/false/Range/1/xyz[xyz",
168              "2:null/0/null/0/true/None/0/"})
169     public void empty() throws Exception {
170         final String jsSnippet = ""
171             + "    selection.selectAllChildren(s2);\n"
172             + "    alertSelection(selection);\n"
173             + "    selection.empty();\n"
174             + "    alertSelection(selection);\n";
175 
176         tester(jsSnippet);
177     }
178 
179     /**
180      * @throws Exception if an error occurs
181      */
182     @Test
183     @Alerts({"1:null/0/null/0/true/None/0/",
184              "2:null/0/null/0/true/None/0/",
185              "3:s2/1/s3/1/false/Range/1/foo[foo"})
186     public void addRange() throws Exception {
187         final String jsSnippet = ""
188             + "      alertSelection(selection);\n"
189 
190             + "      var range = document.createRange();\n"
191             + "      range.setStart(s2, 1);\n"
192             + "      range.setEnd(s3, 1);\n"
193             + "      alertSelection(selection);\n"
194 
195             + "      selection.addRange(range);\n"
196             + "      alertSelection(selection);\n";
197 
198         tester(jsSnippet);
199     }
200 
201     /**
202      * @throws Exception if an error occurs
203      */
204     @Test
205     @Alerts({"1:null/0/null/0/true/None/0/",
206              "2:s1/1/s3/1/false/Range/1/xyzfoo[xyzfoo",
207              "3:null/0/null/0/true/None/0/"})
208     public void removeAllRanges() throws Exception {
209         final String jsSnippet = ""
210             + "      alertSelection(selection);\n"
211 
212             + "      var range = document.createRange();\n"
213             + "      range.setStart(s1, 1);\n"
214             + "      range.setEnd(s3, 1);\n"
215             + "      selection.addRange(range);\n"
216             + "      alertSelection(selection);\n"
217 
218             + "      selection.removeAllRanges();\n"
219             + "      alertSelection(selection);\n";
220 
221         tester(jsSnippet);
222     }
223 
224     /**
225      * @throws Exception if an error occurs
226      */
227     @Test
228     @Alerts({"1:s1/1/s3/1/false/Range/1/xyzfoo[xyzfoo",
229              "2:null/0/null/0/true/None/0/"})
230     public void removeAllRanges2() throws Exception {
231         final String jsSnippet = ""
232             + "      var range = document.createRange();\n"
233             + "      range.setStart(s1, 1);\n"
234             + "      range.setEnd(s3, 1);\n"
235             + "      selection.addRange(range);\n"
236             + "      alertSelection(selection);\n"
237 
238             + "      selection.removeAllRanges();\n"
239             + "      alertSelection(selection);\n";
240 
241         tester(jsSnippet);
242     }
243 
244     /**
245      * @throws Exception if an error occurs
246      */
247     @Test
248     @Alerts(DEFAULT = {"1:null/0/null/0/true/None/0/",
249                        "2:s1/0/s1/1/false/Range/1/abc[abc",
250                        "3:null/0/null/0/true/None/0/"},
251             FF = {"1:null/0/null/0/true/None/0/",
252                   "2:s1/1/s3/1/false/Range/2/abcxyzfoo[abc[xyzfoo",
253                   "3:null/0/null/0/true/None/0/"},
254             FF_ESR = {"1:null/0/null/0/true/None/0/",
255                       "2:s1/1/s3/1/false/Range/2/abcxyzfoo[abc[xyzfoo",
256                       "3:null/0/null/0/true/None/0/"})
257     @HtmlUnitNYI(CHROME = {"1:null/0/null/0/true/None/0/",
258                            "2:s1/0/s1/1/false/Range/2/abcxyzfoo[abc[xyzfoo",
259                            "3:null/0/null/0/true/None/0/"},
260             EDGE = {"1:null/0/null/0/true/None/0/",
261                     "2:s1/0/s1/1/false/Range/2/abcxyzfoo[abc[xyzfoo",
262                     "3:null/0/null/0/true/None/0/"},
263             FF = {"1:null/0/null/0/true/None/0/",
264                   "2:s1/0/s1/1/false/Range/2/abcxyzfoo[abc[xyzfoo",
265                   "3:null/0/null/0/true/None/0/"},
266             FF_ESR = {"1:null/0/null/0/true/None/0/",
267                       "2:s1/0/s1/1/false/Range/2/abcxyzfoo[abc[xyzfoo",
268                       "3:null/0/null/0/true/None/0/"})
269     public void selectAllChildrenAddRange() throws Exception {
270         final String jsSnippet = ""
271             + "      alertSelection(selection);\n"
272 
273             + "      selection.selectAllChildren(s1);\n"
274             + "      var range = document.createRange();\n"
275             + "      range.setStart(s1, 1);\n"
276             + "      range.setEnd(s3, 1);\n"
277             + "      selection.addRange(range);\n"
278             + "      alertSelection(selection);\n"
279 
280             + "      selection.removeAllRanges();\n"
281             + "      alertSelection(selection);\n";
282 
283         tester(jsSnippet);
284     }
285 
286     /**
287      * @throws Exception if an error occurs
288      */
289     @Test
290     @Alerts({"1:null/0/null/0/true/None/0/",
291              "2:s1/0/s1/1/false/Range/1/abc[abc",
292              "3:null/0/null/0/true/None/0/"})
293     public void addRangeSelectAllChildren() throws Exception {
294         final String jsSnippet = ""
295             + "      alertSelection(selection);\n"
296 
297             + "      var range = document.createRange();\n"
298             + "      range.setStart(s1, 1);\n"
299             + "      range.setEnd(s3, 1);\n"
300             + "      selection.addRange(range);\n"
301             + "      selection.selectAllChildren(s1);\n"
302             + "      alertSelection(selection);\n"
303 
304             + "      selection.removeAllRanges();\n"
305             + "      alertSelection(selection);\n";
306 
307         tester(jsSnippet);
308     }
309 
310     /**
311      * @throws Exception if an error occurs
312      */
313     @Test
314     @Alerts(DEFAULT = {"1:null/0/null/0/true/None/0/",
315                        "2:s1/0/s1/1/false/Range/1/abc[abc",
316                        "3:s1/0/s1/1/false/Range/1/abc[abc",
317                        "4:null/0/null/0/true/None/0/"},
318             FF = {"1:null/0/null/0/true/None/0/",
319                   "2:s1/1/s2/1/false/Range/2/abcxyz[abc[xyz",
320                   "3:s2/1/s3/3/false/Range/3/abcxyzfoo---foo[abc[xyz[foo---foo",
321                   "4:null/0/null/0/true/None/0/"},
322             FF_ESR = {"1:null/0/null/0/true/None/0/",
323                       "2:s1/1/s2/1/false/Range/2/abcxyz[abc[xyz",
324                       "3:s2/1/s3/3/false/Range/3/abcxyzfoo---foo[abc[xyz[foo---foo",
325                       "4:null/0/null/0/true/None/0/"})
326     @HtmlUnitNYI(CHROME = {"1:null/0/null/0/true/None/0/",
327                            "2:s1/0/s1/1/false/Range/2/abcxyz[abc[xyz",
328                            "3:s2/1/s3/3/false/Range/3/abcxyzfoo---foo[abc[xyz[foo---foo",
329                            "4:null/0/null/0/true/None/0/"},
330             EDGE = {"1:null/0/null/0/true/None/0/",
331                     "2:s1/0/s1/1/false/Range/2/abcxyz[abc[xyz",
332                     "3:s2/1/s3/3/false/Range/3/abcxyzfoo---foo[abc[xyz[foo---foo",
333                     "4:null/0/null/0/true/None/0/"},
334             FF = {"1:null/0/null/0/true/None/0/",
335                   "2:s1/0/s1/1/false/Range/2/abcxyz[abc[xyz",
336                   "3:s2/1/s3/3/false/Range/3/abcxyzfoo---foo[abc[xyz[foo---foo",
337                   "4:null/0/null/0/true/None/0/"},
338             FF_ESR = {"1:null/0/null/0/true/None/0/",
339                       "2:s1/0/s1/1/false/Range/2/abcxyz[abc[xyz",
340                       "3:s2/1/s3/3/false/Range/3/abcxyzfoo---foo[abc[xyz[foo---foo",
341                       "4:null/0/null/0/true/None/0/"})
342     public void addRangeAddRange() throws Exception {
343         final String jsSnippet = ""
344             + "      alertSelection(selection);\n"
345 
346             + "      selection.selectAllChildren(s1);\n"
347             + "      var range = document.createRange();\n"
348             + "      range.setStart(s1, 1);\n"
349             + "      range.setEnd(s2, 1);\n"
350             + "      selection.addRange(range);\n"
351             + "      alertSelection(selection);\n"
352 
353             + "      var range = document.createRange();\n"
354             + "      range.setStart(s2, 1);\n"
355             + "      range.setEnd(s3, 3);\n"
356             + "      selection.addRange(range);\n"
357             + "      alertSelection(selection);\n"
358 
359             + "      selection.removeAllRanges();\n"
360             + "      alertSelection(selection);\n";
361 
362         tester(jsSnippet);
363     }
364 
365     /**
366      * Test selection's anchorNode and focusNode after call to removeRange. Surprisingly, this is
367      * not null.
368      * @throws Exception if an error occurs
369      */
370     @Test
371     @Alerts({"1:[object Text]/1/[object Text]/2/false/Range/1/yzfo[yzfo",
372              "2:null/0/null/0/true/None/0/",
373              "false", "true"})
374     public void aLittleBitOfEverything_removeRange() throws Exception {
375         final String jsSnippet = ""
376             + "    var range = document.createRange();\n"
377             + "    range.setStart(s2.firstChild, 1);\n"
378             + "    range.setEnd(s3.firstChild, 2);\n"
379             + "    selection.addRange(range);\n"
380             + "    alertSelection(selection);\n"
381             + "    selection.removeRange(range);\n"
382             + "    alertSelection(selection);\n"
383             + "    log(range.collapsed);\n"
384             + "    selection.addRange(range);\n"
385             + "    log(selection.getRangeAt(0) == selection.getRangeAt(0));\n";
386 
387         tester(jsSnippet);
388     }
389 
390     private void tester(final String jsSnippet) throws Exception {
391         final String html = DOCTYPE_HTML
392             + "<html>\n"
393             + "<body onload='test()'>\n"
394             + "  <span id='s1'>abc</span><span id='s2'>xyz</span><span id='s3'>foo<span>---</span>foo</span>\n"
395 
396             + "<script>\n"
397             + LOG_TITLE_FUNCTION
398             + "  var x = 1;\n"
399             + "  function test() {\n"
400             + "    var selection = window.getSelection();\n"
401             + "    var s1 = document.getElementById('s1');\n"
402             + "    var s2 = document.getElementById('s2');\n"
403             + "    var s3 = document.getElementById('s3');\n"
404             + "    try {\n"
405                         + jsSnippet
406             + "    } catch(e) { logEx(e); }\n"
407             + "  }\n"
408 
409             + "  function alertSelection(s) {\n"
410             + "    var anchorNode = (s.anchorNode && s.anchorNode.id ? s.anchorNode.id : s.anchorNode);\n"
411             + "    var focusNode = (s.focusNode && s.focusNode.id ? s.focusNode.id : s.focusNode);\n"
412             + "    var msg = (x++) + ':' + anchorNode + '/' + s.anchorOffset + '/' + focusNode + '/' +\n"
413             + "       s.focusOffset + '/' + s.isCollapsed + '/' + s.type + '/' + s.rangeCount + '/' + s;\n"
414             + "    for(var i = 0; i < s.rangeCount; i++) {\n"
415             + "      msg += '[' + s.getRangeAt(i);\n"
416             + "    }\n"
417             + "    log(msg);\n"
418             + "  }\n"
419             + "</script>\n"
420             + "</body></html>";
421         loadPageVerifyTitle2(html);
422     }
423 
424     /**
425      * @throws Exception if an error occurs
426      */
427     @Test
428     @Alerts(DEFAULT = {"", "null-0", "", "null-0", "", "null-0", "", "null-0"},
429             FF = {"", "null-0", "", "null-0", "null", "null"},
430             FF_ESR = {"", "null-0", "", "null-0", "null", "null"})
431     public void getSelection_display() throws Exception {
432         final String html = DOCTYPE_HTML
433             + "<html>\n"
434             + "<body onload='test()'>\n"
435             + "  <iframe id='frame1' src='about:blank'></iframe>\n"
436             + "  <iframe id='frame2' src='about:blank' style='display: none'></iframe>\n"
437             + "  <div style='display: none'>\n"
438             + "    <iframe id='frame3' src='about:blank'></iframe>\n"
439             + "  </div>\n"
440 
441             + "  <script>\n"
442             + LOG_TITLE_FUNCTION
443             + "    function sel(win) {\n"
444             + "      if (win.getSelection) {\n"
445             + "        var range = win.getSelection();\n"
446             + "        log(range);\n"
447             + "        if (range) {\n"
448             + "          log(range.anchorNode + '-' + range.rangeCount);\n"
449             + "        }\n"
450             + "      }\n"
451             + "    }\n"
452 
453             + "    function test() {\n"
454             + "      sel(window);\n"
455             + "      sel(document.getElementById('frame1').contentWindow);\n"
456             + "      sel(document.getElementById('frame2').contentWindow);\n"
457             + "      sel(document.getElementById('frame3').contentWindow);\n"
458             + "    }\n"
459             + "  </script>\n"
460             + "</body></html>";
461         loadPageVerifyTitle2(html);
462     }
463 
464     /**
465      * @throws Exception if an error occurs
466      */
467     @Test
468     @Alerts({"", "", ""})
469     public void testToString() throws Exception {
470         final String html = DOCTYPE_HTML
471             + "<html>\n"
472             + "<body onload='test()'>\n"
473             + "<input id='i' value='abcdefghi'/>\n"
474             + "<script>\n"
475             + LOG_TITLE_FUNCTION
476             + "  function test() {\n"
477             + "    var i = document.getElementById('i');\n"
478             + "    var s = window.getSelection();\n"
479             + "    log(s);\n"
480             + "    log('' + s);\n"
481             + "    log(s.toString());\n"
482             + "  }\n"
483             + "</script>\n"
484             + "</body></html>";
485         loadPageVerifyTitle2(html);
486     }
487 }