Skip to content

Dev Loop and Testing

DooDesch edited this page Jul 27, 2026 · 1 revision

Dev Loop and Testing

🛟 Need help or found a bug? Get support at support.doodesch.de/sideload.

Hot reload

Any file under Mods/<appId>/ overrides the copy embedded in your DLL. Save it and the page rebuilds about 250 ms after the last write - no game restart, no rebuild of your mod.

Mods/mystash/index.html
Mods/mystash/app.css
Mods/mystash/app.js

Hard rules of the loop, each of which has cost someone an hour:

  • The folder must exist when the page first builds, and this only works in a development build. If the overlay says "not watching - create Mods// to edit live", quit, create the folder, relaunch.
  • A leftover override outranks a fresh build. Once that folder exists it wins over the embedded copy for every file it holds, so rebuilding your mod appears to change nothing and you debug a version of the page you stopped editing hours ago. Copy the bundle over after every build, or delete the folder when you are done.
  • A reload drops the script engine, so JS state and every listener start over. That is deliberate - the script itself may be what changed.
  • C# changes still need a rebuild and a game restart. Only the web files hot reload.
  • An editor's UTF-8 BOM is stripped for you.

The same mechanism is what lets players reskin an app, so it is not a development-only hack.

The F9 overlay

F9 shows, per mounted page: fps, the viewport size and scale, box / rule / wired counts, the render count with the last render time in ms, the reload count, the script status with its last error, and whether the watcher is running.

The render count is the one to watch. A page that rebuilds once per change is healthy; a page whose render count climbs every frame is being told to rebuild by something that runs every frame.

Ctrl+F10 outlines every box in magenta and every text leaf in cyan.

[Sideload/layout] in the log is a full rect dump after every render - x y w h right bottom per node, plus the compiled text verbatim for leaves. When a box is the wrong size, this says so numerically instead of you squinting at a screenshot.

The font list is dumped once per session, which is where font-family names come from.

Real Chrome DevTools

Turn on the DevTools preference and Sideload speaks the Chrome DevTools Protocol on 127.0.0.1. Attach the real inspector to a page running inside the game: console, evaluate, the Elements tree with computed styles, and Page.reload re-reading your files from disk.

Setting Default What it does
DevTools false The gate. Off means nothing listens and no page can be inspected from outside the game. Anything that can reach the port can run code in your pages, so leave it off unless you are building an app.
DevToolsPort 9333 The loopback port. Change it only on a clash. Clamped 1024-65535.
DevToolsAutoOpen true Opens Chrome or Edge at the landing page once the first page mounts. Off writes the address to the log instead.
DevToolsFrontend empty Point it at your own copy of the DevTools frontend to override everything below.
DevToolsFetchFrontend true Downloads the frontend once per machine (~4.5 MB) so DevTools works offline afterwards. Never while DevTools is off, never blocking the game.

Nothing about your page leaves the machine either way: the frontend is static JavaScript talking to 127.0.0.1.

Browser preview

The same app.js and app.css can run in Chrome with a mock bridge, so layout and logic work costs no game launch. The reference app ships preview.html + s1-mock.js to copy.

The preview shell needs three rules, because the engine's defaults are not the browser's:

:where(#viewport, #viewport *) {
  display: flex;
  flex-direction: column;
  box-sizing: border-box;
}

:where() so they carry zero specificity - written plainly they would outrank every class in your stylesheet.

Chrome lays the page out with its own engine, so the preview proves your logic and roughly the look, never the exact pixels. Layout itself belongs in the headless tests below and, finally, in the game.

Headless tests

Your page's JavaScript can run against your real C# handlers, with no Unity and no game, in about a second. One second against a game launch is why this is the first thing to set up.

The pattern: a net8.0 console project that links your mod's logic files, embeds app.js as a resource, and runs it on the same Jint version with a stand-in document, s1 and console.

var app = new AppHarness();                       // runs app.js against the real handlers
app.Click(app.El("add"));
app.Input(app.El("entry"), "wire up the phone");
Check.Eq("1", app.El("count").TextContent);

Structure your mod so this is possible: keep the MelonMod entry point free of logic and put the handlers and data in their own files. Keep those files free of any engine reference - that is what lets the suite compile them without Unity and catch an accidental dependency in a second rather than in a game launch. Anything that does need the game goes in a separate folder behind an interface.

Two things to know about such a harness:

  • Adding an s1.call name means adding a case to the fake bridge's switch. Miss it and the call returns "" with no warning: your page renders an empty state and the test asserts happily on the wrong thing.
  • Assert on behaviour, then break it on purpose. A test that stays green after you delete the line it covers is not testing that line.

What headless tests cannot cover: layout, text measurement, painting, anything touching TextMeshPro. A suite of green tests still shipped a text box one line too short. Verify visuals in the game.

The reference app's suite is a working example, at Workspace/Tests/WhatsDab.Tests in the development monorepo.

Clone this wiki locally