const { JSDOM } = require('jsdom'); const fs = require('fs'); const dom = new JSDOM(`
LOOT `, { 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);