128 lines
3.8 KiB
JavaScript
128 lines
3.8 KiB
JavaScript
import assert from 'node:assert/strict';
|
|
import { readFile } from 'node:fs/promises';
|
|
import test from 'node:test';
|
|
|
|
import { CanvasState } from '../js/canvas/canvas_state.js';
|
|
|
|
const [canvasStateSource, maskEditorSource] = await Promise.all([
|
|
readFile(new URL('../src/canvas/canvas_state.ts', import.meta.url), 'utf8'),
|
|
readFile(new URL('../src/mask/mask_editor_integration.ts', import.meta.url), 'utf8'),
|
|
]);
|
|
|
|
function installImageStub() {
|
|
const originalImage = Object.getOwnPropertyDescriptor(globalThis, 'Image');
|
|
|
|
Object.defineProperty(globalThis, 'Image', {
|
|
configurable: true,
|
|
value: class TestImage {
|
|
constructor() {
|
|
this.onload = null;
|
|
this.onerror = null;
|
|
this.crossOrigin = '';
|
|
this.width = 4;
|
|
this.height = 3;
|
|
}
|
|
|
|
set src(value) {
|
|
this.source = value;
|
|
queueMicrotask(() => {
|
|
if (value === 'bad-image') {
|
|
this.onerror?.(new Error('image decode failed'));
|
|
} else {
|
|
this.onload?.();
|
|
}
|
|
});
|
|
}
|
|
},
|
|
});
|
|
|
|
return () => {
|
|
if (originalImage) {
|
|
Object.defineProperty(globalThis, 'Image', originalImage);
|
|
} else {
|
|
delete globalThis.Image;
|
|
}
|
|
};
|
|
}
|
|
|
|
function installCanvasStub() {
|
|
const originalDocument = Object.getOwnPropertyDescriptor(globalThis, 'document');
|
|
|
|
Object.defineProperty(globalThis, 'document', {
|
|
configurable: true,
|
|
value: {
|
|
createElement(tagName) {
|
|
assert.equal(tagName, 'canvas');
|
|
return {
|
|
getContext() {
|
|
return { drawImage() {} };
|
|
},
|
|
toDataURL() {
|
|
return 'bitmap-data';
|
|
},
|
|
};
|
|
},
|
|
},
|
|
});
|
|
|
|
return () => {
|
|
if (originalDocument) {
|
|
Object.defineProperty(globalThis, 'document', originalDocument);
|
|
} else {
|
|
delete globalThis.document;
|
|
}
|
|
};
|
|
}
|
|
|
|
test('CanvasState preserves image loading, cross-origin, and failure behavior', async () => {
|
|
const restore = installImageStub();
|
|
const state = Object.create(CanvasState.prototype);
|
|
|
|
try {
|
|
const loadedLayer = await new Promise(resolve => {
|
|
state._createLayerFromSrc({ id: 'layer-1' }, 'good-image', 2, resolve);
|
|
});
|
|
assert.equal(loadedLayer.image.source, 'good-image');
|
|
assert.equal(loadedLayer.image.crossOrigin, 'anonymous');
|
|
|
|
const failedLayer = await new Promise(resolve => {
|
|
state._createLayerFromSrc({ id: 'layer-2' }, 'bad-image', 3, resolve);
|
|
});
|
|
assert.equal(failedLayer, null);
|
|
} finally {
|
|
restore();
|
|
}
|
|
});
|
|
|
|
test('CanvasState preserves ImageBitmap conversion before shared image loading', async () => {
|
|
const restoreImage = installImageStub();
|
|
const restoreCanvas = installCanvasStub();
|
|
const state = Object.create(CanvasState.prototype);
|
|
|
|
try {
|
|
const loadedLayer = await new Promise(resolve => {
|
|
state._createLayerFromSrc({ id: 'layer-bitmap' }, { width: 8, height: 6 }, 4, resolve);
|
|
});
|
|
assert.equal(loadedLayer.image.source, 'bitmap-data');
|
|
assert.equal(loadedLayer.image.crossOrigin, 'anonymous');
|
|
} finally {
|
|
restoreCanvas();
|
|
restoreImage();
|
|
}
|
|
});
|
|
|
|
test('CanvasState layer source loading has no local Image construction', () => {
|
|
const methodStart = canvasStateSource.indexOf('_createLayerFromSrc(');
|
|
const methodEnd = canvasStateSource.indexOf('\n async saveStateToDB', methodStart);
|
|
const methodBody = canvasStateSource.slice(methodStart, methodEnd);
|
|
|
|
assert.equal((methodBody.match(/loadImage\(/g) ?? []).length, 2);
|
|
assert.doesNotMatch(methodBody, /new Image\(/);
|
|
});
|
|
|
|
test('Mask editor image loaders preserve their rejection and cleanup policies', () => {
|
|
assert.match(maskEditorSource, /return loadImage\(this\.maskTool\.getMask\(\)\.toDataURL\(\)\)/);
|
|
assert.match(maskEditorSource, /resultImage = await loadImage\(this\.node\.imgs\[0\]\.src\)/);
|
|
assert.match(maskEditorSource, /this\.node\.imgs = \[\]/);
|
|
});
|