// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import ( "context" "os" "strings" "testing" "time" ) func TestNativeSpeechMetadata(t *testing.T) { if os.Getenv("CTHULHU_SPEECH_METADATA") != "1" { t.Skip("requires private GTK speech fixture") } ctx, cancel := context.WithTimeout(context.Background(), 12*time.Second) defer cancel() adapter := New() adapter.SetCaptureTextAttributes(true) if err := adapter.Start(ctx); err != nil { t.Fatal(err) } defer func() { stopCtx, stopCancel := context.WithTimeout(context.Background(), 3*time.Second) defer stopCancel() if err := adapter.Stop(stopCtx); err != nil { t.Error(err) } }() for _, probe := range []struct{ key, name string }{{"F9", "Mnemonic probe"}, {"F10", "Indentation probe"}} { pressTreeProbeKey(t, probe.key) for { select { case <-ctx.Done(): t.Fatal("missing speech metadata focus event", probe.name) case event := <-adapter.Events(): if !((event.Type == FocusEvent || event.Type == FocusedStateEvent) && event.Source.Name == probe.name) { continue } windowContext := false for _, ancestor := range event.Source.BrailleContext { if ancestor.Role == "frame" && ancestor.Name != "" { windowContext = true } } if !windowContext { t.Fatalf("missing native braille window context: %+v", event.Source.BrailleContext) } if probe.key == "F9" { if !strings.Contains(strings.ToLower(event.Source.Mnemonic), "a") { t.Fatalf("missing native mnemonic: %q", event.Source.Mnemonic) } } else { if event.Source.BrailleLine != "\t Indented É" { t.Fatalf("raw indentation lost: %q", event.Source.BrailleLine) } found := false for _, ancestor := range event.Source.SpeechContext { if (ancestor.Role == "list" || ancestor.Role == "list box") && ancestor.Name == "Dynamic controls" { if !ancestor.ChildCountKnown || ancestor.ChildCount != 10 { t.Fatalf("native list count unavailable: %+v", ancestor) } found = true } } if !found { t.Fatalf("missing native list context: %+v", event.Source.SpeechContext) } } goto nextProbe } } nextProbe: } pressTreeProbeKey(t, "F11") for { select { case <-ctx.Done(): t.Fatal("missing color attribute focus event") case event := <-adapter.Events(): if (event.Type != FocusEvent && event.Type != FocusedStateEvent) || event.Source.Name != "Color attribute probe" { continue } if len(event.Source.TextAttributeRuns) == 0 || event.Source.TextAttributeRuns[0].Values["fg-color"] == "" { t.Fatalf("native braille attribute runs missing: %+v", event.Source.TextAttributeRuns) } if !event.Source.Misspelled || !TextAttributesMisspelled(event.Source.TextAttributeRuns[0].Values) { t.Fatalf("native error underline lost its spelling indicator: %+v", event.Source.TextAttributeRuns) } attributes, err := adapter.ReadCharacterAttributes(ctx, event.Source.ID()) if err != nil { t.Fatal(err) } if !TextAttributesMisspelled(attributes.Values) { t.Fatalf("character attributes lost spelling indicator: %+v", attributes.Values) } if color := attributes.Values["fg-color"]; color == "" { t.Fatalf("native foreground color missing: %+v", attributes.Values) } else { t.Logf("native foreground color: %q", color) } goto protectedProbe } } protectedProbe: pressTreeProbeKey(t, "F7") for { select { case <-ctx.Done(): t.Fatal("missing protected text focus event") case event := <-adapter.Events(): if (event.Type != FocusEvent && event.Type != FocusedStateEvent) || event.Source.Name != "Protected text probe" { continue } attributes, err := adapter.ReadCharacterAttributes(ctx, event.Source.ID()) if err != nil || len(attributes.Values) != 0 { t.Fatalf("password attributes: %+v, %v", attributes.Values, err) } return } } }