61 lines
6.2 KiB
JavaScript
61 lines
6.2 KiB
JavaScript
|
|
const test=require('node:test'),assert=require('node:assert/strict'),vm=require('node:vm'),fs=require('node:fs'),Ranges=require('./web/recording-ranges.js');
|
||
|
|
const start=Date.parse('2026-10-04T00:00:00Z'),date=n=>new Date(start+n*1000);
|
||
|
|
test('ranges clip to selected day and merge overlaps without filling gaps',()=>{
|
||
|
|
const items=[{start:date(-10),duration:40},{start:date(20),duration:30},{start:date(60),duration:100},{start:'invalid',duration:20},{start:date(3),duration:-4},{start:date(3),duration:Infinity}];
|
||
|
|
assert.deepEqual(Ranges.normalize(items,date(0),date(100)),[{start:+date(0),end:+date(50)},{start:+date(60),end:+date(100)}]);
|
||
|
|
});
|
||
|
|
test('long recording continues after each one-hour session',()=>{
|
||
|
|
const ranges=[{start:+date(0),end:+date(8000)}];assert.deepEqual(Ranges.at(ranges,date(0)),{start:+date(0),end:+date(3600)});
|
||
|
|
assert.deepEqual(Ranges.next(ranges,date(3600)),{start:+date(3600),end:+date(7200)});assert.equal(Ranges.next(ranges,date(8000)),null);
|
||
|
|
});
|
||
|
|
test('next segment skips the actual gap and does not replay the previous end',()=>{
|
||
|
|
const ranges=[{start:+date(0),end:+date(10)},{start:+date(30),end:+date(40)}];assert.equal(Ranges.at(ranges,date(10)),null);
|
||
|
|
assert.deepEqual(Ranges.next(ranges,date(10)),{start:+date(30),end:+date(40)});
|
||
|
|
});
|
||
|
|
test('clip requires valid, positive, fully recorded interval up to one hour',()=>{
|
||
|
|
const r=[{start:+date(0),end:+date(4000)},{start:+date(5000),end:+date(8000)}];
|
||
|
|
for(const [a,b] of [[date(1),date(1)],[date(4),date(3)],[date(0),date(3601)],[date(3900),date(5100)],[date(-1),date(5)],[new Date(NaN),date(5)]])assert.throws(()=>Ranges.clip(r,a,b));
|
||
|
|
assert.deepEqual(Ranges.clip(r,date(0),date(3600)),{start:+date(0),end:+date(3600)});
|
||
|
|
});
|
||
|
|
function section(file,a,b){const s=fs.readFileSync('web/'+file,'utf8'),i=s.indexOf(a),j=s.indexOf(b,i+a.length);assert.ok(i>=0&&j>i);return s.slice(i,j);}
|
||
|
|
function deferred(){let resolve;const promise=new Promise(r=>resolve=r);return{promise,resolve};}
|
||
|
|
function ui(extra={}){const elements=new Map();const $=s=>{if(!elements.has(s))elements.set(s,{replaceChildren(){},reset(){},value:'',hidden:false});return elements.get(s);};return vm.createContext({$,Number,Date,URLSearchParams,loggedIn:true,activePage:'playback',RecordingRanges:Ranges,...extra});}
|
||
|
|
test('auto-continue preserves user choice, stops short clips and ends at last segment',()=>{
|
||
|
|
const c=ui({playbackRange:{start:0,end:10000},spans:[{start:0,end:10000},{start:20000,end:30000}],playAt:d=>c.played=+d});
|
||
|
|
vm.runInContext(section('recording-ui.js','function continuePlayback(','function makeClip('),c);
|
||
|
|
c.$('#play-video').ended=true;c.$('#play-video').currentTime=10;c.$('#play-autonext').checked=false;c.continuePlayback();assert.equal(c.played,undefined);
|
||
|
|
c.$('#play-autonext').checked=true;c.$('#play-video').currentTime=1;c.continuePlayback();assert.match(c.$('#play-info').textContent,/提前结束/);assert.equal(c.played,undefined);
|
||
|
|
c.$('#play-video').currentTime=10;c.continuePlayback();assert.equal(c.played,20000);assert.match(c.$('#play-info').textContent,/跳过/);
|
||
|
|
c.playbackRange={start:20000,end:30000};c.continuePlayback();assert.match(c.$('#play-info').textContent,/播放完毕/);
|
||
|
|
});
|
||
|
|
test('late recording list cannot replace another camera or a logged-out view',async()=>{
|
||
|
|
const p=deferred(),c=ui({playbackGeneration:2,queryCamera:'',resetPlayback(){},api:()=>p.promise,drawTimeline(){throw Error('late draw');}});
|
||
|
|
c.$('#play-camera').value='camera';c.$('#play-date').value='2026-10-04';c.$('#archive-source').value='local';
|
||
|
|
vm.runInContext(section('recording-ui.js','async function queryRecordings(','function drawTimeline('),c);
|
||
|
|
const work=c.queryRecordings();c.playbackGeneration++;p.resolve({items:[]});await work;assert.equal(c.spans,undefined);
|
||
|
|
});
|
||
|
|
test('download generation refuses gaps and invalidates old links first',()=>{
|
||
|
|
const c=ui({queryCamera:'camera',spans:[{start:+date(0),end:+date(60)}],playbackParams:()=>new URLSearchParams({start:'test'}),Error});
|
||
|
|
vm.runInContext(section('recording-ui.js','function makeClip(',"$('#query-recordings')"),c);
|
||
|
|
c.$('#clip-start').value=date(0).toISOString();c.$('#clip-end').value=date(61).toISOString();c.makeClip();assert.equal(c.$('#clip-download').hidden,true);assert.match(c.$('#clip-status').textContent,/超出/);
|
||
|
|
});
|
||
|
|
test('preset load from a closed panel is ignored',async()=>{
|
||
|
|
const p=deferred(),c=ui({ptzCamera:'first',ptzBusy:false,ptzPresetGeneration:0,clearPresetConfirmation(){},api:()=>p.promise,renderPresets(){throw Error('late render');}});
|
||
|
|
vm.runInContext(section('monitor.js','async function loadPresets(','async function openPtz('),c);
|
||
|
|
const work=c.loadPresets();c.ptzPresetGeneration++;c.ptzCamera='';p.resolve({items:[]});await work;
|
||
|
|
});
|
||
|
|
test('late goto result cannot overwrite a newer stop response',async()=>{
|
||
|
|
const p=deferred(),c=ui({ptzBusy:false,ptzCamera:'camera',ptzPresets:{revision:'1'},ptzGeneration:1,ptzCommandGeneration:1,clearPresetConfirmation(){},setPtzBusy(){},api:()=>p.promise});
|
||
|
|
vm.runInContext(section('monitor.js','async function presetCommand(','function closePtz('),c);
|
||
|
|
const work=c.presetCommand('goto','1');c.ptzCommandGeneration++;c.$('#preset-status').textContent='stopped';p.resolve({message:'old goto'});await work;assert.equal(c.$('#preset-status').textContent,'stopped');
|
||
|
|
});
|
||
|
|
test('snapshot callback cannot reveal a prior login frame after logout',async()=>{
|
||
|
|
let callback,created=0;const c=ui({sessionGeneration:1,snapshotGeneration:0,clearSnapshot(){c.snapshotGeneration++;},toast(){},URL:{createObjectURL(){created++;}},document:{createElement(){return{getContext:()=>({drawImage(){}}),toBlob:f=>callback=f};}}});
|
||
|
|
vm.runInContext(section('snapshot.js','async function captureFrame(','document.addEventListener'),c);
|
||
|
|
const work=c.captureFrame({readyState:4,videoWidth:100,videoHeight:50},'camera','Test');c.sessionGeneration++;c.loggedIn=false;callback({});await work;assert.equal(created,0);
|
||
|
|
});
|
||
|
|
test('snapshot rejects empty or errored video without producing a blob',async()=>{
|
||
|
|
let messages=0;const c=ui({toast:()=>messages++});vm.runInContext(section('snapshot.js','async function captureFrame(','document.addEventListener'),c);
|
||
|
|
await c.captureFrame({readyState:0,videoWidth:0});await c.captureFrame({readyState:4,videoWidth:100,error:{code:3}});assert.equal(messages,2);
|
||
|
|
});
|