diff --git a/internal/web/static/js/openfsd/airport-editor/history.js b/internal/web/static/js/openfsd/airport-editor/history.js index 609a4d5..32f73f5 100644 --- a/internal/web/static/js/openfsd/airport-editor/history.js +++ b/internal/web/static/js/openfsd/airport-editor/history.js @@ -54,10 +54,9 @@ function deepClone(value) { * @returns {HistoryStack} */ export function createHistory(opts = {}) { + const n = Math.floor(Number(opts.maxDepth)); const maxDepth = - typeof opts.maxDepth === 'number' && opts.maxDepth > 0 - ? Math.floor(opts.maxDepth) - : DEFAULT_HISTORY_MAX_DEPTH; + Number.isFinite(n) && n > 0 ? n : DEFAULT_HISTORY_MAX_DEPTH; return { undoStack: [], redoStack: [], diff --git a/webjs/airport-editor/history.test.js b/webjs/airport-editor/history.test.js index 86fedb5..1a80311 100644 --- a/webjs/airport-editor/history.test.js +++ b/webjs/airport-editor/history.test.js @@ -86,8 +86,15 @@ test('createHistory defaults and custom maxDepth', () => { const h2 = createHistory({ maxDepth: 3 }); assert.equal(h2.maxDepth, 3); - const h3 = createHistory({ maxDepth: 0 }); - assert.equal(h3.maxDepth, DEFAULT_HISTORY_MAX_DEPTH); + // Invalid / non-positive / non-finite → default + assert.equal(createHistory({ maxDepth: 0 }).maxDepth, DEFAULT_HISTORY_MAX_DEPTH); + assert.equal(createHistory({ maxDepth: 0.5 }).maxDepth, DEFAULT_HISTORY_MAX_DEPTH); + assert.equal(createHistory({ maxDepth: -2 }).maxDepth, DEFAULT_HISTORY_MAX_DEPTH); + assert.equal(createHistory({ maxDepth: Infinity }).maxDepth, DEFAULT_HISTORY_MAX_DEPTH); + assert.equal(createHistory({ maxDepth: NaN }).maxDepth, DEFAULT_HISTORY_MAX_DEPTH); + assert.equal(createHistory({ maxDepth: 'nope' }).maxDepth, DEFAULT_HISTORY_MAX_DEPTH); + // Floor then accept: 2.9 → 2 + assert.equal(createHistory({ maxDepth: 2.9 }).maxDepth, 2); }); // --------------------------------------------------------------------------- @@ -269,6 +276,30 @@ test('maxDepth drops oldest on overflow', () => { assert.equal(h.undoStack[2]._nextSurfaceId, 5); }); +test('maxDepth trims undo on redo path when stack would exceed max', () => { + // Normal undo/redo from a max-sized stack never exceeds max (undo shrinks + // undo as it grows redo). Cover the defensive trim in redo by pre-filling + // undo to maxDepth with a pending redo entry so redo would push past max. + const h = createHistory({ maxDepth: 2 }); + const doc = createEmptyDocument(); + + doc._nextSurfaceId = 1; + h.undoStack.push(captureSnapshot(doc)); + doc._nextSurfaceId = 2; + h.undoStack.push(captureSnapshot(doc)); + doc._nextSurfaceId = 99; + h.redoStack.push(captureSnapshot(doc)); + doc._nextSurfaceId = 3; // live present; redo captures this onto undo + + const r = redo(h, doc); + assert.equal(r.ok, true); + assert.equal(doc._nextSurfaceId, 99); + assert.equal(h.undoStack.length, 2); // not 3 — oldest dropped + assert.equal(h.undoStack[0]._nextSurfaceId, 2); + assert.equal(h.undoStack[1]._nextSurfaceId, 3); + assert.equal(h.redoStack.length, 0); +}); + // --------------------------------------------------------------------------- // gestures // ---------------------------------------------------------------------------