const assert = require('node:assert/strict'); const fs = require('node:fs'); const path = require('node:path'); const vm = require('node:vm'); const ts = require(process.argv[2]); const root = process.argv[3]; let timers = new Map(), nextTimer = 0; function load(name) { const exports = {}; vm.runInNewContext(ts.transpileModule(fs.readFileSync(path.join(root, name + '.ets'), 'utf8'), { compilerOptions: { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2020 } }).outputText, { exports, console, setTimeout: fn => { timers.set(++nextTimer, fn); return nextTimer; }, clearTimeout: id => timers.delete(id), Date, Map, Set, Number, Math, JSON, require: name => { if (name.startsWith('./')) return load(name.slice(2)); throw Error(name); } }); return exports; } const { RemoteViewport, MouseInput, TouchInput, VideoSettings, NetworkStatistics, RecentPresence, asciiKey } = load('ControllerModels'); const viewport = new RemoteViewport(); viewport.fit(320, 200, 1920, 1080); assert.equal(viewport.baseHeight, 180); assert.equal(viewport.top(), 10); const before = viewport.x(100); viewport.zoom(2, 100, 100); assert.equal(viewport.x(100), before); viewport.zoom(100, 160, 100); assert.equal(viewport.scale, 10); viewport.pan(1e6, -1e6); assert.equal(viewport.offsetX, viewport.baseWidth * 4.5); viewport.zoom(.00001, 160, 100); assert.equal(viewport.scale, 1); assert.equal(viewport.offsetX, 0); assert(!viewport.contains(100, 0)); assert.equal(viewport.x(-100), 0); const packets = []; const mouse = new MouseInput((x, y, flag, wheel) => packets.push({ x, y, flag, wheel })); const touch = new TouchInput(viewport, mouse); function input(kind, points = []) { if (kind === 'down') touch.begin(); touch.touch(points.map(([x,y]) => ({x,y})), kind); } input('down', [[100,100]]); input('move', [[120,100]]); input('up'); assert(packets.every(p => p.flag === 0)); // Relative movement never drags. packets.length = 0; input('down', [[100,100]]); input('up'); assert.deepEqual(packets.map(p => p.flag), [1,2]); packets.length = 0; input('down', [[100,100]]); touch.hold(); input('up'); assert.deepEqual(packets.map(p => p.flag), [3,4]); touch.relative = false; packets.length = 0; input('down', [[100,100]]); input('move', [[120,100]]); input('move', [[400,300]]); input('up'); assert.deepEqual(packets.map(p => p.flag), [0,1,0,0,2]); assert.equal(packets.at(-2).x, 1); packets.length = 0; input('down', [[100,100]]); input('down', [[100,100],[200,100]]); input('move', [[80,100],[220,100]]); input('up', [[80,100]]); input('move', [[90,110]]); input('up'); assert(viewport.scale > 1); assert(packets.every(p => p.flag === 0)); viewport.reset(); packets.length = 0; input('down', [[100,100]]); input('down', [[100,100],[200,100]]); input('move', [[100,125],[200,125]]); input('up'); assert.equal(packets.filter(p => p.flag === 7).length, 1); mouse.button(true, true); const revision = mouse.revision; packets.length = 0; input('down', [[100,100]]); input('move', [[110,100]]); input('up'); assert(mouse.left); assert(packets.every(p => p.flag === 0)); assert.equal(mouse.revision, revision); mouse.button(false, true); mouse.drag(32,0,320,180); mouse.release(); assert.equal(packets.at(-2).flag, 2); assert.equal(packets.at(-1).flag, 4); assert.equal(asciiKey('a'), 65); assert.equal(asciiKey('A'), 321); assert.equal(asciiKey('!'), 305); assert.equal(asciiKey('中'), 0); const settings = new VideoSettings(1, 1280); assert.equal(settings.selection.quality, 0); assert.equal(settings.begin({quality:1,frameRate:30,preference:1}), -1); settings.support(true); const one = {quality:2,frameRate:60,preference:0}, two = {quality:1,frameRate:30,preference:2}; const id1 = settings.begin(one), id2 = settings.begin(two); settings.receive(one, id1, true); assert(settings.pending); assert.equal(settings.applied.quality, 2); settings.receive(two, id2, false); assert(!settings.pending); assert.equal(settings.selection.quality, 2); const id3 = settings.begin(two); settings.expire(id2); assert(settings.pending); settings.expire(id3); assert.equal(settings.selection.quality, 2); settings.receive(two,id3+1,true); assert.equal(settings.selection.quality,2); settings.requestId = 0xffffffff; settings.appliedRequestId = 0xffffffff; const wrapped = settings.begin(two); assert.equal(wrapped,0); settings.receive(two,wrapped,true); assert.equal(settings.selection.preference,2); settings.support(false); assert(!settings.pending); const network = new NetworkStatistics(); const traffic = {inbound:8000000,outbound:10000,loss:.02}; network.receive({rtt:40,mode:0,srtp:true,video:traffic,audio:traffic,data:traffic,total:traffic}, 100); network.receive({rtt:80,mode:1,srtp:false,video:traffic,audio:traffic,data:traffic,total:traffic}, 200); assert.equal(network.rtt,50); assert.equal(network.mode(201),'TURN 中继'); assert.equal(network.mode(3200),'—'); network.frame(6,7000,1920,1080,200); network.frame(6,1,1920,1080,250); assert.equal(network.fps(250),6); assert.equal(network.latency(250),7000); assert.equal(network.fps(1200),0); network.reset(); assert.equal(network.width,0); const subscriptions = []; const presence = new RecentPresence(ids => subscriptions.push([...ids])); presence.watch(['111111111'],0); presence.connect(true,10); assert.equal(subscriptions.length,1); presence.receive({type:'presence_update',id:'111111111',online:true},20); assert(!presence.online('111111111',21)); presence.receive({type:'presence',devices:[{id:'111111111',online:true},{id:'222222222',online:true}]},30); assert(presence.online('111111111',31)); assert(!presence.online('222222222',31)); assert(!presence.online('111111111',60030)); presence.tick(30010); assert.equal(subscriptions.length,2); presence.connect(false,40000); presence.connect(true,41000); assert(!presence.online('111111111',41001)); const { AnnouncementInbox } = load('AnnouncementInbox'); const { announcementSpans, announcementByteLength, isAnnouncementURL } = load('AnnouncementText'); const spans = announcementSpans('说明\r\n[下载](HTTP://example.com/a(b));https://example.com/end. 普通文本'); assert.equal(spans.map(p=>p.text).join(''),'说明\n下载;https://example.com/end. 普通文本'); assert.deepEqual(Array.from(spans.filter(p=>p.url),p=>p.url),['HTTP://example.com/a(b)','https://example.com/end']); assert(!isAnnouncementURL('javascript:alert(1)'));assert(!isAnnouncementURL('https://a\\b'));assert(!isAnnouncementURL('https://')); assert.equal(announcementByteLength('中'.repeat(80)),240);assert.equal(announcementByteLength('😀'),4); const requests = []; const saved = new Map(); let failSave = false; const inbox = new AnnouncementInbox(r => requests.push({...r}), s => saved.get(s) ?? '{}', (s,v) => { if(failSave)throw Error(); saved.set(s,v); }); inbox.configure('server:9099:identity'); inbox.connect(true); function answer(request, entries, total, revision=1) { inbox.receive({type:'announcements',request_id:request.request_id,summary_only:request.summary_only,offset:request.offset,total,catalog_revision:revision,items:entries}); } const entries = Array.from({length:21},(_,i)=>({id:i+1,revision:1,title:`Title ${i+1}`,body:'Body',updated_at:1700000000})); const first = requests.at(-1); answer(first,entries,21); assert.equal(requests.at(-1).summary_only,false); answer(requests.at(-1),entries.slice(0,20),21); assert.equal(inbox.items.length,20); assert.equal(inbox.unread,21); inbox.markRead(inbox.items[0]); assert.equal(inbox.unread,20); failSave = true; inbox.dismiss(inbox.items[0]); assert.equal(inbox.total,21); assert(inbox.saveFailed); failSave = false; inbox.dismiss(inbox.items[0]); answer(requests.at(-1),entries.slice(20),21); assert.equal(inbox.items.length,20); assert.equal(inbox.total,20); const stale = requests.at(-1); inbox.refresh(); answer(stale,[],0); assert(inbox.loading); answer(requests.at(-1),entries.map(e=>({...e,revision:2})),21,2); answer(requests.at(-1),entries.slice(0,20).map(e=>({...e,revision:2})),21,2); assert.equal(inbox.unread,21); inbox.more(); answer(requests.at(-1),entries.slice(20).map(e=>({...e,revision:2})),21,2); assert.equal(inbox.items.length,21); inbox.connect(false); const count=requests.length; inbox.more(); assert.equal(requests.length,count); inbox.connect(true); for(const timer of [...timers.values()])timer(); assert(inbox.failed); inbox.configure('utf8');inbox.connect(true);answer(requests.at(-1),[entries[0]],1); answer(requests.at(-1),[{...entries[0],title:'中'.repeat(81)}],1);assert(inbox.failed);assert.equal(inbox.items.length,0); console.log('PASS: mobile viewport/gestures, shared button ownership, keyboard, video ACK ordering, network expiry, presence and announcement pagination/revisions');