1
\$\begingroup\$

For fun, I decided to inspect the obfuscated source code on TypingTest and figure out how to write an auto typer for it. I came up with the following UserScript.

A couple of notes:

  • The site's code simply checks for focus on the .test-edit-area element, and handles each keydown and keypress event that occurs on the element, starting the countdown timer on the first encountered keypress event.
  • The site's keydown handler uses keyCode exclusively, and its keypress handler uses keyCode as a fallback after which and charCode, so I thought it would be easier to just assign that single getter for keyCode.
  • I used a mutation observer to watch for the .test-text-area and .test-edit-area because they're dynamically loaded after the document idles and the script was occasionally failing due to race condition.
  • I intentionally worked to avoid scoped variable references in the generator function and the keyCode getter, because they improved iteration speed significantly (from ~1900 wpm to ~2450 wpm)
  • I used detail to store the text, as per the convention for CustomEvent.
  • I used non-lexical scoping for i because it slightly improves iteration speed (~2550 wpm instead of ~2450 wpm running on macOS Sierra Chrome 54)
  • Changing interval to 0 seemed to make no discernible difference, so I left it at 1 to emphasize the asynchrony of the script.
// ==UserScript==
// @name         Auto Typer
// @namespace    http://tampermonkey.net/
// @version      0.1
// @description  Automatically takes typing test in an infinite loop
// @author       Patrick Roberts
// @match        http://www.typingtest.com/test.html*
// @grant        none
// ==/UserScript==

(function() {
  'use strict';

  // select the target node
  const container = document.querySelector('#test-container');

  // configure observer to look for added children
  const config = { childList: true };

  // create an observer instance
  const observer = new MutationObserver(verifyDependencies);

  // pass in the target node, as well as the observer options
  observer.observe(container, config);

  // check for existence of elements independently of mutations
  // in case mutations occurred before observer was initialized
  verifyDependencies();

  function verifyDependencies() {
    // get references to needed elements to check for existence after each mutation
    const textarea = container.querySelector('.test-text-area');
    const editor = container.querySelector('.test-edit-area');

    // wait to initialize when needed elements are in existence
    if (textarea !== null && editor !== null) {
      // pass references to textarea and editor
      initializeAutoTyper(textarea, editor);

      // disconnect observer to end callbacks
      observer.disconnect();
    }
  }

  function initializeAutoTyper(textarea, editor) {
    // formatted string from text area
    const text = textarea.textContent.replace(/\n| +(?= )/g, '').trim().replace(/\u21a9|$/g, '\r');
    // initialize iterator
    const iterator = typer(text, editor);
    // wait 0 ms between keystrokes
    const interval = 1;

    // typer generator function
    function* typer(text, editor) {
      // want non-lexical scoping to speed up iteration
      var i;

      // initialize properties
      const descriptor = {
        // use `detail` to avoid a scoped reference in the `keyCode` getter
        detail: {
          value: text
        },
        keyCode: {
          get: function getter() {
            return this.detail.charCodeAt(i);
          }
        }
      };

      // initialize re-useable events
      const down = new KeyboardEvent('keydown');
      const press = new KeyboardEvent('keypress');

      // use descriptor because simply assigning `keyCode` does not "stick"
      Object.defineProperties(down, descriptor);
      Object.defineProperties(press, descriptor);

      // loop the test infinitely
      while (true) {
        // iterate through test string
        for (i = 0; i < text.length; i++) {
          // keydown and keypress events automatically update necessary getters by assigning `i`
          editor.dispatchEvent(down);
          editor.dispatchEvent(press);

          // pause between events to allow for page reflow
          yield;
        }
      }
    }

    // focus then type
    editor.focus();
    setInterval(iterator.next.bind(iterator), interval);
  }
})();

I am aware that generator functions are not optimized by the v8 runtime compiler, so I'm sure there are performance improvements to make, but this approach seemed to be the "cleanest" method in terms of readability to me.

One specific issue (not a bug, because the script executes as intended) is that the iteration speed steadily drops the farther along the text it goes, and then picks back up when the inner for loop is finished and restarts at the beginning of the string again. I don't know why this is happening.

Any suggestions to improve readability, performance, and code standardization are very welcome.

\$\endgroup\$
  • \$\begingroup\$ You may update your question as long as no one has answered it yet. \$\endgroup\$ – Joseph Nov 29 '16 at 19:06
  • \$\begingroup\$ @JosephtheDreamer updated \$\endgroup\$ – Patrick Roberts Nov 29 '16 at 20:04

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.