Bug 1787274: Use mCachedFields viewport cache for determining offscreen state r=Jamie,geckoview-reviewers,owlish

Differential Revision: https://phabricator.services.mozilla.com/D155903
This commit is contained in:
Morgan Rae Reschenberg
2022-10-18 22:30:10 +00:00
parent 13803a5ee0
commit bf7515d46c
13 changed files with 379 additions and 136 deletions

View File

@@ -889,6 +889,18 @@ void NotificationController::WillRefresh(mozilla::TimeStamp aTime) {
CoalesceMutationEvents();
ProcessMutationEvents();
// When firing mutation events, mObservingState is set to
// eRefreshProcessing. Any calls to ScheduleProcessing() that
// occur before mObservingState is reset will be dropped because we only
// schedule a tick if mObservingState == eNotObservingRefresh.
// This sometimes results in our viewport cache being out-of-date after
// processing mutation events. Call ProcessQueuedCacheUpdates again to
// ensure it is updated.
if (IPCAccessibilityActive() && mDocument) {
mDocument->ProcessQueuedCacheUpdates();
}
mEventGeneration = 0;
// Now that we are done with them get rid of the events we fired.

View File

@@ -152,7 +152,6 @@ class DocAccessible : public HyperTextAccessibleWrap,
bool IsHidden() const;
bool IsViewportCacheDirty() { return mViewportCacheDirty; }
void SetViewportCacheDirty(bool aDirty) { mViewportCacheDirty = aDirty; }
/**

View File

@@ -3192,6 +3192,12 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
nsTHashSet<LocalAccessible*> inViewAccs;
nsTArray<uint64_t> viewportCache;
// Layout considers table rows fully occluded by their containing cells.
// This means they don't have their own display list items, and they won't
// show up in the list returned from GetFramesForArea. To prevent table
// rows from appearing offscreen, we manually add any rows for which we
// have on-screen cells.
LocalAccessible* prevParentRow = nullptr;
for (nsIFrame* frame : frames) {
nsIContent* content = frame->GetContent();
if (!content) {
@@ -3210,8 +3216,28 @@ already_AddRefed<AccAttributes> LocalAccessible::BundleFieldsForCache(
if (acc->IsTextLeaf() && nsAccUtils::MustPrune(acc->LocalParent())) {
acc = acc->LocalParent();
}
if (acc->IsImageMap()) {
if (acc->IsTableCell()) {
LocalAccessible* parent = acc->LocalParent();
if (parent && parent->IsTableRow() && parent != prevParentRow) {
// If we've entered a new row since the last cell we saw, add the
// previous parent row to our viewport cache here to maintain
// hittesting order. Keep track of the current parent row.
if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) {
viewportCache.AppendElement(prevParentRow->ID());
}
prevParentRow = parent;
}
} else if (acc->IsTable()) {
// If we've encountered a table, we know we've already
// handled all of this table's content (because we're traversing
// in hittesting order). Add our table's final row to the viewport
// cache before adding the table itself. Reset our marker for the next
// table.
if (prevParentRow && inViewAccs.EnsureInserted(prevParentRow)) {
viewportCache.AppendElement(prevParentRow->ID());
}
prevParentRow = nullptr;
} else if (acc->IsImageMap()) {
// Layout doesn't walk image maps, so we do that
// manually here. We do this before adding the map itself
// so the children come earlier in the hittesting order.

View File

@@ -338,6 +338,10 @@ class DocAccessibleParent : public RemoteAccessible,
nsTHashMap<uint64_t, nsTHashMap<uint64_t, nsTArray<uint64_t>>>
mReverseRelations;
// Computed from the viewport cache, the accs referenced by these ids
// are currently on screen (making any acc not in this list offscreen).
nsTHashSet<uint64_t> mOnScreenAccessibles;
static DocAccessibleParent* GetFrom(dom::BrowsingContext* aBrowsingContext);
private:

View File

@@ -1003,17 +1003,61 @@ uint64_t RemoteAccessibleBase<Derived>::State() {
state &= ~states::OPAQUE1;
}
auto* browser = static_cast<dom::BrowserParent*>(Document()->Manager());
if (browser == dom::BrowserParent::GetFocused()) {
if (this == Document()->GetFocusedAcc()) {
state |= states::FOCUSED;
}
}
auto* cbc = mDoc->GetBrowsingContext();
if (cbc && !cbc->IsActive()) {
// If our browsing context is _not_ active, we're in a background tab
// and inherently offscreen.
state |= states::OFFSCREEN;
} else {
// If we're in an active browsing context, there are a few scenarios we
// need to address:
// - We are an iframe document in the visual viewport
// - We are an iframe document out of the visual viewport
// - We are non-iframe content in the visual viewport
// - We are non-iframe content out of the visual viewport
// We assume top level tab docs are on screen if their BC is active, so
// we don't need additional handling for them here.
if (!mDoc->IsTopLevel()) {
// Here we handle iframes and iframe content.
// We use an iframe's outer doc's position in the embedding document's
// viewport to determine if the iframe has been scrolled offscreen.
Accessible* docParent = mDoc->Parent();
// In rare cases, we might not have an outer doc yet. Return if that's
// the case.
if (NS_WARN_IF(!docParent || !docParent->IsRemote())) {
return state;
}
RemoteAccessible* outerDoc = docParent->AsRemote();
DocAccessibleParent* embeddingDocument = outerDoc->Document();
if (embeddingDocument &&
!embeddingDocument->mOnScreenAccessibles.Contains(outerDoc->ID())) {
// Our embedding document's viewport cache doesn't contain the ID of
// our outer doc, so this iframe (and any of its content) is
// offscreen.
state |= states::OFFSCREEN;
} else if (this != mDoc && !mDoc->mOnScreenAccessibles.Contains(ID())) {
// Our embedding document's viewport cache contains the ID of our
// outer doc, but the iframe's viewport cache doesn't contain our ID.
// We are offscreen.
state |= states::OFFSCREEN;
}
} else if (this != mDoc && !mDoc->mOnScreenAccessibles.Contains(ID())) {
// We are top level tab content (but not a top level tab doc).
// If our tab doc's viewport cache doesn't contain our ID, we're
// offscreen.
state |= states::OFFSCREEN;
}
}
}
auto* browser = static_cast<dom::BrowserParent*>(Document()->Manager());
if (browser == dom::BrowserParent::GetFocused()) {
if (this == Document()->GetFocusedAcc()) {
state |= states::FOCUSED;
}
}
return state;
}

View File

@@ -258,6 +258,19 @@ class RemoteAccessibleBase : public Accessible, public HyperTextAccessibleBase {
void ApplyCache(CacheUpdateType aUpdateType, AccAttributes* aFields) {
const nsTArray<bool> relUpdatesNeeded = PreProcessRelations(aFields);
if (auto maybeViewportCache =
aFields->GetAttribute<nsTArray<uint64_t>>(nsGkAtoms::viewport)) {
// Updating the viewport cache means the offscreen state of this
// document's accessibles has changed. Update the HashSet we use for
// checking offscreen state here.
MOZ_ASSERT(IsDoc(),
"Fetched the viewport cache from a non-doc accessible?");
AsDoc()->mOnScreenAccessibles.Clear();
for (auto id : *maybeViewportCache) {
AsDoc()->mOnScreenAccessibles.Insert(id);
}
}
if (aUpdateType == CacheUpdateType::Initial) {
mCachedFields = aFields;
} else {

View File

@@ -13,7 +13,7 @@ skip-if = verify
https_first_disabled = true
skip-if =
os == 'win' && bits == 64 && !debug # bug 1652192
[browser_offscreen_element_in_out_of_process_iframe.js]
[browser_test_visibility_2.js]
https_first_disabled = true
skip-if =
os == 'win' # bug 1580706
apple_catalina # high frequency intermittent
os == 'win' && bits == 64 && !debug # bug 1652192

View File

@@ -1,98 +0,0 @@
/* 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";
const parentURL =
"data:text/html;charset=utf-8," +
'<div id="scroller" style="width: 300px; height: 300px; overflow-y: scroll; overflow-x: hidden;">' +
' <div style="width: 100%; height: 1000px;"></div>' +
' <iframe frameborder="0"/>' +
"</div>";
const iframeURL =
"data:text/html;charset=utf-8," +
"<style>" +
" html,body {" +
" /* Convenient for calculation of element positions */" +
" margin: 0;" +
" padding: 0;" +
" }" +
"</style>" +
'<div id="target" style="width: 100px; height: 100px;">target</div>';
add_task(async function() {
const win = await BrowserTestUtils.openNewBrowserWindow({
fission: true,
});
try {
const browser = win.gBrowser.selectedTab.linkedBrowser;
BrowserTestUtils.loadURI(browser, parentURL);
await BrowserTestUtils.browserLoaded(browser, false, parentURL);
async function setup(url) {
const iframe = content.document.querySelector("iframe");
iframe.contentWindow.location = url;
await new Promise(resolve => {
iframe.addEventListener("load", resolve, { once: true });
});
return iframe.browsingContext;
}
async function scrollTo(x, y) {
await SpecialPowers.spawn(browser, [x, y], async (scrollX, scrollY) => {
const scroller = content.document.getElementById("scroller");
scroller.scrollTo(scrollX, scrollY);
await new Promise(resolve => {
scroller.addEventListener("scroll", resolve, { once: true });
});
});
await waitForIFrameUpdates();
}
// Setup an out-of-process iframe which is initially scrolled out.
const iframe = await SpecialPowers.spawn(browser, [iframeURL], setup);
await waitForIFrameA11yReady(iframe);
await spawnTestStates(
iframe,
"target",
nsIAccessibleStates.STATE_OFFSCREEN,
nsIAccessibleStates.STATE_INVISIBLE
);
// Scroll the iframe into view and the target element is also visible but
// the visible area height is 11px.
await scrollTo(0, 711);
await spawnTestStates(
iframe,
"target",
nsIAccessibleStates.STATE_OFFSCREEN,
nsIAccessibleStates.STATE_INVISIBLE
);
// Scroll to a position where the visible height is 13px.
await scrollTo(0, 713);
await spawnTestStates(
iframe,
"target",
0,
nsIAccessibleStates.STATE_OFFSCREEN
);
// Scroll the iframe out again.
await scrollTo(0, 0);
await spawnTestStates(
iframe,
"target",
nsIAccessibleStates.STATE_OFFSCREEN,
nsIAccessibleStates.STATE_INVISIBLE
);
} finally {
await BrowserTestUtils.closeWindow(win);
}
});

View File

@@ -4,13 +4,6 @@
"use strict";
/* import-globals-from ../../mochitest/role.js */
/* import-globals-from ../../mochitest/states.js */
loadScripts(
{ name: "role.js", dir: MOCHITESTS_DIR },
{ name: "states.js", dir: MOCHITESTS_DIR }
);
async function runTests(browser, accDoc) {
let getAcc = id => findAccessibleChildByID(accDoc, id);

View File

@@ -4,42 +4,110 @@
"use strict";
/* import-globals-from ../../mochitest/role.js */
/* import-globals-from ../../mochitest/states.js */
loadScripts(
{ name: "role.js", dir: MOCHITESTS_DIR },
{ name: "states.js", dir: MOCHITESTS_DIR }
);
// Temporary debug logging for bug 1652192.
EventsLogger.enabled = true;
async function runTest(browser, accDoc) {
let getAcc = id => findAccessibleChildByID(accDoc, id);
testStates(getAcc("div"), 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN);
await untilCacheOk(
() => testVisibility(getAcc("div"), false, false),
"Div should be on screen"
);
let input = getAcc("input_scrolledoff");
testStates(input, STATE_OFFSCREEN, 0, STATE_INVISIBLE);
await untilCacheOk(
() => testVisibility(input, true, false),
"Input should be offscreen"
);
// scrolled off item (twice)
let lastLi = getAcc("li_last");
testStates(lastLi, STATE_OFFSCREEN, 0, STATE_INVISIBLE);
await untilCacheOk(
() => testVisibility(lastLi, true, false),
"Last list item should be offscreen"
);
// scroll into view the item
await invokeContentTask(browser, [], () => {
content.document.getElementById("li_last").scrollIntoView(true);
});
testStates(lastLi, 0, 0, STATE_OFFSCREEN | STATE_INVISIBLE);
await untilCacheOk(
() => testVisibility(lastLi, false, false),
"Last list item should no longer be offscreen"
);
// first item is scrolled off now (testcase for bug 768786)
let firstLi = getAcc("li_first");
testStates(firstLi, STATE_OFFSCREEN, 0, STATE_INVISIBLE);
await untilCacheOk(
() => testVisibility(firstLi, true, false),
"First listitem should now be offscreen"
);
await untilCacheOk(
() => testVisibility(getAcc("frame"), false, false),
"iframe should initially be onscreen"
);
let loaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, "iframeDoc");
await invokeContentTask(browser, [], () => {
content.document.querySelector("iframe").src =
'data:text/html,<body id="iframeDoc"><p id="p">hi</p></body>';
});
const iframeDoc = (await loaded).accessible;
await untilCacheOk(
() => testVisibility(getAcc("frame"), false, false),
"iframe outer doc should now be on screen"
);
await untilCacheOk(
() => testVisibility(iframeDoc, false, false),
"iframe inner doc should be on screen"
);
const iframeP = findAccessibleChildByID(iframeDoc, "p");
await untilCacheOk(
() => testVisibility(iframeP, false, false),
"iframe content should also be on screen"
);
// scroll into view the div
await invokeContentTask(browser, [], () => {
content.document.getElementById("div").scrollIntoView(true);
});
await untilCacheOk(
() => testVisibility(getAcc("frame"), true, false),
"iframe outer doc should now be off screen"
);
// See bug 1792256
await untilCacheOk(
() => !isCacheEnabled || testVisibility(iframeDoc, true, false),
"iframe inner doc should now be off screen"
);
await untilCacheOk(
() => testVisibility(iframeP, true, false),
"iframe content should now be off screen"
);
let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
// Accessibles in background tab should have offscreen state and no
// invisible state.
testStates(getAcc("div"), STATE_OFFSCREEN, 0, STATE_INVISIBLE);
await untilCacheOk(
() => testVisibility(getAcc("div"), true, false),
"Accs in background tab should be offscreen but not invisible."
);
await untilCacheOk(
() => testVisibility(getAcc("frame"), true, false),
"iframe outer doc should still be off screen"
);
// See bug 1792256
await untilCacheOk(
() => !isCacheEnabled || testVisibility(iframeDoc, true, false),
"iframe inner doc should still be off screen"
);
await untilCacheOk(
() => testVisibility(iframeP, true, false),
"iframe content should still be off screen"
);
BrowserTestUtils.removeTab(newTab);
}
@@ -50,7 +118,43 @@ addAccessibleTask(
<ul style="border:2px solid red; width: 100px; height: 50px; overflow: auto;">
<li id="li_first">item1</li><li>item2</li><li>item3</li>
<li>item4</li><li>item5</li><li id="li_last">item6</li>
</ul>`,
</ul>
<iframe id="frame"></iframe>
`,
runTest,
{ chrome: !isCacheEnabled, iframe: true, remoteIframe: true }
);
/**
* Test div containers are reported as onscreen, even if some of their contents are
* offscreen.
*/
addAccessibleTask(
`
<div id="outer" style="width:200vw; background: green; overflow:scroll;"><div id="inner"><div style="display:inline-block; width:100vw; background:red;" id="on">on screen</div><div style="background:blue; display:inline;" id="off">offscreen</div></div></div>
`,
async function(browser, accDoc) {
const outer = findAccessibleChildByID(accDoc, "outer");
const inner = findAccessibleChildByID(accDoc, "inner");
const on = findAccessibleChildByID(accDoc, "on");
const off = findAccessibleChildByID(accDoc, "off");
await untilCacheOk(
() => testVisibility(outer, false, false),
"outer should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(inner, false, false),
"inner should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(on, false, false),
"on should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(off, true, false),
"off should be off screen and visible"
);
},
{ chrome: true, iframe: true, remoteIframe: true }
);

View File

@@ -0,0 +1,131 @@
/* 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";
/**
* Test tables, table rows are reported on screen, even if some cells of a given row are
* offscreen.
*/
addAccessibleTask(
`
<table id="table" style="width:150vw;" border><tr id="row"><td id="one" style="width:50vw;">one</td><td style="width:50vw;" id="two">two</td><td id="three">three</td></tr></table>
`,
async function(browser, accDoc) {
const table = findAccessibleChildByID(accDoc, "table");
const row = findAccessibleChildByID(accDoc, "row");
const one = findAccessibleChildByID(accDoc, "one");
const two = findAccessibleChildByID(accDoc, "two");
const three = findAccessibleChildByID(accDoc, "three");
await untilCacheOk(
() => testVisibility(table, false, false),
"table should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(row, false, false),
"row should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(one, false, false),
"one should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(two, false, false),
"two should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(three, true, false),
"three should be off screen and visible"
);
},
{ chrome: true, iframe: true, remoteIframe: true }
);
/**
* Test rows and cells outside of the viewport are reported as offscreen.
*/
addAccessibleTask(
`
<table id="table" style="height:150vh;" border><tr style="height:100vh;" id="rowA"><td id="one">one</td></tr><tr id="rowB"><td id="two">two</td></tr></table>
`,
async function(browser, accDoc) {
const table = findAccessibleChildByID(accDoc, "table");
const rowA = findAccessibleChildByID(accDoc, "rowA");
const one = findAccessibleChildByID(accDoc, "one");
const rowB = findAccessibleChildByID(accDoc, "rowB");
const two = findAccessibleChildByID(accDoc, "two");
await untilCacheOk(
() => testVisibility(table, false, false),
"table should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(rowA, false, false),
"rowA should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(one, false, false),
"one should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(rowB, true, false),
"rowB should be off screen and visible"
);
await untilCacheOk(
() => testVisibility(two, true, false),
"two should be off screen and visible"
);
},
{ chrome: true, iframe: true, remoteIframe: true }
);
addAccessibleTask(
`
<div id="div">hello</div>
`,
async function(browser, accDoc) {
let textLeaf = findAccessibleChildByID(accDoc, "div").firstChild;
await untilCacheOk(
() => testVisibility(textLeaf, false, false),
"text should be on screen and visible"
);
let p = waitForEvent(EVENT_TEXT_INSERTED, "div");
await invokeContentTask(browser, [], () => {
content.document.getElementById("div").textContent = "goodbye";
});
await p;
textLeaf = findAccessibleChildByID(accDoc, "div").firstChild;
await untilCacheOk(
() => testVisibility(textLeaf, false, false),
"text should be on screen and visible"
);
},
{ chrome: true, iframe: true, remoteIframe: true }
);
/**
* Overlapping, opaque divs with the same bounds should not be considered
* offscreen.
*/
addAccessibleTask(
`
<style>div { height: 5px; width: 5px; background: green; }</style>
<div id="outer" role="group"><div style="background:blue;" id="inner" role="group">hi</div></div>
`,
async function(browser, accDoc) {
const outer = findAccessibleChildByID(accDoc, "outer");
const inner = findAccessibleChildByID(accDoc, "inner");
await untilCacheOk(
() => testVisibility(outer, false, false),
"outer should be on screen and visible"
);
await untilCacheOk(
() => testVisibility(inner, false, false),
"inner should be on screen and visible"
);
},
{ chrome: true, iframe: true, remoteIframe: true }
);

View File

@@ -4,7 +4,7 @@
"use strict";
/* exported waitForIFrameA11yReady, waitForIFrameUpdates, spawnTestStates */
/* exported waitForIFrameA11yReady, waitForIFrameUpdates, spawnTestStates, testVisibility */
// Load the shared-head file first.
/* import-globals-from ../shared-head.js */
@@ -15,9 +15,13 @@ Services.scriptloader.loadSubScript(
// Loading and common.js from accessible/tests/mochitest/ for all tests, as
// well as promisified-events.js.
/* import-globals-from ../../mochitest/states.js */
/* import-globals-from ../../mochitest/role.js */
loadScripts(
{ name: "common.js", dir: MOCHITESTS_DIR },
{ name: "promisified-events.js", dir: MOCHITESTS_DIR }
{ name: "promisified-events.js", dir: MOCHITESTS_DIR },
{ name: "role.js", dir: MOCHITESTS_DIR },
{ name: "states.js", dir: MOCHITESTS_DIR }
);
// This is another version of addA11yLoadEvent for fission.
@@ -79,3 +83,10 @@ async function spawnTestStates(browsingContext, elementId, expectedStates) {
testStates
);
}
function testVisibility(acc, shouldBeOffscreen, shouldBeInvisible) {
const [states] = getStates(acc);
let looksGood = shouldBeOffscreen == ((states & STATE_OFFSCREEN) != 0);
looksGood &= shouldBeInvisible == ((states & STATE_INVISIBLE) != 0);
return looksGood;
}

View File

@@ -1320,6 +1320,7 @@ class AccessibilityTest : BaseSessionTest() {
var rootBounds = Rect()
rootNode.getBoundsInScreen(rootBounds)
assertThat("Root node bounds are not empty", rootBounds.isEmpty, equalTo(false))
assertThat("Root node is visible to user", rootNode.isVisibleToUser, equalTo(true))
var labelBounds = Rect()
val labelNode = createNodeInfo(rootNode.getChildId(0))
@@ -1328,11 +1329,13 @@ class AccessibilityTest : BaseSessionTest() {
assertThat("Label bounds are in parent", rootBounds.contains(labelBounds), equalTo(true))
assertThat("First node is a label", labelNode.className.toString(), equalTo("android.view.View"))
assertThat("Label has text", labelNode.text.toString(), equalTo("Name:"))
assertThat("Label node is visible to user", labelNode.isVisibleToUser, equalTo(true))
val entryNode = createNodeInfo(rootNode.getChildId(1))
assertThat("Second node is an entry", entryNode.className.toString(), equalTo("android.widget.EditText"))
assertThat("Entry has vieIdwResourceName of 'name'", entryNode.viewIdResourceName, equalTo("name"))
assertThat("Entry value is text", entryNode.text.toString(), equalTo("Julie"))
assertThat("Entry node is visible to user", entryNode.isVisibleToUser, equalTo(true))
if (Build.VERSION.SDK_INT >= 19) {
assertThat("Entry hint is label",
entryNode.extras.getString("AccessibilityNodeInfo.hint"),
@@ -1346,6 +1349,7 @@ class AccessibilityTest : BaseSessionTest() {
// The child text leaf is pruned, so this button is childless.
assertThat("Button has a single text leaf", buttonNode.childCount, equalTo(0))
assertThat("Button has correct text", buttonNode.text.toString(), equalTo("Submit"))
assertThat("Button is visible to user", buttonNode.isVisibleToUser, equalTo(true))
}
@Test fun testLoadUnloadIframeDoc() {