Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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:

  1. Identify the intended frame by a stable attribute, URL, or predicate.
  2. Wait inside that frame for a marker that means the application has finished rendering.
  3. If an action navigates the frame, register frame.waitForNavigation() before clicking and await both operations together.
  4. 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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 A4 or provide explicit dimensions. Margins affect clipping and page breaks.
  • Backgrounds: printBackground: true includes background colors and images that would otherwise be omitted.
  • CSS page size: preferCSSPageSize: true lets the document’s @page size 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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().

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Content 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 finally block.
  • 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.Support on Ko-Fi

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Best Value
The SQL Programming Language: .
  • 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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

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.

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.