OLD | NEW |
1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 /** | 5 /** |
6 * @fileoverview Tests for cr-action-menu element. Runs as an interactive UI | 6 * @fileoverview Tests for cr-action-menu element. Runs as an interactive UI |
7 * test, since many of these tests check focus behavior. | 7 * test, since many of these tests check focus behavior. |
8 */ | 8 */ |
9 suite('CrActionMenu', function() { | 9 suite('CrActionMenu', function() { |
10 /** @type {?CrActionMenuElement} */ | 10 /** @type {?CrActionMenuElement} */ |
11 var menu = null; | 11 var menu = null; |
12 | 12 |
13 /** @type {?NodeList<HTMLElement>} */ | 13 /** @type {?NodeList<HTMLElement>} */ |
14 var items = null; | 14 var items = null; |
15 | 15 |
16 /** @type {HTMLElement} */ | |
17 var dots = null; | |
18 | |
19 setup(function() { | 16 setup(function() { |
20 PolymerTest.clearBody(); | 17 PolymerTest.clearBody(); |
21 | 18 |
22 document.body.innerHTML = ` | 19 document.body.innerHTML = ` |
23 <button id="dots">...</button> | 20 <button id="dots">...</button> |
24 <dialog is="cr-action-menu"> | 21 <dialog is="cr-action-menu"> |
25 <button class="dropdown-item">Un</button> | 22 <button class="dropdown-item">Un</button> |
26 <hr> | 23 <hr> |
27 <button class="dropdown-item">Dos</button> | 24 <button class="dropdown-item">Dos</button> |
28 <button class="dropdown-item">Tres</button> | 25 <button class="dropdown-item">Tres</button> |
29 </dialog> | 26 </dialog> |
30 `; | 27 `; |
31 | 28 |
32 menu = document.querySelector('dialog[is=cr-action-menu]'); | 29 menu = document.querySelector('dialog[is=cr-action-menu]'); |
33 items = menu.querySelectorAll('.dropdown-item'); | 30 items = menu.querySelectorAll('.dropdown-item'); |
34 dots = document.querySelector('#dots'); | |
35 assertEquals(3, items.length); | 31 assertEquals(3, items.length); |
36 }); | 32 }); |
37 | 33 |
38 teardown(function() { | 34 teardown(function() { |
39 document.body.style.direction = 'ltr'; | |
40 | |
41 if (menu.open) | 35 if (menu.open) |
42 menu.close(); | 36 menu.close(); |
43 }); | 37 }); |
44 | 38 |
45 function down() { | 39 function down() { |
46 MockInteractions.keyDownOn(menu, 'ArrowDown', [], 'ArrowDown'); | 40 MockInteractions.keyDownOn(menu, 'ArrowDown', [], 'ArrowDown'); |
47 } | 41 } |
48 | 42 |
49 function up() { | 43 function up() { |
50 MockInteractions.keyDownOn(menu, 'ArrowUp', [], 'ArrowUp'); | 44 MockInteractions.keyDownOn(menu, 'ArrowUp', [], 'ArrowUp'); |
51 } | 45 } |
52 | 46 |
53 test('hidden or disabled items', function() { | 47 test('hidden or disabled items', function() { |
54 menu.showAt(dots); | 48 menu.showAt(document.querySelector('#dots')); |
55 down(); | 49 down(); |
56 assertEquals(menu.root.activeElement, items[0]); | 50 assertEquals(menu.root.activeElement, items[0]); |
57 | 51 |
58 menu.close(); | 52 menu.close(); |
59 items[0].hidden = true; | 53 items[0].hidden = true; |
60 menu.showAt(dots); | 54 menu.showAt(document.querySelector('#dots')); |
61 down(); | 55 down(); |
62 assertEquals(menu.root.activeElement, items[1]); | 56 assertEquals(menu.root.activeElement, items[1]); |
63 | 57 |
64 menu.close(); | 58 menu.close(); |
65 items[1].disabled = true; | 59 items[1].disabled = true; |
66 menu.showAt(dots); | 60 menu.showAt(document.querySelector('#dots')); |
67 down(); | 61 down(); |
68 assertEquals(menu.root.activeElement, items[2]); | 62 assertEquals(menu.root.activeElement, items[2]); |
69 }); | 63 }); |
70 | 64 |
71 test('focus after down/up arrow', function() { | 65 test('focus after down/up arrow', function() { |
72 menu.showAt(dots); | 66 menu.showAt(document.querySelector('#dots')); |
73 | 67 |
74 // The menu should be focused when shown, but not on any of the items. | 68 // The menu should be focused when shown, but not on any of the items. |
75 assertEquals(menu, document.activeElement); | 69 assertEquals(menu, document.activeElement); |
76 assertNotEquals(items[0], menu.root.activeElement); | 70 assertNotEquals(items[0], menu.root.activeElement); |
77 assertNotEquals(items[1], menu.root.activeElement); | 71 assertNotEquals(items[1], menu.root.activeElement); |
78 assertNotEquals(items[2], menu.root.activeElement); | 72 assertNotEquals(items[2], menu.root.activeElement); |
79 | 73 |
80 down(); | 74 down(); |
81 assertEquals(items[0], menu.root.activeElement); | 75 assertEquals(items[0], menu.root.activeElement); |
82 down(); | 76 down(); |
83 assertEquals(items[1], menu.root.activeElement); | 77 assertEquals(items[1], menu.root.activeElement); |
84 down(); | 78 down(); |
85 assertEquals(items[2], menu.root.activeElement); | 79 assertEquals(items[2], menu.root.activeElement); |
86 down(); | 80 down(); |
87 assertEquals(items[0], menu.root.activeElement); | 81 assertEquals(items[0], menu.root.activeElement); |
88 up(); | 82 up(); |
89 assertEquals(items[2], menu.root.activeElement); | 83 assertEquals(items[2], menu.root.activeElement); |
90 up(); | 84 up(); |
91 assertEquals(items[1], menu.root.activeElement); | 85 assertEquals(items[1], menu.root.activeElement); |
92 up(); | 86 up(); |
93 assertEquals(items[0], menu.root.activeElement); | 87 assertEquals(items[0], menu.root.activeElement); |
94 up(); | 88 up(); |
95 assertEquals(items[2], menu.root.activeElement); | 89 assertEquals(items[2], menu.root.activeElement); |
96 | 90 |
97 items[1].disabled = true; | 91 items[1].disabled = true; |
98 up(); | 92 up(); |
99 assertEquals(items[0], menu.root.activeElement); | 93 assertEquals(items[0], menu.root.activeElement); |
100 }); | 94 }); |
101 | 95 |
102 test('pressing up arrow when no focus will focus last item', function() { | 96 test('pressing up arrow when no focus will focus last item', function(){ |
103 menu.showAt(dots); | 97 menu.showAt(document.querySelector('#dots')); |
104 assertEquals(menu, document.activeElement); | 98 assertEquals(menu, document.activeElement); |
105 | 99 |
106 up(); | 100 up(); |
107 assertEquals(items[items.length - 1], menu.root.activeElement); | 101 assertEquals(items[items.length - 1], menu.root.activeElement); |
108 }); | 102 }); |
109 | 103 |
110 test('can navigate to dynamically added items', function() { | 104 test('can navigate to dynamically added items', function() { |
111 // Can modify children after attached() and before showAt(). | 105 // Can modify children after attached() and before showAt(). |
112 var item = document.createElement('button'); | 106 var item = document.createElement('button'); |
113 item.classList.add('dropdown-item'); | 107 item.classList.add('dropdown-item'); |
114 menu.insertBefore(item, items[0]); | 108 menu.insertBefore(item, items[0]); |
115 menu.showAt(dots); | 109 menu.showAt(document.querySelector('#dots')); |
116 | 110 |
117 down(); | 111 down(); |
118 assertEquals(item, menu.root.activeElement); | 112 assertEquals(item, menu.root.activeElement); |
119 down(); | 113 down(); |
120 assertEquals(items[0], menu.root.activeElement); | 114 assertEquals(items[0], menu.root.activeElement); |
121 | 115 |
122 // Can modify children while menu is open. | 116 // Can modify children while menu is open. |
123 menu.removeChild(item); | 117 menu.removeChild(item); |
124 | 118 |
125 up(); | 119 up(); |
126 // Focus should have wrapped around to final item. | 120 // Focus should have wrapped around to final item. |
127 assertEquals(items[2], menu.root.activeElement); | 121 assertEquals(items[2], menu.root.activeElement); |
128 }); | 122 }); |
129 | 123 |
130 test('close on resize', function() { | 124 test('close on resize', function() { |
131 menu.showAt(dots); | 125 menu.showAt(document.querySelector('#dots')); |
132 assertTrue(menu.open); | 126 assertTrue(menu.open); |
133 | 127 |
134 window.dispatchEvent(new CustomEvent('resize')); | 128 window.dispatchEvent(new CustomEvent('resize')); |
135 assertFalse(menu.open); | 129 assertFalse(menu.open); |
136 }); | 130 }); |
137 | 131 |
138 test('close on popstate', function() { | 132 test('close on popstate', function() { |
139 menu.showAt(dots); | 133 menu.showAt(document.querySelector('#dots')); |
140 assertTrue(menu.open); | 134 assertTrue(menu.open); |
141 | 135 |
142 window.dispatchEvent(new CustomEvent('popstate')); | 136 window.dispatchEvent(new CustomEvent('popstate')); |
143 assertFalse(menu.open); | 137 assertFalse(menu.open); |
144 }); | 138 }); |
145 | 139 |
146 /** @param {string} key The key to use for closing. */ | 140 /** @param {string} key The key to use for closing. */ |
147 function testFocusAfterClosing(key) { | 141 function testFocusAfterClosing(key) { |
148 return new Promise(function(resolve) { | 142 return new Promise(function(resolve) { |
| 143 var dots = document.querySelector('#dots'); |
149 menu.showAt(dots); | 144 menu.showAt(dots); |
150 assertTrue(menu.open); | 145 assertTrue(menu.open); |
151 | 146 |
152 // Check that focus returns to the anchor element. | 147 // Check that focus returns to the anchor element. |
153 dots.addEventListener('focus', resolve); | 148 dots.addEventListener('focus', resolve); |
154 MockInteractions.keyDownOn(menu, key, [], key); | 149 MockInteractions.keyDownOn(menu, key, [], key); |
155 assertFalse(menu.open); | 150 assertFalse(menu.open); |
156 }); | 151 }); |
157 } | 152 } |
158 | 153 |
159 test('close on Tab', function() { | 154 test('close on Tab', function() { return testFocusAfterClosing('Tab'); }); |
160 return testFocusAfterClosing('Tab'); | |
161 }); | |
162 test('close on Escape', function() { | 155 test('close on Escape', function() { |
163 return testFocusAfterClosing('Escape'); | 156 return testFocusAfterClosing('Escape'); |
164 }); | 157 }); |
165 | 158 |
166 test('mouse movement focus options', function() { | 159 test('mouse movement focus options', function() { |
167 function makeMouseoverEvent(node) { | 160 function makeMouseoverEvent(node) { |
168 var e = new MouseEvent('mouseover', {bubbles: true}); | 161 var e = new MouseEvent('mouseover', {bubbles: true}); |
169 node.dispatchEvent(e); | 162 node.dispatchEvent(e); |
170 } | 163 } |
171 | 164 |
172 menu.showAt(dots); | 165 menu.showAt(document.querySelector('#dots')); |
173 | 166 |
174 // Moving mouse on option 1 should focus it. | 167 // Moving mouse on option 1 should focus it. |
175 assertNotEquals(items[0], menu.root.activeElement); | 168 assertNotEquals(items[0], menu.root.activeElement); |
176 makeMouseoverEvent(items[0]); | 169 makeMouseoverEvent(items[0]); |
177 assertEquals(items[0], menu.root.activeElement); | 170 assertEquals(items[0], menu.root.activeElement); |
178 | 171 |
179 // Moving mouse on the menu (not on option) should focus the menu. | 172 // Moving mouse on the menu (not on option) should focus the menu. |
180 makeMouseoverEvent(menu); | 173 makeMouseoverEvent(menu); |
181 assertNotEquals(items[0], menu.root.activeElement); | 174 assertNotEquals(items[0], menu.root.activeElement); |
182 assertEquals(menu, document.activeElement); | 175 assertEquals(menu, document.activeElement); |
(...skipping 76 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
259 menu.close(); | 252 menu.close(); |
260 | 253 |
261 // If the viewport can't fit the menu, align the menu to the viewport. | 254 // If the viewport can't fit the menu, align the menu to the viewport. |
262 menu.showAtPosition({ | 255 menu.showAtPosition({ |
263 left: menuWidth - 5, | 256 left: menuWidth - 5, |
264 top: 0, | 257 top: 0, |
265 width: 0, | 258 width: 0, |
266 height: 0, | 259 height: 0, |
267 maxX: menuWidth * 2 - 10, | 260 maxX: menuWidth * 2 - 10, |
268 }); | 261 }); |
269 assertEquals(`${menuWidth - 10}px`, menu.style.left); | 262 assertEquals(`${menuWidth - 10}px`, menu.style.left); |
270 assertEquals(`0px`, menu.style.top); | 263 assertEquals(`0px`, menu.style.top); |
271 menu.close(); | 264 menu.close(); |
272 | 265 |
273 // Alignment is reversed in RTL. | 266 // Alignment is reversed in RTL. |
274 document.body.style.direction = 'rtl'; | 267 document.body.style.direction = 'rtl'; |
275 menu.showAtPosition(config); | 268 menu.showAtPosition(config); |
276 assertTrue(menu.open); | 269 assertTrue(menu.open); |
277 assertEquals(140 - menuWidth, menu.offsetLeft); | 270 assertEquals(140 - menuWidth, menu.offsetLeft); |
278 assertEquals('250px', menu.style.top); | 271 assertEquals('250px', menu.style.top); |
279 menu.close(); | 272 menu.close(); |
| 273 document.body.style.direction = 'ltr'; |
280 }); | 274 }); |
281 | 275 |
282 suite('offscreen scroll positioning', function() { | 276 test('offscreen scroll positioning', function() { |
283 var bodyHeight = 10000; | 277 var bodyHeight = 10000; |
284 var bodyWidth = 20000; | 278 var bodyWidth = 20000; |
285 var containerLeft = 5000; | 279 var containerLeft = 5000; |
286 var containerTop = 10000; | 280 var containerTop = 10000; |
287 var containerWidth = 500; | 281 var containerWidth = 500; |
288 var containerHeight = 500; | 282 var containerHeight = 500; |
| 283 document.body.innerHTML = ` |
| 284 <style> |
| 285 body { |
| 286 height: ${bodyHeight}px; |
| 287 width: ${bodyWidth}px; |
| 288 } |
289 | 289 |
290 setup(function() { | 290 #container { |
291 document.body.scrollTop = 0; | 291 overflow: auto; |
292 document.body.scrollLeft = 0; | 292 position: absolute; |
293 document.body.innerHTML = ` | 293 top: ${containerTop}px; |
294 <style> | 294 left: ${containerLeft}px; |
295 body { | 295 height: ${containerHeight}px; |
296 height: ${bodyHeight}px; | 296 width: ${containerWidth}px; |
297 width: ${bodyWidth}px; | 297 } |
298 } | |
299 | 298 |
300 #container { | 299 #inner-container { |
301 overflow: auto; | 300 height: 1000px; |
302 position: absolute; | 301 width: 1000px; |
303 top: ${containerTop}px; | 302 } |
304 left: ${containerLeft}px; | 303 </style> |
305 right: ${containerLeft}px; | 304 <div id="container"> |
306 height: ${containerHeight}px; | 305 <div id="inner-container"> |
307 width: ${containerWidth}px; | 306 <button id="dots">...</button> |
308 } | 307 <dialog is="cr-action-menu"> |
309 | 308 <button class="dropdown-item">Un</button> |
310 #inner-container { | 309 <hr> |
311 height: 1000px; | 310 <button class="dropdown-item">Dos</button> |
312 width: 1000px; | 311 <button class="dropdown-item">Tres</button> |
313 } | 312 </dialog> |
314 </style> | |
315 <div id="container"> | |
316 <div id="inner-container"> | |
317 <button id="dots">...</button> | |
318 <dialog is="cr-action-menu"> | |
319 <button class="dropdown-item">Un</button> | |
320 <hr> | |
321 <button class="dropdown-item">Dos</button> | |
322 <button class="dropdown-item">Tres</button> | |
323 </dialog> | |
324 </div> | |
325 </div> | 313 </div> |
326 `; | 314 </div> |
327 menu = document.querySelector('dialog[is=cr-action-menu]'); | 315 `; |
328 dots = document.querySelector('#dots'); | 316 menu = document.querySelector('dialog[is=cr-action-menu]'); |
329 }) | 317 var dots = document.querySelector('#dots'); |
330 | 318 |
331 // Show the menu, scrolling the body to the button. | 319 // Show the menu, scrolling the body to the button. |
332 test('simple offscreen', function() { | 320 menu.showAt( |
333 menu.showAt(dots, {anchorAlignmentX: AnchorAlignment.AFTER_START}); | 321 dots, |
334 assertEquals(`${containerLeft}px`, menu.style.left); | 322 {anchorAlignmentX: AnchorAlignment.AFTER_START}); |
335 assertEquals(`${containerTop}px`, menu.style.top); | 323 assertEquals(`${containerLeft}px`, menu.style.left); |
336 menu.close(); | 324 assertEquals(`${containerTop}px`, menu.style.top); |
337 }); | 325 menu.close(); |
338 | 326 |
339 // Show the menu, scrolling the container to the button, and the body to the | 327 // Show the menu, scrolling the container to the button, and the body to the |
340 // button. | 328 // button. |
341 test('offscreen and out of scroll container viewport', function() { | 329 document.body.scrollLeft = bodyWidth; |
342 document.body.scrollLeft = bodyWidth; | 330 document.body.scrollTop = bodyHeight; |
343 document.body.scrollTop = bodyHeight; | |
344 var container = document.querySelector('#container'); | |
345 | 331 |
346 container.scrollLeft = containerLeft; | 332 document.querySelector('#container').scrollLeft = containerLeft; |
347 container.scrollTop = containerTop; | 333 document.querySelector('#container').scrollTop = containerTop; |
348 | 334 |
349 menu.showAt(dots, {anchorAlignmentX: AnchorAlignment.AFTER_START}); | 335 menu.showAt(dots, {anchorAlignmentX: AnchorAlignment.AFTER_START}); |
350 assertEquals(`${containerLeft}px`, menu.style.left); | 336 assertEquals(`${containerLeft}px`, menu.style.left); |
351 assertEquals(`${containerTop}px`, menu.style.top); | 337 assertEquals(`${containerTop}px`, menu.style.top); |
352 menu.close(); | 338 menu.close(); |
353 }); | |
354 | 339 |
355 // Show the menu for an already onscreen button. The anchor should be | 340 // Show the menu for an already onscreen button. The anchor should be |
356 // overridden so that no scrolling happens. | 341 // overridden so that no scrolling happens. |
357 test('onscreen forces anchor change', function() { | 342 document.body.scrollLeft = 0; |
358 var rect = dots.getBoundingClientRect(); | 343 document.body.scrollTop = 0; |
359 document.body.scrollLeft = rect.right - document.body.clientWidth; | |
360 document.body.scrollTop = rect.bottom - document.body.clientHeight; | |
361 | 344 |
362 menu.showAt(dots, {anchorAlignmentX: AnchorAlignment.AFTER_START}); | 345 var rect = dots.getBoundingClientRect(); |
363 var menuWidth = menu.offsetWidth; | 346 document.body.scrollLeft = rect.right - document.body.clientWidth; |
364 var menuHeight = menu.offsetHeight; | 347 document.body.scrollTop = rect.bottom - document.body.clientHeight; |
365 var buttonWidth = dots.offsetWidth; | |
366 var buttonHeight = dots.offsetHeight; | |
367 assertEquals(containerLeft - menuWidth + buttonWidth, menu.offsetLeft); | |
368 assertEquals(containerTop - menuHeight + buttonHeight, menu.offsetTop); | |
369 menu.close(); | |
370 }); | |
371 | 348 |
372 test('scroll position maintained for showAtPosition', function() { | 349 menu.showAt(dots, {anchorAlignmentX: AnchorAlignment.AFTER_START}); |
373 document.body.scrollLeft = 500; | 350 var menuWidth = menu.offsetWidth; |
374 document.body.scrollTop = 1000; | 351 var menuHeight = menu.offsetHeight; |
375 menu.showAtPosition({top: 50, left: 50}); | 352 var buttonWidth = dots.offsetWidth; |
376 assertEquals(550, menu.offsetLeft); | 353 var buttonHeight = dots.offsetHeight; |
377 assertEquals(1050, menu.offsetTop); | 354 assertEquals(containerLeft - menuWidth + buttonWidth, menu.offsetLeft); |
378 menu.close(); | 355 assertEquals(containerTop - menuHeight + buttonHeight, menu.offsetTop); |
379 }); | |
380 | |
381 test('rtl', function() { | |
382 // Anchor to an item in RTL. | |
383 document.body.style.direction = 'rtl'; | |
384 menu.showAt(dots, {anchorAlignmentX: AnchorAlignment.AFTER_START}); | |
385 assertEquals( | |
386 container.offsetLeft + containerWidth - menu.offsetWidth, | |
387 menu.offsetLeft); | |
388 assertEquals(containerTop, menu.offsetTop); | |
389 menu.close(); | |
390 }); | |
391 }); | 356 }); |
392 }); | 357 }); |
OLD | NEW |