| 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 |