gecko-dev/layout/generic/test/test_dynamic_reflow_root_di...

639 строки
25 KiB
HTML

<!DOCTYPE HTML>
<html>
<!--
https://bugzilla.mozilla.org/show_bug.cgi?id=1508420
-->
<head>
<meta charset="utf-8">
<title>
Test for Bug 1508420: Cases where a frame isn't allowed to be a dynamic
reflow root
</title>
<script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="/tests/SimpleTest/WindowSnapshot.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body onload="main()">
<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1508420">Mozilla Bug 1508420</a>
<p id="display">
<!-- Here's the iframe that we'll do all of our testing/snapshotting in: -->
<iframe srcdoc="<!DOCTYPE html><body></body>"></iframe>
</p>
<script type="application/javascript">
/** Test for Bug 1508420 **/
/**
* This test exercises various cases where we exclude a frame from being
* flagged as a dynamic reflow root. (We prevent this because we know that
* there are cases where we'd produce incorrect layout if we initiated reflow
* from the frame in question.)
*
* Roughly, the idea in each subtest here is to do the following:
* 1) Set up a scenario with some condition that we think should prevent a
* particular frame from being flagged as a dynamic reflow root.
* 2) Make a dynamic tweak that we expect would result in broken layout, if
* we had allowed the frame in question to be a dynamic reflow root.
* Take a snapshot.
* 3) Force a full reconstruct + reflow of the document's frames (by
* toggling "display:none" on the root element). Take another snapshot.
* 4) Assert that snapshots look the same -- i.e. that our incremental
* reflow didn't produce the wrong layout.
*
* Ideally, every condition in ReflowInput::InitDynamicReflowRoot()
* should have a corresponding subtest here (and the subtest should fail if
* we remove the condition from InitDynamicReflowRoot).
*/
// Styles that are sufficient to make a typical element into a reflow root.
// We apply these styles to "reflow root candidates" throughout this test
// (and then add other styles that should make the candidate ineligible,
// typically).
const gReflowRootCandidateStyles =
"display: flow-root; will-change: transform; width: 10px; height: 10px;";
// Some convenience globals for the document inside the iframe:
// (initialized in 'main' after the iframe gets a chance to load)
// --------------------------------------------------------------
let gFWindow;
let gFDoc;
let gFBody;
// Some utility functions used in each test function:
// --------------------------------------------------
function createStyledDiv(divStyleStr, divInnerText) {
let div = gFDoc.createElement("div");
div.style.cssText = divStyleStr;
if (typeof divInnerText !== "undefined") {
div.innerText = divInnerText;
}
return div;
}
// This function takes an initial snapshot, then a second snapshot after
// invoking the given tweakFunc, and finally a third after forcing the frame
// tree to be reconstructed from scratch. Then it compares the snapshots to
// validate that the tweak did produce a visible change, & that the
// after-tweak rendering looks the same in the last two snapshots.
function tweakAndCompareSnapshots(tweakFunc, descPrefix) {
let snapPreTweak = snapshotWindow(gFWindow, false);
let descPreTweak = descPrefix + "-initial-rendering";
// Now we invoke the tweak (changing the size of some content inside the
// reflow root candidate). If this influences the size of the candidate
// itself, and we fail to do any reflow outside of the candidate because
// we made it a reflow root, then we expect to end up with a broken layout
// due to a parent or sibling not having been resized/repositioned.
// We'll discover that when comparing snapIncReflow against snapFullReflow
// below.
tweakFunc();
let snapIncReflow = snapshotWindow(gFWindow, false);
let descIncReflow = descPrefix + "-after-tweak-inc-reflow";
// Now we trigger a "full" reflow (not incremental), by forcing
// frame reconstruction all the way from the body element. This should
// force us to reflow from the actual document root, even if we have
// promoted any frames to be dynamic reflow roots.
gFBody.style.display = "none";
gFBody.offsetTop; // flush layout
gFBody.style.display = "";
let snapFullReflow = snapshotWindow(gFWindow, false);
let descFullReflow = descPrefix + "-after-tweak-full-reflow";
assertSnapshots(snapIncReflow, snapPreTweak, false, null,
descIncReflow, descPreTweak);
assertSnapshots(snapIncReflow, snapFullReflow, true, null,
descIncReflow, descFullReflow);
}
// Test functions (called from "main"), with a subtest array in most cases:
// ------------------------------------------------------------------------
// Subtests for intrinsic size keywords (and equivalent, e.g. percentages) as
// values for width/height/{min,max}-{width,height} on reflow root candidates:
let intrinsicSizeSubtests = [
{ desc: "width-auto",
candStyle: "width:auto",
},
{ desc: "width-pct",
candStyle: "width:80%",
},
{ desc: "width-calc-pct",
candStyle: "width:calc(10px + 80%)",
},
{ desc: "width-min-content",
candStyle: "width:-moz-min-content; width:min-content;",
},
{ desc: "width-max-content",
candStyle: "width:-moz-max-content; width:max-content;",
},
{ desc: "min-width-min-content",
candStyle: "min-width:-moz-min-content; min-width:min-content;",
},
{ desc: "min-width-max-content",
candStyle: "min-width:-moz-max-content; min-width:max-content;",
},
{ desc: "max-width-min-content",
// Note: hardcoded 'width' here must be larger than what 'inner'
// gets resized to, so that max-width gets a chance to clamp.
candStyle: "width: 50px; \
max-width:-moz-min-content; max-width:min-content;",
},
{ desc: "max-width-max-content",
candStyle: "width: 50px; \
max-width:-moz-max-content; max-width:max-content;",
},
{ desc: "height-auto",
candStyle: "height:auto",
},
{ desc: "height-pct",
candStyle: "height:80%",
},
{ desc: "height-calc-pct",
candStyle: "height:calc(10px + 80%)",
},
{ desc: "height-min-content",
candStyle: "height:-moz-min-content; height:min-content;",
},
{ desc: "height-max-content",
candStyle: "height:-moz-max-content; height:max-content;",
},
{ desc: "min-height-min-content",
candStyle: "min-height:-moz-min-content; min-height:min-content;",
},
{ desc: "min-height-max-content",
candStyle: "min-height:-moz-max-content; min-height:max-content;",
},
{ desc: "max-height-min-content",
// Note: hardcoded 'height' here must be larger than what 'inner'
// gets resized to, so that max-height gets a chance to clamp.
candStyle: "height: 50px; \
max-height:-moz-min-content; max-height:min-content;",
},
{ desc: "max-height-max-content",
candStyle: "height: 50px; \
max-height:-moz-max-content; max-height:max-content;",
},
];
// Intrinsic heights (e.g. 'height:auto') should prevent
// an element from being a reflow root.
function runIntrinsicSizeSubtest(subtest) {
// Run each testcase in horizontal & vertical writing mode:
for (let wmVal of ["horizontal-tb", "vertical-lr"]) {
gFBody.style.writingMode = wmVal;
// Short version of WM, for use in logging for snapshot comparison below:
let wmDesc = (wmVal == "horizontal-tb" ? "-horizWM" : "-vertWM");
// This outer div is intrinsically sized, and it needs to be reflowed
// when the size of its child (the reflow root candidate) changes.
let outer = createStyledDiv("border: 2px solid teal; \
inline-size: -moz-max-content; \
inline-size: max-content");
// The reflow root candidate:
let cand = createStyledDiv(gReflowRootCandidateStyles +
subtest.candStyle);
// Something whose size we can adjust, inside the reflow root candidate:
let inner = createStyledDiv("height:20px; width:20px; \
border: 1px solid purple");
cand.appendChild(inner);
outer.appendChild(cand);
gFBody.appendChild(outer);
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, subtest.desc + wmDesc);
// clean up
outer.remove();
gFBody.style.writingMode = "";
}
}
let flexItemSubtests = [
{ desc: "flex-basis-content",
candStyle: "flex-basis:content;",
},
{ desc: "flex-basis-min-content",
candStyle: "flex-basis:-moz-min-content;flex-basis:min-content;",
},
{ desc: "flex-basis-auto-width-auto",
candStyle: "flex-basis:auto;width:auto;",
},
// For percent flex-basis, we're concerned with cases where the percent
// triggers content-based sizing during the flex container's intrinsic
// sizing step. So we need to get the container to be intrinsically sized;
// hence the use of the (optional) "isContainerIntrinsicallySized" flag.
{ desc: "flex-basis-pct",
candStyle: "flex-basis:80%;",
isContainerIntrinsicallySized: true,
},
{ desc: "flex-basis-from-pct-isize",
candStyle: "inline-size:80%",
isContainerIntrinsicallySized: true,
},
{ desc: "flex-basis-calc-pct",
candStyle: "flex-basis:calc(10px + 80%);",
isContainerIntrinsicallySized: true,
},
{ desc: "flex-basis-from-calc-pct-isize",
candStyle: "inline-size:calc(10px + 80%);",
isContainerIntrinsicallySized: true,
},
// Testing the magic "min-main-size:auto" keyword
// and other intrinsic min/max sizes
{ desc: "flex-min-inline-size-auto",
candStyle: "flex:0 5px;min-inline-size:auto",
},
{ desc: "flex-min-inline-size-min-content",
candStyle: "flex:0 5px;min-inline-size:min-content",
},
{ desc: "flex-min-block-size-auto",
candStyle: "flex:0 5px;min-block-size:auto",
isContainerColumnOriented: true,
},
{ desc: "flex-min-block-size-auto",
candStyle: "flex:0 5px;min-block-size:min-content",
isContainerColumnOriented: true,
},
];
// Content-dependent flex-basis values should prevent a flex item
// from being a reflow root.
function runFlexItemSubtest(subtest) {
// We create a flex container with two flex items:
// - a simple flex item that just absorbs all extra space
// - the reflow root candidate
let containerSizeVal = subtest.isContainerIntrinsicallySized ?
"max-content" : "100px";
let containerSizeDecl =
"inline-size: " + containerSizeVal + "; " +
"block-size: " + containerSizeVal + ";";
let containerFlexDirectionDecl = "flex-direction: " +
(subtest.isContainerColumnOriented ? "column" : "row") + ";"
let flexContainer = createStyledDiv("display: flex; \
border: 2px solid teal" +
containerSizeDecl +
containerFlexDirectionDecl);
let simpleItem = createStyledDiv("border: 1px solid gray; \
background: yellow; \
min-inline-size: 10px; \
flex: 1");
// The reflow root candidate
// (Note that we use min-width:0/min-height:0 by default, but subtests
// might override that with other values in 'candStyle'.)
let cand = createStyledDiv(gReflowRootCandidateStyles +
" min-width: 0; min-height: 0; " +
subtest.candStyle);
// Something whose size we can adjust, inside the reflow root candidate:
let inner = createStyledDiv("height:20px; width:20px; \
border: 1px solid purple");
cand.appendChild(inner);
flexContainer.appendChild(simpleItem);
flexContainer.appendChild(cand);
gFBody.appendChild(flexContainer);
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, subtest.desc);
flexContainer.remove(); // clean up
}
let tableSubtests = [
{ desc: "table",
/* Testing the default "display:table" styling that runTableTest uses: */
candStyle: "",
},
{ desc: "inline-table",
candStyle: "display:inline-table;",
},
{ desc: "table-caption",
candStyle: "display:table-caption;",
},
{ desc: "table-cell",
candStyle: "display:table-cell;",
},
{ desc: "table-column",
candStyle: "display:table-column;",
isColumn: true,
},
{ desc: "table-column-group",
candStyle: "display:table-column-group;",
isColumn: true,
},
{ desc: "table-row",
candStyle: "display:table-row;",
},
{ desc: "table-row-group",
candStyle: "display:table-row-group;",
},
];
function runTableSubtest(subtest) {
let outer = createStyledDiv("");
let shrinkWrapIB = createStyledDiv("display: inline-block; \
border: 2px solid teal");
let cand = createStyledDiv("display: table; \
width: 1px; height: 1px; \
will-change: transform; \
border: 1px solid purple;" +
subtest.candStyle);
let inner = createStyledDiv("display: block; \
width: 10px; height: 10px; \
background: pink;");
if (subtest.isColumn) {
// The candidate is a table-column / table-column-group, so
// the inner content that we tweak shouldn't be inside of it.
// Create an explicit table, separately, and put the candidate
// (the column/column-group) and the tweakable inner element
// both inside of that explicit table.
let table = createStyledDiv("display: table");
table.appendChild(inner);
table.appendChild(cand);
shrinkWrapIB.appendChild(table);
} else {
// The candidate is a table or some other table part
// that can hold content. Just put the tweakable inner
// element directly inside of it, and let anonymous table parts
// be generated as-needed.
cand.appendChild(inner);
shrinkWrapIB.appendChild(cand);
}
outer.appendChild(gFDoc.createTextNode("a"));
outer.appendChild(shrinkWrapIB);
gFBody.appendChild(outer);
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, subtest.desc);
outer.remove(); // clean up
}
let inlineSubtests = [
{ desc: "inline",
candStyle: "display:inline",
},
];
function runInlineSubtest(subtest) {
let outer = createStyledDiv("");
let shrinkWrapIB = createStyledDiv("display: inline-block; \
border: 2px solid teal");
let cand = createStyledDiv(gReflowRootCandidateStyles +
subtest.candStyle);
let inner = createStyledDiv("display: inline-block; \
width: 20px; height: 20px; \
background: pink;");
cand.appendChild(inner);
shrinkWrapIB.appendChild(cand);
outer.appendChild(gFDoc.createTextNode("a"));
outer.appendChild(shrinkWrapIB);
gFBody.appendChild(outer);
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, subtest.desc);
outer.remove(); // clean up
}
let rubySubtests = [
{ desc: "ruby",
candStyle: "display:ruby",
},
{ desc: "ruby-base",
candStyle: "display:ruby-base",
},
{ desc: "ruby-base-container",
candStyle: "display:ruby-base-container",
},
{ desc: "ruby-text",
candStyle: "display:ruby-text",
},
{ desc: "ruby-text-container",
candStyle: "display:ruby-text-container",
},
];
function runRubySubtest(subtest) {
let outer = createStyledDiv("");
let shrinkWrapIB = createStyledDiv("display: inline-block; \
border: 2px solid teal");
let cand = createStyledDiv(gReflowRootCandidateStyles +
subtest.candStyle);
let inner = createStyledDiv("display: inline-block; \
width: 20px; height: 20px; \
background: pink;");
cand.appendChild(inner);
shrinkWrapIB.appendChild(cand);
outer.appendChild(gFDoc.createTextNode("a"));
outer.appendChild(shrinkWrapIB);
gFBody.appendChild(outer);
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, subtest.desc);
outer.remove(); // clean up
}
function runMozBoxTest() {
// We create a -moz-box, with a child that looks like a good candidate
// for being a reflow root, except that really its size depends on its
// child's size (due to the min-sizing behavior of children of -moz-box).
// We exercise this size dependency (& reveal the issue, if the candidate is
// mistakenly flagged as a reflow root) by growing the size of the
// content and seeing how the rendering changes.
let mozBox = createStyledDiv("display: -moz-box; \
width: 10px; \
border: 2px solid teal");
let cand = createStyledDiv(gReflowRootCandidateStyles +
" border: 2px solid black;");
let inner = createStyledDiv("height: 20px; width: 20px; \
background: pink;");
cand.appendChild(inner);
mozBox.appendChild(cand);
gFBody.appendChild(mozBox);
// Note: This "is()" check is just validating that we're testing what we
// intend to be testing. Eventually, we'll remove support for -moz-box
// (as part of XUL), at which point this "is" check will start failing
// and we'll probably want to remove this test function, along with the
// IsXULBoxFrame() check that this test function is targeting in
// ReflowInput::InitDynamicReflowRoot.
is(gFWindow.getComputedStyle(mozBox, "").display, "-moz-box",
"'display:-moz-box' should be honored and show up in computed style");
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, "child-of-moz-box");
mozBox.remove(); // clean up
}
function runFixedPosTest() {
// We reset the 'will-change' value on the candidate (overriding
// 'will-change:transform'), so that it won't be a fixed-pos CB. We also
// give the candidate some margins to shift it away from the origin, to
// make it visually clearer that its child's fixed-pos offsets are being
// resolved against the viewport rather than against the candidate div.
let cand = createStyledDiv(gReflowRootCandidateStyles +
"will-change: initial; \
margin: 20px 0 0 30px; \
border: 2px solid black;");
let inner = createStyledDiv("height: 20px; width: 20px; \
background: pink;");
let fixedPos = createStyledDiv("position: fixed; \
width: 10px; height: 10px; \
background: gray;");
cand.appendChild(inner);
cand.appendChild(fixedPos);
gFBody.appendChild(cand);
// For our tweak, we'll adjust the size of "inner". This change impacts
// the position of the "fixedPos" placeholder (specifically, its static
// position), so this will require an incremental reflow that is rooted at
// the viewport (the containing block of "fixedPos") in order to produce
// the correct final layout. This is why "cand" isn't allowed to be a
// reflow root.
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, "fixed-pos");
cand.remove(); // clean up
}
function runMarginCollapseTest() {
let outer = createStyledDiv("background: lime");
// We use 'display:block' on the candidate (overriding 'display:flow-root')
// so that it won't be a block formatting context. (See usage/definition of
// NS_BLOCK_FORMATTING_CONTEXT_STATE_BITS in our c++ layout code.)
let cand = createStyledDiv(gReflowRootCandidateStyles +
"display: block; \
background: purple;");
// We'll add border to this div in the "tweak" function, which will break
// the stack of margin collapsed divs.
let divWithEventualBorder = createStyledDiv("");
let divWithMargin = createStyledDiv("margin-top: 30px; \
width: 10px; height: 10px; \
background: pink;");
divWithEventualBorder.appendChild(divWithMargin);
cand.appendChild(divWithEventualBorder);
outer.appendChild(cand);
gFBody.appendChild(outer);
// For our tweak, we'll add a border around "divWithEventualBorder", which
// prevents the margin (on "divWithMargin") from collapsing all the way up
// to the outermost div wrapper (which it does, before the tweak).
// So: this tweak effectively moves the y-position towards 0, for all
// div wrappers outside the new border. This includes "outer", the parent
// of our reflow root candidate. So: if we mistakenly allow "cand" to be a
// reflow root, then we probably would neglect to adjust the position of
// "outer" when reacting to this tweak (and we'd catch that & report a
// test failure in our screenshot comparisons below).
let tweakFunc = function() {
divWithEventualBorder.style.border = "2px solid black";
};
tweakAndCompareSnapshots(tweakFunc, "margin-uncollapse");
outer.remove(); // clean up
}
function runFloatTest() {
let outer = createStyledDiv("");
// We use 'display:block' on the candidate (overriding 'display:flow-root')
// so that it won't be a block formatting context. (See usage/definition of
// NS_BLOCK_FORMATTING_CONTEXT_STATE_BITS in our c++ layout code.)
// This allows floats inside the candidate to affect the position of
// inline-level content outside of it.
let cand = createStyledDiv(gReflowRootCandidateStyles +
"display: block; \
border: 2px solid black;");
let floatChild = createStyledDiv("float: left; \
width: 60px; height: 60px; \
background: pink;");
let inlineBlock = createStyledDiv("display: inline-block; \
width: 80px; height: 80px; \
background: teal");
cand.appendChild(floatChild);
outer.appendChild(cand);
outer.appendChild(inlineBlock);
gFBody.appendChild(outer);
let tweakFunc = function() {
floatChild.style.width = floatChild.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, "float");
outer.remove(); // clean up
}
function main() {
SimpleTest.waitForExplicitFinish();
// Initialize our convenience aliases:
gFWindow = frames[0].window;
gFDoc = frames[0].document;
gFBody = frames[0].document.body;
for (let subtest of intrinsicSizeSubtests) {
runIntrinsicSizeSubtest(subtest);
}
for (let subtest of flexItemSubtests) {
runFlexItemSubtest(subtest);
}
for (let subtest of tableSubtests) {
runTableSubtest(subtest);
}
for (let subtest of inlineSubtests) {
runInlineSubtest(subtest);
}
for (let subtest of rubySubtests) {
runRubySubtest(subtest);
}
runFixedPosTest();
runMarginCollapseTest();
runFloatTest();
// Turn on 'display:-moz-box' support, so that we can validate that
// children of XUL boxes are excluded from being tagged as dynamic
// reflow roots. We run this test last (and then call finish), since
// pref-setting is async and this lets the rest of this mochitest
// (everything up to this point) be straightforward & synchronous.
SpecialPowers.pushPrefEnv(
{ set: [["layout.css.xul-box-display-values.content.enabled", true]]},
function() {
runMozBoxTest();
SimpleTest.finish();
}
);
}
</script>
</body>
</html>