mirror of
https://github.com/zen-browser/desktop.git
synced 2026-06-13 15:03:41 +00:00
gh-13439: Add tests coverage for boosts (gh-13977)
This commit is contained in:
@@ -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",
|
||||
|
||||
104
src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp
Normal file
104
src/zen/boosts/gtest/TestZenBoostsAccentCache.cpp
Normal 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));
|
||||
}
|
||||
42
src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp
Normal file
42
src/zen/boosts/gtest/TestZenBoostsResolveStyleColor.cpp
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"]
|
||||
|
||||
52
src/zen/tests/boosts/browser_boosts_animation.js
Normal file
52
src/zen/tests/boosts/browser_boosts_animation.js
Normal 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
|
||||
)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
50
src/zen/tests/boosts/browser_boosts_background.js
Normal file
50
src/zen/tests/boosts/browser_boosts_background.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
53
src/zen/tests/boosts/browser_boosts_border.js
Normal file
53
src/zen/tests/boosts/browser_boosts_border.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
83
src/zen/tests/boosts/browser_boosts_gradient.js
Normal file
83
src/zen/tests/boosts/browser_boosts_gradient.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
106
src/zen/tests/boosts/browser_boosts_inline_svg.js
Normal file
106
src/zen/tests/boosts/browser_boosts_inline_svg.js
Normal 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
|
||||
)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
59
src/zen/tests/boosts/browser_boosts_input_text.js
Normal file
59
src/zen/tests/boosts/browser_boosts_input_text.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
46
src/zen/tests/boosts/browser_boosts_invert.js
Normal file
46
src/zen/tests/boosts/browser_boosts_invert.js
Normal 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)"
|
||||
);
|
||||
});
|
||||
});
|
||||
54
src/zen/tests/boosts/browser_boosts_outline.js
Normal file
54
src/zen/tests/boosts/browser_boosts_outline.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
57
src/zen/tests/boosts/browser_boosts_placeholder.js
Normal file
57
src/zen/tests/boosts/browser_boosts_placeholder.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
59
src/zen/tests/boosts/browser_boosts_pseudo_before.js
Normal file
59
src/zen/tests/boosts/browser_boosts_pseudo_before.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
62
src/zen/tests/boosts/browser_boosts_shadow.js
Normal file
62
src/zen/tests/boosts/browser_boosts_shadow.js
Normal 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");
|
||||
});
|
||||
});
|
||||
62
src/zen/tests/boosts/browser_boosts_svg_background_image.js
Normal file
62
src/zen/tests/boosts/browser_boosts_svg_background_image.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
63
src/zen/tests/boosts/browser_boosts_svg_image.js
Normal file
63
src/zen/tests/boosts/browser_boosts_svg_image.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
71
src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js
Normal file
71
src/zen/tests/boosts/browser_boosts_svg_linear_gradient.js
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
61
src/zen/tests/boosts/browser_boosts_svg_use_sprite.js
Normal file
61
src/zen/tests/boosts/browser_boosts_svg_use_sprite.js
Normal 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)}`
|
||||
);
|
||||
});
|
||||
});
|
||||
50
src/zen/tests/boosts/browser_boosts_text_color.js
Normal file
50
src/zen/tests/boosts/browser_boosts_text_color.js
Normal 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"
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user