Source code

Revision control

Copy as Markdown

Other Tools

Test Info:

<!DOCTYPE HTML>
<html>
<!--
-->
<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 src="/tests/SimpleTest/SimpleTest.js"></script>
<script src="/tests/SimpleTest/WindowSnapshot.js"></script>
<link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
</head>
<body onload="main()">
<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.
// FIXME(bug 1548078): the following two tests fail to produce a rendering difference:
// { desc: "flex-basis-pct",
// candStyle: "flex-basis:80%;",
// isContainerIntrinsicallySized: true,
// },
// { desc: "flex-basis-calc-pct",
// candStyle: "flex-basis:calc(10px + 80%);",
// isContainerIntrinsicallySized: true,
// },
{ desc: "flex-basis-from-pct-isize",
candStyle: "inline-size: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; inline-size:auto; min-inline-size:auto",
},
{ desc: "flex-min-inline-size-min-content",
candStyle: "flex:0 5px; inline-size:auto; min-inline-size:min-content",
},
{ desc: "flex-min-block-size-auto",
candStyle: "flex:0 5px; block-size:auto; min-block-size:auto",
isContainerColumnOriented: true,
},
{ desc: "flex-min-block-size-auto",
candStyle: "flex:0 5px; block-size:auto; 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");
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 gridItemSubtests = [
{ desc: "grid-pct-inline-isize",
candStyle: "inline-size:80%",
isContainerIntrinsicallySized: true,
},
{ desc: "grid-calc-pct-inline-isize",
candStyle: "inline-size:calc(10px + 80%);",
isContainerIntrinsicallySized: true,
},
{ desc: "grid-min-inline-size-min-content",
candStyle: "min-inline-size:min-content",
},
];
// 'auto' and intrinsic size keywords on some properties should prevent
// a grid item from becoming a reflow root.
function runGridItemSubtest(subtest) {
// We create a 4x4 grid container with two grid items:
// - a simple grid 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 containerGridDirectionDecl = "grid-auto-flow: " +
(subtest.isContainerColumnOriented ? "column" : "row") + ";"
let gridContainer = createStyledDiv("display: grid; \
grid: 1fr auto / 1fr auto; \
border: 2px solid teal; " +
containerSizeDecl +
containerGridDirectionDecl);
let simpleItem = createStyledDiv("border: 1px solid gray; \
background: yellow;");
// The reflow root candidate
let cand = createStyledDiv(gReflowRootCandidateStyles +
"background: blue; " +
"grid-area:2/2; " +
"min-width: 10px; min-height: 10px; " +
subtest.candStyle);
// Something whose size we can adjust, inside the reflow root candidate:
let inner = createStyledDiv("height:20px; width:20px;");
cand.appendChild(inner);
gridContainer.appendChild(simpleItem);
gridContainer.appendChild(cand);
gFBody.appendChild(gridContainer);
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, subtest.desc);
gridContainer.remove(); // clean up
}
let gridContainerSubtests = [
{ desc: "grid-column-start",
candStyle: "grid-column-start:2",
},
{ desc: "grid-column-end",
candStyle: "grid-column-end:3",
},
{ desc: "grid-row-start",
candStyle: "grid-row-start:2",
},
{ desc: "grid-row-end",
candStyle: "grid-row-end:3",
},
];
// Test that changes to grid item properties that affect grid container
// layout causes a grid container reflow when the item is a reflow root.
function runGridContainerSubtest(subtest) {
// We create a 4x4 grid container with one grid item:
// - a reflow root grid item that we'll tweak from
// the list above. By default it's placed at 1,1
// but after the tweak it should be placed elsewhere
let gridContainer = createStyledDiv("display: grid; \
width: 100px; \
height: 100px; \
grid: 1fr 10px / 1fr 10px; \
border: 2px solid teal");
// The reflow root candidate
let cand = createStyledDiv(gReflowRootCandidateStyles +
"background: blue; " +
" min-width: 10px; min-height: 10px; ");
gridContainer.appendChild(cand);
gFBody.appendChild(gridContainer);
let tweakFunc = function() {
cand.style.cssText += "; " + subtest.candStyle;
};
tweakAndCompareSnapshots(tweakFunc, subtest.desc);
gridContainer.remove(); // clean up
}
let gridSubgridSubtests = [
{ desc: "subgrid",
candStyle: "grid: subgrid / subgrid",
},
{ desc: "subgrid-rows",
candStyle: "grid: subgrid / 20px",
},
{ desc: "subgrid-columns",
candStyle: "grid: 20px / subgrid",
},
];
// Test that a subgrid is not a reflow root.
function runGridSubgridSubtest(subtest) {
// We create a 4x4 grid container a with one grid item:
// - a reflow root display:grid that we'll style as a subgrid from
// the list above. We place an item inside it that we'll tweak
// the size of, which should affect the outer grid track sizes.
let gridContainer = createStyledDiv("display: grid; \
width: 100px; \
height: 100px; \
grid: 1fr auto / 1fr auto; \
border: 2px solid teal");
// The reflow root candidate
let cand = createStyledDiv(gReflowRootCandidateStyles +
"display: grid;" +
"grid-area: 2/2;" +
"background: blue;" +
"min-width: 10px; min-height: 10px;" +
subtest.candStyle);
// Something whose size we can adjust, inside the subgrid:
let inner = createStyledDiv("height:20px; width:20px;");
cand.appendChild(inner);
gridContainer.appendChild(cand);
gFBody.appendChild(gridContainer);
let tweakFunc = function() {
inner.style.width = inner.style.height = "40px";
};
tweakAndCompareSnapshots(tweakFunc, subtest.desc);
gridContainer.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 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_BFC_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_BFC_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 gridContainerSubtests) {
runGridContainerSubtest(subtest);
}
for (let subtest of gridSubgridSubtests) {
runGridSubgridSubtest(subtest);
}
for (let subtest of gridItemSubtests) {
runGridItemSubtest(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();
SimpleTest.finish();
}
</script>
</body>
</html>