670 lines
19 KiB
JavaScript
670 lines
19 KiB
JavaScript
/* 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 }
|
|
);
|