Files
dione/test/test_tabs.js
Dione a61e0b0457 feat: multi-wallet architecture with localStorage state, EIP-712 verification, cross-tab WS leadership
- wallets.js: WalletManager state management with chain validation
- verifier.js: WalletVerifier EIP-712 signing via window.ethereum
- stream.js: WalletStreamManager with BroadcastChannel leader election, backoff + jitter
- AGENTS.md: updated with coding guidelines
2026-06-06 12:16:41 +00:00

123 lines
4.9 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { WalletManager } from '../wallets.js';
import { WalletStreamManager } from '../stream.js';
// --- 1. Infrastructure Mocks ---
const mockStorage = {};
globalThis.localStorage = {
getItem: (key) => mockStorage[key] || null,
setItem: (key, value) => { mockStorage[key] = String(value); },
clear: () => { Object.keys(mockStorage).forEach(k => delete mockStorage[k]); }
};
// Accelerate timers so leader election (200ms) and backoff happen instantly
const originalSetTimeout = globalThis.setTimeout;
globalThis.setTimeout = (callback, delay) => originalSetTimeout(callback, 1);
// Standard WebSocket Mock
class MockWebSocket {
constructor(url) {
this.readyState = 1; // Immediately OPEN
originalSetTimeout(() => { if (this.onopen) this.onopen(); }, 1);
}
send(data) { globalLastSentPayload = JSON.parse(data); }
close() { if (this.onclose) this.onclose({ code: 1000 }); }
}
globalThis.WebSocket = MockWebSocket;
// --- 2. The Cross-Tab Broadcast Router Mock ---
const activeChannels = new Set();
class MockBroadcastChannel {
constructor(name) {
this.name = name;
activeChannels.add(this);
}
postMessage(data) {
// Distribute the message to all OTHER open channel instances simulation offline cross-tab delivery
for (const channel of activeChannels) {
if (channel !== this && channel.onmessage) {
channel.onmessage({ data });
}
}
}
close() { activeChannels.delete(this); }
}
globalThis.BroadcastChannel = MockBroadcastChannel;
// Global tracking variables for assertions
let globalLastSentPayload = null;
function assert(condition, message) {
if (!condition) throw new Error(`❌ FAIL: ${message}`);
console.log(`✅ PASS: ${message}`);
}
// --- 3. The Multi-Tab Simulation Scenario ---
async function runMultiTabTest() {
console.log("🚀 Starting Multi-Tab Synchronization & Leader Election Test Suite...\n");
localStorage.clear();
// Seed initial wallet configuration
const wm = new WalletManager();
wm.addWallet("0x04f728C520C438A000f7A5E9d904F0e725FFAEFE", "base", "Shared Wallet");
// --- STEP 1: Boot First Tab ---
console.log("▶️ Initializing Tab 1...");
const tab1 = new WalletStreamManager(wm);
tab1.connect();
await new Promise(r => originalSetTimeout(r, 15)); // Await leader election window
assert(tab1.isLeader === true, "Tab 1 detected no existing leaders and claimed leadership role.");
assert(tab1.ws !== null, "Tab 1 successfully initialized a physical WebSocket network connection.");
// --- STEP 2: Boot Second Tab ---
console.log("\n▶ Initializing Tab 2 (Simulating a new browser window/tab)...");
const tab2 = new WalletStreamManager(wm);
tab2.connect();
await new Promise(r => originalSetTimeout(r, 15));
assert(tab2.isLeader === false, "Tab 2 discovered active Leader and assigned itself as a Follower.");
assert(tab2.ws === undefined || tab2.ws === null, "Tab 2 kept its local WebSocket offline to save system resources.");
// --- STEP 3: Verify Event Cross-Broadcasting Flow ---
let tab2ReceivedData = false;
tab2.on('balance_update', (msg) => {
if (msg.balance === "9999") tab2ReceivedData = true;
});
// Simulate server pushing an update specifically to Tab 1's socket connection
tab1.ws.onmessage({
data: JSON.stringify({ type: "balance_update", balance: "9999" })
});
await new Promise(r => originalSetTimeout(r, 5));
assert(tab2ReceivedData === true, "Tab 1 received WebSocket data and pushed it locally across the BroadcastChannel to Tab 2.");
// --- STEP 4: Proxy Subscription Modification Check ---
console.log("\n▶ Simulating Follower (Tab 2) subscribing to a new asset dynamically...");
const newAsset = { address: "1A1zP1eP5QGefi2DMPTfTL5SLmv7DivfNa", chain: "bitcoin" };
globalLastSentPayload = null;
tab2.subscribeToNewWallet(newAsset);
await new Promise(r => originalSetTimeout(r, 5));
assert(globalLastSentPayload !== null, "Tab 2's subscription was successfully intercepted and routed to the channel.");
assert(globalLastSentPayload.action === "subscribe_wallet", "Tab 1 successfully executed the proxy network subscription on behalf of Tab 2.");
// --- STEP 5: Graceful Failover Handover (Leader Death Scenario) ---
console.log("\n🕵 Simulating user abruptly closing Tab 1 (Leader)...");
// Simulate browser exit/unload routine for Tab 1
tab1.disconnect();
// Dispatch the exit event if your module binds directly to window event listeners
if (tab1._handleExit) tab1._handleExit();
await new Promise(r => originalSetTimeout(r, 15)); // Await emergency election frame
assert(tab2.isLeader === true, "Tab 2 caught the LEADER_DEATH signal and successfully promoted itself to Leader.");
assert(tab2.ws !== null, "Tab 2 gracefully opened a brand new physical WebSocket connection to pick up the stream.");
console.log("\n🎉 Massive Success! Cross-tab synchronization, proxy forwarding, and leader failovers function perfectly.");
}
runMultiTabTest().catch(console.error);