| name | test-napplet |
| description | Use to verify a napplet before publishing - run protocol conformance with the @napplet/conformance-cli runner (real Chromium, reference shell), interpret failures, confirm OUTBOX-first boundaries, the single-file artifact, runtime guard, and CI wiring. Run after build-napplet, before shipping. |
Testing a Napplet
Conformance proves the build speaks NIP-5D correctly inside a real sandbox="allow-scripts" iframe driven by a reference shell — catching malformed envelopes, manifest problems, boot failures, and forbidden browser-authority references locally instead of after publishing. Truth: NIP-5D (https://github.com/nostr-protocol/nips/pull/2303) + NAPs (https://github.com/napplet/naps).
Sandbox Authority Contract
Testing is not green if the napplet still owns browser authority that belongs to
the shell. Treat any of these in served source or built output as a release
blocker:
fetch, XMLHttpRequest, WebSocket, relay pools, direct NIP-65 routing, or
app-owned network fanout.
localStorage, sessionStorage, IndexedDB, cookies, or other browser-local
persistence.
- External network-loaded
<script>, stylesheet, image, audio, video, font, CSS
URL, or dynamic import.
window.nostr, raw keys, app-owned signing, or app-owned encryption.
Expected replacements are resource for bytes, storage for state, outbox /
common / lists / count / dm for social Nostr behavior, relay only for
a documented relay-local escape hatch, and link for external URL opening.
Step 1 — Build first
pnpm build
Conformance runs against built output (./dist, preferring dist/index.html, falling back to ./index.html). Always test the build, not source.
Step 2 — Run conformance
napplet-conformance (from @napplet/conformance-cli) loads the build into headless Chromium, drives the protocol with a reference shell, runs the check catalog, and sets the exit code.
{ "scripts": { "test:conformance": "napplet-conformance ./dist" } }
pnpm test:conformance
npx napplet-conformance ./dist
Exit codes: 0 conformant, 1 non-conformant, 2 usage/runtime error.
For a visual, live report (like vitest --ui):
napplet-conformance --ui ./dist
Step 3 — Interpret failures
Failures map to a NIP-5D / NAP requirement — fix the napplet, not the check. Common classes:
- Boot failure — the runtime did not inject
window.napplet before app code ran, or a top-level throw blocked boot.
- Malformed envelope — a message that isn't a valid
{ type: "domain.action", … } for its NAP. Re-check arguments to the window.napplet.* call.
- Manifest problem — missing/invalid
napplet-type meta or manifest tags. Confirm the vite-plugin ran and nappletType is set.
- Forbidden global — the bundle references
fetch, localStorage, window.nostr, XMLHttpRequest, WebSocket, etc. Replace with resource.bytes / storage / relay. (Static scan — even unreachable references flag.)
A check that fails a spec-faithful napplet but passes only this toolchain is a tooling bug — flag it; do not work around it.
Step 4 — Boundary Audit
Before publishing, inspect source and built output for wrong-layer code:
| Check | Expected |
|---|
| Normal social reads/publishes | outbox, common, lists, count, or dm, not default relay |
Any relay.subscribe / relay.publish use | A comment or spec note naming the relay-local escape hatch |
| Signing/encryption | No window.nostr, private keys, local signing, or app-owned encryption |
| Relay routing | No app-owned NIP-65 resolver, relay pool, WebSocket relay client, or relay fanout policy |
| Network and media bytes | No direct fetch, XMLHttpRequest, WebSocket, or external <img src>; use resource |
| Persistence | No localStorage, IndexedDB, cookies, or direct filesystem state; use storage |
| Optional-domain fallback checks | window.napplet?.domain after runtime injection; no window.napplet.shell, shell.ready(), shell.supports(...), or generic service probing |
If a direct relay use remains, prove why outbox, common, lists, count, and dm do not fit. Otherwise refactor before shipping.
Also scan for direct browser authority with exact strings before completion:
grep -RInE "fetch\\s*\\(|XMLHttpRequest|WebSocket|localStorage|sessionStorage|indexedDB|document\\.cookie|window\\.nostr|<img[^>]+src=['\\\"]?https?:|<script[^>]+src=['\\\"]?https?:|<link[^>]+href=['\\\"]?https?:" src dist index.html
Any hit in authored or bundled napplet code must be removed or explained as a
tooling false positive before shipping.
Step 5 — Confirm the artifact & guard
- Single file: the published napplet is one
index.html with inline JS (opaque-origin srcdoc, no external <script src>). Use artifactMode: 'single-file'. Verify dist/index.html is self-contained.
- Runtime guard: opened without a runtime it shows an explanatory modal rather than failing silently. For standalone manual testing, opt out before the shim loads via
window.__NAPPLET_ALLOW_STANDALONE__ = true or <meta name="napplet-allow-standalone">.
Step 6 — Scenario Smoke Tests
Exercise the feature against the NAP domains it declares:
- Signed-out path: identity returns
""; app degrades without publish/list/dm actions.
- OUTBOX path: current option fields only:
outbox.getEvent uses author, relays, timeoutMs; outbox.query / outbox.subscribe use authors, relays, limit, timeoutMs; outbox.publish uses relays, toOutbox, toInboxes. No strategy, subscribe live, publish timeoutMs, or outbox.eose.
- Optional-domain path: remove an optional domain from the mock runtime and verify fallback UI.
- Theme path: run with dark and light themes, verify
html/body/app-root
backgrounds as well as text, surfaces, borders, and primary colors, then
emit a theme change and verify the whole surface repaints. A dark card on a
white page is a failed theme integration.
- Escape hatch path: if
relay is used, test the explicit relay-local behavior and teardown.
Step 7 — Runtime Preview
Conformance is not a user preview. When a local shell is available, launch the
project through Paja and report its URL:
napplet paja -- pnpm vite --host 127.0.0.1
The final preview link must be the Paja/runtime URL, not the underlying Vite
server URL. If napplet, Kehto, or Paja is not installed, report the exact
missing prerequisite and leave the manual runtime check open; never present
the raw Vite URL as a valid napplet preview.
Step 8 — CI
Run conformance headless in CI: cache Playwright's Chromium, build, then run the CLI. See .github/workflows/conformance.yml in the napplet monorepo for the reference pattern.
- run: pnpm build
- run: npx napplet-conformance ./dist
Green conformance + a self-contained single-file build = ready to publish.