Files
htmx/test/tests/ext/hx-upsert.js
MichaelWest22 7dbb8acfa6 Upsert swap extension (#3595)
* add upsert swap extension

* improve upsert

* simplify upsert to not use morph

* add doco

* Add hx-upsert tag support as well

---------

Co-authored-by: MichaelWest22 <michael.west@docuvera.com>
2025-12-31 12:47:08 -07:00

232 lines
10 KiB
JavaScript

describe('hx-upsert extension', function() {
let extBackup;
before(async () => {
extBackup = backupExtensions();
clearExtensions();
let script = document.createElement('script');
script.src = '../src/ext/hx-upsert.js';
await new Promise(resolve => {
script.onload = resolve;
document.head.appendChild(script);
});
})
after(() => {
restoreExtensions(extBackup);
})
beforeEach(() => {
setupTest(this.currentTest)
})
afterEach(() => {
cleanupTest(this.currentTest)
})
it('updates existing element by id', async function () {
mockResponse('GET', '/test', '<div id="item-1">Updated</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert"><div id="item-1">Original</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.querySelector('#item-1').textContent, 'Updated')
})
it('inserts new element', async function () {
mockResponse('GET', '/test', '<div id="item-2">New</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert"><div id="item-1">Original</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children.length, 2)
assert.equal(div.querySelector('#item-2').textContent, 'New')
})
it('preserves existing elements not in response', async function () {
mockResponse('GET', '/test', '<div id="item-2">New</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert"><div id="item-1">Original</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children.length, 2)
assert.equal(div.querySelector('#item-1').textContent, 'Original')
})
it('prepends unmatched elements', async function () {
mockResponse('GET', '/test', '<div>No Key</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert prepend"><div id="item-1">Original</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children[0].textContent, 'No Key')
assert.equal(div.children[1].id, 'item-1')
})
it('appends unmatched elements by default', async function () {
mockResponse('GET', '/test', '<div>No Key</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert"><div id="item-1">Original</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children[0].id, 'item-1')
assert.equal(div.children[1].textContent, 'No Key')
})
it('updates multiple existing elements', async function () {
mockResponse('GET', '/test', '<div id="item-1">Updated 1</div><div id="item-2">Updated 2</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert"><div id="item-1">Original 1</div><div id="item-2">Original 2</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.querySelector('#item-1').textContent, 'Updated 1')
assert.equal(div.querySelector('#item-2').textContent, 'Updated 2')
})
it('handles mixed keyed and unkeyed elements', async function () {
mockResponse('GET', '/test', '<div id="item-2">Two</div><div>Unkeyed</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert"><div id="item-1">One</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children.length, 3)
assert.equal(div.children[0].id, 'item-1')
assert.equal(div.children[1].id, 'item-2')
assert.equal(div.children[2].textContent, 'Unkeyed')
})
it('sort with prepend puts unkeyed first', async function () {
mockResponse('GET', '/test', '<div id="item-2">Two</div><div>Unkeyed</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert sort prepend"><div id="item-1">One</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children[0].textContent, 'Unkeyed')
assert.equal(div.children[1].id, 'item-1')
assert.equal(div.children[2].id, 'item-2')
})
it('preserves element order when all matched', async function () {
mockResponse('GET', '/test', '<div id="item-2">Updated 2</div><div id="item-1">Updated 1</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert"><div id="item-1">Original 1</div><div id="item-2">Original 2</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children[0].id, 'item-1')
assert.equal(div.children[1].id, 'item-2')
assert.equal(div.children[0].textContent, 'Updated 1')
assert.equal(div.children[1].textContent, 'Updated 2')
})
it('handles empty response', async function () {
mockResponse('GET', '/test', '')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert"><div id="item-1">Original</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children.length, 1)
assert.equal(div.querySelector('#item-1').textContent, 'Original')
})
it('works with hx-swap-oob upsert', async function () {
mockResponse('GET', '/test', '<div id="main-item">Main</div><div id="other" hx-swap-oob="upsert"><div id="oob-1">OOB</div></div>')
let container = createProcessedHTML('<div><div hx-get="/test" hx-swap="innerHTML">Original</div><div id="other"><div id="oob-2">Existing</div></div></div>');
let div = container.children[0]
div.click()
await htmx.timeout(50)
let updatedOther = container.querySelector('#other')
let mainItem = div.querySelector('#main-item')
assert.isNotNull(mainItem)
assert.equal(mainItem.textContent, 'Main')
assert.equal(updatedOther.children.length, 2)
assert.equal(updatedOther.querySelector('#oob-1').textContent, 'OOB')
assert.equal(updatedOther.querySelector('#oob-2').textContent, 'Existing')
})
it('works with hx-partial', async function () {
mockResponse('GET', '/test', '<hx-partial hx-target="#list1" hx-swap="upsert"><div id="item-2">Two</div></hx-partial><hx-partial hx-target="#list2" hx-swap="upsert"><div id="item-b">B</div></hx-partial>')
let container = createProcessedHTML('<div hx-get="/test"><div id="list1"><div id="item-1">One</div></div><div id="list2"><div id="item-a">A</div></div></div>');
container.click()
await htmx.timeout(20)
let list1 = container.querySelector('#list1')
let list2 = container.querySelector('#list2')
assert.equal(list1.children.length, 2)
assert.equal(list1.querySelector('#item-1').textContent, 'One')
assert.equal(list1.querySelector('#item-2').textContent, 'Two')
assert.equal(list2.children.length, 2)
assert.equal(list2.querySelector('#item-a').textContent, 'A')
assert.equal(list2.querySelector('#item-b').textContent, 'B')
})
it('sorts descending with sort:desc', async function () {
mockResponse('GET', '/test', '<div id="item-2">Two</div>')
let div = createProcessedHTML('<div hx-get="/test" hx-swap="upsert sort:desc"><div id="item-3">Three</div><div id="item-1">One</div></div>');
div.click()
await htmx.timeout(20)
assert.equal(div.children[0].id, 'item-3')
assert.equal(div.children[1].id, 'item-2')
assert.equal(div.children[2].id, 'item-1')
})
it('hx-upsert tag with basic upsert', async function () {
mockResponse('GET', '/test', '<hx-upsert hx-target="#list"><div id="item-2">Two</div></hx-upsert>')
let container = createProcessedHTML('<div hx-get="/test"><div id="list"><div id="item-1">One</div></div></div>');
container.click()
await htmx.timeout(20)
let list = container.querySelector('#list')
assert.equal(list.children.length, 2)
assert.equal(list.querySelector('#item-1').textContent, 'One')
assert.equal(list.querySelector('#item-2').textContent, 'Two')
})
it('hx-upsert tag with sort attribute', async function () {
mockResponse('GET', '/test', '<hx-upsert hx-target="#list" sort><div id="item-2">Two</div></hx-upsert>')
let container = createProcessedHTML('<div hx-get="/test"><div id="list"><div id="item-1">One</div><div id="item-3">Three</div></div></div>');
container.click()
await htmx.timeout(20)
let list = container.querySelector('#list')
assert.equal(list.children[0].id, 'item-1')
assert.equal(list.children[1].id, 'item-2')
assert.equal(list.children[2].id, 'item-3')
})
it('hx-upsert tag with sort="desc"', async function () {
mockResponse('GET', '/test', '<hx-upsert hx-target="#list" sort="desc"><div id="item-2">Two</div></hx-upsert>')
let container = createProcessedHTML('<div hx-get="/test"><div id="list"><div id="item-3">Three</div><div id="item-1">One</div></div></div>');
container.click()
await htmx.timeout(20)
let list = container.querySelector('#list')
assert.equal(list.children[0].id, 'item-3')
assert.equal(list.children[1].id, 'item-2')
assert.equal(list.children[2].id, 'item-1')
})
it('hx-upsert tag with key attribute', async function () {
mockResponse('GET', '/test', '<hx-upsert hx-target="#list" key="data-priority" sort><div id="task-2" data-priority="2">Medium</div></hx-upsert>')
let container = createProcessedHTML('<div hx-get="/test"><div id="list"><div id="task-3" data-priority="1">High</div><div id="task-1" data-priority="3">Low</div></div></div>');
container.click()
await htmx.timeout(20)
let list = container.querySelector('#list')
assert.equal(list.children[0].getAttribute('data-priority'), '1')
assert.equal(list.children[1].getAttribute('data-priority'), '2')
assert.equal(list.children[2].getAttribute('data-priority'), '3')
})
it('hx-upsert tag with prepend attribute', async function () {
mockResponse('GET', '/test', '<hx-upsert hx-target="#list" prepend><div>No Key</div></hx-upsert>')
let container = createProcessedHTML('<div hx-get="/test"><div id="list"><div id="item-1">One</div></div></div>');
container.click()
await htmx.timeout(20)
let list = container.querySelector('#list')
assert.equal(list.children[0].textContent, 'No Key')
assert.equal(list.children[1].id, 'item-1')
})
})