diff --git a/accessible/interfaces/nsIAccessibleText.idl b/accessible/interfaces/nsIAccessibleText.idl
index d3c1ce0b16ae5..d2c1ef19f71cf 100644
--- a/accessible/interfaces/nsIAccessibleText.idl
+++ b/accessible/interfaces/nsIAccessibleText.idl
@@ -37,6 +37,8 @@ interface nsIAccessibleText : nsISupports
*/
attribute long caretOffset;
+ void getCaretRect(out long x, out long y, out long width, out long height);
+
readonly attribute long characterCount;
readonly attribute long selectionCount;
diff --git a/accessible/tests/browser/bounds/browser.ini b/accessible/tests/browser/bounds/browser.ini
index 0327310554b7d..078659a97da69 100644
--- a/accessible/tests/browser/bounds/browser.ini
+++ b/accessible/tests/browser/bounds/browser.ini
@@ -10,6 +10,7 @@ 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
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 0000000000000..ac0ee3aa5014f
--- /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(
+ `
+
+
+ `,
+ 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/xpcom/xpcAccessibleHyperText.cpp b/accessible/xpcom/xpcAccessibleHyperText.cpp
index 0eb51dbeab6f1..2708d902f57d5 100644
--- a/accessible/xpcom/xpcAccessibleHyperText.cpp
+++ b/accessible/xpcom/xpcAccessibleHyperText.cpp
@@ -246,6 +246,28 @@ xpcAccessibleHyperText::SetCaretOffset(int32_t aCaretOffset) {
return NS_OK;
}
+NS_IMETHODIMP
+xpcAccessibleHyperText::GetCaretRect(int32_t* aX, int32_t* aY, int32_t* aWidth,
+ int32_t* aHeight) {
+ NS_ENSURE_ARG_POINTER(aX);
+ NS_ENSURE_ARG_POINTER(aY);
+ NS_ENSURE_ARG_POINTER(aWidth);
+ NS_ENSURE_ARG_POINTER(aHeight);
+ *aX = *aY = *aWidth = *aHeight;
+
+ if (!mIntl) {
+ return NS_ERROR_FAILURE;
+ }
+ if (mIntl->IsRemote()) {
+ return NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ nsIWidget* widget;
+ LayoutDeviceIntRect rect = IntlLocal()->GetCaretRect(&widget);
+ rect.GetRect(aX, aY, aWidth, aHeight);
+ return NS_OK;
+}
+
NS_IMETHODIMP
xpcAccessibleHyperText::GetSelectionCount(int32_t* aSelectionCount) {
NS_ENSURE_ARG_POINTER(aSelectionCount);