Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/browser/cdp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
scrollJs,
autoScrollJs,
networkRequestsJs,
waitForDomStableJs,
} from './dom-helpers.js';

export interface CDPTarget {
Expand Down Expand Up @@ -166,10 +167,11 @@ class CDPPage implements IPage {
.catch(() => {}); // Don't fail if event times out
await this.bridge.send('Page.navigate', { url });
await loadPromise;
// Post-load settle: SPA frameworks need extra time to render after load event
// Smart settle: use DOM stability detection instead of fixed sleep.
// settleMs is now a timeout cap (default 1000ms), not a fixed wait.
if (options?.waitUntil !== 'none') {
const settleMs = options?.settleMs ?? 1000;
await new Promise(resolve => setTimeout(resolve, settleMs));
const maxMs = options?.settleMs ?? 1000;
await this.evaluate(waitForDomStableJs(maxMs, Math.min(500, maxMs)));
}
}

Expand Down
34 changes: 34 additions & 0 deletions src/browser/dom-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,37 @@ export function networkRequestsJs(includeStatic: boolean): string {
})()
`;
}

/**
* Generate JS to wait until the DOM stabilizes (no mutations for `quietMs`),
* with a hard cap at `maxMs`. Uses MutationObserver in the browser.
*
* Returns as soon as the page stops changing, avoiding unnecessary fixed waits.
* If document.body is not available, falls back to a fixed sleep of maxMs.
*/
export function waitForDomStableJs(maxMs: number, quietMs: number): string {
return `
new Promise(resolve => {
if (!document.body) {
setTimeout(() => resolve('nobody'), ${maxMs});
return;
}
let timer = null;
let cap = null;
const done = (reason) => {
clearTimeout(timer);
clearTimeout(cap);
obs.disconnect();
resolve(reason);
};
const resetQuiet = () => {
clearTimeout(timer);
timer = setTimeout(() => done('quiet'), ${quietMs});
};
const obs = new MutationObserver(resetQuiet);
obs.observe(document.body, { childList: true, subtree: true, attributes: true });
resetQuiet();
cap = setTimeout(() => done('capped'), ${maxMs});
})
`;
}
13 changes: 9 additions & 4 deletions src/browser/page.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
scrollJs,
autoScrollJs,
networkRequestsJs,
waitForDomStableJs,
} from './dom-helpers.js';

/**
Expand Down Expand Up @@ -53,11 +54,15 @@ export class Page implements IPage {
if (result?.tabId) {
this._tabId = result.tabId;
}
// Post-load settle: the extension already waits for tab.status === 'complete',
// but SPA frameworks (React/Vue) need extra time to render after DOM load.
// Smart settle: use DOM stability detection instead of fixed sleep.
// settleMs is now a timeout cap (default 1000ms), not a fixed wait.
if (options?.waitUntil !== 'none') {
const settleMs = options?.settleMs ?? 1000;
await new Promise(resolve => setTimeout(resolve, settleMs));
const maxMs = options?.settleMs ?? 1000;
await sendCommand('exec', {
code: waitForDomStableJs(maxMs, Math.min(500, maxMs)),
...this._workspaceOpt(),
...this._tabOpt(),
});
}
}

Expand Down
Loading