Bug 1873147 - [devtools] Update token highlight and popup position when Popup updates. r=devtools-reviewers,ochameau.

In Popup, we were only adding/removing the highlight on hovered token on mount/unmount.
But it can happen that the component gets updated (e.g. when the user moves quickly
between valid tokens).
In such case, we want to remove the highlighted class on all elements that it was
set on, and only add it back on the "preview target".
We also want to update the Popover position so it points to the actual token the
preview was computed for.

Differential Revision: https://phabricator.services.mozilla.com/D197582
This commit is contained in:
Nicolas Chevobbe 2024-01-05 10:12:24 +00:00
parent b21894581e
commit ff6752ce82
3 changed files with 133 additions and 12 deletions

View file

@ -46,27 +46,39 @@ export class Popup extends Component {
}
componentDidMount() {
this.addHighlightToToken();
this.addHighlightToToken(this.props.preview.target);
}
componentWillUnmount() {
this.removeHighlightFromToken();
this.removeHighlightFromToken(this.props.preview.target);
}
addHighlightToToken() {
componentDidUpdate(prevProps) {
const { target } = this.props.preview;
if (target) {
target.classList.add("preview-token");
addHighlightToTargetSiblings(target, this.props);
if (prevProps.target == target) {
return;
}
this.removeHighlightFromToken(prevProps.preview.target);
this.addHighlightToToken(target);
}
removeHighlightFromToken() {
const { target } = this.props.preview;
if (target) {
target.classList.remove("preview-token");
removeHighlightForTargetSiblings(target);
addHighlightToToken(target) {
if (!target) {
return;
}
target.classList.add("preview-token");
addHighlightToTargetSiblings(target, this.props);
}
removeHighlightFromToken(target) {
if (!target) {
return;
}
target.classList.remove("preview-token");
removeHighlightForTargetSiblings(target);
}
calculateMaxHeight = () => {

View file

@ -54,7 +54,10 @@ class Popover extends Component {
componentDidUpdate(prevProps) {
// We have to update `coords` when the Popover type changes
if (prevProps.type != this.props.type) {
if (
prevProps.type != this.props.type ||
prevProps.target !== this.props.target
) {
const coords =
this.props.type == "popover"
? this.getPopoverCoords()

View file

@ -103,6 +103,8 @@ add_task(async function () {
ok(true, `"hello" was expanded`);
await closePreviewForToken(dbg, tokenEl, "popup");
await resume(dbg);
await testMovingFromATokenToAnother(dbg);
});
async function testPreviews(dbg, fnName, previews) {
@ -149,6 +151,110 @@ async function assertNoPreviews(dbg, expression, line, column) {
is(result, "TIMEOUT", `No popup was displayed when hovering "${expression}"`);
}
async function testMovingFromATokenToAnother(dbg) {
info(
"Check that moving the mouse to another token when popup is displayed updates highlighted token and popup position"
);
invokeInTab("classPreview");
await waitForPaused(dbg);
info("Hover token `Foo` in `Foo.#privateStatic` expression");
const fooTokenEl = getTokenElAtLine(dbg, "Foo", 50, 44);
const cm = getCM(dbg);
const onScrolled = waitForScrolling(cm);
cm.scrollIntoView({ line: 49, ch: 0 }, 0);
await onScrolled;
const { element: fooPopupEl } = await tryHoverToken(dbg, fooTokenEl, "popup");
ok(!!fooPopupEl, "popup is displayed");
ok(
fooTokenEl.classList.contains("preview-token"),
"`Foo` token is highlighted"
);
// store original position
const originalPopupPosition = fooPopupEl.getBoundingClientRect().x;
info(
"Move mouse over the `#privateStatic` token in `Foo.#privateStatic` expression"
);
const privateStaticTokenEl = getTokenElAtLine(dbg, "#privateStatic", 50, 48);
// The sequence of event to trigger the bug this is covering isn't easily reproducible
// by firing a few chosen events (because of React async rendering), so we are going to
// mimick moving the mouse from the `Foo` to `#privateStatic` in a given amount of time
// So get all the different token quads to compute their center
const fooTokenQuad = fooTokenEl.getBoxQuads()[0];
const privateStaticTokenQuad = privateStaticTokenEl.getBoxQuads()[0];
const fooXCenter =
fooTokenQuad.p1.x + (fooTokenQuad.p2.x - fooTokenQuad.p1.x) / 2;
const fooYCenter =
fooTokenQuad.p1.y + (fooTokenQuad.p3.y - fooTokenQuad.p1.y) / 2;
const privateStaticXCenter =
privateStaticTokenQuad.p1.x +
(privateStaticTokenQuad.p2.x - privateStaticTokenQuad.p1.x) / 2;
const privateStaticYCenter =
privateStaticTokenQuad.p1.y +
(privateStaticTokenQuad.p3.y - privateStaticTokenQuad.p1.y) / 2;
// we can then compute the distance to cover between the two token centers
const xDistance = privateStaticXCenter - fooXCenter;
const yDistance = privateStaticYCenter - fooYCenter;
const movementDuration = 50;
const xIncrements = xDistance / movementDuration;
const yIncrements = yDistance / movementDuration;
// Finally, we're going to fire a mouseover event every ms
info("Move mousecursor between the `Foo` token to the `#privateStatic` one");
for (let i = 0; i < movementDuration; i++) {
const x = fooXCenter + (yDistance + i * xIncrements);
const y = fooYCenter + (yDistance + i * yIncrements);
EventUtils.synthesizeMouseAtPoint(
x,
y,
{
type: "mouseover",
},
fooTokenEl.ownerGlobal
);
await wait(1);
}
info("Wait for the popup to display the data for `#privateStatic`");
await waitFor(() => {
const popup = findElement(dbg, "popup");
if (!popup) {
return false;
}
// for `Foo`, the header text content is "Foo", so when it's "Object", we know the
// popup was updated
return (
popup.querySelector(".preview-popup .node .objectBox")?.textContent ===
"Object"
);
});
ok(true, "Popup is displayed for #privateStatic");
ok(
!fooTokenEl.classList.contains("preview-token"),
"`Foo` token is not highlighted anymore"
);
ok(
privateStaticTokenEl.classList.contains("preview-token"),
"`#privateStatic` token is highlighted"
);
const privateStaticPopupEl = await waitForElement(dbg, "popup");
const newPopupPosition = privateStaticPopupEl.getBoundingClientRect().x;
isnot(
Math.round(newPopupPosition),
Math.round(originalPopupPosition),
`Popup position was updated`
);
await resume(dbg);
}
async function testBucketedArray(dbg) {
invokeInTab("largeArray");
await waitForPaused(dbg);