a post-component library for building user-interfaces on the web.
at main 356 lines 9.8 kB view raw
1import { html, keyed, type Displayable } from 'dhtml' 2import { invalidate } from 'dhtml/client' 3import { assert, assert_eq, test } from '../../../scripts/test/test.ts' 4import { setup } from './setup.ts' 5 6function shuffle<T>(array: T[]) { 7 for (let i = 0; i < array.length; i++) { 8 const j = Math.floor(Math.random() * i) 9 ;[array[i], array[j]] = [array[j], array[i]] 10 } 11} 12 13test('basic list operations work correctly', () => { 14 const { root, el } = setup() 15 16 let items: Displayable[] | null = null 17 const listOfItems = () => html` 18 <ul> 19 <li>Before</li> 20 ${items} 21 <li>After</li> 22 </ul> 23 ` 24 25 root.render(listOfItems()) 26 assert_eq(el.innerHTML.replace(/\s+/g, ' '), ' <ul> <li>Before</li> <li>After</li> </ul> ') 27 28 items = [html`<li>Item 1</li>`, html`<li>Item 2</li>`, html`<li>Item 3</li>`] 29 30 root.render(listOfItems()) 31 assert_eq( 32 el.innerHTML.replace(/\s+/g, ' '), 33 ' <ul> <li>Before</li> <li>Item 1</li><li>Item 2</li><li>Item 3</li> <li>After</li> </ul> ', 34 ) 35 const [item1, item2, item3] = el.querySelectorAll('li') 36 37 items.push(html`<li>Item 4</li>`) 38 root.render(listOfItems()) 39 assert_eq( 40 el.innerHTML.replace(/\s+/g, ' '), 41 ' <ul> <li>Before</li> <li>Item 1</li><li>Item 2</li><li>Item 3</li><li>Item 4</li> <li>After</li> </ul> ', 42 ) 43 const [item1b, item2b, item3b] = el.querySelectorAll('li') 44 assert_eq(item1, item1b) 45 assert_eq(item2, item2b) 46 assert_eq(item3, item3b) 47 48 items.pop() 49 items.pop() 50 root.render(listOfItems()) 51 assert_eq( 52 el.innerHTML.replace(/\s+/g, ' '), 53 ' <ul> <li>Before</li> <li>Item 1</li><li>Item 2</li> <li>After</li> </ul> ', 54 ) 55 const [item1c, item2c] = el.querySelectorAll('li') 56 assert_eq(item1, item1c) 57 assert_eq(item2, item2c) 58}) 59 60test('pop operation works correctly on lists', () => { 61 const { root, el } = setup() 62 63 const items = [html`<p>Item 1</p>`, html`<p>Item 2</p>`, html`<p>Item 3</p>`] 64 const wrapped = html`[${items}]` 65 66 root.render(wrapped) 67 assert_eq(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 68 const [item1, item2] = el.children 69 70 items.pop() 71 root.render(wrapped) 72 assert_eq(el.innerHTML, '[<p>Item 1</p><p>Item 2</p>]') 73 assert_eq(el.children[0], item1) 74 assert_eq(el.children[1], item2) 75 76 items.pop() 77 root.render(wrapped) 78 assert_eq(el.innerHTML, '[<p>Item 1</p>]') 79 assert_eq(el.children[0], item1) 80 81 items.pop() 82 root.render(wrapped) 83 assert_eq(el.innerHTML, '[]') 84}) 85 86test('swap operation works correctly on lists', () => { 87 const { root, el } = setup() 88 89 const items = [html`<p>Item 1</p>`, html`<p>Item 2</p>`, html`<p>Item 3</p>`] 90 const wrapped = html`[${items}]` 91 92 root.render(wrapped) 93 assert_eq(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 94 const [item1, item2, item3] = el.children 95 96 // swap the first two items 97 ;[items[0], items[1]] = [items[1], items[0]] 98 root.render(wrapped) 99 assert_eq(el.innerHTML, '[<p>Item 2</p><p>Item 1</p><p>Item 3</p>]') 100 assert_eq(el.children[0], item2) 101 assert_eq(el.children[1], item1) 102 assert_eq(el.children[2], item3) 103 104 // swap the last two items 105 ;[items[1], items[2]] = [items[2], items[1]] 106 root.render(wrapped) 107 assert_eq(el.innerHTML, '[<p>Item 2</p><p>Item 3</p><p>Item 1</p>]') 108 assert_eq(el.children[0], item2) 109 assert_eq(el.children[1], item3) 110 assert_eq(el.children[2], item1) 111 112 // swap the first and last items 113 ;[items[0], items[2]] = [items[2], items[0]] 114 root.render(wrapped) 115 assert_eq(el.innerHTML, '[<p>Item 1</p><p>Item 3</p><p>Item 2</p>]') 116 assert_eq(el.children[0], item1) 117 assert_eq(el.children[1], item3) 118 assert_eq(el.children[2], item2) 119 120 // put things back 121 ;[items[1], items[2]] = [items[2], items[1]] 122 root.render(wrapped) 123 assert_eq(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 124 assert_eq(el.children[0], item1) 125 assert_eq(el.children[1], item2) 126 assert_eq(el.children[2], item3) 127}) 128 129test('shift operation works correctly on lists', () => { 130 const { root, el } = setup() 131 132 const items = [html`<p>Item 1</p>`, html`<p>Item 2</p>`, html`<p>Item 3</p>`] 133 const wrapped = html`[${items}]` 134 135 root.render(wrapped) 136 assert_eq(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 137 const [, item2, item3] = el.children 138 139 items.shift() 140 root.render(wrapped) 141 assert_eq(el.innerHTML, '[<p>Item 2</p><p>Item 3</p>]') 142 assert_eq(el.children[0], item2) 143 assert_eq(el.children[1], item3) 144 145 items.shift() 146 root.render(wrapped) 147 assert_eq(el.innerHTML, '[<p>Item 3</p>]') 148 assert_eq(el.children[0], item3) 149 150 items.shift() 151 root.render(wrapped) 152 assert_eq(el.innerHTML, '[]') 153}) 154 155test('full then empty then full list renders correctly', () => { 156 const { root, el } = setup() 157 158 root.render([1]) 159 assert_eq(el.innerHTML, '1') 160 161 root.render([]) 162 assert_eq(el.innerHTML, '') 163 164 root.render([2]) 165 assert_eq(el.innerHTML, '2') 166}) 167 168test('list can disappear when condition changes', async () => { 169 const { root, el } = setup() 170 171 const app = { 172 show: true, 173 render() { 174 if (!this.show) return null 175 return [1, 2, 3].map(i => html`<div>${i}</div>`) 176 }, 177 } 178 179 root.render(app) 180 assert_eq(el.innerHTML, '<div>1</div><div>2</div><div>3</div>') 181 182 app.show = false 183 await invalidate(app) 184 assert_eq(el.innerHTML, '') 185}) 186 187test('unkeyed lists recreate elements when reordered', () => { 188 const { root, el } = setup() 189 190 const a = () => html`<h1>Item 1</h1>` 191 const b = () => html`<h2>Item 2</h2>` 192 193 root.render([a(), b()]) 194 assert_eq(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 195 196 const [h1, h2] = el.children 197 assert_eq(h1.tagName, 'H1') 198 assert_eq(h2.tagName, 'H2') 199 200 root.render([b(), a()]) 201 assert_eq(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 202 203 // visually they should be swapped 204 assert_eq(el.children[0].innerHTML, h2.innerHTML) 205 assert_eq(el.children[1].innerHTML, h1.innerHTML) 206 207 // but there's no stable identity, so they're recreated 208 assert(el.children[0] !== h2) 209 assert(el.children[1] !== h1) 210}) 211 212test('explicit keyed lists preserve identity when reordered', () => { 213 const { root, el } = setup() 214 215 const a = () => keyed(html`<h1>Item 1</h1>`, 1) 216 const b = () => keyed(html`<h2>Item 2</h2>`, 2) 217 218 root.render([a(), b()]) 219 assert_eq(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 220 221 const [h1, h2] = el.children 222 assert_eq(h1.tagName, 'H1') 223 assert_eq(h2.tagName, 'H2') 224 225 root.render([b(), a()]) 226 assert_eq(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 227 228 assert_eq(el.children[0], h2) 229 assert_eq(el.children[1], h1) 230}) 231 232test('implicit keyed lists preserve identity when reordered', () => { 233 const { root, el } = setup() 234 235 const items = [html`<h1>Item 1</h1>`, html`<h2>Item 2</h2>`] 236 237 root.render(items) 238 assert_eq(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 239 240 const [h1, h2] = el.children 241 assert_eq(h1.tagName, 'H1') 242 assert_eq(h2.tagName, 'H2') 243 ;[items[0], items[1]] = [items[1], items[0]] 244 245 root.render(items) 246 assert_eq(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 247 assert_eq(el.children[0].tagName, 'H2') 248 assert_eq(el.children[1].tagName, 'H1') 249 250 assert_eq(el.children[0], h2) 251 assert_eq(el.children[1], h1) 252}) 253 254test('implicit keyed lists with multiple elements preserve identity when resized', () => { 255 const { root, el } = setup() 256 257 const items = [ 258 html`<h1>Item 1</h1>`, 259 html` 260 <h2>Item 2</h2> 261 <p>Body content</p> 262 `, 263 ] 264 265 root.render(items) 266 assert_eq(el.innerHTML.replace(/\s+/g, ' '), '<h1>Item 1</h1> <h2>Item 2</h2> <p>Body content</p> ') 267 268 const [h1, h2, p] = el.children 269 assert_eq(h1.tagName, 'H1') 270 assert_eq(h2.tagName, 'H2') 271 assert_eq(p.tagName, 'P') 272 273 // Swap 274 ;[items[0], items[1]] = [items[1], items[0]] 275 root.render(items) 276 assert_eq(el.innerHTML.replace(/\s+/g, ' '), ' <h2>Item 2</h2> <p>Body content</p> <h1>Item 1</h1>') 277 assert_eq(el.children[0].tagName, 'H2') 278 assert_eq(el.children[1].tagName, 'P') 279 assert_eq(el.children[2].tagName, 'H1') 280 281 assert_eq(el.children[0], h2) 282 assert_eq(el.children[1], p) 283 assert_eq(el.children[2], h1) 284 285 // Swap back 286 ;[items[0], items[1]] = [items[1], items[0]] 287 root.render(items) 288 assert_eq(el.innerHTML.replace(/\s+/g, ' '), '<h1>Item 1</h1> <h2>Item 2</h2> <p>Body content</p> ') 289 assert_eq(el.children[0].tagName, 'H1') 290 assert_eq(el.children[1].tagName, 'H2') 291 assert_eq(el.children[2].tagName, 'P') 292 assert_eq(el.children[0], h1) 293 assert_eq(el.children[1], h2) 294 assert_eq(el.children[2], p) 295}) 296 297test('implicit keyed renderable lists preserve identity when reordered', () => { 298 const { root, el } = setup() 299 300 const items = [{ render: () => html`<h1>Item 1</h1>` }, { render: () => html`<h2>Item 2</h2>` }] 301 302 root.render(items) 303 assert_eq(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 304 305 const [h1, h2] = el.children 306 assert_eq(h1.tagName, 'H1') 307 assert_eq(h2.tagName, 'H2') 308 ;[items[0], items[1]] = [items[1], items[0]] 309 310 root.render(items) 311 assert_eq(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 312 assert_eq(el.children[0].tagName, 'H2') 313 assert_eq(el.children[1].tagName, 'H1') 314 315 assert_eq(el.children[0], h2) 316 assert_eq(el.children[1], h1) 317}) 318 319test('many items can be reordered', () => { 320 const { root, el } = setup() 321 322 const items = Array.from({ length: 10 }, (_, i) => [html`<p>Item ${i}</p>`, `<p>Item ${i}</p>`]) 323 324 root.render(items.map(([item]) => item)) 325 assert_eq(el.innerHTML, items.map(([, html]) => html).join('')) 326 327 shuffle(items) 328 329 root.render(items.map(([item]) => item)) 330 assert_eq(el.innerHTML, items.map(([, html]) => html).join('')) 331}) 332 333if (__DEV__) { 334 test('keying something twice throws an error', () => { 335 keyed(html``, 1) // make sure this doesn't throw 336 337 try { 338 keyed(keyed(html``, 1), 1) 339 assert(false, 'Expected an error') 340 } catch {} 341 }) 342} 343 344test('can render the same item multiple times', () => { 345 const { root, el } = setup() 346 347 const item = html`<p>Item</p>` 348 root.render([item, item]) 349 assert_eq(el.innerHTML, '<p>Item</p><p>Item</p>') 350 351 root.render([item, item, item]) 352 assert_eq(el.innerHTML, '<p>Item</p><p>Item</p><p>Item</p>') 353 354 root.render([item, item]) 355 assert_eq(el.innerHTML, '<p>Item</p><p>Item</p>') 356})