diff --git a/packages/html2/jest.config.ts b/packages/html2/jest.config.ts
new file mode 100644
index 0000000..580f2eb
--- /dev/null
+++ b/packages/html2/jest.config.ts
@@ -0,0 +1,7 @@
+/// SPDX-License-Identifier: GPL-3.0-or-later
+/// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones
+
+export default {
+ preset: 'ts-jest',
+ testEnvironment: './patched-jsdom.mjs',
+};
diff --git a/packages/html2/package.json b/packages/html2/package.json
index 16d0e62..b407c6d 100644
--- a/packages/html2/package.json
+++ b/packages/html2/package.json
@@ -22,7 +22,9 @@
"rollup": "rollup -c",
"rollup:watch": "rollup -c -w",
"clean": "rm -rf lib/ dist/ index.js index.js.map",
- "veryclean": "yarn run clean && rm -rf node_modules"
+ "veryclean": "yarn run clean && rm -rf node_modules",
+ "test": "../../node_modules/.bin/jest",
+ "test:watch": "yarn test --watch"
},
"dependencies": {
"@syndicate-lang/core": "^0.34.8"
diff --git a/packages/html2/patched-jsdom.mjs b/packages/html2/patched-jsdom.mjs
new file mode 100644
index 0000000..52d3dde
--- /dev/null
+++ b/packages/html2/patched-jsdom.mjs
@@ -0,0 +1,14 @@
+/// SPDX-License-Identifier: GPL-3.0-or-later
+/// SPDX-FileCopyrightText: Copyright © 2024 Tony Garnock-Jones
+
+// https://github.com/jsdom/jsdom/issues/2524
+
+import { TextEncoder, TextDecoder } from 'util';
+import $JSDOMEnvironment from 'jest-environment-jsdom';
+export default class JSDOMEnvironment extends $JSDOMEnvironment {
+ constructor(... args) {
+ const { global } = super(... args);
+ if (!global.TextEncoder) global.TextEncoder = TextEncoder;
+ if (!global.TextDecoder) global.TextDecoder = TextDecoder;
+ }
+}
diff --git a/packages/html2/test/html.test.ts b/packages/html2/test/html.test.ts
new file mode 100644
index 0000000..a0ae91f
--- /dev/null
+++ b/packages/html2/test/html.test.ts
@@ -0,0 +1,47 @@
+/// SPDX-License-Identifier: GPL-3.0-or-later
+/// SPDX-FileCopyrightText: Copyright © 2024 Tony Garnock-Jones
+
+import { template } from '../src/html';
+import './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 x = document.createElement('x');
+ const y = 'abc';
+ x.appendChild(document.createTextNode('abc'));
+ expect('' + template()`${y}`).toEqual('' + [x]);
+ });
+
+ it('should substitute a node', () => {
+ const x = document.createElement('x');
+ const z = document.createElement('z');
+ z.appendChild(document.createTextNode('q'));
+ x.appendChild(z);
+ const y = template()`q`;
+ expect('' + template()`${y}`).toEqual('' + [x]);
+ });
+
+ it('should substitute an array of strings', () => {
+ const x = document.createElement('x');
+ const y = ['abc', 'def'];
+ x.appendChild(document.createTextNode('abcdef'));
+ expect('' + template()`${y}`).toEqual('' + [x]);
+ });
+
+ it('should substitute an array of strings and nodes', () => {
+ const x = document.createElement('x');
+ const y = ['abc', template()`q`, 'def'];
+ const z = document.createElement('z');
+ z.appendChild(document.createTextNode('q'));
+ x.appendChild(document.createTextNode('abc'));
+ x.appendChild(z);
+ x.appendChild(document.createTextNode('def'));
+ expect('' + template()`${y}`).toEqual('' + [x]);
+ });
+});
diff --git a/packages/html2/test/test-utils.ts b/packages/html2/test/test-utils.ts
new file mode 100644
index 0000000..ef7c81e
--- /dev/null
+++ b/packages/html2/test/test-utils.ts
@@ -0,0 +1,38 @@
+/// SPDX-License-Identifier: GPL-3.0-or-later
+/// SPDX-FileCopyrightText: Copyright © 2016-2024 Tony Garnock-Jones
+
+import { is, preserves } from '@preserves/core';
+
+declare global {
+ namespace jest {
+ interface Matchers {
+ is(expected: any): R;
+ toThrowFilter(f: (e: Error) => boolean): R;
+ }
+ }
+}
+
+expect.extend({
+ is(actual, expected) {
+ return is(actual, expected)
+ ? { message: () => preserves`expected ${actual} not to be Preserves.is to ${expected}`,
+ pass: true }
+ : { message: () => preserves`expected ${actual} to be Preserves.is to ${expected}`,
+ pass: false };
+ },
+
+ toThrowFilter(thunk, f) {
+ try {
+ thunk();
+ return { message: () => preserves`expected an exception`, pass: false };
+ } catch (e) {
+ if (f(e)) {
+ return { message: () => preserves`expected an exception not matching the filter`,
+ pass: true };
+ } else {
+ return { message: () => preserves`expected an exception matching the filter: ${(e as any)?.constructor?.name}`,
+ pass: false };
+ }
+ }
+ }
+});