// Fenrir console screen reader. // Copyright (c) 2026 Storm Dragon // SPDX-License-Identifier: GPL-3.0-or-later // // This program is free software: you can redistribute it and/or modify it // under the terms of the GNU General Public License as published by the Free // Software Foundation, either version 3 of the License, or (at your option) // any later version. // // This program is distributed in the hope that it will be useful, but WITHOUT // ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or // FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for // more details. // // You should have received a copy of the GNU General Public License along with // this program. If not, see . package interaction import ( "math" "testing" ) func TestInvalidNumberUpDownStepRejected(t *testing.T) { for _, step := range []float64{-1, math.NaN(), math.Inf(1), math.Inf(-1)} { form := test_form() form.Tabs[0].Controls[1].UpDownStep = step if _, err := New_session(form); err == nil { t.Fatalf("invalid Up/Down step %v accepted", step) } } } func test_form() Form { return Form{Type: TabForm, Title: "Settings", Tabs: []Tab{ {ID: "speech", Label: "Speech", Controls: []Control{ {ID: "enabled", Label: "Speech enabled", Kind: CheckBox, Checked: true}, {ID: "range", Label: "Range", Kind: Number, Number: 2.5, Min: -5, Max: 5, Step: 0.1}, {ID: "text", Label: "Text", Kind: Entry, Text: "a界c"}, {ID: "ok", Label: "OK", Kind: Button, Action: "save"}, {ID: "cancel", Label: "Cancel", Kind: Button, Action: "cancel"}, }}, {ID: "sound", Label: "Sound"}, }} } func TestTabFormTraversalControlsAndIsolation(t *testing.T) { form := test_form() session, err := New_session(form) if err != nil { t.Fatal(err) } if session.Open() != "Settings. Speech tab" { t.Fatal(session.Open()) } for _, name := range []string{"right", "left", "down", "up"} { result := session.Handle(Key{Name: name}) if result.Speech == "" { t.Fatal("silent tab", name) } } if session.Describe() != "Speech tab" { t.Fatal(session.Describe()) } if got := session.Handle(Key{Name: "tab"}).Speech; got != "Speech enabled checked" { t.Fatal(got) } if got := session.Handle(Key{Name: "space"}).Speech; got != "Speech enabled unchecked" { t.Fatal(got) } if !form.Tabs[0].Controls[0].Checked { t.Fatal("form mutated outside session") } session.Handle(Key{Name: "tab"}) for _, fixture := range []struct { name string want float64 }{ {"up", 3.5}, {"right", 3.6}, {"left", 3.5}, {"down", 2.5}, } { session.Handle(Key{Name: fixture.name}) if got := session.Values()["speech.range"].Number; got != fixture.want { t.Fatal(fixture, got) } } for range 100 { session.Handle(Key{Name: "up"}) } if got := session.Values()["speech.range"].Number; got != 5 { t.Fatal("upper bound", got) } for range 100 { session.Handle(Key{Name: "down"}) } if got := session.Values()["speech.range"].Number; got != -5 { t.Fatal("lower bound", got) } session.Handle(Key{Name: "tab"}) session.Handle(Key{Name: "home"}) session.Handle(Key{Name: "right"}) session.Handle(Key{Name: "delete"}) session.Handle(Key{Text: "é"}) if got := session.Values()["speech.text"].Text; got != "aéc" { t.Fatal("Unicode editing", got) } session.Handle(Key{Ctrl: true, Text: "a"}) session.Handle(Key{Text: "replacement"}) if got := session.Values()["speech.text"].Text; got != "replacement" { t.Fatal("select all", got) } if got := session.Handle(Key{Name: "tab"}).Speech; got != "OK button" { t.Fatal(got) } if got := session.Handle(Key{Name: "enter"}).Action; got != "save" { t.Fatal(got) } if got := session.Handle(Key{Name: "tab"}).Speech; got != "Cancel button" { t.Fatal(got) } if got := session.Handle(Key{Name: "enter"}).Action; got != "cancel" { t.Fatal(got) } if got := session.Handle(Key{Name: "tab"}).Speech; got != "Speech tab" { t.Fatal("tab cycle", got) } if got := session.Handle(Key{Name: "tab", Shift: true}).Speech; got != "Cancel button" { t.Fatal("reverse cycle", got) } if !session.Handle(Key{Name: "escape"}).Cancelled { t.Fatal("escape did not cancel") } } func TestListCategoryFormReturnsSelectionWithoutSideEffects(t *testing.T) { form := Form{Type: TabForm, Title: "Picker", Tabs: []Tab{{ID: "category", Label: "Category", Controls: []Control{{ID: "items", Label: "Items", Kind: List, Items: []string{"First", "Second"}, Action: "choose"}}}}} session, err := New_session(form) if err != nil { t.Fatal(err) } session.Handle(Key{Name: "tab"}) if got := session.Handle(Key{Name: "up"}).Speech; got != "Second" { t.Fatal(got) } if got := session.Handle(Key{Name: "enter"}).Action; got != "choose" { t.Fatal(got) } values := session.Values() values["category.items"].Items[1] = "changed" if got := session.Describe(); got != "Items Second" { t.Fatal("values aliased", got) } } func TestUnknownFormTypeRejected(t *testing.T) { form := test_form() form.Type = "unknown" if _, err := New_session(form); err == nil { t.Fatal("unknown form type accepted") } }