// Copyright (c) 2017-2026 Lethean (https://lt.hn) // // Licensed under the European Union Public Licence (EUPL) version 1.2. // SPDX-License-Identifier: EUPL-1.2 package tui import ( "strings" "testing" tea "github.com/charmbracelet/bubbletea" "dappco.re/go/core/blockchain/types" ) func TestExplorerModel_View_Good_BlockList(t *testing.T) { c := seedChain(t, 5) m := NewExplorerModel(c) out := m.View(80, 20) // Should show block heights. if !strings.Contains(out, "4") { t.Errorf("view should contain top block height 4, got:\n%s", out) } if !strings.Contains(out, "0") { t.Errorf("view should contain genesis height 0, got:\n%s", out) } } func TestExplorerModel_View_Good_Empty(t *testing.T) { c := seedChain(t, 0) m := NewExplorerModel(c) out := m.View(80, 20) if !strings.Contains(out, "no blocks") && !strings.Contains(out, "empty") { t.Errorf("empty chain should show empty message, got:\n%s", out) } } func TestExplorerModel_Update_Good_CursorDown(t *testing.T) { c := seedChain(t, 10) m := NewExplorerModel(c) // Move cursor down. m.Update(tea.KeyMsg{Type: tea.KeyDown}) if m.cursor != 1 { t.Errorf("cursor: got %d, want 1", m.cursor) } } func TestExplorerModel_Update_Good_CursorUp(t *testing.T) { c := seedChain(t, 10) m := NewExplorerModel(c) // Move down then up. m.Update(tea.KeyMsg{Type: tea.KeyDown}) m.Update(tea.KeyMsg{Type: tea.KeyDown}) m.Update(tea.KeyMsg{Type: tea.KeyUp}) if m.cursor != 1 { t.Errorf("cursor: got %d, want 1", m.cursor) } } func TestExplorerModel_Update_Good_CursorBoundsTop(t *testing.T) { c := seedChain(t, 5) m := NewExplorerModel(c) // Try to go above 0. m.Update(tea.KeyMsg{Type: tea.KeyUp}) if m.cursor != 0 { t.Errorf("cursor should not go below 0, got %d", m.cursor) } } func TestExplorerModel_Init_Good(t *testing.T) { c := seedChain(t, 5) m := NewExplorerModel(c) cmd := m.Init() if cmd != nil { t.Error("Init should return nil (block list loads synchronously)") } } func TestExplorerModel_Update_Good_Refresh(t *testing.T) { c := seedChain(t, 5) m := NewExplorerModel(c) // A NodeStatusMsg should trigger a refresh of the block list. m.Update(NodeStatusMsg{Height: 5}) out := m.View(80, 20) if !strings.Contains(out, "4") { t.Errorf("view should show height 4 after refresh, got:\n%s", out) } } func TestExplorerModel_Update_Good_EnterBlockDetail(t *testing.T) { c := seedChain(t, 5) m := NewExplorerModel(c) // Cursor starts at 0 which is the newest block (height 4). m.Update(tea.KeyMsg{Type: tea.KeyEnter}) if m.view != viewBlockDetail { t.Errorf("view: got %d, want viewBlockDetail (%d)", m.view, viewBlockDetail) } out := m.View(80, 20) if !strings.Contains(out, "Block 4") { t.Errorf("block detail should contain 'Block 4', got:\n%s", out) } } func TestExplorerModel_Update_Good_EscBackToList(t *testing.T) { c := seedChain(t, 5) m := NewExplorerModel(c) // Enter block detail then press Esc to go back. m.Update(tea.KeyMsg{Type: tea.KeyEnter}) if m.view != viewBlockDetail { t.Fatalf("expected viewBlockDetail after Enter, got %d", m.view) } m.Update(tea.KeyMsg{Type: tea.KeyEsc}) if m.view != viewBlockList { t.Errorf("view: got %d, want viewBlockList (%d)", m.view, viewBlockList) } } func TestExplorerModel_Update_Good_PgDown(t *testing.T) { c := seedChain(t, 50) m := NewExplorerModel(c) m.height = 20 m.Update(tea.KeyMsg{Type: tea.KeyPgDown}) if m.cursor <= 10 { t.Errorf("cursor after PgDown: got %d, want > 10", m.cursor) } } func TestExplorerModel_Update_Good_Home(t *testing.T) { c := seedChain(t, 50) m := NewExplorerModel(c) // Move cursor down 10 times. for i := 0; i < 10; i++ { m.Update(tea.KeyMsg{Type: tea.KeyDown}) } if m.cursor != 10 { t.Fatalf("cursor after 10 downs: got %d, want 10", m.cursor) } m.Update(tea.KeyMsg{Type: tea.KeyHome}) if m.cursor != 0 { t.Errorf("cursor after Home: got %d, want 0", m.cursor) } } func TestExplorerModel_ViewBlockDetail_Good_CoinbaseOnly(t *testing.T) { c := seedChain(t, 3) m := NewExplorerModel(c) // Enter block detail — test blocks have no TxHashes. m.Update(tea.KeyMsg{Type: tea.KeyEnter}) out := m.View(80, 20) if !strings.Contains(out, "coinbase only") { t.Errorf("block detail should contain 'coinbase only' for blocks with no TxHashes, got:\n%s", out) } } func TestDescribeTxInput_Good(t *testing.T) { tests := []struct { name string input types.TxInput want string }{ { name: "genesis", input: types.TxInputGenesis{Height: 12}, want: "coinbase height=12", }, { name: "to_key", input: types.TxInputToKey{ Amount: 42, KeyImage: types.KeyImage{0xaa, 0xbb, 0xcc, 0xdd}, }, want: "to_key amount=42 key_image=aabbccdd", }, { name: "htlc", input: types.TxInputHTLC{ HTLCOrigin: "origin-hash", Amount: 7, KeyImage: types.KeyImage{0x10, 0x20, 0x30, 0x40}, }, want: `htlc origin="origin-hash" amount=7 key_image=10203040`, }, { name: "multisig", input: types.TxInputMultisig{ Amount: 99, SigsCount: 3, MultisigOutID: types.Hash{0x01, 0x02, 0x03, 0x04}, }, want: "multisig amount=99 sigs=3 out=01020304", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := describeTxInput(tt.input) if got != tt.want { t.Fatalf("describeTxInput() = %q, want %q", got, tt.want) } }) } }