@invisibleloop/pulse 0.20.38 → 0.20.40

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/src/md/md.test.js DELETED
@@ -1,217 +0,0 @@
1
- /**
2
- * Markdown parser tests — run with: node src/md/md.test.js
3
- */
4
- import { parseMd } from './index.js'
5
-
6
- let passed = 0
7
- let failed = 0
8
-
9
- function test(label, fn) {
10
- try {
11
- fn()
12
- console.log(` ✓ ${label}`)
13
- passed++
14
- } catch (e) {
15
- console.log(` ✗ ${label}`)
16
- console.log(` ${e.message}`)
17
- failed++
18
- }
19
- }
20
-
21
- function assert(condition, msg) {
22
- if (!condition) throw new Error(msg || 'Assertion failed')
23
- }
24
-
25
- // ---------------------------------------------------------------------------
26
- // Task lists (GFM checkboxes)
27
- // ---------------------------------------------------------------------------
28
-
29
- console.log('\nTask list (checkbox) rendering\n')
30
-
31
- test('unchecked item renders disabled checkbox', () => {
32
- const { html } = parseMd('- [ ] Buy milk')
33
- assert(html.includes('<input type="checkbox" class="ui-checkbox-input" disabled>'), `Got: ${html}`)
34
- assert(html.includes('Buy milk'), `Got: ${html}`)
35
- })
36
-
37
- test('checked item renders checked disabled checkbox', () => {
38
- const { html } = parseMd('- [x] Done')
39
- assert(html.includes('<input type="checkbox" class="ui-checkbox-input" disabled checked>'), `Got: ${html}`)
40
- })
41
-
42
- test('uppercase X is also treated as checked', () => {
43
- const { html } = parseMd('- [X] Done')
44
- assert(html.includes('<input type="checkbox" class="ui-checkbox-input" disabled checked>'), `Got: ${html}`)
45
- })
46
-
47
- test('task list items get task-list-item class', () => {
48
- const { html } = parseMd('- [ ] Item')
49
- assert(html.includes('class="task-list-item"'), `Got: ${html}`)
50
- })
51
-
52
- test('task item checkbox is wrapped in a label for accessibility', () => {
53
- const { html } = parseMd('- [ ] Buy milk')
54
- assert(html.includes('class="ui-checkbox ui-checkbox--disabled"'), `Got: ${html}`)
55
- assert(html.includes('<input type="checkbox" class="ui-checkbox-input" disabled>'), `Got: ${html}`)
56
- assert(html.includes('<span class="ui-checkbox-box" aria-hidden="true">'), `Got: ${html}`)
57
- assert(html.includes('<span class="ui-checkbox-label">Buy milk</span>'), `Got: ${html}`)
58
- })
59
-
60
- test('task list ul gets contains-task-list class', () => {
61
- const { html } = parseMd('- [ ] Item one\n- [x] Item two')
62
- assert(html.includes('class="contains-task-list"'), `Got: ${html}`)
63
- })
64
-
65
- test('mixed task and regular items — ul gets contains-task-list class', () => {
66
- const { html } = parseMd('- [ ] Task\n- Regular item')
67
- assert(html.includes('class="contains-task-list"'), `Got: ${html}`)
68
- })
69
-
70
- test('regular list has no task-list classes', () => {
71
- const { html } = parseMd('- Apple\n- Banana')
72
- assert(!html.includes('contains-task-list'), `Got: ${html}`)
73
- assert(!html.includes('task-list-item'), `Got: ${html}`)
74
- assert(!html.includes('checkbox'), `Got: ${html}`)
75
- })
76
-
77
- test('task item label text is rendered as inline markdown', () => {
78
- const { html } = parseMd('- [ ] Read **important** docs')
79
- assert(html.includes('<strong>important</strong>'), `Got: ${html}`)
80
- })
81
-
82
- test('multiple items preserve checked state independently', () => {
83
- const { html } = parseMd('- [ ] Unchecked\n- [x] Checked\n- [ ] Also unchecked')
84
- const checkedCount = (html.match(/disabled checked>/g) || []).length
85
- const uncheckedCount = (html.match(/disabled>/g) || []).length
86
- assert(checkedCount === 1, `Expected 1 checked, got ${checkedCount}`)
87
- assert(uncheckedCount === 2, `Expected 2 unchecked, got ${uncheckedCount}`)
88
- })
89
-
90
- // ---------------------------------------------------------------------------
91
- // Frontmatter
92
- // ---------------------------------------------------------------------------
93
-
94
- console.log('\nFrontmatter\n')
95
-
96
- test('extracts frontmatter key/value pairs', () => {
97
- const { frontmatter } = parseMd('---\ntitle: Hello World\ndate: 2026-01-01\n---\nBody text')
98
- assert(frontmatter.title === 'Hello World', `Got: ${JSON.stringify(frontmatter)}`)
99
- assert(frontmatter.date === '2026-01-01', `Got: ${JSON.stringify(frontmatter)}`)
100
- })
101
-
102
- test('body is parsed without frontmatter block', () => {
103
- const { html } = parseMd('---\ntitle: Test\n---\n# Heading')
104
- assert(html.includes('<h1'), `Got: ${html}`)
105
- assert(!html.includes('title:'), `Got: ${html}`)
106
- })
107
-
108
- test('no frontmatter — returns empty object', () => {
109
- const { frontmatter } = parseMd('# Just a heading')
110
- assert(Object.keys(frontmatter).length === 0, `Got: ${JSON.stringify(frontmatter)}`)
111
- })
112
-
113
- // ---------------------------------------------------------------------------
114
- // Core inline / block rendering
115
- // ---------------------------------------------------------------------------
116
-
117
- console.log('\nInline and block rendering\n')
118
-
119
- test('bold', () => {
120
- const { html } = parseMd('**bold**')
121
- assert(html.includes('<strong>bold</strong>'), `Got: ${html}`)
122
- })
123
-
124
- test('italic', () => {
125
- const { html } = parseMd('*italic*')
126
- assert(html.includes('<em>italic</em>'), `Got: ${html}`)
127
- })
128
-
129
- test('strikethrough', () => {
130
- const { html } = parseMd('~~strike~~')
131
- assert(html.includes('<del>strike</del>'), `Got: ${html}`)
132
- })
133
-
134
- test('inline code', () => {
135
- const { html } = parseMd('Use `foo()` here')
136
- assert(html.includes('<code>foo()</code>'), `Got: ${html}`)
137
- })
138
-
139
- test('heading levels', () => {
140
- const { html } = parseMd('# H1\n## H2\n### H3')
141
- assert(html.includes('<h1'), `Got: ${html}`)
142
- assert(html.includes('<h2'), `Got: ${html}`)
143
- assert(html.includes('<h3'), `Got: ${html}`)
144
- })
145
-
146
- test('heading gets id from text', () => {
147
- const { html } = parseMd('## My Section')
148
- assert(html.includes('id="my-section"'), `Got: ${html}`)
149
- })
150
-
151
- test('ordered list', () => {
152
- const { html } = parseMd('1. First\n2. Second')
153
- assert(html.includes('<ol>'), `Got: ${html}`)
154
- assert(html.includes('<li>First</li>'), `Got: ${html}`)
155
- })
156
-
157
- test('blockquote', () => {
158
- const { html } = parseMd('> A quote')
159
- assert(html.includes('<blockquote>'), `Got: ${html}`)
160
- })
161
-
162
- test('horizontal rule', () => {
163
- const { html } = parseMd('---')
164
- assert(html.includes('<hr>'), `Got: ${html}`)
165
- })
166
-
167
- test('link', () => {
168
- const { html } = parseMd('[Pulse](https://example.com)')
169
- assert(html.includes('<a href="https://example.com">Pulse</a>'), `Got: ${html}`)
170
- })
171
-
172
- test('HTML is escaped in inline text', () => {
173
- const { html } = parseMd('A <script>alert(1)</script> here')
174
- assert(!html.includes('<script>'), `Got: ${html}`)
175
- assert(html.includes('&lt;script&gt;'), `Got: ${html}`)
176
- })
177
-
178
- // --- URL scheme sanitisation (XSS) -----------------------------------------
179
-
180
- test('link with javascript: scheme is neutralised to #', () => {
181
- const { html } = parseMd('[x](javascript:alert(1))')
182
- assert(!html.toLowerCase().includes('javascript:'), `Got: ${html}`)
183
- assert(html.includes('href="#"'), `Got: ${html}`)
184
- })
185
-
186
- test('javascript: scheme neutralised regardless of case', () => {
187
- const { html } = parseMd('[x](JaVaScRiPt:alert(1))')
188
- assert(!html.toLowerCase().includes('javascript:'), `Got: ${html}`)
189
- })
190
-
191
- test('image with javascript: scheme is neutralised', () => {
192
- const { html } = parseMd('![i](javascript:alert(1))')
193
- assert(!html.toLowerCase().includes('javascript:'), `Got: ${html}`)
194
- assert(html.includes('src="#"'), `Got: ${html}`)
195
- })
196
-
197
- test('non-image data: URI in image is neutralised', () => {
198
- const { html } = parseMd('![i](data:text/html,<script>alert(1)</script>)')
199
- assert(html.includes('src="#"'), `Got: ${html}`)
200
- })
201
-
202
- test('image data: URI is allowed', () => {
203
- const { html } = parseMd('![i](data:image/png;base64,iVBOR)')
204
- assert(html.includes('src="data:image/png;base64,iVBOR"'), `Got: ${html}`)
205
- })
206
-
207
- test('safe schemes and relative/fragment URLs are preserved', () => {
208
- for (const url of ['https://ok.com', 'http://ok.com', '/relative', '#frag', 'mailto:a@b.com', 'tel:+15551234']) {
209
- const { html } = parseMd(`[x](${url})`)
210
- assert(html.includes(`href="${url}"`), `Expected ${url} preserved, got: ${html}`)
211
- }
212
- })
213
-
214
- // ---------------------------------------------------------------------------
215
-
216
- console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
217
- if (failed > 0) process.exit(1)
@@ -1,111 +0,0 @@
1
- /**
2
- * Pulse — Image helper tests
3
- * run: node src/runtime/image.test.js
4
- */
5
-
6
- import { img, picture } from './image.js'
7
-
8
- let passed = 0
9
- let failed = 0
10
-
11
- function test(label, fn) {
12
- try {
13
- fn()
14
- console.log(` ✓ ${label}`)
15
- passed++
16
- } catch (e) {
17
- console.log(` ✗ ${label}`)
18
- console.log(` ${e.message}`)
19
- failed++
20
- }
21
- }
22
-
23
- function assert(condition, msg) {
24
- if (!condition) throw new Error(msg || 'Assertion failed')
25
- }
26
-
27
- // ---------------------------------------------------------------------------
28
-
29
- console.log('\nimg()\n')
30
-
31
- test('renders src and alt', () => {
32
- const out = img({ src: '/photo.jpg', alt: 'A photo' })
33
- assert(out.includes('src="/photo.jpg"'), `Missing src: ${out}`)
34
- assert(out.includes('alt="A photo"'), `Missing alt: ${out}`)
35
- })
36
-
37
- test('renders width and height', () => {
38
- const out = img({ src: '/x.jpg', alt: '', width: 800, height: 600 })
39
- assert(out.includes('width="800"'), `Missing width: ${out}`)
40
- assert(out.includes('height="600"'), `Missing height: ${out}`)
41
- })
42
-
43
- test('defaults to lazy loading', () => {
44
- const out = img({ src: '/x.jpg', alt: '' })
45
- assert(out.includes('loading="lazy"'), `Expected lazy: ${out}`)
46
- assert(!out.includes('fetchpriority="high"'), `Should not have high priority: ${out}`)
47
- })
48
-
49
- test('priority sets eager loading and high fetchpriority', () => {
50
- const out = img({ src: '/hero.jpg', alt: 'Hero', priority: true })
51
- assert(out.includes('loading="eager"'), `Expected eager: ${out}`)
52
- assert(out.includes('fetchpriority="high"'), `Expected high fetchpriority: ${out}`)
53
- })
54
-
55
- test('always includes decoding="async"', () => {
56
- const out = img({ src: '/x.jpg', alt: '' })
57
- assert(out.includes('decoding="async"'), `Missing decoding: ${out}`)
58
- })
59
-
60
- test('applies class attribute', () => {
61
- const out = img({ src: '/x.jpg', alt: '', class: 'card-img' })
62
- assert(out.includes('class="card-img"'), `Missing class: ${out}`)
63
- })
64
-
65
- test('escapes alt text', () => {
66
- const out = img({ src: '/x.jpg', alt: 'A "quoted" & <tagged> image' })
67
- assert(!out.includes('"quoted"'), `Should escape quotes in alt: ${out}`)
68
- assert(out.includes('&amp;'), `Should escape & in alt: ${out}`)
69
- })
70
-
71
- // ---------------------------------------------------------------------------
72
-
73
- console.log('\npicture()\n')
74
-
75
- test('wraps img in picture element', () => {
76
- const out = picture({ src: '/x.jpg', alt: 'test', width: 100, height: 100 })
77
- assert(out.includes('<picture>'), `Missing <picture>: ${out}`)
78
- assert(out.includes('</picture>'), `Missing </picture>: ${out}`)
79
- assert(out.includes('<img '), `Missing <img>: ${out}`)
80
- })
81
-
82
- test('includes source elements for each format', () => {
83
- const out = picture({
84
- src: '/x.jpg', alt: '',
85
- sources: [
86
- { src: '/x.avif', type: 'image/avif' },
87
- { src: '/x.webp', type: 'image/webp' },
88
- ]
89
- })
90
- assert(out.includes('image/avif'), `Missing avif source: ${out}`)
91
- assert(out.includes('image/webp'), `Missing webp source: ${out}`)
92
- assert(out.includes('srcset="/x.avif"'), `Missing avif srcset: ${out}`)
93
- })
94
-
95
- test('sources appear before fallback img', () => {
96
- const out = picture({
97
- src: '/x.jpg', alt: '',
98
- sources: [{ src: '/x.webp', type: 'image/webp' }]
99
- })
100
- assert(out.indexOf('<source') < out.indexOf('<img'), `source must precede img: ${out}`)
101
- })
102
-
103
- test('priority propagates to inner img', () => {
104
- const out = picture({ src: '/x.jpg', alt: 'Hero', priority: true, sources: [] })
105
- assert(out.includes('fetchpriority="high"'), `Expected high priority in picture: ${out}`)
106
- })
107
-
108
- // ---------------------------------------------------------------------------
109
-
110
- console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
111
- if (failed > 0) process.exit(1)
@@ -1,317 +0,0 @@
1
- /**
2
- * Pulse — morph / key-based reconciliation tests
3
- * run: node src/runtime/morph.test.js
4
- *
5
- * Sets up a minimal DOM shim so morphNodes runs its real code path
6
- * (not the innerHTML fallback). Tests verify node identity is preserved
7
- * across updates — the key property of key-based reconciliation.
8
- */
9
-
10
- // ---------------------------------------------------------------------------
11
- // Minimal DOM shim
12
- // ---------------------------------------------------------------------------
13
-
14
- class FakeText {
15
- constructor(value) {
16
- this.nodeType = 3
17
- this.nodeName = '#text'
18
- this.nodeValue = value
19
- this.parentNode = null
20
- this.nextSibling = null
21
- }
22
- cloneNode() { return new FakeText(this.nodeValue) }
23
- }
24
-
25
- class FakeElement {
26
- constructor(tag) {
27
- this.nodeType = 1
28
- this.nodeName = tag.toUpperCase()
29
- this._attrs = {}
30
- this._children = []
31
- this.parentNode = null
32
- this.nextSibling = null
33
- }
34
-
35
- // ---- child access ----
36
- get childNodes() { return this._children }
37
- get lastChild() { return this._children[this._children.length - 1] ?? null }
38
-
39
- // ---- attributes ----
40
- get attributes() { return Object.entries(this._attrs).map(([name, value]) => ({ name, value })) }
41
- getAttribute(n) { return this._attrs[n] ?? null }
42
- setAttribute(n, v) { this._attrs[n] = v }
43
- removeAttribute(n) { delete this._attrs[n] }
44
- hasAttribute(n) { return n in this._attrs }
45
-
46
- // ---- mutation ----
47
- appendChild(node) {
48
- if (node.parentNode) node.parentNode._detach(node)
49
- const prev = this._children[this._children.length - 1]
50
- if (prev) prev.nextSibling = node
51
- node.nextSibling = null
52
- node.parentNode = this
53
- this._children.push(node)
54
- return node
55
- }
56
-
57
- removeChild(node) {
58
- const i = this._children.indexOf(node)
59
- if (i === -1) throw new Error('removeChild: node not a child')
60
- this._children.splice(i, 1)
61
- if (i > 0) this._children[i - 1].nextSibling = this._children[i] ?? null
62
- node.parentNode = null
63
- node.nextSibling = null
64
- return node
65
- }
66
-
67
- replaceChild(newNode, oldNode) {
68
- const i = this._children.indexOf(oldNode)
69
- if (i === -1) throw new Error('replaceChild: oldNode not a child')
70
- if (newNode.parentNode) newNode.parentNode._detach(newNode)
71
- this._children[i] = newNode
72
- newNode.parentNode = this
73
- newNode.nextSibling = this._children[i + 1] ?? null
74
- if (i > 0) this._children[i - 1].nextSibling = newNode
75
- oldNode.parentNode = null
76
- oldNode.nextSibling = null
77
- return oldNode
78
- }
79
-
80
- insertBefore(node, ref) {
81
- if (node.parentNode) node.parentNode._detach(node)
82
- if (ref === null) return this.appendChild(node)
83
- const i = this._children.indexOf(ref)
84
- if (i === -1) throw new Error('insertBefore: ref not a child')
85
- this._children.splice(i, 0, node)
86
- node.parentNode = this
87
- node.nextSibling = ref
88
- if (i > 0) this._children[i - 1].nextSibling = node
89
- return node
90
- }
91
-
92
- // Internal: remove without clearing siblings (used by move operations)
93
- _detach(node) {
94
- const i = this._children.indexOf(node)
95
- if (i === -1) return
96
- this._children.splice(i, 1)
97
- if (i > 0) this._children[i - 1].nextSibling = this._children[i] ?? null
98
- node.parentNode = null
99
- }
100
-
101
- cloneNode(deep = false) {
102
- const clone = new FakeElement(this.nodeName.toLowerCase())
103
- clone._attrs = { ...this._attrs }
104
- if (deep) {
105
- for (const child of this._children) {
106
- clone.appendChild(child.cloneNode(true))
107
- }
108
- }
109
- return clone
110
- }
111
-
112
- // ---- innerHTML (for test setup only) ----
113
- set innerHTML(html) {
114
- this._children = []
115
- for (const node of parseHtml(html)) this.appendChild(node)
116
- }
117
-
118
- get innerHTML() {
119
- return this._children.map(n => {
120
- if (n.nodeType === 3) return n.nodeValue
121
- const attrs = Object.entries(n._attrs).map(([k, v]) => ` ${k}="${v}"`).join('')
122
- return `<${n.nodeName.toLowerCase()}${attrs}>${n.innerHTML}</${n.nodeName.toLowerCase()}>`
123
- }).join('')
124
- }
125
-
126
- addEventListener() {}
127
- querySelectorAll() { return [] }
128
- }
129
-
130
- // Minimal HTML parser — handles flat list of elements with attributes + text content
131
- // Sufficient for testing morph with <li data-key="x">text</li> structures
132
- function parseHtml(html) {
133
- const nodes = []
134
- const re = /<(\w+)((?:\s+[\w-]+=(?:"[^"]*"|'[^']*'|[^\s>]*))*)\s*>([\s\S]*?)<\/\1>|([^<]+)/g
135
- let m
136
- while ((m = re.exec(html)) !== null) {
137
- if (m[4] !== undefined) {
138
- const text = m[4]
139
- if (text.trim()) nodes.push(new FakeText(text))
140
- } else {
141
- const el = new FakeElement(m[1])
142
- const attrRe = /([\w-]+)=(?:"([^"]*)"|'([^']*)'|(\S+))/g
143
- let am
144
- while ((am = attrRe.exec(m[2])) !== null) {
145
- el.setAttribute(am[1], am[2] ?? am[3] ?? am[4])
146
- }
147
- for (const child of parseHtml(m[3])) el.appendChild(child)
148
- nodes.push(el)
149
- }
150
- }
151
- return nodes
152
- }
153
-
154
- // Inject document shim so morph() uses real DOM path
155
- global.document = {
156
- createElement: (tag) => new FakeElement(tag),
157
- }
158
-
159
- // ---------------------------------------------------------------------------
160
- // Import runtime (after shim is in place)
161
- // ---------------------------------------------------------------------------
162
-
163
- import { mount } from './index.js'
164
-
165
- // ---------------------------------------------------------------------------
166
- // Test runner
167
- // ---------------------------------------------------------------------------
168
-
169
- let passed = 0
170
- let failed = 0
171
-
172
- function test(label, fn) {
173
- try {
174
- fn()
175
- console.log(` ✓ ${label}`)
176
- passed++
177
- } catch (e) {
178
- console.log(` ✗ ${label}`)
179
- console.log(` ${e.message}`)
180
- console.log(e.stack.split('\n')[1])
181
- failed++
182
- }
183
- }
184
-
185
- function assert(cond, msg) {
186
- if (!cond) throw new Error(msg || 'Assertion failed')
187
- }
188
-
189
- // ---------------------------------------------------------------------------
190
- // Helper — mount a spec and return the root element + instance
191
- // ---------------------------------------------------------------------------
192
-
193
- function makeListSpec(items) {
194
- return {
195
- route: '/list',
196
- state: { items },
197
- view: (s) => `<ul>${s.items.map(it => `<li data-key="${it.id}">${it.label}</li>`).join('')}</ul>`,
198
- mutations: {
199
- setItems: (_, items) => ({ items }),
200
- },
201
- }
202
- }
203
-
204
- console.log('\nKey-based morphing\n')
205
-
206
- // ---------------------------------------------------------------------------
207
-
208
- test('initial render produces correct keyed list', () => {
209
- const el = new FakeElement('div')
210
- const items = [{ id: 'a', label: 'Alpha' }, { id: 'b', label: 'Beta' }]
211
- mount(makeListSpec(items), el)
212
-
213
- const ul = el.childNodes[0]
214
- assert(ul.nodeName === 'UL', 'expected UL')
215
- assert(ul.childNodes.length === 2, `expected 2 children, got ${ul.childNodes.length}`)
216
- assert(ul.childNodes[0].getAttribute('data-key') === 'a', 'first key should be a')
217
- assert(ul.childNodes[1].getAttribute('data-key') === 'b', 'second key should be b')
218
- })
219
-
220
- test('removing an item removes only that node', () => {
221
- const el = new FakeElement('div')
222
- const items = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }, { id: 'c', label: 'C' }]
223
- const instance = mount(makeListSpec(items), el)
224
-
225
- const ul = el.childNodes[0]
226
- const nodeA = ul.childNodes[0]
227
- const nodeC = ul.childNodes[2]
228
-
229
- instance.dispatch('setItems', [{ id: 'a', label: 'A' }, { id: 'c', label: 'C' }])
230
-
231
- assert(ul.childNodes.length === 2, `expected 2 children after remove, got ${ul.childNodes.length}`)
232
- assert(ul.childNodes[0] === nodeA, 'node A should be the same object (not recreated)')
233
- assert(ul.childNodes[1] === nodeC, 'node C should be the same object (not recreated)')
234
- })
235
-
236
- test('reordering moves nodes without recreating them', () => {
237
- const el = new FakeElement('div')
238
- const items = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }, { id: 'c', label: 'C' }]
239
- const instance = mount(makeListSpec(items), el)
240
-
241
- const ul = el.childNodes[0]
242
- const nodeA = ul.childNodes[0]
243
- const nodeB = ul.childNodes[1]
244
- const nodeC = ul.childNodes[2]
245
-
246
- instance.dispatch('setItems', [{ id: 'c', label: 'C' }, { id: 'a', label: 'A' }, { id: 'b', label: 'B' }])
247
-
248
- assert(ul.childNodes.length === 3, 'length unchanged after reorder')
249
- assert(ul.childNodes[0] === nodeC, 'C should now be first (same node)')
250
- assert(ul.childNodes[1] === nodeA, 'A should now be second (same node)')
251
- assert(ul.childNodes[2] === nodeB, 'B should now be third (same node)')
252
- })
253
-
254
- test('inserting an item at the start preserves existing nodes', () => {
255
- const el = new FakeElement('div')
256
- const items = [{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }]
257
- const instance = mount(makeListSpec(items), el)
258
-
259
- const ul = el.childNodes[0]
260
- const nodeA = ul.childNodes[0]
261
- const nodeB = ul.childNodes[1]
262
-
263
- instance.dispatch('setItems', [{ id: 'x', label: 'X' }, { id: 'a', label: 'A' }, { id: 'b', label: 'B' }])
264
-
265
- assert(ul.childNodes.length === 3, `expected 3 children, got ${ul.childNodes.length}`)
266
- assert(ul.childNodes[0].getAttribute('data-key') === 'x', 'new node X at index 0')
267
- assert(ul.childNodes[1] === nodeA, 'A same node at index 1')
268
- assert(ul.childNodes[2] === nodeB, 'B same node at index 2')
269
- })
270
-
271
- test('text content is updated when label changes on an existing node', () => {
272
- const el = new FakeElement('div')
273
- const items = [{ id: 'a', label: 'Alpha' }, { id: 'b', label: 'Beta' }]
274
- const instance = mount(makeListSpec(items), el)
275
-
276
- const ul = el.childNodes[0]
277
- const nodeA = ul.childNodes[0]
278
-
279
- instance.dispatch('setItems', [{ id: 'a', label: 'UPDATED' }, { id: 'b', label: 'Beta' }])
280
-
281
- assert(ul.childNodes[0] === nodeA, 'node A is same object (reused)')
282
- assert(nodeA.innerHTML === 'UPDATED', `expected text UPDATED, got: ${nodeA.innerHTML}`)
283
- })
284
-
285
- test('clearing to empty list removes all nodes', () => {
286
- const el = new FakeElement('div')
287
- const instance = mount(makeListSpec([{ id: 'a', label: 'A' }, { id: 'b', label: 'B' }]), el)
288
-
289
- const ul = el.childNodes[0]
290
- instance.dispatch('setItems', [])
291
-
292
- assert(ul.childNodes.length === 0, `expected empty list, got ${ul.childNodes.length} nodes`)
293
- })
294
-
295
- test('unkeyed content falls back to position-based morphing', () => {
296
- const el = new FakeElement('div')
297
- const spec = {
298
- route: '/unkeyed',
299
- state: { label: 'hello' },
300
- view: (s) => `<p>${s.label}</p>`,
301
- mutations: { setLabel: (_, v) => ({ label: v }) },
302
- }
303
- const instance = mount(spec, el)
304
- instance.dispatch('setLabel', 'world')
305
-
306
- assert(el.childNodes[0].innerHTML === 'world', `expected 'world', got: ${el.childNodes[0].innerHTML}`)
307
- })
308
-
309
- // ---------------------------------------------------------------------------
310
-
311
- console.log()
312
- if (failed > 0) {
313
- console.log(`${passed} passed, ${failed} failed`)
314
- process.exit(1)
315
- } else {
316
- console.log(`${passed} passed, 0 failed`)
317
- }