openpencil/tests/engine/cli/export.test.ts
Danila Poyarkov 9bc353587f
refactor!: generate Tailwind JSX through dom-css (#763)
* refactor!: generate Tailwind JSX through dom-css

Core kept its own SceneGraph → Tailwind mapper next to the one dom-css
uses for Tailwind HTML, and the two drifted: HTML export turned grid
frames into flex columns and dropped rotation, inner shadows, blur, and
flex grow, while Tailwind JSX had them.

Tailwind JSX is now printed by dom-css from the same CSS projection as
HTML export, with esrap building the JSX and string literals carrying
text or attributes that JSX would otherwise reinterpret. The projection
gains grid layout and placement, rotation, every shadow, layer and
background blur, flex grow, right-to-left direction, and sections, and
writes opaque colors as hex so Tailwind can match its palette.

BREAKING CHANGE: `sceneNodeToJSX` and `selectionToJSX` in
`@open-pencil/core` no longer accept a format, and `JSXFormat` and
`JSXExportOptions` are removed. Use `sceneNodesToTailwindJSX` from
`@open-pencil/dom-css` or `@open-pencil/dom-css/browser`.

* fix(dom-css): keep backslashes and line breaks in Tailwind JSX attributes

JSX attribute strings keep backslashes literally, but the printer
escapes backslashes and line breaks in string literals, so a layer
named `a\b` came back as `a\\b`. Such values are now written as
expression containers, like values containing quotes or `&`.
2026-09-26 10:50:28 +04:00

287 lines
7.4 KiB
TypeScript

import { expect, setDefaultTimeout, test } from 'bun:test'
import { mkdtemp } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { unzipSync } from 'fflate'
import { BUILTIN_IO_FORMATS, IORegistry } from '@open-pencil/core/io'
import { runOpenPencilCLI } from '#tests/helpers/cli'
import { createRect, firstPageId, makeSceneGraph } from '#tests/helpers/scene'
setDefaultTimeout(30_000)
const io = new IORegistry(BUILTIN_IO_FORMATS)
async function createFigFixture() {
const dir = await mkdtemp(join(tmpdir(), 'open-pencil-export-cli-'))
const figPath = join(dir, 'card.fig')
const graph = makeSceneGraph('Export Page')
const firstFrame = graph.createNode('FRAME', firstPageId(graph), {
name: 'First slide',
width: 1280,
height: 720
})
const rect = createRect(graph, firstFrame.id, {
name: 'Export Card',
x: 0,
y: 0,
width: 160,
height: 80
})
rect.layoutMode = 'HORIZONTAL'
rect.itemSpacing = 8
rect.paddingLeft = 16
rect.paddingRight = 16
rect.paddingTop = 16
rect.paddingBottom = 16
rect.fills = [{ type: 'SOLID', color: { r: 1, g: 1, b: 1, a: 1 } }]
const secondPage = graph.addPage('Second Page')
const secondFrame = graph.createNode('FRAME', secondPage.id, {
name: 'Second slide',
width: 1280,
height: 720
})
createRect(graph, secondFrame.id, {
name: 'Second Card',
x: 0,
y: 0,
width: 120,
height: 60
})
const result = await io.writeDocument('fig', graph)
await Bun.write(figPath, result.data as Uint8Array)
return { dir, figPath }
}
test('FIG export preserves the whole document by default', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'whole.fig')
const { stdout, stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'fig',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
expect(stdout).toContain('Target: whole document')
const { graph } = await io.readDocument({
name: output,
data: new Uint8Array(await Bun.file(output).arrayBuffer())
})
expect(graph.getPages()).toHaveLength(3)
expect(graph.getPages().map((page) => page.name)).toContain('Second Page')
})
test('PPTX export includes slides from every page by default', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'whole.pptx')
const { stdout, stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'pptx',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
expect(stdout).toContain('Target: whole document')
const files = unzipSync(new Uint8Array(await Bun.file(output).arrayBuffer()))
expect(files['ppt/slides/slide1.xml']).toBeDefined()
expect(files['ppt/slides/slide2.xml']).toBeDefined()
expect(files['ppt/slides/slide3.xml']).toBeUndefined()
})
test('FIG export requires an explicit page for a partial archive', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'page.fig')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'fig',
'--page',
'Second Page',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const { graph } = await io.readDocument({
name: output,
data: new Uint8Array(await Bun.file(output).arrayBuffer())
})
expect(graph.getPages().map((page) => page.name)).toEqual(['Second Page'])
})
test('export CLI writes HTML with inline styles by default', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'card.html')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'html',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
expect(html).toContain('data-open-pencil-node-id')
expect(html).toContain('style=')
expect(html).toContain('display: flex')
})
test('export CLI can write HTML styles as Tailwind classes', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'card-tailwind.html')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'html',
'--css',
'tailwind',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
expect(html).toContain('data-open-pencil-node-id')
expect(html).toContain('class="')
expect(html).toContain('flex')
expect(html).not.toContain('style=')
})
test('export CLI writes Tailwind JSX for a page', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'card.jsx')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'jsx',
'--style',
'tailwind',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const jsx = await Bun.file(output).text()
expect(jsx).toStartWith('<div data-name="First slide" className="')
expect(jsx).toContain('<div data-name="Export Card" className="flex p-4 gap-2')
expect(jsx).not.toContain('Second Card')
})
test('export CLI can write standalone HTML', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'card-standalone.html')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'html',
'--html',
'standalone',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
expect(html).toContain('<!doctype html>')
expect(html).toContain('data-open-pencil-html="standalone"')
expect(html).toContain('position:relative')
expect(html).toContain('position: absolute')
expect(html).not.toContain('@tailwindcss/browser@4')
})
test('export CLI precompiles Tailwind CSS for standalone Tailwind HTML', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'card-standalone-tailwind.html')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'html',
'--html',
'standalone',
'--css',
'tailwind',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
expect(html).toContain('<!doctype html>')
expect(html).toContain('<style>')
expect(html).toContain('class="')
expect(html).toContain('.flex')
expect(html).not.toContain('@tailwindcss/browser@4')
})
test('export CLI can write external standalone HTML assets', async () => {
const { dir, figPath } = await createFigFixture()
const output = join(dir, 'card-external.html')
const { stderr, exitCode } = await runOpenPencilCLI([
'export',
figPath,
'--format',
'html',
'--html',
'standalone',
'--css',
'tailwind',
'--assets',
'external',
'--output',
output
])
expect(stderr).toBe('')
expect(exitCode).toBe(0)
const html = await Bun.file(output).text()
const cssPath = join(dir, 'card-external.assets', 'openpencil.css')
const css = await Bun.file(cssPath).text()
expect(html).toContain('<link rel="stylesheet" href="card-external.assets/openpencil.css">')
expect(html).not.toContain('<style>')
expect(css).toContain('.flex')
expect(css).toContain('.op-stage')
})