// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package interaction import ( "strings" "testing" ) func TestListNavigationAndIndependentSessions(t *testing.T) { form := Form{Title: "Actions", Elements: []Element{{ID: "actions", Label: "Actions", Kind: List, Items: []Item{{ID: "click", Label: "Click"}, {ID: "skip", Label: "Unavailable", Disabled: true}, {ID: "press", Label: "Press"}, {ID: "ancestor", Label: "Click ancestor"}}}}} first, err := NewSession(form) if err != nil { t.Fatal(err) } second, _ := NewSession(form) if opening := first.Opening().Speech; opening != "Actions. Click" { t.Fatalf("opening speech = %q", opening) } if moved := first.Handle(Event{Key: Down}).Speech; moved != "Press" { t.Fatalf("option speech = %q", moved) } if effect := first.Handle(Event{Key: Enter}); effect.Activated != "press" { t.Fatalf("selection = %+v", effect) } if second.Handle(Event{Key: Enter}).Activated != "click" { t.Fatal("sessions share state") } first.Handle(Event{Key: End}) first.Handle(Event{Key: Down}) if first.Handle(Event{Key: Enter}).Activated != "ancestor" { t.Fatal("boundary moved selection") } first.Handle(Event{Key: Home}) first.Handle(Event{Text: "p"}) if first.Handle(Event{Key: Enter}).Activated != "press" { t.Fatal("typeahead failed") } if !first.Handle(Event{Key: Escape}).Cancelled || first.Handle(Event{Key: Enter}).Activated != "" { t.Fatal("cancelled session remained usable") } form.Elements[0].Items[0].Label = "Changed" if !strings.Contains(second.Presentation().Speech, "Click") { t.Fatal("caller mutation changed session") } } func TestCallerSuppliedMenuTitleAndFormListLabel(t *testing.T) { menu, err := NewSession(Form{Title: "Wormhole Menu", Elements: []Element{{ID: "choices", Label: "Options", Kind: List, Items: []Item{{ID: "send", Label: "Send focused item"}, {ID: "receive", Label: "Receive file"}}}}}) if err != nil { t.Fatal(err) } if opening := menu.Opening().Speech; opening != "Wormhole Menu. Send focused item" { t.Fatalf("custom title = %q", opening) } if moved := menu.Handle(Event{Key: Down}).Speech; moved != "Receive file" { t.Fatalf("custom menu option = %q", moved) } if focus := menu.Handle(Event{Key: Tab}).Speech; focus != "Receive file" { t.Fatalf("menu repeated title on Tab: %q", focus) } form, _ := NewSession(Form{Title: "Example", Elements: []Element{{ID: "code", Label: "Code", Kind: Entry}, {ID: "method", Label: "Method", Kind: List, Items: []Item{{ID: "send", Label: "Send"}}}}}) if focus := form.Handle(Event{Key: Tab}).Speech; focus != "Method. Send" { t.Fatalf("form list lost its field label: %q", focus) } } func TestFormTraversalEditingAndValidation(t *testing.T) { session, err := NewSession(Form{Title: "Receive", Elements: []Element{{ID: "code", Label: "Code", Kind: Entry, Required: true, MaxLength: 8}, {ID: "disabled", Label: "Disabled", Kind: Button, Disabled: true}, {ID: "keep", Label: "Keep", Kind: CheckBox}, {ID: "submit", Label: "Receive", Kind: Button}}}) if err != nil { t.Fatal(err) } if !strings.Contains(session.Handle(Event{Key: Enter}).Speech, "required") { t.Fatal("missing required validation") } session.Handle(Event{Text: "ab猫"}) session.Handle(Event{Key: Left, Shift: true}) session.Insert("é") if session.Values()["code"].Text != "abé" { t.Fatal("Unicode selection replacement") } session.Handle(Event{Key: Home}) session.Handle(Event{Key: Delete}) if session.Values()["code"].Text != "bé" { t.Fatal("Unicode deletion") } session.Handle(Event{Text: "a", Control: true}) if effect := session.Handle(Event{Text: "c", Control: true}); effect.Copy == nil || *effect.Copy != "bé" { t.Fatal("copy selection") } if !session.Handle(Event{Text: "v", Control: true}).Paste { t.Fatal("paste host request missing") } before := session.Values()["code"].Text if !strings.Contains(session.Insert("too long to fit").Speech, "limit") || session.Values()["code"].Text != before { t.Fatal("oversized insertion changed field") } if !strings.Contains(session.Insert("bad\ntext").Speech, "single-line") { t.Fatal("multiline paste accepted") } if session.Insert(string([]byte{0xff})).Speech != "Invalid text" || session.Values()["code"].Text != before { t.Fatal("invalid UTF-8 paste changed entry") } session.Handle(Event{Key: Tab}) session.Handle(Event{Text: " "}) if !session.Values()["keep"].Checked { t.Fatal("checkbox not toggled / disabled control took focus") } session.Handle(Event{Key: Tab}) if session.Handle(Event{Key: Enter}).Activated != "submit" { t.Fatal("button did not submit") } session.Handle(Event{Key: Tab}) if !strings.Contains(session.Presentation().Speech, "Code") { t.Fatal("Tab did not wrap") } session.Handle(Event{Key: Tab, Shift: true}) if session.Handle(Event{Key: Enter}).Activated != "submit" { t.Fatal("Shift Tab did not wrap") } } func TestWordMovementAndSelection(t *testing.T) { session, _ := NewSession(Form{Title: "Edit", Elements: []Element{{ID: "text", Label: "Text", Kind: Entry, Text: "one two 猫"}}}) session.Handle(Event{Key: Left, Control: true, Shift: true}) if effect := session.Handle(Event{Text: "c", Control: true}); effect.Copy == nil || *effect.Copy != "猫" { t.Fatal("word selection") } session.Handle(Event{Key: Backspace}) if session.Values()["text"].Text != "one two " { t.Fatal("word selection deletion") } session.Handle(Event{Key: Home}) session.Handle(Event{Key: Right, Control: true}) session.Insert("é") if session.Values()["text"].Text != "one étwo " { t.Fatalf("word movement = %+v", session.Values()) } } func TestInvalidForms(t *testing.T) { for _, form := range []Form{{}, {Title: "Empty", Elements: []Element{{ID: "list", Label: "List", Kind: List}}}, {Title: "Duplicate", Elements: []Element{{ID: "same", Label: "First", Kind: Button}, {ID: "same", Label: "Second", Kind: Entry}}}} { if _, err := NewSession(form); err == nil { t.Fatalf("accepted invalid form %+v", form) } } } func TestButtonActivationValidatesRequiredEntries(t *testing.T) { for _, event := range []Event{{Key: Enter}, {Key: Text, Text: " "}} { t.Run(map[Key]string{Enter: "Enter", Text: "Space"}[event.Key], func(t *testing.T) { session, err := NewSession(Form{Title: "Receive", Elements: []Element{ {ID: "code", Label: "Code", Kind: Entry, Required: true}, {ID: "submit", Label: "Receive", Kind: Button}, }}) if err != nil { t.Fatal(err) } session.Handle(Event{Key: Tab}) if effect := session.Handle(event); effect.Activated != "" || effect.Speech != "Code is required" { t.Fatalf("empty required field accepted: %+v", effect) } session.Handle(Event{Key: Tab, Shift: true}) session.Insert("1234") session.Handle(Event{Key: Tab}) if effect := session.Handle(event); effect.Activated != "submit" { t.Fatalf("completed form rejected: %+v", effect) } }) } }