esc

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.json declares 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
StepDoes
{"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 shots

shots 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:

SurfaceDrawnNamed
Notch page, widgetsIn the open notch at Compact, Regular and Large width, on a display with a camera notch (outlined) and on one where Ledge is a pillwidget-main@notch-compact.png
Live activitiesBeside the notch, as the pill, and as the card below the notchactivity-backup@pill.png
Settings page, lock screen, menu barOnce: they look the same everywheresettings-page.png
ledgekit shots --shape notch --width compact   # only the tightest

Look 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.