@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.
@@ -1,508 +0,0 @@
1
- /**
2
- * Pulse — Testing helpers tests
3
- * run: node src/testing/testing.test.js
4
- */
5
-
6
- import { render, renderSync } from './index.js'
7
-
8
- // ---------------------------------------------------------------------------
9
- // Test runner
10
- // ---------------------------------------------------------------------------
11
-
12
- let passed = 0
13
- let failed = 0
14
-
15
- async function test(label, fn) {
16
- try {
17
- await fn()
18
- console.log(` ✓ ${label}`)
19
- passed++
20
- } catch (e) {
21
- console.log(` ✗ ${label}`)
22
- console.log(` ${e.message}`)
23
- failed++
24
- }
25
- }
26
-
27
- function assert(condition, msg) {
28
- if (!condition) throw new Error(msg || 'Assertion failed')
29
- }
30
-
31
- function assertEqual(a, b) {
32
- if (a !== b) throw new Error(`Expected ${JSON.stringify(b)}, got ${JSON.stringify(a)}`)
33
- }
34
-
35
- // ---------------------------------------------------------------------------
36
- // Specs used across tests
37
- // ---------------------------------------------------------------------------
38
-
39
- const counterSpec = {
40
- route: '/counter',
41
- state: { count: 0 },
42
- view: (state) => `<div class="counter"><span id="count">${state.count}</span></div>`,
43
- }
44
-
45
- const productSpec = {
46
- route: '/products/:id',
47
- state: {},
48
- server: {
49
- product: async (ctx) => ({ id: ctx.params?.id ?? '1', name: 'Widget', price: 9.99 })
50
- },
51
- view: (_s, server) => `
52
- <main>
53
- <h1 class="title">${server.product.name}</h1>
54
- <p class="price">$${server.product.price}</p>
55
- <button type="button" data-event="buy" disabled>Buy</button>
56
- </main>
57
- `,
58
- }
59
-
60
- const formSpec = {
61
- route: '/contact',
62
- state: { name: '', email: '' },
63
- view: (state) => `
64
- <form data-action="submit">
65
- <input name="name" type="text" value="${state.name}" required>
66
- <input name="email" type="email" value="${state.email}">
67
- <button type="submit">Send</button>
68
- </form>
69
- `,
70
- }
71
-
72
- const segmentedSpec = {
73
- route: '/page',
74
- state: {},
75
- view: {
76
- header: () => '<header><h1>Title</h1></header>',
77
- body: () => '<main><p>Content</p></main>',
78
- },
79
- }
80
-
81
- const listSpec = {
82
- route: '/items',
83
- state: {},
84
- server: {
85
- items: async () => ['Alpha', 'Beta', 'Gamma']
86
- },
87
- view: (_s, server) => `<ul>${server.items.map(i => `<li>${i}</li>`).join('')}</ul>`,
88
- }
89
-
90
- // ---------------------------------------------------------------------------
91
-
92
- console.log('\nrenderSync — basic rendering\n')
93
-
94
- await test('renders view with default state', async () => {
95
- const result = renderSync(counterSpec)
96
- assert(result.html.includes('0'), `Expected 0, got: ${result.html}`)
97
- })
98
-
99
- await test('renders view with state overrides', async () => {
100
- const result = renderSync(counterSpec, { state: { count: 42 } })
101
- assert(result.html.includes('42'), `Expected 42, got: ${result.html}`)
102
- })
103
-
104
- await test('result.state reflects overrides merged with spec defaults', async () => {
105
- const result = renderSync(counterSpec, { state: { count: 7 } })
106
- assertEqual(result.state.count, 7)
107
- })
108
-
109
- await test('renders segmented view by concatenating all segments', async () => {
110
- const result = renderSync(segmentedSpec)
111
- assert(result.html.includes('<header>'), `Missing header: ${result.html}`)
112
- assert(result.html.includes('<main>'), `Missing main: ${result.html}`)
113
- })
114
-
115
- await test('renders view with mock server state', async () => {
116
- const result = renderSync(productSpec, { server: { product: { name: 'Gadget', price: 19.99 } } })
117
- assert(result.html.includes('Gadget'), `Expected Gadget, got: ${result.html}`)
118
- })
119
-
120
- await test('result.server reflects the server state provided', async () => {
121
- const server = { product: { name: 'Gadget', price: 19.99 } }
122
- const result = renderSync(productSpec, { server })
123
- assertEqual(result.server.product.name, 'Gadget')
124
- })
125
-
126
- // ---------------------------------------------------------------------------
127
-
128
- console.log('\nrender — async rendering\n')
129
-
130
- await test('resolves spec.server fetchers and passes to view', async () => {
131
- const result = await render(productSpec)
132
- assert(result.html.includes('Widget'), `Expected Widget, got: ${result.html}`)
133
- })
134
-
135
- await test('passes ctx to server fetchers', async () => {
136
- const result = await render(productSpec, { ctx: { params: { id: '99' } } })
137
- assert(result.html.includes('Widget'), 'Expected Widget in output')
138
- assertEqual(result.server.product.id, '99')
139
- })
140
-
141
- await test('bypasses fetchers when server is provided', async () => {
142
- let fetcherCalled = false
143
- const spec = {
144
- route: '/mock',
145
- state: {},
146
- server: { data: async () => { fetcherCalled = true; return 'real' } },
147
- view: (_s, server) => `<p>${server.data}</p>`,
148
- }
149
- const result = await render(spec, { server: { data: 'mocked' } })
150
- assert(!fetcherCalled, 'Fetcher should not be called when server is provided')
151
- assert(result.html.includes('mocked'), `Expected mocked, got: ${result.html}`)
152
- })
153
-
154
- await test('resolves multiple fetchers in parallel', async () => {
155
- const result = await render(listSpec)
156
- assert(result.html.includes('Alpha'), `Missing Alpha: ${result.html}`)
157
- assert(result.html.includes('Beta'), `Missing Beta: ${result.html}`)
158
- assert(result.html.includes('Gamma'), `Missing Gamma: ${result.html}`)
159
- })
160
-
161
- await test('result.server contains resolved fetcher results', async () => {
162
- const result = await render(listSpec)
163
- assert(Array.isArray(result.server.items), 'Expected items array')
164
- assertEqual(result.server.items.length, 3)
165
- })
166
-
167
- // ---------------------------------------------------------------------------
168
-
169
- console.log('\nresult.text()\n')
170
-
171
- await test('returns all text content with tags stripped', async () => {
172
- const result = renderSync(counterSpec, { state: { count: 5 } })
173
- const text = result.text()
174
- assertEqual(text, '5')
175
- })
176
-
177
- await test('collapses whitespace and joins text from multiple elements', async () => {
178
- const result = renderSync(formSpec)
179
- // Buttons have text "Send"
180
- assert(result.text().includes('Send'), `Expected Send in text: ${result.text()}`)
181
- })
182
-
183
- // ---------------------------------------------------------------------------
184
-
185
- console.log('\nresult.has(selector)\n')
186
-
187
- await test('returns true when element exists', async () => {
188
- const result = renderSync(counterSpec)
189
- assert(result.has('div'), 'Expected div to exist')
190
- })
191
-
192
- await test('returns true for class selector', async () => {
193
- const result = renderSync(counterSpec)
194
- assert(result.has('.counter'), 'Expected .counter to exist')
195
- })
196
-
197
- await test('returns false when element does not exist', async () => {
198
- const result = renderSync(counterSpec)
199
- assert(!result.has('table'), 'Expected no table')
200
- })
201
-
202
- await test('matches attribute selector [attr]', async () => {
203
- const result = renderSync(formSpec)
204
- assert(result.has('[required]'), 'Expected [required] to exist')
205
- })
206
-
207
- await test('matches attribute selector [attr="value"]', async () => {
208
- const result = renderSync(formSpec)
209
- assert(result.has('input[type="email"]'), 'Expected email input')
210
- })
211
-
212
- await test('matches attribute selector [attr="value"] (negative)', async () => {
213
- const result = renderSync(formSpec)
214
- assert(!result.has('input[type="password"]'), 'Expected no password input')
215
- })
216
-
217
- await test('matches attribute selector [attr="value with spaces"]', async () => {
218
- const spec = {
219
- route: '/test-attr-spaces',
220
- view: () => `<div><span aria-label="3 items in cart">3</span></div>`,
221
- }
222
- const result = renderSync(spec)
223
- assert(result.has('span[aria-label="3 items in cart"]'), 'Expected span with aria-label containing spaces')
224
- })
225
-
226
- await test('descendant selector not confused by spaces in attr value', async () => {
227
- const spec = {
228
- route: '/test-desc-attr',
229
- view: () => `<nav><a href="/cart" aria-label="3 items in cart">Cart</a></nav>`,
230
- }
231
- const result = renderSync(spec)
232
- assert(result.has('nav a[aria-label="3 items in cart"]'), 'Expected descendant with spaced attr value')
233
- })
234
-
235
- // ---------------------------------------------------------------------------
236
-
237
- console.log('\nresult.find() and result.get()\n')
238
-
239
- await test('find() returns the first matching element', async () => {
240
- const result = renderSync(counterSpec)
241
- const el = result.find('span')
242
- assert(el !== null, 'Expected span element')
243
- assertEqual(el.tag, 'span')
244
- })
245
-
246
- await test('find() returns null when not found', async () => {
247
- const result = renderSync(counterSpec)
248
- const el = result.find('table')
249
- assertEqual(el, null)
250
- })
251
-
252
- await test('get() returns the element when found', async () => {
253
- const result = renderSync(counterSpec)
254
- const el = result.get('.counter')
255
- assert(el !== null, 'Expected .counter element')
256
- assertEqual(el.tag, 'div')
257
- })
258
-
259
- await test('get() throws when element not found', async () => {
260
- const result = renderSync(counterSpec)
261
- let threw = false
262
- try { result.get('table') } catch (e) {
263
- threw = true
264
- assert(e.message.includes('table'), `Expected selector in error: ${e.message}`)
265
- }
266
- assert(threw, 'Expected get() to throw')
267
- })
268
-
269
- await test('find() by id selector', async () => {
270
- const result = renderSync(counterSpec, { state: { count: 3 } })
271
- const el = result.find('#count')
272
- assert(el !== null, 'Expected #count element')
273
- assertEqual(el.text, '3')
274
- })
275
-
276
- await test('find() by compound selector (tag + class)', async () => {
277
- const result = renderSync(counterSpec)
278
- const el = result.find('div.counter')
279
- assert(el !== null, 'Expected div.counter')
280
- })
281
-
282
- await test('find() by attribute selector', async () => {
283
- const result = await render(productSpec, { server: { product: { name: 'W', price: 1 } } })
284
- const el = result.find('[disabled]')
285
- assert(el !== null, 'Expected disabled button')
286
- assertEqual(el.tag, 'button')
287
- })
288
-
289
- // ---------------------------------------------------------------------------
290
-
291
- console.log('\nresult.findAll()\n')
292
-
293
- await test('findAll() returns all matching elements', async () => {
294
- const result = await render(listSpec)
295
- const items = result.findAll('li')
296
- assertEqual(items.length, 3)
297
- })
298
-
299
- await test('findAll() returns empty array when nothing matches', async () => {
300
- const result = renderSync(counterSpec)
301
- const items = result.findAll('table')
302
- assertEqual(items.length, 0)
303
- })
304
-
305
- // ---------------------------------------------------------------------------
306
-
307
- console.log('\nresult.count()\n')
308
-
309
- await test('count() returns the number of matching elements', async () => {
310
- const result = renderSync(formSpec)
311
- assertEqual(result.count('input'), 2)
312
- })
313
-
314
- await test('count() returns 0 when nothing matches', async () => {
315
- const result = renderSync(counterSpec)
316
- assertEqual(result.count('table'), 0)
317
- })
318
-
319
- // ---------------------------------------------------------------------------
320
-
321
- console.log('\nresult.attr()\n')
322
-
323
- await test('attr() returns attribute value', async () => {
324
- const result = renderSync(formSpec)
325
- assertEqual(result.attr('input[name="email"]', 'type'), 'email')
326
- })
327
-
328
- await test('attr() returns empty string for boolean attributes', async () => {
329
- const result = await render(productSpec, { server: { product: { name: 'W', price: 1 } } })
330
- assertEqual(result.attr('button[disabled]', 'disabled'), '')
331
- })
332
-
333
- await test('attr() returns null when element not found', async () => {
334
- const result = renderSync(counterSpec)
335
- assertEqual(result.attr('table', 'class'), null)
336
- })
337
-
338
- await test('attr() returns null when attribute not present', async () => {
339
- const result = renderSync(counterSpec)
340
- assertEqual(result.attr('span', 'data-missing'), null)
341
- })
342
-
343
- // ---------------------------------------------------------------------------
344
-
345
- console.log('\nElement methods\n')
346
-
347
- await test('element.find() searches within the element', async () => {
348
- const result = renderSync(counterSpec)
349
- const container = result.get('.counter')
350
- const span = container.find('span')
351
- assert(span !== null, 'Expected span inside .counter')
352
- })
353
-
354
- await test('element.find() does not match outside the element', async () => {
355
- const spec = {
356
- route: '/test',
357
- state: {},
358
- view: () => `
359
- <section id="a"><p class="target">A</p></section>
360
- <section id="b"><p class="other">B</p></section>
361
- `,
362
- }
363
- const result = renderSync(spec)
364
- const a = result.get('#a')
365
- assert(a.has('.target'), 'Expected .target in #a')
366
- assert(!a.has('.other'), 'Expected .other to be outside #a')
367
- })
368
-
369
- await test('element.findAll() returns all descendants', async () => {
370
- const result = await render(listSpec)
371
- const ul = result.get('ul')
372
- assertEqual(ul.findAll('li').length, 3)
373
- })
374
-
375
- await test('element.text returns inner text content', async () => {
376
- const result = await render(listSpec)
377
- const items = result.findAll('li')
378
- assertEqual(items[0].text, 'Alpha')
379
- assertEqual(items[1].text, 'Beta')
380
- assertEqual(items[2].text, 'Gamma')
381
- })
382
-
383
- await test('element.attr() returns attribute value', async () => {
384
- const result = renderSync(formSpec)
385
- const email = result.get('input[type="email"]')
386
- assertEqual(email.attr('name'), 'email')
387
- })
388
-
389
- await test('element.has() checks for descendants', async () => {
390
- const result = renderSync(formSpec)
391
- const form = result.get('form')
392
- assert(form.has('button[type="submit"]'), 'Expected submit button in form')
393
- })
394
-
395
- // ---------------------------------------------------------------------------
396
-
397
- console.log('\nHTML entity handling\n')
398
-
399
- await test('text() decodes HTML entities', async () => {
400
- const spec = {
401
- route: '/esc',
402
- state: { msg: 'Hello &amp; World' },
403
- view: (state) => `<p>${state.msg}</p>`,
404
- }
405
- const result = renderSync(spec)
406
- assertEqual(result.get('p').text, 'Hello & World')
407
- })
408
-
409
- await test('text() decodes &lt; and &gt;', async () => {
410
- const spec = {
411
- route: '/esc2',
412
- state: {},
413
- view: () => `<p>&lt;code&gt;</p>`,
414
- }
415
- const result = renderSync(spec)
416
- assertEqual(result.get('p').text, '<code>')
417
- })
418
-
419
- // ---------------------------------------------------------------------------
420
-
421
- console.log('\nEdge cases\n')
422
-
423
- await test('handles void elements without closing tag', async () => {
424
- const spec = {
425
- route: '/void',
426
- state: {},
427
- view: () => `<div><img src="/logo.png" alt="Logo"><input type="text"></div>`,
428
- }
429
- const result = renderSync(spec)
430
- assert(result.has('img'), 'Expected img')
431
- assert(result.has('input'), 'Expected input')
432
- assertEqual(result.attr('img', 'src'), '/logo.png')
433
- })
434
-
435
- await test('handles nested same-tag elements correctly', async () => {
436
- const spec = {
437
- route: '/nested',
438
- state: {},
439
- view: () => `<div class="outer"><div class="inner"><span>deep</span></div></div>`,
440
- }
441
- const result = renderSync(spec)
442
- const outer = result.get('.outer')
443
- const inner = outer.find('.inner')
444
- assert(inner !== null, 'Expected .inner')
445
- const span = inner.find('span')
446
- assert(span !== null, 'Expected span in .inner')
447
- assertEqual(span.text, 'deep')
448
- })
449
-
450
- await test('count() handles multiple disjoint elements', async () => {
451
- const spec = {
452
- route: '/multi',
453
- state: {},
454
- view: () => `<ul><li>A</li><li>B</li><li>C</li></ul>`,
455
- }
456
- const result = renderSync(spec)
457
- assertEqual(result.count('li'), 3)
458
- })
459
-
460
- await test('renderSync returns raw html string', async () => {
461
- const result = renderSync(counterSpec)
462
- assert(typeof result.html === 'string', 'Expected html to be a string')
463
- })
464
-
465
- console.log('\nDescendant selectors\n')
466
-
467
- await test('descendant selector finds nested elements', async () => {
468
- const spec = {
469
- route: '/list',
470
- state: {},
471
- view: () => `<div><ul class="items"><li>A</li><li>B</li></ul><ul class="other"><li>X</li></ul></div>`,
472
- }
473
- const result = renderSync(spec)
474
- assertEqual(result.count('.items li'), 2)
475
- assertEqual(result.count('.other li'), 1)
476
- assertEqual(result.count('ul li'), 3)
477
- })
478
-
479
- await test('descendant selector with multiple levels', async () => {
480
- const spec = {
481
- route: '/table',
482
- state: {},
483
- view: () => `<table><thead><tr><th>Name</th></tr></thead><tbody><tr><td>Alice</td></tr><tr><td>Bob</td></tr></tbody></table>`,
484
- }
485
- const result = renderSync(spec)
486
- assertEqual(result.count('tbody tr'), 2)
487
- assertEqual(result.count('thead tr'), 1)
488
- const cells = result.findAll('tbody td')
489
- assertEqual(cells.length, 2)
490
- assertEqual(cells[0].text, 'Alice')
491
- assertEqual(cells[1].text, 'Bob')
492
- })
493
-
494
- await test('descendant selector returns empty for no matches', async () => {
495
- const spec = {
496
- route: '/empty',
497
- state: {},
498
- view: () => `<div class="container"><span>text</span></div>`,
499
- }
500
- const result = renderSync(spec)
501
- assertEqual(result.count('.container li'), 0)
502
- assertEqual(result.find('.container li'), null)
503
- })
504
-
505
- // ---------------------------------------------------------------------------
506
-
507
- console.log(`\n${passed + failed} tests: ${passed} passed, ${failed} failed\n`)
508
- if (failed > 0) process.exit(1)