diff --git a/docs/src/api/class-websocketroute.md b/docs/src/api/class-websocketroute.md index fcbe1b21f107a..c4c8dfc0b7dc4 100644 --- a/docs/src/api/class-websocketroute.md +++ b/docs/src/api/class-websocketroute.md @@ -377,6 +377,65 @@ Message to send. +## method: WebSocketRoute.protocols +* since: v1.60 +- returns: <[Array]<[string]>> + +The list of WebSocket subprotocols requested by the page, as passed via the second argument to the [`WebSocket` constructor](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket). Corresponds to the `Sec-WebSocket-Protocol` request header. + +Returns an empty array if no protocols were specified. + +**Usage** + +```js +await page.routeWebSocket('wss://example.com/ws', ws => { + if (ws.protocols().includes('chat.v2')) + ws.onMessage(message => ws.send(JSON.stringify({ version: 2, echo: message }))); + else + ws.close({ code: 1002, reason: 'Unsupported protocol' }); +}); +``` + +```java +page.routeWebSocket("wss://example.com/ws", ws -> { + if (ws.protocols().contains("chat.v2")) { + ws.onMessage(frame -> ws.send("v2:" + frame.text())); + } else { + ws.close(1002, "Unsupported protocol"); + } +}); +``` + +```python async +async def handler(ws: WebSocketRoute): + if "chat.v2" in ws.protocols: + ws.on_message(lambda message: ws.send(f"v2:{message}")) + else: + await ws.close(code=1002, reason="Unsupported protocol") + +await page.route_web_socket("wss://example.com/ws", handler) +``` + +```python sync +def handler(ws: WebSocketRoute): + if "chat.v2" in ws.protocols: + ws.on_message(lambda message: ws.send(f"v2:{message}")) + else: + ws.close(code=1002, reason="Unsupported protocol") + +page.route_web_socket("wss://example.com/ws", handler) +``` + +```csharp +await page.RouteWebSocketAsync("wss://example.com/ws", ws => { + if (ws.Protocols.Contains("chat.v2")) + ws.OnMessage(frame => ws.Send($"v2:{frame.Text}")); + else + ws.CloseAsync(new() { Code = 1002, Reason = "Unsupported protocol" }); +}); +``` + + ## method: WebSocketRoute.url * since: v1.48 - returns: <[string]> diff --git a/packages/injected/src/webSocketMock.ts b/packages/injected/src/webSocketMock.ts index 2ae2188e85607..9285dea5dc9d1 100644 --- a/packages/injected/src/webSocketMock.ts +++ b/packages/injected/src/webSocketMock.ts @@ -17,7 +17,7 @@ export type WebSocketMessage = string | ArrayBufferLike | Blob | ArrayBufferView; export type WSData = { data: string, isBase64: boolean }; -export type OnCreatePayload = { type: 'onCreate', id: string, url: string }; +export type OnCreatePayload = { type: 'onCreate', id: string, url: string, protocols: string[] }; export type OnMessageFromPagePayload = { type: 'onMessageFromPage', id: string, data: WSData }; export type OnClosePagePayload = { type: 'onClosePage', id: string, code: number | undefined, reason: string | undefined, wasClean: boolean }; export type OnMessageFromServerPayload = { type: 'onMessageFromServer', id: string, data: WSData }; @@ -147,7 +147,8 @@ export function inject(globalThis: GlobalThis) { this._id = generateId(); idToWebSocket.set(this._id, this); - binding({ type: 'onCreate', id: this._id, url: this.url }); + const protocolsList = Array.isArray(protocols) ? [...protocols] : (protocols ? [protocols] : []); + binding({ type: 'onCreate', id: this._id, url: this.url, protocols: protocolsList }); } // --- native WebSocket implementation --- diff --git a/packages/playwright-client/types/types.d.ts b/packages/playwright-client/types/types.d.ts index db04320501773..30e475fcdea33 100644 --- a/packages/playwright-client/types/types.d.ts +++ b/packages/playwright-client/types/types.d.ts @@ -16538,6 +16538,27 @@ export interface WebSocketRoute { */ connectToServer(): WebSocketRoute; + /** + * The list of WebSocket subprotocols requested by the page, as passed via the second argument to the + * [`WebSocket` constructor](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket). Corresponds to the + * `Sec-WebSocket-Protocol` request header. + * + * Returns an empty array if no protocols were specified. + * + * **Usage** + * + * ```js + * await page.routeWebSocket('wss://example.com/ws', ws => { + * if (ws.protocols().includes('chat.v2')) + * ws.onMessage(message => ws.send(JSON.stringify({ version: 2, echo: message }))); + * else + * ws.close({ code: 1002, reason: 'Unsupported protocol' }); + * }); + * ``` + * + */ + protocols(): Array; + /** * Sends a message to the WebSocket. When called on the original WebSocket, sends the message to the page. When called * on the result of diff --git a/packages/playwright-core/src/client/network.ts b/packages/playwright-core/src/client/network.ts index 146d0679da057..396b8e97795f1 100644 --- a/packages/playwright-core/src/client/network.ts +++ b/packages/playwright-core/src/client/network.ts @@ -485,6 +485,10 @@ export class WebSocketRoute extends ChannelOwner return this._initializer.url; }, + protocols: () => { + return [...this._initializer.protocols]; + }, + close: async (options: { code?: number, reason?: string } = {}) => { await this._channel.closeServer({ ...options, wasClean: true }).catch(() => {}); }, @@ -534,6 +538,10 @@ export class WebSocketRoute extends ChannelOwner return this._initializer.url; } + protocols(): string[] { + return [...this._initializer.protocols]; + } + async close(options: { code?: number, reason?: string } = {}) { await this._channel.closePage({ ...options, wasClean: true }).catch(() => {}); } diff --git a/packages/playwright-core/src/protocol/validator.ts b/packages/playwright-core/src/protocol/validator.ts index 142ad0dcbb9f9..3c24734e63a40 100644 --- a/packages/playwright-core/src/protocol/validator.ts +++ b/packages/playwright-core/src/protocol/validator.ts @@ -2430,6 +2430,7 @@ scheme.RouteFulfillParams = tObject({ scheme.RouteFulfillResult = tOptional(tObject({})); scheme.WebSocketRouteInitializer = tObject({ url: tString, + protocols: tArray(tString), }); scheme.WebSocketRouteMessageFromPageEvent = tObject({ message: tString, diff --git a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts index 35f50af8ba1b0..dc33c47af92e2 100644 --- a/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts +++ b/packages/playwright-core/src/server/dispatchers/webSocketRouteDispatcher.ts @@ -37,8 +37,8 @@ export class WebSocketRouteDispatcher extends Dispatcher(); - constructor(scope: PageDispatcher | BrowserContextDispatcher, id: string, url: string, frame: Frame) { - super(scope, new SdkObject(scope._object, 'webSocketRoute'), 'WebSocketRoute', { url }); + constructor(scope: PageDispatcher | BrowserContextDispatcher, id: string, url: string, protocols: string[], frame: Frame) { + super(scope, new SdkObject(scope._object, 'webSocketRoute'), 'WebSocketRoute', { url, protocols }); this._id = id; this._frame = frame; this._eventListeners.push( @@ -76,7 +76,7 @@ export class WebSocketRouteDispatcher extends Dispatcher {}); diff --git a/packages/playwright-core/types/types.d.ts b/packages/playwright-core/types/types.d.ts index db04320501773..30e475fcdea33 100644 --- a/packages/playwright-core/types/types.d.ts +++ b/packages/playwright-core/types/types.d.ts @@ -16538,6 +16538,27 @@ export interface WebSocketRoute { */ connectToServer(): WebSocketRoute; + /** + * The list of WebSocket subprotocols requested by the page, as passed via the second argument to the + * [`WebSocket` constructor](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/WebSocket). Corresponds to the + * `Sec-WebSocket-Protocol` request header. + * + * Returns an empty array if no protocols were specified. + * + * **Usage** + * + * ```js + * await page.routeWebSocket('wss://example.com/ws', ws => { + * if (ws.protocols().includes('chat.v2')) + * ws.onMessage(message => ws.send(JSON.stringify({ version: 2, echo: message }))); + * else + * ws.close({ code: 1002, reason: 'Unsupported protocol' }); + * }); + * ``` + * + */ + protocols(): Array; + /** * Sends a message to the WebSocket. When called on the original WebSocket, sends the message to the page. When called * on the result of diff --git a/packages/protocol/src/channels.d.ts b/packages/protocol/src/channels.d.ts index 98b15dc23cc2b..d03bbcdd712ec 100644 --- a/packages/protocol/src/channels.d.ts +++ b/packages/protocol/src/channels.d.ts @@ -4197,6 +4197,7 @@ export interface RouteEvents { // ----------- WebSocketRoute ----------- export type WebSocketRouteInitializer = { url: string, + protocols: string[], }; export interface WebSocketRouteEventTarget { on(event: 'messageFromPage', callback: (params: WebSocketRouteMessageFromPageEvent) => void): this; diff --git a/packages/protocol/src/protocol.yml b/packages/protocol/src/protocol.yml index aecc537c57831..45970c3cc14d6 100644 --- a/packages/protocol/src/protocol.yml +++ b/packages/protocol/src/protocol.yml @@ -3744,6 +3744,9 @@ WebSocketRoute: initializer: url: string + protocols: + type: array + items: string commands: diff --git a/tests/library/route-web-socket.spec.ts b/tests/library/route-web-socket.spec.ts index 67438cf21ad63..eb0cb80beb726 100644 --- a/tests/library/route-web-socket.spec.ts +++ b/tests/library/route-web-socket.spec.ts @@ -578,3 +578,37 @@ test('should work with baseURL', async ({ contextFactory, server }) => { `message: data=echo origin=ws://${server.HOST} lastEventId=`, ]); }); + +test('should expose protocols to the route handler', async ({ page, server }) => { + const routes: WebSocketRoute[] = []; + await page.routeWebSocket(/.*/, ws => { + routes.push(ws); + }); + + await page.goto(server.EMPTY_PAGE); + await page.evaluate(({ host }) => { + (window as any).wsNone = new WebSocket('ws://' + host + '/ws-none'); + (window as any).wsString = new WebSocket('ws://' + host + '/ws-string', 'chat.v1'); + (window as any).wsArray = new WebSocket('ws://' + host + '/ws-array', ['chat.v2', 'chat.v1']); + }, { host: server.HOST }); + + await expect.poll(() => routes.length).toBe(3); + + const byUrl = new Map(routes.map(r => [new URL(r.url()).pathname, r] as const)); + expect(byUrl.get('/ws-none')!.protocols()).toEqual([]); + expect(byUrl.get('/ws-string')!.protocols()).toEqual(['chat.v1']); + expect(byUrl.get('/ws-array')!.protocols()).toEqual(['chat.v2', 'chat.v1']); +}); + +test('should expose protocols on server-side route', async ({ page, server }) => { + const { promise, resolve } = withResolvers<{ page: WebSocketRoute, server: WebSocketRoute }>(); + await page.routeWebSocket(/.*/, ws => { + const serverRoute = ws.connectToServer(); + resolve({ page: ws, server: serverRoute }); + }); + + await setupWS(page, server, 'blob', ['chat.v2', 'chat.v1']); + const { page: pageRoute, server: serverRoute } = await promise; + expect(pageRoute.protocols()).toEqual(['chat.v2', 'chat.v1']); + expect(serverRoute.protocols()).toEqual(['chat.v2', 'chat.v1']); +});