From 16e7dce8141c1c85205ee9e6bc2979030a8dbabb Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Tue, 28 Jul 2026 18:13:13 +0700 Subject: [PATCH] docs: add testing-the-userscript project skill Documents the Node stub harness in userscript/test/logic.test.js: the four globals it installs, why document.body is left undefined, the module.exports test hook, and what is deliberately not testable (no DOM harness). Co-Authored-By: Claude Opus 5 --- .../skills/testing-the-userscript/SKILL.md | 91 +++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 .claude/skills/testing-the-userscript/SKILL.md diff --git a/.claude/skills/testing-the-userscript/SKILL.md b/.claude/skills/testing-the-userscript/SKILL.md new file mode 100644 index 0000000..e61c484 --- /dev/null +++ b/.claude/skills/testing-the-userscript/SKILL.md @@ -0,0 +1,91 @@ +--- +name: testing-the-userscript +description: Use when writing, running, or debugging tests for userscript/manga-bookmark.user.js — adding a case to logic.test.js, exporting a function for test, a test that fails with "is not a function"/undefined export, or deciding whether some userscript behaviour is testable at all. +--- + +# Testing the userscript + +`userscript/manga-bookmark.user.js` is a browser IIFE, not a module. It is tested +by `require()`-ing it into Node under a hand-written four-object browser stub in +`userscript/test/logic.test.js`. The harness covers **pure logic only** — adapters, +parsers, helpers. UI, network, and storage behaviour are verified on-device. + +## Commands + +```bash +node --check userscript/manga-bookmark.user.js # parse check, silent on success +node --test userscript/test/logic.test.js # 14 tests as of 2026-07-28 +``` + +Run both before every commit that touches the userscript. + +**Use the file path, not `node --test userscript/test/`.** The directory form +fails `MODULE_NOT_FOUND` on this machine's Node v22.22.2. Older docs and plans +still write the directory form — substitute the file path; do not try to fix it. + +## How the harness works + +The test file installs four globals **before** requiring the userscript: + +| Global | What it is | Why | +|---|---|---| +| `localStorage` | `Map`-backed stub | `loadCache`, `loadQueue`, and the key-migration IIFE touch it at module scope | +| `location` | `{href, hostname, pathname, origin}` | read during boot | +| `document` | `querySelector` for `meta[property="…"]` only, plus a no-op `addEventListener` | adapters read `og:title`/`og:image` | +| `document.body` | **left `undefined`** | this is the whole trick | + +`document.body === undefined` sends the userscript's boot block down its `else` +branch, where it waits for a `DOMContentLoaded` that never fires. `init()`, +`buildUI()`, and every `fetch` stay dormant, so nothing else needs stubbing. + +The export hook near the end of the userscript is what makes `require()` work: + +```js +if (typeof window === "undefined" && typeof module === "object" && module.exports) { + module.exports = { stripBuildHash, asura, demonic, anchorsFromHTML, statusOf }; +} +``` + +`typeof window === "undefined"` is load-bearing: under `@grant none` the script +shares page globals, so it must not clobber a page's own UMD shim. + +## Adding a test + +1. If the function isn't already exported, add it to that `module.exports` list + and to the destructuring `require` at the top of `logic.test.js`. A test + failing with `X is not a function` means you skipped this step. +2. Set `metaTags` (module-level `let` in the test file) for anything that reads + `og:` tags — it is reassigned per test, so set every tag your case needs. +3. Build locations with the `loc(href)` helper; `detect()` reads only + `pathname`, `origin`, `href`, `hostname`. +4. Keep the case pure: inputs in, value out, `assert` on the result. + +```js +test("stripBuildHash removes a trailing 8-hex suffix", () => { + assert.equal(stripBuildHash("solo-leveling-059befe1"), "solo-leveling"); +}); +``` + +## What is NOT testable here + +- **DOM, layout, Shadow DOM, the panel, the spinner.** There is no DOM harness + and **you must not add one** — no jsdom, no happy-dom, no second test file for + UI. Verified on-device (Cromite + Violentmonkey) instead. +- **`fetch`, sync, the retry queue's network behaviour.** Verified against a + running backend. +- Anything reachable only through `init()`/`buildUI()`. + +If a change's only meaningful verification is visual or on-device, say so in the +report rather than inventing coverage. + +## Gotchas + +- **New module-scope code that touches a browser API breaks every test**, not + just a new one — the `require()` runs it. Keep such work inside functions that + only `init()` calls. If you must add module-scope access, extend the stub. +- `stripBuildHash` must stay in sync with `asuraBuildHash` in `backend/store.go`; + changing one without the other silently splits series identity. +- `document.querySelector` only understands `meta[property="…"]`. Any other + selector returns `null` — extend the stub rather than working around it. +- The userscript stays GM-free (no `GM_*` APIs); a test that needs one is + testing something that can't ship.