Bug 1910297. Handle repaint propagation to the root if the root is a table. r=layout-reviewers,emilio

The background image is associate to the table frame, but the table wrapper frame is the primary frame, thus IsPrimaryFrameOfRootOrBodyElement returns false and we don't propagate to the root when we should. So I changed IsPrimaryFrameOfRootOrBodyElement to handle this case and renamed it. I checked the other root element frame types, only table frames had this issue.

Differential Revision: https://phabricator.services.mozilla.com/D217923
This commit is contained in:
Timothy Nikkel 2024-08-09 10:25:21 +00:00
parent 8d68bd2063
commit 2398448e0b
14 changed files with 397 additions and 132 deletions

View File

@ -0,0 +1,144 @@
// This is shared by image/test/mochitest/test_animated_css_image.html
// and image/test/browser/browser_animated_css_image.js
// Make sure any referenced files/images exist in both of those directories.
const kTests = [
// Sanity test: background-image on a regular element.
{
html: `
<!doctype html>
<style>
div {
width: 100px;
height: 100px;
background-image: url(animated1.gif);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1627585: content: url()
{
html: `
<!doctype html>
<style>
div::before {
content: url(animated1.gif);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1627585: content: url() (on an element directly)
{
html: `
<!doctype html>
<style>
div {
content: url(animated1.gif);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1625571: background propagated to canvas.
{
html: `
<!doctype html>
<style>
body {
background-image: url(animated1.gif);
}
</style>
`,
element(doc) {
return doc.documentElement;
},
},
// bug 1910297: background propagated to canvas with display: table
{
html: `
<!doctype html>
<style>
html {
display: table;
background-image: url(animated1.gif);
}
</style>
`,
element(doc) {
return doc.documentElement;
},
},
// bug 1719375: CSS animation in SVG image.
{
html: `
<!doctype html>
<style>
div {
width: 100px;
height: 100px;
background-image: url(animated1.svg);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1730834: stopped window.
{
html: `
<!doctype html>
<style>
div {
width: 100px;
height: 100px;
}
</style>
<body onload="window.stop(); document.querySelector('div').style.backgroundImage = 'url(animated1.gif)';">
<div></div>
</body>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1731138: Animated mask
{
html: `
<!doctype html>
<style>
div {
width: 100px;
height: 100px;
background-color: lime;
mask-clip: border-box;
mask-size: 100% 100%;
mask-image: url(animatedMask.gif);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
];

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -0,0 +1,12 @@
<svg xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
style="animation: colorAnim 1s steps(2) infinite alternate"
width="40" height="40">
<style>
@keyframes colorAnim {
from { background-color: green }
to { background-color: blue }
}
</style>
</svg>

After

Width:  |  Height:  |  Size: 319 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View File

@ -28,3 +28,14 @@ skip-if = ["true"] # Bug 987616
["browser_offscreen_image_in_out_of_process_iframe.js"]
https_first_disabled = true
support-files = ["empty.html"]
["browser_animated_css_image.js"]
support-files = [
"!/gfx/layers/apz/test/mochitest/apz_test_utils.js",
"!/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
"animated1.gif",
"animated1.svg",
"animatedMask.gif",
"helper_animated_css_image.html",
"../animated_image_test_list.js",
]

View File

@ -0,0 +1,188 @@
/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set sts=2 sw=2 et tw=80: */
/*
* This test duplicates image/test/mochitest/test_animated_css_image.html, so keep them in sync.
* This is because we need a browser-chrome test in order to test invalidation (the getSnapshot method here
* uses the same path as painting to the screen, whereas test_animated_css_image.html is doing a
* separate paint to a surface), but browser-chrome isn't run on android, so test_animated_css_image.html
* gets us android coverage.
*/
/* This test is based on
https://searchfox.org/mozilla-central/rev/25d26b0a62cc5bb4aa3bb90a11f3b0b7c52859c4/gfx/layers/apz/test/mochitest/browser_test_position_sticky.js
*/
"use strict";
requestLongerTimeout(2);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_utils.js",
this
);
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/gfx/layers/apz/test/mochitest/apz_test_native_event_utils.js",
this
);
// this contains the kTests array
Services.scriptloader.loadSubScript(
"chrome://mochitests/content/browser/image/test/browser/animated_image_test_list.js",
this
);
async function assertAnimates(thehtml) {
function httpURL(sfilename) {
let chromeURL = getRootDirectory(gTestPath) + sfilename;
return chromeURL.replace(
"chrome://mochitests/content/",
"http://mochi.test:8888/"
);
}
const url = httpURL("helper_animated_css_image.html");
const tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
const { rect } = await SpecialPowers.spawn(
tab.linkedBrowser,
[],
async () => {
let rect = content.document.documentElement.getBoundingClientRect();
rect.x += content.window.mozInnerScreenX;
rect.y += content.window.mozInnerScreenY;
return {
rect,
};
}
);
let blankSnapshot = await getSnapshot({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
});
const kNumRetries = 600;
info("testing: " + thehtml);
await SpecialPowers.spawn(tab.linkedBrowser, [thehtml], async thehtml => {
const theiframe = content.document.getElementById("iframe");
let load = new Promise(resolve => {
theiframe.addEventListener("load", resolve, { once: true });
});
theiframe.srcdoc = thehtml;
await load;
// give time for content/test load handlers to run before we do anything
await new Promise(resolve => {
content.window.requestAnimationFrame(() => {
content.window.requestAnimationFrame(resolve);
});
});
// make sure we are flushed and rendered.
content.document.documentElement.getBoundingClientRect();
await new Promise(resolve => {
content.window.requestAnimationFrame(() => {
content.window.requestAnimationFrame(resolve);
});
});
});
let initial = await getSnapshot({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
});
{
// One test (bug 1730834) loads an image as the background of a div in the
// load handler, so there's no good way to wait for it to be loaded and
// rendered except to poll.
let equal = initial == blankSnapshot;
for (let i = 0; i < kNumRetries; ++i) {
if (!equal) {
break;
}
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
await new Promise(resolve => {
content.window.requestAnimationFrame(() => {
content.window.requestAnimationFrame(resolve);
});
});
});
initial = await getSnapshot({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
});
equal = initial == blankSnapshot;
}
ok(!equal, "Initial snapshot shouldn't be blank");
}
async function checkFrames() {
let foundDifferent = false;
let foundInitialAgain = false;
for (let i = 0; i < kNumRetries; ++i) {
let current = await getSnapshot({
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
});
let equal = initial == current;
if (!foundDifferent && !equal) {
ok(true, `Found different image after ${i} retries`);
foundDifferent = true;
}
// Ensure that we go back to the initial state (animated1.gif) is an
// infinite gif.
if (foundDifferent && equal) {
ok(true, `Found same image again after ${i} retries`);
foundInitialAgain = true;
break;
}
await SpecialPowers.spawn(tab.linkedBrowser, [], async () => {
await new Promise(resolve => {
content.window.requestAnimationFrame(() => {
content.window.requestAnimationFrame(resolve);
});
});
});
}
ok(
foundDifferent && foundInitialAgain,
`Should've found a different snapshot and then an equal one, after ${kNumRetries} retries`
);
}
for (let j = 0; j < 5; j++) {
await checkFrames();
}
BrowserTestUtils.removeTab(tab);
}
add_task(async () => {
// kTests is defined in the imported animated_image_test_list.js so it can
// be shared between tests.
// eslint-disable-next-line no-undef
for (let { html } of kTests) {
await assertAnimates(html);
}
});

View File

@ -0,0 +1,6 @@
<!doctype html>
<!--
scrolling=no is just paranoia to ensure that we don't get invalidations
due to scrollbars
-->
<iframe scrolling="no" id="iframe"></iframe>

View File

@ -118,6 +118,9 @@ skip-if = [
]
["test_animated_css_image.html"]
support-files = [
"../animated_image_test_list.js"
]
["test_animated_gif.html"]
support-files = ["child.html"]

View File

@ -6,9 +6,20 @@
due to scrollbars
-->
<iframe scrolling="no" id="iframe"></iframe>
<!-- this contains the kTests array -->
<script src="animated_image_test_list.js"></script>
<script>
SimpleTest.waitForExplicitFinish();
/*
* This test duplicates image/test/browser/browser_animated_css_image.js, so keep them in sync.
* This is because we need a browser-chrome test in order to test invalidation (the getSnapshot method there
* uses the same path as painting to the screen, whereas here we are doing a
* separate paint to a surface), but browser-chrome isn't run on android, so test_animated_css_image.html
* gets us android coverage.
*/
// We hit an optimized path in WebRender that doesn't cause a repaint on the
// main thread:
//
@ -86,135 +97,13 @@ async function assertAnimates(html, getExpectedRepaintedElement) {
ok(foundDifferent && foundInitialAgain, `Should've found a different snapshot and then an equal one, after ${kNumRetries} retries`);
}
const kTests = [
// Sanity test: background-image on a regular element.
{
html: `
<!doctype html>
<style>
div {
width: 100px;
height: 100px;
background-image: url(animated1.gif);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1627585: content: url()
{
html: `
<!doctype html>
<style>
div::before {
content: url(animated1.gif);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1627585: content: url() (on an element directly)
{
html: `
<!doctype html>
<style>
div {
content: url(animated1.gif);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1625571: background propagated to canvas.
{
html: `
<!doctype html>
<style>
body {
background-image: url(animated1.gif);
}
</style>
`,
element(doc) {
return doc.documentElement;
},
},
// bug 1719375: CSS animation in SVG image.
{
html: `
<!doctype html>
<style>
div {
width: 100px;
height: 100px;
background-image: url(animated1.svg);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1730834: stopped window.
{
html: `
<!doctype html>
<style>
div {
width: 100px;
height: 100px;
}
</style>
<body onload="window.stop(); document.querySelector('div').style.backgroundImage = 'url(animated1.gif)';">
<div></div>
</body>
`,
element(doc) {
return doc.querySelector("div");
},
},
// bug 1731138: Animated mask
{
html: `
<!doctype html>
<style>
div {
width: 100px;
height: 100px;
background-color: lime;
mask-clip: border-box;
mask-size: 100% 100%;
mask-image: url(animatedMask.gif);
}
</style>
<div></div>
`,
element(doc) {
return doc.querySelector("div");
},
},
];
onload = async function() {
// First snapshot the blank window.
blankSnapshot = await snapshotWindow(iframe.contentWindow);
// kTests is defined in the imported animated_image_test_list.js so it can
// be shared between tests.
// eslint-disable-next-line no-undef
for (let { html, element } of kTests)
await assertAnimates(html, element);

View File

@ -1306,7 +1306,7 @@ static void ApplyRenderingChangeToTree(PresShell* aPresShell, nsIFrame* aFrame,
// the html element, we propagate the repaint change hint to the
// viewport. This is necessary for background and scrollbar colors
// propagation.
if (aFrame->IsPrimaryFrameOfRootOrBodyElement()) {
if (aFrame->ShouldPropagateRepaintsToRoot()) {
nsIFrame* rootFrame = aPresShell->GetRootFrame();
MOZ_ASSERT(rootFrame, "No root frame?");
DoApplyRenderingChangeToTree(rootFrame, nsChangeHint_RepaintFrame);

View File

@ -2686,7 +2686,8 @@ void nsCSSFrameConstructor::SetUpDocElementContainingBlock(
ScrollContainerFrame (if needed)
nsCanvasFrame [abs-cb]
root element frame (nsBlockFrame, SVGOuterSVGFrame,
nsTableWrapperFrame, nsPlaceholderFrame)
nsTableWrapperFrame, nsPlaceholderFrame,
nsFlexContainerFrame, nsGridContainerFrame)
Print presentation, non-XUL
@ -2698,7 +2699,9 @@ void nsCSSFrameConstructor::SetUpDocElementContainingBlock(
nsPageContentFrame [fixed-cb]
nsCanvasFrame [abs-cb]
root element frame (nsBlockFrame, SVGOuterSVGFrame,
nsTableWrapperFrame, nsPlaceholderFrame)
nsTableWrapperFrame, nsPlaceholderFrame,
nsFlexContainerFrame,
nsGridContainerFrame)
Print-preview presentation, non-XUL
@ -2712,7 +2715,9 @@ void nsCSSFrameConstructor::SetUpDocElementContainingBlock(
nsCanvasFrame [abs-cb]
root element frame (nsBlockFrame, SVGOuterSVGFrame,
nsTableWrapperFrame,
nsPlaceholderFrame)
nsPlaceholderFrame,
nsFlexContainerFrame,
nsGridContainerFrame)
Print/print preview of XUL is not supported.
[fixed-cb]: the default containing block for fixed-pos content

View File

@ -578,8 +578,15 @@ static void MaybeScheduleReflowSVGNonDisplayText(nsIFrame* aFrame) {
IntrinsicDirty::FrameAncestorsAndDescendants);
}
bool nsIFrame::IsPrimaryFrameOfRootOrBodyElement() const {
bool nsIFrame::ShouldPropagateRepaintsToRoot() const {
if (!IsPrimaryFrame()) {
// special case for table frames because style images are associated to the
// table frame, but the table wrapper frame is the primary frame
if (IsTableFrame()) {
MOZ_ASSERT(GetParent() && GetParent()->IsTableWrapperFrame());
return GetParent()->ShouldPropagateRepaintsToRoot();
}
return false;
}
nsIContent* content = GetContent();

View File

@ -2468,7 +2468,7 @@ class nsIFrame : public nsQueryFrame {
}
}
bool IsPrimaryFrameOfRootOrBodyElement() const;
bool ShouldPropagateRepaintsToRoot() const;
/**
* @return true if this frame is used as a fieldset's rendered legend.

View File

@ -527,7 +527,7 @@ static void InvalidateImages(nsIFrame* aFrame, imgIRequest* aRequest,
return aFrame->InvalidateFrame();
}
if (aFrame->IsPrimaryFrameOfRootOrBodyElement()) {
if (aFrame->ShouldPropagateRepaintsToRoot()) {
if (auto* canvas = aFrame->PresShell()->GetCanvasFrame()) {
// Try to invalidate the canvas too, in the probable case the background
// was propagated to it.