Skip to main content
Glama
1yhy
by 1yhy
layout.test.ts58.2 kB
/** * Layout Detection Algorithm Unit Tests * * Tests the layout detection algorithm for inferring Flexbox layouts * from absolutely positioned Figma elements. */ import { describe, it, expect, beforeAll } from "vitest"; import * as fs from "fs"; import * as path from "path"; import { fileURLToPath } from "url"; import { extractBoundingBox, toElementRect, groupIntoRows, groupIntoColumns, analyzeGaps, analyzeAlignment, calculateBounds, clusterValues, detectGridLayout, calculateIoU, classifyOverlap, detectOverlappingElements, detectBackgroundElement, LayoutOptimizer, analyzeHomogeneity, filterHomogeneousForGrid, type ElementRect, type BoundingBox, } from "~/algorithms/layout/index.js"; import type { SimplifiedNode } from "~/types/index.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const fixturesPath = path.join(__dirname, "../../fixtures"); // Test data types interface FigmaNode { id: string; name: string; type: string; absoluteBoundingBox?: BoundingBox; children?: FigmaNode[]; } // Load test fixture function loadTestData(): FigmaNode { const dataPath = path.join(fixturesPath, "figma-data/real-node-data.json"); const rawData = JSON.parse(fs.readFileSync(dataPath, "utf-8")); const nodeKey = Object.keys(rawData.nodes)[0]; return rawData.nodes[nodeKey].document; } // Extract child elements with bounding boxes function extractChildElements(node: FigmaNode): ElementRect[] { if (!node.children) return []; return node.children .filter((child) => child.absoluteBoundingBox) .map((child, index) => { const box = child.absoluteBoundingBox!; return toElementRect(box, index); }); } describe("Layout Detection Algorithm", () => { let testData: FigmaNode; beforeAll(() => { testData = loadTestData(); }); describe("Bounding Box Extraction", () => { it("should extract bounding box from CSS styles", () => { const cssStyles = { left: "100px", top: "200px", width: "300px", height: "400px" }; const box = extractBoundingBox(cssStyles); expect(box).toBeDefined(); expect(box?.x).toBe(100); expect(box?.y).toBe(200); expect(box?.width).toBe(300); expect(box?.height).toBe(400); }); it("should convert to ElementRect correctly", () => { const box: BoundingBox = { x: 100, y: 200, width: 300, height: 400 }; const rect = toElementRect(box, 0); expect(rect.index).toBe(0); expect(rect.x).toBe(100); expect(rect.y).toBe(200); expect(rect.width).toBe(300); expect(rect.height).toBe(400); expect(rect.right).toBe(400); expect(rect.bottom).toBe(600); expect(rect.centerX).toBe(250); expect(rect.centerY).toBe(400); }); }); describe("Row Grouping (Y-axis overlap)", () => { it("should group horizontally aligned elements into same row", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 10, width: 50, height: 30 }, 0), toElementRect({ x: 60, y: 15, width: 50, height: 30 }, 1), toElementRect({ x: 120, y: 12, width: 50, height: 30 }, 2), ]; const rows = groupIntoRows(elements); expect(rows.length).toBe(1); expect(rows[0].length).toBe(3); }); it("should separate vertically stacked elements into different rows", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 30 }, 0), toElementRect({ x: 0, y: 50, width: 100, height: 30 }, 1), toElementRect({ x: 0, y: 100, width: 100, height: 30 }, 2), ]; const rows = groupIntoRows(elements); // Elements don't overlap on Y-axis, expect multiple rows expect(rows.length).toBeGreaterThanOrEqual(1); }); }); describe("Column Grouping (X-axis overlap)", () => { it("should group vertically aligned elements into same column", () => { const elements: ElementRect[] = [ toElementRect({ x: 10, y: 0, width: 30, height: 50 }, 0), toElementRect({ x: 15, y: 60, width: 30, height: 50 }, 1), toElementRect({ x: 12, y: 120, width: 30, height: 50 }, 2), ]; const columns = groupIntoColumns(elements); expect(columns.length).toBe(1); expect(columns[0].length).toBe(3); }); }); describe("Gap Analysis", () => { it("should detect consistent gaps", () => { const gaps = [16, 16, 16, 16]; const result = analyzeGaps(gaps); expect(result.isConsistent).toBe(true); expect(result.average).toBe(16); }); it("should detect inconsistent gaps", () => { const gaps = [10, 30, 15, 40]; const result = analyzeGaps(gaps); expect(result.isConsistent).toBe(false); }); it("should handle gaps with small variance", () => { const gaps = [15, 16, 17, 16]; const result = analyzeGaps(gaps); expect(result.isConsistent).toBe(true); }); }); describe("Alignment Detection", () => { it("should return alignment object with horizontal and vertical properties", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 30 }, 0), toElementRect({ x: 0, y: 40, width: 150, height: 30 }, 1), toElementRect({ x: 0, y: 80, width: 80, height: 30 }, 2), ]; const bounds: BoundingBox = { x: 0, y: 0, width: 200, height: 110 }; const alignment = analyzeAlignment(elements, bounds); expect(alignment).toHaveProperty("horizontal"); expect(alignment).toHaveProperty("vertical"); }); it("should analyze horizontal alignment", () => { const elements: ElementRect[] = [ toElementRect({ x: 50, y: 0, width: 100, height: 30 }, 0), toElementRect({ x: 25, y: 40, width: 150, height: 30 }, 1), toElementRect({ x: 60, y: 80, width: 80, height: 30 }, 2), ]; const bounds: BoundingBox = { x: 0, y: 0, width: 200, height: 110 }; const alignment = analyzeAlignment(elements, bounds); expect(typeof alignment.horizontal).toBe("string"); }); it("should analyze vertical alignment", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 50, height: 100 }, 0), toElementRect({ x: 60, y: 0, width: 50, height: 80 }, 1), toElementRect({ x: 120, y: 0, width: 50, height: 120 }, 2), ]; const bounds: BoundingBox = { x: 0, y: 0, width: 170, height: 120 }; const alignment = analyzeAlignment(elements, bounds); expect(typeof alignment.vertical).toBe("string"); }); }); describe("Bounds Calculation", () => { it("should calculate correct bounds for multiple elements", () => { const elements: ElementRect[] = [ toElementRect({ x: 10, y: 20, width: 50, height: 30 }, 0), toElementRect({ x: 100, y: 5, width: 40, height: 60 }, 1), ]; const bounds = calculateBounds(elements); expect(bounds.x).toBe(10); expect(bounds.y).toBe(5); expect(bounds.width).toBe(130); expect(bounds.height).toBe(60); }); it("should handle empty array", () => { const bounds = calculateBounds([]); expect(bounds.x).toBe(0); expect(bounds.y).toBe(0); expect(bounds.width).toBe(0); expect(bounds.height).toBe(0); }); }); describe("Real Figma Data", () => { it("should process real Figma node data", () => { expect(testData).toBeDefined(); expect(testData.type).toBe("GROUP"); expect(testData.children).toBeDefined(); }); it("should extract child elements from real data", () => { const elements = extractChildElements(testData); expect(elements.length).toBeGreaterThan(0); elements.forEach((el) => { expect(typeof el.x).toBe("number"); expect(typeof el.width).toBe("number"); expect(typeof el.index).toBe("number"); }); }); }); describe("Value Clustering", () => { it("should cluster similar values together", () => { const values = [10, 12, 11, 50, 51, 52, 100, 101]; const clusters = clusterValues(values, 3); expect(clusters.length).toBe(3); expect(clusters[0].count).toBe(3); // 10, 11, 12 expect(clusters[1].count).toBe(3); // 50, 51, 52 expect(clusters[2].count).toBe(2); // 100, 101 }); it("should handle empty array", () => { const clusters = clusterValues([]); expect(clusters.length).toBe(0); }); it("should handle single value", () => { const clusters = clusterValues([42]); expect(clusters.length).toBe(1); expect(clusters[0].center).toBe(42); }); it("should separate distant values", () => { const values = [0, 100, 200, 300]; const clusters = clusterValues(values, 3); expect(clusters.length).toBe(4); }); }); describe("Grid Detection", () => { it("should detect a perfect 2x3 grid", () => { // 2 rows, 3 columns const elements: ElementRect[] = [ // Row 1 toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 240, y: 0, width: 100, height: 50 }, 2), // Row 2 toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 3), toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 4), toElementRect({ x: 240, y: 70, width: 100, height: 50 }, 5), ]; const result = detectGridLayout(elements); expect(result.isGrid).toBe(true); expect(result.rowCount).toBe(2); expect(result.columnCount).toBe(3); expect(result.confidence).toBeGreaterThanOrEqual(0.6); }); it("should detect a 3x2 grid", () => { // 3 rows, 2 columns const elements: ElementRect[] = [ // Row 1 toElementRect({ x: 0, y: 0, width: 80, height: 40 }, 0), toElementRect({ x: 100, y: 0, width: 80, height: 40 }, 1), // Row 2 toElementRect({ x: 0, y: 60, width: 80, height: 40 }, 2), toElementRect({ x: 100, y: 60, width: 80, height: 40 }, 3), // Row 3 toElementRect({ x: 0, y: 120, width: 80, height: 40 }, 4), toElementRect({ x: 100, y: 120, width: 80, height: 40 }, 5), ]; const result = detectGridLayout(elements); expect(result.isGrid).toBe(true); expect(result.rowCount).toBe(3); expect(result.columnCount).toBe(2); }); it("should calculate consistent row and column gaps", () => { const elements: ElementRect[] = [ // Row 1 (gap = 20) toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), // Row 2 (row gap = 16) toElementRect({ x: 0, y: 66, width: 100, height: 50 }, 2), toElementRect({ x: 120, y: 66, width: 100, height: 50 }, 3), ]; const result = detectGridLayout(elements); expect(result.isGrid).toBe(true); expect(result.rowGap).toBe(16); expect(result.columnGap).toBe(20); }); it("should generate track widths and heights", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 96, height: 64 }, 0), // Using common values toElementRect({ x: 120, y: 0, width: 80, height: 64 }, 1), toElementRect({ x: 0, y: 84, width: 96, height: 40 }, 2), toElementRect({ x: 120, y: 84, width: 80, height: 40 }, 3), ]; const result = detectGridLayout(elements); expect(result.isGrid).toBe(true); expect(result.trackWidths.length).toBe(2); expect(result.trackHeights.length).toBe(2); // Track widths should be max of each column (rounded to common values) expect(result.trackWidths[0]).toBe(96); expect(result.trackWidths[1]).toBe(80); // Track heights should be max of each row expect(result.trackHeights[0]).toBe(64); expect(result.trackHeights[1]).toBe(40); }); it("should build correct cell map", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 50, height: 50 }, 0), toElementRect({ x: 60, y: 0, width: 50, height: 50 }, 1), toElementRect({ x: 0, y: 60, width: 50, height: 50 }, 2), toElementRect({ x: 60, y: 60, width: 50, height: 50 }, 3), ]; const result = detectGridLayout(elements); expect(result.cellMap.length).toBe(2); // 2 rows expect(result.cellMap[0].length).toBe(2); // 2 columns expect(result.cellMap[0][0]).toBe(0); expect(result.cellMap[0][1]).toBe(1); expect(result.cellMap[1][0]).toBe(2); expect(result.cellMap[1][1]).toBe(3); }); it("should NOT detect grid for single row (flex row instead)", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 240, y: 0, width: 100, height: 50 }, 2), ]; const result = detectGridLayout(elements); expect(result.isGrid).toBe(false); }); it("should detect lower confidence for misaligned columns", () => { const elements: ElementRect[] = [ // Row 1: columns at x=0, x=120 toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), // Row 2: columns at x=50, x=200 (misaligned!) toElementRect({ x: 50, y: 70, width: 100, height: 50 }, 2), toElementRect({ x: 200, y: 70, width: 100, height: 50 }, 3), ]; const result = detectGridLayout(elements); // Misaligned columns should result in 4 column positions detected // and lower confidence due to alignment issues expect(result.columnCount).toBeGreaterThan(2); }); it("should NOT detect grid for too few elements", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), ]; const result = detectGridLayout(elements); // 3 elements is not enough for a meaningful grid expect(result.isGrid).toBe(false); }); it("should handle grid with varying element sizes in same column", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 150, height: 50 }, 1), // wider toElementRect({ x: 0, y: 70, width: 100, height: 80 }, 2), // taller toElementRect({ x: 120, y: 70, width: 150, height: 80 }, 3), ]; const result = detectGridLayout(elements); expect(result.isGrid).toBe(true); // Track widths should use max width in column expect(result.trackWidths[1]).toBe(150); }); }); describe("LayoutOptimizer Grid Integration", () => { // Helper to create SimplifiedNode from position/size function createNode( id: string, left: number, top: number, width: number, height: number, ): SimplifiedNode { return { id, name: `Node ${id}`, type: "FRAME", cssStyles: { left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px`, }, }; } it("should detect and apply Grid CSS for 2x2 grid container", () => { const container: SimplifiedNode = { id: "container", name: "Grid Container", type: "FRAME", cssStyles: { width: "260px", height: "140px", }, children: [ createNode("1", 0, 0, 100, 50), createNode("2", 120, 0, 100, 50), createNode("3", 0, 70, 100, 50), createNode("4", 120, 70, 100, 50), ], }; const result = LayoutOptimizer.optimizeContainer(container); expect(result.cssStyles?.display).toBe("grid"); expect(result.cssStyles?.gridTemplateColumns).toBeDefined(); }); it("should apply gap for Grid layout", () => { const container: SimplifiedNode = { id: "container", name: "Grid Container", type: "FRAME", cssStyles: { width: "260px", height: "140px", }, children: [ createNode("1", 0, 0, 100, 50), createNode("2", 120, 0, 100, 50), // 20px column gap createNode("3", 0, 70, 100, 50), // 20px row gap createNode("4", 120, 70, 100, 50), ], }; const result = LayoutOptimizer.optimizeContainer(container); expect(result.cssStyles?.display).toBe("grid"); // Should have gap property expect( result.cssStyles?.gap || result.cssStyles?.rowGap || result.cssStyles?.columnGap, ).toBeDefined(); }); it("should fall back to flex for single row", () => { const container: SimplifiedNode = { id: "container", name: "Row Container", type: "FRAME", cssStyles: { width: "260px", height: "50px", }, children: [ createNode("1", 0, 0, 100, 50), createNode("2", 120, 0, 100, 50), createNode("3", 240, 0, 100, 50), ], }; const result = LayoutOptimizer.optimizeContainer(container); // Should be flex, not grid (single row) expect(result.cssStyles?.display).toBe("flex"); expect(result.cssStyles?.gridTemplateColumns).toBeUndefined(); }); it("should generate correct gridTemplateColumns", () => { // Use common design values (96, 80, 64) that don't get rounded const container: SimplifiedNode = { id: "container", name: "Grid Container", type: "FRAME", cssStyles: { width: "340px", height: "140px", }, children: [ createNode("1", 0, 0, 96, 48), createNode("2", 116, 0, 80, 48), // different width column createNode("3", 216, 0, 96, 48), createNode("4", 0, 68, 96, 48), createNode("5", 116, 68, 80, 48), createNode("6", 216, 68, 96, 48), ], }; const result = LayoutOptimizer.optimizeContainer(container); expect(result.cssStyles?.display).toBe("grid"); expect(result.cssStyles?.gridTemplateColumns).toContain("96px"); expect(result.cssStyles?.gridTemplateColumns).toContain("80px"); }); }); // ==================== IoU and Overlap Detection Tests ==================== describe("IoU (Intersection over Union) Calculation", () => { it("should return 0 for non-overlapping elements", () => { const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0); const b = toElementRect({ x: 200, y: 0, width: 100, height: 100 }, 1); const iou = calculateIoU(a, b); expect(iou).toBe(0); }); it("should return 1 for identical elements", () => { const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0); const b = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 1); const iou = calculateIoU(a, b); expect(iou).toBe(1); }); it("should return correct IoU for partial overlap", () => { // 50% overlap on x-axis const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0); const b = toElementRect({ x: 50, y: 0, width: 100, height: 100 }, 1); const iou = calculateIoU(a, b); // Intersection: 50 * 100 = 5000 // Union: 100*100 + 100*100 - 5000 = 15000 // IoU = 5000 / 15000 = 0.333... expect(iou).toBeCloseTo(0.333, 2); }); it("should handle completely contained element", () => { const outer = toElementRect({ x: 0, y: 0, width: 200, height: 200 }, 0); const inner = toElementRect({ x: 50, y: 50, width: 100, height: 100 }, 1); const iou = calculateIoU(outer, inner); // Intersection: 100 * 100 = 10000 // Union: 200*200 + 100*100 - 10000 = 40000 + 10000 - 10000 = 40000 // IoU = 10000 / 40000 = 0.25 expect(iou).toBeCloseTo(0.25, 2); }); }); describe("Overlap Classification", () => { it("should classify non-overlapping elements as 'none'", () => { const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0); const b = toElementRect({ x: 200, y: 0, width: 100, height: 100 }, 1); const result = classifyOverlap(a, b); expect(result).toBe("none"); }); it("should classify adjacent elements as 'adjacent'", () => { const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0); const b = toElementRect({ x: 101, y: 0, width: 100, height: 100 }, 1); // 1px gap const result = classifyOverlap(a, b); expect(result).toBe("adjacent"); }); it("should classify small overlap as 'partial'", () => { const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0); const b = toElementRect({ x: 95, y: 0, width: 100, height: 100 }, 1); // 5% overlap const result = classifyOverlap(a, b); expect(result).toBe("partial"); }); it("should classify significant overlap as 'significant'", () => { const a = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0); const b = toElementRect({ x: 50, y: 0, width: 100, height: 100 }, 1); // ~33% IoU const result = classifyOverlap(a, b); expect(result).toBe("significant"); }); it("should classify contained element as 'contained'", () => { const outer = toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0); const inner = toElementRect({ x: 10, y: 10, width: 80, height: 80 }, 1); const result = classifyOverlap(outer, inner); expect(result).toBe("contained"); }); }); describe("Overlap Detection", () => { it("should separate overlapping elements from flow elements", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0), toElementRect({ x: 50, y: 50, width: 100, height: 100 }, 1), // Overlaps with 0 toElementRect({ x: 300, y: 0, width: 100, height: 100 }, 2), // No overlap toElementRect({ x: 450, y: 0, width: 100, height: 100 }, 3), // No overlap ]; const result = detectOverlappingElements(elements, 0.1); expect(result.stackedElements.length).toBe(2); // Elements 0 and 1 expect(result.flowElements.length).toBe(2); // Elements 2 and 3 expect(result.stackedIndices.has(0)).toBe(true); expect(result.stackedIndices.has(1)).toBe(true); }); it("should return all elements as flow when no overlap", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0), toElementRect({ x: 150, y: 0, width: 100, height: 100 }, 1), toElementRect({ x: 300, y: 0, width: 100, height: 100 }, 2), ]; const result = detectOverlappingElements(elements, 0.1); expect(result.stackedElements.length).toBe(0); expect(result.flowElements.length).toBe(3); }); it("should detect multiple overlapping pairs", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0), toElementRect({ x: 50, y: 0, width: 100, height: 100 }, 1), // Overlaps with 0 toElementRect({ x: 300, y: 0, width: 100, height: 100 }, 2), toElementRect({ x: 350, y: 0, width: 100, height: 100 }, 3), // Overlaps with 2 ]; const result = detectOverlappingElements(elements, 0.1); expect(result.stackedElements.length).toBe(4); // All overlap with at least one }); it("should use custom IoU threshold", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 100 }, 0), toElementRect({ x: 80, y: 0, width: 100, height: 100 }, 1), // ~11% IoU ]; // With 0.1 threshold, should detect overlap const result1 = detectOverlappingElements(elements, 0.1); expect(result1.stackedElements.length).toBe(2); // With 0.5 threshold, should NOT detect overlap const result2 = detectOverlappingElements(elements, 0.5); expect(result2.stackedElements.length).toBe(0); }); }); describe("Child Style Cleanup", () => { // Helper to create SimplifiedNode with absolute positioning function createAbsoluteNode( id: string, left: number, top: number, width: number, height: number, ): SimplifiedNode { return { id, name: `Node ${id}`, type: "FRAME", cssStyles: { position: "absolute", left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px`, }, }; } it("should remove position:absolute from children when parent becomes flex", () => { const child = createAbsoluteNode("1", 0, 0, 100, 50); const cleaned = LayoutOptimizer.cleanChildStylesForLayout(child, "flex"); expect(cleaned.cssStyles?.position).toBeUndefined(); }); it("should remove left/top from children when parent becomes flex", () => { const child = createAbsoluteNode("1", 100, 200, 100, 50); const cleaned = LayoutOptimizer.cleanChildStylesForLayout(child, "flex"); expect(cleaned.cssStyles?.left).toBeUndefined(); expect(cleaned.cssStyles?.top).toBeUndefined(); }); it("should keep width/height for flex children", () => { const child = createAbsoluteNode("1", 0, 0, 100, 50); const cleaned = LayoutOptimizer.cleanChildStylesForLayout(child, "flex"); expect(cleaned.cssStyles?.width).toBe("100px"); expect(cleaned.cssStyles?.height).toBe("50px"); }); it("should remove all position properties for grid children", () => { const child: SimplifiedNode = { id: "1", name: "Node 1", type: "FRAME", cssStyles: { position: "absolute", left: "10px", top: "20px", right: "30px", bottom: "40px", width: "100px", height: "50px", }, }; const cleaned = LayoutOptimizer.cleanChildStylesForLayout(child, "grid"); expect(cleaned.cssStyles?.position).toBeUndefined(); expect(cleaned.cssStyles?.left).toBeUndefined(); expect(cleaned.cssStyles?.top).toBeUndefined(); expect(cleaned.cssStyles?.right).toBeUndefined(); expect(cleaned.cssStyles?.bottom).toBeUndefined(); }); it("should skip cleaning for stacked elements", () => { const children: SimplifiedNode[] = [ createAbsoluteNode("1", 0, 0, 100, 50), createAbsoluteNode("2", 50, 0, 100, 50), // Overlapping ]; // Mark index 0 and 1 as stacked const stackedIndices = new Set([0, 1]); const cleaned = LayoutOptimizer.cleanChildrenStyles(children, "flex", stackedIndices); // Stacked elements should keep their absolute positioning expect(cleaned[0].cssStyles?.position).toBe("absolute"); expect(cleaned[1].cssStyles?.position).toBe("absolute"); }); it("should remove default CSS values", () => { const styles = { fontWeight: "400", textAlign: "left", opacity: "1", backgroundColor: "transparent", width: "100px", color: "#000", }; const cleaned = LayoutOptimizer.removeDefaultValues(styles); expect(cleaned.fontWeight).toBeUndefined(); expect(cleaned.textAlign).toBeUndefined(); expect(cleaned.opacity).toBeUndefined(); expect(cleaned.backgroundColor).toBeUndefined(); expect(cleaned.width).toBe("100px"); // Keep non-default expect(cleaned.color).toBe("#000"); // Keep non-default }); it("should remove 0px position values", () => { const styles = { left: "0px", top: "0", right: "10px", width: "100px", }; const cleaned = LayoutOptimizer.removeDefaultValues(styles); expect(cleaned.left).toBeUndefined(); expect(cleaned.top).toBeUndefined(); expect(cleaned.right).toBe("10px"); // Keep non-zero expect(cleaned.width).toBe("100px"); }); }); describe("Integrated Overlap and Cleanup in optimizeContainer", () => { function createAbsoluteNode( id: string, left: number, top: number, width: number, height: number, ): SimplifiedNode { return { id, name: `Node ${id}`, type: "FRAME", cssStyles: { position: "absolute", left: `${left}px`, top: `${top}px`, width: `${width}px`, height: `${height}px`, }, }; } it("should clean child styles when parent becomes flex", () => { const container: SimplifiedNode = { id: "container", name: "Flex Container", type: "FRAME", cssStyles: { width: "500px", height: "100px" }, children: [ createAbsoluteNode("1", 0, 0, 100, 50), createAbsoluteNode("2", 120, 0, 100, 50), createAbsoluteNode("3", 240, 0, 100, 50), ], }; const result = LayoutOptimizer.optimizeContainer(container); expect(result.cssStyles?.display).toBe("flex"); // Children should have position:absolute removed result.children?.forEach((child) => { expect(child.cssStyles?.position).toBeUndefined(); expect(child.cssStyles?.left).toBeUndefined(); expect(child.cssStyles?.top).toBeUndefined(); }); }); it("should clean child styles when parent becomes grid", () => { const container: SimplifiedNode = { id: "container", name: "Grid Container", type: "FRAME", cssStyles: { width: "260px", height: "140px" }, children: [ createAbsoluteNode("1", 0, 0, 100, 50), createAbsoluteNode("2", 120, 0, 100, 50), createAbsoluteNode("3", 0, 70, 100, 50), createAbsoluteNode("4", 120, 70, 100, 50), ], }; const result = LayoutOptimizer.optimizeContainer(container); expect(result.cssStyles?.display).toBe("grid"); // Children should have position:absolute removed result.children?.forEach((child) => { expect(child.cssStyles?.position).toBeUndefined(); expect(child.cssStyles?.left).toBeUndefined(); expect(child.cssStyles?.top).toBeUndefined(); }); }); }); describe("Homogeneity Analysis", () => { it("should detect homogeneous elements with similar sizes", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), ]; const result = analyzeHomogeneity(elements); expect(result.isHomogeneous).toBe(true); expect(result.widthCV).toBe(0); expect(result.heightCV).toBe(0); expect(result.homogeneousElements.length).toBe(4); expect(result.outlierElements.length).toBe(0); }); it("should detect non-homogeneous elements with varying sizes", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 200, height: 100 }, 1), // Much larger toElementRect({ x: 0, y: 70, width: 50, height: 25 }, 2), // Much smaller toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), ]; const result = analyzeHomogeneity(elements); // Elements have high size variance expect(result.widthCV).toBeGreaterThan(0.2); }); it("should return not homogeneous for fewer than 4 elements", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), ]; const result = analyzeHomogeneity(elements); expect(result.isHomogeneous).toBe(false); }); it("should filter by node types when provided", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), ]; const nodeTypes = ["FRAME", "FRAME", "FRAME", "FRAME"]; const result = analyzeHomogeneity(elements, nodeTypes); expect(result.isHomogeneous).toBe(true); expect(result.types).toContain("FRAME"); }); it("should reject incompatible node types", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), ]; // TEXT nodes are not allowed in grid const nodeTypes = ["TEXT", "TEXT", "TEXT", "TEXT"]; const result = analyzeHomogeneity(elements, nodeTypes); expect(result.isHomogeneous).toBe(false); }); it("should separate outliers from homogeneous elements", () => { const elements: ElementRect[] = [ // 4 similar-sized elements toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), // 1 outlier (much larger) toElementRect({ x: 0, y: 140, width: 300, height: 200 }, 4), ]; const result = analyzeHomogeneity(elements); expect(result.isHomogeneous).toBe(true); expect(result.homogeneousElements.length).toBe(4); expect(result.outlierElements.length).toBe(1); expect(result.outlierElements[0].index).toBe(4); }); it("should use custom size tolerance", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 110, height: 55 }, 1), // 10% larger toElementRect({ x: 0, y: 70, width: 90, height: 45 }, 2), // 10% smaller toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), ]; // With strict tolerance (5%), should fail const strictResult = analyzeHomogeneity(elements, undefined, 0.05); expect(strictResult.isHomogeneous).toBe(false); // With relaxed tolerance (25%), should pass const relaxedResult = analyzeHomogeneity(elements, undefined, 0.25); expect(relaxedResult.isHomogeneous).toBe(true); }); }); describe("Filter Homogeneous For Grid", () => { it("should return homogeneous elements for grid detection", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), ]; const result = filterHomogeneousForGrid(elements); expect(result.elements.length).toBe(4); expect(result.gridIndices.size).toBe(4); }); it("should return empty array for non-homogeneous elements", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 300, height: 200 }, 1), // Very different toElementRect({ x: 0, y: 70, width: 50, height: 25 }, 2), // Very different toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), ]; const result = filterHomogeneousForGrid(elements); // Not enough homogeneous elements expect(result.elements.length).toBeLessThan(4); }); it("should return empty array for fewer than 4 elements", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), ]; const result = filterHomogeneousForGrid(elements); expect(result.elements.length).toBe(0); expect(result.gridIndices.size).toBe(0); }); it("should filter out outliers and return only homogeneous elements", () => { const elements: ElementRect[] = [ // 4 similar-sized elements (grid candidates) toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), // Outlier (header or title) toElementRect({ x: 0, y: -30, width: 240, height: 20 }, 4), ]; const result = filterHomogeneousForGrid(elements); expect(result.elements.length).toBe(4); // Outlier should not be included expect(result.elements.every((e) => e.index !== 4)).toBe(true); expect(result.gridIndices.has(4)).toBe(false); }); it("should work with node types filtering", () => { const elements: ElementRect[] = [ toElementRect({ x: 0, y: 0, width: 100, height: 50 }, 0), toElementRect({ x: 120, y: 0, width: 100, height: 50 }, 1), toElementRect({ x: 0, y: 70, width: 100, height: 50 }, 2), toElementRect({ x: 120, y: 70, width: 100, height: 50 }, 3), ]; const nodeTypes = ["INSTANCE", "INSTANCE", "INSTANCE", "INSTANCE"]; const result = filterHomogeneousForGrid(elements, nodeTypes); expect(result.elements.length).toBe(4); expect(result.gridIndices.size).toBe(4); }); }); describe("Grid Detection with Real Data", () => { let testData: FigmaNode; beforeAll(() => { testData = loadTestData(); }); it("should detect grid layout in real Figma data", () => { // Find any container with 4+ children that might form a grid function findGridCandidate(node: FigmaNode): FigmaNode | null { if (node.children && node.children.length >= 4) { const childBoxes = node.children .filter((c) => c.absoluteBoundingBox) .map((c, i) => toElementRect(c.absoluteBoundingBox!, i)); if (childBoxes.length >= 4) { const gridResult = detectGridLayout(childBoxes); if (gridResult.isGrid && gridResult.rowCount >= 2) { return node; } } } if (node.children) { for (const child of node.children) { const found = findGridCandidate(child); if (found) return found; } } return null; } const gridCandidate = findGridCandidate(testData); // Test passes if we either find a grid or don't (depends on fixture data) if (gridCandidate) { const childBoxes = gridCandidate .children!.filter((c) => c.absoluteBoundingBox) .map((c, i) => toElementRect(c.absoluteBoundingBox!, i)); const gridResult = detectGridLayout(childBoxes); expect(gridResult.isGrid).toBe(true); expect(gridResult.rowCount).toBeGreaterThanOrEqual(2); expect(gridResult.columnCount).toBeGreaterThanOrEqual(2); expect(gridResult.trackWidths.length).toBe(gridResult.columnCount); } }); it("should filter homogeneous elements before grid detection", () => { // Find a container with mixed children function findMixedContainer(node: FigmaNode): FigmaNode | null { if (node.children && node.children.length >= 5) { const sizes = node.children .filter((c) => c.absoluteBoundingBox) .map((c) => c.absoluteBoundingBox!.width * c.absoluteBoundingBox!.height); if (sizes.length >= 5) { const maxSize = Math.max(...sizes); const minSize = Math.min(...sizes); // Look for containers where max is at least 2x min (mixed sizes) if (maxSize > minSize * 2) { return node; } } } if (node.children) { for (const child of node.children) { const found = findMixedContainer(child); if (found) return found; } } return null; } const mixedContainer = findMixedContainer(testData); if (mixedContainer) { const childBoxes = mixedContainer .children!.filter((c) => c.absoluteBoundingBox) .map((c, i) => toElementRect(c.absoluteBoundingBox!, i)); const nodeTypes = mixedContainer.children!.map((c) => c.type); // Filter should reduce the set const result = filterHomogeneousForGrid(childBoxes, nodeTypes); // Filtered should be less than or equal to original expect(result.elements.length).toBeLessThanOrEqual(childBoxes.length); } }); }); // ==================== Absolute to Relative Position Conversion ==================== describe("Absolute to Relative Position Conversion", () => { describe("collectFlowChildOffsets", () => { it("should collect offsets from flow children only", () => { const children: SimplifiedNode[] = [ { id: "1", name: "child1", type: "FRAME", cssStyles: { left: "10px", top: "20px", width: "100px", height: "50px" }, }, { id: "2", name: "child2", type: "FRAME", cssStyles: { left: "120px", top: "20px", width: "100px", height: "50px" }, }, { id: "3", name: "stacked", type: "FRAME", cssStyles: { left: "50px", top: "30px", width: "80px", height: "40px" }, }, ]; // Child index 2 is stacked (overlapping) const stackedIndices = new Set([2]); const offsets = LayoutOptimizer.collectFlowChildOffsets(children, stackedIndices); expect(offsets.length).toBe(2); // Only 2 flow children expect(offsets[0].index).toBe(0); expect(offsets[0].left).toBe(10); expect(offsets[0].top).toBe(20); expect(offsets[1].index).toBe(1); expect(offsets[1].left).toBe(120); }); it("should skip children without cssStyles", () => { const children: SimplifiedNode[] = [ { id: "1", name: "child1", type: "FRAME" }, { id: "2", name: "child2", type: "FRAME", cssStyles: { left: "10px", top: "20px", width: "100px", height: "50px" }, }, ]; const offsets = LayoutOptimizer.collectFlowChildOffsets(children, new Set()); expect(offsets.length).toBe(1); expect(offsets[0].index).toBe(1); }); }); describe("inferContainerPadding", () => { it("should infer padding from child offsets", () => { const offsets = [ { index: 0, left: 20, top: 15, width: 100, height: 50, right: 120, bottom: 65 }, { index: 1, left: 130, top: 15, width: 100, height: 50, right: 230, bottom: 65 }, ]; const padding = LayoutOptimizer.inferContainerPadding(offsets, 250, 80, "row"); expect(padding.paddingLeft).toBe(20); expect(padding.paddingTop).toBe(15); expect(padding.paddingRight).toBe(20); // 250 - 230 = 20 expect(padding.paddingBottom).toBe(15); // 80 - 65 = 15 }); it("should return zero padding for small offsets (<= 2px)", () => { const offsets = [ { index: 0, left: 1, top: 2, width: 100, height: 50, right: 101, bottom: 52 }, ]; const padding = LayoutOptimizer.inferContainerPadding(offsets, 103, 54, "row"); expect(padding.paddingLeft).toBe(0); // 1 <= 2 expect(padding.paddingTop).toBe(0); // 2 <= 2 expect(padding.paddingRight).toBe(0); // 103 - 101 = 2 <= 2 expect(padding.paddingBottom).toBe(0); // 54 - 52 = 2 <= 2 }); it("should return zero padding for empty offsets", () => { const padding = LayoutOptimizer.inferContainerPadding([], 100, 100, "row"); expect(padding.paddingLeft).toBe(0); expect(padding.paddingTop).toBe(0); expect(padding.paddingRight).toBe(0); expect(padding.paddingBottom).toBe(0); }); }); describe("calculateChildMargins", () => { it("should calculate marginTop for row layout with flex-start alignment", () => { const offsets = [ { index: 0, left: 10, top: 10, width: 100, height: 50, right: 110, bottom: 60 }, { index: 1, left: 120, top: 25, width: 100, height: 30, right: 220, bottom: 55 }, // offset 15px down ]; const padding = { paddingTop: 10, paddingRight: 10, paddingBottom: 10, paddingLeft: 10 }; const margins = LayoutOptimizer.calculateChildMargins( offsets, padding, "row", "flex-start", ); expect(margins.get(0)).toBeUndefined(); // No margin needed expect(margins.get(1)?.marginTop).toBe(15); // 25 - 10 = 15 }); it("should calculate marginLeft for column layout with flex-start alignment", () => { const offsets = [ { index: 0, left: 10, top: 10, width: 100, height: 50, right: 110, bottom: 60 }, { index: 1, left: 30, top: 70, width: 80, height: 50, right: 110, bottom: 120 }, // offset 20px right ]; const padding = { paddingTop: 10, paddingRight: 10, paddingBottom: 10, paddingLeft: 10 }; const margins = LayoutOptimizer.calculateChildMargins( offsets, padding, "column", "flex-start", ); expect(margins.get(0)).toBeUndefined(); // No margin needed expect(margins.get(1)?.marginLeft).toBe(20); // 30 - 10 = 20 }); it("should not add margins for center alignment", () => { const offsets = [ { index: 0, left: 10, top: 10, width: 100, height: 50, right: 110, bottom: 60 }, { index: 1, left: 120, top: 25, width: 100, height: 30, right: 220, bottom: 55 }, ]; const padding = { paddingTop: 10, paddingRight: 10, paddingBottom: 10, paddingLeft: 10 }; const margins = LayoutOptimizer.calculateChildMargins(offsets, padding, "row", "center"); expect(margins.size).toBe(0); // No margins for center alignment }); }); describe("generatePaddingCSS", () => { it("should generate single value when all padding is equal", () => { const padding = { paddingTop: 10, paddingRight: 10, paddingBottom: 10, paddingLeft: 10 }; const css = LayoutOptimizer.generatePaddingCSS(padding); expect(css).toBe("10px"); }); it("should generate two values when top/bottom and left/right are equal", () => { const padding = { paddingTop: 10, paddingRight: 20, paddingBottom: 10, paddingLeft: 20 }; const css = LayoutOptimizer.generatePaddingCSS(padding); expect(css).toBe("10px 20px"); }); it("should generate three values when left/right are equal", () => { const padding = { paddingTop: 10, paddingRight: 20, paddingBottom: 30, paddingLeft: 20 }; const css = LayoutOptimizer.generatePaddingCSS(padding); expect(css).toBe("10px 20px 30px"); }); it("should generate four values when all padding is different", () => { const padding = { paddingTop: 10, paddingRight: 20, paddingBottom: 30, paddingLeft: 40 }; const css = LayoutOptimizer.generatePaddingCSS(padding); expect(css).toBe("10px 20px 30px 40px"); }); it("should return null when all padding is zero", () => { const padding = { paddingTop: 0, paddingRight: 0, paddingBottom: 0, paddingLeft: 0 }; const css = LayoutOptimizer.generatePaddingCSS(padding); expect(css).toBeNull(); }); }); describe("convertAbsoluteToRelative", () => { it("should convert absolute positioning to padding and clean children", () => { const parent: SimplifiedNode = { id: "parent", name: "container", type: "FRAME", cssStyles: { width: "300px", height: "100px" }, }; const children: SimplifiedNode[] = [ { id: "1", name: "child1", type: "FRAME", cssStyles: { position: "absolute", left: "20px", top: "10px", width: "100px", height: "80px", }, }, { id: "2", name: "child2", type: "FRAME", cssStyles: { position: "absolute", left: "140px", top: "10px", width: "140px", height: "80px", }, }, ]; const result = LayoutOptimizer.convertAbsoluteToRelative( parent, children, "flex", "row", new Set(), "flex-start", ); // Should have padding expect(result.parentPaddingStyle).toBe("10px 20px"); // Children should not have position: absolute or left/top expect(result.convertedChildren[0].cssStyles?.position).toBeUndefined(); expect(result.convertedChildren[0].cssStyles?.left).toBeUndefined(); expect(result.convertedChildren[0].cssStyles?.top).toBeUndefined(); expect(result.convertedChildren[1].cssStyles?.position).toBeUndefined(); }); it("should keep stacked elements with absolute positioning", () => { const parent: SimplifiedNode = { id: "parent", name: "container", type: "FRAME", cssStyles: { width: "200px", height: "100px" }, }; const children: SimplifiedNode[] = [ { id: "1", name: "background", type: "RECTANGLE", cssStyles: { position: "absolute", left: "0px", top: "0px", width: "200px", height: "100px", }, }, { id: "2", name: "content", type: "FRAME", cssStyles: { position: "absolute", left: "20px", top: "10px", width: "160px", height: "80px", }, }, ]; // Child 0 is stacked (background) const stackedIndices = new Set([0]); const result = LayoutOptimizer.convertAbsoluteToRelative( parent, children, "flex", "row", stackedIndices, "flex-start", ); // Stacked element should keep absolute positioning expect(result.convertedChildren[0].cssStyles?.position).toBe("absolute"); expect(result.convertedChildren[0].cssStyles?.left).toBe("0px"); // Flow element should be cleaned expect(result.convertedChildren[1].cssStyles?.position).toBeUndefined(); }); }); }); // ==================== Background Element Detection Tests ==================== describe("detectBackgroundElement", () => { it("should detect background element at origin matching parent size", () => { const rects: ElementRect[] = [ { x: 0, y: 0, width: 400, height: 300, index: 0, right: 400, bottom: 300, centerX: 200, centerY: 150, }, { x: 20, y: 20, width: 100, height: 50, index: 1, right: 120, bottom: 70, centerX: 70, centerY: 45, }, { x: 150, y: 100, width: 80, height: 40, index: 2, right: 230, bottom: 140, centerX: 190, centerY: 120, }, ]; const result = detectBackgroundElement(rects, 400, 300); expect(result.hasBackground).toBe(true); expect(result.backgroundIndex).toBe(0); expect(result.contentIndices).toEqual([1, 2]); }); it("should not detect background when no element matches parent size", () => { const rects: ElementRect[] = [ { x: 10, y: 10, width: 200, height: 150, index: 0, right: 210, bottom: 160, centerX: 110, centerY: 85, }, { x: 50, y: 50, width: 100, height: 50, index: 1, right: 150, bottom: 100, centerX: 100, centerY: 75, }, ]; const result = detectBackgroundElement(rects, 400, 300); expect(result.hasBackground).toBe(false); expect(result.backgroundIndex).toBe(-1); }); it("should detect background with small tolerance (within 5%)", () => { const rects: ElementRect[] = [ { x: 0, y: 0, width: 395, height: 290, index: 0, right: 395, bottom: 290, centerX: 197.5, centerY: 145, }, { x: 20, y: 20, width: 100, height: 50, index: 1, right: 120, bottom: 70, centerX: 70, centerY: 45, }, ]; const result = detectBackgroundElement(rects, 400, 300); expect(result.hasBackground).toBe(true); expect(result.backgroundIndex).toBe(0); }); it("should return empty result for single element", () => { const rects: ElementRect[] = [ { x: 0, y: 0, width: 400, height: 300, index: 0, right: 400, bottom: 300, centerX: 200, centerY: 150, }, ]; const result = detectBackgroundElement(rects, 400, 300); expect(result.hasBackground).toBe(false); }); }); // ==================== Background Style Extraction Tests ==================== describe("extractBackgroundStyles", () => { it("should extract backgroundColor from background element", () => { const bgChild: SimplifiedNode = { id: "bg-1", name: "Background", type: "RECTANGLE", cssStyles: { backgroundColor: "rgba(255, 255, 255, 1)", width: "400px", height: "300px", }, }; const result = LayoutOptimizer.extractBackgroundStyles(bgChild); expect(result.backgroundColor).toBe("rgba(255, 255, 255, 1)"); expect(result.width).toBeUndefined(); expect(result.height).toBeUndefined(); }); it("should extract borderRadius from background element", () => { const bgChild: SimplifiedNode = { id: "bg-1", name: "Background", type: "RECTANGLE", cssStyles: { backgroundColor: "rgba(0, 0, 0, 1)", borderRadius: "8px", }, }; const result = LayoutOptimizer.extractBackgroundStyles(bgChild); expect(result.backgroundColor).toBe("rgba(0, 0, 0, 1)"); expect(result.borderRadius).toBe("8px"); }); it("should extract boxShadow from background element", () => { const bgChild: SimplifiedNode = { id: "bg-1", name: "Background", type: "RECTANGLE", cssStyles: { backgroundColor: "white", boxShadow: "0 2px 4px rgba(0,0,0,0.1)", }, }; const result = LayoutOptimizer.extractBackgroundStyles(bgChild); expect(result.boxShadow).toBe("0 2px 4px rgba(0,0,0,0.1)"); }); it("should return empty object for element without styles", () => { const bgChild: SimplifiedNode = { id: "bg-1", name: "Background", type: "RECTANGLE", }; const result = LayoutOptimizer.extractBackgroundStyles(bgChild); expect(Object.keys(result)).toHaveLength(0); }); }); // ==================== isBackgroundElement Tests ==================== describe("isBackgroundElement", () => { it("should return true for valid background element", () => { const child: SimplifiedNode = { id: "bg-1", name: "Background", type: "RECTANGLE", cssStyles: { backgroundColor: "white", }, }; expect(LayoutOptimizer.isBackgroundElement(0, 0, child)).toBe(true); }); it("should return false when index does not match", () => { const child: SimplifiedNode = { id: "bg-1", name: "Background", type: "RECTANGLE", cssStyles: { backgroundColor: "white", }, }; expect(LayoutOptimizer.isBackgroundElement(1, 0, child)).toBe(false); }); it("should return false for non-visual element types", () => { const child: SimplifiedNode = { id: "text-1", name: "Text", type: "TEXT", cssStyles: { backgroundColor: "white", }, }; expect(LayoutOptimizer.isBackgroundElement(0, 0, child)).toBe(false); }); it("should return false for element without visual styles", () => { const child: SimplifiedNode = { id: "bg-1", name: "Background", type: "RECTANGLE", cssStyles: { width: "100px", height: "50px", }, }; expect(LayoutOptimizer.isBackgroundElement(0, 0, child)).toBe(false); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/1yhy/Figma-Context-MCP'

If you have feedback or need assistance with the MCP directory API, please join our Discord server