Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support for WebComponents API #1030

Closed
jimfb opened this issue Feb 17, 2015 · 89 comments
Closed

Support for WebComponents API #1030

jimfb opened this issue Feb 17, 2015 · 89 comments
Labels

Comments

@jimfb
Copy link

@jimfb jimfb commented Feb 17, 2015

We are trying to use jsdom for our unit tests for React core (http://facebook.github.io/react/).

Unfortunately, the web components spec is not natively supported by jsdom, and the webcomponents.js polyfill does not run on jsdom. This issue requests the addition of WebComponent support (custom elements, shadow dom, html imports, etc).

@Sebmaster Sebmaster added the feature label Feb 17, 2015
@domenic

This comment has been minimized.

Copy link
Member

@domenic domenic commented Feb 17, 2015

It'd be interesting to look into which APIs webcomponents.js uses that jsdom doesn't support. If I had to guess, that will be much easier to implement than the full web components spec.

That said, it would be pretty cool to implement web components. Probably not as hard as one might think---the specs are relatively small.

@Sebmaster

This comment has been minimized.

Copy link
Member

@Sebmaster Sebmaster commented Feb 18, 2015

Just had time to dig into this a bit:

First off, we don't have Window defined in the window scope. I just patched this with this.Window = this.prototype in the Window constructor.
Second, webcomponentsjs expects Window to have another prototype, i.e. the EventTarget prototype, which we don't implement as a seperate entity.

Just a bit of info, because I had a bit of time.

@domenic

This comment has been minimized.

Copy link
Member

@domenic domenic commented Feb 18, 2015

Nice. Should be able to expose Window pretty easily. EventTarget prototype is a bit trickier but seems doable given how we currently implement that stuff; it's been a TODO of mine.

@Sebmaster

This comment has been minimized.

Copy link
Member

@Sebmaster Sebmaster commented Feb 18, 2015

Okay, patches so far are rather easy:

  • this.Window = Window; in the Window constructor
  • inherits(dom.EventTarget, Window, dom.EventTarget.prototype); after the definition of Window

The next crash of webcomponents.js happens due to us not implementing HTMLUnknownElement (#1068), after shiming that we need to implement the SVGUseElement. That's what I'm currently blocked on, because webcomponents.js apparently doesn't like the SVGUseElement shimmed by a HTMLDivElement and throws in an assert.

@Sebmaster

This comment has been minimized.

Copy link
Member

@Sebmaster Sebmaster commented Mar 20, 2015

Okay I checked into the Polyfill some more, we need to implement/you need to shim the following:

  • HTMLUnknownElement #1068
  • SVGUseElement
  • window.CanvasRenderingContext2D
  • Range APIs (including: document.getRange(), window.getSelection(), window.Range, window.Selection; #804 might be a start)
  • npm i canvas

(non-exhaustive list for now)

A start is something like the following:

jsdom.env({
  file: __dirname + '/index.htm', // refers to webcomponent.js
  created: function (err, window) {
    jsdom.getVirtualConsole(window).sendTo(console)

    window.document.createRange = function () { }
    window.getSelection = function () { }
    window.Range = function () { }
    window.Selection = function () { }
    window.CanvasRenderingContext2D = function () { } // Object.getPrototypeOf(require("canvas")(0,0).getContext("2d")) might be better
    window.SVGUseElement = window.HTMLUnknownElement
  },
  done: function (err, window) {
    console.log(err[0].data.error);
    console.log(window.CustomElements)
  },
  features: {
    ProcessExternalResources: ['script']
  }
});

That done, there's some bug in our HTMLDocument constructor, which leads to a maximum call stack error. The constructor is at the moment only for internal use, however it's valid that some script on the site makes calls to it so we need to make that constructor available for public consumption.

@bedeoverend

This comment has been minimized.

Copy link

@bedeoverend bedeoverend commented Sep 14, 2015

+1 Would love to see WebComponents on jsdom, particularly as Polymer gains in popularity, would be great to be able to test custom elements on a headless system.

@domenic

This comment has been minimized.

Copy link
Member

@domenic domenic commented Sep 14, 2015

Right now there is no cross-browser definition of web components, so it'd be premature to implement. (We're not just going to copy Chrome.) In the meantime, you can try using Polymer with jsdom.

@bedeoverend

This comment has been minimized.

Copy link

@bedeoverend bedeoverend commented Sep 14, 2015

@domenic fair enough. Well it's more the support for the WebComponents.js polyfill that I'm after, as that's what Polymer depends on - or webcomponents-lite (polyfills all of them barring Shadow DOM) at the moment. Made a few attempts to get Polymer working on jsdom, but no luck so far - I'm assuming @Sebmaster's tasks in the comment above will at least need to be patched first.

@domenic

This comment has been minimized.

Copy link
Member

@domenic domenic commented Sep 14, 2015

My understanding is that there are three separate polyfills in question. The one in the OP is separate from Polymer. Then there's the webcomponents.org polyfills, which used to be used in old-Polymer. Then in Polymer 1.0, they have their own polyfills, I think, which aren't really polyfills, but instead alternate libraries that do things kinda web-component-ish. Maybe that is webcomponents-lite though.

@bedeoverend

This comment has been minimized.

Copy link

@bedeoverend bedeoverend commented Sep 14, 2015

On the WebComponentsJS repo, it says that the webcomponentsjs-lite is a variant, providing polyfills for all but Shadow DOM, which Polymer then independently attempts to shim using their Shady DOM system. So from that I'm pretty sure Polymer relies on WebComponents as much as it can, with the WebComponentsJS polyfill doing the grunt work. The lite version is supposed to be significantly less weight (funnily enough..) so I'll see if I can pinpoint what it is that jsdom needs for the lite version. What do you think the chances are of getting the polyfill (lite or full) working in jsdom is?

@domenic

This comment has been minimized.

Copy link
Member

@domenic domenic commented Sep 14, 2015

It's really hard to say without some investigation... looking forward to what you find out.

@Sebmaster

This comment has been minimized.

Copy link
Member

@Sebmaster Sebmaster commented Sep 14, 2015

Yeah, I think my list of todo tasks is still applicable and required to use the shims. Getting #1227 merged in might make us a lot quicker with implementing standards-compliant interfaces so we can fix the missing ones more quickly.

@matthewp

This comment has been minimized.

Copy link
Contributor

@matthewp matthewp commented Jun 15, 2016

I've (probably naively) started working on adding CustomElementsRegistry as a way to understand how jsdom is structured. I added "custom-elements/custom-elements-registry/define.html" to the web platform tests list and it passes when it shouldn't (i haven't implemented nearly enough yet). I'm pretty sure the test isn't really running as even adding a throw at the top of the test won't prevent it from passing. So I've obviously missed something; aside from adding the test in test/web-platform-tests/index.js is there anything else I need to do?

@Sebmaster

This comment has been minimized.

Copy link
Member

@Sebmaster Sebmaster commented Jun 15, 2016

Seems like that's caused because we fail in the initial const testWindow = iframe.contentDocument.defaultView; line because contentDocument is undefined for some reason. Might be an issue with our loading order vs. script execution, but haven't dug into that. Hope that helps you work around that. We might have to simplify the test for our purposes (for now).

@matthewp

This comment has been minimized.

Copy link
Contributor

@matthewp matthewp commented Jun 15, 2016

That helps very much, thanks! I'll see if I can figure out what is going on there, and if not I'll create a simplified test as you recommended.

@matthewp

This comment has been minimized.

Copy link
Contributor

@matthewp matthewp commented Jun 22, 2016

@Sebmaster Just in case your interested, I did a bit of research into what is going on with that test and the results are surprising to me.

The test is using the named access feature of html. This means you can do stuff like:

<div id="foo"></div>
<script>
  console.log(window.foo === document.getElementById('foo'));
</script>

However, if the element has a nested browsing context, the global should point to that instead (see the linked spec). For iframe's that's the contentWindow. jsdom gets this right, there's even a test. Safari gets it right too.

What's crazy is that Chrome and Firefox get this wrong; the global points to the iframe, not it's contentWindow. Seeing this, I assumed it was a jsdom bug and did some hunting, eventually finding that test, which led me to the spec.

tldr; working on jsdom is very educational and you guys do an amazing job.

Going to file bugs in the respective browsers. Also will send a PR to web-platform-tests, I found some other mistakes in the test as well.

@domenic

This comment has been minimized.

Copy link
Member

@domenic domenic commented Jun 22, 2016

This is even more motivation to upstream tests like https://github.com/tmpvar/jsdom/blob/master/test/living-html/named-properties-window.js to WPT. Thank you for posting! It makes me feel really great about jsdom ^_^

@solkimicreb

This comment has been minimized.

Copy link

@solkimicreb solkimicreb commented Jul 26, 2016

Hi!

I managed to make Custom Elements polyfill work with jsdom by combining

Note: the repo uses jsdom 8.5.0. The reason is that I only had success with a MutationObserver polyfill, that uses Mutation Events internally. Mutation Events were removed after 8.5.0 due to bad performance. If native Mutation Observer comes out I will remove the polyfill and update to the latest jsdom.

@lastmjs

This comment has been minimized.

Copy link

@lastmjs lastmjs commented Nov 19, 2016

I've got the latest jsdom, and https://github.com/WebReflection/document-register-element is working for me! I've been experimenting with the more official polyfills, and I'm having trouble for some reason. My goal is to get at least custom elements and html imports to work...it would be awesome if we could get Polymer to work as well.

@lastmjs

This comment has been minimized.

Copy link

@lastmjs lastmjs commented Nov 19, 2016

I can get the Polymer scripts to run without error. I can even create a component and pass it to the Polymer constructor. After that it fails silently. I think shadow DOM is the issue.

I've been trying to get the webcomponentsjs HTML imports polyfill to work. I can get the script to run, and I believe my HTML imports execute an xmlhttprequest, but it doesn't seem like the scripts in my imports get run.

@snuggs

This comment has been minimized.

Copy link
Contributor

@snuggs snuggs commented Nov 21, 2016

Care to share an example @lastmjs? I'm currently knee deep in web components myself. If I can be of help i'd gladly contribute with you.

@lastmjs

This comment has been minimized.

Copy link

@lastmjs lastmjs commented Nov 22, 2016

@snuggs Thanks! Give me a day or two, I'm in the middle of some pressing things at the moment.

@FaBeyyy

This comment has been minimized.

Copy link

@FaBeyyy FaBeyyy commented Apr 15, 2019

The polyfill at: https://github.com/WebReflection/document-register-element Works like a charm! My most sincere thanks to the author!

For those struggling with the same problem, just do:

npm install -D document-register-element

In your jest configuration set a setup file that will be run before all your tests:

{ "setupFilesAfterEnv": [ "./tests/setup.js" ] }

And finally, inside that file ('tests/setup.js'):

import 'document-register-element'

After doing this, registering and creating custom elements in jsdom via document.createElement('custom-component') works perfectly! Fragments don't seem to work, however. (I'm not using shadow dom, by the way).

It works fine for me but the connectedCallback is never called, any idea?

@majo44

This comment has been minimized.

Copy link

@majo44 majo44 commented Apr 26, 2019

@FaBeyyy same for me :(

@mgibas

This comment has been minimized.

Copy link

@mgibas mgibas commented Apr 26, 2019

@FaBeyyy @majo44 you have to append your component to a document ie. document.body.appendChild(...) for connectedCallback to get fired. By specs it’s being called when component is attached to a Dom.

@ExE-Boss

This comment has been minimized.

Copy link
Contributor

@ExE-Boss ExE-Boss commented Apr 26, 2019

JSDOM is effectively a browser at this point, just not as stable as the big three.

At this point, it’s more like the big two, because Microsoft is ditching theirs, which has been with them for as long as Windows.

@FaBeyyy

This comment was marked as disruptive content.

Copy link

@FaBeyyy FaBeyyy commented Apr 26, 2019

@FaBeyyy @majo44 you have to append your component to a document ie. document.body.appendChild(...) for connectedCallback to get fired. By specs it’s being called when component is attached to a Dom.

thanks captain obvious but that is of course not the issue here. If I didn't know how the component lifecycle works I would probably not be trying to write tests 😄. Will create a repo showcase later when i find the time.

@majo44

This comment has been minimized.

Copy link

@majo44 majo44 commented Apr 26, 2019

@FaBeyyy
So I found the setup which works for me. I had to add polyfill for MutationObserver. I'm using JSDOM for testing porpoise, with Jest, and the working setup is:

// package.json
{  ...
  "jest": {
    "transform": {
      "^.+\\.(mjs|jsx|js)$": "babel-jest"
    },
    "setupFiles": [
      "<rootDir>/node_modules/babel-polyfill/dist/polyfill.js",
      "<rootDir>/node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
      "<rootDir>/node_modules/document-register-element/build/document-register-element.node.js"
    ]
  }
... 
}
//.bablerc
{
    "presets": [
        ["@babel/preset-env", { "modules": "commonjs"}]
    ]
}
@FaBeyyy

This comment has been minimized.

Copy link

@FaBeyyy FaBeyyy commented Apr 26, 2019

@FaBeyyy
So I found the setup which works for me. I had to add polyfill for MutationObserver. I'm using JSDOM for testing porpoise, with Jest, and the working setup is:

// package.json
{  ...
  "jest": {
    "transform": {
      "^.+\\.(mjs|jsx|js)$": "babel-jest"
    },
    "setupFiles": [
      "<rootDir>/node_modules/babel-polyfill/dist/polyfill.js",
      "<rootDir>/node_modules/mutationobserver-shim/dist/mutationobserver.min.js",
      "<rootDir>/node_modules/document-register-element/build/document-register-element.node.js"
    ]
  }
... 
}
//.bablerc
{
    "presets": [
        ["@babel/preset-env", { "modules": "commonjs"}]
    ]
}

Nice, Thanks!

@mgibas

This comment has been minimized.

Copy link

@mgibas mgibas commented Apr 26, 2019

@majo44 this is not required with latest jsdom. When working with Jest (which is using jsdom v11) you can just use updated environment: https://www.npmjs.com/package/jest-environment-jsdom-fourteen

@majo44

This comment has been minimized.

Copy link

@majo44 majo44 commented Apr 26, 2019

@mgibas thanks, with jest-environment-jsdom-fourteen it is also works fine and mutation observer polyfill is not required (but version is 0.1.0, single commit package :) )

@calebdwilliams

This comment has been minimized.

Copy link

@calebdwilliams calebdwilliams commented May 10, 2019

Is there a breakdown of which of the web components APIs are currently supported by JSDOM? Seems like shadow DOM is supported, but not custom elements (at least in the release branch/repo)?

@NMinhNguyen

This comment has been minimized.

Copy link

@NMinhNguyen NMinhNguyen commented Jun 27, 2019

npm install @tbranyen/jsdom@13.0.0 --save-dev

@tbranyen do you have the source code for your fork available somewhere? Would be curious to look at the diff 🙂

@noelmace

This comment has been minimized.

Copy link

@noelmace noelmace commented Aug 8, 2019

I'm using jest-environment-jsdom-fifteen like @majo44 suggested, and the babel-polyfill and document-register-element (see @mgibas answers). But I still get an error when I try to retrieve my web component shadow dom for tests.

el.shadowRoot is null with:

const el;
beforeEach(async() => {
  const tag= 'my-component'
  const myEl = document.createElement(tag);
  document.body.appendChild(myEl);
  await customElements.whenDefined(tag);
  await new Promise(resolve => requestAnimationFrame(() => resolve()));
  el = document.querySelector(tag);
}

it(() => {
  const fooContent = el.shadowRoot.querySelectorAll('slot[name=foo] > *');
})

Any idea of a workaround? FYI, it was already tested with Karma, Mocha, Chai & Jasmine.

Nothing special in my component:

customElements.define(
  'my-component',
  class extends HTMLElement {
    constructor() {
      super();

      const shadowRoot = this.attachShadow({ mode: 'open' });
      ...
  }
})

Edit

I did some debugging with jsdom 15.1.1 in order to better understand my issue.
Still, I don't understand why it's null here...

So, Element.shadowRoot is implemented since 88e72ef

if (this._shadowRoot !== null) {
throw new DOMException(
"Shadow root cannot be created on a host which already hosts a shadow tree.",
"InvalidStateError"
);
}
const shadow = ShadowRoot.createImpl([], {
ownerDocument: this.ownerDocument,
mode: init.mode,
host: this
});
this._shadowRoot = shadow;
return shadow;
}
// https://dom.spec.whatwg.org/#dom-element-shadowroot
get shadowRoot() {
const shadow = this._shadowRoot;
if (shadow === null || shadow.mode === "closed") {
return null;
}
return shadow;
}

After document.createElement, this._shadowDom is ok at https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/living/nodes/Element-impl.js#L403. And every element in the shadow dom is created (Element constructor called with the right values).

But when I call el.shadowDom immediately after document.body.appendChild(el) (https://github.com/jsdom/jsdom/blob/15.1.1/lib/jsdom/living/nodes/Element-impl.js#L408), this. _shadowRoot is null!

Same thing after

 await customElements.whenDefined(tag);
 await new Promise(resolve => requestAnimationFrame(() => resolve()));

Or even if I use the following instead of document.

document.body.innerHTML = `
  <my-component id="fixture"></my-component>
`:

For reproduction, see:
https://github.com/noelmace/devcards/tree/jest

@noelmace

This comment has been minimized.

Copy link

@noelmace noelmace commented Aug 8, 2019

@NMinhNguyen I guess you can find the source code of the fork made by @tbranyan here https://github.com/tbranyen/jsdom/tree/initial-custom-elements-impl

@fernandopasik

This comment has been minimized.

Copy link

@fernandopasik fernandopasik commented Sep 17, 2019

I'm trying to test web components made with lit-html and lit-element and I noticed this difference when creating the elements.

const myElem = new MyElem();

document.body.appendChild(myElem);
await myElem.updateComplete;

console.log(myElem.shadowRoot) // exists and has the rendered markup

and when I use the document.createElement

const myElem = document.createElement('my-elem');

document.body.appendChild(myElem);
await myElem.updateComplete;

console.log(myElem.shadowRoot) // null

For configuring jest I only use one polyfill that is : setupFiles: ['document-register-element']

Seems that the render method in myElem never gets called. Debugging a bit further I've discovered that the method initialize that is in lit-element never gets called.
So the 2nd example would work if I do

const myElem = document.createElement('my-elem');
myElem.initialize();

document.body.appendChild(myElem);
await myElem.updateComplete;

console.log(myElem.shadowRoot) //  exists and has the rendered markup
@capricorn86

This comment has been minimized.

Copy link

@capricorn86 capricorn86 commented Oct 6, 2019

I have created an alternative DOM that supports web components. I first tried to make a PR, but the way JSDOM works made it hard for me to solve my needs there. You are free to use it or look at the code.

DOM:
https://www.npmjs.com/package/happy-dom

Jest environment:
https://www.npmjs.com/package/jest-environment-happy-dom

@motss

This comment has been minimized.

Copy link

@motss motss commented Oct 7, 2019

@TechQuery

This comment has been minimized.

Copy link

@TechQuery TechQuery commented Oct 7, 2019

@capricorn86
Your work makes my Test environment simple, thanks!
https://github.com/EasyWebApp/WebCell/tree/v2/MobX

@capricorn86

This comment has been minimized.

Copy link

@capricorn86 capricorn86 commented Oct 28, 2019

@capricorn86
Your work makes my Test environment simple, thanks!
https://github.com/EasyWebApp/WebCell/tree/v2/MobX

Thank you @TechQuery!

@capricorn86

This comment has been minimized.

Copy link

@capricorn86 capricorn86 commented Oct 28, 2019

Looks awesome. Thank you.

On Mon, Oct 7, 2019, 1:08 AM capricorn86 @.***> wrote: I have created an alternative DOM that supports web components. I first tried to make a PR, but the way JSDOM works made it hard for me to solve my needs there. You are free to use it and/or look at the code. DOM: https://www.npmjs.com/package/happy-dom Jest environment: https://www.npmjs.com/package/jest-environment-happy-dom — You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub <#1030?email_source=notifications&email_token=ACQ5ZD5QUEITPND4SXWOHW3QNILSRA5CNFSM4A4G5SF2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEAOO5ZA#issuecomment-538767076>, or mute the thread https://github.com/notifications/unsubscribe-auth/ACQ5ZDYU465DXI4KHBQH4KTQNILSRANCNFSM4A4G5SFQ .

Thank you @motss!

@alfechner

This comment has been minimized.

Copy link

@alfechner alfechner commented Jan 3, 2020

Is there a breakdown of which of the web components APIs are currently supported by JSDOM? Seems like shadow DOM is supported, but not custom elements (at least in the release branch/repo)?

I'd be interested in this as well :)

@evanplaice evanplaice mentioned this issue Jan 8, 2020
domenic pushed a commit that referenced this issue Feb 16, 2020
Fixes #1030.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

You can’t perform that action at this time.