A little lightning goes a long way.
// Deterministic browser coverage: real post actions and zap state, fake payment
// settlement. No wallet funds, relays, or payment services are contacted.
// Run: bun tools/zap-feedback-test.js
import assert from 'node:assert/strict';
import puppeteer from './puppeteer.js';
const app = await Bun.file('src/app.js').text();
const domHelper = app.slice(app.indexOf('function h(tag,'), app.indexOf('// ---------------------------------------------------------------- morphing'));
const entry = `
import { messagesFeature } from './src/features/messages.js';
import { zapsFeature } from './src/features/zaps.js';
import { arkFeature } from './src/features/ark.js';
${domHelper}
const me = '1'.repeat(64), author = '2'.repeat(64), id = '3'.repeat(64);
const ev = { id, pubkey: author, kind: 1, tags: [], content: 'A little lightning goes a long way.' };
let feature, ark, arkResolve, arkReject, amount = 21, sound = true, mode = 'pending', calls = 0, toasts = [], loginPk = null;
const ui = { screen: 'wallet', chatOpen: true };
const wallet = { nostr: { pk: me }, loadFeatureState: () => ({}), saveFeatureState() {}, loadArkState() {}, registerCacheExtension() {},
nostrProfile: async () => { throw new Error('Payment unavailable'); } };
const ctx = { h, ui, wallet, render, toast: (msg) => toasts.push(msg),
zapDefaultSat: () => amount, setZapDefaultSat: (n) => amount = n,
zapSound: () => sound, setZapSound: (on) => sound = on,
brandHeader: () => null,
testPay: () => new Promise((resolve, reject) => { arkResolve = resolve; arkReject = reject; }),
showSend: () => { ui.chatOpen = false; },
hook(name, ...args) {
if (name === 'zapNpub') { calls++; if (mode === 'ark') ark.testStart(...args); return mode !== 'unavailable'; }
if (name === 'canLnPay' || name === 'arkReady') return true;
if (name === 'zapSettled') return feature.zapSettled(...args);
if (name === 'nostrLoginIdentity') return loginPk ? { pubkey: loginPk } : null;
return false;
},
};
function render() {
if (!feature) return;
document.querySelector('#actions').replaceChildren(feature.testActions(author, ev, { canZap: true }));
document.querySelector('#setup').replaceChildren(...(ui.zapSetup ? [feature.testSetup()] : []));
}
feature = messagesFeature(ctx);
ark = arkFeature(ctx);
function receipt(receiptId, pk, sats) {
feature.testReceipt({ id: receiptId, kind: 9737, pubkey: pk, created_at: Math.floor(Date.now()/1000), tags: [['e', id], ['amount', String(sats)]], content: '' });
}
receipt('base', author, 100); render();
window.test = {
get calls() { return calls; }, get toasts() { return toasts; }, get amount() { return amount; }, ui, render,
settle: (ok) => feature.zapSettled(id, ok, amount),
receipt: () => receipt('ours', me, amount),
unavailable: () => mode = 'unavailable',
setup: () => { amount = 0; mode = 'pending'; },
lightningFailure: () => zapsFeature(ctx).lnZapNpub(author, '', id, 21),
useArk: () => { mode = 'ark'; amount = 21; },
finishArk: (ok) => ok ? arkResolve() : arkReject(new Error('Ark payment failed')),
login: (pk) => loginPk = pk,
zapper: (pubkey, P) => feature.testZapper({ kind: 9737, pubkey, tags: [['e', id], ['amount', '21'], ...(P ? [['P', P]] : [])] }),
senderTag: () => ark.testSenderTag(),
};
`;
const bundle = await Bun.build({
entrypoints: ['zap-test-entry'], target: 'browser',
plugins: [{ name: 'zap-test', setup(build) {
build.onResolve({ filter: /^zap-test-entry$/ }, () => ({ path: 'entry', namespace: 'zap-test' }));
build.onLoad({ filter: /.*/, namespace: 'zap-test' }, () => ({ contents: entry, loader: 'js', resolveDir: process.cwd() }));
build.onLoad({ filter: /src\/features\/messages\.js$/ }, async ({ path }) => ({
contents: (await Bun.file(path).text()).replace("id: 'messages',", "id: 'messages', testActions: noteActions, testReceipt: noteReceipt, testSetup: zapSetupScreen, testZapper: zapperOf,"), loader: 'js',
}));
build.onLoad({ filter: /src\/features\/ark\.js$/ }, async ({ path }) => ({
contents: (await Bun.file(path).text()).replace("id: 'ark',", `id: 'ark', testSenderTag: zapSenderTag, testStart(...args) {
connectArk = async () => ({});
lookupArkZapTarget = async () => ({ status: 'ready', address: 'test' });
performArkZap = ctx.testPay;
startNpubPay(...args);
},`), loader: 'js',
}));
} }],
});
assert(bundle.success, bundle.logs.join('\n'));
const css = await Bun.file('src/style.css').text();
const js = await bundle.outputs[0].text();
const html = ` A little lightning goes a long way.
const { DataTypes } = require('sequelize');
const attributes = {
id: {
type: DataTypes.INTEGER(11),
primaryKey: true,
autoIncrement: true,
allowNull: false
},
user_id: {
type: DataTypes.INTEGER(11),
references: { model: User, key: 'id' },
allowNull: false,
comment: "Referral granted to this user (updated when token used)"
},
username: {
type: DataTypes.STRING(255),
allowNull: false,
},
gravitar: {
type: DataTypes.STRING(255),
},
fiat: {
type: DataTypes.BOOLEAN,
defaultValue: true,
allowNull: false,
comment: "What is this for ?"
},
default_crypto_unit: {
type: DataTypes.ENUM,
values: ['SAT', 'BTC'],
defaultValue: 'SAT',
allowNull: false
},
default_currency_code: {
type: DataTypes.INTEGER(11),
references: { model: Currency, key: 'code' },
defaultValue: "CAD",
allowNull: false
},
currencies: {
type: DataTypes.JSON,
allowNull: false,
defaultValue: ["USD"],
comment: "List of currency options to show for this user",
}
}
const options = {
tableName: "user_preferences",
comment: "settings that can be controlled by users themselves",
timestamps: true,
createdAt: 'created_at',
updatedAt: 'updated_at',
indexes: [
{fields: ['user_id']}
]
};
db["UserPreference"] = db.define("user_preferences_model", attributes, options);