a post-component library for building user-interfaces on the web.
1import { html, type Displayable } from 'dhtml'
2import { assert, assert_eq, test } from '../../../scripts/test/test.ts'
3import { setup } from './setup.ts'
4
5test('basic html renders correctly', () => {
6 const { root, el } = setup()
7
8 root.render(html`<h1>Hello, world!</h1>`)
9 assert_eq(el.innerHTML, '<h1>Hello, world!</h1>')
10})
11
12test('inner content renders correctly', () => {
13 const { root, el } = setup()
14
15 root.render(html`<h1>${html`Inner content!`}</h1>`)
16 assert_eq(el.innerHTML, '<h1>Inner content!</h1>')
17})
18
19test('template with number renders correctly', () => {
20 const { root, el } = setup()
21
22 const template = (n: number) => html`<h1>Hello, ${n}!</h1>`
23
24 root.render(template(1))
25 assert_eq(el.innerHTML, '<h1>Hello, 1!</h1>')
26
27 root.render(template(2))
28 assert_eq(el.innerHTML, '<h1>Hello, 2!</h1>')
29})
30
31test('external sibling nodes are not clobbered', () => {
32 const { root, el } = setup('<div>before</div>')
33
34 root.render(html`<h1>Hello, world!</h1>`)
35 assert_eq(el.innerHTML, '<div>before</div><h1>Hello, world!</h1>')
36
37 el.appendChild(document.createElement('div')).textContent = 'after'
38 assert_eq(el.innerHTML, '<div>before</div><h1>Hello, world!</h1><div>after</div>')
39
40 root.render(html`<h2>Goodbye, world!</h2>`)
41 assert_eq(el.innerHTML, '<div>before</div><h2>Goodbye, world!</h2><div>after</div>')
42
43 root.render(html``)
44 assert_eq(el.innerHTML, '<div>before</div><div>after</div>')
45
46 root.render(html`<h1>Hello, world!</h1>`)
47 assert_eq(el.innerHTML, '<div>before</div><h1>Hello, world!</h1><div>after</div>')
48})
49
50test('identity is updated correctly', () => {
51 const { root, el } = setup()
52
53 const template = (n: Displayable) => html`<h1>Hello, ${n}!</h1>`
54 const template2 = (n: Displayable) => html`<h1>Hello, ${n}!</h1>`
55
56 root.render(template(1))
57 assert_eq(el.innerHTML, '<h1>Hello, 1!</h1>')
58 let h1 = el.children[0]
59 const text = [...h1.childNodes].find((node): node is Text => node instanceof Text && node.data === '1')
60 assert(text)
61
62 root.render(template(2))
63 assert_eq(el.innerHTML, '<h1>Hello, 2!</h1>')
64 assert_eq(el.children[0], h1)
65 assert_eq(text.data, '2')
66 assert([...h1.childNodes].includes(text))
67
68 root.render(template2(3))
69 assert_eq(el.innerHTML, '<h1>Hello, 3!</h1>')
70 assert(el.children[0] !== h1)
71 h1 = el.children[0]
72
73 root.render(template2(template(template('inner'))))
74 assert_eq(el.innerHTML, '<h1>Hello, <h1>Hello, <h1>Hello, inner!</h1>!</h1>!</h1>')
75 assert_eq(el.children[0], h1)
76})
77
78test('basic children render correctly', () => {
79 const { root, el } = setup()
80
81 root.render(html`<span>${'This is a'}</span> ${html`test`} ${html`test`} ${html`test`}`)
82
83 assert_eq(el.innerHTML, '<span>This is a</span> test test test')
84})
85
86test('nodes can be embedded', () => {
87 const { root, el } = setup()
88
89 let node: ParentNode = document.createElement('span')
90
91 root.render(html`<div>${node}</div>`)
92 assert_eq(el.innerHTML, '<div><span></span></div>')
93 assert_eq(el.children[0].children[0], node)
94
95 node = document.createDocumentFragment()
96 node.append(document.createElement('h1'), document.createElement('h2'), document.createElement('h3'))
97
98 root.render(html`<div>${node}</div>`)
99 assert_eq(el.innerHTML, '<div><h1></h1><h2></h2><h3></h3></div>')
100 assert_eq(node.children.length, 0)
101})
102
103if (false)
104 test('extra empty text nodes are not added', () => {
105 const { root, el } = setup()
106
107 root.render(html`${'abc'}`)
108 assert_eq(el.childNodes.length, 1)
109 assert(el.firstChild instanceof Text)
110 assert_eq((el.firstChild as Text).data, 'abc')
111 })
112
113test('ChildPart index shifts correctly', () => {
114 const { root, el } = setup()
115
116 root.render(html`${html`A<!--x-->`}B${'C'}`)
117
118 assert_eq(el.innerHTML, 'A<!--x-->BC')
119})
120
121test('errors are thrown cleanly', () => {
122 const { root, el } = setup()
123
124 const oops = new Error('oops')
125 let thrown
126 try {
127 root.render(
128 html`${{
129 render() {
130 throw oops
131 },
132 }}`,
133 )
134 } catch (error) {
135 thrown = error
136 }
137 assert_eq(thrown, oops)
138
139 // on an error, don't leave any visible artifacts
140 assert_eq(el.innerHTML, '<!--dyn-$0$-->')
141})
142
143if (__DEV__) {
144 test('invalid part placement raises error', () => {
145 const { root, el } = setup()
146
147 try {
148 root.render(html`<${'div'}>${'text'}</${'div'}>`)
149 assert(false)
150 } catch (error) {
151 assert(error instanceof Error)
152 assert_eq(
153 error.message,
154 'expected the same number of dynamics as parts. do you have a ${...} in an unsupported place?',
155 )
156 }
157
158 assert_eq(el.innerHTML, '')
159 })
160
161 test('manually specifying internal template syntax throws', () => {
162 const { root, el } = setup()
163
164 try {
165 root.render(
166 html`${1}
167 <!--dyn-$0$-->`,
168 )
169 assert(false)
170 } catch (error) {
171 assert(error instanceof Error)
172 assert_eq(error.message, 'got more parts than expected')
173 }
174
175 assert_eq(el.innerHTML, '')
176 })
177}
178
179test('syntax close but not exact does not throw', () => {
180 const { root, el } = setup()
181
182 root.render(html`dyn-$${0}1$`)
183
184 assert_eq(el.innerHTML, 'dyn-$01$')
185})
186
187{
188 const values = {
189 text: 'text',
190 number: 1234,
191 null: null,
192 iterable: ['iterable', 'of', 'things'],
193 html: html`html`,
194 html_element: html`<a href="#">element</a>`,
195 renderable: {
196 render() {
197 return 'hello'
198 },
199 },
200 }
201
202 for (const [a_name, a_value] of Object.entries(values)) {
203 for (const [b_name, b_value] of Object.entries(values)) {
204 test(`updating across value kinds: ${a_name} -> ${b_name}`, () => {
205 const { root, el } = setup()
206
207 root.render(a_value)
208 root.render(b_value)
209
210 const { root: root2, el: el2 } = setup()
211 root2.render(b_value)
212
213 assert_eq(el.innerHTML, el2.innerHTML)
214 })
215 }
216 }
217}