From e5d041462411b024db17fd520eebcb9a0c90ec11 Mon Sep 17 00:00:00 2001 From: Pierre-Marie Dartus Date: Wed, 27 Feb 2019 08:40:33 -0800 Subject: [PATCH] Custom element implementation --- lib/api.js | 8 +- lib/jsdom/browser/Window.js | 11 +- lib/jsdom/browser/parser/html.js | 57 ++- lib/jsdom/browser/parser/xml.js | 15 +- lib/jsdom/living/attributes.js | 51 ++- lib/jsdom/living/attributes/Attr.webidl | 6 +- .../CustomElementRegistry-impl.js | 253 ++++++++++++ .../CustomElementRegistry.webidl | 13 + lib/jsdom/living/helpers/create-element.js | 308 ++++++++++++++ lib/jsdom/living/helpers/custom-elements.js | 255 +++++++++++- lib/jsdom/living/helpers/html-constructor.js | 81 ++++ lib/jsdom/living/interfaces.js | 363 ++++++++-------- lib/jsdom/living/node.js | 11 +- .../living/nodes/DOMImplementation-impl.js | 17 +- lib/jsdom/living/nodes/Document-impl.js | 75 ++-- lib/jsdom/living/nodes/Document.webidl | 7 +- lib/jsdom/living/nodes/Element-impl.js | 33 +- .../living/nodes/HTMLFrameElement-impl.js | 16 +- lib/jsdom/living/nodes/Node-impl.js | 29 +- lib/jsdom/living/range/Range-impl.js | 3 +- lib/jsdom/living/register-elements.js | 386 ------------------ lib/jsdom/living/webstorage/Storage-impl.js | 6 +- package.json | 3 +- scripts/webidl/convert.js | 23 +- test/web-platform-tests/to-run.yaml | 45 +- yarn.lock | 10 +- 26 files changed, 1398 insertions(+), 687 deletions(-) create mode 100644 lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js create mode 100644 lib/jsdom/living/custom-elements/CustomElementRegistry.webidl create mode 100644 lib/jsdom/living/helpers/create-element.js create mode 100644 lib/jsdom/living/helpers/html-constructor.js delete mode 100644 lib/jsdom/living/register-elements.js diff --git a/lib/api.js b/lib/api.js index 31772f5923..c28f94ca0d 100644 --- a/lib/api.js +++ b/lib/api.js @@ -10,7 +10,7 @@ const { URL } = require("whatwg-url"); const MIMEType = require("whatwg-mimetype"); const idlUtils = require("./jsdom/living/generated/utils.js"); const VirtualConsole = require("./jsdom/virtual-console.js"); -const Window = require("./jsdom/browser/Window.js"); +const { createWindow } = require("./jsdom/browser/Window.js"); const { parseIntoDocument } = require("./jsdom/browser/parser"); const { fragmentSerialization } = require("./jsdom/living/domparsing/serialization.js"); const ResourceLoader = require("./jsdom/browser/resources/resource-loader.js"); @@ -33,7 +33,7 @@ class JSDOM { options = transformOptions(options, encoding, mimeType); - this[window] = new Window(options.windowOptions); + this[window] = createWindow(options.windowOptions); const documentImpl = idlUtils.implForWrapper(this[window]._document); @@ -45,8 +45,8 @@ class JSDOM { } get window() { - // It's important to grab the global proxy, instead of just the result of `new Window(...)`, since otherwise things - // like `window.eval` don't exist. + // It's important to grab the global proxy, instead of just the result of `createWindow(...)`, since otherwise + // things like `window.eval` don't exist. return this[window]._globalProxy; } diff --git a/lib/jsdom/browser/Window.js b/lib/jsdom/browser/Window.js index 7e378d951c..b8eddf4f35 100644 --- a/lib/jsdom/browser/Window.js +++ b/lib/jsdom/browser/Window.js @@ -4,6 +4,7 @@ const webIDLConversions = require("webidl-conversions"); const { CSSStyleDeclaration } = require("cssstyle"); const { Performance: RawPerformance } = require("w3c-hr-time"); const notImplemented = require("./not-implemented"); +const { installInterfaces } = require("../living/interfaces"); const { define, mixin } = require("../utils"); const Element = require("../living/generated/Element"); const EventTarget = require("../living/generated/EventTarget"); @@ -27,15 +28,15 @@ const { fireAnEvent } = require("../living/helpers/events"); const SessionHistory = require("../living/window/SessionHistory"); const { forEachMatchingSheetRuleOfElement, getResolvedValue, propertiesWithResolvedValueImplemented } = require("../living/helpers/style-rules"); +const CustomElementRegistry = require("../living/generated/CustomElementRegistry"); const jsGlobals = require("./js-globals.json"); const GlobalEventHandlersImpl = require("../living/nodes/GlobalEventHandlers-impl").implementation; const WindowEventHandlersImpl = require("../living/nodes/WindowEventHandlers-impl").implementation; -// NB: the require() must be after assigning `module.exports` because this require() is circular -// TODO: this above note might not even be true anymore... figure out the cycle and document it, or clean up. -module.exports = Window; -const { installInterfaces } = require("../living/interfaces"); +exports.createWindow = function (options) { + return new Window(options); +}; // https://html.spec.whatwg.org/#the-window-object function setupWindow(windowInstance, { runScripts }) { @@ -576,6 +577,8 @@ function Window(options) { return window._document.getSelection(); }; + this.customElements = CustomElementRegistry.create(window); + // The captureEvents() and releaseEvents() methods must do nothing this.captureEvents = function () {}; diff --git a/lib/jsdom/browser/parser/html.js b/lib/jsdom/browser/parser/html.js index 8caf0bb4e1..c6183d6d4f 100644 --- a/lib/jsdom/browser/parser/html.js +++ b/lib/jsdom/browser/parser/html.js @@ -2,6 +2,8 @@ const parse5 = require("parse5"); +const { createElement } = require("../../living/helpers/create-element"); + const DocumentType = require("../../living/generated/DocumentType"); const DocumentFragment = require("../../living/generated/DocumentFragment"); const Text = require("../../living/generated/Text"); @@ -11,15 +13,19 @@ const attributes = require("../../living/attributes"); const nodeTypes = require("../../living/node-type"); const serializationAdapter = require("../../living/domparsing/parse5-adapter-serialization"); +const { + CUSTOM_ELEMENT_REACTIONS_STACK, invokeCEReactions, lookupCEDefinition +} = require("../../living/helpers/custom-elements"); const OpenElementStack = require("parse5/lib/parser/open-element-stack"); const OpenElementStackOriginalPop = OpenElementStack.prototype.pop; const OpenElementStackOriginalPush = OpenElementStack.prototype.push; class JSDOMParse5Adapter { - constructor(documentImpl) { + constructor(documentImpl, options = {}) { this._documentImpl = documentImpl; this._globalObject = documentImpl._globalObject; + this._fragment = options.fragment || false; // Since the createElement hook doesn't provide the parent element, we keep track of this using _currentElement: // https://github.com/inikulin/parse5/issues/285 @@ -49,9 +55,16 @@ class JSDOMParse5Adapter { } _ownerDocument() { - // The _currentElement is undefined when parsing elements at the root of the document. In this case we would - // fallback to the global _documentImpl. - return this._currentElement ? this._currentElement._ownerDocument : this._documentImpl; + const { _currentElement } = this; + + // The _currentElement is undefined when parsing elements at the root of the document. + if (_currentElement) { + return _currentElement.localName === "template" ? + _currentElement.content._ownerDocument : + _currentElement._ownerDocument; + } + + return this._documentImpl; } createDocument() { @@ -63,16 +76,38 @@ class JSDOMParse5Adapter { } createDocumentFragment() { - return DocumentFragment.createImpl(this._globalObject, [], { ownerDocument: this._currentElement._ownerDocument }); + const ownerDocument = this._ownerDocument(); + return DocumentFragment.createImpl(this._globalObject, [], { ownerDocument }); } + // https://html.spec.whatwg.org/#create-an-element-for-the-token createElement(localName, namespace, attrs) { const ownerDocument = this._ownerDocument(); - const element = ownerDocument._createElementWithCorrectElementInterface(localName, namespace); - element._namespaceURI = namespace; + const isAttribute = attrs.find(attr => attr.name === "is"); + const isValue = isAttribute ? isAttribute.value : null; + + const definition = lookupCEDefinition(ownerDocument, namespace, localName); + + let willExecuteScript = false; + if (definition !== null && !this._fragment) { + willExecuteScript = true; + } + + if (willExecuteScript) { + ownerDocument._throwOwnDynamicMarkupInsertionCounter++; + CUSTOM_ELEMENT_REACTIONS_STACK.push([]); + } + + const element = createElement(ownerDocument, localName, namespace, null, isValue, willExecuteScript); this.adoptAttributes(element, attrs); + if (willExecuteScript) { + const queue = CUSTOM_ELEMENT_REACTIONS_STACK.pop(); + invokeCEReactions(queue); + ownerDocument._throwOwnDynamicMarkupInsertionCounter--; + } + if ("_parserInserted" in element) { element._parserInserted = true; } @@ -159,10 +194,14 @@ class JSDOMParse5Adapter { Object.assign(JSDOMParse5Adapter.prototype, serializationAdapter); function parseFragment(markup, contextElement) { - const ownerDocument = contextElement._ownerDocument; + const ownerDocument = contextElement.localName === "template" ? + contextElement.content._ownerDocument : + contextElement._ownerDocument; const config = Object.assign({}, ownerDocument._parseOptions, { - treeAdapter: new JSDOMParse5Adapter(ownerDocument) + treeAdapter: new JSDOMParse5Adapter(ownerDocument, { + fragment: true + }) }); return parse5.parseFragment(contextElement, markup, config); diff --git a/lib/jsdom/browser/parser/xml.js b/lib/jsdom/browser/parser/xml.js index d1721d3df5..76759a0073 100644 --- a/lib/jsdom/browser/parser/xml.js +++ b/lib/jsdom/browser/parser/xml.js @@ -3,6 +3,8 @@ const { SaxesParser } = require("saxes"); const DOMException = require("domexception/webidl2js-wrapper"); +const { createElement } = require("../../living/helpers/create-element"); + const DocumentFragment = require("../../living/generated/DocumentFragment"); const DocumentType = require("../../living/generated/DocumentType"); const CDATASection = require("../../living/generated/CDATASection"); @@ -99,17 +101,18 @@ function createParser(rootNode, globalObject, saxesOptions) { }; parser.onopentag = tag => { - const { local: tagLocal, uri: tagURI, prefix: tagPrefix, attributes: tagAttributes } = tag; - const ownerDocument = getOwnerDocument(); + const { local: tagLocal, attributes: tagAttributes } = tag; - const elem = ownerDocument._createElementWithCorrectElementInterface(tagLocal, tagURI); + const ownerDocument = getOwnerDocument(); + const tagNamespace = tag.uri === "" ? null : tag.uri; + const tagPrefix = tag.prefix === "" ? null : tag.prefix; + const isValue = tagAttributes.is === undefined ? null : tagAttributes.is.value; - elem._prefix = tagPrefix || null; - elem._namespaceURI = tagURI || null; + const elem = createElement(ownerDocument, tagLocal, tagNamespace, tagPrefix, isValue, true); // We mark a script element as "parser-inserted", which prevents it from // being immediately executed. - if (tagLocal === "script" && tagURI === HTML_NS) { + if (tagLocal === "script" && tagNamespace === HTML_NS) { elem._parserInserted = true; } diff --git a/lib/jsdom/living/attributes.js b/lib/jsdom/living/attributes.js index c6f0c29bd0..b753ee226a 100644 --- a/lib/jsdom/living/attributes.js +++ b/lib/jsdom/living/attributes.js @@ -1,9 +1,11 @@ "use strict"; const DOMException = require("domexception/webidl2js-wrapper"); const attrGenerated = require("./generated/Attr"); -const { asciiLowercase } = require("./helpers/strings"); + const { HTML_NS } = require("./helpers/namespaces"); +const { asciiLowercase } = require("./helpers/strings"); const { queueAttributeMutationRecord } = require("./helpers/mutation-observers"); +const { CUSTOM_ELEMENT_STATE, enqueueCECallbackReaction } = require("./helpers/custom-elements"); // The following three are for https://dom.spec.whatwg.org/#concept-element-attribute-has. We don't just have a // predicate tester since removing that kind of flexibility gives us the potential for better future optimizations. @@ -28,21 +30,40 @@ exports.changeAttribute = function (element, attribute, value) { // https://dom.spec.whatwg.org/#concept-element-attributes-change const { _localName, _namespace, _value } = attribute; + queueAttributeMutationRecord(element, _localName, _namespace, _value); - const oldValue = attribute._value; + if (element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) { + enqueueCECallbackReaction(element, "attributeChangedCallback", [ + _localName, + _value, + value, + _namespace + ]); + } + attribute._value = value; // Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is changed." - element._attrModified(attribute._qualifiedName, value, oldValue); + element._attrModified(attribute._qualifiedName, value, _value); }; exports.appendAttribute = function (element, attribute) { // https://dom.spec.whatwg.org/#concept-element-attributes-append - const { _localName, _namespace } = attribute; + const { _localName, _namespace, _value } = attribute; + queueAttributeMutationRecord(element, _localName, _namespace, null); + if (element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) { + enqueueCECallbackReaction(element, "attributeChangedCallback", [ + _localName, + null, + _value, + _namespace + ]); + } + const attributeList = element._attributeList; attributeList.push(attribute); @@ -59,15 +80,25 @@ exports.appendAttribute = function (element, attribute) { entry.push(attribute); // Run jsdom hooks; roughly correspond to spec's "An attribute is set and an attribute is added." - element._attrModified(name, attribute._value, null); + element._attrModified(name, _value, null); }; exports.removeAttribute = function (element, attribute) { // https://dom.spec.whatwg.org/#concept-element-attributes-remove const { _localName, _namespace, _value } = attribute; + queueAttributeMutationRecord(element, _localName, _namespace, _value); + if (element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) { + enqueueCECallbackReaction(element, "attributeChangedCallback", [ + _localName, + _value, + null, + _namespace + ]); + } + const attributeList = element._attributeList; for (let i = 0; i < attributeList.length; ++i) { @@ -96,8 +127,18 @@ exports.replaceAttribute = function (element, oldAttr, newAttr) { // https://dom.spec.whatwg.org/#concept-element-attributes-replace const { _localName, _namespace, _value } = oldAttr; + queueAttributeMutationRecord(element, _localName, _namespace, _value); + if (element._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) { + enqueueCECallbackReaction(element, "attributeChangedCallback", [ + _localName, + _value, + newAttr._value, + _namespace + ]); + } + const attributeList = element._attributeList; for (let i = 0; i < attributeList.length; ++i) { diff --git a/lib/jsdom/living/attributes/Attr.webidl b/lib/jsdom/living/attributes/Attr.webidl index de37136f59..f013eb6311 100644 --- a/lib/jsdom/living/attributes/Attr.webidl +++ b/lib/jsdom/living/attributes/Attr.webidl @@ -7,9 +7,9 @@ interface Attr { readonly attribute DOMString localName; readonly attribute DOMString name; readonly attribute DOMString nodeName; // historical alias of .name - attribute DOMString value; - [TreatNullAs=EmptyString] attribute DOMString nodeValue; // historical alias of .value - [TreatNullAs=EmptyString] attribute DOMString textContent; // historical alias of .value + [CEReactions] attribute DOMString value; + [CEReactions, TreatNullAs=EmptyString] attribute DOMString nodeValue; // historical alias of .value + [CEReactions, TreatNullAs=EmptyString] attribute DOMString textContent; // historical alias of .value readonly attribute Element? ownerElement; diff --git a/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js b/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js new file mode 100644 index 0000000000..ab7d4c788b --- /dev/null +++ b/lib/jsdom/living/custom-elements/CustomElementRegistry-impl.js @@ -0,0 +1,253 @@ +"use strict"; + +const webIDLConversions = require("webidl-conversions"); +const DOMException = require("domexception/webidl2js-wrapper"); + +const NODE_TYPE = require("../node-type"); + +const { HTML_NS } = require("../helpers/namespaces"); +const { getHTMLElementInterface } = require("../helpers/create-element"); +const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shadow-dom"); +const { isValidCustomElementName, tryUpgradeElement, enqueueCEUpgradeReaction } = require("../helpers/custom-elements"); + +const idlUtils = require("../generated/utils"); +const HTMLUnknownElement = require("../generated/HTMLUnknownElement"); + +const LIFECYCLE_CALLBACKS = [ + "connectedCallback", + "disconnectedCallback", + "adoptedCallback", + "attributeChangedCallback" +]; + +function convertToSequenceDOMString(obj) { + if (!obj || !obj[Symbol.iterator]) { + throw new TypeError("Invalid Sequence"); + } + + return Array.from(obj).map(webIDLConversions.DOMString); +} + +// Returns true is the passed value is a valid constructor. +// Borrowed from: https://stackoverflow.com/a/39336206/3832710 +function isConstructor(value) { + try { + const P = new Proxy(value, { + construct() { + return {}; + } + }); + + // eslint-disable-next-line no-new + new P(); + + return true; + } catch (err) { + return false; + } +} + +// https://html.spec.whatwg.org/#customelementregistry +class CustomElementRegistryImpl { + constructor(globalObject) { + this._customElementDefinitions = []; + this._elementDefinitionIsRunning = false; + this._whenDefinedPromiseMap = Object.create(null); + + this._globalObject = globalObject; + } + + // https://html.spec.whatwg.org/#dom-customelementregistry-define + define(name, ctor, options) { + const { _globalObject } = this; + + if (typeof ctor !== "function" || !isConstructor(ctor)) { + throw new TypeError("Constructor argument is not a constructor."); + } + + if (!isValidCustomElementName(name)) { + throw DOMException.create(_globalObject, ["Name argument is not a valid custom element name.", "SyntaxError"]); + } + + const nameAlreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name); + if (nameAlreadyRegistered) { + throw DOMException.create(_globalObject, [ + "This name has already been registered in the registry.", + "NotSupportedError" + ]); + } + + const ctorAlreadyRegistered = this._customElementDefinitions.some(entry => entry.ctor === ctor); + if (ctorAlreadyRegistered) { + throw DOMException.create(_globalObject, [ + "This constructor has already been registered in the registry.", + "NotSupportedError" + ]); + } + + let localName = name; + + let extendsOption = null; + if (options !== undefined && options.extends) { + extendsOption = options.extends; + } + + if (extendsOption !== null) { + if (isValidCustomElementName(extendsOption)) { + throw DOMException.create(_globalObject, [ + "Option extends value can't be a valid custom element name.", + "NotSupportedError" + ]); + } + + const extendsInterface = getHTMLElementInterface(extendsOption); + if (extendsInterface === HTMLUnknownElement) { + throw DOMException.create(_globalObject, [ + `${extendsOption} is an HTMLUnknownElement.`, + "NotSupportedError" + ]); + } + + localName = extendsOption; + } + + if (this._elementDefinitionIsRunning) { + throw DOMException.create(_globalObject, [ + "Invalid nested custom element definition.", + "NotSupportedError" + ]); + } + + this._elementDefinitionIsRunning = true; + + let disableShadow = false; + let observedAttributes = []; + const lifecycleCallbacks = { + connectedCallback: null, + disconnectedCallback: null, + adoptedCallback: null, + attributeChangedCallback: null + }; + + let caughtError; + try { + const { prototype } = ctor; + + if (typeof prototype !== "object") { + throw new TypeError("Invalid constructor prototype."); + } + + for (const callbackName of LIFECYCLE_CALLBACKS) { + const callbackValue = prototype[callbackName]; + + if (callbackValue !== undefined) { + lifecycleCallbacks[callbackName] = webIDLConversions.Function(callbackValue); + } + } + + if (lifecycleCallbacks.attributeChangedCallback !== null) { + const observedAttributesIterable = ctor.observedAttributes; + + if (observedAttributesIterable !== undefined) { + observedAttributes = convertToSequenceDOMString(observedAttributesIterable); + } + } + + let disabledFeatures = []; + const disabledFeaturesIterable = ctor.disabledFeatures; + if (disabledFeaturesIterable) { + disabledFeatures = convertToSequenceDOMString(disabledFeaturesIterable); + } + + disableShadow = disabledFeatures.includes("shadow"); + } catch (err) { + caughtError = err; + } finally { + this._elementDefinitionIsRunning = false; + } + + if (caughtError !== undefined) { + throw caughtError; + } + + const definition = { + name, + localName, + ctor, + observedAttributes, + lifecycleCallbacks, + disableShadow, + constructionStack: [] + }; + + this._customElementDefinitions.push(definition); + + const document = idlUtils.implForWrapper(this._globalObject._document); + + const upgradeCandidates = []; + for (const candidate of shadowIncludingInclusiveDescendantsIterator(document)) { + if ( + (candidate._namespaceURI === HTML_NS && candidate._localName === localName) && + (extendsOption === null || candidate._isValue === name) + ) { + upgradeCandidates.push(candidate); + } + } + + for (const upgradeCandidate of upgradeCandidates) { + enqueueCEUpgradeReaction(upgradeCandidate, definition); + } + + if (this._whenDefinedPromiseMap[name] !== undefined) { + this._whenDefinedPromiseMap[name].resolve(undefined); + delete this._whenDefinedPromiseMap[name]; + } + } + + // https://html.spec.whatwg.org/#dom-customelementregistry-get + get(name) { + const definition = this._customElementDefinitions.find(entry => entry.name === name); + return definition && definition.ctor; + } + + // https://html.spec.whatwg.org/#dom-customelementregistry-whendefined + whenDefined(name) { + if (!isValidCustomElementName(name)) { + return Promise.reject(new DOMException("Name argument is not a valid custom element name.", "SyntaxError")); + } + + const alreadyRegistered = this._customElementDefinitions.some(entry => entry.name === name); + if (alreadyRegistered) { + return Promise.resolve(); + } + + if (this._whenDefinedPromiseMap[name] === undefined) { + let resolve; + const promise = new Promise(r => { + resolve = r; + }); + + // Store the pending Promise along with the extracted resolve callback to actually resolve the returned Promise, + // once the custom element is registered. + this._whenDefinedPromiseMap[name] = { + promise, + resolve + }; + } + + return this._whenDefinedPromiseMap[name].promise; + } + + // https://html.spec.whatwg.org/#dom-customelementregistry-upgrade + upgrade(root) { + for (const candidate of shadowIncludingInclusiveDescendantsIterator(root)) { + if (candidate.nodeType === NODE_TYPE.ELEMENT_NODE) { + tryUpgradeElement(candidate); + } + } + } +} + +module.exports = { + implementation: CustomElementRegistryImpl +}; diff --git a/lib/jsdom/living/custom-elements/CustomElementRegistry.webidl b/lib/jsdom/living/custom-elements/CustomElementRegistry.webidl new file mode 100644 index 0000000000..2e8a55d30a --- /dev/null +++ b/lib/jsdom/living/custom-elements/CustomElementRegistry.webidl @@ -0,0 +1,13 @@ +[Exposed=Window] +interface CustomElementRegistry { + [CEReactions] void define(DOMString name, CustomElementConstructor constructor, optional ElementDefinitionOptions options); + any get(DOMString name); + Promise whenDefined(DOMString name); + [CEReactions] void upgrade(Node root); +}; + +callback CustomElementConstructor = any (); + +dictionary ElementDefinitionOptions { + DOMString extends; +}; diff --git a/lib/jsdom/living/helpers/create-element.js b/lib/jsdom/living/helpers/create-element.js new file mode 100644 index 0000000000..4ca607bbeb --- /dev/null +++ b/lib/jsdom/living/helpers/create-element.js @@ -0,0 +1,308 @@ +"use strict"; + +const DOMException = require("domexception/webidl2js-wrapper"); + +const interfaces = require("../interfaces"); + +const { implForWrapper } = require("../generated/utils"); + +const { HTML_NS, SVG_NS } = require("./namespaces"); +const { domSymbolTree } = require("./internal-constants"); +const { validateAndExtract } = require("./validate-names"); +const reportException = require("./runtime-script-errors"); +const { + CUSTOM_ELEMENT_STATE, isValidCustomElementName, upgradeElement, lookupCEDefinition, + enqueueCEUpgradeReaction +} = require("./custom-elements"); + + +const INTERFACE_TAG_MAPPING = { + // https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom%3Aelement-interface + // https://html.spec.whatwg.org/multipage/indices.html#elements-3 + [HTML_NS]: { + HTMLElement: [ + "abbr", "address", "article", "aside", "b", "bdi", "bdo", "cite", "code", "dd", "dfn", "dt", "em", "figcaption", + "figure", "footer", "header", "hgroup", "i", "kbd", "main", "mark", "nav", "noscript", "rp", "rt", "ruby", "s", + "samp", "section", "small", "strong", "sub", "summary", "sup", "u", "var", "wbr" + ], + HTMLAnchorElement: ["a"], + HTMLAreaElement: ["area"], + HTMLAudioElement: ["audio"], + HTMLBaseElement: ["base"], + HTMLBodyElement: ["body"], + HTMLBRElement: ["br"], + HTMLButtonElement: ["button"], + HTMLCanvasElement: ["canvas"], + HTMLDataElement: ["data"], + HTMLDataListElement: ["datalist"], + HTMLDetailsElement: ["details"], + HTMLDialogElement: ["dialog"], + HTMLDirectoryElement: ["dir"], + HTMLDivElement: ["div"], + HTMLDListElement: ["dl"], + HTMLEmbedElement: ["embed"], + HTMLFieldSetElement: ["fieldset"], + HTMLFontElement: ["font"], + HTMLFormElement: ["form"], + HTMLFrameElement: ["frame"], + HTMLFrameSetElement: ["frameset"], + HTMLHeadingElement: ["h1", "h2", "h3", "h4", "h5", "h6"], + HTMLHeadElement: ["head"], + HTMLHRElement: ["hr"], + HTMLHtmlElement: ["html"], + HTMLIFrameElement: ["iframe"], + HTMLImageElement: ["img"], + HTMLInputElement: ["input"], + HTMLLabelElement: ["label"], + HTMLLegendElement: ["legend"], + HTMLLIElement: ["li"], + HTMLLinkElement: ["link"], + HTMLMapElement: ["map"], + HTMLMarqueeElement: ["marquee"], + HTMLMediaElement: [], + HTMLMenuElement: ["menu"], + HTMLMetaElement: ["meta"], + HTMLMeterElement: ["meter"], + HTMLModElement: ["del", "ins"], + HTMLObjectElement: ["object"], + HTMLOListElement: ["ol"], + HTMLOptGroupElement: ["optgroup"], + HTMLOptionElement: ["option"], + HTMLOutputElement: ["output"], + HTMLParagraphElement: ["p"], + HTMLParamElement: ["param"], + HTMLPictureElement: ["picture"], + HTMLPreElement: ["listing", "pre", "xmp"], + HTMLProgressElement: ["progress"], + HTMLQuoteElement: ["blockquote", "q"], + HTMLScriptElement: ["script"], + HTMLSelectElement: ["select"], + HTMLSlotElement: ["slot"], + HTMLSourceElement: ["source"], + HTMLSpanElement: ["span"], + HTMLStyleElement: ["style"], + HTMLTableCaptionElement: ["caption"], + HTMLTableCellElement: ["th", "td"], + HTMLTableColElement: ["col", "colgroup"], + HTMLTableElement: ["table"], + HTMLTimeElement: ["time"], + HTMLTitleElement: ["title"], + HTMLTableRowElement: ["tr"], + HTMLTableSectionElement: ["thead", "tbody", "tfoot"], + HTMLTemplateElement: ["template"], + HTMLTextAreaElement: ["textarea"], + HTMLTrackElement: ["track"], + HTMLUListElement: ["ul"], + HTMLUnknownElement: [], + HTMLVideoElement: ["video"] + }, + [SVG_NS]: { + SVGElement: [], + SVGGraphicsElement: [], + SVGSVGElement: ["svg"], + SVGTitleElement: ["title"] + } +}; + +const TAG_INTERFACE_LOOKUP = {}; + +for (const namespace of [HTML_NS, SVG_NS]) { + TAG_INTERFACE_LOOKUP[namespace] = {}; + + const interfaceNames = Object.keys(INTERFACE_TAG_MAPPING[namespace]); + for (const interfaceName of interfaceNames) { + const tagNames = INTERFACE_TAG_MAPPING[namespace][interfaceName]; + + for (const tagName of tagNames) { + TAG_INTERFACE_LOOKUP[namespace][tagName] = interfaceName; + } + } +} + +const UNKNOWN_HTML_ELEMENTS_NAMES = ["applet", "bgsound", "blink", "isindex", "keygen", "multicol", "nextid", "spacer"]; +const HTML_ELEMENTS_NAMES = [ + "acronym", "basefont", "big", "center", "nobr", "noembed", "noframes", "plaintext", "rb", "rtc", + "strike", "tt" +]; + +// https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom:element-interface +function getHTMLElementInterface(name) { + if (UNKNOWN_HTML_ELEMENTS_NAMES.includes(name)) { + return interfaces.getInterfaceWrapper("HTMLUnknownElement"); + } + + if (HTML_ELEMENTS_NAMES.includes(name)) { + return interfaces.getInterfaceWrapper("HTMLElement"); + } + + const specDefinedInterface = TAG_INTERFACE_LOOKUP[HTML_NS][name]; + if (specDefinedInterface !== undefined) { + return interfaces.getInterfaceWrapper(specDefinedInterface); + } + + if (isValidCustomElementName(name)) { + return interfaces.getInterfaceWrapper("HTMLElement"); + } + + return interfaces.getInterfaceWrapper("HTMLUnknownElement"); +} + +// https://svgwg.org/svg2-draft/types.html#ElementsInTheSVGDOM +function getSVGInterface(name) { + const specDefinedInterface = TAG_INTERFACE_LOOKUP[SVG_NS][name]; + if (specDefinedInterface !== undefined) { + return interfaces.getInterfaceWrapper(specDefinedInterface); + } + + return interfaces.getInterfaceWrapper("SVGElement"); +} + +/** Returns the list of valid tag names that can bo associated with a element given its namespace and name. */ +function getValidTagNames(namespace, name) { + if (INTERFACE_TAG_MAPPING[namespace] && INTERFACE_TAG_MAPPING[namespace][name]) { + return INTERFACE_TAG_MAPPING[namespace][name]; + } + + return []; +} + +// https://dom.spec.whatwg.org/#concept-create-element +function createElement(document, localName, namespace, prefix = null, isValue = null, synchronousCE = false) { + let result = null; + + const { _globalObject } = document; + const definition = lookupCEDefinition(document, namespace, localName, isValue); + + if (definition !== null && definition.name !== localName) { + const elementInterface = getHTMLElementInterface(localName); + + result = elementInterface.createImpl(_globalObject, [], { + ownerDocument: document, + localName, + namespace: HTML_NS, + prefix, + ceState: CUSTOM_ELEMENT_STATE.UNDEFINED, + ceDefinition: null, + isValue + }); + + if (synchronousCE) { + upgradeElement(definition, result); + } else { + enqueueCEUpgradeReaction(result, definition); + } + } else if (definition !== null) { + if (synchronousCE) { + try { + const C = definition.ctor; + + const resultWrapper = new C(); + result = implForWrapper(resultWrapper); + + if (!result._ceState || !result._ceDefinition || result._namespaceURI !== HTML_NS) { + throw new TypeError("Internal error: Invalid custom element."); + } + + if (result._attributeList.length !== 0) { + throw DOMException.create(_globalObject, ["Unexpected attributes.", "NotSupportedError"]); + } + if (domSymbolTree.hasChildren(result)) { + throw DOMException.create(_globalObject, ["Unexpected child nodes.", "NotSupportedError"]); + } + if (domSymbolTree.parent(result)) { + throw DOMException.create(_globalObject, ["Unexpected element parent.", "NotSupportedError"]); + } + if (result._ownerDocument !== document) { + throw DOMException.create(_globalObject, ["Unexpected element owner document.", "NotSupportedError"]); + } + if (result._namespaceURI !== namespace) { + throw DOMException.create(_globalObject, ["Unexpected element namespace URI.", "NotSupportedError"]); + } + if (result._localName !== localName) { + throw DOMException.create(_globalObject, ["Unexpected element local name.", "NotSupportedError"]); + } + + result._prefix = prefix; + result._isValue = isValue; + } catch (error) { + reportException(document._defaultView, error); + + const interfaceWrapper = interfaces.getInterfaceWrapper("HTMLUnknownElement"); + result = interfaceWrapper.createImpl(_globalObject, [], { + ownerDocument: document, + localName, + namespace: HTML_NS, + prefix, + ceState: CUSTOM_ELEMENT_STATE.FAILED, + ceDefinition: null, + isValue: null + }); + } + } else { + const interfaceWrapper = interfaces.getInterfaceWrapper("HTMLElement"); + result = interfaceWrapper.createImpl(_globalObject, [], { + ownerDocument: document, + localName, + namespace: HTML_NS, + prefix, + ceState: CUSTOM_ELEMENT_STATE.UNDEFINED, + ceDefinition: null, + isValue: null + }); + + enqueueCEUpgradeReaction(result, definition); + } + } else { + let elementInterface; + + switch (namespace) { + case HTML_NS: + elementInterface = getHTMLElementInterface(localName); + break; + + case SVG_NS: + elementInterface = getSVGInterface(localName); + break; + + default: + elementInterface = interfaces.getInterfaceWrapper("Element"); + break; + } + + result = elementInterface.createImpl(_globalObject, [], { + ownerDocument: document, + localName, + namespace, + prefix, + ceState: CUSTOM_ELEMENT_STATE.UNCUSTOMIZED, + ceDefinition: null, + isValue + }); + + if (namespace === HTML_NS && (isValidCustomElementName(localName) || isValue !== null)) { + result._ceState = CUSTOM_ELEMENT_STATE.UNDEFINED; + } + } + + return result; +} + +// https://dom.spec.whatwg.org/#internal-createelementns-steps +function createElementNS(document, namespace, qualifiedName, options) { + const extracted = validateAndExtract(document._globalObject, namespace, qualifiedName); + + let isValue = null; + if (options && options.is !== undefined) { + isValue = options.is; + } + + return createElement(document, extracted.localName, extracted.namespace, extracted.prefix, isValue, true); +} + +module.exports = { + createElement, + createElementNS, + + getValidTagNames, + getHTMLElementInterface +}; diff --git a/lib/jsdom/living/helpers/custom-elements.js b/lib/jsdom/living/helpers/custom-elements.js index 5ab500401a..37d3c41a43 100644 --- a/lib/jsdom/living/helpers/custom-elements.js +++ b/lib/jsdom/living/helpers/custom-elements.js @@ -1,5 +1,64 @@ "use strict"; +const DOMException = require("domexception/webidl2js-wrapper"); +const isPotentialCustomElementName = require("is-potential-custom-element-name"); + +const NODE_TYPE = require("../node-type"); +const { HTML_NS } = require("./namespaces"); +const { shadowIncludingRoot } = require("./shadow-dom"); +const reportException = require("./runtime-script-errors"); + +const { implForWrapper, wrapperForImpl } = require("../generated/utils"); + +// https://dom.spec.whatwg.org/#concept-element-custom-element-state +const CUSTOM_ELEMENT_STATE = { + UNDEFINED: "undefined", + FAILED: "failed", + UNCUSTOMIZED: "uncustomized", + CUSTOM: "custom" +}; + +// https://html.spec.whatwg.org/multipage/custom-elements.html#custom-element-reactions-stack +class CEReactionsStack { + constructor() { + this._stack = []; + + // https://html.spec.whatwg.org/multipage/custom-elements.html#backup-element-queue + this.backupElementQueue = []; + + // https://html.spec.whatwg.org/multipage/custom-elements.html#processing-the-backup-element-queue + this.processingBackupElementQueue = false; + } + + push(elementQueue) { + this._stack.push(elementQueue); + } + + pop() { + return this._stack.pop(); + } + + get currentElementQueue() { + const { _stack } = this; + return _stack[_stack.length - 1]; + } + + isEmpty() { + return this._stack.length === 0; + } +} + +const CUSTOM_ELEMENT_REACTIONS_STACK = new CEReactionsStack(); + +// https://html.spec.whatwg.org/multipage/custom-elements.html#cereactions +function ceReactionsPreSteps() { + CUSTOM_ELEMENT_REACTIONS_STACK.push([]); +} +function ceReactionsPostSteps() { + const queue = CUSTOM_ELEMENT_REACTIONS_STACK.pop(); + invokeCEReactions(queue); +} + const RESTRICTED_CUSTOM_ELEMENT_NAME = new Set([ "annotation-xml", "color-profile", @@ -11,17 +70,205 @@ const RESTRICTED_CUSTOM_ELEMENT_NAME = new Set([ "missing-glyph" ]); -const CUSTOM_ELEMENT_NAME_REGEXP = /^[a-z][-.0-9_a-z]*-[-.0-9_a-z]*$/; - // https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name function isValidCustomElementName(name) { if (RESTRICTED_CUSTOM_ELEMENT_NAME.has(name)) { return false; } - return CUSTOM_ELEMENT_NAME_REGEXP.test(name); + return isPotentialCustomElementName(name); +} + +// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-upgrade-an-element +function upgradeElement(definition, element) { + if (element._ceState !== CUSTOM_ELEMENT_STATE.UNDEFINED || element._ceState === CUSTOM_ELEMENT_STATE.UNCUSTOMIZED) { + return; + } + + element._ceDefinition = definition; + element._ceState = CUSTOM_ELEMENT_STATE.FAILED; + + for (const attribute of element._attributeList) { + const { _localName, _namespace, _value } = attribute; + enqueueCECallbackReaction(element, "attributeChangedCallback", [_localName, null, _value, _namespace]); + } + + if (shadowIncludingRoot(element).nodeType === NODE_TYPE.DOCUMENT_NODE) { + enqueueCECallbackReaction(element, "connectedCallback", []); + } + + definition.constructionStack.push(element); + + const { constructionStack, ctor: C } = definition; + + let constructionError; + try { + if (definition.disableShadow === true && element._shadowRoot !== null) { + throw DOMException.create(element._globalObject, [ + "Can't upgrade a custom element with a shadow root if shadow is disabled", + "NotSupportedError" + ]); + } + + const constructionResult = new C(); + const constructionResultImpl = implForWrapper(constructionResult); + + if (constructionResultImpl !== element) { + throw new TypeError("Invalid custom element constructor return value"); + } + } catch (error) { + constructionError = error; + } + + constructionStack.pop(); + + if (constructionError !== undefined) { + element._ceDefinition = null; + element._ceReactionQueue = []; + + throw constructionError; + } + + element._ceState = CUSTOM_ELEMENT_STATE.CUSTOM; +} + +// https://html.spec.whatwg.org/#concept-try-upgrade +function tryUpgradeElement(element) { + const { _ownerDocument, _namespaceURI, _localName, _isValue } = element; + const definition = lookupCEDefinition(_ownerDocument, _namespaceURI, _localName, _isValue); + + if (definition !== null) { + enqueueCEUpgradeReaction(element, definition); + } +} + +// https://html.spec.whatwg.org/#look-up-a-custom-element-definition +function lookupCEDefinition(document, namespace, localName, isValue) { + const definition = null; + + if (namespace !== HTML_NS) { + return definition; + } + + if (!document._defaultView) { + return definition; + } + + const registry = implForWrapper(document._defaultView.customElements); + + const definitionByName = registry._customElementDefinitions.find(def => { + return def.name === def.localName && def.localName === localName; + }); + if (definitionByName !== undefined) { + return definitionByName; + } + + const definitionByIs = registry._customElementDefinitions.find(def => { + return def.name === isValue && def.localName === localName; + }); + if (definitionByIs !== undefined) { + return definitionByIs; + } + + return definition; +} + +// https://html.spec.whatwg.org/multipage/custom-elements.html#invoke-custom-element-reactions +function invokeCEReactions(elementQueue) { + for (const element of elementQueue) { + const reactions = element._ceReactionQueue; + + try { + while (reactions.length > 0) { + const reaction = reactions.shift(); + + switch (reaction.type) { + case "upgrade": + upgradeElement(reaction.definition, element); + break; + + case "callback": + reaction.callback.apply(wrapperForImpl(element), reaction.args); + break; + } + } + } catch (error) { + reportException(element._ownerDocument._defaultView, error); + } + } +} + +// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-an-element-on-the-appropriate-element-queue +function enqueueElementOnAppropriateElementQueue(element) { + if (CUSTOM_ELEMENT_REACTIONS_STACK.isEmpty()) { + CUSTOM_ELEMENT_REACTIONS_STACK.backupElementQueue.push(element); + + if (CUSTOM_ELEMENT_REACTIONS_STACK.processingBackupElementQueue) { + return; + } + + CUSTOM_ELEMENT_REACTIONS_STACK.processingBackupElementQueue = true; + + Promise.resolve().then(() => { + const elementQueue = CUSTOM_ELEMENT_REACTIONS_STACK.backupElementQueue; + invokeCEReactions(elementQueue); + + CUSTOM_ELEMENT_REACTIONS_STACK.processingBackupElementQueue = false; + }); + } else { + CUSTOM_ELEMENT_REACTIONS_STACK.currentElementQueue.push(element); + } +} + +// https://html.spec.whatwg.org/multipage/custom-elements.html#enqueue-a-custom-element-callback-reaction +function enqueueCECallbackReaction(element, callbackName, args) { + const { _ceDefinition: { lifecycleCallbacks, observedAttributes } } = element; + + const callback = lifecycleCallbacks[callbackName]; + if (callback === null) { + return; + } + + if (callbackName === "attributeChangedCallback") { + const attributeName = args[0]; + if (!observedAttributes.includes(attributeName)) { + return; + } + } + + element._ceReactionQueue.push({ + type: "callback", + callback, + args + }); + + enqueueElementOnAppropriateElementQueue(element); +} + +// https://html.spec.whatwg.org/#enqueue-a-custom-element-upgrade-reaction +function enqueueCEUpgradeReaction(element, definition) { + element._ceReactionQueue.push({ + type: "upgrade", + definition + }); + + enqueueElementOnAppropriateElementQueue(element); } module.exports = { - isValidCustomElementName + CUSTOM_ELEMENT_STATE, + CUSTOM_ELEMENT_REACTIONS_STACK, + + ceReactionsPreSteps, + ceReactionsPostSteps, + + isValidCustomElementName, + + upgradeElement, + tryUpgradeElement, + + lookupCEDefinition, + enqueueCEUpgradeReaction, + enqueueCECallbackReaction, + invokeCEReactions }; diff --git a/lib/jsdom/living/helpers/html-constructor.js b/lib/jsdom/living/helpers/html-constructor.js new file mode 100644 index 0000000000..771dcd0f46 --- /dev/null +++ b/lib/jsdom/living/helpers/html-constructor.js @@ -0,0 +1,81 @@ +/* eslint-disable global-require */ + +"use strict"; + +const { HTML_NS } = require("./namespaces"); +const { CUSTOM_ELEMENT_STATE } = require("./custom-elements"); +const { createElement, getValidTagNames } = require("./create-element"); + +const { implForWrapper, wrapperForImpl } = require("../generated/utils"); + +// https://html.spec.whatwg.org/multipage/custom-elements.html#concept-already-constructed-marker +const ALREADY_CONSTRUCTED_MARKER = Symbol("already-constructed-marker"); + +// https://html.spec.whatwg.org/multipage/dom.html#htmlconstructor +function HTMLConstructor(globalObject, constructorName, newTarget) { + const registry = implForWrapper(globalObject.customElements); + if (newTarget === HTMLConstructor) { + throw new TypeError("Invalid constructor"); + } + + const definition = registry._customElementDefinitions.find(entry => entry.ctor === newTarget); + if (definition === undefined) { + throw new TypeError("Invalid constructor, the constructor is not part of the custom element registry"); + } + + let isValue = null; + + if (definition.localName === definition.name) { + if (constructorName !== "HTMLElement") { + throw new TypeError("Invalid constructor, autonomous custom element should extend from HTMLElement"); + } + } else { + const validLocalNames = getValidTagNames(HTML_NS, constructorName); + if (!validLocalNames.includes(definition.localName)) { + throw new TypeError(`${definition.localName} is not valid local name for ${constructorName}`); + } + + isValue = definition.name; + } + + let { prototype } = newTarget; + + if (prototype === null || typeof prototype !== "object") { + // The following line deviates from the specification. The HTMLElement prototype should be retrieved from the realm + // associated with the "new.target". Because it is impossible to get such information in jsdom, we fallback to the + // HTMLElement prototype associated with the current object. + prototype = globalObject.HTMLElement.prototype; + } + + if (definition.constructionStack.length === 0) { + const documentImpl = implForWrapper(globalObject.document); + + const elementImpl = createElement(documentImpl, definition.localName, HTML_NS); + + const element = wrapperForImpl(elementImpl); + Object.setPrototypeOf(element, prototype); + + elementImpl._ceState = CUSTOM_ELEMENT_STATE.CUSTOM; + elementImpl._ceDefinition = definition; + elementImpl._isValue = isValue; + + return element; + } + + const elementImpl = definition.constructionStack[definition.constructionStack.length - 1]; + const element = wrapperForImpl(elementImpl); + + if (elementImpl === ALREADY_CONSTRUCTED_MARKER) { + throw new TypeError("This instance is already constructed"); + } + + Object.setPrototypeOf(element, prototype); + + definition.constructionStack[definition.constructionStack.length - 1] = ALREADY_CONSTRUCTED_MARKER; + + return element; +} + +module.exports = { + HTMLConstructor +}; diff --git a/lib/jsdom/living/interfaces.js b/lib/jsdom/living/interfaces.js index 131b020515..f9b9857461 100644 --- a/lib/jsdom/living/interfaces.js +++ b/lib/jsdom/living/interfaces.js @@ -1,186 +1,187 @@ /* eslint-disable global-require */ "use strict"; -const registerElements = require("./register-elements"); + const style = require("../level2/style"); const xpath = require("../level3/xpath"); const nodeFilter = require("./node-filter"); -const generatedInterfaces = [ - require("domexception/webidl2js-wrapper"), - - require("whatwg-url/webidl2js-wrapper").URL, - require("whatwg-url/webidl2js-wrapper").URLSearchParams, - - require("./generated/EventTarget"), - - require("./generated/NamedNodeMap"), - require("./generated/Attr"), - require("./generated/Node"), - require("./generated/Element"), - require("./generated/DocumentFragment"), - require("./generated/Document"), - require("./generated/XMLDocument"), - require("./generated/CharacterData"), - require("./generated/Text"), - require("./generated/CDATASection"), - require("./generated/ProcessingInstruction"), - require("./generated/Comment"), - require("./generated/DocumentType"), - require("./generated/DOMImplementation"), - require("./generated/NodeList"), - require("./generated/HTMLCollection"), - require("./generated/HTMLOptionsCollection"), - require("./generated/DOMStringMap"), - require("./generated/DOMTokenList"), - - require("./generated/HTMLElement.js"), - require("./generated/HTMLHeadElement.js"), - require("./generated/HTMLTitleElement.js"), - require("./generated/HTMLBaseElement.js"), - require("./generated/HTMLLinkElement.js"), - require("./generated/HTMLMetaElement.js"), - require("./generated/HTMLStyleElement.js"), - require("./generated/HTMLBodyElement.js"), - require("./generated/HTMLHeadingElement.js"), - require("./generated/HTMLParagraphElement.js"), - require("./generated/HTMLHRElement.js"), - require("./generated/HTMLPreElement.js"), - require("./generated/HTMLUListElement.js"), - require("./generated/HTMLOListElement.js"), - require("./generated/HTMLLIElement.js"), - require("./generated/HTMLMenuElement.js"), - require("./generated/HTMLDListElement.js"), - require("./generated/HTMLDivElement.js"), - require("./generated/HTMLAnchorElement.js"), - require("./generated/HTMLAreaElement.js"), - require("./generated/HTMLBRElement.js"), - require("./generated/HTMLButtonElement.js"), - require("./generated/HTMLCanvasElement.js"), - require("./generated/HTMLDataElement.js"), - require("./generated/HTMLDataListElement.js"), - require("./generated/HTMLDetailsElement.js"), - require("./generated/HTMLDialogElement.js"), - require("./generated/HTMLDirectoryElement.js"), - require("./generated/HTMLFieldSetElement.js"), - require("./generated/HTMLFontElement.js"), - require("./generated/HTMLFormElement.js"), - require("./generated/HTMLHtmlElement.js"), - require("./generated/HTMLImageElement.js"), - require("./generated/HTMLInputElement.js"), - require("./generated/HTMLLabelElement.js"), - require("./generated/HTMLLegendElement.js"), - require("./generated/HTMLMapElement.js"), - require("./generated/HTMLMarqueeElement.js"), - require("./generated/HTMLMediaElement.js"), - require("./generated/HTMLMeterElement.js"), - require("./generated/HTMLModElement.js"), - require("./generated/HTMLOptGroupElement.js"), - require("./generated/HTMLOptionElement.js"), - require("./generated/HTMLOutputElement.js"), - require("./generated/HTMLPictureElement.js"), - require("./generated/HTMLProgressElement.js"), - require("./generated/HTMLQuoteElement.js"), - require("./generated/HTMLScriptElement.js"), - require("./generated/HTMLSelectElement.js"), - require("./generated/HTMLSlotElement.js"), - require("./generated/HTMLSourceElement.js"), - require("./generated/HTMLSpanElement.js"), - require("./generated/HTMLTableCaptionElement.js"), - require("./generated/HTMLTableCellElement.js"), - require("./generated/HTMLTableColElement.js"), - require("./generated/HTMLTableElement.js"), - require("./generated/HTMLTimeElement.js"), - require("./generated/HTMLTableRowElement.js"), - require("./generated/HTMLTableSectionElement.js"), - require("./generated/HTMLTemplateElement.js"), - require("./generated/HTMLTextAreaElement.js"), - require("./generated/HTMLUnknownElement.js"), - require("./generated/HTMLFrameElement.js"), - require("./generated/HTMLFrameSetElement.js"), - require("./generated/HTMLIFrameElement.js"), - require("./generated/HTMLEmbedElement.js"), - require("./generated/HTMLObjectElement.js"), - require("./generated/HTMLParamElement.js"), - require("./generated/HTMLVideoElement.js"), - require("./generated/HTMLAudioElement.js"), - require("./generated/HTMLTrackElement.js"), - - require("./generated/SVGElement.js"), - require("./generated/SVGGraphicsElement.js"), - require("./generated/SVGSVGElement.js"), - require("./generated/SVGTitleElement.js"), - require("./generated/SVGAnimatedString"), - require("./generated/SVGNumber"), - require("./generated/SVGStringList"), - - require("./generated/Event"), - require("./generated/CloseEvent"), - require("./generated/CustomEvent"), - require("./generated/MessageEvent"), - require("./generated/ErrorEvent"), - require("./generated/HashChangeEvent"), - require("./generated/PopStateEvent"), - require("./generated/StorageEvent"), - require("./generated/ProgressEvent"), - require("./generated/PageTransitionEvent"), - - require("./generated/UIEvent"), - require("./generated/FocusEvent"), - require("./generated/InputEvent"), - require("./generated/MouseEvent"), - require("./generated/KeyboardEvent"), - require("./generated/TouchEvent"), - require("./generated/CompositionEvent"), - require("./generated/WheelEvent"), - - require("./generated/BarProp"), - require("./generated/External"), - require("./generated/Location"), - require("./generated/History"), - require("./generated/Screen"), - require("./generated/Performance"), - require("./generated/Navigator"), - - require("./generated/PluginArray"), - require("./generated/MimeTypeArray"), - require("./generated/Plugin"), - require("./generated/MimeType"), - - require("./generated/FileReader"), - require("./generated/Blob"), - require("./generated/File"), - require("./generated/FileList"), - require("./generated/ValidityState"), - - require("./generated/DOMParser"), - require("./generated/XMLSerializer"), - - require("./generated/FormData"), - require("./generated/XMLHttpRequestEventTarget"), - require("./generated/XMLHttpRequestUpload"), - require("./generated/XMLHttpRequest"), - require("./generated/WebSocket"), - - require("./generated/NodeIterator"), - require("./generated/TreeWalker"), - - require("./generated/AbstractRange"), - require("./generated/Range"), - require("./generated/StaticRange"), - require("./generated/Selection"), - - require("./generated/Storage"), - - require("./generated/ShadowRoot"), - - require("./generated/MutationObserver"), - require("./generated/MutationRecord"), - - require("./generated/Headers"), - require("./generated/AbortController"), - require("./generated/AbortSignal") -]; +const generatedInterfaces = { + DOMException: require("domexception/webidl2js-wrapper"), + + URL: require("whatwg-url/webidl2js-wrapper").URL, + URLSearchParams: require("whatwg-url/webidl2js-wrapper").URLSearchParams, + + EventTarget: require("./generated/EventTarget"), + + NamedNodeMap: require("./generated/NamedNodeMap"), + Attr: require("./generated/Attr"), + Node: require("./generated/Node"), + Element: require("./generated/Element"), + DocumentFragment: require("./generated/DocumentFragment"), + Document: require("./generated/Document"), + XMLDocument: require("./generated/XMLDocument"), + CharacterData: require("./generated/CharacterData"), + Text: require("./generated/Text"), + CDATASection: require("./generated/CDATASection"), + ProcessingInstruction: require("./generated/ProcessingInstruction"), + Comment: require("./generated/Comment"), + DocumentType: require("./generated/DocumentType"), + DOMImplementation: require("./generated/DOMImplementation"), + NodeList: require("./generated/NodeList"), + HTMLCollection: require("./generated/HTMLCollection"), + HTMLOptionsCollection: require("./generated/HTMLOptionsCollection"), + DOMStringMap: require("./generated/DOMStringMap"), + DOMTokenList: require("./generated/DOMTokenList"), + + HTMLElement: require("./generated/HTMLElement.js"), + HTMLHeadElement: require("./generated/HTMLHeadElement.js"), + HTMLTitleElement: require("./generated/HTMLTitleElement.js"), + HTMLBaseElement: require("./generated/HTMLBaseElement.js"), + HTMLLinkElement: require("./generated/HTMLLinkElement.js"), + HTMLMetaElement: require("./generated/HTMLMetaElement.js"), + HTMLStyleElement: require("./generated/HTMLStyleElement.js"), + HTMLBodyElement: require("./generated/HTMLBodyElement.js"), + HTMLHeadingElement: require("./generated/HTMLHeadingElement.js"), + HTMLParagraphElement: require("./generated/HTMLParagraphElement.js"), + HTMLHRElement: require("./generated/HTMLHRElement.js"), + HTMLPreElement: require("./generated/HTMLPreElement.js"), + HTMLUListElement: require("./generated/HTMLUListElement.js"), + HTMLOListElement: require("./generated/HTMLOListElement.js"), + HTMLLIElement: require("./generated/HTMLLIElement.js"), + HTMLMenuElement: require("./generated/HTMLMenuElement.js"), + HTMLDListElement: require("./generated/HTMLDListElement.js"), + HTMLDivElement: require("./generated/HTMLDivElement.js"), + HTMLAnchorElement: require("./generated/HTMLAnchorElement.js"), + HTMLAreaElement: require("./generated/HTMLAreaElement.js"), + HTMLBRElement: require("./generated/HTMLBRElement.js"), + HTMLButtonElement: require("./generated/HTMLButtonElement.js"), + HTMLCanvasElement: require("./generated/HTMLCanvasElement.js"), + HTMLDataElement: require("./generated/HTMLDataElement.js"), + HTMLDataListElement: require("./generated/HTMLDataListElement.js"), + HTMLDetailsElement: require("./generated/HTMLDetailsElement.js"), + HTMLDialogElement: require("./generated/HTMLDialogElement.js"), + HTMLDirectoryElement: require("./generated/HTMLDirectoryElement.js"), + HTMLFieldSetElement: require("./generated/HTMLFieldSetElement.js"), + HTMLFontElement: require("./generated/HTMLFontElement.js"), + HTMLFormElement: require("./generated/HTMLFormElement.js"), + HTMLHtmlElement: require("./generated/HTMLHtmlElement.js"), + HTMLImageElement: require("./generated/HTMLImageElement.js"), + HTMLInputElement: require("./generated/HTMLInputElement.js"), + HTMLLabelElement: require("./generated/HTMLLabelElement.js"), + HTMLLegendElement: require("./generated/HTMLLegendElement.js"), + HTMLMapElement: require("./generated/HTMLMapElement.js"), + HTMLMarqueeElement: require("./generated/HTMLMarqueeElement.js"), + HTMLMediaElement: require("./generated/HTMLMediaElement.js"), + HTMLMeterElement: require("./generated/HTMLMeterElement.js"), + HTMLModElement: require("./generated/HTMLModElement.js"), + HTMLOptGroupElement: require("./generated/HTMLOptGroupElement.js"), + HTMLOptionElement: require("./generated/HTMLOptionElement.js"), + HTMLOutputElement: require("./generated/HTMLOutputElement.js"), + HTMLPictureElement: require("./generated/HTMLPictureElement.js"), + HTMLProgressElement: require("./generated/HTMLProgressElement.js"), + HTMLQuoteElement: require("./generated/HTMLQuoteElement.js"), + HTMLScriptElement: require("./generated/HTMLScriptElement.js"), + HTMLSelectElement: require("./generated/HTMLSelectElement.js"), + HTMLSlotElement: require("./generated/HTMLSlotElement.js"), + HTMLSourceElement: require("./generated/HTMLSourceElement.js"), + HTMLSpanElement: require("./generated/HTMLSpanElement.js"), + HTMLTableCaptionElement: require("./generated/HTMLTableCaptionElement.js"), + HTMLTableCellElement: require("./generated/HTMLTableCellElement.js"), + HTMLTableColElement: require("./generated/HTMLTableColElement.js"), + HTMLTableElement: require("./generated/HTMLTableElement.js"), + HTMLTimeElement: require("./generated/HTMLTimeElement.js"), + HTMLTableRowElement: require("./generated/HTMLTableRowElement.js"), + HTMLTableSectionElement: require("./generated/HTMLTableSectionElement.js"), + HTMLTemplateElement: require("./generated/HTMLTemplateElement.js"), + HTMLTextAreaElement: require("./generated/HTMLTextAreaElement.js"), + HTMLUnknownElement: require("./generated/HTMLUnknownElement.js"), + HTMLFrameElement: require("./generated/HTMLFrameElement.js"), + HTMLFrameSetElement: require("./generated/HTMLFrameSetElement.js"), + HTMLIFrameElement: require("./generated/HTMLIFrameElement.js"), + HTMLEmbedElement: require("./generated/HTMLEmbedElement.js"), + HTMLObjectElement: require("./generated/HTMLObjectElement.js"), + HTMLParamElement: require("./generated/HTMLParamElement.js"), + HTMLVideoElement: require("./generated/HTMLVideoElement.js"), + HTMLAudioElement: require("./generated/HTMLAudioElement.js"), + HTMLTrackElement: require("./generated/HTMLTrackElement.js"), + + SVGElement: require("./generated/SVGElement.js"), + SVGGraphicsElement: require("./generated/SVGGraphicsElement.js"), + SVGSVGElement: require("./generated/SVGSVGElement.js"), + SVGTitleElement: require("./generated/SVGTitleElement.js"), + SVGAnimatedString: require("./generated/SVGAnimatedString"), + SVGNumber: require("./generated/SVGNumber"), + SVGStringList: require("./generated/SVGStringList"), + + Event: require("./generated/Event"), + CloseEvent: require("./generated/CloseEvent"), + CustomEvent: require("./generated/CustomEvent"), + MessageEvent: require("./generated/MessageEvent"), + ErrorEvent: require("./generated/ErrorEvent"), + HashChangeEvent: require("./generated/HashChangeEvent"), + PopStateEvent: require("./generated/PopStateEvent"), + StorageEvent: require("./generated/StorageEvent"), + ProgressEvent: require("./generated/ProgressEvent"), + PageTransitionEvent: require("./generated/PageTransitionEvent"), + + UIEvent: require("./generated/UIEvent"), + FocusEvent: require("./generated/FocusEvent"), + InputEvent: require("./generated/InputEvent"), + MouseEvent: require("./generated/MouseEvent"), + KeyboardEvent: require("./generated/KeyboardEvent"), + TouchEvent: require("./generated/TouchEvent"), + CompositionEvent: require("./generated/CompositionEvent"), + WheelEvent: require("./generated/WheelEvent"), + + BarProp: require("./generated/BarProp"), + External: require("./generated/External"), + Location: require("./generated/Location"), + History: require("./generated/History"), + Screen: require("./generated/Screen"), + Performance: require("./generated/Performance"), + Navigator: require("./generated/Navigator"), + + PluginArray: require("./generated/PluginArray"), + MimeTypeArray: require("./generated/MimeTypeArray"), + Plugin: require("./generated/Plugin"), + MimeType: require("./generated/MimeType"), + + FileReader: require("./generated/FileReader"), + Blob: require("./generated/Blob"), + File: require("./generated/File"), + FileList: require("./generated/FileList"), + ValidityState: require("./generated/ValidityState"), + + DOMParser: require("./generated/DOMParser"), + XMLSerializer: require("./generated/XMLSerializer"), + + FormData: require("./generated/FormData"), + XMLHttpRequestEventTarget: require("./generated/XMLHttpRequestEventTarget"), + XMLHttpRequestUpload: require("./generated/XMLHttpRequestUpload"), + XMLHttpRequest: require("./generated/XMLHttpRequest"), + WebSocket: require("./generated/WebSocket"), + + NodeIterator: require("./generated/NodeIterator"), + TreeWalker: require("./generated/TreeWalker"), + + AbstractRange: require("./generated/AbstractRange"), + Range: require("./generated/Range"), + StaticRange: require("./generated/StaticRange"), + Selection: require("./generated/Selection"), + + Storage: require("./generated/Storage"), + + CustomElementRegistry: require("./generated/CustomElementRegistry"), + ShadowRoot: require("./generated/ShadowRoot"), + + MutationObserver: require("./generated/MutationObserver"), + MutationRecord: require("./generated/MutationRecord"), + + Headers: require("./generated/Headers"), + AbortController: require("./generated/AbortController"), + AbortSignal: require("./generated/AbortSignal") +}; function install(window, name, interfaceConstructor) { Object.defineProperty(window, name, { @@ -192,7 +193,7 @@ function install(window, name, interfaceConstructor) { exports.installInterfaces = function (window) { // Install generated interface. - for (const generatedInterface of generatedInterfaces) { + for (const generatedInterface of Object.values(generatedInterfaces)) { generatedInterface.install(window); } @@ -200,9 +201,6 @@ exports.installInterfaces = function (window) { // https://html.spec.whatwg.org/#htmldocument install(window, "HTMLDocument", window.Document); - // Install Elements - registerElements(window); - // These need to be cleaned up... style.addToCore(window); xpath(window); @@ -210,3 +208,8 @@ exports.installInterfaces = function (window) { // This one is OK but needs migration to webidl2js eventually. nodeFilter(window); }; + +/** Returns an interface webidl2js wrapper given its an interface name. */ +exports.getInterfaceWrapper = function (name) { + return generatedInterfaces[name]; +}; diff --git a/lib/jsdom/living/node.js b/lib/jsdom/living/node.js index 34640b455e..57623d3503 100644 --- a/lib/jsdom/living/node.js +++ b/lib/jsdom/living/node.js @@ -1,10 +1,14 @@ "use strict"; + const attributes = require("./attributes"); -const { cloningSteps, domSymbolTree } = require("./helpers/internal-constants"); const NODE_TYPE = require("./node-type"); + const orderedSetParse = require("./helpers/ordered-set").parse; -const { asciiCaseInsensitiveMatch, asciiLowercase } = require("./helpers/strings"); +const { createElement } = require("./helpers/create-element"); const { HTML_NS, XMLNS_NS } = require("./helpers/namespaces"); +const { cloningSteps, domSymbolTree } = require("./helpers/internal-constants"); +const { asciiCaseInsensitiveMatch, asciiLowercase } = require("./helpers/strings"); + const HTMLCollection = require("./generated/HTMLCollection"); module.exports.clone = function (node, document, cloneChildren) { @@ -29,8 +33,7 @@ module.exports.clone = function (node, document, cloneChildren) { break; case NODE_TYPE.ELEMENT_NODE: - copy = document._createElementWithCorrectElementInterface(node._localName, node._namespaceURI); - copy._prefix = node._prefix; + copy = createElement(document, node._localName, node._namespaceURI, node._prefix, node._isValue, false); attributes.copyAttributeList(node, copy); break; diff --git a/lib/jsdom/living/nodes/DOMImplementation-impl.js b/lib/jsdom/living/nodes/DOMImplementation-impl.js index 68285f896f..49fb11bc70 100644 --- a/lib/jsdom/living/nodes/DOMImplementation-impl.js +++ b/lib/jsdom/living/nodes/DOMImplementation-impl.js @@ -1,9 +1,11 @@ "use strict"; const validateNames = require("../helpers/validate-names"); +const { HTML_NS, SVG_NS } = require("../helpers/namespaces"); +const { createElement, createElementNS } = require("../helpers/create-element"); + const DocumentType = require("../generated/DocumentType"); const Document = require("../generated/Document"); -const { HTML_NS, SVG_NS } = require("../helpers/namespaces"); class DOMImplementationImpl { constructor(globalObject, args, privateData) { @@ -26,6 +28,7 @@ class DOMImplementationImpl { }); } + // https://dom.spec.whatwg.org/#dom-domimplementation-createdocument createDocument(namespace, qualifiedName, doctype) { let contentType = "application/xml"; @@ -41,7 +44,7 @@ class DOMImplementationImpl { let element = null; if (qualifiedName !== "") { - element = document.createElementNS(namespace, qualifiedName); + element = createElementNS(document, namespace, qualifiedName, {}); } if (doctype !== null) { @@ -57,6 +60,7 @@ class DOMImplementationImpl { return document; } + // https://dom.spec.whatwg.org/#dom-domimplementation-createhtmldocument createHTMLDocument(title) { // Let doc be a new document that is an HTML document. // Set doc's content type to "text/html". @@ -76,19 +80,19 @@ class DOMImplementationImpl { document.appendChild(doctype); // Create an html element in the HTML namespace, and append it to doc. - const htmlElement = document.createElementNS(HTML_NS, "html"); + const htmlElement = createElement(document, "html", HTML_NS); document.appendChild(htmlElement); // Create a head element in the HTML namespace, and append it to the html // element created in the previous step. - const headElement = document.createElement("head"); + const headElement = createElement(document, "head", HTML_NS); htmlElement.appendChild(headElement); // If the title argument is not omitted: if (title !== undefined) { // Create a title element in the HTML namespace, and append it to the head // element created in the previous step. - const titleElement = document.createElement("title"); + const titleElement = createElement(document, "title", HTML_NS); headElement.appendChild(titleElement); // Create a Text node, set its data to title (which could be the empty @@ -98,7 +102,8 @@ class DOMImplementationImpl { // Create a body element in the HTML namespace, and append it to the html // element created in the earlier step. - htmlElement.appendChild(document.createElement("body")); + const bodyElement = createElement(document, "body", HTML_NS); + htmlElement.appendChild(bodyElement); // doc's origin is an alias to the origin of the context object's associated // document, and doc's effective script origin is an alias to the effective diff --git a/lib/jsdom/living/nodes/Document-impl.js b/lib/jsdom/living/nodes/Document-impl.js index 166e76402a..1cb07a3801 100644 --- a/lib/jsdom/living/nodes/Document-impl.js +++ b/lib/jsdom/living/nodes/Document-impl.js @@ -25,6 +25,8 @@ const validateName = require("../helpers/validate-names").name; const { validateAndExtract } = require("../helpers/validate-names"); const { fireAnEvent } = require("../helpers/events"); const { shadowIncludingInclusiveDescendantsIterator } = require("../helpers/shadow-dom"); +const { enqueueCECallbackReaction, CUSTOM_ELEMENT_STATE } = require("../helpers/custom-elements"); +const { createElement, createElementNS } = require("../helpers/create-element"); const DocumentOrShadowRootImpl = require("./DocumentOrShadowRoot-impl").implementation; const GlobalEventHandlersImpl = require("./GlobalEventHandlers-impl").implementation; @@ -40,9 +42,6 @@ const CDATASection = require("../generated/CDATASection"); const Text = require("../generated/Text"); const DocumentFragment = require("../generated/DocumentFragment"); const DOMImplementation = require("../generated/DOMImplementation"); -const Element = require("../generated/Element"); -const HTMLUnknownElement = require("../generated/HTMLUnknownElement"); -const SVGElement = require("../generated/SVGElement"); const TreeWalker = require("../generated/TreeWalker"); const NodeIterator = require("../generated/NodeIterator"); const ShadowRoot = require("../generated/ShadowRoot"); @@ -196,6 +195,9 @@ class DocumentImpl extends NodeImpl { // to which the browsing context's session history was most recently traversed. When a Document is created, // it initially has no latest entry. this._latestEntry = null; + + // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter + this._throwOwnDynamicMarkupInsertionCounter = 0; } _getTheParent(event) { @@ -275,32 +277,6 @@ class DocumentImpl extends NodeImpl { return Boolean(this._lastFocusedElement); } - _createElementWithCorrectElementInterface(localName, namespace) { - // https://dom.spec.whatwg.org/#concept-element-interface - - if (this._elementBuilders[namespace] && this._elementBuilders[namespace][localName]) { - return this._elementBuilders[namespace][localName](this, localName, namespace); - } else if (namespace === HTML_NS) { - return HTMLUnknownElement.createImpl(this._globalObject, [], { - ownerDocument: this, - localName, - namespace - }); - } else if (namespace === SVG_NS) { - return SVGElement.createImpl(this._globalObject, [], { - ownerDocument: this, - localName, - namespace - }); - } - - return Element.createImpl(this._globalObject, [], { - ownerDocument: this, - localName, - namespace - }); - } - _descendantRemoved(parent, child) { if (child.tagName === "STYLE") { const index = this.styleSheets.indexOf(child.sheet); @@ -325,6 +301,13 @@ class DocumentImpl extends NodeImpl { ]); } + if (this._throwOwnDynamicMarkupInsertionCounter > 0) { + throw DOMException.create(this._globalObject, [ + "Cannot use document.write while an element upgrades", + "InvalidStateError" + ]); + } + if (this._writeAfterElement) { // If called from an script element directly (during the first tick), // the new elements are inserted right after that element. @@ -714,26 +697,27 @@ class DocumentImpl extends NodeImpl { }); } - createElement(localName) { + // https://dom.spec.whatwg.org/#dom-document-createelement + createElement(localName, options) { validateName(this._globalObject, localName); + if (this._parsingMode === "html") { localName = asciiLowercase(localName); } + let isValue = null; + if (options && options.is !== undefined) { + isValue = options.is; + } + const namespace = this._parsingMode === "html" || this.contentType === "application/xhtml+xml" ? HTML_NS : null; - return this._createElementWithCorrectElementInterface(localName, namespace); + return createElement(this, localName, namespace, null, isValue, true); } - createElementNS(namespace, qualifiedName) { - namespace = namespace !== null ? String(namespace) : namespace; - - const extracted = validateAndExtract(this._globalObject, namespace, qualifiedName); - - const element = this._createElementWithCorrectElementInterface(extracted.localName, extracted.namespace); - element._prefix = extracted.prefix; - - return element; + // https://dom.spec.whatwg.org/#dom-document-createelementns + createElementNS(namespace, qualifiedName, options) { + return createElementNS(this, namespace, qualifiedName, options); } createDocumentFragment() { @@ -831,6 +815,15 @@ class DocumentImpl extends NodeImpl { inclusiveDescendant._ownerDocument = newDocument; } + for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) { + if (inclusiveDescendant._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) { + enqueueCECallbackReaction(inclusiveDescendant, "adoptedCallback", [ + idlUtils.wrapperForImpl(oldDocument), + idlUtils.wrapperForImpl(newDocument) + ]); + } + } + for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) { if (inclusiveDescendant._adoptingSteps) { inclusiveDescendant._adoptingSteps(oldDocument); @@ -895,8 +888,6 @@ mixin(DocumentImpl.prototype, GlobalEventHandlersImpl.prototype); mixin(DocumentImpl.prototype, NonElementParentNodeImpl.prototype); mixin(DocumentImpl.prototype, ParentNodeImpl.prototype); -DocumentImpl.prototype._elementBuilders = Object.create(null); - DocumentImpl.prototype.getElementsByTagName = memoizeQuery(function (qualifiedName) { return listOfElementsWithQualifiedName(qualifiedName, this); }); diff --git a/lib/jsdom/living/nodes/Document.webidl b/lib/jsdom/living/nodes/Document.webidl index 9c5e0f5fc8..cf01df72c1 100644 --- a/lib/jsdom/living/nodes/Document.webidl +++ b/lib/jsdom/living/nodes/Document.webidl @@ -18,11 +18,8 @@ interface Document : Node { HTMLCollection getElementsByTagNameNS(DOMString? namespace, DOMString localName); HTMLCollection getElementsByClassName(DOMString classNames); -// We don't support the last argument yet -// [CEReactions, NewObject] Element createElement(DOMString localName, optional ElementCreationOptions options); -// [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional ElementCreationOptions options); - [CEReactions, NewObject] Element createElement(DOMString localName); - [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName); + [CEReactions, NewObject] Element createElement(DOMString localName, optional (DOMString or ElementCreationOptions) options); + [CEReactions, NewObject] Element createElementNS(DOMString? namespace, DOMString qualifiedName, optional (DOMString or ElementCreationOptions) options); [NewObject] DocumentFragment createDocumentFragment(); [NewObject] Text createTextNode(DOMString data); [NewObject] CDATASection createCDATASection(DOMString data); diff --git a/lib/jsdom/living/nodes/Element-impl.js b/lib/jsdom/living/nodes/Element-impl.js index 2cd38364a1..d17c14f16c 100644 --- a/lib/jsdom/living/nodes/Element-impl.js +++ b/lib/jsdom/living/nodes/Element-impl.js @@ -25,7 +25,7 @@ const NonDocumentTypeChildNode = require("./NonDocumentTypeChildNode-impl").impl const ShadowRoot = require("../generated/ShadowRoot"); const Text = require("../generated/Text"); const { isValidHostElementName } = require("../helpers/shadow-dom"); -const { isValidCustomElementName } = require("../helpers/custom-elements"); +const { isValidCustomElementName, lookupCEDefinition } = require("../helpers/custom-elements"); function attachId(id, elm, doc) { if (id && elm && doc) { @@ -59,15 +59,19 @@ class ElementImpl extends NodeImpl { this._initSlotableMixin(); - this.nodeType = NODE_TYPE.ELEMENT_NODE; - this.scrollTop = 0; - this.scrollLeft = 0; - - this._namespaceURI = privateData.namespace || null; - this._prefix = null; + this._namespaceURI = privateData.namespace; + this._prefix = privateData.prefix; this._localName = privateData.localName; + this._ceState = privateData.ceState; + this._ceDefinition = privateData.ceDefinition; + this._isValue = privateData.isValue; this._shadowRoot = null; + this._ceReactionQueue = []; + + this.nodeType = NODE_TYPE.ELEMENT_NODE; + this.scrollTop = 0; + this.scrollLeft = 0; this._attributeList = []; // Used for caching. @@ -382,6 +386,8 @@ class ElementImpl extends NodeImpl { // https://dom.spec.whatwg.org/#dom-element-attachshadow attachShadow(init) { + const { _ownerDocument, _namespaceURI, _localName, _isValue } = this; + if (this.namespaceURI !== HTML_NS) { throw DOMException.create(this._globalObject, [ "This element does not support attachShadow. This element is not part of the HTML namespace.", @@ -389,12 +395,23 @@ class ElementImpl extends NodeImpl { ]); } - if (!isValidHostElementName(this.localName) && !isValidCustomElementName(this.localName)) { + if (!isValidHostElementName(_localName) && !isValidCustomElementName(_localName)) { const message = "This element does not support attachShadow. This element is not a custom element nor " + "a standard element supporting a shadow root."; throw DOMException.create(this._globalObject, [message, "NotSupportedError"]); } + if (isValidCustomElementName(_localName) || _isValue) { + const definition = lookupCEDefinition(_ownerDocument, _namespaceURI, _localName, _isValue); + + if (definition && definition.disableShadow) { + throw DOMException.create(this._globalObject, [ + "Shadow root cannot be create on a custom element with disabled shadow", + "NotSupportedError" + ]); + } + } + if (this._shadowRoot !== null) { throw DOMException.create(this._globalObject, [ "Shadow root cannot be created on a host which already hosts a shadow tree.", diff --git a/lib/jsdom/living/nodes/HTMLFrameElement-impl.js b/lib/jsdom/living/nodes/HTMLFrameElement-impl.js index cf03e0e8a5..f835d14df4 100644 --- a/lib/jsdom/living/nodes/HTMLFrameElement-impl.js +++ b/lib/jsdom/living/nodes/HTMLFrameElement-impl.js @@ -5,7 +5,7 @@ const whatwgEncoding = require("whatwg-encoding"); const { parseURL, serializeURL } = require("whatwg-url"); const sniffHTMLEncoding = require("html-encoding-sniffer"); -const Window = require("../../browser/Window"); +const window = require("../../browser/Window"); const HTMLElementImpl = require("./HTMLElement-impl").implementation; const { evaluateJavaScriptURL } = require("../window/navigation"); const { reflectURLAttribute } = require("../../utils"); @@ -126,7 +126,7 @@ function loadFrame(frame, attaching) { } const serializedURL = serializeURL(url); - const wnd = new Window({ + const wnd = window.createWindow({ parsingMode: "html", url: url.scheme === "javascript" || serializedURL === "about:blank" ? parentDoc.URL : serializedURL, resourceLoader: parentDoc._defaultView._resourceLoader, @@ -187,22 +187,22 @@ function loadFrame(frame, attaching) { } function refreshAccessors(document) { - const window = document._defaultView; + const { _defaultView } = document; - if (!window) { + if (!_defaultView) { return; } const frames = document.querySelectorAll("iframe,frame"); // delete accessors for all frames - for (let i = 0; i < window._length; ++i) { - delete window[i]; + for (let i = 0; i < _defaultView._length; ++i) { + delete _defaultView[i]; } - window._length = frames.length; + _defaultView._length = frames.length; Array.prototype.forEach.call(frames, (frame, i) => { - Object.defineProperty(window, i, { + Object.defineProperty(_defaultView, i, { configurable: true, enumerable: true, get() { diff --git a/lib/jsdom/living/nodes/Node-impl.js b/lib/jsdom/living/nodes/Node-impl.js index 4e5bf989f6..ccdc6bceba 100644 --- a/lib/jsdom/living/nodes/Node-impl.js +++ b/lib/jsdom/living/nodes/Node-impl.js @@ -3,11 +3,11 @@ const DOMException = require("domexception/webidl2js-wrapper"); const EventTargetImpl = require("../events/EventTarget-impl").implementation; -const { simultaneousIterators } = require("../../utils"); const NODE_TYPE = require("../node-type"); const NODE_DOCUMENT_POSITION = require("../node-document-position"); const { clone, locateNamespacePrefix, locateNamespace } = require("../node"); const attributes = require("../attributes"); +const { simultaneousIterators } = require("../../utils"); const NodeList = require("../generated/NodeList"); @@ -15,9 +15,10 @@ const { nodeRoot, nodeLength } = require("../helpers/node"); const { domSymbolTree } = require("../helpers/internal-constants"); const { documentBaseURLSerialized } = require("../helpers/document-base-url"); const { queueTreeMutationRecord } = require("../helpers/mutation-observers"); +const { CUSTOM_ELEMENT_STATE, enqueueCECallbackReaction, tryUpgradeElement } = require("../helpers/custom-elements"); const { - isShadowRoot, shadowIncludingRoot, assignSlot, assignSlotableForTree, assignSlotable, - signalSlotChange, isSlot + isShadowRoot, shadowIncludingRoot, assignSlot, assignSlotableForTree, assignSlotable, signalSlotChange, isSlot, + shadowIncludingInclusiveDescendantsIterator, shadowIncludingDescendantsIterator } = require("../helpers/shadow-dom"); function isObsoleteNodeType(node) { @@ -788,6 +789,16 @@ class NodeImpl extends EventTargetImpl { } this._descendantAdded(this, node); + + for (const inclusiveDescendant of shadowIncludingInclusiveDescendantsIterator(node)) { + if (inclusiveDescendant.isConnected) { + if (inclusiveDescendant._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) { + enqueueCECallbackReaction(inclusiveDescendant, "connectedCallback", []); + } else { + tryUpgradeElement(inclusiveDescendant); + } + } + } } if (!suppressObservers) { @@ -1070,6 +1081,18 @@ class NodeImpl extends EventTargetImpl { nodeImpl._detach(); this._descendantRemoved(this, nodeImpl); + if (this.isConnected) { + if (nodeImpl._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) { + enqueueCECallbackReaction(nodeImpl, "disconnectedCallback", []); + } + + for (const descendantImpl of shadowIncludingDescendantsIterator(nodeImpl)) { + if (descendantImpl._ceState === CUSTOM_ELEMENT_STATE.CUSTOM) { + enqueueCECallbackReaction(descendantImpl, "disconnectedCallback", []); + } + } + } + if (!suppressObservers) { queueTreeMutationRecord(this, [], [nodeImpl], oldPreviousSiblingImpl, oldNextSiblingImpl); } diff --git a/lib/jsdom/living/range/Range-impl.js b/lib/jsdom/living/range/Range-impl.js index e9c4d6a4d6..0cd9f9d7fa 100644 --- a/lib/jsdom/living/range/Range-impl.js +++ b/lib/jsdom/living/range/Range-impl.js @@ -10,6 +10,7 @@ const { HTML_NS } = require("../helpers/namespaces"); const { domSymbolTree } = require("../helpers/internal-constants"); const { compareBoundaryPointsPosition } = require("./boundary-point"); const { nodeRoot, nodeLength, isInclusiveAncestor } = require("../helpers/node"); +const { createElement } = require("../helpers/create-element"); const AbstractRangeImpl = require("./AbstractRange-impl").implementation; @@ -439,7 +440,7 @@ class RangeImpl extends AbstractRangeImpl { element._namespaceURI === HTML_NS ) ) { - element = node._ownerDocument._createElementWithCorrectElementInterface("body", HTML_NS); + element = createElement(node._ownerDocument, "body", HTML_NS); } return parseFragment(fragment, element); diff --git a/lib/jsdom/living/register-elements.js b/lib/jsdom/living/register-elements.js deleted file mode 100644 index 5575c21d8e..0000000000 --- a/lib/jsdom/living/register-elements.js +++ /dev/null @@ -1,386 +0,0 @@ -"use strict"; -/* eslint global-require: 0 */ - -const DocumentImpl = require("./nodes/Document-impl.js"); - -const mappings = { - // https://html.spec.whatwg.org/multipage/dom.html#elements-in-the-dom%3Aelement-interface - // https://html.spec.whatwg.org/multipage/indices.html#elements-3 - "http://www.w3.org/1999/xhtml": { - HTMLElement: { - file: require("./generated/HTMLElement.js"), - tags: [ - "abbr", - "acronym", - "address", - "article", - "aside", - "b", - "basefont", - "bdi", - "bdo", - "big", - "center", - "cite", - "code", - "dd", - "dfn", - "dt", - "em", - "figcaption", - "figure", - "footer", - "header", - "hgroup", - "i", - "kbd", - "main", - "mark", - "nav", - "nobr", - "noembed", - "noframes", - "noscript", - "plaintext", - "rb", - "rp", - "rt", - "rtc", - "ruby", - "s", - "samp", - "section", - "small", - "strike", - "strong", - "sub", - "summary", - "sup", - "tt", - "u", - "var", - "wbr" - ] - }, - HTMLAnchorElement: { - file: require("./generated/HTMLAnchorElement.js"), - tags: ["a"] - }, - HTMLAreaElement: { - file: require("./generated/HTMLAreaElement.js"), - tags: ["area"] - }, - HTMLAudioElement: { - file: require("./generated/HTMLAudioElement.js"), - tags: ["audio"] - }, - HTMLBaseElement: { - file: require("./generated/HTMLBaseElement.js"), - tags: ["base"] - }, - HTMLBodyElement: { - file: require("./generated/HTMLBodyElement.js"), - tags: ["body"] - }, - HTMLBRElement: { - file: require("./generated/HTMLBRElement.js"), - tags: ["br"] - }, - HTMLButtonElement: { - file: require("./generated/HTMLButtonElement.js"), - tags: ["button"] - }, - HTMLCanvasElement: { - file: require("./generated/HTMLCanvasElement.js"), - tags: ["canvas"] - }, - HTMLDataElement: { - file: require("./generated/HTMLDataElement.js"), - tags: ["data"] - }, - HTMLDataListElement: { - file: require("./generated/HTMLDataListElement.js"), - tags: ["datalist"] - }, - HTMLDetailsElement: { - file: require("./generated/HTMLDetailsElement.js"), - tags: ["details"] - }, - HTMLDialogElement: { - file: require("./generated/HTMLDialogElement.js"), - tags: ["dialog"] - }, - HTMLDirectoryElement: { - file: require("./generated/HTMLDirectoryElement.js"), - tags: ["dir"] - }, - HTMLDivElement: { - file: require("./generated/HTMLDivElement.js"), - tags: ["div"] - }, - HTMLDListElement: { - file: require("./generated/HTMLDListElement.js"), - tags: ["dl"] - }, - HTMLEmbedElement: { - file: require("./generated/HTMLEmbedElement.js"), - tags: ["embed"] - }, - HTMLFieldSetElement: { - file: require("./generated/HTMLFieldSetElement.js"), - tags: ["fieldset"] - }, - HTMLFontElement: { - file: require("./generated/HTMLFontElement.js"), - tags: ["font"] - }, - HTMLFormElement: { - file: require("./generated/HTMLFormElement.js"), - tags: ["form"] - }, - HTMLFrameElement: { - file: require("./generated/HTMLFrameElement.js"), - tags: ["frame"] - }, - HTMLFrameSetElement: { - file: require("./generated/HTMLFrameSetElement.js"), - tags: ["frameset"] - }, - HTMLHeadingElement: { - file: require("./generated/HTMLHeadingElement.js"), - tags: ["h1", "h2", "h3", "h4", "h5", "h6"] - }, - HTMLHeadElement: { - file: require("./generated/HTMLHeadElement.js"), - tags: ["head"] - }, - HTMLHRElement: { - file: require("./generated/HTMLHRElement.js"), - tags: ["hr"] - }, - HTMLHtmlElement: { - file: require("./generated/HTMLHtmlElement.js"), - tags: ["html"] - }, - HTMLIFrameElement: { - file: require("./generated/HTMLIFrameElement.js"), - tags: ["iframe"] - }, - HTMLImageElement: { - file: require("./generated/HTMLImageElement.js"), - tags: ["img"] - }, - HTMLInputElement: { - file: require("./generated/HTMLInputElement.js"), - tags: ["input"] - }, - HTMLLabelElement: { - file: require("./generated/HTMLLabelElement.js"), - tags: ["label"] - }, - HTMLLegendElement: { - file: require("./generated/HTMLLegendElement.js"), - tags: ["legend"] - }, - HTMLLIElement: { - file: require("./generated/HTMLLIElement.js"), - tags: ["li"] - }, - HTMLLinkElement: { - file: require("./generated/HTMLLinkElement.js"), - tags: ["link"] - }, - HTMLMapElement: { - file: require("./generated/HTMLMapElement.js"), - tags: ["map"] - }, - HTMLMarqueeElement: { - file: require("./generated/HTMLMarqueeElement.js"), - tags: ["marquee"] - }, - HTMLMediaElement: { - file: require("./generated/HTMLMediaElement.js"), - tags: [] - }, - HTMLMenuElement: { - file: require("./generated/HTMLMenuElement.js"), - tags: ["menu"] - }, - HTMLMetaElement: { - file: require("./generated/HTMLMetaElement.js"), - tags: ["meta"] - }, - HTMLMeterElement: { - file: require("./generated/HTMLMeterElement.js"), - tags: ["meter"] - }, - HTMLModElement: { - file: require("./generated/HTMLModElement.js"), - tags: ["del", "ins"] - }, - HTMLObjectElement: { - file: require("./generated/HTMLObjectElement.js"), - tags: ["object"] - }, - HTMLOListElement: { - file: require("./generated/HTMLOListElement.js"), - tags: ["ol"] - }, - HTMLOptGroupElement: { - file: require("./generated/HTMLOptGroupElement.js"), - tags: ["optgroup"] - }, - HTMLOptionElement: { - file: require("./generated/HTMLOptionElement.js"), - tags: ["option"] - }, - HTMLOutputElement: { - file: require("./generated/HTMLOutputElement.js"), - tags: ["output"] - }, - HTMLParagraphElement: { - file: require("./generated/HTMLParagraphElement.js"), - tags: ["p"] - }, - HTMLParamElement: { - file: require("./generated/HTMLParamElement.js"), - tags: ["param"] - }, - HTMLPictureElement: { - file: require("./generated/HTMLPictureElement.js"), - tags: ["picture"] - }, - HTMLPreElement: { - file: require("./generated/HTMLPreElement.js"), - tags: ["listing", "pre", "xmp"] - }, - HTMLProgressElement: { - file: require("./generated/HTMLProgressElement.js"), - tags: ["progress"] - }, - HTMLQuoteElement: { - file: require("./generated/HTMLQuoteElement.js"), - tags: ["blockquote", "q"] - }, - HTMLScriptElement: { - file: require("./generated/HTMLScriptElement.js"), - tags: ["script"] - }, - HTMLSelectElement: { - file: require("./generated/HTMLSelectElement.js"), - tags: ["select"] - }, - HTMLSlotElement: { - file: require("./generated/HTMLSlotElement.js"), - tags: ["slot"] - }, - HTMLSourceElement: { - file: require("./generated/HTMLSourceElement.js"), - tags: ["source"] - }, - HTMLSpanElement: { - file: require("./generated/HTMLSpanElement.js"), - tags: ["span"] - }, - HTMLStyleElement: { - file: require("./generated/HTMLStyleElement.js"), - tags: ["style"] - }, - HTMLTableCaptionElement: { - file: require("./generated/HTMLTableCaptionElement.js"), - tags: ["caption"] - }, - HTMLTableCellElement: { - file: require("./generated/HTMLTableCellElement.js"), - tags: ["th", "td"] - }, - HTMLTableColElement: { - file: require("./generated/HTMLTableColElement.js"), - tags: ["col", "colgroup"] - }, - HTMLTableElement: { - file: require("./generated/HTMLTableElement.js"), - tags: ["table"] - }, - HTMLTimeElement: { - file: require("./generated/HTMLTimeElement.js"), - tags: ["time"] - }, - HTMLTitleElement: { - file: require("./generated/HTMLTitleElement.js"), - tags: ["title"] - }, - HTMLTableRowElement: { - file: require("./generated/HTMLTableRowElement.js"), - tags: ["tr"] - }, - HTMLTableSectionElement: { - file: require("./generated/HTMLTableSectionElement.js"), - tags: ["thead", "tbody", "tfoot"] - }, - HTMLTemplateElement: { - file: require("./generated/HTMLTemplateElement.js"), - tags: ["template"] - }, - HTMLTextAreaElement: { - file: require("./generated/HTMLTextAreaElement.js"), - tags: ["textarea"] - }, - HTMLTrackElement: { - file: require("./generated/HTMLTrackElement.js"), - tags: ["track"] - }, - HTMLUListElement: { - file: require("./generated/HTMLUListElement.js"), - tags: ["ul"] - }, - HTMLUnknownElement: { - file: require("./generated/HTMLUnknownElement.js"), - tags: [] - }, - HTMLVideoElement: { - file: require("./generated/HTMLVideoElement.js"), - tags: ["video"] - } - }, - "http://www.w3.org/2000/svg": { - SVGElement: { - file: require("./generated/SVGElement.js"), - tags: [] - }, - SVGGraphicsElement: { - file: require("./generated/SVGGraphicsElement.js"), - tags: [] - }, - SVGSVGElement: { - file: require("./generated/SVGSVGElement.js"), - tags: ["svg"] - }, - SVGTitleElement: { - file: require("./generated/SVGTitleElement.js"), - tags: ["title"] - } - } -}; - -module.exports = () => { - for (const ns of Object.keys(mappings)) { - const interfaces = mappings[ns]; - DocumentImpl.implementation.prototype._elementBuilders[ns] = Object.create(null); - - for (const interfaceName of Object.keys(interfaces)) { - const { file, tags } = interfaces[interfaceName]; - - for (const tagName of tags) { - DocumentImpl.implementation.prototype._elementBuilders[ns][tagName] = (document, localName, namespace) => { - const { _globalObject } = document; - return file.createImpl(_globalObject, [], { - ownerDocument: document, - localName, - namespace - }); - }; - } - } - } -}; diff --git a/lib/jsdom/living/webstorage/Storage-impl.js b/lib/jsdom/living/webstorage/Storage-impl.js index 7141682148..c0794b6ff0 100644 --- a/lib/jsdom/living/webstorage/Storage-impl.js +++ b/lib/jsdom/living/webstorage/Storage-impl.js @@ -1,7 +1,7 @@ "use strict"; const DOMException = require("domexception/webidl2js-wrapper"); -const StorageEvent = require("../generated/StorageEvent"); + const idlUtils = require("../generated/utils"); const { fireAnEvent } = require("../helpers/events"); @@ -20,6 +20,10 @@ class StorageImpl { } _dispatchStorageEvent(key, oldValue, newValue) { + // TODO: There is a circular dependency between StorageEvent and Storage. + // eslint-disable-next-line + const StorageEvent = require("../generated/StorageEvent"); + return this._associatedWindow._currentOriginData.windowsInSameOrigin .filter(target => target !== this._associatedWindow) .forEach(target => fireAnEvent("storage", target, StorageEvent, { diff --git a/package.json b/package.json index d4c8c22008..e9d8bfdf97 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "domexception": "^2.0.1", "escodegen": "^1.12.1", "html-encoding-sniffer": "^2.0.0", + "is-potential-custom-element-name": "^1.0.0", "nwsapi": "^2.2.0", "parse5": "5.1.1", "request": "^2.88.0", @@ -81,7 +82,7 @@ "st": "^2.0.0", "watchify": "^3.11.1", "wd": "^1.11.2", - "webidl2js": "^13.0.0" + "webidl2js": "pmdartus/webidl2js#pmdartus/add-processor" }, "browser": { "canvas": false, diff --git a/scripts/webidl/convert.js b/scripts/webidl/convert.js index d916d696a6..c5173c1c34 100644 --- a/scripts/webidl/convert.js +++ b/scripts/webidl/convert.js @@ -10,7 +10,27 @@ const Webidl2js = require("webidl2js"); const transformer = new Webidl2js({ implSuffix: "-impl", - suppressErrors: true + suppressErrors: true, + processCEReactions(code) { + const preSteps = this.addImport("../helpers/custom-elements", "ceReactionsPreSteps"); + const postSteps = this.addImport("../helpers/custom-elements", "ceReactionsPostSteps"); + + return ` + ${preSteps}(globalObject); + try { + ${code} + } finally { + ${postSteps}(globalObject); + } + `; + }, + processHTMLConstructor() { + const identifier = this.addImport("../helpers/html-constructor", "HTMLConstructor"); + + return ` + return ${identifier}(globalObject, interfaceName, new.target); + `; + } }); function addDir(dir) { @@ -21,6 +41,7 @@ function addDir(dir) { addDir("../../lib/jsdom/living/aborting"); addDir("../../lib/jsdom/living/attributes"); addDir("../../lib/jsdom/living/constraint-validation"); +addDir("../../lib/jsdom/living/custom-elements"); addDir("../../lib/jsdom/living/domparsing"); addDir("../../lib/jsdom/living/events"); addDir("../../lib/jsdom/living/fetch"); diff --git a/test/web-platform-tests/to-run.yaml b/test/web-platform-tests/to-run.yaml index ec7357cc96..9ec0b66600 100644 --- a/test/web-platform-tests/to-run.yaml +++ b/test/web-platform-tests/to-run.yaml @@ -103,6 +103,47 @@ window-screen-width.html: [fail, Test not applicable - no output device] --- +DIR: custom-elements + +CustomElementRegistry.html: [fail, webidl2js doesn't deal well with tests using Proxies to verify properties access] +Document-createElement.html: [fail, :defined is not defined and throws] +HTMLElement-attachInternals.html: [fail, Not implemented] +HTMLElement-constructor.html: [fail, webidl2js doesn't deal well with tests using Proxies to verify properties access] +adopted-callback.html: [fail, "Failing test due to https://github.com/whatwg/dom/issues/813"] +attribute-changed-callback.html: [fail, attributeChangedCallback doesn't work with CSSStyleDeclaration] +custom-element-reaction-queue.html: [fail, Document.write implementation is not spec compliant] +custom-element-registry/per-global.html: [fail, iframe location related issue] +form-associated/ElementInternals-NotSupportedError.html: [fail, Not implemented] +form-associated/ElementInternals-accessibility.html: [fail, Not implemented] +form-associated/ElementInternals-labels.html: [fail, Not implemented] +form-associated/ElementInternals-setFormValue.html: [timeout, Not implemented] +form-associated/ElementInternals-validation.html: [fail, Not implemented] +form-associated/form-associated-callback.html: [fail, Not implemented] +form-associated/form-disabled-callback.html: [timeout, Not implemented] +form-associated/form-reset-callback.html: [fail, Not implemented] +htmlconstructor/newtarget.html: [fail, Currently impossible to get the active function associated realm] +microtasks-and-constructors.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413] +parser/parser-constructs-custom-element-synchronously.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413] +parser/parser-fallsback-to-unknown-element.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413] +parser/parser-sets-attributes-and-children.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413] +parser/parser-uses-constructed-element.html: [fail, Usage of external scripts doesn't block HTML parsing, https://github.com/jsdom/jsdom/issues/2413] +parser/serializing-html-fragments.html: [fail, parse5 doesn't support is attribute for serialization] +perform-microtask-checkpoint-before-construction.html: [fail, impossible to implement microtask checkpoint without patching Promise] +pseudo-class-defined.html: [timeout, :defined is not defined and throws] +reactions/CSSStyleDeclaration.html: [fail, CSSStyleDeclaration is not implemented using wedidl2js] +reactions/Document.html: [fail, + Document.execCommand is not implemented, https://github.com/jsdom/jsdom/issues/1539 + Document.write implementation is not spec compliant] +reactions/ElementContentEditable.html: [fail, contentEditable is not implemented] +reactions/HTMLAreaElement.html: [fail, HTMLAreaElement doesn't implement download ping and referrerPolicy] +reactions/HTMLButtonElement.html: [fail, HTMLButtonElement doesn't implement formAction formEnctype and formMethod] +reactions/HTMLElement.html: [fail, translate and spellcheck attributes are not implemented on HTMLElement] +reactions/HTMLImageElement.html: [fail, HTMLImageElement doesn't implement referrerPolicy and decoder] +throw-on-dynamic-markup-insertion-counter-construct.html: [timeout, Document.open and Document.close implementation is not spec compliant] +throw-on-dynamic-markup-insertion-counter-reactions.html: [timeout, Document.open and Document.close implementation is not spec compliant] + +--- + DIR: dom/abort --- @@ -920,7 +961,7 @@ DIR: shadow-dom Document-prototype-currentScript.html: [timeout, Test not up to date next with updating wpt it should work] DocumentOrShadowRoot-prototype-elementFromPoint.html: [fail, offsetTop not implemented] -Element-interface-attachShadow-custom-element.html: [fail, CustomElement.define is not implemented] +Element-interface-attachShadow-custom-element.html: [needs-node12, Need static class fields support] MouseEvent-prototype-offsetX-offsetY.html: [fail, offsetTop not implemented] ShadowRoot-interface.html: [fail, shadowRoot.styleSheet is not yet implemented] focus/click-focus-delegatesFocus-tabindex-varies.html: [fail, element.scrollIntoView is not implemented] @@ -929,12 +970,10 @@ focus/focus-method-delegatesFocus.html: [fail, delegatesFocus is not implemented focus/focus-selector-delegatesFocus.html: [fail, 'https://github.com/dperini/nwsapi/issues/31'] focus/focus-tabindex-order-*: [fail, Uses testdriver.js] form-control-form-attribute.html: [fail, Form association doesn't respect the spec] -innerHTML-setter.xhtml: [fail, customElements is not implemented] leaktests/html-collection.html: [fail, Document.all is not implemented] leaktests/window-frames.html: [fail, Window.name is not implemeneted] offsetParent-across-shadow-boundaries.html: [fail, offsetParent not implemented] scroll-to-the-fragment-in-shadow-tree.html: [fail, Requires a layout engine] -slotchange-customelements.html: [fail, CustomElement.define is not implemented] untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-attributes/test-011.html: [fail, ShadowRoot.stylesheets is not implemented] untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-004.html: [fail, https://github.com/w3c/selection-api/issues/114] untriaged/elements-and-dom-objects/shadowroot-object/shadowroot-methods/test-006.html: [fail, DocumentOrShadowRoot.elementFromPoint() is not implemented. Needs layout engine] diff --git a/yarn.lock b/yarn.lock index aef92e0032..e0fd9c22d1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2466,6 +2466,11 @@ is-plain-object@^2.0.3, is-plain-object@^2.0.4: dependencies: isobject "^3.0.1" +is-potential-custom-element-name@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.0.tgz#0c52e54bcca391bb2c494b21e8626d7336c6e397" + integrity sha1-DFLlS8yjkbssSUsh6GJtczbG45c= + is-promise@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" @@ -4841,10 +4846,9 @@ webidl2@^23.10.1: resolved "https://registry.yarnpkg.com/webidl2/-/webidl2-23.10.1.tgz#2d2786fdff7b72c6b4a9a43e70f1611ded8ec8e6" integrity sha512-SK/KzMGgkPLN73HNqyU9tWPduireSzV6p/WAsmYYweI/ED19FC8+ymsi2InMX80+VjSvsfc3vA7UbMjXYWNPhA== -webidl2js@^13.0.0: +webidl2js@pmdartus/webidl2js#pmdartus/add-processor: version "13.0.0" - resolved "https://registry.yarnpkg.com/webidl2js/-/webidl2js-13.0.0.tgz#005f705056e117429f26a0f937dc1e14aa0f0e46" - integrity sha512-NxVXmRz/I/3vKyMlpRIuiT1hUu0DPTBx4Ig6LN1YF+NtUiDaK7B/3VV7RjAGVVYG7n8oOlRkvkusFdTa5WJatQ== + resolved "https://codeload.github.com/pmdartus/webidl2js/tar.gz/42720bc51e62f8ca501e48fa206fc3cedca4f10c" dependencies: pn "^1.1.0" prettier "^1.19.1"