diff --git a/devtools/client/shared/components/reps/reps.css b/devtools/client/shared/components/reps/reps.css index fc9ceb62e9ab..5cb91fd1032f 100644 --- a/devtools/client/shared/components/reps/reps.css +++ b/devtools/client/shared/components/reps/reps.css @@ -37,6 +37,10 @@ white-space: pre-wrap; } +.objectBox * { + unicode-bidi: isolate; +} + .objectBox-number, .objectBox-styleRule, .objectBox-element, diff --git a/devtools/client/webconsole/test/browser/_webconsole.ini b/devtools/client/webconsole/test/browser/_webconsole.ini index 847343ee8a65..f01bd396f1ef 100644 --- a/devtools/client/webconsole/test/browser/_webconsole.ini +++ b/devtools/client/webconsole/test/browser/_webconsole.ini @@ -178,6 +178,7 @@ support-files = tags = mcb [browser_webconsole_async_stack.js] [browser_webconsole_batching.js] +[browser_webconsole_bidi_string_isolation.js] [browser_webconsole_block_mixedcontent_securityerrors.js] tags = mcb [browser_webconsole_cached_messages_cross_domain_iframe.js] diff --git a/devtools/client/webconsole/test/browser/browser_webconsole_bidi_string_isolation.js b/devtools/client/webconsole/test/browser/browser_webconsole_bidi_string_isolation.js new file mode 100644 index 000000000000..8b2b1dfd4432 --- /dev/null +++ b/devtools/client/webconsole/test/browser/browser_webconsole_bidi_string_isolation.js @@ -0,0 +1,96 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "data:text/html;charset=utf8,Bidi strings"; +const rtlOverride = "\u202e"; + +add_task(async function() { + const hud = await openNewTabAndConsole(TEST_URI); + const browser = gBrowser.selectedBrowser; + + /* eslint-disable-next-line no-shadow */ + await SpecialPowers.spawn(browser, [rtlOverride], rtlOverride => { + const { console } = content.wrappedJSObject; + + console.log(Symbol(rtlOverride + "msg01")); + console.log([rtlOverride + "msg02"]); + console.log({ p: rtlOverride + "msg03" }); + console.log({ [rtlOverride + "msg04"]: null }); + console.log(new Set([rtlOverride + "msg05"])); + console.log(new Map([[rtlOverride + "msg06", null]])); + console.log(new Map([[null, rtlOverride + "msg07"]])); + + const parser = content.document.createElement("div"); + // eslint-disable-next-line no-unsanitized/property + parser.innerHTML = ` +
+
+ + `; + for (const child of parser.children) { + console.log(child); + } + }); + + const texts = [ + `Symbol("${rtlOverride}msg01")`, + `Array [ "${rtlOverride}msg02" ]`, + `Object { p: "${rtlOverride}msg03" }`, + `Object { "${rtlOverride}msg04": null }`, + `Set [ "${rtlOverride}msg05" ]`, + `Map { "${rtlOverride}msg06" → null }`, + `Map { null → "${rtlOverride}msg07" }`, + `
`, + `
`, + ``, + ]; + for (let i = 0; i < texts.length; ++i) { + const msgId = "msg" + String(i + 1).padStart(2, "0"); + const message = await waitFor(() => findMessage(hud, msgId)); + const objectBox = message.querySelector(".objectBox"); + is(objectBox.textContent, texts[i], "Should have all the relevant text"); + checkRects(objectBox); + } +}); + +function getBoundingClientRect(node) { + if (node.nodeType === Node.ELEMENT_NODE) { + return node.getBoundingClientRect(); + } + // There is no Node.getBoundingClientRect, use a Range instead. + const range = document.createRange(); + range.selectNode(node); + return range.getBoundingClientRect(); +} + +/** + * The console prints data build from external strings. They can contain + * characters that change the directionality of the text. For example, RTL + * characters will flow right to left. However, this should be isolated to + * prevent one string from mangling how another one is rendered. + * This function uses getBoundingClientRect() to check that the nodes, as a + * whole, flow LTR (even if the characters in the node flow RTL). + * The bidi algorithm happens at layout time, so we need to check the rects, + * DOM operations like textContent would be useless. + */ +function checkRects(node, parentRect = getBoundingClientRect(node)) { + let prevRect; + for (const child of node.childNodes) { + const rect = getBoundingClientRect(child); + ok(rect.x >= parentRect.x, "Rect should start inside parent"); + ok( + rect.x + rect.width <= parentRect.x + parentRect.width, + "Rect should end inside parent" + ); + if (prevRect) { + ok( + rect.x >= prevRect.x + prevRect.width, + "Rect should start after previous one" + ); + } + prevRect = rect; + checkRects(child, rect); + } +}