On-device testing
How to publish your mini-app and exercise it against a real i99dash host on a head unit.
i99dash dev covers ~80% of the dev loop with a fixture-backed
WebView. The remaining 20% — verifying behaviour against the real
host's catalog flow and platform constraints — only happens against
an actual head unit. This guide walks the publish-and-test loop
end-to-end.
If something breaks, jump to Troubleshooting → On-device testing — each pitfall in this guide links to its symptom→cause→fix entry.
What you need
- A car running an
i99dashhost build, or a head unit you canadbinto. - The head unit's IP on Wi-Fi (Settings → Developer options → Wireless debugging shows it as
IP:port, typically port 5555). i99dashCLI signed in (i99dash whoamishows your developer account).- The mini-app you want to test, with a working
pnpm bundlestep.
Step 1 — Connect ADB
adb connect 192.168.4.72:5555 # use the IP shown by Wireless debugging
adb devices # should print `<ip>:5555 device`If the connection drops mid-session (head-unit Wi-Fi suspends
aggressively), adb kill-server && adb connect <ip>:5555 brings it
back.
Step 2 — Publish your mini-app
# Bump version + bundle
jq '.version = "0.3.7"' manifest.json > manifest.json.tmp \
&& mv manifest.json.tmp manifest.json
pnpm bundle
# Validate, build, upload, register
i99dash publishThe catalog auto-syncs on the head unit on the next mini-app launch. Do not edit the bundle directly on the device — see Catalog auto-pull on launch.
Step 3 — Install on the host
On the head unit's screen:
- Open Apps (grid icon) → Store tab.
- Tap Install on your mini-app.
- Switch to My Apps and tap your mini-app to launch.
To verify the bundle landed (debug-buildable hosts only — release
builds lock down run-as):
adb shell run-as <host-package-id> \
'find files/mini_apps -type f' | grep <your-app-id>
adb shell run-as <host-package-id> \
'cat files/mini_apps/<your-app-id>/<version>/manifest.json'Step 4 — Watch what the bridge actually returns
The on-device WebView console isn't piped to logcat. To see the host
response shape during triage, surface the SDK's cause.issues (the
ZodError it wraps) directly in your tester's UI:
} catch (e) {
setBadge(card.badge, 'err', e?.code ?? 'error');
// ZodError issues — exact field-by-field schema mismatches
const issues = e?.cause?.issues
? '\n\nZod issues:\n' + JSON.stringify(e.cause.issues, null, 2)
: (e?.cause ? '\n\nCause:\n' + String(e.cause) : '');
showResult(card.result, (e?.stack ?? String(e)) + issues);
}Two patterns to recognise:
| Issue shape | Most likely cause |
|---|---|
Every field received: "undefined" | Host returned an error envelope, not a snapshot |
One field received: "string" expected "number" | Host emits the wrong type for that field |
Required on a field that should be optional | Schema drift between the SDK you bundled and the SDK the host expects |
Step 5 — Iterate
Catalog round-trip on every change is slow. To shorten the cycle:
- For JS-only changes (UI, error messages, controller usage): bump
manifest.json,pnpm bundle,i99dash publish— ~10s end-to-end. - For schema changes (you edited a Zod schema in the SDK source): repack the SDK locally (
pnpm pack) and reinstall in your mini-app before re-publishing — see Stale strict schemas. - For pure HTML/CSS tweaks during a single session: stay in
i99dash devagainst the fixture bridge; only round-trip to the device when behaviour depends on the real host (permissions, live data, gating).
What the host gates
The host enforces a single gate on every bridge call:
| Gate | What it checks | Failure shape |
|---|---|---|
| Network egress | External fetch() targets an origin declared in manifest.network | The browser/CSP blocks the request; fetch() rejects with a TypeError |
Mini-apps run with full host capability — there is no manifest-side
permission system. If a family snapshot fails, it's because the
host's bridge handler is unimplemented for that car (catch
<Family>UnavailableError) or the data isn't currently available
(catch the family's typed unavailable error).