blob: 244e80993692a2be4e36ca61c974b0d96fbfa429 [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as SDK from './sdk.js';
describe('ServerSentEventsParser', () => {
let parser: SDK.ServerSentEventProtocol.ServerSentEventsParser;
let events: Array<{eventId: string, eventType: string, data: string}>;
/**
* Encodes `str` first as UTF-8 and then as Base64 to simulate CDP.
* @returns A promise that fulfills when the parser is done handling the chunk.
*/
function enqueue(str: string, options?: {prefixBOM?: true}): Promise<void> {
const maybeBom = options?.prefixBOM ? [0xef, 0xbb, 0xbf] : [];
const bytes = new TextEncoder().encode(str);
return parser.addBase64Chunk(window.btoa(String.fromCodePoint(...maybeBom, ...bytes)));
}
/**
* Same as `enqueue` but feeds the resulting bytes one by one into the parser.
*/
async function enqueueOneByOne(str: string, options?: {prefixBOM?: true}): Promise<void> {
if (options?.prefixBOM) {
await parser.addBase64Chunk(window.btoa('\xef'));
await parser.addBase64Chunk(window.btoa('\xbb'));
await parser.addBase64Chunk(window.btoa('\xbf'));
}
const bytes = new TextEncoder().encode(str);
for (let i = 0; i < bytes.length; ++i) {
await parser.addBase64Chunk(window.btoa(String.fromCodePoint(bytes[i])));
}
}
beforeEach(() => {
events = [];
parser = new SDK.ServerSentEventProtocol.ServerSentEventsParser((eventType, data, eventId) => {
events.push({eventType, data, eventId});
});
});
it('does not dispatch an event for empty messages', async () => {
await enqueue('\n');
assert.lengthOf(events, 0);
});
it('dispatches an event for simple messages', async () => {
await enqueue('data:hello\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].eventId, '');
assert.strictEqual(events[0].data, 'hello');
});
it('accumulates data fields', async () => {
await enqueue('data:hello\ndata:bye\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].eventId, '');
assert.strictEqual(events[0].data, 'hello\nbye');
});
it('dispatches an event with the right id if one was set', async () => {
await enqueue('id:42\ndata:hello\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].eventId, '42');
assert.strictEqual(events[0].data, 'hello');
});
it('remembers the id even when data is empty and no event is dispatched', async () => {
await enqueue('id:42\n\n');
assert.lengthOf(events, 0);
await enqueue('data:hello\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].eventId, '42');
assert.strictEqual(events[0].data, 'hello');
});
it('supports custom event types', async () => {
await enqueue('event:foo\ndata:hello\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'foo');
assert.strictEqual(events[0].eventId, '');
assert.strictEqual(events[0].data, 'hello');
});
it('resets the event type after dispatching it', async () => {
await enqueue('event:foo\ndata:hello\n\ndata:bye\n\n');
assert.lengthOf(events, 2);
assert.strictEqual(events[0].eventType, 'foo');
assert.strictEqual(events[0].data, 'hello');
assert.strictEqual(events[1].eventType, 'message');
assert.strictEqual(events[1].data, 'bye');
});
it('does not accumulate event fields', async () => {
await enqueue('data:hello\nevent:foo\nevent:bar\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'bar');
assert.strictEqual(events[0].data, 'hello');
});
it('does not reset the id after dispatching it', async () => {
await enqueue('id:42\ndata:hello\n\ndata:bye\n\n');
assert.lengthOf(events, 2);
assert.strictEqual(events[0].eventId, '42');
assert.strictEqual(events[0].data, 'hello');
assert.strictEqual(events[1].eventId, '42');
assert.strictEqual(events[1].data, 'bye');
});
it('ignores the retry field', async () => {
await enqueue('retry:9999\n\n');
assert.lengthOf(events, 0);
});
it('supports different types of newlines', async () => {
await enqueue('data:hello\r\n\rdata:bye\r\r');
assert.lengthOf(events, 2);
});
it('ignores unrecognized fields', async () => {
await enqueue('data:hello\nfoo:bar\nanotherRandomFIeld\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].eventId, '');
assert.strictEqual(events[0].data, 'hello');
});
it('ignores comments', async () => {
await enqueue('data:hello\n:comment one\n:comment two\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].eventId, '');
assert.strictEqual(events[0].data, 'hello');
});
it('ignores BOM', async () => {
// This line are the first bytes, so the BOM should be ignored.
await enqueue('data:hello\n', {prefixBOM: true});
// In this line the BOM bytes are part of the field name.
await enqueue('data:bye\n', {prefixBOM: true});
await enqueue('\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].eventId, '');
assert.strictEqual(events[0].data, 'hello');
});
it('ignores BOM (one-by-one)', async () => {
// This line are the first bytes, so the BOM should be ignored.
await enqueueOneByOne('data:hello\n', {prefixBOM: true});
// In this line the BOM bytes are part of the field name.
await enqueueOneByOne('data:bye\n', {prefixBOM: true});
await enqueueOneByOne('\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].eventId, '');
assert.strictEqual(events[0].data, 'hello');
});
it('treats lines without a colon as field name only', async () => {
await enqueue('data:hello\nevent:foo\nevent\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].data, 'hello');
});
it('skips at most one leading space for field values', async () => {
await enqueue('data: hello \nevent: type \n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, ' type ');
assert.strictEqual(events[0].data, ' hello ');
});
it('works correctly if data is received one byte at a time', async () => {
await enqueueOneByOne('data:hello\r\ndata:world\revent:a\revent:b\nid:4\n\nid:8\ndata:bye\r\n\r');
assert.lengthOf(events, 2);
assert.strictEqual(events[0].eventType, 'b');
assert.strictEqual(events[0].eventId, '4');
assert.strictEqual(events[0].data, 'hello\nworld');
assert.strictEqual(events[1].eventType, 'message');
assert.strictEqual(events[1].eventId, '8');
assert.strictEqual(events[1].data, 'bye');
});
it('handles non-ASCII characters correctly', async () => {
await enqueue('data:Iñtërnâtiônàlizætiøn☃𝌆\n\n');
assert.lengthOf(events, 1);
assert.strictEqual(events[0].eventType, 'message');
assert.strictEqual(events[0].data, 'Iñtërnâtiônàlizætiøn☃𝌆');
});
});