unit tests for remaining public methods

This commit is contained in:
Carson Gross
2025-11-02 11:17:16 -07:00
parent f889eb55c7
commit 057fbbc923
9 changed files with 348 additions and 90 deletions

View File

@@ -93,6 +93,7 @@
<!-- Unit tests first -->
<script src="tests/unit/__attributeValue.js"></script>
<script src="tests/unit/__collectFormData.js"></script>
<script src="tests/unit/__extractFilter.js"></script>
<script src="tests/unit/__findAllExt.js"></script>
<script src="tests/unit/__initializeTriggers.js"></script>
<script src="tests/unit/__makeFragment.js"></script>
@@ -105,10 +106,15 @@
<script src="tests/unit/ajax.js"></script>
<script src="tests/unit/core.js"></script>
<script src="tests/unit/find.js"></script>
<script src="tests/unit/forEvent.js"></script>
<script src="tests/unit/htmx.config.prefix.js"></script>
<script src="tests/unit/morph.js"></script>
<script src="tests/unit/on.js"></script>
<script src="tests/unit/parseInterval.js"></script>
<script src="tests/unit/process.js"></script>
<script src="tests/unit/swap.js"></script>
<script src="tests/unit/timeout.js"></script>
<script src="tests/unit/trigger.js"></script>
<!-- Fast attribute tests -->
<script src="tests/direct/hx-get.js"></script>

View File

@@ -0,0 +1,63 @@
describe('__extractFilter unit tests', function() {
it('returns event name and null when no filter', function () {
let [event, filter] = htmx.__extractFilter('click')
assert.equal(event, 'click')
assert.equal(filter, null)
})
it('extracts filter from brackets', function () {
let [event, filter] = htmx.__extractFilter('click[ctrlKey]')
assert.equal(event, 'click')
assert.equal(filter, 'ctrlKey')
})
it('handles empty brackets', function () {
let [event, filter] = htmx.__extractFilter('click[]')
assert.equal(event, 'click')
assert.equal(filter, '')
})
it('handles complex filter expression', function () {
let [event, filter] = htmx.__extractFilter('keydown[key==\'Enter\']')
assert.equal(event, 'keydown')
assert.equal(filter, 'key==\'Enter\'')
})
it('returns only first bracket match', function () {
let [event, filter] = htmx.__extractFilter('click[foo][bar]')
assert.equal(event, 'click')
assert.equal(filter, 'foo')
})
it('handles event name with spaces before bracket', function () {
let [event, filter] = htmx.__extractFilter('my event[filter]')
assert.equal(event, 'my event')
assert.equal(filter, 'filter')
})
it('handles filter with spaces', function () {
let [event, filter] = htmx.__extractFilter('click[a && b]')
assert.equal(event, 'click')
assert.equal(filter, 'a && b')
})
it('returns original string when only opening bracket', function () {
let [event, filter] = htmx.__extractFilter('click[')
assert.equal(event, 'click[')
assert.equal(filter, null)
})
it('returns original string when only closing bracket', function () {
let [event, filter] = htmx.__extractFilter('click]')
assert.equal(event, 'click]')
assert.equal(filter, null)
})
it('handles empty string', function () {
let [event, filter] = htmx.__extractFilter('')
assert.equal(event, '')
assert.equal(filter, null)
})
});

View File

@@ -241,13 +241,13 @@ describe('Unit Tests', function() {
'find',
'findAll',
'forEvent',
'on',
'parseInterval',
'process',
'swap',
'timeout',
'defineExtension',
'trigger',
'waitATick'
].sort();
const expectedPublicProperties = [

View File

@@ -1,89 +0,0 @@
describe('find functions', function() {
it('find() returns first matching element', function() {
createProcessedHTML('<div class="foo"></div><div class="foo"></div>');
const result = htmx.find('.foo');
assert.equal(result.className, 'foo');
});
it('findAll() returns all matching elements', function() {
createProcessedHTML('<div class="foo"></div><div class="foo"></div>');
const results = htmx.findAll('.foo');
assert.equal(results.length, 2);
});
it('_findExt() finds with closest selector', function() {
const child = createProcessedHTML('<div class="parent"><span class="child"></span></div>').querySelector('.child');
const result = htmx.__findExt(child, 'closest .parent');
assert.equal(result.className, 'parent');
});
it('_findExt() finds with next selector', function() {
createProcessedHTML('<div id="first"></div><div id="second"></div>');
const first = document.getElementById('first');
const result = htmx.__findExt(first, 'next');
assert.equal(result.id, 'second');
});
it('_findExt() finds with previous selector', function() {
createProcessedHTML('<div id="first"></div><div id="second"></div>');
const second = document.getElementById('second');
const result = htmx.__findExt(second, 'previous');
assert.equal(result.id, 'first');
});
it('_findExt() finds with hyperscript-style selector', function() {
const div = createProcessedHTML('<div><span class="target"></span></div>');
const result = htmx.__findExt(div, '<.target/>');
assert.equal(result.className, 'target');
});
it('__findAllExt() returns array with multiple selectors', function() {
createProcessedHTML('<div class="a"></div><div class="b"></div>');
const results = htmx.__findAllExt(document, '.a,.b');
assert.equal(results.length, 2);
});
it('__findAllExt() handles next with query', function() {
createProcessedHTML('<div id="start"></div><span></span><div class="target"></div>');
const start = document.getElementById('start');
const results = htmx.__findAllExt(start, 'next .target');
assert.equal(results[0].className, 'target');
});
it('__findAllExt() handles previous with query', function() {
createProcessedHTML('<div class="target"></div><span></span><div id="start"></div>');
const start = document.getElementById('start');
const results = htmx.__findAllExt(start, 'previous .target');
assert.equal(results[0].className, 'target');
});
it('__tokenizeExtendedSelector() splits by comma', function() {
const parts = htmx.__tokenizeExtendedSelector('.foo,.bar');
assert.equal(parts.length, 2);
assert.equal(parts[0], '.foo');
assert.equal(parts[1], '.bar');
});
it('__tokenizeExtendedSelector() respects angle brackets', function() {
const parts = htmx.__tokenizeExtendedSelector('<.foo,.bar/>,.baz');
assert.equal(parts.length, 2);
assert.equal(parts[0], '<.foo,.bar/>');
assert.equal(parts[1], '.baz');
});
it('__scanForwardQuery() finds next matching element', function() {
createProcessedHTML('<div id="start"></div><span></span><div class="target"></div>');
const start = document.getElementById('start');
const result = htmx.__scanForwardQuery(start, '.target');
assert.equal(result.className, 'target');
});
it('__scanBackwardsQuery() finds previous matching element', function() {
createProcessedHTML('<div class="target"></div><span></span><div id="start"></div>');
const start = document.getElementById('start');
const result = htmx.__scanBackwardsQuery(start, '.target');
assert.equal(result.className, 'target');
});
});

View File

@@ -0,0 +1,45 @@
describe('forEvent() unit tests', function() {
it('resolves when event fires', async function () {
let div = createProcessedHTML('<div></div>')
let promise = htmx.forEvent('custom', null, div)
setTimeout(() => div.dispatchEvent(new Event('custom')), 10)
let evt = await promise
assert.isNotNull(evt)
assert.equal(evt.type, 'custom')
})
it('resolves with null on timeout', async function () {
let div = createProcessedHTML('<div></div>')
let evt = await htmx.forEvent('custom', 50, div)
assert.isNull(evt)
})
it('defaults to document', async function () {
let promise = htmx.forEvent('custom:test', null)
setTimeout(() => document.dispatchEvent(new Event('custom:test')), 10)
let evt = await promise
assert.isNotNull(evt)
})
it('resolves before timeout if event fires', async function () {
let div = createProcessedHTML('<div></div>')
let promise = htmx.forEvent('custom', 1000, div)
setTimeout(() => div.dispatchEvent(new Event('custom')), 10)
let start = Date.now()
let evt = await promise
let elapsed = Date.now() - start
assert.isNotNull(evt)
assert.isBelow(elapsed, 500)
})
it('cleans up timeout when event fires', async function () {
let div = createProcessedHTML('<div></div>')
let promise = htmx.forEvent('custom', 1000, div)
setTimeout(() => div.dispatchEvent(new Event('custom')), 10)
await promise
// If timeout wasn't cleared, this test would hang
assert.isTrue(true)
})
});

40
test/tests/unit/on.js Normal file
View File

@@ -0,0 +1,40 @@
describe('on() unit tests', function() {
it('registers event listener on document by default', function () {
let called = false
htmx.on('custom:test', () => called = true)
document.dispatchEvent(new Event('custom:test'))
assert.isTrue(called)
})
it('registers event listener on specific element', function () {
let div = createProcessedHTML('<div></div>')
let called = false
htmx.on(div, 'custom', () => called = true)
div.dispatchEvent(new Event('custom'))
assert.isTrue(called)
})
it('returns the callback', function () {
let callback = () => {}
let returned = htmx.on('custom', callback)
assert.equal(returned, callback)
})
it('receives event object', function () {
let receivedEvent = null
htmx.on('custom:test2', (evt) => receivedEvent = evt)
document.dispatchEvent(new Event('custom:test2'))
assert.isNotNull(receivedEvent)
assert.equal(receivedEvent.type, 'custom:test2')
})
it('works with selector string for element', function () {
createProcessedHTML('<div id="target"></div>')
let called = false
htmx.on('#target', 'custom', () => called = true)
document.getElementById('target').dispatchEvent(new Event('custom'))
assert.isTrue(called)
})
});

View File

@@ -0,0 +1,93 @@
describe('process() unit tests', function() {
beforeEach(function() {
setupTest();
});
afterEach(function() {
cleanupTest();
});
it('initializes element with hx-get', function () {
let div = createHTML('<div hx-get="/test"></div>')
htmx.process(div)
assert.isTrue(div.hasAttribute('data-htmx-powered'))
})
it('initializes descendant elements', function () {
let container = createHTML('<div><button hx-get="/test"></button></div>')
htmx.process(container)
let button = container.querySelector('button')
assert.isTrue(button.hasAttribute('data-htmx-powered'))
})
it('initializes boosted elements', function () {
let a = createHTML('<a href="/test" hx-boost="true">Link</a>')
htmx.process(a)
assert.isTrue(a.hasAttribute('data-htmx-powered'))
})
it('processes scripts by default', function () {
let container = createHTML('<div><script>window.testProcessed = true</script></div>')
htmx.process(container)
assert.isTrue(window.testProcessed)
delete window.testProcessed
})
it('skips scripts when processScripts is false', function () {
let container = createHTML('<div><script>window.testSkipped = true</script></div>')
htmx.process(container, false)
assert.isUndefined(window.testSkipped)
})
it('processes hx-on attributes', function () {
let div = createHTML('<div hx-on:custom="this.setAttribute(\'fired\', \'true\')"></div>')
htmx.process(div)
div.dispatchEvent(new Event('custom'))
assert.equal(div.getAttribute('fired'), 'true')
})
it('ignores elements with hx-ignore', function () {
let container = createHTML('<div hx-ignore><button hx-get="/test"></button></div>')
htmx.process(container)
let button = container.querySelector('button')
assert.isFalse(button.hasAttribute('data-htmx-powered'))
})
it('ignores descendants of hx-ignore', function () {
let container = createHTML('<div><div hx-ignore><button hx-get="/test"></button></div></div>')
htmx.process(container)
let button = container.querySelector('button')
assert.isFalse(button.hasAttribute('data-htmx-powered'))
})
it('processes element itself if it matches', function () {
let div = createHTML('<div hx-get="/test"></div>')
htmx.process(div)
assert.isTrue(div.hasAttribute('data-htmx-powered'))
})
it('triggers htmx:before:process event', function () {
let div = createHTML('<div hx-get="/test"></div>')
let fired = false
div.addEventListener('htmx:before:process', () => fired = true)
htmx.process(div)
assert.isTrue(fired)
})
it('triggers htmx:after:process event', function () {
let div = createHTML('<div hx-get="/test"></div>')
let fired = false
div.addEventListener('htmx:after:process', () => fired = true)
htmx.process(div)
assert.isTrue(fired)
})
it('skips processing if htmx:before:process is cancelled', function () {
let div = createHTML('<div hx-get="/test"></div>')
div.addEventListener('htmx:before:process', (e) => e.preventDefault())
htmx.process(div)
assert.isFalse(div.hasAttribute('data-htmx-powered'))
})
});

View File

@@ -0,0 +1,34 @@
describe('timeout() unit tests', function() {
it('returns promise that resolves after milliseconds', async function () {
let start = Date.now()
await htmx.timeout(50)
let elapsed = Date.now() - start
assert.isAtLeast(elapsed, 45)
})
it('accepts string time format', async function () {
let start = Date.now()
await htmx.timeout('50ms')
let elapsed = Date.now() - start
assert.isAtLeast(elapsed, 45)
})
it('accepts seconds format', async function () {
let start = Date.now()
await htmx.timeout('0.05s')
let elapsed = Date.now() - start
assert.isAtLeast(elapsed, 45)
})
it('returns undefined for zero time', function () {
let result = htmx.timeout(0)
assert.isUndefined(result)
})
it('returns undefined for negative time', function () {
let result = htmx.timeout(-1)
assert.isUndefined(result)
})
});

View File

@@ -0,0 +1,66 @@
describe('trigger() unit tests', function() {
it('triggers event on element', function () {
let div = createProcessedHTML('<div></div>')
let called = false
div.addEventListener('custom', () => called = true)
htmx.trigger(div, 'custom')
assert.isTrue(called)
})
it('passes detail object', function () {
let div = createProcessedHTML('<div></div>')
let receivedDetail = null
div.addEventListener('custom', (e) => receivedDetail = e.detail)
htmx.trigger(div, 'custom', {foo: 'bar'})
assert.deepEqual(receivedDetail, {foo: 'bar'})
})
it('bubbles by default', function () {
let parent = createProcessedHTML('<div><span id="child"></span></div>')
let child = parent.querySelector('#child')
let calledOnParent = false
parent.addEventListener('custom', () => calledOnParent = true)
htmx.trigger(child, 'custom')
assert.isTrue(calledOnParent)
})
it('can disable bubbling', function () {
let parent = createProcessedHTML('<div><span id="child"></span></div>')
let child = parent.querySelector('#child')
let calledOnParent = false
parent.addEventListener('custom', () => calledOnParent = true)
htmx.trigger(child, 'custom', {}, false)
assert.isFalse(calledOnParent)
})
it('returns true when not cancelled', function () {
let div = createProcessedHTML('<div></div>')
let result = htmx.trigger(div, 'custom')
assert.isTrue(result)
})
it('returns false when event prevented', function () {
let div = createProcessedHTML('<div></div>')
div.addEventListener('custom', (e) => e.preventDefault())
let result = htmx.trigger(div, 'custom')
assert.isFalse(result)
})
it('works with selector string', function () {
createProcessedHTML('<div id="target"></div>')
let called = false
document.getElementById('target').addEventListener('custom', () => called = true)
htmx.trigger('#target', 'custom')
assert.isTrue(called)
})
it('triggers on document when element not connected', function () {
let div = document.createElement('div')
let calledOnDocument = false
document.addEventListener('custom:orphan', () => calledOnDocument = true)
htmx.trigger(div, 'custom:orphan')
assert.isTrue(calledOnDocument)
})
});