supapup
Version:
⚡ Lightning-fast MCP browser dev tool. Navigate → Get instant structured data. No screenshots needed! Puppeteer: 📸 → CSS selectors → JS eval. Supapup: semantic IDs ready to use. 10x faster, 90% fewer tokens.
167 lines (166 loc) • 6.82 kB
JavaScript
/**
* PageSettleDetector V2 - Honest about what we can and cannot detect
*
* What we CAN reliably detect:
* - DOM mutations (additions, removals, attribute changes)
* - Network activity
* - Navigation events
* - Specific element appearance/disappearance
*
* What we CANNOT reliably detect without heuristics:
* - When all JavaScript execution is "done"
* - When all animations are complete
* - When all async operations are finished
*
* Our approach:
* 1. Use MutationObserver for DOM changes (reliable)
* 2. Use network idle detection (reliable)
* 3. Use debouncing to wait for mutation "settling" (heuristic)
* 4. Provide options for specific expectations (most reliable)
*/
export class PageSettleDetectorV2 {
page;
constructor(page) {
this.page = page;
}
/**
* Wait for page to settle based on specific expectations
* This is the most reliable approach - tell us what you expect!
*/
async waitForExpectedChanges(expectations) {
const startTime = Date.now();
const timeout = expectations.timeout || 10000;
const result = {
expectations: {},
mutations: { total: 0, additions: 0, removals: 0, attributes: 0 },
duration: 0,
timedOut: false
};
const promises = [];
// 1. Specific element expectations (MOST RELIABLE)
if (expectations.elementToAppear) {
promises.push(this.page.waitForSelector(expectations.elementToAppear, {
visible: true,
timeout
})
.then(() => { result.expectations.elementAppeared = true; })
.catch(() => { result.expectations.elementAppeared = false; }));
}
if (expectations.elementToDisappear) {
promises.push(this.page.waitForSelector(expectations.elementToDisappear, {
hidden: true,
timeout
})
.then(() => { result.expectations.elementDisappeared = true; })
.catch(() => { result.expectations.elementDisappeared = false; }));
}
if (expectations.textToAppear) {
promises.push(this.page.waitForFunction((text) => document.body.textContent?.includes(text), { timeout }, expectations.textToAppear)
.then(() => { result.expectations.textAppeared = true; })
.catch(() => { result.expectations.textAppeared = false; }));
}
// 2. DOM quiet detection (HEURISTIC)
if (expectations.waitForDOMQuiet !== false) {
const quietTime = expectations.domQuietTime || 500;
promises.push(this.page.evaluate((quietTime) => {
return new Promise((resolve) => {
let timeoutId;
let mutationCount = 0;
let additions = 0;
let removals = 0;
let attributes = 0;
const observer = new MutationObserver((mutations) => {
mutations.forEach(m => {
mutationCount++;
if (m.type === 'childList') {
additions += m.addedNodes.length;
removals += m.removedNodes.length;
}
else if (m.type === 'attributes') {
attributes++;
}
});
// Reset quiet timer
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
observer.disconnect();
resolve({
quiet: true,
total: mutationCount,
additions,
removals,
attributes
});
}, quietTime);
});
observer.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
characterData: true
});
// If no mutations occur
timeoutId = setTimeout(() => {
observer.disconnect();
resolve({
quiet: true,
total: 0,
additions: 0,
removals: 0,
attributes: 0
});
}, quietTime);
});
}, quietTime)
.then((mutationResult) => {
result.expectations.domQuiet = mutationResult.quiet;
result.mutations = mutationResult;
}));
}
// 3. Network idle detection
if (expectations.waitForNetworkIdle !== false) {
promises.push(this.page.waitForNetworkIdle({
idleTime: expectations.networkIdleTime || 500,
timeout: timeout
})
.then(() => { result.expectations.networkIdle = true; })
.catch(() => { result.expectations.networkIdle = false; }));
}
// Wait for all expectations
try {
await Promise.race([
Promise.all(promises),
new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), timeout))
]);
}
catch (e) {
result.timedOut = true;
}
result.duration = Date.now() - startTime;
return result;
}
/**
* Perform action and wait for expected changes
* This encourages users to specify what they expect
*/
async performActionAndWaitFor(action, expectations) {
// Execute the action
const actionResult = await action();
// Wait for expected changes
const settleResult = await this.waitForExpectedChanges(expectations);
return { actionResult, settleResult };
}
/**
* Legacy method that uses heuristics (less reliable)
*/
async performActionAndGuessSettlement(action, options = {}) {
console.warn('⚠️ Using heuristic-based settlement detection. Consider using performActionAndWaitFor() with specific expectations instead.');
return this.performActionAndWaitFor(action, {
waitForDOMQuiet: true,
domQuietTime: options.domQuietTime || 500,
waitForNetworkIdle: true,
networkIdleTime: options.networkIdleTime || 500,
timeout: options.timeout || 10000
});
}
}