/// SPDX-License-Identifier: GPL-3.0-or-later /// SPDX-FileCopyrightText: Copyright © 2024-2025 Tony Garnock-Jones import { describe, it, expect } from 'vitest'; import { HtmlTemplater, template } from '../src/html'; import { compareHTML } from './test-utils'; describe('basic templating', () => { it('should produce a node', () => { const x = document.createElement('x'); x.appendChild(document.createTextNode('y')); expect(template()`y`).toEqual([x]); }); it('should substitute a string', () => { const y = 'abc'; compareHTML(template()`${y}`, 'abc'); }); it('should substitute a string without surrounding context', () => { const y = 'abc'; compareHTML(template()`${y}`, 'abc'); }); it('should substitute multiple strings', () => { const y = 'abc'; compareHTML(template()`${y} ${y} ${y}`, 'abc abc abc'); }); it('should substitute a node', () => { const y = template()`q`; compareHTML(template()`${y}`, 'q'); }); it('should substitute an array of strings', () => { const y = ['abc', 'def']; compareHTML(template()`${y}`, 'abcdef'); }); it('should substitute an array of strings and nodes', () => { const y = ['abc', template()`q`, 'def']; compareHTML(template()`${y}`, 'abcqdef'); }); it('should substitute an array of nodes where only certain kinds of content are allowed', () => { // This test is interesting. HTML's TR element only allows TD, TH and a small number // of other kinds of element, including TEMPLATE, but in particular does *not* allow // plain text nodes. Previously, html.ts was using a text node as a placeholder, but // in order to pass this test without the ability to parse partial HTML and do // different things based on the DTD (!), it now inserts a text node *only* if the // initial parameter value is ENTIRELY non-Node-ish; otherwise it synthesises a // placeholder *element* with tag TEMPLATE and ID matching the special placeholder // regular expression. // const tds = ['a', 'b', 'c'].map(s => template()`${s}`); compareHTML(template()`${tds}`, 'abc'); }); it('should substitute into attributes', () => { const v = () => 'aaa'; const ps = [() => '123', () => '234']; compareHTML(template()`${ps.map(p => p())}`, '123234'); }); it('should substitute into attributes, with children', () => { const v = () => 'aaa'; const ps = [() => '123', () => '234']; const f = 'z'; compareHTML(template()`${f}${ps.map(p => p())}`, 'z123234'); }); it('should substitute into attributes, with children and whitespace', () => { const v = () => 'aaa'; const ps = [() => '123', () => '234']; const f = 'z'; compareHTML(template()`${f} ${ps.map(p => p())} `, `z 123234 `); }); it('example from paradise.js', () => { const pieces = ['C', 'D']; compareHTML(template()`

${'B'} ${pieces.map(p => p)}

`, `

B CD

`); }); it('should be callable multiple times', () => { const v = () => 'aaa'; const ps = [() => '123', () => '234', () => '345']; const expected = '123234345'; const t = template(); compareHTML(t`${ps.map(p => p())}`, expected); compareHTML(t`${ps.map(p => p())}`, expected); compareHTML(t`${ps.map(p => p())}`, expected); }); it('should be callable multiple times with the same syntactic location', () => { // Because the fixed portion of a template string, the TemplateStringsArray, is hashconsed const v = () => 'aaa'; const ps = [() => '123', () => '234', () => '345']; const expected = '123234345'; const t = template(); const f = () => compareHTML(t`${ps.map(p => p())}`, expected); f(); f(); f(); }); it('should be callable multiple times with extra items', () => { const v = () => 'aaa'; const t = template(); const ps = [() => '1']; compareHTML(t`${ps.map(p => p())}`, '1'); ps.push(() => '2'); compareHTML(t`${ps.map(p => p())}`, '12'); ps.push(() => '3'); compareHTML(t`${ps.map(p => p())}`, '123'); }); it('should be reusable for supplying arguments to itself', () => { const inner = (t: HtmlTemplater) => t`middle`; const t = template(); compareHTML(t`

n${inner(t)}n

`, '

nmiddlen

'); }); it('should accept strings', () => { const inner = (_t: HtmlTemplater) => `middle`; const t = template(); compareHTML(t`

n${inner(t)}n

`, '

n<i>middle</i>n

'); }); }); describe('template output node stability', () => { it('should work for simple use of a template', () => { const t = template(); let i = 0; const p = () => t`${i++}`; const n1 = p(); compareHTML(n1, '0'); const n2 = p(); compareHTML(n2, '1'); expect(n1[0]).toBe(n2[0]); }); it('should work for reuse of a template', () => { const t = template(); let i = 0; const q = () => t`${i++}`; const p = () => t`${q()}`; const n1 = p(); compareHTML(n1, '0'); const n2 = p(); compareHTML(n2, '1'); expect(n1[0]).toBe(n2[0]); }); }); describe('handling of externally-added nodes', () => { it('should be updatable with node added in front', () => { const t = template(); let v: string | number = 2; const f = () => t`

one${v}three

`; const n = document.createElement('span'); n.innerHTML = 'zero'; let a = f(); a[0].insertBefore(n, a[0].childNodes[0]); compareHTML(a, `

zeroone2three

`); v = 'two'; a = f(); a[0].insertBefore(n, a[0].childNodes[0]); compareHTML(a, `

zeroonetwothree

`); }); }); describe('textareas', () => { it('should update', () => { const t = template(); let value = 'one'; const f = () => t``; compareHTML(f(), ``); value = 'two'; compareHTML(f(), ``); }); });