Buffer stdin may be consumed before the subprocess is ready on Linux. Using pipe mode with explicit write/end ensures the data is available.
213 lines
6.9 KiB
TypeScript
213 lines
6.9 KiB
TypeScript
import { describe, expect, test, setDefaultTimeout } from 'bun:test'
|
|
|
|
setDefaultTimeout(30_000)
|
|
import { join } from 'path'
|
|
import { tmpdir } from 'os'
|
|
import { randomUUID } from 'crypto'
|
|
|
|
const CLI = join(import.meta.dir, '../../packages/cli/src/index.ts')
|
|
const FIXTURE = join(import.meta.dir, '../fixtures/material3.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 }
|
|
}
|
|
|
|
describe('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 = parseInt(stdout)
|
|
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)
|
|
})
|
|
})
|