Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support mocking WebSocket APIs #2011

Merged
merged 123 commits into from
Oct 29, 2024
Merged

feat: support mocking WebSocket APIs #2011

merged 123 commits into from
Oct 29, 2024

Conversation

kettanaito
Copy link
Member

@kettanaito kettanaito commented Feb 3, 2024

Roadmap

  • Support runtime overrides for event handlers.
  • Extend Handler in RequestHandler. This way HttpHandler and GraphQLHandler will be assignable to type Handler, and Array<Handler> can annotate both HTTP and WebSocket handlers.
    • Bad idea. Even abstracted, RequestHandler relies on caching to clone requests, parsed result, etc. Not enough common ground to reuse between it and WebSocketHandler. WS handler is better off as a standalone class (doesn't support once anyway).
  • Call handleWebSocketEvent() in SetupWorkerApi and SetupServerApi.
  • handleRequest: Check that the handler is instanceof RequestHandler to skip WebSocket handlers.
  • Update to @mswjs/interceptors that ships WebSocketInterceptor.
  • Improve types. The data argument of message listeners must be annotated across the board (includes annotating the MessageEvent in the Interceptors).
    • Not a good idea. The data you receive is still string | Blob | ArrayBuffer. You cannot just cast it to something else. Instead, introduce a custom parser that does that casting, as well as the runtime typeof checks. That's the way.
    • Document this approach.
  • Add links to jsdoc blocks of the new API.
  • Bug: look into why pnpm test:modules:node hangs forever on setupServer() without the explicit process.exit() in the generated runtime.* scripts.
  • Add Node.js tests.
  • Add browser tests.
  • Add .use() override tests (i.e. event.stopImmediatePropagation()).
  • Defer the interceptor application to the .start()/.listen() calls.
  • Add server.close() API (a task for Interceptors, here just add tests).
  • Invoke onUnhandledRequest() (or similar) for unhandled WebSocket connections. Technically, there are no requests, so the existing method reads weird. WebSocket Support Beta #2010 (comment)
  • feat(ws): add logging to WebSocket connections #2112
  • WebSocket: Decide how to handle actual server errors interceptors#539
  • Forward client events to the server by default (WebSocket Support Beta #2010 (comment)).
  • Fix .broadcast() not broadcasting from N(1) -> N+ but working from N+ -> N+. (discovered in add WebSocket + ws example examples#111) (fix(WebSocketClientManager): use localStorage for clients persistence #2127).
  • Annotate this: WebSocket in the client/server event listeners.
  • Refreshing the page keeps adding the same client under a new ID to the localStorage (fix: fix: purge persisted clients on page reload #2133)
Screenshot 2024-04-12 at 13 09 35
  • Remove console.log (
    console.log('get clients()', inMemoryClients, this.getSerializedClients())
    )
  • Update the "Client-to-server forwarding" docs. Both forwarding is automatic now, and can be opt-out by calling event.preventDefault(). (proof)
  • Docs: document how runtime handlers are supposed to work (they are event-based) WebSocket Support Beta #2010 (reply in thread)
  • WebSocketClientManager still has the "first-tab-only" issue with multiple tabs.
  • wss://localhost:* (* as port throws on path-to-regexp). Quite likely fixed by Support URLPattern as request predicate #1921
  • Fix an issue where having multiple event handlers on the same page leads to the DOMException: Key already exists in the object store. error from IndexedDB (concurrent writes to the db).
  • Fix (node:15076) MaxListenersExceededWarning: Possible EventTarget memory leak detected in pnpm test:unit src/core/ws/WebSocketClientManager.test.ts

@kettanaito
Copy link
Member Author

📦 2.3.0-ws.rc-11

  • Simplified logging events.

@kettanaito
Copy link
Member Author

📦 v2.3.0-ws.rc-12

  • Supports event.stopPropagation() and event.stopImmediatePropagation() for client and server events. You can now manage the event flow across different listeners and event handlers with better granularity.

@kesupile
Copy link

kesupile commented Oct 21, 2024

📦 v2.3.0-ws.rc-12

  • Supports event.stopPropagation() and event.stopImmediatePropagation() for client and server events. You can now manage the event flow across different listeners and event handlers with better granularity.

This works really well for our needs; thanks for all the hard work!

Is there a timeline for when it might be included in the main release branch? I noticed that none of this functionality is included in release 2.4.11

@kettanaito
Copy link
Member Author

kettanaito commented Oct 21, 2024

@kesupile, happy to hear that!

The WebSocket support is a release candidate right now. I'm planning on publishing it this year after I wrap up the remaining bits. You can use the release candidate for now, it will land on main as pretty much 1-1. I also try to keep this branch up-to-date to have the latest changes from main included in the release candidate.

Also, if you're using this at work, please consider becoming MSW sponsor 🙏 I could use your support to keep improving and maintaining the project.

Let me know if you have any feedback while using the new API!

@kettanaito kettanaito merged commit ae786f5 into main Oct 29, 2024
11 checks passed
@kettanaito kettanaito deleted the feat/ws branch October 29, 2024 11:16
@kettanaito
Copy link
Member Author

Released: v2.6.0 🎉

This has been released in v2.6.0!

Make sure to always update to the latest version (npm i msw@latest) to get the newest features and bug fixes.


Predictable release automation by @ossjs/release.

@AlexDroll
Copy link

Hi @kettanaito
I fear that this PR introduced a breaking change in e.g. the listHandlers() method, since the return types changed (see here].
This is also not reflected yet in the documentation see for example here

@kettanaito
Copy link
Member Author

kettanaito commented Oct 30, 2024

Hi, @AlexDroll. Thanks for sharing this.

Adding a new type to the union isn't a breaking change. I will update the docs to mention that the method now returns all handlers. PR: mswjs/mswjs.io#427

@spawnrider
Copy link

Hi,

I just updated from MSW 2.3.5 to 2.6.5. and there is effectively a breaking change on the listHandlers() method.
Now, you need to cast the handler object to access info attribute as below :

for (const handler of handlers) {
  console.debug('Handler : ', (handler as RequestHandler).info.header);
}

Otherwise, you'll have this issue :

error TS2339: Property 'info' does not exist on type 'RequestHandler<RequestHandlerDefaultInfo, any, any, RequestHandlerOptions> | WebSocketHandler'.
  Property 'info' does not exist on type 'WebSocketHandler'.

21       console.debug('Handler : ', handler.info.header);

@pleunv
Copy link

pleunv commented Nov 19, 2024

You can avoid a cast by narrowing it:

worker.listHandlers().forEach(handler => {
  if ('isUsed' in handler) {
    // Narrowed to a `RequestHandler`
  } else {
    // Narrowed to a `WebSocketHandler`
  }
});

Or in a helper:

function isRequestHandler(handler: RequestHandler | WebSocketHandler): handler is RequestHandler {
  return 'isUsed' in handler;
}

This works because WebSocketHandler has no isUsed field, at least at the moment.

@kettanaito
Copy link
Member Author

The answer from @pleunv is almost there. Don't rely on properties, check the class instance:

import { RequestHandler } from 'msw'

const httpHandlers = worker.listHandlers().filter((handler) => {
  return handler instanceof RequestHandler
})

console.log(httpHandlers)

@pleunv
Copy link

pleunv commented Nov 19, 2024

I generally avoid importing from msw because it pulls in the whole kitchen sink 🙂

@spawnrider
Copy link

Could you update the documentation regarding this issue ?

@kettanaito
Copy link
Member Author

@pleunv, unless you are running a build-less app, I'd expect your bundler to tree-shake anything you don't use from msw. There may be exceptions (#2185), but those are on us and rather non-trivial to get right.

@spawnrider, where do you think this update belongs best? It's hard to assume everyone needs to do the instanceof check. .listHandlers() is primarily intended as console.log(worker.listHandlers()). If you want more, we expect you to know what you are doing.

That being said, I agree this needs to be mentioned. I can propose a new section to worker.listHandlers()/server.listHandlers() that shows how to list handlers only of a particular type (http/ws)? Wdyt?

@spawnrider
Copy link

That's seems pretty good to me.

I just managed to print the handlers list using this code (and not using a cast) :

for (const handler of handlers) {
  if (handler instanceof RequestHandler) {
    console.debug('Handler : ', handler.info.header);
  }
}

@kettanaito
Copy link
Member Author

@spawnrider, please review mswjs/mswjs.io#431.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Support mocking WebSocket APIs
8 participants