mirror of
https://github.com/mum4k/termdash.git
synced 2025-04-25 13:48:50 +08:00
572 lines
14 KiB
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)
|
|
}
|
|
}
|