mirror of
https://github.com/SteamFramelyHomebrew/framely.git
synced 2026-10-18 21:01:35 +02:00
50 lines
6.0 KiB
JavaScript
50 lines
6.0 KiB
JavaScript
import test from 'node:test';import assert from 'node:assert/strict';import {build} from 'esbuild';
|
|
const bundle=async(path)=>{const r=await build({entryPoints:[path],bundle:true,write:false,platform:'node',format:'esm'});return import('data:text/javascript;base64,'+Buffer.from(r.outputFiles[0].contents).toString('base64'));};
|
|
const {FileUploadQueue,ChunkSlots,uploadBatchFileCounts}=await bundle('ui/src/file-upload-queue.ts');
|
|
const {droppedUploadEntries,selectedUploadEntries}=await bundle('ui/src/file-upload-entries.ts');
|
|
const wait=()=>new Promise(r=>setImmediate(r));const until=async(f)=>{for(let i=0;i<200;i++){if(f())return;await wait();}assert.fail('Timed out');};
|
|
const file=(name,size=8)=>({name,webkitRelativePath:'',size,slice:(a,b)=>new Blob([new Uint8Array(b-a)])});
|
|
test('directory readers are fully drained and empty folders retained',async()=>{
|
|
const regular={name:'readme',isFile:true,file:done=>done(file('readme'))};let reads=0;
|
|
const root={name:'folder',isDirectory:true,createReader:()=>({readEntries:done=>done(reads++===0?[regular]:reads===2?[{name:'empty',isDirectory:true,createReader:()=>({readEntries:done=>done([])})}]:[])})};
|
|
const entries=await droppedUploadEntries({items:[{kind:'file',webkitGetAsEntry:()=>root}],files:[]},new AbortController().signal);
|
|
assert.deepEqual(entries.map(e=>e.name),['folder','folder/readme','folder/empty']);assert.equal(reads,3);
|
|
const chosen=file('x');chosen.webkitRelativePath='中文/a/x';assert.deepEqual((await selectedUploadEntries([chosen])).map(e=>e.name),['中文','中文/a','中文/a/x']);
|
|
await assert.rejects(selectedUploadEntries([{...chosen,webkitRelativePath:'../bad'}]),/Invalid upload path/);
|
|
});
|
|
test('directory dependencies, fixed destinations, skips, failures and retries are independent',async()=>{
|
|
let fail=true;const calls=[],chunks=[];
|
|
const q=new FileUploadQueue({api:async p=>{calls.push(p);if(p.operation==='upload.directory')return {status:'done'};if(p.operation==='upload.start'){if(p.name==='skip')return {status:'skipped',path:'/first/skip'};if(p.name==='bad'&&fail)throw Error('permission');return {id:p.name,chunkSize:4};}return {status:'done',path:'/first/'+p.id};},chunk:async(id,offset)=>chunks.push([id,offset])});
|
|
q.add('/first',{dev:1,ino:2},'skip',[{name:'root',directory:true},{name:'root/file',directory:false,file:file('file')},{name:'skip',directory:false,file:file('skip')},{name:'bad',directory:false,file:file('bad')}]);
|
|
await until(()=>!q.pending);assert.deepEqual(q.batches[0].tasks.map(t=>t.state),['done','done','skipped','failed']);assert.equal(chunks.filter(c=>c[0]==='skip').length,0);
|
|
assert.ok(calls.findIndex(c=>c.operation==='upload.directory')<calls.findIndex(c=>c.name==='root/file'));assert.ok(calls.filter(c=>c.operation==='upload.start').every(c=>c.directory==='/first'));
|
|
fail=false;q.retry();await until(()=>!q.pending);assert.equal(q.batches[0].tasks[3].state,'done');q.clear();assert.equal(q.batches.length,0);
|
|
});
|
|
test('file concurrency changes without aborting active tasks and global chunks never exceed eight',async()=>{
|
|
let active=0,max=0;const pending=[];
|
|
const q=new FileUploadQueue({api:async p=>p.operation==='upload.start'?{id:p.name,chunkSize:4}:{status:'done'},chunk:async(id,o,b,signal)=>{active++;max=Math.max(max,active);try{await new Promise((resolve,reject)=>{pending.push(resolve);signal.addEventListener('abort',()=>reject(signal.reason),{once:true});});}finally{active--;}}});
|
|
q.setLimit(8);q.add('/target',{dev:1,ino:2},'keep',Array.from({length:12},(_,i)=>({name:String(i),directory:false,file:file(String(i))})));
|
|
await until(()=>active===8);assert.equal(q.batches[0].tasks.filter(t=>t.state==='uploading').length,8);q.setLimit(1);
|
|
for(let round=0;round<50&&q.pending;round++){pending.splice(0).forEach(f=>f());await wait();}
|
|
await until(()=>!q.pending);assert.equal(max,8);assert.ok(q.batches[0].tasks.every(t=>t.state==='done'));
|
|
});
|
|
test('cancel drains requests and releases chunk slots, allowing another batch',async()=>{
|
|
const q=new FileUploadQueue({api:async p=>p.operation==='upload.start'?{id:p.name,chunkSize:4}:{status:'done'},chunk:async(id,o,b,signal)=>{if(id==='first')await new Promise((resolve,reject)=>signal.addEventListener('abort',()=>reject(signal.reason),{once:true}));}});
|
|
q.setLimit(1);const first=q.add('/one',{dev:1,ino:1},'keep',[{name:'first',directory:false,file:file('first')}]);await wait();q.cancel(undefined,first.id);
|
|
q.add('/two',{dev:1,ino:2},'keep',[{name:'second',directory:false,file:file('second')}]);await until(()=>!q.pending);
|
|
assert.equal(q.batches[0].tasks[0].state,'cancelled');assert.equal(q.batches[1].tasks[0].state,'done');
|
|
const slots=new ChunkSlots(1),hold=await slots.acquire(new AbortController().signal),cancel=new AbortController();const blocked=slots.acquire(cancel.signal);cancel.abort();await assert.rejects(blocked);hold();(await slots.acquire(new AbortController().signal))();
|
|
});
|
|
test('a restored browser page can add a fresh batch after closing its old queue',async()=>{
|
|
const q=new FileUploadQueue({api:async p=>p.operation==='upload.start'?{id:p.name,chunkSize:4}:{status:'done'},chunk:async()=>{}});
|
|
q.close();q.reopen();q.add('/restored',{dev:1,ino:2},'keep',[{name:'new',directory:false,file:file('new',0)}]);
|
|
await until(()=>!q.pending);assert.equal(q.batches[0].tasks[0].state,'done');
|
|
});
|
|
|
|
test('batch file counts exclude directories, skips and failures and survive clearing finished rows',()=>{
|
|
const q=new FileUploadQueue({api:async()=>({}),chunk:async()=>{}});q.close();q.reopen();
|
|
const batch=q.add('/target',{dev:1,ino:2},'keep',[{name:'empty',directory:true,error:'fixture'},{name:'done',directory:false,file:file('done'),error:'fixture'},{name:'skip',directory:false,file:file('skip'),error:'fixture'},{name:'pending',directory:false,file:file('pending'),error:'fixture'}]);
|
|
batch.tasks[0].state='done';batch.tasks[1].state='done';batch.tasks[2].state='skipped';batch.tasks[3].state='waiting';
|
|
assert.deepEqual(uploadBatchFileCounts(batch),{uploaded:1,total:3});q.clear();assert.deepEqual(uploadBatchFileCounts(batch),{uploaded:1,total:3});q.close();
|
|
});
|