83 lines
3.1 KiB
JavaScript
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);
|