diff --git a/scripts/lib/control-pane/ui.js b/scripts/lib/control-pane/ui.js index eb4b0ec0c..2d7db4ae2 100644 --- a/scripts/lib/control-pane/ui.js +++ b/scripts/lib/control-pane/ui.js @@ -438,9 +438,10 @@ function renderControlPaneHtml() { // The board keeps the last snapshot on screen, so a failed refresh has to // say that the data is no longer live, and since when. + let loadedAt = null; function showRefreshFailure(error) { - const since = state.loadedAt - ? ' The data below is from ' + state.loadedAt.toLocaleTimeString() + '.' + const since = loadedAt + ? ' The data below is from ' + loadedAt.toLocaleString() + '.' : ''; showError('#app', 'Live refresh failed.' + since + '\\n' + formatError(error)); } @@ -651,7 +652,7 @@ function renderControlPaneHtml() { ...action, executable: snapshot.execution.allowActions && action.executable }))); - state.loadedAt = new Date(); + loadedAt = new Date(); clearError('#app'); } @@ -692,7 +693,12 @@ function renderControlPaneHtml() { // Live board: refresh on a gentle interval; pause while a prompt/tab is hidden. setInterval(() => { if (document.hidden) return; - load().catch(showRefreshFailure); + const shownSince = loadedAt; + load().catch(error => { + // A load that finished in the meantime already replaced the data this + // refresh was for, so its failure no longer describes the board. + if (loadedAt === shownSince) showRefreshFailure(error); + }); }, 15000); load().catch(error => showError('#app', error)); diff --git a/tests/lib/control-pane-ui.test.js b/tests/lib/control-pane-ui.test.js index 747228988..6e883c3ad 100644 --- a/tests/lib/control-pane-ui.test.js +++ b/tests/lib/control-pane-ui.test.js @@ -28,7 +28,8 @@ function inlineScript(html) { // Runs the page script against a stand-in for the few browser APIs it uses: // elements looked up by selector, fetch, and a setInterval whose callback the -// test fires itself. +// test fires itself. While `hold` is set, a fetch waits in `pending` until the +// test settles it. function openPage(snapshot) { const elements = new Map(); const element = selector => { @@ -44,17 +45,23 @@ function openPage(snapshot) { } return elements.get(selector); }; - const page = { online: true, refresh: null, element }; + const page = { online: true, hold: false, pending: [], refresh: null, element }; vm.runInNewContext(inlineScript(renderControlPaneHtml()), { document: { hidden: false, querySelector: element, querySelectorAll: () => [] }, window: { location: { href: 'http://127.0.0.1:8765/' } }, URL, Intl, console, - fetch: async () => { - if (!page.online) throw new TypeError('Failed to fetch'); - return { ok: true, status: 200, statusText: 'OK', json: async () => snapshot }; - }, + fetch: () => + new Promise((resolve, reject) => { + const reply = { + succeed: () => resolve({ ok: true, status: 200, statusText: 'OK', json: async () => snapshot }), + fail: () => reject(new TypeError('Failed to fetch')) + }; + if (page.hold) page.pending.push(reply); + else if (page.online) reply.succeed(); + else reply.fail(); + }), setInterval: callback => { page.refresh = callback; } @@ -92,6 +99,7 @@ async function runTests() { const box = page.element('#app'); assert.strictEqual(box.hidden, false, 'the failure is shown'); assert.match(box.textContent, /Live refresh failed\. The data below is from /); + assert.ok(box.textContent.includes(String(new Date().getFullYear())), 'the time of the data includes its date'); assert.match(box.textContent, /Failed to fetch/); page.online = true; @@ -103,6 +111,24 @@ async function runTests() { passed++; else failed++; + if ( + await test('a refresh that fails after a newer load succeeded is not reported', async () => { + const page = openPage(snapshot); + await settle(); + + page.hold = true; + page.refresh(); + page.hold = false; + page.refresh(); + await settle(); + page.pending[0].fail(); + await settle(); + assert.strictEqual(page.element('#app').hidden, true, 'the newer data is not marked as stale'); + }) + ) + passed++; + else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }