Using Cypress with `mock-sockets` - websocket

I am trying to use mock-sockets with Cypress, setting up the mock in the onBeforeLoad hook for cy.visit() in my beforeEach block. I can get one test to work but when the mock setup runs on the next beforeEach I get an error that A mock server is already listening on this url.
code under test:
(called from my React app's componentDidiMount)
subscribeToSettings(url: string): W3CWebSocket {
let settingsSubscription = new W3CWebSocket(url);
settingsSubscription.onopen = () => console.log('WebSocket Client Connected (settings)');
settingsSubscription.onclose = () => console.log('WebSocket Client Disconnected (settings)');
settingsSubscription.onmessage = (message: MessageEvent) => this.handleSettingsMessage(message);
return settingsSubscription;
}
/**
* Handler for websocket settings messages, which updates the local settings values.
* #param message the websocket message
*/
handleSettingsMessage(message: MessageEvent) {
const updatedValues = JSON.parse(message.data);
console.log('A message was received on the settings channel.', updatedValues);
this.props.updateSettingsFromBackend(updatedValues);
}
cypress tests
import { Server } from 'mock-socket'
import { defaultSettingsState } from "../../src/reducers/settings.reducer";
import { _createSettingsApiPutPayload } from "../../src/actions/settings.actions";
describe('mock socket method 1', () => {
let mockSocket;
let mockServer;
beforeEach(() => {
cy.visit('/', {
onBeforeLoad(win: Window): void {
// #ts-ignore
cy.stub(win, 'WebSocket', url => {
mockServer = new Server(url)
mockServer.on('connection', socket => {
console.log('mock socket connected');
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket
});
},
});
});
afterEach(() => {
mockSocket.close()
mockServer.stop()
});
it('gets a message', () => {
cy.contains('SETTINGS').click()
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
it('gets another message', () => {
cy.contains('SETTINGS').click()
const object = _createSettingsApiPutPayload(defaultSettingsState)
mockSocket.send(JSON.stringify(object));
cy.contains('Motion threshold')
});
});
Here are the logs from my console:
WebSocket Client Connected (settings)
mock socket connected at url ws://localhost:8702/PM_Settings
A message was received on the settings channel. {…}
mock socket connected at url ws://localhost:3000/sockjs-node/949/mhuyekl3/websocket
The development server has disconnected.
Refresh the page if necessary.
Uncaught Error: A mock server is already listening on this url
I wonder if it has to do with that second call which is for some mystery url.
(Note: calling cy.contains('SETTINGS').click() at the end of beforeEach somehow doesn't work, even in that first test. Even when I have my app set to start on the settings page (instead of having to click to it from inside the tests), clicking on SETTINGS from beforeEach still doesn't work even though we're already there. So that's kind of weird)
These cypress logs may also be helpful:

It only worked for me, when I moved server stopping into WebSocket stub:
cy.stub(window, 'WebSocket', url => {
if (mockServer) {
mockServer.stop();
}
mockServer = new Server(url);
mockServer.on('connection', socket => {
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket;
});
Im probably wrong, but I guess afterEach or mockServer.stop(); is async thats why mock server fails to stop before new init

Related

How to send error manually in the websocket?

I want to send errors in the websocket after successful connection inside tests.
I have a file which will start a websocket server and inside another method there is another websocket that will be connected to another websocket server.
client -> Main file server -> Another server
Client is the test which will connect to the main file's websocket server. I created another websocket server inside beforeEach block of the test with predefined port that will be used from main file to connect to it.
My test file looks like -
describe("some test", async () =>{
beforeEach(async () => {
await new Promise(resolve => {
anotherServer = new WebSocketServer({port:8000, path: '/ws'});
anotherServer.on('connection', function connection(ws) {
ws.on('message', async(data) => {
ws.send(data);
});
ws.on('error',(err) => {
ws.close();
})
ws.on('close',async() =>{
ws.close();
})
ws.send("Connected");
});
resolve(true);
})
})
it("handle error in another server socket",async ()=> {
await main.start(3000);
wb = new Websocket(uri,options);
await new Promise(resolve => { wb.onopen = resolve});
// here I want to send error event in the another server's socket and assert on the logs which are present in the main code.
})
});
My main file has all the event listners for error event and close events. I want to test both of them by sending error and close event in the socket from another server.

How to mock graphql subscription in cypress using mock-socket library?

I am trying to use mock-sockets with Cypress, to mock graphql subscription.
I tried this code but it doesn’t work with Apollo Client because Apollo use ‘WebSocketLink’ instead of 'WebSocket'.
import {
Server,
WebSocket,
} from 'mock-socket';
it('test', () => {
cy.visit('/', {onBeforeLoad(window) {
cy.stub(window, 'WebSocket', url => {
if (mockServer) {
mockServer.stop();
}
mockServer = new Server(url);
mockServer.on('connection', socket => {
mockSocket = socket;
});
mockSocket = new WebSocket(url);
return mockSocket;
});
My question is what is the object of ‘WebSocketLink’ that I need to stub (instead of window)? Or, is there another way to mock graphql web socket?

How to set up a socket connection on a strapi server

I am trying to integrate socket.io with strapi. But unfortunately I have been unable to do so without any proper tutorial or documentation covering this aspect.
I followed along with the only resource I found online which is:
https://medium.com/strapi/strapi-socket-io-a9c856e915a6
But I think the article is outdated. I can't seem to run the code mentioned in it without running into tonnes of errors.
Below is my attempt to implement it and I have been trying to connect it through a chrome websocket plugin smart websocket client But I am not getting any response when I try to run the server.
I'm totally in the dark. Any help will be appreciated
module.exports = ()=> {
// import socket io
var io = require('socket.io')(strapi.server)
console.log(strapi.server) //undefined
// listen for user connection
io.on('connect', socket => {
socket.send('Hello!');
console.log("idit")
// or with emit() and custom event names
socket.emit('greetings', 'Hey!', { 'ms': 'jane' }, Buffer.from([4, 3, 3, 1]));
// handle the event sent with socket.send()
socket.on('message', (data) => {
console.log(data);
});
// handle the event sent with socket.emit()
socket.on('salutations', (elem1, elem2, elem3) => {
console.log(elem1, elem2, elem3);
});
});
};
So I found the solution. Yay. I'll put it here just in case anybody needs it.
boostrap.js
module.exports = async () => {
process.nextTick(() =>{
var io = require('socket.io')(strapi.server);
io.on('connection', async function(socket) {
console.log(`a user connected`)
// send message on user connection
socket.emit('hello', JSON.stringify({message: await strapi.services.profile.update({"posted_by"})}));
// listen for user diconnect
socket.on('disconnect', () =>{
console.log('a user disconnected')
});
});
strapi.io = io; // register socket io inside strapi main object to use it globally anywhere
})
};
Found this at: https://github.com/strapi/strapi/issues/5869#issuecomment-619508153_
Apparently, socket.server is not available when the server starts. So you have to make use of process.nextTick that waits for the socket.server to initialize.
I'll also add a few questions that I faced when setting this up.
How do i connect from an external client like nuxt,vue or react?
You just have to connect through "http://localhost:1337" that is my usual address for strapi.
I am using nuxt as my client side and this is how set up my socketio on the client side
I first installed nuxt-socket-io through npm
Edited the nuxt.config file as per it's documention
modules:[
...
'nuxt-socket-io',
...
],
io: {
// module options
sockets: [
{
name: 'main',
url: 'http://localhost:1337',
},
],
},
And then i finally added a listener in one of my pages.
created() {
this.socket = this.$nuxtSocket({})
this.socket.on('hello', (msg, cb) => {
console.log('SOCKET HI')
console.log(msg)
})
},
And it works.
A clean way to integrate third-party services into Strapi is to use hooks. They are loaded once during the server boot. In this case, we will create a local hook.
The following example has worked with strapi#3.6.
Create a hook for socket.io at ./hooks/socket.io/index.js
module.exports = strapi => {
return {
async initialize() {
const ioServer = require('socket.io')(strapi.server, {
cors: {
origin: process.env['FRONT_APP_URL'],
methods: ['GET', 'POST'],
/* ...other cors options */
}
})
ioServer.on('connection', function(socket) {
socket.emit('hello', `Welcome ${socket.id}`)
})
/* HANDLE CLIENT SOCKET LOGIC HERE */
// store the server.io instance to global var to use elsewhere
strapi.services.ioServer = ioServer
},
}
}
Enable the new hook in order for Strapi to load it - ./config/hook.js
module.exports = {
settings: {
'socket.io': {
enabled: true,
},
},
};
That's done. You can access the websocket server inside ./config/functions/bootstrap.js or models' lifecycle hooks.
// ./api/employee/models/employee.js
module.exports = {
lifecycles: {
async afterUpdate(result, params, data) {
strapi.services.ioServer.emit('update:employee', result)
},
},
};
For those who are looking the answer using Strapi version 4
var io = require("socket.io")(strapi.server.httpServer)

rjxjs handle network errors from socket.io-client

I use rxjs and socket.io client.
Id like to solve this problem.
socket is connected
user send data
socket is having any network error during delivery
delivery is faled
socket throw error to user
Here is my code. how to handle network errors indise Observable?
private sendData(data: any, event: SOCKET_EVENTS): Observable<any> {
return new Observable<any>(observer => {
this.socket
.emit(event, data, function(responseData: Result<any>) {
console.log("Data sended", responseData);
if (responseData.success === true) {
observer.next(responseData.data);
observer.complete();
} else {
console.error(" this.socketData not sended", responseData);
observer.error(data);
}
})
});
}
You can handle errors globaly using event error or something else. For every single emit you can use acknowledgements. Anyway, procedure which handle errors should be more complex. For example:
private sendData(data: any, event: SOCKET_EVENTS): Observable<any> {
return new Observable<any>(observer => {
// set timeout before call error
let errorTimeout = setTimeout(() => {
console.error(" this.socketData not sended");
observer.error();
}, 5000);
this.socket
.emit(event, data, (responseData: Result<any>) => {
console.log("Data sended", responseData);
observer.next(responseData.data);
observer.complete();
// reset timer
clearTimeout(errorTimeout);
})
});
}
We wait 5 seconds for response with acknowledgement. Also you should add logic to handle global errors.

Socket.io sends two messages

I'm trying to setup socket.io and here is part of my server.js
const app = require('express')();
const http = require('http').Server(app);
const io = require('socket.io')(http, { path: '/websocket', origins:'*:*' });
io.on('connection', (socket) => {
socket.send('Hi');
socket.on('message', (message) => {
console.log(message);
socket.emit('hello', `New: ${message}`);
});
console.log('a user connected');
});
http.listen(3030, function(){
console.log('listening on *:3030');
});
and my simple client:
var socket = io('https://*******.com', {
secure: true,
path: '/websocket'
});
const input = document.getElementById('text');
const button = document.getElementById('button');
const msg = document.getElementById('msg');
button.onclick = () => {
socket.emit('message', input.value);
socket.on('hello', (text) => {
const el = document.createElement('p');
el.innerHTML = text;
msg.appendChild(el);
})
}
And if I'll click for third time I receive a 3 messages back and so on. What I'm doing wrong? I wish to send message to the server and receive modified message back.
I'm new in web sockets.
Any help appreciated.
P.S. socket.io v2.0.1
You are adding a socket.on() event handler each time the button is clicked. So, after the button has been clicked twice, you have duplicate socket.on() event handlers. When the event comes back, your two event handlers will each get called and you will think you are getting duplicate messages. Actually, it's just one message, but with duplicate event handlers.
You pretty much never want to add an event handler inside another event handler because that leads to this sort of build-up of duplicate event handlers. You don't describe (in words) exactly what you're code is trying to do so I don't know exactly what alternative to suggest. Usually, you set up the event handlers first, just once, when the socket is connected and then you will never get duplicate handlers.
So, perhaps it's as simple as changing this:
button.onclick = () => {
socket.emit('message', input.value);
socket.on('hello', (text) => {
const el = document.createElement('p');
el.innerHTML = text;
msg.appendChild(el);
})
}
to this:
button.onclick = () => {
socket.emit('message', input.value);
}
socket.on('hello', (text) => {
const el = document.createElement('p');
el.innerHTML = text;
msg.appendChild(el);
});
If you are using Angular and (probably) embedding the Socket in a Service (simpleton) you are creating a persistent listener in ngOnInit every time you load a page.
You need to create some kind of flag to know if the listener was already created in the Service from another instance of your page.

Resources