Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

/* Any copyright is dedicated to the Public Domain.
"use strict";
// Bug 2001667: search tooltips were positioned against an intermediate layout
// (while the "no results" message was still visible) and never repositioned
// once it was hidden, leaving them vertically misaligned after a
// results -> no-results -> results round trip.
//
// This test injects a synthetic anchor element (rather than relying on real
// pane content) so it does not break if specific preference strings are
// removed or renamed. It also stubs performance.now() so that the
// FRAME_THRESHOLD branch fires deterministically on every loop iteration,
// ensuring the tooltip is created mid-loop (the exact condition the fix
// addresses) regardless of machine speed.
//
// Structural requirement: the injected anchor must sit AFTER #no-results-message
// in the mainPrefPane vbox. #search-tooltip-container lives inside
// #header-searchResults which precedes #no-results-message, so anchors below
// the message shift vertically when it toggles while the container does not.
// That asymmetry is what makes a stale tooltip top incorrect. An anchor
// prepended to the top of #mainPrefPane (above the message) would never shift
// and the regression would be undetectable.
const { sinon } = ChromeUtils.importESModule(
);
add_task(async function test_tooltip_realigned_after_no_results_roundtrip() {
await openPreferencesViaOpenPreferencesAPI(DEFAULT_PANE, { leaveOpen: true });
let win = gBrowser.contentWindow;
let doc = gBrowser.contentDocument;
let pane = win.gSearchResultsPane;
let container = doc.getElementById("search-tooltip-container");
let searchInput = doc.getElementById("searchInput");
// A token that exists only in our synthetic element (all lowercase because
// searchFunction lowercases the query at findInPage.js:278).
const TOKEN = "zzztooltipmocktoken";
const NOMATCH = "zzznomatchquery";
// Insert immediately after #no-results-message so the anchor sits below it
// in the mainPrefPane vbox. This is required: toggling the no-results
// message shifts elements below it (adding/removing its height from the
// normal flow), so the anchor moves while #search-tooltip-container (above
// the message) stays put. The tooltip top formula is
// anchorRect.top - containerRect.top
// and that delta changes with the message's visibility, which is exactly
// what the fix corrects.
let noResultsEl = doc.getElementById("no-results-message");
let anchor = doc.createElement("button");
anchor.setAttribute("searchkeywords", TOKEN);
anchor.textContent = "mock tooltip anchor";
noResultsEl.after(anchor);
registerCleanupFunction(() => anchor.remove());
// Structural guard: if this fails, the anchor is not immediately after the
// no-results message and the position assertion will pass vacuously without
// the fix.
Assert.strictEqual(
noResultsEl.nextElementSibling,
anchor,
"mock anchor is the immediate next sibling of #no-results-message"
);
// sendString appends to existing text; select-all first so each call
// replaces the query rather than extending it.
async function setSearch(query) {
searchInput.focus();
EventUtils.synthesizeKey("a", { accelKey: true }, win);
let completed = BrowserTestUtils.waitForEvent(
win,
"PreferencesSearchCompleted",
e => e.detail == query
);
EventUtils.sendString(query, win);
await completed;
}
// 1. Query that matches the synthetic anchor. Confirm it receives a tooltip.
await setSearch(TOKEN);
ok(
pane.listSearchTooltips.has(anchor),
"mock anchor got a tooltip on first search"
);
// 2. Switch to a no-results query so the "no results" message appears above
// the anchor (this shifts the anchor downward relative to the container).
await setSearch(NOMATCH);
is_element_visible(
noResultsEl,
"no-results message is visible for the unmatched query"
);
// 3. Return to the original results query which is the regression path.
//
// Stub performance.now() to return monotonically increasing values far
// above any real rAF timestamp. This makes `performance.now() - ts >
// FRAME_THRESHOLD` (findInPage.js:335) true on every loop iteration:
// - Iteration 1: listSearchTooltips is empty -> mid-loop creation is a
// no-op; our anchor (third child of mainPrefPane) is then processed
// and added to listSearchTooltips.
// - Iteration 2+: the anchor's tooltip is created MID-LOOP while
// #no-results-message is still in the layout (shifted anchor,
// unshifted container) -> stale top pre-fix.
// The post-loop pass (findInPage.js:434) is blocked by the tooltipNode
// guard, so without the fix the stale position is never corrected.
let base = win.performance.now() + 1e9;
let callCount = 0;
let stub = sinon
.stub(win.performance, "now")
.callsFake(() => base + ++callCount * 1000);
let calls;
try {
await setSearch(TOKEN);
calls = stub.callCount;
} finally {
stub.restore();
}
// Timing guard: if callCount is 0 or tiny, the stub did not intercept the
// search's performance.now() calls and the mid-loop path was not exercised.
Assert.greater(
calls,
2,
`performance.now() was intercepted by the stub (callCount=${calls})`
);
ok(
pane.listSearchTooltips.has(anchor),
"mock anchor got a tooltip on regression-path search"
);
// Every tooltip's stored top must equal the position computed against the
// now-settled layout. The formula mirrors _computeTooltipPosition exactly:
// top = anchorRect.top - tooltipContainerRect.top
// A stale tooltip is off by approximately the height of the no-results message.
let containerTop = container.getBoundingClientRect().top;
let tooltip = anchor.tooltipNode;
ok(tooltip, "mock anchor has an associated tooltip node");
let expectedTop = anchor.getBoundingClientRect().top - containerTop;
let actualTop = parseFloat(tooltip.style.top);
Assert.lessOrEqual(
Math.abs(actualTop - expectedTop),
1,
`tooltip top (${actualTop}px) matches settled anchor offset (${expectedTop}px)`
);
BrowserTestUtils.removeTab(gBrowser.selectedTab);
});