Bug 1777608 - [devtools] Change how we render cropped URLs in String rep. r=bomsy.

Instead of rendering the cropped URL, we split the URL in 3 parts, so the full
URL text will be in the DOM, but we visually hide the middle part and replace
it with an ellipsis.
This way copying the message will still put the full URL in the clipboard.
A test case is added to ensure this works as expected.

Differential Revision: https://phabricator.services.mozilla.com/D165805
This commit is contained in:
Nicolas Chevobbe 2023-02-13 16:01:21 +00:00
parent 2c8367836d
commit 15e9036e65
6 changed files with 88 additions and 32 deletions

View file

@ -59,6 +59,10 @@
font-style: italic; font-style: italic;
} }
.objectBox-string a {
word-break: break-all;
}
.objectBox-string a, .objectBox-string a,
.objectBox-string a:visited { .objectBox-string a:visited {
color: currentColor; color: currentColor;
@ -71,6 +75,20 @@
text-decoration: underline; text-decoration: underline;
} }
/* Visually hide the middle of "cropped" url */
.objectBox-string a .cropped-url-middle {
max-width: 0;
max-height: 0;
display: inline-block;
overflow: hidden;
vertical-align: bottom;
}
.objectBox-string a .cropped-url-end::before {
content: "…";
}
.objectBox-function, .objectBox-function,
.objectBox-profile { .objectBox-profile {
color: var(--object-color); color: var(--object-color);

View file

@ -252,7 +252,7 @@ define(function(require, exports, module) {
} }
currentIndex = currentIndex + contentStart; currentIndex = currentIndex + contentStart;
let linkText = getCroppedString( const linkText = getCroppedString(
useUrl, useUrl,
currentIndex, currentIndex,
startCropIndex, startCropIndex,
@ -260,21 +260,35 @@ define(function(require, exports, module) {
); );
if (linkText) { if (linkText) {
if (urlCropLimit && useUrl.length > urlCropLimit) { const linkItems = [];
const shouldCrop = urlCropLimit && useUrl.length > urlCropLimit;
if (shouldCrop) {
const urlCropHalf = Math.ceil((urlCropLimit - ELLIPSIS.length) / 2); const urlCropHalf = Math.ceil((urlCropLimit - ELLIPSIS.length) / 2);
linkText = getCroppedString( // We cut the string into 3 elements and we'll visually hide the second one
useUrl, // in CSS. This way people can still copy the full link.
0, linkItems.push(
urlCropHalf, span(
useUrl.length - urlCropHalf { className: "cropped-url-start" },
useUrl.substring(0, urlCropHalf)
),
span(
{ className: "cropped-url-middle" },
useUrl.substring(urlCropHalf, useUrl.length - urlCropHalf)
),
span(
{ className: "cropped-url-end" },
useUrl.substring(useUrl.length - urlCropHalf)
)
); );
} else {
linkItems.push(linkText);
} }
items.push( items.push(
a( a(
{ {
key: `${useUrl}-${currentIndex}`, key: `${useUrl}-${currentIndex}`,
className: "url", className: "url" + (shouldCrop ? " cropped-url" : ""),
title: useUrl, title: useUrl,
draggable: false, draggable: false,
// Because we don't want the link to be open in the current // Because we don't want the link to be open in the current
@ -291,7 +305,7 @@ define(function(require, exports, module) {
} }
: null, : null,
}, },
linkText linkItems
) )
); );
} }

View file

@ -440,10 +440,16 @@ describe("test String with URL", () => {
urlCropLimit: 20, urlCropLimit: 20,
}); });
expect(element.text()).toEqual("http://xyz…klmnopqrst is the best"); expect(element.text()).toEqual(text);
const link = element.find("a").at(0); const link = element.find("a.cropped-url").at(0);
expect(link.prop("href")).toBe(xyzUrl); expect(link.prop("href")).toBe(xyzUrl);
expect(link.prop("title")).toBe(xyzUrl); expect(link.prop("title")).toBe(xyzUrl);
const linkParts = link.find("span");
expect(linkParts.at(0).hasClass("cropped-url-start")).toBe(true);
expect(linkParts.at(0).text()).toEqual("http://xyz");
expect(linkParts.at(1).hasClass("cropped-url-middle")).toBe(true);
expect(linkParts.at(2).hasClass("cropped-url-end")).toBe(true);
expect(linkParts.at(2).text()).toEqual("klmnopqrst");
}); });
it("renders multiple cropped URL", () => { it("renders multiple cropped URL", () => {
@ -457,17 +463,28 @@ describe("test String with URL", () => {
urlCropLimit: 20, urlCropLimit: 20,
}); });
expect(element.text()).toEqual( expect(element.text()).toEqual(`${xyzUrl} is lit, not ${abcUrl}`);
"http://xyz…klmnopqrst is lit, not http://abc…klmnopqrst"
);
const links = element.find("a"); const links = element.find("a.cropped-url");
const xyzLink = links.at(0); const xyzLink = links.at(0);
expect(xyzLink.prop("href")).toBe(xyzUrl); expect(xyzLink.prop("href")).toBe(xyzUrl);
expect(xyzLink.prop("title")).toBe(xyzUrl); expect(xyzLink.prop("title")).toBe(xyzUrl);
const xyzLinkParts = xyzLink.find("span");
expect(xyzLinkParts.at(0).hasClass("cropped-url-start")).toBe(true);
expect(xyzLinkParts.at(0).text()).toEqual("http://xyz");
expect(xyzLinkParts.at(1).hasClass("cropped-url-middle")).toBe(true);
expect(xyzLinkParts.at(2).hasClass("cropped-url-end")).toBe(true);
expect(xyzLinkParts.at(2).text()).toEqual("klmnopqrst");
const abc = links.at(1); const abc = links.at(1);
expect(abc.prop("href")).toBe(abcUrl); expect(abc.prop("href")).toBe(abcUrl);
expect(abc.prop("title")).toBe(abcUrl); expect(abc.prop("title")).toBe(abcUrl);
const abcLinkParts = abc.find("span");
expect(abcLinkParts.at(0).hasClass("cropped-url-start")).toBe(true);
expect(abcLinkParts.at(0).text()).toEqual("http://abc");
expect(abcLinkParts.at(1).hasClass("cropped-url-middle")).toBe(true);
expect(abcLinkParts.at(2).hasClass("cropped-url-end")).toBe(true);
expect(abcLinkParts.at(2).text()).toEqual("klmnopqrst");
}); });
it("renders full URL if smaller than cropLimit", () => { it("renders full URL if smaller than cropLimit", () => {
@ -484,6 +501,7 @@ describe("test String with URL", () => {
const link = element.find("a").at(0); const link = element.find("a").at(0);
expect(link.prop("href")).toBe(xyzUrl); expect(link.prop("href")).toBe(xyzUrl);
expect(link.prop("title")).toBe(xyzUrl); expect(link.prop("title")).toBe(xyzUrl);
expect(link.find(".cropped-url-start").length).toBe(0);
}); });
it("renders cropped URL followed by cropped string with urlCropLimit", () => { it("renders cropped URL followed by cropped string with urlCropLimit", () => {

View file

@ -21,6 +21,7 @@ httpServer.registerPathHandler("/test.js", function(request, response) {
console.log(new Error("error object")); console.log(new Error("error object"));
console.trace(); console.trace();
for (let i = 0; i < 2; i++) console.log("repeated") for (let i = 0; i < 2; i++) console.log("repeated")
console.log(document.location + "?" + "z".repeat(100))
} }
wrapper(); wrapper();
}; };
@ -122,7 +123,7 @@ async function testMessagesCopy(hud, timestamp) {
); );
is( is(
lines[2], lines[2],
` logStuff ${TEST_URI}test.js:9`, ` logStuff ${TEST_URI}test.js:10`,
"Stacktrace second line has the expected text" "Stacktrace second line has the expected text"
); );
@ -151,7 +152,7 @@ async function testMessagesCopy(hud, timestamp) {
); );
is( is(
lines[2], lines[2],
` logStuff ${TEST_URI}test.js:9`, ` logStuff ${TEST_URI}test.js:10`,
"Error Stacktrace second line has the expected text" "Error Stacktrace second line has the expected text"
); );
@ -174,7 +175,7 @@ async function testMessagesCopy(hud, timestamp) {
} }
is( is(
lines[1], lines[1],
` <anonymous> ${TEST_URI}test.js:11`, ` <anonymous> ${TEST_URI}test.js:12`,
"ReferenceError second line has expected text" "ReferenceError second line has expected text"
); );
ok( ok(
@ -190,6 +191,15 @@ async function testMessagesCopy(hud, timestamp) {
message = await waitFor(() => findConsoleAPIMessage(hud, "repeated 2")); message = await waitFor(() => findConsoleAPIMessage(hud, "repeated 2"));
clipboardText = await copyMessageContent(hud, message); clipboardText = await copyMessageContent(hud, message);
ok(true, "Clipboard text was found and saved"); ok(true, "Clipboard text was found and saved");
info("Test copy menu item for the message with the cropped URL");
message = await waitFor(() => findConsoleAPIMessage(hud, "z".repeat(100)));
ok(!!message.querySelector("a.cropped-url"), "URL is cropped");
clipboardText = await copyMessageContent(hud, message);
ok(
clipboardText.startsWith(TEST_URI) + "?" + "z".repeat(100),
"Full URL was copied to clipboard"
);
} }
function getTimestampText(messageEl) { function getTimestampText(messageEl) {

View file

@ -33,6 +33,11 @@ add_task(async function() {
return `${origin}${params}`; return `${origin}${params}`;
}; };
const getVisibleLinkText = linkEl => {
const [firstPart, , lastPart] = linkEl.children;
return `${firstPart.innerText}${ELLIPSIS}${lastPart.innerText}`;
};
const EXPECTED_MESSAGE = `get more information on this error`; const EXPECTED_MESSAGE = `get more information on this error`;
const msg = await waitFor(() => findErrorMessage(hud, EXPECTED_MESSAGE)); const msg = await waitFor(() => findErrorMessage(hud, EXPECTED_MESSAGE));
@ -42,7 +47,7 @@ add_task(async function() {
is(comLink.getAttribute("href"), url1, "First link has expected url"); is(comLink.getAttribute("href"), url1, "First link has expected url");
is(comLink.getAttribute("title"), url1, "First link has expected tooltip"); is(comLink.getAttribute("title"), url1, "First link has expected tooltip");
is( is(
comLink.textContent, getVisibleLinkText(comLink),
getCroppedUrl("https://example.com"), getCroppedUrl("https://example.com"),
"First link has expected text" "First link has expected text"
); );
@ -50,7 +55,7 @@ add_task(async function() {
is(orgLink.getAttribute("href"), url2, "Second link has expected url"); is(orgLink.getAttribute("href"), url2, "Second link has expected url");
is(orgLink.getAttribute("title"), url2, "Second link has expected tooltip"); is(orgLink.getAttribute("title"), url2, "Second link has expected tooltip");
is( is(
orgLink.textContent, getVisibleLinkText(orgLink),
getCroppedUrl("https://example.org"), getCroppedUrl("https://example.org"),
"Second link has expected text" "Second link has expected text"
); );

View file

@ -405,22 +405,13 @@ describe("PageError component:", () => {
const message = prepareMessage(packet, { getNextId: () => "1" }); const message = prepareMessage(packet, { getNextId: () => "1" });
const wrapper = render(PageError({ message, serviceContainer })); const wrapper = render(PageError({ message, serviceContainer }));
// Keep in sync with `urlCropLimit` in PageError.js.
const cropLimit = 120;
const partLength = cropLimit / 2;
const getCroppedUrl = url =>
`${url}${"a".repeat(partLength - url.length)}${"a".repeat(partLength)}`;
const croppedEvil = getCroppedUrl(evilDomain);
const croppedbad = getCroppedUrl(badDomain);
const text = wrapper.find(".message-body").text(); const text = wrapper.find(".message-body").text();
expect(text).toBe( expect(text).toBe(
`Uncaught “${croppedEvil}“ is evil and “${croppedbad}“ is not good either` `Uncaught “${evilURL}“ is evil and “${badURL}“ is not good either`
); );
// There should be 2 links. // There should be 2 cropped links.
const links = wrapper.find(".message-body a"); const links = wrapper.find(".message-body a.cropped-url");
expect(links.length).toBe(2); expect(links.length).toBe(2);
expect(links.eq(0).attr("href")).toBe(evilURL); expect(links.eq(0).attr("href")).toBe(evilURL);