diff --git a/scripts/lib/control-pane/ui.js b/scripts/lib/control-pane/ui.js index 93d608371..690c8d981 100644 --- a/scripts/lib/control-pane/ui.js +++ b/scripts/lib/control-pane/ui.js @@ -439,10 +439,11 @@ 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; - // Loads are numbered as they start, so a failed refresh can tell whether - // the board already shows data from a load that started after it. + // Loads are numbered as they start. The board follows the newest load that + // has finished, with its data or with its failure, so a load that finishes + // after a newer one leaves the board alone. let loadsStarted = 0; - let shownLoad = 0; + let newestFinished = 0; function showRefreshFailure(error) { const since = loadedAt ? ' The data below is from ' + loadedAt.toLocaleString() + '.' @@ -644,6 +645,8 @@ function renderControlPaneHtml() { if (state.query) url.searchParams.set('query', state.query); const response = await fetch(url); const snapshot = await readJsonResponse(response); + if (id < newestFinished) return; + newestFinished = id; $('#query').value = snapshot.knowledge.query || state.query; $('#db-path').textContent = snapshot.database.exists ? snapshot.dbPath : 'database missing'; state.allowActions = Boolean(snapshot.execution.allowActions); @@ -658,7 +661,6 @@ function renderControlPaneHtml() { executable: snapshot.execution.allowActions && action.executable }))); loadedAt = new Date(); - shownLoad = id; clearError('#app'); } @@ -703,10 +705,9 @@ function renderControlPaneHtml() { // load() numbers itself before its first await. const id = loadsStarted; refresh.catch(error => { - // Data from a load that started later is already on the board, so - // this failure no longer describes it. An older load that succeeded - // in the meantime does not make the board live. - if (shownLoad < id) showRefreshFailure(error); + if (id < newestFinished) return; + newestFinished = id; + showRefreshFailure(error); }); }, 15000); diff --git a/tests/lib/control-pane-ui.test.js b/tests/lib/control-pane-ui.test.js index d7f8a50b3..14809b96b 100644 --- a/tests/lib/control-pane-ui.test.js +++ b/tests/lib/control-pane-ui.test.js @@ -180,6 +180,26 @@ async function runTests() { passed++; else failed++; + if ( + await test('an older refresh that succeeds after a newer one failed leaves the failure up', async () => { + const page = openPage(snapshot); + await settle(); + + page.hold = true; + page.refresh(); + page.refresh(); + page.pending[1].fail(); + await settle(); + page.pending[0].succeed(); + await settle(); + const box = page.element('#app'); + assert.strictEqual(box.hidden, false, 'no load that started after the failed one has succeeded'); + assert.match(box.textContent, /Live refresh failed\. The data below is from /); + }) + ) + passed++; + else failed++; + console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`); process.exit(failed > 0 ? 1 : 0); }