Files
ucvl-home-vision/test_journal.js
T

61 lines
5.0 KiB
JavaScript
Raw Normal View History

const test=require('node:test'),assert=require('node:assert/strict'),crypto=require('node:crypto'),fs=require('node:fs');
const hash=require('./web/file-hash.js');
test('upload MD5 matches RFC vectors and a multi-block file',()=>{
for(const text of ['', 'a', 'abc', 'message digest', 'abcdefghijklmnopqrstuvwxyz', '照片与语音', 'x'.repeat(4194304)]){
const bytes=Buffer.from(text);assert.equal(hash.md5(bytes),crypto.createHash('md5').update(bytes).digest('hex'));
}
});
test('browser hashes in 4 MiB slices rather than loading the entire video',async()=>{
const file=new Blob([Buffer.alloc(4194304,1),Buffer.from('tail')]);const values=await hash.blocks(file);
assert.equal(values.length,2);assert.equal(values[1],crypto.createHash('md5').update('tail').digest('hex'));
await assert.rejects(hash.blocks(file,()=>{},()=>false),/取消/);
});
test('journals default to private and encode content before rendering',()=>{
const html=fs.readFileSync('web/index.html','utf8'),js=fs.readFileSync('web/journal.js','utf8');
assert.match(html,/<select name="visibility"><option value="private">/);assert.match(js,/esc\(e.text\)/);assert.match(js,/esc\(c.text\)/);
assert.doesNotMatch(js,/localStorage|indexedDB|caches\.open|access_token|secretKey.*localStorage/);
});
test('leave clears media URLs, live microphone tracks, and pending requests',()=>{
const js=fs.readFileSync('web/journal.js','utf8');assert.match(js,/journalRequest\?\.abort\(\)/);assert.match(js,/releaseJournalMedia/);assert.match(js,/removeAttribute\('src'\)/);assert.match(js,/getTracks\(\).forEach\(t=>t.stop\(\)\)/);
});
test('photos load by default and audio/video preload metadata with controls, never autoplay',()=>{
const vm=require('node:vm'),js=fs.readFileSync('web/journal.js','utf8');
const code=js.slice(js.indexOf('function journalMediaView('),js.indexOf('function bindJournalMedia('));
const c=vm.createContext({esc:s=>String(s).replaceAll('<','&lt;'),journalMediaUrl:id=>'/media/journal?id='+id});vm.runInContext(code,c);
for(const mime of ['image/png','audio/webm','video/mp4']){
const html=c.journalMediaView({id:'a',name:'<test>',mime});assert.match(html,/src="\/media\/journal\?id=a"/);assert.doesNotMatch(html,/autoplay|<test>/);
if(mime.startsWith('image'))assert.match(html,/loading="lazy"/);else assert.match(html,/controls preload="metadata"/);
}
});
test('successful save clears draft attachment ids, stops recorder, and reloads feed',()=>{
const js=fs.readFileSync('web/journal.js','utf8');
const fn=js.slice(js.indexOf('function newJournal'),js.indexOf('async function loadJournal'));
assert.match(fn,/journalDraftMedia.clear\(\)/);assert.match(fn,/journalSelectedMedia.clear\(\)/);assert.match(fn,/stopJournalVoice\(\)/);assert.match(fn,/journal-library'\).open=false/);
assert.match(js,/newJournal\(\);resetJournalFilters\('mine'\);await loadJournal\(saved.id\)/);
assert.match(js,/available.filter\(m=>journalDraftMedia.has\(m.id\)\)/);
});
test('attachment preview uses a closable dialog and deletion has a restore path',()=>{
const js=fs.readFileSync('web/journal.js','utf8'),html=fs.readFileSync('web/index.html','utf8');
assert.match(html,/<dialog id="journal-preview"/);assert.match(js,/addEventListener\('close'/);assert.match(js,/\/api\/journal\/restore-upload/);
assert.match(js,/journal-library-list/);assert.match(js,/if\(other!==media\)other.pause\(\)/);
});
test('structured place formatting and search preserve separate ancestral/birth/legacy locations',()=>{
const vm=require('node:vm'),js=fs.readFileSync('web/family.js','utf8');
const c=vm.createContext({});vm.runInContext(js.slice(js.indexOf('function personLocation'),js.indexOf('function personName')),c);
const person={nativeProvince:'山东省',nativeCity:'烟台市',nativeDistrict:'莱阳市',birthCity:'济南市',birthplace:'旧村名'};
assert.equal(c.personLocation(person,'native'),'山东省 · 烟台市 · 莱阳市');assert.equal(c.personLocation(person,'birth'),'济南市');assert.ok(c.personSearch(person).includes('旧村名'));
});
test('new entry clears hidden edit identifiers even when form.reset preserves them',()=>{
const vm=require('node:vm'),js=fs.readFileSync('web/journal.js','utf8');
const form={elements:{id:{value:'old-entry'},revision:{value:'9'},clientId:{value:'old-request'}},reset(){}};
const context={$:()=>form};vm.createContext(context);
vm.runInContext(js.match(/function clearJournalForm\(\)\{[^\n]+/)[0]+'\nclearJournalForm();',context);
for(const field of Object.values(form.elements))assert.equal(field.value,'');
assert.match(js,/function newJournal\(\)\{clearJournalForm\(\)/);
assert.match(js,/function resetJournal\(\).*clearJournalForm\(\)/);
});
test('initial composer setup happens before asynchronous loading and never clears a typed draft',()=>{
const js=fs.readFileSync('web/journal.js','utf8');const fn=js.slice(js.indexOf('async function loadJournal'),js.indexOf('function renderJournalUploads'));
assert.ok(fn.indexOf('newJournal()')<fn.indexOf('await api('));assert.equal(fn.match(/newJournal\(\)/g).length,1);
});