Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
/* import-globals-from ../../mochitest/role.js */
loadScripts(
{ name: "layout.js", dir: MOCHITESTS_DIR },
{ name: "role.js", dir: MOCHITESTS_DIR }
);
requestLongerTimeout(2);
const appUnitsPerDevPixel = 60;
function testCachedScrollPosition(
acc,
expectedX,
expectedY,
shouldBeEmpty = false
) {
let cachedPosition = "";
try {
cachedPosition = acc.cache.getStringProperty("scroll-position");
} catch (e) {
info("Cache was not populated");
// If the key doesn't exist, this frame is not scrollable.
return shouldBeEmpty;
}
// The value we retrieve from the cache is in app units, but the values
// passed in are in pixels. Since the retrieved value is a string,
// and harder to modify, adjust our expected x and y values to match its units.
return (
cachedPosition ==
`${expectedX * appUnitsPerDevPixel}, ${expectedY * appUnitsPerDevPixel}`
);
}
function getCachedBounds(acc) {
let cachedBounds = "";
try {
cachedBounds = acc.cache.getStringProperty("relative-bounds");
} catch (e) {
ok(false, "Unable to fetch cached bounds from cache!");
}
return cachedBounds;
}
/**
* Test bounds of accessibles after scrolling
*/
addAccessibleTask(
`
<div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'>
</div>
<div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'>
</div>
`,
async function (browser, docAcc) {
ok(docAcc, "iframe document acc is present");
await testBoundsWithContent(docAcc, "square", browser);
await testBoundsWithContent(docAcc, "rect", browser);
await invokeContentTask(browser, [], () => {
content.document.getElementById("square").scrollIntoView();
});
await waitForContentPaint(browser);
await testBoundsWithContent(docAcc, "square", browser);
await testBoundsWithContent(docAcc, "rect", browser);
// Scroll rect into view, but also make it reflow so we can be sure the
// bounds are correct for reflowed frames.
await invokeContentTask(browser, [], () => {
const rect = content.document.getElementById("rect");
rect.scrollIntoView();
rect.style.width = "300px";
rect.offsetTop; // Flush layout.
rect.style.width = "200px";
rect.offsetTop; // Flush layout.
});
await waitForContentPaint(browser);
await testBoundsWithContent(docAcc, "square", browser);
await testBoundsWithContent(docAcc, "rect", browser);
},
{ iframe: true, remoteIframe: true, chrome: true }
);
/**
* Test scroll offset on cached accessibles
*/
addAccessibleTask(
`
<div id='square' style='height:100px; width:100px; background:green; margin-top:3000px; margin-bottom:4000px;'>
</div>
<div id='rect' style='height:40px; width:200px; background:blue; margin-bottom:3400px'>
</div>
`,
async function (browser, docAcc) {
ok(docAcc, "iframe document acc is present");
await untilCacheOk(
() => testCachedScrollPosition(docAcc, 0, 0),
"Correct initial scroll position."
);
const rectAcc = findAccessibleChildByID(docAcc, "rect");
const rectInitialBounds = getCachedBounds(rectAcc);
await invokeContentTask(browser, [], () => {
content.document.getElementById("square").scrollIntoView();
});
await waitForContentPaint(browser);
// The only content to scroll over is `square`'s top margin
// so our scroll offset here should be 3000px
await untilCacheOk(
() => testCachedScrollPosition(docAcc, 0, 3000),
"Correct scroll position after first scroll."
);
// Scroll rect into view, but also make it reflow so we can be sure the
// bounds are correct for reflowed frames.
await invokeContentTask(browser, [], () => {
const rect = content.document.getElementById("rect");
rect.scrollIntoView();
rect.style.width = "300px";
rect.offsetTop;
rect.style.width = "200px";
});
await waitForContentPaint(browser);
// We have to scroll over `square`'s top margin (3000px),
// `square` itself (100px), and `square`'s bottom margin (4000px).
// This should give us a 7100px offset.
await untilCacheOk(
() => testCachedScrollPosition(docAcc, 0, 7100),
"Correct final scroll position."
);
await untilCacheIs(
() => getCachedBounds(rectAcc),
rectInitialBounds,
"Cached relative bounds don't change when scrolling"
);
},
{ iframe: true, remoteIframe: true }
);
/**
* Test scroll offset fixed-pos acc accs
*/
addAccessibleTask(
`
<div style="margin-top: 100px; margin-left: 75px; border: 1px solid;">
<div id="d" style="position:fixed;">
<button id="top">top</button>
</div>
</div>
`,
async function (browser, docAcc) {
const origTopBounds = await testBoundsWithContent(docAcc, "top", browser);
const origDBounds = await testBoundsWithContent(docAcc, "d", browser);
const e = waitForEvent(EVENT_REORDER, docAcc);
await invokeContentTask(browser, [], () => {
for (let i = 0; i < 1000; ++i) {
const div = content.document.createElement("div");
div.innerHTML = "<button>${i}</button>";
content.document.body.append(div);
}
});
await e;
await invokeContentTask(browser, [], () => {
// scroll to the bottom of the page
content.window.scrollTo(0, content.document.body.scrollHeight);
});
await waitForContentPaint(browser);
let newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
let newDBounds = await testBoundsWithContent(docAcc, "d", browser);
is(
origTopBounds[0],
newTopBounds[0],
"x of fixed elem is unaffected by scrolling"
);
is(
origTopBounds[1],
newTopBounds[1],
"y of fixed elem is unaffected by scrolling"
);
is(
origTopBounds[2],
newTopBounds[2],
"width of fixed elem is unaffected by scrolling"
);
is(
origTopBounds[3],
newTopBounds[3],
"height of fixed elem is unaffected by scrolling"
);
is(
origDBounds[0],
newTopBounds[0],
"x of fixed elem container is unaffected by scrolling"
);
is(
origDBounds[1],
newDBounds[1],
"y of fixed elem container is unaffected by scrolling"
);
is(
origDBounds[2],
newDBounds[2],
"width of fixed container elem is unaffected by scrolling"
);
is(
origDBounds[3],
newDBounds[3],
"height of fixed container elem is unaffected by scrolling"
);
await invokeContentTask(browser, [], () => {
// remove position styling
content.document.getElementById("d").style = "";
});
await waitForContentPaint(browser);
newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
newDBounds = await testBoundsWithContent(docAcc, "d", browser);
is(
origTopBounds[0],
newTopBounds[0],
"x of non-fixed element remains accurate."
);
Assert.less(
newTopBounds[1],
0,
"y coordinate shows item scrolled off page"
);
is(
origTopBounds[2],
newTopBounds[2],
"width of non-fixed element remains accurate."
);
is(
origTopBounds[3],
newTopBounds[3],
"height of non-fixed element remains accurate."
);
is(
origDBounds[0],
newDBounds[0],
"x of non-fixed container element remains accurate."
);
Assert.less(
newDBounds[1],
0,
"y coordinate shows container scrolled off page"
);
// Removing the position styling on this acc causes it to be bound by
// its parent's bounding box, which alters its width as a block element.
// We don't particularly care about width in this test, so skip it.
is(
origDBounds[3],
newDBounds[3],
"height of non-fixed container element remains accurate."
);
await invokeContentTask(browser, [], () => {
// re-add position styling
content.document.getElementById("d").style = "position:fixed;";
});
await waitForContentPaint(browser);
newTopBounds = await testBoundsWithContent(docAcc, "top", browser);
newDBounds = await testBoundsWithContent(docAcc, "d", browser);
is(
origTopBounds[0],
newTopBounds[0],
"x correct when position:fixed is added."
);
is(
origTopBounds[1],
newTopBounds[1],
"y correct when position:fixed is added."
);
is(
origTopBounds[2],
newTopBounds[2],
"width correct when position:fixed is added."
);
is(
origTopBounds[3],
newTopBounds[3],
"height correct when position:fixed is added."
);
is(
origDBounds[0],
newDBounds[0],
"x of container correct when position:fixed is added."
);
is(
origDBounds[1],
newDBounds[1],
"y of container correct when position:fixed is added."
);
is(
origDBounds[2],
newDBounds[2],
"width of container correct when position:fixed is added."
);
is(
origDBounds[3],
newDBounds[3],
"height of container correct when position:fixed is added."
);
},
{ chrome: true, iframe: true, remoteIframe: true }
);
/**
* Test position: fixed for containers that would otherwise be pruned from the
* a11y tree.
*/
addAccessibleTask(
`
<table id="fixed" role="presentation" style="position: fixed;">
<tr><th>fixed</th></tr>
</table>
<div id="mutate" role="presentation">mutate</div>
<hr style="height: 200vh;">
<p>bottom</p>
`,
async function (browser, docAcc) {
const fixed = findAccessibleChildByID(docAcc, "fixed");
ok(fixed, "fixed is accessible");
isnot(fixed.role, ROLE_TABLE, "fixed doesn't have ROLE_TABLE");
ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible");
info("Setting position: fixed on mutate");
let shown = waitForEvent(EVENT_SHOW, "mutate");
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutate").style.position = "fixed";
});
await shown;
const origFixedBounds = await testBoundsWithContent(
docAcc,
"fixed",
browser
);
const origMutateBounds = await testBoundsWithContent(
docAcc,
"mutate",
browser
);
info("Scrolling to bottom of page");
await invokeContentTask(browser, [], () => {
content.window.scrollTo(0, content.document.body.scrollHeight);
});
await waitForContentPaint(browser);
const newFixedBounds = await testBoundsWithContent(
docAcc,
"fixed",
browser
);
Assert.deepEqual(
newFixedBounds,
origFixedBounds,
"fixed bounds are unchanged"
);
const newMutateBounds = await testBoundsWithContent(
docAcc,
"mutate",
browser
);
Assert.deepEqual(
newMutateBounds,
origMutateBounds,
"mutate bounds are unchanged"
);
},
{ chrome: true, iframe: true, remoteIframe: true }
);
/**
* Test scroll offset on sticky-pos acc
*/
addAccessibleTask(
`
<div id="d" style="margin-top: 100px; margin-left: 75px; position:sticky; top:0px;">
<button id="top">top</button>
</div>
`,
async function (browser, docAcc) {
const containerBounds = await testBoundsWithContent(docAcc, "d", browser);
const e = waitForEvent(EVENT_REORDER, docAcc);
await invokeContentTask(browser, [], () => {
for (let i = 0; i < 1000; ++i) {
const div = content.document.createElement("div");
div.innerHTML = "<button>${i}</button>";
content.document.body.append(div);
}
});
await e;
for (let id of ["d", "top"]) {
info(`Verifying bounds for acc with ID ${id}`);
const origBounds = await testBoundsWithContent(docAcc, id, browser);
info("Scrolling partially");
await invokeContentTask(browser, [], () => {
// scroll some of the window
content.window.scrollTo(0, 50);
});
await waitForContentPaint(browser);
let newBounds = await testBoundsWithContent(docAcc, id, browser);
is(
origBounds[0],
newBounds[0],
`x coord of sticky element is unaffected by scrolling`
);
ok(
origBounds[1] > newBounds[1] && newBounds[1] >= 0,
"sticky element scrolled, but not off the page"
);
is(
origBounds[2],
newBounds[2],
`width of sticky element is unaffected by scrolling`
);
is(
origBounds[3],
newBounds[3],
`height of sticky element is unaffected by scrolling`
);
info("Scrolling to bottom");
await invokeContentTask(browser, [], () => {
// scroll to the bottom of the page
content.window.scrollTo(0, content.document.body.scrollHeight);
});
await waitForContentPaint(browser);
newBounds = await testBoundsWithContent(docAcc, id, browser);
is(
origBounds[0],
newBounds[0],
`x coord of sticky element is unaffected by scrolling`
);
// Subtract margin from container screen coords to get chrome height
// which is where our y pos should be
is(
newBounds[1],
containerBounds[1] - 100,
"Sticky element is top of screen"
);
is(
origBounds[2],
newBounds[2],
`width of sticky element is unaffected by scrolling`
);
is(
origBounds[3],
newBounds[3],
`height of sticky element is unaffected by scrolling`
);
info("Removing position style on container");
await invokeContentTask(browser, [], () => {
// remove position styling
content.document.getElementById("d").style =
"margin-top: 100px; margin-left: 75px;";
});
await waitForContentPaint(browser);
newBounds = await testBoundsWithContent(docAcc, id, browser);
is(
origBounds[0],
newBounds[0],
`x coord of non-sticky element remains accurate.`
);
Assert.less(newBounds[1], 0, "y coordinate shows item scrolled off page");
// Removing the position styling on this acc causes it to be bound by
// its parent's bounding box, which alters its width as a block element.
// We don't particularly care about width in this test, so skip it.
is(
origBounds[3],
newBounds[3],
`height of non-sticky element remains accurate.`
);
info("Adding position style on container");
await invokeContentTask(browser, [], () => {
// re-add position styling
content.document.getElementById("d").style =
"margin-top: 100px; margin-left: 75px; position:sticky; top:0px;";
});
await waitForContentPaint(browser);
newBounds = await testBoundsWithContent(docAcc, id, browser);
is(
origBounds[0],
newBounds[0],
`x coord of sticky element is unaffected by scrolling`
);
is(
newBounds[1],
containerBounds[1] - 100,
"Sticky element is top of screen"
);
is(
origBounds[2],
newBounds[2],
`width of sticky element is unaffected by scrolling`
);
is(
origBounds[3],
newBounds[3],
`height of sticky element is unaffected by scrolling`
);
info("Scrolling back up to test next ID");
await invokeContentTask(browser, [], () => {
// scroll some of the window
content.window.scrollTo(0, 0);
});
}
},
{ chrome: false, iframe: false, remoteIframe: false }
);
/**
* Test position: sticky for containers that would otherwise be pruned from the
* a11y tree.
*/
addAccessibleTask(
`
<hr style="height: 100vh;">
<div id="stickyContainer">
<div id="sticky" role="presentation" style="position: sticky; top: 0px;">sticky</div>
<hr style="height: 100vh;">
<p id="stickyEnd">stickyEnd</p>
</div>
<div id="mutateContainer">
<div id="mutate" role="presentation" style="top: 0px;">mutate</div>
<hr style="height: 100vh;">
<p id="mutateEnd">mutateEnd</p>
</div>
`,
async function (browser, docAcc) {
ok(findAccessibleChildByID(docAcc, "sticky"), "sticky is accessible");
info("Scrolling to sticky");
await invokeContentTask(browser, [], () => {
content.document.getElementById("sticky").scrollIntoView();
});
await waitForContentPaint(browser);
const origStickyBounds = await testBoundsWithContent(
docAcc,
"sticky",
browser
);
info("Scrolling to stickyEnd");
await invokeContentTask(browser, [], () => {
content.document.getElementById("stickyEnd").scrollIntoView();
});
await waitForContentPaint(browser);
const newStickyBounds = await testBoundsWithContent(
docAcc,
"sticky",
browser
);
Assert.deepEqual(
newStickyBounds,
origStickyBounds,
"sticky bounds are unchanged"
);
ok(!findAccessibleChildByID(docAcc, "mutate"), "mutate inaccessible");
info("Setting position: sticky on mutate");
let shown = waitForEvent(EVENT_SHOW, "mutate");
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutate").style.position = "sticky";
});
await shown;
info("Scrolling to mutate");
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutate").scrollIntoView();
});
await waitForContentPaint(browser);
const origMutateBounds = await testBoundsWithContent(
docAcc,
"mutate",
browser
);
info("Scrolling to mutateEnd");
await invokeContentTask(browser, [], () => {
content.document.getElementById("mutateEnd").scrollIntoView();
});
await waitForContentPaint(browser);
const newMutateBounds = await testBoundsWithContent(
docAcc,
"mutate",
browser
);
assertBoundsFuzzyEqual(newMutateBounds, origMutateBounds);
},
{ chrome: true, iframe: true, remoteIframe: true }
);
/**
* Test scroll offset on non-scrollable accs
*/
addAccessibleTask(
`
<div id='square' style='height:100px; width: 100px; background:green;'>hello world
</div>
`,
async function (browser, docAcc) {
const square = findAccessibleChildByID(docAcc, "square");
await untilCacheOk(
() => testCachedScrollPosition(square, 0, 0, true),
"Square is not scrollable."
);
info("Adding more text content to square");
await invokeContentTask(browser, [], () => {
const s = content.document.getElementById("square");
s.textContent =
"hello world I am some text and I should overflow this container because I am very long";
s.offsetTop; // Flush layout.
});
await waitForContentPaint(browser);
await untilCacheOk(
() => testCachedScrollPosition(square, 0, 0, true),
"Square is not scrollable (still has overflow:visible)."
);
info("Adding overflow:auto; styling");
await invokeContentTask(browser, [], () => {
const s = content.document.getElementById("square");
s.setAttribute(
"style",
"overflow:auto; height:100px; width: 100px; background:green;"
);
s.offsetTop; // Flush layout.
});
await waitForContentPaint(browser);
await untilCacheOk(
() => testCachedScrollPosition(square, 0, 0),
"Square is scrollable."
);
},
{ iframe: true, remoteIframe: true }
);