diff options
Diffstat (limited to 'accessible/tests/browser/bounds')
12 files changed, 1157 insertions, 0 deletions
diff --git a/accessible/tests/browser/bounds/browser.ini b/accessible/tests/browser/bounds/browser.ini new file mode 100644 index 0000000000..078659a97d --- /dev/null +++ b/accessible/tests/browser/bounds/browser.ini @@ -0,0 +1,27 @@ +[DEFAULT] +subsuite = a11y +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif +prefs = + javascript.options.asyncstack_capture_debuggee_only=false + +[browser_accessible_moved.js] +[browser_caret_rect.js] +[browser_position.js] +[browser_test_resolution.js] +skip-if = os == 'win' # bug 1372296 +[browser_test_zoom.js] +skip-if = true # Bug 1734271 +[browser_test_zoom_text.js] +https_first_disabled = true +skip-if = os == 'win' # bug 1372296 +[browser_zero_area.js] +[browser_test_display_contents.js] +[browser_test_simple_transform.js] +[browser_test_iframe_transform.js] +skip-if = + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure diff --git a/accessible/tests/browser/bounds/browser_accessible_moved.js b/accessible/tests/browser/bounds/browser_accessible_moved.js new file mode 100644 index 0000000000..307c680000 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_accessible_moved.js @@ -0,0 +1,49 @@ +/* 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"; + +function assertBoundsNonZero(acc) { + // XXX We don't use getBounds because it uses BoundsInCSSPixels(), but that + // isn't implemented for the cache yet. + let x = {}; + let y = {}; + let width = {}; + let height = {}; + acc.getBounds(x, y, width, height); + ok(x.value > 0, "x is non-0"); + ok(y.value > 0, "y is non-0"); + ok(width.value > 0, "width is non-0"); + ok(height.value > 0, "height is non-0"); +} + +/** + * Test that bounds aren't 0 after an Accessible is moved (but not re-created). + */ +addAccessibleTask( + ` +<div id="root" role="group"><div id="scrollable" role="presentation" style="height: 1px;"><button id="button">test</button></div></div> + `, + async function (browser, docAcc) { + let button = findAccessibleChildByID(docAcc, "button"); + assertBoundsNonZero(button); + + const root = findAccessibleChildByID(docAcc, "root"); + let reordered = waitForEvent(EVENT_REORDER, root); + // scrollable wasn't in the a11y tree, but this will force it to be created. + // button will be moved inside it. + await invokeContentTask(browser, [], () => { + content.document.getElementById("scrollable").style.overflow = "scroll"; + }); + await reordered; + + const scrollable = findAccessibleChildByID(docAcc, "scrollable"); + assertBoundsNonZero(scrollable); + // XXX button's RemoteAccessible was recreated, so we have to fetch it + // again. This shouldn't be necessary once bug 1739050 is fixed. + button = findAccessibleChildByID(docAcc, "button"); + assertBoundsNonZero(button); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_caret_rect.js b/accessible/tests/browser/bounds/browser_caret_rect.js new file mode 100644 index 0000000000..ac0ee3aa50 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_caret_rect.js @@ -0,0 +1,140 @@ +/* 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"; + +async function getCaretRect(browser, id) { + // The caret rect can only be queried on LocalAccessible. On Windows, we do + // send it across processes with caret events, but this currently can't be + // queried outside of the event, nor with XPCOM. + const [x, y, w, h] = await invokeContentTask(browser, [id], contentId => { + const node = content.document.getElementById(contentId); + const contentAcc = content.CommonUtils.accService.getAccessibleFor(node); + contentAcc.QueryInterface(Ci.nsIAccessibleText); + const caretX = {}; + const caretY = {}; + const caretW = {}; + const caretH = {}; + contentAcc.getCaretRect(caretX, caretY, caretW, caretH); + return [caretX.value, caretY.value, caretW.value, caretH.value]; + }); + info(`Caret bounds: ${x}, ${y}, ${w}, ${h}`); + return [x, y, w, h]; +} + +async function testCaretRect(browser, docAcc, id, offset) { + const acc = findAccessibleChildByID(docAcc, id, [nsIAccessibleText]); + is(acc.caretOffset, offset, `Caret at offset ${offset}`); + const charX = {}; + const charY = {}; + const charW = {}; + const charH = {}; + const atEnd = offset == acc.characterCount; + const empty = offset == 0 && atEnd; + const queryOffset = atEnd && !empty ? offset - 1 : offset; + acc.getCharacterExtents( + queryOffset, + charX, + charY, + charW, + charH, + COORDTYPE_SCREEN_RELATIVE + ); + info( + `Character ${queryOffset} bounds: ${charX.value}, ${charY.value}, ${charW.value}, ${charH.value}` + ); + const [caretX, caretY, caretW, caretH] = await getCaretRect(browser, id); + if (atEnd) { + ok(caretX > charX.value, "Caret x after last character x"); + } else { + is(caretX, charX.value, "Caret x same as character x"); + } + is(caretY, charY.value, "Caret y same as character y"); + is(caretW, 1, "Caret width is 1"); + if (!empty) { + is(caretH, charH.value, "Caret height same as character height"); + } +} + +function getAccBounds(acc) { + const x = {}; + const y = {}; + const w = {}; + const h = {}; + acc.getBounds(x, y, w, h); + return [x.value, y.value, w.value, h.value]; +} + +/** + * Test the caret rect in content documents. + */ +addAccessibleTask( + ` +<input id="input" value="ab"> +<input id="emptyInput"> + `, + async function (browser, docAcc) { + async function runTests() { + const input = findAccessibleChildByID(docAcc, "input", [ + nsIAccessibleText, + ]); + info("Focusing input"); + let caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input); + input.takeFocus(); + await caretMoved; + await testCaretRect(browser, docAcc, "input", 0); + info("Setting caretOffset to 1"); + caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input); + input.caretOffset = 1; + await caretMoved; + await testCaretRect(browser, docAcc, "input", 1); + info("Setting caretOffset to 2"); + caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, input); + input.caretOffset = 2; + await caretMoved; + await testCaretRect(browser, docAcc, "input", 2); + info("Resetting caretOffset to 0"); + input.caretOffset = 0; + + const emptyInput = findAccessibleChildByID(docAcc, "emptyInput", [ + nsIAccessibleText, + ]); + info("Focusing emptyInput"); + caretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, emptyInput); + emptyInput.takeFocus(); + await caretMoved; + await testCaretRect(browser, docAcc, "emptyInput", 0); + } + + await runTests(); + + // Check that the caret rect is correct when the title bar is shown. + if (LINUX || Services.env.get("MOZ_HEADLESS")) { + // Disabling tabs in title bar doesn't change the bounds on Linux or in + // headless mode. + info("Skipping title bar tests"); + return; + } + const [, origDocY] = getAccBounds(docAcc); + info("Showing title bar"); + let titleBarChanged = BrowserTestUtils.waitForMutationCondition( + document.documentElement, + { attributes: true, attributeFilter: ["tabsintitlebar"] }, + () => !document.documentElement.hasAttribute("tabsintitlebar") + ); + await SpecialPowers.pushPrefEnv({ + set: [["browser.tabs.inTitlebar", false]], + }); + await titleBarChanged; + const [, newDocY] = getAccBounds(docAcc); + Assert.greater( + newDocY, + origDocY, + "Doc has larger y after title bar change" + ); + await runTests(); + await SpecialPowers.popPrefEnv(); + }, + { chrome: true, topLevel: true } +); diff --git a/accessible/tests/browser/bounds/browser_position.js b/accessible/tests/browser/bounds/browser_position.js new file mode 100644 index 0000000000..18de7d8a76 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_position.js @@ -0,0 +1,103 @@ +/* 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: "role.js", dir: MOCHITESTS_DIR }); + +/** + * Test changing the left/top CSS properties. + */ +addAccessibleTask( + ` +<div id="div" style="position: relative; left: 0px; top: 0px; width: fit-content;"> + test +</div> + `, + async function (browser, docAcc) { + await testBoundsWithContent(docAcc, "div", browser); + info("Changing left"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("div").style.left = "200px"; + }); + await waitForContentPaint(browser); + await testBoundsWithContent(docAcc, "div", browser); + info("Changing top"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("div").style.top = "200px"; + }); + await waitForContentPaint(browser); + await testBoundsWithContent(docAcc, "div", browser); + }, + { chrome: true, topLevel: true, iframe: true } +); + +/** + * Test moving one element by inserting a second element before it such that the + * second element doesn't reflow. + */ +addAccessibleTask( + ` +<div id="reflowContainer"> + <p>1</p> + <p id="reflow2" hidden>2</p> + <p id="reflow3">3</p> +</div> +<p id="noReflow">noReflow</p> + `, + async function (browser, docAcc) { + for (const id of ["reflowContainer", "reflow3", "noReflow"]) { + await testBoundsWithContent(docAcc, id, browser); + } + // Show p2, which will reflow everything inside "reflowContainer", but just + // move "noReflow" down without reflowing it. + info("Showing p2"); + let shown = waitForEvent(EVENT_SHOW, "reflow2"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("reflow2").hidden = false; + }); + await waitForContentPaint(browser); + await shown; + for (const id of ["reflowContainer", "reflow2", "reflow3", "noReflow"]) { + await testBoundsWithContent(docAcc, id, browser); + } + }, + { chrome: true, topLevel: true, remoteIframe: true } +); + +/** + * Test bounds when an Accessible is re-parented. + */ +addAccessibleTask( + ` +<div id="container"> + <p style="height: 300px;">1</p> + <div class="pParent"> + <p id="p2">2</p> + </div> +</div> + `, + async function (browser, docAcc) { + const paraTree = { PARAGRAPH: [{ TEXT_LEAF: [] }] }; + const container = findAccessibleChildByID(docAcc, "container"); + testAccessibleTree(container, { SECTION: [paraTree, paraTree] }); + await testBoundsWithContent(docAcc, "p2", browser); + // Add a click listener to the div containing p2. This will cause an + // Accessible to be created for that div, which didn't have one before. + info("Adding click listener to pParent"); + let reordered = waitForEvent(EVENT_REORDER, container); + await invokeContentTask(browser, [], () => { + content.document + .querySelector(".pParent") + .addEventListener("click", function () {}); + }); + await reordered; + testAccessibleTree(container, { + SECTION: [paraTree, { SECTION: [paraTree] }], + }); + await testBoundsWithContent(docAcc, "p2", browser); + }, + { chrome: true, topLevel: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_test_display_contents.js b/accessible/tests/browser/bounds/browser_test_display_contents.js new file mode 100644 index 0000000000..db1bfce178 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_display_contents.js @@ -0,0 +1,44 @@ +/* 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/layout.js */ + +async function testContentBounds(browser, acc) { + let [expectedX, expectedY, expectedWidth, expectedHeight] = + await getContentBoundsForDOMElm(browser, getAccessibleDOMNodeID(acc)); + + let contentDPR = await getContentDPR(browser); + let [x, y, width, height] = getBounds(acc, contentDPR); + let prettyAccName = prettyName(acc); + is(x, expectedX, "Wrong x coordinate of " + prettyAccName); + is(y, expectedY, "Wrong y coordinate of " + prettyAccName); + is(width, expectedWidth, "Wrong width of " + prettyAccName); + ok(height >= expectedHeight, "Wrong height of " + prettyAccName); +} + +async function runTests(browser, accDoc) { + let p = findAccessibleChildByID(accDoc, "div"); + let p2 = findAccessibleChildByID(accDoc, "p"); + + await testContentBounds(browser, p); + await testContentBounds(browser, p2); +} + +/** + * Test accessible bounds for accs with display:contents + */ +addAccessibleTask( + ` + <div id="div">before + <ul id="ul" style="display: contents;"> + <li id="li" style="display: contents;"> + <p id="p">item</p> + </li> + </ul> + </div>`, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_test_iframe_transform.js b/accessible/tests/browser/bounds/browser_test_iframe_transform.js new file mode 100644 index 0000000000..a44ab75faf --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_iframe_transform.js @@ -0,0 +1,209 @@ +/* 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 TRANSLATION_OFFSET = 50; +const ELEM_ID = "test-elem-id"; + +// Modify the style of an iframe within the content process. This is different +// from, e.g., invokeSetStyle, because this function doesn't rely on +// invokeContentTask, which runs in the context of the iframe itself. +async function invokeSetStyleIframe(browser, id, style, value) { + if (value) { + Logger.log(`Setting ${style} style to ${value} for iframe with id: ${id}`); + } else { + Logger.log(`Removing ${style} style from iframe with id: ${id}`); + } + + // Translate the iframe itself (not content within it). + await SpecialPowers.spawn( + browser, + [id, style, value], + (iframeId, iframeStyle, iframeValue) => { + const elm = content.document.getElementById(iframeId); + if (iframeValue) { + elm.style[iframeStyle] = iframeValue; + } else { + delete elm.style[iframeStyle]; + } + } + ); +} + +// Test the accessible's bounds, comparing them to the content bounds from DOM. +// This function also accepts an offset, which is necessary in some cases where +// DOM doesn't know about cross-process offsets. +function testBoundsWithOffset(browser, iframeDocAcc, id, domElmBounds, offset) { + // Get the bounds as reported by the accessible. + const acc = findAccessibleChildByID(iframeDocAcc, id); + const accX = {}; + const accY = {}; + const accWidth = {}; + const accHeight = {}; + acc.getBounds(accX, accY, accWidth, accHeight); + + // getContentBoundsForDOMElm's result doesn't include iframe translation + // for in-process iframes, but does for out-of-process iframes. To account + // for that here, manually add in the translation offset when examining an + // in-process iframe. + const addTranslationOffset = !gIsRemoteIframe; + const expectedX = addTranslationOffset + ? domElmBounds[0] + offset + : domElmBounds[0]; + const expectedY = addTranslationOffset + ? domElmBounds[1] + offset + : domElmBounds[1]; + const expectedWidth = domElmBounds[2]; + const expectedHeight = domElmBounds[3]; + + let boundsAreEquivalent = true; + boundsAreEquivalent &&= accX.value == expectedX; + boundsAreEquivalent &&= accY.value == expectedY; + boundsAreEquivalent &&= accWidth.value == expectedWidth; + boundsAreEquivalent &&= accHeight.value == expectedHeight; + return boundsAreEquivalent; +} + +addAccessibleTask( + `<div id='${ELEM_ID}'>hello world</div>`, + async function (browser, iframeDocAcc, contentDocAcc) { + ok(iframeDocAcc, "IFRAME document accessible is present"); + + await testBoundsWithContent(iframeDocAcc, ELEM_ID, browser); + + // Translate the iframe, which should modify cross-process offset. + await invokeSetStyleIframe( + browser, + DEFAULT_IFRAME_ID, + "transform", + `translate(${TRANSLATION_OFFSET}px, ${TRANSLATION_OFFSET}px)` + ); + + // Allow content to advance to update DOM, then capture the DOM bounds. + await waitForContentPaint(browser); + const domElmBoundsAfterTranslate = await getContentBoundsForDOMElm( + browser, + ELEM_ID + ); + + // Ensure that there's enough time for the cache to update. + await untilCacheOk(() => { + return testBoundsWithOffset( + browser, + iframeDocAcc, + ELEM_ID, + domElmBoundsAfterTranslate, + TRANSLATION_OFFSET + ); + }, "Accessible bounds have changed in the cache and match DOM bounds."); + + // Adjust padding of the iframe, then verify bounds adjust properly. + // iframes already have a border by default, so we check padding here. + const PADDING_OFFSET = 100; + await invokeSetStyleIframe( + browser, + DEFAULT_IFRAME_ID, + "padding", + `${PADDING_OFFSET}px` + ); + + // Allow content to advance to update DOM, then capture the DOM bounds. + await waitForContentPaint(browser); + const domElmBoundsAfterAddingPadding = await getContentBoundsForDOMElm( + browser, + ELEM_ID + ); + + await untilCacheOk(() => { + return testBoundsWithOffset( + browser, + iframeDocAcc, + ELEM_ID, + domElmBoundsAfterAddingPadding, + TRANSLATION_OFFSET + ); + }, "Accessible bounds have changed in the cache and match DOM bounds."); + }, + { + topLevel: false, + iframe: true, + remoteIframe: true, + iframeAttrs: { + style: `height: 100px; width: 100px;`, + }, + } +); + +/** + * Test document bounds change notifications. + * Note: This uses iframes to change the doc container size in order + * to have the doc accessible's bounds change. + */ +addAccessibleTask( + `<div id="div" style="width: 30px; height: 30px"></div>`, + async function (browser, accDoc, foo) { + const docWidth = () => { + let width = {}; + accDoc.getBounds({}, {}, width, {}); + return width.value; + }; + + await untilCacheIs(docWidth, 0, "Doc width is 0"); + await invokeSetStyleIframe(browser, DEFAULT_IFRAME_ID, "width", `300px`); + await untilCacheIs(docWidth, 300, "Doc width is 300"); + }, + { + chrome: false, + topLevel: false, + iframe: true, + remoteIframe: true, + iframeAttrs: { style: "width: 0;" }, + } +); + +/** + * Test document bounds after re-creating an iframe. + */ +addAccessibleTask( + ` +<ol id="ol"> + <iframe id="iframe" src="data:text/html,"></iframe> +</ol> + `, + async function (browser, docAcc) { + let iframeDoc = findAccessibleChildByID(docAcc, "iframe").firstChild; + ok(iframeDoc, "Got the iframe document"); + const origX = {}; + const origY = {}; + iframeDoc.getBounds(origX, origY, {}, {}); + let reordered = waitForEvent(EVENT_REORDER, docAcc); + await invokeContentTask(browser, [], () => { + // This will cause a bounds cache update to be queued for the iframe doc. + content.document.getElementById("iframe").width = "600"; + // This will recreate the ol a11y subtree, including the iframe. The + // iframe document will be unbound briefly while this happens. We want to + // be sure processing the bounds cache update queued above doesn't assert + // while the document is unbound. The setTimeout is necessary to get the + // cache update to happen at the right time. + content.setTimeout( + () => (content.document.getElementById("ol").type = "i"), + 0 + ); + }); + await reordered; + const iframe = findAccessibleChildByID(docAcc, "iframe"); + // We don't currently fire an event when a DocAccessible is re-bound to a new OuterDoc. + await BrowserTestUtils.waitForCondition(() => iframe.firstChild); + iframeDoc = iframe.firstChild; + ok(iframeDoc, "Got the iframe document after re-creation"); + const newX = {}; + const newY = {}; + iframeDoc.getBounds(newX, newY, {}, {}); + ok( + origX.value == newX.value && origY.value == newY.value, + "Iframe document x and y are same after iframe re-creation" + ); + } +); diff --git a/accessible/tests/browser/bounds/browser_test_resolution.js b/accessible/tests/browser/bounds/browser_test_resolution.js new file mode 100644 index 0000000000..0b0b47418d --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_resolution.js @@ -0,0 +1,72 @@ +/* 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/layout.js */ + +async function testScaledBounds(browser, accDoc, scale, id, type = "object") { + let acc = findAccessibleChildByID(accDoc, id); + + // Get document offset + let [docX, docY] = getBounds(accDoc); + + // Get the unscaled bounds of the accessible + let [x, y, width, height] = + type == "text" + ? getRangeExtents(acc, 0, -1, COORDTYPE_SCREEN_RELATIVE) + : getBounds(acc); + + await invokeContentTask(browser, [scale], _scale => { + const { Layout } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs" + ); + Layout.setResolution(content.document, _scale); + }); + + let [scaledX, scaledY, scaledWidth, scaledHeight] = + type == "text" + ? getRangeExtents(acc, 0, -1, COORDTYPE_SCREEN_RELATIVE) + : getBounds(acc); + + let name = prettyName(acc); + isWithin(scaledWidth, width * scale, 2, "Wrong scaled width of " + name); + isWithin(scaledHeight, height * scale, 2, "Wrong scaled height of " + name); + isWithin(scaledX - docX, (x - docX) * scale, 2, "Wrong scaled x of " + name); + isWithin(scaledY - docY, (y - docY) * scale, 2, "Wrong scaled y of " + name); + + await invokeContentTask(browser, [], () => { + const { Layout } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs" + ); + Layout.setResolution(content.document, 1.0); + }); +} + +async function runTests(browser, accDoc) { + // The scrollbars get in the way of container bounds calculation. + await SpecialPowers.pushPrefEnv({ + set: [["ui.useOverlayScrollbars", 1]], + }); + + await testScaledBounds(browser, accDoc, 2.0, "p1"); + await testScaledBounds(browser, accDoc, 0.5, "p2"); + await testScaledBounds(browser, accDoc, 3.5, "b1"); + + await testScaledBounds(browser, accDoc, 2.0, "p1", "text"); + await testScaledBounds(browser, accDoc, 0.75, "p2", "text"); +} + +/** + * Test accessible boundaries when page is zoomed + */ +addAccessibleTask( + ` +<p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p> +<p id="p2">para 2</p> +<button id="b1">Hello</button> +`, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_test_simple_transform.js b/accessible/tests/browser/bounds/browser_test_simple_transform.js new file mode 100644 index 0000000000..7197968b40 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_simple_transform.js @@ -0,0 +1,225 @@ +/* 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"; + +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +// test basic translation +addAccessibleTask( + `<p id="translate">hello world</p>`, + async function (browser, iframeDocAcc, contentDocAcc) { + ok(iframeDocAcc, "IFRAME document accessible is present"); + await testBoundsWithContent(iframeDocAcc, "translate", browser); + + await invokeContentTask(browser, [], () => { + let p = content.document.getElementById("translate"); + p.style = "transform: translate(100px, 100px);"; + }); + + await waitForContentPaint(browser); + await testBoundsWithContent(iframeDocAcc, "translate", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// Test translation with two children. +addAccessibleTask( + ` +<div role="main" style="translate: 0 300px;"> + <p id="p1">hello</p> + <p id="p2">world</p> +</div> + `, + async function (browser, docAcc) { + await testBoundsWithContent(docAcc, "p1", browser); + await testBoundsWithContent(docAcc, "p2", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// test basic rotation +addAccessibleTask( + `<p id="rotate">hello world</p>`, + async function (browser, iframeDocAcc, contentDocAcc) { + ok(iframeDocAcc, "IFRAME document accessible is present"); + await testBoundsWithContent(iframeDocAcc, "rotate", browser); + + await invokeContentTask(browser, [], () => { + let p = content.document.getElementById("rotate"); + p.style = "transform: rotate(-40deg);"; + }); + + await waitForContentPaint(browser); + await testBoundsWithContent(iframeDocAcc, "rotate", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// test basic scale +addAccessibleTask( + `<p id="scale">hello world</p>`, + async function (browser, iframeDocAcc, contentDocAcc) { + ok(iframeDocAcc, "IFRAME document accessible is present"); + await testBoundsWithContent(iframeDocAcc, "scale", browser); + + await invokeContentTask(browser, [], () => { + let p = content.document.getElementById("scale"); + p.style = "transform: scale(2);"; + }); + + await waitForContentPaint(browser); + await testBoundsWithContent(iframeDocAcc, "scale", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// Test will-change: transform with no transform. +addAccessibleTask( + ` +<div id="willChangeTop" style="will-change: transform;"> + <p>hello</p> + <p id="willChangeTopP2">world</p> +</div> +<div role="group"> + <div id="willChangeInner" style="will-change: transform;"> + <p>hello</p> + <p id="willChangeInnerP2">world</p> + </div> +</div> + `, + async function (browser, docAcc) { + // Even though willChangeTop has no transform, it has + // will-change: transform, which means nsIFrame::IsTransformed returns + // true. We don't cache identity matrices, but because there is an offset + // to the root frame, layout includes this in the returned transform + // matrix. That means we get a non-identity matrix and thus we cache it. + // This is why we only test the identity matrix cache optimization for + // willChangeInner. + let hasTransform; + try { + const willChangeInner = findAccessibleChildByID( + docAcc, + "willChangeInner" + ); + willChangeInner.cache.getStringProperty("transform"); + hasTransform = true; + } catch (e) { + hasTransform = false; + } + ok(!hasTransform, "willChangeInner has no cached transform"); + await testBoundsWithContent(docAcc, "willChangeTopP2", browser); + await testBoundsWithContent(docAcc, "willChangeInnerP2", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// Verify that a transform forces creation of an accessible. +addAccessibleTask( + ` +<div id="container"> + <div style="transform:translate(100px,100px);"> + <p>test</p> + </div> +</div> + +<div id="div-presentational" role="presentation" style="transform:translate(100px,100px);"> + <p>test</p> +</div> + `, + async function (browser, docAcc) { + const tree = { TEXT_CONTAINER: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }] }; + + const divWithTransform = findAccessibleChildByID( + docAcc, + "container" + ).firstChild; + testAccessibleTree(divWithTransform, tree); + // testBoundsWithContent takes an id, but divWithTransform doesn't have one, + // so we can't test the bounds for it. + + // An accessible should still be created, even if the role is "presentation." + const divPresentational = findAccessibleChildByID( + docAcc, + "div-presentational" + ); + testAccessibleTree(divPresentational, tree); + await testBoundsWithContent(docAcc, "div-presentational", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// Verify that adding a transform on the fly forces creation of an accessible. +addAccessibleTask( + ` +<div id="div-to-transform" role="none" style="position: absolute; width: 300px; height: 300px;"> + <p>test</p> +</div> + `, + async function (browser, docAcc) { + let divToTransform = findAccessibleChildByID(docAcc, "div-to-transform"); + ok(!divToTransform, "There should not be a div accessible."); + + // Translate the div. + await invokeContentTask(browser, [], () => { + let div = content.document.getElementById("div-to-transform"); + div.style.transform = "translate(100%, 100%)"; + }); + await waitForContentPaint(browser); + + // Verify that the SECTION accessible appeared after we gave it a transform. + divToTransform = findAccessibleChildByID(docAcc, "div-to-transform"); + const tree = { + TEXT_CONTAINER: [{ PARAGRAPH: [{ TEXT_LEAF: [] }] }], + }; + testAccessibleTree(divToTransform, tree); + + // Verify that the bounds of the div are correctly modified. + await testBoundsWithContent(docAcc, "div-to-transform", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// Test translated, position: absolute Accessible in a container. +addAccessibleTask( + ` +<div id="container"> + <div id="transform" style="position: absolute; transform: translate(100px, 100px);"> + <p id="p">test</p> + </div> +</div> + `, + async function (browser, docAcc) { + await testBoundsWithContent(docAcc, "transform", browser); + await testBoundsWithContent(docAcc, "p", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); + +// Test bounds of a rotated element after scroll. +addAccessibleTask( + ` +<div id="scrollable" style="transform: rotate(180deg); overflow: scroll; height: 500px;"> + <p id="test">hello world</p><hr style="height: 100vh;"> +</div> + `, + async function (browser, docAcc) { + info( + "Testing that the unscrolled bounds of a transformed element are correct." + ); + await testBoundsWithContent(docAcc, "test", browser); + + await invokeContentTask(browser, [], () => { + // Scroll the scrollable region down (scrolls up due to the transform). + let elem = content.document.getElementById("scrollable"); + elem.scrollTo(0, elem.scrollHeight); + }); + + info( + "Testing that the scrolled bounds of a transformed element are correct." + ); + await testBoundsWithContent(docAcc, "test", browser); + }, + { topLevel: true, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_test_zoom.js b/accessible/tests/browser/bounds/browser_test_zoom.js new file mode 100644 index 0000000000..ac84e485a4 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_zoom.js @@ -0,0 +1,65 @@ +/* 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/layout.js */ + +async function testContentBounds(browser, acc) { + let [expectedX, expectedY, expectedWidth, expectedHeight] = + await getContentBoundsForDOMElm(browser, getAccessibleDOMNodeID(acc)); + + let contentDPR = await getContentDPR(browser); + let [x, y, width, height] = getBounds(acc, contentDPR); + let prettyAccName = prettyName(acc); + is(x, expectedX, "Wrong x coordinate of " + prettyAccName); + is(y, expectedY, "Wrong y coordinate of " + prettyAccName); + is(width, expectedWidth, "Wrong width of " + prettyAccName); + ok(height >= expectedHeight, "Wrong height of " + prettyAccName); +} + +async function runTests(browser, accDoc) { + let p1 = findAccessibleChildByID(accDoc, "p1"); + let p2 = findAccessibleChildByID(accDoc, "p2"); + let imgmap = findAccessibleChildByID(accDoc, "imgmap"); + if (!imgmap.childCount) { + // An image map may not be available even after the doc and image load + // is complete. We don't recieve any DOM events for this change either, + // so we need to wait for a REORDER. + await waitForEvent(EVENT_REORDER, "imgmap"); + } + let area = imgmap.firstChild; + + await testContentBounds(browser, p1); + await testContentBounds(browser, p2); + await testContentBounds(browser, area); + + await SpecialPowers.spawn(browser, [], () => { + const { Layout } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs" + ); + Layout.zoomDocument(content.document, 2.0); + }); + + await testContentBounds(browser, p1); + await testContentBounds(browser, p2); + await testContentBounds(browser, area); +} + +/** + * Test accessible boundaries when page is zoomed + */ +addAccessibleTask( + ` +<p id="p1">para 1</p><p id="p2">para 2</p> +<map name="atoz_map" id="map"> + <area id="area1" href="http://mozilla.org" + coords=17,0,30,14" alt="mozilla.org" shape="rect"> +</map> +<img id="imgmap" width="447" height="15" + usemap="#atoz_map" + src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">`, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_test_zoom_text.js b/accessible/tests/browser/bounds/browser_test_zoom_text.js new file mode 100644 index 0000000000..3f40b698bf --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_zoom_text.js @@ -0,0 +1,86 @@ +/* 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/layout.js */ + +async function runTests(browser, accDoc) { + async function testTextNode(id) { + let hyperTextNode = findAccessibleChildByID(accDoc, id); + let textNode = hyperTextNode.firstChild; + + let contentDPR = await getContentDPR(browser); + let [x, y, width, height] = getBounds(textNode, contentDPR); + testTextBounds( + hyperTextNode, + 0, + -1, + [x, y, width, height], + COORDTYPE_SCREEN_RELATIVE + ); + // A 0 range should return an empty rect. + testTextBounds( + hyperTextNode, + 0, + 0, + [0, 0, 0, 0], + COORDTYPE_SCREEN_RELATIVE + ); + } + + async function testEmptyInputNode(id) { + let inputNode = findAccessibleChildByID(accDoc, id); + + let [x, y, width, height] = getBounds(inputNode); + testTextBounds( + inputNode, + 0, + -1, + [x, y, width, height], + COORDTYPE_SCREEN_RELATIVE + ); + // A 0 range in an empty input should still return + // rect of input node. + testTextBounds( + inputNode, + 0, + 0, + [x, y, width, height], + COORDTYPE_SCREEN_RELATIVE + ); + } + + await testTextNode("p1"); + await testTextNode("p2"); + await testEmptyInputNode("i1"); + + await SpecialPowers.spawn(browser, [], () => { + const { Layout } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs" + ); + Layout.zoomDocument(content.document, 2.0); + }); + + await testTextNode("p1"); + + await SpecialPowers.spawn(browser, [], () => { + const { Layout } = ChromeUtils.importESModule( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.sys.mjs" + ); + Layout.zoomDocument(content.document, 1.0); + }); +} + +/** + * Test the text range boundary when page is zoomed + */ +addAccessibleTask( + ` + <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p> + <p id='p2'>ل</p> + <form><input id='i1' /></form>`, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_zero_area.js b/accessible/tests/browser/bounds/browser_zero_area.js new file mode 100644 index 0000000000..c0f9db2673 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_zero_area.js @@ -0,0 +1,118 @@ +/* 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/layout.js */ + +async function testContentBounds(browser, acc, expectedWidth, expectedHeight) { + let [expectedX, expectedY] = await getContentBoundsForDOMElm( + browser, + getAccessibleDOMNodeID(acc) + ); + + let contentDPR = await getContentDPR(browser); + let [x, y, width, height] = getBounds(acc, contentDPR); + let prettyAccName = prettyName(acc); + is(x, expectedX, "Wrong x coordinate of " + prettyAccName); + is(y, expectedY, "Wrong y coordinate of " + prettyAccName); + is(width, expectedWidth, "Wrong width of " + prettyAccName); + is(height, expectedHeight, "Wrong height of " + prettyAccName); +} +/** + * Test accessible bounds with different combinations of overflow and + * non-zero frame area. + */ +addAccessibleTask( + ` + <div id="a1" style="height:100px; width:100px; background:green;"></div> + <div id="a2" style="height:100px; width:100px; background:green;"><div style="height:300px; max-width: 300px; background:blue;"></div></div> + <div id="a3" style="height:0; width:0;"><div style="height:200px; width:200px; background:green;"></div></div> + `, + async function (browser, accDoc) { + const a1 = findAccessibleChildByID(accDoc, "a1"); + const a2 = findAccessibleChildByID(accDoc, "a2"); + const a3 = findAccessibleChildByID(accDoc, "a3"); + await testContentBounds(browser, a1, 100, 100); + await testContentBounds(browser, a2, 100, 100); + await testContentBounds(browser, a3, 200, 200); + } +); + +/** + * Ensure frames with zero area have their x, y coordinates correctly reported + * in bounds() + */ +addAccessibleTask( + ` +<br> +<div id="a" style="height:0; width:0;"></div> +`, + async function (browser, accDoc) { + const a = findAccessibleChildByID(accDoc, "a"); + await testContentBounds(browser, a, 0, 0); + } +); + +/** + * Ensure accessibles have accurately signed dimensions and position when + * offscreen. + */ +addAccessibleTask( + ` +<input type="radio" id="radio" style="left: -671091em; position: absolute;"> +`, + async function (browser, accDoc) { + const radio = findAccessibleChildByID(accDoc, "radio"); + const contentDPR = await getContentDPR(browser); + const [x, y, width, height] = getBounds(radio, contentDPR); + ok(x < 0, "X coordinate should be negative"); + ok(y > 0, "Y coordinate should be positive"); + ok(width > 0, "Width should be positive"); + ok(height > 0, "Height should be positive"); + // Note: the exact values of x, y, width, and height + // are inconsistent with the DOM element values of those + // fields, so we don't check our bounds against them with + // `testContentBounds` here. DOM reports a negative width, + // positive height, and a slightly different (+/- 20) + // x and y. + } +); + +/** + * Test height: 0 with align-items: flex-end. This causes the content to + * overflow above the frame's main rect. + */ +addAccessibleTask( + ` +<aside style="height: 0; display: flex; align-items: flex-end;"> + <div id="inner0">testing</div> +</aside> +<aside style="height: 1; display: flex; align-items: flex-end;"> + <div id="inner1">testing</div> +</aside> + `, + async function (browser, docAcc) { + await testBoundsWithContent(docAcc, "inner0", browser); + await testBoundsWithContent(docAcc, "inner1", browser); + }, + { chrome: true, topLevel: true, remoteIframe: true } +); + +/** + * Test a div (block) inside a span (inline). This causes the span's primary + * frame to have an empty rect offset from its visible content. + */ +addAccessibleTask( + ` +<span id="span" tabindex="-1"> + <div id="div">Testing</div> +</span> + `, + async function (browser, docAcc) { + await testBoundsWithContent(docAcc, "span", browser); + await testBoundsWithContent(docAcc, "div", browser); + }, + { chrome: true, topLevel: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/head.js b/accessible/tests/browser/bounds/head.js new file mode 100644 index 0000000000..c1882b9495 --- /dev/null +++ b/accessible/tests/browser/bounds/head.js @@ -0,0 +1,19 @@ +/* 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"; + +// Load the shared-head file first. +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "layout.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); |