@pyreon/core 0.24.5 → 0.24.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/lib/analysis/index.js.html +1 -1
  2. package/lib/index.js +53 -31
  3. package/package.json +2 -6
  4. package/src/compat-marker.ts +0 -79
  5. package/src/compat-shared.ts +0 -80
  6. package/src/component.ts +0 -98
  7. package/src/context.ts +0 -349
  8. package/src/defer.ts +0 -279
  9. package/src/dynamic.ts +0 -32
  10. package/src/env.d.ts +0 -6
  11. package/src/error-boundary.ts +0 -90
  12. package/src/for.ts +0 -51
  13. package/src/h.ts +0 -80
  14. package/src/index.ts +0 -80
  15. package/src/jsx-dev-runtime.ts +0 -2
  16. package/src/jsx-runtime.ts +0 -747
  17. package/src/lazy.ts +0 -25
  18. package/src/lifecycle.ts +0 -152
  19. package/src/manifest.ts +0 -579
  20. package/src/map-array.ts +0 -42
  21. package/src/portal.ts +0 -39
  22. package/src/props.ts +0 -269
  23. package/src/ref.ts +0 -32
  24. package/src/show.ts +0 -121
  25. package/src/style.ts +0 -102
  26. package/src/suspense.ts +0 -52
  27. package/src/telemetry.ts +0 -120
  28. package/src/tests/compat-marker.test.ts +0 -96
  29. package/src/tests/compat-shared.test.ts +0 -99
  30. package/src/tests/component.test.ts +0 -281
  31. package/src/tests/context.test.ts +0 -629
  32. package/src/tests/core.test.ts +0 -1290
  33. package/src/tests/cx.test.ts +0 -70
  34. package/src/tests/defer.test.ts +0 -359
  35. package/src/tests/dynamic.test.ts +0 -87
  36. package/src/tests/error-boundary.test.ts +0 -181
  37. package/src/tests/extract-props-overloads.types.test.ts +0 -135
  38. package/src/tests/for.test.ts +0 -117
  39. package/src/tests/h.test.ts +0 -221
  40. package/src/tests/jsx-compat.test.tsx +0 -86
  41. package/src/tests/lazy.test.ts +0 -100
  42. package/src/tests/lifecycle.test.ts +0 -350
  43. package/src/tests/manifest-snapshot.test.ts +0 -100
  44. package/src/tests/map-array.test.ts +0 -313
  45. package/src/tests/native-marker-error-boundary.test.ts +0 -12
  46. package/src/tests/portal.test.ts +0 -48
  47. package/src/tests/props-extended.test.ts +0 -157
  48. package/src/tests/props.test.ts +0 -250
  49. package/src/tests/reactive-context.test.ts +0 -69
  50. package/src/tests/reactive-props.test.ts +0 -157
  51. package/src/tests/ref.test.ts +0 -70
  52. package/src/tests/show.test.ts +0 -314
  53. package/src/tests/style.test.ts +0 -157
  54. package/src/tests/suspense.test.ts +0 -139
  55. package/src/tests/telemetry.test.ts +0 -297
  56. package/src/types.ts +0 -116
@@ -1,350 +0,0 @@
1
- import {
2
- getCurrentHooks,
3
- onErrorCaptured,
4
- onMount,
5
- onUnmount,
6
- onUpdate,
7
- setCurrentHooks,
8
- } from '../lifecycle'
9
- import type { LifecycleHooks } from '../types'
10
-
11
- describe('setCurrentHooks / getCurrentHooks', () => {
12
- afterEach(() => {
13
- setCurrentHooks(null)
14
- })
15
-
16
- test('getCurrentHooks returns null by default', () => {
17
- expect(getCurrentHooks()).toBeNull()
18
- })
19
-
20
- test('setCurrentHooks sets the current hooks context', () => {
21
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
22
- setCurrentHooks(hooks)
23
- expect(getCurrentHooks()).toBe(hooks)
24
- })
25
-
26
- test('setCurrentHooks(null) clears the context', () => {
27
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
28
- setCurrentHooks(hooks)
29
- expect(getCurrentHooks()).toBe(hooks)
30
- setCurrentHooks(null)
31
- expect(getCurrentHooks()).toBeNull()
32
- })
33
- })
34
-
35
- describe('onMount', () => {
36
- afterEach(() => {
37
- setCurrentHooks(null)
38
- })
39
-
40
- test('registers callback on current hooks', () => {
41
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
42
- setCurrentHooks(hooks)
43
- const fn = () => undefined
44
- onMount(fn)
45
- expect(hooks.mount!).toHaveLength(1)
46
- expect(hooks.mount![0]).toBe(fn)
47
- })
48
-
49
- test('multiple onMount calls accumulate', () => {
50
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
51
- setCurrentHooks(hooks)
52
- onMount(() => undefined)
53
- onMount(() => undefined)
54
- onMount(() => undefined)
55
- expect(hooks.mount!).toHaveLength(3)
56
- })
57
-
58
- test('warns when called outside component setup', () => {
59
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
60
- onMount(() => {})
61
- expect(warnSpy).toHaveBeenCalledWith(
62
- expect.stringContaining('onMount() called outside component setup'),
63
- )
64
- warnSpy.mockRestore()
65
- })
66
-
67
- test('is a no-op outside component setup (no crash)', () => {
68
- expect(() => onMount(() => {})).not.toThrow()
69
- })
70
-
71
- test('accepts callback returning cleanup function', () => {
72
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
73
- setCurrentHooks(hooks)
74
- const cleanup = () => {}
75
- onMount(() => cleanup)
76
- expect(hooks.mount!).toHaveLength(1)
77
- expect(hooks.mount![0]!()).toBe(cleanup)
78
- })
79
-
80
- test('accepts callback returning void', () => {
81
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
82
- setCurrentHooks(hooks)
83
- onMount(() => {})
84
- expect(hooks.mount!).toHaveLength(1)
85
- expect(hooks.mount![0]!()).toBeUndefined()
86
- })
87
- })
88
-
89
- describe('onUnmount', () => {
90
- afterEach(() => {
91
- setCurrentHooks(null)
92
- })
93
-
94
- test('registers callback on current hooks', () => {
95
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
96
- setCurrentHooks(hooks)
97
- const fn = () => {}
98
- onUnmount(fn)
99
- expect(hooks.unmount!).toHaveLength(1)
100
- expect(hooks.unmount![0]).toBe(fn)
101
- })
102
-
103
- test('warns when called outside component setup', () => {
104
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
105
- onUnmount(() => {})
106
- expect(warnSpy).toHaveBeenCalledWith(
107
- expect.stringContaining('onUnmount() called outside component setup'),
108
- )
109
- warnSpy.mockRestore()
110
- })
111
- })
112
-
113
- describe('onUpdate', () => {
114
- afterEach(() => {
115
- setCurrentHooks(null)
116
- })
117
-
118
- test('registers callback on current hooks', () => {
119
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
120
- setCurrentHooks(hooks)
121
- const fn = () => {}
122
- onUpdate(fn)
123
- expect(hooks.update!).toHaveLength(1)
124
- expect(hooks.update![0]).toBe(fn)
125
- })
126
-
127
- test('warns when called outside component setup', () => {
128
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
129
- onUpdate(() => {})
130
- expect(warnSpy).toHaveBeenCalledWith(
131
- expect.stringContaining('onUpdate() called outside component setup'),
132
- )
133
- warnSpy.mockRestore()
134
- })
135
- })
136
-
137
- describe('onErrorCaptured', () => {
138
- afterEach(() => {
139
- setCurrentHooks(null)
140
- })
141
-
142
- test('registers callback on current hooks', () => {
143
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
144
- setCurrentHooks(hooks)
145
- const fn = () => true
146
- onErrorCaptured(fn)
147
- expect(hooks.error!).toHaveLength(1)
148
- expect(hooks.error![0]).toBe(fn)
149
- })
150
-
151
- test('warns when called outside component setup', () => {
152
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
153
- onErrorCaptured(() => true)
154
- expect(warnSpy).toHaveBeenCalledWith(
155
- expect.stringContaining('onErrorCaptured() called outside component setup'),
156
- )
157
- warnSpy.mockRestore()
158
- })
159
-
160
- test('registered handler receives the error', () => {
161
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
162
- setCurrentHooks(hooks)
163
- let captured: unknown = null
164
- onErrorCaptured((err) => {
165
- captured = err
166
- return true
167
- })
168
- // Simulate calling the handler
169
- const testError = new Error('test')
170
- hooks.error![0]!(testError)
171
- expect(captured).toBe(testError)
172
- })
173
- })
174
-
175
- describe('lifecycle hooks interaction', () => {
176
- afterEach(() => {
177
- setCurrentHooks(null)
178
- })
179
-
180
- test('all hook types can be registered in same context', () => {
181
- const hooks: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
182
- setCurrentHooks(hooks)
183
-
184
- onMount(() => undefined)
185
- onUnmount(() => {})
186
- onUpdate(() => {})
187
- onErrorCaptured(() => true)
188
-
189
- expect(hooks.mount!).toHaveLength(1)
190
- expect(hooks.unmount!).toHaveLength(1)
191
- expect(hooks.update!).toHaveLength(1)
192
- expect(hooks.error!).toHaveLength(1)
193
- })
194
-
195
- test('hooks from different setCurrentHooks calls go to different stores', () => {
196
- const hooks1: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
197
- const hooks2: LifecycleHooks = { mount: [], unmount: [], update: [], error: [] }
198
-
199
- setCurrentHooks(hooks1)
200
- onMount(() => undefined)
201
- setCurrentHooks(hooks2)
202
- onMount(() => undefined)
203
- onMount(() => undefined)
204
-
205
- expect(hooks1.mount).toHaveLength(1)
206
- expect(hooks2.mount).toHaveLength(2)
207
- })
208
- })
209
-
210
- // ─── Call-site warning enhancement (Phase 4) ───────────────────────────────
211
- // Dev warnings now include the first non-framework stack frame so the
212
- // developer can see WHICH of their components called the hook outside
213
- // setup — previously the warning just said "lifecycle.ts:17" which was
214
- // useless for debugging.
215
-
216
- describe('warnOutsideSetup — call-site capture', () => {
217
- afterEach(() => {
218
- setCurrentHooks(null)
219
- })
220
-
221
- test('warning includes a "Called from:" frame for debugging', () => {
222
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
223
- // Call onUnmount outside setup — from a user-land helper so the stack
224
- // has a distinct non-framework frame we can assert against.
225
- const userLandHelper = () => {
226
- onUnmount(() => {})
227
- }
228
- userLandHelper()
229
- expect(warnSpy).toHaveBeenCalled()
230
- const message = warnSpy.mock.calls[0]?.[0] as string
231
- expect(message).toContain('onUnmount() called outside component setup')
232
- expect(message).toContain('Called from:')
233
- warnSpy.mockRestore()
234
- })
235
-
236
- test('onUnmount warning includes provide() hint for diagnosability', () => {
237
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
238
- onUnmount(() => {})
239
- const message = warnSpy.mock.calls[0]?.[0] as string
240
- expect(message).toContain('provide()')
241
- warnSpy.mockRestore()
242
- })
243
-
244
- test('onMount warning does NOT include the onUnmount-specific hint', () => {
245
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
246
- onMount(() => {})
247
- const message = warnSpy.mock.calls[0]?.[0] as string
248
- // The hint is specific to onUnmount (provide() uses it), so onMount
249
- // shouldn't carry it — keeps the warning message targeted.
250
- expect(message).not.toContain('provide()')
251
- warnSpy.mockRestore()
252
- })
253
- })
254
-
255
- describe('captureCallSite — skip patterns cover published-bundle paths', () => {
256
- // Real bug surfaced from a 0.24.1 consumer report: the pre-fix patterns
257
- // only matched source-tree paths (`/lifecycle\.ts/`, `/core\/src\//`,
258
- // etc.). Published packages bundle to `node_modules/@pyreon/<name>/lib/`,
259
- // so the framework's own stack frames slipped through the filter — the
260
- // warning's "Called from:" line pointed at the warning emitter itself
261
- // (or the framework's lib code) instead of the user's call site.
262
- //
263
- // We can't easily synthesise a real published-bundle stack in a test,
264
- // but we CAN exercise the regex set directly. The fix added matchers
265
- // for `@pyreon/<name>/lib/` paths AND function-name matches
266
- // (`captureCallSite`, `warnOutsideSetup`) that survive bundling.
267
-
268
- // Mirror the patterns from the impl. (If this list drifts, the impl
269
- // and this test fall out of sync — that's the regression signal.)
270
- const skipPatterns = [
271
- /\/lifecycle\.[tj]s/,
272
- /\/context\.[tj]s/,
273
- /\/component\.[tj]s/,
274
- /\bcaptureCallSite\b/,
275
- /\bwarnOutsideSetup\b/,
276
- /\/(core|reactivity|runtime-dom|runtime-server|router|head|ui-core|styler|unistyle|rocketstyle|attrs|elements|kinetic)\/src\//,
277
- /node_modules\/@pyreon\/[^/]+\/lib\//,
278
- /@pyreon\/[a-z-]+\/lib\//,
279
- /node:internal/,
280
- /webpack-internal/,
281
- /<anonymous>/,
282
- ]
283
-
284
- const isSkipped = (line: string): boolean =>
285
- skipPatterns.some((p) => p.test(line))
286
-
287
- test('skips published-bundle lib paths (`@pyreon/X/lib/`)', () => {
288
- expect(
289
- isSkipped(
290
- 'at HeadProvider (file:///app/node_modules/@pyreon/head/lib/index.js:42:7)',
291
- ),
292
- ).toBe(true)
293
- expect(
294
- isSkipped(
295
- 'at provide (file:///app/node_modules/@pyreon/core/lib/index.js:96:5)',
296
- ),
297
- ).toBe(true)
298
- expect(
299
- isSkipped(
300
- 'at ThemeProvider (file:///app/node_modules/@pyreon/styler/lib/index.js:24:3)',
301
- ),
302
- ).toBe(true)
303
- })
304
-
305
- test('skips workspace source paths (`bun` condition consumers)', () => {
306
- expect(
307
- isSkipped(
308
- 'at provide (/Users/me/proj/packages/core/core/src/context.ts:88:3)',
309
- ),
310
- ).toBe(true)
311
- expect(
312
- isSkipped('at HeadProvider (/Users/me/proj/packages/core/head/src/provider.ts:56:5)'),
313
- ).toBe(true)
314
- expect(
315
- isSkipped(
316
- 'at RouterProvider (/Users/me/proj/packages/core/router/src/components.tsx:30:5)',
317
- ),
318
- ).toBe(true)
319
- })
320
-
321
- test('skips the warning infrastructure itself (function-name match)', () => {
322
- // Even if the file path is mangled (minified / bundled to a single
323
- // file like `lib/index.js`), the symbol names survive when bundlers
324
- // preserve exports. The function-name pattern catches both.
325
- expect(isSkipped('at captureCallSite (lib/index.js:22:33)')).toBe(true)
326
- expect(isSkipped('at warnOutsideSetup (lib/index.js:55:21)')).toBe(true)
327
- expect(isSkipped(' at captureCallSite (mangled-bundle.js:9999:11)')).toBe(true)
328
- })
329
-
330
- test('skips engine / anonymous frames', () => {
331
- expect(isSkipped('at <anonymous>')).toBe(true)
332
- expect(isSkipped('at runMicrotasks (node:internal/process/task_queues:96:5)')).toBe(true)
333
- })
334
-
335
- test('does NOT skip user-code paths in src/ or pages/ etc.', () => {
336
- // These should ALL fall through the skip filter so captureCallSite
337
- // returns them as the "Called from:" line — the user-actionable hint.
338
- expect(isSkipped('at MyComponent (/Users/me/proj/src/components/Foo.tsx:42:15)')).toBe(false)
339
- expect(isSkipped('at HomePage (/Users/me/proj/src/pages/Home.tsx:10:5)')).toBe(false)
340
- expect(isSkipped('at NotFound (/Users/me/proj/src/routes/_not-found.tsx:5:7)')).toBe(false)
341
- })
342
-
343
- test('does NOT skip user code that happens to be in node_modules but NOT @pyreon/', () => {
344
- // A user-installed third-party package's component shouldn't be
345
- // skipped — only `@pyreon/*` framework bundles are silenced.
346
- expect(
347
- isSkipped('at SomeLib (file:///app/node_modules/some-third-party/lib/index.js:42:7)'),
348
- ).toBe(false)
349
- })
350
- })
@@ -1,100 +0,0 @@
1
- import {
2
- renderApiReferenceEntries,
3
- renderLlmsFullSection,
4
- renderLlmsTxtLine,
5
- } from '@pyreon/manifest'
6
- import coreManifest from '../manifest'
7
-
8
- describe('gen-docs — core snapshot', () => {
9
- it('renders @pyreon/core to its expected llms.txt bullet', () => {
10
- expect(renderLlmsTxtLine(coreManifest)).toMatchInlineSnapshot(`"- @pyreon/core — VNode, h(), Fragment, lifecycle, context, JSX runtime, Suspense, ErrorBoundary, lazy(), Dynamic, cx(), splitProps, mergeProps, createUniqueId. Pyreon components are plain functions that execute a single time. Reactivity comes from reading signals inside reactive scopes (JSX expression thunks, \`effect()\`, \`computed()\`), not from re-running the component function. \`if (!cond()) return null\` at the top level runs once and is static — use \`return (() => { if (!cond()) return null; return <div /> })\` for reactive conditional rendering."`)
11
- })
12
-
13
- it('renders @pyreon/core to its expected llms-full.txt section — full body snapshot', () => {
14
- expect(renderLlmsFullSection(coreManifest)).toMatchInlineSnapshot(`
15
- "## @pyreon/core — Complete API
16
-
17
- Component model and lifecycle for Pyreon. Provides the VNode type system, \`h()\` hyperscript function, JSX automatic runtime (\`@pyreon/core/jsx-runtime\`), lifecycle hooks (\`onMount\`, \`onUnmount\`), two-tier context system (\`createContext\` for static values, \`createReactiveContext\` for signal-backed values), control-flow components (\`Show\`, \`Switch\`/\`Match\`, \`For\`, \`Suspense\`, \`ErrorBoundary\`), code-splitting via \`lazy()\`, dynamic rendering via \`Dynamic\`, and props utilities (\`splitProps\`, \`mergeProps\`, \`cx\`, \`createUniqueId\`). Components are plain functions (\`ComponentFn<P> = (props: P) => VNodeChild\`) that run ONCE — reactivity comes from reading signals inside reactive scopes, not from re-running the component.
18
-
19
- \`\`\`typescript
20
- import { h, Fragment, onMount, onUnmount, provide, createContext, createReactiveContext, useContext, Show, Switch, Match, For, Suspense, ErrorBoundary, lazy, Dynamic, cx, splitProps, mergeProps, createUniqueId, untrack } from "@pyreon/core"
21
- import { signal, computed } from "@pyreon/reactivity"
22
-
23
- // Context — static (destructure-safe) vs reactive (must call to read)
24
- const ThemeCtx = createContext<"light" | "dark">("light")
25
- const ModeCtx = createReactiveContext<"light" | "dark">("light")
26
-
27
- const App = (props: { children: any }) => {
28
- const mode = signal<"light" | "dark">("dark")
29
- provide(ThemeCtx, "dark") // static — safe to destructure
30
- provide(ModeCtx, () => mode()) // reactive — consumer must call
31
-
32
- return <>{props.children}</>
33
- }
34
-
35
- // Lifecycle
36
- const Timer = () => {
37
- const count = signal(0)
38
- onMount(() => {
39
- const id = setInterval(() => count.update(n => n + 1), 1000)
40
- return () => clearInterval(id) // cleanup runs on unmount
41
- })
42
- return <div>{() => count()}</div>
43
- }
44
-
45
- // Control flow — reactive conditional rendering
46
- const Page = (props: { items: { id: number; name: string }[]; loggedIn: () => boolean }) => (
47
- <div>
48
- <Show when={props.loggedIn()} fallback={<p>Please log in</p>}>
49
- <For each={props.items} by={item => item.id}>
50
- {item => <li>{item.name}</li>}
51
- </For>
52
- </Show>
53
- </div>
54
- )
55
-
56
- // Props utilities — preserve reactivity
57
- const Button = (props: { class?: string; size?: string; onClick: () => void; children: any }) => {
58
- const [local, rest] = splitProps(props, ["class", "size"])
59
- const merged = mergeProps({ size: "md" }, local)
60
- const id = createUniqueId()
61
- return <button id={id} {...rest} class={cx("btn", \`btn-\${merged.size}\`, local.class)} />
62
- }
63
-
64
- // Code splitting
65
- const HeavyPage = lazy(() => import("./HeavyPage"))
66
- const LazyApp = () => (
67
- <Suspense fallback={<div>Loading...</div>}>
68
- <HeavyPage />
69
- </Suspense>
70
- )
71
- \`\`\`
72
-
73
- > **Components run once**: Pyreon components are plain functions that execute a single time. Reactivity comes from reading signals inside reactive scopes (JSX expression thunks, \`effect()\`, \`computed()\`), not from re-running the component function. \`if (!cond()) return null\` at the top level runs once and is static — use \`return (() => { if (!cond()) return null; return <div /> })\` for reactive conditional rendering.
74
- >
75
- > **Destructuring props kills reactivity**: \`const { name } = props\` captures the value at setup time — it becomes static. Use \`props.name\` inside reactive scopes, or \`splitProps(props, ["name"])\` for rest patterns. The compiler handles \`const x = props.y; return <div>{x}</div>\` by inlining \`props.y\` back at the use site, but only for \`const\` (not \`let\`/\`var\`).
76
- >
77
- > **Two context types**: \`createContext<T>\` returns \`T\` from \`useContext()\` — safe to destructure. \`createReactiveContext<T>\` returns \`() => T\` — must call to read. Using the wrong one is a common source of stale-value bugs (static context for dynamic values) or unnecessary ceremony (reactive context for constants).
78
- >
79
- > **For uses by, not key**: The \`<For>\` component uses the \`by\` prop for its key function because JSX extracts \`key\` as a special VNode reconciliation prop. Writing \`<For each={items()} key={fn}>\` silently passes the key to the VNode system instead of the list reconciler.
80
- >
81
- > **JSX uses standard HTML attributes**: Use \`class\` not \`className\`, \`for\` not \`htmlFor\`, \`onInput\` not \`onChange\` for per-keystroke updates. Pyreon maps to native DOM events, not the React synthetic event system.
82
- "
83
- `)
84
- })
85
-
86
- it('renders @pyreon/core to MCP api-reference entries — one per api[] item', () => {
87
- const record = renderApiReferenceEntries(coreManifest)
88
- expect(Object.keys(record).length).toBe(31)
89
- expect(Object.keys(record)).toContain('core/h')
90
- // Compat-mode native marker — added so framework JSX components opt out
91
- // of `@pyreon/{react,preact,vue,solid}-compat` wrapping.
92
- expect(Object.keys(record)).toContain('core/nativeCompat')
93
- expect(Object.keys(record)).toContain('core/isNativeCompat')
94
- expect(Object.keys(record)).toContain('core/NATIVE_COMPAT_MARKER')
95
- // Spot-check the flagship API — h() is the hyperscript function
96
- const h = record['core/h']!
97
- expect(h.notes).toContain('JSX')
98
- expect(h.mistakes?.split('\n').length).toBeGreaterThan(2)
99
- })
100
- })