Closes #60. Spec: #55. Originating bug: #47. Architecture: `docs/adr/0007-backend-hosts-cover-bytes.md`. Neither #47 nor #55 is closed from here. ## What this branch does The panel now renders Covers from the deployment's own origin, and both userscripts stop having an opinion about where a Cover lives. **The public route was already in place.** `GET /covers/{address}` landed with #59 (`92eba07`) and is registered on the bare mux, outside `httpmw.Auth` and outside the web UI's Discord session — `backend/main.go:210-214`, handler `backend/internal/api/handlers.go:142-158`. It reads no cookie and no header, answers `404` for an address that was never stored (and for a row whose file has gone missing — recorded-but-gone is not-found, never a fabricated body), refuses anything that is not `^[0-9a-f]{64}$` *before* the value becomes a path, and sets `Cache-Control: public, max-age=604800, immutable`. Those four properties are asserted by `backend/cover_test.go:231-278`. This branch re-verified them rather than re-implementing them; the only backend line it touches is a comment. **Both userscripts lose cover scraping entirely.** Every adapter's `cover:` field is gone, along with the two helpers that fed them: the manga script's `coverFromPage()` (the `img[alt]` DOM scan comix needed, because comix publishes no `og:image`) and the novel script's `metaName()` plus the now-callerless module-level `meta()`. Nothing under `userscript/` reads `og:image`, `meta[name=image]`, or `img[alt]` any more. **Nothing sends a cover either.** `delete body.cover` sits in `apiPut` — `manga-bookmark.user.js:486`, `novel-bookmark.user.js:275` — which is the single chokepoint every write passes through (`pushBookmark`, the retry-queue flush, `toggleFavorite`, `toggleArchive`). It operates on the `Object.assign` copy, so the in-memory row keeps the cover it renders with. This matters beyond tidiness: a Reader upgrading from an older copy has `localStorage` rows carrying third-party scraped URLs, and without the strip those would ride back up on the next write. The handler discards the field regardless (`handlers.go:53-59`) — it is permanently inert, not pending removal. **Failed loads get the designed empty state, not the broken-image glyph.** `onerror: (e) => e.target.replaceWith(el("div", { class: "cover ph" }))` on the cover `<img>` in both card renderers (`manga:1380-1390`, `novel:1134-1144`). The replacement is byte-identical to the existing no-cover branch on the very next line, so it picks up the `.cover.ph` styling already in the panel CSS — no new tokens, no new rule. `el()` routes any `on*` prop through `addEventListener`, so this is a listener, not an inline attribute string, and the swap is a `createElement` + DOM call with no markup parsing anywhere near it. This is the half of #47 that was visible on kagane. **The deleted scraping's tests went with it**: the two comix cover cases, the `pageImages` and `namedMetas` fixtures, the `img[alt]` and `meta[name=...]` stub branches, the now-dead `querySelectorAll` stub member, and every stale `og:image` fixture and `p.cover` assertion across both suites. The export lists needed no change and that was checked, not assumed — `coverFromPage` and `metaName` were module-private on `origin/main` and no cover symbol ever appeared in `module.exports`. Docs that described the deleted behaviour were corrected in the same breath, because leaving them would instruct the next agent to put the scraping back: `userscript/AGENTS.md` (adapter contract + the per-site notes for comix, kagane and novelfull), the README's adapter reference, and the userscript testing skill's stub table. ## Verification - `go test -count=1 ./...` — green across all nine packages (`backend` 29.8s, `latest`, `store`, `session`, `token`, `userscript`, `web`). - `node --check` clean on both userscripts; `node --test` on both logic suites — 46 tests, 46 pass. - `gofmt -l` clean; `go build ./...` clean. - The `onerror` swap is DOM behaviour and deliberately has no coverage in the Node harness — that harness stubs a browser precisely so it never needs a DOM, and #60 says not to invent coverage for it. It was instead exercised for real: the `el()` helper and the exact render expression were loaded into a headless Chromium with a deliberately unloadable `src`, and the resulting DOM was `<div class="cover ph"></div>`. Ad hoc, not committed. - **Not done, needs you:** the on-device criterion — a comix Series bookmarked mid-chapter showing its Cover in the panel. That needs a real install against the deployment and is the one box left unticked on #60. ## Reviewed Both `/code-review` axes ran against `cc0fa92`. Spec found no missed requirement and no scope creep; standards found the diff clean on the four areas it scrutinised (the `delete body.cover` placement, the `onerror` handler's DOM safety, comment quality, dead-code removal). Their combined findings — the dead `querySelectorAll` stub, the stale README and skill text, and the handler comment whose premise this change invalidates — are fixed in `8b58019`. ## Out of scope, deliberately The kagane-specific cover proxy still exists and still carries its session gate (#63 deletes it). The poll's blank-Cover fill (#61) and browser-backed Sites joining the pipeline (#62) are untouched. Reviewed-on: #69 Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com> Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
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 # 35 tests as of 2026-08-10
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
- If the function isn't already exported, add it to that
module.exportslist and to the destructuringrequireat the top oflogic.test.js. A test failing withX is not a functionmeans you skipped this step. - Set
metaTags(module-levelletin the test file) for anything that readsog:tags — it is reassigned per test, so set every tag your case needs. - Build locations with the
loc(href)helper;detect()reads onlypathname,origin,href,hostname. - Keep the case pure: inputs in, value out,
asserton 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 onlyinit()calls. If you must add module-scope access, extend the stub. stripBuildHashmust stay in sync withasuraBuildHashinbackend/store.go; changing one without the other silently splits series identity.document.querySelectoronly understandsmeta[property="…"]. Any other selector returnsnull— 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.