125 lines
3.6 KiB
JavaScript
125 lines
3.6 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
|
|
import {HistoryStack} from '../js/canvas/canvas_history.js';
|
|
import {ImageReferenceManager} from '../js/persistence/image_reference_manager.js';
|
|
|
|
function createReferenceCanvas(layers = []) {
|
|
return {
|
|
layers,
|
|
canvasState: {
|
|
layersUndoStack: [],
|
|
layersRedoStack: [],
|
|
},
|
|
imageCache: {
|
|
has: () => false,
|
|
delete: () => false,
|
|
},
|
|
};
|
|
}
|
|
|
|
test('history stack remains bounded during a large edit sequence', () => {
|
|
const history = new HistoryStack({
|
|
clone: value => ({ ...value }),
|
|
historyLimit: 64,
|
|
});
|
|
|
|
for (let index = 0; index < 5000; index += 1) {
|
|
assert.equal(history.push({ index }), true);
|
|
}
|
|
|
|
assert.equal(history.undoStack.length, 64);
|
|
assert.equal(history.undoStack[0].index, 4936);
|
|
assert.equal(history.undoStack.at(-1).index, 4999);
|
|
assert.equal(history.redoStack.length, 0);
|
|
});
|
|
|
|
test('reference manager rebuilds unique ids across current and historical layers', () => {
|
|
const currentLayers = Array.from({ length: 2000 }, (_, index) => ({
|
|
imageId: `current-${index % 100}`,
|
|
}));
|
|
const canvas = createReferenceCanvas(currentLayers);
|
|
canvas.canvasState.layersUndoStack = [[{ imageId: 'undo-only' }]];
|
|
canvas.canvasState.layersRedoStack = [[{ imageId: 'redo-only' }]];
|
|
|
|
const manager = new ImageReferenceManager(canvas);
|
|
const usedImageIds = manager.collectAllUsedImageIds();
|
|
|
|
assert.equal(usedImageIds.size, 102);
|
|
assert.equal(usedImageIds.has('undo-only'), true);
|
|
assert.equal(usedImageIds.has('redo-only'), true);
|
|
});
|
|
|
|
test('reference manager cleanup stops its timer and releases tracked state', () => {
|
|
const originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
|
|
const originalClearInterval = Object.getOwnPropertyDescriptor(globalThis, 'clearInterval');
|
|
const timers = [];
|
|
const clearedTimers = [];
|
|
|
|
Object.defineProperty(globalThis, 'window', {
|
|
configurable: true,
|
|
value: {
|
|
setInterval(callback, delay) {
|
|
const timer = { callback, delay };
|
|
timers.push(timer);
|
|
return timer;
|
|
},
|
|
clearInterval(timer) {
|
|
clearedTimers.push(timer);
|
|
},
|
|
},
|
|
});
|
|
Object.defineProperty(globalThis, 'clearInterval', {
|
|
configurable: true,
|
|
value: timer => clearedTimers.push(timer),
|
|
});
|
|
|
|
try {
|
|
const manager = new ImageReferenceManager(createReferenceCanvas());
|
|
manager.addReference('image-1');
|
|
manager.addReference('image-1');
|
|
manager.startGarbageCollection();
|
|
|
|
assert.equal(timers.length, 1);
|
|
assert.equal(manager.getStats().totalReferences, 2);
|
|
|
|
manager.destroy();
|
|
|
|
assert.deepEqual(clearedTimers, [timers[0]]);
|
|
assert.deepEqual(manager.getStats(), {
|
|
trackedImages: 0,
|
|
totalReferences: 0,
|
|
isRunning: false,
|
|
gcInterval: 5 * 60 * 1000,
|
|
maxAge: 30 * 60 * 1000,
|
|
});
|
|
} finally {
|
|
if (originalWindow) {
|
|
Object.defineProperty(globalThis, 'window', originalWindow);
|
|
} else {
|
|
delete globalThis.window;
|
|
}
|
|
if (originalClearInterval) {
|
|
Object.defineProperty(globalThis, 'clearInterval', originalClearInterval);
|
|
} else {
|
|
delete globalThis.clearInterval;
|
|
}
|
|
}
|
|
});
|
|
|
|
test('garbage collection ignores reentrant calls while one run is active', async () => {
|
|
const manager = new ImageReferenceManager(createReferenceCanvas());
|
|
let nestedRunCompleted = false;
|
|
|
|
manager.findUnusedImages = async () => {
|
|
await manager.performGarbageCollection();
|
|
nestedRunCompleted = true;
|
|
return [];
|
|
};
|
|
|
|
await manager.performGarbageCollection();
|
|
|
|
assert.equal(nestedRunCompleted, true);
|
|
assert.equal(manager.getStats().isRunning, false);
|
|
});
|