package.test.ext.sse.js Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of htmx-ext-sse Show documentation
Show all versions of htmx-ext-sse Show documentation
The `Server Sent Events` extension connects to an [EventSource](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) directly from HTML. It manages the connections to your web server, listens for server events, an
The newest version!
describe('sse extension', function() {
function mockEventSource() {
var listeners = {}
var mockEventSource = {
_listeners: listeners,
removeEventListener: function(name, l) {
listeners[name] = listeners[name].filter(function(elt, idx, arr) {
if (arr[idx] === l) {
return false
}
return true
})
},
addEventListener: function(message, l) {
if (!listeners[message]) {
listeners[message] = []
}
listeners[message].push(l)
},
sendEvent: function(eventName, data) {
this.readyState.should.equal(EventSource.OPEN)
var eventListeners = listeners[eventName]
if (eventListeners) {
eventListeners.forEach(function(listener) {
var event = htmx._('makeEvent')(eventName)
event.data = data
listener(event)
})
}
},
close: function() {
this.readyState = EventSource.CLOSED
},
connect: function(url) {
this.url = url
setTimeout(function() {
if (mockEventSource.failConnections) {
mockEventSource.readyState = EventSource.CLOSED
if (typeof mockEventSource.onerror === 'function') {
mockEventSource.onerror('Simulated EventSource connection failure')
}
} else {
mockEventSource.readyState = EventSource.OPEN
if (typeof mockEventSource.onopen === 'function') {
mockEventSource.onopen()
}
}
}, 0)
},
simulateConnectionError: function() {
this.close()
if (typeof this.onerror === "function") {
this.onerror()
}
},
/** @type {EventSource.CONNECTING|EventSource.OPEN|EventSource.CLOSED|0|1|2} */
readyState: EventSource.CONNECTING,
failConnections: false,
}
return mockEventSource
}
beforeEach(function() {
this.server = makeServer()
this.closeType = ""
this.clock = sinon.useFakeTimers();
var test = this
clearWorkArea()
htmx.createEventSource = function(url) {
var eventSource = mockEventSource()
test.eventSource = eventSource
eventSource.connect(url)
return eventSource
}
})
afterEach(function() {
this.server.restore()
this.clock.restore();
clearWorkArea()
})
it('correctly subscribes to events', function() {
make('' +
'' +
'div1' +
'' +
'')
this.clock.tick(1)
this.eventSource.url.should.be.equal('/foo');
this.eventSource._listeners.e1.should.be.lengthOf(1)
})
it('correctly behaves when ignored', function() {
make('' +
'' +
'div1' +
'' +
'');
this.clock.tick(1)
this.eventSource.url.should.be.equal('/foo');
(this.eventSource._listeners.e1 == undefined).should.be.true
})
it('handles basic sse triggering', function() {
this.server.respondWith('GET', '/d1', 'div1 updated')
this.server.respondWith('GET', '/d2', 'div2 updated')
var div = make('' +
'div1' +
'div2' +
'')
this.clock.tick(1)
this.eventSource.sendEvent('e1')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2')
this.eventSource.sendEvent('e2')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2 updated')
})
it('supports hx-trigger\'s multiple triggers syntax', function() {
this.server.respondWith('GET', '/d1', 'div1 updated')
this.server.respondWith('GET', '/d2', 'div2 updated')
this.server.respondWith('GET', '/d3', 'div3 updated')
var div = make('' +
'div1' +
'div2' +
'div3' +
'')
this.clock.tick(1)
this.eventSource.sendEvent('e1')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2')
byId('d3').innerHTML.should.equal('div3')
this.eventSource.sendEvent('e2')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2 updated')
byId('d3').innerHTML.should.equal('div3')
this.eventSource.sendEvent('e3')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2 updated')
byId('d3').innerHTML.should.equal('div3 updated')
})
it('does not trigger events that arent named', function() {
this.server.respondWith('GET', '/d1', 'div1 updated')
var div = make('' +
'div1' +
'')
this.clock.tick(1)
this.eventSource.sendEvent('foo')
this.server.respond()
byId('d1').innerHTML.should.equal('div1')
this.eventSource.sendEvent('e2')
this.server.respond()
byId('d1').innerHTML.should.equal('div1')
this.eventSource.sendEvent('e1')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
})
it('does not trigger events not on descendents', function() {
this.server.respondWith('GET', '/d1', 'div1 updated')
var div = make('' +
'div1')
this.clock.tick(1)
this.eventSource.sendEvent('foo')
this.server.respond()
byId('d1').innerHTML.should.equal('div1')
this.eventSource.sendEvent('e2')
this.server.respond()
byId('d1').innerHTML.should.equal('div1')
this.eventSource.sendEvent('e1')
this.server.respond()
byId('d1').innerHTML.should.equal('div1')
})
it('is closed after removal, hx-trigger', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var div = make('' +
'div1' +
'')
htmx.on(div, "htmx:sseClose", (evt) => {
this.closeType = evt.detail.type
})
this.clock.tick(1)
div.click()
this.server.respond()
this.closeType.should.equal("nodeReplaced")
this.eventSource.readyState.should.equal(EventSource.CLOSED)
})
it('is closed after removal, hx-swap', function() {
this.server.respondWith('GET', '/test', 'Clicked!')
var div = make('' +
'div1' +
'')
htmx.on(div, "htmx:sseClose", (evt) => {
this.closeType = evt.detail.type
})
this.clock.tick(1)
div.click()
this.server.respond()
this.closeType.should.equal("nodeReplaced")
this.eventSource.readyState.should.equal(EventSource.CLOSED)
})
it('is closed after removal with no close and activity, hx-trigger', function() {
var div = make('' +
'div1' +
'')
this.clock.tick(1)
div.parentElement.removeChild(div)
htmx.on(div, "htmx:sseClose", (evt) => {
this.closeType = evt.detail.type
})
this.eventSource.sendEvent('e1')
this.closeType.should.equal("nodeMissing")
this.eventSource.readyState.should.equal(EventSource.CLOSED)
})
it('is closed after close message from server', function() {
var div = make('' +
'' +
'');
htmx.on(div, "htmx:sseClose", (evt) => {
this.closeType = evt.detail.type
})
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.OPEN);
this.eventSource.sendEvent("close");
this.closeType.should.equal("message")
this.eventSource.readyState.should.equal(EventSource.CLOSED);
})
it('is closed after close message from server in nested content', function() {
var div = make('' +
'' +
'');
htmx.on(div, "htmx:sseClose", (evt) => {
this.closeType = evt.detail.type
})
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.OPEN);
this.eventSource.sendEvent("close");
this.closeType.should.equal("message")
this.eventSource.readyState.should.equal(EventSource.CLOSED);
})
it('is closed after close message from server, non-nested', function(){
var div = make(``
)
htmx.on(div, "htmx:sseClose", (evt) => {
this.closeType = evt.detail.type;
});
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.OPEN);
this.eventSource.sendEvent('close', '')
this.closeType.should.equal('message')
this.eventSource.readyState.should.equal(EventSource.CLOSED);
})
it('is not listening for events after hx-swap element removed', function() {
var div = make('' +
'div1' +
'div1' +
'')
this.clock.tick(1)
this.eventSource._listeners.e1.should.be.lengthOf(1)
this.eventSource._listeners.e2.should.be.lengthOf(2)
div.removeChild(byId('d1'))
this.eventSource.sendEvent('e1', 'Test')
this.eventSource.sendEvent('e2', 'Test')
this.eventSource._listeners.e1.should.be.empty
this.eventSource._listeners.e2.should.be.lengthOf(1)
div.removeChild(byId('d2'))
this.eventSource.sendEvent('e1', 'Test')
this.eventSource.sendEvent('e2', 'Test')
this.eventSource._listeners.e1.should.be.empty
this.eventSource._listeners.e2.should.be.empty
})
it('is not listening for events after hx-trigger element removed', function() {
this.server.respondWith('GET', '/test', function(xhr) {
xhr.respond(200, {})
})
var div = make('' +
'div1' +
'div1' +
'')
this.clock.tick(1)
this.eventSource._listeners.e1.should.be.lengthOf(1)
this.eventSource._listeners.e2.should.be.lengthOf(2)
div.removeChild(byId('d1'))
this.eventSource.sendEvent('e1', 'Test')
this.eventSource.sendEvent('e2', 'Test')
this.eventSource._listeners.e1.should.be.empty
this.eventSource._listeners.e2.should.be.lengthOf(1)
div.removeChild(byId('d2'))
this.eventSource.sendEvent('e1', 'Test')
this.eventSource.sendEvent('e2', 'Test')
this.eventSource._listeners.e1.should.be.empty
this.eventSource._listeners.e2.should.be.empty
})
// sse and hx-trigger handlers are distinct
it('is closed after removal with no close and activity, sse-swap', function() {
var div = make('' +
'div1' +
'')
this.clock.tick(1)
div.parentElement.removeChild(div)
this.eventSource.sendEvent('e1')
this.eventSource.readyState.should.equal(EventSource.CLOSED)
})
it('swaps content properly on SSE swap', function() {
var div = make('\n' +
' \n' +
' \n' +
'\n')
this.clock.tick(1)
byId('d1').innerText.should.equal('')
byId('d2').innerText.should.equal('')
this.eventSource.sendEvent('e1', 'Event 1')
byId('d1').innerText.should.equal('Event 1')
byId('d2').innerText.should.equal('')
this.eventSource.sendEvent('e2', 'Event 2')
byId('d1').innerText.should.equal('Event 1')
byId('d2').innerText.should.equal('Event 2')
})
it('swaps swapped in content', function() {
var div = make('\n' +
'\n' +
'\n'
)
this.clock.tick(1)
this.eventSource.sendEvent('e1', '')
this.eventSource.sendEvent('e2', 'Event 2')
byId('d2').innerText.should.equal('Event 2')
})
it('works in a child of an hx-ext="sse" element', function() {
var div = make('\n' +
'div1\n' +
'\n'
)
this.clock.tick(1)
this.eventSource.sendEvent('e1', 'Event 1')
byId('d1').innerText.should.equal('Event 1')
})
it('only adds sseEventSource to elements with sse-connect', function() {
var div = make('\n' +
'\n' +
'');
this.clock.tick(1);
(byId('d1')['htmx-internal-data'].sseEventSource == undefined).should.be.true
// Even when content is swapped in
this.eventSource.sendEvent('e1', '');
(byId('d2')['htmx-internal-data'].sseEventSource == undefined).should.be.true
})
it('triggers events with naked hx-trigger', function() {
var div = make( 'div2')
this.clock.tick(1)
let triggerCounter = 0
div.addEventListener("htmx:trigger", () => triggerCounter++)
let sseMessageCounter = 0
div.addEventListener("htmx:sseMessage", () => sseMessageCounter++)
this.eventSource.sendEvent('e2')
triggerCounter.should.be.equal(1)
sseMessageCounter.should.be.equal(1)
})
it('initializes connections in swapped content', function() {
this.server.respondWith('GET', '/d1', 'div2')
this.server.respondWith('GET', '/d2', 'div2 updated')
var div = make('')
this.clock.tick(1)
div.click()
this.server.respond()
this.clock.tick(1)
this.eventSource.sendEvent('e2')
this.server.respond()
byId('d2').innerHTML.should.equal('div2 updated')
})
it('creates an eventsource on elements with sse-connect', function() {
var div = make('');
this.clock.tick(1);
(byId('d1')['htmx-internal-data'].sseEventSource == undefined).should.be.false
})
it('raises htmx:sseBeforeMessage when receiving message from the server', function() {
var myEventCalled = false
function handle(evt) {
myEventCalled = true
}
htmx.on('htmx:sseBeforeMessage', handle)
var div = make('')
this.clock.tick(1)
this.eventSource.sendEvent('e1', '')
myEventCalled.should.be.true
htmx.off('htmx:sseBeforeMessage', handle)
})
it('cancels swap when htmx:sseBeforeMessage was cancelled', function() {
var myEventCalled = false
function handle(evt) {
myEventCalled = true
evt.preventDefault()
}
htmx.on('htmx:sseBeforeMessage', handle)
var div = make('div1')
this.clock.tick(1)
this.eventSource.sendEvent('e1', 'replaced')
myEventCalled.should.be.true
byId('d1').innerHTML.should.equal('div1')
htmx.off('htmx:sseBeforeMessage', handle)
})
it('raises htmx:sseMessage when message was completely processed', function() {
var myEventCalled = false
function handle(evt) {
myEventCalled = true
}
htmx.on('htmx:sseMessage', handle)
var div = make('div1')
this.clock.tick(1)
this.eventSource.sendEvent('e1', 'replaced')
myEventCalled.should.be.true
byId('d1').innerHTML.should.equal('replaced')
htmx.off('htmx:sseMessage', handle)
})
it('handles sse reconnection', function() {
this.server.respondWith('GET', '/d1', 'div1 updated')
this.server.respondWith('GET', '/d2', 'div2 updated')
var div = make('' +
'div1' +
'div2' +
'')
this.clock.tick(1)
this.eventSource.sendEvent('e1')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2')
var oldEventSource = this.eventSource
this.eventSource.should.equal(oldEventSource)
var sseErrorCalled = false
div.addEventListener("htmx:sseError", function(){
sseErrorCalled = true
})
this.eventSource.simulateConnectionError()
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(500)
sseErrorCalled.should.equal(true)
this.eventSource.should.not.equal(oldEventSource)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.OPEN)
this.eventSource.sendEvent('e2')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2 updated')
})
it('reconnection retry timeout properly increases over attempts', function() {
this.server.respondWith('GET', '/d1', 'div1 updated')
this.server.respondWith('GET', '/d2', 'div2 updated')
var div = make('' +
'div1' +
'div2' +
'')
this.clock.tick(1)
this.eventSource.sendEvent('e1')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2')
var oldEventSource = this.eventSource
this.eventSource.should.equal(oldEventSource)
this.eventSource.simulateConnectionError()
this.clock.tick(300)
this.eventSource.should.equal(oldEventSource)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(200)
this.eventSource.should.not.equal(oldEventSource)
this.eventSource.failConnections = true
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(800)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(200)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(1950)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(50)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(3950)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(50)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(7999)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(15999)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(31999)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(63999)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
// Doesn't go higher than 64s
this.clock.tick(63999)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(63999)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.OPEN)
this.eventSource.sendEvent('e2')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2 updated')
})
it('reconnection retry timeout properly resets on successful connection', function() {
this.server.respondWith('GET', '/d1', 'div1 updated')
this.server.respondWith('GET', '/d2', 'div2 updated')
var div = make('' +
'div1' +
'div2' +
'')
this.clock.tick(1)
this.eventSource.sendEvent('e1')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2')
var oldEventSource = this.eventSource
this.eventSource.should.equal(oldEventSource)
this.eventSource.simulateConnectionError()
// Reconnection delays starts at 500ms
this.clock.tick(300)
this.eventSource.should.equal(oldEventSource)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(200)
this.eventSource.should.not.equal(oldEventSource)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.eventSource.failConnections = true
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
oldEventSource = this.eventSource
// Delay increases to 1s on the second attempt
this.clock.tick(800)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.eventSource.should.equal(oldEventSource)
this.clock.tick(200)
this.eventSource.should.not.equal(oldEventSource)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.OPEN)
oldEventSource = this.eventSource
// Delay resets to minimum (500ms) after a successful connection
this.eventSource.simulateConnectionError()
this.clock.tick(499)
this.eventSource.should.equal(oldEventSource)
this.eventSource.readyState.should.equal(EventSource.CLOSED)
this.clock.tick(1)
this.eventSource.should.not.equal(oldEventSource)
this.eventSource.readyState.should.equal(EventSource.CONNECTING)
this.clock.tick(1)
this.eventSource.readyState.should.equal(EventSource.OPEN)
this.eventSource.sendEvent('e2')
this.server.respond()
byId('d1').innerHTML.should.equal('div1 updated')
byId('d2').innerHTML.should.equal('div2 updated')
})
})