openpencil/tests/engine/cli/eval.test.ts
Joseph Cumines f862beb08d Add heuristic overlap detection tool and CLI
Implement the analyze overlaps CLI command, RPC endpoint, and ToolDef
to enable heuristic detection of visual overlaps, parent overflows, and
overlay patterns. Geometry computation now uses world-matrix visual
bounds to handle nested ancestor rotations and clipping frames correctly.

Update file-backed CLI commands to use Node fs/promises instead of Bun
runtime file APIs. This is required for the published CLI to function
when installed and executed in a standard Node environment. Add the
package.json subpath export to the core package for metadata consumers.
2026-06-19 09:54:27 +10:00

251 lines
7.2 KiB
TypeScript

import { expect, setDefaultTimeout, test } from 'bun:test'
import { randomUUID } from 'node:crypto'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { cliSourcePath, repoPath, requireBuiltWorkspacePackages } from '#tests/helpers/paths'
import { heavy } from '#tests/helpers/test-utils'
requireBuiltWorkspacePackages()
setDefaultTimeout(30_000)
const CLI = cliSourcePath('index.ts')
const FIXTURE = repoPath('tests/fixtures/gold-preview.fig')
async function run(
args: string[],
stdin?: string
): Promise<{ stdout: string; stderr: string; exitCode: number }> {
const proc = Bun.spawn(['bun', CLI, ...args], {
stdout: 'pipe',
stderr: 'pipe',
stdin: stdin ? Buffer.from(stdin) : undefined
})
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text()
])
const exitCode = await proc.exited
return { stdout: stdout.trim(), stderr: stderr.trim(), exitCode }
}
heavy('eval CLI', () => {
test('returns page name', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--code',
'return figma.currentPage.name'
])
expect(exitCode).toBe(0)
expect(stdout.length).toBeGreaterThan(0)
})
test('returns primitive number', async () => {
const { stdout, exitCode } = await run(['eval', FIXTURE, '--code', 'return 42'])
expect(exitCode).toBe(0)
expect(stdout).toBe('42')
})
test('returns page count', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--code',
'return figma.root.children.length'
])
expect(exitCode).toBe(0)
const count = Number.parseInt(stdout, 10)
expect(count).toBeGreaterThan(0)
})
test('findAll queries nodes', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--json',
'--code',
'return figma.currentPage.findAll(n => n.type === "TEXT").slice(0, 3).map(n => ({ name: n.name, type: n.type }))'
])
expect(exitCode).toBe(0)
const data = JSON.parse(stdout)
expect(Array.isArray(data)).toBe(true)
expect(data.length).toBeGreaterThan(0)
expect(data[0].type).toBe('TEXT')
})
test('create frame and return properties', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--json',
'--code',
`const f = figma.createFrame(); f.name = "Test"; f.resize(300, 200); return f`
])
expect(exitCode).toBe(0)
const data = JSON.parse(stdout)
expect(data.name).toBe('Test')
expect(data.width).toBe(300)
expect(data.height).toBe(200)
expect(data.type).toBe('FRAME')
})
test('auto-layout script', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--json',
'--code',
`const f = figma.createFrame()
f.layoutMode = "VERTICAL"
f.itemSpacing = 8
f.paddingTop = f.paddingBottom = 16
return { layoutMode: f.layoutMode, itemSpacing: f.itemSpacing, paddingTop: f.paddingTop }`
])
expect(exitCode).toBe(0)
const data = JSON.parse(stdout)
expect(data.layoutMode).toBe('VERTICAL')
expect(data.itemSpacing).toBe(8)
expect(data.paddingTop).toBe(16)
})
test('text node with characters', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--json',
'--code',
`const t = figma.createText()
t.characters = "Hello"
t.fontSize = 24
return { characters: t.characters, fontSize: t.fontSize }`
])
expect(exitCode).toBe(0)
const data = JSON.parse(stdout)
expect(data.characters).toBe('Hello')
expect(data.fontSize).toBe(24)
})
test('top-level await works', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--code',
'await figma.loadFontAsync({ family: "Inter", style: "Regular" }); return "ok"'
])
expect(exitCode).toBe(0)
expect(stdout).toContain('ok')
})
test('stdin reads code from pipe', async () => {
const proc = Bun.spawn(['bun', CLI, 'eval', FIXTURE, '--stdin'], {
stdout: 'pipe',
stderr: 'pipe',
stdin: 'pipe'
})
proc.stdin.write('return figma.currentPage.name')
proc.stdin.end()
const [stdout, stderr] = await Promise.all([
new Response(proc.stdout).text(),
new Response(proc.stderr).text()
])
const exitCode = await proc.exited
if (exitCode !== 0) throw new Error(`exit ${exitCode}: ${stderr}`)
expect(stdout.trim().length).toBeGreaterThan(0)
})
test('--json outputs valid JSON', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--json',
'--code',
'return { a: 1, b: "two" }'
])
expect(exitCode).toBe(0)
const data = JSON.parse(stdout)
expect(data).toEqual({ a: 1, b: 'two' })
})
test('syntax error exits with error', async () => {
const { exitCode, stderr } = await run(['eval', FIXTURE, '--code', 'return {{{'])
expect(exitCode).not.toBe(0)
expect(stderr.length).toBeGreaterThan(0)
})
test('runtime error exits with error', async () => {
const { exitCode, stderr } = await run(['eval', FIXTURE, '--code', 'throw new Error("boom")'])
expect(exitCode).not.toBe(0)
expect(stderr).toContain('boom')
})
test('no code exits with error', async () => {
const { exitCode, stderr } = await run(['eval', FIXTURE])
expect(exitCode).not.toBe(0)
expect(stderr).toContain('code')
})
test('undefined result produces no output', async () => {
const { stdout, exitCode } = await run(['eval', FIXTURE, '--code', 'figma.createFrame()'])
expect(exitCode).toBe(0)
expect(stdout).toBe('')
})
test('--output writes file', async () => {
const outFile = join(tmpdir(), `eval-test-${randomUUID()}.fig`)
const { exitCode, stderr } = await run([
'eval',
FIXTURE,
'--quiet',
'--code',
'const f = figma.createFrame(); f.name = "Written"',
'-o',
outFile
])
// exportFigFile may fail on files with COMPONENT nodes (Kiwi schema limitation)
// but the eval itself should succeed — verify it at least attempted to write
if (exitCode === 0) {
const stat = Bun.file(outFile)
expect(await stat.exists()).toBe(true)
expect(stat.size).toBeGreaterThan(0)
} else {
expect(stderr).toContain('NodeType')
}
})
test('array of nodes serializes', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--json',
'--code',
`const a = figma.createFrame(); a.name = "A"
const b = figma.createRectangle(); b.name = "B"
return [a, b]`
])
expect(exitCode).toBe(0)
const data = JSON.parse(stdout)
expect(Array.isArray(data)).toBe(true)
expect(data[0].name).toBe('A')
expect(data[1].name).toBe('B')
})
test('getNodeById on real file', async () => {
const { stdout, exitCode } = await run([
'eval',
FIXTURE,
'--json',
'--code',
`const page = figma.currentPage
const first = page.children[0]
const found = figma.getNodeById(first.id)
return { same: found.id === first.id, name: found.name }`
])
expect(exitCode).toBe(0)
const data = JSON.parse(stdout)
expect(data.same).toBe(true)
})
})