174 lines
6.8 KiB
Go
174 lines
6.8 KiB
Go
// 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)
|
|
}
|
|
})
|
|
}
|
|
}
|