a post-component library for building user-interfaces on the web.

migrate to bun:test (#52)

This is the 4th test runner I've used now, but it's way faster than node:test and I can always revert (note the use of node:assert and not bun:tests expect)

authored by tombl.dev and committed by

GitHub faea0c68 1231ce56

+364 -340
+3 -5
.github/workflows/test.yml
··· 9 9 10 10 steps: 11 11 - uses: actions/checkout@v4 12 - - uses: actions/setup-node@v4 13 - with: 14 - node-version: 23.x 12 + - uses: oven-sh/setup-bun@v2 15 13 - run: npm install 16 - - run: npm run test:coverage 14 + - run: npm run test -- --coverage-reporter=lcov 17 15 - if: always() 18 16 uses: coverallsapp/github-action@v2 19 17 with: 20 - file: lcov.info 18 + file: coverage/lcov.info 21 19 format: lcov
+1 -1
.gitignore
··· 1 1 node_modules 2 2 dist 3 - lcov.info 3 + coverage
+22
package-lock.json
··· 10 10 "@happy-dom/global-registrator": "^17.4.4", 11 11 "@rollup/plugin-terser": "^0.4.4", 12 12 "@types/node": "^22.13.11", 13 + "bun-types": "^1.2.5", 13 14 "dhtml": ".", 14 15 "dts-buddy": "^0.5.5", 15 16 "htmlparser2": "^10.0.0", ··· 651 652 "undici-types": "~6.20.0" 652 653 } 653 654 }, 655 + "node_modules/@types/ws": { 656 + "version": "8.5.14", 657 + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.14.tgz", 658 + "integrity": "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw==", 659 + "dev": true, 660 + "license": "MIT", 661 + "dependencies": { 662 + "@types/node": "*" 663 + } 664 + }, 654 665 "node_modules/@valibot/to-json-schema": { 655 666 "version": "1.0.0-rc.0", 656 667 "resolved": "https://registry.npmjs.org/@valibot/to-json-schema/-/to-json-schema-1.0.0-rc.0.tgz", ··· 676 687 "version": "1.1.2", 677 688 "dev": true, 678 689 "license": "MIT" 690 + }, 691 + "node_modules/bun-types": { 692 + "version": "1.2.5", 693 + "resolved": "https://registry.npmjs.org/bun-types/-/bun-types-1.2.5.tgz", 694 + "integrity": "sha512-3oO6LVGGRRKI4kHINx5PIdIgnLRb7l/SprhzqXapmoYkFl5m4j6EvALvbDVuuBFaamB46Ap6HCUxIXNLCGy+tg==", 695 + "dev": true, 696 + "license": "MIT", 697 + "dependencies": { 698 + "@types/node": "*", 699 + "@types/ws": "~8.5.10" 700 + } 679 701 }, 680 702 "node_modules/commander": { 681 703 "version": "2.20.3",
+4 -4
package.json
··· 10 10 "build": "node build.js", 11 11 "format": "prettier --write . --cache", 12 12 "check": "tsc", 13 - "test": "node --no-warnings --test --experimental-test-coverage src/*/tests/*", 14 - "test:coverage": "node --no-warnings --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stderr --test-reporter=lcov --test-reporter-destination=lcov.info src/*/tests/*", 15 - "test:watch": "node --no-warnings --test --watch src/*/tests/*", 16 - "test:prod": "npm run build && NODE_ENV=production npm test" 13 + "test": "bun test --coverage --define __DEV__=true", 14 + "test:watch": "npm test -- --watch", 15 + "test:prod": "bun test --define __DEV__=false" 17 16 }, 18 17 "devDependencies": { 19 18 "@happy-dom/global-registrator": "^17.4.4", 20 19 "@rollup/plugin-terser": "^0.4.4", 21 20 "@types/node": "^22.13.11", 21 + "bun-types": "^1.2.5", 22 22 "dhtml": ".", 23 23 "dts-buddy": "^0.5.5", 24 24 "htmlparser2": "^10.0.0",
+30 -29
src/client/tests/attributes.test.ts
··· 1 + import { mock, test } from 'bun:test' 1 2 import { html } from 'dhtml' 2 - import test, { type TestContext } from 'node:test' 3 + import assert from 'node:assert/strict' 3 4 import { setup } from './setup.ts' 4 5 5 - test('regular attributes', (t: TestContext) => { 6 + test('regular attributes', () => { 6 7 const { root, el } = setup() 7 8 8 9 root.render(html`<h1 style=${'color: red'}>Hello, world!</h1>`) 9 - t.assert.strictEqual(el.querySelector('h1')!.getAttribute('style'), 'color: red;') 10 + assert.equal(el.querySelector('h1')!.getAttribute('style'), 'color: red;') 10 11 }) 11 12 12 - test('can toggle attributes', (t: TestContext) => { 13 + test('can toggle attributes', () => { 13 14 const { root, el } = setup() 14 15 15 16 let hidden: unknown = false 16 17 const template = () => html`<h1 hidden=${hidden}>Hello, world!</h1>` 17 18 18 19 root.render(template()) 19 - t.assert.ok(!el.querySelector('h1')!.hasAttribute('hidden')) 20 + assert(!el.querySelector('h1')!.hasAttribute('hidden')) 20 21 21 22 hidden = true 22 23 root.render(template()) 23 - t.assert.ok(el.querySelector('h1')!.hasAttribute('hidden')) 24 + assert(el.querySelector('h1')!.hasAttribute('hidden')) 24 25 25 26 hidden = null 26 27 root.render(template()) 27 - t.assert.ok(!el.querySelector('h1')!.hasAttribute('hidden')) 28 + assert(!el.querySelector('h1')!.hasAttribute('hidden')) 28 29 }) 29 30 30 - test('supports property attributes', (t: TestContext) => { 31 + test('supports property attributes', () => { 31 32 const { root, el } = setup() 32 33 33 34 root.render(html`<details open=${true}></details>`) 34 - t.assert.ok(el.querySelector('details')!.open) 35 + assert(el.querySelector('details')!.open) 35 36 36 37 root.render(html`<details open=${false}></details>`) 37 - t.assert.ok(!el.querySelector('details')!.open) 38 + assert(!el.querySelector('details')!.open) 38 39 }) 39 40 40 - test('infers the case of properties', (t: TestContext) => { 41 + test('infers the case of properties', () => { 41 42 const { root, el } = setup() 42 43 43 44 const innerHTML = '<h1>Hello, world!</h1>' 44 45 45 46 root.render(html`<div innerhtml=${innerHTML}></div>`) 46 - t.assert.strictEqual(el.querySelector('div')!.innerHTML, innerHTML) 47 + assert.equal(el.querySelector('div')!.innerHTML, innerHTML) 47 48 48 49 root.render(html`<span innerHTML=${innerHTML}></span>`) 49 - t.assert.strictEqual(el.querySelector('span')!.innerHTML, innerHTML) 50 + assert.equal(el.querySelector('span')!.innerHTML, innerHTML) 50 51 }) 51 52 52 - test('treats class/for specially', (t: TestContext) => { 53 + test('treats class/for specially', () => { 53 54 const { root, el } = setup() 54 55 55 56 root.render(html`<h1 class=${'foo'}>Hello, world!</h1>`) 56 - t.assert.strictEqual(el.querySelector('h1')!.className, 'foo') 57 + assert.equal(el.querySelector('h1')!.className, 'foo') 57 58 58 59 root.render(html`<label for=${'foo'}>Hello, world!</label>`) 59 - t.assert.strictEqual(el.querySelector('label')!.htmlFor, 'foo') 60 + assert.equal(el.querySelector('label')!.htmlFor, 'foo') 60 61 }) 61 62 62 - test('handles data attributes', (t: TestContext) => { 63 + test('handles data attributes', () => { 63 64 const { root, el } = setup() 64 65 65 66 root.render(html`<h1 data-foo=${'bar'}>Hello, world!</h1>`) 66 - t.assert.strictEqual(el.querySelector('h1')!.dataset.foo, 'bar') 67 + assert.equal(el.querySelector('h1')!.dataset.foo, 'bar') 67 68 }) 68 69 69 - test('supports events', (t: TestContext) => { 70 + test('supports events', () => { 70 71 const { root, el } = setup() 71 72 72 73 let clicks = 0 ··· 80 81 </button> 81 82 `) 82 83 83 - t.assert.strictEqual(clicks, 0) 84 + assert.equal(clicks, 0) 84 85 el.querySelector('button')!.click() 85 - t.assert.strictEqual(clicks, 1) 86 + assert.equal(clicks, 1) 86 87 el.querySelector('button')!.click() 87 - t.assert.strictEqual(clicks, 2) 88 + assert.equal(clicks, 2) 88 89 }) 89 90 90 - test('supports event handlers that change', (t: TestContext) => { 91 + test('supports event handlers that change', () => { 91 92 const { root, el } = setup() 92 93 93 - const template = (handler: (() => void) | null) => html`<input onblur=${handler}>Click me</input>` 94 + const template = (handler: ((event: Event) => void) | null) => html`<input onblur=${handler}>Click me</input>` 94 95 95 - const handler = t.mock.fn() 96 + const handler = mock((_event: Event) => {}) 96 97 root.render(template(handler)) 97 - t.assert.strictEqual(handler.mock.callCount(), 0) 98 + assert.equal(handler.mock.calls.length, 0) 98 99 99 100 const event = new Event('blur') 100 101 el.querySelector('input')!.dispatchEvent(event) 101 - t.assert.strictEqual(handler.mock.callCount(), 1) 102 - t.assert.strictEqual(handler.mock.calls[0].arguments[0], event) 102 + assert.equal(handler.mock.calls.length, 1) 103 + assert.equal(handler.mock.calls[0][0], event) 103 104 104 105 root.render(template(null)) 105 106 el.querySelector('input')!.dispatchEvent(new Event('blur')) 106 - t.assert.strictEqual(handler.mock.callCount(), 1) 107 + assert.equal(handler.mock.calls.length, 1) 107 108 })
+62 -63
src/client/tests/basic.test.ts
··· 1 + import { test } from 'bun:test' 1 2 import { html, type Displayable } from 'dhtml' 2 - import test, { type TestContext } from 'node:test' 3 + import assert from 'node:assert/strict' 3 4 import { setup } from './setup.ts' 4 5 5 - test('basic html renders correctly', (t: TestContext) => { 6 + const dev_test = test.skipIf(!__DEV__) 7 + 8 + test('basic html renders correctly', () => { 6 9 const { root, el } = setup() 7 10 8 11 root.render(html`<h1>Hello, world!</h1>`) 9 - t.assert.strictEqual(el.innerHTML, '<h1>Hello, world!</h1>') 12 + assert.equal(el.innerHTML, '<h1>Hello, world!</h1>') 10 13 }) 11 14 12 - test('inner content renders correctly', (t: TestContext) => { 15 + test('inner content renders correctly', () => { 13 16 const { root, el } = setup() 14 17 15 18 root.render(html`<h1>${html`Inner content!`}</h1>`) 16 - t.assert.strictEqual(el.innerHTML, '<h1>Inner content!</h1>') 19 + assert.equal(el.innerHTML, '<h1>Inner content!</h1>') 17 20 }) 18 21 19 - test('template with number renders correctly', (t: TestContext) => { 22 + test('template with number renders correctly', () => { 20 23 const { root, el } = setup() 21 24 22 25 const template = (n: number) => html`<h1>Hello, ${n}!</h1>` 23 26 24 27 root.render(template(1)) 25 - t.assert.strictEqual(el.innerHTML, '<h1>Hello, 1!</h1>') 28 + assert.equal(el.innerHTML, '<h1>Hello, 1!</h1>') 26 29 27 30 root.render(template(2)) 28 - t.assert.strictEqual(el.innerHTML, '<h1>Hello, 2!</h1>') 31 + assert.equal(el.innerHTML, '<h1>Hello, 2!</h1>') 29 32 }) 30 33 31 - test('external sibling nodes are not clobbered', (t: TestContext) => { 34 + test('external sibling nodes are not clobbered', () => { 32 35 const { root, el } = setup('<div>before</div>') 33 36 34 37 root.render(html`<h1>Hello, world!</h1>`) 35 - t.assert.strictEqual(el.innerHTML, '<div>before</div><h1>Hello, world!</h1>') 38 + assert.equal(el.innerHTML, '<div>before</div><h1>Hello, world!</h1>') 36 39 37 40 el.appendChild(document.createElement('div')).textContent = 'after' 38 - t.assert.strictEqual(el.innerHTML, '<div>before</div><h1>Hello, world!</h1><div>after</div>') 41 + assert.equal(el.innerHTML, '<div>before</div><h1>Hello, world!</h1><div>after</div>') 39 42 40 43 root.render(html`<h2>Goodbye, world!</h2>`) 41 - t.assert.strictEqual(el.innerHTML, '<div>before</div><h2>Goodbye, world!</h2><div>after</div>') 44 + assert.equal(el.innerHTML, '<div>before</div><h2>Goodbye, world!</h2><div>after</div>') 42 45 43 46 root.render(html``) 44 - t.assert.strictEqual(el.innerHTML, '<div>before</div><div>after</div>') 47 + assert.equal(el.innerHTML, '<div>before</div><div>after</div>') 45 48 46 49 root.render(html`<h1>Hello, world!</h1>`) 47 - t.assert.strictEqual(el.innerHTML, '<div>before</div><h1>Hello, world!</h1><div>after</div>') 50 + assert.equal(el.innerHTML, '<div>before</div><h1>Hello, world!</h1><div>after</div>') 48 51 }) 49 52 50 - test('identity is updated correctly', (t: TestContext) => { 53 + test('identity is updated correctly', () => { 51 54 const { root, el } = setup() 52 55 53 56 const template = (n: Displayable) => html`<h1>Hello, ${n}!</h1>` 54 57 const template2 = (n: Displayable) => html`<h1>Hello, ${n}!</h1>` 55 58 56 59 root.render(template(1)) 57 - t.assert.strictEqual(el.innerHTML, '<h1>Hello, 1!</h1>') 60 + assert.equal(el.innerHTML, '<h1>Hello, 1!</h1>') 58 61 let h1 = el.children[0] 59 62 const text = h1.childNodes[1] as Text 60 - t.assert.ok(text instanceof Text) 61 - t.assert.strictEqual(text.data, '1') 63 + assert(text instanceof Text) 64 + assert.equal(text.data, '1') 62 65 63 66 root.render(template(2)) 64 - t.assert.strictEqual(el.innerHTML, '<h1>Hello, 2!</h1>') 65 - t.assert.strictEqual(el.children[0], h1) 66 - t.assert.strictEqual(text.data, '2') 67 - t.assert.strictEqual(h1.childNodes[1], text) 67 + assert.equal(el.innerHTML, '<h1>Hello, 2!</h1>') 68 + assert.equal(el.children[0], h1) 69 + assert.equal(text.data, '2') 70 + assert.equal(h1.childNodes[1], text) 68 71 69 72 root.render(template2(3)) 70 - t.assert.strictEqual(el.innerHTML, '<h1>Hello, 3!</h1>') 71 - t.assert.notStrictEqual(el.children[0], h1) 73 + assert.equal(el.innerHTML, '<h1>Hello, 3!</h1>') 74 + assert.notStrictEqual(el.children[0], h1) 72 75 h1 = el.children[0] 73 76 74 77 root.render(template2(template(template('inner')))) 75 - t.assert.strictEqual(el.innerHTML, '<h1>Hello, <h1>Hello, <h1>Hello, inner!</h1>!</h1>!</h1>') 76 - t.assert.strictEqual(el.children[0], h1) 78 + assert.equal(el.innerHTML, '<h1>Hello, <h1>Hello, <h1>Hello, inner!</h1>!</h1>!</h1>') 79 + assert.equal(el.children[0], h1) 77 80 }) 78 81 79 - test('basic children render correctly', (t: TestContext) => { 82 + test('basic children render correctly', () => { 80 83 const { root, el } = setup() 81 84 82 85 root.render(html`<span>${'This is a'}</span> ${html`test`} ${html`test`} ${html`test`}`) 83 86 84 - t.assert.strictEqual(el.innerHTML, '<span>This is a</span> test test test') 87 + assert.equal(el.innerHTML, '<span>This is a</span> test test test') 85 88 }) 86 89 87 - test('nodes can be embedded', (t: TestContext) => { 90 + test('nodes can be embedded', () => { 88 91 const { root, el } = setup() 89 92 90 93 let node: ParentNode = document.createElement('span') 91 94 92 95 root.render(html`<div>${node}</div>`) 93 - t.assert.strictEqual(el.innerHTML, '<div><span></span></div>') 94 - t.assert.strictEqual(el.children[0].children[0], node) 96 + assert.equal(el.innerHTML, '<div><span></span></div>') 97 + assert.equal(el.children[0].children[0], node) 95 98 96 99 node = document.createDocumentFragment() 97 100 node.append(document.createElement('h1'), document.createElement('h2'), document.createElement('h3')) 98 101 99 102 root.render(html`<div>${node}</div>`) 100 - t.assert.strictEqual(el.innerHTML, '<div><h1></h1><h2></h2><h3></h3></div>') 101 - t.assert.strictEqual(node.children.length, 0) 103 + assert.equal(el.innerHTML, '<div><h1></h1><h2></h2><h3></h3></div>') 104 + assert.equal(node.children.length, 0) 102 105 }) 103 106 104 - test.skip('extra empty text nodes are not added', (t: TestContext) => { 107 + test.skip('extra empty text nodes are not added', () => { 105 108 const { root, el } = setup() 106 109 107 110 root.render(html`${'abc'}`) 108 - t.assert.strictEqual(el.childNodes.length, 1) 109 - t.assert.ok(el.firstChild instanceof Text) 110 - t.assert.strictEqual((el.firstChild as Text).data, 'abc') 111 + assert.equal(el.childNodes.length, 1) 112 + assert(el.firstChild instanceof Text) 113 + assert.equal((el.firstChild as Text).data, 'abc') 111 114 }) 112 115 113 - test('ChildPart index shifts correctly', (t: TestContext) => { 116 + test('ChildPart index shifts correctly', () => { 114 117 const { root, el } = setup() 115 118 116 119 root.render(html`${html`A<!--x-->`}B${'C'}`) 117 120 118 - t.assert.strictEqual(el.innerHTML, 'A<!--x-->BC') 121 + assert.equal(el.innerHTML, 'A<!--x-->BC') 119 122 }) 120 123 121 - test('errors are thrown cleanly', (t: TestContext) => { 124 + test('errors are thrown cleanly', () => { 122 125 const { root, el } = setup() 123 126 124 127 const oops = new Error('oops') ··· 134 137 } catch (error) { 135 138 thrown = error 136 139 } 137 - t.assert.strictEqual(thrown, oops) 140 + assert.equal(thrown, oops) 138 141 139 142 // on an error, don't leave any visible artifacts 140 - t.assert.strictEqual(el.innerHTML, '<!---->') 143 + assert.equal(el.innerHTML, '<!---->') 141 144 }) 142 145 143 - test('invalid part placement raises error', { skip: process.env.NODE_ENV === 'production' }, (t: TestContext) => { 146 + dev_test('invalid part placement raises error', () => { 144 147 const { root, el } = setup() 145 148 146 - t.assert.throws(() => root.render(html`<${'div'}>${'text'}</${'div'}>`), { 149 + assert.throws(() => root.render(html`<${'div'}>${'text'}</${'div'}>`), { 147 150 message: 'expected the same number of dynamics as parts. do you have a ${...} in an unsupported place?', 148 151 }) 149 - t.assert.strictEqual(el.innerHTML, '') 152 + assert.equal(el.innerHTML, '') 150 153 }) 151 154 152 - test('parts in comments do not throw', (t: TestContext) => { 155 + test('parts in comments do not throw', () => { 153 156 const { root, el } = setup() 154 157 155 158 root.render(html`<!-- ${'text'} -->`) 156 - t.assert.strictEqual(el.innerHTML, '<!-- dyn-$0$ -->') 159 + assert.equal(el.innerHTML, '<!-- dyn-$0$ -->') 157 160 }) 158 161 159 - test( 160 - 'manually specifying internal template syntax throws', 161 - { skip: process.env.NODE_ENV === 'production' }, 162 - (t: TestContext) => { 163 - const { root, el } = setup() 162 + dev_test('manually specifying internal template syntax throws', () => { 163 + const { root, el } = setup() 164 164 165 - t.assert.throws( 166 - () => { 167 - root.render(html`${1} dyn-$0$`) 168 - }, 169 - { message: 'got more parts than expected' }, 170 - ) 165 + assert.throws( 166 + () => { 167 + root.render(html`${1} dyn-$0$`) 168 + }, 169 + { message: 'got more parts than expected' }, 170 + ) 171 171 172 - t.assert.strictEqual(el.innerHTML, '') 173 - }, 174 - ) 172 + assert.equal(el.innerHTML, '') 173 + }) 175 174 176 - test('syntax close but not exact does not throw', (t: TestContext) => { 175 + test('syntax close but not exact does not throw', () => { 177 176 const { root, el } = setup() 178 177 179 178 root.render(html`dyn-$${0}1$`) 180 179 181 - t.assert.strictEqual(el.innerHTML, 'dyn-$01$') 180 + assert.equal(el.innerHTML, 'dyn-$01$') 182 181 })
+9 -8
src/client/tests/custom-elements.test.ts
··· 1 + import { test } from 'bun:test' 1 2 import { html } from 'dhtml' 2 3 import { createRoot } from 'dhtml/client' 3 - import test, { type TestContext } from 'node:test' 4 + import assert from 'node:assert/strict' 4 5 import { setup } from './setup.ts' 5 6 6 7 class CustomElement extends HTMLElement { ··· 20 21 21 22 customElements.define('custom-element', CustomElement) 22 23 23 - test('custom elements instantiate correctly', (t: TestContext) => { 24 + test('custom elements instantiate correctly', () => { 24 25 const { root, el } = setup() 25 26 26 27 root.render(html`<custom-element thing=${'hello'}></custom-element>`) 27 - t.assert.strictEqual(el.innerHTML, `<custom-element>inside custom element</custom-element>`) 28 + assert.equal(el.innerHTML, `<custom-element>inside custom element</custom-element>`) 28 29 29 30 const customElement = el.querySelector('custom-element') as CustomElement 30 - t.assert.ok(customElement instanceof CustomElement) 31 - t.assert.strictEqual(customElement.thing, 'HELLO') 31 + assert(customElement instanceof CustomElement) 32 + assert.equal(customElement.thing, 'HELLO') 32 33 }) 33 34 34 - test('content renders into shadow dom', (t: TestContext) => { 35 + test('content renders into shadow dom', () => { 35 36 const { el } = setup() 36 37 const shadowRoot = el.attachShadow({ mode: 'open' }) 37 38 38 39 const root = createRoot(shadowRoot) 39 40 root.render(html`<p>hello</p>`) 40 41 41 - t.assert.strictEqual(el.innerHTML, ``) 42 - t.assert.strictEqual(shadowRoot.innerHTML, `<p>hello</p>`) 42 + assert.equal(el.innerHTML, ``) 43 + assert.equal(shadowRoot.innerHTML, `<p>hello</p>`) 43 44 })
+19 -18
src/client/tests/directives.test.ts
··· 1 + import { test } from 'bun:test' 1 2 import { html } from 'dhtml' 2 3 import { attr, type Directive } from 'dhtml/client' 3 - import test, { type TestContext } from 'node:test' 4 + import assert from 'node:assert/strict' 4 5 import { setup } from './setup.ts' 5 6 6 - test('directive functions work correctly', (t: TestContext) => { 7 + test('directive functions work correctly', () => { 7 8 const { root, el } = setup() 8 9 9 10 const redifier: Directive = node => { ··· 25 26 26 27 root.render(template(redifier)) 27 28 const div = el.firstChild as HTMLElement 28 - t.assert.strictEqual(div.tagName, 'DIV') 29 - t.assert.strictEqual(div.style.cssText, 'color: red;') 29 + assert.equal(div.tagName, 'DIV') 30 + assert.equal(div.style.cssText, 'color: red;') 30 31 31 32 root.render(template(flipper)) 32 - t.assert.strictEqual(div.style.cssText, 'transform: scaleX(-1);') 33 + assert.equal(div.style.cssText, 'transform: scaleX(-1);') 33 34 34 35 root.render(template(null)) 35 - t.assert.strictEqual(div.style.cssText, '') 36 + assert.equal(div.style.cssText, '') 36 37 37 38 root.render(null) 38 39 }) 39 40 40 - test('directive functions with values work correctly', (t: TestContext) => { 41 + test('directive functions with values work correctly', () => { 41 42 const { root, el } = setup() 42 43 43 44 function classes(value: string[]): Directive { ··· 54 55 55 56 root.render(template(['a', 'b'])) 56 57 const div = el.firstChild as HTMLElement 57 - t.assert.strictEqual(div.tagName, 'DIV') 58 - t.assert.strictEqual(div.className, 'foo a b') 58 + assert.equal(div.tagName, 'DIV') 59 + assert.equal(div.className, 'foo a b') 59 60 60 61 root.render(template(['c', 'd'])) 61 - t.assert.strictEqual(div.className, 'foo c d') 62 + assert.equal(div.className, 'foo c d') 62 63 63 64 root.render(template([])) 64 - t.assert.strictEqual(div.className, 'foo') 65 + assert.equal(div.className, 'foo') 65 66 }) 66 67 67 - test('attr directive works correctly', (t: TestContext) => { 68 + test('attr directive works correctly', () => { 68 69 const { root, el } = setup() 69 70 70 71 const template = (value: string | null) => html` ··· 73 74 ` 74 75 75 76 root.render(template('attr-works-input')) 76 - t.assert.strictEqual(el.querySelector('label')!.htmlFor, 'attr-works-input') 77 + assert.equal(el.querySelector('label')!.htmlFor, 'attr-works-input') 77 78 78 79 root.render(template('updated')) 79 - t.assert.strictEqual(el.querySelector('label')!.htmlFor, 'updated') 80 + assert.equal(el.querySelector('label')!.htmlFor, 'updated') 80 81 81 82 root.render(template(null)) 82 - t.assert.strictEqual(el.querySelector('label')!.htmlFor, '') 83 + assert.equal(el.querySelector('label')!.htmlFor, '') 83 84 }) 84 85 85 - test('attr directive supports booleans', (t: TestContext) => { 86 + test('attr directive supports booleans', () => { 86 87 const { root, el } = setup() 87 88 88 89 const template = (value: boolean) => html`<input ${attr('disabled', value)} />` 89 90 90 91 root.render(template(true)) 91 - t.assert.strictEqual(el.querySelector('input')!.disabled, true) 92 + assert.equal(el.querySelector('input')!.disabled, true) 92 93 93 94 root.render(template(false)) 94 - t.assert.strictEqual(el.querySelector('input')!.disabled, false) 95 + assert.equal(el.querySelector('input')!.disabled, false) 95 96 })
+114 -111
src/client/tests/lists.test.ts
··· 1 + import { test } from 'bun:test' 1 2 import { html, type Displayable } from 'dhtml' 2 3 import { keyed } from 'dhtml/client' 3 - import test, { type TestContext } from 'node:test' 4 + import assert from 'node:assert/strict' 4 5 import { setup } from './setup.ts' 5 6 7 + const dev_test = test.skipIf(!__DEV__) 8 + 6 9 function shuffle<T>(array: T[]) { 7 10 for (let i = 0; i < array.length; i++) { 8 11 const j = Math.floor(Math.random() * i) ··· 10 13 } 11 14 } 12 15 13 - test('basic list operations work correctly', (t: TestContext) => { 16 + test('basic list operations work correctly', () => { 14 17 const { root, el } = setup() 15 18 16 19 let items: Displayable[] | null = null ··· 23 26 ` 24 27 25 28 root.render(listOfItems()) 26 - t.assert.strictEqual(el.innerHTML.replace(/\s+/g, ' '), ' <ul> <li>Before</li> <li>After</li> </ul> ') 29 + assert.equal(el.innerHTML.replace(/\s+/g, ' '), ' <ul> <li>Before</li> <li>After</li> </ul> ') 27 30 28 31 items = [html`<li>Item 1</li>`, html`<li>Item 2</li>`, html`<li>Item 3</li>`] 29 32 30 33 root.render(listOfItems()) 31 - t.assert.strictEqual( 34 + assert.equal( 32 35 el.innerHTML.replace(/\s+/g, ' '), 33 36 ' <ul> <li>Before</li> <li>Item 1</li><li>Item 2</li><li>Item 3</li> <li>After</li> </ul> ', 34 37 ) ··· 36 39 37 40 items.push(html`<li>Item 4</li>`) 38 41 root.render(listOfItems()) 39 - t.assert.strictEqual( 42 + assert.equal( 40 43 el.innerHTML.replace(/\s+/g, ' '), 41 44 ' <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 45 ) 43 46 const [item1b, item2b, item3b] = el.querySelectorAll('li') 44 - t.assert.strictEqual(item1, item1b) 45 - t.assert.strictEqual(item2, item2b) 46 - t.assert.strictEqual(item3, item3b) 47 + assert.equal(item1, item1b) 48 + assert.equal(item2, item2b) 49 + assert.equal(item3, item3b) 47 50 48 51 items.pop() 49 52 items.pop() 50 53 root.render(listOfItems()) 51 - t.assert.strictEqual( 54 + assert.equal( 52 55 el.innerHTML.replace(/\s+/g, ' '), 53 56 ' <ul> <li>Before</li> <li>Item 1</li><li>Item 2</li> <li>After</li> </ul> ', 54 57 ) 55 58 const [item1c, item2c] = el.querySelectorAll('li') 56 - t.assert.strictEqual(item1, item1c) 57 - t.assert.strictEqual(item2, item2c) 59 + assert.equal(item1, item1c) 60 + assert.equal(item2, item2c) 58 61 }) 59 62 60 - test('pop operation works correctly on lists', (t: TestContext) => { 63 + test('pop operation works correctly on lists', () => { 61 64 const { root, el } = setup() 62 65 63 66 const items = [html`<p>Item 1</p>`, html`<p>Item 2</p>`, html`<p>Item 3</p>`] 64 67 const wrapped = html`[${items}]` 65 68 66 69 root.render(wrapped) 67 - t.assert.strictEqual(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 70 + assert.equal(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 68 71 const [item1, item2] = el.children 69 72 70 73 items.pop() 71 74 root.render(wrapped) 72 - t.assert.strictEqual(el.innerHTML, '[<p>Item 1</p><p>Item 2</p>]') 73 - t.assert.strictEqual(el.children[0], item1) 74 - t.assert.strictEqual(el.children[1], item2) 75 + assert.equal(el.innerHTML, '[<p>Item 1</p><p>Item 2</p>]') 76 + assert.equal(el.children[0], item1) 77 + assert.equal(el.children[1], item2) 75 78 76 79 items.pop() 77 80 root.render(wrapped) 78 - t.assert.strictEqual(el.innerHTML, '[<p>Item 1</p>]') 79 - t.assert.strictEqual(el.children[0], item1) 81 + assert.equal(el.innerHTML, '[<p>Item 1</p>]') 82 + assert.equal(el.children[0], item1) 80 83 81 84 items.pop() 82 85 root.render(wrapped) 83 - t.assert.strictEqual(el.innerHTML, '[]') 86 + assert.equal(el.innerHTML, '[]') 84 87 }) 85 88 86 - test('swap operation works correctly on lists', (t: TestContext) => { 89 + test('swap operation works correctly on lists', () => { 87 90 const { root, el } = setup() 88 91 89 92 const items = [html`<p>Item 1</p>`, html`<p>Item 2</p>`, html`<p>Item 3</p>`] 90 93 const wrapped = html`[${items}]` 91 94 92 95 root.render(wrapped) 93 - t.assert.strictEqual(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 96 + assert.equal(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 94 97 const [item1, item2, item3] = el.children 95 98 96 99 // swap the first two items 97 100 ;[items[0], items[1]] = [items[1], items[0]] 98 101 root.render(wrapped) 99 - t.assert.strictEqual(el.innerHTML, '[<p>Item 2</p><p>Item 1</p><p>Item 3</p>]') 100 - t.assert.strictEqual(el.children[0], item2) 101 - t.assert.strictEqual(el.children[1], item1) 102 - t.assert.strictEqual(el.children[2], item3) 102 + assert.equal(el.innerHTML, '[<p>Item 2</p><p>Item 1</p><p>Item 3</p>]') 103 + assert.equal(el.children[0], item2) 104 + assert.equal(el.children[1], item1) 105 + assert.equal(el.children[2], item3) 103 106 104 107 // swap the last two items 105 108 ;[items[1], items[2]] = [items[2], items[1]] 106 109 root.render(wrapped) 107 - t.assert.strictEqual(el.innerHTML, '[<p>Item 2</p><p>Item 3</p><p>Item 1</p>]') 108 - t.assert.strictEqual(el.children[0], item2) 109 - t.assert.strictEqual(el.children[1], item3) 110 - t.assert.strictEqual(el.children[2], item1) 110 + assert.equal(el.innerHTML, '[<p>Item 2</p><p>Item 3</p><p>Item 1</p>]') 111 + assert.equal(el.children[0], item2) 112 + assert.equal(el.children[1], item3) 113 + assert.equal(el.children[2], item1) 111 114 112 115 // swap the first and last items 113 116 ;[items[0], items[2]] = [items[2], items[0]] 114 117 root.render(wrapped) 115 - t.assert.strictEqual(el.innerHTML, '[<p>Item 1</p><p>Item 3</p><p>Item 2</p>]') 116 - t.assert.strictEqual(el.children[0], item1) 117 - t.assert.strictEqual(el.children[1], item3) 118 - t.assert.strictEqual(el.children[2], item2) 118 + assert.equal(el.innerHTML, '[<p>Item 1</p><p>Item 3</p><p>Item 2</p>]') 119 + assert.equal(el.children[0], item1) 120 + assert.equal(el.children[1], item3) 121 + assert.equal(el.children[2], item2) 119 122 120 123 // put things back 121 124 ;[items[1], items[2]] = [items[2], items[1]] 122 125 root.render(wrapped) 123 - t.assert.strictEqual(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 124 - t.assert.strictEqual(el.children[0], item1) 125 - t.assert.strictEqual(el.children[1], item2) 126 - t.assert.strictEqual(el.children[2], item3) 126 + assert.equal(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 127 + assert.equal(el.children[0], item1) 128 + assert.equal(el.children[1], item2) 129 + assert.equal(el.children[2], item3) 127 130 }) 128 131 129 - test('shift operation works correctly on lists', (t: TestContext) => { 132 + test('shift operation works correctly on lists', () => { 130 133 const { root, el } = setup() 131 134 132 135 const items = [html`<p>Item 1</p>`, html`<p>Item 2</p>`, html`<p>Item 3</p>`] 133 136 const wrapped = html`[${items}]` 134 137 135 138 root.render(wrapped) 136 - t.assert.strictEqual(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 139 + assert.equal(el.innerHTML, '[<p>Item 1</p><p>Item 2</p><p>Item 3</p>]') 137 140 const [, item2, item3] = el.children 138 141 139 142 items.shift() 140 143 root.render(wrapped) 141 - t.assert.strictEqual(el.innerHTML, '[<p>Item 2</p><p>Item 3</p>]') 142 - t.assert.strictEqual(el.children[0], item2) 143 - t.assert.strictEqual(el.children[1], item3) 144 + assert.equal(el.innerHTML, '[<p>Item 2</p><p>Item 3</p>]') 145 + assert.equal(el.children[0], item2) 146 + assert.equal(el.children[1], item3) 144 147 145 148 items.shift() 146 149 root.render(wrapped) 147 - t.assert.strictEqual(el.innerHTML, '[<p>Item 3</p>]') 148 - t.assert.strictEqual(el.children[0], item3) 150 + assert.equal(el.innerHTML, '[<p>Item 3</p>]') 151 + assert.equal(el.children[0], item3) 149 152 150 153 items.shift() 151 154 root.render(wrapped) 152 - t.assert.strictEqual(el.innerHTML, '[]') 155 + assert.equal(el.innerHTML, '[]') 153 156 }) 154 157 155 - test('full then empty then full list renders correctly', (t: TestContext) => { 158 + test('full then empty then full list renders correctly', () => { 156 159 const { root, el } = setup() 157 160 158 161 root.render([1]) 159 - t.assert.strictEqual(el.innerHTML, '1') 162 + assert.equal(el.innerHTML, '1') 160 163 161 164 root.render([]) 162 - t.assert.strictEqual(el.innerHTML, '') 165 + assert.equal(el.innerHTML, '') 163 166 164 167 root.render([2]) 165 - t.assert.strictEqual(el.innerHTML, '2') 168 + assert.equal(el.innerHTML, '2') 166 169 }) 167 170 168 - test('list can disappear when condition changes', (t: TestContext) => { 171 + test('list can disappear when condition changes', () => { 169 172 const { root, el } = setup() 170 173 171 174 const app = { ··· 177 180 } 178 181 179 182 root.render(app) 180 - t.assert.strictEqual(el.innerHTML, '<div>1</div><div>2</div><div>3</div>') 183 + assert.equal(el.innerHTML, '<div>1</div><div>2</div><div>3</div>') 181 184 182 185 app.show = false 183 186 root.render(app) 184 - t.assert.strictEqual(el.innerHTML, '') 187 + assert.equal(el.innerHTML, '') 185 188 }) 186 189 187 - test('unkeyed lists recreate elements when reordered', (t: TestContext) => { 190 + test('unkeyed lists recreate elements when reordered', () => { 188 191 const { root, el } = setup() 189 192 190 193 const a = () => html`<h1>Item 1</h1>` 191 194 const b = () => html`<h2>Item 2</h2>` 192 195 193 196 root.render([a(), b()]) 194 - t.assert.strictEqual(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 197 + assert.equal(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 195 198 196 199 const [h1, h2] = el.children 197 - t.assert.strictEqual(h1.tagName, 'H1') 198 - t.assert.strictEqual(h2.tagName, 'H2') 200 + assert.equal(h1.tagName, 'H1') 201 + assert.equal(h2.tagName, 'H2') 199 202 200 203 root.render([b(), a()]) 201 - t.assert.strictEqual(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 204 + assert.equal(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 202 205 203 206 // visually they should be swapped 204 - t.assert.strictEqual(el.children[0].innerHTML, h2.innerHTML) 205 - t.assert.strictEqual(el.children[1].innerHTML, h1.innerHTML) 207 + assert.equal(el.children[0].innerHTML, h2.innerHTML) 208 + assert.equal(el.children[1].innerHTML, h1.innerHTML) 206 209 207 210 // but there's no stable identity, so they're recreated 208 - t.assert.notStrictEqual(el.children[0], h2) 209 - t.assert.notStrictEqual(el.children[1], h1) 211 + assert.notStrictEqual(el.children[0], h2) 212 + assert.notStrictEqual(el.children[1], h1) 210 213 }) 211 214 212 - test('explicit keyed lists preserve identity when reordered', (t: TestContext) => { 215 + test('explicit keyed lists preserve identity when reordered', () => { 213 216 const { root, el } = setup() 214 217 215 218 const a = () => keyed(html`<h1>Item 1</h1>`, 1) 216 219 const b = () => keyed(html`<h2>Item 2</h2>`, 2) 217 220 218 221 root.render([a(), b()]) 219 - t.assert.strictEqual(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 222 + assert.equal(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 220 223 221 224 const [h1, h2] = el.children 222 - t.assert.strictEqual(h1.tagName, 'H1') 223 - t.assert.strictEqual(h2.tagName, 'H2') 225 + assert.equal(h1.tagName, 'H1') 226 + assert.equal(h2.tagName, 'H2') 224 227 225 228 root.render([b(), a()]) 226 - t.assert.strictEqual(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 229 + assert.equal(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 227 230 228 - t.assert.strictEqual(el.children[0], h2) 229 - t.assert.strictEqual(el.children[1], h1) 231 + assert.equal(el.children[0], h2) 232 + assert.equal(el.children[1], h1) 230 233 }) 231 234 232 - test('implicit keyed lists preserve identity when reordered', (t: TestContext) => { 235 + test('implicit keyed lists preserve identity when reordered', () => { 233 236 const { root, el } = setup() 234 237 235 238 const items = [html`<h1>Item 1</h1>`, html`<h2>Item 2</h2>`] 236 239 237 240 root.render(items) 238 - t.assert.strictEqual(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 241 + assert.equal(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 239 242 240 243 const [h1, h2] = el.children 241 - t.assert.strictEqual(h1.tagName, 'H1') 242 - t.assert.strictEqual(h2.tagName, 'H2') 244 + assert.equal(h1.tagName, 'H1') 245 + assert.equal(h2.tagName, 'H2') 243 246 ;[items[0], items[1]] = [items[1], items[0]] 244 247 245 248 root.render(items) 246 - t.assert.strictEqual(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 247 - t.assert.strictEqual(el.children[0].tagName, 'H2') 248 - t.assert.strictEqual(el.children[1].tagName, 'H1') 249 + assert.equal(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 250 + assert.equal(el.children[0].tagName, 'H2') 251 + assert.equal(el.children[1].tagName, 'H1') 249 252 250 - t.assert.strictEqual(el.children[0], h2) 251 - t.assert.strictEqual(el.children[1], h1) 253 + assert.equal(el.children[0], h2) 254 + assert.equal(el.children[1], h1) 252 255 }) 253 256 254 - test('implicit keyed lists with multiple elements preserve identity when resized', (t: TestContext) => { 257 + test('implicit keyed lists with multiple elements preserve identity when resized', () => { 255 258 const { root, el } = setup() 256 259 257 260 const items = [ ··· 263 266 ] 264 267 265 268 root.render(items) 266 - t.assert.strictEqual(el.innerHTML.replace(/\s+/g, ' '), '<h1>Item 1</h1> <h2>Item 2</h2> <p>Body content</p> ') 269 + assert.equal(el.innerHTML.replace(/\s+/g, ' '), '<h1>Item 1</h1> <h2>Item 2</h2> <p>Body content</p> ') 267 270 268 271 const [h1, h2, p] = el.children 269 - t.assert.strictEqual(h1.tagName, 'H1') 270 - t.assert.strictEqual(h2.tagName, 'H2') 271 - t.assert.strictEqual(p.tagName, 'P') 272 + assert.equal(h1.tagName, 'H1') 273 + assert.equal(h2.tagName, 'H2') 274 + assert.equal(p.tagName, 'P') 272 275 273 276 // Swap 274 277 ;[items[0], items[1]] = [items[1], items[0]] 275 278 root.render(items) 276 - t.assert.strictEqual(el.innerHTML.replace(/\s+/g, ' '), ' <h2>Item 2</h2> <p>Body content</p> <h1>Item 1</h1>') 277 - t.assert.strictEqual(el.children[0].tagName, 'H2') 278 - t.assert.strictEqual(el.children[1].tagName, 'P') 279 - t.assert.strictEqual(el.children[2].tagName, 'H1') 279 + assert.equal(el.innerHTML.replace(/\s+/g, ' '), ' <h2>Item 2</h2> <p>Body content</p> <h1>Item 1</h1>') 280 + assert.equal(el.children[0].tagName, 'H2') 281 + assert.equal(el.children[1].tagName, 'P') 282 + assert.equal(el.children[2].tagName, 'H1') 280 283 281 - t.assert.strictEqual(el.children[0], h2) 282 - t.assert.strictEqual(el.children[1], p) 283 - t.assert.strictEqual(el.children[2], h1) 284 + assert.equal(el.children[0], h2) 285 + assert.equal(el.children[1], p) 286 + assert.equal(el.children[2], h1) 284 287 285 288 // Swap back 286 289 ;[items[0], items[1]] = [items[1], items[0]] 287 290 root.render(items) 288 - t.assert.strictEqual(el.innerHTML.replace(/\s+/g, ' '), '<h1>Item 1</h1> <h2>Item 2</h2> <p>Body content</p> ') 289 - t.assert.strictEqual(el.children[0].tagName, 'H1') 290 - t.assert.strictEqual(el.children[1].tagName, 'H2') 291 - t.assert.strictEqual(el.children[2].tagName, 'P') 292 - t.assert.strictEqual(el.children[0], h1) 293 - t.assert.strictEqual(el.children[1], h2) 294 - t.assert.strictEqual(el.children[2], p) 291 + assert.equal(el.innerHTML.replace(/\s+/g, ' '), '<h1>Item 1</h1> <h2>Item 2</h2> <p>Body content</p> ') 292 + assert.equal(el.children[0].tagName, 'H1') 293 + assert.equal(el.children[1].tagName, 'H2') 294 + assert.equal(el.children[2].tagName, 'P') 295 + assert.equal(el.children[0], h1) 296 + assert.equal(el.children[1], h2) 297 + assert.equal(el.children[2], p) 295 298 }) 296 299 297 - test('implicit keyed renderable lists preserve identity when reordered', (t: TestContext) => { 300 + test('implicit keyed renderable lists preserve identity when reordered', () => { 298 301 const { root, el } = setup() 299 302 300 303 const items = [{ render: () => html`<h1>Item 1</h1>` }, { render: () => html`<h2>Item 2</h2>` }] 301 304 302 305 root.render(items) 303 - t.assert.strictEqual(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 306 + assert.equal(el.innerHTML, '<h1>Item 1</h1><h2>Item 2</h2>') 304 307 305 308 const [h1, h2] = el.children 306 - t.assert.strictEqual(h1.tagName, 'H1') 307 - t.assert.strictEqual(h2.tagName, 'H2') 309 + assert.equal(h1.tagName, 'H1') 310 + assert.equal(h2.tagName, 'H2') 308 311 ;[items[0], items[1]] = [items[1], items[0]] 309 312 310 313 root.render(items) 311 - t.assert.strictEqual(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 312 - t.assert.strictEqual(el.children[0].tagName, 'H2') 313 - t.assert.strictEqual(el.children[1].tagName, 'H1') 314 + assert.equal(el.innerHTML, '<h2>Item 2</h2><h1>Item 1</h1>') 315 + assert.equal(el.children[0].tagName, 'H2') 316 + assert.equal(el.children[1].tagName, 'H1') 314 317 315 - t.assert.strictEqual(el.children[0], h2) 316 - t.assert.strictEqual(el.children[1], h1) 318 + assert.equal(el.children[0], h2) 319 + assert.equal(el.children[1], h1) 317 320 }) 318 321 319 - test('many items can be reordered', (t: TestContext) => { 322 + test('many items can be reordered', () => { 320 323 const { root, el } = setup() 321 324 322 325 const items = Array.from({ length: 10 }, (_, i) => [html`<p>Item ${i}</p>`, `<p>Item ${i}</p>`]) 323 326 324 327 root.render(items.map(([item]) => item)) 325 - t.assert.strictEqual(el.innerHTML, items.map(([, html]) => html).join('')) 328 + assert.equal(el.innerHTML, items.map(([, html]) => html).join('')) 326 329 327 330 shuffle(items) 328 331 329 332 root.render(items.map(([item]) => item)) 330 - t.assert.strictEqual(el.innerHTML, items.map(([, html]) => html).join('')) 333 + assert.equal(el.innerHTML, items.map(([, html]) => html).join('')) 331 334 }) 332 335 333 - test('keying something twice throws an error', { skip: process.env.NODE_ENV === 'production' }, (t: TestContext) => { 334 - t.assert.doesNotThrow(() => keyed(html``, 1)) 335 - t.assert.throws(() => keyed(keyed(html``, 1), 1)) 336 + dev_test('keying something twice throws an error', () => { 337 + assert.doesNotThrow(() => keyed(html``, 1)) 338 + assert.throws(() => keyed(keyed(html``, 1), 1)) 336 339 })
+6 -5
src/client/tests/recursion.test.ts
··· 1 + import { test } from 'bun:test' 1 2 import { html } from 'dhtml' 2 - import test, { type TestContext } from 'node:test' 3 + import assert from 'node:assert/strict' 3 4 import { setup } from './setup.ts' 4 5 5 6 const DEPTH = 10 6 7 7 - test('basic recursion is handled correctly', (t: TestContext) => { 8 + test('basic recursion is handled correctly', () => { 8 9 const { root, el } = setup() 9 10 10 11 const app = { ··· 15 16 }, 16 17 } 17 18 root.render(app) 18 - t.assert.strictEqual(el.innerHTML, 'hello!') 19 + assert.equal(el.innerHTML, 'hello!') 19 20 }) 20 21 21 - test('nested recursion is handled correctly', (t: TestContext) => { 22 + test('nested recursion is handled correctly', () => { 22 23 const { root, el } = setup() 23 24 24 25 const app = { ··· 29 30 }, 30 31 } 31 32 root.render(app) 32 - t.assert.strictEqual(el.innerHTML, '<span>'.repeat(DEPTH) + 'hello!' + '</span>'.repeat(DEPTH)) 33 + assert.equal(el.innerHTML, '<span>'.repeat(DEPTH) + 'hello!' + '</span>'.repeat(DEPTH)) 33 34 })
+67 -66
src/client/tests/renderable.test.ts
··· 1 + import { mock, test } from 'bun:test' 1 2 import { html, type Renderable } from 'dhtml' 2 3 import { getParentNode, invalidate, onMount, onUnmount } from 'dhtml/client' 3 - import test, { type TestContext } from 'node:test' 4 + import assert from 'node:assert/strict' 4 5 import { setup } from './setup.ts' 5 6 6 - test('renderables work correctly', async (t: TestContext) => { 7 + test('renderables work correctly', async () => { 7 8 const { root, el } = setup() 8 9 9 10 root.render( ··· 13 14 }, 14 15 }}`, 15 16 ) 16 - t.assert.strictEqual(el.innerHTML, '<h1>Hello, world!</h1>') 17 + assert.equal(el.innerHTML, '<h1>Hello, world!</h1>') 17 18 18 19 const app = { 19 20 i: 0, ··· 22 23 }, 23 24 } 24 25 root.render(app) 25 - t.assert.strictEqual(el.innerHTML, 'Count: 0') 26 + assert.equal(el.innerHTML, 'Count: 0') 26 27 root.render(app) 27 - t.assert.strictEqual(el.innerHTML, 'Count: 1') 28 + assert.equal(el.innerHTML, 'Count: 1') 28 29 await invalidate(app) 29 - t.assert.strictEqual(el.innerHTML, 'Count: 2') 30 + assert.equal(el.innerHTML, 'Count: 2') 30 31 await invalidate(app) 31 - t.assert.strictEqual(el.innerHTML, 'Count: 3') 32 - t.assert.strictEqual(app.i, 4) 32 + assert.equal(el.innerHTML, 'Count: 3') 33 + assert.equal(app.i, 4) 33 34 }) 34 35 35 - test('renderables handle undefined correctly', (t: TestContext) => { 36 + test('renderables handle undefined correctly', () => { 36 37 const { root, el } = setup() 37 38 38 39 root.render({ ··· 40 41 render() {}, 41 42 }) 42 43 43 - t.assert.strictEqual(el.innerHTML, '') 44 + assert.equal(el.innerHTML, '') 44 45 }) 45 46 46 - test('onMount calls in the right order', (t: TestContext) => { 47 + test('onMount calls in the right order', () => { 47 48 const { root, el } = setup() 48 49 49 50 const sequence: string[] = [] ··· 86 87 87 88 outer.show = true 88 89 root.render(outer) 89 - t.assert.strictEqual(el.innerHTML, 'inner') 90 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner render', 'inner mount', 'outer mount']) 90 + assert.equal(el.innerHTML, 'inner') 91 + assert.deepStrictEqual(sequence, ['outer render', 'inner render', 'inner mount', 'outer mount']) 91 92 sequence.length = 0 92 93 93 94 outer.show = false 94 95 root.render(outer) 95 - t.assert.strictEqual(el.innerHTML, '') 96 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner cleanup']) 96 + assert.equal(el.innerHTML, '') 97 + assert.deepStrictEqual(sequence, ['outer render', 'inner cleanup']) 97 98 sequence.length = 0 98 99 99 100 outer.show = true 100 101 root.render(outer) 101 - t.assert.strictEqual(el.innerHTML, 'inner') 102 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 102 + assert.equal(el.innerHTML, 'inner') 103 + assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 103 104 sequence.length = 0 104 105 }) 105 106 106 - test('onMount registers multiple callbacks', (t: TestContext) => { 107 + test('onMount registers multiple callbacks', () => { 107 108 const { root } = setup() 108 109 109 110 const sequence: string[] = [] ··· 125 126 } 126 127 127 128 root.render(app) 128 - t.assert.deepStrictEqual(sequence, ['mount 1', 'mount 2']) 129 + assert.deepStrictEqual(sequence, ['mount 1', 'mount 2']) 129 130 sequence.length = 0 130 131 131 132 root.render(null) 132 - t.assert.deepStrictEqual(sequence, ['cleanup 1', 'cleanup 2']) 133 + assert.deepStrictEqual(sequence, ['cleanup 1', 'cleanup 2']) 133 134 }) 134 135 135 - test('onMount registers a fixed callback once', (t: TestContext) => { 136 + test('onMount registers a fixed callback once', () => { 136 137 const { root } = setup() 137 138 138 139 const sequence: string[] = [] ··· 151 152 } 152 153 153 154 root.render(app) 154 - t.assert.deepStrictEqual(sequence, ['mount']) 155 + assert.deepStrictEqual(sequence, ['mount']) 155 156 sequence.length = 0 156 157 157 158 root.render(null) 158 - t.assert.deepStrictEqual(sequence, ['cleanup']) 159 + assert.deepStrictEqual(sequence, ['cleanup']) 159 160 }) 160 161 161 - test('onMount registers callbacks outside of render', (t: TestContext) => { 162 + test('onMount registers callbacks outside of render', () => { 162 163 const { root } = setup() 163 164 164 165 const sequence: string[] = [] ··· 175 176 return () => sequence.push('cleanup') 176 177 }) 177 178 178 - t.assert.deepStrictEqual(sequence, []) 179 + assert.deepStrictEqual(sequence, []) 179 180 180 181 root.render(app) 181 - t.assert.deepStrictEqual(sequence, ['render', 'mount']) 182 + assert.deepStrictEqual(sequence, ['render', 'mount']) 182 183 sequence.length = 0 183 184 184 185 root.render(null) 185 - t.assert.deepStrictEqual(sequence, ['cleanup']) 186 + assert.deepStrictEqual(sequence, ['cleanup']) 186 187 }) 187 188 188 - test('onMount can access the dom in callback', (t: TestContext) => { 189 + test('onMount can access the dom in callback', () => { 189 190 const { root } = setup() 190 191 191 192 const app = { 192 193 render() { 193 194 onMount(this, () => { 194 195 const parent = getParentNode(this) as Element 195 - t.assert.ok(parent.firstElementChild instanceof HTMLParagraphElement) 196 + assert(parent.firstElementChild instanceof HTMLParagraphElement) 196 197 }) 197 198 return html`<p>Hello, world!</p>` 198 199 }, ··· 201 202 root.render(app) 202 203 }) 203 204 204 - test('onMount works after render', (t: TestContext) => { 205 + test('onMount works after render', () => { 205 206 const { root } = setup() 206 207 207 208 const app = { ··· 212 213 213 214 root.render(app) 214 215 215 - const mounted = t.mock.fn() 216 + const mounted = mock(() => {}) 216 217 onMount(app, mounted) 217 - t.assert.strictEqual(mounted.mock.callCount(), 1) 218 + assert.equal(mounted.mock.calls.length, 1) 218 219 }) 219 220 220 - test('onUnmount deep works correctly', (t: TestContext) => { 221 + test('onUnmount deep works correctly', () => { 221 222 const { root, el } = setup() 222 223 223 224 const sequence: string[] = [] ··· 256 257 257 258 outer.show = true 258 259 root.render(outer) 259 - t.assert.strictEqual(el.innerHTML, 'inner') 260 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 260 + assert.equal(el.innerHTML, 'inner') 261 + assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 261 262 sequence.length = 0 262 263 263 264 outer.show = false 264 265 root.render(outer) 265 - t.assert.strictEqual(el.innerHTML, '') 266 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner abort']) 266 + assert.equal(el.innerHTML, '') 267 + assert.deepStrictEqual(sequence, ['outer render', 'inner abort']) 267 268 sequence.length = 0 268 269 269 270 outer.show = true 270 271 root.render(outer) 271 - t.assert.strictEqual(el.innerHTML, 'inner') 272 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 272 + assert.equal(el.innerHTML, 'inner') 273 + assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 273 274 sequence.length = 0 274 275 275 276 outer.show = false 276 277 root.render(outer) 277 - t.assert.strictEqual(el.innerHTML, '') 278 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner abort']) 278 + assert.equal(el.innerHTML, '') 279 + assert.deepStrictEqual(sequence, ['outer render', 'inner abort']) 279 280 sequence.length = 0 280 281 }) 281 282 282 - test('onUnmount shallow works correctly', (t: TestContext) => { 283 + test('onUnmount shallow works correctly', () => { 283 284 const { root, el } = setup() 284 285 285 286 const sequence: string[] = [] ··· 317 318 318 319 outer.show = true 319 320 root.render(outer) 320 - t.assert.strictEqual(el.innerHTML, 'inner') 321 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 321 + assert.equal(el.innerHTML, 'inner') 322 + assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 322 323 sequence.length = 0 323 324 324 325 outer.show = false 325 326 root.render(outer) 326 - t.assert.strictEqual(el.innerHTML, '') 327 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner abort']) 327 + assert.equal(el.innerHTML, '') 328 + assert.deepStrictEqual(sequence, ['outer render', 'inner abort']) 328 329 sequence.length = 0 329 330 330 331 outer.show = true 331 332 root.render(outer) 332 - t.assert.strictEqual(el.innerHTML, 'inner') 333 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 333 + assert.equal(el.innerHTML, 'inner') 334 + assert.deepStrictEqual(sequence, ['outer render', 'inner render']) 334 335 sequence.length = 0 335 336 336 337 outer.show = false 337 338 root.render(outer) 338 - t.assert.strictEqual(el.innerHTML, '') 339 - t.assert.deepStrictEqual(sequence, ['outer render', 'inner abort']) 339 + assert.equal(el.innerHTML, '') 340 + assert.deepStrictEqual(sequence, ['outer render', 'inner abort']) 340 341 sequence.length = 0 341 342 }) 342 343 343 - test('onUnmount works externally', async (t: TestContext) => { 344 + test('onUnmount works externally', async () => { 344 345 const { root, el } = setup() 345 346 346 347 const app = { ··· 349 350 }, 350 351 } 351 352 352 - const unmounted = t.mock.fn() 353 + const unmounted = mock(() => {}) 353 354 onUnmount(app, unmounted) 354 355 355 356 root.render(app) 356 - t.assert.strictEqual(el.innerHTML, '<div>1</div><div>2</div><div>3</div>') 357 - t.assert.strictEqual(unmounted.mock.callCount(), 0) 357 + assert.equal(el.innerHTML, '<div>1</div><div>2</div><div>3</div>') 358 + assert.equal(unmounted.mock.calls.length, 0) 358 359 359 360 root.render(null) 360 - t.assert.strictEqual(unmounted.mock.callCount(), 1) 361 + assert.equal(unmounted.mock.calls.length, 1) 361 362 }) 362 363 363 - test('getParentNode works externally', (t: TestContext) => { 364 + test('getParentNode works externally', () => { 364 365 const { root, el } = setup() 365 366 366 367 const app = { ··· 370 371 } 371 372 372 373 root.render(app) 373 - t.assert.strictEqual(el.innerHTML, '<div></div>') 374 - t.assert.strictEqual(getParentNode(app), el) 374 + assert.equal(el.innerHTML, '<div></div>') 375 + assert.equal(getParentNode(app), el) 375 376 }) 376 377 377 - test('getParentNode works internally', (t: TestContext) => { 378 + test('getParentNode works internally', () => { 378 379 const { root, el } = setup() 379 380 380 381 root.render({ ··· 383 384 }, 384 385 } satisfies Renderable) 385 386 386 - t.assert.strictEqual(el.innerHTML, '<div>true</div>') 387 + assert.equal(el.innerHTML, '<div>true</div>') 387 388 }) 388 389 389 - test('getParentNode handles nesting', (t: TestContext) => { 390 + test('getParentNode handles nesting', () => { 390 391 const { root, el } = setup() 391 392 392 393 const inner = { 393 394 render() { 394 395 const parent = getParentNode(this) 395 396 396 - t.assert.ok(parent instanceof HTMLDivElement) 397 - t.assert.strictEqual((parent as HTMLDivElement).outerHTML, '<div class="the-app"><!----></div>') 398 - t.assert.strictEqual(parent.parentNode, el) 397 + assert(parent instanceof HTMLDivElement) 398 + assert.equal((parent as HTMLDivElement).outerHTML, '<div class="the-app"><!----></div>') 399 + assert.equal(parent.parentNode, el) 399 400 400 401 return null 401 402 }, 402 403 } 403 404 404 - const spy = t.mock.fn(inner.render) 405 + const spy = mock(inner.render) 405 406 inner.render = spy 406 407 407 408 root.render({ ··· 410 411 }, 411 412 }) 412 413 413 - t.assert.strictEqual(spy.mock.callCount(), 1) 414 + assert.equal(spy.mock.calls.length, 1) 414 415 })
+1 -2
src/client/tests/setup.ts
··· 1 1 import { GlobalRegistrator } from '@happy-dom/global-registrator' 2 + import { afterEach } from 'bun:test' 2 3 import { createRoot, type Root } from 'dhtml/client' 3 - import { afterEach } from 'node:test' 4 4 5 5 GlobalRegistrator.register() 6 - globalThis.__DEV__ = process.env.NODE_ENV !== 'production' 7 6 8 7 const roots: Root[] = [] 9 8
+24 -27
src/server/tests/basic.test.ts
··· 1 + import { test } from 'bun:test' 1 2 import { html } from 'dhtml' 2 3 import { renderToString } from 'dhtml/server' 3 - import test, { type TestContext } from 'node:test' 4 + import assert from 'node:assert/strict' 4 5 5 - globalThis.__DEV__ = process.env.NODE_ENV !== 'production' 6 + const dev_test = test.skipIf(!__DEV__) 6 7 7 - test('basic html renders correctly', (t: TestContext) => { 8 - t.assert.strictEqual(renderToString(html`<h1>Hello, world!</h1>`), '<h1>Hello, world!</h1>') 8 + test('basic html renders correctly', () => { 9 + assert.equal(renderToString(html`<h1>Hello, world!</h1>`), '<h1>Hello, world!</h1>') 9 10 }) 10 11 11 - test('inner content renders correctly', (t: TestContext) => { 12 - t.assert.strictEqual(renderToString(html`<h1>${html`Inner content!`}</h1>`), '<h1>Inner content!</h1>') 12 + test('inner content renders correctly', () => { 13 + assert.equal(renderToString(html`<h1>${html`Inner content!`}</h1>`), '<h1>Inner content!</h1>') 13 14 }) 14 15 15 - test('template with number renders correctly', (t: TestContext) => { 16 + test('template with number renders correctly', () => { 16 17 const template = (n: number) => html`<h1>Hello, ${n}!</h1>` 17 - t.assert.strictEqual(renderToString(template(1)), '<h1>Hello, 1!</h1>') 18 - t.assert.strictEqual(renderToString(template(2)), '<h1>Hello, 2!</h1>') 18 + assert.equal(renderToString(template(1)), '<h1>Hello, 1!</h1>') 19 + assert.equal(renderToString(template(2)), '<h1>Hello, 2!</h1>') 19 20 }) 20 21 21 - test('basic children render correctly', (t: TestContext) => { 22 - t.assert.strictEqual( 22 + test('basic children render correctly', () => { 23 + assert.equal( 23 24 renderToString(html`<span>${'This is a'}</span> ${html`test`} ${html`test`} ${html`test`}`), 24 25 '<span>This is a</span> test test test', 25 26 ) 26 27 }) 27 28 28 - test('errors are thrown cleanly', (t: TestContext) => { 29 + test('errors are thrown cleanly', () => { 29 30 const oops = new Error('oops') 30 31 let thrown 31 32 try { ··· 39 40 } catch (error) { 40 41 thrown = error 41 42 } 42 - t.assert.strictEqual(thrown, oops) 43 + assert.equal(thrown, oops) 43 44 }) 44 45 45 - test('invalid part placement raises error', { skip: process.env.NODE_ENV === 'production' }, (t: TestContext) => { 46 - t.assert.throws(() => renderToString(html`<${'div'}>${'text'}</${'div'}>`)) 46 + dev_test('invalid part placement raises error', () => { 47 + assert.throws(() => renderToString(html`<${'div'}>${'text'}</${'div'}>`)) 47 48 }) 48 49 49 - test('parts in comments do not throw', (t: TestContext) => { 50 + test('parts in comments do not throw', () => { 50 51 renderToString(html`<!-- ${'text'} -->`) 51 52 }) 52 53 53 - test( 54 - 'manually specifying internal template syntax throws', 55 - { skip: process.env.NODE_ENV === 'production' }, 56 - (t: TestContext) => { 57 - t.assert.throws(() => { 58 - renderToString(html`${1} dyn-$0$`) 59 - }) 60 - }, 61 - ) 54 + dev_test('manually specifying internal template syntax throws', () => { 55 + assert.throws(() => { 56 + renderToString(html`${1} dyn-$0$`) 57 + }) 58 + }) 62 59 63 - test('syntax close but not exact does not throw', (t: TestContext) => { 64 - t.assert.strictEqual(renderToString(html`dyn-$${0}1$`), 'dyn-$01$') 60 + test('syntax close but not exact does not throw', () => { 61 + assert.equal(renderToString(html`dyn-$${0}1$`), 'dyn-$01$') 65 62 })
+2 -1
tsconfig.json
··· 11 11 "allowImportingTsExtensions": true, 12 12 "stripInternal": true, 13 13 "isolatedDeclarations": true, 14 - "declaration": true 14 + "declaration": true, 15 + "types": ["bun-types"] 15 16 }, 16 17 "include": ["src"] 17 18 }