DEV Community

Cover image for How to use π—₯π˜…π‘“π‘₯ for a realistic user-typing simulation.
Dean Radcliffe
Dean Radcliffe

Posted on

How to use π—₯π˜…π‘“π‘₯ for a realistic user-typing simulation.

Here we will use a couple of components from the π—₯π˜…π‘“π‘₯ toolkit to write some text into a textarea, much as a user would fill out a form.

If we use a library like @testing-library/user-event, we can automate typing into a field with a configurable delay between characters. However, real users don't type this way, they type in spurts, like this:

Realistic typing

Filling out a form in a randomized way will stress-test your form more realistically - some of the time (about 1/6 of the time) your user's consecutive key presses will be half the average duration. Particularly if you are using React controlled inputs, this can reveal stutter in the system that you would not have known about. Also, I find that lorem ipsum text is more pleasurable to see typed with randomized timing than at a uniform rate! But importantly, we don't want total randomness - we want it to hover around a certain given amount.

So let's bring together a few π—₯π˜…π‘“π‘₯ tools together in order to pull this off, we will:

  • Use a bus, typed for character events.
  • Trigger character events from a source text.
  • Create a listener that returns a time-deferred typing from a character event
  • Allow the bus to execute and sequence those time-deferred typings
  • Randomize the time amounts

In the final step, the code will change only by one line in order to switch to random from not random! Let's dive right in!

See the CodeSandbox

First, we can type the defaultBus from @rxfx/bus.

import { defaultBus, Bus } from "@rxfx/bus";
const bus = defaultBus as Bus<string>;
Enter fullscreen mode Exit fullscreen mode

Next we'll use a good old-fashioned for loop to trigger an event for each character:

for (let i = 0; i < srcText?.length; i++) {
  bus.trigger(srcText?.charAt(i));
}
Enter fullscreen mode Exit fullscreen mode

Building the listener will be tricky - first we know we can't have overlapping character typings - we want them serially. So we will create our listener with bus.listenQueueing. What will we return? We could have written any delayed Promise function and that would work, but the π—₯π˜…π‘“π‘₯ utility after is designed just for that.

import { after } from "@rxfx/after";

const typer = bus.listenQueueing(
  () => true, 
  (char) => {
    return after(AVERAGE_DELAY, () => {
      console.write(char);
    });
  }
);
Enter fullscreen mode Exit fullscreen mode

This is our entire async behavior! Those after values just queue up - they're lazy Observables. But let's not forget that final touch - the randomization.

In case you are thinking this will be very hard, never fear, π—₯π˜…π‘“π‘₯' @rxfx/perception exports a function that randomizes numbers given it, but in a way that the average is preserved. We just drop it in a place that used to have a constant, and we're good.

+ import { randomizePreservingAverage } from "@rxfx/perception";


const typer = bus.listenQueueing(
  () => true, 
  (char) => {
-    return after(AVERAGE_DELAY, () => {
+    return after(randomizePreservingAverage(AVERAGE_DELAY), () => {
      console.write(char);
    });
  }
);
Enter fullscreen mode Exit fullscreen mode

There we go! A pseudo-human typist - something that will look good for chatbot responses, AI, Chat GPT - whatever!

Check out the CodeSandbox and then see how you can use, and enjoy using this.

β€”π—₯π˜…π‘“π‘₯

PS How does randomizePreserveAverage work? One answer I could tell you is that it scales a number by the absolute value of a randomly chosen logarithm between 0 and 1. But a more intuitive way to say it is that it makes 1/3 of numbers larger, while 2/3 get smaller β€” given that a single doubling event requires two halving events to preserve the average.

Top comments (0)