Files
SerialTerminalForWindowsTer…/tui_test.go
T
JiXieShi e0de872740 refactor: extract pkg/charset and internal/event packages
Extract ConvertChunk/FormatHexFrame into pkg/charset (zero external deps).
Extract UIEvent/UIEventKind/UIPanelKind types into internal/event.
Update all references across main package to use qualified imports.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 02:35:30 +08:00

310 lines
8.0 KiB
Go

package main
import (
"strings"
"testing"
"github.com/jixishi/SerialTerminalForWindowsTerminal/internal/event"
)
func TestParseCtrlKey(t *testing.T) {
tests := []struct {
in string
want byte
ok bool
reason string
}{
{in: "ctrl+c", want: 'c', ok: true, reason: "plain ctrl"},
{in: "ctrl+shift+c", ok: false, reason: "ctrl+shift reserved for local"},
{in: "ctrl+enter", ok: false, reason: "non-letter"},
{in: "alt+c", ok: false, reason: "wrong modifier"},
}
for _, tt := range tests {
got, ok := parseCtrlKey(tt.in)
if ok != tt.ok || got != tt.want {
t.Fatalf("%s parseCtrlKey(%q) got=(%q,%v) want=(%q,%v)", tt.reason, tt.in, got, ok, tt.want, tt.ok)
}
}
}
func TestRenderModal(t *testing.T) {
modal := renderModal("Title", "line1\nline2", 80)
if !strings.Contains(modal, "Title") {
t.Fatalf("renderModal missing title: %q", modal)
}
if !strings.Contains(modal, "line1") || !strings.Contains(modal, "line2") {
t.Fatalf("renderModal missing lines: %q", modal)
}
if !strings.Contains(modal, "╭") || !strings.Contains(modal, "╮") || !strings.Contains(modal, "╰") || !strings.Contains(modal, "╯") {
t.Fatalf("renderModal missing box borders: %q", modal)
}
}
func TestHandleCtrlShiftLocalHelp(t *testing.T) {
a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+alt"}}
a.SetUIEnabled(true)
m := uiModel{app: a}
ok := handleLocalHotkey(&m, "ctrl+alt+h")
if !ok {
t.Fatalf("expected local hotkey to be handled")
}
ev := mustReadEvent(t, a.uiEvents)
if ev.Kind != event.UIEventModal {
t.Fatalf("expected modal event, got %+v", ev)
}
}
func TestNormalizeHotkeyPrefix(t *testing.T) {
tests := []struct {
in, want string
}{
{"", "ctrl+alt"},
{"ctrl+alt", "ctrl+alt"},
{"ctrl+shift", "ctrl+shift"},
{"CTRL+ALT", "ctrl+alt"},
{" ctrl+SHIFT ", "ctrl+shift"},
{"invalid", "ctrl+alt"},
}
for _, tt := range tests {
got := normalizeHotkeyPrefix(tt.in)
if got != tt.want {
t.Fatalf("normalizeHotkeyPrefix(%q) got=%q want=%q", tt.in, got, tt.want)
}
}
}
func TestHotkeyWith(t *testing.T) {
got := hotkeyWith("ctrl+alt", "h")
if got != "ctrl+alt+h" {
t.Fatalf("hotkeyWith ctrl+alt+h got=%q", got)
}
got = hotkeyWith("ctrl+shift", "c")
if got != "ctrl+shift+c" {
t.Fatalf("hotkeyWith ctrl+shift+c got=%q", got)
}
}
func TestIsLocalHotkeyAll(t *testing.T) {
tests := []struct {
key, mod string
action string
want bool
}{
{"ctrl+alt+c", "ctrl+alt", "c", true},
{"ctrl+shift+c", "ctrl+shift", "c", true},
{"ctrl+alt+c", "ctrl+shift", "c", false},
{"ctrl+shift+c", "ctrl+alt", "c", false},
{"alt+c", "ctrl+alt", "c", false},
{"ctrl+c", "ctrl+alt", "c", false},
}
for _, tt := range tests {
a := &App{cfg: &Config{hotkeyMod: tt.mod}}
m := uiModel{app: a}
got := m.isLocalHotkey(tt.key, tt.action)
if got != tt.want {
t.Fatalf("isLocalHotkey(%q, %q) hotkeyMod=%q got=%v want=%v", tt.key, tt.action, tt.mod, got, tt.want)
}
}
}
func TestParseCtrlKeyEdgeCases(t *testing.T) {
tests := []struct {
in string
want byte
ok bool
}{
{in: "ctrl+z", want: 'z', ok: true},
{in: "ctrl+a", want: 'a', ok: true},
{in: "ctrl+shift+c", want: 0, ok: false},
{in: "ctrl+alt+c", want: 0, ok: false},
{in: "ctrl+", want: 0, ok: false},
{in: "ctrl+ab", want: 0, ok: false},
{in: "ctrl+A", want: 0, ok: false},
{in: "ctrl+1", want: 0, ok: false},
}
for _, tt := range tests {
got, ok := parseCtrlKey(tt.in)
if ok != tt.ok || got != tt.want {
t.Fatalf("parseCtrlKey(%q) got=(%q,%v) want=(%q,%v)", tt.in, got, ok, tt.want, tt.ok)
}
}
}
func TestRenderModalLongContent(t *testing.T) {
longBody := "line1\nline2\nline3\nline4\nline5\nline6\nline7\nline8\nline9\nline10\nline11\nline12\nline13\nline14"
modal := renderModal("Title", longBody, 80)
if !strings.Contains(modal, "... (press Esc/Enter to close)") {
t.Fatalf("long modal should be truncated: %q", modal)
}
if strings.Contains(modal, "line14") {
t.Fatalf("line14 should not appear in truncated modal")
}
}
func TestRenderModalEmpty(t *testing.T) {
modal := renderModal("", "", 80)
if !strings.Contains(modal, "Info") {
t.Fatalf("empty title should default to Info: %q", modal)
}
}
func TestTruncateToWidth(t *testing.T) {
tests := []struct {
in string
width int
want string
}{
{"hello", 3, "hel"},
{"hello", 10, "hello"},
{"", 5, ""},
{"hello", 0, "hello"},
}
for _, tt := range tests {
got := truncateToWidth(tt.in, tt.width)
if got != tt.want {
t.Fatalf("truncateToWidth(%q, %d) got=%q want=%q", tt.in, tt.width, got, tt.want)
}
}
}
func TestClampIndex(t *testing.T) {
tests := []struct {
idx, n int
want int
}{
{2, 5, 2},
{-1, 5, 0},
{10, 5, 4},
{0, 0, 0},
{0, 1, 0},
}
for _, tt := range tests {
got := clampIndex(tt.idx, tt.n)
if got != tt.want {
t.Fatalf("clampIndex(%d, %d) got=%d want=%d", tt.idx, tt.n, got, tt.want)
}
}
}
func TestMinInt(t *testing.T) {
if got := minInt(1, 2); got != 1 {
t.Fatalf("minInt(1,2) got=%d", got)
}
if got := minInt(5, 3); got != 3 {
t.Fatalf("minInt(5,3) got=%d", got)
}
if got := minInt(0, 0); got != 0 {
t.Fatalf("minInt(0,0) got=%d", got)
}
}
func TestMaxIntFunc(t *testing.T) {
if got := maxInt(1, 2); got != 2 {
t.Fatalf("maxInt(1,2) got=%d", got)
}
if got := maxInt(5, 3, 7); got != 7 {
t.Fatalf("maxInt(5,3,7) got=%d", got)
}
}
func TestHandleLocalHotkeyForward(t *testing.T) {
a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+alt"}}
a.SetUIEnabled(true)
m := uiModel{app: a}
if !handleLocalHotkey(&m, "ctrl+alt+f") {
t.Fatalf("expected forward hotkey handled")
}
ev := mustReadEvent(t, a.uiEvents)
if ev.Kind != event.UIEventPanel || ev.Panel != event.UIPanelForward {
t.Fatalf("expected forward panel, got %+v", ev)
}
}
func TestHandleLocalHotkeyPlugin(t *testing.T) {
a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+alt"}}
a.SetUIEnabled(true)
m := uiModel{app: a}
if !handleLocalHotkey(&m, "ctrl+alt+p") {
t.Fatalf("expected plugin hotkey handled")
}
ev := mustReadEvent(t, a.uiEvents)
if ev.Kind != event.UIEventPanel || ev.Panel != event.UIPanelPlugin {
t.Fatalf("expected plugin panel, got %+v", ev)
}
}
func TestHandleLocalHotkeyMode(t *testing.T) {
a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+alt"}}
a.SetUIEnabled(true)
m := uiModel{app: a}
if !handleLocalHotkey(&m, "ctrl+alt+m") {
t.Fatalf("expected mode hotkey handled")
}
ev := mustReadEvent(t, a.uiEvents)
if ev.Kind != event.UIEventPanel || ev.Panel != event.UIPanelMode {
t.Fatalf("expected mode panel, got %+v", ev)
}
}
func TestHandleLocalHotkeyUnknown(t *testing.T) {
a := &App{cfg: &Config{hotkeyMod: "ctrl+alt"}}
m := uiModel{app: a}
if handleLocalHotkey(&m, "ctrl+alt+x") {
t.Fatalf("unknown hotkey should not be handled")
}
}
func TestHandleLocalHotkeyCtrlShift(t *testing.T) {
a := &App{uiEvents: make(chan event.UIEvent, 4), cfg: &Config{hotkeyMod: "ctrl+shift"}}
a.SetUIEnabled(true)
m := uiModel{app: a}
if !handleLocalHotkey(&m, "ctrl+shift+h") {
t.Fatalf("expected ctrl+shift+h to be handled")
}
ev := mustReadEvent(t, a.uiEvents)
if ev.Kind != event.UIEventModal {
t.Fatalf("expected help modal with ctrl+shift+h")
}
}
func TestRenderPanelModal(t *testing.T) {
lines := []panelLine{
{text: "Header", selected: false},
{text: "Selected Row", selected: true},
}
out := renderPanelModal("Test Panel", lines, "Footer text", 80)
if !strings.Contains(out, "Test Panel") {
t.Fatalf("missing title: %q", out)
}
if !strings.Contains(out, "Header") {
t.Fatalf("missing header line: %q", out)
}
if !strings.Contains(out, "Selected Row") {
t.Fatalf("missing selected line: %q", out)
}
if !strings.Contains(out, "Footer text") {
t.Fatalf("missing footer: %q", out)
}
}
func TestStyleFunctions(t *testing.T) {
_ = modalFooterLineStyle()
rendered := selectedPanelLineStyle().Render("test")
if !strings.Contains(rendered, "test") {
t.Fatalf("selectedPanelLineStyle should render text")
}
}