// Copyright (c) 2026 Storm Dragon // Authors: Storm Dragon, Angus Kola // SPDX-License-Identifier: GPL-3.0-or-later package atspi import ( "fmt" "sync" "testing" ) func headerTestSnapshot() (Snapshot, ObjectID) { return Snapshot{BusName: ":1.42", Path: "/cell", TablePath: "/sheet", Role: "table cell", ApplicationProcess: "soffice.bin", TablePositionKnown: true, TableRowIndex: 2, TableColumnIndex: 3, LibreOffice: &LibreOfficeContext{Cell: true, InSpreadsheet: true, Document: NewObjectID(":1.42", "/document"), SheetName: "Example"}}, NewObjectID(":1.42", "/window") } func TestLibreOfficeHeadersScopeLifetimeAndIndependentAxes(t *testing.T) { adapter := New() snapshot, window := headerTestSnapshot() key, _ := libreOfficeHeaderIdentity(snapshot, window) if err := adapter.ConfigureLibreOfficeHeader(snapshot, window, true, false); err != nil { t.Fatal(err) } if err := adapter.ConfigureLibreOfficeHeader(snapshot, window, false, false); err != nil { t.Fatal(err) } choice := adapter.libreOfficeHeaders.get(key) if !choice.rowSet || choice.row != 2 || !choice.columnSet || choice.column != 3 { t.Fatalf("choice = %+v", choice) } snapshot.TablePath = "/recreated-sheet-table" recreated, _ := libreOfficeHeaderIdentity(snapshot, window) if adapter.libreOfficeHeaders.get(recreated) != choice { t.Fatal("sheet switch lost the document choice") } for _, other := range []libreOfficeHeaderKey{ {NewObjectID(":1.42", "/other-window"), key.document, key.sheet}, {key.window, NewObjectID(":1.42", "/other-document"), key.sheet}, {key.window, key.document, "Other"}, {NewObjectID(":1.43", "/window"), NewObjectID(":1.43", "/document"), "Example"}, } { if got := adapter.libreOfficeHeaders.get(other); got.rowSet || got.columnSet { t.Fatalf("headers crossed identity: %+v", other) } } if err := adapter.ConfigureLibreOfficeHeader(snapshot, window, true, true); err != nil { t.Fatal(err) } if got := adapter.libreOfficeHeaders.get(key); got.rowSet || !got.columnSet { t.Fatalf("clearing one axis changed the other: %+v", got) } // Locking retires output/native focus, but document choices survive. adapter.SetSecureInputActive(true) if err := adapter.ConfigureLibreOfficeHeader(snapshot, window, true, false); err == nil { t.Fatal("configuration accepted while locked") } adapter.SetSecureInputActive(false) if !adapter.libreOfficeHeaders.get(key).columnSet { t.Fatal("lock discarded the document choice") } adapter.libreOfficeHeaders.forget(NewObjectID(":1.42", "/other-window")) if !adapter.libreOfficeHeaders.get(key).columnSet { t.Fatal("unrelated destruction discarded choice") } for _, id := range []ObjectID{key.document, key.window} { if err := adapter.ConfigureLibreOfficeHeader(snapshot, window, false, false); err != nil { t.Fatal(err) } adapter.libreOfficeHeaders.forget(id) if got := adapter.libreOfficeHeaders.get(key); got.rowSet || got.columnSet { t.Fatalf("destruction retained choice: %+v", id) } } _ = adapter.ConfigureLibreOfficeHeader(snapshot, window, false, false) adapter.libreOfficeHeaders.reset() if len(adapter.libreOfficeHeaders.choices) != 0 { t.Fatal("shutdown retained choices") } _ = adapter.ConfigureLibreOfficeHeader(snapshot, window, true, false) adapter.libreOfficeHeaders.forget(NewObjectID(":1.42", "/other-window")) if !adapter.libreOfficeHeaders.get(key).rowSet { t.Fatal("unrelated tab changes retired choice") } adapter.libreOfficeHeaders.forget(window) if adapter.libreOfficeHeaders.get(key).rowSet { t.Fatal("sheet topology change retained choice") } } func TestLibreOfficeHeadersRejectInvalidContextAndBoundChoices(t *testing.T) { for _, invalid := range []string{"process", "editor", "defunct", "position", "negative", "document", "sheet", "bus", "window"} { snapshot, window := headerTestSnapshot() switch invalid { case "process": snapshot.ApplicationProcess = "chromium" case "editor": snapshot.Role = "paragraph" case "defunct": snapshot.Defunct = true case "position": snapshot.TablePositionKnown = false case "negative": snapshot.TableColumnIndex = -1 case "document": snapshot.LibreOffice.Document = ObjectID{} case "sheet": snapshot.LibreOffice.SheetName = "" case "bus": window.BusName = ":1.43" case "window": window = ObjectID{} } adapter := New() if err := adapter.ConfigureLibreOfficeHeader(snapshot, window, true, false); err == nil { t.Fatalf("accepted invalid %s context", invalid) } } adapter := New() snapshot, window := headerTestSnapshot() for index := 0; index < 65; index++ { snapshot.LibreOffice.SheetName = fmt.Sprintf("Sheet%d", index) err := adapter.ConfigureLibreOfficeHeader(snapshot, window, true, false) if (err != nil) != (index == 64) { t.Fatalf("bounded choice %d: %v", index, err) } } snapshot.LibreOffice.SheetName = "Sheet0" if err := adapter.ConfigureLibreOfficeHeader(snapshot, window, true, true); err != nil { t.Fatal(err) } snapshot.LibreOffice.SheetName = "Sheet64" if err := adapter.ConfigureLibreOfficeHeader(snapshot, window, true, false); err != nil { t.Fatal("clear did not free capacity:", err) } } func TestLibreOfficeHeadersConcurrentCaptureAndCommands(t *testing.T) { adapter := New() snapshot, window := headerTestSnapshot() key, _ := libreOfficeHeaderIdentity(snapshot, window) var workers sync.WaitGroup for index := 0; index < 3; index++ { workers.Add(1) go func(index int) { defer workers.Done() for count := 0; count < 100; count++ { switch index { case 0: _ = adapter.ConfigureLibreOfficeHeader(snapshot, window, true, count%2 == 0) case 1: _ = adapter.libreOfficeHeaders.get(key) case 2: adapter.libreOfficeHeaders.forget(window) } } }(index) } workers.Wait() }