Files
cthulhu/internal/app/browser_mode_test.go

639 lines
23 KiB
Go

// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
// SPDX-License-Identifier: GPL-3.0-or-later
package app
import (
"context"
"errors"
"testing"
"time"
"git.stormux.org/storm/cthulhu/internal/atspi"
"git.stormux.org/storm/cthulhu/internal/input"
"git.stormux.org/storm/cthulhu/internal/review"
"git.stormux.org/storm/cthulhu/internal/sound"
)
type modeKeys struct {
enabled bool
nativeButtonFocus bool
}
func (keys *modeKeys) SetBrowseNavigation(enabled bool) { keys.enabled = enabled }
func (keys *modeKeys) SetNativeButtonFocus(focused bool) {
keys.nativeButtonFocus = focused
}
func TestBrowserModeClearsNativeButtonFocus(t *testing.T) {
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, nil)
browser.nativeButtonFocus(true)
if !keys.nativeButtonFocus {
t.Fatal("focused button did not request native Return")
}
browser.clear()
if keys.nativeButtonFocus {
t.Fatal("departed button retained native Return policy")
}
}
type modeSoundQueue struct {
speech *fakeSpeechQueue
sounds chan sound.Cue
}
func (queue *modeSoundQueue) Enqueue(request speechRequest) {
queue.speech.Enqueue(request)
for _, cue := range request.Sound {
queue.sounds <- cue
}
}
func (queue *modeSoundQueue) Interrupt() {
queue.speech.Interrupt()
}
func modeContext(t *testing.T, role string, editableAncestor bool) atspi.DocumentContext {
t.Helper()
return modeContextInDocument(t, role, editableAncestor, "/document")
}
func modeContextInDocument(t *testing.T, role string, editableAncestor bool, documentPath string) atspi.DocumentContext {
t.Helper()
snapshot := atspi.Snapshot{BusName: ":1.42", Path: "/target", Role: role, StatesKnown: true}
doc := atspi.Snapshot{BusName: ":1.42", Path: documentPath, Role: "document web", StatesKnown: true, Editable: editableAncestor}
tree, err := atspi.NewAccessibleTree(doc.ID(), []atspi.TreeNode{{Snapshot: doc, Children: []atspi.ObjectID{snapshot.ID()}}, {Snapshot: snapshot}})
if err != nil {
t.Fatal(err)
}
ref, _ := tree.Reference(snapshot.ID())
owner, err := tree.DocumentContext(ref)
if err != nil {
t.Fatal(err)
}
return owner
}
func TestBrowserModeRejectsDepartedFocusAndPreservesTyping(t *testing.T) {
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, nil)
queue := &fakeSpeechQueue{spoken: make(chan string, 16)}
heading := modeContext(t, "heading", false)
focus := heading.Ancestors[0].ID()
browser.follow(focus)
browser.accept(browserModeResult{focus: focus, generation: 1, owner: heading}, queue)
if !keys.enabled {
t.Fatal("heading did not enable browse keys")
}
browser.toggle(queue)
if keys.enabled {
t.Fatal("manual focus mode retained letters")
}
browser.toggle(queue)
if !keys.enabled {
t.Fatal("manual browse mode did not restore letters")
}
chrome := atspi.NewObjectID(":1.42", "/chrome")
browser.follow(chrome)
if keys.enabled {
t.Fatal("leaving content did not immediately release letters")
}
browser.accept(browserModeResult{focus: focus, generation: 1, owner: heading}, queue)
if keys.enabled {
t.Fatal("late result re-enabled letters in chrome")
}
browser.accept(browserModeResult{focus: chrome, generation: 2, err: atspi.ErrNotInDocument}, queue)
browser.toggle(queue)
if keys.enabled {
t.Fatal("toggle in chrome enabled letters")
}
entry := modeContext(t, "entry", false)
entryFocus := atspi.NewObjectID(":1.42", "/entry")
browser.follow(entryFocus)
browser.accept(browserModeResult{focus: entryFocus, generation: 3, owner: entry}, queue)
if keys.enabled || browser.mode != review.FocusMode {
t.Fatal("entry did not preserve typing")
}
browser.follow(focus)
browser.accept(browserModeResult{focus: focus, generation: 4, owner: heading}, queue)
if !keys.enabled {
t.Fatal("return to content did not restore navigation")
}
}
func TestBrowserModeDuplicateFocusKeepsBrowseRouting(t *testing.T) {
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, nil)
heading := modeContext(t, "heading", false)
focus := heading.Ancestors[0].ID()
browser.follow(focus)
browser.accept(browserModeResult{focus: focus, generation: 1, owner: heading}, &fakeSpeechQueue{spoken: make(chan string, 1)})
if !browser.ready || !keys.enabled {
t.Fatal("initial browse routing was not ready")
}
browser.follow(focus)
if !browser.ready || !keys.enabled || browser.generation != 1 {
t.Fatal("duplicate focus restarted browser capture or released browse routing")
}
}
func TestBrowserCaretLeavesFocusModeForDocumentText(t *testing.T) {
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, nil)
queue := &fakeSpeechQueue{spoken: make(chan string, 4)}
entry := modeContext(t, "entry", false)
entryFocus := atspi.NewObjectID(":1.42", "/entry")
browser.follow(entryFocus)
browser.accept(browserModeResult{focus: entryFocus, generation: 1, owner: entry}, queue)
if browser.mode != review.FocusMode || keys.enabled {
t.Fatal("entry did not enter focus mode")
}
paragraph := modeContext(t, "paragraph", false)
caret := paragraph.Ancestors[0].ID()
browser.followCaret(caret)
if browser.focus != caret || browser.origin != review.CaretNavigation {
t.Fatal("document caret was not tracked")
}
browser.accept(browserModeResult{focus: caret, generation: 2, owner: paragraph, origin: review.CaretNavigation}, queue)
if browser.mode != review.BrowseMode || !keys.enabled {
t.Fatal("caret navigation did not restore browse keys")
}
browser.followCaret(caret)
if browser.generation != 2 || !keys.enabled {
t.Fatal("repeated caret movement restarted browse routing")
}
}
func TestBrowserModePlaysModeChangeSounds(t *testing.T) {
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, nil)
speech := &fakeSpeechQueue{spoken: make(chan string, 16)}
queue := &modeSoundQueue{speech: speech, sounds: make(chan sound.Cue, 16)}
heading := modeContext(t, "heading", false)
entry := modeContext(t, "entry", false)
focus := heading.Ancestors[0].ID()
browser.follow(focus)
browser.accept(browserModeResult{focus: focus, generation: 1, owner: heading}, queue)
if len(queue.sounds) != 0 || len(speech.spoken) != 0 {
t.Fatal("initial browse mode was announced")
}
browser.toggle(queue)
assertModeSound(t, queue, speech, "focus_mode", "Focus mode")
browser.follow(focus)
browser.accept(browserModeResult{focus: focus, generation: 2, owner: entry}, queue)
if len(queue.sounds) != 0 {
t.Fatalf("unchanged mode queued cue %#v", <-queue.sounds)
}
browser.toggle(queue)
assertModeSound(t, queue, speech, "browse_mode", "Browse mode")
}
func assertModeSound(t *testing.T, queue *modeSoundQueue, speech *fakeSpeechQueue, wantCue, wantText string) {
t.Helper()
select {
case cue := <-queue.sounds:
if cue.Name != wantCue || cue.Pan != 0 {
t.Fatalf("got mode cue %#v want centered %q", cue, wantCue)
}
case <-time.After(time.Second):
t.Fatalf("mode cue %q was not queued", wantCue)
}
select {
case text := <-speech.spoken:
if text != wantText {
t.Fatalf("got mode text %q want %q", text, wantText)
}
case <-time.After(time.Second):
t.Fatalf("mode text %q was not spoken", wantText)
}
}
func TestBrowserModeAcceptsSameDocumentNavigationDuringRecapture(t *testing.T) {
browser := newBrowserModeController(nil, &modeKeys{}, nil)
heading := modeContext(t, "heading", false)
focus := heading.Ancestors[0].ID()
browser.follow(focus)
queue := &fakeSpeechQueue{spoken: make(chan string, 1)}
browser.accept(browserModeResult{focus: focus, generation: 1, owner: heading}, queue)
position := review.DocumentPosition{
Document: heading.TopLevel,
Snapshot: heading.Ancestors[0],
}
if !browser.acceptsDocumentResult(review.Result{DocumentPosition: position}) {
t.Fatal("same-document navigation result rejected")
}
browser.follow(atspi.NewObjectID(":1.42", "/second"))
if browser.ready {
t.Fatal("new focus did not start recapture")
}
if browser.acceptsDocumentResult(review.Result{DocumentPosition: position}) {
t.Fatal("old result accepted while the new focus document is unknown")
}
browser.follow(position.Snapshot.ID())
if !browser.acceptsDocumentResult(review.Result{DocumentPosition: position}) {
t.Fatal("native focus echo on the structural target rejected")
}
other := modeContextInDocument(t, "heading", false, "/other_document").TopLevel
if browser.acceptsDocumentResult(review.Result{DocumentPosition: review.DocumentPosition{Document: other}}) {
t.Fatal("other-document result accepted")
}
if browser.acceptsDocumentResult(review.Result{Err: errors.New("capture failed"), DocumentPosition: position}) {
t.Fatal("failed result accepted")
}
if !browser.acceptsDocumentResult(review.Result{Err: errors.New("capture failed"), DocumentFocus: position.Snapshot.ID()}) {
t.Fatal("current navigation failure would be silently discarded")
}
}
func TestBrowserRuntimeModeProtectsAmbiguousAndEditableContent(t *testing.T) {
for _, tc := range []struct {
role string
editable bool
want review.DocumentMode
}{
{"heading", false, review.BrowseMode}, {"entry", false, review.FocusMode},
{"paragraph", true, review.FocusMode}, {"table cell", false, review.FocusMode},
{"embedded", false, review.FocusMode}, {"menu item", false, review.FocusMode},
} {
t.Run(tc.role, func(t *testing.T) {
var state review.DocumentModeState
got := state.Update(runtimeModeInput(modeContext(t, tc.role, tc.editable)), review.DefaultModeSettings())
if got != tc.want {
t.Fatalf("got %v want %v", got, tc.want)
}
})
}
}
type canceledModeCapture struct{ started chan struct{} }
func (capture canceledModeCapture) CaptureDocumentContext(ctx context.Context, _ atspi.ObjectID) (atspi.DocumentContext, error) {
close(capture.started)
<-ctx.Done()
return atspi.DocumentContext{}, ctx.Err()
}
func TestBrowserModeShutdownCancelsNativeWait(t *testing.T) {
capture := canceledModeCapture{make(chan struct{})}
browser := newBrowserModeController(nil, &modeKeys{}, capture)
if err := browser.Start(context.Background()); err != nil {
t.Fatal(err)
}
browser.follow(atspi.NewObjectID(":1.42", "/target"))
select {
case <-capture.started:
case <-time.After(time.Second):
t.Fatal("capture did not start")
}
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
if err := browser.Stop(ctx); err != nil && !errors.Is(err, context.Canceled) {
t.Fatal(err)
}
}
type timeoutThenModeCapture struct {
calls chan struct{}
owner atspi.DocumentContext
}
func (capture *timeoutThenModeCapture) CaptureDocumentContext(ctx context.Context, _ atspi.ObjectID) (atspi.DocumentContext, error) {
capture.calls <- struct{}{}
if len(capture.calls) == 1 {
<-ctx.Done()
return atspi.DocumentContext{}, ctx.Err()
}
return capture.owner, nil
}
func TestBrowserModeRetriesTimedOutCurrentFocus(t *testing.T) {
owner := modeContext(t, "heading", false)
focus := owner.Ancestors[0].ID()
capture := &timeoutThenModeCapture{calls: make(chan struct{}, 2), owner: owner}
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, capture)
browser.captureTimeout = 20 * time.Millisecond
if err := browser.Start(context.Background()); err != nil {
t.Fatal(err)
}
defer browser.Stop(context.Background())
browser.follow(focus)
select {
case result := <-browser.results:
browser.accept(result, &fakeSpeechQueue{spoken: make(chan string, 1)})
if result.err != nil || result.focus != focus || !keys.enabled || len(capture.calls) != 2 {
t.Fatalf("timed-out capture did not recover: %+v, calls %d", result, len(capture.calls))
}
case <-time.After(time.Second):
t.Fatal("timed-out capture was not retried")
}
}
type cancelableModeCapture struct {
started chan atspi.ObjectID
canceled chan atspi.ObjectID
}
func (capture *cancelableModeCapture) CaptureDocumentContext(ctx context.Context, focus atspi.ObjectID) (atspi.DocumentContext, error) {
started := false
for !started {
select {
case capture.started <- focus:
started = true
case <-ctx.Done():
return atspi.DocumentContext{}, ctx.Err()
}
}
<-ctx.Done()
select {
case capture.canceled <- focus:
case <-context.Background().Done():
}
return atspi.DocumentContext{}, ctx.Err()
}
func TestBrowserModeCancelsCaptureOnLoadLifecycle(t *testing.T) {
first := atspi.NewObjectID(":1.42", "/first")
second := atspi.NewObjectID(":1.42", "/second")
capture := &cancelableModeCapture{
started: make(chan atspi.ObjectID, 1),
canceled: make(chan atspi.ObjectID, 1),
}
browser := newBrowserModeController(nil, &modeKeys{}, capture)
if err := browser.Start(context.Background()); err != nil {
t.Fatal(err)
}
defer browser.Stop(context.Background())
browser.follow(first)
select {
case got := <-capture.started:
if got != first {
t.Fatalf("first capture focus = %v", got)
}
case <-time.After(time.Second):
t.Fatal("first capture did not start")
}
browser.loading()
select {
case got := <-capture.canceled:
if got != first {
t.Fatalf("canceled capture focus = %v", got)
}
case <-time.After(time.Second):
t.Fatal("page loading did not cancel the old capture")
}
if len(capture.started) != 0 {
t.Fatal("page loading started a new capture before load completion")
}
browser.loaded(second)
select {
case got := <-capture.started:
if got != second {
t.Fatalf("second capture focus = %v", got)
}
case <-time.After(time.Second):
t.Fatal("load completion did not start the latest capture")
}
}
func TestBrowserLoadingRejectsOldCaptureAndRecoversOnStop(t *testing.T) {
owner := modeContext(t, "heading", false)
focus := owner.Ancestors[0].ID()
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, nil)
speech := &fakeSpeechQueue{spoken: make(chan string, 1)}
browser.follow(focus)
browser.accept(browserModeResult{focus: focus, generation: 1, owner: owner}, speech)
if !keys.enabled {
t.Fatal("initial page did not enable browse mode")
}
browser.loading()
if browser.ready || keys.enabled {
t.Fatal("reloading page retained old browse keys")
}
browser.accept(browserModeResult{focus: focus, generation: 1, owner: owner}, speech)
if browser.ready || keys.enabled {
t.Fatal("late pre-reload capture restored the old page")
}
browser.loaded(focus)
browser.accept(browserModeResult{focus: focus, generation: browser.generation, owner: owner}, speech)
if !browser.ready || !keys.enabled {
t.Fatal("stopped page load did not restore browse mode")
}
}
func TestBrowserModeSuppressesStructuralNavigationEcho(t *testing.T) {
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, nil)
target := atspi.NewSnapshot("/target", "Target", "heading", "", "")
target.BusName = ":1.42"
owner := modeContext(t, "heading", false)
browser.navigated(review.DocumentPosition{
Document: owner.TopLevel,
Snapshot: target,
}, target.ID())
for _, eventType := range []string{atspi.CaretEvent, atspi.FocusEvent, atspi.FocusedStateEvent} {
if !browser.suppressNavigationEcho(atspi.NewEvent(eventType, target)) {
t.Fatalf("%s echo was not suppressed", eventType)
}
}
child := atspi.NewSnapshot("/new-child", "Child", "static", "", "")
child.BusName = ":1.42"
for _, eventType := range []string{atspi.CaretEvent, atspi.FocusEvent, atspi.FocusedStateEvent} {
if !browser.suppressNavigationEcho(atspi.NewEvent(eventType, child)) {
t.Fatalf("%s descendant echo was not suppressed", eventType)
}
}
other := atspi.NewSnapshot("/other", "Other", "heading", "", "")
other.BusName = ":1.99"
if browser.suppressNavigationEcho(atspi.NewEvent(atspi.FocusEvent, other)) {
t.Fatal("unrelated focus echo was suppressed")
}
browser.observeCommand(input.ToggleDocumentMode)
if browser.suppressNavigationEcho(atspi.NewEvent(atspi.FocusEvent, target)) {
t.Fatal("non-structural command retained navigation suppression")
}
}
func TestBrowserNavigationEchoEndsOnNativeKey(t *testing.T) {
tracker := &input.IntentTracker{}
tracker.Observe(input.NewKeyEvent(0, true, "h"))
browser := newBrowserModeController(tracker, &modeKeys{}, nil)
target := atspi.Snapshot{BusName: ":1.42", Path: "/heading", Role: "heading"}
browser.navigated(review.DocumentPosition{Snapshot: target}, target.ID())
if !browser.suppressNavigationEcho(atspi.NewEvent(atspi.CaretEvent, target)) {
t.Fatal("structural echo was not suppressed")
}
tracker.Observe(input.NewKeyEvent(0, true, "Up"))
if browser.suppressNavigationEcho(atspi.NewEvent(atspi.CaretEvent, target)) {
t.Fatal("native movement back into heading was suppressed")
}
}
func TestBrowserTracksRecentlyConsumedCommand(t *testing.T) {
browser := newBrowserModeController(nil, &modeKeys{}, nil)
if browser.recentConsumedCommand() {
t.Fatal("new browser controller reported a consumed command")
}
browser.observeCommand(input.ReviewNextLine)
if !browser.recentConsumedCommand() {
t.Fatal("fresh command was not reported as consumed")
}
browser.mutex.Lock()
browser.commandAt = time.Now().Add(-time.Second)
browser.mutex.Unlock()
if browser.recentConsumedCommand() {
t.Fatal("stale command was reported as recently consumed")
}
}
type activeCommandKeys struct {
modeKeys
active bool
}
func (keys *activeCommandKeys) CommandGrabActive() bool { return keys.active }
func TestBrowserRecognizesCommandGrabBeforeDispatch(t *testing.T) {
keys := &activeCommandKeys{active: true}
browser := newBrowserModeController(nil, keys, nil)
if !browser.recentConsumedCommand() {
t.Fatal("active command grab was not recognized before dispatch")
}
keys.active = false
if browser.recentConsumedCommand() {
t.Fatal("inactive command grab was reported without a dispatched command")
}
}
func TestBrowserNavigationEchoSurvivesFastStructuralRepeat(t *testing.T) {
tracker := &input.IntentTracker{}
tracker.Observe(input.NewKeyEvent(0, true, "h"))
browser := newBrowserModeController(tracker, &modeKeys{}, nil)
owner := modeContext(t, "heading", false)
target := atspi.Snapshot{BusName: ":1.42", Path: "/heading", Role: "heading"}
browser.navigated(review.DocumentPosition{Document: owner.TopLevel, Snapshot: target}, target.ID())
tracker.Observe(input.NewKeyEvent(0, true, "h"))
child := atspi.Snapshot{BusName: ":1.42", Path: "/replacement", Role: "static"}
if !browser.suppressNavigationEcho(atspi.NewEvent(atspi.CaretEvent, child)) {
t.Fatal("fast repeated structural command exposed prior caret echo")
}
}
func TestBrowserLoadRetiresPositionEvenWhenPathIsReused(t *testing.T) {
browser := newBrowserModeController(nil, &modeKeys{}, nil)
owner := modeContext(t, "heading", false)
position := review.DocumentPosition{Document: owner.TopLevel, Snapshot: owner.Ancestors[0]}
browser.navigated(position, position.Snapshot.ID())
browser.loaded(position.Snapshot.ID())
if browser.ready || browser.mode != review.InactiveMode || browser.acceptsDocumentResult(review.Result{DocumentPosition: position}) {
t.Fatal("load retained the old document position")
}
if browser.suppressNavigationEcho(atspi.NewEvent(atspi.FocusEvent, position.Snapshot)) {
t.Fatal("load retained old navigation echo suppression")
}
select {
case <-browser.wake:
default:
t.Fatal("same-path load did not schedule fresh context capture")
}
}
func TestBrowserLoadAcceptsCurrentFragmentTarget(t *testing.T) {
browser := newBrowserModeController(nil, &modeKeys{}, nil)
document := atspi.NewObjectID(":1.42", "/document")
browser.loaded(document)
result := review.Result{DocumentFragment: true, DocumentFocus: document, Moved: true}
if !browser.acceptsDocumentResult(result) {
t.Fatal("current document fragment was rejected before context resolution")
}
result.DocumentFocus = atspi.NewObjectID(":1.42", "/old_document")
if browser.acceptsDocumentResult(result) {
t.Fatal("old document fragment crossed a page load")
}
}
func TestBrowserModeRetriesSameFocusAfterCaptureFailure(t *testing.T) {
browser := newBrowserModeController(nil, &modeKeys{}, nil)
owner := modeContext(t, "heading", false)
focus := owner.Ancestors[0].ID()
browser.follow(focus)
browser.accept(browserModeResult{focus: focus, generation: 1, err: atspi.ErrStaleObject}, &fakeSpeechQueue{})
browser.follow(focus)
if browser.generation != 2 {
t.Fatal("transient context failure permanently disabled same-object navigation")
}
browser.accept(browserModeResult{focus: focus, generation: 2, owner: owner}, &fakeSpeechQueue{})
if !browser.ready || browser.mode != review.BrowseMode {
t.Fatal("same-object retry did not restore browse mode")
}
}
func TestBrowserReportsNavigationFailureAfterCaretMovesFromNativeFocus(t *testing.T) {
browser := newBrowserModeController(nil, &modeKeys{}, nil)
owner := modeContext(t, "heading", false)
anchor := owner.TopLevel.ID()
position := review.DocumentPosition{Document: owner.TopLevel, Snapshot: owner.Ancestors[0], Origin: review.CaretNavigation}
browser.navigated(position, anchor)
for _, result := range []review.Result{
{DocumentCaretCommand: review.CaretNextLine, DocumentFocus: anchor, Err: context.DeadlineExceeded},
{DocumentTarget: review.DocumentHeading, DocumentFocus: anchor, Err: context.DeadlineExceeded},
{DocumentActivate: true, DocumentFocus: anchor, Err: context.DeadlineExceeded},
} {
if !browser.acceptsDocumentResult(result) {
t.Fatalf("current navigation error was silently discarded: %+v", result)
}
result.DocumentFocus = atspi.NewObjectID(anchor.BusName, "/another-document")
if browser.acceptsDocumentResult(result) {
t.Fatal("accepted an unrelated navigation error")
}
}
browser.follow(atspi.NewObjectID(anchor.BusName, "/address-bar"))
if browser.acceptsDocumentResult(review.Result{DocumentCaretCommand: review.CaretNextLine, DocumentFocus: anchor, Err: context.DeadlineExceeded}) {
t.Fatal("old document error interrupted chrome focus")
}
}
func TestStickyBrowserModeSurvivesNavigationChromeAndWindowSuspension(t *testing.T) {
for _, mode := range []review.DocumentMode{review.BrowseMode, review.FocusMode} {
keys := &modeKeys{}
browser := newBrowserModeController(nil, keys, nil)
queue := &fakeSpeechQueue{spoken: make(chan string, 20)}
owner := modeContext(t, "paragraph", false)
focus := owner.Ancestors[0].ID()
accept := func(owner atspi.DocumentContext) {
browser.follow(focus)
browser.accept(browserModeResult{focus: focus, generation: browser.generation, owner: owner}, queue)
}
accept(owner)
browser.sticky(mode, queue)
browser.navigated(review.DocumentPosition{Document: owner.TopLevel, Snapshot: owner.Ancestors[0]}, focus)
if browser.mode != mode {
t.Fatal("navigation discarded sticky mode")
}
browser.clear()
accept(modeContext(t, "entry", false))
if browser.mode != mode || keys.enabled != (mode == review.BrowseMode) {
t.Fatal("window return lost sticky mode")
}
browser.loaded(focus)
accept(modeContextInDocument(t, "entry", false, "/new_document"))
if browser.mode != mode {
t.Fatal("new document lost sticky mode")
}
browser.toggle(queue)
if browser.state.Sticky() != review.InactiveMode || len(browser.stickyModes) != 0 {
t.Fatal("single press failed to clear sticky mode")
}
}
}