Listen on editor being ready (rendered)

I’m trying to write automatic tests for CodeMirror.

The way I do it now, i I do

const state = // make state
const parent = document.createElement("div");
new EditorView({parent, state});
return parent;

And then I write tests on parent. It works great!

But everyonce in a while, like once every 20 runs, random tests fail. And they fail, because parent don’t have applied styles.

I think new Editor({parent, state}) doesn’t immediately apply the view to the parent, and sometimes there’s a race condition, where test does assertion before the code mirror apply its styles.

I did a test, and when I apply 100ms “wait” in the tests, the problem goes away, however I know I don’t want to do it.

So my question is, how to “be notified” when new EditorView() “finished” updating the node? I would like to set an event, or maybe some callback of some kind?

I was under the impression that when new EventView() constructor returns, then its ready, but its not, because this random tests 1 in 20 runs fails.

Or is it so that it truly does return after it fails, and its my testing framework that messes it up?

The editor will render synchronously when you create it, but it will not read back its layout and adjust its viewport and geometry based on that until the ‘measure’ animation frame callback it’ll register on init (see requestMeasure). But even for those, them not being available synchronously should be predictable, so I’m not sure how that’d lead to non-deterministic failures. Are you able to construct a minimal test, using just the library code and no extra dependencies, that shows this issue?

1 Like

I am, but even if you run it 10 times, it will probably pass. You probably’d have to run it like 30-40 times, to get a single failure. And its never the same test that fails. I have written around 200 them, and its always random. In all cases it just looks as if the styles aren’t applied.

What I do is I basically run


function renderedView(text, extensions) {
  const state = EditorState.create({doc: text, extensions});
  const parent = document.createElement('div');
  new EditorView({parent, state});
  return parent;
}

and then I just do

const style = window.getComputedStyle(parent.querySelector(".cm-line"));

and in the test I do

expect(style.fontWeight).toBe("bold"); // I'm using jest

However I follow your suggestion with requestMessure(), I used this code

async function renderedView(text, extensions) {
  const state = EditorState.create({doc:text, extensions});
  const parent = document.createElement('div');
  const view = new EditorView({parent, state});
  return await new Promise(resolve => {
    view.requestMeasure({
      read: () => resolve(parent),
      write: () => {
      },
    });
  });
}

And still it sometimes fails, just like before :confused:

It looks like you’re not putting the parent element in your document at all? Wouldn’t that make getComputedStyle always fail to compute anything?

1 Like

It looks like you’re not putting the parent element in your document at all? Wouldn’t that make getComputedStyle always fail to compute anything?

Maybe, however I do get the styles. For example I have these tests, and they pass

test('## heading', () =>
  expectation('## Heading', {
    'font-size': '1.3em',
    'font-weight': 'bold'
  }));

test('### heading', () =>
  expectation('### Heading', {
    'font-size': '1.2em',
    'font-weight': 'bold'
  }));

I’m using JEST and I’ve set "testEnvironment": "jest-environment-jsdom", maybe it overrides document.createElement('div') so it does some pollyfills.

I’m not really sure how it works, but the tests do run and do pass. I see styles applied 99.9% of the time, they just sometimes 1 in 30 runs fail one random tests. About 1 in 200 runs two tests fails, because of unapplied styles, so it really looks like a race condition. After I’ve added setTimeout(, 100), they never fail, altough I’m pretty sure that with that timeout probably 1 in 100000000 could still fail (in a rare case when render takes 100ms or more).

So your suggestion would be to add parent to the DOM tree before reading the styles?

I don’t know what you’re doing precisely, so I’m not giving any specific suggestion. Styles not loading reliably seems more likely to be related to the way you’re loading CSS, rather than to something inside CodeMirror, unless the styles in question are applied via CodeMirror’s theme system, in which case they too are being added to the DOM synchronously.

1 Like

I appreciate your positive attitude!

Okay, I’ve performed a more through search, and I admit, I’ve made a mistake of assuming its the styles, it weren’t the styles. I’ve run console.log() on each element before running the tests.

I used code

function renderedView(doc, extensions) {
  const state = EditorState.create({doc: '<u>underline</u>', extensions});
  const parent = document.createElement('div');
  new EditorView({parent, state});
  return parent;
}

This is an example of an element, when a test fails

  console.log
    &lt;u&gt;underline&lt;/u&gt;

      at flatMap (test/framework/src/rendered.js:32:15)
          at Array.flatMap (<anonymous>)

This is an element when a test passes

  console.log
    <span class="ͼ15">&lt;</span><span class="ͼ16">u</span><span class="ͼ15">&gt;</span>underline<span class="ͼ15">&lt;/</span><span class="ͼ16">u</span><span class="ͼ15">&gt;</span>

      at flatMap (test/framework/src/rendered.js:32:15)
          at Array.flatMap (<anonymous>)

So its not the styles being applied or not, but the view appling highlight themes to the DOM. I incorrectly assumed it was the missing styles.

So the way I see it, CodeMirror puts the doc from state into the DOM into the div.cm-line, but doesn’t yet applies highlighting, I think.

Is there a way to run a callback, when the highlighting is being done applying?

I’m using

import {commonmarkLanguage} from '@codemirror/lang-markdown';

for the highlight of HTML.

Ah, that makes more sense. To avoid being unresponsive, the editor limits parsing work to ~25ms of synchronous work. If you have some kind of system hiccup, or the garbage collector or JIT compiler kicks in at the wrong moment, it might be possible that the parser doesn’t get very far at all (though I’ve personally never seen it not even parsing the first line, I suppose that might be possible in the Markdown parser).

A background (requestIdleCallback) worker will continue to parse until at least the entire viewport is highlighted. You could check if syntaxTree(state).length == state.doc.length, and if not, wait for view updates (for example with EditorView.updateListener) until that is true, for tests that need the content to be highlighted.

1 Like

Okay! Great! I followed your advice, and it turns out you were right. I added the check in the EditorView.updateListener and every test now passes 100% of the time! Thanks you so much.

But I’ve got another problem. I’ve written more tests, this time for this content:

```js
console.log("Welcome");
```

and so it happens, that syntaxTree(state).length == state.doc.length is true, even if the code syntax hasn’t been parsed yet. I do console.log(parent.innerHTML) and I see the DOM is:

<div class="highlight-code cm-line">
   <span class="ͼr">```</span>
   <span class="ͼ17">js</span>
</div>
<div class="highlight-code cm-line">console.log("Welcome");</div>
<div class="highlight-code cm-line">
   <span class="ͼr">```</span>
</div>

When I do setTimeout(, 4000) before returning, I get

<div class="highlight-code cm-line">
   <span class="ͼr">```</span>
   <span class="ͼ17">js</span>
</div>
<div class="highlight-code cm-line">
   <span class="ͼ19">console</span>
   <span class="ͼ14">.</span>
   <span class="ͼ18">log</span>
   <span class="ͼ15">(</span>
   <span class="ͼu">"Welcome"</span>
   <span class="ͼ15">)</span>
   <span class="ͼ15">;</span>
</div>
<div class="highlight-code cm-line">
   <span class="ͼr">```</span>
</div>

So it looks like after the parsing is done, and syntaxTree(state).length == state.doc.length is true, there’s still some processing to be done, and the problably is highlighting the code blocks.

Is there some advise you can make, to do another check to see if that is done?

I assume the Markdown mode is loading the JavaScript mode asynchronously via language-data? You could pre-load it (await LanguageDescription.matchLanguageName("js").load() or something like that) to avoid that extra step.

1 Like

I assume the Markdown mode is loading the JavaScript mode asynchronously via language-data ? You could pre-load it (await LanguageDescription.matchLanguageName("js").load() or something like that) to avoid that extra step.

Yes, it is! I didn’t think about that for a second, you’re a genius!

But is there some check I can do to check/react to the language being loaded?

Because for the test to be reliable, I’d need to preload all languages, or provide the name of the language in the test.

What’s best for me I think would be some kind of a listener, that can listen and check if all the language-data has been loaded, and then just resolve() the Promise. Is there something like that?

No, that doesn’t exist. The dynamic loading will let the outer parser finish its tree, and then schedule a re-parse of the range covered by the loaded language when it finishes. That state is not currently exposed. It might be reasonable to add some interface that produces a promise that resolves then a given part of the document has been parsed, but it might be a while until I have time to do that.

1 Like

Okay, it’s not enough.

I just did a check, and my test failed after some runs. Most runs it works, which is better (because previously every run failed). I’ve been running it and I caught the fail when with this content

```js
console.log("Welcome");
```

the editor parsed console as variable, . as punctuation, log as property, but didn’t parse "Welcome" as string. I can probably catch it again, if you’d like to see the DOM, but I clearly saw it in the middle of parsing. And it occurs similarly, roughly 1 in 10 runs fails.

Can I do something about it?

@marijn Perhaps there’s another check that I can do, like an if, to see if the whole thing was highlighted?

@codemirror/language 0.19.5 exports a syntaxTreeAvailable function, that you should be able to check for this (in an update handler).

Okay, here’s the thing.

Currently, I’m using @codemirror/language/0.19.3 and this code works fine for me (Too much code, I know, I’m sorry!):

import {EditorState} from '@codemirror/state';
import {EditorView} from '@codemirror/view';
import {syntaxTree} from '@codemirror/language';

export async function viewNode(content, extensions) {
  const parent = document.createElement('div');
  await editorView(parent, content, extensions);
  return parent;
}

async function editorView(parent, content, extensions) {
  return await new Promise(resolve => {
    const listener = viewUpdateListener(state => {
      if (viewStateIsReady(state)) {
        resolve();
      }
    });
    const state = editorState(parent, content, [listener, ...extensions]);
    if (viewStateIsReady(state)) {
      resolve();
    }
  });
}

function viewUpdateListener(callback) {
  return EditorView.updateListener.of(({view}) => callback(view.state));
}

function editorState(parent, doc, extensions) {
  const state = EditorState.create({doc, extensions});
  new EditorView({parent, state});
  return state;
}

function viewStateIsReady(state) {
  return syntaxTree(state).length === state.doc.length;
}

When I update @codemirror/language to mere 0.19.4, the tests timeout.
That’s because viewStateIsReady() always returns false now, and that’s because syntaxTree(state).length is always 0, and then EditorView.updateListener.of is never called.

Did you double-check all that? Because the changes between 0.19.3 and 0.19.4 are definitely not causing any changes to the parse tree lengths (the only change is a small addition to LanguageDescription, which is really unlikely to affect any of this).

Okay, I read

/**
Get the syntax tree for a state, which is the current (possibly
incomplete) parse tree of active [language](https://codemirror.net/6/docs/ref/#language.Language),
or the empty tree if there is no language available.
*/
declare function syntaxTree(state: EditorState): Tree;

It’s possible that there is no language available, and that’s why syntaxTree() returns 0 always.

Okay, here’s my investigation.

I replaced my code

import {syntaxTree} from '@codemirror/language';

function viewStateIsReady(state) {
  return syntaxTree(state).length === state.doc.length;
}

with this code, that I found in your library:

function viewStateIsReady(state) {
  return syntaxTree(state).length === state.doc.length;
}

function syntaxTree(state) {
  const field = state.field(Language.state, false);
  if (field) {
    return field.tree; // This always is called for 0.19.3
  }
  throw "No active language"; // This is always called for 0.19.4
}

Any idea what’s the reason?

I don’t know what you’re doing, really, and the fact that you seem to have entirely failed to pick up on what I wrote earlier (above) makes me a bit doubtful that you’re not sending me on a wild goose chase here. You should upgrade to 0.19.5, not 0.19.4, and there really is no way the changes in that version would produce the effects you’re describing here. So check your code, I guess.

1 Like