Best practices for testing userscripts?

Okay, the WK community probably isn’t the best forum for this kind of question, but the only scripts I’ve written are for Wanikani, so here goes:

I’m a novice at javascript but I’ve recently been building increasingly complex user scripts, and I’m having a great time learning.

To my observation, the biggest differences between professional and amateur code boils down to:

  • Code reviews, and
  • Automated testing

There are lots of tools to help with the latter, but it looks like the Jest testing framework is extremely popular and offers everything I’d want for unit/integration/e2e testing.

I used to really love the “red-light, green-light, refactor” style of test-driven development the last time I did any semi-serious programming (in ruby). I’d love to get back to it with javascript.

I’d really like to write some Jest tests for my Ganbarometer user script, but I’m kinda stumped regarding the best way to import my code into a test script.

Jest and other frameworks appear to be oriented toward React/Vue/Angular types of development frameworks with multiple source files and exported modules/components.

Since a user script is usually one monolithic file (a single IIFE in fact) with a bunch of imperative code, I’m unsure how to require my script such that I can access my functions/classes/objects/methods/variables in my test scripts (and not execute any immediate, imperative code).

Jest expects your code to be written something like

function sum(a, b) {
  return a + b;
}
module.exports = sum;

Instead of a typical imperative script like:

(function () {
  "use strict";
  function sum(a, b) {
    return a + b;
  }
  console.log(sum(2, 3));
  // imperatively do a bunch of other DOM manipulations, etc.
})();

Any suggestions on how to restructure my scripts such that I can require them in my test files? I’m unclear on how to “reach into” the functions, etc. that I define in the IIFE.

I think I need to put all of my preliminary object/class/function definition stuff into a separate file that I require in the top-level user script (using Tampermonkey’s @require directive). Is that the best practice?

Any and all suggestions/advice are more than welcome. I really have very little idea what I’m doing: please give examples if you have any specific advice! :slight_smile:

Thanks in advance!

1 Like

I’m not aware of any WK scripters who use test frameworks for WK userscripts, but maybe someone has. That being said, I did somewhat automate my environment for developing and deploying the Wanikani Open Framework. Rather than describe what I did, [here] is something similar. (I used SlickEdit, which is a commercial editor. This link uses VS Code, which is free).

Since I’m not primarily a web developer, my experience with javascript test frameworks is relatively minimal. I’m sure there are plenty of people on WK who do web development for a living and could give better ideas for test workflows in the browser.

But in the meantime, I’ll make a suggestion. Maybe you could expose a test interface somewhat like this:

window.myscript = {};

(function (gobj) {
   gobj.test = {
      func1,
      func2,
      func3,
   };

   function func1() { console.log('Function 1'); }
   function func2() { console.log('Function 2'); }
   function func3() { console.log('Function 3'); }

   function private_func1() { console.log('Private function 1'); }

})(window.myscript);

myscript.test.func1();

gobj is short for “global object”. I use it as a generic export in my scripts for things that I either want to expose for other users to use, or just because it’s something I might want to access from the console while a script is running. Maybe it would also be a good way to expose a test interface.

2 Likes

Geez. Could have fooled me.

Ooooh. I’m on the cusp of understanding this. I’ll play with it tomorrow.

Thanks for the reply!

1 Like

Heh… I’m an embedded systems engineer. When I code, it’s mostly in C/C++, and runs on microprocessors inside various industrial or military equipment. About half of my web-oriented experience is on Wanikani userscripts. The other half was a web framework I built way back when jQuery was just starting to catch on. That was my first Javascript experience, and it wasn’t the prettiest thing… but Javascript itself was still pretty ugly back then. :slight_smile:

3 Likes

Oh, man. Many years ago, I used to work for Mitsubishi semiconductors. I was more on the ASIC side than マイコン but I know the space.

Thanks for WKOF - I’m learning just by reading your code!

JavaScript weirds me out because it’s so … painfully evolved. The latter evolutions seem mostly sensible (ES6 etc.) but there still seem to be a lot of platypuses wandering about. I struggle to understand what’s what even before Node.js, typescript and other things start wandering into the scene.

I still can’t get over how many ways there are just to write a function, much less prototypes vs. real classes and objects. I’m also a poor enough programmer that I NEED strict type checking, truly immutable objects, and clear scoping!

1 Like

Haha! I totally understand. There are a lot of really great ideas out there, but I’m continually amazed by the amount of churn as the industry searches for the One True Way™ to develop software. I’ve been using essentially the same imperfect tools for decades, but I know every square inch of them, so I just Get Stuff Done™ :slight_smile:

2 Likes

I just want to chime in to confirm that I do not do any rigorous testing on my scripts. Other than the Heatmap most of my scripts are fairly simple and it wouldn’t really be worth the time to write tests. Because they are just userscripts and not official in any sense I don’t really care if they contain bugs or break. If something is obviously wrong someone will eventually report it and I’ll fix it

2 Likes

Oh, this is swatting flies with a sledgehammer for sure.

Test driven design (writing tests that fail first, then the code to make them pass) really does make for better code, though, if you’ve never tried it. It forces you to think through the design more, and the huge payoff comes when you start refactoring and optimizing: After coding the simplest possible thing to make the tests pass, you know the tests work, so you can hack away at your code without worrying too much about unknowingly breaking functionality.

1 Like

After much poking around, I think I at least understand enough to be dangerous.

As I’m just getting started with front-end development, I’d like to write code using modern (but proven) idioms. In particular, since all modern browsers support ECMAscript 2015 (ES6) style import and export of modules, I’d like to write any non-trivial code with multiple files, modules, and clear scoping, preferring ES6 idioms and syntax over older styles. 2015 was six coming on seven years ago! It seems silly not to at least start with ECMAscript 2015 conventions.

But as I’ve explained, I’d also like to use a test-driven development process (TDD).

The most popular Javascript testing framework, Jest, apparently still requires “commonjs” style modules, however, so I’ll configure it to use Babel to automate the conversion when running my tests.

To simplify deployment, I’ll also use webpack to bundle all of my module files into a smaller number of files that I can @require in my Tampermonkey scripts. (Rather than @requireing every individual module directly. The user script itself then becomes simple, imperative code, calling objects and methods from the bundle (where all the heavy lifting occurs).

Specific to my GanbarOmeter script, I’m planning to re-write it to import two modules that might be useful in other contexts:

  • DashboardWidgets to render and display gauges and bar graphs.
  • ReviewMetrics to retrieve, parse, calculate, and cache various things using WKOF and the Wanikani APIv2.

If I do it right, the overall user script should become much shorter, easier to understand, and more maintainable, and the code in the modules should be well-tested and easy to use in other contexts.

I write all this in the hope that someone will stop me if I’m about to make a huge mistake or have misunderstood anything important!

3 Likes

I’ve been having a grand old time wallowing in Javascript for the past few days. I’ve successfully tested the first part of my plan and have started on the second.

I doubt anyone cares, but it was pretty fun to re-write the dial-gauge and bar-graph widgets using TDD. The code is at GitHub - wrex/gbWidgets: Dial gauge and bar graph widgets for user scripts in the extremely unlikely event that anyone might want to use those elements in a script.

Verbose test results
$ jest --verbose
 PASS  __tests__/dialGauge-test.js
  DialGauge
    ✓ Should render a div.dial-gauge element (17 ms)
    ✓ Should include the innerHTML as a header (6 ms)
    ✓ Should include the data-footer attribute value as a <p> (3 ms)
    ✓ Should display an explicit data-display (2 ms)
    ✓ Should display a percentage if no data-display (3 ms)
    ✓ Should rotate the fill according to data-value (25 ms)
    ✓ Should default to 25% if no data-value (3 ms)
    ✓ Should display 0% if data-value < 0 (2 ms)
    ✓ Should display 100% if data-value > 1 (4 ms)

 PASS  __tests__/barGraph-test.js
  BarGraph
    ✓ Should render a div.bar-graph element (3 ms)
    ✓ Should include the innerHTML as a heading (2 ms)
    ✓ Should create an inner chart div (2 ms)
    ✓ Should create bars for each passed value (5 ms)
    ✓ Should add span.bar-value to each bar (4 ms)
    ✓ Should add a label to each bar (4 ms)
    ✓ Should adjust the height of each bar correctly (4 ms)

Test Suites: 2 passed, 2 total
Tests:       16 passed, 16 total
Snapshots:   0 total
Time:        1.226 s
Ran all test suites.

I created custom HTML elements called dial-gauge and bar-graph. You can pass whatever data values and titles you’d like via attributes:

<section class="container">
  <dial-gauge data-value="0.33" data-footer="147 total (<b>13</b> new)"
  >Difficulty</dial-gauge>

  <dial-gauge
      data-value="0.63"
      data-display="147"
      data-footer="5 sessions, 493 reviews"
  >Reviews/day</dial-gauge>

  <bar-graph
      data-values="[247,115,69,250,6,2,0,1,3]"
      data-labels='["10s","20s","30s","1m","1.5m","2m","5m","10m",">10m"]'
   >Review Intervals</bar-graph>
</section>

The custom elements render in a shadow-DOM. (Yesterday I couldn’t spell “Front-end Developer,” today I are one!)

I think the only way to pass styling information through the shadow is via CSS variables, so I used variables for all the colors and the font-family. You can use something like the following to style:

section.container {
  --fill-color: purple;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
}

Which should render something like:

Screen Shot 2021-10-06 at 2.20.04 PM

I am now onto creating a module to calculate review-session and workload stats using cached reviews from the API. Whee!

Recent learnings
  • Was it Java or Ada that once promised “write once, run anywhere”? The old joke was “Write once, doesn’t run anywhere.”

    I’ve discovered that Javascript takes this to a whole new level: “Write once, and it won’t run anywhere else.”

    Every version of every browser engine, the node interpreter, and umpteen different frameworks all understand different versions of the language. There is no canonical form of Javascript — we’re reduced to using transpilers like Bable to “transpile” to whatever version is likely to have the most reach (and sites like MDN and “caniuse.com” to figure that out).

  • Jest uses node.js so it also expects a particular flavor and has to use Babel to transpile “ES6” syntax.

  • I didn’t understand the value of packager tools like webpack and Rollup – I was under the impression that they were just glorified versions of zip. Now I understand that they also try to transpile modules into a form that can be used by everything.

  • Looking into it further, Rollup seems to be precisely what I’m looking for. From the “Why” section: “…the ES6 revision of JavaScript, which includes a syntax for importing and exporting functions and data so they can be shared between separate scripts. The specification is now fixed, but it is only implemented in modern browsers and not finalised in Node.js. Rollup allows you to write your code using the new module system.”

  • As a newbie to Javascript, I’d like to focus on what appears to be most likely to be gospel going forward. This appears to at least be ECMAscript 2015 (ES6) which seems to have very wide adoption in browsers (though as I’ve mentioned Jest and other tools still require transpiling).

  • As far as user scripts go, I think that instead of using the @require directive with Tampermonkey, I’m better off using import directly.

  • The next modules I write will require me to learn how to mock/stub API queries. I’ll also need to figure out testing asynchronous code. But I’m confident that TDD will help me to clean up the code significantly.

  • Stuff that manipulates the DOM requires an explicit @jest-environment jsdom directive in a comment in any Jest test for those things. That was non-obvious.

  • VScode with live-server and the Jest auto-runner is fabulously great. It’s so cool to write a new test, hit CMD-s to save my changes, see the red line under any failing tests, then go back and write code until it passes. Seeing the output rendered in a browser every time I save the code is also great. It’s equally wonderful to see an “innocuous” change to your code suddenly light up the board with a bunch of failing tests! It’s much harder to introduce bugs this way (though of course I’ll find a way).

  • I have yet how to figure out how to write decent tests for styling inside of a shadow DOM. I can’t get computedStyles() to return anything useful. I just punted and tested the styling manually … err, visually.

Onward!

4 Likes

I finally saw enough references to typescript that I spent some time looking at the truly excellent documentation.

Man! Its amazingly well-written documentation, and unsurprisingly, static type checking would have helped immensely with my barely-more-than-trivial scripts.

Typescript looks like a language I’ll be much more comfortable actually developing with (even if what I ship is “vanilla” Javascript).

I’m used to languages having versions, but I’ve always started with the most modern stable version in the past. I finally understand that that isn’t really completely feasible with JS. Surprisingly, it appears it’s the server-side, development-environment node.js stuff that currently lags behind the browsers in supporting the newer features of the language.

The “Typescript for New/JS/C#/Java/Functional Programmers” pages were like a breath of fresh air to me.

Despite my desire to just start programming with “modern vanilla Javascript” I now understand that it really does make sense to deal with an entire suite of tools, a complete development environment comprising:

ALL of these were new to me just a couple of weeks ago, so it’s been a steep climb, but I finally feel like I’m understanding how to use this crazy language!

There are other choices for almost all of these, and it is possible to avoid all of the above and just write a single monolithic “IIFE” inside of the tampermonkey code editor. But for anything more than a few lines of code, I think I like the specific toolchain above.

It’s a shame I only write one or two programs in any language every few years! (laugh)

3 Likes

Pretty sure you can write tests, if you go as far as using TypeScript.

As for myself, not really a test, but I wrote a web app to check if functions in Kanjipedia / Weblio UserScript is working properly.

Currently, I use only TypeScript, Prettier and ESBuild. If for tests, probably ts-mocha, but I don’t know much about frontend testing.

But as a project grows, tests are need anyway. Libraries / shared code would probably need testing as well. Maybe, if errors have more effect, more proactive should your tests go.

1 Like