import { compileTailwindCSS, designDocumentToSceneGraph, jsx, jsxToDesignDocument, type DesignDocument } from '@open-pencil/dom-css' import { DOM_CSS_COLORS, fixtureMatrixCSS, fixtureMatrixHTML, tailwindBadgeClasses, tailwindButtonClasses, tailwindCardClasses, tailwindInputClasses, tailwindNavClasses } from '#tests/helpers/dom-css' import { browserRuntimeComputeStyles, computedStyleProperties, publicBrowserHTMLSceneGraph, publicBrowserSceneGraph, publicBrowserTextNode, setStyledContent } from '#tests/helpers/dom-css-browser' import { expect, test } from '../fixtures' test.describe('@open-pencil/dom-css browser CSS runtime oracle', () => { test('resolves Tailwind card variables and calc values in a real browser', async ({ page }) => { const css = await compileTailwindCSS(tailwindCardClasses) await setStyledContent( page, css, `

OpenPencil

` ) 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, `` ) 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 fixture matrix layout, form, badge, and dialog styles', async ({ page }) => { await setStyledContent(page, fixtureMatrixCSS, fixtureMatrixHTML) const navbar = await computedStyleProperties(page, '.navbar', [ 'align-items', 'border-radius', 'display', 'height', 'justify-content', 'padding-left', 'width' ]) expect(navbar.display).toBe('flex') expect(navbar['align-items']).toBe('center') expect(navbar['justify-content']).toBe('space-between') expect(navbar.width).toBe('416px') expect(navbar.height).toBe('48px') expect(navbar['padding-left']).toBe('16px') expect(navbar['border-radius']).toBe('12px') const badge = await computedStyleProperties(page, '.badge', [ 'align-items', 'background-color', 'border-radius', 'color', 'display', 'font-size', 'font-weight', 'height', 'justify-content', 'padding-left' ]) expect(badge.display).toBe('flex') expect(badge['align-items']).toBe('center') expect(badge['justify-content']).toBe('center') expect(badge.height).toBe('24px') expect(badge['padding-left']).toBe('10px') expect(badge['border-radius']).toBe('9999px') expect(badge['font-size']).toBe('12px') expect(badge['font-weight']).toBe('600') expect(badge['background-color']).toBe('rgb(224, 242, 254)') expect(badge.color).toBe('rgb(3, 105, 161)') const input = await computedStyleProperties(page, '.input', [ 'border-bottom-width', 'border-radius', 'font-size', 'height', 'padding-left', 'width' ]) expect(input.width).toBe('312px') expect(input.height).toBe('40px') expect(input['padding-left']).toBe('12px') expect(input['border-radius']).toBe('8px') expect(input['border-bottom-width']).toBe('1px') expect(input['font-size']).toBe('14px') const dialog = await computedStyleProperties(page, '.dialog', [ 'box-shadow', 'display', 'flex-direction', 'gap', 'max-width', 'min-width', 'padding-top', 'width' ]) expect(dialog.display).toBe('flex') expect(dialog['flex-direction']).toBe('column') expect(dialog.gap).toBe('16px') expect(dialog.width).toBe('360px') expect(dialog['min-width']).toBe('320px') expect(dialog['max-width']).toBe('420px') expect(dialog['padding-top']).toBe('24px') expect(dialog['box-shadow']).toContain('rgba(15, 23, 42, 0.16)') }) test('resolves Tailwind input, badge, and nav utilities in a real browser', async ({ page }) => { const inputClasses = [...tailwindInputClasses] const badgeClasses = [...tailwindBadgeClasses] const navClasses = [...tailwindNavClasses] const css = await compileTailwindCSS([...inputClasses, ...badgeClasses, ...navClasses]) await setStyledContent( page, css, ` ` ) const nav = await computedStyleProperties(page, 'nav', [ 'align-items', 'border-radius', 'display', 'height', 'justify-content', 'padding-left', 'width' ]) expect(nav.display).toBe('flex') expect(nav['align-items']).toBe('center') expect(nav['justify-content']).toBe('space-between') expect(nav.width).toBe('384px') expect(nav.height).toBe('48px') expect(nav['padding-left']).toBe('16px') expect(nav['border-radius']).toBe('12px') const badge = await computedStyleProperties(page, 'nav span:last-child', [ 'align-items', 'border-radius', 'display', 'font-size', 'font-weight', 'height', 'justify-content', 'padding-left' ]) expect(badge.display).toBe('flex') expect(badge['align-items']).toBe('center') expect(badge['justify-content']).toBe('center') expect(badge.height).toBe('24px') expect(badge['padding-left']).toBe('10px') expect(badge['border-radius']).toMatch(/^3\.\d+e\+\d+px$/) expect(badge['font-size']).toBe('12px') expect(badge['font-weight']).toBe('600') const input = await computedStyleProperties(page, 'input', [ 'border-bottom-width', 'border-radius', 'font-size', 'height', 'padding-left', 'width' ]) expect(input.width).toBe('320px') expect(input.height).toBe('40px') expect(input['padding-left']).toBe('12px') expect(input['border-radius']).toBe('6px') expect(input['border-bottom-width']).toBe('1px') expect(input['font-size']).toBe('14px') }) test('resolves custom properties when assigned to real CSS properties', async ({ page }) => { await page.setContent(`

Title

`) const styles = await page.locator('.card').evaluate((element) => { const computed = getComputedStyle(element) return { customGap: computed.getPropertyValue('--card-gap'), gap: computed.getPropertyValue('gap'), paddingTop: computed.getPropertyValue('padding-top'), width: computed.getPropertyValue('width') } }) expect(styles.customGap).toContain('calc') expect(styles.gap).toBe('12px') expect(styles.paddingTop).toBe('24px') expect(styles.width).toBe('176px') }) test('resolves border styles in a real browser', async ({ page }) => { await setStyledContent( page, ` .outline { border: 2px dashed #0f172a; width: 160px; height: 80px; } `, '
' ) const outline = await computedStyleProperties(page, '.outline', [ 'border-bottom-color', 'border-bottom-style', 'border-bottom-width', 'height', 'width' ]) expect(outline['border-bottom-color']).toBe('rgb(15, 23, 42)') expect(outline['border-bottom-style']).toBe('dashed') expect(outline['border-bottom-width']).toBe('2px') expect(outline.width).toBe('160px') expect(outline.height).toBe('80px') }) test('resolves aspect ratio, object fit, text transform, and white space in a real browser', async ({ page }) => { await setStyledContent( page, ` .ratio { aspect-ratio: 16 / 9; width: 320px; } .media { object-fit: contain; width: 320px; height: 180px; } .title { text-transform: uppercase; white-space: nowrap; } `, '
Preview

OpenPencil

' ) const ratio = await computedStyleProperties(page, '.ratio', ['aspect-ratio', 'height', 'width']) expect(ratio['aspect-ratio']).toBe('16 / 9') expect(ratio.width).toBe('320px') expect(ratio.height).toBe('180px') const media = await computedStyleProperties(page, '.media', ['height', 'object-fit', 'width']) expect(media['object-fit']).toBe('contain') expect(media.width).toBe('320px') expect(media.height).toBe('180px') const title = await computedStyleProperties(page, '.title', ['text-transform', 'white-space']) expect(title['text-transform']).toBe('uppercase') expect(title['white-space']).toBe('nowrap') }) test('projects browser text transform and nowrap into scene graph text fields', async ({ page }) => { const textNode = await publicBrowserTextNode( page, '

OpenPencil

', '.title { color: #111827; text-transform: uppercase; white-space: nowrap; }' ) expect(textNode?.type).toBe('TEXT') expect(textNode?.textCase).toBe('UPPER') expect(textNode?.maxLines).toBe(1) }) test('resolves flex wrap, self alignment, absolute positioning, and clipping', async ({ page }) => { await setStyledContent( page, ` .wrap { display: flex; flex-wrap: wrap; gap: 12px 20px; overflow: clip; position: relative; width: 240px; height: 120px; } .chip { align-self: center; position: absolute; left: 16px; top: 24px; min-width: 48px; max-width: 96px; width: 80px; height: 32px; } `, '
Chip
' ) const wrap = await computedStyleProperties(page, '.wrap', [ 'column-gap', 'display', 'flex-wrap', 'height', 'overflow', 'position', 'row-gap', 'width' ]) expect(wrap.display).toBe('flex') expect(wrap['flex-wrap']).toBe('wrap') expect(wrap['column-gap']).toBe('20px') expect(wrap['row-gap']).toBe('12px') expect(wrap.overflow).toBe('clip') expect(wrap.position).toBe('relative') expect(wrap.width).toBe('240px') expect(wrap.height).toBe('120px') const chip = await computedStyleProperties(page, '.chip', [ 'align-self', 'height', 'left', 'max-width', 'min-width', 'position', 'top', 'width' ]) expect(chip['align-self']).toBe('center') expect(chip.position).toBe('absolute') expect(chip.left).toBe('16px') expect(chip.top).toBe('24px') expect(chip['min-width']).toBe('48px') expect(chip['max-width']).toBe('96px') expect(chip.width).toBe('80px') expect(chip.height).toBe('32px') }) test('computes styles through the browser runtime sandbox', async ({ page }) => { await page.goto('/') await setStyledContent(page, '.card { width: 20px; }', '
Host
') const document: DesignDocument = { type: 'document', children: [ { type: 'element', tagName: 'article', attrs: { class: 'card' }, children: [{ type: 'text', text: 'OpenPencil' }] } ] } const css = ` :root { --spacing: 0.25rem; } .card { display: flex; flex-direction: column; gap: calc(var(--spacing) * 3); width: calc(10rem + 16px); padding: calc(var(--spacing) * 6); box-shadow: 0px 8px 24px 0px ${DOM_CSS_COLORS.dialogShadow}; } ` const iframeDocument = await browserRuntimeComputeStyles(page, document, css, 'iframe') const shadowDocument = await browserRuntimeComputeStyles(page, document, css, 'shadow-root') const iframeCard = iframeDocument.children[0] const shadowCard = shadowDocument.children[0] expect(iframeCard?.type).toBe('element') expect(shadowCard?.type).toBe('element') if (iframeCard?.type !== 'element' || shadowCard?.type !== 'element') return expect(iframeCard.computedStyle?.width).toBe('176px') expect(shadowCard.computedStyle?.width).toBe('176px') expect(iframeCard.computedStyle?.gap).toBe('12px') expect(iframeCard.computedStyle?.['padding-top']).toBe('24px') expect(iframeCard.computedStyle?.['box-shadow']).toContain('rgba(15, 23, 42, 0.16)') const hostWidth = await page .locator('article.card') .evaluate((element) => getComputedStyle(element).width) expect(hostWidth).toBe('20px') }) test('projects HTML through public browser helpers into scene graph', async ({ page }) => { const css = ` .card { display: flex; flex-direction: column; gap: 12px; width: 320px; height: 176px; padding: 24px; } ` const card = await publicBrowserHTMLSceneGraph( page, '

OpenPencil

', css ) expect(card?.type).toBe('FRAME') expect(card?.width).toBe(320) expect(card?.height).toBe(176) expect(card?.layoutMode).toBe('VERTICAL') expect(card?.itemSpacing).toBe(12) expect(card?.paddingLeft).toBe(24) }) test('projects embedded HTML styles through public browser helpers', async ({ page }) => { const card = await publicBrowserHTMLSceneGraph( page, `

Embedded CSS

` ) expect(card?.type).toBe('FRAME') expect(card?.width).toBe(300) expect(card?.height).toBe(160) expect(card?.layoutMode).toBe('VERTICAL') expect(card?.itemSpacing).toBe(14) expect(card?.paddingLeft).toBe(22) }) test('projects JSX through public browser helpers into scene graph', async ({ page }) => { const css = await compileTailwindCSS(tailwindCardClasses) const card = await publicBrowserSceneGraph(page, [...tailwindCardClasses], css) expect(card?.type).toBe('FRAME') expect(card?.width).toBe(320) expect(card?.height).toBe(176) expect(card?.layoutMode).toBe('VERTICAL') expect(card?.itemSpacing).toBe(12) expect(card?.paddingLeft).toBe(24) }) test('projects JSX and Tailwind through browser computed styles into scene graph', async ({ page }) => { const classes = [...tailwindCardClasses] const document = await jsxToDesignDocument( jsx('article', { class: classes.join(' '), children: jsx('h1', { children: 'OpenPencil' }) }) ) const css = await compileTailwindCSS(classes) const computedDocument = await browserRuntimeComputeStyles(page, document, css, 'iframe') const graph = designDocumentToSceneGraph(computedDocument) const designCard = computedDocument.children[0] const pageNode = graph.getPages()[0] const card = pageNode ? graph.getChildren(pageNode.id)[0] : undefined expect(designCard?.type).toBe('element') if (designCard?.type !== 'element') return expect(designCard.computedStyle?.display).toBe('flex') expect(designCard.computedStyle?.width).toBe('320px') expect(designCard.computedStyle?.height).toBe('176px') expect(designCard.computedStyle?.['padding-top']).toBe('24px') expect(card?.type).toBe('FRAME') if (card?.type !== 'FRAME') return expect(card.width).toBe(320) expect(card.height).toBe(176) expect(card.layoutMode).toBe('VERTICAL') expect(card.itemSpacing).toBe(12) expect(card.paddingLeft).toBe(24) }) })