Files
den-repo/test/sim.js
2026-08-14 15:56:15 -04:00

83 lines
3.1 KiB
JavaScript

const { JSDOM } = require('jsdom');
const fs = require('fs');
const dom = new JSDOM(`<!DOCTYPE html><html><head></head><body>
<span id="loot">LOOT</span>
</body></html>`, { runScripts: 'outside-only', pretendToBeVisual: false });
const w = dom.window;
const doc = w.document;
// viewport
Object.defineProperty(w, 'innerWidth', { value: 1200 });
Object.defineProperty(w, 'innerHeight', { value: 800 });
// clock + rAF pump
let NOW = 0;
let rafQ = [];
w.performance.now = () => NOW;
w.requestAnimationFrame = (cb) => { rafQ.push(cb); return rafQ.length; };
function pump(ms, step = 16) {
const end = NOW + ms;
while (NOW < end) {
NOW += step;
// run any pending timeouts jsdom scheduled? jsdom timers use real time.
const q = rafQ; rafQ = [];
q.forEach(cb => cb(NOW));
}
}
// matchMedia
w.matchMedia = () => ({ matches: false });
// loot element rect
const loot = doc.getElementById('loot');
loot.setAttribute('data-den-steal', '');
loot.getBoundingClientRect = () => ({ left: 300, top: 300, right: 400, bottom: 330, width: 100, height: 30 });
// generic rects so pois()/terminal don't explode
const origGBCR = w.Element.prototype.getBoundingClientRect;
w.Element.prototype.getBoundingClientRect = function(){
if (this === loot) return loot.getBoundingClientRect();
return { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 };
};
// run den.js
const src = fs.readFileSync(process.argv[2] || 'src/den.js', 'utf8');
w.eval(src);
// jsdom setTimeout is real-time; the spawn sequence uses setTimeout(700/2600).
// We can't fast-forward those, but they don't gate mischief. Wait them out for real.
function log(label){
const clone = [...doc.querySelectorAll('div')].find(d => (d.getAttribute('style')||'').includes('pointer-events:none') && d.textContent.includes('LOOT'));
const says = [...doc.querySelectorAll('.emote.say')].map(e => e.textContent);
console.log(`[t=${NOW}ms] ${label} | loot.visibility='${loot.style.visibility}' | clone=${clone ? 'YES@'+clone.style.left+','+clone.style.top : 'no'} | says=${JSON.stringify(says)}`);
}
setTimeout(() => {
pump(4000); // past spawn
log('before mischief');
w.denEvent('mischief');
log('event fired');
pump(100); log('after 100ms');
pump(1000); log('after ~1.1s (should be walking to loot)');
pump(2000); log('after ~3.1s (grabbed? carrying?)');
// simulate the user moving the mouse mid-carry
const ev = new w.MouseEvent('mousemove', { clientX: 900, clientY: 200 });
doc.dispatchEvent(ev);
log('mouse moved');
pump(3000); log('after ~6.1s');
pump(6000); log('after ~12.1s (dropped by now?)');
pump(8000); log('after ~20.1s (returned by now?)');
pump(10000); log('after ~30.1s (MUST be returned)');
const restored = loot.style.visibility !== 'hidden';
const noOrphan = ![...doc.querySelectorAll('div')]
.some(d => (d.getAttribute('style') || '').includes('pointer-events:none')
&& d.textContent.includes('LOOT'));
console.log('');
console.log(restored ? 'PASS: stolen element restored' : 'FAIL: element still hidden');
console.log(noOrphan ? 'PASS: no orphaned clone left behind' : 'FAIL: clone leaked');
process.exit(restored && noOrphan ? 0 : 1);
}, 3500);