Free tools Windows power users keep installed
One-click scans. No signup required.
Wait for the iframe’s application-level ready state, not merely for the <iframe> element, before calling page.pdf(). In Puppeteer, locate the correct Frame, wait for a selector or function that proves its data is complete, coordinate any frame navigation with the action that triggers it, and only then print the outer page. This prevents PDFs that contain an empty frame, a loading spinner, or a partially rendered report.
The reliable sequence
A page and each iframe have separate Puppeteer contexts. A selector wait on the outer Page does not observe elements inside a child frame. The dependable sequence is:
- Identify the intended frame by a stable attribute, URL, or predicate.
- Wait inside that frame for a marker that means the application has finished rendering.
- If an action navigates the frame, register
frame.waitForNavigation()before clicking and await both operations together. - Generate the PDF after the readiness condition succeeds.
The marker is application-specific. An iframe element appearing, a generic container existing, or a network request finishing can all occur before the report data is visible.
Complete Puppeteer example
The following CommonJS script finds an iframe named report, waits for a visible completion marker, and writes a PDF. Replace the URL and selector with values from your application.
#1 Best Overall
const puppeteer = require('puppeteer');
(async () => {
const browser = await puppeteer.launch({headless: true});
const page = await browser.newPage();
try {
await page.goto('https://example.com/dashboard', {
waitUntil: 'domcontentloaded',
timeout: 60_000,
});
const frame = await page.waitForFrame(async frame => {
const element = await frame.frameElement();
if (!element) return false;
return await element.evaluate(el => el.getAttribute('name') === 'report');
});
await frame.waitForSelector('[data-report-status="complete"]', {
visible: true,
timeout: 30_000,
});
await page.pdf({
path: 'report.pdf',
format: 'A4',
printBackground: true,
margin: {top: '16mm', right: '16mm', bottom: '16mm', left: '16mm'},
});
} finally {
await browser.close();
}
})();
Page.waitForFrame() accepts a URL or a predicate. The predicate above inspects the frame element and matches its name. A stable attribute or URL is safer than assuming that the first child in page.frames() is the report; pages commonly contain analytics, payment, advertising, or authentication frames as well.
Choose and locate the right frame
Wait for an iframe created later
When the application inserts the iframe asynchronously, wait directly for it:
const frame = await page.waitForFrame(async candidate => {
const element = await candidate.frameElement();
return Boolean(element && await element.evaluate(
el => el.matches('iframe[data-role="report"]')
));
});
This avoids polling the DOM yourself and handles a frame that does not exist at the first page load.
Use the current frame tree when it already exists
for (const candidate of page.frames()) {
const element = await candidate.frameElement();
if (element && await element.evaluate(el => el.id === 'report-frame')) {
// candidate is the frame to use
}
}
page.frames() returns the page’s current frame tree, and a frame’s childFrames() exposes descendants. Position-based selection such as page.frames()[1] is fragile because frame order can change when a site adds a widget.
Recommended Free Tools
Match by URL when the source is stable
If the report always loads from a recognizable origin or path, a URL predicate can be concise:
const frame = await page.waitForFrame(candidate =>
candidate.url().startsWith('https://reports.example.com/'))
;
Use an attribute when the URL contains unpredictable tokens, and use a URL when several identical iframe elements exist but only one loads the report origin.
Rank #2
Wait for application readiness inside the frame
Selector presence versus visibility
frame.waitForSelector() waits in the frame context and continues to work across frame navigations. By default, the surfaced reference uses a 30-second timeout. visible: true additionally requires the element to be visible:
await frame.waitForSelector('[data-report-status="complete"]', {
visible: true,
timeout: 45_000,
});
Presence alone is useful when a completion node is inserted only after data arrives. Visibility is useful when the node exists in the DOM but is hidden until rendering finishes. Neither option proves that every chart, image, or custom component has painted; choose a marker that the report code sets after its own data and rendering work.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
Wait for a function when readiness is state, not a node
await frame.waitForFunction(() => {
const report = window.__REPORT_STATE__;
return report && report.status === 'complete' && report.rows > 0;
}, {timeout: 45_000});
A function is appropriate when the application exposes a status object, a row count, or another deterministic condition. Keep the condition narrow: waiting for document.body or a generic spinner to disappear can succeed while data is still being assembled.
Handle nested iframes
If the report itself embeds another iframe, first identify the report frame, then inspect its childFrames() or use another waitForFrame-style predicate as the nested frame appears. Run the final readiness wait against the frame that owns the completion marker; a selector in the outer document cannot cross iframe boundaries.
When an action navigates the iframe
Clicks that submit a form, change a report URL, or invoke a full frame navigation must pair the navigation wait with the action. Registering the wait after the click can miss a fast navigation and leave the script hanging or printing the old document.
const [response] = await Promise.all([
frame.waitForNavigation({waitUntil: 'domcontentloaded', timeout: 60_000}),
frame.click('a.generate-report'),
]);
// response is the main-resource response, or null for a History API change.
await frame.waitForSelector('[data-report-status="complete"]', {
visible: true,
timeout: 45_000,
});
await page.pdf({path: 'report.pdf', format: 'A4'});
History API URL changes count as navigation, and the navigation result can be null. Navigation completion still does not mean that client-side data fetching or chart rendering is finished, so retain the application-specific readiness wait.
When no navigation is expected
For a button that updates the existing document through XHR or fetch, wait for the completion marker directly:
await frame.click('button.refresh-report');
await frame.waitForSelector('[data-report-status="complete"]', {
visible: true,
timeout: 45_000,
});
If the same marker was already present before the click, wait for a state change instead—for example, a new report ID, a changed timestamp, or a function that verifies the expected row count. Otherwise the old marker can satisfy the wait immediately.
Generate a PDF that matches the intended design
page.pdf() uses print CSS media by default. If the report is designed for screen media, set it explicitly before printing:
await page.emulateMediaType('screen');
await page.pdf({
path: 'report-screen-style.pdf',
format: 'A4',
printBackground: true,
preferCSSPageSize: true,
displayHeaderFooter: false,
margin: {top: '12mm', right: '12mm', bottom: '12mm', left: '12mm'},
});
- Print versus screen: leave the default print media for print-specific CSS; use
emulateMediaType('screen')when screen rules are the desired appearance. - Paper and margins: choose a format such as
A4or provide explicit dimensions. Margins affect clipping and page breaks. - Backgrounds:
printBackground: trueincludes background colors and images that would otherwise be omitted. - CSS page size:
preferCSSPageSize: truelets the document’s@pagesize take priority over the selected format. - Fonts: the PDF API reference defaults to waiting for fonts (
waitForFonts: true). Continue to wait for any application-specific charts, images, or data after fonts are ready.
Do not use a long arbitrary delay as the primary readiness mechanism. A delay can make fast jobs slower and still fail on a slow report. If a site has a known animation, combine a short delay with a real completion marker.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Timeouts, failures, and recovery
“Waiting failed: 30,000ms exceeded”
The selector never appeared, appeared in another frame, or the report failed before setting its completion state. Confirm the frame predicate, inspect the selector in that frame, and increase the timeout only after establishing that the expected load can legitimately take longer. Treat the timeout as a failed PDF job rather than printing a partial document.
The PDF has an empty iframe
Most often, the script waited on the outer page or on the iframe element itself. Move the wait to the target Frame and use a marker set after the iframe’s data and rendering complete. Also check whether the frame navigates after your current wait; if it does, pair the triggering action with frame.waitForNavigation().
Rank #4
The script hangs after a click
A navigation wait registered after the click can lose the race. Put both calls in one Promise.all. If the click uses History API routing, expect a null response and then wait for the application marker.
The wrong iframe is selected
Log each frame’s URL and inspect its element attributes. Replace positional selection with a stable name, id, data-* attribute, or origin predicate. If the frame is inserted later, use page.waitForFrame() instead of taking a snapshot too early.
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Scan for outdated or missing drivers - takes under a minute3Repair Windows errors before they cause bigger problemsContent is clipped or styling is wrong
Check print versus screen media, paper format, margins, preferCSSPageSize, and background printing. Ensure the report’s CSS has finished loading and that the frame is not horizontally constrained by a fixed viewport. A PDF can be technically complete while still reflecting print rules you did not intend.
Cross-origin access errors
Puppeteer can operate on a frame context without your page JavaScript directly reading cross-origin variables. Do not design a readiness function that assumes access to an unrelated origin’s objects. Prefer a visible marker exposed in that frame, a URL predicate, or a cooperative postMessage-driven marker in the page you control.
Reliability and performance practices
- Use one browser per worker or job pool: repeatedly launching Chromium is expensive, while reusing a controlled browser reduces startup time. Close pages in a
finallyblock. - Set explicit navigation and readiness timeouts: separate the page-load budget from the report-render budget so failures are diagnosable.
- Wait for the smallest meaningful condition: an application completion flag is faster and more reliable than a multi-second sleep.
- Capture diagnostics on failure: save the current URL, frame URLs, console messages, and a screenshot or HTML snapshot before closing the page.
- Control external variability: use a deterministic account, timezone, locale, and test data where possible. Third-party widgets can add frames and change frame ordering.
- Retry selectively: retry transient navigation or network failures, but do not repeatedly retry a deterministic selector mismatch without fixing the frame identity or readiness condition.
Check the Puppeteer reference that matches your installed package. The surfaced documentation covers versions labeled 25.9.0 through 25.12.0, and method signatures or defaults can change between releases.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
For a straightforward website capture, ScreenshotNeo accepts one GET request and returns a PNG, JPEG, WebP, or PDF. It handles the browser infrastructure and can wait for a selector, delay, or network idle; it can also click an element, run custom JavaScript, hide selectors, load lazy images, and capture a CSS-selected element.
PC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchBest Value
- Used Book in Good Condition
Example PDF request (see the ScreenshotNeo API documentation):
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/dashboard -o report.pdf
ScreenshotNeo removes cookie or consent banners, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies the result with X-Page-Verdict and X-Billed headers. An MCP server provides take_screenshot, get_page_info, and capture_pdf tools for Claude, Cursor, and other MCP clients.
The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is included on every plan, and yearly billing gives two months free. Create a free ScreenshotNeo account to start without a card.
Frequently asked questions
Does waitUntil: 'networkidle0' guarantee iframe content is ready?
No. It describes network activity for the page navigation and does not prove that the iframe’s application has committed its final data or rendered its components. Use it only as one signal, followed by a frame-specific readiness condition.
Can I call page.pdf() from the iframe’s page object?
PDF generation is a Page operation. Keep the readiness wait on the target Frame, then call page.pdf() on the outer page.
What should the readiness marker be?
Use a stable marker your application sets after its own data and rendering work, such as data-report-status="complete", a final row count, or a JavaScript state flag. Avoid generic selectors whose presence precedes real completion.
Why does navigation sometimes return null?
History API transitions can count as navigation without a traditional main-resource response. Continue with the frame’s application-level readiness wait rather than requiring a non-null response.
Frequently Asked Questions
Does waitUntil: 'networkidle0' guarantee iframe content is ready?
No. It does not prove that the iframe’s application has finished rendering; use a frame-specific completion condition.
Can I call page.pdf() from an iframe?
No. Wait on the target Frame, then call page.pdf() on the outer Page.
What should the readiness marker be?
Choose a marker your application sets after data and rendering complete, such as a completion attribute, final row count, or state flag.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

