Files
yoga/src/Layout-test-utils.js

522 lines
15 KiB
JavaScript
Raw Normal View History

/**
* Copyright (c) 2014, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
2015-08-11 16:52:57 +01:00
/* globals document, computeLayout, navigator */
var layoutTestUtils = (function() {
2014-12-11 13:30:46 +00:00
//
// Sets the test cases precision, by default set to 1.0, aka pixel precision
// (assuming the browser does pixel snapping - and that we're ok with being
// 'only' pixel perfect).
//
// Set it to '10' for .1 precision, etc... in theory the browser is doing
// 'pixel' snapping so 1.0 should do, the code is left for clarity...
//
// Set it to undefined to disable and use full precision.
//
var testMeasurePrecision = 1.0;
if (typeof jasmine !== 'undefined') {
jasmine.matchersUtil.buildFailureMessage = function() {
var args = Array.prototype.slice.call(arguments, 0);
var matcherName = args[0];
var isNot = args[1];
var actual = args[2];
var expected = args.slice(3);
var englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
var pp = function(node) {
return jasmine.pp(node)
.replace(/([\{\[]) /g, '$1')
.replace(/ ([\}\]:])/g, '$1');
};
var message = 'Expected ' +
pp(actual) +
(isNot ? ' not ' : ' ') +
'\n' +
englishyPredicate;
if (expected.length > 0) {
for (var i = 0; i < expected.length; i++) {
if (i > 0) {
message += ',';
}
message += ' ' + pp(expected[i]);
2014-12-11 13:30:46 +00:00
}
}
return message + '.';
};
}
2014-12-11 13:30:46 +00:00
var _cachedIframe;
function renderIframe() {
var iframe = document.createElement('iframe');
document.body.appendChild(iframe);
return iframe;
}
function getIframe(iframe) {
if (_cachedIframe) {
return _cachedIframe;
}
var doc = iframe.contentDocument;
if (doc.readyState === 'complete') {
var style = document.createElement('style');
style.textContent = (function() {/*
body, div {
box-sizing: border-box;
border: 0 solid black;
position: relative;
2014-12-11 13:44:03 +00:00
display: flex;
display: -webkit-flex;
flex-direction: column;
-webkit-flex-direction: column;
align-items: stretch;
-webkit-align-items: stretch;
justify-content: flex-start;
-webkit-justify-content: flex-start;
flex-shrink: 0;
-webkit-flex-shrink: 0;
margin: 0;
padding: 0;
}
hack to ignore three hundred px width of the body {}
body > div {
align-self: flex-start;
}
*/} + '').slice(15, -4);
doc.head.appendChild(style);
_cachedIframe = iframe;
return iframe;
} else {
setTimeout(getIframe.bind(null, iframe), 0);
}
}
if (typeof window !== 'undefined') {
var iframe = renderIframe();
getIframe(iframe);
}
2014-04-26 19:02:16 -07:00
2015-02-07 00:01:35 -05:00
if (typeof computeLayout === 'object') {
var fillNodes = computeLayout.fillNodes;
var realComputeLayout = computeLayout.computeLayout;
}
function extractNodes(node) {
var layout = node.layout;
delete node.layout;
if (node.children && node.children.length > 0) {
layout.children = node.children.map(extractNodes);
} else {
delete node.children;
}
delete layout.right;
delete layout.bottom;
delete layout.direction;
return layout;
}
function roundLayout(layout) {
// Chrome rounds all the numbers with a precision of 1/64
// Reproduce the same behavior
function round(number) {
var floored = Math.floor(number);
var decimal = number - floored;
if (decimal === 0) {
return number;
}
var minDifference = Infinity;
var minDecimal = Infinity;
for (var i = 1; i < 64; ++i) {
var roundedDecimal = i / 64;
var difference = Math.abs(roundedDecimal - decimal);
if (difference < minDifference) {
minDifference = difference;
minDecimal = roundedDecimal;
}
}
return floored + minDecimal;
}
function rec(layout) {
layout.top = round(layout.top);
layout.left = round(layout.left);
layout.width = round(layout.width);
layout.height = round(layout.height);
if (layout.children) {
for (var i = 0; i < layout.children.length; ++i) {
rec(layout.children[i]);
}
}
}
rec(layout);
return layout;
}
function capitalizeFirst(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
function computeCSSLayout(rootNode) {
fillNodes(rootNode);
realComputeLayout(rootNode);
return roundLayout(extractNodes(rootNode));
}
function computeDOMLayout(node) {
var body = getIframe().contentDocument.body;
function transfer(div, node, name, ext) {
if (name in node.style) {
var value = node.style[name] + (ext || '');
div.style['-webkit-' + name] = value;
div.style['webkit' + capitalizeFirst(name)] = value;
div.style[name] = value;
}
}
2014-04-22 11:31:42 -07:00
function transferSpacing(div, node, type, suffix) {
transfer(div, node, type + suffix, 'px');
transfer(div, node, type + 'Left' + suffix, 'px');
transfer(div, node, type + 'Top' + suffix, 'px');
transfer(div, node, type + 'Bottom' + suffix, 'px');
transfer(div, node, type + 'Right' + suffix, 'px');
transfer(div, node, type + 'Start' + suffix, 'px');
transfer(div, node, type + 'End' + suffix, 'px');
}
function renderNode(parent, node) {
var div = document.createElement('div');
transfer(div, node, 'width', 'px');
transfer(div, node, 'height', 'px');
transfer(div, node, 'minWidth', 'px');
transfer(div, node, 'minHeight', 'px');
transfer(div, node, 'maxWidth', 'px');
transfer(div, node, 'maxHeight', 'px');
transfer(div, node, 'top', 'px');
transfer(div, node, 'left', 'px');
transfer(div, node, 'right', 'px');
transfer(div, node, 'bottom', 'px');
2014-04-22 11:31:42 -07:00
transferSpacing(div, node, 'margin', '');
transferSpacing(div, node, 'padding', '');
transferSpacing(div, node, 'border', 'Width');
transfer(div, node, 'flexDirection');
transfer(div, node, 'direction');
transfer(div, node, 'flex');
2014-12-11 13:30:46 +00:00
transfer(div, node, 'flexWrap');
transfer(div, node, 'justifyContent');
transfer(div, node, 'alignSelf');
transfer(div, node, 'alignItems');
transfer(div, node, 'alignContent');
transfer(div, node, 'position');
parent.appendChild(div);
(node.children || []).forEach(function(child) {
renderNode(div, child);
});
2014-04-26 17:11:22 -07:00
if (node.style.measure) {
div.innerText = node.style.measure.toString();
2014-04-26 12:16:27 -07:00
}
return div;
}
var div = renderNode(body, node);
function buildLayout(absoluteRect, div) {
var rect = div.getBoundingClientRect();
var result = {
2014-09-11 09:23:30 -07:00
width: rect.width,
height: rect.height,
top: rect.top - absoluteRect.top,
left: rect.left - absoluteRect.left
};
var children = [];
for (var child = div.firstChild; child; child = child.nextSibling) {
2014-04-26 12:16:27 -07:00
if (child.nodeType !== 3 /* textNode */) {
children.push(buildLayout(rect, child));
}
}
if (children.length) {
result.children = children;
}
return result;
}
var layout = buildLayout({left: 0, top: 0}, div);
body.removeChild(div);
return layout;
}
2015-05-09 11:46:28 +08:00
function inplaceRoundNumbersInObject(obj) {
if (!testMeasurePrecision) {
// undefined/0, disables rounding
return;
2015-05-09 11:46:28 +08:00
}
2015-05-09 11:46:28 +08:00
for (var key in obj) {
if (!obj.hasOwnProperty(key)) {
continue;
}
2015-05-09 11:46:28 +08:00
var val = obj[key];
if (typeof val === 'number') {
obj[key] = Math.floor((val * testMeasurePrecision) + 0.5) / testMeasurePrecision;
} else if (typeof val === 'object') {
2015-05-09 11:46:28 +08:00
inplaceRoundNumbersInObject(val);
}
}
}
function nameLayout(name, layout) {
var namedLayout = {name: name};
for (var key in layout) {
namedLayout[key] = layout[key];
}
return namedLayout;
}
2015-02-07 00:01:35 -05:00
function testFillNodes(node, filledNode) {
expect(fillNodes(node)).toEqual(filledNode);
}
function testExtractNodes(node, extractedNode) {
expect(extractNodes(node)).toEqual(extractedNode);
}
function testNamedLayout(name, layoutA, layoutB) {
expect(nameLayout(name, layoutA))
.toEqual(nameLayout(name, layoutB));
}
2014-04-21 16:52:53 -07:00
function isEqual(a, b) {
// computeCSSLayout and computeDOMLayout output a tree with same ordered elements
2014-04-21 16:52:53 -07:00
return JSON.stringify(a) === JSON.stringify(b);
}
function reduceTest(node) {
function isWorking() {
return isEqual(
computeDOMLayout(node),
computeCSSLayout(node)
2014-04-21 16:52:53 -07:00
);
}
if (isWorking()) {
return node;
2014-04-21 16:52:53 -07:00
}
var isModified = true;
function rec(node) {
var key;
var value;
2014-04-21 16:52:53 -07:00
// Style
for (key in node.style) {
value = node.style[key];
2014-04-21 16:52:53 -07:00
delete node.style[key];
if (isWorking()) {
node.style[key] = value;
} else {
isModified = true;
}
}
// Round values
for (key in node.style) {
value = node.style[key];
2014-04-21 16:52:53 -07:00
if (value > 100) {
node.style[key] = Math.round(value / 100) * 100;
} else if (value > 10) {
node.style[key] = Math.round(value / 10) * 10;
} else if (value > 1) {
2014-04-21 16:52:53 -07:00
node.style[key] = 5;
}
if (node.style[key] !== value) {
if (isWorking()) {
node.style[key] = value;
} else {
isModified = true;
}
}
}
// Children
for (var i = 0; node.children && i < node.children.length; ++i) {
value = node.children[i];
2014-04-21 16:52:53 -07:00
node.children.splice(i, 1);
if (isWorking()) {
if (!node.children) {
node.children = [];
}
2014-04-21 16:52:53 -07:00
node.children.splice(i, 0, value);
rec(node.children[i]);
} else {
i--;
isModified = true;
}
}
}
while (isModified) {
isModified = false;
rec(node);
}
return node;
2014-04-21 16:52:53 -07:00
}
var iframeText;
2014-09-19 18:36:18 -07:00
function measureTextSizes(text, width) {
iframeText = iframeText || document.createElement('iframe');
document.body.appendChild(iframeText);
2014-09-19 18:36:18 -07:00
var body = iframeText.contentDocument.body;
if (width === undefined || isNaN(width)) {
2014-09-19 18:36:18 -07:00
width = Infinity;
}
var div = document.createElement('div');
div.style.width = (width === Infinity ? 10000000 : width) + 'px';
div.style.display = 'flex';
div.style.flexDirection = 'column';
div.style.alignItems = 'flex-start';
div.style.alignContent = 'flex-start';
2014-09-19 18:36:18 -07:00
var span = document.createElement('span');
span.style.display = 'flex';
span.style.flexDirection = 'column';
span.style.alignItems = 'flex-start';
span.style.alignContent = 'flex-start';
2014-09-19 18:36:18 -07:00
span.innerText = text;
div.appendChild(span);
body.appendChild(div);
var rect = span.getBoundingClientRect();
body.removeChild(div);
return {
width: rect.width,
height: rect.height
};
}
var texts = {
small: 'small',
big: 'loooooooooong with space'
2014-09-19 18:36:18 -07:00
};
var preDefinedTextSizes = {
smallWidth: 34.671875,
2015-04-24 14:00:40 +01:00
smallHeight: 18,
bigWidth: 172.421875,
2015-08-05 21:57:06 -07:00
bigHeight: 36,
2015-04-24 14:00:40 +01:00
bigMinWidth: 100.4375
};
// Note(prenaux): Clearly not what I would like, but it seems to be the only
// way :( My guess is that since the font on Windows is
// different than on OSX it has a different size.
2015-08-11 16:52:57 +01:00
if (typeof navigator !== 'undefined' && navigator.userAgent.indexOf('Windows NT') > -1) {
preDefinedTextSizes.bigHeight = 36;
}
var textSizes;
if (typeof require === 'function') {
textSizes = preDefinedTextSizes;
} else {
textSizes = {
smallWidth: measureTextSizes(texts.small, 0).width,
smallHeight: measureTextSizes(texts.small, 0).height,
bigWidth: measureTextSizes(texts.big).width,
bigHeight: measureTextSizes(texts.big, 0).height,
bigMinWidth: measureTextSizes(texts.big, 0).width
};
}
// round the text sizes so that we dont have to update it for every browser
// update, assumes we're ok with pixel precision
inplaceRoundNumbersInObject(preDefinedTextSizes);
inplaceRoundNumbersInObject(textSizes);
return {
2014-09-19 18:36:18 -07:00
texts: texts,
textSizes: textSizes,
preDefinedTextSizes: preDefinedTextSizes,
testLayout: function(node, expectedLayout) {
var layout = computeCSSLayout(node);
var domLayout = computeDOMLayout(node);
inplaceRoundNumbersInObject(layout);
inplaceRoundNumbersInObject(domLayout);
inplaceRoundNumbersInObject(expectedLayout);
testNamedLayout('expected-dom', expectedLayout, domLayout);
testNamedLayout('layout-dom', layout, domLayout);
},
2015-08-11 16:52:57 +01:00
testLayoutAgainstDomOnly: function(node) {
var layout = computeCSSLayout(node);
var domLayout = computeDOMLayout(node);
inplaceRoundNumbersInObject(layout);
inplaceRoundNumbersInObject(domLayout);
testNamedLayout('layout-dom', layout, domLayout);
},
2015-02-07 00:01:35 -05:00
testFillNodes: testFillNodes,
testExtractNodes: testExtractNodes,
testRandomLayout: function(node) {
var layout = computeCSSLayout(node);
var domLayout = computeDOMLayout(node);
inplaceRoundNumbersInObject(layout);
inplaceRoundNumbersInObject(domLayout);
expect({node: node, layout: layout})
.toEqual({node: node, layout: domLayout});
2014-04-19 22:08:10 -07:00
},
testsFinished: function() {
console.log('tests finished!');
},
computeLayout: computeCSSLayout,
2014-04-21 16:52:53 -07:00
computeDOMLayout: computeDOMLayout,
2014-04-26 17:11:22 -07:00
reduceTest: reduceTest,
text: function(text) {
var fn = function(width) {
if (width === undefined || isNaN(width)) {
width = Infinity;
}
2014-04-28 12:34:04 -07:00
// Constants for testing purposes between C/JS and other platforms
// Comment this block of code if you want to use the browser to
// generate proper sizes
2014-09-19 18:36:18 -07:00
if (text === texts.small) {
return {
width: Math.min(textSizes.smallWidth, width),
height: textSizes.smallHeight
};
2014-04-28 12:34:04 -07:00
}
2014-09-19 18:36:18 -07:00
if (text === texts.big) {
var res = {
width: width >= textSizes.bigWidth ? textSizes.bigWidth : Math.max(textSizes.bigMinWidth, width),
height: width >= textSizes.bigWidth ? textSizes.smallHeight : textSizes.bigHeight
};
return res;
2014-04-28 12:34:04 -07:00
}
2014-04-26 17:11:22 -07:00
};
2014-09-19 18:36:18 -07:00
fn.toString = function() { return text; };
2014-04-26 17:11:22 -07:00
return fn;
}
};
})();
if (typeof module !== 'undefined') {
module.exports = layoutTestUtils;
}