@pyreon/document-primitives 0.12.12 → 0.12.14

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pyreon/document-primitives",
3
- "version": "0.12.12",
3
+ "version": "0.12.14",
4
4
  "description": "Rocketstyle document components — render in browser, export to 18 formats",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -35,32 +35,34 @@
35
35
  "build:watch": "bun run vl_rolldown_build-watch",
36
36
  "lint": "oxlint .",
37
37
  "test": "vitest run",
38
+ "test:browser": "vitest run --config ./vitest.browser.config.ts",
38
39
  "test:coverage": "vitest run --coverage",
39
40
  "test:watch": "vitest",
40
41
  "typecheck": "tsc --noEmit"
41
42
  },
42
43
  "dependencies": {
43
- "@pyreon/connector-document": "^0.12.12"
44
+ "@pyreon/connector-document": "^0.12.14"
44
45
  },
45
46
  "devDependencies": {
46
- "@pyreon/core": "^0.12.12",
47
- "@pyreon/elements": "^0.12.12",
48
- "@pyreon/reactivity": "^0.12.12",
49
- "@pyreon/rocketstyle": "^0.12.12",
50
- "@pyreon/runtime-dom": "^0.12.12",
51
- "@pyreon/styler": "^0.12.12",
47
+ "@pyreon/core": "^0.12.14",
48
+ "@pyreon/elements": "^0.12.14",
49
+ "@pyreon/reactivity": "^0.12.14",
50
+ "@pyreon/rocketstyle": "^0.12.14",
51
+ "@pyreon/runtime-dom": "^0.12.14",
52
+ "@pyreon/styler": "^0.12.14",
52
53
  "@pyreon/test-utils": "^0.12.10",
53
- "@pyreon/typescript": "^0.12.12",
54
- "@pyreon/ui-core": "^0.12.12",
54
+ "@pyreon/typescript": "^0.12.14",
55
+ "@pyreon/ui-core": "^0.12.14",
56
+ "@vitest/browser-playwright": "^4.1.4",
55
57
  "@vitus-labs/tools-rolldown": "^1.15.4"
56
58
  },
57
59
  "peerDependencies": {
58
- "@pyreon/core": "^0.12.12",
59
- "@pyreon/document": "^0.12.12",
60
- "@pyreon/elements": "^0.12.12",
61
- "@pyreon/rocketstyle": "^0.12.12",
62
- "@pyreon/styler": "^0.12.12",
63
- "@pyreon/ui-core": "^0.12.12"
60
+ "@pyreon/core": "^0.12.14",
61
+ "@pyreon/document": "^0.12.14",
62
+ "@pyreon/elements": "^0.12.14",
63
+ "@pyreon/rocketstyle": "^0.12.14",
64
+ "@pyreon/styler": "^0.12.14",
65
+ "@pyreon/ui-core": "^0.12.14"
64
66
  },
65
67
  "engines": {
66
68
  "node": ">= 22"
@@ -0,0 +1,253 @@
1
+ import { h } from '@pyreon/core'
2
+ import { extractDocumentTree } from '@pyreon/connector-document'
3
+ import { signal } from '@pyreon/reactivity'
4
+ import { mountInBrowser } from '@pyreon/test-utils/browser'
5
+ import { afterEach, describe, expect, it } from 'vitest'
6
+ import DocCode from '../primitives/DocCode'
7
+ import DocDocument from '../primitives/DocDocument'
8
+ import DocHeading from '../primitives/DocHeading'
9
+ import DocImage from '../primitives/DocImage'
10
+ import DocLink from '../primitives/DocLink'
11
+ import DocList from '../primitives/DocList'
12
+ import DocListItem from '../primitives/DocListItem'
13
+ import DocSection from '../primitives/DocSection'
14
+ import DocTable from '../primitives/DocTable'
15
+ import DocText from '../primitives/DocText'
16
+
17
+ // Real-browser smoke suite for @pyreon/document-primitives.
18
+ //
19
+ // The contract under test here is the one PR #197 fixed: when you pass a real
20
+ // rocketstyle-wrapped primitive (not a hand-constructed mock vnode) through
21
+ // `extractDocumentTree`, the extractor must invoke the component to reach the
22
+ // post-attrs vnode where `_documentProps` actually lives. Before PR #197,
23
+ // every real primitive silently dropped its metadata during export.
24
+ //
25
+ // The existing unit tests in `connector-document/src/__tests__/` use a
26
+ // hand-constructed `DocDocLike` function. This suite closes the gap by
27
+ // using the ACTUAL `DocDocument` primitive with real rocketstyle runtime,
28
+ // in a real browser.
29
+
30
+ describe('document-primitives in real browser', () => {
31
+ afterEach(() => {
32
+ // Each test cleans up its own mount; extract-only tests have nothing to do.
33
+ })
34
+
35
+ it('extracts title + author from a real DocDocument vnode (PR #197 regression)', () => {
36
+ const vnode = h(DocDocument, { title: 'Resume', author: 'Alice' })
37
+ const tree = extractDocumentTree(vnode)
38
+
39
+ expect(tree.type).toBe('document')
40
+ expect(tree.props.title).toBe('Resume')
41
+ expect(tree.props.author).toBe('Alice')
42
+ })
43
+
44
+ it('resolves reactive accessor props at extraction time (live signal reads)', () => {
45
+ const name = signal('Alice')
46
+ const vnode = h(DocDocument, {
47
+ title: () => `${name()} — Resume`,
48
+ author: () => name(),
49
+ })
50
+
51
+ const first = extractDocumentTree(vnode)
52
+ expect(first.props.title).toBe('Alice — Resume')
53
+ expect(first.props.author).toBe('Alice')
54
+
55
+ name.set('Bob')
56
+ const second = extractDocumentTree(vnode)
57
+ expect(second.props.title).toBe('Bob — Resume')
58
+ expect(second.props.author).toBe('Bob')
59
+ })
60
+
61
+ it('renders a nested DocDocument tree to real DOM and extracts the same tree', () => {
62
+ const vnode = h(
63
+ DocDocument,
64
+ { title: 'Report' },
65
+ h(DocHeading, { level: 'h1' }, 'Q1 Results'),
66
+ h(DocText, null, 'Revenue grew 12%.'),
67
+ )
68
+
69
+ const { container, unmount } = mountInBrowser(vnode)
70
+ // Real browser renders the rocketstyle-wrapped elements.
71
+ expect(container.textContent).toContain('Q1 Results')
72
+ expect(container.textContent).toContain('Revenue grew 12%.')
73
+
74
+ // Same vnode drives the export path.
75
+ const tree = extractDocumentTree(vnode)
76
+ expect(tree.type).toBe('document')
77
+ expect(tree.props.title).toBe('Report')
78
+ expect(tree.children.length).toBeGreaterThanOrEqual(2)
79
+ const nodeChildren = tree.children.filter(
80
+ (c): c is Exclude<typeof c, string> => typeof c !== 'string',
81
+ )
82
+ expect(nodeChildren.some((c) => c.type === 'heading')).toBe(true)
83
+ expect(nodeChildren.some((c) => c.type === 'text')).toBe(true)
84
+
85
+ unmount()
86
+ })
87
+
88
+ it('extracts heading level from real DocHeading primitive', () => {
89
+ const tree = extractDocumentTree(
90
+ h(DocDocument, { title: 't' }, h(DocHeading, { level: 'h2' }, 'Section')),
91
+ )
92
+ const heading = (tree.children.find((c) => typeof c !== 'string' && c.type === 'heading') ??
93
+ null) as { type: string; props: { level?: number } } | null
94
+ expect(heading).not.toBeNull()
95
+ // DocHeading converts the 'h2' level prop into a numeric `level: 2`.
96
+ expect(heading?.props.level).toBe(2)
97
+ })
98
+
99
+ it('extracts href from real DocLink primitive', () => {
100
+ const tree = extractDocumentTree(
101
+ h(
102
+ DocDocument,
103
+ { title: 't' },
104
+ h(DocLink, { href: 'https://example.com' }, 'click me'),
105
+ ),
106
+ )
107
+ const link = (tree.children.find((c) => typeof c !== 'string' && c.type === 'link') ??
108
+ null) as { type: string; props: { href?: string } } | null
109
+ expect(link).not.toBeNull()
110
+ expect(link?.props.href).toBe('https://example.com')
111
+ })
112
+
113
+ it('extracts ordered flag from real DocList primitive', () => {
114
+ const tree = extractDocumentTree(
115
+ h(
116
+ DocDocument,
117
+ { title: 't' },
118
+ h(
119
+ DocList,
120
+ { ordered: true },
121
+ h(DocListItem, null, 'one'),
122
+ h(DocListItem, null, 'two'),
123
+ ),
124
+ ),
125
+ )
126
+ const list = (tree.children.find((c) => typeof c !== 'string' && c.type === 'list') ??
127
+ null) as { type: string; props: { ordered?: boolean }; children: unknown[] } | null
128
+ expect(list).not.toBeNull()
129
+ expect(list?.props.ordered).toBe(true)
130
+ expect(list?.children.length).toBe(2)
131
+ })
132
+
133
+ it('extracts language from real DocCode primitive', () => {
134
+ const tree = extractDocumentTree(
135
+ h(
136
+ DocDocument,
137
+ { title: 't' },
138
+ h(DocCode, { language: 'typescript' }, 'const x = 1'),
139
+ ),
140
+ )
141
+ const code = (tree.children.find((c) => typeof c !== 'string' && c.type === 'code') ??
142
+ null) as { type: string; props: { language?: string } } | null
143
+ expect(code).not.toBeNull()
144
+ expect(code?.props.language).toBe('typescript')
145
+ })
146
+
147
+ it('extracts src + alt from real DocImage primitive (no DOM forwarding crash)', () => {
148
+ // Image element has read-only `naturalWidth`/`naturalHeight` properties —
149
+ // a clean test that DocImage doesn't try to set them via property assignment.
150
+ const tree = extractDocumentTree(
151
+ h(
152
+ DocDocument,
153
+ { title: 't' },
154
+ h(DocImage, { src: 'https://example.com/x.png', alt: 'logo' }),
155
+ ),
156
+ )
157
+ const img = (tree.children.find((c) => typeof c !== 'string' && c.type === 'image') ??
158
+ null) as { type: string; props: { src?: string; alt?: string } } | null
159
+ expect(img).not.toBeNull()
160
+ expect(img?.props.src).toBe('https://example.com/x.png')
161
+ expect(img?.props.alt).toBe('logo')
162
+ })
163
+
164
+ it('extracts table rows + columns without read-only-property crash', () => {
165
+ // DocTable uses `.attrs(callback, { filter: ['rows', 'columns', ...] })`
166
+ // to strip export-only props before they reach the DOM, because
167
+ // HTMLTableElement.rows is a read-only HTMLCollection getter — assigning
168
+ // to it would crash. This test exercises that filter path.
169
+ const tree = extractDocumentTree(
170
+ h(
171
+ DocDocument,
172
+ { title: 't' },
173
+ h(DocTable, {
174
+ rows: [
175
+ ['a1', 'b1'],
176
+ ['a2', 'b2'],
177
+ ],
178
+ columns: ['col-a', 'col-b'],
179
+ }),
180
+ ),
181
+ )
182
+ const table = (tree.children.find((c) => typeof c !== 'string' && c.type === 'table') ??
183
+ null) as
184
+ | {
185
+ type: string
186
+ props: {
187
+ rows?: unknown[]
188
+ columns?: unknown[]
189
+ }
190
+ }
191
+ | null
192
+ expect(table).not.toBeNull()
193
+ expect(Array.isArray(table?.props.rows)).toBe(true)
194
+ expect(table?.props.rows).toHaveLength(2)
195
+ expect(table?.props.columns).toEqual(['col-a', 'col-b'])
196
+ })
197
+
198
+ it('extracts deeply nested tree (DocSection wrapping multiple primitives)', () => {
199
+ const tree = extractDocumentTree(
200
+ h(
201
+ DocDocument,
202
+ { title: 'Report' },
203
+ h(
204
+ DocSection,
205
+ null,
206
+ h(DocHeading, { level: 'h1' }, 'Intro'),
207
+ h(DocText, null, 'paragraph'),
208
+ h(DocSection, null, h(DocText, null, 'nested-paragraph')),
209
+ ),
210
+ ),
211
+ )
212
+ expect(tree.type).toBe('document')
213
+
214
+ // The traversal into nested sections should preserve depth.
215
+ const findByType = (node: unknown, type: string): unknown => {
216
+ if (typeof node !== 'object' || !node) return null
217
+ const n = node as { type?: string; children?: unknown[] }
218
+ if (n.type === type) return n
219
+ for (const c of n.children ?? []) {
220
+ const found = findByType(c, type)
221
+ if (found) return found
222
+ }
223
+ return null
224
+ }
225
+ const heading = findByType(tree, 'heading')
226
+ expect(heading).not.toBeNull()
227
+ // 'nested-paragraph' should appear as a text node anywhere in the tree.
228
+ const treeStr = JSON.stringify(tree)
229
+ expect(treeStr).toContain('nested-paragraph')
230
+ })
231
+
232
+ it('handles primitives that have NO _documentProps cleanly (DocText)', () => {
233
+ // DocText has `_documentProps: {}` — empty object. Confirms the
234
+ // extractor doesn't choke on absent metadata.
235
+ const tree = extractDocumentTree(
236
+ h(DocDocument, { title: 't' }, h(DocText, null, 'just text')),
237
+ )
238
+ const text = (tree.children.find((c) => typeof c !== 'string' && c.type === 'text') ??
239
+ null) as { type: string; children?: unknown[] } | null
240
+ expect(text).not.toBeNull()
241
+ expect(JSON.stringify(text)).toContain('just text')
242
+ })
243
+
244
+ it('handles undefined / nullish optional metadata (omits, does not emit `key: undefined`)', () => {
245
+ // DocDocument explicitly only sets keys when non-null. Verify by
246
+ // omitting all optional props.
247
+ const tree = extractDocumentTree(h(DocDocument, {}))
248
+ expect(tree.type).toBe('document')
249
+ expect(tree.props).not.toHaveProperty('title')
250
+ expect(tree.props).not.toHaveProperty('author')
251
+ expect(tree.props).not.toHaveProperty('subject')
252
+ })
253
+ })