@pyreon/connector-document 0.24.4 → 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.
- package/package.json +8 -10
- package/src/__tests__/connector-document.browser.test.tsx +0 -146
- package/src/__tests__/cssValueParser.test.ts +0 -136
- package/src/__tests__/extractDocumentTree.test.ts +0 -548
- package/src/__tests__/resolveStyles.test.ts +0 -124
- package/src/cssValueParser.ts +0 -110
- package/src/extractDocumentTree.ts +0 -280
- package/src/index.ts +0 -10
- package/src/resolveStyles.ts +0 -101
- package/src/types.ts +0 -1
|
@@ -1,548 +0,0 @@
|
|
|
1
|
-
import { h } from '@pyreon/core'
|
|
2
|
-
import { describe, expect, it } from 'vitest'
|
|
3
|
-
import type { DocumentMarker } from '../extractDocumentTree'
|
|
4
|
-
import { extractDocumentTree } from '../extractDocumentTree'
|
|
5
|
-
|
|
6
|
-
// Helper: build a real VNode via @pyreon/core's h(). The third arg
|
|
7
|
-
// is an array (kept for parity with the prior mock helper) and
|
|
8
|
-
// spreads into h()'s varargs. All tests below run real-h() VNodes
|
|
9
|
-
// through the extraction pipeline — see PR #197 for why mock-only
|
|
10
|
-
// tests masked a silent metadata drop in the real attrs HOC path.
|
|
11
|
-
const node = (
|
|
12
|
-
type: string | ((...args: any[]) => any),
|
|
13
|
-
props: Record<string, any> = {},
|
|
14
|
-
children: unknown[] = [],
|
|
15
|
-
) => h(type as any, props, ...(children as any[])) as any
|
|
16
|
-
|
|
17
|
-
// Helper: create a document-marked component function
|
|
18
|
-
const docComponent = (docType: string, render?: (...args: any[]) => any) => {
|
|
19
|
-
const fn = render ?? ((props: any) => node('div', props, props.children ? [props.children] : []))
|
|
20
|
-
;(fn as any)._documentType = docType
|
|
21
|
-
return fn as ((...args: any[]) => any) & DocumentMarker
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
describe('extractDocumentTree', () => {
|
|
25
|
-
it('extracts a simple document node', () => {
|
|
26
|
-
const Heading = docComponent('heading')
|
|
27
|
-
const tree = node(
|
|
28
|
-
Heading,
|
|
29
|
-
{ $rocketstyle: { fontSize: 24, fontWeight: 'bold' }, _documentProps: { level: 1 } },
|
|
30
|
-
['Hello World'],
|
|
31
|
-
)
|
|
32
|
-
|
|
33
|
-
const result = extractDocumentTree(tree)
|
|
34
|
-
|
|
35
|
-
expect(result.type).toBe('heading')
|
|
36
|
-
expect(result.props).toEqual({ level: 1 })
|
|
37
|
-
expect(result.children).toEqual(['Hello World'])
|
|
38
|
-
expect(result.styles).toEqual({ fontSize: 24, fontWeight: 'bold' })
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('extracts nested document nodes', () => {
|
|
42
|
-
const Section = docComponent('section')
|
|
43
|
-
const Text = docComponent('text')
|
|
44
|
-
|
|
45
|
-
const tree = node(Section, { $rocketstyle: { padding: 16 } }, [
|
|
46
|
-
node(Text, { $rocketstyle: { fontSize: 14, color: '#333' } }, ['Paragraph one']),
|
|
47
|
-
node(Text, { $rocketstyle: { fontSize: 14, color: '#333' } }, ['Paragraph two']),
|
|
48
|
-
])
|
|
49
|
-
|
|
50
|
-
const result = extractDocumentTree(tree)
|
|
51
|
-
|
|
52
|
-
expect(result.type).toBe('section')
|
|
53
|
-
expect(result.styles).toEqual({ padding: 16 })
|
|
54
|
-
expect(result.children).toHaveLength(2)
|
|
55
|
-
expect((result.children[0] as any).type).toBe('text')
|
|
56
|
-
expect((result.children[1] as any).type).toBe('text')
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('flattens transparent wrappers', () => {
|
|
60
|
-
const Section = docComponent('section')
|
|
61
|
-
const Text = docComponent('text')
|
|
62
|
-
|
|
63
|
-
// A plain div wrapper (no _documentType) should be transparent
|
|
64
|
-
const tree = node(Section, {}, [
|
|
65
|
-
node('div', {}, [node(Text, { $rocketstyle: { fontSize: 14 } }, ['Hello'])]),
|
|
66
|
-
])
|
|
67
|
-
|
|
68
|
-
const result = extractDocumentTree(tree)
|
|
69
|
-
|
|
70
|
-
expect(result.type).toBe('section')
|
|
71
|
-
expect(result.children).toHaveLength(1)
|
|
72
|
-
expect((result.children[0] as any).type).toBe('text')
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('handles string children', () => {
|
|
76
|
-
const Text = docComponent('text')
|
|
77
|
-
const tree = node(Text, {}, ['Hello', ' ', 'World'])
|
|
78
|
-
|
|
79
|
-
const result = extractDocumentTree(tree)
|
|
80
|
-
|
|
81
|
-
expect(result.children).toEqual(['Hello', ' ', 'World'])
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('handles number children', () => {
|
|
85
|
-
const Text = docComponent('text')
|
|
86
|
-
const tree = node(Text, {}, [42])
|
|
87
|
-
|
|
88
|
-
const result = extractDocumentTree(tree)
|
|
89
|
-
|
|
90
|
-
expect(result.children).toEqual(['42'])
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
it('skips null and boolean children', () => {
|
|
94
|
-
const Section = docComponent('section')
|
|
95
|
-
const tree = node(Section, {}, [null, false, true, 'visible'])
|
|
96
|
-
|
|
97
|
-
const result = extractDocumentTree(tree)
|
|
98
|
-
|
|
99
|
-
expect(result.children).toEqual(['visible'])
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('resolves reactive getter children', () => {
|
|
103
|
-
const Text = docComponent('text')
|
|
104
|
-
const tree = node(Text, {}, [() => 'dynamic text'])
|
|
105
|
-
|
|
106
|
-
const result = extractDocumentTree(tree)
|
|
107
|
-
|
|
108
|
-
expect(result.children).toEqual(['dynamic text'])
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('omits styles when includeStyles is false', () => {
|
|
112
|
-
const Heading = docComponent('heading')
|
|
113
|
-
const tree = node(Heading, { $rocketstyle: { fontSize: 24 } }, ['Hello'])
|
|
114
|
-
|
|
115
|
-
const result = extractDocumentTree(tree, { includeStyles: false })
|
|
116
|
-
|
|
117
|
-
expect(result.styles).toBeUndefined()
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('wraps in document node when root has no _documentType', () => {
|
|
121
|
-
const tree = node('div', {}, ['raw text'])
|
|
122
|
-
|
|
123
|
-
const result = extractDocumentTree(tree)
|
|
124
|
-
|
|
125
|
-
expect(result.type).toBe('document')
|
|
126
|
-
expect(result.children).toEqual(['raw text'])
|
|
127
|
-
})
|
|
128
|
-
|
|
129
|
-
it('handles component functions without _documentType by calling them', () => {
|
|
130
|
-
const Text = docComponent('text')
|
|
131
|
-
const Wrapper = (props: any) =>
|
|
132
|
-
node(Text, { $rocketstyle: { fontSize: 14 } }, [props.children])
|
|
133
|
-
|
|
134
|
-
const tree = node(Wrapper, {}, ['wrapped text'])
|
|
135
|
-
|
|
136
|
-
const result = extractDocumentTree(tree)
|
|
137
|
-
|
|
138
|
-
expect(result.type).toBe('text')
|
|
139
|
-
expect(result.children).toEqual(['wrapped text'])
|
|
140
|
-
})
|
|
141
|
-
|
|
142
|
-
it('handles function passed directly', () => {
|
|
143
|
-
const Text = docComponent('text')
|
|
144
|
-
const template = () => node(Text, { $rocketstyle: { fontSize: 14 } }, ['Hello'])
|
|
145
|
-
|
|
146
|
-
const result = extractDocumentTree(template)
|
|
147
|
-
|
|
148
|
-
expect(result.type).toBe('text')
|
|
149
|
-
expect(result.children).toEqual(['Hello'])
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
it('creates empty document for null input', () => {
|
|
153
|
-
const result = extractDocumentTree(null)
|
|
154
|
-
|
|
155
|
-
expect(result.type).toBe('document')
|
|
156
|
-
expect(result.children).toEqual([])
|
|
157
|
-
})
|
|
158
|
-
|
|
159
|
-
describe('_documentProps function value resolution (D1)', () => {
|
|
160
|
-
// Document primitives like DocDocument now accept reactive
|
|
161
|
-
// accessors (e.g. `title={() => store.name()}`) and store the
|
|
162
|
-
// function in _documentProps. extractDocumentTree resolves the
|
|
163
|
-
// function at extraction time so the export pipeline always
|
|
164
|
-
// sees the live value, not a stale snapshot from component
|
|
165
|
-
// mount time.
|
|
166
|
-
//
|
|
167
|
-
// These tests lock in the behavior at the connector-document
|
|
168
|
-
// boundary so any future change to the resolution logic gets
|
|
169
|
-
// caught by a focused unit test.
|
|
170
|
-
|
|
171
|
-
it('calls function values in _documentProps and stores the result', () => {
|
|
172
|
-
const Document = docComponent('document')
|
|
173
|
-
const tree = node(
|
|
174
|
-
Document,
|
|
175
|
-
{
|
|
176
|
-
_documentProps: {
|
|
177
|
-
title: () => 'Resolved title',
|
|
178
|
-
author: () => 'Alice',
|
|
179
|
-
subject: 'Plain string still works',
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
[],
|
|
183
|
-
)
|
|
184
|
-
|
|
185
|
-
const result = extractDocumentTree(tree)
|
|
186
|
-
|
|
187
|
-
expect(result.props).toEqual({
|
|
188
|
-
title: 'Resolved title',
|
|
189
|
-
author: 'Alice',
|
|
190
|
-
subject: 'Plain string still works',
|
|
191
|
-
})
|
|
192
|
-
})
|
|
193
|
-
|
|
194
|
-
it('reads the live value each time extractDocumentTree is called', () => {
|
|
195
|
-
// The whole point of accessors: every export call sees the
|
|
196
|
-
// current state, not a value frozen at component mount.
|
|
197
|
-
// We simulate this by mutating the closure variable between
|
|
198
|
-
// two extractDocumentTree calls on the same vnode.
|
|
199
|
-
let counter = 0
|
|
200
|
-
const Document = docComponent('document')
|
|
201
|
-
const tree = node(
|
|
202
|
-
Document,
|
|
203
|
-
{
|
|
204
|
-
_documentProps: {
|
|
205
|
-
title: () => `Export ${++counter}`,
|
|
206
|
-
},
|
|
207
|
-
},
|
|
208
|
-
[],
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
const first = extractDocumentTree(tree)
|
|
212
|
-
const second = extractDocumentTree(tree)
|
|
213
|
-
const third = extractDocumentTree(tree)
|
|
214
|
-
|
|
215
|
-
expect(first.props.title).toBe('Export 1')
|
|
216
|
-
expect(second.props.title).toBe('Export 2')
|
|
217
|
-
expect(third.props.title).toBe('Export 3')
|
|
218
|
-
})
|
|
219
|
-
|
|
220
|
-
it('mixes function and plain values in the same _documentProps object', () => {
|
|
221
|
-
const Image = docComponent('image')
|
|
222
|
-
const tree = node(
|
|
223
|
-
Image,
|
|
224
|
-
{
|
|
225
|
-
_documentProps: {
|
|
226
|
-
src: 'static-url.png', // plain
|
|
227
|
-
alt: () => 'dynamic alt text', // accessor
|
|
228
|
-
width: 800, // plain number
|
|
229
|
-
caption: () => 'caption ' + 42, // accessor returning a string
|
|
230
|
-
},
|
|
231
|
-
},
|
|
232
|
-
[],
|
|
233
|
-
)
|
|
234
|
-
|
|
235
|
-
const result = extractDocumentTree(tree)
|
|
236
|
-
expect(result.props).toEqual({
|
|
237
|
-
src: 'static-url.png',
|
|
238
|
-
alt: 'dynamic alt text',
|
|
239
|
-
width: 800,
|
|
240
|
-
caption: 'caption 42',
|
|
241
|
-
})
|
|
242
|
-
})
|
|
243
|
-
|
|
244
|
-
it('preserves backward compatibility — existing primitives with plain props still work', () => {
|
|
245
|
-
// Regression case: every existing document primitive uses
|
|
246
|
-
// plain values in _documentProps. The function-resolution
|
|
247
|
-
// change must not break them. This test mirrors the shape
|
|
248
|
-
// of DocHeading's existing _documentProps.
|
|
249
|
-
const Heading = docComponent('heading')
|
|
250
|
-
const tree = node(
|
|
251
|
-
Heading,
|
|
252
|
-
{ _documentProps: { level: 1 } },
|
|
253
|
-
['Hello'],
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
const result = extractDocumentTree(tree)
|
|
257
|
-
expect(result.props).toEqual({ level: 1 })
|
|
258
|
-
expect(typeof result.props.level).toBe('number')
|
|
259
|
-
})
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
describe('component invocation path (extracts from post-attrs vnodes)', () => {
|
|
263
|
-
// Real-world case: rocketstyle-based primitives never have
|
|
264
|
-
// `_documentProps` on the JSX vnode. The user passes
|
|
265
|
-
// `<DocDocument title="X" />`, which produces a JSX vnode
|
|
266
|
-
// with `props = { title: 'X' }`. The `_documentProps` only
|
|
267
|
-
// appears AFTER the rocketstyle attrs HOC runs the
|
|
268
|
-
// `.attrs()` callback.
|
|
269
|
-
//
|
|
270
|
-
// Before the fix, extractDocumentTree only looked for
|
|
271
|
-
// `_documentProps` on the JSX vnode's props directly — so
|
|
272
|
-
// every real primitive's metadata was silently dropped during
|
|
273
|
-
// export. The mock-vnode tests above hand-constructed
|
|
274
|
-
// `_documentProps` to bypass this and never noticed.
|
|
275
|
-
//
|
|
276
|
-
// After the fix, extractDocumentTree CALLS the component
|
|
277
|
-
// function for documentType vnodes that don't have
|
|
278
|
-
// `_documentProps` directly, captures the post-attrs result,
|
|
279
|
-
// and reads `_documentProps` from THAT.
|
|
280
|
-
//
|
|
281
|
-
// These tests use a hand-constructed component that mimics
|
|
282
|
-
// the rocketstyle attrs pattern: the component is a function
|
|
283
|
-
// with `_documentType` set, and calling it returns a vnode
|
|
284
|
-
// whose props contain `_documentProps`. No real rocketstyle
|
|
285
|
-
// dependency needed for the unit test.
|
|
286
|
-
|
|
287
|
-
it('calls the component function and reads _documentProps from the post-attrs vnode', () => {
|
|
288
|
-
// Component that mimics a rocketstyle-wrapped primitive:
|
|
289
|
-
// takes user props, returns a vnode with _documentProps
|
|
290
|
-
// populated by the "attrs callback".
|
|
291
|
-
const DocDocLike = ((userProps: { title?: string; author?: string }) =>
|
|
292
|
-
node('div', {
|
|
293
|
-
_documentProps: {
|
|
294
|
-
...(userProps.title ? { title: userProps.title } : {}),
|
|
295
|
-
...(userProps.author ? { author: userProps.author } : {}),
|
|
296
|
-
},
|
|
297
|
-
})) as ((...args: any[]) => any) & DocumentMarker
|
|
298
|
-
;(DocDocLike as any)._documentType = 'document'
|
|
299
|
-
|
|
300
|
-
// The JSX vnode has user props but NO _documentProps directly
|
|
301
|
-
const jsxVnode = node(DocDocLike, { title: 'My Doc', author: 'Alice' }, [])
|
|
302
|
-
|
|
303
|
-
const result = extractDocumentTree(jsxVnode)
|
|
304
|
-
|
|
305
|
-
expect(result.type).toBe('document')
|
|
306
|
-
expect(result.props.title).toBe('My Doc')
|
|
307
|
-
expect(result.props.author).toBe('Alice')
|
|
308
|
-
})
|
|
309
|
-
|
|
310
|
-
it('also resolves function values from the post-attrs path', () => {
|
|
311
|
-
// The two fixes compose: a real primitive that stores
|
|
312
|
-
// accessor functions in its _documentProps (via the attrs
|
|
313
|
-
// callback) gets the live value resolved at extraction time.
|
|
314
|
-
let liveTitle = 'First'
|
|
315
|
-
const DocDocLike = ((userProps: { title?: () => string }) =>
|
|
316
|
-
node('div', {
|
|
317
|
-
_documentProps: {
|
|
318
|
-
title: userProps.title, // store the accessor as-is
|
|
319
|
-
},
|
|
320
|
-
})) as ((...args: any[]) => any) & DocumentMarker
|
|
321
|
-
;(DocDocLike as any)._documentType = 'document'
|
|
322
|
-
|
|
323
|
-
const jsxVnode = node(DocDocLike, { title: () => liveTitle }, [])
|
|
324
|
-
|
|
325
|
-
const first = extractDocumentTree(jsxVnode)
|
|
326
|
-
expect(first.props.title).toBe('First')
|
|
327
|
-
|
|
328
|
-
liveTitle = 'Second'
|
|
329
|
-
const second = extractDocumentTree(jsxVnode)
|
|
330
|
-
expect(second.props.title).toBe('Second')
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
it('prefers JSX-vnode _documentProps when both paths are available (back-compat)', () => {
|
|
334
|
-
// If a vnode has _documentProps directly on its props (the
|
|
335
|
-
// mock-vnode test pattern), extractDocumentTree should use
|
|
336
|
-
// it WITHOUT calling the component. This preserves the
|
|
337
|
-
// existing tests' fast path and avoids invoking components
|
|
338
|
-
// unnecessarily.
|
|
339
|
-
let componentCalled = false
|
|
340
|
-
const DocDocLike = (() => {
|
|
341
|
-
componentCalled = true
|
|
342
|
-
return node('div', { _documentProps: { title: 'from-call' } })
|
|
343
|
-
}) as ((...args: any[]) => any) & DocumentMarker
|
|
344
|
-
;(DocDocLike as any)._documentType = 'document'
|
|
345
|
-
|
|
346
|
-
const jsxVnode = node(
|
|
347
|
-
DocDocLike,
|
|
348
|
-
{ _documentProps: { title: 'from-jsx' } },
|
|
349
|
-
[],
|
|
350
|
-
)
|
|
351
|
-
|
|
352
|
-
const result = extractDocumentTree(jsxVnode)
|
|
353
|
-
expect(result.props.title).toBe('from-jsx')
|
|
354
|
-
expect(componentCalled).toBe(false)
|
|
355
|
-
})
|
|
356
|
-
})
|
|
357
|
-
})
|
|
358
|
-
|
|
359
|
-
// ─── Real h() round-trip (parallel to the mock-vnode tests above) ────────
|
|
360
|
-
//
|
|
361
|
-
// This is the exact file PR #197 fixed — `extractDocumentTree` was
|
|
362
|
-
// silently dropping metadata from real rocketstyle primitives because
|
|
363
|
-
// the existing tests ONLY used the local `node(...)` helper that
|
|
364
|
-
// hardcodes `{ type, props, children }` literals. The mock path
|
|
365
|
-
// worked; the real pipeline (where `_documentProps` is only attached
|
|
366
|
-
// AFTER the attrs HOC runs) didn't. The `audit_test_environment` tool
|
|
367
|
-
// from PR #311 flagged this file HIGH (27 mock-helper call-sites, 0
|
|
368
|
-
// real `h()` calls, no `@pyreon/core` import).
|
|
369
|
-
//
|
|
370
|
-
// This block adds a parallel: the same tree shapes built via real
|
|
371
|
-
// `h(...)` from `@pyreon/core`. The mock `node()` helper returns a
|
|
372
|
-
// hand-built object literal; real `h()` returns whatever the current
|
|
373
|
-
// Pyreon VNode shape is — if the two ever drift, only the real-`h()`
|
|
374
|
-
// path catches it. The mock tests above stay as the fast unit-test
|
|
375
|
-
// path; these are the safety net.
|
|
376
|
-
|
|
377
|
-
describe('extractDocumentTree — real h() round-trip', () => {
|
|
378
|
-
it('extracts a simple document node built via real h()', () => {
|
|
379
|
-
const Heading = docComponent('heading')
|
|
380
|
-
const tree = h(
|
|
381
|
-
Heading,
|
|
382
|
-
{ $rocketstyle: { fontSize: 24, fontWeight: 'bold' }, _documentProps: { level: 1 } },
|
|
383
|
-
'Hello World',
|
|
384
|
-
)
|
|
385
|
-
|
|
386
|
-
const result = extractDocumentTree(tree)
|
|
387
|
-
expect(result.type).toBe('heading')
|
|
388
|
-
expect(result.props).toEqual({ level: 1 })
|
|
389
|
-
expect(result.children).toEqual(['Hello World'])
|
|
390
|
-
expect(result.styles).toEqual({ fontSize: 24, fontWeight: 'bold' })
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
it('extracts nested document nodes through real h() trees', () => {
|
|
394
|
-
const Section = docComponent('section')
|
|
395
|
-
const Text = docComponent('text')
|
|
396
|
-
|
|
397
|
-
const tree = h(
|
|
398
|
-
Section,
|
|
399
|
-
{ $rocketstyle: { padding: 16 } },
|
|
400
|
-
h(Text, { $rocketstyle: { fontSize: 14 } }, 'Paragraph one'),
|
|
401
|
-
h(Text, { $rocketstyle: { fontSize: 14 } }, 'Paragraph two'),
|
|
402
|
-
)
|
|
403
|
-
|
|
404
|
-
const result = extractDocumentTree(tree)
|
|
405
|
-
expect(result.type).toBe('section')
|
|
406
|
-
expect(result.styles).toEqual({ padding: 16 })
|
|
407
|
-
expect(result.children).toHaveLength(2)
|
|
408
|
-
const child0 = result.children[0] as { type: string; children: unknown[] }
|
|
409
|
-
const child1 = result.children[1] as { type: string; children: unknown[] }
|
|
410
|
-
expect(child0.type).toBe('text')
|
|
411
|
-
expect(child0.children).toEqual(['Paragraph one'])
|
|
412
|
-
expect(child1.type).toBe('text')
|
|
413
|
-
expect(child1.children).toEqual(['Paragraph two'])
|
|
414
|
-
})
|
|
415
|
-
|
|
416
|
-
it('transparent wrappers (no _documentType) are flattened by the extractor', () => {
|
|
417
|
-
const Section = docComponent('section')
|
|
418
|
-
const Text = docComponent('text')
|
|
419
|
-
// A plain `div` wrapper nested inside a section — no document
|
|
420
|
-
// marker on the div — should be invisible to the extractor.
|
|
421
|
-
// Consumers sprinkle layout containers without breaking the
|
|
422
|
-
// extraction pipeline.
|
|
423
|
-
const tree = h(
|
|
424
|
-
Section,
|
|
425
|
-
{},
|
|
426
|
-
h('div', {}, h(Text, { $rocketstyle: { fontSize: 14 } }, 'Hello')),
|
|
427
|
-
)
|
|
428
|
-
|
|
429
|
-
const result = extractDocumentTree(tree)
|
|
430
|
-
expect(result.type).toBe('section')
|
|
431
|
-
expect(result.children).toHaveLength(1)
|
|
432
|
-
const child0 = result.children[0] as { type: string; children: unknown[] }
|
|
433
|
-
expect(child0.type).toBe('text')
|
|
434
|
-
expect(child0.children).toEqual(['Hello'])
|
|
435
|
-
})
|
|
436
|
-
|
|
437
|
-
it('component is INVOKED during extraction (the PR #197 fix)', () => {
|
|
438
|
-
// The fix in PR #197: when a docComponent has attrs-HOC-style
|
|
439
|
-
// post-processing, extractDocumentTree must call the component to
|
|
440
|
-
// see its post-attrs VNode. With real `h()` the contract is the
|
|
441
|
-
// same — the component function on `vnode.type` must be invoked
|
|
442
|
-
// so that attrs-populated `_documentProps` surface correctly.
|
|
443
|
-
let callCount = 0
|
|
444
|
-
const Enriched = docComponent('heading', (props: any) => {
|
|
445
|
-
callCount++
|
|
446
|
-
// Mimic an attrs HOC that stamps post-attrs metadata onto a
|
|
447
|
-
// child VNode instead of the outer wrapper (the exact shape
|
|
448
|
-
// PR #197 discovered).
|
|
449
|
-
return h('div', { ...props, _documentProps: { level: 2 } }, props.children)
|
|
450
|
-
})
|
|
451
|
-
|
|
452
|
-
const tree = h(Enriched, {}, 'After attrs')
|
|
453
|
-
const result = extractDocumentTree(tree)
|
|
454
|
-
expect(callCount).toBeGreaterThan(0)
|
|
455
|
-
expect(result.type).toBe('heading')
|
|
456
|
-
expect(result.props.level).toBe(2)
|
|
457
|
-
})
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
// ─── T3.1 hoisted-attrs fast path (Path C) ────────────────────────────
|
|
461
|
-
//
|
|
462
|
-
// Real rocketstyle primitives now expose `__rs_attrs` — the accumulated
|
|
463
|
-
// `.attrs()` callback chain — on the component function itself.
|
|
464
|
-
// `extractDocumentTree` runs that chain DIRECTLY instead of invoking
|
|
465
|
-
// the full component, so `_documentProps` resolution doesn't pay for
|
|
466
|
-
// the styled wrapper / dimension resolution / JSX tree creation.
|
|
467
|
-
//
|
|
468
|
-
// The test below mimics rocketstyle's exposed surface (no real
|
|
469
|
-
// `@pyreon/rocketstyle` import needed — the contract is just "if
|
|
470
|
-
// `__rs_attrs` is present on the component function, use it"). A
|
|
471
|
-
// counter-spied "render" function asserts the body is NEVER called when
|
|
472
|
-
// the fast path is taken.
|
|
473
|
-
|
|
474
|
-
describe('extractDocumentTree — T3.1 hoisted-attrs fast path', () => {
|
|
475
|
-
it('uses __rs_attrs without invoking the component function (Path C)', () => {
|
|
476
|
-
let callCount = 0
|
|
477
|
-
// Mimic a rocketstyle primitive: function with _documentType static
|
|
478
|
-
// AND __rs_attrs static (the hoisted attrs chain). The body is what
|
|
479
|
-
// would be the styled wrapper; we count its invocations.
|
|
480
|
-
const FakeRocketDoc = ((props: any) => {
|
|
481
|
-
callCount++
|
|
482
|
-
return h('div', props, props.children)
|
|
483
|
-
}) as ((p: any) => any) & DocumentMarker
|
|
484
|
-
;(FakeRocketDoc as any)._documentType = 'document'
|
|
485
|
-
;(FakeRocketDoc as any).__rs_attrs = [
|
|
486
|
-
(props: { title?: string; author?: string }) => ({
|
|
487
|
-
_documentProps: {
|
|
488
|
-
...(props.title ? { title: props.title } : {}),
|
|
489
|
-
...(props.author ? { author: props.author } : {}),
|
|
490
|
-
},
|
|
491
|
-
}),
|
|
492
|
-
]
|
|
493
|
-
|
|
494
|
-
const tree = h(FakeRocketDoc, { title: 'My Doc', author: 'Alice' })
|
|
495
|
-
const result = extractDocumentTree(tree)
|
|
496
|
-
|
|
497
|
-
expect(result.type).toBe('document')
|
|
498
|
-
expect(result.props.title).toBe('My Doc')
|
|
499
|
-
expect(result.props.author).toBe('Alice')
|
|
500
|
-
// The architectural assertion — the component body must NOT run.
|
|
501
|
-
expect(callCount).toBe(0)
|
|
502
|
-
})
|
|
503
|
-
|
|
504
|
-
it('also resolves accessor function values from __rs_attrs (composes with D1 fix)', () => {
|
|
505
|
-
let liveTitle = 'First'
|
|
506
|
-
let callCount = 0
|
|
507
|
-
const FakeRocketDoc = ((props: any) => {
|
|
508
|
-
callCount++
|
|
509
|
-
return h('div', props, props.children)
|
|
510
|
-
}) as ((p: any) => any) & DocumentMarker
|
|
511
|
-
;(FakeRocketDoc as any)._documentType = 'document'
|
|
512
|
-
;(FakeRocketDoc as any).__rs_attrs = [
|
|
513
|
-
(props: { title?: () => string }) => ({
|
|
514
|
-
_documentProps: { title: props.title },
|
|
515
|
-
}),
|
|
516
|
-
]
|
|
517
|
-
|
|
518
|
-
const jsx = h(FakeRocketDoc, { title: () => liveTitle })
|
|
519
|
-
const first = extractDocumentTree(jsx)
|
|
520
|
-
expect(first.props.title).toBe('First')
|
|
521
|
-
|
|
522
|
-
liveTitle = 'Second'
|
|
523
|
-
const second = extractDocumentTree(jsx)
|
|
524
|
-
expect(second.props.title).toBe('Second')
|
|
525
|
-
|
|
526
|
-
// Two extractions, zero component invocations.
|
|
527
|
-
expect(callCount).toBe(0)
|
|
528
|
-
})
|
|
529
|
-
|
|
530
|
-
it('falls back to Path B (full component invocation) when __rs_attrs is absent', () => {
|
|
531
|
-
// Non-rocketstyle docComponents (test fixtures, hand-rolled HOCs)
|
|
532
|
-
// don't have __rs_attrs and must still work via the legacy Path B.
|
|
533
|
-
let callCount = 0
|
|
534
|
-
const PlainDoc = ((props: any) => {
|
|
535
|
-
callCount++
|
|
536
|
-
return h('div', { ...props, _documentProps: { level: 3 } }, props.children)
|
|
537
|
-
}) as ((p: any) => any) & DocumentMarker
|
|
538
|
-
;(PlainDoc as any)._documentType = 'heading'
|
|
539
|
-
// Note: NO __rs_attrs here — Path B should fire
|
|
540
|
-
|
|
541
|
-
const tree = h(PlainDoc, {}, 'Body')
|
|
542
|
-
const result = extractDocumentTree(tree)
|
|
543
|
-
|
|
544
|
-
expect(result.type).toBe('heading')
|
|
545
|
-
expect(result.props.level).toBe(3)
|
|
546
|
-
expect(callCount).toBeGreaterThan(0)
|
|
547
|
-
})
|
|
548
|
-
})
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import { resolveStyles } from '../resolveStyles'
|
|
3
|
-
|
|
4
|
-
describe('resolveStyles', () => {
|
|
5
|
-
it('resolves typography properties', () => {
|
|
6
|
-
const result = resolveStyles({
|
|
7
|
-
fontSize: '14px',
|
|
8
|
-
fontFamily: 'system-ui, sans-serif',
|
|
9
|
-
fontWeight: 'bold',
|
|
10
|
-
fontStyle: 'italic',
|
|
11
|
-
textDecoration: 'underline',
|
|
12
|
-
color: '#333333',
|
|
13
|
-
textAlign: 'center',
|
|
14
|
-
lineHeight: 1.5,
|
|
15
|
-
letterSpacing: '0.5px',
|
|
16
|
-
})
|
|
17
|
-
|
|
18
|
-
expect(result).toEqual({
|
|
19
|
-
fontSize: 14,
|
|
20
|
-
fontFamily: 'system-ui, sans-serif',
|
|
21
|
-
fontWeight: 'bold',
|
|
22
|
-
fontStyle: 'italic',
|
|
23
|
-
textDecoration: 'underline',
|
|
24
|
-
color: '#333333',
|
|
25
|
-
textAlign: 'center',
|
|
26
|
-
lineHeight: 1.5,
|
|
27
|
-
letterSpacing: 0.5,
|
|
28
|
-
})
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('resolves box model properties', () => {
|
|
32
|
-
const result = resolveStyles({
|
|
33
|
-
padding: '8px 16px',
|
|
34
|
-
margin: '12px',
|
|
35
|
-
})
|
|
36
|
-
|
|
37
|
-
expect(result).toEqual({
|
|
38
|
-
padding: [8, 16],
|
|
39
|
-
margin: 12,
|
|
40
|
-
})
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
it('resolves border properties', () => {
|
|
44
|
-
const result = resolveStyles({
|
|
45
|
-
borderRadius: '4px',
|
|
46
|
-
borderWidth: '1px',
|
|
47
|
-
borderColor: '#dddddd',
|
|
48
|
-
borderStyle: 'solid',
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
expect(result).toEqual({
|
|
52
|
-
borderRadius: 4,
|
|
53
|
-
borderWidth: 1,
|
|
54
|
-
borderColor: '#dddddd',
|
|
55
|
-
borderStyle: 'solid',
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('resolves sizing properties', () => {
|
|
60
|
-
const result = resolveStyles({
|
|
61
|
-
width: '200px',
|
|
62
|
-
height: 100,
|
|
63
|
-
maxWidth: '100%',
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
expect(result).toEqual({
|
|
67
|
-
width: 200,
|
|
68
|
-
height: 100,
|
|
69
|
-
maxWidth: '100%',
|
|
70
|
-
})
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
it('handles numeric values directly', () => {
|
|
74
|
-
const result = resolveStyles({
|
|
75
|
-
fontSize: 14,
|
|
76
|
-
padding: 8,
|
|
77
|
-
opacity: 0.5,
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
expect(result).toEqual({
|
|
81
|
-
fontSize: 14,
|
|
82
|
-
padding: 8,
|
|
83
|
-
opacity: 0.5,
|
|
84
|
-
})
|
|
85
|
-
})
|
|
86
|
-
|
|
87
|
-
it('ignores irrelevant CSS properties', () => {
|
|
88
|
-
const result = resolveStyles({
|
|
89
|
-
fontSize: 14,
|
|
90
|
-
transition: 'all 0.2s',
|
|
91
|
-
cursor: 'pointer',
|
|
92
|
-
display: 'flex',
|
|
93
|
-
position: 'relative',
|
|
94
|
-
transform: 'translateX(10px)',
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
expect(result).toEqual({ fontSize: 14 })
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
it('skips invalid values', () => {
|
|
101
|
-
const result = resolveStyles({
|
|
102
|
-
fontStyle: 'oblique',
|
|
103
|
-
textDecoration: 'overline',
|
|
104
|
-
borderStyle: 'none',
|
|
105
|
-
textAlign: 'start',
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
expect(result).toEqual({})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
it('returns empty object for empty input', () => {
|
|
112
|
-
expect(resolveStyles({})).toEqual({})
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('handles backgroundColor', () => {
|
|
116
|
-
const result = resolveStyles({ backgroundColor: '#4f46e5' })
|
|
117
|
-
expect(result).toEqual({ backgroundColor: '#4f46e5' })
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
it('converts rem values using rootSize', () => {
|
|
121
|
-
const result = resolveStyles({ fontSize: '1.5rem' }, 20)
|
|
122
|
-
expect(result).toEqual({ fontSize: 30 })
|
|
123
|
-
})
|
|
124
|
-
})
|