From ed0cd44c53c82996a9ed2bd2d763b10f4e91611e Mon Sep 17 00:00:00 2001 From: debbie Date: Sat, 30 May 2026 16:56:48 +0000 Subject: [PATCH] test: add dashboard and contracts page tests (#85) Cover empty state, populated state, active webhook filter, and stats rendering (contracts count, alerts today, per-contract alert filter) via the storage layer that drives both pages. --- lib/__tests__/dashboard.test.ts | 162 ++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 lib/__tests__/dashboard.test.ts diff --git a/lib/__tests__/dashboard.test.ts b/lib/__tests__/dashboard.test.ts new file mode 100644 index 0000000..5b8fbc6 --- /dev/null +++ b/lib/__tests__/dashboard.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, beforeEach } from 'vitest' +import { + getContracts, + saveContract, + deleteContract, + getTodayAlertCount, + addAlert, + getAlerts, +} from '../storage' +import { WatchedContract, AlertPayload } from '@/types' + +// Minimal localStorage shim for node environment +const store: Record = {} +const localStorageMock = { + getItem: (key: string) => store[key] ?? null, + setItem: (key: string, value: string) => { store[key] = value }, + removeItem: (key: string) => { delete store[key] }, + clear: () => { Object.keys(store).forEach((k) => delete store[k]) }, + get length() { return Object.keys(store).length }, + key: (i: number) => Object.keys(store)[i] ?? null, +} +Object.defineProperty(globalThis, 'localStorage', { value: localStorageMock, writable: true }) +Object.defineProperty(globalThis, 'window', { value: globalThis, writable: true }) + +function makeContract(overrides: Partial = {}): WatchedContract { + return { + id: 'c1', + label: 'Test Contract', + contract_id: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7', + network: 'testnet', + rules: [], + webhook_url: '', + created_at: Date.now(), + updated_at: Date.now(), + ...overrides, + } +} + +function makeAlert(overrides: Partial = {}): AlertPayload { + return { + label: 'Test', + contract_id: 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7', + network: 'testnet', + rule_triggered: 'AnyTransaction', + transaction_hash: 'abc123', + timestamp: Date.now(), + horizon_link: 'https://horizon-testnet.stellar.org/transactions/abc123', + ...overrides, + } +} + +beforeEach(() => { + localStorageMock.clear() +}) + +// ── Contracts list (empty / populated) ────────────────────────────────────── + +describe('contracts list — empty state', () => { + it('returns empty array when no contracts saved', () => { + expect(getContracts()).toEqual([]) + }) +}) + +describe('contracts list — populated state', () => { + it('returns saved contracts', () => { + saveContract(makeContract({ id: 'c1', label: 'Alpha' })) + saveContract(makeContract({ id: 'c2', label: 'Beta' })) + const contracts = getContracts() + expect(contracts).toHaveLength(2) + expect(contracts.map((c) => c.label)).toEqual(expect.arrayContaining(['Alpha', 'Beta'])) + }) + + it('reflects deletion', () => { + saveContract(makeContract({ id: 'c1' })) + saveContract(makeContract({ id: 'c2' })) + deleteContract('c1') + const contracts = getContracts() + expect(contracts).toHaveLength(1) + expect(contracts[0].id).toBe('c2') + }) + + it('updates existing contract in place', () => { + saveContract(makeContract({ id: 'c1', label: 'Old' })) + saveContract(makeContract({ id: 'c1', label: 'New' })) + const contracts = getContracts() + expect(contracts).toHaveLength(1) + expect(contracts[0].label).toBe('New') + }) +}) + +// ── Filter behavior: active webhooks ──────────────────────────────────────── + +describe('active webhooks filter', () => { + it('counts contracts with a non-empty webhook_url', () => { + saveContract(makeContract({ id: 'c1', webhook_url: 'https://example.com/hook' })) + saveContract(makeContract({ id: 'c2', webhook_url: '' })) + saveContract(makeContract({ id: 'c3', webhook_url: 'https://other.com/hook' })) + const contracts = getContracts() + const activeWebhooks = contracts.filter((c) => c.webhook_url).length + expect(activeWebhooks).toBe(2) + }) + + it('returns 0 when no contracts have webhooks', () => { + saveContract(makeContract({ id: 'c1', webhook_url: '' })) + const contracts = getContracts() + expect(contracts.filter((c) => c.webhook_url).length).toBe(0) + }) +}) + +// ── Stats: contracts count ─────────────────────────────────────────────────── + +describe('dashboard stats — contracts count', () => { + it('reflects the number of saved contracts', () => { + expect(getContracts().length).toBe(0) + saveContract(makeContract({ id: 'c1' })) + expect(getContracts().length).toBe(1) + saveContract(makeContract({ id: 'c2' })) + expect(getContracts().length).toBe(2) + }) +}) + +// ── Stats: alerts today ────────────────────────────────────────────────────── + +describe('dashboard stats — alerts today', () => { + it('returns 0 when no alerts exist', () => { + expect(getTodayAlertCount()).toBe(0) + }) + + it('counts only alerts from today', () => { + const todayAlert = makeAlert({ timestamp: Date.now() }) + const oldAlert = makeAlert({ + transaction_hash: 'old', + timestamp: Date.now() - 2 * 24 * 60 * 60 * 1000, // 2 days ago + }) + addAlert(todayAlert) + addAlert(oldAlert) + expect(getTodayAlertCount()).toBe(1) + }) + + it('counts multiple alerts from today', () => { + addAlert(makeAlert({ transaction_hash: 'tx1' })) + addAlert(makeAlert({ transaction_hash: 'tx2' })) + addAlert(makeAlert({ transaction_hash: 'tx3' })) + expect(getTodayAlertCount()).toBe(3) + }) +}) + +// ── Stats: alerts per contract ─────────────────────────────────────────────── + +describe('getAlerts — per contract filter', () => { + it('returns empty array for contract with no alerts', () => { + expect(getAlerts('CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA7')).toEqual([]) + }) + + it('returns only alerts for the specified contract', () => { + addAlert(makeAlert({ contract_id: 'CONTRACT_A', transaction_hash: 'tx-a' })) + addAlert(makeAlert({ contract_id: 'CONTRACT_B', transaction_hash: 'tx-b' })) + expect(getAlerts('CONTRACT_A')).toHaveLength(1) + expect(getAlerts('CONTRACT_B')).toHaveLength(1) + expect(getAlerts('CONTRACT_C')).toHaveLength(0) + }) +})