test: cover partial observer chart construction failures (#2354)

Partial follow-up for #1778; addresses the chart-cleanup test gap
identified in the review of #2339.

The observer detail unit harness now throws during the second or fourth
chart constructor. Both cases require existing chart instances to be
destroyed, metadata/calibration to remain visible, the error to be
rendered as text, and a later time-window retry to recover without extra
requests.

Production cleanup already behaves correctly; this PR adds coverage
only. The parent-run ownership suite passes 30/30. Removing the actual
cleanup produces exactly the two intended assertion failures. The
remaining unit sources and runner match the current-master baseline
whose 222 frontend suites passed; only this updated unit file differs
within that test set.

TDD scope: no production change or new behavior. The negative control
removes existing cleanup to prove the new assertions detect its loss. No
configuration, dependencies or runtime performance changes. Exact-head
CI passed, including the browser shards. Independent adversarial,
correctness and TDD reviews found no must-fix items.

Co-authored-by: n30nex <6686206+n30nex@users.noreply.github.com>
This commit is contained in:
n30nexandn30nex authored and GitHub committed 2026-10-31 12:00:06 +01:00
1 parent b3a9f2a9e9
commit d083f9ade9
1 file changed
+30 -2
+30 -2
View File
@@ -5,10 +5,11 @@ const fs = require('fs');
const path = require('path');
const vm = require('vm');
function harness() {
function harness(failChartAt = 0) {
const elements = new Map();
const requests = [];
const charts = [];
let chartAttempts = 0;
let page;
function element(id) {
const children = [];
@@ -36,7 +37,12 @@ function harness() {
const app = element('app');
class Chart {
static defaults = {};
constructor(canvas, config) { this.config = config; this.destroyed = false; charts.push(this); }
constructor(canvas, config) {
if (++chartAttempts === failChartAt) throw new Error('<chart construction failure>');
this.config = config;
this.destroyed = false;
charts.push(this);
}
destroy() { this.destroyed = true; }
}
const context = vm.createContext({
@@ -147,6 +153,28 @@ async function test(name, fn) {
});
}
for (const failedChart of [2, 4]) {
await test('chart construction failure ' + failedChart + ' cleans up partial analytics and permits retry', async () => {
const h = harness(failedChart);
complete(h.visit('A'), 'A');
await settled();
assert.strictEqual(h.charts.length, failedChart - 1, 'failure follows successful chart construction');
assert.ok(h.charts.every(chart => chart.destroyed), 'already constructed charts must be destroyed');
assert.strictEqual(h.snapshot().charts.length, 0, 'failed analytics must retain no active charts');
assert.strictEqual(h.snapshot().title, 'A');
assert.match(h.snapshot().html, /Test radio/);
assert.match(h.snapshot().html, /2 seconds/);
assert.strictEqual(h.elements.get('obsAnalytics').firstChild.textContent, 'Error: <chart construction failure>');
assert.doesNotMatch(h.snapshot().html, /<chart construction failure>/, 'construction error uses textContent');
complete(h.days(1), 'A', 'retry');
await settled();
assert.strictEqual(h.snapshot().charts.length, 4, 'a later load can construct its own charts');
assert.ok(h.charts.slice(0, failedChart - 1).every(chart => chart.destroyed), 'retry cannot revive failed charts');
assert.strictEqual(h.requests.length, 6, 'only the requested retry fetches again');
});
}
await test('identity failure stays visible when secondary responses settle later', async () => {
const h = harness();
const pending = h.visit('A');