2026-02-23 00:00:05 +00:00
|
|
|
// 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"
|
2026-04-04 22:51:57 +00:00
|
|
|
|
|
|
|
|
"dappco.re/go/core/blockchain/types"
|
2026-02-23 00:00:05 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-23 00:05:10 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-04 22:51:57 +00:00
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|