Make web document navigation survive live-updating pages

Recreated-node caret recovery, tolerant caret validation, per-root retained document trees and cursors across tab switches, document warm-up, coalesced structural moves, and hung-provider fast-fail with an 'Application is not responding' announcement.
This commit is contained in:
hjozwiak committed 2026-11-03 10:34:55 -05:00
1 parent 8ce5e5cdad
commit 7eef6e8be6
30 files changed
+1484 -174

No files matched your search

+114
View File
@@ -513,3 +513,117 @@ func TestSecureInputCancelsCapturesAcrossLockTransitions(t *testing.T) {
t.Fatal("unlock revived old capture")
}
}
// Switching browser tabs must not discard the previous document's captured
// tree: returning to a tab reuses its tree so capture only re-reads nodes
// dirtied while away, instead of snapshotting the whole document cold.
func TestDocumentSlotsRetainTreesAcrossRoots(t *testing.T) {
adapter := New()
install := func(rootPath, childPath string) *AccessibleTree {
t.Helper()
root := NewObjectID(":1.42", rootPath)
adapter.rotateDocumentSlots(root)
rootSnapshot := NewSnapshot(rootPath, "Document", "document web", "", "")
rootSnapshot.BusName = root.BusName
childSnapshot := NewSnapshot(childPath, "Heading", "heading", "", "")
childSnapshot.BusName = root.BusName
tree, err := adapter.installCapturedTree(root, []TreeNode{
{Snapshot: rootSnapshot, Children: []ObjectID{NewObjectID(":1.42", childPath)}},
{Snapshot: childSnapshot},
}, true, false)
if err != nil {
t.Fatal(err)
}
return tree
}
treeA := install("/docA", "/a_heading")
childA, err := treeA.Reference(NewObjectID(":1.42", "/a_heading"))
if err != nil {
t.Fatal(err)
}
treeB := install("/docB", "/b_heading")
if treeB == treeA {
t.Fatal("second document replaced first document tree object")
}
adapter.rotateDocumentSlots(NewObjectID(":1.42", "/docA"))
if adapter.documentTree != treeA {
t.Fatal("returning to first document did not restore its retained tree")
}
if _, err := adapter.documentTree.Resolve(childA); err != nil {
t.Fatalf("retained document lost its captured nodes: %v", err)
}
// Retention is bounded: cycling through more documents than the limit
// evicts the least recently used one.
for _, rootPath := range []string{"/docC", "/docD", "/docE"} {
install(rootPath, rootPath+"_heading")
}
adapter.rotateDocumentSlots(NewObjectID(":1.42", "/docB"))
if adapter.documentTree == treeB {
t.Fatal("least recently used document tree was not evicted")
}
if adapter.documentTree != nil {
if root, err := adapter.documentTree.Root(); err == nil && root.ID().Path == "/docB" {
t.Fatal("evicted document tree retained its nodes")
}
}
// Events for a stashed document must still invalidate it while inactive.
adapter.rotateDocumentSlots(NewObjectID(":1.42", "/docE"))
stashedE := adapter.documentTree
if stashedE == nil {
t.Fatal("most recent document tree missing")
}
childE, err := stashedE.Reference(NewObjectID(":1.42", "/docE_heading"))
if err != nil {
t.Fatal(err)
}
adapter.rotateDocumentSlots(NewObjectID(":1.42", "/docD"))
adapter.invalidateTreeEvent(DefunctStateEvent, 1, NewObjectID(":1.42", "/docE_heading"), ObjectID{})
if _, err := stashedE.Resolve(childE); !errors.Is(err, ErrStaleObject) {
t.Fatalf("inactive document ignored defunct invalidation: %v", err)
}
}
// A provider whose capture just timed out is probably still stuck. The
// next document capture for that bus must fail fast with ErrProviderHung
// instead of queueing another multi-second native stall, and the mark
// must expire so a recovered provider is used again.
func TestCaptureFailsFastWhileProviderHung(t *testing.T) {
adapter := New()
now := time.Now()
adapter.markProviderHung(":1.99", now)
if !adapter.providerHung(":1.99", now.Add(time.Second)) {
t.Fatal("fresh hung mark not reported")
}
if adapter.providerHung(":1.99", now.Add(time.Hour)) {
t.Fatal("hung mark never expires")
}
if adapter.providerHung(":1.98", now) {
t.Fatal("hung mark leaked to another bus")
}
// The expiry probe above purged the mark, as a real post-cooldown
// capture attempt would. Mark again for the capture assertion.
adapter.markProviderHung(":1.99", time.Now())
if _, err := adapter.CaptureDocument(context.Background(), NewObjectID(":1.99", "/doc")); !errors.Is(err, ErrProviderHung) {
t.Fatalf("hung capture error = %v, want ErrProviderHung", err)
}
tree, err := NewAccessibleTree(NewObjectID(":1.99", "/doc"), []TreeNode{
{Snapshot: func() Snapshot {
snapshot := NewSnapshot("/doc", "Document", "document web", "", "")
snapshot.BusName = ":1.99"
return snapshot
}()},
})
if err != nil {
t.Fatal(err)
}
position, err := tree.Reference(NewObjectID(":1.99", "/doc"))
if err != nil {
t.Fatal(err)
}
if _, err := adapter.ReadDocumentCaret(context.Background(), position, 0, DocumentCaretLine); !errors.Is(err, ErrProviderHung) {
t.Fatalf("hung read error = %v, want ErrProviderHung", err)
}
if err := adapter.SetDocumentPosition(context.Background(), position, position.ID()); !errors.Is(err, ErrProviderHung) {
t.Fatalf("hung placement error = %v, want ErrProviderHung", err)
}
}