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:
1 parent
8ce5e5cdad
commit
7eef6e8be6
30 files changed
+1484
-174
No files matched your search
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user