diff --git a/js/app-library.js b/js/app-library.js index bb1d111..2929a56 100644 --- a/js/app-library.js +++ b/js/app-library.js @@ -225,16 +225,29 @@ class AppLibrary { } } - async _loadTemplate(t) { - if (!await this._confirmGuard(`Load "${t.name}"? Your current diagram will be replaced (Ctrl+Z to undo).`, 'Load template')) return; + // Swap the current diagram for a starter template. Shared with _loadDemo so + // the two cannot drift; each caller adds its own last step. + _installTemplate(t) { const prev = this._snapshot(); this._clearAll(); t.load(); this.diagram.meta.name = t.name; this.diagram.meta.description = t.desc; this._applyMeta(); + // Reset AFTER the template has built its nodes, not before. _clearAll resets + // too, but that runs against an empty diagram, so the step-0 baseline it + // records holds nothing. Leaving it that way meant the first point of every + // chart was a hole: run a freshly loaded template and the series began at + // step 1, so the starting amounts never plotted and spike attribution had no + // "from" value for the first step. + this.engine.reset(); this._commitReplace(prev); this.renderer.fitView(); + } + + async _loadTemplate(t) { + if (!await this._confirmGuard(`Load "${t.name}"? Your current diagram will be replaced (Ctrl+Z to undo).`, 'Load template')) return; + this._installTemplate(t); this._hideModal('lib-overlay'); } diff --git a/js/app.js b/js/app.js index 3500e7a..aa9feee 100644 --- a/js/app.js +++ b/js/app.js @@ -850,14 +850,7 @@ class App { _loadDemo() { const t = this._templates[0]; // Predator & Prey if (!t) return; - const prev = this._snapshot(); - this._clearAll(); - t.load(); - this.diagram.meta.name = t.name; - this.diagram.meta.description = t.desc; - this._applyMeta(); - this._commitReplace(prev); - this.renderer.fitView(); + this._installTemplate(t); this._renderProps(); } diff --git a/test/smoke.js b/test/smoke.js index c061694..77e0f93 100644 --- a/test/smoke.js +++ b/test/smoke.js @@ -175,6 +175,37 @@ const URL = process.env.SMOKE_URL || 'http://localhost:8080/'; if (n > 0) ok(`template "${name}" loaded (${n} nodes)`); else fail(`template "${name}" empty`); } + // Loading a template has to reset the engine AFTER the template builds its + // nodes. _clearAll resets too, but against an empty diagram, so the step-0 + // baseline it writes is empty and every chart series opens on a hole: the + // starting amounts never plot and the curve begins at step 1. Both entry + // points (Library template, welcome demo) go through _installTemplate. + const step0 = await page.evaluate(async () => { + const out = {}; + // Drive the real entry points, not the shared helper, so this test measures + // behaviour rather than the presence of a particular function. + for (const [label, load] of [['template', () => window.app._loadTemplate(window.app._templates[0])], + ['demo', () => window.app._loadDemo()]]) { + await load(); + const e = window.app.engine, d = window.app.diagram; + const h0 = e.history[0]; + const snap = h0 ? h0.snap : null; + const ids = snap ? Object.keys(snap) : []; + // Every node the template built must appear in the step-0 baseline. + const covered = d.nodes.size > 0 && [...d.nodes.keys()].every(id => snap && snap[id] !== undefined); + // And stepping must leave no hole at index 0 of a series. + const probe = [...d.nodes.values()][0]; + for (let i = 0; i < 3; i++) e.doStep(); + const series = e.history.map(h => (h.snap && h.snap[probe.id] !== undefined) ? h.snap[probe.id] : null); + out[label] = { nodes: d.nodes.size, step0Entries: ids.length, covered, firstIsNull: series[0] === null, len: series.length }; + } + return out; + }); + if (step0.template.covered && !step0.template.firstIsNull + && step0.demo.covered && !step0.demo.firstIsNull) + ok('template/demo load records a step-0 baseline, so charts start at step 0 instead of a hole'); + else fail('template step-0 baseline: ' + JSON.stringify(step0)); + // Navigation: zoom controls step the scale and update the readout; fit-to-content // re-frames without error. const nav = await page.evaluate(() => {