mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-09 08:04:45 +00:00
A self-contained napplet (tools/napplet-test/index.html) that calls every window.napplet.* API and renders each result on screen, for verifying the NIP-5D host end to end on a real device — including the new identity.getList/getZaps/getBadges, identity.onChanged, keys.onAction, and resource.bytes nostr: paths. publish.sh uploads it to a Blossom server (BUD-02) and publishes the NIP-5D named-napplet event (kind 35129) via nak; README documents the flow and a per-feature verification checklist. Tooling only — no app code or deps. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016ncMHuBBVHEf7spAoSssde
160 lines
8.3 KiB
HTML
160 lines
8.3 KiB
HTML
<!doctype html>
|
|
<!--
|
|
Napplet test harness — a single-file napplet that exercises every window.napplet.* API and
|
|
renders the result of each on screen, so the Android host can be verified end to end on a device.
|
|
|
|
Read-only checks run automatically on load (each triggers a one-time consent prompt per capability).
|
|
Side-effectful ops (publish / upload / pay) are behind buttons so you trigger them deliberately.
|
|
Live pushes (identity.changed, keys.action) appear in the banner at the top when they fire.
|
|
|
|
Publish it with publish.sh, then open it from Amethyst (your Apps / Napplets list, or its feed card).
|
|
-->
|
|
<html>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
<title>Napplet Test Harness</title>
|
|
<style>
|
|
body { font: 14px/1.4 system-ui, sans-serif; margin: 0; padding: 12px; background: #fff; color: #111; }
|
|
h1 { font-size: 16px; margin: 0 0 8px; }
|
|
#banner { min-height: 1.4em; padding: 8px; margin-bottom: 10px; border-radius: 8px; background: #eef; color: #224; font-weight: 600; }
|
|
.row { padding: 6px 8px; border-bottom: 1px solid #eee; display: flex; gap: 8px; align-items: baseline; }
|
|
.name { flex: 0 0 12em; font-weight: 600; }
|
|
.val { flex: 1; white-space: pre-wrap; word-break: break-word; font-family: ui-monospace, monospace; font-size: 12px; }
|
|
.ok { color: #0a0; } .err { color: #c00; } .pending { color: #888; }
|
|
section { margin-top: 14px; }
|
|
input, button { font: inherit; padding: 6px 8px; margin: 2px 0; }
|
|
input { width: 100%; box-sizing: border-box; }
|
|
button { cursor: pointer; }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<h1>Napplet Test Harness</h1>
|
|
<div id="banner">Waiting for live pushes (switch account → identity.changed; press Ctrl+S → keys.action)…</div>
|
|
<div id="results"></div>
|
|
|
|
<section>
|
|
<h1>Side-effectful (tap to run)</h1>
|
|
<input id="pub" placeholder="note content to publish (relay.publish, signs as you)" />
|
|
<button onclick="doPublish()">relay.publish</button>
|
|
<hr/>
|
|
<input id="res" placeholder="resource.bytes URL — https://… or nostr:nevent1…/note1…/npub1…" />
|
|
<button onclick="doResource()">resource.bytes</button>
|
|
<hr/>
|
|
<button onclick="doUpload()">upload.blob (uploads a tiny text blob)</button>
|
|
<hr/>
|
|
<input id="inv" placeholder="BOLT-11 invoice for value.payInvoice" />
|
|
<button onclick="doPay()">value.payInvoice</button>
|
|
</section>
|
|
|
|
<script>
|
|
var results = document.getElementById('results');
|
|
var banner = document.getElementById('banner');
|
|
|
|
function row(name) {
|
|
var r = document.createElement('div'); r.className = 'row';
|
|
var n = document.createElement('div'); n.className = 'name'; n.textContent = name;
|
|
var v = document.createElement('div'); v.className = 'val pending'; v.textContent = '…';
|
|
r.appendChild(n); r.appendChild(v); results.appendChild(r);
|
|
return v;
|
|
}
|
|
function show(v, ok, value) {
|
|
v.className = 'val ' + (ok ? 'ok' : 'err');
|
|
v.textContent = (ok ? '' : 'ERROR: ') + (typeof value === 'string' ? value : JSON.stringify(value));
|
|
}
|
|
// Run an async napplet call and render pass/fail.
|
|
function check(name, fn) {
|
|
var v = row(name);
|
|
try {
|
|
Promise.resolve(fn()).then(function (out) { show(v, true, out === undefined ? 'ok' : out); },
|
|
function (e) { show(v, false, (e && e.message) || String(e)); });
|
|
} catch (e) { show(v, false, (e && e.message) || String(e)); }
|
|
}
|
|
function flash(msg) { banner.textContent = msg + ' (' + new Date().toLocaleTimeString() + ')'; }
|
|
|
|
function run() {
|
|
if (!window.napplet) { banner.textContent = 'window.napplet is MISSING — shim not injected!'; return; }
|
|
|
|
// ---- shell negotiation ----
|
|
check('shell.supports(identity)', function () { return napplet.shell.supports('identity'); });
|
|
check('shell.supports(bogus)', function () { return napplet.shell.supports('bogus'); });
|
|
|
|
// ---- identity (read-only) ----
|
|
check('identity.getPublicKey', function () { return napplet.identity.getPublicKey(); });
|
|
check('identity.getProfile', function () { return napplet.identity.getProfile(); });
|
|
check('identity.getRelays', function () { return napplet.identity.getRelays(); });
|
|
check('identity.getFollows', function () { return napplet.identity.getFollows().then(trunc); });
|
|
check('identity.getMutes', function () { return napplet.identity.getMutes().then(trunc); });
|
|
check('identity.getBlocked', function () { return napplet.identity.getBlocked().then(trunc); });
|
|
check('identity.getList(bookmarks)', function () { return napplet.identity.getList('bookmarks').then(trunc); });
|
|
check('identity.getZaps', function () { return napplet.identity.getZaps().then(trunc); });
|
|
check('identity.getBadges', function () { return napplet.identity.getBadges().then(trunc); });
|
|
|
|
// identity.onChanged → live push when you switch / log out
|
|
try {
|
|
napplet.identity.onChanged(function (pubkey) { flash('identity.changed → ' + (pubkey || '(logged out)')); });
|
|
row('identity.onChanged').textContent = 'registered — switch account to fire';
|
|
} catch (e) { show(row('identity.onChanged'), false, e.message); }
|
|
|
|
// ---- relay (read) ----
|
|
check('relay.query(kind1 limit3)', function () {
|
|
return napplet.relay.query({ kinds: [1], limit: 3 }).then(function (evs) { return (evs ? evs.length : 0) + ' events'; });
|
|
});
|
|
check('relay.subscribe(kind1)', function () {
|
|
return new Promise(function (resolve) {
|
|
var n = 0;
|
|
var sub = napplet.relay.subscribe({ kinds: [1], limit: 5 }, function () { n++; }, function () { sub.close(); resolve(n + ' events, EOSE ok'); });
|
|
setTimeout(function () { try { sub.close(); } catch (_) {} resolve(n + ' events (timeout)'); }, 8000);
|
|
});
|
|
});
|
|
|
|
// ---- storage round-trip ----
|
|
check('storage round-trip', function () {
|
|
var key = 'k' + Date.now();
|
|
return napplet.storage.setItem(key, 'v1')
|
|
.then(function () { return napplet.storage.getItem(key); })
|
|
.then(function (got) {
|
|
if (got !== 'v1') throw new Error('getItem returned ' + got);
|
|
return napplet.storage.keys();
|
|
})
|
|
.then(function (keys) { return napplet.storage.removeItem(key).then(function () { return 'set/get/keys(' + keys.length + ')/remove ok'; }); });
|
|
});
|
|
|
|
// ---- keys: register Ctrl+S, fire via onAction ----
|
|
try {
|
|
napplet.keys.registerAction({ id: 'save', label: 'Save', defaultKey: 'Ctrl+S' }).then(function (reg) {
|
|
show(row('keys.registerAction'), true, reg);
|
|
});
|
|
napplet.keys.onAction('save', function () { flash('keys.action → save fired'); });
|
|
} catch (e) { show(row('keys.registerAction'), false, e.message); }
|
|
}
|
|
|
|
function trunc(x) { var s = JSON.stringify(x); return s.length > 200 ? s.slice(0, 200) + '…' : s; }
|
|
|
|
// ---- side-effectful handlers ----
|
|
function doPublish() {
|
|
var c = document.getElementById('pub').value || ('napplet test ' + Date.now());
|
|
check('relay.publish', function () { return napplet.relay.publish({ kind: 1, content: c, tags: [] }).then(function (ev) { return 'published ' + ev.id; }); });
|
|
}
|
|
function doResource() {
|
|
var u = document.getElementById('res').value;
|
|
if (!u) { flash('enter a URL first'); return; }
|
|
check('resource.bytes(' + u.slice(0, 24) + '…)', function () {
|
|
return napplet.resource.bytes(u).then(function (blob) { return blob.size + ' bytes, type=' + blob.type; });
|
|
});
|
|
}
|
|
function doUpload() {
|
|
var bytes = new TextEncoder().encode('napplet upload test ' + Date.now());
|
|
check('upload.blob', function () { return napplet.upload.blob(bytes, 'text/plain').then(function (url) { return url; }); });
|
|
}
|
|
function doPay() {
|
|
var inv = document.getElementById('inv').value;
|
|
if (!inv) { flash('enter an invoice first'); return; }
|
|
check('value.payInvoice', function () { return napplet.value.payInvoice(inv).then(function (pre) { return 'preimage ' + pre; }); });
|
|
}
|
|
|
|
run();
|
|
</script>
|
|
</body>
|
|
</html>
|