Do these 3 things before closing this tab:
1Repair Windows errors before they cause bigger problems2Fix the driver behind crashes, sound loss and screen glitches3Clear out junk files and repair common Windows errorsWait for the state your screenshot actually needs—not merely for the browser’s readyState. In Ruby, use a Capybara matcher or Selenium explicit wait that observes an application-level signal such as data-ready="true", expected text, or a component-defined completion event. Then capture the page. If you only need the browser to register a custom-element class, JavaScript’s customElements.whenDefined() is appropriate, but it does not prove that the component has finished rendering or loading data.
Why navigation completion is not custom-element readiness
WebDriver navigation can finish while JavaScript is still fetching data, upgrading elements, inserting shadow DOM, loading images, or running animations. The Selenium Project describes this distinction: readyState covers assets declared in the HTML, while loaded JavaScript can continue changing the page afterward.
A custom element has several different milestones:
- Element present: the tag appears in the DOM.
- Definition registered: the browser has associated the tag name with a class.
- Connected: the element’s lifecycle callbacks can run.
- Application ready: data, child content, images, and visual state required by your screenshot are complete.
Only the last milestone is a reliable screenshot boundary, and its signal is application-specific. A selector such as my-widget[data-ready="true"] below is illustrative; use a real contract exposed by the page.
Choose a readiness signal you can observe
Preferred: a semantic ready attribute
Ask the component to expose a stable attribute, for example <my-widget data-ready="true">. This is easy for Capybara and Selenium to test and remains meaningful if the component’s internal implementation changes.
#1 Best Overall
Expected text or child content
If you cannot change the component, wait for text that only appears after the data request succeeds, or for a child selector that is inserted as the final rendering step. Avoid waiting for a generic tag presence when the tag is created before its content arrives.
Definition registration
customElements.whenDefined("my-widget") resolves when the registry defines the name. It is useful when registration itself is the requirement, but it does not wait for asynchronous fetching, images, animations, or component-specific rendering.
Completion event
A component can dispatch a custom event such as widget-ready. Waiting for that event is precise when the component author documents that it fires only after all screenshot-critical work is complete.
Capybara: wait with a retrying matcher
Capybara automatically retries asynchronous finders and matchers until its configured wait period expires. The documented default for Capybara.default_max_wait_time is two seconds, but a project can change it.
- Visit the page with the JavaScript-capable driver configured by your test suite.
- Assert the application’s ready condition with a waiting matcher.
- Save the screenshot only after the matcher succeeds.
require "capybara/dsl"
Capybara.default_max_wait_time = 10
visit("https://example.test/dashboard")
# Replace this selector with the page's documented readiness signal.
expect(page).to have_css('my-widget[data-ready="true"]')
page.save_screenshot("dashboard.png", full: true)
The matcher retries until the element exists in the required state or the wait expires. A successful have_css("my-widget") only proves that the host element is present; it is not evidence that its data or visual output is complete.
Waiting for absence
For a loading mask or skeleton that must disappear, use Capybara’s waiting negative matcher rather than negating an immediate predicate:
Rank #2
expect(page).to have_no_css("my-widget .loading-skeleton")
expect(page).to have_css('my-widget[data-ready="true"]')
page.save_screenshot("dashboard.png")
Using both a positive and negative condition can prevent a screenshot from being taken during a transition where the ready marker appears before the old loading UI is removed.
Configure the timeout narrowly
Set a timeout based on the slowest legitimate response in your environment, not a universal number. You can configure the global default or pass a matcher-specific wait option where supported by your Capybara version. Keep the wait finite so a broken endpoint fails the test instead of hanging indefinitely.
The Tool Desk
Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →Outbyte PC Repair FREERepair Windows errors before they cause bigger problemsFix Now →Selenium WebDriver from Ruby: an explicit condition
Selenium’s Ruby binding exposes explicit waits, but exact method signatures can vary by installed selenium-webdriver version. Check the API for the version in your Gemfile. The principle is stable: create a wait, poll an observable condition, and capture after it returns true.
require "selenium-webdriver"
options = Selenium::WebDriver::Chrome::Options.new
options.add_argument("--headless=new")
driver = Selenium::WebDriver.for(:chrome, options: options)
begin
driver.navigate.to("https://example.test/dashboard")
wait = Selenium::WebDriver::Wait.new(timeout: 15, interval: 0.2)
wait.until do
element = driver.find_element(css: 'my-widget[data-ready="true"]')
element.displayed?
rescue Selenium::WebDriver::Error::NoSuchElementError,
Selenium::WebDriver::Error::StaleElementReferenceError
false
end
# Optional second guard for a loading layer that must be gone.
wait.until do
driver.find_elements(css: "my-widget .loading-skeleton").none?(&:displayed?)
end
driver.save_screenshot("dashboard.png")
ensure
driver.quit
end
The rescue is important because a framework may replace the element while rendering; that transient stale reference should cause another poll, not an immediate failure. If your driver or Selenium version uses different wait APIs, retain the same condition and polling behavior while adjusting syntax to that version.
Waiting for text
wait.until do
text = driver.find_element(css: "my-widget").text
text.include?("Account loaded")
rescue Selenium::WebDriver::Error::NoSuchElementError,
Selenium::WebDriver::Error::StaleElementReferenceError
false
end
Text is a practical fallback, but choose a phrase that cannot appear in a placeholder, error state, or previous result.
Use whenDefined when registration is the actual requirement
Run this JavaScript in the page when you need to know that the browser has registered one custom-element name:
Rank #3
await customElements.whenDefined("my-widget");
The promise resolves at definition time. Because a component can continue work in connectedCallback() and later callbacks, follow it with an application condition before capturing. For example, a Selenium script can execute:
driver.execute_async_script(<<~JS)
const done = arguments[arguments.length - 1];
customElements.whenDefined("my-widget").then(() => done(true));
JS
wait.until do
driver.find_element(css: 'my-widget[data-ready="true"]')
true
rescue Selenium::WebDriver::Error::NoSuchElementError
false
end
driver.save_screenshot("dashboard.png")
If a container can contain several undefined custom-element tags, collect distinct local names and await all definitions:
const host = document.querySelector("#app");
const names = [...host.querySelectorAll("*")]
.map(el => el.localName)
.filter(name => name.includes("-"));
await Promise.all(
[...new Set(names)].map(name => customElements.whenDefined(name))
);
This waits for registration only. Add the component’s ready marker, content check, or completion event afterward.
Capturing components with shadow DOM
A host selector may be present while the useful content lives in an open shadow root. If your readiness marker is inside that root, evaluate it directly:
Free tools Windows power users keep installed
One-click scans. No signup required.
wait.until do
driver.execute_script(<<~JS)
const host = document.querySelector("my-widget");
host && host.shadowRoot &&
host.shadowRoot.querySelector("[data-ready='true']") !== null
JS
end
Closed shadow roots cannot be inspected through ordinary page JavaScript. In that case, wait for a host-level attribute or event provided by the component rather than trying to pierce the boundary.
Make the screenshot deterministic
- Disable or finish animations when the page supports a test mode; otherwise two captures can differ even after data is ready.
- Wait for images that affect the composition, not merely the custom element. A ready flag should be set after those images have loaded if they are part of the contract.
- Use a stable viewport, device scale factor, locale, timezone, and test data so layout changes do not masquerade as timing failures.
- Capture once per readiness condition. Repeated screenshots do not make an incorrect condition more reliable.
- Log the elapsed wait and the condition that succeeded. This helps distinguish a slow but healthy page from a component that never signals readiness.
Troubleshooting common failures
Timeout while the element is visible
The selector may target a state the application never sets, or the page may be using a different attribute value. Inspect the DOM after timeout, verify spelling and case, and confirm that the test driver actually runs JavaScript.
Rank #4
Screenshot shows a skeleton or empty card
You waited for host presence rather than content readiness. Replace the presence check with a ready attribute, final text, child selector, or documented completion event.
whenDefined resolves but the screenshot is still incomplete
That is expected: registration and rendering are separate milestones. Keep whenDefined as a preliminary wait and add an application-specific condition.
Intermittent stale-element errors
Framework re-rendering replaced the node between polls. Locate the element inside the wait block each time and retry stale references, as in the Selenium example.
Negative assertion passes too early
An immediate negated predicate can succeed before the loading element has ever been inserted. Use Capybara’s waiting have_no_css or an explicit Selenium condition that observes the intended transition.
Headless and headed captures differ
Compare viewport dimensions, device scale, fonts, browser version, and reduced-motion settings. A timing fix cannot correct a different rendering environment.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Or skip the browser setup
ScreenshotNeo provides a single HTTP request for a URL and supports waits for a selector, delay, or network idle, along with custom JavaScript when your page exposes a readiness condition. It can remove cookie/consent banners, newsletter popups, and chat widgets before capture. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed; the response reports the result in 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.
curl -G "https://api.screenshotneo.com/v1/shot"
-d access_key=YOUR_API_KEY
--data-urlencode url=https://example.test/dashboard
-o dashboard.webp
See the ScreenshotNeo documentation for the wait and custom-script parameters. The same request in Python:
Best Value
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.test/dashboard"},
timeout=90,
)
r.raise_for_status()
open("dashboard.webp", "wb").write(r.content)
And Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.test/dashboard' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const fs = await import('node:fs/promises');
await fs.writeFile('dashboard.webp', Buffer.from(await res.arrayBuffer()));
ScreenshotNeo has a free plan with 1,000 screenshots per month and no card; paid plans start at $5 for 3,000 screenshots. Create a free ScreenshotNeo account to try the API.
Cost, reliability, and maintenance decisions
Local Capybara or Selenium gives maximum control and is appropriate when the screenshot is part of an end-to-end test running beside your application. It also requires browser binaries, driver maintenance, fonts, network access, and a repeatable rendering environment.
An API is useful for scheduled captures, documentation, previews, and CI jobs that should not manage browsers. Use a readiness selector or script that is stable across deployments, set a finite request timeout, and inspect response headers so a failed or blocked page is distinguishable from a valid capture. Caching can reduce repeated work when the page is unchanged; disable or shorten the cache TTL when validating live state.
Recommended Free Tools
Ruby checklist
- Define what “ready” means for this component.
- Prefer a semantic attribute, final content, or documented event over tag presence.
- Use Capybara’s retrying matcher or a Selenium explicit wait.
- Use
whenDefinedonly for definition registration, then apply the app-level check. - Account for shadow DOM, images, loading masks, and animations.
- Capture immediately after the condition succeeds and retain timeout diagnostics.
Frequently Asked Questions
Can I wait for every custom element on a page with one generic Ruby command?
No. You can await registration for known tag names with JavaScript, but there is no universal signal that every component has finished fetching data and rendering. Each application needs a readiness contract.
Does increasing Capybara’s timeout guarantee a correct screenshot?
No. A longer timeout only gives the existing condition more time to succeed. If the condition checks host presence instead of completed content, the screenshot can still be early.
Which approach is better for CI: Capybara or Selenium?
Use the framework already driving your tests. Capybara offers concise retrying matchers and screenshot support; Selenium exposes lower-level explicit conditions. The readiness signal matters more than the choice.
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.
Quick wins for a faster PC:
Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →Clear out junk files and repair common Windows errorsFree Scan →

