import process from 'node:process' import { test, type Page } from '@playwright/test' import type { DesignDocument } from '@open-pencil/dom-css' import type { SceneNode } from '@open-pencil/scene-graph' const BROWSER_RUNTIME_MODULE = `/@fs${process.cwd()}/packages/dom-css/src/runtime/browser.ts` const DOM_CSS_BROWSER_MODULE = '/@id/@open-pencil/dom-css/browser' async function ensureAppPage(page: Page) { const baseURL = test.info().project.use.baseURL if (!baseURL) throw new Error('App tests require a configured baseURL') const origin = new URL(baseURL).origin if (new URL(page.url()).origin !== origin) { await page.goto(origin) } } /** * The token stylesheet for a small theme, written in the page so values and conditions are * checked by the browser's own CSS parser: valid ones kept, ones that close their rule left out. */ export async function browserTokenStylesheet(page: Page) { await ensureAppPage(page) return page.evaluate( async ({ exportModule, graphModule }) => { const { tokenStylesheet } = await import(exportModule) const { SceneGraph } = await import(graphModule) const graph = new SceneGraph() graph.addCollection({ id: 'theme', name: 'Theme', modes: [ { modeId: 'light', name: 'Light' }, { modeId: 'dark', name: 'Dark' }, { modeId: 'evil', name: 'Evil', condition: '.x } body { display: none' } ], defaultModeId: 'light', variableIds: [] }) const add = (id: string, name: string, type: string, values: object, extra = {}) => graph.addVariable({ id, name, type, collectionId: 'theme', valuesByMode: values, description: '', hiddenFromPublishing: false, ...extra }) add('surface', 'Surface', 'COLOR', { light: { r: 1, g: 1, b: 1, a: 1 }, dark: { r: 0, g: 0, b: 0, a: 1 }, evil: { r: 1, g: 0, b: 0, a: 1 } }) add( 'gutter', 'Gutter', 'FLOAT', { light: 24, dark: 24, evil: 24 }, { scopes: ['GAP'], expressions: { light: { css: 'clamp(1rem, 4vw, 2rem)', resolved: 24 } } } ) add( 'gap', 'Gap', 'FLOAT', { light: 8, dark: 8, evil: 8 }, { expressions: { light: { css: '1px; } body { display: none', resolved: 8 } } } ) const { css, issues } = await tokenStylesheet(graph, { format: 'css' }) return { css, issues: issues.map((issue: { message: string }) => issue.message) } }, { exportModule: '/@id/@open-pencil/dom-css/export', graphModule: '/@id/@open-pencil/scene-graph' } ) } export async function setStyledContent(page: Page, css: string, body: string) { await page.setContent(` ${body} `) } export async function browserRuntimeComputeStyles( page: Page, document: DesignDocument, cssText: string, sandbox: 'shadow-root' | 'iframe' = 'iframe' ) { await ensureAppPage(page) return page.evaluate( async ({ designDocument, css, modulePath, sandboxMode }) => { const { createBrowserCSSRuntime } = await import(modulePath) const runtime = createBrowserCSSRuntime({ document: window.document, sandbox: sandboxMode }) return runtime.computeStyles(designDocument, css) }, { designDocument: document, css: cssText, modulePath: BROWSER_RUNTIME_MODULE, sandboxMode: sandbox } ) } export async function publicBrowserHTMLSceneGraph(page: Page, html: string, cssText = '') { await ensureAppPage(page) await page.setContent('
') return page.evaluate( async ({ sourceHTML, css, modulePath }) => { const { browserHTMLToSceneGraph } = await import(modulePath) const graph = await browserHTMLToSceneGraph(sourceHTML, { cssText: css }) const pageNode = graph.getPages()[0] const card = pageNode ? graph.getChildren(pageNode.id)[0] : undefined return card ? { height: card.height, itemSpacing: card.itemSpacing, layoutMode: card.layoutMode, paddingLeft: card.paddingLeft, type: card.type, width: card.width } : null }, { sourceHTML: html, css: cssText, modulePath: DOM_CSS_BROWSER_MODULE } ) } export async function publicBrowserSceneGraph(page: Page, classes: string[], cssText: string) { await ensureAppPage(page) await page.setContent('
') return page.evaluate( async ({ candidates, css, modulePath }) => { const { browserJSXToSceneGraph, jsx } = await import(modulePath) const graph = await browserJSXToSceneGraph( jsx('article', { class: candidates.join(' '), children: jsx('h1', { children: 'OpenPencil' }) }), { cssText: css } ) const pageNode = graph.getPages()[0] const card = pageNode ? graph.getChildren(pageNode.id)[0] : undefined return card ? { height: card.height, itemSpacing: card.itemSpacing, layoutMode: card.layoutMode, paddingLeft: card.paddingLeft, type: card.type, width: card.width } : null }, { candidates: classes, css: cssText, modulePath: DOM_CSS_BROWSER_MODULE } ) } export async function publicBrowserImageNode(page: Page, html: string, cssText: string) { await ensureAppPage(page) await page.setContent('
') return page.evaluate( async ({ sourceHTML, css, modulePath }) => { const { browserHTMLToSceneGraph } = await import(modulePath) const graph = await browserHTMLToSceneGraph(sourceHTML, { cssText: css }) const pageNode = graph.getPages()[0] const image = pageNode ? graph.getChildren(pageNode.id)[0] : undefined const fill = image?.fills[0] return image ? { fillType: fill?.type, hasImageBytes: fill?.imageHash ? graph.images.has(fill.imageHash) : false, height: image.height, imageScaleMode: fill?.imageScaleMode, type: image.type, width: image.width } : null }, { sourceHTML: html, css: cssText, modulePath: DOM_CSS_BROWSER_MODULE } ) } export async function publicBrowserTextNode(page: Page, html: string, cssText: string) { await ensureAppPage(page) await page.setContent('
') return page.evaluate( async ({ sourceHTML, css, modulePath }) => { const { browserHTMLToSceneGraph } = await import(modulePath) const graph = await browserHTMLToSceneGraph(sourceHTML, { cssText: css }) return graph.getAllNodes().find((node: SceneNode) => node.type === 'TEXT') }, { sourceHTML: html, css: cssText, modulePath: DOM_CSS_BROWSER_MODULE } ) } export 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) }