Files
mangaBookmark/.claude/skills/testing-the-userscript/SKILL.md
T
sulthan cc0fa92a1a feat(userscript): render Covers from the public route (#60)
Both userscripts stop scraping Covers and stop putting one on the wire.
A scraped address has no rendering path left now that the backend
acquires, stores and serves every Cover from its own origin (ADR-0007),
and keeping one would reintroduce third-party URLs into exactly the
place #47 came from.

- Adapters no longer read og:image / meta[name=image], and comix's
  img[alt] cover scan and novelfull's metaName helper are deleted.
- apiPut strips `cover` off every outgoing body, so a cached row's
  address (already ours) never travels back either. The server ignores
  the field regardless.
- Both card renderers swap in the existing `.cover.ph` placeholder when
  the image fails to load, so a failure looks designed rather than
  broken - the other half of what #47 reported.
- The deleted scraping's test cases go with it: the comix cover cases,
  the img[alt] and meta[name] stub branches, and the stale og:image
  fixtures. Export lists are unchanged; nothing cover-specific was
  exported.

The cover route itself is already public and uncredentialed, with the
immutable cache directive and 404-for-unknown covered by the tests that
landed with #59.
2026-08-10 04:20:23 +07:00

4.2 KiB

name, description
name description
testing-the-userscript 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

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 (covers are the backend's, never scraped)
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:

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.
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.