DEV Community

Cover image for Beyond Cypress `cy.intercept`: real-time SignalR websocket payments testing in Cypress hybrid frameworks
Daniil
Daniil

Posted on

Beyond Cypress `cy.intercept`: real-time SignalR websocket payments testing in Cypress hybrid frameworks

In the first part of this article, we explored the limitations of cy.intercept for WebSocket testing and introduced a Cypress hybrid framework for handling real-time WebSocket interactions. Now, let’s dive into real-world payment testing scenarios, where WebSockets play a vital role in ensuring accurate transaction updates, live balance tracking, and seamless user experiences.

Why WebSockets does matter for payment testing?

Built-in payment systems in a web applications frequently rely on real-time updates to provide users with immediate feedback on their transactions. Unlike traditional HTTP polling, which repeatedly requests data from the server, WebSockets enable instant push notifications, making them ideal for:

  • Live transaction status updates (e.g., "Payment Processing", "Payment Successful", "Payment Failed").
  • Real-time balance updates after deposits, withdrawals, or refunds.
  • Instant fraud alerts and security notifications.

Since Cypress doesn’t natively support WebSocket testing, we need to extend its capabilities using custom commands and WebSocket event listeners. This section will walk through specific payment testing scenarios, including:

  • Validating real-time payment status updates.
  • Ensuring balance updates after transactions.
  • Handling error cases, disconnections, and retries.

Real-time SignalR connection for balance and payment status updates

The connectWStoListen is a custom Cypress command is designed to establish a real-time WebSocket connection using SignalR. It listens for updates related to balance changes and payment status in applications that rely on WebSockets for instant transaction validation.

Cypress.Commands.add(
  "connectWStoListen",
  (url, { onBalanceUpdate, onPaymentStatusUpdate }) => {
    return new Promise((resolve, reject) => {
      const ws = new WebSocket(url);
      Cypress.env("currentWebSocket", ws); // Store WebSocket instance globally
...
Enter fullscreen mode Exit fullscreen mode

Introducing the handshake data

To establish a SignalR connection, we first send a handshake request to the server. This ensures that both client and server agree on communication protocols before exchanging actual data. The format is commonly used across the different types of a web sockets:

...     
      const handshakeData = '{"protocol":"json","version":1}\x1e';
      let receivedEmptyResponse = false; // Track server's empty {} response
      let readyToListen = false; // Track when we're ready to listen for custom messages
...
Enter fullscreen mode Exit fullscreen mode

The \x1e at the end of handshakeData is a special SignalR delimiter, marking the end of the message.

Once the handshake is complete, we can start listening for messages and handling errors.

Handling WebSocket messages & errors

Once the connection is established, the SignalR receives real-time messages from the server. Our Cypress command processes these messages and handles different server responses.

ws.onopen = () => {
        console.log("SignalR WebSocket connection established");
        ws.send(handshakeData); // Step 1: Send handshake message

        ws.onmessage = (event) => {
          const rawMessage = event.data;
          const messages = rawMessage.split("\x1e").filter((m) => m);

          messages.forEach((message) => {
            try {
              const parsedMessage = JSON.parse(message);

              // Step 2: Check for initial empty response {}
              if (
                Object.keys(parsedMessage).length === 0 &&
                !receivedEmptyResponse
              ) {
                console.log("Received empty response {} from server.");
                receivedEmptyResponse = true;
                ws.send('{"type":6}\x1e'); // Step 3: Send type after  empty server response
              }
              // Step 4: Wait for server to respond with {"type":6}
              else if (
                parsedMessage.type === 6 &&
                receivedEmptyResponse &&
                !readyToListen
              ) {
                console.log(
                  "Received server confirmation type 6. Ready for further messages.",
                );
                readyToListen = true;
                resolve(); // Signal that we're ready for payment deposit request
              }
              // Only listen for custom messages 
              // ReceivePaymentStatusUpdate and ReceiveBalanceUpdate
              // after setup is complete
              if (
                readyToListen &&
                parsedMessage.type === 1 &&
                parsedMessage.target
              ) {
                if (
                  parsedMessage.target === "ReceivePaymentStatusUpdate" &&
                  onPaymentStatusUpdate
                ) {
                  onPaymentStatusUpdate(parsedMessage.arguments[0]);
                }
                if (
                  parsedMessage.target === "ReceiveBalanceUpdate" &&
                  onBalanceUpdate
                ) {
                  onBalanceUpdate(parsedMessage.arguments[0]);
                }
              }
            } catch (error) {
              console.error("Failed to parse SignalR message:", error);
            }
          });
        };
        ws.onerror = (error) => {
          console.log(WebSocket error: ${error.message});
          reject(error);
        };
        ws.onclose = () => {
          console.log("SignalR WebSocket connection closed");
        };
      };
    });
  },
);
Enter fullscreen mode Exit fullscreen mode

Error Handling: The WebSocket gracefully logs errors and closes connections to prevent resource leaks.

Automatic Retrying: If pending transactions remain unresolved, Cypress will retry until confirmation is received.

Implementing WebSocket balance verification

Now that our WebSocket connection is in place, we can create a dedicated method to validate real-time balance updates.

connectWStoVerifyBalance() {
      // Construct your WebSocket SignalR URL
      // The URL is based on configuration for specific testing 
      // environment
      const urlWS = `${envConfig.webSocketAPI}${bearerToken}`;
      cy.connectWStoListen(urlWS, {
        onPaymentStatusUpdate: (paymentStatus) => {
          const parsedStatus = JSON.parse(paymentStatus);
          // Wait and retry if payment status is still 'pending'
          if (parsedStatus.status === "pending") {
            console.warn("Payment status is pending. Retrying...");
          }
          // Do an assertion that status is updated to Success
          expect(parsedStatus.status).to.equal("success");
          console.log("Payment status updated successfully:", parsedStatus);
        },
        onBalanceUpdate: (updatedBalance) => {
          // Do required assertions for a balance update
          // Save new balance value if it is necessary
          console.log("Balance updated successfully:", updatedBalance);
          // Close WebSocket connection
          const ws = Cypress.env("currentWebSocket");
          if (ws) ws.close();
        },
      });
    });
  }
Enter fullscreen mode Exit fullscreen mode

Using WebSockets in a Cypress test case

Now, let’s implement WebSocket validation in a Cypress test case to verify that payments are processed correctly and that balance updates happen in real time.

describe("Deposit Feature - Real-Time WebSocket Validation", () => {

  it(`Verify that a user can deposit min amount and payment is successful`, () => {
    // Step 1: User Registration & Setup
    cy.registerNewUser();
    cy.confirmUserIsRegistered();

    // Step 2: Establish WebSocket Connection for Balance Monitoring
    // using our created method
    cy.connectWStoVerifyBalance();

    // Step 3: Navigate to payments and do required actions to
    // initiate a deposit transaction
    cy.navigateToPayments();
    cy.accessDepositTab();
    cy.verifyMinimumDepositAmount();
    cy.validatePendingPaymentStatus();

    // Step 4: Simulate payment process via API/webhook/UI
    cy.updateDepositStatusViaAPI();

    // Step 5: Verify successful payment update on UI
    cy.validateSuccessfulPaymentMessage();
  });
});
Enter fullscreen mode Exit fullscreen mode

Since we've added console logs to our Cypress custom command connectWStoListen, we will see real-time status updates in the Cypress console. This provides full visibility into how SignalR WebSocket connections handle transaction updates.

Conclusion

WebSockets provide real-time transaction tracking for modern payment systems, ensuring that users get immediate feedback. By extending Cypress with custom WebSocket commands, we can:

  1. Test instant balance updates.
  2. Validate payment processing workflows.
  3. Ensure seamless, real-time UX in financial apps.

Would you like to know more about Cypress and QA insights? Follow me on LinkedIn🚀 or subscribe to my TG channel🚀

Top comments (0)