Simulated typing for integration tests?

I’m having trouble getting my text input to show up correctly in some integration tests, and I’m wondering if anyone else has simulated typing in their own tests. Most of the unit tests seem to use the doc() function and its various elements, which are fine for isolated testing, but pose an issue when I want to simulate the action of a user.

My current implementation looks like this:

function typeWord(word, editor, startPos) {
  let chars = word.split("");
  let transactions = [];
  let nextPos = startPos ? startPos : 
  editor.view.state.doc.content.child(0).content.size;
  editor.view.state.tr.setSelection(nextPos);
  for(let i = 0; i < word.length; i++) {
    let newTr = editor.view.state.tr.insertText(chars[i], nextPos);
    transactions.push(newTr);
    editor.view.dispatch(transactions[i]);

    nextPos = append ? editor.view.state.doc.content.child(0).content.size : nextPos + 1;
  }
  return transactions;
}

The append logic here works, but mid-text insertions don’t work. Is there any simpler way to do this? I’ve tried using newTr.mapping.map(nextpos + 1) to get the next position, but that does some really funky things when certain plugins are turned on.

I’m wondering if the issue I’m seeing could have to do with appended transactions causing me to miss out on some of the required step maps. Is there a reliable way to obtain the full mapping between a starting state and a final (post-dispatch/update) state?

For those who might want to do this in future, I ended up wrapping collab in a plugin that filters out all of the appended transactions and letting the remote client re-apply them instead. It’s a bit gross, but it works.

On the bright side, I learned how to properly calculate a position without duplicating the nitty-gritty details of the node system I was doing all this. Given the importance of positions in the system, I’m guessing most people are writing some syntactical sugar for that.

You’d have to take the transactions returned by applyTransaction and concatenate their individual position maps with appendMapping.

That’s what I figured. I wrote this for my integration tests:

let innerApply = editor.view.state.applyTransaction;
editor.view.state.applyTransaction = (tr) => {
  let ref = innerApply.call(editor.view.state, tr);
  state = ref.state;
  transactions = ref.transactions;
  return ref;
}

transactions.forEach((tr) => { nextPos = tr.mapping.map(nextPos)});

Seems to work decently well, though getting the correct starting position is still pretty finicky. This feels to me like a piece of the API that needs work. Testing functionality shouldn’t be this hard.