Files
behind_glass/scripts/test-photo-working-image.mjs
T
horoli e358e24436 Replace project with current server release and Windows setup
Import the 2026-10-09 source release, Fastify/SQLite backend, restored source
assets, and analytics-enabled server build. Replace files from the previous
release while retaining the existing repository history.

Make builds portable across Windows and Linux, configure local startup,
normalize Windows static asset paths, and fix test fixture cleanup and file
URL handling. Exclude local environment settings, databases, dependencies,
validation output, and TypeScript build cache from version control.

Validation: npm ci, static and server builds, 106 arrival tests, and actual
local HTTP checks passed. The full suite passed 467 of 470 tests; three tests
still require QA fixtures omitted from the supplied release, documented in
README-서버실행.md.
2026-10-09 16:43:35 +09:00

25 lines
10 KiB
JavaScript

import assert from 'node:assert/strict';
import {test} from 'node:test';
import {loadTypescript} from './support/load-typescript.mjs';
const load=()=>loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));
function png(width,height){const b=new Uint8Array(45),v=new DataView(b.buffer);b.set([137,80,78,71,13,10,26,10]);v.setUint32(8,13);b.set(new TextEncoder().encode('IHDR'),12);v.setUint32(16,width);v.setUint32(20,height);b.set(new TextEncoder().encode('IEND'),37);return new Blob([b],{type:'image/png'});}
const withBitmap=async(fn,body)=>{const old=globalThis.createImageBitmap;globalThis.createImageBitmap=fn;try{await body();}finally{globalThis.createImageBitmap=old;}};
test('ordinary PNG requests bounded first decode, keeps aspect and no upscale',async()=>{await withBitmap(async(_f,o)=>({width:o.resizeWidth??8000,height:o.resizeHeight??6000,close(){}}),async()=>{const{createPhotoSnapshot}=await load();const s=await createPhotoSnapshot(png(8000,6000),1,new AbortController().signal);assert.deepEqual([s.width,s.height],[1024,768]);const small=await createPhotoSnapshot(png(320,240),2,new AbortController().signal);assert.deepEqual([small.width,small.height],[320,240]);});});
test('oversize dimensions are rejected before native decode',async()=>{let calls=0;await withBitmap(async()=>{calls++;return{width:1024,height:1024,close(){}};},async()=>{const{createPhotoSnapshot}=await load();await assert.rejects(createPhotoSnapshot(png(100000,100000),1,new AbortController().signal),/크|픽셀|64/);assert.equal(calls,0);});});
test('cancelled decode closes its result and cannot become a working photo',async()=>{let release,closed=0;await withBitmap(()=>new Promise(r=>{release=r;}),async()=>{const{createPhotoSnapshot}=await load(),abort=new AbortController(),pending=createPhotoSnapshot(png(4000,3000),1,abort.signal);while(!release)await new Promise(r=>setImmediate(r));abort.abort();release({width:1024,height:768,close(){closed++;}});await assert.rejects(pending,{name:'AbortError'});assert.equal(closed,1);});});
test('late resize cancellation releases both native bitmaps',async()=>{let release,calls=0,closed=0;await withBitmap(async()=>{if(++calls===1)return{width:4000,height:3000,close(){closed++;}};return new Promise(r=>{release=r;});},async()=>{const{createPhotoSnapshot}=await load(),abort=new AbortController(),pending=createPhotoSnapshot(png(4000,3000),1,abort.signal);while(!release)await new Promise(r=>setImmediate(r));abort.abort();release({width:1024,height:768,close(){closed++;}});await assert.rejects(pending,{name:'AbortError'});assert.equal(closed,2);});});
function imageHarness({abortOnBlob=false,decodeError=false}={}){const originals={Image:globalThis.Image,document:globalThis.document,createImageBitmap:globalThis.createImageBitmap,create:URL.createObjectURL,revoke:URL.revokeObjectURL};const abort=new AbortController(),urls=[],revoked=[],canvases=[],images=[];let closes=0;globalThis.createImageBitmap=async(source,options)=>({width:options?.resizeWidth??source.width,height:options?.resizeHeight??source.height,close(){closes++;}});globalThis.Image=class{constructor(){images.push(this);}set src(value){this._src=value;if(value)queueMicrotask(()=>decodeError?this.onerror?.():this.onload?.());}get src(){return this._src;}removeAttribute(){this._src='';}};globalThis.document={createElement(){const c={width:0,height:0,draws:[],getContext(){return{drawImage:(...args)=>c.draws.push(args)};},toBlob(callback,type){c.mime=type;if(abortOnBlob)abort.abort();callback(new Blob(['working-png'],{type}));}};canvases.push(c);return c;}};URL.createObjectURL=blob=>{urls.push(blob);return'blob:bounded'+urls.length;};URL.revokeObjectURL=url=>revoked.push(url);return{abort,urls,revoked,canvases,images,get closes(){return closes;},restore(){globalThis.Image=originals.Image;globalThis.document=originals.document;globalThis.createImageBitmap=originals.createImageBitmap;URL.createObjectURL=originals.create;URL.revokeObjectURL=originals.revoke;}};}
test('lossless DOM adapter encodes only normalized RGB, retains alpha format and releases on dispose',async()=>{const h=imageHarness();try{const {createWorkingPhotoImage}=await loadTypescript(new URL('../src/input/WorkingPhotoImage.ts',import.meta.url));const p=await createWorkingPhotoImage(png(8000,6000),1,h.abort.signal);assert.equal(h.canvases[0].mime,'image/png');assert.deepEqual([h.canvases[0].draws[0][0].width,h.canvases[0].draws[0][0].height],[1024,768]);assert.equal(h.urls[0].type,'image/png');assert.equal(h.closes,1);assert.equal(h.canvases[0].width,1);assert.equal(h.revoked.length,0);p.dispose();p.dispose();assert.equal(h.revoked.length,1);assert.equal(p.image.src,'');}finally{h.restore();}});
test('cancel during PNG encode releases working bitmap/canvas without creating a URL',async()=>{const h=imageHarness({abortOnBlob:true});try{const {createWorkingPhotoImage}=await loadTypescript(new URL('../src/input/WorkingPhotoImage.ts',import.meta.url));await assert.rejects(createWorkingPhotoImage(png(4000,3000),1,h.abort.signal),{name:'AbortError'});assert.equal(h.closes,1);assert.equal(h.canvases[0].width,1);assert.equal(h.urls.length,0);}finally{h.restore();}});
test('failed preview decode revokes its URL and bitmap, never returns half-ready RGB',async()=>{const h=imageHarness({decodeError:true});try{const {createWorkingPhotoImage}=await loadTypescript(new URL('../src/input/WorkingPhotoImage.ts',import.meta.url));await assert.rejects(createWorkingPhotoImage(png(4000,3000),1,h.abort.signal),/작업용 사진/);assert.equal(h.closes,1);assert.equal(h.revoked.length,1);}finally{h.restore();}});
test('browser Blob-resize rejection falls back to a bounded alpha canvas and revokes source URL',async()=>{const h=imageHarness();let calls=0;globalThis.createImageBitmap=async(source,options)=>{calls++;if(source instanceof Blob)throw new Error('resize unsupported');assert.deepEqual([source.width,source.height],[1024,768]);return{width:source.width,height:source.height,close(){}};};globalThis.Image=class{naturalWidth=8000;naturalHeight=6000;set src(_value){queueMicrotask(()=>this.onload?.());}removeAttribute(){}};try{const {createPhotoSnapshot}=await load();const result=await createPhotoSnapshot(png(8000,6000),1,h.abort.signal);assert.deepEqual([result.width,result.height],[1024,768]);assert.equal(calls,2);assert.equal(h.revoked.length,1);assert.equal(h.canvases[0].width,1);}finally{h.restore();}});
test('unknown-format decoded dimensions still get the source guard and release',async()=>{let closed=0;await withBitmap(async()=>({width:100000,height:100000,close(){closed++;}}),async()=>{const{createPhotoSnapshot}=await load();await assert.rejects(createPhotoSnapshot(new Blob(['unknown-format']),1,new AbortController().signal),/사진이 너무 커요/);assert.equal(closed,1);});});
test('unsupported browser codec produces useful error and releases fallback object URL',async()=>{const h=imageHarness({decodeError:true});globalThis.createImageBitmap=async()=>{throw new Error('unsupported');};try{const{createPhotoSnapshot}=await load();await assert.rejects(createPhotoSnapshot(new Blob(['fake-heic'],{type:'image/heic'}),1,h.abort.signal),/HEIC.*JPEG.*PNG/);assert.equal(h.revoked.length,1);}finally{h.restore();}});
test('abort during fallback image decode revokes source URL without waiting for onload',async()=>{const h=imageHarness();let image;globalThis.createImageBitmap=async()=>{throw new Error('resize unsupported');};globalThis.Image=class{constructor(){image=this;}set src(v){this._src=v;}removeAttribute(){this._src='';}};try{const{createPhotoSnapshot}=await load(),pending=createPhotoSnapshot(png(4000,3000),1,h.abort.signal);while(!image)await new Promise(r=>setImmediate(r));h.abort.abort();await assert.rejects(pending,{name:'AbortError'});assert.equal(h.revoked.length,1);assert.equal(image._src,'');assert.equal(image.onload,null);}finally{h.restore();}});
test('browser silently ignoring bitmap resize options uses bounded canvas fallback',async()=>{const h=imageHarness();let closes=0,calls=0;globalThis.createImageBitmap=async(source)=>{calls++;const canvas=!(source instanceof Blob)&&source.getContext;return{width:canvas?source.width:4000,height:canvas?source.height:3000,close(){closes++;}};};try{const{createPhotoSnapshot}=await load();const result=await createPhotoSnapshot(png(4000,3000),1,h.abort.signal);assert.deepEqual([result.width,result.height],[1024,768]);assert.equal(calls,3);assert.equal(closes,2);result.bitmap.close();assert.equal(closes,3);}finally{h.restore();}});
// Retained original-photo coverage previously colocated with the retired cutout provider.
const tick=async()=>{for(let i=0;i<8;i++)await Promise.resolve();};
test('photo snapshot applies EXIF once then constrains canonical longest edge',async()=>{const original=globalThis.createImageBitmap;const calls=[];let closed=0;globalThis.createImageBitmap=async(source,options)=>{calls.push(options);return calls.length===1?{width:1600,height:800,close(){closed++;}}:{width:1024,height:512,close(){closed++;}};};try{const {createPhotoSnapshot}=await loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));const result=await createPhotoSnapshot({},2,new AbortController().signal);assert.equal(result.width,1024);assert.equal(result.height,512);assert.equal(calls[0].imageOrientation,'from-image');assert.equal(calls[1].resizeWidth,1024);assert.equal(closed,1);}finally{globalThis.createImageBitmap=original;}});
test('snapshot abort during resize closes decoded and resized bitmaps exactly once',async()=>{const old=globalThis.createImageBitmap;let resolveResize,decodedClosed=0,resizedClosed=0;let count=0;globalThis.createImageBitmap=async()=>{if(++count===1)return{width:1600,height:800,close(){decodedClosed++;}};return new Promise(resolve=>{resolveResize=resolve;});};try{const {createPhotoSnapshot}=await loadTypescript(new URL('../src/input/PhotoSnapshot.ts',import.meta.url));const abort=new AbortController(),task=createPhotoSnapshot({},2,abort.signal),rejected=assert.rejects(task,{name:'AbortError'});await tick();abort.abort();resolveResize({width:1024,height:512,close(){resizedClosed++;}});await rejected;assert.equal(decodedClosed,1);assert.equal(resizedClosed,1);}finally{globalThis.createImageBitmap=old;}});