playwright icon indicating copy to clipboard operation
playwright copied to clipboard

[Feature] Websocket interception

Open gabrielmanara opened this issue 4 years ago • 40 comments

I've been following up on the WebSockets supports feature, and I saw you've already implemented the option to listen to the WebSockets on the page and would be nice to be possible to intercept these WebSockets calls as well, something similar with Route.

gabrielmanara avatar Nov 19 '20 14:11 gabrielmanara

@gabrielmanara would you like to intercept websocket creation or websocket frames?

aslushnikov avatar Nov 19 '20 15:11 aslushnikov

@aslushnikov I'd like to intercept the frames to be able to change the messages (send and received).

gabrielmanara avatar Nov 19 '20 16:11 gabrielmanara

It would be very cool: to block received or sent websocket frames by regexp on data, to match&replace websocket received or sent frames, to send on "client" my own websocket frame, on existing connection.

kiririk avatar Nov 20 '20 09:11 kiririk

I asked about this in the Slack chat yesterday and Andrey said this hadn't been added as the team were unsure of the use case and asked me to file an issue but I can see there is one already :)

Just to elaborate a bit on what has already been posted above by others:

For HTTP requests Playwright can control the responses so the UI can be tested in isolation without having to actually connect to a server - it would be good if something similar could be done for websockets.

Playwright's own websocket tests use a TestServer to host a websocket server on localhost in order to send the 'incoming' message, ideally this would not be necessary in the same way it is not necessary for HTTP requests/responses.

The use case is to test a UI that uses websocket communication in isolation from a real/fake backend. The current websocket inspection capabilities make it possible to e.g. verify that a certain message is sent when the user clicks on a button, but the missing part is the ability to verify a change to the DOM based on a message that was received. I'd like to be able to do this entirely within Playwright, without having to mock websockets myself or use another library to do it.

jonny-philip avatar May 20 '21 02:05 jonny-philip

For us, I think this could help us out with our usage of firestore that if I understand correctly uses websockets under the hood as well. We would be interested to listen to the websocket frames and have a sort of waitUntil. What @kiririk proposes above would be very useful as well.

LanderBeeuwsaert avatar May 20 '21 08:05 LanderBeeuwsaert

For us, we would like to have the ability to intercept the messages (sent and received) and mock them.

luisfmsouza avatar May 20 '21 12:05 luisfmsouza

My team is also interested in this. We are adding a page to our site that is going to be heavily reliant on websockets to keep the UI up to date, so being able to match what @jonny-philip is requesting would help ensure that the UI is responding correctly to the messages coming through that WebSocket

btvanhooser avatar May 27 '21 23:05 btvanhooser

Just out of sheer curiosity since my team is in dire need of this type of functionality, how long does the feedback collection process usually take? We'd like to choose this as our solution, but we also just need some kind of solution by a given date so we'd like to get an understanding if we should be looking elsewhere for a temporary solution, or if we can prioritize other efforts and come back to this.

btvanhooser avatar Jun 14 '21 23:06 btvanhooser

@btvanhooser if you are looking for a temporary solution you can find details in the playwright slack channel, I used mock-socket. It's not a pretty solution due to the difficulty in importing a package in a playwright script and making that available in the page context, but it does work.

I do hope this feature gets some traction through :)

jonny-philip avatar Jun 15 '21 01:06 jonny-philip

Definitely something I'll try out :) thanks for the heads up

btvanhooser avatar Jun 15 '21 01:06 btvanhooser

I'll migrate my automation framework from Cypress to Playwright if this feature comes out. This will be a real game changer for my use case. We're moving from http request polling to websocket connections for almost every major feature of the application under test.

mrpicklez70 avatar Jul 28 '21 16:07 mrpicklez70

I agree with @mrpicklez70 , this would be a gamechanger for e2e testing

unlikelyzero avatar Aug 30 '21 23:08 unlikelyzero

Checking in on this again. Really anticipating this feature more than anything else in the library. Trying other routes has failed for us unfortunately so we really need this in order to get coverage where web sockets exist and that's growing with each sprint :/

btvanhooser avatar Oct 06 '21 16:10 btvanhooser

Subscribing.

Note that if you use Socket.io, you can disable websockets in your server with io.set('transports', ['polling']);, or if you're on NestJS, @WebSocketGateway({ transports: ['polling'] }).

Disabling websockets temporarily in test is obviously not ideal, but it can be a temporary stopgap measure for e2e testing.

rinogo avatar Dec 17 '21 18:12 rinogo

Sadly having this feature missing makes our lives miserable :< Some time ago we were blocked because duplicate websocket was crashing Playwright - this was fixed. But now we have another bug in Playwright wegarding iframes and Websockets and we are blocked again :< With this feature we could mock/disable websockets and use the framework.

Evilweed avatar Dec 20 '21 15:12 Evilweed

Been another Quarter, so I felt like checking in to see if this is at least being planned. Really looking forward to this feature as application continues to switch over to using web sockets.

btvanhooser avatar Feb 09 '22 00:02 btvanhooser

That would be an amazing feature to have. We recently deployed an application that relies on websocket, and being able to be independent from the backend would be a game changer for us.

thryyy avatar Apr 28 '22 22:04 thryyy

I came up with an example of how to work around this: https://github.com/kylecoberly/playwright-socket-mocking-example

It's not bulletproof, but it's fairly simple and should allow you to send socket messages to a client and assert that messages were sent to the server.

kylecoberly avatar Aug 13 '22 18:08 kylecoberly

@aslushnikov any updates on a possible timeline for this feature. My team is still highly interested and it's coming up on 2 years of collecting feedback

btvanhooser avatar Sep 15 '22 16:09 btvanhooser

I came up with an example of how to work around this: https://github.com/kylecoberly/playwright-socket-mocking-example

It's not bulletproof, but it's fairly simple and should allow you to send socket messages to a client and assert that messages were sent to the server.

The issue with this is that simultaneous tests all run the extend independently, causing port conflicts attempting to set up multiple servers :(

mattoni avatar Oct 19 '22 16:10 mattoni

You could probably lift that socket server out of the function to keep the same reference for every call.

kylecoberly avatar Oct 19 '22 19:10 kylecoberly

how about you implement a sharedWorker (https://developer.mozilla.org/en-US/docs/Web/API/SharedWorker) that sits on the network stack and simulates websockets - postMessaging to the worker via an api and let the worker listen/emit on local ?

This would give you the capability to both intercept from userspace and emit to userspace - while leaving normal ws to passthrough.

You'd be able to mock things like you do http requests etc.

klh avatar Feb 03 '23 10:02 klh

Echoing previous requests to say this feature would be incredibly helpful. Hopefully the requests gain enough traction to get it onto the roadmap soon.

linda-lai avatar Mar 08 '23 05:03 linda-lai

For now, even if PlayWright would allow rerouting WebSocket connection requests to a different (local) URL, that would enable people to build out solutions that mock responses from a local WebSocketServer.

The MVP of this would also be really helpful - basically - allowing page.route() to also intercept the web socket protocol - and then rather than mocking and stubbing messages from there - allowing devs to overwrite the request.url so it is routed elsewhere.

GrayedFox avatar Apr 13 '23 07:04 GrayedFox

I managed to figure out how to do this given a pretty complex but typical setup: we have an iFrame that is embedded in a form which itself loads an external document (a payment form) which establishes a websocket connection. I also needed to monkey patch the content security directives but combining this with a locally hosted WebSocket server now allows us to meaningfully sniff and record web socket messages using the WebSocket PlayWright class and then replay those messages later on from a local WS server.

It's a long example but shows how to both sniff WebSocket messages using the built in PlayWright class as well as demonstrates how, by searching for matching wss protocol urls (i.e. wss://payments.yourapp.com/ws), you can monkey patch your application to instead connect to a local server.

import { Page } from '@playwright/test';
import { Mockmock } from 'mock-mock';
import WebSocket, { WebSocketServer } from 'ws';

const MM = Mockmock.instance;

const interceptWebSocketMessages = async (
  page: Page,
  frameUrl: string,
  remoteWssUrl: string,
  localWssUrl: string,
  port = 3030
) => {
  const webSocketId = 'wsPaymentMsgs';
  // if recording sniff all websocket frames (incoming msgs) and save them
  if (MM.isRecording) {
    page.on('websocket', (ws) => {
      ws.on('framereceived', (data) => {
        MM.record(webSocketId, JSON.parse(data.payload.toString()));
      });
    });
  }

  // if replaying intercept the payments form request, relax CSP policies,
  // and point payments form to local wss server
  if (MM.isReplaying) {
    await page.route(frameUrl, async (route) => {
      const response = await route.fetch();
      const frameHtml = await response.text();
      const body = frameHtml.replace(remoteWssUrl, localWssUrl);
      const headers = response.headers();
      const csp = headers['content-security-policy'];
      headers['content-security-policy'] = csp.replace(
        "connect-src 'self'",
        "connect-src 'self' localhost:* ws://localhost:*"
      );
      await route.fulfill({ response, body, headers });
    });

    // launch the local web socket server
    const wss = new WebSocketServer({ port });

    wss.on('connection', (ws: WebSocket) => {
      // handle errors
      ws.on('error', (error: Error) => {
        console.log('Local websocket server error!');
        console.log(JSON.stringify(error));
      });
      // once a connection is established, replay the mocked data every 250ms
      const recursivelySendData = () => {
        const frame = MM.replay(webSocketId, 'data');
        if (typeof frame === 'undefined') {
          // we've run out of messages to replay so stop sending data
          return;
        }
        ws.send(JSON.stringify(frame.mock));
        setTimeout(recursivelySendData, 250);
      };

      recursivelySendData();
    });
  }
};

MM is shorthand for accessing the Mockmock singleton instance, it's an in house tool used for recording and replaying mocked data saved to local fixture files that we are hoping to open source soon - replace MM calls with your own mock logic as needed.

GrayedFox avatar Apr 13 '23 16:04 GrayedFox

Greetings, it would be good to have this feature, any updates when it will be implemented ?))

UserCI2 avatar May 06 '23 20:05 UserCI2

Please when will it be released

luqy avatar May 16 '23 09:05 luqy

I managed to figure out how to do this given a pretty complex but typical setup: we have an iFrame that is embedded in a form which itself loads an external document (a payment form) which establishes a websocket connection. I also needed to monkey patch the content security directives but combining this with a locally hosted WebSocket server now allows us to meaningfully sniff and record web socket messages using the WebSocket PlayWright class and then replay those messages later on from a local WS server.

It's a long example but shows how to both sniff WebSocket messages using the built in PlayWright class as well as demonstrates how, by searching for matching wss protocol urls (i.e. wss://payments.yourapp.com/ws), you can monkey patch your application to instead connect to a local server.

import { Page } from '@playwright/test';
import { Mockmock } from 'mock-mock';
import WebSocket, { WebSocketServer } from 'ws';

const MM = Mockmock.instance;

const interceptWebSocketMessages = async (
  page: Page,
  frameUrl: string,
  remoteWssUrl: string,
  localWssUrl: string,
  port = 3030
) => {
  const webSocketId = 'wsPaymentMsgs';
  // if recording sniff all websocket frames (incoming msgs) and save them
  if (MM.isRecording) {
    page.on('websocket', (ws) => {
      ws.on('framereceived', (data) => {
        MM.record(webSocketId, JSON.parse(data.payload.toString()));
      });
    });
  }

  // if replaying intercept the payments form request, relax CSP policies,
  // and point payments form to local wss server
  if (MM.isReplaying) {
    await page.route(frameUrl, async (route) => {
      const response = await route.fetch();
      const frameHtml = await response.text();
      const body = frameHtml.replace(remoteWssUrl, localWssUrl);
      const headers = response.headers();
      const csp = headers['content-security-policy'];
      headers['content-security-policy'] = csp.replace(
        "connect-src 'self'",
        "connect-src 'self' localhost:* ws://localhost:*"
      );
      await route.fulfill({ response, body, headers });
    });

    // launch the local web socket server
    const wss = new WebSocketServer({ port });

    wss.on('connection', (ws: WebSocket) => {
      // handle errors
      ws.on('error', (error: Error) => {
        console.log('Local websocket server error!');
        console.log(JSON.stringify(error));
      });
      // once a connection is established, replay the mocked data every 250ms
      const recursivelySendData = () => {
        const frame = MM.replay(webSocketId, 'data');
        if (typeof frame === 'undefined') {
          // we've run out of messages to replay so stop sending data
          return;
        }
        ws.send(JSON.stringify(frame.mock));
        setTimeout(recursivelySendData, 250);
      };

      recursivelySendData();
    });
  }
};

MM is shorthand for accessing the Mockmock singleton instance, it's an in house tool used for recording and replaying mocked data saved to local fixture files that we are hoping to open source soon - replace MM calls with your own mock logic as needed.

This is fine for a typical scenario where you use playwright for component testing, but if you want to do a smoketest or a system integration test you'd need to be able to do a Man-in-the-middle approach instead. It should be fairly simple to implement in playwright since they playwright team could potentially just always proxy network request in the browser to an internal abstraction layer - if nothing is hooking into that just continue as normal - otherwise just echo messages to whatever listener I registered.

klh avatar May 16 '23 09:05 klh

We need an official release for this feature

icbarbu avatar Jun 19 '23 12:06 icbarbu

It's really helpful if we could have this feature!

RatexMak avatar Aug 14 '23 10:08 RatexMak