From 45075e2fbc3ee94c10522b62e6e8bc9afd5062ca Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mr-cheffy@users.noreply.github.com> Date: Mon, 1 Jun 2026 11:24:45 +0200 Subject: [PATCH] gh-13439: Add tests coverage for boosts (gh-13977) --- package.json | 2 +- .../boosts/gtest/TestZenBoostsAccentCache.cpp | 104 +++++++++++++++++ .../gtest/TestZenBoostsResolveStyleColor.cpp | 42 +++++++ src/zen/boosts/gtest/moz.build | 2 + src/zen/boosts/nsZenBoostsBackend.cpp | 30 ++++- src/zen/boosts/nsZenBoostsBackend.h | 11 +- src/zen/tests/boosts/browser.toml | 35 ++++++ .../tests/boosts/browser_boosts_animation.js | 52 +++++++++ .../tests/boosts/browser_boosts_background.js | 50 +++++++++ src/zen/tests/boosts/browser_boosts_border.js | 53 +++++++++ .../tests/boosts/browser_boosts_gradient.js | 83 ++++++++++++++ .../tests/boosts/browser_boosts_inline_svg.js | 106 ++++++++++++++++++ .../tests/boosts/browser_boosts_input_text.js | 59 ++++++++++ src/zen/tests/boosts/browser_boosts_invert.js | 46 ++++++++ .../tests/boosts/browser_boosts_outline.js | 54 +++++++++ .../boosts/browser_boosts_placeholder.js | 57 ++++++++++ .../boosts/browser_boosts_pseudo_before.js | 59 ++++++++++ src/zen/tests/boosts/browser_boosts_shadow.js | 62 ++++++++++ .../browser_boosts_svg_background_image.js | 62 ++++++++++ .../tests/boosts/browser_boosts_svg_image.js | 63 +++++++++++ .../browser_boosts_svg_linear_gradient.js | 71 ++++++++++++ .../boosts/browser_boosts_svg_use_sprite.js | 61 ++++++++++ .../tests/boosts/browser_boosts_text_color.js | 50 +++++++++ src/zen/tests/boosts/head.js | 105 +++++++++++++++++ 24 files changed, 1312 insertions(+), 7 deletions(-) create mode 100644 src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp create mode 100644 src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp create mode 100644 src/zen/tests/boosts/browser_boosts_animation.js create mode 100644 src/zen/tests/boosts/browser_boosts_background.js create mode 100644 src/zen/tests/boosts/browser_boosts_border.js create mode 100644 src/zen/tests/boosts/browser_boosts_gradient.js create mode 100644 src/zen/tests/boosts/browser_boosts_inline_svg.js create mode 100644 src/zen/tests/boosts/browser_boosts_input_text.js create mode 100644 src/zen/tests/boosts/browser_boosts_invert.js create mode 100644 src/zen/tests/boosts/browser_boosts_outline.js create mode 100644 src/zen/tests/boosts/browser_boosts_placeholder.js create mode 100644 src/zen/tests/boosts/browser_boosts_pseudo_before.js create mode 100644 src/zen/tests/boosts/browser_boosts_shadow.js create mode 100644 src/zen/tests/boosts/browser_boosts_svg_background_image.js create mode 100644 src/zen/tests/boosts/browser_boosts_svg_image.js create mode 100644 src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js create mode 100644 src/zen/tests/boosts/browser_boosts_svg_use_sprite.js create mode 100644 src/zen/tests/boosts/browser_boosts_text_color.js diff --git a/package.json b/package.json index 6e7fd8fd0..c62853c08 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp b/src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp new file mode 100644 index 000000000..39896f863 --- /dev/null +++ b/src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp @@ -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)); +} diff --git a/src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp b/src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp new file mode 100644 index 000000000..625bda981 --- /dev/null +++ b/src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp @@ -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); + } +} diff --git a/src/zen/boosts/gtest/moz.build b/src/zen/boosts/gtest/moz.build index cc14100b7..0d375c734 100644 --- a/src/zen/boosts/gtest/moz.build +++ b/src/zen/boosts/gtest/moz.build @@ -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" diff --git a/src/zen/boosts/nsZenBoostsBackend.cpp b/src/zen/boosts/nsZenBoostsBackend.cpp index 6a64cd724..9efc6acef 100644 --- a/src/zen/boosts/nsZenBoostsBackend.cpp +++ b/src/zen/boosts/nsZenBoostsBackend.cpp @@ -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 sZenBoostsBackend; diff --git a/src/zen/boosts/nsZenBoostsBackend.h b/src/zen/boosts/nsZenBoostsBackend.h index 1d2073372..2905dae5e 100644 --- a/src/zen/boosts/nsZenBoostsBackend.h +++ b/src/zen/boosts/nsZenBoostsBackend.h @@ -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: diff --git a/src/zen/tests/boosts/browser.toml b/src/zen/tests/boosts/browser.toml index 9cacfe47f..a514586e6 100644 --- a/src/zen/tests/boosts/browser.toml +++ b/src/zen/tests/boosts/browser.toml @@ -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"] diff --git a/src/zen/tests/boosts/browser_boosts_animation.js b/src/zen/tests/boosts/browser_boosts_animation.js new file mode 100644 index 000000000..f723e1094 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_animation.js @@ -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 = ` + +
+
`; + + 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 + )}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_background.js b/src/zen/tests/boosts/browser_boosts_background.js new file mode 100644 index 000000000..9f62f3d96 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_background.js @@ -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 = ` + +
`; + + 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)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_border.js b/src/zen/tests/boosts/browser_boosts_border.js new file mode 100644 index 000000000..ffe4ba1ed --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_border.js @@ -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 = ` + +
`; + + 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)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_gradient.js b/src/zen/tests/boosts/browser_boosts_gradient.js new file mode 100644 index 000000000..b2167b14b --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_gradient.js @@ -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 = ` + +
`; + + 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)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_inline_svg.js b/src/zen/tests/boosts/browser_boosts_inline_svg.js new file mode 100644 index 000000000..afe3ae655 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_inline_svg.js @@ -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 = ` + +
+ + + `; + + 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 = ` + +
+
+ + + +
`; + + 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 + )}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_input_text.js b/src/zen/tests/boosts/browser_boosts_input_text.js new file mode 100644 index 000000000..454e0722b --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_input_text.js @@ -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 / `; + // 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)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_invert.js b/src/zen/tests/boosts/browser_boosts_invert.js new file mode 100644 index 000000000..ae74bd99c --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_invert.js @@ -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 = ` + +
+
`; + + 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)" + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_outline.js b/src/zen/tests/boosts/browser_boosts_outline.js new file mode 100644 index 000000000..3e8710c34 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_outline.js @@ -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 = ` + +
`; + + 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)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_placeholder.js b/src/zen/tests/boosts/browser_boosts_placeholder.js new file mode 100644 index 000000000..39a5b0a1e --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_placeholder.js @@ -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 = ` + + `; + + 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)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_pseudo_before.js b/src/zen/tests/boosts/browser_boosts_pseudo_before.js new file mode 100644 index 000000000..fa67c95de --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_pseudo_before.js @@ -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 = ` + +
`; + + 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)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_shadow.js b/src/zen/tests/boosts/browser_boosts_shadow.js new file mode 100644 index 000000000..e7fdc0246 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_shadow.js @@ -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 = ` + +
`; + + 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"); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_svg_background_image.js b/src/zen/tests/boosts/browser_boosts_svg_background_image.js new file mode 100644 index 000000000..743674508 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_svg_background_image.js @@ -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 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( + `` + + `` + + `` + ); + const html = ` + +
+
`; + + 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)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_svg_image.js b/src/zen/tests/boosts/browser_boosts_svg_image.js new file mode 100644 index 000000000..09516b4d0 --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_svg_image.js @@ -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 () 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 -rendered SVG to an +// inline 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( + `` + + `` + + `` + ); + const html = ` + + + + + `; + + await BrowserTestUtils.withNewTab(dataUrl(html), async browser => { + await waitForRepaint(browser); + // SVG images are loaded async; give the 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 -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), + "-rendered SVG must tint under boost" + ); + Assert.ok( + pixelsClose(imgBoosted, inlineBoosted, 4), + `-rendered SVG must match inline SVG after boost. img=` + + `${JSON.stringify(imgBoosted)} inline=${JSON.stringify(inlineBoosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js b/src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js new file mode 100644 index 000000000..dea6b29fe --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js @@ -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 () 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 = ` + + + + + + + + + + `; + + 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 first stop must tint" + ); + Assert.ok( + pixelsDiffer(rightBaseline, rightBoosted, 3), + "SVG last stop must tint" + ); + Assert.ok( + pixelsDiffer(leftBoosted, rightBoosted, 8), + "SVG stops must stay distinguishable after boost" + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_svg_use_sprite.js b/src/zen/tests/boosts/browser_boosts_svg_use_sprite.js new file mode 100644 index 000000000..3e7fb9e4a --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_svg_use_sprite.js @@ -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 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 = ` + + + + + + + + + + + + `; + + 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), + "-cloned content must tint under boost (use-shadow not exempt)" + ); + Assert.ok( + pixelsClose(directBoosted, usedBoosted, 4), + ` clone must match direct inline rect after boost. direct=` + + `${JSON.stringify(directBoosted)} used=${JSON.stringify(usedBoosted)}` + ); + }); +}); diff --git a/src/zen/tests/boosts/browser_boosts_text_color.js b/src/zen/tests/boosts/browser_boosts_text_color.js new file mode 100644 index 000000000..3c1de02bd --- /dev/null +++ b/src/zen/tests/boosts/browser_boosts_text_color.js @@ -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 = ` + + `; + // 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" + ); + }); +}); diff --git a/src/zen/tests/boosts/head.js b/src/zen/tests/boosts/head.js index 7514c6dea..011447b43 100644 --- a/src/zen/tests/boosts/head.js +++ b/src/zen/tests/boosts/head.js @@ -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;