0416354c06
Serves the userscript from the backend so Violentmonkey auto-updates it, plus two panel fixes.
## Backend: `GET /u/{token}/manga-bookmark.user.js`
The script is read off disk per request from `USERSCRIPT_PATH` and streamed back with its `@version` line rewritten.
- **Token in the path, not a header.** Violentmonkey's update poll sends no `Authorization` header, and the script embeds `API_TOKEN` in plain text — an open URL would hand that token to anyone who guessed it. Compare is constant-time.
- **404, never 401**, for both a wrong token and a missing file: a prober learns nothing about whether the route exists.
- Registered outside `withAuth` and outside the `WEB_PASSWORD` gate, so the script is installable on a deployment that never enabled the web UI.
- Stdlib only (`crypto/subtle`, `os`, `regexp`) — no new Go dependencies.
**The served `@version` is derived from the file's mtime** (`YYYY.MM.DD.HHMM`, UTC), discarding whatever the file body says. Violentmonkey only updates when the served version sorts higher than the installed one, so a body-derived version means one typo or accidental downgrade freezes updates forever. An mtime-derived version is monotonic by construction. A file with no `@version` line is served byte-identical. `os.Stat` runs before `os.ReadFile`, so a concurrent edit can only serve new content under an old stamp — which self-heals on the next poll — never the reverse.
## Bindmount
`./userscript` is bindmounted read-only at `/userscript`. The script is deliberately **not** copied into the image: the build context stays `./backend`, and widening it would churn every `COPY` path for a file the mount always supplies. Editing the file on the VPS is live on the next poll — no rebuild, no restart. `git pull` restores the committed version, so a redeploy always ships the repo's script; checkout sets mtime to now, so even a rollback serves a *higher* version and is adopted. Without the mount the endpoint 404s and logs it; bookmark sync is unaffected.
`@downloadURL` / `@updateURL` are literal URLs in the metadata block — it is parsed before any JS runs, so `API_BASE`/`API_TOKEN` cannot be interpolated. The token was already committed in this file, so this adds no new exposure.
## Userscript UI
- **Card actions moved under the subtitle.** Only the cover and the title continue reading now; the subtitle and the action row are inert siblings in the text column. A thumb that misses ★ lands on nothing, and Remove is never inside a link.
- **Loading spinner** while the first fetch is in flight — the panel used to read as frozen on the first open after a cold start. It draws only when there is nothing cached to draw instead, so a populated list never flaps.
## Verification
- `go test -count=1 ./...` — ok, 7.070s
- `node --check` clean; `node --test userscript/test/logic.test.js` — 14/14
- Live `docker compose` smoke: `/healthz` 200, wrong token 404, script served with a stamped `@version 2026.07.28.1057` and both metadata URLs present; `touch`ing the file advanced the served version to `2026.07.28.1100` with no restart.
Layout and spinner are verified on-device — there is deliberately no DOM test harness.
Reviewed-on: #8
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
92 lines
4.2 KiB
Markdown
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`/`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.
|