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();