a post-component library for building user-interfaces on the web.
at main 217 lines 5.6 kB view raw
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}