From 698a391107b970c13380ceb3666c01453f3fa861 Mon Sep 17 00:00:00 2001 From: uzlopak Date: Wed, 10 Jan 2024 09:30:11 +0100 Subject: [PATCH] feat: implement EventSource --- .eslintrc.js | 1 + .../message-event-instantiate.js | 21 + .../bootstrap/web/exposed-window-or-worker.js | 4 + lib/internal/event_source.js | 757 ++++++++++++++++++ lib/internal/process/pre_execution.js | 12 + test/common/index.js | 4 + test/fixtures/wpt/README.md | 1 + test/fixtures/wpt/eventsource/META.yml | 5 + test/fixtures/wpt/eventsource/README.md | 4 + .../dedicated-worker/eventsource-close.htm | 24 + .../dedicated-worker/eventsource-close.js | 9 + .../dedicated-worker/eventsource-close2.htm | 23 + .../dedicated-worker/eventsource-close2.js | 3 + .../eventsource-constructor-no-new.any.js | 7 + ...ventsource-constructor-non-same-origin.htm | 34 + ...eventsource-constructor-non-same-origin.js | 10 + .../eventsource-constructor-url-bogus.js | 7 + .../eventsource-eventtarget.worker.js | 11 + .../dedicated-worker/eventsource-onmesage.js | 9 + .../eventsource-onmessage.htm | 24 + .../dedicated-worker/eventsource-onopen.htm | 27 + .../dedicated-worker/eventsource-onopen.js | 9 + .../eventsource-prototype.htm | 25 + .../dedicated-worker/eventsource-prototype.js | 8 + .../dedicated-worker/eventsource-url.htm | 25 + .../dedicated-worker/eventsource-url.js | 7 + .../wpt/eventsource/event-data.any.js | 21 + .../eventsource/eventsource-close.window.js | 70 ++ ...urce-constructor-document-domain.window.js | 18 + .../eventsource-constructor-empty-url.any.js | 6 + ...urce-constructor-non-same-origin.window.js | 21 + ...ventsource-constructor-stringify.window.js | 28 + .../eventsource-constructor-url-bogus.any.js | 8 + ...entsource-constructor-url-multi-window.htm | 37 + .../eventsource-cross-origin.window.js | 51 ++ .../eventsource-eventtarget.any.js | 16 + .../eventsource-onmessage-realm.htm | 25 + .../eventsource-onmessage-trusted.any.js | 12 + .../eventsource/eventsource-onmessage.any.js | 14 + .../wpt/eventsource/eventsource-onopen.any.js | 17 + .../eventsource/eventsource-prototype.any.js | 10 + .../eventsource-reconnect.window.js | 47 ++ ...tsource-request-cancellation.any.window.js | 21 + .../wpt/eventsource/eventsource-url.any.js | 8 + .../wpt/eventsource/format-bom-2.any.js | 24 + .../wpt/eventsource/format-bom.any.js | 24 + .../wpt/eventsource/format-comments.any.js | 16 + ...format-data-before-final-empty-line.any.js | 17 + .../wpt/eventsource/format-field-data.any.js | 23 + .../format-field-event-empty.any.js | 13 + .../wpt/eventsource/format-field-event.any.js | 15 + .../wpt/eventsource/format-field-id-2.any.js | 25 + .../eventsource/format-field-id-3.window.js | 56 ++ .../format-field-id-null.window.js | 25 + .../wpt/eventsource/format-field-id.any.js | 21 + .../eventsource/format-field-parsing.any.js | 14 + .../format-field-retry-bogus.any.js | 19 + .../format-field-retry-empty.any.js | 13 + .../wpt/eventsource/format-field-retry.any.js | 21 + .../eventsource/format-field-unknown.any.js | 13 + .../eventsource/format-leading-space.any.js | 14 + .../wpt/eventsource/format-mime-bogus.any.js | 25 + .../format-mime-trailing-semicolon.any.js | 20 + .../format-mime-valid-bogus.any.js | 24 + .../wpt/eventsource/format-newlines.any.js | 13 + .../eventsource/format-null-character.any.js | 17 + .../wpt/eventsource/format-utf-8.any.js | 12 + .../wpt/eventsource/request-accept.any.js | 13 + .../eventsource/request-cache-control.any.js | 35 + .../request-credentials.any.window.js | 37 + .../request-redirect.any.window.js | 24 + .../request-status-error.window.js | 27 + .../eventsource/resources/accept.event_stream | 2 + .../resources/cache-control.event_stream | 2 + .../resources/eventsource-onmessage-realm.htm | 2 + .../wpt/eventsource/resources/init.htm | 9 + .../shared-worker/eventsource-close.htm | 24 + .../shared-worker/eventsource-close.js | 12 + ...ventsource-constructor-non-same-origin.htm | 34 + ...eventsource-constructor-non-same-origin.js | 13 + .../eventsource-constructor-url-bogus.js | 10 + .../shared-worker/eventsource-eventtarget.htm | 24 + .../shared-worker/eventsource-eventtarget.js | 13 + .../shared-worker/eventsource-onmesage.js | 12 + .../shared-worker/eventsource-onmessage.htm | 24 + .../shared-worker/eventsource-onopen.htm | 27 + .../shared-worker/eventsource-onopen.js | 12 + .../shared-worker/eventsource-prototype.htm | 25 + .../shared-worker/eventsource-prototype.js | 11 + .../shared-worker/eventsource-url.htm | 25 + .../shared-worker/eventsource-url.js | 10 + test/fixtures/wpt/versions.json | 4 + test/parallel/test-eventsource-connect.mjs | 26 + .../parallel/test-eventsource-constructor.mjs | 15 + test/parallel/test-eventsource-error.mjs | 28 + .../test-eventsource-eventhandler-idl.mjs | 40 + ...entsource-eventsourcestream-parse-line.mjs | 92 +++ ...source-eventsourcestream-process-event.mjs | 138 ++++ .../test-eventsource-eventsourcestream.mjs | 101 +++ .../test-eventsource-instance-constants.mjs | 58 ++ .../test-eventsource-is-valid-helpers.mjs | 24 + .../test-eventsource-messageevent.mjs | 213 +++++ test/parallel/test-eventsource-redirect.mjs | 76 ++ .../test-eventsource-static-constants.mjs | 36 + test/parallel/test-eventsource.mjs | 5 + test/wpt/status/eventsource.json | 137 ++++ test/wpt/test-eventsource.js | 16 + 107 files changed, 3401 insertions(+) create mode 100644 benchmark/events_source/message-event-instantiate.js create mode 100644 lib/internal/event_source.js create mode 100644 test/fixtures/wpt/eventsource/META.yml create mode 100644 test/fixtures/wpt/eventsource/README.md create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close.htm create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close2.htm create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close2.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-no-new.any.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-non-same-origin.htm create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-non-same-origin.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-url-bogus.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-eventtarget.worker.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onmesage.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onmessage.htm create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onopen.htm create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onopen.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-prototype.htm create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-prototype.js create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-url.htm create mode 100644 test/fixtures/wpt/eventsource/dedicated-worker/eventsource-url.js create mode 100644 test/fixtures/wpt/eventsource/event-data.any.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-close.window.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-constructor-document-domain.window.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-constructor-empty-url.any.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-constructor-non-same-origin.window.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-constructor-stringify.window.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-constructor-url-bogus.any.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-constructor-url-multi-window.htm create mode 100644 test/fixtures/wpt/eventsource/eventsource-cross-origin.window.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-eventtarget.any.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-onmessage-realm.htm create mode 100644 test/fixtures/wpt/eventsource/eventsource-onmessage-trusted.any.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-onmessage.any.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-onopen.any.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-prototype.any.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-reconnect.window.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-request-cancellation.any.window.js create mode 100644 test/fixtures/wpt/eventsource/eventsource-url.any.js create mode 100644 test/fixtures/wpt/eventsource/format-bom-2.any.js create mode 100644 test/fixtures/wpt/eventsource/format-bom.any.js create mode 100644 test/fixtures/wpt/eventsource/format-comments.any.js create mode 100644 test/fixtures/wpt/eventsource/format-data-before-final-empty-line.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-data.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-event-empty.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-event.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-id-2.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-id-3.window.js create mode 100644 test/fixtures/wpt/eventsource/format-field-id-null.window.js create mode 100644 test/fixtures/wpt/eventsource/format-field-id.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-parsing.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-retry-bogus.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-retry-empty.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-retry.any.js create mode 100644 test/fixtures/wpt/eventsource/format-field-unknown.any.js create mode 100644 test/fixtures/wpt/eventsource/format-leading-space.any.js create mode 100644 test/fixtures/wpt/eventsource/format-mime-bogus.any.js create mode 100644 test/fixtures/wpt/eventsource/format-mime-trailing-semicolon.any.js create mode 100644 test/fixtures/wpt/eventsource/format-mime-valid-bogus.any.js create mode 100644 test/fixtures/wpt/eventsource/format-newlines.any.js create mode 100644 test/fixtures/wpt/eventsource/format-null-character.any.js create mode 100644 test/fixtures/wpt/eventsource/format-utf-8.any.js create mode 100644 test/fixtures/wpt/eventsource/request-accept.any.js create mode 100644 test/fixtures/wpt/eventsource/request-cache-control.any.js create mode 100644 test/fixtures/wpt/eventsource/request-credentials.any.window.js create mode 100644 test/fixtures/wpt/eventsource/request-redirect.any.window.js create mode 100644 test/fixtures/wpt/eventsource/request-status-error.window.js create mode 100644 test/fixtures/wpt/eventsource/resources/accept.event_stream create mode 100644 test/fixtures/wpt/eventsource/resources/cache-control.event_stream create mode 100644 test/fixtures/wpt/eventsource/resources/eventsource-onmessage-realm.htm create mode 100644 test/fixtures/wpt/eventsource/resources/init.htm create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-close.htm create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-close.js create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.htm create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.js create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-url-bogus.js create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.htm create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.js create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-onmesage.js create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-onmessage.htm create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.htm create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.js create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.htm create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.js create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-url.htm create mode 100644 test/fixtures/wpt/eventsource/shared-worker/eventsource-url.js create mode 100644 test/parallel/test-eventsource-connect.mjs create mode 100644 test/parallel/test-eventsource-constructor.mjs create mode 100644 test/parallel/test-eventsource-error.mjs create mode 100644 test/parallel/test-eventsource-eventhandler-idl.mjs create mode 100644 test/parallel/test-eventsource-eventsourcestream-parse-line.mjs create mode 100644 test/parallel/test-eventsource-eventsourcestream-process-event.mjs create mode 100644 test/parallel/test-eventsource-eventsourcestream.mjs create mode 100644 test/parallel/test-eventsource-instance-constants.mjs create mode 100644 test/parallel/test-eventsource-is-valid-helpers.mjs create mode 100644 test/parallel/test-eventsource-messageevent.mjs create mode 100644 test/parallel/test-eventsource-redirect.mjs create mode 100644 test/parallel/test-eventsource-static-constants.mjs create mode 100644 test/parallel/test-eventsource.mjs create mode 100644 test/wpt/status/eventsource.json create mode 100644 test/wpt/test-eventsource.js diff --git a/.eslintrc.js b/.eslintrc.js index a154d00794ab4d..ac04fcc02da203 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -341,6 +341,7 @@ module.exports = { Crypto: 'readable', CryptoKey: 'readable', DecompressionStream: 'readable', + EventSource: 'readable', fetch: 'readable', FormData: 'readable', navigator: 'readable', diff --git a/benchmark/events_source/message-event-instantiate.js b/benchmark/events_source/message-event-instantiate.js new file mode 100644 index 00000000000000..fde29ca0289f60 --- /dev/null +++ b/benchmark/events_source/message-event-instantiate.js @@ -0,0 +1,21 @@ +'use strict'; + +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + n: [100000], +}, { + flags: ['--expose-internals'], +}); + +function main({ n }) { + const { MessageEvent } = require('internal/event_source'); + + bench.start(); + + for (let i = 0; i < n; i++) { + new MessageEvent('message', { }); + } + + bench.end(n); +} diff --git a/lib/internal/bootstrap/web/exposed-window-or-worker.js b/lib/internal/bootstrap/web/exposed-window-or-worker.js index 37e4518a5400b5..918531ec9e225a 100644 --- a/lib/internal/bootstrap/web/exposed-window-or-worker.js +++ b/lib/internal/bootstrap/web/exposed-window-or-worker.js @@ -55,3 +55,7 @@ defineReplaceableLazyAttribute(globalThis, 'perf_hooks', ['performance']); // https://w3c.github.io/FileAPI/#creating-revoking const { installObjectURLMethods } = require('internal/url'); installObjectURLMethods(); + +// https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate-dev +exposeLazyInterfaces(globalThis, 'internal/event_source', ['EventSource']); +defineReplaceableLazyAttribute(globalThis, 'internal/event_source', ['EventSource'], false); diff --git a/lib/internal/event_source.js b/lib/internal/event_source.js new file mode 100644 index 00000000000000..b8a8a6b4124b4c --- /dev/null +++ b/lib/internal/event_source.js @@ -0,0 +1,757 @@ +'use strict'; + +const { + NumberParseInt, + ObjectFreeze, + ObjectDefineProperties, + SymbolToStringTag, +} = primordials; +const { + Buffer, +} = require('buffer'); +const { + Transform, + pipeline, +} = require('stream'); +const { clearTimeout, setTimeout } = require('timers'); +const { + AbortController, +} = require('internal/abort_controller'); +const { fetch } = require('internal/deps/undici/undici'); +const { URL } = require('internal/url'); +const { + codes: { + ERR_MISSING_OPTION, + }, +} = require('internal/errors'); +const { + Event, + NodeEventTarget, + kEvents, +} = require('internal/event_target'); +const { kEmptyObject, kEnumerableProperty } = require('internal/util'); + +/** + * @type {number[]} BOM + */ +const BOM = [0xEF, 0xBB, 0xBF]; +/** + * @type {10} LF + */ +const LF = 0x0A; +/** + * @type {13} CR + */ +const CR = 0x0D; +/** + * @type {58} COLON + */ +const COLON = 0x3A; +/** + * @type {32} SPACE + */ +const SPACE = 0x20; + +/** + * The event stream format's MIME type is text/event-stream. + * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#parsing-an-event-stream + */ +const mimeType = 'text/event-stream'; + +/** + * A reconnection time, in milliseconds. This must initially be an implementation-defined value, + * probably in the region of a few seconds. + * + * In Comparison: + * - Chrome uses 3000ms. + * - Deno uses 5000ms. + */ +const defaultReconnectionTime = 3000; + +/** + * The readyState attribute represents the state of the connection. + * @enum + * @readonly + * @see https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate-dev + */ + +/** + * The connection has not yet been established, or it was closed and the user + * agent is reconnecting. + * @type {0} + */ +const CONNECTING = 0; + +/** + * The user agent has an open connection and is dispatching events as it + * receives them. + * @type {1} + */ +const OPEN = 1; + +/** + * The connection is not open, and the user agent is not trying to reconnect. + * @type {2} + */ +const CLOSED = 2; + +/** + * @typedef {MessagePort|ServiceWorker|WindowProxy} MessageEventSource + */ + +/** + * @typedef {object} MessageEventInit + * @property {*} [data] The data of the message. + * @property {string} [origin] The origin of the message emitter. + * @property {MessagePort[]} [ports] The ports associated with the channel the + * message is being sent through (where appropriate, e.g. in channel messaging + * or when sending a message to a shared worker). + * @property {MessageEventSource} [source] A MessageEventSource (which can be a + * WindowProxy, MessagePort, or ServiceWorker object) representing the message + * emitter. + * @property {string} [lastEventId] A DOMString representing a unique ID for + * the event. + */ + +/** + * The MessageEvent interface represents a message received by a target object. + * @class MessageEvent + * @extends {Event} + * @see https://html.spec.whatwg.org/multipage/comms.html#dom-messageevent-initmessageevent + */ +class MessageEvent extends Event { + #data = null; + #origin = null; + #source = null; + #ports = null; + #lastEventId = null; + + /** + * Creates a new MessageEvent object. + * @param {string} type + * @param {MessageEventInit} [options] + */ + constructor(type, options = kEmptyObject) { + super(type, options); + + this.#data = options.data ?? null; + this.#origin = options.origin || ''; + this.#ports = options.ports || []; + this.#source = options.source ?? null; + this.#lastEventId = options.lastEventId || ''; + } + + /** + * Returns the event's data. This can be any data type. + * @readonly + * @returns {*} + */ + get data() { + return this.#data; + } + + /** + * Returns the origin of the message, for server-sent events and + * cross-document messaging. + * @readonly + * @returns {string} + */ + get origin() { + return this.#origin; + } + + /** + * Returns the last event ID string, for server-sent events. + * @readonly + * @returns {string} + */ + get lastEventId() { + return this.#lastEventId; + } + + /** + * Returns a MessageEventSource (which can be a WindowProxy, MessagePort, + * or ServiceWorker object) representing the message source. + * @readonly + * @returns {MessageEventSource} + */ + get source() { + return this.#source; + } + + /** + * Returns the array containing the MessagePort objects + * representing the ports associated with the channel the message is being + * sent through (where appropriate, e.g. in channel messaging or when + * sending a message to a shared worker). + * @readonly + * @returns {MessagePort[]} + */ + get ports() { + return ObjectFreeze(this.#ports); + } + + /** + * Initializes the value of a MessageEvent created using the + * MessageEvent() constructor. + * @param {string} type + * @param {boolean} [bubbles=false] + * @param {boolean} [cancelable=false] + * @param {*} [data=null] + * @param {string} [origin=""] + * @param {string} [lastEventId=""] + * @param {MessageEventSource} [source=null] + * @param {MessagePort[]} [ports=[]] + */ + initMessageEvent( + type, + bubbles = false, + cancelable = false, + data = null, + origin = '', + lastEventId = '', + source = null, + ports = [], + ) { + this.initEvent(type, bubbles, cancelable); + + this.#data = data; + this.#origin = origin; + this.#lastEventId = lastEventId; + this.#source = source; + this.#ports = ports; + } +} + +ObjectDefineProperties(MessageEvent.prototype, { + [SymbolToStringTag]: { + __proto__: null, + writable: false, + enumerable: false, + configurable: true, + value: 'MessageEvent', + }, + detail: kEnumerableProperty, +}); + +/** + * Checks if the given value is a valid LastEventId. + * @param {Buffer | string} value + * @returns {boolean} + */ +function isValidLastEventId(value) { + // LastEventId should not contain U+0000 NULL + return ( + typeof value === 'string' && (value.indexOf('\u0000') === -1) + ); +} + +/** + * Checks if the given value is a base 10 digit. + * @param {Buffer | string} value + * @returns {boolean} + */ +function isASCIINumber(value) { + for (let i = 0; i < value.length; i++) { + if (value.charCodeAt(i) < 0x30 || value.charCodeAt(i) > 0x39) return false; + } + return true; +} + +/** + * @typedef {object} EventSourceStreamEvent + * @type {object} + * @property {string} [event] The event type. + * @property {string} [data] The data of the message. + * @property {string} [id] A unique ID for the event. + * @property {string} [retry] The reconnection time, in milliseconds. + */ + +/** + * @typedef EventSourceState + * @type {object} + * @property {string} lastEventId The last event ID received from the server. + * @property {string} origin The origin of the event source. + * @property {number} reconnectionTime The reconnection time, in milliseconds. + */ + +class EventSourceStream extends Transform { + /** + * @type {EventSourceState} + */ + state = null; + + /** + * Leading byte-order-mark check. + * @type {boolean} + */ + checkBOM = true; + + /** + * @type {boolean} + */ + crlfCheck = false; + + /** + * @type {boolean} + */ + eventEndCheck = false; + + /** + * @type {Buffer} + */ + buffer = null; + + pos = 0; + + event = { + data: undefined, + event: undefined, + id: undefined, + retry: undefined, + }; + + /** + * @param {object} options + * @param {EventSourceState} options.eventSourceState + * @param {Function} [options.push] + */ + constructor(options = {}) { + options.readableObjectMode = true; + super(options); + this.state = options.eventSourceState; + if (options.push) { + this.push = options.push; + } + } + + /** + * @param {Buffer} chunk + * @param {string} _encoding + * @param {Function} callback + * @returns {void} + */ + _transform(chunk, _encoding, callback) { + if (chunk.length === 0) { + callback(); + return; + } + this.buffer = this.buffer ? Buffer.concat([this.buffer, chunk]) : chunk; + + // Strip leading byte-order-mark if any + if (this.checkBOM) { + switch (this.buffer.length) { + case 1: + if (this.buffer[0] === BOM[0]) { + callback(); + return; + } + this.checkBOM = false; + break; + case 2: + if (this.buffer[0] === BOM[0] && this.buffer[1] === BOM[1]) { + callback(); + return; + } + this.checkBOM = false; + break; + case 3: + if (this.buffer[0] === BOM[0] && this.buffer[1] === BOM[1] && this.buffer[2] === BOM[2]) { + this.buffer = this.buffer.slice(3); + this.checkBOM = false; + callback(); + return; + } + this.checkBOM = false; + break; + default: + if (this.buffer[0] === BOM[0] && this.buffer[1] === BOM[1] && this.buffer[2] === BOM[2]) { + this.buffer = this.buffer.slice(3); + } + this.checkBOM = false; + break; + } + } + + while (this.pos < this.buffer.length) { + if (this.buffer[this.pos] === LF || this.buffer[this.pos] === CR) { + if (this.eventEndCheck) { + this.eventEndCheck = false; + this.processEvent(this.event); + this.event = { + data: undefined, + event: undefined, + id: undefined, + retry: undefined, + }; + this.buffer = this.buffer.slice(1); + continue; + } + if (this.buffer[0] === COLON) { + this.buffer = this.buffer.slice(1); + continue; + } + this.parseLine(this.buffer.slice(0, this.pos), this.event); + + // Remove the processed line from the buffer + this.buffer = this.buffer.slice(this.pos + 1); + // Reset the position + this.pos = 0; + this.eventEndCheck = true; + continue; + } + this.pos++; + } + + callback(); + } + + /** + * @param {Buffer} line + * @param {EventSourceStreamEvent} event + */ + parseLine(line, event) { + if (line.length === 0) { + return; + } + const fieldNameEnd = line.indexOf(COLON); + let fieldValueStart; + + if (fieldNameEnd === -1) { + return; + // fieldNameEnd = line.length; + // fieldValueStart = line.length; + } + fieldValueStart = fieldNameEnd + 1; + if (line[fieldValueStart] === SPACE) { + fieldValueStart += 1; + } + + + const fieldValueSize = line.length - fieldValueStart; + const fieldName = line.slice(0, fieldNameEnd).toString('utf8'); + switch (fieldName) { + case 'data': + event.data = line.slice(fieldValueStart, fieldValueStart + fieldValueSize).toString('utf8'); + break; + case 'event': + event.event = line.slice(fieldValueStart, fieldValueStart + fieldValueSize).toString('utf8'); + break; + case 'id': + event.id = line.slice(fieldValueStart, fieldValueStart + fieldValueSize).toString('utf8'); + break; + case 'retry': + event.retry = line.slice(fieldValueStart, fieldValueStart + fieldValueSize).toString('utf8'); + break; + } + } + + /** + * @param {EventSourceStreamEvent} event + */ + processEvent(event) { + if (event.retry) { + if (isASCIINumber(event.retry)) { + this.state.reconnectionTime = NumberParseInt(event.retry, 10); + } + } + const { + id, + data = null, + event: type = 'message', + } = event; + + if (id && isValidLastEventId(id)) { + this.state.lastEventId = id; + } + + this.push( + new MessageEvent(type, { + data, + lastEventId: this.state.lastEventId, + origin: this.state.origin, + }), + ); + } +} + +/** + * @typedef {object} EventSourceInit + * @property {boolean} [withCredentials] indicates whether the request + * should include credentials. + */ + +/** + * The EventSource interface is used to receive server-sent events. It + * connects to a server over HTTP and receives events in text/event-stream + * format without closing the connection. + * @extends {NodeEventTarget} + * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource + * @api public + */ +class EventSource extends NodeEventTarget { + #url = null; + #withCredentials = false; + #readyState = CONNECTING; + #lastEventId = ''; + #connection = null; + #reconnectionTimer = null; + #controller = new AbortController(); + /** + * @type {EventSourceState} + */ + #state = { + lastEventId: '', + origin: '', + reconnectionTime: defaultReconnectionTime, + }; + + /** + * Creates a new EventSource object. + * @param {string} url + * @param {EventSourceInit} [eventSourceInitDict] + */ + constructor(url, eventSourceInitDict) { + super(); + + if (arguments.length === 0) { + throw new ERR_MISSING_OPTION('url'); + } + + this.#url = `${url}`; + this.#state.origin = new URL(this.#url).origin; + + if (eventSourceInitDict) { + if (eventSourceInitDict.withCredentials) { + this.#withCredentials = eventSourceInitDict.withCredentials; + } + } + + this.#connect(); + } + + /** + * Returns the state of this EventSource object's connection. It can have the + * values described below. + * @returns {0|1|2} + * @readonly + */ + get readyState() { + return this.#readyState; + } + + /** + * Returns the URL providing the event stream. + * @readonly + * @returns {string} + */ + get url() { + return this.#url; + } + + /** + * Returns a boolean indicating whether the EventSource object was + * instantiated with CORS credentials set (true), or not (false, the default). + */ + get withCredentials() { + return this.#withCredentials; + } + + async #connect() { + this.#readyState = CONNECTING; + this.#connection = null; + + /** + * @type {RequestInit} + */ + const options = { + method: 'GET', + redirect: 'manual', + keepalive: true, + headers: { + 'Accept': mimeType, + 'Cache-Control': 'no-cache', + 'Connection': 'keep-alive', + }, + signal: this.#controller.signal, + }; + + if (this.#lastEventId) { + options.headers['Last-Event-ID'] = this.#lastEventId; + } + + options.credentials = this.#withCredentials ? 'include' : 'omit'; + + try { + this.#connection = await fetch(this.#url, options); + + // Handle HTTP redirects + // https://html.spec.whatwg.org/multipage/server-sent-events.html#server-sent-events-intro + switch (this.#connection.status) { + // Redirecting status codes + case 301: // 301 Moved Permanently + case 302: // 302 Found + case 307: // 307 Temporary Redirect + case 308: // 308 Permanent Redirect + if (!this.#connection.headers.has('Location')) { + this.close(); + this.dispatchEvent(new Event('error')); + return; + } + this.#url = new URL(this.#connection.headers.get('Location'), new URL(this.#url).origin).href; + this.#state.origin = new URL(this.#url).origin; + this.#connect(); + return; + case 204: // 204 No Content + // Clients will reconnect if the connection is closed; a client can be told to stop reconnecting + // using the HTTP 204 No Content response code. + this.close(); + this.dispatchEvent(new Event('error')); + return; + case 200: + if (this.#connection.headers.get('Content-Type') !== mimeType) { + this.close(); + this.dispatchEvent(new Event('error')); + return; + } + break; + default: + this.close(); + this.dispatchEvent(new Event('error')); + return; + } + + if (this.#connection === null) { + this.close(); + this.dispatchEvent(new Event('error')); + return; + } + + const self = this; + + pipeline(this.#connection.body, + new EventSourceStream({ + eventSourceState: this.#state, + push: function push(chunk) { + self.dispatchEvent(chunk); + }, + }), + (err) => { + if (err) { + this.dispatchEvent(new Event('error')); + this.close(); + } + }); + + this.dispatchEvent(new Event('open')); + this.#readyState = OPEN; + + } catch (error) { + if (error.name === 'AbortError') { + return; + } + this.dispatchEvent(new Event('error')); + + // Always set to CONNECTING as the readyState could be OPEN + this.#readyState = CONNECTING; + this.#connection = null; + + this.#reconnectionTimer = setTimeout(() => { + this.#connect(); + }, this.#state.reconnectionTime); + } + + } + + /** + * Closes the connection, if any, and sets the readyState attribute to + * CLOSED. + */ + close() { + if (this.#readyState === CLOSED) return; + clearTimeout(this.#reconnectionTimer); + this.#controller.abort(); + if (this.#connection) { + this.#connection = null; + } + this.#readyState = CLOSED; + } +} + +ObjectDefineProperties(EventSource, { + CONNECTING: { + __proto__: null, + configurable: false, + enumerable: true, + value: CONNECTING, + writable: false, + }, + OPEN: { + __proto__: null, + configurable: false, + enumerable: true, + value: OPEN, + writable: false, + }, + CLOSED: { + __proto__: null, + configurable: false, + enumerable: true, + value: CLOSED, + writable: false, + }, +}); + +EventSource.prototype.CONNECTING = CONNECTING; +EventSource.prototype.OPEN = OPEN; +EventSource.prototype.CLOSED = CLOSED; + +ObjectDefineProperties(EventSource.prototype, { + 'onopen': { + __proto__: null, + get: function get() { + const listener = this[kEvents].get('open'); + return listener && listener.size > 0 ? listener.next.listener : undefined; + }, + + set: function set(listener) { + if (typeof listener !== 'function') return; + this.removeAllListeners('open'); + this.addEventListener('open', listener); + }, + }, + 'onmessage': { + __proto__: null, + get: function get() { + const listener = this[kEvents].get('message'); + return listener && listener.size > 0 ? listener.next.listener : undefined; + }, + + set: function set(listener) { + if (typeof listener !== 'function') return; + this.removeAllListeners('message'); + this.addEventListener('message', listener); + }, + }, + 'onerror': { + __proto__: null, + get: function get() { + const listener = this[kEvents].get('error'); + return listener && listener.size > 0 ? listener.next.listener : undefined; + }, + + set: function set(listener) { + if (typeof listener !== 'function') return; + this.removeAllListeners('error'); + this.addEventListener('error', listener); + }, + }, +}); + +module.exports = { + EventSource, + EventSourceStream, + isValidLastEventId, + isASCIINumber, + MessageEvent, +}; diff --git a/lib/internal/process/pre_execution.js b/lib/internal/process/pre_execution.js index b6bdb4785003f7..98bbbcddeb57fe 100644 --- a/lib/internal/process/pre_execution.js +++ b/lib/internal/process/pre_execution.js @@ -98,6 +98,7 @@ function prepareExecution(options) { setupTraceCategoryState(); setupInspectorHooks(); setupNavigator(); + setupEventSource(); setupWarningHandler(); setupUndici(); setupWebCrypto(); @@ -371,6 +372,17 @@ function setupNavigator() { defineReplaceableLazyAttribute(globalThis, 'internal/navigator', ['navigator'], false); } +function setupEventSource() { + if (getEmbedderOptions().noBrowserGlobals || + getOptionValue('--no-experimental-global-eventsource')) { + return; + } + + // https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource-readystate-dev + exposeLazyInterfaces(globalThis, 'internal/event_source', ['EventSource']); + defineReplaceableLazyAttribute(globalThis, 'internal/event_source', ['EventSource'], false); +} + // TODO(aduh95): move this to internal/bootstrap/web/* when the CLI flag is // removed. function setupWebCrypto() { diff --git a/test/common/index.js b/test/common/index.js index 2ac981608b4e92..3524ebe9d6dbd9 100644 --- a/test/common/index.js +++ b/test/common/index.js @@ -308,6 +308,10 @@ if (global.gc) { knownGlobals.push(global.gc); } +if (global.EventSource) { + knownGlobals.push(global.EventSource); +} + if (global.navigator) { knownGlobals.push(global.navigator); } diff --git a/test/fixtures/wpt/README.md b/test/fixtures/wpt/README.md index 33af47703674bb..509d0c2b6023ff 100644 --- a/test/fixtures/wpt/README.md +++ b/test/fixtures/wpt/README.md @@ -16,6 +16,7 @@ Last update: - dom/abort: https://github.com/web-platform-tests/wpt/tree/d1f1ecbd52/dom/abort - dom/events: https://github.com/web-platform-tests/wpt/tree/ab8999891c/dom/events - encoding: https://github.com/web-platform-tests/wpt/tree/a58bbf6d8c/encoding +- eventsource: https://github.com/web-platform-tests/wpt/tree/9dafa89214/eventsource - fetch/data-urls/resources: https://github.com/web-platform-tests/wpt/tree/7c79d998ff/fetch/data-urls/resources - FileAPI: https://github.com/web-platform-tests/wpt/tree/e36dbb6f00/FileAPI - hr-time: https://github.com/web-platform-tests/wpt/tree/34cafd797e/hr-time diff --git a/test/fixtures/wpt/eventsource/META.yml b/test/fixtures/wpt/eventsource/META.yml new file mode 100644 index 00000000000000..437da600931424 --- /dev/null +++ b/test/fixtures/wpt/eventsource/META.yml @@ -0,0 +1,5 @@ +spec: https://html.spec.whatwg.org/multipage/server-sent-events.html +suggested_reviewers: + - odinho + - Yaffle + - annevk diff --git a/test/fixtures/wpt/eventsource/README.md b/test/fixtures/wpt/eventsource/README.md new file mode 100644 index 00000000000000..e19a0ba6c7448e --- /dev/null +++ b/test/fixtures/wpt/eventsource/README.md @@ -0,0 +1,4 @@ +These are the Server-sent events (`EventSource`) tests for the +[Server-sent events chapter of the HTML Standard](https://html.spec.whatwg.org/multipage/comms.html#server-sent-events). + +IDL tests are part of the `/html/dom/idlharness.*` resources. diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close.htm b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close.htm new file mode 100644 index 00000000000000..f26aaaa4a900dc --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close.htm @@ -0,0 +1,24 @@ + + + + dedicated worker - EventSource: close() + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close.js new file mode 100644 index 00000000000000..875c9098bac380 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close.js @@ -0,0 +1,9 @@ +try { + var source = new EventSource("../resources/message.py") + source.onopen = function(e) { + this.close() + postMessage([true, this.readyState]) + } +} catch(e) { + postMessage([false, String(e)]) +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close2.htm b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close2.htm new file mode 100644 index 00000000000000..34e07a2694e26e --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close2.htm @@ -0,0 +1,23 @@ + + + + dedicated worker - EventSource created after: worker.close() + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close2.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close2.js new file mode 100644 index 00000000000000..4a9cbd20b8ab17 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-close2.js @@ -0,0 +1,3 @@ +self.close() +var source = new EventSource("../resources/message.py") +postMessage(source.readyState) \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-no-new.any.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-no-new.any.js new file mode 100644 index 00000000000000..48bc551130ca85 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-no-new.any.js @@ -0,0 +1,7 @@ +test(function() { + assert_throws_js(TypeError, + function() { + EventSource(''); + }, + "Calling EventSource constructor without 'new' must throw"); +}) diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-non-same-origin.htm b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-non-same-origin.htm new file mode 100644 index 00000000000000..b49d7ed609d071 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-non-same-origin.htm @@ -0,0 +1,34 @@ + + + + dedicated worker - EventSource: constructor (act as if there is a network error) + + + + + +
+ + + + diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-non-same-origin.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-non-same-origin.js new file mode 100644 index 00000000000000..5ec25a0678ce3a --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-non-same-origin.js @@ -0,0 +1,10 @@ +try { + var url = decodeURIComponent(location.hash.substr(1)) + var source = new EventSource(url) + source.onerror = function(e) { + postMessage([true, this.readyState, 'data' in e]) + this.close(); + } +} catch(e) { + postMessage([false, String(e)]) +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-url-bogus.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-url-bogus.js new file mode 100644 index 00000000000000..2a450a346314de --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-constructor-url-bogus.js @@ -0,0 +1,7 @@ +try { + var source = new EventSource("http://this is invalid/") + postMessage([false, 'no exception thrown']) + source.close() +} catch(e) { + postMessage([true, e.code]) +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-eventtarget.worker.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-eventtarget.worker.js new file mode 100644 index 00000000000000..73b30556c49786 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-eventtarget.worker.js @@ -0,0 +1,11 @@ +importScripts("/resources/testharness.js"); + +async_test(function() { + var source = new EventSource("../resources/message.py") + source.addEventListener("message", this.step_func_done(function(e) { + assert_equals(e.data, 'data'); + source.close(); + }), false) +}, "dedicated worker - EventSource: addEventListener()"); + +done(); diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onmesage.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onmesage.js new file mode 100644 index 00000000000000..9629f5e7936430 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onmesage.js @@ -0,0 +1,9 @@ +try { + var source = new EventSource("../resources/message.py") + source.onmessage = function(e) { + postMessage([true, e.data]) + this.close() + } +} catch(e) { + postMessage([false, String(e)]) +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onmessage.htm b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onmessage.htm new file mode 100644 index 00000000000000..c61855f5249da7 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onmessage.htm @@ -0,0 +1,24 @@ + + + + dedicated worker - EventSource: onmessage + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onopen.htm b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onopen.htm new file mode 100644 index 00000000000000..010b0c66a8c065 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onopen.htm @@ -0,0 +1,27 @@ + + + + dedicated worker - EventSource: onopen (announcing the connection) + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onopen.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onopen.js new file mode 100644 index 00000000000000..72a1053263040b --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-onopen.js @@ -0,0 +1,9 @@ +try { + var source = new EventSource("../resources/message.py") + source.onopen = function(e) { + postMessage([true, source.readyState, 'data' in e, e.bubbles, e.cancelable]) + this.close() + } +} catch(e) { + postMessage([false, String(e)]) +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-prototype.htm b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-prototype.htm new file mode 100644 index 00000000000000..5a5ac4ec2a7217 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-prototype.htm @@ -0,0 +1,25 @@ + + + + dedicated worker - EventSource: prototype et al + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-prototype.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-prototype.js new file mode 100644 index 00000000000000..26993cb4efdc50 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-prototype.js @@ -0,0 +1,8 @@ +try { + EventSource.prototype.ReturnTrue = function() { return true } + var source = new EventSource("../resources/message.py") + postMessage([true, source.ReturnTrue(), 'EventSource' in self]) + source.close() +} catch(e) { + postMessage([false, String(e)]) +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-url.htm b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-url.htm new file mode 100644 index 00000000000000..59e77cba57c3b4 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-url.htm @@ -0,0 +1,25 @@ + + + + dedicated worker - EventSource: url + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-url.js b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-url.js new file mode 100644 index 00000000000000..7a3c8030d27581 --- /dev/null +++ b/test/fixtures/wpt/eventsource/dedicated-worker/eventsource-url.js @@ -0,0 +1,7 @@ +try { + var source = new EventSource("../resources/message.py") + postMessage([true, source.url]) + source.close() +} catch(e) { + postMessage([false, String(e)]) +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/event-data.any.js b/test/fixtures/wpt/eventsource/event-data.any.js new file mode 100644 index 00000000000000..12867694f856f1 --- /dev/null +++ b/test/fixtures/wpt/eventsource/event-data.any.js @@ -0,0 +1,21 @@ +// META: title=EventSource: lines and data parsing + + var test = async_test(); + test.step(function() { + var source = new EventSource("resources/message2.py"), + counter = 0; + source.onmessage = test.step_func(function(e) { + if(counter == 0) { + assert_equals(e.data,"msg\nmsg"); + } else if(counter == 1) { + assert_equals(e.data,""); + } else if(counter == 2) { + assert_equals(e.data,"end"); + source.close(); + test.done(); + } else { + assert_unreached(); + } + counter++; + }); + }); diff --git a/test/fixtures/wpt/eventsource/eventsource-close.window.js b/test/fixtures/wpt/eventsource/eventsource-close.window.js new file mode 100644 index 00000000000000..e5693e6314bf21 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-close.window.js @@ -0,0 +1,70 @@ +// META: title=EventSource: close() + + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py") + assert_equals(source.readyState, source.CONNECTING, "connecting readyState"); + source.onopen = this.step_func(function() { + assert_equals(source.readyState, source.OPEN, "open readyState"); + source.close() + assert_equals(source.readyState, source.CLOSED, "closed readyState"); + this.done() + }) + }) + + var test2 = async_test(document.title + ", test events"); + test2.step(function() { + var count = 0, reconnected = false, + source = new EventSource("resources/reconnect-fail.py?id=" + new Date().getTime()); + + source.onerror = this.step_func(function(e) { + assert_equals(e.type, 'error'); + switch(count) { + // reconnecting after first message + case 1: + assert_equals(source.readyState, source.CONNECTING, "reconnecting readyState"); + + reconnected = true; + break; + + // one more reconnect to get to the closing + case 2: + assert_equals(source.readyState, source.CONNECTING, "last reconnecting readyState"); + count++; + break; + + // close + case 3: + assert_equals(source.readyState, source.CLOSED, "closed readyState"); + + // give some time for errors to hit us + test2.step_timeout(function() { this.done(); }, 100); + break; + + default: + assert_unreached("Error handler with msg count " + count); + } + + }); + + source.onmessage = this.step_func(function(e) { + switch(count) { + case 0: + assert_true(!reconnected, "no error event run"); + assert_equals(e.data, "opened", "data"); + break; + + case 1: + assert_true(reconnected, "have reconnected"); + assert_equals(e.data, "reconnected", "data"); + break; + + default: + assert_unreached("Dunno what to do with message number " + count); + } + + count++; + }); + + }); + diff --git a/test/fixtures/wpt/eventsource/eventsource-constructor-document-domain.window.js b/test/fixtures/wpt/eventsource/eventsource-constructor-document-domain.window.js new file mode 100644 index 00000000000000..defaee5b36e61a --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-constructor-document-domain.window.js @@ -0,0 +1,18 @@ +// META: title=EventSource: document.domain + + var test = async_test() + test.step(function() { + document.domain = document.domain + source = new EventSource("resources/message.py") + source.onopen = function(e) { + test.step(function() { + assert_equals(source.readyState, source.OPEN) + assert_false(e.hasOwnProperty('data')) + assert_false(e.bubbles) + assert_false(e.cancelable) + this.close() + }, this) + test.done() + } + }) + // Apart from document.domain equivalent to the onopen test. diff --git a/test/fixtures/wpt/eventsource/eventsource-constructor-empty-url.any.js b/test/fixtures/wpt/eventsource/eventsource-constructor-empty-url.any.js new file mode 100644 index 00000000000000..850d854db4d22b --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-constructor-empty-url.any.js @@ -0,0 +1,6 @@ +// META: global=window,worker + +test(function() { + const source = new EventSource(""); + assert_equals(source.url, self.location.toString()); +}, "EventSource constructor with an empty url."); diff --git a/test/fixtures/wpt/eventsource/eventsource-constructor-non-same-origin.window.js b/test/fixtures/wpt/eventsource/eventsource-constructor-non-same-origin.window.js new file mode 100644 index 00000000000000..bb32ed4b76e5c7 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-constructor-non-same-origin.window.js @@ -0,0 +1,21 @@ +// META: title=EventSource: constructor (act as if there is a network error) + + function fetchFail(url) { + var test = async_test(document.title + " (" + url + ")") + test.step(function() { + var source = new EventSource(url) + source.onerror = function(e) { + test.step(function() { + assert_equals(source.readyState, source.CLOSED) + assert_false(e.hasOwnProperty('data')) + }) + test.done() + } + }) + } + fetchFail("ftp://example.not/") + fetchFail("about:blank") + fetchFail("mailto:whatwg@awesome.example") + fetchFail("javascript:alert('FAIL')") + // This tests "fails the connection" as well as making sure a simple + // event is dispatched and not a MessageEvent diff --git a/test/fixtures/wpt/eventsource/eventsource-constructor-stringify.window.js b/test/fixtures/wpt/eventsource/eventsource-constructor-stringify.window.js new file mode 100644 index 00000000000000..ba14f90c6c65e1 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-constructor-stringify.window.js @@ -0,0 +1,28 @@ +// META: title=EventSource: stringify argument + + async_test(function (test) { + test.step(function() { + var source = new EventSource({toString:function(){return "resources/message.py";}}) + source.onopen = function(e) { + test.step(function() { + assert_false(e.hasOwnProperty('data')) + source.close() + test.done() + }) + } + }); + }, document.title + ', object'); + + test(function(){ + var source = new EventSource(1); + assert_regexp_match(source.url, /\/1$/); + }, document.title + ', 1'); + test(function(){ + var source = new EventSource(null); + assert_regexp_match(source.url, /\/null$/); + }, document.title + ', null'); + test(function(){ + var source = new EventSource(undefined); + assert_regexp_match(source.url, /\/undefined$/); + }, document.title + ', undefined'); + diff --git a/test/fixtures/wpt/eventsource/eventsource-constructor-url-bogus.any.js b/test/fixtures/wpt/eventsource/eventsource-constructor-url-bogus.any.js new file mode 100644 index 00000000000000..53c3205e8a55d7 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-constructor-url-bogus.any.js @@ -0,0 +1,8 @@ +// META: global=window,worker +// META: title=EventSource: constructor (invalid URL) + +test(() => { + assert_throws_dom('SyntaxError', () => { new EventSource("http://this is invalid/"); }); +}); + +done(); diff --git a/test/fixtures/wpt/eventsource/eventsource-constructor-url-multi-window.htm b/test/fixtures/wpt/eventsource/eventsource-constructor-url-multi-window.htm new file mode 100644 index 00000000000000..99fecb972c08a2 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-constructor-url-multi-window.htm @@ -0,0 +1,37 @@ + + + + EventSource: resolving URLs + + + + +
+ + + + diff --git a/test/fixtures/wpt/eventsource/eventsource-cross-origin.window.js b/test/fixtures/wpt/eventsource/eventsource-cross-origin.window.js new file mode 100644 index 00000000000000..23bd27a7dceacd --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-cross-origin.window.js @@ -0,0 +1,51 @@ +// META: title=EventSource: cross-origin + + const crossdomain = location.href.replace('://', '://élève.').replace(/\/[^\/]*$/, '/'), + origin = location.origin.replace('://', '://xn--lve-6lad.'); + + + function doCORS(url, title) { + async_test(document.title + " " + title).step(function() { + var source = new EventSource(url, { withCredentials: true }) + source.onmessage = this.step_func_done(e => { + assert_equals(e.data, "data"); + assert_equals(e.origin, origin); + source.close(); + }) + }) + } + + doCORS(crossdomain + "resources/cors.py?run=message", + "basic use") + doCORS(crossdomain + "resources/cors.py?run=redirect&location=/eventsource/resources/cors.py?run=message", + "redirect use") + doCORS(crossdomain + "resources/cors.py?run=status-reconnect&status=200", + "redirect use recon") + + function failCORS(url, title) { + async_test(document.title + " " + title).step(function() { + var source = new EventSource(url) + source.onerror = this.step_func(function(e) { + assert_equals(source.readyState, source.CLOSED, 'readyState') + assert_false(e.hasOwnProperty('data')) + source.close() + this.done() + }) + + /* Shouldn't happen */ + source.onmessage = this.step_func(function(e) { + assert_unreached("shouldn't fire message event") + }) + source.onopen = this.step_func(function(e) { + assert_unreached("shouldn't fire open event") + }) + }) + } + + failCORS(crossdomain + "resources/cors.py?run=message&origin=http://example.org", + "allow-origin: http://example.org should fail") + failCORS(crossdomain + "resources/cors.py?run=message&origin=", + "allow-origin:'' should fail") + failCORS(crossdomain + "resources/message.py", + "No allow-origin should fail") + diff --git a/test/fixtures/wpt/eventsource/eventsource-eventtarget.any.js b/test/fixtures/wpt/eventsource/eventsource-eventtarget.any.js new file mode 100644 index 00000000000000..b0d0017dd25912 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-eventtarget.any.js @@ -0,0 +1,16 @@ +// META: title=EventSource: addEventListener() + + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py") + source.addEventListener("message", listener, false) + }) + function listener(e) { + test.step(function() { + assert_equals("data", e.data) + this.close() + }, this) + test.done() + } + + diff --git a/test/fixtures/wpt/eventsource/eventsource-onmessage-realm.htm b/test/fixtures/wpt/eventsource/eventsource-onmessage-realm.htm new file mode 100644 index 00000000000000..db2218b5168121 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-onmessage-realm.htm @@ -0,0 +1,25 @@ + + +EventSource: message event Realm + + + + + + + diff --git a/test/fixtures/wpt/eventsource/eventsource-onmessage-trusted.any.js b/test/fixtures/wpt/eventsource/eventsource-onmessage-trusted.any.js new file mode 100644 index 00000000000000..d0be4d03e8b286 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-onmessage-trusted.any.js @@ -0,0 +1,12 @@ +// META: title=EventSource message events are trusted + +"use strict"; + +async_test(t => { + const source = new EventSource("resources/message.py"); + + source.onmessage = t.step_func_done(e => { + source.close(); + assert_equals(e.isTrusted, true); + }); +}, "EventSource message events are trusted"); diff --git a/test/fixtures/wpt/eventsource/eventsource-onmessage.any.js b/test/fixtures/wpt/eventsource/eventsource-onmessage.any.js new file mode 100644 index 00000000000000..391fa4b1933a44 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-onmessage.any.js @@ -0,0 +1,14 @@ +// META: title=EventSource: onmessage + + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py") + source.onmessage = function(e) { + test.step(function() { + assert_equals("data", e.data) + source.close() + }) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/eventsource-onopen.any.js b/test/fixtures/wpt/eventsource/eventsource-onopen.any.js new file mode 100644 index 00000000000000..3977cb176e096c --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-onopen.any.js @@ -0,0 +1,17 @@ +// META: title=EventSource: onopen (announcing the connection) + + var test = async_test() + test.step(function() { + source = new EventSource("resources/message.py") + source.onopen = function(e) { + test.step(function() { + assert_equals(source.readyState, source.OPEN) + assert_false(e.hasOwnProperty('data')) + assert_false(e.bubbles) + assert_false(e.cancelable) + this.close() + }, this) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/eventsource-prototype.any.js b/test/fixtures/wpt/eventsource/eventsource-prototype.any.js new file mode 100644 index 00000000000000..b7aefb32f44acc --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-prototype.any.js @@ -0,0 +1,10 @@ +// META: title=EventSource: prototype et al + + test(function() { + EventSource.prototype.ReturnTrue = function() { return true } + var source = new EventSource("resources/message.py") + assert_true(source.ReturnTrue()) + assert_own_property(self, "EventSource") + source.close() + }) + diff --git a/test/fixtures/wpt/eventsource/eventsource-reconnect.window.js b/test/fixtures/wpt/eventsource/eventsource-reconnect.window.js new file mode 100644 index 00000000000000..551fbdc88b25b1 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-reconnect.window.js @@ -0,0 +1,47 @@ +// META: title=EventSource: reconnection + + function doReconn(url, title) { + var test = async_test(document.title + " " + title) + test.step(function() { + var source = new EventSource(url) + source.onmessage = test.step_func(function(e) { + assert_equals(e.data, "data") + source.close() + test.done() + }) + }) + } + + doReconn("resources/status-reconnect.py?status=200", + "200") + + + var t = async_test(document.title + ", test reconnection events"); + t.step(function() { + var opened = false, reconnected = false, + source = new EventSource("resources/status-reconnect.py?status=200&ok_first&id=2"); + + source.onerror = t.step_func(function(e) { + assert_equals(e.type, 'error'); + assert_equals(source.readyState, source.CONNECTING, "readyState"); + assert_true(opened, "connection is opened earlier"); + + reconnected = true; + }); + + source.onmessage = t.step_func(function(e) { + if (!opened) { + opened = true; + assert_false(reconnected, "have reconnected before first message"); + assert_equals(e.data, "ok"); + } + else { + assert_true(reconnected, "Got reconnection event"); + assert_equals(e.data, "data"); + source.close() + t.done() + } + }); + }); + + diff --git a/test/fixtures/wpt/eventsource/eventsource-request-cancellation.any.window.js b/test/fixtures/wpt/eventsource/eventsource-request-cancellation.any.window.js new file mode 100644 index 00000000000000..1cee9b742ea284 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-request-cancellation.any.window.js @@ -0,0 +1,21 @@ +// META: title=EventSource: request cancellation + + var t = async_test(); + onload = t.step_func(function() { + var url = "resources/message.py?sleep=1000&message=" + encodeURIComponent("retry:1000\ndata:abc\n\n"); + var es = new EventSource(url); + es.onerror = t.step_func(function() { + assert_equals(es.readyState, EventSource.CLOSED) + t.step_timeout(function () { + assert_equals(es.readyState, EventSource.CLOSED, + "After stopping the eventsource readyState should be CLOSED") + t.done(); + }, 1000); + }); + + t.step_timeout(function() { + window.stop() + es.onopen = t.unreached_func("Got open event"); + es.onmessage = t.unreached_func("Got message after closing source"); + }, 0); + }); diff --git a/test/fixtures/wpt/eventsource/eventsource-url.any.js b/test/fixtures/wpt/eventsource/eventsource-url.any.js new file mode 100644 index 00000000000000..92207ea78a1423 --- /dev/null +++ b/test/fixtures/wpt/eventsource/eventsource-url.any.js @@ -0,0 +1,8 @@ +// META: title=EventSource: url + + test(function() { + var url = "resources/message.py", + source = new EventSource(url) + assert_equals(source.url.substr(-(url.length)), url) + source.close() + }) diff --git a/test/fixtures/wpt/eventsource/format-bom-2.any.js b/test/fixtures/wpt/eventsource/format-bom-2.any.js new file mode 100644 index 00000000000000..8b7be8402c08d5 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-bom-2.any.js @@ -0,0 +1,24 @@ +// META: title=EventSource: Double BOM + + var test = async_test(), + hasbeenone = false, + hasbeentwo = false + test.step(function() { + var source = new EventSource("resources/message.py?message=%EF%BB%BF%EF%BB%BFdata%3A1%0A%0Adata%3A2%0A%0Adata%3A3") + source.addEventListener("message", listener, false) + }) + function listener(e) { + test.step(function() { + if(e.data == "1") + hasbeenone = true + if(e.data == "2") + hasbeentwo = true + if(e.data == "3") { + assert_false(hasbeenone) + assert_true(hasbeentwo) + this.close() + test.done() + } + }, this) + } + diff --git a/test/fixtures/wpt/eventsource/format-bom.any.js b/test/fixtures/wpt/eventsource/format-bom.any.js new file mode 100644 index 00000000000000..05d1abd18b1f17 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-bom.any.js @@ -0,0 +1,24 @@ +// META: title=EventSource: BOM + + var test = async_test(), + hasbeenone = false, + hasbeentwo = false + test.step(function() { + var source = new EventSource("resources/message.py?message=%EF%BB%BFdata%3A1%0A%0A%EF%BB%BFdata%3A2%0A%0Adata%3A3") + source.addEventListener("message", listener, false) + }) + function listener(e) { + test.step(function() { + if(e.data == "1") + hasbeenone = true + if(e.data == "2") + hasbeentwo = true + if(e.data == "3") { + assert_true(hasbeenone) + assert_false(hasbeentwo) + this.close() + test.done() + } + }, this) + } + diff --git a/test/fixtures/wpt/eventsource/format-comments.any.js b/test/fixtures/wpt/eventsource/format-comments.any.js new file mode 100644 index 00000000000000..186e4714ba356f --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-comments.any.js @@ -0,0 +1,16 @@ +// META: title=EventSource: comment fest + + var test = async_test() + test.step(function() { + var longstring = (new Array(2*1024+1)).join("x"), // cannot make the string too long; causes timeout + message = encodeURI("data:1\r:\0\n:\r\ndata:2\n:" + longstring + "\rdata:3\n:data:fail\r:" + longstring + "\ndata:4\n"), + source = new EventSource("resources/message.py?message=" + message + "&newline=none") + source.onmessage = function(e) { + test.step(function() { + assert_equals("1\n2\n3\n4", e.data) + source.close() + }) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-data-before-final-empty-line.any.js b/test/fixtures/wpt/eventsource/format-data-before-final-empty-line.any.js new file mode 100644 index 00000000000000..5a4d84d28d3bc2 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-data-before-final-empty-line.any.js @@ -0,0 +1,17 @@ +// META: title=EventSource: a data before final empty line + + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?newline=none&message=" + encodeURIComponent("retry:1000\ndata:test1\n\nid:test\ndata:test2")) + var count = 0; + source.onmessage = function(e) { + if (++count === 2) { + test.step(function() { + assert_equals(e.lastEventId, "", "lastEventId") + assert_equals(e.data, "test1", "data") + source.close() + }) + test.done() + } + } + }) diff --git a/test/fixtures/wpt/eventsource/format-field-data.any.js b/test/fixtures/wpt/eventsource/format-field-data.any.js new file mode 100644 index 00000000000000..bea9be174249c7 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-data.any.js @@ -0,0 +1,23 @@ +// META: title=EventSource: data field parsing + + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?message=data%3A%0A%0Adata%0Adata%0A%0Adata%3Atest"), + counter = 0 + source.onmessage = function(e) { + test.step(function() { + if(counter == 0) { + assert_equals("", e.data) + } else if(counter == 1) { + assert_equals("\n", e.data) + } else if(counter == 2) { + assert_equals("test", e.data) + source.close() + test.done() + } else { + assert_unreached() + } + counter++ + }) + } + }) diff --git a/test/fixtures/wpt/eventsource/format-field-event-empty.any.js b/test/fixtures/wpt/eventsource/format-field-event-empty.any.js new file mode 100644 index 00000000000000..ada8e5725feb3c --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-event-empty.any.js @@ -0,0 +1,13 @@ +// META: title=EventSource: empty "event" field + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?message=event%3A%20%0Adata%3Adata") + source.onmessage = function(e) { + test.step(function() { + assert_equals("data", e.data) + this.close() + }, this) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-field-event.any.js b/test/fixtures/wpt/eventsource/format-field-event.any.js new file mode 100644 index 00000000000000..0c7d1fc26625a4 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-event.any.js @@ -0,0 +1,15 @@ +// META: title=EventSource: custom event name + var test = async_test(), + dispatchedtest = false + test.step(function() { + var source = new EventSource("resources/message.py?message=event%3Atest%0Adata%3Ax%0A%0Adata%3Ax") + source.addEventListener("test", function() { test.step(function() { dispatchedtest = true }) }, false) + source.onmessage = function() { + test.step(function() { + assert_true(dispatchedtest) + this.close() + }, this) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-field-id-2.any.js b/test/fixtures/wpt/eventsource/format-field-id-2.any.js new file mode 100644 index 00000000000000..9933f46b875723 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-id-2.any.js @@ -0,0 +1,25 @@ +// META: title=EventSource: Last-Event-ID (2) + var test = async_test() + test.step(function() { + var source = new EventSource("resources/last-event-id.py"), + counter = 0 + source.onmessage = function(e) { + test.step(function() { + if(e.data == "hello" && counter == 0) { + counter++ + assert_equals(e.lastEventId, "…") + } else if(counter == 1) { + counter++ + assert_equals("…", e.data) + assert_equals("…", e.lastEventId) + } else if(counter == 2) { + counter++ + assert_equals("…", e.data) + assert_equals("…", e.lastEventId) + source.close() + test.done() + } else + assert_unreached() + }) + } + }) diff --git a/test/fixtures/wpt/eventsource/format-field-id-3.window.js b/test/fixtures/wpt/eventsource/format-field-id-3.window.js new file mode 100644 index 00000000000000..3766fbf7bb1ea8 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-id-3.window.js @@ -0,0 +1,56 @@ +const ID_PERSISTS = 1, +ID_RESETS_1 = 2, +ID_RESETS_2 = 3; + +async_test(testPersist, "EventSource: lastEventId persists"); +async_test(testReset(ID_RESETS_1), "EventSource: lastEventId resets"); +async_test(testReset(ID_RESETS_2), "EventSource: lastEventId resets (id without colon)"); + +function testPersist(t) { + const source = new EventSource("resources/last-event-id2.py?type=" + ID_PERSISTS); + let counter = 0; + t.add_cleanup(() => source.close()); + source.onmessage = t.step_func(e => { + counter++; + if (counter === 1) { + assert_equals(e.lastEventId, "1"); + assert_equals(e.data, "1"); + } else if (counter === 2) { + assert_equals(e.lastEventId, "1"); + assert_equals(e.data, "2"); + } else if (counter === 3) { + assert_equals(e.lastEventId, "2"); + assert_equals(e.data, "3"); + } else if (counter === 4) { + assert_equals(e.lastEventId, "2"); + assert_equals(e.data, "4"); + t.done(); + } else { + assert_unreached(); + } + }); +} + +function testReset(type) { + return function (t) { + const source = new EventSource("resources/last-event-id2.py?type=" + type); + let counter = 0; + t.add_cleanup(() => source.close()); + source.onmessage = t.step_func(e => { + counter++; + if (counter === 1) { + assert_equals(e.lastEventId, "1"); + assert_equals(e.data, "1"); + } else if (counter === 2) { + assert_equals(e.lastEventId, ""); + assert_equals(e.data, "2"); + } else if (counter === 3) { + assert_equals(e.lastEventId, ""); + assert_equals(e.data, "3"); + t.done(); + } else { + assert_unreached(); + } + }); + } +} diff --git a/test/fixtures/wpt/eventsource/format-field-id-null.window.js b/test/fixtures/wpt/eventsource/format-field-id-null.window.js new file mode 100644 index 00000000000000..6d564dde0f211e --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-id-null.window.js @@ -0,0 +1,25 @@ +[ + "\u0000\u0000", + "x\u0000", + "\u0000x", + "x\u0000x", + " \u0000" +].forEach(idValue => { + const encodedIdValue = encodeURIComponent(idValue); + async_test(t => { + const source = new EventSource("resources/last-event-id.py?idvalue=" + encodedIdValue); + t.add_cleanup(() => source.close()); + let seenhello = false; + source.onmessage = t.step_func(e => { + if (e.data == "hello" && !seenhello) { + seenhello = true; + assert_equals(e.lastEventId, ""); + } else if(seenhello) { + assert_equals(e.data, "hello"); + assert_equals(e.lastEventId, ""); + t.done(); + } else + assert_unreached(); + }); + }, "EventSource: id field set to " + encodedIdValue); +}); diff --git a/test/fixtures/wpt/eventsource/format-field-id.any.js b/test/fixtures/wpt/eventsource/format-field-id.any.js new file mode 100644 index 00000000000000..26f1aea7091c6e --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-id.any.js @@ -0,0 +1,21 @@ +// META: title=EventSource: Last-Event-ID + var test = async_test() + test.step(function() { + var source = new EventSource("resources/last-event-id.py"), + seenhello = false + source.onmessage = function(e) { + test.step(function() { + if(e.data == "hello" && !seenhello) { + seenhello = true + assert_equals(e.lastEventId, "…") + } else if(seenhello) { + assert_equals("…", e.data) + assert_equals("…", e.lastEventId) + source.close() + test.done() + } else + assert_unreached() + }) + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-field-parsing.any.js b/test/fixtures/wpt/eventsource/format-field-parsing.any.js new file mode 100644 index 00000000000000..9b05187153a3ff --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-parsing.any.js @@ -0,0 +1,14 @@ +// META: title=EventSource: field parsing + var test = async_test() + test.step(function() { + var message = encodeURI("data:\0\ndata: 2\rData:1\ndata\0:2\ndata:1\r\0data:4\nda-ta:3\rdata_5\ndata:3\rdata:\r\n data:32\ndata:4\n"), + source = new EventSource("resources/message.py?message=" + message + "&newline=none") + source.onmessage = function(e) { + test.step(function() { + assert_equals(e.data, "\0\n 2\n1\n3\n\n4") + source.close() + }) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-field-retry-bogus.any.js b/test/fixtures/wpt/eventsource/format-field-retry-bogus.any.js new file mode 100644 index 00000000000000..86d9b9ea4090d6 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-retry-bogus.any.js @@ -0,0 +1,19 @@ +// META: title=EventSource: "retry" field (bogus) + var test = async_test() + test.step(function() { + var timeoutms = 3000, + source = new EventSource("resources/message.py?message=retry%3A3000%0Aretry%3A1000x%0Adata%3Ax"), + opened = 0 + source.onopen = function() { + test.step(function() { + if(opened == 0) + opened = new Date().getTime() + else { + var diff = (new Date().getTime()) - opened + assert_true(Math.abs(1 - diff / timeoutms) < 0.25) // allow 25% difference + this.close(); + test.done() + } + }, this) + } + }) diff --git a/test/fixtures/wpt/eventsource/format-field-retry-empty.any.js b/test/fixtures/wpt/eventsource/format-field-retry-empty.any.js new file mode 100644 index 00000000000000..e7d5e76a13470c --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-retry-empty.any.js @@ -0,0 +1,13 @@ +// META: title=EventSource: empty retry field + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?message=retry%0Adata%3Atest") + source.onmessage = function(e) { + test.step(function() { + assert_equals("test", e.data) + source.close() + }) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-field-retry.any.js b/test/fixtures/wpt/eventsource/format-field-retry.any.js new file mode 100644 index 00000000000000..819241dbd406d2 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-retry.any.js @@ -0,0 +1,21 @@ +// META: title=EventSource: "retry" field + var test = async_test(); + test.step(function() { + var timeoutms = 3000, + timeoutstr = "03000", // 1536 in octal, but should be 3000 + source = new EventSource("resources/message.py?message=retry%3A" + timeoutstr + "%0Adata%3Ax"), + opened = 0 + source.onopen = function() { + test.step(function() { + if(opened == 0) + opened = new Date().getTime() + else { + var diff = (new Date().getTime()) - opened + assert_true(Math.abs(1 - diff / timeoutms) < 0.25) // allow 25% difference + this.close(); + test.done() + } + }, this) + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-field-unknown.any.js b/test/fixtures/wpt/eventsource/format-field-unknown.any.js new file mode 100644 index 00000000000000..f702ed8565d3b0 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-field-unknown.any.js @@ -0,0 +1,13 @@ +// META: title=EventSource: unknown fields and parsing fun + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?message=data%3Atest%0A%20data%0Adata%0Afoobar%3Axxx%0Ajustsometext%0A%3Athisisacommentyay%0Adata%3Atest") + source.onmessage = function(e) { + test.step(function() { + assert_equals("test\n\ntest", e.data) + source.close() + }) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-leading-space.any.js b/test/fixtures/wpt/eventsource/format-leading-space.any.js new file mode 100644 index 00000000000000..0ddfd9b32bb2c9 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-leading-space.any.js @@ -0,0 +1,14 @@ +// META: title=EventSource: leading space + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?message=data%3A%09test%0Ddata%3A%20%0Adata%3Atest") + source.onmessage = function(e) { + test.step(function() { + assert_equals("\ttest\n\ntest", e.data) + source.close() + }) + test.done() + } + }) + // also used a CR as newline once + diff --git a/test/fixtures/wpt/eventsource/format-mime-bogus.any.js b/test/fixtures/wpt/eventsource/format-mime-bogus.any.js new file mode 100644 index 00000000000000..18c7c7d4a49e24 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-mime-bogus.any.js @@ -0,0 +1,25 @@ +// META: title=EventSource: bogus MIME type + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?mime=x%20bogus") + source.onmessage = function() { + test.step(function() { + assert_unreached() + source.close() + }) + test.done() + } + source.onerror = function(e) { + test.step(function() { + assert_equals(this.readyState, this.CLOSED) + assert_false(e.hasOwnProperty('data')) + assert_false(e.bubbles) + assert_false(e.cancelable) + this.close() + }, this) + test.done() + } + }) + // This tests "fails the connection" as well as making sure a simple + // event is dispatched and not a MessageEvent + diff --git a/test/fixtures/wpt/eventsource/format-mime-trailing-semicolon.any.js b/test/fixtures/wpt/eventsource/format-mime-trailing-semicolon.any.js new file mode 100644 index 00000000000000..55a314bf524c10 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-mime-trailing-semicolon.any.js @@ -0,0 +1,20 @@ +// META: title=EventSource: MIME type with trailing ; + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?mime=text/event-stream%3B") + source.onopen = function() { + test.step(function() { + assert_equals(source.readyState, source.OPEN) + source.close() + }) + test.done() + } + source.onerror = function() { + test.step(function() { + assert_unreached() + source.close() + }) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-mime-valid-bogus.any.js b/test/fixtures/wpt/eventsource/format-mime-valid-bogus.any.js new file mode 100644 index 00000000000000..355ba6c524fd35 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-mime-valid-bogus.any.js @@ -0,0 +1,24 @@ +// META: title=EventSource: incorrect valid MIME type + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?mime=text/x-bogus") + source.onmessage = function() { + test.step(function() { + assert_unreached() + source.close() + }) + test.done() + } + source.onerror = function(e) { + test.step(function() { + assert_equals(source.readyState, source.CLOSED) + assert_false(e.hasOwnProperty('data')) + assert_false(e.bubbles) + assert_false(e.cancelable) + }) + test.done() + } + }) + // This tests "fails the connection" as well as making sure a simple + // event is dispatched and not a MessageEvent + diff --git a/test/fixtures/wpt/eventsource/format-newlines.any.js b/test/fixtures/wpt/eventsource/format-newlines.any.js new file mode 100644 index 00000000000000..0768171d33357e --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-newlines.any.js @@ -0,0 +1,13 @@ +// META: title=EventSource: newline fest + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?message=data%3Atest%0D%0Adata%0Adata%3Atest%0D%0A%0D&newline=none") + source.onmessage = function(e) { + test.step(function() { + assert_equals("test\n\ntest", e.data) + source.close() + }) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-null-character.any.js b/test/fixtures/wpt/eventsource/format-null-character.any.js new file mode 100644 index 00000000000000..943628d2c02453 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-null-character.any.js @@ -0,0 +1,17 @@ +// META: title=EventSource: null character in response + var test = async_test() + test.step(function() { + var source = new EventSource("resources/message.py?message=data%3A%00%0A%0A") + source.onmessage = function(e) { + test.step(function() { + assert_equals("\x00", e.data) + source.close() + }, this) + test.done() + } + source.onerror = function() { + test.step(function() { assert_unreached() }) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/format-utf-8.any.js b/test/fixtures/wpt/eventsource/format-utf-8.any.js new file mode 100644 index 00000000000000..7976abfb55df19 --- /dev/null +++ b/test/fixtures/wpt/eventsource/format-utf-8.any.js @@ -0,0 +1,12 @@ +// META: title=EventSource always UTF-8 +async_test().step(function() { + var source = new EventSource("resources/message.py?mime=text/event-stream%3bcharset=windows-1252&message=data%3Aok%E2%80%A6") + source.onmessage = this.step_func(function(e) { + assert_equals('ok…', e.data, 'decoded data') + source.close() + this.done() + }) + source.onerror = this.step_func(function() { + assert_unreached("Got error event") + }) +}) diff --git a/test/fixtures/wpt/eventsource/request-accept.any.js b/test/fixtures/wpt/eventsource/request-accept.any.js new file mode 100644 index 00000000000000..2e181735564338 --- /dev/null +++ b/test/fixtures/wpt/eventsource/request-accept.any.js @@ -0,0 +1,13 @@ +// META: title=EventSource: Accept header + var test = async_test() + test.step(function() { + var source = new EventSource("resources/accept.event_stream?pipe=sub") + source.onmessage = function(e) { + test.step(function() { + assert_equals(e.data, "text/event-stream") + this.close() + }, this) + test.done() + } + }) + diff --git a/test/fixtures/wpt/eventsource/request-cache-control.any.js b/test/fixtures/wpt/eventsource/request-cache-control.any.js new file mode 100644 index 00000000000000..95b71d7a589563 --- /dev/null +++ b/test/fixtures/wpt/eventsource/request-cache-control.any.js @@ -0,0 +1,35 @@ +// META: title=EventSource: Cache-Control + var crossdomain = location.href + .replace('://', '://www2.') + .replace(/\/[^\/]*$/, '/') + + // running it twice to check whether it stays consistent + function cacheTest(url) { + var test = async_test(url + "1") + // Recursive test. This avoids test that timeout + var test2 = async_test(url + "2") + test.step(function() { + var source = new EventSource(url) + source.onmessage = function(e) { + test.step(function() { + assert_equals(e.data, "no-cache") + this.close() + test2.step(function() { + var source2 = new EventSource(url) + source2.onmessage = function(e) { + test2.step(function() { + assert_equals(e.data, "no-cache") + this.close() + }, this) + test2.done() + } + }) + }, this) + test.done() + } + }) + } + + cacheTest("resources/cache-control.event_stream?pipe=sub") + cacheTest(crossdomain + "resources/cors.py?run=cache-control") + diff --git a/test/fixtures/wpt/eventsource/request-credentials.any.window.js b/test/fixtures/wpt/eventsource/request-credentials.any.window.js new file mode 100644 index 00000000000000..d7c554aa4a2c48 --- /dev/null +++ b/test/fixtures/wpt/eventsource/request-credentials.any.window.js @@ -0,0 +1,37 @@ +// META: title=EventSource: credentials + var crossdomain = location.href + .replace('://', '://www2.') + .replace(/\/[^\/]*$/, '/') + + function testCookie(desc, success, props, id) { + var test = async_test(document.title + ': credentials ' + desc) + test.step(function() { + var source = new EventSource(crossdomain + "resources/cors-cookie.py?ident=" + id, props) + + source.onmessage = test.step_func(function(e) { + if(e.data.indexOf("first") == 0) { + assert_equals(e.data, "first NO_COOKIE", "cookie status") + } + else if(e.data.indexOf("second") == 0) { + if (success) + assert_equals(e.data, "second COOKIE", "cookie status") + else + assert_equals(e.data, "second NO_COOKIE", "cookie status") + + source.close() + test.done() + } + else { + assert_unreached("unrecognized data returned: " + e.data) + source.close() + test.done() + } + }) + }) + } + + testCookie('enabled', true, { withCredentials: true }, '1_' + new Date().getTime()) + testCookie('disabled', false, { withCredentials: false }, '2_' + new Date().getTime()) + testCookie('default', false, { }, '3_' + new Date().getTime()) + + diff --git a/test/fixtures/wpt/eventsource/request-redirect.any.window.js b/test/fixtures/wpt/eventsource/request-redirect.any.window.js new file mode 100644 index 00000000000000..3788dd8450256c --- /dev/null +++ b/test/fixtures/wpt/eventsource/request-redirect.any.window.js @@ -0,0 +1,24 @@ +// META: title=EventSource: redirect + function redirectTest(status) { + var test = async_test(document.title + " (" + status +")") + test.step(function() { + var source = new EventSource("/common/redirect.py?location=/eventsource/resources/message.py&status=" + status) + source.onopen = function() { + test.step(function() { + assert_equals(this.readyState, this.OPEN) + this.close() + }, this) + test.done() + } + source.onerror = function() { + test.step(function() { assert_unreached() }) + test.done() + } + }) + } + + redirectTest("301") + redirectTest("302") + redirectTest("303") + redirectTest("307") + diff --git a/test/fixtures/wpt/eventsource/request-status-error.window.js b/test/fixtures/wpt/eventsource/request-status-error.window.js new file mode 100644 index 00000000000000..8632d8e8c6b5ac --- /dev/null +++ b/test/fixtures/wpt/eventsource/request-status-error.window.js @@ -0,0 +1,27 @@ +// META: title=EventSource: incorrect HTTP status code + function statusTest(status) { + var test = async_test(document.title + " (" + status +")") + test.step(function() { + var source = new EventSource("resources/status-error.py?status=" + status) + source.onmessage = function() { + test.step(function() { + assert_unreached() + }) + test.done() + } + source.onerror = function() { + test.step(function() { + assert_equals(this.readyState, this.CLOSED) + }, this) + test.done() + } + }) + } + statusTest("204") + statusTest("205") + statusTest("210") + statusTest("299") + statusTest("404") + statusTest("410") + statusTest("503") + diff --git a/test/fixtures/wpt/eventsource/resources/accept.event_stream b/test/fixtures/wpt/eventsource/resources/accept.event_stream new file mode 100644 index 00000000000000..24da54826784e6 --- /dev/null +++ b/test/fixtures/wpt/eventsource/resources/accept.event_stream @@ -0,0 +1,2 @@ +data: {{headers[accept]}} + diff --git a/test/fixtures/wpt/eventsource/resources/cache-control.event_stream b/test/fixtures/wpt/eventsource/resources/cache-control.event_stream new file mode 100644 index 00000000000000..aa9f2d6c090bcc --- /dev/null +++ b/test/fixtures/wpt/eventsource/resources/cache-control.event_stream @@ -0,0 +1,2 @@ +data: {{headers[cache-control]}} + diff --git a/test/fixtures/wpt/eventsource/resources/eventsource-onmessage-realm.htm b/test/fixtures/wpt/eventsource/resources/eventsource-onmessage-realm.htm new file mode 100644 index 00000000000000..63e6d012b4d82b --- /dev/null +++ b/test/fixtures/wpt/eventsource/resources/eventsource-onmessage-realm.htm @@ -0,0 +1,2 @@ + +This page is just used to grab an EventSource constructor diff --git a/test/fixtures/wpt/eventsource/resources/init.htm b/test/fixtures/wpt/eventsource/resources/init.htm new file mode 100644 index 00000000000000..7c56d88800da94 --- /dev/null +++ b/test/fixtures/wpt/eventsource/resources/init.htm @@ -0,0 +1,9 @@ + + + + support init file + + + + + diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.htm new file mode 100644 index 00000000000000..30fbc309ab67c2 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.htm @@ -0,0 +1,24 @@ + + + + shared worker - EventSource: close() + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.js new file mode 100644 index 00000000000000..8d160b605f2b17 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-close.js @@ -0,0 +1,12 @@ +onconnect = function(e) { +try { + var port = e.ports[0] + var source = new EventSource("../resources/message.py") + source.onopen = function(e) { + this.close() + port.postMessage([true, this.readyState]) + } +} catch(e) { + port.postMessage([false, String(e)]) +} +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.htm new file mode 100644 index 00000000000000..690cde36002456 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.htm @@ -0,0 +1,34 @@ + + + + shared worker - EventSource: constructor (act as if there is a network error) + + + + + +
+ + + + diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.js new file mode 100644 index 00000000000000..a68dc5b0b7dac6 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-non-same-origin.js @@ -0,0 +1,13 @@ +onconnect = function(e) { +try { + var port = e.ports[0] + var url = decodeURIComponent(location.hash.substr(1)) + var source = new EventSource(url) + source.onerror = function(e) { + port.postMessage([true, this.readyState, 'data' in e]) + this.close(); + } +} catch(e) { + port.postMessage([false, String(e)]) +} +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-url-bogus.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-url-bogus.js new file mode 100644 index 00000000000000..80847357b55197 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-constructor-url-bogus.js @@ -0,0 +1,10 @@ +onconnect = function(e) { +try { + var port = e.ports[0] + var source = new EventSource("http://this is invalid/") + port.postMessage([false, 'no exception thrown']) + source.close() +} catch(e) { + port.postMessage([true, e.code]) +} +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.htm new file mode 100644 index 00000000000000..f25509dfd4a858 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.htm @@ -0,0 +1,24 @@ + + + + shared worker - EventSource: addEventListener() + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.js new file mode 100644 index 00000000000000..761165118ac788 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-eventtarget.js @@ -0,0 +1,13 @@ +onconnect = function(e) { +try { + var port = e.ports[0] + var source = new EventSource("../resources/message.py") + source.addEventListener("message", listener, false) + function listener(e) { + port.postMessage([true, e.data]) + this.close() + } +} catch(e) { + port.postMessage([false, String(e)]) +} +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmesage.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmesage.js new file mode 100644 index 00000000000000..f5e2c898df0cda --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmesage.js @@ -0,0 +1,12 @@ +onconnect = function(e) { +try { + var port = e.ports[0] + var source = new EventSource("../resources/message.py") + source.onmessage = function(e) { + port.postMessage([true, e.data]) + this.close() + } +} catch(e) { + port.postMessage([false, String(e)]) +} +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmessage.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmessage.htm new file mode 100644 index 00000000000000..bcd6093454d2a0 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onmessage.htm @@ -0,0 +1,24 @@ + + + + shared worker - EventSource: onmessage + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.htm new file mode 100644 index 00000000000000..752a6e449f958e --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.htm @@ -0,0 +1,27 @@ + + + + shared worker - EventSource: onopen (announcing the connection) + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.js new file mode 100644 index 00000000000000..6dc9424a213d79 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-onopen.js @@ -0,0 +1,12 @@ +onconnect = function(e) { +try { + var port = e.ports[0] + var source = new EventSource("../resources/message.py") + source.onopen = function(e) { + port.postMessage([true, source.readyState, 'data' in e, e.bubbles, e.cancelable]) + this.close() + } +} catch(e) { + port.postMessage([false, String(e)]) +} +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.htm new file mode 100644 index 00000000000000..16c932a3384eb6 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.htm @@ -0,0 +1,25 @@ + + + + shared worker - EventSource: prototype et al + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.js new file mode 100644 index 00000000000000..f4c809a9b3e332 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-prototype.js @@ -0,0 +1,11 @@ +onconnect = function(e) { +try { + var port = e.ports[0] + EventSource.prototype.ReturnTrue = function() { return true } + var source = new EventSource("../resources/message.py") + port.postMessage([true, source.ReturnTrue(), 'EventSource' in self]) + source.close() +} catch(e) { + port.postMessage([false, String(e)]) +} +} \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.htm b/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.htm new file mode 100644 index 00000000000000..a1c9ca8455fb8a --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.htm @@ -0,0 +1,25 @@ + + + + shared worker - EventSource: url + + + + +
+ + + \ No newline at end of file diff --git a/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.js b/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.js new file mode 100644 index 00000000000000..491dbac33320d9 --- /dev/null +++ b/test/fixtures/wpt/eventsource/shared-worker/eventsource-url.js @@ -0,0 +1,10 @@ +onconnect = function(e) { +try { + var port = e.ports[0] + var source = new EventSource("../resources/message.py") + port.postMessage([true, source.url]) + source.close() +} catch(e) { + port.postMessage([false, String(e)]) +} +} \ No newline at end of file diff --git a/test/fixtures/wpt/versions.json b/test/fixtures/wpt/versions.json index 0e04684b381f2f..bc10e99e770eef 100644 --- a/test/fixtures/wpt/versions.json +++ b/test/fixtures/wpt/versions.json @@ -23,6 +23,10 @@ "commit": "a58bbf6d8c0db1f1fd5352e846acb0754ee55567", "path": "encoding" }, + "eventsource": { + "commit": "9dafa892146c4b5b1f604a39b3cf8677f8f70d44", + "path": "eventsource" + }, "fetch/data-urls/resources": { "commit": "7c79d998ff42e52de90290cb847d1b515b3b58f7", "path": "fetch/data-urls/resources" diff --git a/test/parallel/test-eventsource-connect.mjs b/test/parallel/test-eventsource-connect.mjs new file mode 100644 index 00000000000000..33e636942ffd05 --- /dev/null +++ b/test/parallel/test-eventsource-connect.mjs @@ -0,0 +1,26 @@ +import { + mustCall, + mustNotCall, +} from '../common/index.mjs'; + +import assert from 'assert'; +import events from 'events'; +import http from 'http'; + +{ // Should error if the Content-Type is not text/event-stream + const server = http.createServer((req, res) => { + res.writeHead(200, undefined, { 'Content-Type': 'text/plain' }); + res.end(); + }); + + server.listen(0); + await events.once(server, 'listening'); + const port = server.address().port; + + const eventSourceInstance = new EventSource(`http://localhost:${port}`); + eventSourceInstance.onopen = mustNotCall(); + eventSourceInstance.onerror = mustCall(function() { + assert.strictEqual(this.readyState, EventSource.CLOSED); + server.close(); + }); +} diff --git a/test/parallel/test-eventsource-constructor.mjs b/test/parallel/test-eventsource-constructor.mjs new file mode 100644 index 00000000000000..f138610913eb21 --- /dev/null +++ b/test/parallel/test-eventsource-constructor.mjs @@ -0,0 +1,15 @@ +import '../common/index.mjs'; +import assert from 'assert'; + +{ + // Not providing url argument should throw + assert.throws(() => new EventSource(), TypeError); +} + +{ + // Stringify url argument + // eventsource-constructor-stringify.window.js + // assert.strictEqual(new EventSource(1).url, '1'); + // assert.strictEqual(new EventSource(undefined).url, 'undefined'); + // assert.strictEqual(new EventSource(null).url, 'null'); +} diff --git a/test/parallel/test-eventsource-error.mjs b/test/parallel/test-eventsource-error.mjs new file mode 100644 index 00000000000000..614890c9f6ad4e --- /dev/null +++ b/test/parallel/test-eventsource-error.mjs @@ -0,0 +1,28 @@ +import { + mustCall, + mustNotCall, +} from '../common/index.mjs'; + +import assert from 'assert'; +import events from 'events'; +import http from 'http'; + +// request-status-error.window.js +// EventSource: incorrect HTTP status code +[204, 205, 210, 299, 404, 410, 503].forEach(async (statusCode) => { + const server = http.createServer((req, res) => { + res.writeHead(statusCode, undefined); + res.end(); + }); + + server.listen(0); + await events.once(server, 'listening'); + const port = server.address().port; + + const eventSourceInstance = new EventSource(`http://localhost:${port}`); + eventSourceInstance.onopen = mustNotCall(); + eventSourceInstance.onerror = mustCall(function() { + assert.strictEqual(this.readyState, EventSource.CLOSED); + server.close(); + }); +}); diff --git a/test/parallel/test-eventsource-eventhandler-idl.mjs b/test/parallel/test-eventsource-eventhandler-idl.mjs new file mode 100644 index 00000000000000..6b93d2d7d536dd --- /dev/null +++ b/test/parallel/test-eventsource-eventhandler-idl.mjs @@ -0,0 +1,40 @@ +import '../common/index.mjs'; + +import assert from 'assert'; +import events from 'events'; +import http from 'http'; + +const server = http.createServer((req, res) => { + res.writeHead(200, 'dummy'); +}); + +server.listen(0); +await events.once(server, 'listening'); +const port = server.address().port; + +let done = 0; +const eventhandlerIdl = ['onmessage', 'onerror', 'onopen']; + +eventhandlerIdl.forEach((type) => { + const eventSourceInstance = new EventSource(`http://localhost:${port}`); + + // Eventsource eventhandler idl is by default undefined, + assert.strictEqual(eventSourceInstance[type], undefined); + + // The eventhandler idl is by default not enumerable. + assert.strictEqual(Object.prototype.propertyIsEnumerable.call(eventSourceInstance, type), false); + + // The eventhandler idl ignores non-functions. + eventSourceInstance[type] = 7; + assert.strictEqual(EventSource[type], undefined); + + // The eventhandler idl accepts functions. + function fn() {} + eventSourceInstance[type] = fn; + assert.strictEqual(eventSourceInstance[type], fn); + + eventSourceInstance.close(); + done++; + + if (done === eventhandlerIdl.length) server.close(); +}); diff --git a/test/parallel/test-eventsource-eventsourcestream-parse-line.mjs b/test/parallel/test-eventsource-eventsourcestream-parse-line.mjs new file mode 100644 index 00000000000000..d801b7a6904ee4 --- /dev/null +++ b/test/parallel/test-eventsource-eventsourcestream-parse-line.mjs @@ -0,0 +1,92 @@ +// Flags: --expose-internals +import '../common/index.mjs'; + +import assert from 'assert'; +import eventSource from 'internal/event_source'; + +const EventSourceStream = eventSource.EventSourceStream; + +const defaultEventSourceState = { + origin: 'example.com', + reconnectionTime: 1000, +}; + +{ + // Should set the data field + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState + } + }); + + const event = {}; + + stream.parseLine(Buffer.from('data: Hello', 'utf8'), event); + + assert.strictEqual(typeof event, 'object'); + assert.strictEqual(Object.keys(event).length, 1); + assert.strictEqual(event.data, 'Hello'); + assert.strictEqual(event.id, undefined); + assert.strictEqual(event.event, undefined); + assert.strictEqual(event.retry, undefined); +} + +{ + // Should set retry field + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState + } + }); + + const event = {}; + + stream.parseLine(Buffer.from('retry: 1000', 'utf8'), event); + + assert.strictEqual(typeof event, 'object'); + assert.strictEqual(Object.keys(event).length, 1); + assert.strictEqual(event.data, undefined); + assert.strictEqual(event.id, undefined); + assert.strictEqual(event.event, undefined); + assert.strictEqual(event.retry, '1000'); +} + +{ + // Should set id field + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState + } + }); + + const event = {}; + + stream.parseLine(Buffer.from('id: 1234', 'utf8'), event); + + assert.strictEqual(typeof event, 'object'); + assert.strictEqual(Object.keys(event).length, 1); + assert.strictEqual(event.data, undefined); + assert.strictEqual(event.id, '1234'); + assert.strictEqual(event.event, undefined); + assert.strictEqual(event.retry, undefined); +} + +{ + // Should set id field + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState + } + }); + + const event = {}; + + stream.parseLine(Buffer.from('event: custom', 'utf8'), event); + + assert.strictEqual(typeof event, 'object'); + assert.strictEqual(Object.keys(event).length, 1); + assert.strictEqual(event.data, undefined); + assert.strictEqual(event.id, undefined); + assert.strictEqual(event.event, 'custom'); + assert.strictEqual(event.retry, undefined); +} diff --git a/test/parallel/test-eventsource-eventsourcestream-process-event.mjs b/test/parallel/test-eventsource-eventsourcestream-process-event.mjs new file mode 100644 index 00000000000000..b4404e7e0b5c14 --- /dev/null +++ b/test/parallel/test-eventsource-eventsourcestream-process-event.mjs @@ -0,0 +1,138 @@ +// Flags: --expose-internals +import { + mustCall, +} from '../common/index.mjs'; + +import assert from 'assert'; +import eventSource from 'internal/event_source'; + +const MessageEvent = eventSource.MessageEvent; +const EventSourceStream = eventSource.EventSourceStream; + +const defaultEventSourceState = { + origin: 'example.com', + reconnectionTime: 1000, +}; + +{ + // Should set the defined origin as the origin of the MessageEvent + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState + } + }); + + stream.on('data', mustCall((event) => { + assert.strictEqual(event instanceof MessageEvent, true); + assert.strictEqual(event.data, null); + assert.strictEqual(event.lastEventId, ''); + assert.strictEqual(event.type, 'message'); + assert.strictEqual(stream.state.reconnectionTime, 1000); + assert.strictEqual(event.origin, 'example.com'); + })); + + stream.processEvent({}); +} + +{ + // Should set reconnectionTime to 4000 if event contains retry field + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState + } + }); + + stream.processEvent({ + retry: '4000', + }); + + assert.strictEqual(stream.state.reconnectionTime, 4000); +} + +{ + // Dispatches a MessageEvent with data + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState + } + }); + + stream.on('data', mustCall((event) => { + assert.strictEqual(event instanceof MessageEvent, true); + assert.strictEqual(event.data, 'Hello'); + assert.strictEqual(event.lastEventId, ''); + assert.strictEqual(event.type, 'message'); + assert.strictEqual(event.origin, 'example.com'); + assert.strictEqual(stream.state.reconnectionTime, 1000); + })); + + stream.processEvent({ + data: 'Hello', + }); +} + +{ + // Dispatches a MessageEvent with lastEventId, when event contains id field + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState, + } + }); + + stream.on('data', mustCall((event) => { + assert.strictEqual(event instanceof MessageEvent, true); + assert.strictEqual(event.data, null); + assert.strictEqual(event.lastEventId, '1234'); + assert.strictEqual(event.type, 'message'); + assert.strictEqual(event.origin, 'example.com'); + assert.strictEqual(stream.state.reconnectionTime, 1000); + })); + + stream.processEvent({ + id: '1234', + }); +} + +{ + // Dispatches a MessageEvent with lastEventId, reusing the persisted + // lastEventId + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState, + lastEventId: '1234', + } + }); + + stream.on('data', mustCall((event) => { + assert.strictEqual(event instanceof MessageEvent, true); + assert.strictEqual(event.data, null); + assert.strictEqual(event.lastEventId, '1234'); + assert.strictEqual(event.type, 'message'); + assert.strictEqual(event.origin, 'example.com'); + assert.strictEqual(stream.state.reconnectionTime, 1000); + })); + + stream.processEvent({}); +} + +{ + // Dispatches a MessageEvent with type custom, when event contains type field + const stream = new EventSourceStream({ + eventSourceState: { + ...defaultEventSourceState, + } + }); + + stream.on('data', mustCall((event) => { + assert.strictEqual(event instanceof MessageEvent, true); + assert.strictEqual(event.data, null); + assert.strictEqual(event.lastEventId, ''); + assert.strictEqual(event.type, 'custom'); + assert.strictEqual(event.origin, 'example.com'); + assert.strictEqual(stream.state.reconnectionTime, 1000); + })); + + stream.processEvent({ + event: 'custom', + }); +} diff --git a/test/parallel/test-eventsource-eventsourcestream.mjs b/test/parallel/test-eventsource-eventsourcestream.mjs new file mode 100644 index 00000000000000..3ef57b9a376726 --- /dev/null +++ b/test/parallel/test-eventsource-eventsourcestream.mjs @@ -0,0 +1,101 @@ +// Flags: --expose-internals +import { + mustCall, + mustNotCall, +} from '../common/index.mjs'; + +import assert from 'assert'; +import eventSource from 'internal/event_source'; + +const EventSourceStream = eventSource.EventSourceStream; + +{ + // Remove BOM from the beginning of the stream. + const content = Buffer.from('\uFEFFdata: Hello\n\n', 'utf8'); + + const stream = new EventSourceStream(); + + stream.processEvent = mustCall(function(event) { + assert.strictEqual(typeof event, 'object'); + assert.strictEqual(event.event, undefined); + assert.strictEqual(event.data, 'Hello'); + assert.strictEqual(event.id, undefined); + assert.strictEqual(event.retry, undefined); + }); + + for (let i = 0; i < content.length; i++) { + stream.write(Buffer.from([content[i]])); + } +} + +{ + // Simple event with data field. + const content = Buffer.from('data: Hello\n\n', 'utf8'); + + const stream = new EventSourceStream(); + + stream.processEvent = mustCall(function(event) { + assert.strictEqual(typeof event, 'object'); + assert.strictEqual(event.event, undefined); + assert.strictEqual(event.data, 'Hello'); + assert.strictEqual(event.id, undefined); + assert.strictEqual(event.retry, undefined); + }); + + for (let i = 0; i < content.length; i++) { + stream.write(Buffer.from([content[i]])); + } +} + +{ + // Ignore comments + const content = Buffer.from(':data: Hello\n\n', 'utf8'); + + const stream = new EventSourceStream(); + + stream.processEvent = mustNotCall(function(event) { + assert.fail('Should not be called'); + }); + + for (let i = 0; i < content.length; i++) { + stream.write(Buffer.from([content[i]])); + } +} + +{ + // Should fire two events. + // @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + const content = Buffer.from('data\n\ndata\ndata\n\ndata:', 'utf8'); + const stream = new EventSourceStream(); + + stream.processEvent = mustCall(function(event) { + assert.strictEqual(typeof event, 'object'); + assert.strictEqual(event.event, undefined); + assert.strictEqual(event.data, undefined); + assert.strictEqual(event.id, undefined); + assert.strictEqual(event.retry, undefined); + }, 2); + + for (let i = 0; i < content.length; i++) { + stream.write(Buffer.from([content[i]])); + } +} + +{ + // Should fire two identical events. + // @see https://html.spec.whatwg.org/multipage/server-sent-events.html#event-stream-interpretation + const content = Buffer.from('data:test\n\ndata: test\n\n', 'utf8'); + const stream = new EventSourceStream(); + + stream.processEvent = mustCall(function(event) { + assert.strictEqual(typeof event, 'object'); + assert.strictEqual(event.event, undefined); + assert.strictEqual(event.data, 'test'); + assert.strictEqual(event.id, undefined); + assert.strictEqual(event.retry, undefined); + }, 2); + + for (let i = 0; i < content.length; i++) { + stream.write(Buffer.from([content[i]])); + } +} diff --git a/test/parallel/test-eventsource-instance-constants.mjs b/test/parallel/test-eventsource-instance-constants.mjs new file mode 100644 index 00000000000000..14f74bcd81a20f --- /dev/null +++ b/test/parallel/test-eventsource-instance-constants.mjs @@ -0,0 +1,58 @@ +import '../common/index.mjs'; + +import assert from 'assert'; +import events from 'events'; +import http from 'http'; + +const server = http.createServer((req, res) => { + res.writeHead(200, 'dummy'); +}); + +server.listen(0); +await events.once(server, 'listening'); +const port = server.address().port; + +let done = 0; + +[ + ['CONNECTING', 0], + ['OPEN', 1], + ['CLOSED', 2], +].forEach((config) => { + { + const [constant, value] = config; + + const eventSourceInstance = new EventSource(`http://localhost:${port}`); + + // EventSource instance does not expose the constant as an own property. + assert.strictEqual(Object.hasOwn(eventSourceInstance, constant), false); + + // EventSource instance exposes the constant as an inherited property. + assert.strictEqual(constant in eventSourceInstance, true); + + // The value is properly set. + assert.strictEqual(eventSourceInstance[constant], value); + + // The constant is not enumerable. + assert.strictEqual(Object.prototype.propertyIsEnumerable.call(eventSourceInstance, constant), false); + + // The constant is not writable. + try { + eventSourceInstance[constant] = 666; + } catch (e) { + assert.strictEqual(e instanceof TypeError, true); + } + // The constant is not configurable. + try { + delete eventSourceInstance[constant]; + } catch (e) { + assert.strictEqual(e instanceof TypeError, true); + } + assert.strictEqual(eventSourceInstance[constant], value); + + eventSourceInstance.close(); + done++; + + if (done === 3) server.close(); + } +}); diff --git a/test/parallel/test-eventsource-is-valid-helpers.mjs b/test/parallel/test-eventsource-is-valid-helpers.mjs new file mode 100644 index 00000000000000..270f8eae009b11 --- /dev/null +++ b/test/parallel/test-eventsource-is-valid-helpers.mjs @@ -0,0 +1,24 @@ +// Flags: --expose-internals +import '../common/index.mjs'; +import assert from 'assert'; + +import eventSource from 'internal/event_source'; + +{ + const isValidLastEventId = eventSource.isValidLastEventId; + + assert.strictEqual(isValidLastEventId('valid'), true); + assert.strictEqual(isValidLastEventId('in\u0000valid'), false); + assert.strictEqual(isValidLastEventId('in\x00valid'), false); + + assert.strictEqual(isValidLastEventId(null), false); + assert.strictEqual(isValidLastEventId(undefined), false); + assert.strictEqual(isValidLastEventId(7), false); +} + +{ + const isASCIINumber = eventSource.isASCIINumber; + + assert.strictEqual(isASCIINumber('123'), true); + assert.strictEqual(isASCIINumber('123a'), false); +} diff --git a/test/parallel/test-eventsource-messageevent.mjs b/test/parallel/test-eventsource-messageevent.mjs new file mode 100644 index 00000000000000..3cf2064c7ac09e --- /dev/null +++ b/test/parallel/test-eventsource-messageevent.mjs @@ -0,0 +1,213 @@ +// Flags: --expose-internals +import '../common/index.mjs'; + +import assert from 'assert'; +import eventTarget from 'internal/event_target'; +import eventSource from 'internal/event_source'; + +const MessageEvent = eventSource.MessageEvent; + +{ + // MessageEvent is a subclass of Event + assert.strictEqual(new MessageEvent() instanceof eventTarget.Event, true); +} + +{ + // default values + assert.strictEqual(new MessageEvent().data, null); + assert.strictEqual(new MessageEvent().origin, ''); + assert.strictEqual(new MessageEvent().lastEventId, ''); + assert.strictEqual(new MessageEvent().source, null); + assert.deepStrictEqual(new MessageEvent().ports, []); +} + +{ + // Ports is a frozen array + assert.throws(() => { + new MessageEvent('message', { ports: [] }).ports.push(null); + }, TypeError); +} + +{ + // The data attribute must return the value it was initialized to. It + // represents the message being sent. + assert.strictEqual(new MessageEvent('message', { data: null }).data, null); + assert.strictEqual(new MessageEvent('message', { data: 1 }).data, 1); + assert.strictEqual(new MessageEvent('message', { data: 'foo' }).data, 'foo'); + assert.deepStrictEqual(new MessageEvent('message', { data: {} }).data, {}); + assert.deepStrictEqual(new MessageEvent('message', { data: [] }).data, []); + assert.strictEqual(new MessageEvent('message', { data: true }).data, true); + assert.strictEqual(new MessageEvent('message', { data: false }).data, false); + + // The data attribute is a read-only attribute. + assert.throws(() => { + new MessageEvent('message', { data: null }).data = 1; + }, TypeError); + + // The data attribute is non-writable + const event = new MessageEvent('message', { data: 'dataValue' }); + delete event.data; + assert.strictEqual(event.data, 'dataValue'); +} + +{ + // The origin attribute must return the value it was initialized to. It + // represents the origin of the message being sent. + + assert.strictEqual(new MessageEvent('message', { origin: '' }).origin, ''); + assert.strictEqual(new MessageEvent('message', { origin: 'foo' }).origin, 'foo'); + + // The origin attribute is a read-only attribute. + assert.throws(() => { + new MessageEvent('message', { origin: '' }).origin = 'foo'; + }, TypeError); + + // The origin attribute is non-writable + const event = new MessageEvent('message', { origin: 'originValue' }); + delete event.origin; + assert.strictEqual(event.origin, 'originValue'); +} + +{ + // The source attribute must return the value it was initialized to. It + // represents the source of the message being sent. + + assert.strictEqual(new MessageEvent('message', { source: null }).source, null); + assert.strictEqual(new MessageEvent('message', { source: 'foo' }).source, 'foo'); + + // The source attribute is a read-only attribute. + assert.throws(() => { + new MessageEvent('message', { source: '' }).source = 'foo'; + }, TypeError); + + // The source attribute is non-writable + const event = new MessageEvent('message', { source: 'sourceValue' }); + delete event.source; + assert.strictEqual(event.source, 'sourceValue'); +} + +{ + // The ports attribute must return the value it was initialized to. It + // represents the ports of the message being sent. + + assert.deepStrictEqual(new MessageEvent('message', { ports: [] }).ports, []); + const target = new EventTarget(); + assert.deepStrictEqual(new MessageEvent('message', { ports: [target] }).ports, [target]); + + // The ports attribute is a read-only attribute. + assert.throws(() => { + new MessageEvent('message', { ports: [] }).ports = []; + }, TypeError); + + // The ports attribute is non-writable + const event = new MessageEvent('message', { ports: [target] }); + delete event.ports; + assert.deepStrictEqual(event.ports, [target]); +} + +{ + // The lastEventId attribute must return the value it was initialized to. It + // represents the last event ID string of the message being sent. + + assert.strictEqual(new MessageEvent('message', { lastEventId: '' }).lastEventId, ''); + assert.strictEqual(new MessageEvent('message', { lastEventId: 'foo' }).lastEventId, 'foo'); + + // The lastEventId attribute is a read-only attribute. + assert.throws(() => { + new MessageEvent('message', { lastEventId: '' }).lastEventId = 'foo'; + }, TypeError); + + // The lastEventId attribute is non-writable + const event = new MessageEvent('message', { lastEventId: 'lastIdValue' }); + delete event.lastEventId; + assert.strictEqual(event.lastEventId, 'lastIdValue'); +} + +{ + // initMessageEvent initializes the event in a manner analogous to the + // similarly-named method in the DOM Events interfaces. + + const event = new MessageEvent(); + const eventTarget = new EventTarget(); + + event.initMessageEvent('message'); + assert.strictEqual(event.type, 'message'); + assert.strictEqual(event.bubbles, false); + assert.strictEqual(event.cancelable, false); + assert.strictEqual(event.data, null); + assert.strictEqual(event.origin, ''); + assert.strictEqual(event.lastEventId, ''); + assert.strictEqual(event.source, null); + assert.deepStrictEqual(event.ports, []); + + event.initMessageEvent( + 'message', + false, + false, + 'dataValue', + 'originValue', + 'lastIdValue', + 'sourceValue', + [eventTarget] + ); + assert.strictEqual(event.type, 'message'); + assert.strictEqual(event.bubbles, false); + assert.strictEqual(event.cancelable, false); + assert.strictEqual(event.data, 'dataValue'); + assert.strictEqual(event.origin, 'originValue'); + assert.strictEqual(event.lastEventId, 'lastIdValue'); + assert.strictEqual(event.source, 'sourceValue'); + assert.deepStrictEqual(event.ports, [eventTarget]); + + event.initMessageEvent( + 'message', + true, + true, + 'dataValue', + 'originValue', + 'lastIdValue', + 'sourceValue', + [eventTarget] + ); + assert.strictEqual(event.bubbles, true); + assert.strictEqual(event.cancelable, true); + + event.initMessageEvent( + 'message', + true, + false, + 'dataValue', + 'originValue', + 'lastIdValue', + 'sourceValue', + [eventTarget] + ); + assert.strictEqual(event.bubbles, true); + assert.strictEqual(event.cancelable, false); + + event.initMessageEvent( + 'message', + false, + true, + 'dataValue', + 'originValue', + 'lastIdValue', + 'sourceValue', + [eventTarget] + ); + assert.strictEqual(event.bubbles, false); + assert.strictEqual(event.cancelable, true); +} + +{ + // toString returns [object MessageEvent] + const event = new MessageEvent('message', { + data: 'dataValue', + origin: 'originValue', + lastEventId: 'lastIdValue', + source: 'sourceValue', + ports: [] + }); + + assert.strictEqual(event.toString(), '[object MessageEvent]'); +} diff --git a/test/parallel/test-eventsource-redirect.mjs b/test/parallel/test-eventsource-redirect.mjs new file mode 100644 index 00000000000000..e2e9b047e2ce5b --- /dev/null +++ b/test/parallel/test-eventsource-redirect.mjs @@ -0,0 +1,76 @@ +import '../common/index.mjs'; + +import assert from 'assert'; +import events from 'events'; +import http from 'http'; + +[301, 302, 307, 308].forEach(async (statusCode) => { + const server = http.createServer((req, res) => { + if (res.req.url === '/redirect') { + res.writeHead(statusCode, undefined, { Location: '/target' }); + res.end(); + } else if (res.req.url === '/target') { + res.writeHead(200, 'dummy', { 'Content-Type': 'text/event-stream' }); + res.end(); + } + }); + + server.listen(0); + await events.once(server, 'listening'); + const port = server.address().port; + + const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`); + eventSourceInstance.onopen = () => { + assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/target`); + eventSourceInstance.close(); + server.close(); + }; +}); + +{ + // Stop trying to connect when getting a 204 response + const server = http.createServer((req, res) => { + if (res.req.url === '/redirect') { + res.writeHead(301, undefined, { Location: '/target' }); + res.end(); + } else if (res.req.url === '/target') { + res.writeHead(204, 'OK'); + res.end(); + } + }); + + server.listen(0); + await events.once(server, 'listening'); + const port = server.address().port; + + const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`); + eventSourceInstance.onerror = () => { + assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/target`); + assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED); + server.close(); + }; +} + +{ + // Throw an error when missing a Location header + const server = http.createServer((req, res) => { + if (res.req.url === '/redirect') { + res.writeHead(301, undefined); + res.end(); + } else if (res.req.url === '/target') { + res.writeHead(204, 'OK'); + res.end(); + } + }); + + server.listen(0); + await events.once(server, 'listening'); + const port = server.address().port; + + const eventSourceInstance = new EventSource(`http://localhost:${port}/redirect`); + eventSourceInstance.onerror = () => { + assert.strictEqual(eventSourceInstance.url, `http://localhost:${port}/redirect`); + assert.strictEqual(eventSourceInstance.readyState, EventSource.CLOSED); + server.close(); + }; +} diff --git a/test/parallel/test-eventsource-static-constants.mjs b/test/parallel/test-eventsource-static-constants.mjs new file mode 100644 index 00000000000000..9ff4410ced1954 --- /dev/null +++ b/test/parallel/test-eventsource-static-constants.mjs @@ -0,0 +1,36 @@ +import '../common/index.mjs'; + +import assert from 'assert'; + +[ + ['CONNECTING', 0], + ['OPEN', 1], + ['CLOSED', 2], +].forEach((config) => { + { + const [constant, value] = config; + + // EventSource exposes the constant. + assert.strictEqual(Object.hasOwn(EventSource, constant), true); + + // The value is properly set. + assert.strictEqual(EventSource[constant], value); + + // The constant is enumerable. + assert.strictEqual(Object.prototype.propertyIsEnumerable.call(EventSource, constant), true); + + // The constant is not writable. + try { + EventSource[constant] = 666; + } catch (e) { + assert.strictEqual(e instanceof TypeError, true); + } + // The constant is not configurable. + try { + delete EventSource[constant]; + } catch (e) { + assert.strictEqual(e instanceof TypeError, true); + } + assert.strictEqual(EventSource[constant], value); + } +}); diff --git a/test/parallel/test-eventsource.mjs b/test/parallel/test-eventsource.mjs new file mode 100644 index 00000000000000..f934ef2d54f3d2 --- /dev/null +++ b/test/parallel/test-eventsource.mjs @@ -0,0 +1,5 @@ +import '../common/index.mjs'; + +import assert from 'assert'; + +assert.strictEqual(typeof EventSource, 'function'); diff --git a/test/wpt/status/eventsource.json b/test/wpt/status/eventsource.json new file mode 100644 index 00000000000000..fe1346d88d21c1 --- /dev/null +++ b/test/wpt/status/eventsource.json @@ -0,0 +1,137 @@ +{ + "dedicated-worker/eventsource-eventtarget.worker.js": { + "skip": "importScripts not supported" + }, + "eventsource-constructor-stringify.window.js": { + "skip": "void" + }, + "eventsource-constructor-document-domain.window.js": { + "skip": "void" + }, + "eventsource-cross-origin.window.js": { + "skip": "void" + }, + "eventsource-constructor-non-same-origin.window.js": { + "skip": "void" + }, + "eventsource-reconnect.window.js": { + "skip": "void" + }, + "eventsource-close.window.js": { + "skip": "void" + }, + "event-data.any.js": { + "skip": "void" + }, + "eventsource-constructor-empty-url.any.js": { + "skip": "void" + }, + "eventsource-constructor-url-bogus.any.js": { + "skip": "void" + }, + "eventsource-eventtarget.any.js": { + "skip": "void" + }, + "eventsource-onmessage-trusted.any.js": { + "skip": "void" + }, + "eventsource-onmessage.any.js": { + "skip": "void" + }, + "eventsource-onopen.any.js": { + "skip": "void" + }, + "eventsource-prototype.any.js": { + "skip": "void" + }, + "eventsource-request-cancellation.any.window.js": { + "skip": "void" + }, + "eventsource-url.any.js": { + "skip": "void" + }, + "format-bom-2.any.js": { + "skip": "void" + }, + "format-bom.any.js": { + "skip": "void" + }, + "format-comments.any.js": { + "skip": "void" + }, + "format-data-before-final-empty-line.any.js": { + "skip": "void" + }, + "format-field-event-empty.any.js": { + "skip": "void" + }, + "format-field-event.any.js": { + "skip": "void" + }, + "format-field-id-null.window.js": { + "skip": "void" + }, + "format-field-id-3.window.js": { + "skip": "void" + }, + "format-field-id-2.any.js": { + "skip": "void" + }, + "format-field-data.any.js": { + "skip": "void" + }, + "format-field-parsing.any.js": { + "skip": "void" + }, + "format-field-retry.any.js": { + "skip": "void" + }, + "format-field-retry-bogus.any.js": { + "skip": "void" + }, + "format-field-retry-empty.any.js": { + "skip": "void" + }, + "format-field-id.any.js": { + "skip": "void" + }, + "format-leading-space.any.js": { + "skip": "void" + }, + "format-mime-bogus.any.js": { + "skip": "void" + }, + "format-field-unknown.any.js": { + "skip": "void" + }, + "format-mime-trailing-semicolon.any.js": { + "skip": "void" + }, + "format-newlines.any.js": { + "skip": "void" + }, + "format-mime-valid-bogus.any.js": { + "skip": "void" + }, + "request-accept.any.js": { + "skip": "void" + }, + "format-utf-8.any.js": { + "skip": "void" + }, + "request-cache-control.any.js": { + "skip": "void" + }, + "format-null-character.any.js": { + "skip": "void" + }, + "request-redirect.any.window.js": { + "skip": "void" + }, + "request-credentials.any.window.js": { + "skip": "void" + }, + "request-status-error.window.js": { + "skip": "void" + } +} \ No newline at end of file diff --git a/test/wpt/test-eventsource.js b/test/wpt/test-eventsource.js new file mode 100644 index 00000000000000..46038523d6b2c8 --- /dev/null +++ b/test/wpt/test-eventsource.js @@ -0,0 +1,16 @@ +'use strict'; + +const { WPTRunner } = require('../common/wpt'); + +const runner = new WPTRunner('eventsource'); + +runner.pretendGlobalThisAs('Window'); + +runner.setInitScript(` + const document = { + title: Window.META_TITLE, + domain: 'localhost', + } +`); + +runner.runJsTests();