Skip to content

Commit

Permalink
Convert JSDOM to use callback functions
Browse files Browse the repository at this point in the history
This involves updating to webidl2js 16.2.0, which brings along other changes and improvements.
  • Loading branch information
ExE-Boss authored and domenic committed Mar 7, 2021
1 parent 19df6bc commit ff69a75
Show file tree
Hide file tree
Showing 19 changed files with 180 additions and 65 deletions.
103 changes: 103 additions & 0 deletions lib/jsdom/browser/Window.js
Expand Up @@ -8,6 +8,9 @@ const { installInterfaces } = require("../living/interfaces");
const { define, mixin } = require("../utils");
const Element = require("../living/generated/Element");
const EventTarget = require("../living/generated/EventTarget");
const EventHandlerNonNull = require("../living/generated/EventHandlerNonNull");
const OnBeforeUnloadEventHandlerNonNull = require("../living/generated/OnBeforeUnloadEventHandlerNonNull");
const OnErrorEventHandlerNonNull = require("../living/generated/OnErrorEventHandlerNonNull");
const PageTransitionEvent = require("../living/generated/PageTransitionEvent");
const namedPropertiesWindow = require("../living/named-properties-window");
const postMessage = require("../living/post-message");
Expand All @@ -34,6 +37,59 @@ const jsGlobals = require("./js-globals.json");
const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation;
const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation;

const events = new Set([
// GlobalEventHandlers
"abort", "autocomplete",
"autocompleteerror", "blur",
"cancel", "canplay", "canplaythrough",
"change", "click",
"close", "contextmenu",
"cuechange", "dblclick",
"drag", "dragend",
"dragenter",
"dragleave", "dragover",
"dragstart", "drop",
"durationchange", "emptied",
"ended", "focus",
"input", "invalid",
"keydown", "keypress",
"keyup", "load", "loadeddata",
"loadedmetadata", "loadstart",
"mousedown", "mouseenter",
"mouseleave", "mousemove",
"mouseout", "mouseover",
"mouseup", "wheel",
"pause", "play",
"playing", "progress",
"ratechange", "reset",
"resize", "scroll",
"securitypolicyviolation",
"seeked", "seeking",
"select", "sort", "stalled",
"submit", "suspend",
"timeupdate", "toggle",
"volumechange", "waiting",

// WindowEventHandlers
"afterprint",
"beforeprint",
"hashchange",
"languagechange",
"message",
"messageerror",
"offline",
"online",
"pagehide",
"pageshow",
"popstate",
"rejectionhandled",
"storage",
"unhandledrejection",
"unload"

// "error" and "beforeunload" are added separately
]);

exports.createWindow = function (options) {
return new Window(options);
};
Expand Down Expand Up @@ -100,6 +156,53 @@ function setupWindow(windowInstance, { runScripts }) {
mixin(windowInstance, GlobalEventHandlersImpl.prototype);
windowInstance._initGlobalEvents();

// The getters are already obtained from the above mixins.
// eslint-disable-next-line accessor-pairs
Object.defineProperty(windowInstance, "onbeforeunload", {
set(V) {
if (!idlUtils.isObject(V)) {
V = null;
} else {
V = OnBeforeUnloadEventHandlerNonNull.convert(V, {
context: "Failed to set the 'onbeforeunload' property on 'Window': The provided value"
});
}
this._setEventHandlerFor("beforeunload", V);
}
});

// The getters are already obtained from the above mixins.
// eslint-disable-next-line accessor-pairs
Object.defineProperty(windowInstance, "onerror", {
set(V) {
if (!idlUtils.isObject(V)) {
V = null;
} else {
V = OnErrorEventHandlerNonNull.convert(V, {
context: "Failed to set the 'onerror' property on 'Window': The provided value"
});
}
this._setEventHandlerFor("error", V);
}
});

for (const event of events) {
// The getters are already obtained from the above mixins.
// eslint-disable-next-line accessor-pairs
Object.defineProperty(windowInstance, `on${event}`, {
set(V) {
if (!idlUtils.isObject(V)) {
V = null;
} else {
V = EventHandlerNonNull.convert(V, {
context: `Failed to set the 'on${event}' property on 'Window': The provided value`
});
}
this._setEventHandlerFor(event, V);
}
});
}

windowInstance._globalObject = windowInstance;
}

Expand Down
13 changes: 9 additions & 4 deletions lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js
Expand Up @@ -11,6 +11,7 @@ const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shad
const { isValidCustomElementName, tryUpgradeElement, enqueueCEUpgradeReaction } = require("../helpers/custom-elements");

const idlUtils = require("../generated/utils");
const IDLFunction = require("../generated/Function.js");
const HTMLUnknownElement = require("../generated/HTMLUnknownElement");

const LIFECYCLE_CALLBACKS = [
Expand Down Expand Up @@ -62,8 +63,9 @@ class CustomElementRegistryImpl {
}

// https://html.spec.whatwg.org/#dom-customelementregistry-define
define(name, ctor, options) {
define(name, constructor, options) {
const { _globalObject } = this;
const ctor = constructor.objectReference;

if (!isConstructor(ctor)) {
throw new TypeError("Constructor argument is not a constructor.");
Expand All @@ -81,7 +83,7 @@ class CustomElementRegistryImpl {
]);
}

const ctorAlreadyRegistered = this._customElementDefinitions.some(entry => entry.ctor === ctor);
const ctorAlreadyRegistered = this._customElementDefinitions.some(entry => entry.objectReference === ctor);
if (ctorAlreadyRegistered) {
throw DOMException.create(_globalObject, [
"This constructor has already been registered in the registry.",
Expand Down Expand Up @@ -145,7 +147,9 @@ class CustomElementRegistryImpl {
const callbackValue = prototype[callbackName];

if (callbackValue !== undefined) {
lifecycleCallbacks[callbackName] = webIDLConversions.Function(callbackValue);
lifecycleCallbacks[callbackName] = IDLFunction.convert(callbackValue, {
context: `The lifecycle callback "${callbackName}"`
});
}
}

Expand Down Expand Up @@ -177,7 +181,8 @@ class CustomElementRegistryImpl {
const definition = {
name,
localName,
ctor,
constructor,
objectReference: ctor,
observedAttributes,
lifecycleCallbacks,
disableShadow,
Expand Down
@@ -1,6 +1,6 @@
[Exposed=Window]
interface CustomElementRegistry {
[CEReactions] void define(DOMString name, CustomElementConstructor constructor, optional ElementDefinitionOptions options);
[CEReactions] void define(DOMString name, CustomElementConstructor constructor, optional ElementDefinitionOptions options = {});
any get(DOMString name);
Promise<void> whenDefined(DOMString name);
[CEReactions] void upgrade(Node root);
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/events/CloseEvent.webidl
@@ -1,7 +1,7 @@
// https://html.spec.whatwg.org/multipage/web-sockets.html#the-closeevent-interface
[Exposed=(Window,Worker)]
interface CloseEvent : Event {
constructor(DOMString type, optional CloseEventInit eventInitDict);
constructor(DOMString type, optional CloseEventInit eventInitDict = {});

readonly attribute boolean wasClean;
readonly attribute unsigned short code;
Expand Down
20 changes: 20 additions & 0 deletions lib/jsdom/living/events/event-handlers.webidl
@@ -0,0 +1,20 @@
// https://html.spec.whatwg.org/multipage/webappapis.html#eventhandlernonnull
[LegacyTreatNonObjectAsNull]
callback EventHandlerNonNull = any (Event event);
typedef EventHandlerNonNull? EventHandler;

// https://html.spec.whatwg.org/multipage/webappapis.html#onerroreventhandlernonnull
[LegacyTreatNonObjectAsNull]
callback OnErrorEventHandlerNonNull = any (
(Event or DOMString) event,
optional DOMString source,
optional unsigned long lineno,
optional unsigned long colno,
optional any error
);
typedef OnErrorEventHandlerNonNull? OnErrorEventHandler;

// https://html.spec.whatwg.org/multipage/webappapis.html#onbeforeunloadeventhandlernonnull
[LegacyTreatNonObjectAsNull]
callback OnBeforeUnloadEventHandlerNonNull = DOMString? (Event event);
typedef OnBeforeUnloadEventHandlerNonNull? OnBeforeUnloadEventHandler;
4 changes: 2 additions & 2 deletions lib/jsdom/living/helpers/create-element.js
Expand Up @@ -199,9 +199,9 @@ function createElement(
} else if (definition !== null) {
if (synchronousCE) {
try {
const C = definition.ctor;
const C = definition.constructor;

const resultWrapper = new C();
const resultWrapper = C.construct();
result = implForWrapper(resultWrapper);

if (!result._ceState || !result._ceDefinition || result._namespaceURI !== HTML_NS) {
Expand Down
53 changes: 21 additions & 32 deletions lib/jsdom/living/helpers/create-event-accessor.js
@@ -1,8 +1,10 @@
"use strict";

const conversions = require("webidl-conversions");
const idlUtils = require("../generated/utils");
const ErrorEvent = require("../generated/ErrorEvent");
const EventHandlerNonNull = require("../generated/EventHandlerNonNull.js");
const OnBeforeUnloadEventHandlerNonNull = require("../generated/OnBeforeUnloadEventHandlerNonNull.js");
const OnErrorEventHandlerNonNull = require("../generated/OnErrorEventHandlerNonNull.js");
const reportException = require("./runtime-script-errors");

exports.appendHandler = function appendHandler(el, eventName) {
Expand All @@ -18,25 +20,20 @@ exports.appendHandler = function appendHandler(el, eventName) {
event.currentTarget.constructor.name === "Window";

let returnValue = null;
const thisValue = idlUtils.tryWrapperForImpl(event.currentTarget);
// https://heycam.github.io/webidl/#es-invoking-callback-functions
if (typeof callback === "function") {
if (specialError) {
returnValue = callback.call(
thisValue, event.message,
event.currentTarget, event.message,
event.filename, event.lineno, event.colno, event.error
);
} else {
// This will no longer be necessary once EventHandler and Window are implemented in IDL:
const eventWrapper = idlUtils.wrapperForImpl(event);
returnValue = callback.call(thisValue, eventWrapper);
returnValue = callback.call(event.currentTarget, event);
}
}

if (event.type === "beforeunload") { // TODO: we don't implement BeforeUnloadEvent so we can't brand-check here
// Perform conversion which in the spec is done by the event handler return type being DOMString?
returnValue = returnValue === undefined || returnValue === null ? null : conversions.DOMString(returnValue);

// TODO: we don't implement BeforeUnloadEvent so we can't brand-check here
if (event.type === "beforeunload") {
if (returnValue !== null) {
event._canceledFlag = true;
if (event.returnValue === "") {
Expand Down Expand Up @@ -82,7 +79,9 @@ exports.createEventAccessor = function createEventAccessor(obj, event) {
Object.defineProperty(obj, "on" + event, {
configurable: true,
enumerable: true,
get() { // https://html.spec.whatwg.org/#getting-the-current-value-of-the-event-handler
get() {
// TODO: Extract this into a helper function
// https://html.spec.whatwg.org/multipage/webappapis.html#getting-the-current-value-of-the-event-handler
const value = this._getEventHandlerFor(event);
if (!value) {
return null;
Expand Down Expand Up @@ -126,6 +125,8 @@ exports.createEventAccessor = function createEventAccessor(obj, event) {
fn = createFunction("window", `with (window) { return function onerror(event, source, lineno, colno, error) {
${wrapperBody}
}; }`)(window);

fn = OnErrorEventHandlerNonNull.convert(fn);
} else {
const argNames = [];
const args = [];
Expand Down Expand Up @@ -157,34 +158,22 @@ return function on${event}(event) {
}
argNames.push(wrapperBody);
fn = createFunction(...argNames)(...args);

if (event === "beforeunload") {
fn = OnBeforeUnloadEventHandlerNonNull.convert(fn);
} else {
fn = EventHandlerNonNull.convert(fn);
}
}

this._setEventHandlerFor(event, fn);
}
return this._getEventHandlerFor(event);

// tryWrapperForImpl() is currently required due to use in Window.js
return idlUtils.tryWrapperForImpl(this._getEventHandlerFor(event));
},
set(val) {
val = eventHandlerArgCoercion(val);
this._setEventHandlerFor(event, val);
}
});
};

function typeIsObject(v) {
return (typeof v === "object" && v !== null) || typeof v === "function";
}

// Implements:
// [TreatNonObjectAsNull]
// callback EventHandlerNonNull = any (Event event);
// typedef EventHandlerNonNull? EventHandler;
// Also implements the part of https://heycam.github.io/webidl/#es-invoking-callback-functions which treats
// non-callable callback functions as callback functions that return undefined.
// TODO: replace with webidl2js typechecking when it has sufficient callback support
function eventHandlerArgCoercion(val) {
if (!typeIsObject(val)) {
return null;
}

return val;
}
4 changes: 2 additions & 2 deletions lib/jsdom/living/helpers/custom-elements.js
Expand Up @@ -94,7 +94,7 @@ function upgradeElement(definition, element) {

definition.constructionStack.push(element);

const { constructionStack, ctor: C } = definition;
const { constructionStack, constructor: C } = definition;

let constructionError;
try {
Expand All @@ -105,7 +105,7 @@ function upgradeElement(definition, element) {
]);
}

const constructionResult = new C();
const constructionResult = C.construct();
const constructionResultImpl = implForWrapper(constructionResult);

if (constructionResultImpl !== element) {
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/helpers/html-constructor.js
Expand Up @@ -15,7 +15,7 @@ function HTMLConstructor(globalObject, constructorName, newTarget) {
throw new TypeError("Invalid constructor");
}

const definition = registry._customElementDefinitions.find(entry => entry.ctor === newTarget);
const definition = registry._customElementDefinitions.find(entry => entry.objectReference === newTarget);
if (definition === undefined) {
throw new TypeError("Invalid constructor, the constructor is not part of the custom element registry");
}
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/mutation-observer/MutationObserver.webidl
Expand Up @@ -3,7 +3,7 @@
interface MutationObserver {
constructor(MutationCallback callback);

void observe(Node target, optional MutationObserverInit options);
void observe(Node target, optional MutationObserverInit options = {});
void disconnect();
sequence<MutationRecord> takeRecords();
};
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/nodes/DOMStringMap.webidl
Expand Up @@ -2,7 +2,7 @@
[Exposed=Window,
LegacyOverrideBuiltins]
interface DOMStringMap {
[WebIDL2JSValueAsUnsupported=undefined] getter DOMString (DOMString name);
[WebIDL2JSValueAsUnsupported=_undefined] getter DOMString (DOMString name);
[CEReactions] setter void (DOMString name, DOMString value);
[CEReactions] deleter void (DOMString name);
};
6 changes: 3 additions & 3 deletions lib/jsdom/living/nodes/Element.webidl
Expand Up @@ -74,11 +74,11 @@ partial interface Element {
DOMRectList getClientRects();
[NewObject] DOMRect getBoundingClientRect();
// void scrollIntoView(optional (boolean or ScrollIntoViewOptions) arg);
// void scroll(optional ScrollToOptions options);
// void scroll(optional ScrollToOptions options = {});
// void scroll(unrestricted double x, unrestricted double y);
// void scrollTo(optional ScrollToOptions options);
// void scrollTo(optional ScrollToOptions options = {});
// void scrollTo(unrestricted double x, unrestricted double y);
// void scrollBy(optional ScrollToOptions options);
// void scrollBy(optional ScrollToOptions options = {});
// void scrollBy(unrestricted double x, unrestricted double y);
attribute unrestricted double scrollTop;
attribute unrestricted double scrollLeft;
Expand Down
2 changes: 1 addition & 1 deletion lib/jsdom/living/nodes/HTMLOrSVGElement.webidl
Expand Up @@ -5,7 +5,7 @@ interface mixin HTMLOrSVGElement {

[CEReactions] attribute long tabIndex;
// We don't support FocusOptions yet
// void focus(optional FocusOptions options);
// void focus(optional FocusOptions options = {});
void focus();
void blur();
};

0 comments on commit ff69a75

Please sign in to comment.