The harness
Run your droplet in Droppy's Settings panel, without installing it.
DroppyKitHarness opens your droplet in a window that is Droppy's Settings panel: the window chrome, the sidebar rows, the page opener and the content column are the app's own source files, copied into the SDK. What it adds is room — the window is larger than the real one, so a solo and a paired widget fit side by side without scrolling.
It never touches the system. Preferences live in memory, permissions are whatever you toggle, and workspace.open records the URL instead of opening it, so a droplet under test cannot fling Safari at you every time you press its button. That is also why you can run it while Droppy itself is running: the two never fight over shortcuts, the pasteboard, or the notch.
Adding it
One file in an executable target:
import DroppyKit
import DroppyKitHarness
@main
struct WorldClockHarness: DropletHarnessApp {
static func makeDroplet() -> any Droplet { WorldClockDroplet() }
}Important. The file must not be called main.swift. Swift treats that name as top-level code, which cannot coexist with @main.
Running it
droppykit run # or Scripts/run-harness.sh WorldClockHarness
droppykit run --page shelf-widget # open straight onto one surfaceThe harness is built into a real signed .app and launched with open. A bare swift run produces an unbundled binary, and macOS gives one of those fallback window chrome — visibly not the panel you are trying to match.
The pages
| Page | Shows |
|---|---|
| Overview | Identity, icon, creator, and which surfaces you provide against which you declared |
| Shelf widgets | Every widget solo and paired, at its own declared widths |
| Live activity | The compact row in a simulated notch, the expanded detail, and the seat |
| Expanded surface | The takeover at the size you return, and the chrome it suppresses |
| Settings pane | Your pane in the real content column |
| Lock screen | Your published row |
| Menu bar | Your extra and its menu |
| Capabilities | A switch per declared capability, and a warning for undeclared ones |
| Stored values | Every key you have written, with the key Droppy would really use |
| Activity | Every host call and log line, in order |
What it catches
Surfaces you declared but did not implement, and the reverse. The Overview page compares your conformances against droplet.json and names the difference. The two disagreeing is the single most common reason a droplet validates locally and then does nothing once installed.
Refusals. Turn a capability off and the Activity page shows the call being refused, with the capability that was missing — which is exactly what a user who declined a permission will experience.
The notchless case. Toggle Hardware notch off on Overview and every surface re-renders as a floating island. Content that assumes wings either side of a cut-out has nowhere to go, and this is where you find that out.
Losing the live-activity seat. Take the seat away from the Live activity page and watch what your droplet does. Losing it is normal; a droplet that keeps working as though it still had it is not.
What it does not do
It does not simulate Droppy's open and close springs. Those are the app's, and a harness guessing at them would teach the wrong timing. It also previews your icon rather than compositing it the way macOS does — if it looks wrong here it is wrong, but check Icon Composer before you submit.
DropletHarnessApp is the protocol the entry point conforms to. It lives in the DroppyKitHarness module, which ships alongside this one.