gh-13439: Add tests coverage for boosts (gh-13977)

This commit is contained in:
mr. m
2026-06-01 11:24:45 +02:00
committed by GitHub
parent 64e2e49a00
commit 45075e2fbc
24 changed files with 1312 additions and 7 deletions

View File

@@ -28,7 +28,7 @@
"surfer": "surfer",
"test": "python3 scripts/run_tests.py",
"test:dbg": "python3 scripts/run_tests.py --jsdebugger --debug-on-failure",
"test:gtest": "cd engine && ./mach gtest",
"test:gtest": "cd engine && ./mach gtest Zen*",
"ffprefs": "cd tools/ffprefs && cargo run --bin ffprefs -- ../../",
"lc": "surfer license-check",
"lc:fix": "surfer license-check --fix",

View File

@@ -0,0 +1,104 @@
/* 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/. */
#include "gtest/gtest.h"
#include "mozilla/nsZenBoostsBackend.h"
using zen::detail::AccentCacheSize;
using zen::detail::EnsureCachedAccent;
using zen::detail::IsAccentCached;
using zen::detail::ResetAccentCache;
namespace {
class ZenBoostsAccentCache : public ::testing::Test {
protected:
void SetUp() override { ResetAccentCache(); }
void TearDown() override { ResetAccentCache(); }
};
constexpr nscolor kAccentA = NS_RGBA(80, 120, 200, 200);
constexpr nscolor kAccentB = NS_RGBA(200, 80, 80, 200);
constexpr nscolor kAccentC = NS_RGBA(80, 200, 120, 200);
constexpr nscolor kAccentD = NS_RGBA(200, 200, 80, 200);
constexpr nscolor kAccentE = NS_RGBA(120, 80, 200, 200);
} // namespace
TEST_F(ZenBoostsAccentCache, SizeIsAtLeastFour) {
EXPECT_GE(AccentCacheSize(), 4u);
}
TEST_F(ZenBoostsAccentCache, EmptyAfterReset) {
EnsureCachedAccent(kAccentA, 0.0f);
ResetAccentCache();
EXPECT_FALSE(IsAccentCached(kAccentA, 0.0f));
}
TEST_F(ZenBoostsAccentCache, SameKeyIsCachedAfterEnsure) {
EXPECT_FALSE(IsAccentCached(kAccentA, 0.0f));
EnsureCachedAccent(kAccentA, 0.0f);
EXPECT_TRUE(IsAccentCached(kAccentA, 0.0f));
}
// Keying on accent alone would silently serve a stale complementary accent
// when the rotation changes.
TEST_F(ZenBoostsAccentCache, DifferentRotationOccupiesDistinctEntry) {
EnsureCachedAccent(kAccentA, 0.0f);
EnsureCachedAccent(kAccentA, 90.0f);
EXPECT_TRUE(IsAccentCached(kAccentA, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentA, 90.0f));
}
TEST_F(ZenBoostsAccentCache, DifferentAccentOccupiesDistinctEntry) {
EnsureCachedAccent(kAccentA, 30.0f);
EnsureCachedAccent(kAccentB, 30.0f);
EXPECT_TRUE(IsAccentCached(kAccentA, 30.0f));
EXPECT_TRUE(IsAccentCached(kAccentB, 30.0f));
}
TEST_F(ZenBoostsAccentCache, RoundRobinEvictsOldestEntry) {
ASSERT_EQ(AccentCacheSize(), 4u);
EnsureCachedAccent(kAccentA, 0.0f);
EnsureCachedAccent(kAccentB, 0.0f);
EnsureCachedAccent(kAccentC, 0.0f);
EnsureCachedAccent(kAccentD, 0.0f);
EXPECT_TRUE(IsAccentCached(kAccentA, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentB, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentC, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentD, 0.0f));
EnsureCachedAccent(kAccentE, 0.0f);
EXPECT_FALSE(IsAccentCached(kAccentA, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentB, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentC, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentD, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentE, 0.0f));
}
// A cache hit must not consume a fresh slot, otherwise repeated paints with
// the same accent would evict their own neighbours.
TEST_F(ZenBoostsAccentCache, RepeatEnsureDoesNotChurnTheCache) {
ASSERT_EQ(AccentCacheSize(), 4u);
EnsureCachedAccent(kAccentA, 0.0f);
EnsureCachedAccent(kAccentB, 0.0f);
EnsureCachedAccent(kAccentC, 0.0f);
for (int i = 0; i < 16; ++i) {
EnsureCachedAccent(kAccentA, 0.0f);
}
EXPECT_TRUE(IsAccentCached(kAccentA, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentB, 0.0f));
EXPECT_TRUE(IsAccentCached(kAccentC, 0.0f));
EnsureCachedAccent(kAccentD, 0.0f);
EnsureCachedAccent(kAccentE, 0.0f);
EXPECT_FALSE(IsAccentCached(kAccentA, 0.0f));
}

View File

@@ -0,0 +1,42 @@
/* 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/. */
#include "gtest/gtest.h"
#include "mozilla/nsZenBoostsBackend.h"
using zen::nsZenBoostsBackend;
namespace {
const nscolor kResolveColors[] = {
NS_RGBA(0, 0, 0, 255), NS_RGBA(255, 255, 255, 255),
NS_RGBA(128, 128, 128, 255), NS_RGBA(255, 0, 0, 255),
NS_RGBA(0, 255, 0, 255), NS_RGBA(0, 0, 255, 255),
NS_RGBA(40, 44, 52, 255), NS_RGBA(248, 248, 248, 255),
NS_RGBA(20, 22, 28, 255), NS_RGBA(80, 80, 80, 200),
NS_RGBA(240, 17, 99, 1), NS_RGBA(0, 0, 0, 0),
};
} // namespace
// Removing the null-frame guard would crash chrome-process callers that
// legitimately pass nullptr (canvas getComputedStyle, font-palette binding,
// the StyleColor(nscolor)/StyleColor(StyleAbsoluteColor) overloads).
TEST(ZenBoostsResolveStyleColor, NullFrameIsIdentity)
{
for (nscolor c : kResolveColors) {
EXPECT_EQ(nsZenBoostsBackend::ResolveStyleColor(c, nullptr), c);
}
}
TEST(ZenBoostsResolveStyleColor, NullFrameIsIdempotent)
{
for (nscolor c : kResolveColors) {
nscolor once = nsZenBoostsBackend::ResolveStyleColor(c, nullptr);
nscolor twice = nsZenBoostsBackend::ResolveStyleColor(once, nullptr);
EXPECT_EQ(once, c);
EXPECT_EQ(twice, c);
}
}

View File

@@ -3,7 +3,9 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
UNIFIED_SOURCES += [
"TestZenBoostsAccentCache.cpp",
"TestZenBoostsColorFilter.cpp",
"TestZenBoostsResolveStyleColor.cpp",
]
FINAL_LIBRARY = "xul-gtest"

View File

@@ -450,11 +450,9 @@ inline static void GetZenBoostsDataForFrame(const nsIFrame* aFrame,
} // namespace
#ifdef ENABLE_TESTS
namespace detail {
// Thin forwarders that give unit tests access to the pure color math without
// pulling in the singleton / BrowsingContext. They are defined here, after the
// anonymous namespace, so they can reach those file-local implementations.
nsZenAccentOklab PrecomputeAccent(nscolor aAccentColor) {
return zenPrecomputeAccent(aAccentColor);
}
@@ -474,7 +472,33 @@ nscolor InvertColorChannel(nscolor aColor) {
return zenInvertColorChannel(aColor);
}
size_t AccentCacheSize() { return kAccentCacheSize; }
void ResetAccentCache() {
for (auto& entry : sAccentCache) {
entry.valid = false;
entry.accentNS = 0;
entry.rotationDeg = 0.0f;
}
sAccentCacheNext = 0;
}
bool IsAccentCached(nscolor aAccentNS, float aRotationDeg) {
for (const auto& entry : sAccentCache) {
if (entry.valid && entry.accentNS == aAccentNS &&
entry.rotationDeg == aRotationDeg) {
return true;
}
}
return false;
}
void EnsureCachedAccent(nscolor aAccentNS, float aRotationDeg) {
(void)GetCachedAccent(aAccentNS, aRotationDeg);
}
} // namespace detail
#endif // ENABLE_TESTS
static mozilla::StaticRefPtr<nsZenBoostsBackend> sZenBoostsBackend;

View File

@@ -27,10 +27,9 @@ struct nsZenAccentOklab {
float contrastFactor;
};
#ifdef ENABLE_TESTS
// Test-only forwarders into the file-local color math and accent cache.
namespace detail {
// Pure color-math primitives, exposed for unit testing. These have no
// dependency on the singleton, the BrowsingContext, or the process type, so
// they can be exercised directly from gtest.
nsZenAccentOklab PrecomputeAccent(nscolor aAccentColor);
nsZenAccentOklab RotateAccent(const nsZenAccentOklab& aBase,
float aRotationDeg);
@@ -38,7 +37,13 @@ nscolor FilterColorChannel(nscolor aOriginalColor,
const nsZenAccentOklab& aAccent,
const nsZenAccentOklab& aComplementary);
nscolor InvertColorChannel(nscolor aColor);
size_t AccentCacheSize();
void ResetAccentCache();
bool IsAccentCached(nscolor aAccentNS, float aRotationDeg);
void EnsureCachedAccent(nscolor aAccentNS, float aRotationDeg);
} // namespace detail
#endif // ENABLE_TESTS
class nsZenBoostsBackend final : public nsISupports {
public:

View File

@@ -8,6 +8,41 @@ support-files = [
]
["browser_boost_selector_basic.js"]
["browser_boost_selector_escaping.js"]
["browser_boost_selector_invalid.js"]
["browser_boost_selector_nthchild.js"]
["browser_boosts_animation.js"]
["browser_boosts_background.js"]
["browser_boosts_border.js"]
["browser_boosts_gradient.js"]
["browser_boosts_inline_svg.js"]
["browser_boosts_input_text.js"]
["browser_boosts_invert.js"]
["browser_boosts_outline.js"]
["browser_boosts_placeholder.js"]
["browser_boosts_pseudo_before.js"]
["browser_boosts_shadow.js"]
["browser_boosts_svg_background_image.js"]
["browser_boosts_svg_image.js"]
["browser_boosts_svg_linear_gradient.js"]
["browser_boosts_svg_use_sprite.js"]
["browser_boosts_text_color.js"]

View File

@@ -0,0 +1,52 @@
/* 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";
// The compositor animation path in AnimationInfo.cpp resolves a colour with
// the host frame, then either takes it as-is (currentColor keyframe) or
// passes it through ResolveStyleColor (absolute keyframe). Both paths must
// end up at the same boosted colour as the static equivalent. We pin a
// background-color animation at 50% via animation-delay and compare against
// a static element that holds the interpolated colour.
add_task(async function animated_background_is_boosted() {
// Paused-at-50% animation between #000 and #fff → mid grey at the sampled
// time. We compare against a static rgb(128,128,128) swatch and require
// both to land at the same colour after boost.
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
.swatch { width: 200px; height: 200px; display: inline-block;
vertical-align: top; }
#static { background-color: rgb(128, 128, 128); }
@keyframes fade { from { background-color: black; } to { background-color: white; } }
#animated {
background-color: black;
animation: fade 4s linear infinite;
animation-delay: -2s;
animation-play-state: paused;
}
</style>
<div id="static" class="swatch"></div>
<div id="animated" class="swatch"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const staticBoosted = await pixelInElement(browser, "#static");
const animBoosted = await pixelInElement(browser, "#animated");
Assert.ok(
pixelsClose(staticBoosted, animBoosted, 6),
`animated and static mid-grey must land at the same boosted colour; ` +
`static=${JSON.stringify(staticBoosted)} animated=${JSON.stringify(
animBoosted
)}`
);
});
});

View File

@@ -0,0 +1,50 @@
/* 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";
// Verifies that activating a boost on a tab moves the painted colour of a
// plain CSS `background-color` block. Catches regressions where the per-color
// boost in StyleAbsoluteColor::ToColor or CalcColor is bypassed for
// backgrounds, and the gap that would surface if nsCSSRendering ever stopped
// passing the frame through GetVisitedDependentColor.
add_task(async function bg_color_is_tinted() {
const html = `
<style>
html, body { margin: 0; padding: 0; }
#bg { width: 200px; height: 200px; background-color: rgb(120, 120, 120); }
</style>
<div id="bg"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
await setBoost(browser, { accent: 0 });
const baseline = await pixelInElement(browser, "#bg");
Assert.equal(baseline.r, 120, "baseline R is the literal background");
Assert.equal(baseline.g, 120, "baseline G is the literal background");
Assert.equal(baseline.b, 120, "baseline B is the literal background");
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const boosted = await pixelInElement(browser, "#bg");
Assert.ok(
pixelsDiffer(baseline, boosted, 3),
`boost should tint the background; got baseline=${JSON.stringify(
baseline
)} boosted=${JSON.stringify(boosted)}`
);
// Sanity: clear the boost and the painted colour returns home.
await setBoost(browser, { accent: 0 });
const cleared = await pixelInElement(browser, "#bg");
Assert.ok(
pixelsClose(cleared, baseline, 2),
`clearing boost should restore original; got ${JSON.stringify(cleared)}`
);
});
});

View File

@@ -0,0 +1,53 @@
/* 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";
// Borders go through nsCSSRendering::ComputeBorderColors → CalcColor with the
// frame, so the boost should reach them. Use a thick solid border and sample
// inside the border band (which is fully the border colour) rather than the
// element interior.
add_task(async function border_color_is_tinted() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#b { width: 200px; height: 200px; margin: 50px;
background: white;
border: 30px solid rgb(120, 120, 120); }
</style>
<div id="b"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
// Sample a point inside the top border band: x = centre of element, y just
// below the top edge (well inside the 30px-wide border).
const point = await SpecialPowers.spawn(browser, [], () => {
const r = content.document.querySelector("#b").getBoundingClientRect();
return {
x: Math.round(r.left + r.width / 2),
y: Math.round(r.top + 15),
};
});
await setBoost(browser, { accent: 0 });
const baseline = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsClose(baseline, { r: 120, g: 120, b: 120 }, 5),
`baseline border colour ≈ rgb(120,120,120); got ${JSON.stringify(baseline)}`
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const boosted = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsDiffer(baseline, boosted, 3),
`border colour should be tinted; baseline=${JSON.stringify(baseline)} ` +
`boosted=${JSON.stringify(boosted)}`
);
});
});

View File

@@ -0,0 +1,83 @@
/* 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";
// CSS linear-gradient stops are resolved via the nsCSSGradientRenderer +
// ColorStopInterpolator path, which threads the frame down into
// gfxUtils::ToDeviceColor(StyleAbsoluteColor, frame). If that threading
// regresses, gradient stops paint without the boost while everything else
// around them gets tinted — a particularly visible regression.
add_task(async function linear_gradient_stops_are_boosted() {
// Use a two-stop horizontal gradient so a sample near the left edge is
// dominated by the first stop and a sample near the right edge by the
// second. The element is sized 400×200 so we have generous sample regions.
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#g { width: 400px; height: 200px;
background: linear-gradient(to right,
rgb(180, 60, 60),
rgb(60, 60, 180)); }
</style>
<div id="g"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
// Sample 5% in (mostly first stop) and 95% in (mostly last stop).
const points = await SpecialPowers.spawn(browser, [], () => {
const r = content.document.querySelector("#g").getBoundingClientRect();
return {
left: {
x: Math.round(r.left + r.width * 0.05),
y: Math.round(r.top + r.height / 2),
},
right: {
x: Math.round(r.left + r.width * 0.95),
y: Math.round(r.top + r.height / 2),
},
};
});
await setBoost(browser, { accent: 0 });
const leftBaseline = await pixelAt(browser, points.left.x, points.left.y);
const rightBaseline = await pixelAt(
browser,
points.right.x,
points.right.y
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const leftBoosted = await pixelAt(browser, points.left.x, points.left.y);
const rightBoosted = await pixelAt(browser, points.right.x, points.right.y);
Assert.ok(
pixelsDiffer(leftBaseline, leftBoosted, 3),
`left gradient stop must tint; baseline=${JSON.stringify(
leftBaseline
)} boosted=${JSON.stringify(leftBoosted)}`
);
Assert.ok(
pixelsDiffer(rightBaseline, rightBoosted, 3),
`right gradient stop must tint; baseline=${JSON.stringify(
rightBaseline
)} boosted=${JSON.stringify(rightBoosted)}`
);
// The two stops must remain distinguishable after boost — otherwise the
// gradient has flattened, which would be a separate regression (e.g.,
// ToDeviceColor losing per-stop frame context and collapsing to a single
// tinted value).
Assert.ok(
pixelsDiffer(leftBoosted, rightBoosted, 8),
`boosted gradient endpoints collapsed; left=${JSON.stringify(
leftBoosted
)} right=${JSON.stringify(rightBoosted)}`
);
});
});

View File

@@ -0,0 +1,106 @@
/* 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";
// The "twice" diagnostic: paint an inline SVG fill and a CSS background-color
// with the *same* source colour, side by side. Under boost they must end up
// painted with the same boosted colour. If the SVG sample comes out
// noticeably more saturated / further from the baseline than the CSS one, the
// SVG paint path is applying the boost twice (the symptom you reported).
add_task(async function inline_svg_fill_matches_css_bg_under_boost() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#div, #svg { width: 200px; height: 200px; display: inline-block;
vertical-align: top; }
#div { background-color: rgb(51, 54, 57); }
</style>
<div id="div"></div>
<svg id="svg" width="200" height="200" viewBox="0 0 200 200">
<rect width="200" height="200" fill="rgb(51, 54, 57)"/>
</svg>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
await setBoost(browser, { accent: 0 });
const divBaseline = await pixelInElement(browser, "#div");
const svgBaseline = await pixelInElement(browser, "#svg");
// Sanity: both paint the same source colour before any boost.
Assert.ok(
pixelsClose(divBaseline, svgBaseline, 2),
`pre-boost div/svg should already match; div=${JSON.stringify(
divBaseline
)} svg=${JSON.stringify(svgBaseline)}`
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const divBoosted = await pixelInElement(browser, "#div");
const svgBoosted = await pixelInElement(browser, "#svg");
Assert.ok(
pixelsDiffer(divBaseline, divBoosted, 3),
"div background must be tinted under boost (sanity)"
);
Assert.ok(
pixelsDiffer(svgBaseline, svgBoosted, 3),
"SVG fill must be tinted under boost (sanity)"
);
// Headline assertion: the SVG fill and the CSS background, both starting
// from rgb(51, 54, 57) on the same page, must land at the same colour
// after the boost. A larger gap is the "filtered twice" symptom.
Assert.ok(
pixelsClose(divBoosted, svgBoosted, 4),
`SVG fill drifted from CSS background under boost — likely double-` +
`applied boost on the SVG path. div=${JSON.stringify(
divBoosted
)} svg=${JSON.stringify(svgBoosted)}`
);
});
});
// Same comparison but with `fill="currentColor"` — your reported case. The SVG
// inherits `color` and resolves it via the path frame; the CSS swatch resolves
// via its own frame. Both must land in the same place after one boost pass.
add_task(async function inline_svg_currentcolor_matches_css_under_boost() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
.row { color: rgb(51, 54, 57); }
#div, #svg { width: 200px; height: 200px; display: inline-block;
vertical-align: top; }
#div { background-color: currentColor; }
</style>
<div class="row">
<div id="div"></div>
<svg id="svg" width="200" height="200" viewBox="0 0 200 200"
fill="currentColor">
<rect width="200" height="200"/>
</svg>
</div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const divBoosted = await pixelInElement(browser, "#div");
const svgBoosted = await pixelInElement(browser, "#svg");
Assert.ok(
pixelsClose(divBoosted, svgBoosted, 4),
`SVG currentColor fill must match the same-colour CSS swatch after ` +
`boost. div=${JSON.stringify(divBoosted)} svg=${JSON.stringify(
svgBoosted
)}`
);
});
});

View File

@@ -0,0 +1,59 @@
/* 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";
// The editor content of <input> / <textarea> sits in a UA-widget shadow tree;
// the boost-exemption logic must treat it as author content. To get a clean
// sample we use textarea (more text area, no themed background overdraw on
// the sample point) with an explicit colour so we know the baseline.
add_task(async function input_text_is_boosted() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
textarea {
appearance: none; /* avoid theme repainting over our sample */
background: white;
color: rgb(40, 44, 52);
font: 200px/1 system-ui, sans-serif;
border: none;
padding: 0 20px;
width: 600px;
height: 240px;
}
</style>
<textarea id="t">█</textarea>`;
// Full-block U+2588 again — the centre pixel is the solid foreground colour.
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
// The block character sits near the start of the textarea content box.
const point = await SpecialPowers.spawn(browser, [], () => {
const r = content.document.querySelector("#t").getBoundingClientRect();
// Estimate the block's centre: x ≈ left padding + half a glyph width;
// y ≈ vertical centre of the line box.
return {
x: Math.round(r.left + 120),
y: Math.round(r.top + 120),
};
});
await setBoost(browser, { accent: 0 });
const baseline = await pixelAt(browser, point.x, point.y);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const boosted = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsDiffer(baseline, boosted, 3),
`editor text must tint with boost; baseline=${JSON.stringify(
baseline
)} boosted=${JSON.stringify(boosted)}`
);
});
});

View File

@@ -0,0 +1,46 @@
/* 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";
// Invert mode: white must come out dark, black must come out light. A double-
// invert regression (invert applied twice somewhere in the paint pipeline)
// looks like "white stays white" / "black stays black" — which is exactly
// what this test guards.
add_task(async function invert_flips_lightness() {
const html = `
<style>
html, body { margin: 0; padding: 0; }
.swatch { width: 200px; height: 200px; display: inline-block; }
#white { background: white; }
#black { background: black; }
</style>
<div id="white" class="swatch"></div>
<div id="black" class="swatch"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
await setBoost(browser, { accent: 0, inverted: false });
const whiteOff = await pixelInElement(browser, "#white");
const blackOff = await pixelInElement(browser, "#black");
Assert.greater(pxLuma(whiteOff), 240, "baseline white is bright");
Assert.less(pxLuma(blackOff), 16, "baseline black is dark");
await setBoost(browser, { accent: 0, inverted: true });
const whiteOn = await pixelInElement(browser, "#white");
const blackOn = await pixelInElement(browser, "#black");
Assert.less(
pxLuma(whiteOn),
pxLuma(whiteOff),
"white must darken under invert; double-invert would leave it bright"
);
Assert.greater(
pxLuma(blackOn),
pxLuma(blackOff),
"black must lighten under invert (and stay off pure black via the floor)"
);
});
});

View File

@@ -0,0 +1,54 @@
/* 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";
// CSS `outline` is painted by nsCSSRendering with its own color path. Pin
// that it gets the boost: a thick solid outline must tint when the page is
// boosted (just like a border does), and the sample point inside the outline
// band must move noticeably.
add_task(async function outline_color_is_tinted() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#o { width: 200px; height: 200px; margin: 60px;
background: white;
outline: 30px solid rgb(120, 120, 120);
outline-offset: 0; }
</style>
<div id="o"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
// Sample inside the outline band on the left side of the element.
const point = await SpecialPowers.spawn(browser, [], () => {
const r = content.document.querySelector("#o").getBoundingClientRect();
return {
x: Math.round(r.left - 15),
y: Math.round(r.top + r.height / 2),
};
});
await setBoost(browser, { accent: 0 });
const baseline = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsClose(baseline, { r: 120, g: 120, b: 120 }, 5),
`baseline outline ≈ rgb(120,120,120); got ${JSON.stringify(baseline)}`
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const boosted = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsDiffer(baseline, boosted, 3),
`outline must tint; baseline=${JSON.stringify(
baseline
)} boosted=${JSON.stringify(boosted)}`
);
});
});

View File

@@ -0,0 +1,57 @@
/* 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";
// `::placeholder` is an element-backed pseudo inside a text-control's UA
// widget shadow tree. The boost-exemption logic must un-exempt it the same
// way it does the editor's typed text. Use a solid-block character as the
// placeholder so we get a clean foreground sample.
add_task(async function placeholder_is_boosted() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
input {
appearance: none;
background: white;
color: rgb(40, 44, 52);
font: 200px/1 system-ui, sans-serif;
border: none;
padding: 0 20px;
width: 600px;
height: 260px;
}
input::placeholder { color: rgb(40, 44, 52); opacity: 1; }
</style>
<input id="i" placeholder="█">`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
const point = await SpecialPowers.spawn(browser, [], () => {
const r = content.document.querySelector("#i").getBoundingClientRect();
// The placeholder block sits near the left of the input.
return {
x: Math.round(r.left + 120),
y: Math.round(r.top + 130),
};
});
await setBoost(browser, { accent: 0 });
const baseline = await pixelAt(browser, point.x, point.y);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const boosted = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsDiffer(baseline, boosted, 3),
`::placeholder must tint with boost; baseline=${JSON.stringify(
baseline
)} boosted=${JSON.stringify(boosted)}`
);
});
});

View File

@@ -0,0 +1,59 @@
/* 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";
// Generated content (::before/::after) lives in a native-anonymous subtree,
// so IsBoostExemptFrame has historically over-exempted it. This test pins the
// fix: an inline-block ::before with a solid background-color must take the
// page's tint just like a regular element.
add_task(async function pseudo_before_is_boosted() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#host::before {
content: "";
display: inline-block;
width: 200px;
height: 200px;
background-color: rgb(120, 120, 120);
}
</style>
<div id="host"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
// ::before sits inside the host: sample inside its area (start of host).
const point = await SpecialPowers.spawn(browser, [], () => {
const r = content.document.querySelector("#host").getBoundingClientRect();
return {
x: Math.round(r.left + 100),
y: Math.round(r.top + 100),
};
});
await setBoost(browser, { accent: 0 });
const baseline = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsClose(baseline, { r: 120, g: 120, b: 120 }, 4),
`baseline ::before colour ≈ rgb(120,120,120); got ${JSON.stringify(
baseline
)}`
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const boosted = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsDiffer(baseline, boosted, 3),
`::before background must tint with the boost; baseline=${JSON.stringify(
baseline
)} boosted=${JSON.stringify(boosted)}`
);
});
});

View File

@@ -0,0 +1,62 @@
/* 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";
// box-shadow is the property where the "alpha byte is contrast for accents but
// must stay opacity for content colours" invariant is most visible. We render
// a thick, fully-opaque box-shadow on a white background and verify (a) it's
// tinted by the boost and (b) the sampled pixel's alpha — after compositing —
// is not the accent's contrast byte bleeding through.
add_task(async function box_shadow_is_tinted_alpha_preserved() {
// Use a solid (alpha = 1.0) shadow colour so we can sample inside the shadow
// band on a white background without dealing with partial transparency.
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#s { width: 100px; height: 100px; margin: 80px; background: white;
box-shadow: 0 80px 0 0 rgb(80, 80, 80); }
</style>
<div id="s"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
const point = await SpecialPowers.spawn(browser, [], () => {
const r = content.document.querySelector("#s").getBoundingClientRect();
// 40px inside the 80px tall shadow band that lives below the box.
return {
x: Math.round(r.left + r.width / 2),
y: Math.round(r.bottom + 40),
};
});
await setBoost(browser, { accent: 0 });
const baseline = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsClose(baseline, { r: 80, g: 80, b: 80 }, 5),
`baseline shadow ≈ rgb(80,80,80); got ${JSON.stringify(baseline)}`
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const boosted = await pixelAt(browser, point.x, point.y);
Assert.ok(
pixelsDiffer(baseline, boosted, 3),
`box-shadow colour should be tinted; baseline=${JSON.stringify(
baseline
)} boosted=${JSON.stringify(boosted)}`
);
// The compositor combines RGB only; the rendered pixel from drawWindow is
// always alpha=255 because the canvas backing is opaque. The real
// alpha-preservation invariant is enforced as a gtest on the filter
// primitive (TestZenBoostsColorFilter.ShadowAlphaPreserved). Here we just
// assert the visible pixel isn't pathological (e.g., turned transparent).
Assert.equal(boosted.a, 255, "composited shadow pixel has full alpha");
});
});

View File

@@ -0,0 +1,62 @@
/* 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";
// `background-image: url(*.svg)` goes through nsImageRenderer's WebRender
// blob path, which is a separate code path from the <img> case. Verify that
// path also has the boost propagated: a div whose background is an SVG image
// must tint just like a div with a CSS background-color of the same value.
add_task(async function svg_background_image_is_boosted() {
const svgSrc = encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">` +
`<rect width="200" height="200" fill="rgb(120, 120, 120)"/>` +
`</svg>`
);
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#css, #bg { width: 200px; height: 200px; display: inline-block;
vertical-align: top; }
#css { background-color: rgb(120, 120, 120); }
#bg { background-image: url('data:image/svg+xml;charset=utf-8,${svgSrc}');
background-size: 200px 200px; background-repeat: no-repeat; }
</style>
<div id="css"></div>
<div id="bg"></div>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
for (let i = 0; i < 4; i++) {
await waitForRepaint(browser);
}
await setBoost(browser, { accent: 0 });
const cssBaseline = await pixelInElement(browser, "#css");
const bgBaseline = await pixelInElement(browser, "#bg");
Assert.ok(
pixelsClose(cssBaseline, bgBaseline, 3),
`baseline mismatch: css=${JSON.stringify(
cssBaseline
)} bg-image=${JSON.stringify(bgBaseline)}`
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const cssBoosted = await pixelInElement(browser, "#css");
const bgBoosted = await pixelInElement(browser, "#bg");
Assert.ok(
pixelsDiffer(bgBaseline, bgBoosted, 3),
"background-image SVG must tint under boost"
);
Assert.ok(
pixelsClose(cssBoosted, bgBoosted, 4),
`SVG background-image must match CSS background-color after boost. ` +
`css=${JSON.stringify(cssBoosted)} bg=${JSON.stringify(bgBoosted)}`
);
});
});

View File

@@ -0,0 +1,63 @@
/* 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";
// SVG used as an image (<img src=*.svg>) renders inside its own image document
// with no BrowsingContext, so the boost must be propagated through the
// SVGImageContext + AutoRestoreSVGState plumbing onto the image document's
// PresContext. Compare the painted colour of an <img>-rendered SVG to an
// inline <svg> with the same fill — both should land at the same boosted
// colour after one pass.
add_task(async function svg_as_img_matches_inline_under_boost() {
const svgSrc = encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="200" height="200">` +
`<rect width="200" height="200" fill="rgb(120, 120, 120)"/>` +
`</svg>`
);
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#img, #inline { width: 200px; height: 200px; display: inline-block;
vertical-align: top; }
</style>
<img id="img" src="data:image/svg+xml;charset=utf-8,${svgSrc}">
<svg id="inline" width="200" height="200" viewBox="0 0 200 200">
<rect width="200" height="200" fill="rgb(120, 120, 120)"/>
</svg>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
// SVG images are loaded async; give the <img> a few frames to paint.
for (let i = 0; i < 4; i++) {
await waitForRepaint(browser);
}
await setBoost(browser, { accent: 0 });
const imgBaseline = await pixelInElement(browser, "#img");
const inlineBaseline = await pixelInElement(browser, "#inline");
Assert.ok(
pixelsClose(imgBaseline, inlineBaseline, 3),
`baseline mismatch between <img>-svg and inline svg: img=` +
`${JSON.stringify(imgBaseline)} inline=${JSON.stringify(inlineBaseline)}`
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const imgBoosted = await pixelInElement(browser, "#img");
const inlineBoosted = await pixelInElement(browser, "#inline");
Assert.ok(
pixelsDiffer(imgBaseline, imgBoosted, 3),
"<img>-rendered SVG must tint under boost"
);
Assert.ok(
pixelsClose(imgBoosted, inlineBoosted, 4),
`<img>-rendered SVG must match inline SVG after boost. img=` +
`${JSON.stringify(imgBoosted)} inline=${JSON.stringify(inlineBoosted)}`
);
});
});

View File

@@ -0,0 +1,71 @@
/* 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";
// SVG paint-server gradients (<linearGradient>) go through SVGGradientFrame
// which threads the host frame into ToDeviceColor for each stop. Coverage
// here pins that threading: a paint-server gradient must tint stops the same
// way a CSS gradient does.
add_task(async function svg_linear_gradient_stops_are_boosted() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
</style>
<svg id="g" width="400" height="200" viewBox="0 0 400 200">
<defs>
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stop-color="rgb(180, 60, 60)"/>
<stop offset="100%" stop-color="rgb(60, 60, 180)"/>
</linearGradient>
</defs>
<rect width="400" height="200" fill="url(#grad)"/>
</svg>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
const points = await SpecialPowers.spawn(browser, [], () => {
const r = content.document.querySelector("#g").getBoundingClientRect();
return {
left: {
x: Math.round(r.left + r.width * 0.05),
y: Math.round(r.top + r.height / 2),
},
right: {
x: Math.round(r.left + r.width * 0.95),
y: Math.round(r.top + r.height / 2),
},
};
});
await setBoost(browser, { accent: 0 });
const leftBaseline = await pixelAt(browser, points.left.x, points.left.y);
const rightBaseline = await pixelAt(
browser,
points.right.x,
points.right.y
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const leftBoosted = await pixelAt(browser, points.left.x, points.left.y);
const rightBoosted = await pixelAt(browser, points.right.x, points.right.y);
Assert.ok(
pixelsDiffer(leftBaseline, leftBoosted, 3),
"SVG <linearGradient> first stop must tint"
);
Assert.ok(
pixelsDiffer(rightBaseline, rightBoosted, 3),
"SVG <linearGradient> last stop must tint"
);
Assert.ok(
pixelsDiffer(leftBoosted, rightBoosted, 8),
"SVG <linearGradient> stops must stay distinguishable after boost"
);
});
});

View File

@@ -0,0 +1,61 @@
/* 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";
// Inline <use href="#symbol"> clones the symbol's content into a shadow tree.
// The clone is native-anonymous, so IsBoostExemptFrame must NOT exempt it
// (that's what the GetContainingShadow carve-out is for). Compare against a
// direct inline rect with the same fill — both must land at the same boosted
// colour, demonstrating the use-clone isn't being skipped.
add_task(async function svg_use_clone_is_boosted_like_direct_inline() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
.swatch { width: 200px; height: 200px; display: inline-block;
vertical-align: top; }
</style>
<svg width="0" height="0" style="position:absolute">
<symbol id="sym" viewBox="0 0 200 200">
<rect width="200" height="200" fill="rgb(120, 120, 120)"/>
</symbol>
</svg>
<svg id="direct" class="swatch" viewBox="0 0 200 200">
<rect width="200" height="200" fill="rgb(120, 120, 120)"/>
</svg>
<svg id="used" class="swatch" viewBox="0 0 200 200">
<use href="#sym"/>
</svg>`;
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
await setBoost(browser, { accent: 0 });
const directBaseline = await pixelInElement(browser, "#direct");
const usedBaseline = await pixelInElement(browser, "#used");
Assert.ok(
pixelsClose(directBaseline, usedBaseline, 3),
`baseline mismatch between direct and used; direct=${JSON.stringify(
directBaseline
)} used=${JSON.stringify(usedBaseline)}`
);
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const directBoosted = await pixelInElement(browser, "#direct");
const usedBoosted = await pixelInElement(browser, "#used");
Assert.ok(
pixelsDiffer(usedBaseline, usedBoosted, 3),
"<use>-cloned content must tint under boost (use-shadow not exempt)"
);
Assert.ok(
pixelsClose(directBoosted, usedBoosted, 4),
`<use> clone must match direct inline rect after boost. direct=` +
`${JSON.stringify(directBoosted)} used=${JSON.stringify(usedBoosted)}`
);
});
});

View File

@@ -0,0 +1,50 @@
/* 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";
// Verifies that text colour is tinted under a boost. Uses a large solid-colour
// glyph and samples at its geometric centre, where the rendered pixel is fully
// the foreground colour (no anti-aliased blend with the background).
add_task(async function text_color_is_tinted() {
const html = `
<style>
html, body { margin: 0; padding: 0; background: white; }
#t { color: rgb(40, 44, 52); font: 200px/1 system-ui, sans-serif;
display: inline-block; padding: 0 20px; }
</style>
<span id="t">█</span>`;
// Full-block U+2588 fills its glyph cell with the foreground colour, so the
// centre pixel is a clean sample of the painted text colour.
await BrowserTestUtils.withNewTab(dataUrl(html), async browser => {
await waitForRepaint(browser);
await setBoost(browser, { accent: 0 });
const baseline = await pixelInElement(browser, "#t");
await setBoost(browser, {
accent: PAGE_ACCENT,
complementaryRotation: PAGE_COMPLEMENTARY_ROTATION,
});
const boosted = await pixelInElement(browser, "#t");
Assert.ok(
pixelsDiffer(baseline, boosted, 3),
`text colour should be tinted; baseline=${JSON.stringify(baseline)} ` +
`boosted=${JSON.stringify(boosted)}`
);
// The text was clearly darker than white before the boost, and the boost
// preserves perceived luminance roughly, so it must stay darker than its
// (white) background afterwards. A broken filter that inverts the
// luminance direction would flip this.
const bg = await pixelAt(browser, 5, 5);
Assert.greater(
pxLuma(bg),
pxLuma(boosted),
"boosted text must remain darker than its boosted white background"
);
});
});

View File

@@ -5,3 +5,108 @@
const { SelectorComponent } = ChromeUtils.importESModule(
"resource:///modules/zen/boosts/ZenSelectorComponent.sys.mjs"
);
// --- Boost pixel-level test helpers --------------------------------------
//
// Used by browser_boosts_*.js. Each helper documents what regression in the
// boost paint paths it's meant to catch.
// Construct an nscolor in Firefox's ABGR encoding from RGB + the alpha byte.
// The boost backend reuses the alpha byte as the accent's contrast/strength
// (see NS_GET_CONTRAST in nsZenBoostsBackend.cpp), so for boost activation
// use `contrast` as the fourth arg with a typical value of 200.
function nsRGBA(r, g, b, a = 255) {
return (((a >>> 0) << 24) | (b << 16) | (g << 8) | r) >>> 0;
}
// Two animation frames is enough for a BC-field-triggered restyle + repaint
// to settle in our tests; the DidSet handlers in nsZenBCOverrides.cpp
// dispatch a RecascadeSubtree + visual hint that's processed by the next
// refresh tick.
async function waitForRepaint(browser) {
await SpecialPowers.spawn(browser, [], async () => {
await new Promise(r => content.requestAnimationFrame(r));
await new Promise(r => content.requestAnimationFrame(r));
});
}
// Apply (or clear) a boost on the tab's top-level content BrowsingContext.
// Passing accent = 0 clears the boost so a test can sample a no-boost
// baseline and a boosted state on the same loaded page.
async function setBoost(
browser,
{ accent = 0, complementaryRotation = 0, inverted = false } = {}
) {
const bc = browser.browsingContext;
bc.zenBoostsData = accent;
bc.zenBoostsComplementaryRotation = complementaryRotation;
bc.isZenBoostsInverted = inverted;
await waitForRepaint(browser);
}
// Read the RGBA pixel at content coordinates (x, y). Runs in the content
// process so drawWindow targets the real painted output of the tab.
async function pixelAt(browser, x, y) {
return SpecialPowers.spawn(browser, [x, y], async (px, py) => {
const w = content.innerWidth;
const h = content.innerHeight;
const canvas = content.document.createElementNS(
"http://www.w3.org/1999/xhtml",
"canvas"
);
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext("2d");
ctx.drawWindow(content, 0, 0, w, h, "rgba(0,0,0,0)");
const data = ctx.getImageData(px, py, 1, 1).data;
return { r: data[0], g: data[1], b: data[2], a: data[3] };
});
}
// Sample the centre of the element matching |selector|.
async function pixelInElement(browser, selector) {
const point = await SpecialPowers.spawn(browser, [selector], sel => {
const el = content.document.querySelector(sel);
if (!el) {
throw new Error(`No element matches selector: ${sel}`);
}
const r = el.getBoundingClientRect();
return {
x: Math.round(r.left + r.width / 2),
y: Math.round(r.top + r.height / 2),
};
});
return pixelAt(browser, point.x, point.y);
}
// Coarse RGB-distance threshold for "the colour clearly changed". The boost's
// duotone moves channels by tens of units even for a modest accent; tolerance
// 3 is comfortably below that while ignoring sub-pixel/anti-aliasing noise.
function pixelsDiffer(a, b, tol = 3) {
return (
Math.abs(a.r - b.r) > tol ||
Math.abs(a.g - b.g) > tol ||
Math.abs(a.b - b.b) > tol
);
}
function pixelsClose(a, b, tol = 3) {
return !pixelsDiffer(a, b, tol);
}
// BT.601-ish perceived luminance, integer-valued. Matches the coefficients
// used by InvertColorChannel in the backend, so a test expressing "X stays
// darker than Y after boost" maps to what the user actually perceives.
function pxLuma({ r, g, b }) {
return (r * 54 + g * 183 + b * 19) >> 8;
}
function dataUrl(html) {
return "data:text/html;charset=utf-8," + encodeURIComponent(html);
}
// A "page accent" colour used across the property tests. Strong enough to
// move a mid-grey by tens of units per channel; rotation kept small so the
// duotone stays cohesive.
const PAGE_ACCENT = nsRGBA(80, 120, 200, /*contrast*/ 200);
const PAGE_COMPLEMENTARY_ROTATION = 30;