describe("web-sockets extension", function () { // mock-socket isn't IE11 compatible, thus this handmade one // Using the same syntax as the library, so the initial tests didn't require changes to work // TODO when we get rid of IE11 for htmx2, replace this by mock-socket since it ofc doesn't implement every feature function mockWebsocket() { var mockSocketClient = { addEventListener: function (event, handler) { var handlers = this._listeners[event] || [] handlers.push(handler) this._listeners[event] = handlers }, on: function (event, handler) { this.addEventListener(event, handler) }, send: function (data) { mockSocketServer._fireEvent("message", data) }, connect: function () { this._open = true mockSocketServer._fireEvent("connection", mockSocketServer) setTimeout(function () { this._fireEvent("open", {type: "open"}) }.bind(this), 2) }, close: function () { if (this._open) { this._open = false this._fireEvent("close", {type: "close", code: 0}) } }, _listeners: {}, _fireEvent: function (event, data) { var handlers = this._listeners[event] || [] if (typeof this["on" + event] === "function") { handlers.push(this["on" + event]) } for (var i = 0; i < handlers.length; i++) { handlers[i](data) } }, _open: false, } var mockSocketServer = { addEventListener: function (event, handler) { var handlers = this._listeners[event] || [] handlers.push(handler) this._listeners[event] = handlers }, on: function (event, handler) { this.addEventListener(event, handler) }, close: function () { mockSocketClient.close() }, stop: function () { }, emit: function (event, data) { mockSocketClient._fireEvent(event, {data: data}) }, clients: function () { // Replicate old mock-socket syntax to avoid huge file diff to merge return mockSocketClient._open ? [1] : [] }, _listeners: {}, _fireEvent: function (event, data) { var handlers = this._listeners[event] || [] for (var i = 0; i < handlers.length; i++) { handlers[i](data) } }, } return { client: mockSocketClient, server: mockSocketServer, } } beforeEach(function () { this.server = makeServer(); // this.socketServer = new Mock.Server('ws://localhost:8080'); var mockedSocket = mockWebsocket(); this.socketServer = mockedSocket.server; this.messages = []; this.clock = sinon.useFakeTimers(); this.socketServer.on('connection', function (socket) { socket.on('message', function (event) { this.messages.push(event) }.bind(this)) }.bind(this)) /* Mock socket library is cool, but it uses setTimeout to emulate asynchronous nature of the network. * To avoid unexpected behavior, make sure to call this method whenever socket would have a network communication, * e.g., when connecting, disconnecting, sending messages. */ this.tickMock = function () { this.clock.tick(5); } clearWorkArea(); this.oldCreateWebSocket = htmx.createWebSocket; htmx.createWebSocket = function () { mockedSocket.client.connect() return mockedSocket.client }; }); afterEach(function () { this.server.restore(); clearWorkArea(); this.socketServer.close(); this.socketServer.stop(); this.clock.restore(); htmx.createWebSocket = this.oldCreateWebSocket; }); it('can establish connection with the server', function () { this.socketServer.clients().length.should.equal(0); make('
'); this.socketServer.clients().length.should.equal(1); this.tickMock(); }) it('is closed after removal by swap', function () { this.server.respondWith("GET", "/test", "Clicked!"); var div = make('
'); this.tickMock(); this.socketServer.clients().length.should.equal(1); div.click(); this.server.respond(); this.tickMock(); this.socketServer.clients().length.should.equal(0); }) it('is closed after removal by js when message is received', function () { this.server.respondWith("GET", "/test", "Clicked!"); var div = make('
'); this.tickMock(); this.socketServer.clients().length.should.equal(1); div.parentElement.removeChild(div); this.socketServer.emit('message', 'foo'); this.tickMock(); this.socketServer.clients().length.should.equal(0); }) it('sends data to the server', function () { var div = make('
div1
'); this.tickMock(); byId("d1").click(); this.tickMock(); this.messages.length.should.equal(1); }) it('sends data to the server with specific trigger', function () { var div = make('
div1
'); this.tickMock(); byId("d1").click(); this.tickMock(); this.messages.length.should.equal(1); }) it('sends data to the server with polling trigger', function () { var div = make('
div1
'); this.tickMock(); this.clock.tick(2000); byId("d1").click(); this.tickMock(); this.messages.length.should.equal(2); }) it('sends expected headers to the server', function () { var div = make('
'); this.tickMock(); byId("d1").click(); this.tickMock(); this.messages.length.should.equal(1); var message = JSON.parse(this.messages[0]); var headers = message.HEADERS; console.log(headers); headers['HX-Request'].should.be.equal('true'); headers['HX-Current-URL'].should.be.equal(document.location.href) headers['HX-Trigger'].should.be.equal('d1'); headers['HX-Trigger-Name'].should.be.equal('d1-name'); headers['HX-Target'].should.be.equal('target'); }) it('handles message from the server', function () { var div = make('
div1
div2
'); this.tickMock(); this.socketServer.emit('message', "
replaced
"); this.tickMock(); byId("d1").innerHTML.should.equal("replaced"); byId("d2").innerHTML.should.equal("div2"); }) it('raises lifecycle events (connecting, open, close) in correct order', function () { var handledEventTypes = []; var handler = function (evt) { handledEventTypes.push(evt.detail.event.type) }; htmx.on("htmx:wsConnecting", handler); var div = make('
'); htmx.on(div, "htmx:wsOpen", handler); htmx.on(div, "htmx:wsClose", handler); this.tickMock(); div.parentElement.removeChild(div); this.socketServer.emit('message', 'foo'); this.tickMock(); handledEventTypes.should.eql(['connecting', 'open', 'close']); this.tickMock(); htmx.off("htmx:wsConnecting", handler); htmx.off(div, "htmx:wsOpen", handler); htmx.off(div, "htmx:wsClose", handler); }) it('raises htmx:wsConfigSend when sending, allows message modification', function () { var myEventCalled = false; function handle(evt) { myEventCalled = true; evt.detail.parameters.foo = "bar"; } htmx.on("htmx:wsConfigSend", handle) var div = make('
div1
'); this.tickMock(); byId("d1").click(); this.tickMock(); myEventCalled.should.be.true; this.messages.length.should.equal(1); this.messages[0].should.contains('"foo":"bar"') htmx.off("htmx:wsConfigSend", handle) }) it('passes socketWrapper to htmx:wsConfigSend', function () { var socketWrapper = null; function handle(evt) { evt.preventDefault(); socketWrapper = evt.detail.socketWrapper; socketWrapper.send(JSON.stringify({foo: 'bar'}), evt.detail.elt) } htmx.on("htmx:wsConfigSend", handle) var div = make('
div1
'); this.tickMock(); byId("d1").click(); this.tickMock(); socketWrapper.should.not.be.null; socketWrapper.send.should.be.a('function'); socketWrapper.sendImmediately.should.be.a('function'); socketWrapper.queue.should.be.an('array'); this.messages.length.should.equal(1); this.messages[0].should.contains('"foo":"bar"') htmx.off("htmx:wsConfigSend", handle); }) it('cancels sending when htmx:wsConfigSend is cancelled', function () { var myEventCalled = false; function handle(evt) { myEventCalled = true; evt.preventDefault(); } htmx.on("htmx:wsConfigSend", handle) var div = make('
div1
'); this.tickMock(); byId("d1").click(); this.messages.length.should.equal(0); myEventCalled.should.be.true; htmx.off("htmx:wsConfigSend", handle); }) it('raises htmx:wsBeforeSend when sending', function () { var myEventCalled = false; function handle(evt) { myEventCalled = true; } htmx.on("htmx:wsBeforeSend", handle) var div = make('
div1
'); this.tickMock(); byId("d1").click(); this.tickMock(); myEventCalled.should.be.true; this.messages.length.should.equal(1); htmx.off("htmx:wsBeforeSend", handle) }) it('cancels sending when htmx:wsBeforeSend is cancelled', function () { var myEventCalled = false; function handle(evt) { myEventCalled = true; evt.preventDefault(); } htmx.on("htmx:wsBeforeSend", handle) var div = make('
div1
'); this.tickMock(); byId("d1").click(); this.tickMock(); myEventCalled.should.be.true; this.messages.length.should.equal(0); htmx.off("htmx:wsBeforeSend", handle) }) it('raises htmx:wsAfterSend when sending', function () { var myEventCalled = false; function handle(evt) { myEventCalled = true; } htmx.on("htmx:wsAfterSend", handle) var div = make('
div1
'); this.tickMock(); byId("d1").click(); this.tickMock(); myEventCalled.should.be.true; this.messages.length.should.equal(1); htmx.off("htmx:wsAfterSend", handle) }) it('raises htmx:wsBeforeMessage when receiving message from the server', function () { var myEventCalled = false; function handle(evt) { myEventCalled = true; } htmx.on("htmx:wsBeforeMessage", handle) var div = make('
div1
div2
'); this.tickMock(); this.socketServer.emit('message', "
replaced
") this.tickMock(); myEventCalled.should.be.true; htmx.off("htmx:wsBeforeMessage", handle) }) it('cancels swap when htmx:wsBeforeMessage was cancelled', function () { var myEventCalled = false; function handle(evt) { myEventCalled = true; evt.preventDefault(); } htmx.on("htmx:wsBeforeMessage", handle) var div = make('
div1
div2
'); this.tickMock(); this.socketServer.emit('message', "
replaced
") this.tickMock(); myEventCalled.should.be.true; byId("d1").innerHTML.should.equal("div1"); byId("d2").innerHTML.should.equal("div2"); htmx.off("htmx:wsBeforeMessage", handle) }) it('raises htmx:wsAfterMessage when message was completely processed', function () { var myEventCalled = false; function handle(evt) { myEventCalled = true; } htmx.on("htmx:wsAfterMessage", handle) var div = make('
div1
div2
'); this.tickMock(); this.socketServer.emit('message', "
replaced
") this.tickMock(); myEventCalled.should.be.true; htmx.off("htmx:wsAfterMessage", handle) }) it('sends data to the server with non-htmx form + submit button & value', function () { make('
' + '' + '' + '' + '
'); this.tickMock(); byId("b1").click(); this.tickMock(); this.messages.length.should.equal(1); this.messages[0].should.contains('"foo":"bar"') this.messages[0].should.contains('"action":"A"') byId("b2").click(); this.tickMock(); this.messages.length.should.equal(2); this.messages[1].should.contains('"foo":"bar"') this.messages[1].should.contains('"action":"B"') }) it('sends data to the server with non-htmx form + submit input & value', function () { make('
' + '' + '' + '' + '
'); this.tickMock(); byId("b1").click(); this.tickMock(); this.messages.length.should.equal(1); this.messages[0].should.contains('"foo":"bar"') this.messages[0].should.contains('"action":"A"') byId("b2").click(); this.tickMock(); this.messages.length.should.equal(2); this.messages[1].should.contains('"foo":"bar"') this.messages[1].should.contains('"action":"B"') }) it('sends data to the server with child non-htmx form + submit button & value', function () { make('
' + '
' + '' + '' + '' + '
' + '
'); this.tickMock(); byId("b1").click(); this.tickMock(); this.messages.length.should.equal(1); this.messages[0].should.contains('"foo":"bar"') this.messages[0].should.contains('"action":"A"') byId("b2").click(); this.tickMock(); this.messages.length.should.equal(2); this.messages[1].should.contains('"foo":"bar"') this.messages[1].should.contains('"action":"B"') }) it('sends data to the server with child non-htmx form + submit input & value', function () { make('
' + '
' + '' + '' + '' + '
' + '
'); this.tickMock(); byId("b1").click(); this.tickMock(); this.messages.length.should.equal(1); this.messages[0].should.contains('"foo":"bar"') this.messages[0].should.contains('"action":"A"') byId("b2").click(); this.tickMock(); this.messages.length.should.equal(2); this.messages[1].should.contains('"foo":"bar"') this.messages[1].should.contains('"action":"B"') }) it('sends data to the server with external non-htmx form + submit button & value', function () { make('
' + '
' + '' + '
' + '
' + '' + ''); this.tickMock(); byId("b1").click(); this.tickMock(); this.messages.length.should.equal(1); this.messages[0].should.contains('"foo":"bar"') this.messages[0].should.contains('"action":"A"') byId("b2").click(); this.tickMock(); this.messages.length.should.equal(2); this.messages[1].should.contains('"foo":"bar"') this.messages[1].should.contains('"action":"B"') }) it('sends data to the server with external non-htmx form + submit input & value', function () { make('
' + '
' + '' + '
' + '
' + '' + ''); this.tickMock(); byId("b1").click(); this.tickMock(); this.messages.length.should.equal(1); this.messages[0].should.contains('"foo":"bar"') this.messages[0].should.contains('"action":"A"') byId("b2").click(); this.tickMock(); this.messages.length.should.equal(2); this.messages[1].should.contains('"foo":"bar"') this.messages[1].should.contains('"action":"B"') }) describe("Send immediately", function() { function checkCallForWsBeforeSend(spy, wrapper, message, target) { // Utility function to always check the same for htmx:wsBeforeSend caught by a spy spy.calledOnce.should.be.true; var call = spy.getCall(0); call.args.length.should.equal(1); var arg = call.args[0]; arg.target.should.equal(target); arg.detail.socketWrapper.should.equal(wrapper); arg.detail.message.should.equal(message); } it('triggers wsBeforeSend on body if provided to sendImmediately', function (done) { var myEventCalled = sinon.spy(); var message = '{"foo":"bar"}'; var handler = function(e){ var socketWrapper = e.detail.socketWrapper; window.document.body.addEventListener("htmx:wsBeforeSend", myEventCalled) try { socketWrapper.sendImmediately(message, window.document.body) checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, window.document.body) } finally { window.document.body.removeEventListener("htmx:wsBeforeSend", myEventCalled) } done() } try { window.document.addEventListener("htmx:wsOpen", handler) var div = make('
div1
'); this.tickMock(); } finally { window.document.removeEventListener("htmx:wsOpen", handler) } }) it('triggers wsBeforeSend on any send element provided to sendImmediately', function (done) { var myEventCalled = sinon.spy(); var message = '{"a":"b"}'; var handler = function(e){ var socketWrapper = e.detail.socketWrapper; var id1 = byId("d1"); id1.addEventListener("htmx:wsBeforeSend", myEventCalled) try { socketWrapper.sendImmediately(message, d1) checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, d1) } finally { id1.removeEventListener("htmx:wsBeforeSend", myEventCalled) } done() } window.document.addEventListener("htmx:wsOpen", handler) try { var div = make('
div1
'); this.tickMock(); } finally { window.document.removeEventListener("htmx:wsOpen", handler) } }) it('triggers wsAfterSend on body if provided to sendImmediately', function (done) { var myEventCalled = sinon.spy(); var message = '{"foo":"bar"}'; var handler = function(e){ var socketWrapper = e.detail.socketWrapper; window.document.body.addEventListener("htmx:wsAfterSend", myEventCalled) try { socketWrapper.sendImmediately(message, window.document.body) checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, window.document.body) } finally { window.document.body.removeEventListener("htmx:wsAfterSend", myEventCalled) } done() } try { window.document.addEventListener("htmx:wsOpen", handler) var div = make('
div1
'); this.tickMock(); } finally { window.document.removeEventListener("htmx:wsOpen", handler) } }) it('triggers wsAfterSend on any send element provided to sendImmediately', function (done) { var myEventCalled = sinon.spy(); var message = '{"a":"b"}'; var handler = function(e){ var socketWrapper = e.detail.socketWrapper; var id1 = byId("d1"); id1.addEventListener("htmx:wsAfterSend", myEventCalled) try { socketWrapper.sendImmediately(message, d1) checkCallForWsBeforeSend(myEventCalled, socketWrapper, message, d1) } finally { id1.removeEventListener("htmx:wsAfterSend", myEventCalled) } done() } window.document.addEventListener("htmx:wsOpen", handler) try { var div = make('
div1
'); this.tickMock(); } finally { window.document.removeEventListener("htmx:wsOpen", handler) } }) it('sends message if event is not prevented', function (done) { var message = '{"a":"b"}'; var noop = function() {} var handler = function(e){ var socketWrapper = e.detail.socketWrapper; var id1 = byId("d1"); id1.addEventListener("htmx:wsBeforeSend", noop) try { socketWrapper.sendImmediately(message, d1) this.tickMock(); this.messages.should.eql([message]) } finally { id1.removeEventListener("htmx:wsBeforeSend", noop) } done() }.bind(this) window.document.addEventListener("htmx:wsOpen", handler) try { var div = make('
div1
'); this.tickMock(); } finally { window.document.removeEventListener("htmx:wsOpen", handler) } }) it('sends message if no sending element is provided', function (done) { var message = '{"a":"b"}'; var handler = function(e){ var socketWrapper = e.detail.socketWrapper; socketWrapper.sendImmediately(message) this.tickMock(); this.messages.should.eql([message]) done() }.bind(this) window.document.addEventListener("htmx:wsOpen", handler) try { var div = make('
div1
'); this.tickMock(); } finally { window.document.removeEventListener("htmx:wsOpen", handler) } }) it('sends message if sending element has no event listener for beforeSend', function (done) { var message = '{"a":"b"}'; var handler = function(e){ var socketWrapper = e.detail.socketWrapper; var d1 = byId("d1"); socketWrapper.sendImmediately(message, d1) this.tickMock(); this.messages.should.eql([message]) done() }.bind(this) window.document.addEventListener("htmx:wsOpen", handler) try { var div = make('
div1
'); this.tickMock(); } finally { window.document.removeEventListener("htmx:wsOpen", handler) } }) it('does not send message if beforeSend is prevented', function (done) { var message = '{"a":"b"}'; var eventPrevented = function(e) {e.preventDefault()} var handler = function(e){ var socketWrapper = e.detail.socketWrapper; var id1 = byId("d1"); id1.addEventListener("htmx:wsBeforeSend", eventPrevented) try { socketWrapper.sendImmediately(message, d1) this.tickMock(); this.messages.should.eql([]) } finally { id1.removeEventListener("htmx:wsBeforeSend", eventPrevented) } done() }.bind(this) window.document.addEventListener("htmx:wsOpen", handler) try { var div = make('
div1
'); this.tickMock(); } finally { window.document.removeEventListener("htmx:wsOpen", handler) } }) }) });