msw 2.2.3 → 2.3.0-ws.rc-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/cli/init.js +0 -0
  2. package/config/scripts/postinstall.js +0 -0
  3. package/lib/browser/index.d.mts +7 -6
  4. package/lib/browser/index.d.ts +7 -6
  5. package/lib/browser/index.js +32 -15
  6. package/lib/browser/index.js.map +1 -1
  7. package/lib/browser/index.mjs +32 -15
  8. package/lib/browser/index.mjs.map +1 -1
  9. package/lib/core/{GraphQLHandler-jOzqbxSK.d.ts → GraphQLHandler-Cbu12sb0.d.ts} +1 -1
  10. package/lib/core/{GraphQLHandler-AenIUdwE.d.mts → GraphQLHandler-QGQY_9Rc.d.mts} +1 -1
  11. package/lib/core/{HttpResponse-wcp03c7-.d.mts → HttpResponse-BWB1yDNM.d.mts} +2 -2
  12. package/lib/core/{HttpResponse-_514VQ9z.d.ts → HttpResponse-DeJBWGN5.d.ts} +2 -2
  13. package/lib/core/HttpResponse.d.mts +1 -1
  14. package/lib/core/HttpResponse.d.ts +1 -1
  15. package/lib/core/SetupApi.d.mts +15 -12
  16. package/lib/core/SetupApi.d.ts +15 -12
  17. package/lib/core/SetupApi.js +3 -1
  18. package/lib/core/SetupApi.js.map +1 -1
  19. package/lib/core/SetupApi.mjs +3 -1
  20. package/lib/core/SetupApi.mjs.map +1 -1
  21. package/lib/core/getResponse.d.mts +1 -1
  22. package/lib/core/getResponse.d.ts +1 -1
  23. package/lib/core/graphql.d.mts +2 -2
  24. package/lib/core/graphql.d.ts +2 -2
  25. package/lib/core/handlers/GraphQLHandler.d.mts +2 -2
  26. package/lib/core/handlers/GraphQLHandler.d.ts +2 -2
  27. package/lib/core/handlers/HttpHandler.d.mts +1 -1
  28. package/lib/core/handlers/HttpHandler.d.ts +1 -1
  29. package/lib/core/handlers/RequestHandler.d.mts +1 -1
  30. package/lib/core/handlers/RequestHandler.d.ts +1 -1
  31. package/lib/core/handlers/WebSocketHandler.d.mts +38 -0
  32. package/lib/core/handlers/WebSocketHandler.d.ts +38 -0
  33. package/lib/core/handlers/WebSocketHandler.js +65 -0
  34. package/lib/core/handlers/WebSocketHandler.js.map +1 -0
  35. package/lib/core/handlers/WebSocketHandler.mjs +47 -0
  36. package/lib/core/handlers/WebSocketHandler.mjs.map +1 -0
  37. package/lib/core/http.d.mts +1 -1
  38. package/lib/core/http.d.ts +1 -1
  39. package/lib/core/index.d.mts +5 -2
  40. package/lib/core/index.d.ts +5 -2
  41. package/lib/core/index.js +3 -1
  42. package/lib/core/index.js.map +1 -1
  43. package/lib/core/index.mjs +3 -1
  44. package/lib/core/index.mjs.map +1 -1
  45. package/lib/core/passthrough.d.mts +1 -1
  46. package/lib/core/passthrough.d.ts +1 -1
  47. package/lib/core/utils/HttpResponse/decorators.d.mts +1 -1
  48. package/lib/core/utils/HttpResponse/decorators.d.ts +1 -1
  49. package/lib/core/utils/executeHandlers.d.mts +1 -1
  50. package/lib/core/utils/executeHandlers.d.ts +1 -1
  51. package/lib/core/utils/executeHandlers.js +4 -0
  52. package/lib/core/utils/executeHandlers.js.map +1 -1
  53. package/lib/core/utils/executeHandlers.mjs +6 -0
  54. package/lib/core/utils/executeHandlers.mjs.map +1 -1
  55. package/lib/core/utils/handleRequest.d.mts +2 -2
  56. package/lib/core/utils/handleRequest.d.ts +2 -2
  57. package/lib/core/utils/handleRequest.js.map +1 -1
  58. package/lib/core/utils/handleRequest.mjs.map +1 -1
  59. package/lib/core/utils/handleWebSocketEvent.d.mts +10 -0
  60. package/lib/core/utils/handleWebSocketEvent.d.ts +10 -0
  61. package/lib/core/utils/handleWebSocketEvent.js +56 -0
  62. package/lib/core/utils/handleWebSocketEvent.js.map +1 -0
  63. package/lib/core/utils/handleWebSocketEvent.mjs +40 -0
  64. package/lib/core/utils/handleWebSocketEvent.mjs.map +1 -0
  65. package/lib/core/utils/internal/parseGraphQLRequest.d.mts +2 -2
  66. package/lib/core/utils/internal/parseGraphQLRequest.d.ts +2 -2
  67. package/lib/core/utils/internal/parseMultipartData.d.mts +1 -1
  68. package/lib/core/utils/internal/parseMultipartData.d.ts +1 -1
  69. package/lib/core/utils/internal/requestHandlerUtils.d.mts +1 -1
  70. package/lib/core/utils/internal/requestHandlerUtils.d.ts +1 -1
  71. package/lib/core/utils/matching/matchRequestUrl.d.mts +2 -1
  72. package/lib/core/utils/matching/matchRequestUrl.d.ts +2 -1
  73. package/lib/core/utils/matching/matchRequestUrl.js +4 -0
  74. package/lib/core/utils/matching/matchRequestUrl.js.map +1 -1
  75. package/lib/core/utils/matching/matchRequestUrl.mjs +4 -0
  76. package/lib/core/utils/matching/matchRequestUrl.mjs.map +1 -1
  77. package/lib/core/ws/WebSocketClientManager.d.mts +64 -0
  78. package/lib/core/ws/WebSocketClientManager.d.ts +64 -0
  79. package/lib/core/ws/WebSocketClientManager.js +123 -0
  80. package/lib/core/ws/WebSocketClientManager.js.map +1 -0
  81. package/lib/core/ws/WebSocketClientManager.mjs +103 -0
  82. package/lib/core/ws/WebSocketClientManager.mjs.map +1 -0
  83. package/lib/core/ws/webSocketInterceptor.d.mts +5 -0
  84. package/lib/core/ws/webSocketInterceptor.d.ts +5 -0
  85. package/lib/core/ws/webSocketInterceptor.js +26 -0
  86. package/lib/core/ws/webSocketInterceptor.js.map +1 -0
  87. package/lib/core/ws/webSocketInterceptor.mjs +6 -0
  88. package/lib/core/ws/webSocketInterceptor.mjs.map +1 -0
  89. package/lib/core/ws/ws.d.mts +44 -0
  90. package/lib/core/ws/ws.d.ts +44 -0
  91. package/lib/core/ws/ws.js +82 -0
  92. package/lib/core/ws/ws.js.map +1 -0
  93. package/lib/core/ws/ws.mjs +65 -0
  94. package/lib/core/ws/ws.mjs.map +1 -0
  95. package/lib/iife/index.js +703 -17
  96. package/lib/iife/index.js.map +1 -1
  97. package/lib/mockServiceWorker.js +1 -1
  98. package/lib/native/index.d.mts +6 -5
  99. package/lib/native/index.d.ts +6 -5
  100. package/lib/native/index.js +7 -0
  101. package/lib/native/index.js.map +1 -1
  102. package/lib/native/index.mjs +7 -0
  103. package/lib/native/index.mjs.map +1 -1
  104. package/lib/node/index.d.mts +8 -7
  105. package/lib/node/index.d.ts +8 -7
  106. package/lib/node/index.js +7 -0
  107. package/lib/node/index.js.map +1 -1
  108. package/lib/node/index.mjs +7 -0
  109. package/lib/node/index.mjs.map +1 -1
  110. package/package.json +27 -22
  111. package/src/browser/setupWorker/glossary.ts +10 -10
  112. package/src/browser/setupWorker/setupWorker.ts +17 -3
  113. package/src/core/SetupApi.ts +28 -20
  114. package/src/core/handlers/WebSocketHandler.ts +89 -0
  115. package/src/core/index.ts +3 -0
  116. package/src/core/utils/executeHandlers.ts +6 -2
  117. package/src/core/utils/handleRequest.ts +1 -2
  118. package/src/core/utils/handleWebSocketEvent.ts +49 -0
  119. package/src/core/utils/matching/matchRequestUrl.test.ts +44 -0
  120. package/src/core/utils/matching/matchRequestUrl.ts +4 -0
  121. package/src/core/ws/WebSocketClientManager.test.ts +159 -0
  122. package/src/core/ws/WebSocketClientManager.ts +170 -0
  123. package/src/core/ws/webSocketInterceptor.ts +3 -0
  124. package/src/core/ws/ws.test.ts +23 -0
  125. package/src/core/ws/ws.ts +108 -0
  126. package/src/node/SetupServerApi.ts +8 -7
  127. package/src/node/SetupServerCommonApi.ts +10 -1
  128. package/src/node/glossary.ts +5 -7
  129. package/src/node/setupServer.ts +2 -1
@@ -5,13 +5,11 @@ import {
5
5
  SharedOptions,
6
6
  } from '~/core/sharedOptions'
7
7
  import { ServiceWorkerMessage } from './start/utils/createMessageChannel'
8
- import {
9
- RequestHandler,
10
- RequestHandlerDefaultInfo,
11
- } from '~/core/handlers/RequestHandler'
8
+ import { RequestHandler } from '~/core/handlers/RequestHandler'
12
9
  import type { HttpRequestEventMap, Interceptor } from '@mswjs/interceptors'
13
- import { Path } from '~/core/utils/matching/matchRequestUrl'
14
- import { RequiredDeep } from '~/core/typeUtils'
10
+ import type { Path } from '~/core/utils/matching/matchRequestUrl'
11
+ import type { RequiredDeep } from '~/core/typeUtils'
12
+ import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
15
13
 
16
14
  export type ResolvedPath = Path | URL
17
15
 
@@ -84,7 +82,7 @@ export interface SetupWorkerInternalContext {
84
82
  startOptions: RequiredDeep<StartOptions>
85
83
  worker: ServiceWorker | null
86
84
  registration: ServiceWorkerRegistration | null
87
- getRequestHandlers(): Array<RequestHandler>
85
+ getRequestHandlers(): Array<RequestHandler | WebSocketHandler>
88
86
  requests: Map<string, Request>
89
87
  emitter: Emitter<LifeCycleEventsMap>
90
88
  keepAliveInterval?: number
@@ -208,7 +206,7 @@ export interface SetupWorker {
208
206
  *
209
207
  * @see {@link https://mswjs.io/docs/api/setup-worker/use `worker.use()` API reference}
210
208
  */
211
- use: (...handlers: RequestHandler[]) => void
209
+ use: (...handlers: Array<RequestHandler | WebSocketHandler>) => void
212
210
 
213
211
  /**
214
212
  * Marks all request handlers that respond using `res.once()` as unused.
@@ -223,14 +221,16 @@ export interface SetupWorker {
223
221
  *
224
222
  * @see {@link https://mswjs.io/docs/api/setup-worker/reset-handlers `worker.resetHandlers()` API reference}
225
223
  */
226
- resetHandlers: (...nextHandlers: RequestHandler[]) => void
224
+ resetHandlers: (
225
+ ...nextHandlers: Array<RequestHandler | WebSocketHandler>
226
+ ) => void
227
227
 
228
228
  /**
229
229
  * Returns a readonly list of currently active request handlers.
230
230
  *
231
231
  * @see {@link https://mswjs.io/docs/api/setup-worker/list-handlers `worker.listHandlers()` API reference}
232
232
  */
233
- listHandlers(): ReadonlyArray<RequestHandler<RequestHandlerDefaultInfo, any>>
233
+ listHandlers(): ReadonlyArray<RequestHandler | WebSocketHandler>
234
234
 
235
235
  /**
236
236
  * Life-cycle events.
@@ -18,9 +18,12 @@ import { createFallbackStop } from './stop/createFallbackStop'
18
18
  import { devUtils } from '~/core/utils/internal/devUtils'
19
19
  import { SetupApi } from '~/core/SetupApi'
20
20
  import { mergeRight } from '~/core/utils/internal/mergeRight'
21
- import { LifeCycleEventsMap } from '~/core/sharedOptions'
21
+ import type { LifeCycleEventsMap } from '~/core/sharedOptions'
22
+ import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
22
23
  import { SetupWorker } from './glossary'
23
24
  import { supportsReadableStreamTransfer } from '../utils/supportsReadableStreamTransfer'
25
+ import { webSocketInterceptor } from '~/core/ws/webSocketInterceptor'
26
+ import { handleWebSocketEvent } from '~/core/utils/handleWebSocketEvent'
24
27
 
25
28
  interface Listener {
26
29
  target: EventTarget
@@ -37,7 +40,7 @@ export class SetupWorkerApi
37
40
  private stopHandler: StopHandler = null as any
38
41
  private listeners: Array<Listener>
39
42
 
40
- constructor(...handlers: Array<RequestHandler>) {
43
+ constructor(...handlers: Array<RequestHandler | WebSocketHandler>) {
41
44
  super(...handlers)
42
45
 
43
46
  invariant(
@@ -176,6 +179,15 @@ export class SetupWorkerApi
176
179
  options,
177
180
  ) as SetupWorkerInternalContext['startOptions']
178
181
 
182
+ handleWebSocketEvent(() => {
183
+ return this.handlersController.currentHandlers()
184
+ })
185
+ webSocketInterceptor.apply()
186
+
187
+ this.subscriptions.push(() => {
188
+ webSocketInterceptor.dispose()
189
+ })
190
+
179
191
  return await this.startHandler(this.context.startOptions, options)
180
192
  }
181
193
 
@@ -193,6 +205,8 @@ export class SetupWorkerApi
193
205
  *
194
206
  * @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker()` API reference}
195
207
  */
196
- export function setupWorker(...handlers: Array<RequestHandler>): SetupWorker {
208
+ export function setupWorker(
209
+ ...handlers: Array<RequestHandler | WebSocketHandler>
210
+ ): SetupWorker {
197
211
  return new SetupWorkerApi(...handlers)
198
212
  }
@@ -1,38 +1,42 @@
1
1
  import { invariant } from 'outvariant'
2
2
  import { EventMap, Emitter } from 'strict-event-emitter'
3
- import {
4
- RequestHandler,
5
- RequestHandlerDefaultInfo,
6
- } from './handlers/RequestHandler'
3
+ import { RequestHandler } from './handlers/RequestHandler'
7
4
  import { LifeCycleEventEmitter } from './sharedOptions'
8
5
  import { devUtils } from './utils/internal/devUtils'
9
6
  import { pipeEvents } from './utils/internal/pipeEvents'
10
7
  import { toReadonlyArray } from './utils/internal/toReadonlyArray'
11
8
  import { Disposable } from './utils/internal/Disposable'
9
+ import type { WebSocketHandler } from './handlers/WebSocketHandler'
12
10
 
13
11
  export abstract class HandlersController {
14
- abstract prepend(runtimeHandlers: Array<RequestHandler>): void
15
- abstract reset(nextHandles: Array<RequestHandler>): void
16
- abstract currentHandlers(): Array<RequestHandler>
12
+ abstract prepend(
13
+ runtimeHandlers: Array<RequestHandler | WebSocketHandler>,
14
+ ): void
15
+ abstract reset(nextHandles: Array<RequestHandler | WebSocketHandler>): void
16
+ abstract currentHandlers(): Array<RequestHandler | WebSocketHandler>
17
17
  }
18
18
 
19
19
  export class InMemoryHandlersController implements HandlersController {
20
- private handlers: Array<RequestHandler>
20
+ private handlers: Array<RequestHandler | WebSocketHandler>
21
21
 
22
- constructor(private initialHandlers: Array<RequestHandler>) {
22
+ constructor(
23
+ private initialHandlers: Array<RequestHandler | WebSocketHandler>,
24
+ ) {
23
25
  this.handlers = [...initialHandlers]
24
26
  }
25
27
 
26
- public prepend(runtimeHandles: Array<RequestHandler>): void {
28
+ public prepend(
29
+ runtimeHandles: Array<RequestHandler | WebSocketHandler>,
30
+ ): void {
27
31
  this.handlers.unshift(...runtimeHandles)
28
32
  }
29
33
 
30
- public reset(nextHandlers: Array<RequestHandler>): void {
34
+ public reset(nextHandlers: Array<RequestHandler | WebSocketHandler>): void {
31
35
  this.handlers =
32
36
  nextHandlers.length > 0 ? [...nextHandlers] : [...this.initialHandlers]
33
37
  }
34
38
 
35
- public currentHandlers(): Array<RequestHandler> {
39
+ public currentHandlers(): Array<RequestHandler | WebSocketHandler> {
36
40
  return this.handlers
37
41
  }
38
42
  }
@@ -47,7 +51,7 @@ export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
47
51
 
48
52
  public readonly events: LifeCycleEventEmitter<EventsMap>
49
53
 
50
- constructor(...initialHandlers: Array<RequestHandler>) {
54
+ constructor(...initialHandlers: Array<RequestHandler | WebSocketHandler>) {
51
55
  super()
52
56
 
53
57
  invariant(
@@ -71,12 +75,14 @@ export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
71
75
  })
72
76
  }
73
77
 
74
- private validateHandlers(handlers: ReadonlyArray<RequestHandler>): boolean {
78
+ private validateHandlers(handlers: ReadonlyArray<unknown>): boolean {
75
79
  // Guard against incorrect call signature of the setup API.
76
80
  return handlers.every((handler) => !Array.isArray(handler))
77
81
  }
78
82
 
79
- public use(...runtimeHandlers: Array<RequestHandler>): void {
83
+ public use(
84
+ ...runtimeHandlers: Array<RequestHandler | WebSocketHandler>
85
+ ): void {
80
86
  invariant(
81
87
  this.validateHandlers(runtimeHandlers),
82
88
  devUtils.formatMessage(
@@ -89,17 +95,19 @@ export abstract class SetupApi<EventsMap extends EventMap> extends Disposable {
89
95
 
90
96
  public restoreHandlers(): void {
91
97
  this.handlersController.currentHandlers().forEach((handler) => {
92
- handler.isUsed = false
98
+ if ('isUsed' in handler) {
99
+ handler.isUsed = false
100
+ }
93
101
  })
94
102
  }
95
103
 
96
- public resetHandlers(...nextHandlers: Array<RequestHandler>): void {
104
+ public resetHandlers(
105
+ ...nextHandlers: Array<RequestHandler | WebSocketHandler>
106
+ ): void {
97
107
  this.handlersController.reset(nextHandlers)
98
108
  }
99
109
 
100
- public listHandlers(): ReadonlyArray<
101
- RequestHandler<RequestHandlerDefaultInfo, any, any>
102
- > {
110
+ public listHandlers(): ReadonlyArray<RequestHandler | WebSocketHandler> {
103
111
  return toReadonlyArray(this.handlersController.currentHandlers())
104
112
  }
105
113
 
@@ -0,0 +1,89 @@
1
+ import { Emitter } from 'strict-event-emitter'
2
+ import type {
3
+ WebSocketClientConnection,
4
+ WebSocketServerConnection,
5
+ } from '@mswjs/interceptors/WebSocket'
6
+ import {
7
+ type Match,
8
+ type Path,
9
+ type PathParams,
10
+ matchRequestUrl,
11
+ } from '../utils/matching/matchRequestUrl'
12
+
13
+ type WebSocketHandlerParsedResult = {
14
+ match: Match
15
+ }
16
+
17
+ export type WebSocketHandlerEventMap = {
18
+ connection: [
19
+ args: {
20
+ client: WebSocketClientConnection
21
+ server: WebSocketServerConnection
22
+ params: PathParams
23
+ },
24
+ ]
25
+ }
26
+
27
+ type WebSocketHandlerIncomingEvent = MessageEvent<{
28
+ client: WebSocketClientConnection
29
+ server: WebSocketServerConnection
30
+ }>
31
+
32
+ export const kEmitter = Symbol('kEmitter')
33
+ export const kDispatchEvent = Symbol('kDispatchEvent')
34
+ export const kDefaultPrevented = Symbol('kDefaultPrevented')
35
+
36
+ export class WebSocketHandler {
37
+ protected [kEmitter]: Emitter<WebSocketHandlerEventMap>
38
+
39
+ constructor(private readonly url: Path) {
40
+ this[kEmitter] = new Emitter()
41
+ }
42
+
43
+ public parse(args: {
44
+ event: WebSocketHandlerIncomingEvent
45
+ }): WebSocketHandlerParsedResult {
46
+ const connection = args.event.data
47
+ const match = matchRequestUrl(connection.client.url, this.url)
48
+
49
+ return {
50
+ match,
51
+ }
52
+ }
53
+
54
+ public predicate(args: {
55
+ event: WebSocketHandlerIncomingEvent
56
+ parsedResult: WebSocketHandlerParsedResult
57
+ }): boolean {
58
+ return args.parsedResult.match.matches
59
+ }
60
+
61
+ async [kDispatchEvent](event: MessageEvent<any>): Promise<void> {
62
+ const parsedResult = this.parse({ event })
63
+ const shouldIntercept = this.predicate({ event, parsedResult })
64
+
65
+ if (!shouldIntercept) {
66
+ return
67
+ }
68
+
69
+ // Account for other matching event handlers that've already prevented this event.
70
+ if (!Reflect.get(event, kDefaultPrevented)) {
71
+ // At this point, the WebSocket connection URL has matched the handler.
72
+ // Prevent the default behavior of establishing the connection as-is.
73
+ // Use internal symbol because we aren't actually dispatching this
74
+ // event. Events can only marked as cancelable and can be prevented
75
+ // when dispatched on an EventTarget.
76
+ Reflect.set(event, kDefaultPrevented, true)
77
+ }
78
+
79
+ const connection = event.data
80
+
81
+ // Emit the connection event on the handler.
82
+ // This is what the developer adds listeners for.
83
+ this[kEmitter].emit('connection', {
84
+ client: connection.client,
85
+ server: connection.server,
86
+ params: parsedResult.match.params || {},
87
+ })
88
+ }
89
+ }
package/src/core/index.ts CHANGED
@@ -9,6 +9,9 @@ export { HttpHandler, HttpMethods } from './handlers/HttpHandler'
9
9
  export { graphql } from './graphql'
10
10
  export { GraphQLHandler } from './handlers/GraphQLHandler'
11
11
 
12
+ /* WebSocket */
13
+ export { ws } from './ws/ws'
14
+
12
15
  /* Utils */
13
16
  export { matchRequestUrl } from './utils/matching/matchRequestUrl'
14
17
  export * from './utils/handleRequest'
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  RequestHandler,
3
- RequestHandlerExecutionResult,
3
+ type RequestHandlerExecutionResult,
4
4
  } from '../handlers/RequestHandler'
5
5
 
6
6
  export interface HandlersExecutionResult {
@@ -18,7 +18,7 @@ export interface ResponseResolutionContext {
18
18
  * Returns the execution result object containing any matching request
19
19
  * handler and any mocked response it returned.
20
20
  */
21
- export const executeHandlers = async <Handlers extends Array<RequestHandler>>({
21
+ export const executeHandlers = async <Handlers extends Array<unknown>>({
22
22
  request,
23
23
  requestId,
24
24
  handlers,
@@ -33,6 +33,10 @@ export const executeHandlers = async <Handlers extends Array<RequestHandler>>({
33
33
  let result: RequestHandlerExecutionResult<any> | null = null
34
34
 
35
35
  for (const handler of handlers) {
36
+ if (!(handler instanceof RequestHandler)) {
37
+ continue
38
+ }
39
+
36
40
  result = await handler.run({ request, requestId, resolutionContext })
37
41
 
38
42
  // If the handler produces some result for this request,
@@ -1,6 +1,5 @@
1
1
  import { until } from '@open-draft/until'
2
2
  import { Emitter } from 'strict-event-emitter'
3
- import { RequestHandler } from '../handlers/RequestHandler'
4
3
  import { LifeCycleEventsMap, SharedOptions } from '../sharedOptions'
5
4
  import { RequiredDeep } from '../typeUtils'
6
5
  import { HandlersExecutionResult, executeHandlers } from './executeHandlers'
@@ -45,7 +44,7 @@ export interface HandleRequestOptions {
45
44
  export async function handleRequest(
46
45
  request: Request,
47
46
  requestId: string,
48
- handlers: Array<RequestHandler>,
47
+ handlers: Array<unknown>,
49
48
  options: RequiredDeep<SharedOptions>,
50
49
  emitter: Emitter<LifeCycleEventsMap>,
51
50
  handleRequestOptions?: HandleRequestOptions,
@@ -0,0 +1,49 @@
1
+ import { RequestHandler } from '../handlers/RequestHandler'
2
+ import {
3
+ WebSocketHandler,
4
+ kDefaultPrevented,
5
+ kDispatchEvent,
6
+ } from '../handlers/WebSocketHandler'
7
+ import { webSocketInterceptor } from '../ws/webSocketInterceptor'
8
+
9
+ export function handleWebSocketEvent(
10
+ getCurrentHandlers: () => Array<RequestHandler | WebSocketHandler>,
11
+ ) {
12
+ webSocketInterceptor.on('connection', (connection) => {
13
+ const handlers = getCurrentHandlers()
14
+
15
+ const connectionEvent = new MessageEvent('connection', {
16
+ data: connection,
17
+ /**
18
+ * @note This message event should be marked as "cancelable"
19
+ * to have its default prevented using "event.preventDefault()".
20
+ * There's a bug in Node.js that breaks the "cancelable" flag.
21
+ * @see https://github.com/nodejs/node/issues/51767
22
+ */
23
+ })
24
+
25
+ Object.defineProperty(connectionEvent, kDefaultPrevented, {
26
+ enumerable: false,
27
+ writable: true,
28
+ value: false,
29
+ })
30
+
31
+ // Iterate over the handlers and forward the connection
32
+ // event to WebSocket event handlers. This is equivalent
33
+ // to dispatching that event onto multiple listeners.
34
+ for (const handler of handlers) {
35
+ if (handler instanceof WebSocketHandler) {
36
+ handler[kDispatchEvent](connectionEvent)
37
+ }
38
+ }
39
+
40
+ // If none of the "ws" handlers matched,
41
+ // establish the WebSocket connection as-is.
42
+ if (!Reflect.get(connectionEvent, kDefaultPrevented)) {
43
+ connection.server.connect()
44
+ connection.client.addEventListener('message', (event) => {
45
+ connection.server.send(event.data)
46
+ })
47
+ }
48
+ })
49
+ }
@@ -61,6 +61,50 @@ describe('matchRequestUrl', () => {
61
61
  expect(match).toHaveProperty('matches', false)
62
62
  expect(match).toHaveProperty('params', {})
63
63
  })
64
+
65
+ test('returns true for matching WebSocket URL', () => {
66
+ expect(
67
+ matchRequestUrl(new URL('ws://test.mswjs.io'), 'ws://test.mswjs.io'),
68
+ ).toEqual({
69
+ matches: true,
70
+ params: {},
71
+ })
72
+ expect(
73
+ matchRequestUrl(new URL('wss://test.mswjs.io'), 'wss://test.mswjs.io'),
74
+ ).toEqual({
75
+ matches: true,
76
+ params: {},
77
+ })
78
+ })
79
+
80
+ test('returns false for non-matching WebSocket URL', () => {
81
+ expect(
82
+ matchRequestUrl(new URL('ws://test.mswjs.io'), 'ws://foo.mswjs.io'),
83
+ ).toEqual({
84
+ matches: false,
85
+ params: {},
86
+ })
87
+ expect(
88
+ matchRequestUrl(new URL('wss://test.mswjs.io'), 'wss://completely.diff'),
89
+ ).toEqual({
90
+ matches: false,
91
+ params: {},
92
+ })
93
+ })
94
+
95
+ test('returns path parameters when matched a WebSocket URL', () => {
96
+ expect(
97
+ matchRequestUrl(
98
+ new URL('wss://test.mswjs.io'),
99
+ 'wss://:service.mswjs.io',
100
+ ),
101
+ ).toEqual({
102
+ matches: true,
103
+ params: {
104
+ service: 'test',
105
+ },
106
+ })
107
+ })
64
108
  })
65
109
 
66
110
  describe('coercePath', () => {
@@ -71,3 +71,7 @@ export function matchRequestUrl(url: URL, path: Path, baseUrl?: string): Match {
71
71
  params,
72
72
  }
73
73
  }
74
+
75
+ export function isPath(value: unknown): value is Path {
76
+ return typeof value === 'string' || value instanceof RegExp
77
+ }
@@ -0,0 +1,159 @@
1
+ /**
2
+ * @vitest-environment node-websocket
3
+ */
4
+ import { randomUUID } from 'node:crypto'
5
+ import {
6
+ WebSocketClientConnection,
7
+ WebSocketTransport,
8
+ } from '@mswjs/interceptors/WebSocket'
9
+ import {
10
+ WebSocketClientManager,
11
+ WebSocketBroadcastChannelMessage,
12
+ WebSocketRemoteClientConnection,
13
+ } from './WebSocketClientManager'
14
+
15
+ const channel = new BroadcastChannel('test:channel')
16
+ vi.spyOn(channel, 'postMessage')
17
+
18
+ const socket = new WebSocket('ws://localhost')
19
+ const transport = {
20
+ onOutgoing: vi.fn(),
21
+ onIncoming: vi.fn(),
22
+ onClose: vi.fn(),
23
+ send: vi.fn(),
24
+ close: vi.fn(),
25
+ } satisfies WebSocketTransport
26
+
27
+ afterEach(() => {
28
+ vi.resetAllMocks()
29
+ })
30
+
31
+ it('adds a client from this runtime to the list of clients', () => {
32
+ const manager = new WebSocketClientManager(channel)
33
+ const connection = new WebSocketClientConnection(socket, transport)
34
+
35
+ manager.addConnection(connection)
36
+
37
+ // Must add the client to the list of clients.
38
+ expect(Array.from(manager.clients.values())).toEqual([connection])
39
+
40
+ // Must emit the connection open event to notify other runtimes.
41
+ expect(channel.postMessage).toHaveBeenCalledWith({
42
+ type: 'connection:open',
43
+ payload: {
44
+ clientId: connection.id,
45
+ url: socket.url,
46
+ },
47
+ } satisfies WebSocketBroadcastChannelMessage)
48
+ })
49
+
50
+ it('adds a client from another runtime to the list of clients', async () => {
51
+ const clientId = randomUUID()
52
+ const url = new URL('ws://localhost')
53
+ const manager = new WebSocketClientManager(channel)
54
+
55
+ channel.dispatchEvent(
56
+ new MessageEvent<WebSocketBroadcastChannelMessage>('message', {
57
+ data: {
58
+ type: 'connection:open',
59
+ payload: {
60
+ clientId,
61
+ url: url.href,
62
+ },
63
+ },
64
+ }),
65
+ )
66
+
67
+ await vi.waitFor(() => {
68
+ expect(Array.from(manager.clients.values())).toEqual([
69
+ new WebSocketRemoteClientConnection(clientId, url, channel),
70
+ ])
71
+ })
72
+ })
73
+
74
+ it('replays a "send" event coming from another runtime', async () => {
75
+ const manager = new WebSocketClientManager(channel)
76
+ const connection = new WebSocketClientConnection(socket, transport)
77
+ manager.addConnection(connection)
78
+ vi.spyOn(connection, 'send')
79
+
80
+ // Emulate another runtime signaling this connection to receive data.
81
+ channel.dispatchEvent(
82
+ new MessageEvent<WebSocketBroadcastChannelMessage>('message', {
83
+ data: {
84
+ type: 'extraneous:send',
85
+ payload: {
86
+ clientId: connection.id,
87
+ data: 'hello',
88
+ },
89
+ },
90
+ }),
91
+ )
92
+
93
+ await vi.waitFor(() => {
94
+ // Must execute the requested operation on the connection.
95
+ expect(connection.send).toHaveBeenCalledWith('hello')
96
+ expect(connection.send).toHaveBeenCalledTimes(1)
97
+ })
98
+ })
99
+
100
+ it('replays a "close" event coming from another runtime', async () => {
101
+ const manager = new WebSocketClientManager(channel)
102
+ const connection = new WebSocketClientConnection(socket, transport)
103
+ manager.addConnection(connection)
104
+ vi.spyOn(connection, 'close')
105
+
106
+ // Emulate another runtime signaling this connection to close.
107
+ channel.dispatchEvent(
108
+ new MessageEvent<WebSocketBroadcastChannelMessage>('message', {
109
+ data: {
110
+ type: 'extraneous:close',
111
+ payload: {
112
+ clientId: connection.id,
113
+ code: 1000,
114
+ reason: 'Normal closure',
115
+ },
116
+ },
117
+ }),
118
+ )
119
+
120
+ await vi.waitFor(() => {
121
+ // Must execute the requested operation on the connection.
122
+ expect(connection.close).toHaveBeenCalledWith(1000, 'Normal closure')
123
+ expect(connection.close).toHaveBeenCalledTimes(1)
124
+ })
125
+ })
126
+
127
+ it('removes the extraneous message listener when the connection closes', async () => {
128
+ const manager = new WebSocketClientManager(channel)
129
+ const connection = new WebSocketClientConnection(socket, transport)
130
+ vi.spyOn(connection, 'close').mockImplementationOnce(() => {
131
+ /**
132
+ * @note This is a nasty hack so we don't have to uncouple
133
+ * the connection from transport. Creating a mock transport
134
+ * is difficult because it relies on the `WebSocketOverride` class.
135
+ * All we care here is that closing the connection triggers
136
+ * the transport closure, which it always does.
137
+ */
138
+ connection['transport'].onClose()
139
+ })
140
+ vi.spyOn(connection, 'send')
141
+
142
+ manager.addConnection(connection)
143
+ connection.close()
144
+
145
+ // Signals from other runtimes have no effect on the closed connection.
146
+ channel.dispatchEvent(
147
+ new MessageEvent<WebSocketBroadcastChannelMessage>('message', {
148
+ data: {
149
+ type: 'extraneous:send',
150
+ payload: {
151
+ clientId: connection.id,
152
+ data: 'hello',
153
+ },
154
+ },
155
+ }),
156
+ )
157
+
158
+ expect(connection.send).not.toHaveBeenCalled()
159
+ })