Skip to content

Commit ac8ab1e

Browse files
authored
feat(websocket): add WebSocket.waitForEvent and isClosed (#4301)
1 parent c446bf6 commit ac8ab1e

File tree

4 files changed

+91
-3
lines changed

4 files changed

+91
-3
lines changed

docs/api.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4158,7 +4158,9 @@ The [WebSocket] class represents websocket connections in the page.
41584158
- [event: 'framereceived'](#event-framereceived)
41594159
- [event: 'framesent'](#event-framesent)
41604160
- [event: 'socketerror'](#event-socketerror)
4161+
- [webSocket.isClosed()](#websocketisclosed)
41614162
- [webSocket.url()](#websocketurl)
4163+
- [webSocket.waitForEvent(event[, optionsOrPredicate])](#websocketwaitforeventevent-optionsorpredicate)
41624164
<!-- GEN:stop -->
41634165

41644166
#### event: 'close'
@@ -4182,11 +4184,25 @@ Fired when the websocket sends a frame.
41824184

41834185
Fired when the websocket has an error.
41844186

4187+
#### webSocket.isClosed()
4188+
- returns: <[boolean]>
4189+
4190+
Indicates that the web socket has been closed.
4191+
41854192
#### webSocket.url()
41864193
- returns: <[string]>
41874194

41884195
Contains the URL of the WebSocket.
41894196

4197+
#### webSocket.waitForEvent(event[, optionsOrPredicate])
4198+
- `event` <[string]> Event name, same one would pass into `webSocket.on(event)`.
4199+
- `optionsOrPredicate` <[Function]|[Object]> Either a predicate that receives an event or an options object.
4200+
- `predicate` <[Function]> receives the event data and resolves to truthy value when the waiting should resolve.
4201+
- `timeout` <[number]> maximum time to wait for in milliseconds. Defaults to `30000` (30 seconds). Pass `0` to disable timeout. The default value can be changed by using the [browserContext.setDefaultTimeout(timeout)](#browsercontextsetdefaulttimeouttimeout) or [page.setDefaultTimeout(timeout)](#pagesetdefaulttimeouttimeout) methods.
4202+
- returns: <[Promise]<[Object]>> Promise which resolves to the event data value.
4203+
4204+
Waits for event to fire and passes its value into the predicate function. Resolves when the predicate returns truthy value. Will throw an error if the webSocket is closed before the event
4205+
is fired.
41904206

41914207
### class: TimeoutError
41924208

src/client/network.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,14 @@ import { URLSearchParams } from 'url';
1818
import * as channels from '../protocol/channels';
1919
import { ChannelOwner } from './channelOwner';
2020
import { Frame } from './frame';
21-
import { Headers } from './types';
21+
import { Headers, WaitForEventOptions } from './types';
2222
import * as fs from 'fs';
2323
import * as mime from 'mime';
2424
import * as util from 'util';
2525
import { isString, headersObjectToArray, headersArrayToObject } from '../utils/utils';
2626
import { Events } from './events';
27+
import { Page } from './page';
28+
import { Waiter } from './waiter';
2729

2830
export type NetworkCookie = {
2931
name: string,
@@ -314,12 +316,17 @@ export class Response extends ChannelOwner<channels.ResponseChannel, channels.Re
314316
}
315317

316318
export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.WebSocketInitializer> {
319+
private _page: Page;
320+
private _isClosed: boolean;
321+
317322
static from(webSocket: channels.WebSocketChannel): WebSocket {
318323
return (webSocket as any)._object;
319324
}
320325

321326
constructor(parent: ChannelOwner, type: string, guid: string, initializer: channels.WebSocketInitializer) {
322327
super(parent, type, guid, initializer);
328+
this._isClosed = false;
329+
this._page = parent as Page;
323330
this._channel.on('frameSent', (event: { opcode: number, data: string }) => {
324331
const payload = event.opcode === 2 ? Buffer.from(event.data, 'base64') : event.data;
325332
this.emit(Events.WebSocket.FrameSent, { payload });
@@ -329,12 +336,34 @@ export class WebSocket extends ChannelOwner<channels.WebSocketChannel, channels.
329336
this.emit(Events.WebSocket.FrameReceived, { payload });
330337
});
331338
this._channel.on('error', ({ error }) => this.emit(Events.WebSocket.Error, error));
332-
this._channel.on('close', () => this.emit(Events.WebSocket.Close));
339+
this._channel.on('close', () => {
340+
this._isClosed = true;
341+
this.emit(Events.WebSocket.Close);
342+
});
333343
}
334344

335345
url(): string {
336346
return this._initializer.url;
337347
}
348+
349+
isClosed(): boolean {
350+
return this._isClosed;
351+
}
352+
353+
async waitForEvent(event: string, optionsOrPredicate: WaitForEventOptions = {}): Promise<any> {
354+
const timeout = this._page._timeoutSettings.timeout(typeof optionsOrPredicate === 'function' ? {} : optionsOrPredicate);
355+
const predicate = typeof optionsOrPredicate === 'function' ? optionsOrPredicate : optionsOrPredicate.predicate;
356+
const waiter = new Waiter();
357+
waiter.rejectOnTimeout(timeout, `Timeout while waiting for event "${event}"`);
358+
if (event !== Events.WebSocket.Error)
359+
waiter.rejectOnEvent(this, Events.WebSocket.Error, new Error('Socket error'));
360+
if (event !== Events.WebSocket.Close)
361+
waiter.rejectOnEvent(this, Events.WebSocket.Close, new Error('Socket closed'));
362+
waiter.rejectOnEvent(this._page, Events.Page.Close, new Error('Page closed'));
363+
const result = await waiter.waitForEvent(this, event, predicate as any);
364+
waiter.dispose();
365+
return result;
366+
}
338367
}
339368

340369
export function validateHeaders(headers: Headers) {

src/server/frames.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,7 @@ export class FrameManager {
352352

353353
onWebSocketResponse(requestId: string, status: number, statusText: string) {
354354
const ws = this._webSockets.get(requestId);
355-
if (status >= 200 && status < 400)
355+
if (status < 400)
356356
return;
357357
if (ws)
358358
ws.error(`${statusText}: ${status}`);

test/web-socket.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ it('should emit close events', async ({ page, server }) => {
3232
let socketClosed;
3333
const socketClosePromise = new Promise(f => socketClosed = f);
3434
const log = [];
35+
let webSocket;
3536
page.on('websocket', ws => {
3637
log.push(`open<${ws.url()}>`);
38+
webSocket = ws;
3739
ws.on('close', () => { log.push('close'); socketClosed(); });
3840
});
3941
await page.evaluate(port => {
@@ -42,6 +44,7 @@ it('should emit close events', async ({ page, server }) => {
4244
}, server.PORT);
4345
await socketClosePromise;
4446
expect(log.join(':')).toBe(`open<ws://localhost:${server.PORT}/ws>:close`);
47+
expect(webSocket.isClosed()).toBeTruthy();
4548
});
4649

4750
it('should emit frame events', async ({ page, server, isFirefox }) => {
@@ -104,3 +107,43 @@ it('should emit error', async ({page, server, isFirefox}) => {
104107
else
105108
expect(message).toContain(': 400');
106109
});
110+
111+
it('should not have stray error events', async ({page, server, isFirefox}) => {
112+
const [ws] = await Promise.all([
113+
page.waitForEvent('websocket'),
114+
page.evaluate(port => {
115+
(window as any).ws = new WebSocket('ws://localhost:' + port + '/ws');
116+
}, server.PORT)
117+
]);
118+
let error;
119+
ws.on('socketerror', e => error = e);
120+
await ws.waitForEvent('framereceived');
121+
await page.evaluate('window.ws.close()');
122+
expect(error).toBeFalsy();
123+
});
124+
125+
it('should reject waitForEvent on socket close', async ({page, server, isFirefox}) => {
126+
const [ws] = await Promise.all([
127+
page.waitForEvent('websocket'),
128+
page.evaluate(port => {
129+
(window as any).ws = new WebSocket('ws://localhost:' + port + '/ws');
130+
}, server.PORT)
131+
]);
132+
await ws.waitForEvent('framereceived');
133+
const error = ws.waitForEvent('framesent').catch(e => e);
134+
await page.evaluate('window.ws.close()');
135+
expect((await error).message).toContain('Socket closed');
136+
});
137+
138+
it('should reject waitForEvent on page close', async ({page, server, isFirefox}) => {
139+
const [ws] = await Promise.all([
140+
page.waitForEvent('websocket'),
141+
page.evaluate(port => {
142+
(window as any).ws = new WebSocket('ws://localhost:' + port + '/ws');
143+
}, server.PORT)
144+
]);
145+
await ws.waitForEvent('framereceived');
146+
const error = ws.waitForEvent('framesent').catch(e => e);
147+
await page.close();
148+
expect((await error).message).toContain('Page closed');
149+
});

0 commit comments

Comments
 (0)