How to Mask Timestamps and Ads in Puppeteer Screenshot Tests
Stabilize Puppeteer screenshot tests by hiding or replacing volatile timestamps and ad content while preserving the layout your test needs.
Direct answer: Puppeteer does not document a built-in screenshot mask option or screenshot stylesheet option. Before capturing, select the changing timestamp and ad elements and apply CSS or replace their content with deterministic fixtures. Use visibility: hidden to preserve their space, display: none to collapse it, or stable replacement content when the region’s appearance and geometry are part of the test. Capture with page.screenshot() or, for a component-only assertion, elementHandle.screenshot(). Puppeteer’s screenshot options describe capture configuration such as clipping, full-page capture and image type.
This guide uses Puppeteer with Node.js. It shows a complete masking workflow, how to choose selectors and masking behavior, how to keep comparisons meaningful, and how to diagnose common failures.
1. Choose what the test should still verify
First decide whether the visual test is supposed to check the timestamp or ad region’s footprint, or just the rest of the page. Masking is a test decision: if you hide too much, a real layout regression can disappear along with the noisy content.
| Strategy | What happens | Use when | Trade-off |
|---|---|---|---|
visibility: hidden |
The content is invisible, but its layout space remains. | The page should retain the timestamp or ad slot’s dimensions and placement. | It will not test the hidden content’s appearance. |
display: none |
The element is removed from layout. | The region should not affect the page layout being tested. | Neighbors may move; a slot-size or placement regression can be concealed. |
| Stable replacement | Variable text or creative is replaced with a fixed fixture. | The region’s geometry, styling, or reserved space matters. | The fixture can drift from real content unless maintained deliberately. |
Prefer selectors owned by your application, such as data-testid attributes, over incidental class names or broad selectors. Mask a timestamp text node’s containing element if the timestamp’s width can change and affect layout. For ads, decide whether to suppress just the creative or the whole slot. If slot size and placement are under test, keep a deterministic slot-shaped fixture in place.
2. Complete Puppeteer example
The following runnable example launches Chromium, loads a local HTML fixture, injects a stylesheet before capture, and writes page.png. Install Puppeteer with npm install puppeteer, save the code as mask-screenshot.mjs, and run node mask-screenshot.mjs. It needs no external site or selectors.
import puppeteer from 'puppeteer';
const html = `
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<style>
body { font: 16px sans-serif; margin: 32px; }
.ad-slot { height: 90px; background: #eee; }
</style>
</head>
<body>
<h1>Release notes</h1>
<p data-testid="published-at">Published at 2026-10-04T12:00:00Z</p>
<div class="ad-slot" data-testid="ad-slot">Rotating ad creative</div>
<p>Stable article content for the visual assertion.</p>
</body>
</html>
`;
const browser = await puppeteer.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1280, height: 800 } });
await page.setContent(html, { waitUntil: 'load' });
// Hide volatile content while retaining its layout footprint.
await page.addStyleTag({
content: `
[data-testid="published-at"],
[data-testid="ad-slot"] {
visibility: hidden !important;
}
`,
});
await page.screenshot({ path: 'page.png', fullPage: true });
} finally {
await browser.close();
}
For your application, replace the fixture with page.goto('https://your-app.example/path', { waitUntil: 'networkidle2' }) or the navigation condition appropriate to the app, then use its stable selectors. networkidle2 is not a guarantee that every application is finished rendering: pages with continuous polling or long-lived connections may never reach a useful idle state. In those cases, wait for a meaningful selector or application-ready signal instead.
Collapse a region
Use this only if removing the slot from layout is intentional:
await page.addStyleTag({
content: `
[data-testid="published-at"],
[data-testid="ad-slot"] {
display: none !important;
}
`,
});
Replace variable content deterministically
To keep the region and its styling in the assertion, replace the changing value with a fixed value. The example below updates text in the page, so application-specific code should select the narrowest appropriate element.
await page.evaluate(() => {
const timestamp = document.querySelector('[data-testid="published-at"]');
if (timestamp) timestamp.textContent = 'Published at 2025-01-01T00:00:00Z';
const ad = document.querySelector('[data-testid="ad-slot"]');
if (ad) {
ad.textContent = 'Advertisement';
ad.style.background = '#eee';
}
});
await page.screenshot({ path: 'page.png', fullPage: true });
Do not use replacement text that changes the intended layout unpredictably. If text width is important, choose a fixture with the expected dimensions or assert the geometry separately.
Capture one component
If the assertion concerns a single component, an element screenshot can reduce unrelated page noise. Puppeteer supports ElementHandle.screenshot(); the element must exist and be visible for a useful capture.
const card = await page.$('[data-testid="article-card"]');
if (!card) throw new Error('article card was not found');
await card.screenshot({ path: 'article-card.png' });
Apply the same deterministic styling before taking the element screenshot. A full-page screenshot and an element screenshot answer different questions: the former includes page layout and surrounding content; the latter isolates a component.
3. Make masking reliable
- Navigate to the state under test. Wait for a page-specific ready condition, not just navigation completion, when content is client-rendered.
- Confirm the targets exist. Use stable test IDs and fail clearly if a required target is missing. A silently unmatched selector may leave dynamic content in the image.
- Apply styles or replacement content. Do this after the target elements exist and before capture. Use
!importantwhere application styles may override the mask. - Capture at fixed conditions. Use consistent viewport, browser version, operating system, headless mode, and relevant rendering settings across baseline and comparison runs.
- Review the masked regions when the page changes. Keep the mask narrow so it excludes known volatility without hiding layout, placement, or ad-slot regressions.
For a required selector, make absence an explicit error:
const targets = await page.$$('[data-testid="published-at"], [data-testid="ad-slot"]');
if (targets.length !== 2) {
throw new Error(`Expected 2 volatile regions, found ${targets.length}`);
}
That count assumes one timestamp and one ad slot. Adapt it if your page has repeated slots. Alternatively, check each required selector separately so the error identifies which element is missing.
4. Puppeteer and Playwright masking are different APIs
Playwright documents locator masking and screenshot stylesheet options. Those APIs belong to Playwright; they are not Puppeteer screenshot options. Puppeteer users can achieve a similar testing outcome by modifying the page’s styling or content before calling Puppeteer’s screenshot methods. See the Playwright screenshot guide and Page screenshot API for that product’s documented options.
Do not copy a Playwright mask option into page.screenshot() and expect Puppeteer to honor it. In Puppeteer, the CSS and replacement examples above are page setup steps, not a native screenshot mask feature.
5. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The timestamp or ad is still visible. | The selector does not match, the target was added after styling, or a later style overrides the injected rule. | Check the selector with page.$() or page.$$() after the page is ready. Inject the CSS after rendering and use !important. |
| The page shifts after masking. | display: none removed the element from layout. |
Use visibility: hidden to keep its footprint, or replace the content with a fixture of suitable dimensions. |
| The screenshot still differs between runs. | Other dynamic content remains, or browser and rendering conditions differ. | Identify the changing pixels, narrow down their owning elements, and stabilize only those. Keep browser version, viewport, OS and rendering settings consistent. |
| The page never becomes idle. | Polling, analytics, streaming, or other persistent requests keep the network active. | Wait for a page-specific selector or ready signal, then apply masks and capture. Avoid treating network idle as a universal readiness condition. |
| The screenshot is blank or captures the wrong state. | Capture ran before navigation or client rendering completed, or the wrong page/frame was used. | Await navigation and the relevant content selector; confirm the page URL and target count immediately before capture. |
| An element screenshot fails or is empty. | The element was not found, is detached, or is not visible. | Check the handle exists after rendering and reacquire it if the page replaced the node. Use a page screenshot if the target cannot be isolated reliably. |
| Masking hides a real regression. | The selector covers too much of the page or hides the entire slot whose geometry should be tested. | Restrict the selector to the volatile subregion, or use a deterministic replacement. Review the mask when markup changes. |
6. Performance, reliability, and cost
Applying a small stylesheet or replacing a few text nodes is generally a light page-setup step, but actual runtime depends on the page, browser, and test environment; no benchmark is implied here. Keep selectors narrow and avoid repeatedly injecting styles in a loop. If the test captures many pages, share browser setup where your test runner allows it while keeping page state isolated.
For reliable comparisons, use the same browser build, viewport, device scale factor, fonts, operating system, and capture mode for the baseline and the new screenshot. Wait for the exact content state the test needs. Dynamic content beyond timestamps and ads—such as animations, rotating recommendations, personalized content, or delayed images—can still cause diffs and may need its own explicit treatment.
The direct cost is the browser and CI time needed to run the screenshot test; the supplied research contains no cost or benchmark figures. Masking does not eliminate the need to maintain selectors and inspect baselines. A broader mask can reduce noisy diffs but also reduces what the test verifies.
7. Or skip the browser setup
ScreenshotNeo provides a screenshot API and MCP server. Its API accepts one GET request with a URL and returns an image or PDF; see the API documentation. For example, this cURL call saves a WebP capture of Stripe:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
The same request in Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
And in Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets before the shot. Bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. For browser-based visual tests requiring deterministic selectors and fixtures, use the Puppeteer workflow above; the API is an alternative for capturing pages without setting up browser automation.
Sign up free for 1,000 screenshots a month, with no card required.
8. FAQ
Does Puppeteer have a native screenshot mask option?
The documented screenshot options do not list Playwright’s locator mask or screenshot stylesheet settings. Change page styles or content before capture instead.
Should I hide the timestamp or replace it?
Hide it if its content is outside the assertion. Replace it with a stable fixture if its dimensions or visual treatment should remain covered by the test.
Can I mask an ad but still test the ad slot?
Yes. Hide or replace the creative while preserving the slot container’s dimensions, then assert the container’s layout separately or keep it visible as a stable placeholder.
Will CSS masking change the actual website?
The injected style changes the page instance controlled by that Puppeteer run. It does not by itself change the site’s source code or content for other visitors.


