aboutsummaryrefslogtreecommitdiffstats
path: root/tools
diff options
context:
space:
mode:
authorAdam Soltys <[email protected]>2026-08-01 22:49:26 -0700
committerAdam Soltys <[email protected]>2026-08-01 22:49:26 -0700
commit232e2935a3c390ef78f670d978ff98e934693526 (patch)
tree5060df4554bbcef7f15f2a1f7289545b773781e0 /tools
parentc8e6de17311e3eaa18a73c2aa0688c80e89db254 (diff)
downloadcoinosv3-232e2935a3c390ef78f670d978ff98e934693526.tar.gz
coinosv3-232e2935a3c390ef78f670d978ff98e934693526.zip
Restore two helpers an earlier edit deleted, and guard the class of bug
A bulk replacement removed lookupMine() and the flag along with the block it was rewriting, so names setup threw at runtime — the kind of break every existing test happily passed over because nothing ever constructed a feature and started it. tools/features-smoke-test.js does exactly that: builds every feature against a stubbed wallet with the network cut, runs init/render/stop, and fails on undefined identifiers. It caught the second missing helper immediately. Co-Authored-By: Claude Fable 5 <[email protected]>
Diffstat (limited to 'tools')
-rw-r--r--tools/features-smoke-test.js77
1 files changed, 77 insertions, 0 deletions
diff --git a/tools/features-smoke-test.js b/tools/features-smoke-test.js
new file mode 100644
index 0000000..3bc7324
--- /dev/null
+++ b/tools/features-smoke-test.js
@@ -0,0 +1,77 @@
+// Every feature must survive being built, started, rendered and stopped
+// against a stubbed wallet — the cheap check that catches typos and
+// undefined helpers that only bite at runtime in the browser.
+// Run: bun tools/features-smoke-test.js
+import { buildFeatures } from '../src/features/index.js';
+
+let fails = 0;
+const check = (name, ok, detail = '') => {
+ console.log(` ${ok ? '✓' : '✗'} ${name}${detail ? ' — ' + String(detail).slice(0, 160) : ''}`);
+ if (!ok) fails++;
+};
+
+// Nothing may reach the network in a smoke test; failing fast also exercises
+// each feature's error path, which is where the runtime typos hide.
+const realFetch = globalThis.fetch;
+globalThis.fetch = async () => { throw new Error('offline (smoke test)'); };
+globalThis.window = undefined;
+globalThis.localStorage = {
+ _d: {}, getItem(k) { return this._d[k] ?? null; }, setItem(k, v) { this._d[k] = String(v); },
+ removeItem(k) { delete this._d[k]; }, key(i) { return Object.keys(this._d)[i] ?? null; },
+ get length() { return Object.keys(this._d).length; },
+};
+
+const errors = [];
+const store = {};
+const wallet = {
+ watchOnly: false, offline: false, mnemonic: 'x '.repeat(11) + 'y',
+ utxos: [], txs: [], receive: [], change: [],
+ account: () => null,
+ loadFeatureState: (n, fb) => JSON.parse(store[n] || 'null') || fb,
+ saveFeatureState: (n, st) => { store[n] = JSON.stringify(st); },
+ _cacheKey: () => 'smoke', saveCache: () => {},
+ nostrPubkey: () => 'ab'.repeat(32),
+ nostrNpub: () => 'npub1smoketestsmoketestsmoketestsmoketestsmoketestsmoke',
+ nostrSign: (e) => ({ ...e, id: 'ab'.repeat(32), pubkey: 'ab'.repeat(32), sig: 'cd'.repeat(64) }),
+ registerCacheExtension: () => {}, registerRealtimeHook: () => {}, registerLoadHook: () => {},
+ registerScanHook: () => {}, registerCacheSavedHook: () => {}, registerCoinLock: () => {},
+ loadArkState: () => null, saveArkState: () => {}, _arkNs: () => 'ns', _arkKey: () => 'k',
+ _arkHoldKey: () => 'h', _arkVtxoOwner: () => null, adoptArkState: () => null,
+ freshReceive: () => ({ address: 'bc1qsmoke' }), freshChange: () => ({ address: 'bc1qchange' }),
+ balance: () => ({ spendableSat: 0 }), emit: () => {}, on: () => {},
+};
+const ctx = {
+ h: (tag, attrs, ...kids) => ({ tag, attrs, kids }),
+ ui: {}, render: () => {}, wallet,
+ hook: () => null, openMnemonic: async () => {},
+ fmtAmount: String, unitLabel: () => 'sats', unitTag: () => 'sats', getUnit: () => 'sats',
+ parseAmount: (v) => parseInt(v, 10) || 0,
+ copyBtn: () => null, pasteBtn: () => null, toast: () => {}, blankSend: () => ({}),
+ openExternal: () => {},
+};
+
+let features = [];
+try {
+ features = buildFeatures(ctx);
+ check('all features construct', features.length > 0, `${features.length} features`);
+} catch (e) { check('all features construct', false, e.message); }
+
+// Surface async failures (an undefined helper inside an async init shows up
+// as an unhandled rejection, not a throw).
+process.on('unhandledRejection', (e) => errors.push(String(e && e.message || e)));
+
+for (const f of features) {
+ for (const m of ['init', 'settingsCards', 'receiveModes', 'historyEntries', 'stop']) {
+ if (typeof f[m] !== 'function') continue;
+ try { f[m](); } catch (e) { errors.push(`${f.id}.${m}: ${e.message}`); }
+ }
+}
+await new Promise((r) => setTimeout(r, 300)); // let async inits settle
+
+const runtime = errors.filter((e) => /is not defined|is not a function|Cannot read/.test(e));
+check('no undefined identifiers at runtime', runtime.length === 0, runtime.join(' | '));
+if (errors.length && !runtime.length) console.log(` (${errors.length} benign offline error(s) ignored)`);
+
+globalThis.fetch = realFetch;
+console.log(fails ? `\n❌ ${fails} failure(s)` : '\n✅ features survive a cold start');
+process.exit(fails ? 1 : 0);