// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import "testing" func TestEchoEditFieldRequiresEditableMetadata(t *testing.T) { for _, tc := range []struct { name string snapshot Snapshot want bool }{ {"entry", Snapshot{Role: "text", Editable: true}, true}, {"web contenteditable", Snapshot{Role: "section", Editable: true}, true}, {"editable combo", Snapshot{Role: "combo box", Editable: true}, true}, {"unknown text", Snapshot{Role: "text"}, false}, {"read only", Snapshot{Role: "text", Editable: true, ReadOnly: true}, false}, {"password", Snapshot{Role: "password text", Editable: true}, false}, {"terminal", Snapshot{Role: "terminal", Editable: true}, false}, {"defunct", Snapshot{Role: "text", Editable: true, Defunct: true}, false}, } { t.Run(tc.name, func(t *testing.T) { if got := IsEchoEditField(tc.snapshot); got != tc.want { t.Fatalf("editable=%v want=%v", got, tc.want) } }) } } func TestEchoFocusLifetime(t *testing.T) { adapter := New() source := Snapshot{BusName: ":1.5", Path: "/edit", Role: "text", Editable: true, Focused: true, StatesKnown: true} event := Event{Type: FocusedStateEvent, Detail1: 1, Source: source, Window: NewObjectID(source.BusName, "/window"), OriginPath: "/child"} if adapter.EditFieldFocused() { t.Fatal("unknown focus allowed") } gain := func() { adapter.observeEchoFocus(event) if !adapter.EditFieldFocused() { t.Fatal("focused editor rejected") } } gain() // Background updates and losses do not replace the current editor. adapter.retireEchoFocus(FocusedStateEvent, 0, NewObjectID(":1.6", "/edit")) other := event other.Source.Path = "/other" other.ForegroundMismatch = true adapter.observeEchoFocus(other) if !adapter.EditFieldFocused() { t.Fatal("unrelated event retired editor") } for _, tc := range []struct { kind string detail int id ObjectID }{ {FocusedStateEvent, 0, source.ID()}, {FocusedStateEvent, 0, NewObjectID(source.BusName, "/child")}, {DefunctStateEvent, 1, source.ID()}, {WindowDestroyedEvent, 0, event.Window}, {WindowDeactivatedEvent, 0, event.Window}, {FocusedStateEvent, 1, NewObjectID(source.BusName, "/button")}, } { gain() adapter.retireEchoFocus(tc.kind, tc.detail, tc.id) if adapter.EditFieldFocused() { t.Fatalf("stale focus after %s", tc.kind) } } gain() readonly := event readonly.Type = "object:state-changed:read-only" readonly.Source.ReadOnly = true adapter.observeEchoFocus(readonly) if adapter.EditFieldFocused() { t.Fatal("read-only change retained echo") } gain() adapter.foreground.Store(&ForegroundState{Epoch: 1}) if adapter.EditFieldFocused() { t.Fatal("foreground change retained echo") } adapter.observeEchoFocus(event) if adapter.EditFieldFocused() { t.Fatal("old event restored echo") } adapter.foreground.Store(nil) gain() unknownWindow := event unknownWindow.Type = WindowActivatedEvent unknownWindow.Window = NewObjectID(":1.6", "/window") unknownWindow.Source = Snapshot{BusName: ":1.6", Path: "/window", Role: "frame"} adapter.observeEchoFocus(unknownWindow) if adapter.EditFieldFocused() { t.Fatal("unknown window retained previous editor without X11 metadata") } gain() adapter.SetSecureInputActive(true) if adapter.EditFieldFocused() { t.Fatal("lock retained echo") } adapter.SetSecureInputActive(false) if adapter.EditFieldFocused() { t.Fatal("unlock restored stale echo") } }