@msw/playwright 0.4.2 → 0.4.4
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.
- package/build/index.d.ts +16 -3
- package/build/index.js +34 -11
- package/package.json +10 -9
- package/src/fixture.ts +432 -0
- package/src/index.ts +5 -367
package/build/index.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { LifeCycleEventsMap, RequestHandler, SetupApi, WebSocketHandler } from "msw";
|
|
1
|
+
import { LifeCycleEventsMap, RequestHandler, SetupApi, UnhandledRequestStrategy, WebSocketHandler } from "msw";
|
|
2
2
|
import { Page, PlaywrightTestArgs, PlaywrightWorkerArgs, TestFixture } from "@playwright/test";
|
|
3
3
|
|
|
4
|
-
//#region src/
|
|
4
|
+
//#region src/fixture.d.ts
|
|
5
5
|
interface CreateNetworkFixtureArgs {
|
|
6
6
|
initialHandlers: Array<RequestHandler | WebSocketHandler>;
|
|
7
|
+
onUnhandledRequest?: UnhandledRequestStrategy;
|
|
7
8
|
}
|
|
8
9
|
/**
|
|
9
10
|
* Creates a fixture that controls the network in your tests.
|
|
@@ -27,11 +28,23 @@ interface CreateNetworkFixtureArgs {
|
|
|
27
28
|
declare function createNetworkFixture(args?: CreateNetworkFixtureArgs): [TestFixture<NetworkFixture, PlaywrightTestArgs & PlaywrightWorkerArgs>, {
|
|
28
29
|
auto: boolean;
|
|
29
30
|
}];
|
|
31
|
+
/**
|
|
32
|
+
* @note Use a match-all RegExp with an optional group as the predicate
|
|
33
|
+
* for the `page.route()`/`page.unroute()` calls. Playwright treats given RegExp
|
|
34
|
+
* as the handler ID, which allows us to remove only those handlers introduces by us
|
|
35
|
+
* without carrying the reference to the handler function around.
|
|
36
|
+
*/
|
|
37
|
+
|
|
30
38
|
declare class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
|
|
31
|
-
|
|
39
|
+
protected args: {
|
|
40
|
+
page: Page;
|
|
41
|
+
initialHandlers: Array<RequestHandler | WebSocketHandler>;
|
|
42
|
+
onUnhandledRequest?: UnhandledRequestStrategy;
|
|
43
|
+
};
|
|
32
44
|
constructor(args: {
|
|
33
45
|
page: Page;
|
|
34
46
|
initialHandlers: Array<RequestHandler | WebSocketHandler>;
|
|
47
|
+
onUnhandledRequest?: UnhandledRequestStrategy;
|
|
35
48
|
});
|
|
36
49
|
start(): Promise<void>;
|
|
37
50
|
stop(): Promise<void>;
|
package/build/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { invariant } from "outvariant";
|
|
2
|
-
import { RequestHandler, SetupApi, WebSocketHandler,
|
|
2
|
+
import { RequestHandler, SetupApi, WebSocketHandler, handleRequest } from "msw";
|
|
3
3
|
import { CancelableCloseEvent, CancelableMessageEvent } from "@mswjs/interceptors/WebSocket";
|
|
4
4
|
|
|
5
|
-
//#region src/
|
|
5
|
+
//#region src/fixture.ts
|
|
6
6
|
/**
|
|
7
7
|
* Creates a fixture that controls the network in your tests.
|
|
8
8
|
*
|
|
@@ -26,29 +26,40 @@ function createNetworkFixture(args) {
|
|
|
26
26
|
return [async ({ page }, use) => {
|
|
27
27
|
const worker = new NetworkFixture({
|
|
28
28
|
page,
|
|
29
|
-
initialHandlers: args?.initialHandlers || []
|
|
29
|
+
initialHandlers: args?.initialHandlers || [],
|
|
30
|
+
onUnhandledRequest: args?.onUnhandledRequest
|
|
30
31
|
});
|
|
31
32
|
await worker.start();
|
|
32
33
|
await use(worker);
|
|
33
34
|
await worker.stop();
|
|
34
35
|
}, { auto: true }];
|
|
35
36
|
}
|
|
37
|
+
/**
|
|
38
|
+
* @note Use a match-all RegExp with an optional group as the predicate
|
|
39
|
+
* for the `page.route()`/`page.unroute()` calls. Playwright treats given RegExp
|
|
40
|
+
* as the handler ID, which allows us to remove only those handlers introduces by us
|
|
41
|
+
* without carrying the reference to the handler function around.
|
|
42
|
+
*/
|
|
43
|
+
const INTERNAL_MATCH_ALL_REG_EXP = /.+(__MSW_PLAYWRIGHT_PREDICATE__)?/;
|
|
36
44
|
var NetworkFixture = class extends SetupApi {
|
|
37
|
-
#page;
|
|
38
45
|
constructor(args) {
|
|
39
46
|
super(...args.initialHandlers);
|
|
40
|
-
this
|
|
47
|
+
this.args = args;
|
|
41
48
|
}
|
|
42
49
|
async start() {
|
|
43
|
-
await this
|
|
50
|
+
await this.args.page.route(INTERNAL_MATCH_ALL_REG_EXP, async (route, request) => {
|
|
44
51
|
const fetchRequest = new Request(request.url(), {
|
|
45
52
|
method: request.method(),
|
|
46
53
|
headers: new Headers(await request.allHeaders()),
|
|
47
54
|
body: request.postDataBuffer()
|
|
48
55
|
});
|
|
49
|
-
const
|
|
56
|
+
const handlers = this.handlersController.currentHandlers().filter((handler) => {
|
|
50
57
|
return handler instanceof RequestHandler;
|
|
51
|
-
})
|
|
58
|
+
});
|
|
59
|
+
const response = await handleRequest(fetchRequest, crypto.randomUUID(), handlers, { onUnhandledRequest: this.args.onUnhandledRequest || "warn" }, this.emitter, { resolutionContext: {
|
|
60
|
+
quiet: true,
|
|
61
|
+
baseUrl: this.getPageUrl()
|
|
62
|
+
} });
|
|
52
63
|
if (response) {
|
|
53
64
|
if (response.status === 0) {
|
|
54
65
|
route.abort();
|
|
@@ -63,7 +74,7 @@ var NetworkFixture = class extends SetupApi {
|
|
|
63
74
|
}
|
|
64
75
|
route.continue();
|
|
65
76
|
});
|
|
66
|
-
await this
|
|
77
|
+
await this.args.page.routeWebSocket(INTERNAL_MATCH_ALL_REG_EXP, async (ws) => {
|
|
67
78
|
const allWebSocketHandlers = this.handlersController.currentHandlers().filter((handler) => {
|
|
68
79
|
return handler instanceof WebSocketHandler;
|
|
69
80
|
});
|
|
@@ -82,7 +93,8 @@ var NetworkFixture = class extends SetupApi {
|
|
|
82
93
|
}
|
|
83
94
|
async stop() {
|
|
84
95
|
super.dispose();
|
|
85
|
-
await this.#page.unroute(
|
|
96
|
+
await this.#page.unroute(INTERNAL_MATCH_ALL_REG_EXP);
|
|
97
|
+
await unrouteWebSocket(this.#page, INTERNAL_MATCH_ALL_REG_EXP);
|
|
86
98
|
}
|
|
87
99
|
getPageUrl() {
|
|
88
100
|
const url = this.#page.url();
|
|
@@ -228,6 +240,17 @@ var PlaywrightWebSocketServerConnection = class {
|
|
|
228
240
|
console.warn("@msw/playwright: WebSocketRoute does not support removing event listeners");
|
|
229
241
|
}
|
|
230
242
|
};
|
|
243
|
+
/**
|
|
244
|
+
* Custom implementation of the missing `page.unrouteWebSocket()` to remove
|
|
245
|
+
* WebSocket route handlers from the page. Loosely inspired by `page.unroute()`.
|
|
246
|
+
*/
|
|
247
|
+
async function unrouteWebSocket(page, url, handler) {
|
|
248
|
+
if (!("_webSocketRoutes" in page && Array.isArray(page._webSocketRoutes))) return;
|
|
249
|
+
for (let i = page._webSocketRoutes.length - 1; i >= 0; i--) {
|
|
250
|
+
const route = page._webSocketRoutes[i];
|
|
251
|
+
if (route.url === url && (handler != null ? route.handler === handler : true)) page._webSocketRoutes.splice(i, 1);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
231
254
|
|
|
232
255
|
//#endregion
|
|
233
|
-
export {
|
|
256
|
+
export { createNetworkFixture };
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@msw/playwright",
|
|
4
|
-
"version": "0.4.
|
|
4
|
+
"version": "0.4.4",
|
|
5
5
|
"description": "Mock Service Worker binding for Playwright",
|
|
6
6
|
"main": "./build/index.js",
|
|
7
7
|
"types": "./build/index.d.ts",
|
|
@@ -20,8 +20,7 @@
|
|
|
20
20
|
"funding": "https://github.com/sponsors/mswjs",
|
|
21
21
|
"homepage": "https://mswjs.io",
|
|
22
22
|
"repository": {
|
|
23
|
-
"
|
|
24
|
-
"url": "git+https://github.com/mswjs/playwright.git"
|
|
23
|
+
"url": "https://github.com/mswjs/playwright"
|
|
25
24
|
},
|
|
26
25
|
"author": {
|
|
27
26
|
"name": "Artem Zakharchenko",
|
|
@@ -38,16 +37,18 @@
|
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
39
|
"@epic-web/test-server": "^0.1.6",
|
|
41
|
-
"@ossjs/release": "^0.
|
|
42
|
-
"@playwright/test": "^1.
|
|
40
|
+
"@ossjs/release": "^0.10.1",
|
|
41
|
+
"@playwright/test": "^1.57.0",
|
|
43
42
|
"@types/node": "^22.15.29",
|
|
44
|
-
"
|
|
43
|
+
"@types/sinon": "^21.0.0",
|
|
44
|
+
"msw": "^2.12.7",
|
|
45
|
+
"sinon": "^21.0.1",
|
|
45
46
|
"tsdown": "^0.12.7",
|
|
46
|
-
"typescript": "^5.
|
|
47
|
-
"vite": "^7.
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"vite": "^7.3.1"
|
|
48
49
|
},
|
|
49
50
|
"dependencies": {
|
|
50
|
-
"@mswjs/interceptors": "^0.
|
|
51
|
+
"@mswjs/interceptors": "^0.40.0",
|
|
51
52
|
"outvariant": "^1.4.3"
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
package/src/fixture.ts
ADDED
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { invariant } from 'outvariant'
|
|
2
|
+
import type {
|
|
3
|
+
Page,
|
|
4
|
+
PlaywrightTestArgs,
|
|
5
|
+
PlaywrightWorkerArgs,
|
|
6
|
+
Request,
|
|
7
|
+
Route,
|
|
8
|
+
TestFixture,
|
|
9
|
+
WebSocketRoute,
|
|
10
|
+
} from '@playwright/test'
|
|
11
|
+
import {
|
|
12
|
+
type LifeCycleEventsMap,
|
|
13
|
+
type UnhandledRequestStrategy,
|
|
14
|
+
SetupApi,
|
|
15
|
+
RequestHandler,
|
|
16
|
+
WebSocketHandler,
|
|
17
|
+
handleRequest,
|
|
18
|
+
} from 'msw'
|
|
19
|
+
import {
|
|
20
|
+
type WebSocketClientEventMap,
|
|
21
|
+
type WebSocketData,
|
|
22
|
+
type WebSocketServerEventMap,
|
|
23
|
+
CancelableMessageEvent,
|
|
24
|
+
CancelableCloseEvent,
|
|
25
|
+
WebSocketClientConnectionProtocol,
|
|
26
|
+
WebSocketServerConnectionProtocol,
|
|
27
|
+
} from '@mswjs/interceptors/WebSocket'
|
|
28
|
+
|
|
29
|
+
export interface CreateNetworkFixtureArgs {
|
|
30
|
+
initialHandlers?: Array<RequestHandler | WebSocketHandler>
|
|
31
|
+
onUnhandledRequest?: UnhandledRequestStrategy
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Creates a fixture that controls the network in your tests.
|
|
36
|
+
*
|
|
37
|
+
* @note The returned fixture already has the `auto` option set to `true`.
|
|
38
|
+
*
|
|
39
|
+
* **Usage**
|
|
40
|
+
* ```ts
|
|
41
|
+
* import { test as testBase } from '@playwright/test'
|
|
42
|
+
* import { createNetworkFixture, type WorkerFixture } from '@msw/playwright'
|
|
43
|
+
*
|
|
44
|
+
* interface Fixtures {
|
|
45
|
+
* network: WorkerFixture
|
|
46
|
+
* }
|
|
47
|
+
*
|
|
48
|
+
* export const test = testBase.extend<Fixtures>({
|
|
49
|
+
* network: createNetworkFixture()
|
|
50
|
+
* })
|
|
51
|
+
* ```
|
|
52
|
+
*/
|
|
53
|
+
export function createNetworkFixture(
|
|
54
|
+
args?: CreateNetworkFixtureArgs,
|
|
55
|
+
): [
|
|
56
|
+
TestFixture<NetworkFixture, PlaywrightTestArgs & PlaywrightWorkerArgs>,
|
|
57
|
+
{ auto: boolean },
|
|
58
|
+
] {
|
|
59
|
+
return [
|
|
60
|
+
async ({ page }, use) => {
|
|
61
|
+
const worker = new NetworkFixture({
|
|
62
|
+
page,
|
|
63
|
+
initialHandlers: args?.initialHandlers || [],
|
|
64
|
+
onUnhandledRequest: args?.onUnhandledRequest,
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
await worker.start()
|
|
68
|
+
await use(worker)
|
|
69
|
+
await worker.stop()
|
|
70
|
+
},
|
|
71
|
+
{ auto: true },
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @note Use a match-all RegExp with an optional group as the predicate
|
|
77
|
+
* for the `page.route()`/`page.unroute()` calls. Playwright treats given RegExp
|
|
78
|
+
* as the handler ID, which allows us to remove only those handlers introduces by us
|
|
79
|
+
* without carrying the reference to the handler function around.
|
|
80
|
+
*/
|
|
81
|
+
export const INTERNAL_MATCH_ALL_REG_EXP = /.+(__MSW_PLAYWRIGHT_PREDICATE__)?/
|
|
82
|
+
|
|
83
|
+
export class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
|
|
84
|
+
constructor(
|
|
85
|
+
protected args: {
|
|
86
|
+
page: Page
|
|
87
|
+
initialHandlers: Array<RequestHandler | WebSocketHandler>
|
|
88
|
+
onUnhandledRequest?: UnhandledRequestStrategy
|
|
89
|
+
},
|
|
90
|
+
) {
|
|
91
|
+
super(...args.initialHandlers)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
public async start(): Promise<void> {
|
|
95
|
+
// Handle HTTP requests.
|
|
96
|
+
await this.args.page.route(
|
|
97
|
+
INTERNAL_MATCH_ALL_REG_EXP,
|
|
98
|
+
async (route: Route, request: Request) => {
|
|
99
|
+
const fetchRequest = new Request(request.url(), {
|
|
100
|
+
method: request.method(),
|
|
101
|
+
headers: new Headers(await request.allHeaders()),
|
|
102
|
+
body: request.postDataBuffer(),
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
const handlers = this.handlersController
|
|
106
|
+
.currentHandlers()
|
|
107
|
+
.filter((handler) => {
|
|
108
|
+
return handler instanceof RequestHandler
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* @note Use `handleRequest` instead of `getResponse` so we can pass
|
|
113
|
+
* the `onUnhandledRequest` option as-is and benefit from MSW's default behaviors.
|
|
114
|
+
*/
|
|
115
|
+
const response = await handleRequest(
|
|
116
|
+
fetchRequest,
|
|
117
|
+
crypto.randomUUID(),
|
|
118
|
+
handlers,
|
|
119
|
+
{
|
|
120
|
+
onUnhandledRequest: this.args.onUnhandledRequest || 'bypass',
|
|
121
|
+
},
|
|
122
|
+
this.emitter,
|
|
123
|
+
{
|
|
124
|
+
resolutionContext: {
|
|
125
|
+
quiet: true,
|
|
126
|
+
baseUrl: this.getPageUrl(),
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
if (response) {
|
|
132
|
+
if (response.status === 0) {
|
|
133
|
+
route.abort()
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
route.fulfill({
|
|
138
|
+
status: response.status,
|
|
139
|
+
headers: Object.fromEntries(response.headers),
|
|
140
|
+
body: response.body
|
|
141
|
+
? Buffer.from(await response.arrayBuffer())
|
|
142
|
+
: undefined,
|
|
143
|
+
})
|
|
144
|
+
return
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
route.continue()
|
|
148
|
+
},
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
// Handle WebSocket connections.
|
|
152
|
+
await this.args.page.routeWebSocket(
|
|
153
|
+
INTERNAL_MATCH_ALL_REG_EXP,
|
|
154
|
+
async (ws) => {
|
|
155
|
+
const allWebSocketHandlers = this.handlersController
|
|
156
|
+
.currentHandlers()
|
|
157
|
+
.filter((handler) => {
|
|
158
|
+
return handler instanceof WebSocketHandler
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
if (allWebSocketHandlers.length === 0) {
|
|
162
|
+
ws.connectToServer()
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const client = new PlaywrightWebSocketClientConnection(ws)
|
|
167
|
+
const server = new PlaywrightWebSocketServerConnection(ws)
|
|
168
|
+
|
|
169
|
+
for (const handler of allWebSocketHandlers) {
|
|
170
|
+
await handler.run(
|
|
171
|
+
{
|
|
172
|
+
client,
|
|
173
|
+
server,
|
|
174
|
+
info: { protocols: [] },
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
baseUrl: this.getPageUrl(),
|
|
178
|
+
},
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
public async stop(): Promise<void> {
|
|
186
|
+
super.dispose()
|
|
187
|
+
await this.args.page.unroute(INTERNAL_MATCH_ALL_REG_EXP)
|
|
188
|
+
await unrouteWebSocket(this.args.page, INTERNAL_MATCH_ALL_REG_EXP)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
private getPageUrl(): string | undefined {
|
|
192
|
+
const url = this.args.page.url()
|
|
193
|
+
return url !== 'about:blank' ? url : undefined
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
class PlaywrightWebSocketClientConnection
|
|
198
|
+
implements WebSocketClientConnectionProtocol
|
|
199
|
+
{
|
|
200
|
+
public id: string
|
|
201
|
+
public url: URL
|
|
202
|
+
|
|
203
|
+
constructor(protected readonly ws: WebSocketRoute) {
|
|
204
|
+
this.id = crypto.randomUUID()
|
|
205
|
+
this.url = new URL(ws.url())
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
public send(data: WebSocketData): void {
|
|
209
|
+
if (data instanceof Blob) {
|
|
210
|
+
/**
|
|
211
|
+
* @note Playwright does not support sending Blob data.
|
|
212
|
+
* Read the blob as buffer, then send the buffer instead.
|
|
213
|
+
*/
|
|
214
|
+
data.bytes().then((bytes) => {
|
|
215
|
+
this.ws.send(Buffer.from(bytes))
|
|
216
|
+
})
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (typeof data === 'string') {
|
|
221
|
+
this.ws.send(data)
|
|
222
|
+
return
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
this.ws.send(
|
|
226
|
+
/**
|
|
227
|
+
* @note Forcefully cast all data to Buffer because Playwright
|
|
228
|
+
* has trouble digesting ArrayBuffer and Blob directly.
|
|
229
|
+
*/
|
|
230
|
+
Buffer.from(
|
|
231
|
+
/**
|
|
232
|
+
* @note Playwright type definitions are tailored to Node.js
|
|
233
|
+
* while MSW describes all data types that can be sent over
|
|
234
|
+
* the WebSocket protocol, like ArrayBuffer and Blob.
|
|
235
|
+
*/
|
|
236
|
+
data as any,
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
public close(code?: number, reason?: string): void {
|
|
242
|
+
const resolvedCode = code ?? 1000
|
|
243
|
+
this.ws.close({ code: resolvedCode, reason })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
public addEventListener<EventType extends keyof WebSocketClientEventMap>(
|
|
247
|
+
type: EventType,
|
|
248
|
+
listener: (
|
|
249
|
+
this: WebSocket,
|
|
250
|
+
event: WebSocketClientEventMap[EventType],
|
|
251
|
+
) => void,
|
|
252
|
+
options?: AddEventListenerOptions | boolean,
|
|
253
|
+
): void {
|
|
254
|
+
/**
|
|
255
|
+
* @note Playwright does not expose the actual WebSocket reference.
|
|
256
|
+
*/
|
|
257
|
+
const target = {} as WebSocket
|
|
258
|
+
|
|
259
|
+
switch (type) {
|
|
260
|
+
case 'message': {
|
|
261
|
+
this.ws.onMessage((data) => {
|
|
262
|
+
listener.call(
|
|
263
|
+
target,
|
|
264
|
+
new CancelableMessageEvent('message', {
|
|
265
|
+
data,
|
|
266
|
+
}) as any,
|
|
267
|
+
)
|
|
268
|
+
})
|
|
269
|
+
break
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
case 'close': {
|
|
273
|
+
this.ws.onClose((code, reason) => {
|
|
274
|
+
listener.call(
|
|
275
|
+
target,
|
|
276
|
+
new CancelableCloseEvent('close', {
|
|
277
|
+
code,
|
|
278
|
+
reason,
|
|
279
|
+
}) as any,
|
|
280
|
+
)
|
|
281
|
+
})
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
public removeEventListener<EventType extends keyof WebSocketClientEventMap>(
|
|
288
|
+
event: EventType,
|
|
289
|
+
listener: (
|
|
290
|
+
this: WebSocket,
|
|
291
|
+
event: WebSocketClientEventMap[EventType],
|
|
292
|
+
) => void,
|
|
293
|
+
options?: EventListenerOptions | boolean,
|
|
294
|
+
): void {
|
|
295
|
+
console.warn(
|
|
296
|
+
'@msw/playwright: WebSocketRoute does not support removing event listeners',
|
|
297
|
+
)
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
class PlaywrightWebSocketServerConnection
|
|
302
|
+
implements WebSocketServerConnectionProtocol
|
|
303
|
+
{
|
|
304
|
+
#server?: WebSocketRoute
|
|
305
|
+
#bufferedEvents: Array<
|
|
306
|
+
Parameters<WebSocketServerConnectionProtocol['addEventListener']>
|
|
307
|
+
>
|
|
308
|
+
#bufferedData: Array<WebSocketData>
|
|
309
|
+
|
|
310
|
+
constructor(protected readonly ws: WebSocketRoute) {
|
|
311
|
+
this.#bufferedEvents = []
|
|
312
|
+
this.#bufferedData = []
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
public connect(): void {
|
|
316
|
+
this.#server = this.ws.connectToServer()
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* @note Playwright does not support event buffering.
|
|
320
|
+
* Manually add event listeners that might have been registered
|
|
321
|
+
* before `connect()` was called.
|
|
322
|
+
*/
|
|
323
|
+
for (const [type, listener, options] of this.#bufferedEvents) {
|
|
324
|
+
this.addEventListener(type, listener, options)
|
|
325
|
+
}
|
|
326
|
+
this.#bufferedEvents.length = 0
|
|
327
|
+
|
|
328
|
+
// Same for the buffered data.
|
|
329
|
+
for (const data of this.#bufferedData) {
|
|
330
|
+
this.send(data)
|
|
331
|
+
}
|
|
332
|
+
this.#bufferedData.length = 0
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
public send(data: WebSocketData): void {
|
|
336
|
+
if (this.#server == null) {
|
|
337
|
+
this.#bufferedData.push(data)
|
|
338
|
+
return
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
this.#server.send(data as any)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
public close(code?: number, reason?: string): void {
|
|
345
|
+
invariant(
|
|
346
|
+
this.#server,
|
|
347
|
+
'Failed to close connection to the actual WebSocket server: connection not established. Did you forget to call `connect()`?',
|
|
348
|
+
)
|
|
349
|
+
|
|
350
|
+
this.#server.close({ code, reason })
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
public addEventListener<EventType extends keyof WebSocketServerEventMap>(
|
|
354
|
+
type: EventType,
|
|
355
|
+
listener: (
|
|
356
|
+
this: WebSocket,
|
|
357
|
+
event: WebSocketServerEventMap[EventType],
|
|
358
|
+
) => void,
|
|
359
|
+
options?: AddEventListenerOptions | boolean,
|
|
360
|
+
): void {
|
|
361
|
+
if (this.#server == null) {
|
|
362
|
+
this.#bufferedEvents.push([type, listener as any, options])
|
|
363
|
+
return
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const target = {} as WebSocket
|
|
367
|
+
switch (type) {
|
|
368
|
+
case 'message': {
|
|
369
|
+
this.#server.onMessage((data) => {
|
|
370
|
+
listener.call(
|
|
371
|
+
target,
|
|
372
|
+
new CancelableMessageEvent('message', { data }) as any,
|
|
373
|
+
)
|
|
374
|
+
})
|
|
375
|
+
break
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
case 'close': {
|
|
379
|
+
this.#server.onClose((code, reason) => {
|
|
380
|
+
listener.call(
|
|
381
|
+
target,
|
|
382
|
+
new CancelableCloseEvent('close', { code, reason }) as any,
|
|
383
|
+
)
|
|
384
|
+
})
|
|
385
|
+
break
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
public removeEventListener<EventType extends keyof WebSocketServerEventMap>(
|
|
391
|
+
type: EventType,
|
|
392
|
+
listener: (
|
|
393
|
+
this: WebSocket,
|
|
394
|
+
event: WebSocketServerEventMap[EventType],
|
|
395
|
+
) => void,
|
|
396
|
+
options?: EventListenerOptions | boolean,
|
|
397
|
+
): void {
|
|
398
|
+
console.warn(
|
|
399
|
+
'@msw/playwright: WebSocketRoute does not support removing event listeners',
|
|
400
|
+
)
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
interface InternalWebSocketRoute {
|
|
405
|
+
url: Parameters<Page['routeWebSocket']>[0]
|
|
406
|
+
handler: Parameters<Page['routeWebSocket']>[1]
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Custom implementation of the missing `page.unrouteWebSocket()` to remove
|
|
411
|
+
* WebSocket route handlers from the page. Loosely inspired by `page.unroute()`.
|
|
412
|
+
*/
|
|
413
|
+
async function unrouteWebSocket(
|
|
414
|
+
page: Page,
|
|
415
|
+
url: InternalWebSocketRoute['url'],
|
|
416
|
+
handler?: InternalWebSocketRoute['handler'],
|
|
417
|
+
): Promise<void> {
|
|
418
|
+
if (!('_webSocketRoutes' in page && Array.isArray(page._webSocketRoutes))) {
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
for (let i = page._webSocketRoutes.length - 1; i >= 0; i--) {
|
|
423
|
+
const route = page._webSocketRoutes[i] as InternalWebSocketRoute
|
|
424
|
+
|
|
425
|
+
if (
|
|
426
|
+
route.url === url &&
|
|
427
|
+
(handler != null ? route.handler === handler : true)
|
|
428
|
+
) {
|
|
429
|
+
page._webSocketRoutes.splice(i, 1)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,367 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
TestFixture,
|
|
7
|
-
WebSocketRoute,
|
|
8
|
-
} from '@playwright/test'
|
|
9
|
-
import {
|
|
10
|
-
type LifeCycleEventsMap,
|
|
11
|
-
SetupApi,
|
|
12
|
-
RequestHandler,
|
|
13
|
-
WebSocketHandler,
|
|
14
|
-
getResponse,
|
|
15
|
-
} from 'msw'
|
|
16
|
-
import {
|
|
17
|
-
type WebSocketClientEventMap,
|
|
18
|
-
type WebSocketData,
|
|
19
|
-
type WebSocketServerEventMap,
|
|
20
|
-
CancelableMessageEvent,
|
|
21
|
-
CancelableCloseEvent,
|
|
22
|
-
WebSocketClientConnectionProtocol,
|
|
23
|
-
WebSocketServerConnectionProtocol,
|
|
24
|
-
} from '@mswjs/interceptors/WebSocket'
|
|
25
|
-
|
|
26
|
-
export interface CreateNetworkFixtureArgs {
|
|
27
|
-
initialHandlers: Array<RequestHandler | WebSocketHandler>
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Creates a fixture that controls the network in your tests.
|
|
32
|
-
*
|
|
33
|
-
* @note The returned fixture already has the `auto` option set to `true`.
|
|
34
|
-
*
|
|
35
|
-
* **Usage**
|
|
36
|
-
* ```ts
|
|
37
|
-
* import { test as testBase } from '@playwright/test'
|
|
38
|
-
* import { createNetworkFixture, type WorkerFixture } from '@msw/playwright'
|
|
39
|
-
*
|
|
40
|
-
* interface Fixtures {
|
|
41
|
-
* network: WorkerFixture
|
|
42
|
-
* }
|
|
43
|
-
*
|
|
44
|
-
* export const test = testBase.extend<Fixtures>({
|
|
45
|
-
* network: createNetworkFixture()
|
|
46
|
-
* })
|
|
47
|
-
* ```
|
|
48
|
-
*/
|
|
49
|
-
export function createNetworkFixture(
|
|
50
|
-
args?: CreateNetworkFixtureArgs,
|
|
51
|
-
/** @todo `onUnhandledRequest`? */
|
|
52
|
-
): [
|
|
53
|
-
TestFixture<NetworkFixture, PlaywrightTestArgs & PlaywrightWorkerArgs>,
|
|
54
|
-
{ auto: boolean },
|
|
55
|
-
] {
|
|
56
|
-
return [
|
|
57
|
-
async ({ page }, use) => {
|
|
58
|
-
const worker = new NetworkFixture({
|
|
59
|
-
page,
|
|
60
|
-
initialHandlers: args?.initialHandlers || [],
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
await worker.start()
|
|
64
|
-
await use(worker)
|
|
65
|
-
await worker.stop()
|
|
66
|
-
},
|
|
67
|
-
{ auto: true },
|
|
68
|
-
]
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
export class NetworkFixture extends SetupApi<LifeCycleEventsMap> {
|
|
72
|
-
#page: Page
|
|
73
|
-
|
|
74
|
-
constructor(args: {
|
|
75
|
-
page: Page
|
|
76
|
-
initialHandlers: Array<RequestHandler | WebSocketHandler>
|
|
77
|
-
}) {
|
|
78
|
-
super(...args.initialHandlers)
|
|
79
|
-
this.#page = args.page
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
public async start() {
|
|
83
|
-
// Handle HTTP requests.
|
|
84
|
-
await this.#page.route(/.+/, async (route, request) => {
|
|
85
|
-
const fetchRequest = new Request(request.url(), {
|
|
86
|
-
method: request.method(),
|
|
87
|
-
headers: new Headers(await request.allHeaders()),
|
|
88
|
-
body: request.postDataBuffer(),
|
|
89
|
-
})
|
|
90
|
-
|
|
91
|
-
const response = await getResponse(
|
|
92
|
-
this.handlersController.currentHandlers().filter((handler) => {
|
|
93
|
-
return handler instanceof RequestHandler
|
|
94
|
-
}),
|
|
95
|
-
fetchRequest,
|
|
96
|
-
{
|
|
97
|
-
baseUrl: this.getPageUrl(),
|
|
98
|
-
},
|
|
99
|
-
)
|
|
100
|
-
|
|
101
|
-
if (response) {
|
|
102
|
-
if (response.status === 0) {
|
|
103
|
-
route.abort()
|
|
104
|
-
return
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
route.fulfill({
|
|
108
|
-
status: response.status,
|
|
109
|
-
headers: Object.fromEntries(response.headers),
|
|
110
|
-
body: response.body
|
|
111
|
-
? Buffer.from(await response.arrayBuffer())
|
|
112
|
-
: undefined,
|
|
113
|
-
})
|
|
114
|
-
return
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
route.continue()
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
// Handle WebSocket connections.
|
|
121
|
-
await this.#page.routeWebSocket(/.+/, async (ws) => {
|
|
122
|
-
const allWebSocketHandlers = this.handlersController
|
|
123
|
-
.currentHandlers()
|
|
124
|
-
.filter((handler) => {
|
|
125
|
-
return handler instanceof WebSocketHandler
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
if (allWebSocketHandlers.length === 0) {
|
|
129
|
-
ws.connectToServer()
|
|
130
|
-
return
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
const client = new PlaywrightWebSocketClientConnection(ws)
|
|
134
|
-
const server = new PlaywrightWebSocketServerConnection(ws)
|
|
135
|
-
|
|
136
|
-
for (const handler of allWebSocketHandlers) {
|
|
137
|
-
await handler.run(
|
|
138
|
-
{
|
|
139
|
-
client,
|
|
140
|
-
server,
|
|
141
|
-
info: { protocols: [] },
|
|
142
|
-
},
|
|
143
|
-
{
|
|
144
|
-
baseUrl: this.getPageUrl(),
|
|
145
|
-
},
|
|
146
|
-
)
|
|
147
|
-
}
|
|
148
|
-
})
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
public async stop() {
|
|
152
|
-
super.dispose()
|
|
153
|
-
await this.#page.unroute(/.+/)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
private getPageUrl(): string | undefined {
|
|
157
|
-
const url = this.#page.url()
|
|
158
|
-
return url !== 'about:blank' ? url : undefined
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
class PlaywrightWebSocketClientConnection
|
|
163
|
-
implements WebSocketClientConnectionProtocol
|
|
164
|
-
{
|
|
165
|
-
public id: string
|
|
166
|
-
public url: URL
|
|
167
|
-
|
|
168
|
-
constructor(protected readonly ws: WebSocketRoute) {
|
|
169
|
-
this.id = crypto.randomUUID()
|
|
170
|
-
this.url = new URL(ws.url())
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
public send(data: WebSocketData): void {
|
|
174
|
-
if (data instanceof Blob) {
|
|
175
|
-
/**
|
|
176
|
-
* @note Playwright does not support sending Blob data.
|
|
177
|
-
* Read the blob as buffer, then send the buffer instead.
|
|
178
|
-
*/
|
|
179
|
-
data.bytes().then((bytes) => {
|
|
180
|
-
this.ws.send(Buffer.from(bytes))
|
|
181
|
-
})
|
|
182
|
-
return
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
if (typeof data === 'string') {
|
|
186
|
-
this.ws.send(data)
|
|
187
|
-
return
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
this.ws.send(
|
|
191
|
-
/**
|
|
192
|
-
* @note Forcefully cast all data to Buffer because Playwright
|
|
193
|
-
* has trouble digesting ArrayBuffer and Blob directly.
|
|
194
|
-
*/
|
|
195
|
-
Buffer.from(
|
|
196
|
-
/**
|
|
197
|
-
* @note Playwright type definitions are tailored to Node.js
|
|
198
|
-
* while MSW describes all data types that can be sent over
|
|
199
|
-
* the WebSocket protocol, like ArrayBuffer and Blob.
|
|
200
|
-
*/
|
|
201
|
-
data as any,
|
|
202
|
-
),
|
|
203
|
-
)
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
public close(code?: number, reason?: string): void {
|
|
207
|
-
const resolvedCode = code ?? 1000
|
|
208
|
-
this.ws.close({ code: resolvedCode, reason })
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
public addEventListener<EventType extends keyof WebSocketClientEventMap>(
|
|
212
|
-
type: EventType,
|
|
213
|
-
listener: (
|
|
214
|
-
this: WebSocket,
|
|
215
|
-
event: WebSocketClientEventMap[EventType],
|
|
216
|
-
) => void,
|
|
217
|
-
options?: AddEventListenerOptions | boolean,
|
|
218
|
-
): void {
|
|
219
|
-
/**
|
|
220
|
-
* @note Playwright does not expose the actual WebSocket reference.
|
|
221
|
-
*/
|
|
222
|
-
const target = {} as WebSocket
|
|
223
|
-
|
|
224
|
-
switch (type) {
|
|
225
|
-
case 'message': {
|
|
226
|
-
this.ws.onMessage((data) => {
|
|
227
|
-
listener.call(
|
|
228
|
-
target,
|
|
229
|
-
new CancelableMessageEvent('message', {
|
|
230
|
-
data,
|
|
231
|
-
}) as any,
|
|
232
|
-
)
|
|
233
|
-
})
|
|
234
|
-
break
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
case 'close': {
|
|
238
|
-
this.ws.onClose((code, reason) => {
|
|
239
|
-
listener.call(
|
|
240
|
-
target,
|
|
241
|
-
new CancelableCloseEvent('close', {
|
|
242
|
-
code,
|
|
243
|
-
reason,
|
|
244
|
-
}) as any,
|
|
245
|
-
)
|
|
246
|
-
})
|
|
247
|
-
break
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
public removeEventListener<EventType extends keyof WebSocketClientEventMap>(
|
|
253
|
-
event: EventType,
|
|
254
|
-
listener: (
|
|
255
|
-
this: WebSocket,
|
|
256
|
-
event: WebSocketClientEventMap[EventType],
|
|
257
|
-
) => void,
|
|
258
|
-
options?: EventListenerOptions | boolean,
|
|
259
|
-
): void {
|
|
260
|
-
console.warn(
|
|
261
|
-
'@msw/playwright: WebSocketRoute does not support removing event listeners',
|
|
262
|
-
)
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
class PlaywrightWebSocketServerConnection
|
|
267
|
-
implements WebSocketServerConnectionProtocol
|
|
268
|
-
{
|
|
269
|
-
#server?: WebSocketRoute
|
|
270
|
-
#bufferedEvents: Array<
|
|
271
|
-
Parameters<WebSocketServerConnectionProtocol['addEventListener']>
|
|
272
|
-
>
|
|
273
|
-
#bufferedData: Array<WebSocketData>
|
|
274
|
-
|
|
275
|
-
constructor(protected readonly ws: WebSocketRoute) {
|
|
276
|
-
this.#bufferedEvents = []
|
|
277
|
-
this.#bufferedData = []
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
public connect(): void {
|
|
281
|
-
this.#server = this.ws.connectToServer()
|
|
282
|
-
|
|
283
|
-
/**
|
|
284
|
-
* @note Playwright does not support event buffering.
|
|
285
|
-
* Manually add event listeners that might have been registered
|
|
286
|
-
* before `connect()` was called.
|
|
287
|
-
*/
|
|
288
|
-
for (const [type, listener, options] of this.#bufferedEvents) {
|
|
289
|
-
this.addEventListener(type, listener, options)
|
|
290
|
-
}
|
|
291
|
-
this.#bufferedEvents.length = 0
|
|
292
|
-
|
|
293
|
-
// Same for the buffered data.
|
|
294
|
-
for (const data of this.#bufferedData) {
|
|
295
|
-
this.send(data)
|
|
296
|
-
}
|
|
297
|
-
this.#bufferedData.length = 0
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
public send(data: WebSocketData): void {
|
|
301
|
-
if (this.#server == null) {
|
|
302
|
-
this.#bufferedData.push(data)
|
|
303
|
-
return
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
this.#server.send(data as any)
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
public close(code?: number, reason?: string): void {
|
|
310
|
-
invariant(
|
|
311
|
-
this.#server,
|
|
312
|
-
'Failed to close connection to the actual WebSocket server: connection not established. Did you forget to call `connect()`?',
|
|
313
|
-
)
|
|
314
|
-
|
|
315
|
-
this.#server.close({ code, reason })
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
public addEventListener<EventType extends keyof WebSocketServerEventMap>(
|
|
319
|
-
type: EventType,
|
|
320
|
-
listener: (
|
|
321
|
-
this: WebSocket,
|
|
322
|
-
event: WebSocketServerEventMap[EventType],
|
|
323
|
-
) => void,
|
|
324
|
-
options?: AddEventListenerOptions | boolean,
|
|
325
|
-
): void {
|
|
326
|
-
if (this.#server == null) {
|
|
327
|
-
this.#bufferedEvents.push([type, listener as any, options])
|
|
328
|
-
return
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
const target = {} as WebSocket
|
|
332
|
-
switch (type) {
|
|
333
|
-
case 'message': {
|
|
334
|
-
this.#server.onMessage((data) => {
|
|
335
|
-
listener.call(
|
|
336
|
-
target,
|
|
337
|
-
new CancelableMessageEvent('message', { data }) as any,
|
|
338
|
-
)
|
|
339
|
-
})
|
|
340
|
-
break
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
case 'close': {
|
|
344
|
-
this.#server.onClose((code, reason) => {
|
|
345
|
-
listener.call(
|
|
346
|
-
target,
|
|
347
|
-
new CancelableCloseEvent('close', { code, reason }) as any,
|
|
348
|
-
)
|
|
349
|
-
})
|
|
350
|
-
break
|
|
351
|
-
}
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
public removeEventListener<EventType extends keyof WebSocketServerEventMap>(
|
|
356
|
-
type: EventType,
|
|
357
|
-
listener: (
|
|
358
|
-
this: WebSocket,
|
|
359
|
-
event: WebSocketServerEventMap[EventType],
|
|
360
|
-
) => void,
|
|
361
|
-
options?: EventListenerOptions | boolean,
|
|
362
|
-
): void {
|
|
363
|
-
console.warn(
|
|
364
|
-
'@msw/playwright: WebSocketRoute does not support removing event listeners',
|
|
365
|
-
)
|
|
366
|
-
}
|
|
367
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
type CreateNetworkFixtureArgs,
|
|
3
|
+
type NetworkFixture,
|
|
4
|
+
createNetworkFixture,
|
|
5
|
+
} from './fixture.js'
|