feat!: rewrite package to new network source architecture#48
feat!: rewrite package to new network source architecture#48christoph-fricke wants to merge 23 commits intomswjs:mainfrom
Conversation
There was a problem hiding this comment.
@kettanaito I am struggling a bit with implementing WebSocket handling, and would like to align on an approach before continuing. I don't have experience with using WebSocket mocking in MSW, which makes this a bit harder as well. So far I thought about three approaches to tackling this.
1. PlaywrightWebSocketNetworkFrame extends WebSocketNetworkFrame
Similar to the PlaywrightHttpNetworkFrame implementation, this seems like the most obvious choice. When constructing an implementation of the frame, it requires a WebSocketClientConnection and WebSocketServerConnection connection. First idea: Let's use the already existing Playwright connection classes that implement the WebSocket(Client|Server)ConnectionProtocol.
const frame = new PlaywrightWebSocketNetworkFrame({
connection: {
client: new PlaywrightWebSocketClientConnection(route),
server: new PlaywrightWebSocketServerConnection(route),
info: { protocols: [] },
},
})This does not work! It results in type errors for both client and server:
- client: Type 'PlaywrightWebSocketClientConnection' is missing the following properties from type 'WebSocketClientConnection': socket, transport, [kEmitter$1]
- server: Type 'PlaywrightWebSocketServerConnection' is missing the following properties from type 'WebSocketServerConnection': client, transport, createConnection, mockCloseController, and 7 more.
This also explains the TS errors in the existing fixture after updating MSW when calling webSockerHandler.run(...), and might explain why all WebSocket related tests have been failing.
=> Takeaway: Implementing WebSocket(Client|Server)ConnectionProtocol is not enough. We have to use WebSocket(Client|Server)Connection if we want to extend WebSocketNetworkFrame.
2. Implementing a WebSocketTransport
Since WebSocket(Client|Server)Connection are already usable classes, it might be enough to implement a custom WebSocketTransport, and construct WebSocket(Client|Server)Connection directly with a PlaywrightWebSocketTransport.
My concern with this approach is that this might not work. The two classes appear to be a bit too closely related to an actual WebSocket. Both require a WebSocket instance for construction and do operations with it to some extend. I fear that we cannot provide a reliable WebSocket instance (mock) with the stuff we get from Playwright's WebSocketRoute.
3. Extend a "low-level" NetworkFrame<"ws">
Maybe we do not need to extend WebSocketNetworkFrame. All we have to do is resolve/proxy WebSocket messages using the WebSocketRoute (famous last words...). Maybe the simplest, most robust approach, is a PlaywrightWebSocketNetworkFrame that extends NetworkFrame<"ws"> directly, and uses WebSocketRoute instead of WebSocket(Client|Server)Connection.
I started this third approach in this file. But as you can see, msw/experimental is missing a bunch of exports to be able to directly extend NetworkFrame. So I am wondering if this approach should be avoided at all cost.
What are your thoughts on this? What do you think is the best approach for handling Playwright WebSocket routes with the new network source architecture? Are there missing pieces in MSW to make this feasible?
There was a problem hiding this comment.
Your first approach sounds correct. The type mismatch might as well be a bug. WebSocketClientConnectionProtocol type is a general type I created exactly for this reason (so the implementers don't have to worry about transport).
Yeah, it's definitely a bug. In websocket-frame.ts:
export interface WebSocketNetworkFrameOptions {
connection: WebSocketConnectionData
}I'm getting the WebSocketConnectionData from the Interceptors as-is, but that's a mistake. That type is a narrower type than what the frame should care about.
We should probably annotate that connection option as WebSocketHandlerConnection from WebSocketHandler.ts, which contains the broader WebSocketClientConnectionProtocol.
Can you open an issue for this in the MSW repo, please? 🙏 I think this is a great find.
There was a problem hiding this comment.
@kettanaito The rewrite is complete so far. All tests pass (when the "type as value" bug is fixed in MSW or locally) and the new implementation feels quite solid already. I think next good steps are:
- We give the implementation a proper review
- Clarify open questions and resolve missing MSW parts
Remove the oldfixture.tsimplementationUpdate the README and add some JSDoc comments to the source class and its options.
| import type { RequestHandler } from 'msw' | ||
| import type { NetworkFrameResolutionContext } from '../../node_modules/msw/lib/core/experimental/frames/network-frame.mjs' | ||
| import type { UnhandledFrameHandle } from '../../node_modules/msw/lib/core/experimental/on-unhandled-frame.mjs' |
There was a problem hiding this comment.
❌ MSW should export these types before we merge this.
| import type { WebSocketHandler } from 'msw' | ||
| import type { NetworkFrameResolutionContext } from '../../node_modules/msw/lib/core/experimental/frames/network-frame.mjs' | ||
| import type { UnhandledFrameHandle } from '../../node_modules/msw/lib/core/experimental/on-unhandled-frame.mjs' |
There was a problem hiding this comment.
❌ MSW should export these types before we merge this.
| } | ||
|
|
||
| passthrough(): void { | ||
| this.#route.connectToServer() |
There was a problem hiding this comment.
💭 Not sure if this approach, or calling this.data.connection.server.connect() is better? The latter registers pending event listeners from the mock. Does it even matter in the cases where passthrough is called?
| } | ||
| } | ||
|
|
||
| class PlaywrightWebSocketClientConnection implements WebSocketClientConnectionProtocol { |
There was a problem hiding this comment.
💡 PlaywrightWebSocketClientConnection and PlaywrightWebSocketServerConnection are pretty much copied. Only made slight adjustment such as aligning the #route field naming.
| } from './route-utils.js' | ||
|
|
||
| export interface PlaywrightSourceOptions { | ||
| skipAssetRequests?: boolean |
There was a problem hiding this comment.
💭 With support for custom route patterns, skipAssetRequests might be obsolete.
I added support for custom patterns, because in my experience most mock setups mock API(s) behind one (a few at most) endpoints. Alongside registering multiple PlaywrightSource sources, custom patterns can help reduce the interception overhead for this common case. What do you think about this?
There was a problem hiding this comment.
Yes, if we go with the route of mapping handlers to page.route(), then I suppose there's no need in skipAssetRequests anymore. Just need to make sure the tests pass.
| expect.soft(consoleSpy.callCount).toBe(2) | ||
| expect(consoleSpy.getCall(1)?.args).toEqual([ | ||
| expect.soft(consoleSpy.callCount).toBe(3) | ||
| expect(consoleSpy.getCall(2)?.args).toEqual([ |
There was a problem hiding this comment.
💭 The implementation in fixture.ts never tried to resolve Vite's dev-server WebSocket here, because it aborts early when no WebSocket handlers are defined. I let the orchestration happen completely in WebSocketNetworkFrame.resolve(), which leads to the additional warning.
If needed, we could add an early passthrough in PlaywrightWebSocketNetworkFrame to avoid the additional WebSocket warning.
| connection: { | ||
| client: new PlaywrightWebSocketClientConnection(options.route), | ||
| server: new PlaywrightWebSocketServerConnection(options.route), | ||
| info: { protocols: [] }, |
There was a problem hiding this comment.
❌ Depends on work in mswjs/msw#2710. Currently, this code surfaces a TS error.
Although, it (surprisingly) works at runtime already. At least all tests pass.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
This PR rewrites
@msw/playwrightto be based on the new network source architecture released in MSW v2.13.0.Changes
defineNetworkFrameuses the newdefineNetworkAPI with aPlaywrightSourceunderneathcontextoption has been widened to accept either aBrowserContextorPageroutePatternoption for providing a custom pattern passed tocontext.route(...).websocketPatternoptions for providing a custom pattern passed tocontext.routeWebSocket(...).Todo
PlaywrightSourcethat extendsNetworkSource.BrowserContextorPageas an interception targetPlaywrightHttpNetworkFrame extends HttpNetworkFrame.PlaywrightHttpNetworkFrameintoPlaywrightSource.PlaywrightWebSocketNetworkFrame extends WebSocketNetworkFrame.PlaywrightHttpNetworkFrameintoPlaywrightSource.defineNetworkFixtureimplementation withdefineNetworkwrapper implementation