openpencil/tests/engine/io/fig/import/instance-regressions.test.ts
2026-05-21 14:54:18 +03:00

257 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { beforeAll, describe, expect, setDefaultTimeout, test } from 'bun:test'
import { computeAllLayouts, parseFigFile, type SceneGraph, type SceneNode } from '@open-pencil/core'
import { computeContentBounds } from '#core/io/formats/raster/render'
import { parseFixture, readFixtureBytes } from '#tests/helpers/fig-fixtures'
import {
childMatching,
childNamed,
collectAllNodes,
previewChild
} from '#tests/helpers/fig-traversal'
setDefaultTimeout(60_000)
describe('derived instance layout regressions', () => {
let layoutGraph: SceneGraph
let layoutNodes: SceneNode[]
beforeAll(async () => {
layoutGraph = await parseFixture('gold-preview.fig')
computeAllLayouts(layoutGraph)
layoutNodes = collectAllNodes(layoutGraph)
})
test('preserves repeated badge overrides without moving sibling component wrappers', () => {
const input = previewChild(layoutGraph, layoutNodes, 'Input')
const inputRoot = childNamed(layoutGraph, input, '_input')
const inputFrame = childNamed(layoutGraph, inputRoot, 'Input')
const content = childNamed(layoutGraph, inputFrame, 'Content')
const tags = childNamed(layoutGraph, content, 'Tags')
const firstBadge = childNamed(layoutGraph, tags, 'Badge')
const firstBadgeContent = childNamed(layoutGraph, firstBadge, '_badge-and-tag')
const placeholderFrame = childNamed(layoutGraph, content, 'Placeholder')
const placeholderText = childNamed(layoutGraph, placeholderFrame, 'Placeholder')
expect(inputFrame).toMatchObject({ x: 0, y: 0 })
expect(inputFrame?.width).toBeCloseTo(375.7498, 3)
expect(inputFrame?.height).toBeCloseTo(39.3803, 3)
expect(content).toMatchObject({ x: 0, y: 0 })
expect(firstBadge?.x).toBeCloseTo(8, 3)
expect(firstBadge?.y).toBeCloseTo(6, 3)
expect(firstBadge?.width).toBeCloseTo(85.3239, 3)
expect(firstBadge?.height).toBeCloseTo(28.6901, 3)
expect(firstBadgeContent).toMatchObject({ x: 0, y: 0 })
expect(placeholderFrame?.x).toBeCloseTo(277.352, 3)
expect(placeholderFrame?.y).toBeCloseTo(0, 3)
expect(placeholderText?.x).toBeCloseTo(14.2535, 3)
expect(placeholderText?.y).toBeCloseTo(10.6901, 3)
})
test('propagates nested badge component property overrides through cloned instances', () => {
const input = previewChild(layoutGraph, layoutNodes, 'Input')
const inputRoot = childNamed(layoutGraph, input, '_input')
const inputFrame = childNamed(layoutGraph, inputRoot, 'Input')
const content = childNamed(layoutGraph, inputFrame, 'Content')
const tags = childNamed(layoutGraph, content, 'Tags')
const badges = tags
? layoutGraph.getChildren(tags.id).filter((node) => node.name === 'Badge')
: []
expect(badges).toHaveLength(3)
for (const badge of badges) {
const badgeContent = childNamed(layoutGraph, badge, '_badge-and-tag')
const avatar = childNamed(layoutGraph, badgeContent, 'Avatar')
const closeIcon = childNamed(layoutGraph, badgeContent, 'Close-Icon')
const avatarShape = avatar ? layoutGraph.getChildren(avatar.id)[0] : undefined
const closeGlyph = childNamed(layoutGraph, closeIcon, 'x')
expect(avatar?.visible).toBe(true)
expect(closeIcon?.visible).toBe(true)
expect(closeGlyph?.visible).toBe(true)
expect(avatarShape?.fills.some((fill) => fill.type === 'IMAGE' && fill.visible)).toBe(true)
}
})
test('does not collapse unrelated datepicker instances to the page origin', () => {
const datepicker = previewChild(layoutGraph, layoutNodes, '_datepicker')
expect(datepicker?.x).toBeCloseTo(765.2428, 3)
expect(datepicker?.y).toBeCloseTo(518, 3)
expect(datepicker?.width).toBeCloseTo(362, 3)
expect(datepicker?.height).toBeCloseTo(422, 3)
})
test('keeps checked-list icons aligned with labels', () => {
const title = previewChild(layoutGraph, layoutNodes, 'Title + Description')
const checkedList = childNamed(layoutGraph, title, 'Checked List')
const listItems = checkedList ? layoutGraph.getChildren(checkedList.id) : []
expect(listItems).toHaveLength(3)
for (const item of listItems) {
const list = childNamed(layoutGraph, item, '_list')
const inline = childNamed(layoutGraph, list, 'Inline')
const icon = childNamed(layoutGraph, inline, 'Static Icon')
const content = childNamed(layoutGraph, inline, 'Content')
const label = content
? layoutGraph.getChildren(content.id).find((node) => node.text)
: undefined
expect(icon?.y).toBeCloseTo(0, 3)
expect(label?.y).toBeCloseTo(0, 3)
expect(icon?.height).toBeCloseTo(label?.height ?? 0, 3)
}
})
test('propagates static icon color overrides through checked-list clones', () => {
const title = previewChild(layoutGraph, layoutNodes, 'Title + Description')
const checkedList = childNamed(layoutGraph, title, 'Checked List')
const listItems = checkedList ? layoutGraph.getChildren(checkedList.id) : []
expect(listItems).toHaveLength(3)
for (const item of listItems) {
const list = childNamed(layoutGraph, item, '_list')
const inline = childNamed(layoutGraph, list, 'Inline')
const icon = childNamed(layoutGraph, inline, 'Static Icon')
const iconRoot = childNamed(layoutGraph, icon, '_icon-xs')
const check = childNamed(layoutGraph, iconRoot, 'check')
const vector = check ? layoutGraph.getChildren(check.id)[0] : undefined
const stroke = vector?.strokes[0]
expect(stroke?.visible).toBe(true)
expect(stroke?.color).toMatchObject({ r: 1, g: 1, b: 1, a: 1 })
}
})
test('propagates nested component overrides with lazy first-page import', async () => {
const graph = await parseFigFile(readFixtureBytes('gold-preview.fig').buffer as ArrayBuffer, {
populate: 'first-page'
})
computeAllLayouts(graph)
const nodes = collectAllNodes(graph)
const title = previewChild(graph, nodes, 'Title + Description')
const checkedList = childNamed(graph, title, 'Checked List')
const listItems = checkedList ? graph.getChildren(checkedList.id) : []
expect(listItems).toHaveLength(3)
for (const item of listItems) {
const list = childNamed(graph, item, '_list')
const inline = childNamed(graph, list, 'Inline')
const icon = childNamed(graph, inline, 'Static Icon')
const iconRoot = childNamed(graph, icon, '_icon-xs')
const check = childNamed(graph, iconRoot, 'check')
const vector = check ? graph.getChildren(check.id)[0] : undefined
expect(vector?.strokes[0]?.color).toMatchObject({ r: 1, g: 1, b: 1, a: 1 })
}
const dropzone = previewChild(graph, nodes, 'Drag’ n’ Drop File Uploads')
const dropzoneRoot = childNamed(graph, dropzone, '_drag-n-drop-file-upload')
const content = childNamed(graph, dropzoneRoot, 'Content')
const titleRow = childNamed(graph, content, 'Title')
const links = childNamed(graph, titleRow, 'Links')
const linkRoot = childNamed(graph, links, '_link-default')
const chevronLeft = childNamed(graph, linkRoot, 'chevron-left')
const placeholder = childNamed(graph, linkRoot, 'Placeholder')
const linkText = childNamed(graph, placeholder, 'Link')
expect(chevronLeft?.visible).toBe(false)
expect(placeholder?.x).toBeCloseTo(0, 3)
expect(linkText?.text).toBe('browse')
expect(linkText?.fills[0]?.color).toMatchObject({
r: 0.14509804546833038,
g: 0.38823530077934265,
b: 0.9215686321258545,
a: 1
})
expect(linkText?.figmaDerivedTextGlyphs?.length).toBeGreaterThan(0)
const input = previewChild(graph, nodes, 'Input')
const inputRoot = childNamed(graph, input, '_input')
const inputFrame = childNamed(graph, inputRoot, 'Input')
const inputContent = childNamed(graph, inputFrame, 'Content')
const tags = childNamed(graph, inputContent, 'Tags')
const badge = childNamed(graph, tags, 'Badge')
const badgeContent = childNamed(graph, badge, '_badge-and-tag')
const avatar = childNamed(graph, badgeContent, 'Avatar')
const avatarShape = avatar ? graph.getChildren(avatar.id)[0] : undefined
expect(avatar?.visible).toBe(true)
expect(avatarShape?.visible).toBe(true)
expect(avatarShape?.fills.some((fill) => fill.type === 'IMAGE' && fill.visible)).toBe(true)
})
test('preserves WYSIWYG toolbar padding', () => {
const wysiwyg = previewChild(layoutGraph, layoutNodes, '_WYSIWYG-editor')
const toolbarRoot = childNamed(layoutGraph, wysiwyg, '_on-text-WYSIWYG-toolbar')
const toolbar = childMatching(
layoutGraph,
toolbarRoot,
(node) => node.name === 'Toolbar' && node.width === 286 && node.height === 48
)
const buttons = childMatching(
layoutGraph,
toolbar,
(node) => node.name === 'Toolbar' && node.width === 270 && node.height === 32
)
expect(toolbar?.x).toBeCloseTo(0, 3)
expect(toolbar?.y).toBeCloseTo(37, 3)
expect(buttons?.x).toBeCloseTo(8, 3)
expect(buttons?.y).toBeCloseTo(8, 3)
expect(buttons?.width).toBeCloseTo(270, 3)
expect(buttons?.height).toBeCloseTo(32, 3)
})
test('scales WYSIWYG icon vector geometry with nested instances', () => {
const wysiwyg = previewChild(layoutGraph, layoutNodes, '_WYSIWYG-editor')
const toolbarRoot = childNamed(layoutGraph, wysiwyg, '_on-text-WYSIWYG-toolbar')
const toolbar = childMatching(
layoutGraph,
toolbarRoot,
(node) => node.name === 'Toolbar' && node.width === 286 && node.height === 48
)
const buttons = childMatching(
layoutGraph,
toolbar,
(node) => node.name === 'Toolbar' && node.width === 270 && node.height === 32
)
const linkButton = layoutGraph.getChildren(buttons?.id ?? '').find((node) => node.x === 136)
const linkIcon = childNamed(layoutGraph, linkButton, 'link')
const linkVectors = linkIcon ? layoutGraph.getChildren(linkIcon.id) : []
expect(linkIcon?.width).toBeCloseTo(18, 3)
expect(linkIcon?.height).toBeCloseTo(18, 3)
for (const vector of linkVectors) {
const xs = vector.vectorNetwork?.vertices.map((vertex) => vertex.x) ?? []
const ys = vector.vectorNetwork?.vertices.map((vertex) => vertex.y) ?? []
expect(Math.max(...xs)).toBeLessThanOrEqual(vector.width + 0.001)
expect(Math.max(...ys)).toBeLessThanOrEqual(vector.height + 0.001)
}
})
test('exports WYSIWYG and logo visual overflow instead of clipping to node boxes', () => {
const wysiwyg = previewChild(layoutGraph, layoutNodes, '_WYSIWYG-editor')
expect(wysiwyg).toBeDefined()
const wysiwygBounds = wysiwyg ? computeContentBounds(layoutGraph, [wysiwyg.id]) : null
expect(wysiwygBounds?.minX).toBeCloseTo(5, 3)
expect(wysiwygBounds?.minY).toBeCloseTo(577, 3)
expect(wysiwygBounds ? wysiwygBounds.maxX - wysiwygBounds.minX : 0).toBeCloseTo(294, 3)
expect(wysiwygBounds ? wysiwygBounds.maxY - wysiwygBounds.minY : 0).toBeCloseTo(93, 3)
const title = previewChild(layoutGraph, layoutNodes, 'Title + Description')
const logoGroup = childNamed(
layoutGraph,
childNamed(layoutGraph, title, 'Logo'),
'logo-short-6'
)
expect(logoGroup).toBeDefined()
const logoBounds = logoGroup ? computeContentBounds(layoutGraph, [logoGroup.id]) : null
const logoAbs = logoGroup ? layoutGraph.getAbsolutePosition(logoGroup.id) : { x: 0, y: 0 }
const logo = logoGroup
expect(logoBounds?.minX).toBeLessThanOrEqual(logoAbs.x)
expect(logoBounds?.minY).toBeLessThanOrEqual(logoAbs.y)
expect(logoBounds?.maxX).toBeGreaterThanOrEqual(logoAbs.x + (logo?.width ?? 0))
expect(logoBounds?.maxY).toBeGreaterThanOrEqual(logoAbs.y + (logo?.height ?? 0))
})
})