Testing an add-on
Three layers, fastest first: Core's tests, a check that runs the add-on with no Ledge, and your own Ledge drawing its surfaces. Each one catches what the one before can't.
Core's tests
An add-on made with ledgekit new keeps its logic and its surfaces in <Name>Core, so swift test can check both. Every surface type has a validate() that runs Ledge's own limits, so test each one empty and full:
@Test(arguments: [0, 1, 99_999])
func everySurfaceIsOneLedgeDraws(count: Int) throws {
let model = BatteryCyclesModel(count: count)
try model.settingsPage().validate()
try model.widget().validate()
try model.page().validate()
}ledgekit check
ledgekit check builds the add-on and runs it with no Ledge, the way Ledge would. It uses the same code Ledge uses to start an add-on and to read what it sends, so it can't disagree with the app. It reports:
- Every surface the add-on sent, and whether Ledge would take it or refuse it, and why.
- Its calls to Ledge, answered as Ledge answers them, or refused for a missing capability or outside the moments Ledge allows.
- Anything newer than
kit.minAPI, which an older Ledge couldn't read. - Surfaces
addon.jsondeclares but the add-on never sent, errors it logged, and whether it started at all.
$ ledgekit check
Checking Battery cycles 0.1.0…
✓ notch-page: sent 2, Ledge took 2
✓ settings-page: sent 2, Ledge took 2
✗ widget: sent 2, Ledge took 1
✗ Ledge refused its widget: Too large: text over 1000 characters.
1 problem.It exits 1 when Ledge wouldn't run the add-on as it is. --report report.json writes everything as JSON, for a script or a coding agent, including the last version of each surface Ledge took, as surfaces[].last: what a person would see when the check ends.
Scripts
Without a script, a check starts the add-on, opens each surface it declares and closes them again. A script plays a session: a JSON list of steps, run in order.
[
{ "wait": 1 },
{ "visible": ["notch-page"] },
{ "press": "new", "screen": "list" },
{ "edit": "body", "text": "# Shopping\nOat milk", "screen": "note-a1" },
{ "press": "back" },
{ "press": "trashAll", "on": "settings-page" }
]ledgekit check --script steps.json --report report.json| Step | Does |
|---|---|
{"wait": 1} | Waits, in seconds |
{"visible": [...]} | Tells the add-on which of its surfaces are on screen, as onVisibilityChange |
{"press": "id"} | A press. screen and item say which screen and row |
{"edit": "id", "text": "…"} | An editor's .edited; end sends .ended |
{"toggle": "id", "to": true} | A switch |
{"pick": "id", "value": "x"} | A picker |
{"change": "id", "value": 0.5} | A slider |
Steps act on the notch page unless on names another surface, like settings-page or widget.
Without its capabilities
A person can turn off any capability your add-on asks for. --deny checks it that way: Ledge refuses every call that needs one, as it would, and those refusals aren't problems. What matters is that the add-on keeps running and says what it can't do, rather than doing nothing.
ledgekit check --script copy.json --deny clipboard-write
ledgekit check --deny all• calls to Ledge: 2, 1 allowed
⊘ turned off: clipboard-write. Ledge refused 1 call; look at what the add-on showed instead.
✓ Ledge would run it as it is.all turns off every capability in addon.json; naming one it doesn't ask for is an error. The report lists the denied calls with "result": "denied", and surfaces[].last shows what the add-on said instead. With copying off, Notes' note reads “Copying is off in Ledge” in its header.
In your Ledge: developer mode
To see the add-on as Ledge draws it, turn on Developer Mode: in Ledge, open Settings, then Add-on Store, the ⋯ menu, Developer Mode. Ledge asks first, and it turns off whenever Ledge quits. While it's on, ledgekit can install and run add-ons without the trust prompt.
ledgekit run # installs it and says whether it started
ledgekit shots --out shotsshots installs the build, then Ledge draws each surface into a PNG exactly as it draws it: the notch page's current screen, every widget, the settings page, the lock screen pill and the menu bar tiles. First it tells the add-on those surfaces are on screen, as opening them would, so an add-on that works only while it's seen is drawn with what people see. Ledge draws them with the notch's own view, in every shape people meet them in:
| Surface | Drawn | Named |
|---|---|---|
| Notch page, widgets | In the open notch at Compact, Regular and Large width, on a display with a camera notch (outlined) and on one where Ledge is a pill | widget-main@notch-compact.png |
| Live activities | Beside the notch, as the pill, and as the card below the notch | activity-backup@pill.png |
| Settings page, lock screen, menu bar | Once: they look the same everywhere | settings-page.png |
ledgekit shots --shape notch --width compact # only the tightestLook at the narrowest first. Beside a camera, a notch page's header gets about 39 points a side at Compact, and an activity's text about 64. The pictures are drawn off screen, so they don't show the glass behind the notch.