microsoft / playwright

Playwright is a framework for Web Testing and Automation. It allows testing Chromium, Firefox and WebKit with a single API.
https://playwright.dev
Apache License 2.0
63.64k stars 3.44k forks source link

[Feature] Websocket interception #4488

Open gabrielmanara opened 3 years ago

gabrielmanara commented 3 years ago

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.

aslushnikov commented 3 years ago

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

gabrielmanara commented 3 years ago

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

kiririk commented 3 years ago

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.

jonny-philip commented 3 years ago

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.

LanderBeeuwsaert commented 3 years ago

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.

luisfmsouza commented 3 years ago

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

btvanhooser commented 3 years ago

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 commented 3 years ago

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.

jonny-philip commented 3 years ago

@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 :)

btvanhooser commented 3 years ago

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

mrpicklez70 commented 2 years ago

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.

unlikelyzero commented 2 years ago

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

btvanhooser commented 2 years ago

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 :/

rinogo commented 2 years ago

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.

Evilweed commented 2 years ago

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.

btvanhooser commented 2 years ago

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.

thryyy commented 2 years ago

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.

kylecoberly commented 1 year ago

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.

btvanhooser commented 1 year ago

@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

mattoni commented 1 year ago

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 :(

kylecoberly commented 1 year ago

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

klh commented 1 year ago

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.

linda-lai commented 1 year ago

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

GrayedFox commented 1 year ago

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 commented 1 year ago

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.

UserCI2 commented 1 year ago

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

luqy commented 1 year ago

Please when will it be released

klh commented 1 year ago

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.

icbarbu commented 1 year ago

We need an official release for this feature

RatexMak commented 10 months ago

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

AbdulAhadKhan0308 commented 10 months ago

We really need websocket interception, while sniffing of websocket data can be done with existing websocket class, we can't change the contents of sent messages or decide to send them or not.

kemery-discovery commented 10 months ago

I agree that it would be useful for testing websocket based APIs and the way that they behave when sockets are blocked

manjumuthaiya commented 10 months ago

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.

@mattoni - Hi, did you have any success with this method? I've also been dealing with the problem of port conflicts, and have to stick to 1 worker because of this.

lucianoratamero commented 10 months ago

just echoing what everyone here is saying: we really need this feature. :] I love playwright, and this is the first blocker I've had with it (thanks for the good work, btw).

I've tried most of the workarounds, and unfortunately none of them worked for me =/

vrknetha commented 8 months ago

We really need this feature, the workarounds are feasible on the long run.

mattoni commented 8 months ago

@manjumuthaiya Sorry for the late reply. Not exactly, I created a custom websocket server that sets up a 'controller' and receivers, where the controller can send ws messages that get propagated to all 'receivers'. the receivers are paired up with a controller id, so when multiple tests are running a controller id is set up and the receiver i inject into the running browser matches so it only receives messages sent by that controller. It was a lot of work and not perfect. A solution native to playwright would be more ideal but it solves our needs for now.

LoaderB0T commented 7 months ago

I also totally agree with everyone here, that official support for mocking web socket messages would be an awesome addition to playwright. For now, I also created a generic workaround solution that is very limited at the moment but might be a good starting point for further improvements. Parallelization of tests for example would need a slight modification. You can find the helper class here: https://github.com/LoaderB0T/playwright-easy-network-stub/blob/main/src/playwright-easy-ws-stub.ts

DHFW commented 7 months ago

@LoaderB0T Thanks for sharing this. This worked for Playwright 1.40.0. I just had to also mock the connection creation which returned the address to websocket connection url.

jaktestowac commented 4 months ago

Any plans to implement this in 2024?

marliz97 commented 2 months ago

Any updates?

JamesODonoghue commented 1 month ago

Would love this feature as well

Kuro091 commented 3 weeks ago

Adding my voice to the thread that support for this would be huge! Please!

brabenetz commented 2 weeks ago

FYI: My Workaround for my special Setup with Stomp-js and Spring-Stomp on the backend:

Using the spring-stomp-server as Mock for automatic tests:

On a deployed stage the real backend can also be used for the automatic tests, but for local runs, it is easier to start spring-stomp-server concurrently with "ng serve" without transitive dependencies like databases.

With Stomp-js you can then directly push messages from your tests to the spring-stomp-server (or the real backend). And then verify that your App reacts accordingly.

Downside: The tests cannot be executed in parallel with the other tests. For that, the tests must be split into "parallel" and "serial" runs.