// ==UserScript==
// @name Quiz.com Slide Debug + Highlight (invisible toggle)
// @namespace quiz-debug
// @version 2.5
// @description Detect exposed correct answers from Quiz.com worker messages, invisible toggle
// @match https://quiz.com/*
// @match https://*.quiz.com/*
// @run-at document-start
// @grant unsafeWindow
// ==/UserScript==
(function () {
'use strict';
const win = unsafeWindow;
let correctAnswers = [];
let lastQuestion = '';
let highlightVisible = false; // hidden by default
console.log('[Quiz Debug] v2.5 loaded — invisible toggle, hidden by default');
// =========================================================
// Helpers
// =========================================================
function normalize(value) {
return String(value ?? '')
.replace(/\s+/g, ' ')
.trim()
.toLowerCase();
}
function setCorrectAnswers(question, answers) {
const found = answers
.filter(a => a && a.isCorrect === true)
.map(a => String(a.text ?? '').trim())
.filter(Boolean);
if (!found.length) return;
const signature =
String(question || '') + '|' + found.join('|');
const oldSignature =
lastQuestion + '|' + correctAnswers.join('|');
if (signature === oldSignature)
return;
lastQuestion = String(question || '');
correctAnswers = found;
console.log(
'%c[Quiz Debug] QUESTION:',
'color:#00bfff;font-weight:bold',
lastQuestion
);
console.log(
'%c[Quiz Debug] CORRECT:',
'color:#00ff66;font-weight:bold',
correctAnswers
);
clearHighlights();
scheduleHighlight();
}
// =========================================================
// Recursively inspect any object coming from Worker
// =========================================================
function inspect(value, depth = 0, seen = new WeakSet()) {
if (depth > 12 || value == null)
return;
if (typeof value === 'string') {
const str = value.trim();
if (str.startsWith('{') || str.startsWith('[')) {
try {
inspect(JSON.parse(str), depth + 1, seen);
} catch (_) {}
}
return;
}
if (typeof value !== 'object')
return;
if (seen.has(value))
return;
seen.add(value);
if (
Array.isArray(value.answers) &&
value.answers.some(
a =>
a &&
typeof a === 'object' &&
'isCorrect' in a
)
) {
setCorrectAnswers(value.question, value.answers);
}
if (
value.path === '/slideObject' &&
value.value &&
Array.isArray(value.value.answers)
) {
setCorrectAnswers(
value.value.question,
value.value.answers
);
}
if (Array.isArray(value)) {
for (const item of value) {
inspect(item, depth + 1, seen);
}
} else {
for (const key of Object.keys(value)) {
try {
inspect(value[key], depth + 1, seen);
} catch (_) {}
}
}
}
// =========================================================
// Highlight DOM
// =========================================================
function applyOutline(el) {
el.style.setProperty('outline', '6px solid #00ff55', 'important');
el.style.setProperty('outline-offset', '3px', 'important');
el.style.setProperty('box-shadow', '0 0 20px #00ff55', 'important');
}
function removeOutline(el) {
el.style.removeProperty('outline');
el.style.removeProperty('outline-offset');
el.style.removeProperty('box-shadow');
}
function clearHighlights() {
document
.querySelectorAll('[data-quiz-correct-highlight]')
.forEach(el => {
removeOutline(el);
el.removeAttribute('data-quiz-correct-highlight');
});
}
function highlightAnswers() {
if (!correctAnswers.length)
return;
const expected = correctAnswers.map(normalize);
const elements = Array.from(
document.querySelectorAll(
'button, [role="button"], div, span, p'
)
);
for (const answer of expected) {
let matches = elements.filter(el => {
if (el.children.length > 3) return false;
if (normalize(el.textContent) !== answer) return false;
const r = el.getBoundingClientRect();
return (
r.width > 20 &&
r.height > 10 &&
r.width < window.innerWidth * 0.8 &&
r.height < 180
);
});
if (!matches.length) {
console.log(
'[Quiz Debug] Text not found in DOM:',
answer
);
continue;
}
matches.sort((a, b) => {
const ar = a.getBoundingClientRect();
const br = b.getBoundingClientRect();
return ar.width * ar.height - br.width * br.height;
});
let target = matches[0];
let node = target;
for (
let i = 0;
i < 8 && node && node.parentElement;
i++
) {
const parent = node.parentElement;
const rect = parent.getBoundingClientRect();
const text = normalize(parent.textContent);
if (
text === answer &&
rect.width >= 120 &&
rect.height >= 30 &&
rect.height <= 140
) {
target = parent;
}
node = parent;
}
target.setAttribute('data-quiz-correct-highlight', 'true');
if (highlightVisible) {
applyOutline(target);
} else {
removeOutline(target);
}
console.log(
'%c[Quiz Debug] Marked:',
'color:#00ff55;font-weight:bold',
answer,
highlightVisible ? '(visible)' : '(hidden)',
target
);
}
}
function scheduleHighlight() {
[
0, 50, 150, 300, 600,
1000, 1500, 2500, 4000
].forEach(ms => {
setTimeout(highlightAnswers, ms);
});
}
// =========================================================
// Toggle button — invisible until hovered
// =========================================================
function makeToggleButton() {
if (document.getElementById('quiz-debug-toggle-wrap'))
return;
if (!document.body) {
setTimeout(makeToggleButton, 50);
return;
}
// invisible hover zone
const wrap = document.createElement('div');
wrap.id = 'quiz-debug-toggle-wrap';
Object.assign(wrap.style, {
position: 'fixed',
bottom: '0',
right: '0',
width: '90px',
height: '90px',
zIndex: '2147483647',
background: 'transparent',
pointerEvents: 'auto'
});
const btn = document.createElement('button');
btn.id = 'quiz-debug-toggle';
btn.type = 'button';
btn.textContent = '○';
Object.assign(btn.style, {
position: 'absolute',
bottom: '16px',
right: '16px',
width: '32px',
height: '32px',
borderRadius: '50%',
border: '2px solid #777',
background: 'rgba(0,0,0,0.55)',
color: '#777',
fontSize: '16px',
fontWeight: 'bold',
cursor: 'pointer',
padding: '0',
lineHeight: '1',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
userSelect: 'none',
opacity: '0',
transition: 'opacity 0.2s, background 0.2s, color 0.2s, border-color 0.2s',
pointerEvents: 'none'
});
function paintState() {
if (highlightVisible) {
btn.style.borderColor = '#00ff55';
btn.style.color = '#00ff55';
btn.textContent = '●';
} else {
btn.style.borderColor = '#777';
btn.style.color = '#777';
btn.textContent = '○';
}
}
paintState();
wrap.addEventListener('mouseenter', () => {
btn.style.opacity = '1';
btn.style.pointerEvents = 'auto';
});
wrap.addEventListener('mouseleave', () => {
btn.style.opacity = '0';
btn.style.pointerEvents = 'none';
});
btn.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
highlightVisible = !highlightVisible;
console.log(
'[Quiz Debug] highlight visible =',
highlightVisible
);
paintState();
document
.querySelectorAll('[data-quiz-correct-highlight]')
.forEach(el => {
if (highlightVisible) {
applyOutline(el);
} else {
removeOutline(el);
}
});
});
wrap.appendChild(btn);
document.body.appendChild(wrap);
}
// =========================================================
// Hook WebSocket — safe wrapper
// =========================================================
try {
const NativeWebSocket = win.WebSocket;
function PatchedWebSocket(...args) {
const socket = new NativeWebSocket(...args);
console.log('[Quiz Debug] window WebSocket:', args[0]);
socket.addEventListener('message', event => {
try { inspect(event.data); } catch (_) {}
});
const nativeSend = socket.send;
socket.send = function (data) {
try { inspect(data); } catch (_) {}
return nativeSend.call(this, data);
};
return socket;
}
PatchedWebSocket.prototype = NativeWebSocket.prototype;
Object.setPrototypeOf(PatchedWebSocket, NativeWebSocket);
for (const k of ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED']) {
try {
Object.defineProperty(PatchedWebSocket, k, {
value: NativeWebSocket[k],
writable: false,
configurable: false
});
} catch (_) {}
}
win.WebSocket = PatchedWebSocket;
} catch (error) {
console.warn('[Quiz Debug] WebSocket hook failed:', error);
}
// =========================================================
// Hook Worker — safe wrapper
// =========================================================
try {
const NativeWorker = win.Worker;
function PatchedWorker(...args) {
console.log('[Quiz Debug] Worker created:', args[0]);
const worker = new NativeWorker(...args);
worker.addEventListener('message', event => {
try { inspect(event.data); } catch (_) {}
});
const nativePostMessage = worker.postMessage;
worker.postMessage = function (...msgArgs) {
try { inspect(msgArgs[0]); } catch (_) {}
return nativePostMessage.apply(this, msgArgs);
};
return worker;
}
PatchedWorker.prototype = NativeWorker.prototype;
Object.setPrototypeOf(PatchedWorker, NativeWorker);
win.Worker = PatchedWorker;
console.log('[Quiz Debug] Worker hook installed');
} catch (error) {
console.error('[Quiz Debug] Worker hook failed:', error);
}
// =========================================================
// Hook SharedWorker
// =========================================================
if (win.SharedWorker) {
try {
const NativeSharedWorker = win.SharedWorker;
function PatchedSharedWorker(...args) {
console.log('[Quiz Debug] SharedWorker:', args[0]);
const shared = new NativeSharedWorker(...args);
if (shared.port) {
hookMessagePort(shared.port);
try { shared.port.start(); } catch (_) {}
}
return shared;
}
PatchedSharedWorker.prototype = NativeSharedWorker.prototype;
Object.setPrototypeOf(PatchedSharedWorker, NativeSharedWorker);
win.SharedWorker = PatchedSharedWorker;
} catch (error) {
console.warn('[Quiz Debug] SharedWorker hook failed:', error);
}
}
// =========================================================
// Hook MessagePort
// =========================================================
function hookMessagePort(port) {
if (!port || port.__quizDebugHooked) return;
try {
Object.defineProperty(port, '__quizDebugHooked', { value: true });
} catch (_) { return; }
port.addEventListener('message', event => {
try { inspect(event.data); } catch (_) {}
});
const nativePostMessage = port.postMessage;
port.postMessage = function (...args) {
try { inspect(args[0]); } catch (_) {}
return nativePostMessage.apply(this, args);
};
}
// =========================================================
// Hook MessageChannel
// =========================================================
try {
const NativeMessageChannel = win.MessageChannel;
if (NativeMessageChannel) {
function PatchedMessageChannel(...args) {
const channel = new NativeMessageChannel(...args);
hookMessagePort(channel.port1);
hookMessagePort(channel.port2);
return channel;
}
PatchedMessageChannel.prototype = NativeMessageChannel.prototype;
Object.setPrototypeOf(PatchedMessageChannel, NativeMessageChannel);
win.MessageChannel = PatchedMessageChannel;
}
} catch (_) {}
// =========================================================
// React rerender observer
// =========================================================
const observer = new MutationObserver(() => {
if (correctAnswers.length) {
highlightAnswers();
}
});
function startObserver() {
if (!document.documentElement) {
setTimeout(startObserver, 20);
return;
}
observer.observe(document.documentElement, {
childList: true,
subtree: true
});
}
startObserver();
// =========================================================
// Boot the toggle button
// =========================================================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', makeToggleButton);
} else {
makeToggleButton();
}
setInterval(() => {
if (!document.getElementById('quiz-debug-toggle-wrap')) {
makeToggleButton();
}
}, 1000);
})();