Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
255 changes: 255 additions & 0 deletions tests/differential-fuzz.fuzz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,7 @@ function layoutsMatch(
interface NodeStyle {
width?: number
height?: number
widthPercent?: number
flexDirection?: number
flexGrow?: number
flexShrink?: number
Expand All @@ -174,6 +175,8 @@ interface NodeStyle {
gap?: number
positionType?: number
positionEdges?: { edge: number; value: number }[]
measureContentWidth?: number
measureCustomHeight?: number
}

interface TreeSpec {
Expand Down Expand Up @@ -351,6 +354,7 @@ function generateNestedTree(ctx: RandomContext, outerChildCount: number, innerCh
function applyStyleToFlexilyNode(node: Flexily.Node, style: NodeStyle): void {
if (style.width !== undefined) node.setWidth(style.width)
if (style.height !== undefined) node.setHeight(style.height)
if (style.widthPercent !== undefined) node.setWidthPercent(style.widthPercent)
if (style.flexDirection !== undefined) {
node.setFlexDirection(style.flexDirection)
}
Expand All @@ -371,11 +375,23 @@ function applyStyleToFlexilyNode(node: Flexily.Node, style: NodeStyle): void {
node.setPosition(edge, value)
}
}
if (style.measureContentWidth !== undefined && style.measureContentWidth > 0) {
const contentWidth = style.measureContentWidth
node.setMeasureFunc((_w: number, wm: number) => {
if (wm === Flexily.MEASURE_MODE_UNDEFINED) {
return { width: contentWidth, height: 1 }
}
if (_w >= contentWidth) return { width: contentWidth, height: 1 }
const customHeight = style.measureCustomHeight ?? 35
return { width: Math.min(contentWidth, _w), height: customHeight }
})
}
}

function applyStyleToYogaNode(node: YogaNode, style: NodeStyle): void {
if (style.width !== undefined) node.setWidth(style.width)
if (style.height !== undefined) node.setHeight(style.height)
if (style.widthPercent !== undefined) node.setWidthPercent(style.widthPercent)
if (style.flexDirection !== undefined) {
node.setFlexDirection(style.flexDirection as FlexDirection)
}
Expand All @@ -396,6 +412,17 @@ function applyStyleToYogaNode(node: YogaNode, style: NodeStyle): void {
node.setPosition(edge as Edge, value)
}
}
if (style.measureContentWidth !== undefined && style.measureContentWidth > 0) {
const contentWidth = style.measureContentWidth
node.setMeasureFunc((_w: number, wm: number) => {
if (wm === yoga.MEASURE_MODE_UNDEFINED) {
return { width: contentWidth, height: 1 }
}
if (_w >= contentWidth) return { width: contentWidth, height: 1 }
const customHeight = style.measureCustomHeight ?? 35
return { width: Math.min(contentWidth, _w), height: customHeight }
})
}
}

function buildFlexilyTree(spec: TreeSpec): Flexily.Node {
Expand Down Expand Up @@ -831,6 +858,234 @@ describe("Fuzz: Stress Test (50 Nested)", () => {
}
})

// ============================================================================
// Tests: Percent Width + MeasureFunc Leaf
// ============================================================================

/**
* Generates trees where leaf nodes have widthPercent + measureFunc.
*/
function generatePercentMeasureTree(ctx: RandomContext, childCount: number): TreeSpec {
const rootWidth = randomInt(ctx, 100, 500)
const rootHeight = randomInt(ctx, 80, 400)
const flexDirection = randomChoice(ctx, [Flexily.FLEX_DIRECTION_ROW, Flexily.FLEX_DIRECTION_COLUMN])

const children: TreeSpec[] = []
for (let i = 0; i < childCount; i++) {
const isPercent = randomBool(ctx, 0.7)
const hasMeasure = randomBool(ctx, 0.7)
const childStyle: NodeStyle = {}

if (isPercent) {
// widthPercent from 10% to 90%
childStyle.widthPercent = randomInt(ctx, 10, 90)
}

if (hasMeasure) {
// Intrinsic content wider than the resolved percent to force wrapping.
// contentWidth is 1.5x–4x the expected resolved width.
const pct = childStyle.widthPercent ?? 50
const resolvedEstimate = Math.floor((rootWidth * pct) / 100)
childStyle.measureContentWidth = randomInt(ctx, Math.floor(resolvedEstimate * 1.5), resolvedEstimate * 4)
childStyle.measureCustomHeight = randomInt(ctx, 10, 100)
} else if (!isPercent) {
// Fixed dimensions for non-percent, non-measure children
if (flexDirection === Flexily.FLEX_DIRECTION_ROW) {
childStyle.width = randomInt(ctx, 20, Math.floor(rootWidth / childCount) - 10)
childStyle.height = randomInt(ctx, 20, rootHeight - 20)
} else {
childStyle.width = randomInt(ctx, 20, rootWidth - 20)
childStyle.height = randomInt(ctx, 20, Math.floor(rootHeight / childCount) - 10)
}
}

// Optional flexGrow (but not always — we want to test pure percent resolution too)
if (randomBool(ctx, 0.3)) {
childStyle.flexGrow = randomInt(ctx, 1, 3)
}

// Optional padding/margin
if (randomBool(ctx, 0.3)) {
childStyle.padding = randomInt(ctx, 1, 8)
}
if (randomBool(ctx, 0.2)) {
childStyle.margin = randomInt(ctx, 1, 5)
}

children.push({ style: childStyle, children: [] })
}

// Sort: non-percent children first, then percent children.
// This creates more interesting scenarios (remaining space after fixed siblings).
children.sort((a, b) => {
const aPct = a.style.widthPercent !== undefined ? 1 : 0
const bPct = b.style.widthPercent !== undefined ? 1 : 0
return aPct - bPct
})

return {
style: {
width: rootWidth,
height: rootHeight,
flexDirection,
padding: randomBool(ctx, 0.3) ? randomInt(ctx, 5, 15) : undefined,
gap: randomBool(ctx, 0.3) ? randomInt(ctx, 2, 10) : undefined,
alignItems: randomBool(ctx, 0.3)
? randomChoice(ctx, [Flexily.ALIGN_FLEX_START, Flexily.ALIGN_CENTER, Flexily.ALIGN_STRETCH])
: undefined,
},
children,
}
}

/**
* Nested variant: root → intermediate flex containers → percent measure leaves.
* Tests percent resolution through multiple layout passes (flexGrow parent → child).
*/
function generatePercentMeasureNestedTree(ctx: RandomContext, outerCount: number, leafCount: number): TreeSpec {
const rootWidth = randomInt(ctx, 200, 500)
const rootHeight = randomInt(ctx, 150, 400)
const rootDirection = randomChoice(ctx, [Flexily.FLEX_DIRECTION_ROW, Flexily.FLEX_DIRECTION_COLUMN])

const children: TreeSpec[] = []
for (let i = 0; i < outerCount; i++) {
const innerDirection = randomChoice(ctx, [Flexily.FLEX_DIRECTION_ROW, Flexily.FLEX_DIRECTION_COLUMN])

const leaves: TreeSpec[] = []
for (let j = 0; j < leafCount; j++) {
const pct = randomInt(ctx, 10, 50)
const resolvedEstimate = Math.floor((rootWidth * pct) / 100)
leaves.push({
style: {
widthPercent: pct,
measureContentWidth: randomInt(ctx, Math.floor(resolvedEstimate * 1.5), resolvedEstimate * 4),
measureCustomHeight: randomInt(ctx, 10, 100),
flexGrow: randomBool(ctx, 0.3) ? 1 : undefined,
},
children: [],
})
}

children.push({
style: {
flexGrow: 1,
flexDirection: innerDirection,
padding: randomBool(ctx, 0.3) ? randomInt(ctx, 5, 10) : undefined,
gap: randomBool(ctx, 0.3) ? randomInt(ctx, 2, 6) : undefined,
},
children: leaves,
})
}

return {
style: {
width: rootWidth,
height: rootHeight,
flexDirection: rootDirection,
padding: randomBool(ctx, 0.3) ? randomInt(ctx, 5, 15) : undefined,
gap: randomBool(ctx, 0.3) ? randomInt(ctx, 5, 10) : undefined,
},
children,
}
}

// ============================================================================
// Percent Measure Test Runners
// ============================================================================

function runPercentMeasureTest(seed: number, childCount: number): { passed: boolean; diff?: string } {
const rng = createRng(seed)
const ctx: RandomContext = { rng }
const spec = generatePercentMeasureTree(ctx, childCount)

const flexilyRoot = buildFlexilyTree(spec)
const yogaRoot = buildYogaTree(spec)

const rootWidth = spec.style.width ?? 300
const rootHeight = spec.style.height ?? 200

flexilyRoot.calculateLayout(rootWidth, rootHeight, Flexily.DIRECTION_LTR)
yogaRoot.calculateLayout(rootWidth, rootHeight, yoga.DIRECTION_LTR)

const flexilyLayout = getFlexilyLayout(flexilyRoot)
const yogaLayout = getYogaLayout(yogaRoot)
yogaRoot.freeRecursive()

const result = layoutsMatch(flexilyLayout, yogaLayout)
return { passed: result.match, diff: result.diff }
}

function runPercentMeasureNestedTest(seed: number, outer: number, leaves: number): { passed: boolean; diff?: string } {
const rng = createRng(seed)
const ctx: RandomContext = { rng }
const spec = generatePercentMeasureNestedTree(ctx, outer, leaves)

const flexilyRoot = buildFlexilyTree(spec)
const yogaRoot = buildYogaTree(spec)

const rootWidth = spec.style.width ?? 300
const rootHeight = spec.style.height ?? 200

flexilyRoot.calculateLayout(rootWidth, rootHeight, Flexily.DIRECTION_LTR)
yogaRoot.calculateLayout(rootWidth, rootHeight, yoga.DIRECTION_LTR)

const flexilyLayout = getFlexilyLayout(flexilyRoot)
const yogaLayout = getYogaLayout(yogaRoot)
yogaRoot.freeRecursive()

const result = layoutsMatch(flexilyLayout, yogaLayout)
return { passed: result.match, diff: result.diff }
}

// ============================================================================
// Tests: Percent Measure Leaf (Flat)
// ============================================================================

describe("Fuzz: Percent Measure Leaf (Row)", () => {
for (let seed = 12000; seed < 12050; seed++) {
const childCount = 3 + (seed % 4)
it(`seed=${seed} children=${childCount}`, () => {
const result = runPercentMeasureTest(seed, childCount)
if (!result.passed) {
log.debug?.(`Difference: ${result.diff}`)
}
expect(result.passed).toBe(true)
})
}
})

describe("Fuzz: Percent Measure Leaf (Column)", () => {
for (let seed = 13000; seed < 13050; seed++) {
const childCount = 3 + (seed % 4)
it(`seed=${seed} children=${childCount}`, () => {
const result = runPercentMeasureTest(seed, childCount)
expect(result.passed).toBe(true)
})
}
})

// ============================================================================
// Tests: Percent Measure Leaf (Nested)
// ============================================================================

describe("Fuzz: Percent Measure Leaf (Nested 2x2)", () => {
for (let seed = 14000; seed < 14030; seed++) {
it(`seed=${seed}`, () => {
const result = runPercentMeasureNestedTest(seed, 2, 2)
expect(result.passed).toBe(true)
})
}
})

describe("Fuzz: Percent Measure Leaf (Nested 3x3)", () => {
for (let seed = 14500; seed < 14530; seed++) {
it(`seed=${seed}`, () => {
const result = runPercentMeasureNestedTest(seed, 3, 3)
expect(result.passed).toBe(true)
})
}
})

// ============================================================================
// Summary
// ============================================================================
Expand Down
Loading