openpencil/tools/dev/unit-tests/src/shards.ts
Danila Poyarkov 9b418de13e
refactor: print OpenPencil JSX export as syntax trees (#799)
* refactor(codegen): share syntax-tree code generation between exporters

dom-css printed Tailwind JSX with its own esrap JSX builders and kept TypeScript template helpers under its Storybook export. The OpenPencil JSX exporter needs the same JSX builders, and design-jsx and dom-css may not depend on each other.

@open-pencil/codegen holds both: es for ESTree templates and modules (moved from dom-css) and jsx for JSX elements, attributes, text, and printing, including the literal rules that keep exported strings from being reinterpreted. dom-css no longer depends on acorn and esrap directly.

* refactor(design-jsx): print JSX export as syntax trees

sceneNodeToJSX concatenated strings with hand-written escaping and indentation. It now collects typed props (moved to export/props.ts) and prints them with @open-pencil/codegen's JSX builders. Output is unchanged except for text: special characters print as a string expression instead of entities, and multi-line text keeps its line breaks, which the old line-splitting lost on render.

* docs(codegen): fix package metadata and dependency rules

codegen's repository.directory still named design-jsx, the README mentioned es without showing it, and the design-jsx and dom-css guides still said they depend only on scene-graph.

* test(core): move the JSX export round-trips to the design-jsx test home

Covers tabs in layer names and text, which JSX keeps as written.
2026-10-01 21:22:43 +04:00

141 lines
4.6 KiB
TypeScript

import { existsSync } from 'node:fs'
import { readdir } from 'node:fs/promises'
import { isAbsolute, join, relative, resolve, sep } from 'node:path'
import { resolveWorkspaceRoot } from '@open-pencil/package-artifacts-tools'
const REPO_ROOT = await resolveWorkspaceRoot(import.meta.dir)
/**
* Unit test shards, keyed by owning package or application area.
*
* Each group lists the owner's canonical test home from
* `packages/docs/development/testing.md` (`packages/<owner>/tests`,
* `tests/app`, `tests/integration`) together with the `tests/engine/**`
* directories it still owns, so a file is discovered from either place and a
* move needs no shard change. A path that does not exist yet contributes no
* files. `render` is Core-owned but
* sharded separately because canvas suites parse large fixtures.
*/
export const UNIT_TEST_GROUPS = {
app: [
'tests/app',
'tests/integration',
'tests/engine/acp',
'tests/engine/app',
'tests/engine/collab',
'tests/engine/tauri'
],
cli: ['packages/cli/tests', 'tests/engine/cli'],
core: [
'packages/core/tests',
'tests/engine/clipboard',
'tests/engine/color',
'tests/engine/core',
'tests/engine/editor',
'tests/engine/icons',
'tests/engine/layout',
'tests/engine/library',
'tests/engine/lint',
'tests/engine/profiler',
'tests/engine/text',
'tests/engine/tools',
'tests/engine/vector'
],
dom: [
'packages/codegen/tests',
'packages/design-jsx/tests',
'packages/dom-css/tests',
'packages/pen/tests',
'tests/engine/dom-css',
'tests/engine/pen'
],
fig: [
'packages/fig/tests',
'packages/kiwi/tests',
'tests/engine/figma',
'tests/engine/io',
'tests/engine/kiwi'
],
mcp: ['packages/mcp/tests', 'tests/engine/mcp'],
render: ['tests/engine/render'],
'scene-graph': [
'packages/scene-graph/tests',
'tests/engine/geometry',
'tests/engine/hit-test',
'tests/engine/random',
'tests/engine/scene-graph',
'tests/engine/snap'
],
vue: ['packages/vue/tests', 'tests/engine/vue']
} as const
export type UnitTestGroup = keyof typeof UNIT_TEST_GROUPS | 'all'
export const HEAVY_UNIT_TEST_PATTERNS = [
'tests/engine/clipboard/fixtures/',
'tests/engine/io/fig/heavy/',
'packages/core/tests/io/formats/fig/roundtrip/exhaustive.test.ts',
'packages/core/tests/io/formats/fig/roundtrip/glyph-blob.test.ts',
'packages/core/tests/io/formats/fig/roundtrip/variables.test.ts',
'tests/engine/io/fig/export/text.test.ts',
'tests/engine/io/fig/export/worker.test.ts',
'tests/engine/io/fig/import/group-reclassify.test.ts',
'tests/engine/layout/auto-layout/text/measurement.test.ts',
'tests/engine/render/canvas/cache.test.ts'
] as const
export function unitTestGroupNames(): UnitTestGroup[] {
return [...Object.keys(UNIT_TEST_GROUPS), 'all'] as UnitTestGroup[]
}
export function pathsForUnitTestGroup(group: UnitTestGroup): string[] {
if (group === 'all') return Object.values(UNIT_TEST_GROUPS).flat()
return [...UNIT_TEST_GROUPS[group]]
}
export function isHeavyUnitTest(path: string): boolean {
const normalized = normalizePath(path)
return HEAVY_UNIT_TEST_PATTERNS.some(
(pattern) => normalized.startsWith(pattern) || normalized === pattern
)
}
export async function listUnitTests(
group: UnitTestGroup,
options: { includeHeavy?: boolean } = {}
): Promise<string[]> {
const files = await listTestFiles(pathsForUnitTestGroup(group))
return options.includeHeavy ? files : files.filter((file) => !isHeavyUnitTest(file))
}
export async function listHeavyUnitTests(group: UnitTestGroup = 'all'): Promise<string[]> {
const files = await listTestFiles(pathsForUnitTestGroup(group))
return files.filter(isHeavyUnitTest)
}
async function listTestFiles(paths: string[]): Promise<string[]> {
const files = await Promise.all(paths.map((path) => listTestFilesInPath(path)))
return [...new Set(files.flat())].sort()
}
async function listTestFilesInPath(path: string): Promise<string[]> {
const absolutePath = resolve(REPO_ROOT, path)
if (!existsSync(absolutePath)) return []
const entries = await readdir(absolutePath, { withFileTypes: true })
const files = await Promise.all(
entries.map(async (entry) => {
const childPath = join(path, entry.name)
if (entry.isDirectory()) return listTestFilesInPath(childPath)
if (entry.isFile() && entry.name.endsWith('.test.ts')) return [normalizePath(childPath)]
return []
})
)
return files.flat()
}
function normalizePath(path: string): string {
if (!isAbsolute(path)) return path.split(sep).join('/')
return relative(REPO_ROOT, path).split(sep).join('/') || path.split(sep).join('/')
}