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