quixote
Version:
CSS unit and integration testing
523 lines (406 loc) • 19.7 kB
JavaScript
// Copyright (c) 2017 Titanium I.T. LLC. All rights reserved. For license, see "README" or "LICENSE" file.
;
var assert = require("../util/assert.js");
var reset = require("../__reset.js");
var quixote = require("../quixote.js");
var ElementRenderEdge = require("./element_render_edge.js");
var PositionDescriptor = require("./position_descriptor.js");
var Position = require("../values/position.js");
describe("DESCRIPTOR: ElementRenderEdge", function() {
var frame;
var qGrandparent;
var qParent;
var qElement;
var top;
var right;
var bottom;
var left;
beforeEach(function() {
frame = reset.frame;
qGrandparent = frame.add("<div>grandparent</div>", "grandparent");
qParent = qGrandparent.add("<div>intermediate element</div>").add("<div>parent</div>", "parent");
qElement = qParent.add("<div>element</div>", "element");
top = ElementRenderEdge.top(qElement);
right = ElementRenderEdge.right(qElement);
bottom = ElementRenderEdge.bottom(qElement);
left = ElementRenderEdge.left(qElement);
});
it("is a position descriptor", function() {
assert.implements(top, PositionDescriptor);
});
it("converts to string", function() {
assertDesc(qElement, top, "top rendered edge of ", "top");
assertDesc(qElement, right, "right rendered edge of ", "right");
assertDesc(qElement, bottom, "bottom rendered edge of ", "bottom");
assertDesc(qElement, left, "left rendered edge of ", "left");
function assertDesc(element, edge, expected, message) {
assert.equal(edge.toString(), expected + element, message);
}
});
it("has assertions", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
assert.exception(
function() { left.should.equal(30); },
"left rendered edge of 'element' should be 30px to right.\n" +
" Expected: 30px\n" +
" But was: 0px"
);
});
it("defaults to bounding box", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
assertRendered(
"position: absolute; top: 10px; height: 20px; left: 40px; width: 80px;",
10, 120, 30, 40
);
});
it("accounts for elements positioned completely off-screen", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
assertNotRendered("position: absolute; top: -100px; height: 20px; left: 40px; width: 80px;", "outside top");
assertNotRendered("position: absolute; top: 10px; height: 20px; left: -400px; width: 80px;", "outside left");
// it's not possible to position off-screen to right or bottom--the page always expands to fit
});
it("accounts for elements positioned partly off-screen", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
assertRendered(
"position: absolute; top: -100px; height: 200px; left: -400px; width: 800px;",
0, 400, 100, 0
);
});
it("accounts for elements with zero width or height", function() {
assertNotRendered("position: absolute; top: 10px; height: 0px; left: 10px; width: 10px;", "zero height");
assertNotRendered("position: absolute; top: 10px; height: 10px; left: 10px; width: 0px;", "zero width");
});
it("accounts for elements using display:none", function() {
assertNotRendered("display: none;");
});
it("accounts for detached elements", function() {
qElement.remove();
assertNotRendered("position: absolute; top: 10px; height: 10px; left: 10px; width: 10px;");
});
describe("clip-path CSS property", function() {
var EXPECTED_FAIL_FAST_MESSAGE = "Can't determine element rendering because the element is affected by the " +
"'clip-path' property, which Quixote doesn't support.";
it("fails fast when used in element", function() {
element("clip-path: url('#something');");
if (!browserSupportsClipPath()) return;
assert.exception(function() { top.value(); }, EXPECTED_FAIL_FAST_MESSAGE);
});
it("fails fast when used anywhere in parent hierarchy", function() {
grandparent("clip-path: url('#something');");
if (!browserSupportsClipPath()) return;
assert.exception(function() { top.value(); }, EXPECTED_FAIL_FAST_MESSAGE);
});
function browserSupportsClipPath() {
var clipPath = qElement.getRawStyle("clip-path");
return (clipPath !== "" && clipPath !== "none");
}
});
describe("overflow CSS property", function() {
it("accounts for elements positioned completely outside overflow-clipped parent", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
parent("overflow: hidden; position: absolute; top: 50px; height: 100px; left: 50px; width: 100px;");
assertNotRendered("position: absolute; top: -20px; height: 10px; left: 30px; width: 10px;", "outside top");
assertNotRendered("position: absolute; top: 20px; height: 10px; left: 130px; width: 10px;", "outside right");
assertNotRendered("position: absolute; top: 120px; height: 10px; left: 30px; width: 10px;", "outside bottom");
assertNotRendered("position: absolute; top: 20px; height: 10px; left: -30px; width: 10px;", "outside left");
});
it("accounts for elements partially clipped by overflow parent", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
parent("overflow: hidden; position: absolute; top: 50px; height: 100px; left: 60px; width: 100px;");
assertRendered(
"position: absolute; top: -10px; height: 100px; left: -30px; width: 100px;",
50, 130, 140, 60,
"clipped on top left"
);
assertRendered(
"position: absolute; top: 10px; height: 100px; left: 30px; width: 100px;",
60, 160, 150, 90,
"clipped on bottom right"
);
assertRendered(
"position: absolute; top: -10px; height: 200px; left: -30px; width: 200px;",
50, 160, 150, 60,
"clipped on all sides"
);
});
it("accounts for parents that have zero width or height", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
parent("overflow: hidden; position: absolute; top: 10px; height: 0px; left: 10px; width: 100px;");
assertNotRendered("", "zero height");
parent("overflow: hidden; position: absolute; top: 10px; height: 100px; left: 10px; width: 0px;");
assertNotRendered("", "zero width");
});
it("recognizes all forms of clipped overflow", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
test("overflow: hidden;");
test("overflow: scroll;");
test("overflow: auto;");
function test(overflowStyle) {
parent(overflowStyle + " position: absolute; top: 50px; height: 100px; left: 50px; width: 100px;");
assertNotRendered("position: absolute; top: -20px; height: 10px;");
}
});
it("accounts for different X and Y overflow settings", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
parent("overflow-x: visible; overflow-y: hidden; position: absolute; top: 50px; height: 100px; left: 60px; width: 100px; background-color: red;");
assertRendered(
"position: absolute; top: -10px; height: 200px; left: -10px; width: 200px; background-color: blue",
50, 160, 150, 60,
"clips on all sides despite `overflow-x: visible` (spec says it is set to 'auto')"
);
parent("overflow-x: hidden; overflow-y: visible; position: absolute; top: 50px; height: 100px; left: 60px; width: 100px; background-color: red;");
assertRendered(
"position: absolute; top: -10px; height: 200px; left: -10px; width: 200px; background-color: blue",
50, 160, 150, 60,
"clips on all sides despite `overflow-y: visible` (spec says it is set to 'auto')"
);
});
it("does not crash when compound 'overflow' parameter used (but results correctly vary by browser)", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
parent("overflow: hidden visible");
assert.noException(
function() { top.value(); }
);
});
it("accounts for clipped overflow anywhere in parent hierarchy", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
grandparent("overflow: hidden; position: absolute; top: 50px; height: 100px; left: 50px; width: 100px;");
assertNotRendered("position:absolute; top: -20px; height: 10px;");
});
it("accounts for multiple uses of clipped overflow", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
grandparent("overflow: hidden; position: absolute; top: 50px; height: 100px; left: 50px; width: 100px;");
parent("overflow: hidden; position: absolute; top: 20px; height: 100px; left: 40px; width: 100px;");
assertRendered(
"position:absolute; top: -10px; height: 200px; left: -20px; width: 200px;",
70, 150, 150, 90
);
});
it("ignores overflow when position is fixed", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
parent("overflow: hidden; position: absolute; top: 50px; height: 100px; left: 40px; width: 100px;");
assertRendered(
"position:fixed; top: 10px; height: 10px; left: 15px; width: 10px;",
10, 25, 20, 15
);
});
});
describe("clip CSS property", function() {
describe("on an element", function() {
it("accounts for `clip:auto` (which means 'no clipping')", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
assertRendered(
"position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; clip: auto;",
50, 140, 150, 40
);
});
it("fails fast when browser can't detect 'clip: auto' property", function() {
if (!quixote.browser.misreportsClipAutoProperty()) return;
element("position: absolute; clip: auto");
assert.exception(function() {
top.value();
}, "Can't determine element rendering on this browser because it misreports the value of the " +
"`clip: auto` property. You can use `quixote.browser.misreportsClipAutoProperty()` to skip this browser.");
});
it("fails fast when browser can't compute individual clip properties", function() {
if (!quixote.browser.misreportsAutoValuesInClipProperty()) return;
element("position: absolute; clip: rect(auto, auto, auto, auto);");
assert.exception(function() {
top.value();
}, "Can't determine element rendering on this browser because it misreports the value of the `clip`" +
" property. You can use `quixote.browser.misreportsAutoValuesInClipProperty()` to skip this browser.");
});
it("accounts for pixel values in clip property", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
assertRendered(
"position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; clip: rect(10px, 30px, 25px, 15px);",
60, 70, 75, 55
);
});
it("treats 'auto' values as equivalent to element edge", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
assertRendered(
"position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; clip: rect(auto, auto, auto, auto);",
50, 140, 150, 40
);
});
it("clips element out of existence when clip values are the same or nonsensical", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
var style = "position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; ";
assertNotRendered(style + " clip: rect(10px, auto, 10px, auto);", "same top and bottom");
assertNotRendered(style + " clip: rect(auto, 10px, auto, 10px);", "same left and right");
assertNotRendered(style + " clip: rect(10px, auto, 5px, auto);", "bottom higher than top");
assertNotRendered(style + " clip: rect(auto, 5px, auto, 10px);", "right less than left");
});
it("handles negative clip values", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
var style = "position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; ";
assertRendered(
style + " clip: rect(-10px, auto, auto, auto)",
50, 140, 150, 40, "negative top"
);
assertNotRendered(style + " clip: rect(auto, -10px, auto, auto)", "negative right");
assertNotRendered(style + " clip: rect(auto, auto, -10px, auto)", "negative bottom");
assertRendered(
style + " clip: rect(auto, auto, auto, -10px)",
50, 140, 150, 40, "negative left"
);
});
it("handles fractional clip values", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
assertRendered("position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; " +
"clip: rect(10.9px, 9.1px, 30.3px, 4.6px);",
60.9, 49.1, 80.3, 44.6
);
});
it("handles non-pixel values in clip property", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
assertRendered(
"position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; clip: rect(1em, 2em, 2em, 1em);",
66, 72, 82, 56
);
});
it("doesn't clip padding", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
assertRendered(
"position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; " +
"clip: rect(auto, auto, auto, auto); padding: 10px 10px 10px 10px;",
50, 160, 170, 40
);
});
it("doesn't clip border", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
assertRendered(
"position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; " +
"clip: rect(auto, auto, auto, auto); border: 10px solid black;",
50, 160, 170, 40
);
});
it("doesn't clip margin", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
assertRendered(
"position: absolute; top: 50px; height: 100px; left: 40px; width: 100px; " +
"clip: rect(auto, auto, auto, auto); margin: 10px 10px 10px 10px;",
60, 150, 160, 50
);
});
it("only applies when position is 'absolute' or 'fixed'", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
assertClip("position: absolute;");
assertClip("position: fixed;");
assertNoClip("position: static;");
assertNoClip("position: relative;");
assertNoClip("position: sticky;");
function assertClip(positionStyle) {
assertNotRendered(positionStyle + " clip: rect(0px, 0px, 0px, 0px);", positionStyle);
}
function assertNoClip(positionStyle) {
element(positionStyle + " clip: rect(0px, 0px, 0px, 0px);");
assert.equal(top.value().equals(Position.noY()), false, "clipping when '" + positionStyle + "'");
}
});
});
describe("on ancestor elements", function() {
var tenPxParent = "position: absolute; top: 100px; height: 10px; left: 100px; width: 10px; ";
var hundredPxChild = "position: absolute; top: -50px; height: 100px; left: -50px; width: 100px; ";
it("clips children when ancestor is clipped", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
parent(tenPxParent + "clip: rect(1px, 4px, 3px, 2px);");
assertRendered(hundredPxChild, 101, 104, 103, 102);
});
it("clips children when multiple ancestors are clipped", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
grandparent(tenPxParent + "clip: rect(1px, auto, 3px, auto);");
parent("position: absolute; top: 0px; height: 100px; left: 0px; width: 100px; " +
"clip: rect(auto, 4px, auto, 2px);");
assertRendered(hundredPxChild, 101, 104, 103, 102);
});
it("applies both element's 'clip' and ancestor's 'clip'", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
parent(tenPxParent + "clip: rect(1px, auto, 3px, auto);");
assertRendered(hundredPxChild + "clip: rect(auto, 54px, auto, 52px);", 101, 104, 103, 102);
});
it("doesn't clip children when ancestor uses 'clip: auto'", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
parent(tenPxParent + "clip: auto;");
assertRendered(hundredPxChild, 50, 150, 150, 50);
});
it("clips children to edges of ancestor when ancestor uses 'clip: rect(auto, auto, auto, auto)'", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
parent(tenPxParent + "clip: rect(auto, auto, auto, auto)");
assertRendered(hundredPxChild, 100, 110, 110, 100);
});
it("clips children above and to left of ancestor when ancestor uses negative clip values", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
parent(tenPxParent + "clip: rect(-10px, -8px, -5px, -18px);");
assertRendered(hundredPxChild, 100 - 10, 100 - 8, 100 - 5, 100 - 18);
});
it("clips children even when they have 'position: fixed' property", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
parent(tenPxParent + "clip: rect(auto, auto, auto, auto);");
assertRendered(
"position: fixed; top: 0px; height: 105px; left: 107px; width: 100px;",
100, 110, 105, 107
);
});
});
});
it("applies overflow and clip together", function() {
if (quixote.browser.misreportsClipAutoProperty()) return;
if (quixote.browser.misreportsAutoValuesInClipProperty()) return;
parent(
"overflow: hidden; " +
"clip: rect(auto, 5px, auto, auto); " +
"position: absolute; " +
"top: 50px; height: 100px; left: 60px; width: 100px;"
);
assertRendered(
"position: absolute; top: -10px; height: 100px; left: -30px; width: 100px;",
50, 65, 140, 60
);
});
function grandparent(style) {
if (style === undefined) style = "";
qGrandparent.toDomElement().style.cssText = style;
}
function parent(style) {
if (style === undefined) style = "";
qParent.toDomElement().style.cssText = style;
}
function element(style) {
if (style === undefined) style = "";
qElement.toDomElement().style.cssText = style;
}
function assertNotRendered(elementStyle, message) {
message = message ? message + " - " : "";
element(elementStyle);
assert.objEqual(top.value(), Position.noY(), message + "top");
assert.objEqual(right.value(), Position.noX(), message + "right");
assert.objEqual(bottom.value(), Position.noY(), message + "bottom");
assert.objEqual(left.value(), Position.noX(), message + "left");
}
function assertRendered(elementStyle, expectedTop, expectedRight, expectedBottom, expectedLeft, message) {
message = message ? message + " - " : "";
element(elementStyle);
assert.objEqual(top.value(), Position.y(expectedTop), message + "top");
assert.objEqual(right.value(), Position.x(expectedRight), message + "right");
assert.objEqual(bottom.value(), Position.y(expectedBottom), message + "bottom");
assert.objEqual(left.value(), Position.x(expectedLeft), message + "left");
}
});