Jsdom: add support for MutationObserver

Created on 8 Jun 2013  ·  53Comments  ·  Source: jsdom/jsdom

Most helpful comment

All 53 comments

I also need this. I would be willing to work on a pull request for this if someone can point me in the right direction.

@SegFaultx64 The URL provided by schettino72 in the first post on this issue is the "right direction".

Fair enough, I am not even sure how we would register the observers in a project this large. Is there a place where stuff like that is getting stored?

@SegFaultx64 It is unclear to me what the best method would be because I've not looked at that part of jsdom. When I worked on fixing the NS methods (see #727; nominally, it is a bug fix but it required substantial changes to the guts of jsdom) I looked for analogical structures and went with that for deciding where to add new classes/data/etc. I pretty much went ahead with what I thought was best, Domenic made some comments, I adjusted what needed adjustment, and that was that.

@lddubeau Okay, thanks for the pointers. I actually found a nice looking shim for MutationObserver https://github.com/megawac/MutationObserver.js/blob/master/MutationObserver.js. It looks like a nice jumping off point. I will get started on this later today.

My only advice would be to peruse https://dom.spec.whatwg.org/ and look into the details of where a mutation record is enqueued. Finding the appropriate counterparts in jsdom might be tricky since we're still transitioning to the new DOM standard, but at least the spec will give you guidance on what needs to be implemented.

any updates on this?

@kresli pull request welcome!

I have no idea how to do it, but I've decided to do some experiments. Worst case I learn a little, best case I might be able to contribute.

I'm currently trying to understand how jsdom is structured and how webidl is working (and is used in jsdom)

Would it be possible to depart from this webidl on MutationObserver: https://dxr.mozilla.org/mozilla-central/source/dom/webidl/MutationObserver.webidl~~~

I understand it a bit better now - the interface(s) to use should be those described here: https://dom.spec.whatwg.org/#interface-mutationobserver - right?

@henrikkorsgaard correct! To get it working in jsdom, you need to start with a couple things:

  • Add the appropriate MutationObserver.idl to https://github.com/tmpvar/jsdom/tree/master/lib/jsdom/living/nodes. (In the future it should probably go in a better folder than that. But our current setup makes that a bit annoying, so you can stick with nodes until you get something working.)
  • Add a MutationObserver-impl.js file to that folder as well. This is where the actual implementation work goes on. For example, the spec says "Each MutationObserver object has these associated concepts." Those will probably be instance variables in the impl (= implementation) class.

You then "just" need to implement the observe, disconnect, and takeRecords methods, plus the constructor, in the impl class. In general you should try to follow the spec as much as possible. (@Sebmaster, can you explain how to do the constructor?)

In this case, unlike something simple like CharacterData, a lot of your changes will require modifying other jsdom impl files. For example, the spec says "Each node has an associated list of registered observers." This will require adding an instance variable to the Node impl.

The spec also says "Each unit of related similar-origin browsing contexts has a mutation observer compound microtask queued flag, which is initially unset, and an associated list of MutationObserver objects, which is initially empty." This is a little trickier, since "unit of related similar-origin browsing contexts" is not obvious in jsdom. What I would do for this is to start by just putting them on Window objects. Window objects do not use IDL yet so you'll just add an underscore-prefixed property in Window.js. In the future we can try to worry about making sure things are tracked across multiple windows (i.e. take care of iframes). But for now window is a good place to put them.

Finally, a lot of the implementation will be taken care of by finding appropriate places to "queue a mutation record". If you go to https://dom.spec.whatwg.org/#queue-a-mutation-record and click "queue a mutation record" you can see all the places in the spec that happens. This will be a bit tricky because jsdom doesn't have all the exact same hooks the spec does for when things happen. But it should be doable, using jsdom's _attrModified, _descendantRemoved, and _descendantAdded hooks. https://github.com/tmpvar/jsdom/blob/master/lib/jsdom/living/attributes.js also has "TODO mutation observer stuff" throughout.

Hope that helps!

Great :)

Again, I'm not super experienced with webidl and larger codebases. I'm also a bit swarmed with a few deadlines ;)

I will do my best and give it a go over the next few weeks. I will also try and make a test case or two.

Ok,

I've gotten so far to create the idl and Impl and required it into window.js.

Now I am trying to create a test file for it and I can call new MutationObserver() with the window prefix.

const window = jsdom("<html><body><div id='mutation'></div></body></html>").defaultView;

let observer = new window.MutationObserver(function(mutations){
      console.log(mutations);
});

Am I missing some trickery here or is there somewhere I need to expose the MutationObserver as a global object (called without calling it on the window object).

I've added the following line in Window.js

const MutationObserver = require("../living/generated/MutationObserver");

Sorry about all this asking. I'm just trying to establish some entry-points so I can start testing and follow the jsdom architecture/pattern.

I have a question related to queueing the mutation task. The spec seem to indicate a central place where multiple micro tasks are queued and excuted in order. I can find such place in jsdom and I think @domenic's idea on having that responsibility in Window.js will work (webkit adds mutation records to a dedicated thread/queue). But this would also require some form of queued and more importantly some logic that executes the what is in the queue. This leads me to two questions:

Is there any other components in jsdom that might benefit from such a queue? Is there something I should consider here in terms of architecture and (future) integration?

Would it be smartest to execute tasks in the queue based on a timer (is there a global tick in jsdom?) or just based on a promises/done callback structure?

I'm focusing on getting the very basic implementation done and then writing extensive test cases to guide the future implementation.

So, a microtask is basically just process.nextTick(fn). This is a bit trickier for mutation observers because of the compound microtask business and the "execute a compound microtask subtask". I _think_ you can mostly just ignore that; jsdom doesn't need to worry about the stuff it's setting up.

So: when the spec says "Queue a compound microtask to notify mutation observers", you do process.nextTick(notifyMutationObservers). Then, inside notifyMutationObservers, I think step 3 can just be a loop over the mutation observers, which performs the substeps synchronously.

For tests, be sure to check out https://github.com/tmpvar/jsdom/blob/master/Contributing.md. There may be some existing web-platform-tests you can use (although those aren't always that easy to get passing for a from-scratch implementation). And any tests you author should be in the to-upstream folder, following that format.

This might not be the right place to ask, but I'm having some trouble understanding how to pass webidl scheme objects (e.g. /generated/MutationRecord.js) back and forth between the impl files (Node-impl -> MutationObserver-Impl).

I'm tempted to construct the objects as pure JSON objects that mirror the schema of MutationRecords and just pass that from Node-impl to MutationObserver when a mutation occurs. I guess this would break the ambition to implement all aspects of MutationObservers as described in the specification.

I suspect this is because I am not familiar with jsdom webidl architecture/object model/interfaces. An example within the code would also help me understand working with /generated/ objects in the impl files.

We should probably document this somewhere, but here goes:

In general, any arguments going through the generated API will automatically be unboxed/reboxed, so you never have to care about the generated objects - only the implementation objects are important. Except - not really. This would be the case, if we had _everything_ switched to IDL-based already, which is not the case. Arguments and return values do work, however every time you interact with a non-IDL class (like Window), you'll have to do the unboxing/reboxing when you provide them any arguments yourself (see for example https://github.com/tmpvar/jsdom/blob/master/lib/jsdom/living/events/EventTarget-impl.js#L103, where we have to unwrap a Window, since Window is not idl'd yet, but EventTarget (of which Window inherits from) is). You do this manual boxing with idlUtils.wrapperForImpl / idlUtils.implForWrapper if necessary.

So in general, you want to construct an Impl object. To do so, you require the generated file and call createImpl on it's exports. It'll take an array of args (for public-constructor arguments) and an object of private args (both of those are provided to the impl class). See https://github.com/tmpvar/jsdom/blob/9dd9069354e36c077032f4cbcb1616a7d9e6f0c4/lib/jsdom/living/nodes/Document-impl.js#L549 for an example for that.

I realize that this is not nearly in-depth enough to grasp the whole thing (I think), so if you have something more specific I'd be happy to explain more.

Thank you @Sebmaster, it helped a lot.

Yes, documenting this with a few examples on how one should integrate and use API/objects would be helpful, but I think I know enough for the next few steps.

Quick update!
The W3C tests will fail if MutationRecord follow specification (I think). I have made a comment in their repository.

I will update them locally for my purpose but I'm not sure if I will commit these to W3C/jsdom. That is, unless I have the time to finish that task as well.

But attributtes mutations now pass most of the tests and I will make a pull request sometime during the weekend or in the beginning of next week.

That's super-exciting! Can you explain in a bit more detail what the issue is? I wasn't quite able to follow https://github.com/w3c/web-platform-tests/issues/2482 so maybe just: what property value of a MutationRecord do the tests expect, and what do you think the spec requires?

The issue is that the test does not specify a full mutationRecord to compare with. So in the first test case the test will change the id value and as a consequence the returned mutation record will have the old value property. The oldValue property is not defined in the expected object and the test will fail. As I understand it a mutation record should always contain all the properties, even when they are null. In that case they should be DOMString null. When properties are not set in the testcase, the test will end up comparing null (typeof object) with null (typeof string) and fail.

The tests are constructed lazily, e.g. undefined fields are automatically set to null (object). This will cause tests to fail when a) the mutationRecord returns a property that is not set in the expected record object (e.g. oldValue) and b) when the mutation record property is DOMstring null (e.g. attributteNamespace).

Makes sense?

If I'm right is is fairly easy to fix, but requires going through all the cases one by one. This can be a bit hard as an outsider to both jsdom and w3c testing :)

I'm sorry, can you simplify? I'd prefer an answer in the form: the tests expect oldValue to be null or undefined, but the spec gives a value "null". Or similar.

The test referenced above implicitly expect oldValue to be null. It should be "n"

All the test cases in that file expects attributeNamespace to be null (object), they should be DOMString null according to the specification.

The type of attributeNamespace is DOMString?, so it's allowed to be null (not "null", just null).

Thanks for clarifying on the oldValue case :).

Ok, thanks for clarifying - I think I've missed important aspects of the spec.

I've added a branch MutationObserver in my fork and made a push to it. Currently, Attribute and CharacterData mutations are passing the most important w3c tests.

Where do I document the tests that do not pass for known reasons/issues (missing Range support #317 etc.)?

What we do is add them to the index.js file for web-platform tests, but commented out, with a comment as to why. See e.g. what we do for template.

Is this far from being finalized?

Seems Mutation Events were removed in 8.5. Is there any way to test custom elements (requires Mutation Observers) without going back to 8.5 and relying on a polyfill? DOMParser wasn't implemented in 8.5 and I require that for our Shadow DOM polyfill so we're kinda blocked by being able to run tests in JSDOM at the moment. Any guidance would be helpful here. I don't have the capacity at the moment to try and dive in and help implement this right now, unfortunately.

I'm not aware of any way of doing so, sorry :(.

Can I help move this one along? It looks like some work was started here: https://github.com/henrikkorsgaard/jsdom/commits/MutationObserver

But I am not sure what is still required to move this one along. It feels like a big gap for this not to exist given that MutationObserver is supported basically everywhere: http://caniuse.com/#feat=mutationobserver

Could we leverage the webcomponentsjs polyfill? https://github.com/webcomponents/webcomponentsjs

A pull request is always welcome. @henrikkorsgaard's work is definitely a good place to start. I don't think the polyfill makes sense though.

From what I can tell, Mutation Events were removed because of performance considerations; the same reason Mutation Observer was spec'd and implemented by browsers. However, browsers kept that functionality at least until MO support was implemented. The pragmatic approach here might be to re-add support for that so, at least, MOs can be polyfilled until this gets implemented. I realise it's not ideal for the maintenance of JSDOM and the codebase may have diverged since then. That said, I think it's worth considering. This leaves a huge gap, especially at this stage where web components are a viable solution for production apps and they require MO to be polyfilled. It'd be nice to be able to run tests in JSDOM.

if it helps; I poly-filled MO on-top jsdom; here and here . I wouldn't say its a good solution, but it works and without timers too.

4 years ago and still in progress? this issue is mentioned in the changelog since version 9.0.0. Do you guys have any news about this?

@MeirionHughes the file worked smoothly, thanks!

Any update on this issue?

Just an extension to @domenic 's link:
try to put that file that @MeirionHughes suggested, it worked well so far.

@mtrabelsi @MeirionHughes Hello, I didn't manage to make it work in Jest, I got this error https://stackoverflow.com/questions/43190171/jsdom-cannot-read-property-location-of-null
Could you explain how did you manage to use MO in JSDOM?

EDIT:
Ok, it seems I manage to make it works https://gist.github.com/romuleald/1b9272fce11d344e257d0bdfd3a984b0

@romuleald there is an error in your gist, you are setting this.expando and this.counter in the constructor but then accessing it statically. This will cause severe problems in _findMutations. You'll note that in the typescript files that you were trying to convert, both properties were static (not possible with ES6). I suggest adding these lines below the Util class:

Util.counter = 1;
Util.expando = 'mo_id';

And then you can get rid of the constructor entirely (the class is never instantiated anyway).

It took me embarrassingly long to spot this as I've been debugging an issue caused by it for the past day and a half.

Additionally, the source you based the shim on does not support changes to the .data property of CharacterData nodes. You can see my linked issue above this comment for a simple fix.

I've been able to test this using https://github.com/megawac/MutationObserver.js polyfill for the time being.

I use AVA. And 'm' is a my module containing MutationObserver.

import test from 'ava';
import delay from 'delay';
import jsdom from 'jsdom';
import m from '.';

const dom = new jsdom.JSDOM();
global.window = dom.window;
global.document = dom.window.document;

require('mutationobserver-shim');

global.MutationObserver = window.MutationObserver;

test('MutationObserver test', async t => {
    delay(500).then(() => {
        const el = document.createElement('div');
        el.id = 'late';
        document.body.appendChild(el);
    });

    const checkEl = await m('#late');
    t.is(checkEl.id, 'late');
});

Because it is Polyfill, there are some things different from the standard interface. But this issue seems to have no progress, so please refer as one option.

Another way to use it as polyfill with jsdom and jest is to load the shim as script manually.

npm install mutationobserver-shim

and for example inside beforeAll function:

const mo = fs.readFileSync(
  path.resolve('node_modules', 'mutationobserver-shim', 'dist', 'mutationobserver.min.js'),
  { encoding: 'utf-8' },
);
const moScript = win.document.createElement('script');
moScript.textContent = mo;

win.document.body.appendChild(moScript);

This "hack" works for me, maybe for others too ;)

IIRC I found it worked well to load the shim (I used a tweaked version of the code from pal-nodejs, not sure what the npm package above is based on) as part of setupFiles for Jest. I could give more specific information once I am at my other laptop if anyone is interested.

so this has been open for almost 5 years, any status updates?

@mendrik mocking it worked for me https://github.com/benitogf/corsarial/blob/master/test/specs/utils.js#L29 and as mentioned above there's also the polyfill solution, have a good day :)

@benitogf I tried that but somehow the records didn't fire for me :( it just didn't throw any errors.
@treshugart how can I use that? :)

@mendrik in your tests, import this https://github.com/aurelia/pal-nodejs/blob/master/src/polyfills/mutation-observer.ts
and set it to the global variable. thats it!

@mendrik if you're willing to opt-out of JSDOM https://github.com/skatejs/skatejs/tree/master/packages/ssr#usage. If not, then you can import that file directly and add the exports (https://github.com/skatejs/skatejs/blob/master/packages/ssr/register/MutationObserver.js#L109) to your global.

This was enough to fix this issue for me trying to test a component that extended react-quill:

import 'mutationobserver-shim';

document.getSelection = () => null;
Was this page helpful?
0 / 5 - 0 ratings

Related issues

cg433n picture cg433n  ·  3Comments

philipwalton picture philipwalton  ·  4Comments

khalyomede picture khalyomede  ·  3Comments

vsemozhetbyt picture vsemozhetbyt  ·  4Comments

amfio picture amfio  ·  3Comments