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

92 lines
4.2 KiB
Markdown

---
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` (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:
```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.