257 lines
11 KiB
TypeScript
257 lines
11 KiB
TypeScript
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))
|
||
})
|
||
})
|