// ==UserScript==
// @name Testbook Mod Clean
// @namespace tb-clean
// @version 1.0.4
// @description Blocks trackers/bloat, removes promos, cleans UI on testbook.com
// @author quantavil
// @match https://testbook.com/*
// @match https://*.testbook.com/*
// @run-at document-start
// @license MIT
// @grant none
// ==/UserScript==
(() => {
'use strict';
// ------------- Utilities -------------
const dbg = false;
const log = (...a) => dbg && console.log('[TB Clean]', ...a);
const injectPageScript = (fn, ...args) => {
const s = document.createElement('script');
s.textContent = `(${fn})(...${JSON.stringify(args)})`;
(document.documentElement || document.head || document.body).appendChild(s);
s.remove();
};
// ------------- Network Blocker (document-start, page context) -------------
const BLOCK_PATTERNS = [
// Analytics / Tags / Pixels
/googletagmanager\.com/i,
/google-analytics\.com/i,
/g\.(?:doubleclick|google)\.net/i,
/doubleclick\.net/i,
/google\.com\/ccm\/collect/i,
/unpkg\.com\/web-vitals/i,
// Facebook
/connect\.facebook\.net/i,
/facebook\.com\/tr/i,
// Microsoft
/bat\.bing\.com/i,
/clarity\.ms/i,
/c\.bing\.com\/c\.gif/i,
// Twitter
/static\.ads-twitter\.com/i,
/analytics\.twitter\.com/i,
/t\.co\/1\/i\/adsct/i,
// Quora
/a\.quora\.com/i,
/q\.quora\.com/i,
// Criteo and ad sync chains
/criteo\.com|static\.criteo\.net|sslwidget\.criteo\.com|gum\.criteo\.com|gumi\.criteo\.com/i,
/cm\.g\.doubleclick\.net/i,
/x\.bidswitch\.net|contextual\.media\.net|r\.casalemedia\.com|ad\.360yield\.com|idsync\.rlcdn\.com|rubiconproject\.com|smartadserver\.com|taboola\.com|outbrain\.com|3lift\.com|agkn\.com|adnxs\.com|dmxleo\.com/i,
// Vendor SDKs / Beacons
/cloudflareinsights\.com/i,
/amplitude\.com/i,
/openfpcdn\.io/i,
/webengage\.com|webengage\.co|wsdk-files\.webengage\.com|c\.webengage\.com|ssl\.widgets\.webengage\.com|survey\.webengage\.com|z\d+.*\.webengage\.co/i,
/intercom\.io|intercomcdn\.com|widget\.intercom\.io|api-iam\.intercom\.io|nexus-websocket-a\.intercom\.io/i,
/onesignal\.com/i,
/hotjar\.com/i,
/sentry\.io/i,
// Payment (blocked on request)
/checkout\.razorpay\.com|checkout-static-next\.razorpay\.com|api\.razorpay\.com/i,
// TB internal bloat
/\/wcapi\/live-panel\.js/i,
/\/js\/live-panel\.js/i,
/live-panel\.template\.html/i,
/live-panel\.styles\.css/i,
/\/cdn-cgi\/rum/i,
/coldboot\/dist\/coldboot\.min\.js/i,
/sourcebuster\/dist\/sourcebuster\.min\.js/i,
// Service workers from site/vendor
/\/service-worker\.js$/i,
];
// Patch fetch/XHR/WS/ES/beacon/src/href setters at document-start inside page context
injectPageScript((patternSources) => {
const BLOCK_PATTERNS = patternSources.map(s => new RegExp(s, 'i'));
const shouldBlock = (rawUrl) => {
try {
const url = typeof rawUrl === 'string' ? new URL(rawUrl, location.href) : rawUrl;
const str = url.toString();
return BLOCK_PATTERNS.some(re => re.test(str));
} catch {
return false;
}
};
// fetch
const origFetch = window.fetch;
if (origFetch) {
window.fetch = function (input, init) {
const url = typeof input === 'string' ? input : (input && input.url);
if (url && shouldBlock(url)) {
return Promise.reject(new Error('Blocked by userscript: ' + url));
}
return origFetch.apply(this, arguments);
};
}
// XHR
const XHR = XMLHttpRequest;
if (XHR && XHR.prototype) {
const origOpen = XHR.prototype.open;
const origSend = XHR.prototype.send;
XHR.prototype.open = function (method, url, async, user, password) {
this.__tbBlocked = url && shouldBlock(url);
if (!this.__tbBlocked) return origOpen.apply(this, arguments);
// Open dummy data URL so site code doesn't crash, then abort on send.
return origOpen.call(this, method, 'data:application/json,{}', true);
};
XHR.prototype.send = function (body) {
if (this.__tbBlocked) {
try { this.abort(); } catch { }
return;
}
return origSend.apply(this, arguments);
};
}
// sendBeacon
if (navigator && 'sendBeacon' in navigator) {
const origBeacon = navigator.sendBeacon.bind(navigator);
navigator.sendBeacon = function (url, data) {
if (shouldBlock(url)) return false;
return origBeacon(url, data);
};
}
// WebSocket
if ('WebSocket' in window) {
const OrigWS = window.WebSocket;
window.WebSocket = function (url, protocols) {
if (shouldBlock(url)) throw new Error('WebSocket blocked: ' + url);
return new OrigWS(url, protocols);
};
window.WebSocket.prototype = OrigWS.prototype;
window.WebSocket.CLOSING = OrigWS.CLOSING;
window.WebSocket.CLOSED = OrigWS.CLOSED;
window.WebSocket.CONNECTING = OrigWS.CONNECTING;
window.WebSocket.OPEN = OrigWS.OPEN;
}
// EventSource
if ('EventSource' in window) {
const OrigES = window.EventSource;
window.EventSource = function (url, conf) {
if (shouldBlock(url)) throw new Error('EventSource blocked: ' + url);
return new OrigES(url, conf);
};
window.EventSource.prototype = OrigES.prototype;
window.EventSource.CLOSED = OrigES.CLOSED;
window.EventSource.CONNECTING = OrigES.CONNECTING;
window.EventSource.OPEN = OrigES.OPEN;
}
// Patch src/href setters and setAttribute for script/link/img/iframe
const patchSrcHref = (proto, prop) => {
const desc = Object.getOwnPropertyDescriptor(proto, prop);
if (!desc || !desc.set) return;
Object.defineProperty(proto, prop, {
configurable: true,
enumerable: desc.enumerable,
get: desc.get ? function () { return desc.get.call(this); } : undefined,
set: function (v) {
if (typeof v === 'string' && shouldBlock(v)) {
this.setAttribute('data-blocked-' + prop, v);
return;
}
return desc.set.call(this, v);
}
});
};
const patchSetAttribute = (proto) => {
const orig = proto.setAttribute;
proto.setAttribute = function (name, value) {
if ((name === 'src' || name === 'href') && typeof value === 'string' && shouldBlock(value)) {
this.setAttribute('data-blocked-' + name, value);
return;
}
return orig.call(this, name, value);
};
};
[HTMLScriptElement.prototype, HTMLLinkElement.prototype, HTMLImageElement.prototype, HTMLIFrameElement.prototype]
.forEach(p => p && patchSetAttribute(p));
patchSrcHref(HTMLScriptElement.prototype, 'src');
patchSrcHref(HTMLLinkElement.prototype, 'href');
patchSrcHref(HTMLImageElement.prototype, 'src');
patchSrcHref(HTMLIFrameElement.prototype, 'src');
// Kill document.write (GTM/pixels sometimes use it)
document.write = () => { };
document.writeln = () => { };
// Stub common trackers to avoid ReferenceErrors
window.dataLayer = window.dataLayer || [];
try { Object.defineProperty(window.dataLayer, 'push', { value: function () { }, writable: false }); } catch { }
window.gtag = function () { };
window.ga = function () { };
window.fbq = function () { };
window.clarity = function () { };
window.Intercom = function () { };
window.amplitude = {
getInstance: () => ({
init() { }, logEvent() { }, setUserId() { }, setUserProperties() { }, identify() { },
})
};
window.OneSignal = { push() { }, init() { }, on() { }, off() { } };
// Block service workers + unregister existing
if ('serviceWorker' in navigator) {
const origRegister = navigator.serviceWorker.register?.bind(navigator.serviceWorker);
navigator.serviceWorker.register = function () {
return Promise.reject(new Error('ServiceWorker registration blocked by userscript'));
};
navigator.serviceWorker.getRegistrations?.().then(list => {
list.forEach(reg => reg.unregister().catch(() => { }));
}).catch(() => { });
}
// Deny Notifications / Push permission
try {
if (window.Notification) {
const origReq = window.Notification.requestPermission?.bind(window.Notification);
window.Notification.requestPermission = function () {
return Promise.resolve('denied');
};
Object.defineProperty(window.Notification, 'permission', { get: () => 'denied' });
}
const origPerms = navigator.permissions?.query?.bind(navigator.permissions);
if (origPerms) {
navigator.permissions.query = function (q) {
if (q && (q.name === 'notifications' || q.name === 'push')) {
return Promise.resolve({ state: 'denied', status: 'denied' });
}
return origPerms(q);
};
}
} catch { }
}, BLOCK_PATTERNS.map(re => re.source));
// ------------- UI Cleaner (DOM removal + CSS, mutation-safe) -------------
const css = `
/* System font and minimal look */
:root { --tb-fm-maxw: 1180px; --tb-fg: #0b0d10; --tb-bg: #ffffff; }
html, body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, Helvetica, Arial, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol" !important; }
body { background: var(--tb-bg) !important; color: var(--tb-fg) !important; }
/* Disable most animations/transitions */
*, *::before, *::after { animation: none !important; transition: none !important; scroll-behavior: auto !important; }
/* Keep content centered/wider */
main, [role="main"], .main, .content, .container, .wrapper, .dashboard, .page-wrapper, #content, #site-content {
max-width: var(--tb-fm-maxw);
margin-left: auto; margin-right: auto;
}
/* Hide live panel and promo components */
promotion-homepage-banner, refer-earn, goal-pitch-wrapper, goal-features-pitch, goal-combo-cards,
master-class-cards, why-testbook-ts, testimonials-ts, faqs { display: none !important; }
.promotional-banner,
[class*="live-panel"], #livePanel, .lp-tabs, .lp-badge-live, .lp-icon,
[onclick*="livePanel"], [src*="/live-panel/"], link[href*="live-panel"],
.tab-area.pav-class-livePanelTabShrunk { display: none !important; }
/* Hide common cookie bars/popups/newsletters/chats */
[id*="cookie"], [class*="cookie"], [aria-label*="cookie"],
[class*="newsletter"], [id*="newsletter"],
[id^="intercom-"], [class*="intercom"], iframe[src*="intercom"],
.we-popup, .we-survey, .we-banner, [class*="webengage"] { display: none !important; }
/* Copy-to-Markdown button in toolbar */
#tb-copy-md-btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 6px 12px;
margin-left: 8px;
border: none;
background: transparent;
color: #86A1AE;
cursor: pointer;
font-size: 13px;
outline: none;
position: relative;
vertical-align: middle;
}
#tb-copy-md-btn:hover {
color: #0AD0F4;
}
#tb-copy-md-btn svg {
width: 15px;
height: 15px;
fill: currentColor;
}
#tb-copy-md-toast {
position: absolute;
top: -30px;
left: 50%;
transform: translateX(-50%);
padding: 4px 8px;
background: #1a7f37;
color: white;
border-radius: 4px;
font-size: 11px;
white-space: nowrap;
opacity: 0;
pointer-events: none;
transition: opacity 0.2s;
}
#tb-copy-md-toast.show {
opacity: 1;
}
#tb-copy-md-toast::after {
content: '';
position: absolute;
bottom: -4px;
left: 50%;
transform: translateX(-50%);
width: 0;
height: 0;
border-left: 4px solid transparent;
border-right: 4px solid transparent;
border-top: 4px solid #1a7f37;
}
`;
const style = document.createElement('style');
style.id = 'tb-clean-style';
style.textContent = css;
(document.head || document.documentElement).appendChild(style);
// Remove targeted DOM and prune nav
const removeSelectors = [
'promotion-homepage-banner', 'refer-earn', 'goal-pitch-wrapper', 'goal-features-pitch', 'goal-combo-cards',
'master-class-cards', 'why-testbook-ts', 'testimonials-ts', 'faqs',
'.promotional-banner', '#masterClassCards',
'.tab-area.pav-class-livePanelTabShrunk',
'[class*="live-panel"]', '#livePanel', '.lp-tabs', '.lp-badge-live', '.lp-icon',
'[onclick*="livePanel"]', '[src*="/live-panel/"]', 'link[href*="live-panel"]',
// Common popups/cookie/chat
'[id*="cookie"]', '[class*="cookie"]', '[aria-label*="cookie"]',
'[class*="newsletter"]', '[id*="newsletter"]',
'[id^="intercom-"]', '[class*="intercom"]', 'iframe[src*="intercom"]',
'.we-popup', '.we-survey', '.we-banner', '[class*="webengage"]',
];
const navPathRegexes = [
/^\/super-coaching/i,
/^\/free-live-classes/i,
/^\/skill-academy/i,
/^\/pass$/i, /^\/pass-pro$/i, /^\/pass-elite$/i,
/^\/reported-questions$/i, /^\/doubts$/i,
/^\/current-affairs\/current-affairs-quiz$/i,
/^\/e-cards$/i,
/^\/teachers-training-program$/i,
/^\/referrals$/i,
/^\/success-stories$/i,
];
function pruneNav() {
const nav = document.querySelectorAll('ul.header__sidebar__nav a[href]');
nav.forEach(a => {
try {
const href = a.getAttribute('href') || '';
const u = new URL(href, location.origin);
if (navPathRegexes.some(re => re.test(u.pathname))) {
const li = a.closest('li') || a;
li.remove();
}
} catch { }
});
// Remove "Learn" and "More" dividers
document.querySelectorAll('ul.header__sidebar__nav .header__divider').forEach(div => {
const t = (div.textContent || '').trim().toLowerCase();
if (t === 'learn' || t === 'more') div.remove();
});
}
function removeJunk() {
removeSelectors.forEach(sel => {
document.querySelectorAll(sel).forEach(n => n.remove());
});
// Live classes blocks with "Classes" title fallback
document.querySelectorAll('.lp-title').forEach(n => {
if ((n.textContent || '').trim().toLowerCase() === 'classes') {
const card = n.closest('.tab-area, .lp-tabs, .live, .pav-class') || n;
card.remove();
}
});
pruneNav();
}
// Disable most autoplay for HTML5 videos
const blockAutoPlay = () => {
try {
const proto = HTMLMediaElement.prototype;
const origPlay = proto.play;
proto.play = function () {
const hasAuto = this.autoplay || this.getAttribute('autoplay') !== null;
if (hasAuto) {
return Promise.reject(new DOMException('Autoplay blocked by userscript', 'NotAllowedError'));
}
return origPlay.apply(this, arguments);
};
} catch { }
};
// ------------- Copy-to-Markdown injector and extractor -------------
function absUrl(u) {
try {
if (!u) return '';
if (u.startsWith('//')) return location.protocol + u;
return new URL(u, location.href).toString();
} catch { return u || ''; }
}
function isHidden(el) {
if (!el) return true;
if (el.closest('.ng-hide')) return true;
const cs = getComputedStyle(el);
return cs.display === 'none' || cs.visibility === 'hidden';
}
// Minimal HTML -> Markdown converter (supports p/div/br, strong/b, em/i, a, img, ul/ol/li, h1-h6)
function htmlToMarkdown(root) {
function textify(str) {
return (str || '').replace(/\s+/g, ' ').replace(/\u00A0/g, ' ').trim();
}
function walk(node, ctx = {}) {
if (!node) return '';
const T = Node;
switch (node.nodeType) {
case T.TEXT_NODE:
return textify(node.nodeValue);
case T.ELEMENT_NODE: {
const tag = node.tagName.toLowerCase();
// Collect children first
const childMD = Array.from(node.childNodes).map(n => walk(n, ctx)).join('');
if (tag === 'br') return ' \n';
if (tag === 'strong' || tag === 'b') return childMD ? `**${childMD}**` : '';
if (tag === 'em' || tag === 'i') return childMD ? `*${childMD}*` : '';
if (tag === 'u') return childMD; // no underline in MD
if (tag === 'a') {
const href = absUrl(node.getAttribute('href') || '');
const txt = childMD || href || '';
return href ? `[${txt}](${href})` : txt;
}
if (tag === 'img') {
const src = absUrl(node.getAttribute('src') || node.src || '');
const alt = node.getAttribute('alt') || '';
return src ? `` : '';
}
if (tag === 'ul' || tag === 'ol') {
const ordered = tag === 'ol';
const items = Array.from(node.children).filter(li => li.tagName && li.tagName.toLowerCase() === 'li');
return items.map((li, idx) => {
const prefix = ordered ? `${idx + 1}. ` : `- `;
const liMD = Array.from(li.childNodes).map(n => walk(n, ctx)).join('');
return `${prefix}${liMD}\n`;
}).join('') + '\n';
}
if (/^h[1-6]$/.test(tag)) {
const level = Number(tag[1]);
return `${'#'.repeat(level)} ${childMD}\n\n`;
}
if (tag === 'p' || tag === 'div' || tag === 'section' || tag === 'article') {
const content = childMD.trim();
return content ? `${content}\n\n` : '';
}
return childMD;
}
default: return '';
}
}
const md = walk(root).replace(/\n{3,}/g, '\n\n').trim();
return md;
}
function getQuestionBox() {
const boxes = Array.from(document.querySelectorAll('.que-ans-box'));
if (!boxes.length) return null;
const visible = boxes.find(b => !isHidden(b));
return visible || boxes[0];
}
function getComprehensionEl() {
return document.querySelector('.aei-comprehension [ng-bind-html]') || null;
}
function getQuestionEl(qaBox) {
if (!qaBox) return null;
const all = qaBox.querySelectorAll('.qns-view-box');
for (const el of all) {
if (el.closest('li.option')) continue;
if (el.closest('[ng-bind-html*="getSolutionDesc"]')) continue;
return el; // first non-option, non-solution qns-view-box inside question box
}
return null;
}
function getOptions(qaBox) {
if (!qaBox) return [];
// First list that actually contains option nodes
const lists = Array.from(qaBox.querySelectorAll('ul'));
let list = lists.find(u => u.querySelector('li.option'));
if (!list) return [];
const items = Array.from(list.querySelectorAll('li.option')).filter(li => li.querySelector('.qns-view-box'));
return items.map((li, idx) => {
const box = li.querySelector('.qns-view-box');
const md = htmlToMarkdown(box);
return { index: idx, textMD: md, el: li };
});
}
function getCorrectOptionIndex(qaBox) {
if (!qaBox) return -1;
// Try to find correct option by class markers (works when solution visibility marks it)
const correctLI = qaBox.querySelector('li.option.correct-option, li.option.reattempt-correct-option');
if (!correctLI) return -1;
const all = Array.from(qaBox.querySelectorAll('ul li.option'));
const idx = all.indexOf(correctLI);
return idx >= 0 ? idx : -1;
}
function getSolutionEl(qaBox) {
if (!qaBox) return null;
// Present in DOM even when hidden by ng-hide
return qaBox.querySelector('[ng-bind-html*="getSolutionDesc"]') || null;
}
function buildMarkdownForCurrentQuestion() {
const qaBox = getQuestionBox();
const parts = [];
const comp = getComprehensionEl();
if (comp) {
parts.push('## Comprehension', htmlToMarkdown(comp));
}
const qEl = getQuestionEl(qaBox);
if (qEl) {
parts.push('## Question', htmlToMarkdown(qEl));
}
const opts = getOptions(qaBox);
if (opts.length) {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
parts.push('## Options');
const lines = opts.map(o => `${letters[o.index]}. ${o.textMD}`);
parts.push(lines.join('\n'));
}
// Try to add Answer if we can detect the correct option
const correctIdx = getCorrectOptionIndex(qaBox);
if (correctIdx >= 0 && opts[correctIdx]) {
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
parts.push('## Answer', `${letters[correctIdx]}. ${opts[correctIdx].textMD}`);
}
const solEl = getSolutionEl(qaBox);
if (solEl) {
parts.push('## Solution', htmlToMarkdown(solEl));
}
const md = parts.filter(Boolean).join('\n\n').replace(/\n{3,}/g, '\n\n').trim();
return md || 'No content found.';
}
async function copyTextToClipboard(text) {
try {
if (navigator.clipboard && navigator.clipboard.writeText) {
await navigator.clipboard.writeText(text);
return true;
}
} catch { }
// Fallback
const ta = document.createElement('textarea');
ta.value = text;
ta.style.position = 'fixed';
ta.style.top = '-9999px';
document.body.appendChild(ta);
ta.select();
let ok = false;
try { ok = document.execCommand('copy'); } catch { }
ta.remove();
return ok;
}
function ensureCopyButton() {
// Find the toolbar area
const toolbar = document.querySelector('.tp-pos-neg-marks');
if (!toolbar) return;
// Avoid duplicates
if (toolbar.querySelector('#tb-copy-md-btn')) return;
const btn = document.createElement('button');
btn.id = 'tb-copy-md-btn';
btn.type = 'button';
btn.title = 'Copy question, options, and solution as Markdown';
btn.innerHTML = `
<svg viewBox="0 0 16 16" aria-hidden="true">
<path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25ZM5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/>
</svg>
`;
const toast = document.createElement('span');
toast.id = 'tb-copy-md-toast';
toast.textContent = 'Copied!';
btn.appendChild(toast);
// Insert before the first child (or append if no children)
if (toolbar.firstChild) {
toolbar.insertBefore(btn, toolbar.firstChild);
} else {
toolbar.appendChild(btn);
}
btn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
try {
const md = buildMarkdownForCurrentQuestion();
const ok = await copyTextToClipboard(md);
toast.textContent = ok ? 'Copied!' : 'Failed';
} catch {
toast.textContent = 'Failed';
}
toast.classList.add('show');
setTimeout(() => toast.classList.remove('show'), 1500);
});
}
// ------------- Bootstrapping -------------
const onReady = (fn) => {
if (document.readyState === 'complete' || document.readyState === 'interactive') fn();
else document.addEventListener('DOMContentLoaded', fn, { once: true });
};
onReady(() => {
// Initial clean + inject button
removeJunk();
blockAutoPlay();
ensureCopyButton();
// Observe SPA changes
const obs = new MutationObserver(() => {
removeJunk();
ensureCopyButton();
});
obs.observe(document.documentElement, { childList: true, subtree: true });
// Also re-run on history changes (Angular/SPA)
const pushState = history.pushState;
const replaceState = history.replaceState;
history.pushState = function () {
const r = pushState.apply(this, arguments);
setTimeout(() => { removeJunk(); ensureCopyButton(); }, 50);
return r;
};
history.replaceState = function () {
const r = replaceState.apply(this, arguments);
setTimeout(() => { removeJunk(); ensureCopyButton(); }, 50);
return r;
};
window.addEventListener('popstate', () => setTimeout(() => { removeJunk(); ensureCopyButton(); }, 50));
});
})();