browser_test_scroll_bounds.js (19717B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 /* import-globals-from ../../mochitest/role.js */ 8 loadScripts( 9 { name: "layout.js", dir: MOCHITESTS_DIR }, 10 { name: "role.js", dir: MOCHITESTS_DIR } 11 ); 12 requestLongerTimeout(2); 13 14 const appUnitsPerDevPixel = 60; 15 16 function testCachedScrollPosition( 17 acc, 18 expectedX, 19 expectedY, 20 shouldBeEmpty = false 21 ) { 22 let cachedPosition = ""; 23 try { 24 cachedPosition = acc.cache.getStringProperty("scroll-position"); 25 } catch (e) { 26 info("Cache was not populated"); 27 // If the key doesn't exist, this frame is not scrollable. 28 return shouldBeEmpty; 29 } 30 31 // The value we retrieve from the cache is in app units, but the values 32 // passed in are in pixels. Since the retrieved value is a string, 33 // and harder to modify, adjust our expected x and y values to match its units. 34 return ( 35 cachedPosition == 36 `${expectedX * appUnitsPerDevPixel}, ${expectedY * appUnitsPerDevPixel}` 37 ); 38 } 39 40 function getCachedBounds(acc) { 41 let cachedBounds = ""; 42 try { 43 cachedBounds = acc.cache.getStringProperty("relative-bounds"); 44 } catch (e) { 45 ok(false, "Unable to fetch cached bounds from cache!"); 46 } 47 return cachedBounds; 48 } 49 50 /** 51 * Test bounds of accessibles after scrolling 52 */ 53 addAccessibleTask( 54 ` 55 <div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'> 56 </div> 57 58 <div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'> 59 </div> 60 `, 61 async function (browser, docAcc) { 62 ok(docAcc, "iframe document acc is present"); 63 await testBoundsWithContent(docAcc, "square", browser); 64 await testBoundsWithContent(docAcc, "rect", browser); 65 66 await invokeContentTask(browser, [], () => { 67 content.document.getElementById("square").scrollIntoView(); 68 }); 69 70 await waitForContentPaint(browser); 71 72 await testBoundsWithContent(docAcc, "square", browser); 73 await testBoundsWithContent(docAcc, "rect", browser); 74 75 // Scroll rect into view, but also make it reflow so we can be sure the 76 // bounds are correct for reflowed frames. 77 await invokeContentTask(browser, [], () => { 78 const rect = content.document.getElementById("rect"); 79 rect.scrollIntoView(); 80 rect.style.width = "300px"; 81 rect.offsetTop; // Flush layout. 82 rect.style.width = "200px"; 83 rect.offsetTop; // Flush layout. 84 }); 85 86 await waitForContentPaint(browser); 87 await testBoundsWithContent(docAcc, "square", browser); 88 await testBoundsWithContent(docAcc, "rect", browser); 89 }, 90 { iframe: true, remoteIframe: true, chrome: true } 91 ); 92 93 /** 94 * Test scroll offset on cached accessibles 95 */ 96 addAccessibleTask( 97 ` 98 <div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'> 99 </div> 100 101 <div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'> 102 </div> 103 `, 104 async function (browser, docAcc) { 105 ok(docAcc, "iframe document acc is present"); 106 await untilCacheOk( 107 () => testCachedScrollPosition(docAcc, 0, 0), 108 "Correct initial scroll position." 109 ); 110 const rectAcc = findAccessibleChildByID(docAcc, "rect"); 111 const rectInitialBounds = getCachedBounds(rectAcc); 112 113 await invokeContentTask(browser, [], () => { 114 content.document.getElementById("square").scrollIntoView(); 115 }); 116 117 await waitForContentPaint(browser); 118 119 // The only content to scroll over is `square`'s top margin 120 // so our scroll offset here should be 3000px 121 await untilCacheOk( 122 () => testCachedScrollPosition(docAcc, 0, 3000), 123 "Correct scroll position after first scroll." 124 ); 125 126 // Scroll rect into view, but also make it reflow so we can be sure the 127 // bounds are correct for reflowed frames. 128 await invokeContentTask(browser, [], () => { 129 const rect = content.document.getElementById("rect"); 130 rect.scrollIntoView(); 131 rect.style.width = "300px"; 132 rect.offsetTop; 133 rect.style.width = "200px"; 134 }); 135 136 await waitForContentPaint(browser); 137 // We have to scroll over `square`'s top margin (3000px), 138 // `square` itself (100px), and `square`'s bottom margin (4000px). 139 // This should give us a 7100px offset. 140 await untilCacheOk( 141 () => testCachedScrollPosition(docAcc, 0, 7100), 142 "Correct final scroll position." 143 ); 144 await untilCacheIs( 145 () => getCachedBounds(rectAcc), 146 rectInitialBounds, 147 "Cached relative bounds don't change when scrolling" 148 ); 149 }, 150 { iframe: true, remoteIframe: true } 151 ); 152 153 /** 154 * Test scroll offset fixed-pos acc accs 155 */ 156 addAccessibleTask( 157 ` 158 <div style="margin-top: 100px; margin-left: 75px; border: 1px solid;"> 159 <div id="d" style="position:fixed;"> 160 <button id="top">top</button> 161 </div> 162 </div> 163 `, 164 async function (browser, docAcc) { 165 const origTopBounds = await testBoundsWithContent(docAcc, "top", browser); 166 const origDBounds = await testBoundsWithContent(docAcc, "d", browser); 167 const e = waitForEvent(EVENT_REORDER, docAcc); 168 await invokeContentTask(browser, [], () => { 169 for (let i = 0; i < 1000; ++i) { 170 const div = content.document.createElement("div"); 171 div.innerHTML = "<button>${i}</button>"; 172 content.document.body.append(div); 173 } 174 }); 175 await e; 176 177 await invokeContentTask(browser, [], () => { 178 // scroll to the bottom of the page 179 content.window.scrollTo(0, content.document.body.scrollHeight); 180 }); 181 182 await waitForContentPaint(browser); 183 184 let newTopBounds = await testBoundsWithContent(docAcc, "top", browser); 185 let newDBounds = await testBoundsWithContent(docAcc, "d", browser); 186 is( 187 origTopBounds[0], 188 newTopBounds[0], 189 "x of fixed elem is unaffected by scrolling" 190 ); 191 is( 192 origTopBounds[1], 193 newTopBounds[1], 194 "y of fixed elem is unaffected by scrolling" 195 ); 196 is( 197 origTopBounds[2], 198 newTopBounds[2], 199 "width of fixed elem is unaffected by scrolling" 200 ); 201 is( 202 origTopBounds[3], 203 newTopBounds[3], 204 "height of fixed elem is unaffected by scrolling" 205 ); 206 is( 207 origDBounds[0], 208 newTopBounds[0], 209 "x of fixed elem container is unaffected by scrolling" 210 ); 211 is( 212 origDBounds[1], 213 newDBounds[1], 214 "y of fixed elem container is unaffected by scrolling" 215 ); 216 is( 217 origDBounds[2], 218 newDBounds[2], 219 "width of fixed container elem is unaffected by scrolling" 220 ); 221 is( 222 origDBounds[3], 223 newDBounds[3], 224 "height of fixed container elem is unaffected by scrolling" 225 ); 226 227 await invokeContentTask(browser, [], () => { 228 // remove position styling 229 content.document.getElementById("d").style = ""; 230 }); 231 232 await waitForContentPaint(browser); 233 234 newTopBounds = await testBoundsWithContent(docAcc, "top", browser); 235 newDBounds = await testBoundsWithContent(docAcc, "d", browser); 236 is( 237 origTopBounds[0], 238 newTopBounds[0], 239 "x of non-fixed element remains accurate." 240 ); 241 Assert.less( 242 newTopBounds[1], 243 0, 244 "y coordinate shows item scrolled off page" 245 ); 246 is( 247 origTopBounds[2], 248 newTopBounds[2], 249 "width of non-fixed element remains accurate." 250 ); 251 is( 252 origTopBounds[3], 253 newTopBounds[3], 254 "height of non-fixed element remains accurate." 255 ); 256 is( 257 origDBounds[0], 258 newDBounds[0], 259 "x of non-fixed container element remains accurate." 260 ); 261 Assert.less( 262 newDBounds[1], 263 0, 264 "y coordinate shows container scrolled off page" 265 ); 266 // Removing the position styling on this acc causes it to be bound by 267 // its parent's bounding box, which alters its width as a block element. 268 // We don't particularly care about width in this test, so skip it. 269 is( 270 origDBounds[3], 271 newDBounds[3], 272 "height of non-fixed container element remains accurate." 273 ); 274 275 await invokeContentTask(browser, [], () => { 276 // re-add position styling 277 content.document.getElementById("d").style = "position:fixed;"; 278 }); 279 280 await waitForContentPaint(browser); 281 282 newTopBounds = await testBoundsWithContent(docAcc, "top", browser); 283 newDBounds = await testBoundsWithContent(docAcc, "d", browser); 284 is( 285 origTopBounds[0], 286 newTopBounds[0], 287 "x correct when position:fixed is added." 288 ); 289 is( 290 origTopBounds[1], 291 newTopBounds[1], 292 "y correct when position:fixed is added." 293 ); 294 is( 295 origTopBounds[2], 296 newTopBounds[2], 297 "width correct when position:fixed is added." 298 ); 299 is( 300 origTopBounds[3], 301 newTopBounds[3], 302 "height correct when position:fixed is added." 303 ); 304 is( 305 origDBounds[0], 306 newDBounds[0], 307 "x of container correct when position:fixed is added." 308 ); 309 is( 310 origDBounds[1], 311 newDBounds[1], 312 "y of container correct when position:fixed is added." 313 ); 314 is( 315 origDBounds[2], 316 newDBounds[2], 317 "width of container correct when position:fixed is added." 318 ); 319 is( 320 origDBounds[3], 321 newDBounds[3], 322 "height of container correct when position:fixed is added." 323 ); 324 }, 325 { chrome: true, iframe: true, remoteIframe: true } 326 ); 327 328 /** 329 * Test position: fixed for containers that would otherwise be pruned from the 330 * a11y tree. 331 */ 332 addAccessibleTask( 333 ` 334 <table id="fixed" role="presentation" style="position: fixed;"> 335 <tr><th>fixed</th></tr> 336 </table> 337 <div id="mutate" role="presentation">mutate</div> 338 <hr style="height: 200vh;"> 339 <p>bottom</p> 340 `, 341 async function (browser, docAcc) { 342 const fixed = findAccessibleChildByID(docAcc, "fixed"); 343 ok(fixed, "fixed is accessible"); 344 isnot(fixed.role, ROLE_TABLE, "fixed doesn't have ROLE_TABLE"); 345 ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible"); 346 info("Setting position: fixed on mutate"); 347 let shown = waitForEvent(EVENT_SHOW, "mutate"); 348 await invokeContentTask(browser, [], () => { 349 content.document.getElementById("mutate").style.position = "fixed"; 350 }); 351 await shown; 352 const origFixedBounds = await testBoundsWithContent( 353 docAcc, 354 "fixed", 355 browser 356 ); 357 const origMutateBounds = await testBoundsWithContent( 358 docAcc, 359 "mutate", 360 browser 361 ); 362 info("Scrolling to bottom of page"); 363 await invokeContentTask(browser, [], () => { 364 content.window.scrollTo(0, content.document.body.scrollHeight); 365 }); 366 await waitForContentPaint(browser); 367 const newFixedBounds = await testBoundsWithContent( 368 docAcc, 369 "fixed", 370 browser 371 ); 372 Assert.deepEqual( 373 newFixedBounds, 374 origFixedBounds, 375 "fixed bounds are unchanged" 376 ); 377 const newMutateBounds = await testBoundsWithContent( 378 docAcc, 379 "mutate", 380 browser 381 ); 382 Assert.deepEqual( 383 newMutateBounds, 384 origMutateBounds, 385 "mutate bounds are unchanged" 386 ); 387 }, 388 { chrome: true, iframe: true, remoteIframe: true } 389 ); 390 391 /** 392 * Test scroll offset on sticky-pos acc 393 */ 394 addAccessibleTask( 395 ` 396 <div id="d" style="margin-top: 100px; margin-left: 75px; position:sticky; top:0px;"> 397 <button id="top">top</button> 398 </div> 399 `, 400 async function (browser, docAcc) { 401 const containerBounds = await testBoundsWithContent(docAcc, "d", browser); 402 const e = waitForEvent(EVENT_REORDER, docAcc); 403 await invokeContentTask(browser, [], () => { 404 for (let i = 0; i < 1000; ++i) { 405 const div = content.document.createElement("div"); 406 div.innerHTML = "<button>${i}</button>"; 407 content.document.body.append(div); 408 } 409 }); 410 await e; 411 for (let id of ["d", "top"]) { 412 info(`Verifying bounds for acc with ID ${id}`); 413 const origBounds = await testBoundsWithContent(docAcc, id, browser); 414 415 info("Scrolling partially"); 416 await invokeContentTask(browser, [], () => { 417 // scroll some of the window 418 content.window.scrollTo(0, 50); 419 }); 420 421 await waitForContentPaint(browser); 422 423 let newBounds = await testBoundsWithContent(docAcc, id, browser); 424 is( 425 origBounds[0], 426 newBounds[0], 427 `x coord of sticky element is unaffected by scrolling` 428 ); 429 ok( 430 origBounds[1] > newBounds[1] && newBounds[1] >= 0, 431 "sticky element scrolled, but not off the page" 432 ); 433 is( 434 origBounds[2], 435 newBounds[2], 436 `width of sticky element is unaffected by scrolling` 437 ); 438 is( 439 origBounds[3], 440 newBounds[3], 441 `height of sticky element is unaffected by scrolling` 442 ); 443 444 info("Scrolling to bottom"); 445 await invokeContentTask(browser, [], () => { 446 // scroll to the bottom of the page 447 content.window.scrollTo(0, content.document.body.scrollHeight); 448 }); 449 450 await waitForContentPaint(browser); 451 452 newBounds = await testBoundsWithContent(docAcc, id, browser); 453 is( 454 origBounds[0], 455 newBounds[0], 456 `x coord of sticky element is unaffected by scrolling` 457 ); 458 // Subtract margin from container screen coords to get chrome height 459 // which is where our y pos should be 460 is( 461 newBounds[1], 462 containerBounds[1] - 100, 463 "Sticky element is top of screen" 464 ); 465 is( 466 origBounds[2], 467 newBounds[2], 468 `width of sticky element is unaffected by scrolling` 469 ); 470 is( 471 origBounds[3], 472 newBounds[3], 473 `height of sticky element is unaffected by scrolling` 474 ); 475 476 info("Removing position style on container"); 477 await invokeContentTask(browser, [], () => { 478 // remove position styling 479 content.document.getElementById("d").style = 480 "margin-top: 100px; margin-left: 75px;"; 481 }); 482 483 await waitForContentPaint(browser); 484 485 newBounds = await testBoundsWithContent(docAcc, id, browser); 486 487 is( 488 origBounds[0], 489 newBounds[0], 490 `x coord of non-sticky element remains accurate.` 491 ); 492 Assert.less(newBounds[1], 0, "y coordinate shows item scrolled off page"); 493 494 // Removing the position styling on this acc causes it to be bound by 495 // its parent's bounding box, which alters its width as a block element. 496 // We don't particularly care about width in this test, so skip it. 497 is( 498 origBounds[3], 499 newBounds[3], 500 `height of non-sticky element remains accurate.` 501 ); 502 503 info("Adding position style on container"); 504 await invokeContentTask(browser, [], () => { 505 // re-add position styling 506 content.document.getElementById("d").style = 507 "margin-top: 100px; margin-left: 75px; position:sticky; top:0px;"; 508 }); 509 510 await waitForContentPaint(browser); 511 512 newBounds = await testBoundsWithContent(docAcc, id, browser); 513 is( 514 origBounds[0], 515 newBounds[0], 516 `x coord of sticky element is unaffected by scrolling` 517 ); 518 is( 519 newBounds[1], 520 containerBounds[1] - 100, 521 "Sticky element is top of screen" 522 ); 523 is( 524 origBounds[2], 525 newBounds[2], 526 `width of sticky element is unaffected by scrolling` 527 ); 528 is( 529 origBounds[3], 530 newBounds[3], 531 `height of sticky element is unaffected by scrolling` 532 ); 533 534 info("Scrolling back up to test next ID"); 535 await invokeContentTask(browser, [], () => { 536 // scroll some of the window 537 content.window.scrollTo(0, 0); 538 }); 539 } 540 }, 541 { chrome: false, iframe: false, remoteIframe: false } 542 ); 543 544 /** 545 * Test position: sticky for containers that would otherwise be pruned from the 546 * a11y tree. 547 */ 548 addAccessibleTask( 549 ` 550 <hr style="height: 100vh;"> 551 <div id="stickyContainer"> 552 <div id="sticky" role="presentation" style="position: sticky; top: 0px;">sticky</div> 553 <hr style="height: 100vh;"> 554 <p id="stickyEnd">stickyEnd</p> 555 </div> 556 <div id="mutateContainer"> 557 <div id="mutate" role="presentation" style="top: 0px;">mutate</div> 558 <hr style="height: 100vh;"> 559 <p id="mutateEnd">mutateEnd</p> 560 </div> 561 `, 562 async function (browser, docAcc) { 563 ok(findAccessibleChildByID(docAcc, "sticky"), "sticky is accessible"); 564 info("Scrolling to sticky"); 565 await invokeContentTask(browser, [], () => { 566 content.document.getElementById("sticky").scrollIntoView(); 567 }); 568 await waitForContentPaint(browser); 569 const origStickyBounds = await testBoundsWithContent( 570 docAcc, 571 "sticky", 572 browser 573 ); 574 info("Scrolling to stickyEnd"); 575 await invokeContentTask(browser, [], () => { 576 content.document.getElementById("stickyEnd").scrollIntoView(); 577 }); 578 await waitForContentPaint(browser); 579 const newStickyBounds = await testBoundsWithContent( 580 docAcc, 581 "sticky", 582 browser 583 ); 584 Assert.deepEqual( 585 newStickyBounds, 586 origStickyBounds, 587 "sticky bounds are unchanged" 588 ); 589 590 ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible"); 591 info("Setting position: sticky on mutate"); 592 let shown = waitForEvent(EVENT_SHOW, "mutate"); 593 await invokeContentTask(browser, [], () => { 594 content.document.getElementById("mutate").style.position = "sticky"; 595 }); 596 await shown; 597 info("Scrolling to mutate"); 598 await invokeContentTask(browser, [], () => { 599 content.document.getElementById("mutate").scrollIntoView(); 600 }); 601 await waitForContentPaint(browser); 602 const origMutateBounds = await testBoundsWithContent( 603 docAcc, 604 "mutate", 605 browser 606 ); 607 info("Scrolling to mutateEnd"); 608 await invokeContentTask(browser, [], () => { 609 content.document.getElementById("mutateEnd").scrollIntoView(); 610 }); 611 await waitForContentPaint(browser); 612 const newMutateBounds = await testBoundsWithContent( 613 docAcc, 614 "mutate", 615 browser 616 ); 617 assertBoundsFuzzyEqual(newMutateBounds, origMutateBounds); 618 }, 619 { chrome: true, iframe: true, remoteIframe: true } 620 ); 621 622 /** 623 * Test scroll offset on non-scrollable accs 624 */ 625 addAccessibleTask( 626 ` 627 <div id='square' style='height:100px; width: 100px; background:green;'>hello world 628 </div> 629 `, 630 async function (browser, docAcc) { 631 const square = findAccessibleChildByID(docAcc, "square"); 632 await untilCacheOk( 633 () => testCachedScrollPosition(square, 0, 0, true), 634 "Square is not scrollable." 635 ); 636 637 info("Adding more text content to square"); 638 await invokeContentTask(browser, [], () => { 639 const s = content.document.getElementById("square"); 640 s.textContent = 641 "hello world I am some text and I should overflow this container because I am very long"; 642 s.offsetTop; // Flush layout. 643 }); 644 645 await waitForContentPaint(browser); 646 647 await untilCacheOk( 648 () => testCachedScrollPosition(square, 0, 0, true), 649 "Square is not scrollable (still has overflow:visible)." 650 ); 651 652 info("Adding overflow:auto; styling"); 653 await invokeContentTask(browser, [], () => { 654 const s = content.document.getElementById("square"); 655 s.setAttribute( 656 "style", 657 "overflow:auto; height:100px; width: 100px; background:green;" 658 ); 659 s.offsetTop; // Flush layout. 660 }); 661 662 await waitForContentPaint(browser); 663 664 await untilCacheOk( 665 () => testCachedScrollPosition(square, 0, 0), 666 "Square is scrollable." 667 ); 668 }, 669 { iframe: true, remoteIframe: true } 670 );