From b7c9a0b921301462564043571d0281790d03da01 Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Fri, 21 Aug 2026 11:34:40 +0200 Subject: [PATCH 1/2] fix(assets): delay connection-loss recovery --- lib/assets/AI.md | 9 ++- lib/assets/jaws.js | 62 ++++++++++----- lib/assets/js_test.go | 171 ++++++++++++++++++++++++++++++++++++++++-- 3 files changed, 212 insertions(+), 30 deletions(-) diff --git a/lib/assets/AI.md b/lib/assets/AI.md index 5d76e6be..c61219e8 100644 --- a/lib/assets/AI.md +++ b/lib/assets/AI.md @@ -37,9 +37,12 @@ set or remove the managed `id` attribute and that accept only canonical positive - Each command in a batched frame is isolated. A failing DOM command is logged and later commands in the same frame still run. -The reconnect path probes `/jaws/.ping` and reloads only after the navigation is -old enough; pagehide closes the active socket and a bfcache pageshow reloads. -Keep reconnect constants and behavior covered by the JavaScript runtime tests. +The reconnect path observes a five-second grace period after a WebSocket +failure. It neither shows the connection-lost indicator nor probes +`/jaws/.ping` before that period elapses. It reloads only after the navigation +is old enough. Pagehide invalidates active and reconnecting state, and a bfcache +pageshow reloads. Keep reconnect constants and behavior covered by the +JavaScript runtime tests. ## Browser helpers diff --git a/lib/assets/jaws.js b/lib/assets/jaws.js index 001631b7..00cf84dc 100644 --- a/lib/assets/jaws.js +++ b/lib/assets/jaws.js @@ -11,10 +11,13 @@ // the initial HTTP request. var jaws = null; +var jawsFailureGraceTimer = null; var jawsIdPrefix = 'Jid.'; var jawsDebug = false; const jawsJidRx = /^[1-9]\d*$/; const jawsMaxJid = '9223372036854775807'; +// Milliseconds after WebSocket failure before the lost indicator and probes begin. +const jawsFailureGracePeriod = 5 * 1000; // Milliseconds; bounds reconnect probes that never receive a network result. const jawsReconnectTimeout = 10 * 1000; // Minimum navigation age for a reconnect-triggered page reload. @@ -344,28 +347,26 @@ function jawsSetValue(elem, str) { elem.value = str; } -function jawsLost() { - let delay = 1; +function jawsShowLost() { + if (!(jaws instanceof Date)) { + return; + } let innerHTML = 'Server connection lost'; - if (jaws instanceof Date) { - let elapsed = Math.floor((Date.now() - jaws) / 1000); - if (elapsed > 0) { - let units = ' second'; - delay = elapsed; + let elapsed = Math.floor((Date.now() - jaws) / 1000); + if (elapsed > 0) { + let units = ' second'; + if (elapsed >= 60) { + units = ' minute'; + elapsed = Math.floor(elapsed / 60); if (elapsed >= 60) { - delay = 60; - units = ' minute'; + units = ' hour'; elapsed = Math.floor(elapsed / 60); - if (elapsed >= 60) { - units = ' hour'; - elapsed = Math.floor(elapsed / 60); - } } - if (elapsed > 1) { - units += 's'; - } - innerHTML += ' ' + elapsed + units + ' ago'; } + if (elapsed > 1) { + units += 's'; + } + innerHTML += ' ' + elapsed + units + ' ago'; } innerHTML += '. Trying to reconnect.'; let elem = document.querySelector('[data-jaws-lost]'); @@ -376,10 +377,22 @@ function jawsLost() { } else { elem.innerHTML = innerHTML; } +} + +function jawsLost() { + if (!(jaws instanceof Date)) { + return; + } + jawsShowLost(); + const elapsed = Math.floor((Date.now() - jaws) / 1000); + const delay = Math.max(1, Math.min(60, elapsed)); setTimeout(jawsReconnect, delay * 1000); } function jawsHandleReconnect(e) { + if (!(jaws instanceof Date)) { + return; + } // Reloading resets the navigation clock, so repeated failures enter the backoff. if (e.currentTarget.status === 204 && performance.now() >= jawsReconnectReloadMinPageAge) { window.location.reload(); @@ -389,6 +402,9 @@ function jawsHandleReconnect(e) { } function jawsReconnect() { + if (!(jaws instanceof Date)) { + return; + } const req = new XMLHttpRequest(); req.open("GET", window.location.protocol + "//" + window.location.host + "/jaws/.ping", true); req.timeout = jawsReconnectTimeout; @@ -399,17 +415,25 @@ function jawsReconnect() { function jawsFailed() { if (jaws instanceof WebSocket) { jaws = new Date(); - jawsReconnect(); + jawsFailureGraceTimer = setTimeout(function() { + jawsFailureGraceTimer = null; + jawsShowLost(); + jawsReconnect(); + }, jawsFailureGracePeriod); } } function jawsUnloading() { + if (jawsFailureGraceTimer !== null) { + clearTimeout(jawsFailureGraceTimer); + jawsFailureGraceTimer = null; + } if (jaws instanceof WebSocket) { jaws.removeEventListener('close', jawsFailed); jaws.removeEventListener('error', jawsFailed); jaws.close(); - jaws = null; } + jaws = null; } function jawsElement(html) { diff --git a/lib/assets/js_test.go b/lib/assets/js_test.go index f093c6cb..0bd54cc3 100644 --- a/lib/assets/js_test.go +++ b/lib/assets/js_test.go @@ -2025,6 +2025,7 @@ jawsElement = function(html) { func TestJawsJS_LostUsesDataAttributeHook(t *testing.T) { raw := runJawsJSSnippet(t, lostIndicatorStubs+` setTimeout = function() {}; +jaws = new Date(Date.now() - 5000); jawsLost(); lostIndicatorElem = { innerHTML: "old" }; @@ -2057,6 +2058,158 @@ process.stdout.write(JSON.stringify({ } } +func TestJawsJS_FailureRecoveryWaitsFiveSeconds(t *testing.T) { + raw := runJawsJSSnippet(t, lostIndicatorStubs+` +let now = 100000; +Date.now = function() { return now; }; +let reconnects = 0; +jawsReconnect = function() { reconnects++; }; +const timers = []; +let nextTimer = 1; +setTimeout = function(fn, ms) { + const timer = { id: nextTimer++, fn: fn, ms: ms }; + timers.push(timer); + return timer.id; +}; + +function FakeSocket() {} +WebSocket = FakeSocket; +jaws = new FakeSocket(); +jawsFailed(); +jawsFailed(); +jaws = new Date(now); +const afterFailure = { + reconnects: reconnects, + delays: timers.map(function(timer) { return timer.ms; }), + selectors: lostIndicatorSelectors.length, + prepended: lostIndicatorPrepended +}; + +now += 5000; +timers[0].fn(); +const afterGrace = { + reconnects: reconnects, + delays: timers.map(function(timer) { return timer.ms; }), + selectors: lostIndicatorSelectors.length, + prepended: lostIndicatorPrepended, + created: lostIndicatorCreated +}; + +// A failed probe after the grace period updates the banner and backs off. +jawsLost(); +const afterFailedProbe = { + reconnects: reconnects, + delays: timers.map(function(timer) { return timer.ms; }), + selectors: lostIndicatorSelectors.length, + prepended: lostIndicatorPrepended +}; + +process.stdout.write(JSON.stringify({ + afterFailure: afterFailure, + afterGrace: afterGrace, + afterFailedProbe: afterFailedProbe +})); +`) + + type state struct { + Reconnects int `json:"reconnects"` + Delays []int `json:"delays"` + Selectors int `json:"selectors"` + Prepended int `json:"prepended"` + Created string `json:"created"` + } + var got struct { + AfterFailure state `json:"afterFailure"` + AfterGrace state `json:"afterGrace"` + AfterFailedProbe state `json:"afterFailedProbe"` + } + if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &got); err != nil { + t.Fatalf("failed to parse snippet output %q: %v", raw, err) + } + if want := (state{Delays: []int{5000}}); !reflect.DeepEqual(got.AfterFailure, want) { + t.Fatalf("state after failure = %+v, want %+v", got.AfterFailure, want) + } + if got.AfterGrace.Reconnects != 1 || !reflect.DeepEqual(got.AfterGrace.Delays, []int{5000}) || + got.AfterGrace.Selectors != 1 || got.AfterGrace.Prepended != 1 || + !strings.Contains(got.AfterGrace.Created, "Server connection lost 5 seconds ago") { + t.Fatalf("state after grace = %+v, want delayed recovery to begin", got.AfterGrace) + } + if want := (state{Reconnects: 1, Delays: []int{5000, 5000}, Selectors: 2, Prepended: 1}); !reflect.DeepEqual(got.AfterFailedProbe, want) { + t.Fatalf("state after failed probe = %+v, want %+v", got.AfterFailedProbe, want) + } +} + +func TestJawsJS_PagehideCancelsDelayedFailureRecovery(t *testing.T) { + raw := runJawsJSSnippet(t, lostIndicatorStubs+` +global.performance = { now: function() { return 60000; } }; +let reloads = 0; +window.location.reload = function() { reloads++; }; +const timers = []; +const cleared = []; +let nextTimer = 1; +setTimeout = function(fn, ms) { + const timer = { id: nextTimer++, fn: fn, ms: ms }; + timers.push(timer); + return timer.id; +}; +clearTimeout = function(id) { cleared.push(id); }; + +const requests = []; +function FakeXHR() { + this.status = 0; + this.listeners = {}; + requests.push(this); +} +FakeXHR.prototype.open = function() {}; +FakeXHR.prototype.addEventListener = function(name, fn) { + this.listeners[name] = fn; +}; +FakeXHR.prototype.send = function() {}; +XMLHttpRequest = FakeXHR; + +function FakeSocket() {} +WebSocket = FakeSocket; +jaws = new FakeSocket(); +jawsFailed(); +const delayedRecovery = timers[0].fn; + +// The committed navigation arrives after the transport failure. +jawsUnloading(); +delayedRecovery(); +jawsHandleReconnect({ currentTarget: { status: 0 } }); +jawsHandleReconnect({ currentTarget: { status: 204 } }); +jawsReconnect(); + +process.stdout.write(JSON.stringify({ + jawsIsNull: jaws === null, + requests: requests.length, + delays: timers.map(function(timer) { return timer.ms; }), + cleared: cleared, + selectors: lostIndicatorSelectors.length, + prepended: lostIndicatorPrepended, + reloads: reloads +})); +`) + + var got struct { + JawsIsNull bool `json:"jawsIsNull"` + Requests int `json:"requests"` + Delays []int `json:"delays"` + Cleared []int `json:"cleared"` + Selectors int `json:"selectors"` + Prepended int `json:"prepended"` + Reloads int `json:"reloads"` + } + if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &got); err != nil { + t.Fatalf("failed to parse snippet output %q: %v", raw, err) + } + if !got.JawsIsNull || got.Requests != 0 || !reflect.DeepEqual(got.Delays, []int{5000}) || + !reflect.DeepEqual(got.Cleared, []int{1}) || got.Selectors != 0 || + got.Prepended != 0 || got.Reloads != 0 { + t.Fatalf("state after pagehide = %+v, want delayed work canceled", got) + } +} + func TestJawsJS_ReconnectXHRResultsAreHandledOnce(t *testing.T) { raw := runJawsJSSnippet(t, lostIndicatorStubs+` global.performance = { now: function() { return 60000; } }; @@ -2118,6 +2271,7 @@ FakeXHR.prototype.finish = function(name, status) { XMLHttpRequest = FakeXHR; jawsFailed(); +scheduled[0].fn(); for (let i = 1; i < 5; i++) jawsReconnect(); const pending = { timers: timers, lost: lost, reloaded: reloaded }; @@ -2135,7 +2289,7 @@ results.push({ timers: timers, lost: lost, reloaded: reloaded }); // One-shot completion prevents stale terminal events from changing the result. requests.slice(0, 5).forEach(function(req) { req.finish("timeout", 0); }); -scheduled[0].fn(); +scheduled[1].fn(); process.stdout.write(JSON.stringify({ jawsIsDate: jaws instanceof Date, @@ -2184,8 +2338,8 @@ process.stdout.write(JSON.stringify({ if len(got.Attempts) != 6 || got.Sends != 6 { t.Fatalf("reconnect attempts = %d requests, %d sends; want 6 each", len(got.Attempts), got.Sends) } - if got.Pending != (counts{}) { - t.Fatalf("pending reconnect changed state: %+v", got.Pending) + if got.Pending != (counts{Timers: 1}) { + t.Fatalf("pending reconnect state = %+v, want one failure-grace timer", got.Pending) } for i, attempt := range got.Attempts { if attempt.Method != "GET" || attempt.URL != "http://example.test/jaws/.ping" || !attempt.Async { @@ -2196,11 +2350,11 @@ process.stdout.write(JSON.stringify({ } } wants := []counts{ - {Timers: 1, Lost: 1}, - {Timers: 2, Lost: 2}, - {Timers: 3, Lost: 3}, - {Timers: 4, Lost: 4}, - {Timers: 4, Lost: 4, Reloaded: 1}, + {Timers: 2, Lost: 1}, + {Timers: 3, Lost: 2}, + {Timers: 4, Lost: 3}, + {Timers: 5, Lost: 4}, + {Timers: 5, Lost: 4, Reloaded: 1}, } if !reflect.DeepEqual(got.Results, wants) { t.Fatalf("reconnect results = %+v, want %+v", got.Results, wants) @@ -2217,6 +2371,7 @@ func TestJawsJS_ReconnectReloadUsesNavigationAge(t *testing.T) { raw := runJawsJSSnippet(t, lostIndicatorStubs+` let navigationAge = 59999; global.performance = { now: function() { return navigationAge; } }; +jaws = new Date(); let reloads = 0; window.location.reload = function() { reloads++; }; const delays = []; From 257ba2764e5ee7d538253a96eb2a07d198c28fc0 Mon Sep 17 00:00:00 2001 From: Johan Lindh Date: Fri, 21 Aug 2026 12:21:23 +0200 Subject: [PATCH 2/2] fix(assets): probe before showing connection loss --- lib/assets/AI.md | 14 ++++++--- lib/assets/jaws.js | 26 ++++------------- lib/assets/js_test.go | 67 +++++++++++++++++++++++++++++-------------- 3 files changed, 60 insertions(+), 47 deletions(-) diff --git a/lib/assets/AI.md b/lib/assets/AI.md index c61219e8..9ed0cfb5 100644 --- a/lib/assets/AI.md +++ b/lib/assets/AI.md @@ -39,10 +39,16 @@ set or remove the managed `id` attribute and that accept only canonical positive The reconnect path observes a five-second grace period after a WebSocket failure. It neither shows the connection-lost indicator nor probes -`/jaws/.ping` before that period elapses. It reloads only after the navigation -is old enough. Pagehide invalidates active and reconnecting state, and a bfcache -pageshow reloads. Keep reconnect constants and behavior covered by the -JavaScript runtime tests. +`/jaws/.ping` before that period elapses. If the old document remains active, +the first probe reloads an old enough page without showing the indicator when +the server is available; otherwise, the indicator appears after the probe +completes and elapsed-time backoff begins. A stalled probe can defer the +indicator until its ten-second timeout, roughly 15 seconds after failure. A +cross-document navigation that leaves the old document active beyond the grace +period can still show the indicator or be superseded by a reconnect-triggered +reload before pagehide invalidates reconnecting state. A bfcache pageshow +reloads. Keep reconnect constants and behavior covered by the JavaScript +runtime tests. ## Browser helpers diff --git a/lib/assets/jaws.js b/lib/assets/jaws.js index 00cf84dc..c87154b8 100644 --- a/lib/assets/jaws.js +++ b/lib/assets/jaws.js @@ -11,12 +11,11 @@ // the initial HTTP request. var jaws = null; -var jawsFailureGraceTimer = null; var jawsIdPrefix = 'Jid.'; var jawsDebug = false; const jawsJidRx = /^[1-9]\d*$/; const jawsMaxJid = '9223372036854775807'; -// Milliseconds after WebSocket failure before the lost indicator and probes begin. +// Milliseconds after WebSocket failure before reconnect probing begins. const jawsFailureGracePeriod = 5 * 1000; // Milliseconds; bounds reconnect probes that never receive a network result. const jawsReconnectTimeout = 10 * 1000; @@ -347,13 +346,15 @@ function jawsSetValue(elem, str) { elem.value = str; } -function jawsShowLost() { +function jawsLost() { if (!(jaws instanceof Date)) { return; } + let delay = 1; let innerHTML = 'Server connection lost'; let elapsed = Math.floor((Date.now() - jaws) / 1000); if (elapsed > 0) { + delay = Math.min(60, elapsed); let units = ' second'; if (elapsed >= 60) { units = ' minute'; @@ -377,15 +378,6 @@ function jawsShowLost() { } else { elem.innerHTML = innerHTML; } -} - -function jawsLost() { - if (!(jaws instanceof Date)) { - return; - } - jawsShowLost(); - const elapsed = Math.floor((Date.now() - jaws) / 1000); - const delay = Math.max(1, Math.min(60, elapsed)); setTimeout(jawsReconnect, delay * 1000); } @@ -415,19 +407,11 @@ function jawsReconnect() { function jawsFailed() { if (jaws instanceof WebSocket) { jaws = new Date(); - jawsFailureGraceTimer = setTimeout(function() { - jawsFailureGraceTimer = null; - jawsShowLost(); - jawsReconnect(); - }, jawsFailureGracePeriod); + setTimeout(jawsReconnect, jawsFailureGracePeriod); } } function jawsUnloading() { - if (jawsFailureGraceTimer !== null) { - clearTimeout(jawsFailureGraceTimer); - jawsFailureGraceTimer = null; - } if (jaws instanceof WebSocket) { jaws.removeEventListener('close', jawsFailed); jaws.removeEventListener('error', jawsFailed); diff --git a/lib/assets/js_test.go b/lib/assets/js_test.go index 0bd54cc3..eb7e82a3 100644 --- a/lib/assets/js_test.go +++ b/lib/assets/js_test.go @@ -2025,7 +2025,7 @@ jawsElement = function(html) { func TestJawsJS_LostUsesDataAttributeHook(t *testing.T) { raw := runJawsJSSnippet(t, lostIndicatorStubs+` setTimeout = function() {}; -jaws = new Date(Date.now() - 5000); +jaws = new Date(); jawsLost(); lostIndicatorElem = { innerHTML: "old" }; @@ -2062,6 +2062,9 @@ func TestJawsJS_FailureRecoveryWaitsFiveSeconds(t *testing.T) { raw := runJawsJSSnippet(t, lostIndicatorStubs+` let now = 100000; Date.now = function() { return now; }; +global.performance = { now: function() { return 60000; } }; +let reloads = 0; +window.location.reload = function() { reloads++; }; let reconnects = 0; jawsReconnect = function() { reconnects++; }; const timers = []; @@ -2095,18 +2098,34 @@ const afterGrace = { created: lostIndicatorCreated }; -// A failed probe after the grace period updates the banner and backs off. -jawsLost(); +// An available server reloads an old page without flashing the banner first. +jawsHandleReconnect({ currentTarget: { status: 204 } }); +const afterSuccessfulProbe = { + reconnects: reconnects, + delays: timers.map(function(timer) { return timer.ms; }), + selectors: lostIndicatorSelectors.length, + prepended: lostIndicatorPrepended, + reloads: reloads, + bodyScrollTop: document.body.scrollTop, + documentScrollTop: document.documentElement.scrollTop +}; + +// Exercise the failed-probe outcome independently of the stubbed reload above. +reloads = 0; +jawsHandleReconnect({ currentTarget: { status: 0 } }); const afterFailedProbe = { reconnects: reconnects, delays: timers.map(function(timer) { return timer.ms; }), selectors: lostIndicatorSelectors.length, - prepended: lostIndicatorPrepended + prepended: lostIndicatorPrepended, + created: lostIndicatorCreated, + reloads: reloads }; process.stdout.write(JSON.stringify({ afterFailure: afterFailure, afterGrace: afterGrace, + afterSuccessfulProbe: afterSuccessfulProbe, afterFailedProbe: afterFailedProbe })); `) @@ -2117,11 +2136,15 @@ process.stdout.write(JSON.stringify({ Selectors int `json:"selectors"` Prepended int `json:"prepended"` Created string `json:"created"` + Reloads int `json:"reloads"` + BodyScroll int `json:"bodyScrollTop"` + DocScroll int `json:"documentScrollTop"` } var got struct { - AfterFailure state `json:"afterFailure"` - AfterGrace state `json:"afterGrace"` - AfterFailedProbe state `json:"afterFailedProbe"` + AfterFailure state `json:"afterFailure"` + AfterGrace state `json:"afterGrace"` + AfterSuccessfulProbe state `json:"afterSuccessfulProbe"` + AfterFailedProbe state `json:"afterFailedProbe"` } if err := json.Unmarshal([]byte(strings.TrimSpace(raw)), &got); err != nil { t.Fatalf("failed to parse snippet output %q: %v", raw, err) @@ -2129,30 +2152,33 @@ process.stdout.write(JSON.stringify({ if want := (state{Delays: []int{5000}}); !reflect.DeepEqual(got.AfterFailure, want) { t.Fatalf("state after failure = %+v, want %+v", got.AfterFailure, want) } - if got.AfterGrace.Reconnects != 1 || !reflect.DeepEqual(got.AfterGrace.Delays, []int{5000}) || - got.AfterGrace.Selectors != 1 || got.AfterGrace.Prepended != 1 || - !strings.Contains(got.AfterGrace.Created, "Server connection lost 5 seconds ago") { - t.Fatalf("state after grace = %+v, want delayed recovery to begin", got.AfterGrace) + if want := (state{Reconnects: 1, Delays: []int{5000}}); !reflect.DeepEqual(got.AfterGrace, want) { + t.Fatalf("state after grace = %+v, want probe without lost indicator %+v", got.AfterGrace, want) + } + if want := (state{Reconnects: 1, Delays: []int{5000}, Reloads: 1, BodyScroll: 10, DocScroll: 10}); !reflect.DeepEqual(got.AfterSuccessfulProbe, want) { + t.Fatalf("state after successful probe = %+v, want reload without indicator %+v", got.AfterSuccessfulProbe, want) } - if want := (state{Reconnects: 1, Delays: []int{5000, 5000}, Selectors: 2, Prepended: 1}); !reflect.DeepEqual(got.AfterFailedProbe, want) { - t.Fatalf("state after failed probe = %+v, want %+v", got.AfterFailedProbe, want) + if got.AfterFailedProbe.Reconnects != 1 || + !reflect.DeepEqual(got.AfterFailedProbe.Delays, []int{5000, 5000}) || + got.AfterFailedProbe.Selectors != 1 || got.AfterFailedProbe.Prepended != 1 || + got.AfterFailedProbe.Reloads != 0 || + !strings.Contains(got.AfterFailedProbe.Created, "Server connection lost 5 seconds ago") { + t.Fatalf("state after failed probe = %+v, want indicator and delayed retry", got.AfterFailedProbe) } } -func TestJawsJS_PagehideCancelsDelayedFailureRecovery(t *testing.T) { +func TestJawsJS_PagehideInvalidatesDelayedFailureRecovery(t *testing.T) { raw := runJawsJSSnippet(t, lostIndicatorStubs+` global.performance = { now: function() { return 60000; } }; let reloads = 0; window.location.reload = function() { reloads++; }; const timers = []; -const cleared = []; let nextTimer = 1; setTimeout = function(fn, ms) { const timer = { id: nextTimer++, fn: fn, ms: ms }; timers.push(timer); return timer.id; }; -clearTimeout = function(id) { cleared.push(id); }; const requests = []; function FakeXHR() { @@ -2184,7 +2210,6 @@ process.stdout.write(JSON.stringify({ jawsIsNull: jaws === null, requests: requests.length, delays: timers.map(function(timer) { return timer.ms; }), - cleared: cleared, selectors: lostIndicatorSelectors.length, prepended: lostIndicatorPrepended, reloads: reloads @@ -2195,7 +2220,6 @@ process.stdout.write(JSON.stringify({ JawsIsNull bool `json:"jawsIsNull"` Requests int `json:"requests"` Delays []int `json:"delays"` - Cleared []int `json:"cleared"` Selectors int `json:"selectors"` Prepended int `json:"prepended"` Reloads int `json:"reloads"` @@ -2204,9 +2228,8 @@ process.stdout.write(JSON.stringify({ t.Fatalf("failed to parse snippet output %q: %v", raw, err) } if !got.JawsIsNull || got.Requests != 0 || !reflect.DeepEqual(got.Delays, []int{5000}) || - !reflect.DeepEqual(got.Cleared, []int{1}) || got.Selectors != 0 || - got.Prepended != 0 || got.Reloads != 0 { - t.Fatalf("state after pagehide = %+v, want delayed work canceled", got) + got.Selectors != 0 || got.Prepended != 0 || got.Reloads != 0 { + t.Fatalf("state after pagehide = %+v, want delayed work inert", got) } } @@ -2339,7 +2362,7 @@ process.stdout.write(JSON.stringify({ t.Fatalf("reconnect attempts = %d requests, %d sends; want 6 each", len(got.Attempts), got.Sends) } if got.Pending != (counts{Timers: 1}) { - t.Fatalf("pending reconnect state = %+v, want one failure-grace timer", got.Pending) + t.Fatalf("reconnect state before results = %+v, want only the elapsed grace timeout", got.Pending) } for i, attempt := range got.Attempts { if attempt.Method != "GET" || attempt.URL != "http://example.test/jaws/.ping" || !attempt.Async {