Files
yoga/gentest/gentest.js
Joe Vilches 484118c89b Change gentest to only write position relative to parent
Summary:
`child.offsetLeft/Top` calculates the offset from child to its nearest positioned ancestor, not its direct parent. These are often the same and have not mattered in the past since we have not supported position static. Since are are in the process of supporting that, we would like our tests to be usable so this adjusts the gentest methodology to only speak the same language as Yoga - that is left/top are always relative to direct parents.

It works by using `getBoundingClientRect().left/top` instead. Then we pass that down to children and subtract it from the childs `getBoundingClientRect()` to get the position relative to the parent. Note we have to round the final result as `child.offsetLeft/Top` is rounded.

Reviewed By: NickGerleman

Differential Revision: D51053629

fbshipit-source-id: 8809588d12953565228ae50fdf38197213c46182
2023-11-07 09:57:04 -08:00

805 lines
20 KiB
JavaScript
Executable File

/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
/* eslint-env browser */
/* global CPPEmitter:readable, JavaEmitter:readable, JavascriptEmitter:readable */
const DEFAULT_EXPERIMENTS = ['AbsolutePercentageAgainstPaddingEdge'];
window.onload = function () {
checkDefaultValues();
printTest(
new CPPEmitter(),
'cpp',
document.body.children[0],
document.body.children[1],
document.body.children[2],
);
printTest(
new JavaEmitter(),
'java',
document.body.children[0],
document.body.children[1],
document.body.children[2],
);
printTest(
new JavascriptEmitter(),
'js',
document.body.children[0],
document.body.children[1],
document.body.children[2],
);
};
function assert(condition, message) {
if (!condition) {
throw new Error(message);
}
}
function printTest(e, ext, LTRContainer, RTLContainer, genericContainer) {
e.push([
ext === 'js' ? '/**' : '/*',
' * Copyright (c) Meta Platforms, Inc. and affiliates.',
' *',
' * This source code is licensed under the MIT license found in the',
' * LICENSE file in the root directory of this source tree.',
' */',
ext === 'cpp' ? '\n// clang-format off' : '',
'// @' +
'generated by gentest/gentest.rb from gentest/fixtures/' +
document.title +
'.html',
'',
]);
e.emitPrologue();
const LTRLayoutTree = calculateTree(LTRContainer, 0, 0);
const RTLLayoutTree = calculateTree(RTLContainer, 0, 0);
const genericLayoutTree = calculateTree(genericContainer, 0, 0);
for (let i = 0; i < genericLayoutTree.length; i++) {
e.emitTestPrologue(
genericLayoutTree[i].name,
genericLayoutTree[i].experiments,
genericLayoutTree[i].disabled,
);
if (genericLayoutTree[i].name == 'wrap_column') {
// Modify width and left values due to both safari and chrome not abiding by the
// specification. The undefined dimension of a parent should be defined by the total size
// of their children in that dimension.
// See diagram under flex-wrap header https://www.w3.org/TR/css-flexbox-1/
assert(
LTRLayoutTree[0].width == 30,
'wrap_column LTR root.width should be 30',
);
LTRLayoutTree[0].width = 60;
assert(
RTLLayoutTree[0].width == 30,
'wrap_column RTL root.width should be 30',
);
RTLLayoutTree[0].width = 60;
const children = RTLLayoutTree[0].children;
assert(
children[0].left == 0,
'wrap_column RTL root_child0.left should be 0',
);
children[0].left = 30;
assert(
children[1].left == 0,
'wrap_column RTL root_child0.left should be 0',
);
children[1].left = 30;
assert(
children[2].left == 0,
'wrap_column RTL root_child2.left should be 0',
);
children[2].left = 30;
assert(
children[3].left == -30,
'wrap_column RTL root_child3.left should be -30',
);
children[3].left = 0;
}
setupTestTree(
e,
undefined,
LTRLayoutTree[i],
genericLayoutTree[i],
'root',
null,
);
e.YGNodeCalculateLayout(
'root',
e.YGDirectionLTR,
genericLayoutTree[i].experiments,
);
e.push('');
assertTestTree(e, LTRLayoutTree[i], 'root', null);
e.push('');
e.YGNodeCalculateLayout(
'root',
e.YGDirectionRTL,
genericLayoutTree[i].experiments,
);
e.push('');
assertTestTree(e, RTLLayoutTree[i], 'root', null);
e.emitTestEpilogue(genericLayoutTree[i].experiments);
}
e.emitEpilogue();
e.print();
}
function assertTestTree(e, node, nodeName, _parentName) {
e.AssertEQ(node.left, e.YGNodeLayoutGetLeft(nodeName));
e.AssertEQ(node.top, e.YGNodeLayoutGetTop(nodeName));
e.AssertEQ(node.width, e.YGNodeLayoutGetWidth(nodeName));
e.AssertEQ(node.height, e.YGNodeLayoutGetHeight(nodeName));
for (let i = 0; i < node.children.length; i++) {
e.push('');
const childName = nodeName + '_child' + i;
assertTestTree(e, node.children[i], childName, nodeName);
}
}
function checkDefaultValues() {
// Sanity check of the Yoga default values by test-template.html
[
{style: 'flex-direction', value: 'column'},
{style: 'justify-content', value: 'flex-start'},
{style: 'align-content', value: 'flex-start'},
{style: 'align-items', value: 'stretch'},
{style: 'position', value: 'static'},
{style: 'flex-wrap', value: 'nowrap'},
{style: 'overflow', value: 'visible'},
{style: 'flex-grow', value: '0'},
{style: 'flex-shrink', value: '0'},
{style: 'left', value: 'undefined'},
{style: 'top', value: 'undefined'},
{style: 'right', value: 'undefined'},
{style: 'bottom', value: 'undefined'},
{style: 'display', value: 'flex'},
].forEach(item => {
assert(
isDefaultStyleValue(item.style, item.value),
item.style + ' should be ' + item.value,
);
});
}
function setupTestTree(
e,
parent,
node,
genericNode,
nodeName,
parentName,
index,
) {
e.emitTestTreePrologue(nodeName);
for (const style in node.style) {
// Skip position info for root as it messes up tests
if (
node.declaredStyle[style] === '' &&
(style == 'left' ||
style == 'top' ||
style == 'right' ||
style == 'bottom' ||
style == 'width' ||
style == 'height')
) {
continue;
}
if (!isDefaultStyleValue(style, node.style[style])) {
switch (style) {
case 'aspect-ratio':
e.YGNodeStyleSetAspectRatio(
nodeName,
pointValue(e, node.style[style]),
);
break;
case 'gap':
e.YGNodeStyleSetGap(
nodeName,
e.YGGutterAll,
pointValue(e, node.style[style]),
);
break;
case 'column-gap':
e.YGNodeStyleSetGap(
nodeName,
e.YGGutterColumn,
pointValue(e, node.style[style]),
);
break;
case 'row-gap':
e.YGNodeStyleSetGap(
nodeName,
e.YGGutterRow,
pointValue(e, node.style[style]),
);
break;
case 'direction':
e.YGNodeStyleSetDirection(
nodeName,
directionValue(e, node.style[style]),
);
break;
case 'flex-direction':
e.YGNodeStyleSetFlexDirection(
nodeName,
flexDirectionValue(e, node.style[style]),
);
break;
case 'justify-content':
e.YGNodeStyleSetJustifyContent(
nodeName,
justifyValue(e, node.style[style]),
);
break;
case 'align-content':
e.YGNodeStyleSetAlignContent(
nodeName,
alignValue(e, node.style[style]),
);
break;
case 'align-items':
e.YGNodeStyleSetAlignItems(
nodeName,
alignValue(e, node.style[style]),
);
break;
case 'align-self':
if (!parent || node.style[style] !== parent.style['align-items']) {
e.YGNodeStyleSetAlignSelf(
nodeName,
alignValue(e, node.style[style]),
);
}
break;
case 'position':
e.YGNodeStyleSetPositionType(
nodeName,
positionValue(e, node.style[style]),
);
break;
case 'flex-wrap':
e.YGNodeStyleSetFlexWrap(nodeName, wrapValue(e, node.style[style]));
break;
case 'overflow':
e.YGNodeStyleSetOverflow(
nodeName,
overflowValue(e, node.style[style]),
);
break;
case 'flex-grow':
e.YGNodeStyleSetFlexGrow(nodeName, node.style[style]);
break;
case 'flex-shrink':
e.YGNodeStyleSetFlexShrink(nodeName, node.style[style]);
break;
case 'flex-basis':
e.YGNodeStyleSetFlexBasis(nodeName, pointValue(e, node.style[style]));
break;
case 'left':
if (genericNode.rawStyle.indexOf('start:') >= 0) {
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeStart,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeLeft,
pointValue(e, node.style[style]),
);
}
break;
case 'top':
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeTop,
pointValue(e, node.style[style]),
);
break;
case 'right':
if (genericNode.rawStyle.indexOf('end:') >= 0) {
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeEnd,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeRight,
pointValue(e, node.style[style]),
);
}
break;
case 'bottom':
e.YGNodeStyleSetPosition(
nodeName,
e.YGEdgeBottom,
pointValue(e, node.style[style]),
);
break;
case 'margin-left':
if (genericNode.rawStyle.indexOf('margin-start:') >= 0) {
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeStart,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeLeft,
pointValue(e, node.style[style]),
);
}
break;
case 'margin-top':
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeTop,
pointValue(e, node.style[style]),
);
break;
case 'margin-right':
if (genericNode.rawStyle.indexOf('margin-end:') >= 0) {
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeEnd,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeRight,
pointValue(e, node.style[style]),
);
}
break;
case 'margin-bottom':
e.YGNodeStyleSetMargin(
nodeName,
e.YGEdgeBottom,
pointValue(e, node.style[style]),
);
break;
case 'padding-left':
if (genericNode.rawStyle.indexOf('padding-start:') >= 0) {
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeStart,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeLeft,
pointValue(e, node.style[style]),
);
}
break;
case 'padding-top':
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeTop,
pointValue(e, node.style[style]),
);
break;
case 'padding-right':
if (genericNode.rawStyle.indexOf('padding-end:') >= 0) {
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeEnd,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeRight,
pointValue(e, node.style[style]),
);
}
break;
case 'padding-bottom':
e.YGNodeStyleSetPadding(
nodeName,
e.YGEdgeBottom,
pointValue(e, node.style[style]),
);
break;
case 'border-left-width':
if (genericNode.rawStyle.indexOf('border-start-width:') >= 0) {
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeStart,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeLeft,
pointValue(e, node.style[style]),
);
}
break;
case 'border-top-width':
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeTop,
pointValue(e, node.style[style]),
);
break;
case 'border-right-width':
if (genericNode.rawStyle.indexOf('border-end-width:') >= 0) {
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeEnd,
pointValue(e, node.style[style]),
);
} else {
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeRight,
pointValue(e, node.style[style]),
);
}
break;
case 'border-bottom-width':
e.YGNodeStyleSetBorder(
nodeName,
e.YGEdgeBottom,
pointValue(e, node.style[style]),
);
break;
case 'width':
e.YGNodeStyleSetWidth(nodeName, pointValue(e, node.style[style]));
break;
case 'min-width':
e.YGNodeStyleSetMinWidth(nodeName, pointValue(e, node.style[style]));
break;
case 'max-width':
e.YGNodeStyleSetMaxWidth(nodeName, pointValue(e, node.style[style]));
break;
case 'height':
e.YGNodeStyleSetHeight(nodeName, pointValue(e, node.style[style]));
break;
case 'min-height':
e.YGNodeStyleSetMinHeight(nodeName, pointValue(e, node.style[style]));
break;
case 'max-height':
e.YGNodeStyleSetMaxHeight(nodeName, pointValue(e, node.style[style]));
break;
case 'display':
e.YGNodeStyleSetDisplay(nodeName, displayValue(e, node.style[style]));
break;
}
}
}
if (parentName) {
e.YGNodeInsertChild(parentName, nodeName, index);
}
for (let i = 0; i < node.children.length; i++) {
e.push('');
const childName = nodeName + '_child' + i;
setupTestTree(
e,
node,
node.children[i],
genericNode.children[i],
childName,
nodeName,
i,
);
}
}
function overflowValue(e, value) {
switch (value) {
case 'visible':
return e.YGOverflowVisible;
case 'hidden':
return e.YGOverflowHidden;
case 'scroll':
return e.YGOverflowScroll;
}
}
function wrapValue(e, value) {
switch (value) {
case 'wrap':
return e.YGWrapWrap;
case 'wrap-reverse':
return e.YGWrapWrapReverse;
case 'nowrap':
return e.YGWrapNoWrap;
}
}
function flexDirectionValue(e, value) {
switch (value) {
case 'row':
return e.YGFlexDirectionRow;
case 'row-reverse':
return e.YGFlexDirectionRowReverse;
case 'column':
return e.YGFlexDirectionColumn;
case 'column-reverse':
return e.YGFlexDirectionColumnReverse;
}
}
function justifyValue(e, value) {
switch (value) {
case 'center':
return e.YGJustifyCenter;
case 'space-around':
return e.YGJustifySpaceAround;
case 'space-between':
return e.YGJustifySpaceBetween;
case 'space-evenly':
return e.YGJustifySpaceEvenly;
case 'flex-start':
return e.YGJustifyFlexStart;
case 'flex-end':
return e.YGJustifyFlexEnd;
}
}
function positionValue(e, value) {
switch (value) {
case 'absolute':
return e.YGPositionTypeAbsolute;
case 'static':
return e.YGPositionTypeStatic;
default:
return e.YGPositionTypeRelative;
}
}
function directionValue(e, value) {
switch (value) {
case 'ltr':
return e.YGDirectionLTR;
case 'rtl':
return e.YGDirectionRTL;
case 'inherit':
return e.YGDirectionInherit;
}
}
function alignValue(e, value) {
switch (value) {
case 'auto':
return e.YGAlignAuto;
case 'center':
return e.YGAlignCenter;
case 'stretch':
return e.YGAlignStretch;
case 'flex-start':
return e.YGAlignFlexStart;
case 'flex-end':
return e.YGAlignFlexEnd;
case 'space-between':
return e.YGAlignSpaceBetween;
case 'space-around':
return e.YGAlignSpaceAround;
case 'space-evenly':
return e.YGAlignSpaceEvenly;
case 'baseline':
return e.YGAlignBaseline;
}
}
function pointValue(e, value) {
switch (value) {
case 'auto':
return e.YGAuto;
case 'undefined':
return e.YGUndefined;
default:
return value;
}
}
function displayValue(e, value) {
switch (value) {
case 'flex':
return e.YGDisplayFlex;
case 'none':
return e.YGDisplayNone;
}
}
const DEFAULT_STYLES = new Map();
function isDefaultStyleValue(style, value) {
let defaultStyle = DEFAULT_STYLES.get(style);
if (defaultStyle == null) {
switch (style) {
case 'position':
defaultStyle = new Set(['static']);
break;
case 'left':
case 'top':
case 'right':
case 'bottom':
case 'start':
case 'end':
defaultStyle = new Set(['undefined']);
break;
case 'min-height':
case 'min-width':
defaultStyle = new Set(['0', '0px', 'auto']);
break;
default: {
const node = document.getElementById('default');
defaultStyle = new Set([getComputedStyle(node, null)[style]]);
break;
}
}
DEFAULT_STYLES.set(style, defaultStyle);
}
return DEFAULT_STYLES.get(style).has(value);
}
function getRoundedSize(node) {
const boundingRect = node.getBoundingClientRect();
return {
width: Math.round(boundingRect.right) - Math.round(boundingRect.left),
height: Math.round(boundingRect.bottom) - Math.round(boundingRect.top),
};
}
function calculateTree(root, parentOffsetLeft, parentOffsetTop) {
const rootLayout = [];
for (let i = 0; i < root.children.length; i++) {
const child = root.children[i];
const boundingRect = child.getBoundingClientRect();
const layout = {
name: child.id !== '' ? child.id : 'INSERT_NAME_HERE',
left: Math.round(boundingRect.left - parentOffsetLeft),
top: Math.round(boundingRect.top - parentOffsetTop),
width: child.offsetWidth,
height: child.offsetHeight,
children: calculateTree(child, boundingRect.left, boundingRect.top),
style: getYogaStyle(child),
declaredStyle: child.style,
rawStyle: child.getAttribute('style'),
experiments: child.dataset.experiments
? child.dataset.experiments.split(' ')
: DEFAULT_EXPERIMENTS,
disabled: child.dataset.disabled === 'true',
};
const size = getRoundedSize(child);
layout.width = size.width;
layout.height = size.height;
rootLayout.push(layout);
}
return rootLayout;
}
function getYogaStyle(node) {
// TODO: Relying on computed style means we cannot test shorthand props like
// "padding", "margin", "gap", or negative values.
return [
'direction',
'flex-direction',
'justify-content',
'align-content',
'align-items',
'align-self',
'position',
'flex-wrap',
'overflow',
'flex-grow',
'flex-shrink',
'flex-basis',
'left',
'top',
'right',
'bottom',
'margin-left',
'margin-top',
'margin-right',
'margin-bottom',
'padding-left',
'padding-top',
'padding-right',
'padding-bottom',
'border-left-width',
'border-top-width',
'border-right-width',
'border-bottom-width',
'width',
'min-width',
'max-width',
'height',
'min-height',
'max-height',
'column-gap',
'row-gap',
'display',
'aspect-ratio',
].reduce((map, key) => {
map[key] =
node.style[key] || getComputedStyle(node, null).getPropertyValue(key);
return map;
}, {});
}
const Emitter = function (lang, indent) {
this.lang = lang;
this.indent = indent;
this.indents = [];
this.lines = [];
};
Emitter.prototype = Object.create(Object.prototype, {
constructor: {value: Emitter},
pushIndent: {
value: function () {
this.indents.push(this.indent);
},
},
popIndent: {
value: function () {
this.indents.pop();
},
},
push: {
value: function (line) {
if (line instanceof Array) {
line.forEach(function (element) {
this.push(element);
}, this);
return;
} else if (line.length > 0) {
line = this.indents.join('') + line;
}
this.lines.push(line);
},
},
print: {
value: function () {
console.log(this.lines.join('\n'));
},
},
});