1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
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, getAccount: () => 'savings', 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);
|