1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-25 13:48:50 +08:00
termdash/widgets/treeview/treeview_test.go

572 lines
14 KiB
Go

// treeview_test.go
package treeview
import (
"image"
"testing"
"time"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/keyboard"
"github.com/mum4k/termdash/mouse"
"github.com/mum4k/termdash/terminal/terminalapi"
"github.com/mum4k/termdash/widgetapi"
)
// MockCanvas is a mock implementation of canvas.Canvas for testing purposes.
type MockCanvas struct {
Cells map[image.Point]rune
}
func NewMockCanvas() *MockCanvas {
return &MockCanvas{
Cells: make(map[image.Point]rune),
}
}
// SetCell sets a rune at the specified point.
func (mc *MockCanvas) SetCell(p image.Point, r rune, opts ...cell.Option) (bool, error) {
mc.Cells[p] = r
return true, nil
}
// Clear clears the canvas.
func (mc *MockCanvas) Clear() error {
mc.Cells = make(map[image.Point]rune)
return nil
}
// Area returns the area of the canvas.
func (mc *MockCanvas) Area() image.Rectangle {
return image.Rect(0, 0, 80, 24) // Default terminal size
}
// Write writes a string starting at the given point.
func (mc *MockCanvas) Write(p image.Point, s string, opts ...cell.Option) error {
for i, char := range s {
mc.Cells[image.Point{X: p.X + i, Y: p.Y}] = char
}
return nil
}
// MockMeta is a mock implementation of widgetapi.Meta for testing purposes.
type MockMeta struct {
area image.Rectangle
}
func NewMockMeta(area image.Rectangle) *MockMeta {
return &MockMeta{
area: area,
}
}
// Area returns the area of the widget.
func (m *MockMeta) Area() image.Rectangle {
return m.area
}
// TestNew tests the initialization of the Treeview widget.
func TestNew(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{Label: "Child1"},
{Label: "Child2"},
},
},
}
tv, err := New(
Nodes(root...),
Indentation(4),
Icons("▼", "▶", "•"), // Corrected Icons order
LabelColor(cell.ColorRed),
WaitingIcons([]string{"|", "/", "-", "\\"}),
Truncate(true),
EnableLogging(false),
)
if err != nil {
t.Fatalf("Failed to create Treeview: %v", err)
}
// Verify selectedNode is initialized to "Root"
if tv.selectedNode == nil {
t.Errorf("Expected selectedNode to be initialized, got nil")
} else if tv.selectedNode.Label != "Root" {
t.Errorf("Expected selectedNode to be 'Root', got '%s'", tv.selectedNode.Label)
}
// Verify the number of root nodes
if len(tv.opts.nodes) != 1 {
t.Errorf("Expected 1 root node, got %d", len(tv.opts.nodes))
}
// Verify indentation
if tv.opts.indentation != 4 {
t.Errorf("Expected indentation to be 4, got %d", tv.opts.indentation)
}
// Verify Icons
if tv.opts.expandedIcon != "▼" || tv.opts.collapsedIcon != "▶" || tv.opts.leafIcon != "•" {
t.Errorf("Icons not set correctly: got expandedIcon=%s, collapsedIcon=%s, leafIcon=%s",
tv.opts.expandedIcon, tv.opts.collapsedIcon, tv.opts.leafIcon)
}
// Verify LabelColor
if tv.opts.labelColor != cell.ColorRed {
t.Errorf("Expected labelColor to be Red, got %v", tv.opts.labelColor)
}
// Verify WaitingIcons
if len(tv.opts.waitingIcons) != 4 {
t.Errorf("Expected 4 waitingIcons, got %d", len(tv.opts.waitingIcons))
}
// Verify Truncate
if !tv.opts.truncate {
t.Errorf("Expected truncate to be true")
}
// Verify EnableLogging
if tv.opts.enableLogging {
t.Errorf("Expected enableLogging to be false")
}
}
// TestNextPrevious tests navigating through the nodes using Next and Previous methods.
func TestNextPrevious(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{Label: "Child1"},
{Label: "Child2"},
{Label: "Child3"},
},
},
}
tv, err := New(
Nodes(root...),
Indentation(4),
Icons("▼", "▶", "•"),
)
if err != nil {
t.Fatalf("Failed to create Treeview: %v", err)
}
// Manually set Root to be expanded to make children visible
root[0].ExpandedState = true
tv.updateVisibleNodes()
// Initially selected node should be "Root"
if tv.selectedNode.Label != "Root" {
t.Errorf("Expected selectedNode to be 'Root', got '%s'", tv.selectedNode.Label)
}
// Navigate down to "Child1"
tv.Next()
if tv.selectedNode.Label != "Child1" {
t.Errorf("Expected selectedNode to be 'Child1', got '%s'", tv.selectedNode.Label)
}
// Navigate down to "Child2"
tv.Next()
if tv.selectedNode.Label != "Child2" {
t.Errorf("Expected selectedNode to be 'Child2', got '%s'", tv.selectedNode.Label)
}
// Navigate up to "Child1"
tv.Previous()
if tv.selectedNode.Label != "Child1" {
t.Errorf("Expected selectedNode to be 'Child1', got '%s'", tv.selectedNode.Label)
}
// Navigate up to "Root"
tv.Previous()
if tv.selectedNode.Label != "Root" {
t.Errorf("Expected selectedNode to be 'Root', got '%s'", tv.selectedNode.Label)
}
// Navigate up at top; should stay at "Root"
tv.Previous()
if tv.selectedNode.Label != "Root" {
t.Errorf("Expected selectedNode to remain 'Root', got '%s'", tv.selectedNode.Label)
}
}
// TestMouseScroll adjusted to align with actual behavior
func TestMouseScroll(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{Label: "Child1"},
{Label: "Child2"},
{Label: "Child3"},
{Label: "Child4"},
{Label: "Child5"},
},
},
}
tv, err := New(Nodes(root...), Indentation(2))
if err != nil {
t.Fatalf("Failed to create Treeview: %v", err)
}
// Mock a large canvas height
tv.canvasHeight = 3
tv.updateVisibleNodes()
// Initially, scrollOffset should be 0
if tv.scrollOffset != 0 {
t.Errorf("Expected initial scrollOffset to be 0, got %d", tv.scrollOffset)
}
// Simulate mouse wheel down
mouseEvent := &terminalapi.Mouse{
Button: mouse.ButtonWheelDown,
Position: image.Point{X: 0, Y: 0},
}
err = tv.Mouse(mouseEvent, &widgetapi.EventMeta{})
if err != nil {
t.Errorf("Mouse method returned an error: %v", err)
}
// After scrolling down, scrollOffset should be updated accordingly
maxOffset := len(tv.visibleNodes) - tv.canvasHeight
if tv.scrollOffset != maxOffset {
t.Errorf("Expected scrollOffset to be clamped to %d, got %d", maxOffset, tv.scrollOffset)
}
// Simulate mouse wheel up
mouseEvent = &terminalapi.Mouse{
Button: mouse.ButtonWheelUp,
Position: image.Point{X: 0, Y: 0},
}
err = tv.Mouse(mouseEvent, &widgetapi.EventMeta{})
if err != nil {
t.Errorf("Mouse method returned an error: %v", err)
}
// After scrolling up, scrollOffset should be 0
if tv.scrollOffset != 0 {
t.Errorf("Expected scrollOffset to be clamped to 0, got %d", tv.scrollOffset)
}
}
// TestKeyboardScroll tests keyboard navigation in the Treeview
func TestKeyboardScroll(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{Label: "Child1"},
{Label: "Child2"},
{Label: "Child3"},
{Label: "Child4"},
{Label: "Child5"},
},
},
}
tv, err := New(Nodes(root...), Indentation(2))
if err != nil {
t.Fatalf("Failed to create Treeview: %v", err)
}
// Mock a canvas height of 3
tv.canvasHeight = 3
tv.updateVisibleNodes()
// Navigate to Child1
tv.Keyboard(&terminalapi.Keyboard{Key: keyboard.KeyArrowDown}, &widgetapi.EventMeta{})
if tv.selectedNode.Label != "Child1" {
t.Errorf("Expected selectedNode to be 'Child1', got '%s'", tv.selectedNode.Label)
}
// Navigate to Child2
tv.Keyboard(&terminalapi.Keyboard{Key: keyboard.KeyArrowDown}, &widgetapi.EventMeta{})
if tv.selectedNode.Label != "Child2" {
t.Errorf("Expected selectedNode to be 'Child2', got '%s'", tv.selectedNode.Label)
}
// Ensure scrollOffset is updated correctly when navigating further
tv.Keyboard(&terminalapi.Keyboard{Key: keyboard.KeyArrowDown}, &widgetapi.EventMeta{})
if tv.selectedNode.Label != "Child3" {
t.Errorf("Expected selectedNode to be 'Child3', got '%s'", tv.selectedNode.Label)
}
if tv.scrollOffset != 1 {
t.Errorf("Expected scrollOffset to be 1, got %d", tv.scrollOffset)
}
}
// TestUpdateVisibleNodes tests the visibility of nodes based on expansion state.
func TestUpdateVisibleNodes(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{Label: "Child1"},
{Label: "Child2"},
{Label: "Child3"},
},
},
}
tv, err := New(Nodes(root...), Indentation(2))
if err != nil {
t.Fatalf("Failed to create Treeview: %v", err)
}
// Lock the Treeview before modifying node states
tv.mu.Lock()
root[0].SetExpandedState(true)
root[0].Children[0].SetExpandedState(false)
tv.mu.Unlock()
tv.updateVisibleNodes()
// Lock before accessing visibleNodes
tv.mu.Lock()
visibleNodes := make([]string, len(tv.visibleNodes))
for i, node := range tv.visibleNodes {
visibleNodes[i] = node.Label
}
tv.mu.Unlock()
expectedVisible := []string{"Root", "Child1", "Child2", "Child3"}
if len(visibleNodes) != len(expectedVisible) {
t.Errorf("Expected %d visible nodes, got %d", len(expectedVisible), len(visibleNodes))
}
for i, label := range expectedVisible {
if i >= len(visibleNodes) || visibleNodes[i] != label {
t.Errorf("Expected node at index %d to be '%s', got '%s'", i, label, visibleNodes[i])
}
}
}
// TestNodeExpansionAndCollapse adjusted for actual behavior
func TestNodeExpansionAndCollapse(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{Label: "Child1"},
{Label: "Child2"},
},
},
}
tv, err := New(Nodes(root...), Indentation(2))
if err != nil {
t.Fatalf("Failed to create Treeview: %v", err)
}
// Initially, all nodes should be visible
tv.updateVisibleNodes()
if len(tv.visibleNodes) != 3 { // Root + 2 children
t.Errorf("Expected 3 visible nodes, got %d", len(tv.visibleNodes))
}
// Collapse Root
root[0].SetExpandedState(false)
tv.updateVisibleNodes()
if len(tv.visibleNodes) != 1 { // Only Root
t.Errorf("Expected 1 visible node after collapsing Root, got %d", len(tv.visibleNodes))
}
// Expand Root again
root[0].SetExpandedState(true)
tv.updateVisibleNodes()
if len(tv.visibleNodes) != 3 { // Root + 2 children
t.Errorf("Expected 3 visible nodes after expanding Root, got %d", len(tv.visibleNodes))
}
}
// TestSelectNoVisibleNodes tests selecting a node when no nodes are visible.
func TestSelectNoVisibleNodes(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{}, // No children, making it a leaf node
},
}
tv, err := New(
Nodes(root...),
Indentation(2),
Icons("▼", "▶", "•"),
)
if err != nil {
t.Fatalf("Failed to create Treeview: %v", err)
}
// Manually set selectedNode to nil to simulate no visible nodes
tv.selectedNode = nil
label, err := tv.Select()
if err == nil {
t.Errorf("Expected Select to return an error when no node is selected")
}
if label != "" {
t.Errorf("Expected Select to return empty string when no node is selected, got '%s'", label)
}
}
// TestKeyboardNonArrowKeys tests that non-arrow keys do not affect navigation.
func TestKeyboardNonArrowKeys(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{Label: "Child1"},
},
},
}
tv, err := New(
Nodes(root...),
Indentation(2),
Icons("▼", "▶", "•"),
)
if err != nil {
t.Fatalf("Failed to create Treeview: %v", err)
}
// Manually expand Root to make children visible
root[0].ExpandedState = true
tv.updateVisibleNodes()
// Initial selection is "Root"
if tv.selectedNode.Label != "Root" {
t.Errorf("Expected selectedNode to be 'Root', got '%s'", tv.selectedNode.Label)
}
// Send a non-arrow key event (e.g., 'a')
tv.Keyboard(&terminalapi.Keyboard{Key: 'a'}, &widgetapi.EventMeta{})
if tv.selectedNode.Label != "Root" {
t.Errorf("Expected selectedNode to remain 'Root', got '%s'", tv.selectedNode.Label)
}
}
// TestRunSpinner tests the runSpinner method.
func TestRunSpinner(t *testing.T) {
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{Label: "Child1"},
},
},
}
tv, err := New(
Nodes(root...),
WaitingIcons([]string{"|", "/", "-", "\\"}),
)
if err != nil {
t.Fatalf("Failed to create TreeView: %v", err)
}
// Start spinner on "Child1"
root[0].Children[0].SetShowSpinner(true)
// Wait to allow spinner to update
time.Sleep(500 * time.Millisecond)
// Check that SpinnerIndex has incremented
root[0].Children[0].mu.Lock()
spinnerIndex := root[0].Children[0].SpinnerIndex
root[0].Children[0].mu.Unlock()
if spinnerIndex == 0 {
t.Errorf("Expected SpinnerIndex to have incremented, got %d", spinnerIndex)
}
// Stop the spinner ticker
tv.StopSpinnerTicker()
}
// TestHandleNodeClick tests the handleNodeClick method.
// TestHandleNodeClick tests the handleNodeClick method.
func TestHandleNodeClick(t *testing.T) {
// Create a channel to signal when OnClick has been executed.
clickCh := make(chan struct{})
// Define the tree structure with an OnClick handler.
root := []*TreeNode{
{
Label: "Root",
Children: []*TreeNode{
{
Label: "Child1",
OnClick: func() error {
// Signal that OnClick has been called.
clickCh <- struct{}{}
return nil
},
},
},
},
}
// Initialize the TreeView.
tv, err := New(
Nodes(root...),
Indentation(2),
Icons("▼", "▶", "•"),
)
if err != nil {
t.Fatalf("Failed to create TreeView: %v", err)
}
// Expand Root to make its children visible.
root[0].SetExpandedState(true)
tv.updateVisibleNodes()
// Select "Child1".
tv.selectedNode = root[0].Children[0]
err = tv.handleNodeClick(tv.selectedNode)
if err != nil {
t.Errorf("handleNodeClick returned an error: %v", err)
}
// Wait for the OnClick handler to signal completion or timeout after 1 second.
select {
case <-clickCh:
// OnClick was called successfully.
case <-time.After(1 * time.Second):
t.Errorf("OnClick was not called within the expected time")
}
// Verify that the spinner has been reset.
if tv.selectedNode.GetShowSpinner() {
t.Errorf("Expected spinner to be false after OnClick execution")
}
}
// TestTruncateString tests the truncateString function.
func TestTruncateString(t *testing.T) {
longString := "This is a very long string that should be truncated"
truncated := truncateString(longString, 10)
if truncated != "This is a…" {
t.Errorf("Expected 'This is a…', got '%s'", truncated)
}
// Test with a maxWidth smaller than ellipsis
truncated = truncateString(longString, 1)
if truncated != "…" {
t.Errorf("Expected '…', got '%s'", truncated)
}
}