test(dom-css): share CSS fixture suite

This commit is contained in:
Danila Poyarkov 2026-06-02 19:51:12 +03:00
parent 95fedac2b7
commit 101a047e4d
5 changed files with 218 additions and 145 deletions

View file

@ -1,44 +1,96 @@
import type { Page } from '@playwright/test'
import { compileTailwindCSS } from '@open-pencil/dom-css'
import { tailwindButtonClasses, tailwindCardClasses } from '#tests/helpers/dom-css'
import { expect, test } from '../fixtures'
const STYLE_PROPERTIES = [
'background-color',
'border-radius',
'color',
'display',
'gap',
'padding-top',
'width'
] as const
async function setStyledContent(page: Page, css: string, body: string) {
await page.setContent(`
<style>${css}</style>
${body}
`)
}
async function computedStyleProperties(
page: Page,
selector: string,
properties: readonly string[]
) {
return page.locator(selector).evaluate((element, styleProperties) => {
const computed = getComputedStyle(element)
return Object.fromEntries(
styleProperties.map((property) => [property, computed.getPropertyValue(property)])
)
}, properties)
}
test.describe('@open-pencil/dom-css browser CSS runtime oracle', () => {
test('resolves Tailwind variables and calc values in a real browser', async ({ page }) => {
const classes = ['flex', 'gap-3', 'w-80', 'p-6', 'rounded-xl', 'bg-white', 'text-slate-900']
const css = await compileTailwindCSS(classes)
await page.setContent(`
<style>${css}</style>
<article class="${classes.join(' ')}">
<h1>OpenPencil</h1>
</article>
`)
test('resolves Tailwind card variables and calc values in a real browser', async ({ page }) => {
const css = await compileTailwindCSS(tailwindCardClasses)
await setStyledContent(
page,
css,
`<article class="${tailwindCardClasses.join(' ')}"><h1>OpenPencil</h1></article>`
)
const styles = await page.locator('article').evaluate((element, properties) => {
const computed = getComputedStyle(element)
return Object.fromEntries(
properties.map((property) => [property, computed.getPropertyValue(property)])
)
}, STYLE_PROPERTIES)
const styles = await computedStyleProperties(page, 'article', [
'background-color',
'border-radius',
'color',
'display',
'flex-direction',
'gap',
'height',
'padding-top',
'width'
])
expect(styles.display).toBe('flex')
expect(styles['flex-direction']).toBe('column')
expect(styles.gap).toBe('12px')
expect(styles.width).toBe('320px')
expect(styles.height).toBe('176px')
expect(styles['padding-top']).toBe('24px')
expect(styles['border-radius']).toBe('12px')
expect(styles['background-color']).toBe('rgb(255, 255, 255)')
expect(styles.color).toMatch(/^(oklch|rgb)\(/)
})
test('resolves Tailwind button alignment, spacing, and typography', async ({ page }) => {
const css = await compileTailwindCSS(tailwindButtonClasses)
await setStyledContent(
page,
css,
`<button class="${tailwindButtonClasses.join(' ')}">Create design</button>`
)
const styles = await computedStyleProperties(page, 'button', [
'align-items',
'border-radius',
'color',
'display',
'font-size',
'font-weight',
'gap',
'justify-content',
'padding-left',
'padding-top'
])
expect(styles.display).toBe('inline-flex')
expect(styles['align-items']).toBe('center')
expect(styles['justify-content']).toBe('center')
expect(styles.gap).toBe('8px')
expect(styles['padding-left']).toBe('16px')
expect(styles['padding-top']).toBe('8px')
expect(styles['font-size']).toBe('14px')
expect(styles['font-weight']).toBe('500')
expect(styles['border-radius']).toBe('6px')
expect(styles.color).toBe('rgb(255, 255, 255)')
})
test('resolves custom properties when assigned to real CSS properties', async ({ page }) => {
await page.setContent(`
<style>

View file

@ -1,89 +1,23 @@
import { describe, expect, it } from 'bun:test'
import { colorToCSS } from '@open-pencil/core/color'
import {
compileTailwindCSS,
createHeadlessCSSRuntime,
designDocumentToSceneGraph,
sceneGraphToDesignDocument,
serializeHTML,
type DesignDocument
serializeHTML
} from '@open-pencil/dom-css'
const slate900 = colorToCSS({ r: 17 / 255, g: 24 / 255, b: 39 / 255, a: 1 })
const slateShadow = colorToCSS({ r: 15 / 255, g: 23 / 255, b: 42 / 255, a: 0.12 })
const slate200 = colorToCSS({ r: 229 / 255, g: 231 / 255, b: 235 / 255, a: 1 })
const cardHTML = `
<article class="card">
<h1 class="title">OpenPencil</h1>
<p class="description">Design with code-shaped CSS.</p>
</article>
`
const cardCSS = `
.card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 24px;
width: 320px;
height: 180px;
border: 1px solid ${slate200};
border-radius: 16px;
background: white;
box-shadow: 0px 8px 24px 0px ${slateShadow};
color: ${slate900};
}
.card .title {
font-size: 24px;
font-weight: 700;
line-height: 32px;
}
.description {
font-size: 14px;
line-height: 20px;
}
`
const cardDocument: DesignDocument = {
type: 'document',
children: [
{
type: 'element',
tagName: 'div',
attrs: { class: 'card' },
computedStyle: {
width: '320px',
height: '160px',
display: 'flex',
'flex-direction': 'column',
gap: '12px',
padding: '24px',
'border-radius': '16px',
'background-color': 'rgb(255, 255, 255)'
},
children: [
{
type: 'element',
tagName: 'h1',
attrs: {},
computedStyle: {
color: 'rgb(17, 24, 39)',
'font-size': '24px',
'font-weight': '700',
'line-height': '32px'
},
children: [{ type: 'text', text: 'OpenPencil' }]
}
]
}
]
}
import {
computedCardDocument,
cssCardCSS,
cssCardHTML,
tailwindCardClasses
} from '#tests/helpers/dom-css'
describe('@open-pencil/dom-css conversion', () => {
it('projects a DesignDOM card into a scene graph', () => {
const graph = designDocumentToSceneGraph(cardDocument)
const graph = designDocumentToSceneGraph(computedCardDocument)
const page = graph.getPages()[0]
expect(page?.name).toBe('DesignDOM')
@ -106,7 +40,7 @@ describe('@open-pencil/dom-css conversion', () => {
it('projects a parsed and styled HTML/CSS card into a scene graph', async () => {
const runtime = createHeadlessCSSRuntime()
const document = await runtime.computeStyles(runtime.parseHTML(cardHTML), cardCSS)
const document = await runtime.computeStyles(runtime.parseHTML(cssCardHTML), cssCardCSS)
const graph = designDocumentToSceneGraph(document)
const page = graph.getPages()[0]
const card = page ? graph.getChildren(page.id)[0] : undefined
@ -139,17 +73,7 @@ describe('@open-pencil/dom-css conversion', () => {
it('projects a Tailwind card through generated CSS into a scene graph', async () => {
const runtime = createHeadlessCSSRuntime()
const classes = [
'flex',
'flex-col',
'gap-3',
'w-80',
'h-44',
'p-6',
'rounded-xl',
'bg-white',
'text-slate-900'
]
const classes = [...tailwindCardClasses]
const document = await runtime.computeStyles(
runtime.parseHTML(`<article class="${classes.join(' ')}"><h1>OpenPencil</h1></article>`),
await compileTailwindCSS(classes)
@ -170,7 +94,7 @@ describe('@open-pencil/dom-css conversion', () => {
})
it('projects a scene graph back into DesignDOM', () => {
const graph = designDocumentToSceneGraph(cardDocument)
const graph = designDocumentToSceneGraph(computedCardDocument)
const document = sceneGraphToDesignDocument(graph)
const page = document.children[0]
expect(page?.type).toBe('element')

View file

@ -1,31 +1,12 @@
import { describe, expect, it } from 'bun:test'
import { colorToCSS } from '@open-pencil/core/color'
import {
createCSSRuntime,
createHeadlessCSSRuntime,
serializeHTML,
type DesignDocument
} from '@open-pencil/dom-css'
import { createCSSRuntime, createHeadlessCSSRuntime, serializeHTML } from '@open-pencil/dom-css'
const slate900 = colorToCSS({ r: 17 / 255, g: 24 / 255, b: 39 / 255, a: 1 })
const slate700 = colorToCSS({ r: 31 / 255, g: 41 / 255, b: 55 / 255, a: 1 })
const documentFixture: DesignDocument = {
type: 'document',
children: [
{
type: 'element',
tagName: 'div',
attrs: { class: 'card', 'data-id': 'node-1' },
children: [{ type: 'text', text: 'OpenPencil' }]
}
]
}
import { DOM_CSS_COLORS, simpleCardDocument } from '#tests/helpers/dom-css'
describe('@open-pencil/dom-css', () => {
it('serializes DesignDOM as HTML', () => {
expect(serializeHTML(documentFixture)).toBe(
expect(serializeHTML(simpleCardDocument)).toBe(
'<div class="card" data-id="node-1">OpenPencil</div>'
)
})
@ -34,7 +15,7 @@ describe('@open-pencil/dom-css', () => {
const runtime = createCSSRuntime()
expect(runtime.kind).toBe('headless')
expect(runtime.serializeHTML(documentFixture)).toBe(
expect(runtime.serializeHTML(simpleCardDocument)).toBe(
'<div class="card" data-id="node-1">OpenPencil</div>'
)
})
@ -58,15 +39,15 @@ describe('@open-pencil/dom-css', () => {
it('computes simple headless styles from CSSOM rules', async () => {
const runtime = createHeadlessCSSRuntime()
const document = await runtime.computeStyles(
documentFixture,
`.card { width: 320px; color: ${slate900}; } #missing { color: red; }`
simpleCardDocument,
`.card { width: 320px; color: ${DOM_CSS_COLORS.slate900}; } #missing { color: red; }`
)
const card = document.children[0]
expect(card?.type).toBe('element')
if (card?.type !== 'element') return
expect(card.computedStyle?.width).toBe('320px')
expect(card.computedStyle?.color).toBe(slate900)
expect(card.computedStyle?.color).toBe(DOM_CSS_COLORS.slate900)
})
it('applies specificity, descendant selectors, child selectors, inheritance, and shorthands', async () => {
@ -79,8 +60,8 @@ describe('@open-pencil/dom-css', () => {
const document = await runtime.computeStyles(
parsed,
`
article { color: ${slate900}; padding: 8px 16px; }
.card { width: 300px; color: ${slate700}; }
article { color: ${DOM_CSS_COLORS.slate900}; padding: 8px 16px; }
.card { width: 300px; color: ${DOM_CSS_COLORS.slate700}; }
article.card > header { gap: 12px; }
.card .title { font-size: 24px; }
#hero { width: 320px; background: white; }
@ -91,7 +72,7 @@ describe('@open-pencil/dom-css', () => {
expect(card?.type).toBe('element')
if (card?.type !== 'element') return
expect(card.computedStyle?.width).toBe('320px')
expect(card.computedStyle?.color).toBe(slate700)
expect(card.computedStyle?.color).toBe(DOM_CSS_COLORS.slate700)
expect(card.computedStyle?.['padding-top']).toBe('8px')
expect(card.computedStyle?.['padding-right']).toBe('16px')
expect(card.computedStyle?.['background-color']).toBe('white')
@ -100,12 +81,12 @@ describe('@open-pencil/dom-css', () => {
expect(header?.type).toBe('element')
if (header?.type !== 'element') return
expect(header.computedStyle?.gap).toBe('12px')
expect(header.computedStyle?.color).toBe(slate700)
expect(header.computedStyle?.color).toBe(DOM_CSS_COLORS.slate700)
const title = header.children[0]
expect(title?.type).toBe('element')
if (title?.type !== 'element') return
expect(title.computedStyle?.['font-size']).toBe('24px')
expect(title.computedStyle?.color).toBe(slate700)
expect(title.computedStyle?.color).toBe(DOM_CSS_COLORS.slate700)
})
})

View file

@ -2,9 +2,11 @@ import { describe, expect, it } from 'bun:test'
import { compileTailwindCSS, createHeadlessCSSRuntime } from '@open-pencil/dom-css'
import { tailwindCardClasses } from '#tests/helpers/dom-css'
describe('@open-pencil/dom-css Tailwind', () => {
it('compiles utility candidates through Tailwind', async () => {
const css = await compileTailwindCSS('flex w-80 p-6 rounded-xl bg-white')
const css = await compileTailwindCSS(tailwindCardClasses)
expect(css).toContain('.flex')
expect(css).toContain('.w-80')
@ -14,9 +16,9 @@ describe('@open-pencil/dom-css Tailwind', () => {
it('feeds Tailwind CSS variables through headless style computation', async () => {
const runtime = createHeadlessCSSRuntime()
const css = await compileTailwindCSS(['w-80', 'p-6'])
const css = await compileTailwindCSS(tailwindCardClasses)
const document = await runtime.computeStyles(
runtime.parseHTML('<section class="w-80 p-6"></section>'),
runtime.parseHTML(`<section class="${tailwindCardClasses.join(' ')}"></section>`),
css
)
const element = document.children[0]

114
tests/helpers/dom-css.ts Normal file
View file

@ -0,0 +1,114 @@
import { colorToCSS } from '@open-pencil/core/color'
import type { DesignDocument } from '@open-pencil/dom-css'
export const DOM_CSS_COLORS = {
slate900: colorToCSS({ r: 17 / 255, g: 24 / 255, b: 39 / 255, a: 1 }),
slate700: colorToCSS({ r: 31 / 255, g: 41 / 255, b: 55 / 255, a: 1 }),
slate200: colorToCSS({ r: 229 / 255, g: 231 / 255, b: 235 / 255, a: 1 }),
slateShadow: colorToCSS({ r: 15 / 255, g: 23 / 255, b: 42 / 255, a: 0.12 })
} as const
export const simpleCardDocument: DesignDocument = {
type: 'document',
children: [
{
type: 'element',
tagName: 'div',
attrs: { class: 'card', 'data-id': 'node-1' },
children: [{ type: 'text', text: 'OpenPencil' }]
}
]
}
export const cssCardHTML = `
<article class="card">
<h1 class="title">OpenPencil</h1>
<p class="description">Design with code-shaped CSS.</p>
</article>
`
export const cssCardCSS = `
.card {
display: flex;
flex-direction: column;
gap: 12px;
padding: 24px;
width: 320px;
height: 180px;
border: 1px solid ${DOM_CSS_COLORS.slate200};
border-radius: 16px;
background: white;
box-shadow: 0px 8px 24px 0px ${DOM_CSS_COLORS.slateShadow};
color: ${DOM_CSS_COLORS.slate900};
}
.card .title {
font-size: 24px;
font-weight: 700;
line-height: 32px;
}
.description {
font-size: 14px;
line-height: 20px;
}
`
export const computedCardDocument: DesignDocument = {
type: 'document',
children: [
{
type: 'element',
tagName: 'div',
attrs: { class: 'card' },
computedStyle: {
width: '320px',
height: '160px',
display: 'flex',
'flex-direction': 'column',
gap: '12px',
padding: '24px',
'border-radius': '16px',
'background-color': 'rgb(255, 255, 255)'
},
children: [
{
type: 'element',
tagName: 'h1',
attrs: {},
computedStyle: {
color: 'rgb(17, 24, 39)',
'font-size': '24px',
'font-weight': '700',
'line-height': '32px'
},
children: [{ type: 'text', text: 'OpenPencil' }]
}
]
}
]
}
export const tailwindCardClasses = [
'flex',
'flex-col',
'gap-3',
'w-80',
'h-44',
'p-6',
'rounded-xl',
'bg-white',
'text-slate-900'
] as const
export const tailwindButtonClasses = [
'inline-flex',
'items-center',
'justify-center',
'gap-2',
'rounded-md',
'bg-slate-900',
'px-4',
'py-2',
'text-sm',
'font-medium',
'text-white'
] as const