Efficient WebSocket Traffic Tracking in Cypress

Efficient WebSocket Traffic Tracking in Cypress

One of Cypress’s strengths is its ability to mock and test WebSocket traffic, which is crucial for real-time applications. However, managing asynchronous tasks, especially those involving Cypress commands, can be challenging. Here, we will explore how to track WebSocket traffic effectively in Cypress, address common pitfalls, and implement a robust solution for writing WebSocket messages to a file.

The Challenge

When testing WebSocket interactions, capturing and storing the messages exchanged between the client and server is often necessary. Cypress makes this possible through its extensive set of commands and stubbing capabilities. However, integrating asynchronous tasks, such as writing data to a file, within Cypress’s command queue can lead to issues.

The following code snippet outlines a common approach to tracking WebSocket traffic:

export function trackWebSocketTraffic() {
let messages = []

cy.window().then((win) => {
cy.stub(win, 'WebSocketFactory').callsFake((url, handlers) => {
console.log('WebSocket URL:', url)
const webSocket = new WebSocket(url)
webSocket.onopen = handlers.onopen
webSocket.onmessage = (event) => {
messages.push(event.data)
handlers.onmessage(event)
}
webSocket.onclose = (event) => {
cy.writeFile('./test', messages) // !!! Can't put cy.writeFile in async handler
handlers.onclose(event)
}
return webSocket
})
})
}

This triggers an error:

The following error originated from your test code, not from Cypress.> Cypress detected that you returned a promise from a command while also invoking one or more cy commands in that promise.The command that returned the promise was:> cy.get()The cy command you invoked inside the promise was:> cy.writeFile()Because Cypress commands are already promise-like, you don’t need to wrap them or return your own promise.Cypress will resolve your command with whatever the final Cypress command yields.The reason this is an error instead of a warning is because Cypress internally queues commands serially whereas Promises execute as soon as they are invoked. Attempting to reconcile this would prevent Cypress from ever resolving.When Cypress detects uncaught errors originating from your test code it will automatically

The trackWebSocketTraffic function stubs the WebSocketFactory and attempts to write messages to a file when the connection closes. However, placing cy.writeFile inside the onclose event handler causes an error because Cypress commands should not be invoked within asynchronous callbacks.

The Solution

To resolve this issue, we must ensure that Cypress commands are executed within its controlled environment. One effective way is to utilize Cypress hooks like afterEach to handle asynchronous tasks cleanly. Here’s an improved approach:

Refactored Code

describe('websocket stubbing and tracking', () => {
const messages = []

afterEach(() => {
cy.writeFile('./test.json', { messages })
})

function trackWebSocketTraffic() {
cy.window().then((win) => {
cy.stub(win, 'WebSocketFactory').callsFake((url, handlers) => {
const webSocket = new WebSocket(url)
webSocket.onopen = handlers.onopen
webSocket.onmessage = (event) => {
messages.push(event.data)
handlers.onmessage(event)
}
webSocket.onclose = (event) => {
handlers.onclose(event)
}
return webSocket
})
})
}

Here is additional piece of code that is basically an app simulation that allows us to test the code above (just combine it together):

it('tests the websocket', () => {
    // Dummy WebSocketFactory to allow the stub to run
    cy.state('window').WebSocketFactory = () => {}

    trackWebSocketTraffic()

    // Simulate the app
    let ws
    cy.window().then((win) => {
      const handlers = {
        onopen: () => {},
        onclose: () => {},
        onerror: () => {},
        onmessage: () => {},
      }
      ws = win.WebSocketFactory('ws://localhost:8080', handlers)
      ws.onopen = () => {
        ws.send('a message')
        ws.send('another message')
      }
    })
    cy.wait(500).then(() => ws.close())
  })
})

Explanation

  1. Initialization: We initialize an empty array messages to store WebSocket messages.
  2. afterEach Hook: This hook is executed after each test, ensuring that cy.writeFile is called within Cypress’s controlled environment. By placing cy.writeFile in afterEach, we avoid the asynchronous issue.
  3. trackWebSocketTraffic Function:
    • The function stubs WebSocketFactory and intercepts the creation of WebSocket connections.
    • It captures messages and appends them to the messages array.
    • The onclose event handler remains simple, only calling the original handler without additional Cypress commands.
  4. Test Implementation:
    • We define a dummy WebSocketFactory to allow the stub to function.
    • The trackWebSocketTraffic function is called to set up the stubbing.
    • A simulated WebSocket connection is created and tested by sending and receiving messages.
    • Finally, the connection is closed after a brief wait, triggering the afterEach hook to write the captured messages to a file.

Final thought

Testing WebSocket traffic in Cypress requires careful handling of asynchronous operations. By leveraging Cypress hooks such as afterEach, we can ensure that commands like cy.writeFile are executed within Cypress’s controlled flow, avoiding potential errors.

This approach makes our tests more robust and ensures that we can effectively capture and analyze WebSocket communications in our applications. As real-time web applications continue to grow in complexity, mastering these techniques will be invaluable for any Cypress test suite.

Scroll to Top