···11-import { html } from 'dhtml'
11+import { html, type Displayable } from 'dhtml'
22import { invalidate, onMount, onUnmount } from 'dhtml/client'
33-import { assert_deep_eq, assert_eq, test } from '../../../scripts/test/test.ts'
33+import { assert, assert_deep_eq, assert_eq, test } from '../../../scripts/test/test.ts'
44import { setup } from './setup.ts'
5566-test('renderables work correctly', () => {
66+test('renderables work correctly', async () => {
77 const { root, el } = setup()
8899 root.render(
···2929 assert_eq(el.innerHTML, 'Count: 0')
30303131 // but invalidating it shouldn't:
3232- invalidate(app)
3232+ await invalidate(app)
3333 assert_eq(el.innerHTML, 'Count: 1')
3434- invalidate(app)
3434+ await invalidate(app)
3535 assert_eq(el.innerHTML, 'Count: 2')
3636 assert_eq(app.i, 3)
3737})
···5959 assert_eq(el.innerHTML, 'this was thrown')
6060})
61616262-test('onMount calls in the right order', () => {
6262+test('onMount calls in the right order', async () => {
6363 const { root, el } = setup()
64646565 const sequence: string[] = []
···100100 sequence.length = 0
101101102102 outer.show = false
103103- invalidate(outer)
103103+ await invalidate(outer)
104104 assert_eq(el.innerHTML, '')
105105 assert_deep_eq(sequence, ['outer render', 'inner cleanup'])
106106 sequence.length = 0
107107108108 outer.show = true
109109- invalidate(outer)
109109+ await invalidate(outer)
110110 assert_eq(el.innerHTML, 'inner')
111111 // inner is mounted a second time because of the above cleanup
112112 assert_deep_eq(sequence, ['outer render', 'inner mount', 'inner render'])
···214214 assert_eq(calls, 1)
215215})
216216217217-test('onUnmount deep works correctly', () => {
217217+test('onUnmount deep works correctly', async () => {
218218 const { root, el } = setup()
219219220220 const sequence: string[] = []
···250250 sequence.length = 0
251251252252 outer.show = false
253253- invalidate(outer)
253253+ await invalidate(outer)
254254 assert_eq(el.innerHTML, '')
255255 assert_deep_eq(sequence, ['outer render', 'inner abort'])
256256 sequence.length = 0
257257258258 outer.show = true
259259- invalidate(outer)
259259+ await invalidate(outer)
260260 assert_eq(el.innerHTML, 'inner')
261261 assert_deep_eq(sequence, ['outer render', 'inner render'])
262262 sequence.length = 0
263263264264 outer.show = false
265265- invalidate(outer)
265265+ await invalidate(outer)
266266 assert_eq(el.innerHTML, '')
267267 assert_deep_eq(sequence, ['outer render', 'inner abort'])
268268 sequence.length = 0
269269})
270270271271-test('onUnmount shallow works correctly', () => {
271271+test('onUnmount shallow works correctly', async () => {
272272 const { root, el } = setup()
273273274274 const sequence: string[] = []
···307307 sequence.length = 0
308308309309 outer.show = false
310310- invalidate(outer)
310310+ await invalidate(outer)
311311 assert_eq(el.innerHTML, '')
312312 assert_deep_eq(sequence, ['outer render', 'inner abort'])
313313 sequence.length = 0
314314315315 outer.show = true
316316- invalidate(outer)
316316+ await invalidate(outer)
317317 assert_eq(el.innerHTML, 'inner')
318318 assert_deep_eq(sequence, ['outer render', 'inner render'])
319319 sequence.length = 0
320320321321 outer.show = false
322322- invalidate(outer)
322322+ await invalidate(outer)
323323 assert_eq(el.innerHTML, '')
324324 assert_deep_eq(sequence, ['outer render', 'inner abort'])
325325 sequence.length = 0
···374374 }
375375})
376376377377-test('renderables can be rendered in multiple places at once', () => {
377377+test('renderables can be rendered in multiple places at once', async () => {
378378 const { root: root1, el: el1 } = setup()
379379 const { root: root2, el: el2 } = setup()
380380···404404405405 // Update the renderable - both should update
406406 app.value = 'updated'
407407- invalidate(app)
407407+ await invalidate(app)
408408 assert_eq(el1.innerHTML, 'updated')
409409 assert_eq(el2.innerHTML, 'updated')
410410···418418 assert_eq(mounted, 0) // Now unmounted
419419})
420420421421-test('renderables can be rendered in multiple places at once with a single root', () => {
421421+test('renderables can be rendered in multiple places at once with a single root', async () => {
422422 const { root, el } = setup()
423423424424 let mounted = 0
···441441 assert_eq(el.innerHTML, '<span>shared</span><span>shared</span>')
442442443443 thing.value = 'updated'
444444- invalidate(thing)
444444+ await invalidate(thing)
445445 assert_eq(mounted, 1)
446446 assert_eq(el.innerHTML, '<span>updated</span><span>updated</span>')
447447···449449 assert_eq(mounted, 0)
450450})
451451452452-test('invalidating an unmounted renderable does nothing', () => {
452452+test('invalidating an unmounted renderable does nothing', async () => {
453453 const { root, el } = setup()
454454455455 const app1 = {
···470470 root.render(app2)
471471 assert_eq(el.textContent, 'app2')
472472473473- invalidate(app1)
473473+ await invalidate(app1)
474474 assert_eq(el.textContent, 'app2')
475475})
476476···502502 assert_eq(unmounted, 1)
503503})
504504505505-test('invalidating a parent does not re-render a child', () => {
505505+test('invalidating a parent does not re-render a child', async () => {
506506 const { root, el } = setup()
507507508508 let renders = 0
···523523 assert_eq(el.innerHTML, 'child')
524524 assert_eq(renders, 1)
525525526526- invalidate(parent)
526526+ await invalidate(parent)
527527 assert_eq(el.innerHTML, 'child')
528528 assert_eq(renders, 1)
529529})
530530+531531+test('invalidating parent during child render triggers update', async () => {
532532+ const { root, el } = setup()
533533+534534+ let promise: Promise<void>
535535+ const item = {
536536+ render() {
537537+ app.loading = true
538538+ promise = invalidate(app)
539539+ return 'created'
540540+ },
541541+ }
542542+543543+ const app = {
544544+ loading: false,
545545+546546+ render() {
547547+ if (this.loading) return 'loading'
548548+ return item
549549+ },
550550+ }
551551+552552+ root.render(app)
553553+ assert(promise!)
554554+ await promise
555555+ assert_eq(el.innerHTML, 'loading')
556556+})
557557+558558+test('invalidating grandparent during child render triggers update', async () => {
559559+ const { root, el } = setup()
560560+561561+ let promise: Promise<void>
562562+ const item = {
563563+ render() {
564564+ app.loading = true
565565+ promise = invalidate(app)
566566+ return 'created'
567567+ },
568568+ }
569569+570570+ const middle = {
571571+ item: null as Displayable,
572572+573573+ render() {
574574+ return this.item
575575+ },
576576+ }
577577+578578+ const app = {
579579+ loading: false,
580580+581581+ render() {
582582+ if (this.loading) return 'loading'
583583+ return middle
584584+ },
585585+ }
586586+587587+ root.render(app)
588588+ assert_eq(el.innerHTML, '')
589589+590590+ middle.item = item
591591+ await invalidate(middle)
592592+ assert(promise!)
593593+ await promise
594594+ assert_eq(el.innerHTML, 'loading')
595595+})
596596+597597+test('invalidate drains reinvalidation of the same renderable before resolve', async () => {
598598+ const { root, el } = setup()
599599+600600+ let state = 0
601601+ let nested: Promise<void> | undefined
602602+ const app = {
603603+ render() {
604604+ if (state === 1) {
605605+ state = 2
606606+ nested = invalidate(app)
607607+ }
608608+ return '' + state
609609+ },
610610+ }
611611+612612+ root.render(app)
613613+ assert_eq(el.innerHTML, '0')
614614+615615+ state = 1
616616+ const promise = invalidate(app)
617617+ await promise
618618+ assert_eq(el.innerHTML, '2')
619619+ assert(nested!)
620620+ await nested
621621+})
+5-5
src/server.ts
···117117}
118118119119function render_directive(value: unknown) {
120120- // Treat null/undefined as no-op, matching client behavior.
121121- if (value == null) return ''
120120+ // Treat null/undefined as no-op, matching client behavior.
121121+ if (value == null) return ''
122122123123- // In dev, ensure anything else is a function; on the server we don't execute it.
124124- assert(typeof value === 'function')
125125- return ''
123123+ // In dev, ensure anything else is a function; on the server we don't execute it.
124124+ assert(typeof value === 'function')
125125+ return ''
126126}
127127128128function render_attribute(name: string, value: unknown) {