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 surface

The 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

PageShows
OverviewIdentity, icon (and the built icon in all six icon styles), creator, and which surfaces you provide against which you declared
Shelf widgetsEvery widget solo and paired, at its own declared widths; wrapped around the camera when it opts in; the wash controls to use
Live activityThe compact row in a simulated notch, the expanded detail, and the seat
Expanded surfaceThe takeover at the size you return, and the chrome it suppresses
Settings paneYour pane in the real content column
Lock screenYour published row
Menu barYour extra as a Command Center tile, Small and Medium, and the detail it opens at the panel's width
CapabilitiesA switch per declared capability, and a warning for undeclared ones
Stored valuesEvery key you have written, with the key Droppy would really use
ActivityEvery 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.

A row under the camera. A widget that declares notchWrap gets a third stage on the Shelf widgets page: alone on a notched shelf, raised beside the camera, with the ShelfWidgetNotchWrap the host would hand it, computed the host's way for the simulated notch, and the housing outlined over it. A half of the header that strays under the camera shows there, and so does a widget that offsets its row on top of the host's.

The controls to use. Under the stages, the Shelf widgets page draws the wash controls on the shelf's black at every size Droppy uses (the neutral and accent disc at 34, 28 and 24, the pill at 34 and 28), each labelled with the style that draws it. See Design guidelines.

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.

Pictures of every surface

droppykit run -- --shots ./shots
droppykit run -- --shots ./shots --shot-height 1900

--shots renders every harness page to a PNG and exits, without ever putting a window on screen. A relative directory is taken from your package root, so ./shots lands beside Package.swift. The images are of the real views, laid out and drawn by the same code, so they are the pictures a submission wants and the fastest way to compare a change against the previous build. The default height is the harness window's; --shot-height renders a page taller than that in one image instead of cutting it at the fold, which a page with several stages needs.

A headless run stays in the foreground and prints what it wrote, so a script or a coding agent gets the exit status and the file list rather than a window that opened somewhere else.

A verdict a tool can read

droppykit run -- --shots ./shots --report ./shots/report.json
droppykit run -- --report ./report.json

--report writes the harness's findings as JSON: the same verdicts the Overview and Capabilities pages draw, for something that cannot read a window. surfaces[] carries declared, implemented and a verdict per surface (provided, missing, undeclared, not-provided); problems lists everything the pages would flag, in words: a manifest that did not decode, an activate(host:) that threw, an id that disagrees between Swift and droplet.json, a widget that leaves a width to the host, artwork that did not load. activity and log are the Activity page, refused calls marked. With --shots the report also lists the images. A droplet is not done while problems has anything in it, which is exactly the rule the coding-agent brief gives an agent; see Build with a coding agent.

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. The page header's icon is a flat preview of the icon document, not the composited macOS icon, and while you build that is the scaffold's placeholder: Jordy designs the Store icon (see The icon). The Overview's six icon styles are the real thing, drawn by the system's renderer from the bundle droppykit build wrote into .build, on macOS 26 and later; before the first build, or on an older macOS, the page says so instead.

DropletHarnessApp is the protocol the entry point conforms to. It lives in the DroppyKitHarness module, which ships alongside this one.

See also