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.html;
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.AfterEach;
21  import org.junit.jupiter.api.Test;
22  import org.openqa.selenium.By;
23  import org.openqa.selenium.WebDriver;
24  
25  /**
26   * Tests for elements with onblur and onfocus attributes.
27   *
28   * @author David D. Kilzer
29   * @author Marc Guillemot
30   * @author Ahmed Ashour
31   * @author Ronald Brill
32   * @author Frank Danek
33   */
34  public class FocusableElement2Test extends WebDriverTestCase {
35  
36      /**
37       * We like to start with a new browser for each test.
38       * @throws Exception If an error occurs
39       */
40      @AfterEach
41      public void shutDownRealBrowsers() throws Exception {
42          super.shutDownAll();
43      }
44  
45      /**
46       * @throws Exception if the test fails
47       */
48      @Test
49      @Alerts(DEFAULT = {"body", "active: body", "onload", "active: body",
50                         "onfocus:[object Window]", "active: body"},
51              CHROME = {"body", "active: body", "onload", "active: body"},
52              EDGE = {"body", "active: body", "onload", "active: body"})
53      @HtmlUnitNYI(FF = {"body", "active: body", "onload", "active: body"},
54              FF_ESR = {"body", "active: body", "onload", "active: body"})
55      // TODO FF & FF68 fail due to wrong body vs. window event handling
56      public void bodyLoad() throws Exception {
57          final String html = DOCTYPE_HTML
58              + "<html>\n"
59              + "  <head>\n"
60              + "    <script>\n"
61              + logger()
62              + "      function test() {\n"
63              + "        log('onload');\n"
64              + "      }\n"
65              + "    </script>\n"
66              + "  </head>\n"
67              + "  <body id='body' onload='test()' " + logEvents("") + ">\n"
68              + "    <script>\n"
69              + "      log('body');"
70              + "    </script>\n"
71              + "  </body>\n"
72              + "</html>\n";
73  
74          final WebDriver driver = loadPage2(html);
75          assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
76      }
77  
78      /**
79       * @throws Exception if the test fails
80       */
81      @Test
82      @Alerts(DEFAULT = {"onfocus:[object Window]", "active: focusId",
83                         "before", "active: focusId", "after", "active: focusId"},
84              CHROME = {"before", "active: focusId", "after", "active: focusId"},
85              EDGE = {"before", "active: focusId", "after", "active: focusId"})
86      @HtmlUnitNYI(FF = {"before", "active: focusId", "after", "active: focusId"},
87              FF_ESR = {"before", "active: focusId", "after", "active: focusId"})
88      // TODO FF & FF68 fail due to wrong body vs. window event handling
89      public void body() throws Exception {
90          final String html = DOCTYPE_HTML
91              + "<html>\n"
92              + "  <head>\n"
93              + "    <script>\n"
94              + logger()
95              + "      function test() {\n"
96              + "        log('before');\n"
97              + "        document.getElementById('focusId').focus();\n"
98              + "        document.getElementById('focusId').focus();\n"
99              + "        document.getElementById('focusId').blur();\n"
100             + "        document.getElementById('focusId').blur();\n"
101             + "        log('after');\n"
102             + "      }\n"
103             + "    </script>\n"
104             + "  </head>\n"
105             + "  <body id='focusId' onload='setTimeout(test, 10)' " + logEvents("") + ">\n"
106             + "  </body>\n"
107             + "</html>\n";
108 
109         final WebDriver driver = loadPage2(html);
110         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
111     }
112 
113     /**
114      * @throws Exception if the test fails
115      */
116     @Test
117     @Alerts(DEFAULT = {"onfocus:[object Window]", "active: body",
118                        "before", "active: body", "after", "active: body"},
119             CHROME = {"before", "active: body", "after", "active: body"},
120             EDGE = {"before", "active: body", "after", "active: body"})
121     @HtmlUnitNYI(FF = {"before", "active: body", "after", "active: body"},
122             FF_ESR = {"before", "active: body", "after", "active: body"})
123     // TODO FF & FF68 fail due to wrong body vs. window event handling
124     public void bodySwitchFromBodyToNotFocusable() throws Exception {
125         final String html = DOCTYPE_HTML
126             + "<html>\n"
127             + "  <head>\n"
128             + "    <script>\n"
129             + logger()
130             + "      function test() {\n"
131             + "        log('before');\n"
132             + "        document.getElementById('focusId').focus();\n"
133             + "        document.getElementById('focusId').blur();\n"
134             + "        log('after');\n"
135             + "      }\n"
136             + "    </script>\n"
137             + "  </head>\n"
138             + "  <body id='body' onload='setTimeout(test, 10)' " + logEvents("") + ">\n"
139             + "    <div id='focusId' " + logEvents("F") + ">div</div>\n"
140             + "  </body>\n"
141             + "</html>\n";
142 
143         final WebDriver driver = loadPage2(html);
144         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
145     }
146 
147     /**
148      * @throws Exception if the test fails
149      */
150     @Test
151     @Alerts(DEFAULT = {"onfocus:[object Window]", "active: body",
152                        "before", "active: body",
153                        "onfocusF:focusId", "active: focusId", "onfocusinF:focusId",
154                        "active: focusId", "onfocusin:focusId", "active: focusId",
155                        "onblurF:focusId", "active: body", "onfocusoutF:focusId",
156                        "active: body", "onfocusout:focusId", "active: body",
157                        "after", "active: body"},
158             CHROME = {"before", "active: body",
159                       "onfocusF:focusId", "active: focusId", "onfocusinF:focusId",
160                       "active: focusId", "onfocusin:focusId", "active: focusId",
161                       "onblurF:focusId", "active: body", "onfocusoutF:focusId",
162                       "active: body", "onfocusout:focusId", "active: body",
163                       "after", "active: body"},
164             EDGE = {"before", "active: body",
165                     "onfocusF:focusId", "active: focusId", "onfocusinF:focusId",
166                     "active: focusId", "onfocusin:focusId", "active: focusId",
167                     "onblurF:focusId", "active: body", "onfocusoutF:focusId",
168                     "active: body", "onfocusout:focusId", "active: body",
169                     "after", "active: body"})
170     @HtmlUnitNYI(FF = {"before", "active: body",
171                        "onfocusF:focusId", "active: focusId", "onfocusinF:focusId",
172                        "active: focusId", "onfocusin:focusId", "active: focusId",
173                        "onblurF:focusId", "active: body", "onfocusoutF:focusId",
174                        "active: body", "onfocusout:focusId", "active: body",
175                        "after", "active: body"},
176             FF_ESR = {"before", "active: body",
177                       "onfocusF:focusId", "active: focusId", "onfocusinF:focusId",
178                       "active: focusId", "onfocusin:focusId", "active: focusId",
179                       "onblurF:focusId", "active: body", "onfocusoutF:focusId",
180                       "active: body", "onfocusout:focusId", "active: body",
181                       "after", "active: body"})
182     // TODO FF & FF68 fail due to wrong body vs. window event handling
183     public void bodySwitchFromBodyToFocusable() throws Exception {
184         final String html = DOCTYPE_HTML
185             + "<html>\n"
186             + "  <head>\n"
187             + "    <script>\n"
188             + logger()
189             + "      function test() {\n"
190             + "        log('before');\n"
191             + "        document.getElementById('focusId').focus();\n"
192             + "        document.getElementById('focusId').blur();\n"
193             + "        log('after');\n"
194             + "      }\n"
195             + "    </script>\n"
196             + "  </head>\n"
197             + "  <body id='body' onload='setTimeout(test, 10)' " + logEvents("") + ">\n"
198             + "    <input type='text' id='focusId' " + logEvents("F") + ">\n"
199             + "  </body>\n"
200             + "</html>\n";
201 
202         final WebDriver driver = loadPage2(html);
203         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
204     }
205 
206     /**
207      * @throws Exception if the test fails
208      */
209     @Test
210     @Alerts(DEFAULT = {"onfocus:[object Window]", "active: body",
211                        "before", "active: body", "onfocusin:focusId1", "active: focusId1", "after", "active: focusId1"},
212             CHROME = {"before", "active: body", "onfocusin:focusId1", "active: focusId1", "after", "active: focusId1"},
213             EDGE = {"before", "active: body", "onfocusin:focusId1", "active: focusId1", "after", "active: focusId1"})
214     @HtmlUnitNYI(FF = {"before", "active: body", "onfocusin:focusId1", "active: focusId1", "after", "active: focusId1"},
215             FF_ESR = {"before", "active: body", "onfocusin:focusId1", "active: focusId1", "after", "active: focusId1"})
216     // TODO FF & FF68 fail due to wrong body vs. window event handling
217     public void bodySwitchFromFocusableToNotFocusable() throws Exception {
218         testBodySwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
219             + "<div id='focusId2'>div</div>");
220     }
221 
222     /**
223      * @throws Exception if the test fails
224      */
225     @Test
226     @Alerts(DEFAULT = {"onfocus:[object Window]", "active: body", "before", "active: body",
227                        "onfocusin:focusId1", "active: focusId1",
228                        "onfocusout:focusId1", "active: body", "onfocusin:focusId2", "active: focusId2",
229                        "after", "active: focusId2"},
230             CHROME = {"before", "active: body",
231                       "onfocusin:focusId1", "active: focusId1",
232                       "onfocusout:focusId1", "active: body", "onfocusin:focusId2", "active: focusId2",
233                       "after", "active: focusId2"},
234             EDGE = {"before", "active: body",
235                     "onfocusin:focusId1", "active: focusId1",
236                     "onfocusout:focusId1", "active: body", "onfocusin:focusId2", "active: focusId2",
237                     "after", "active: focusId2"})
238     @HtmlUnitNYI(FF = {"before", "active: body",
239                        "onfocusin:focusId1", "active: focusId1",
240                        "onfocusout:focusId1", "active: body", "onfocusin:focusId2", "active: focusId2",
241                        "after", "active: focusId2"},
242             FF_ESR = {"before", "active: body",
243                       "onfocusin:focusId1", "active: focusId1",
244                       "onfocusout:focusId1", "active: body", "onfocusin:focusId2", "active: focusId2",
245                       "after", "active: focusId2"})
246     // TODO FF & FF68 fail due to wrong body vs. window event handling
247     public void bodySwitchFromFocusableToFocusable() throws Exception {
248         testBodySwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
249             + "<input type='text' id='focusId2'>");
250     }
251 
252     private void testBodySwitchWithCallFocusAndBlur(final String snippet) throws Exception {
253         final String html = DOCTYPE_HTML
254             + "<html>\n"
255             + "  <head>\n"
256             + "    <script>\n"
257             + logger()
258             + "      function test() {\n"
259             + "        log('before');\n"
260             + "        document.getElementById('focusId1').focus();\n"
261             + "        document.getElementById('focusId2').focus();\n"
262             + "        log('after');\n"
263             + "      }\n"
264             + "    </script>\n"
265             + "  </head>\n"
266             + "  <body id='body' onload='setTimeout(test, 10)' " + logEvents("") + ">\n"
267             + snippet
268             + "  </body>\n"
269             + "</html>\n";
270 
271         final WebDriver driver = loadPage2(html);
272         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
273     }
274 
275     /**
276      * @throws Exception if the test fails
277      */
278     @Test
279     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
280     public void notFocusable() throws Exception {
281         testWithCallFocusAndBlur("<div id='focusId'>div</div>");
282     }
283 
284     /**
285      * @throws Exception if the test fails
286      */
287     @Test
288     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
289     public void notFocusableWithTabIndexEmpty() throws Exception {
290         testWithCallFocusAndBlur("<div tabindex='' id='focusId'>div</div>");
291     }
292 
293     /**
294      * @throws Exception if the test fails
295      */
296     @Test
297     @Alerts({"before", "active: body",
298              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
299              "between", "active: focusId",
300              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
301              "after", "active: body"})
302     public void notFocusableWithTabIndexNegative() throws Exception {
303         testWithCallFocusAndBlur("<div tabindex='-1' id='focusId'>div</div>");
304     }
305 
306     /**
307      * @throws Exception if the test fails
308      */
309     @Test
310     @Alerts({"before", "active: body",
311              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
312              "between", "active: focusId",
313              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
314              "after", "active: body"})
315     public void notFocusableWithTabIndexZero() throws Exception {
316         testWithCallFocusAndBlur("<div tabindex='0' id='focusId'>div</div>");
317     }
318 
319     /**
320      * @throws Exception if the test fails
321      */
322     @Test
323     @Alerts({"before", "active: body",
324              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
325              "between", "active: focusId",
326              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
327              "after", "active: body"})
328     public void notFocusableWithTabIndexPositive() throws Exception {
329         testWithCallFocusAndBlur("<div tabindex='1' id='focusId'>div</div>");
330     }
331 
332     /**
333      * @throws Exception if the test fails
334      */
335     @Test
336     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
337     public void notFocusableWithTabIndexNotDisplayed() throws Exception {
338         testWithCallFocusAndBlur("<div tabindex='0' style='display: none;' id='focusId'>div</div>");
339     }
340 
341     /**
342      * @throws Exception if the test fails
343      */
344     @Test
345     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
346     public void notFocusableWithTabIndexNotVisible() throws Exception {
347         testWithCallFocusAndBlur("<div tabindex='0' style='visibility: hidden;' id='focusId'>div</div>");
348     }
349 
350     /**
351      * @throws Exception if the test fails
352      */
353     @Test
354     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
355     public void notFocusableWithTabIndexHidden() throws Exception {
356         testWithCallFocusAndBlur("<div tabindex='0' hidden id='focusId'>div</div>");
357     }
358 
359     /**
360      * @throws Exception if the test fails
361      */
362     @Test
363     @Alerts({"before", "active: body",
364              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
365              "between", "active: focusId",
366              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
367              "after", "active: body"})
368     public void anchor() throws Exception {
369         testWithCallFocusAndBlur("<a href='#' id='focusId'>link</a>");
370     }
371 
372     /**
373      * @throws Exception if the test fails
374      */
375     @Test
376     @Alerts({"before", "active: body",
377              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
378              "between", "active: focusId",
379              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
380              "after", "active: body"})
381     public void anchorWithEmptyHref() throws Exception {
382         testWithCallFocusAndBlur("<a href='' id='focusId'>link</a>");
383     }
384 
385     /**
386      * @throws Exception if the test fails
387      */
388     @Test
389     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
390     public void anchorWithoutHref() throws Exception {
391         testWithCallFocusAndBlur("<a id='focusId'>link</a>");
392     }
393 
394     /**
395      * @throws Exception if the test fails
396      */
397     @Test
398     @Alerts({"before", "active: body",
399              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
400              "between", "active: focusId",
401              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
402              "after", "active: body"})
403     public void area() throws Exception {
404         testWithCallFocusAndBlur("<map name='dot'><area shape='rect' coords='0,0,5,5' href='#' id='focusId' /></map>"
405             + "<img usemap='#dot'"
406             + " src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAA"
407             + "HElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='>");
408     }
409 
410     /**
411      * @throws Exception if the test fails
412      */
413     @Test
414     @Alerts({"before", "active: body",
415              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
416              "between", "active: focusId",
417              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
418              "after", "active: body"})
419     public void areaWithEmptyHref() throws Exception {
420         testWithCallFocusAndBlur("<img usemap='#dot'"
421             + " src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAA"
422             + "HElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='>\n"
423             + "<map name='dot'><area shape='rect' coords='0,0,1,1' href='' id='focusId' /></map>");
424     }
425 
426     /**
427      * @throws Exception if the test fails
428      */
429     @Test
430     @Alerts(DEFAULT = {"before", "active: body", "between", "active: body", "after", "active: body"},
431             FF = {"before", "active: body",
432                   "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
433                   "between", "active: focusId",
434                   "onblur:focusId", "active: body", "onfocusout:focusId",
435                   "active: body", "after", "active: body"},
436             FF_ESR = {"before", "active: body",
437                       "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
438                       "between", "active: focusId",
439                       "onblur:focusId", "active: body", "onfocusout:focusId",
440                       "active: body", "after", "active: body"})
441     public void areaWithoutHref() throws Exception {
442         testWithCallFocusAndBlur("<img usemap='#dot'"
443             + " src='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAA"
444             + "HElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg=='>\n"
445             + "<map name='dot'><area shape='rect' coords='0,0,1,1' id='focusId' /></map>");
446     }
447 
448     /**
449      * @throws Exception if the test fails
450      */
451     @Test
452     @Alerts({"before", "active: body",
453              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
454              "between", "active: focusId",
455              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
456              "after", "active: body"})
457     public void button() throws Exception {
458         testWithCallFocusAndBlur("<button id='focusId'>button</button>");
459     }
460 
461     /**
462      * @throws Exception if the test fails
463      */
464     @Test
465     @Alerts({"before", "active: body",
466              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
467              "between", "active: focusId",
468              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
469              "after", "active: body"})
470     public void input() throws Exception {
471         testWithCallFocusAndBlur("<input type='text' id='focusId'>");
472     }
473 
474     /**
475      * @throws Exception if the test fails
476      */
477     @Test
478     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
479     public void inputHidden() throws Exception {
480         testWithCallFocusAndBlur("<input type='hidden' id='focusId'>");
481     }
482 
483     /**
484      * @throws Exception if the test fails
485      */
486     @Test
487     @Alerts({"before", "active: body",
488              "onfocusT:textId", "active: textId", "onfocusinT:textId", "active: textId",
489              "between", "active: textId", "after", "active: textId"})
490     public void labelFor() throws Exception {
491         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label</label><input type='text' id='textId' "
492                 + logEvents("T") + ">");
493     }
494 
495     /**
496      * @throws Exception if the test fails
497      */
498     @Test
499     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
500     public void labelForDisabled() throws Exception {
501         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label</label>"
502                 + "<input type='text' disabled id='textId' " + logEvents("T") + ">");
503     }
504 
505     /**
506      * @throws Exception if the test fails
507      */
508     @Test
509     @Alerts({"before", "active: body",
510              "onfocusT:textId", "active: textId", "onfocusinT:textId", "active: textId",
511              "between", "active: textId", "after", "active: textId"})
512     public void labelForReadonly() throws Exception {
513         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label</label>"
514                 + "<input type='text' readonly id='textId' " + logEvents("T") + ">");
515     }
516 
517     /**
518      * @throws Exception if the test fails
519      */
520     @Test
521     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
522     public void labelForNotDisplayed() throws Exception {
523         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label"
524                 + "</label><input type='text' style='display: none;' id='textId' " + logEvents("T") + ">");
525     }
526 
527     /**
528      * @throws Exception if the test fails
529      */
530     @Test
531     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
532     public void labelForNotVisible() throws Exception {
533         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label</label>"
534                 + "<input type='text' style='visibility: hidden;' id='textId' " + logEvents("T") + ">");
535     }
536 
537     /**
538      * @throws Exception if the test fails
539      */
540     @Test
541     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
542     public void labelForHidden() throws Exception {
543         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label</label>"
544                 + "<input type='text' hidden id='textId' " + logEvents("T") + ">");
545     }
546 
547     /**
548      * @throws Exception if the test fails
549      */
550     @Test
551     @Alerts({"before", "active: body",
552              "onfocusT:textId", "active: textId", "onfocusinT:textId", "active: textId",
553              "between", "active: textId", "after", "active: textId"})
554     public void labelNotDisplayedFor() throws Exception {
555         testWithCallFocusAndBlur("<label for='textId' style='display: none;' id='focusId'>label</label>"
556                 + "<input type='text' id='textId' " + logEvents("T") + ">");
557     }
558 
559     /**
560      * @throws Exception if the test fails
561      */
562     @Test
563     @Alerts({"before", "active: body",
564              "onfocusT:textId", "active: textId", "onfocusinT:textId", "active: textId",
565              "between", "active: textId", "after", "active: textId"})
566     public void labelNotVisibleFor() throws Exception {
567         testWithCallFocusAndBlur("<label for='textId' style='visibility: hidden;' id='focusId'>label</label>"
568                 + "<input type='text' id='textId' " + logEvents("T") + ">");
569     }
570 
571     /**
572      * @throws Exception if the test fails
573      */
574     @Test
575     @Alerts({"before", "active: body",
576              "onfocusT:textId", "active: textId", "onfocusinT:textId", "active: textId",
577              "between", "active: textId", "after", "active: textId"})
578     public void labelHiddenFor() throws Exception {
579         testWithCallFocusAndBlur("<label for='textId' hidden id='focusId'>label</label>"
580                 + "<input type='text' id='textId' " + logEvents("T") + ">");
581     }
582 
583     /**
584      * @throws Exception if the test fails
585      */
586     @Test
587     @Alerts({"before", "active: body",
588              "onfocusT:textId", "active: textId", "onfocusinT:textId",
589              "active: textId", "onfocusin:textId", "active: textId",
590              "between", "active: textId", "after", "active: textId"})
591     public void labelNested() throws Exception {
592         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label <input type='text' id='textId' "
593                 + logEvents("T") + "></label>");
594     }
595 
596     /**
597      * @throws Exception if the test fails
598      */
599     @Test
600     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
601     public void labelNestedDisabled() throws Exception {
602         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label "
603                 + "<input type='text' disabled id='textId' " + logEvents("T") + "></label>");
604     }
605 
606     /**
607      * @throws Exception if the test fails
608      */
609     @Test
610     @Alerts({"before", "active: body",
611              "onfocusT:textId", "active: textId", "onfocusinT:textId",
612              "active: textId", "onfocusin:textId", "active: textId",
613              "between", "active: textId", "after", "active: textId"})
614     public void labelNestedReadonly() throws Exception {
615         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label <input type='text' readonly id='textId' "
616                 + logEvents("T") + "></label>");
617     }
618 
619     /**
620      * @throws Exception if the test fails
621      */
622     @Test
623     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
624     public void labelNestedNotDisplayed() throws Exception {
625         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label "
626                 + "<input type='text' style='display: none;' id='textId' " + logEvents("T") + "></label>");
627     }
628 
629     /**
630      * @throws Exception if the test fails
631      */
632     @Test
633     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
634     public void labelNestedNotVisible() throws Exception {
635         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label "
636                 + "<input type='text' style='display: none;' id='textId' "
637                 + logEvents("T") + "></label>");
638     }
639 
640     /**
641      * @throws Exception if the test fails
642      */
643     @Test
644     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
645     public void labelNestedHidden() throws Exception {
646         testWithCallFocusAndBlur("<label for='textId' id='focusId'>label <input type='text' hidden id='textId' "
647                 + logEvents("T") + "></label>");
648     }
649 
650     /**
651      * @throws Exception if the test fails
652      */
653     @Test
654     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
655     public void labelNotDisplayedNested() throws Exception {
656         testWithCallFocusAndBlur("<label for='textId' style='display: none;' id='focusId'>label "
657                 + "<input type='text' id='textId' " + logEvents("T") + "></label>");
658     }
659 
660     /**
661      * @throws Exception if the test fails
662      */
663     @Test
664     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
665     public void labelNotVisibleNested() throws Exception {
666         testWithCallFocusAndBlur("<label for='textId' style='visibility: hidden;' id='focusId'>label "
667                 + "<input type='text' id='textId' " + logEvents("T") + "></label>");
668     }
669 
670     /**
671      * @throws Exception if the test fails
672      */
673     @Test
674     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
675     public void labelHiddenNested() throws Exception {
676         testWithCallFocusAndBlur("<label for='textId' hidden id='focusId'>label "
677                 + "<input type='text' id='textId' " + logEvents("T") + "></label>");
678     }
679 
680     /**
681      * @throws Exception if the test fails
682      */
683     @Test
684     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
685     public void optionGroup() throws Exception {
686         testWithCallFocusAndBlur("<select><optgroup label='group' id='focusId'><option>1</option></optgroup></select>");
687     }
688 
689     /**
690      * @throws Exception if the test fails
691      */
692     @Test
693     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
694     public void option() throws Exception {
695         testWithCallFocusAndBlur("<select><option id='focusId'>1</option></select>");
696     }
697 
698     /**
699      * @throws Exception if the test fails
700      */
701     @Test
702     @Alerts({"before", "active: body",
703              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
704              "between", "active: focusId",
705              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
706              "after", "active: body"})
707     public void select() throws Exception {
708         testWithCallFocusAndBlur("<select id='focusId'><option>1</option></select>");
709     }
710 
711     /**
712      * @throws Exception if the test fails
713      */
714     @Test
715     @Alerts({"before", "active: body",
716              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
717              "between", "active: focusId",
718              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
719              "after", "active: body"})
720     public void textArea() throws Exception {
721         testWithCallFocusAndBlur("<textarea id='focusId'></textarea>");
722     }
723 
724     /**
725      * @throws Exception if the test fails
726      */
727     @Test
728     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
729     public void focusableDisabled() throws Exception {
730         testWithCallFocusAndBlur("<input type='text' disabled id='focusId'>");
731     }
732 
733     /**
734      * @throws Exception if the test fails
735      */
736     @Test
737     @Alerts({"before", "active: body",
738              "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
739              "between", "active: focusId",
740              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
741              "after", "active: body"})
742     public void focusableReadonly() throws Exception {
743         testWithCallFocusAndBlur("<input type='text' readonly id='focusId'>");
744     }
745 
746     /**
747      * @throws Exception if the test fails
748      */
749     @Test
750     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
751     public void focusableNotDisplayed() throws Exception {
752         testWithCallFocusAndBlur("<input type='text' style='display: none;' id='focusId'>");
753     }
754 
755     /**
756      * @throws Exception if the test fails
757      */
758     @Test
759     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
760     public void focusableNotVisible() throws Exception {
761         testWithCallFocusAndBlur("<input type='text' style='visibility: hidden;' id='focusId'>");
762     }
763 
764     /**
765      * @throws Exception if the test fails
766      */
767     @Test
768     @Alerts({"before", "active: body", "between", "active: body", "after", "active: body"})
769     public void focusableHidden() throws Exception {
770         testWithCallFocusAndBlur("<input type='text' hidden id='focusId'>");
771     }
772 
773     private void testWithCallFocusAndBlur(String snippet) throws Exception {
774         snippet = snippet.replaceFirst("id='focusId'( /)?>", "id='focusId' " + logEvents("") + "$1>");
775 
776         final String html = DOCTYPE_HTML
777             + "<html>\n"
778             + "  <head>\n"
779             + "    <script>\n"
780             + logger()
781             + "      function test() {\n"
782             + "        log('before');\n"
783             + "        document.getElementById('focusId').focus();\n"
784             + "        document.getElementById('focusId').focus();\n"
785             + "        log('between');\n"
786             + "        document.getElementById('focusId').blur();\n"
787             + "        document.getElementById('focusId').blur();\n"
788             + "        log('after');\n"
789             + "      }\n"
790             + "    </script>\n"
791             + "  </head>\n"
792             + "  <body id='body' onload='setTimeout(test, 10)'>\n"
793             + snippet
794             + "  </body>\n"
795             + "</html>\n";
796 
797         final WebDriver driver = loadPage2(html);
798         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
799     }
800 
801     /**
802      * @throws Exception if the test fails
803      */
804     @Test
805     @Alerts({"before", "active: body",
806              "onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
807              "after", "active: focusId1"})
808     public void switchFromFocusableToNotFocusable() throws Exception {
809         testSwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
810             + "<div id='focusId2'>div</div>");
811     }
812 
813     /**
814      * @throws Exception if the test fails
815      */
816     @Test
817     @Alerts({"before", "active: body",
818              "onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
819              "onblur1:focusId1", "active: body", "onfocusout1:focusId1", "active: body",
820              "onfocus2:focusId2", "active: focusId2", "onfocusin2:focusId2", "active: focusId2",
821              "after", "active: focusId2"})
822     public void switchFromFocusableToFocusable() throws Exception {
823         testSwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
824             + "<input type='text' id='focusId2'>");
825     }
826 
827     /**
828      * @throws Exception if the test fails
829      */
830     @Test
831     @Alerts({"before", "active: body",
832              "onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
833              "after", "active: focusId1"})
834     public void switchFromFocusableToFocusableDisabled() throws Exception {
835         testSwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
836             + "<input type='text' disabled id='focusId2'>");
837     }
838 
839     /**
840      * @throws Exception if the test fails
841      */
842     @Test
843     @Alerts({"before", "active: body",
844              "onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
845              "onblur1:focusId1", "active: body", "onfocusout1:focusId1", "active: body",
846              "onfocus2:focusId2", "active: focusId2", "onfocusin2:focusId2", "active: focusId2",
847              "after", "active: focusId2"})
848     public void switchFromFocusableToFocusableReadonly() throws Exception {
849         testSwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
850             + "<input type='text' readonly id='focusId2'>");
851     }
852 
853     /**
854      * @throws Exception if the test fails
855      */
856     @Test
857     @Alerts({"before", "active: body",
858              "onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
859              "after", "active: focusId1"})
860     public void switchFromFocusableToFocusableNotDisplayed() throws Exception {
861         testSwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
862             + "<input type='text' style='display: none;' id='focusId2'>");
863     }
864 
865     /**
866      * @throws Exception if the test fails
867      */
868     @Test
869     @Alerts({"before", "active: body",
870              "onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
871              "after", "active: focusId1"})
872     public void switchFromFocusableToFocusableNotVisible() throws Exception {
873         testSwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
874             + "<input type='text' style='visibility: hidden;' id='focusId2'>");
875     }
876 
877     /**
878      * @throws Exception if the test fails
879      */
880     @Test
881     @Alerts({"before", "active: body",
882              "onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
883              "after", "active: focusId1"})
884     public void switchFromFocusableToFocusableHidden() throws Exception {
885         testSwitchWithCallFocusAndBlur("<input type='text' id='focusId1'>\n"
886             + "<input type='text' hidden id='focusId2'>");
887     }
888 
889     private void testSwitchWithCallFocusAndBlur(String snippet) throws Exception {
890         snippet = snippet.replaceFirst("id='focusId1'( /)?>", "id='focusId1' " + logEvents("1") + "$1>");
891         snippet = snippet.replaceFirst("id='focusId2'( /)?>", "id='focusId2' " + logEvents("2") + "$1>");
892 
893         final String html = DOCTYPE_HTML
894             + "<html>\n"
895             + "  <head>\n"
896             + "    <script>\n"
897             + logger()
898             + "      function test() {\n"
899             + "        log('before');\n"
900             + "        document.getElementById('focusId1').focus();\n"
901             + "        document.getElementById('focusId2').focus();\n"
902             + "        log('after');\n"
903             + "      }\n"
904             + "    </script>\n"
905             + "  </head>\n"
906             + "  <body id='body' onload='setTimeout(test, 10)'>\n"
907             + snippet
908             + "  </body>\n"
909             + "</html>\n";
910 
911         final WebDriver driver = loadPage2(html);
912         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
913     }
914 
915     /**
916      * @throws Exception if the test fails
917      */
918     @Test
919     @Alerts({"before", "active: body", "after", "active: body"})
920     public void jsClickOnNotFocusable() throws Exception {
921         testWithCallClick("<div id='focusId'>div</div>");
922     }
923 
924     /**
925      * @throws Exception if the test fails
926      */
927     @Test
928     @Alerts({"before", "active: body", "after", "active: body"})
929     @HtmlUnitNYI(CHROME = {"before", "active: body",
930                            "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
931                            "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
932                            "after", "active: body"},
933             EDGE = {"before", "active: body",
934                     "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
935                     "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
936                     "after", "active: body"},
937             FF = {"before", "active: body",
938                   "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
939                   "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
940                   "after", "active: body"},
941             FF_ESR = {"before", "active: body",
942                       "onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
943                       "onblur:focusId", "active: body", "onfocusout:focusId", "active: body",
944                       "after", "active: body"})
945     // FIXME click for either divs or inputs seems broken... :(
946     public void jsClickOnNotFocusableWithTabIndex() throws Exception {
947         testWithCallClick("<div tabindex='0' id='focusId'>div</div>");
948     }
949 
950     /**
951      * @throws Exception if the test fails
952      */
953     @Test
954     @Alerts({"before", "active: body", "after", "active: body"})
955     public void jsClickOnFocusable() throws Exception {
956         testWithCallClick("<input type='text' id='focusId'>");
957     }
958 
959     private void testWithCallClick(String snippet) throws Exception {
960         snippet = snippet.replaceFirst("id='focusId'( /)?>", "id='focusId' " + logEvents("") + "$1>");
961 
962         final String html = DOCTYPE_HTML
963             + "<html>\n"
964             + "  <head>\n"
965             + "    <script>\n"
966             + logger()
967             + "      function test() {\n"
968             + "        log('before');\n"
969             + "        document.getElementById('focusId').click();\n"
970             + "        document.getElementById('focusId').click();\n"
971             + "        document.getElementById('otherId').click();\n"
972             + "        log('after');\n"
973             + "      }\n"
974             + "    </script>\n"
975             + "  </head>\n"
976             + "  <body id='body' onload='setTimeout(test, 10)'>\n"
977             + snippet
978             + "    <div id='otherId'>div</div>\n"
979             + "  </body>\n"
980             + "</html>\n";
981 
982         final WebDriver driver = loadPage2(html);
983         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
984     }
985 
986     /**
987      * @throws Exception if the test fails
988      */
989     @Test
990     public void clickOnNotFocusable() throws Exception {
991         testWithClick("<div id='focusId'>div</div>");
992     }
993 
994     /**
995      * @throws Exception if the test fails
996      */
997     @Test
998     @Alerts({"onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
999              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body"})
1000     public void clickOnNotFocusableWithTabIndex() throws Exception {
1001         testWithClick("<div tabindex='0' id='focusId'>div</div>");
1002     }
1003 
1004     /**
1005      * @throws Exception if the test fails
1006      */
1007     @Test
1008     @Alerts({"onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
1009              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body"})
1010     public void clickOnFocusable() throws Exception {
1011         testWithClick("<input type='text' id='focusId'>");
1012     }
1013 
1014     /**
1015      * @throws Exception if the test fails
1016      */
1017     @Test
1018     public void clickOnFocusableDisabled() throws Exception {
1019         testWithClick("<input type='text' disabled id='focusId'>");
1020     }
1021 
1022     /**
1023      * @throws Exception if the test fails
1024      */
1025     @Test
1026     @Alerts({"onfocus:focusId", "active: focusId", "onfocusin:focusId", "active: focusId",
1027              "onblur:focusId", "active: body", "onfocusout:focusId", "active: body"})
1028     public void clickOnFocusableReadonly() throws Exception {
1029         testWithClick("<input type='text' readonly id='focusId'>");
1030     }
1031 
1032     /**
1033      * @throws Exception if the test fails
1034      */
1035     @Test
1036     @Alerts({"onfocusT:textId", "active: textId", "onfocusinT:textId", "active: textId",
1037              "onblurT:textId", "active: body", "onfocusoutT:textId", "active: body"})
1038     public void clickOnLabelFor() throws Exception {
1039         testWithClick("<label for='textId' id='focusId'>label</label><input type='text' id='textId' "
1040                 + logEvents("T") + ">");
1041     }
1042 
1043     /**
1044      * @throws Exception if the test fails
1045      */
1046     @Test
1047     @Alerts({"onfocusT:textId", "active: textId", "onfocusinT:textId",
1048              "active: textId", "onfocusin:textId", "active: textId",
1049              "onblurT:textId", "active: body", "onfocusoutT:textId",
1050              "active: body", "onfocusout:textId", "active: body"})
1051     public void clickOnLabelNested() throws Exception {
1052         testWithClick("<label for='textId' id='focusId'>label <input type='text' id='textId' "
1053                 + logEvents("T") + "></label>");
1054     }
1055 
1056     private void testWithClick(String snippet) throws Exception {
1057         snippet = snippet.replaceFirst("id='focusId'( /)?>", "id='focusId' " + logEvents("") + "$1>");
1058 
1059         final String html = DOCTYPE_HTML
1060             + "<html>\n"
1061             + "  <head>\n"
1062             + "    <script>\n"
1063             + logger()
1064             + "    </script>\n"
1065             + "  </head>\n"
1066             + "  <body id='body'>\n"
1067             + snippet
1068             + "    <div id='otherId'>div</div>\n"
1069             + "  </body>\n"
1070             + "</html>\n";
1071 
1072         final WebDriver driver = loadPage2(html);
1073 
1074         driver.findElement(By.id("focusId")).click();
1075         driver.findElement(By.id("otherId")).click();
1076 
1077         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
1078     }
1079 
1080     /**
1081      * @throws Exception if the test fails
1082      */
1083     @Test
1084     @Alerts({"onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
1085              "onblur1:focusId1", "active: body", "onfocusout1:focusId1", "active: body",
1086              "onfocus2:focusId2", "active: focusId2", "onfocusin2:focusId2", "active: focusId2"})
1087     public void clickFromFocusableToFocusable() throws Exception {
1088         testSwitchWithClick("<input type='text' id='focusId1'>\n"
1089             + "<input type='text' id='focusId2'>");
1090     }
1091 
1092     /**
1093      * @throws Exception if the test fails
1094      */
1095     @Test
1096     @Alerts({"onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
1097              "onblur1:focusId1", "active: body", "onfocusout1:focusId1", "active: body"})
1098     @HtmlUnitNYI(CHROME = {"onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1"},
1099             EDGE = {"onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1"},
1100             FF = {"onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1"},
1101             FF_ESR = {"onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1"})
1102     public void clickFromFocusableToFocusableDisabled() throws Exception {
1103         testSwitchWithClick("<input type='text' id='focusId1'>\n"
1104             + "<input type='text' disabled id='focusId2'>");
1105     }
1106 
1107     /**
1108      * @throws Exception if the test fails
1109      */
1110     @Test
1111     @Alerts({"onfocus1:focusId1", "active: focusId1", "onfocusin1:focusId1", "active: focusId1",
1112              "onblur1:focusId1", "active: body", "onfocusout1:focusId1", "active: body",
1113              "onfocus2:focusId2", "active: focusId2", "onfocusin2:focusId2", "active: focusId2"})
1114     public void clickFromFocusableToFocusableReadonly() throws Exception {
1115         testSwitchWithClick("<input type='text' id='focusId1'>\n"
1116             + "<input type='text' readonly id='focusId2'>");
1117     }
1118 
1119     private void testSwitchWithClick(String snippet) throws Exception {
1120         snippet = snippet.replaceFirst("id='focusId1'( /)?>", "id='focusId1' " + logEvents("1") + "$1>");
1121         snippet = snippet.replaceFirst("id='focusId2'( /)?>", "id='focusId2' " + logEvents("2") + "$1>");
1122 
1123         final String html = DOCTYPE_HTML
1124             + "<html>\n"
1125             + "  <head>\n"
1126             + "    <script>\n"
1127             + logger()
1128             + "    </script>\n"
1129             + "  </head>\n"
1130             + "  <body id='body'>\n"
1131             + snippet
1132             + "  </body>\n"
1133             + "</html>\n";
1134 
1135         final WebDriver driver = loadPage2(html);
1136 
1137         driver.findElement(By.id("focusId1")).click();
1138         driver.findElement(By.id("focusId2")).click();
1139 
1140         assertTitle(driver, String.join(";", getExpectedAlerts()) + (getExpectedAlerts().length > 0 ? ";" : ""));
1141     }
1142 
1143     private static String logger() {
1144         return  "      function log(x, e) {\n"
1145             + "        document.title += x + (e ? ':' + (e.target.id ? e.target.id : e.target) : '') + ';';\n"
1146             + "        document.title += 'active: ' "
1147                         + "+ (document.activeElement ? document.activeElement.id : 'null') + ';';\n"
1148             + "      }\n";
1149     }
1150 
1151     private static String logEvents(final String aSuffix) {
1152         return "onblur=\"log('onblur" + aSuffix + "', event)\" "
1153             + "onfocusin=\"log('onfocusin" + aSuffix + "', event)\" "
1154             + "onfocusout=\"log('onfocusout" + aSuffix + "', event)\" "
1155             + "onfocus=\"log('onfocus" + aSuffix + "', event)\"";
1156     }
1157 }