2026-06-11 19:41:52 +00:00
|
|
|
import { expect, test } from 'bun:test'
|
build(tools): group tools by role and gate them like the rest of the repo (#791)
Tools live under tools/<role>/<domain> (checks, generate, release, ci, dev), every tool is a workspace named @open-pencil/<domain>-tools, a shared tools/tsconfig.json backs the new check:tools gate that fixed 55 latent type errors, test:tools runs through bun --filter, the placement check is its own checks/test-homes package, and every tool resolves the repository through resolveWorkspaceRoot. Bun, Node, and mdast types live in a tools-root workspace so they never reach the app program.
2026-09-30 01:00:31 +00:00
|
|
|
|
|
|
|
|
import { resolveWorkspaceRoot } from '@open-pencil/package-artifacts-tools'
|
2026-06-11 19:41:52 +00:00
|
|
|
|
|
|
|
|
import {
|
|
|
|
|
isHeavyUnitTest,
|
2026-10-05 14:13:53 +00:00
|
|
|
LARGE_FIXTURES,
|
2026-06-11 19:41:52 +00:00
|
|
|
listHeavyUnitTests,
|
|
|
|
|
listUnitTests,
|
|
|
|
|
pathsForUnitTestGroup,
|
|
|
|
|
unitTestGroupNames
|
|
|
|
|
} from '../src/shards'
|
|
|
|
|
|
build(tools): group tools by role and gate them like the rest of the repo (#791)
Tools live under tools/<role>/<domain> (checks, generate, release, ci, dev), every tool is a workspace named @open-pencil/<domain>-tools, a shared tools/tsconfig.json backs the new check:tools gate that fixed 55 latent type errors, test:tools runs through bun --filter, the placement check is its own checks/test-homes package, and every tool resolves the repository through resolveWorkspaceRoot. Bun, Node, and mdast types live in a tools-root workspace so they never reach the app program.
2026-09-30 01:00:31 +00:00
|
|
|
const REPO_ROOT = await resolveWorkspaceRoot(import.meta.dir)
|
2026-09-17 07:18:16 +00:00
|
|
|
|
|
|
|
|
async function discover(pattern: string): Promise<string[]> {
|
|
|
|
|
return Array.fromAsync(new Bun.Glob(pattern).scan({ cwd: REPO_ROOT }))
|
|
|
|
|
}
|
|
|
|
|
|
fix: explain unsupported browsers instead of a blank window (#745)
* fix: explain unsupported browsers instead of a blank window
The desktop app on macOS 13 with WebKit older than Safari 17.4 opened an
empty window because startup called Promise.withResolvers, which Vite lowers
nothing for: build.target only rewrites syntax and never polyfills APIs, and
the target itself was an implicit Vite default (#744).
Make the supported baseline explicit in src/app/shell/support/baseline.ts and
feed it to build.target, a lint rule that rejects newer static built-ins in
browser-shipped sources, and the documented system requirements. Replace
Promise.withResolvers with a createDeferred() helper.
Turn src/main.ts into a small gate that checks sentinel features before
dynamically importing the app, so an old engine still evaluates enough code
to render platform-specific update guidance: macOS/Safari via Software
Update, WebKitGTK and WebView2 on Linux and Windows, and each browser's
own update path on the web, with a prefilled bug report link. Render-blocking
errors during the first route are captured through app.config.errorHandler
and shown the same way instead of leaving the window blank.
Desktop facts come from tauri-plugin-os and a webview_version command; the
bundle now declares macOS 13 as its minimum system version.
* build: enforce the browser baseline from compatibility data
Replace the hand-maintained list of built-ins newer than the baseline with
two data-driven checks. The app and browser-shipped packages pin their
TypeScript lib to ES2023, the last edition Chrome 111, Firefox 128 and
Safari 16.4 implement in full, so a newer built-in such as
Promise.withResolvers fails type-checking. Web APIs, which lib.dom does not
version, go through eslint-plugin-compat under oxlint with the same browsers
in settings.browsers, scoped to sources that ship to a browser.
A unit test keeps the oxlint browser list and the tsconfig libs derived from
src/app/shell/support/baseline.ts, so the three cannot drift apart.
* fix: recognise production error codes in the boot observer
Vue passes the error reference URL as the errorHandler info argument in
production builds instead of the development string, so the observer never
classified a setup or render failure as fatal in the shipped app and the
boot-failure notice only appeared on the dev server. Match Vue's exported
ErrorCodes in both forms, and cover the component-setup path in the E2E
spec; the scenario was also verified against a production build.
2026-09-22 10:40:59 +00:00
|
|
|
test('every app, engine and package-local test belongs to exactly one shard', async () => {
|
2026-09-17 07:18:16 +00:00
|
|
|
const discovered = [
|
fix: explain unsupported browsers instead of a blank window (#745)
* fix: explain unsupported browsers instead of a blank window
The desktop app on macOS 13 with WebKit older than Safari 17.4 opened an
empty window because startup called Promise.withResolvers, which Vite lowers
nothing for: build.target only rewrites syntax and never polyfills APIs, and
the target itself was an implicit Vite default (#744).
Make the supported baseline explicit in src/app/shell/support/baseline.ts and
feed it to build.target, a lint rule that rejects newer static built-ins in
browser-shipped sources, and the documented system requirements. Replace
Promise.withResolvers with a createDeferred() helper.
Turn src/main.ts into a small gate that checks sentinel features before
dynamically importing the app, so an old engine still evaluates enough code
to render platform-specific update guidance: macOS/Safari via Software
Update, WebKitGTK and WebView2 on Linux and Windows, and each browser's
own update path on the web, with a prefilled bug report link. Render-blocking
errors during the first route are captured through app.config.errorHandler
and shown the same way instead of leaving the window blank.
Desktop facts come from tauri-plugin-os and a webview_version command; the
bundle now declares macOS 13 as its minimum system version.
* build: enforce the browser baseline from compatibility data
Replace the hand-maintained list of built-ins newer than the baseline with
two data-driven checks. The app and browser-shipped packages pin their
TypeScript lib to ES2023, the last edition Chrome 111, Firefox 128 and
Safari 16.4 implement in full, so a newer built-in such as
Promise.withResolvers fails type-checking. Web APIs, which lib.dom does not
version, go through eslint-plugin-compat under oxlint with the same browsers
in settings.browsers, scoped to sources that ship to a browser.
A unit test keeps the oxlint browser list and the tsconfig libs derived from
src/app/shell/support/baseline.ts, so the three cannot drift apart.
* fix: recognise production error codes in the boot observer
Vue passes the error reference URL as the errorHandler info argument in
production builds instead of the development string, so the observer never
classified a setup or render failure as fatal in the shipped app and the
boot-failure notice only appeared on the dev server. Match Vue's exported
ErrorCodes in both forms, and cover the component-setup path in the E2E
spec; the scenario was also verified against a production build.
2026-09-22 10:40:59 +00:00
|
|
|
...(await discover('tests/app/**/*.test.ts')),
|
|
|
|
|
...(await discover('tests/integration/**/*.test.ts')),
|
2026-09-17 07:18:16 +00:00
|
|
|
...(await discover('tests/engine/**/*.test.ts')),
|
|
|
|
|
...(await discover('packages/*/tests/**/*.test.ts'))
|
|
|
|
|
].filter((file) => !file.startsWith('packages/harness/'))
|
2026-09-06 20:29:45 +00:00
|
|
|
const paths = pathsForUnitTestGroup('all')
|
|
|
|
|
const invalidAssignments = discovered.flatMap((file) => {
|
|
|
|
|
const owners = paths.filter((path) => file.startsWith(`${path}/`))
|
|
|
|
|
return owners.length === 1 ? [] : [{ file, owners }]
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
expect(discovered.length).toBeGreaterThan(0)
|
|
|
|
|
expect(invalidAssignments).toEqual([])
|
|
|
|
|
expect((await listUnitTests('all', { includeHeavy: true })).sort()).toEqual(discovered.sort())
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-17 07:18:16 +00:00
|
|
|
test('canonical destinations are registered before they hold files', async () => {
|
|
|
|
|
const paths = pathsForUnitTestGroup('all')
|
|
|
|
|
expect(paths).toContain('tests/app')
|
|
|
|
|
expect(paths).toContain('tests/integration')
|
|
|
|
|
for (const owner of [
|
|
|
|
|
'core',
|
|
|
|
|
'scene-graph',
|
|
|
|
|
'vue',
|
|
|
|
|
'fig',
|
|
|
|
|
'kiwi',
|
|
|
|
|
'dom-css',
|
refactor(design-jsx): extract design JSX into its own package (#793)
* refactor(design-jsx): extract design JSX into its own package
Design JSX elements, helpers, schema, reference, and JSX export only need
the scene graph, yet lived in Core, so every consumer of the authoring API
pulled in the renderer, layout, and file formats.
@open-pencil/design-jsx now owns them and depends only on scene-graph. The
renderer takes icon lookup, SVG conversion, vector creation, and layout as
DesignJSXServices; Core binds its own and exports the bound renderJSX and
renderTree from @open-pencil/core/design-jsx.
* feat(design-jsx): export the JSX runtime for TSX authoring
The package already had a JSX runtime, but nothing exported it, so design
trees could only be written as function calls or JSX strings. Export
`./jsx-runtime` and `./jsx-dev-runtime` so `jsxImportSource` works, and make
`Fragment` produce the same empty-type node as `<>` in `renderJSX` strings.
* fix(design-jsx): render fragments nested in other elements
A fragment builds a node with an empty type, which only renderJSX expanded, and only at the root. Nested fragments and fragments passed to renderTree failed with 'Unknown element: <>'. Inline fragment children when trees are built, and share root expansion between renderTree and renderJSX.
2026-09-30 16:55:27 +00:00
|
|
|
'design-jsx',
|
2026-10-04 11:22:20 +00:00
|
|
|
'emit',
|
2026-09-17 07:18:16 +00:00
|
|
|
'pen',
|
|
|
|
|
'cli',
|
|
|
|
|
'mcp'
|
|
|
|
|
]) {
|
|
|
|
|
expect(paths).toContain(`packages/${owner}/tests`)
|
|
|
|
|
}
|
|
|
|
|
await expect(listUnitTests('all')).resolves.toBeArray()
|
|
|
|
|
})
|
|
|
|
|
|
2026-09-06 20:29:45 +00:00
|
|
|
test('quick and explicit heavy tests partition the full engine suite', async () => {
|
|
|
|
|
const quick = await listUnitTests('all')
|
|
|
|
|
const heavy = await listHeavyUnitTests()
|
|
|
|
|
const all = await listUnitTests('all', { includeHeavy: true })
|
|
|
|
|
|
|
|
|
|
expect(quick.filter((file) => heavy.includes(file))).toEqual([])
|
|
|
|
|
expect([...quick, ...heavy].sort()).toEqual(all)
|
|
|
|
|
})
|
|
|
|
|
|
2026-06-11 19:41:52 +00:00
|
|
|
test('unit test groups cover all declared shards', () => {
|
|
|
|
|
expect(unitTestGroupNames()).toContain('all')
|
|
|
|
|
expect(pathsForUnitTestGroup('dom')).toContain('tests/engine/dom-css')
|
2026-09-17 07:18:16 +00:00
|
|
|
expect(pathsForUnitTestGroup('fig')).toContain('packages/fig/tests')
|
2026-06-11 19:41:52 +00:00
|
|
|
expect(pathsForUnitTestGroup('all')).toContain('tests/engine/io')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('heavy unit test matcher excludes fixture-heavy tests', () => {
|
|
|
|
|
expect(isHeavyUnitTest('tests/engine/io/fig/heavy/fixtures.test.ts')).toBe(true)
|
test(core): move the fig round-trip suite home, and stop test imports drilling
* test(core): move the fig round-trip suite into the package test home
The reader change added `verifier-contracts.test.ts` under
`tests/engine` and grew the migration baseline to admit it, which the
testing architecture forbids: new tests go to the canonical home and
the baseline only shrinks. It also started
`packages/core/tests/io/formats/fig/roundtrip/`, leaving two homes for
one domain.
These tests drive Core's exporter, so Core's home is the canonical one.
The directory moves whole — eleven files, its helpers and raw verifiers
with it — and picks up the local `assert`, `traversal`, `test-utils`
and fixture helpers already there. The baseline loses nine entries.
* refactor(core): address test helpers by alias instead of drilling
`open-pencil/no-deep-parent-relative-imports` already forbids `../../`
drilling, but `lint:structure` names the directories it checks and
`packages/{core,scene-graph,vue}/tests` were never added — so the 72
drilled imports in Core's tests, including the ones the round-trip move
just wrote, were never seen.
Core gains `#core-tests/*` beside fig's `#fig-tests/*`, registered with
the architecture rules, and its tests reach their helpers through it
and their source through `#core/*`. The three test directories join
`lint:structure`, which also surfaced six errors they had never been
checked for: four duplicated imports, an empty `noop`, and a
self-assignment the test meant, now written through a named binding.
Both guides state the rule, since a lint nobody can find in prose is
how this went unnoticed.
* fix(tools): point the heavy unit patterns at the relocated round-trip tests
`HEAVY_UNIT_TEST_PATTERNS` still listed the three heavy round-trip
tests under `tests/engine`. `test:unit` discovers them by directory so
the move looked clean, but `--heavy-only` selects by path and silently
stopped choosing them: 8 files, 52 tests, where it now runs 11 and 80.
2026-10-01 10:11:53 +00:00
|
|
|
expect(isHeavyUnitTest('packages/core/tests/io/formats/fig/roundtrip/glyph-blob.test.ts')).toBe(
|
|
|
|
|
true
|
|
|
|
|
)
|
2026-06-11 19:41:52 +00:00
|
|
|
expect(isHeavyUnitTest('tests/engine/dom-css/runtime.test.ts')).toBe(false)
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('quick unit test listing excludes heavy tests', async () => {
|
|
|
|
|
const quickFiles = await listUnitTests('all')
|
|
|
|
|
expect(quickFiles).toContain('tests/engine/dom-css/runtime.test.ts')
|
|
|
|
|
expect(quickFiles).not.toContain('tests/engine/io/fig/heavy/fixtures.test.ts')
|
|
|
|
|
expect(quickFiles).not.toContain('tests/engine/io/fig/roundtrip/glyph-blob.test.ts')
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
test('heavy unit test listing contains only heavy tests', async () => {
|
|
|
|
|
const heavyFiles = await listHeavyUnitTests()
|
|
|
|
|
expect(heavyFiles).toContain('tests/engine/io/fig/heavy/fixtures.test.ts')
|
|
|
|
|
expect(heavyFiles.every(isHeavyUnitTest)).toBe(true)
|
|
|
|
|
})
|
2026-10-05 14:13:53 +00:00
|
|
|
|
|
|
|
|
test('every test that reads a large fixture is heavy', async () => {
|
|
|
|
|
const quick = await listUnitTests('all')
|
|
|
|
|
const readers = await Promise.all(
|
|
|
|
|
quick.map(async (file) => {
|
|
|
|
|
const source = await Bun.file(`${REPO_ROOT}/${file}`).text()
|
|
|
|
|
return LARGE_FIXTURES.some((fixture) => source.includes(fixture)) ? [file] : []
|
|
|
|
|
})
|
|
|
|
|
)
|
|
|
|
|
// One of these can take several gigabytes; add it to HEAVY_UNIT_TEST_PATTERNS.
|
|
|
|
|
expect(readers.flat()).toEqual([])
|
|
|
|
|
})
|