grafana/k6 · error
node is either not visible or not an HTMLElement: %w
Error message
node is either not visible or not an HTMLElement: %w
What it means
After scrolling into view, elementHandle.screenshot() calls boundingBox() to get the element's box; that call errored, which the message renders as 'node is either not visible or not an HTMLElement'. boundingBox returns null/error for elements with no layout box: display:none, detached nodes, or non-HTMLElement nodes.
Source
Thrown at internal/js/modules/k6/browser/common/screenshotter.go:286
return visualViewportScale, visualViewportPageX, visualViewportPageY, nil
}
func (s *screenshotter) screenshotElement(h *ElementHandle, opts *ElementHandleScreenshotOptions) ([]byte, error) {
format := opts.Format
viewportSize, originalViewportSize, err := s.originalViewportSize(h.frame.page)
if err != nil {
return nil, fmt.Errorf("getting original viewport size: %w", err)
}
err = h.waitAndScrollIntoViewIfNeeded(h.ctx, false, true, opts.Timeout)
if err != nil {
return nil, fmt.Errorf("scrolling element into view: %w", err)
}
bbox, err := h.boundingBox()
if err != nil {
return nil, fmt.Errorf("node is either not visible or not an HTMLElement: %w", err)
}
if bbox.Width <= 0 {
return nil, fmt.Errorf("node has 0 width")
}
if bbox.Height <= 0 {
return nil, fmt.Errorf("node has 0 height")
}
var overriddenViewportSize *Size
fitsViewport := bbox.Width <= viewportSize.Width && bbox.Height <= viewportSize.Height
if !fitsViewport { //nolint:nestif
overriddenViewportSize = Size{
Width: math.Max(viewportSize.Width, bbox.Width),
Height: math.Max(viewportSize.Height, bbox.Height),
}.enclosingIntSize()
if err := h.frame.page.setViewportSize(overriddenViewportSize); err != nil {
return nil, fmt.Errorf("setting viewport size to %s: %w",
overriddenViewportSize, err)View on GitHub (pinned to 93accf6570)
Solutions
- Make the element visible first: open the tab/modal/accordion or remove hiding CSS, then await locator.waitFor({state:'visible'}).
- Re-resolve the handle right before the screenshot to avoid stale references.
- If you must capture it, temporarily force layout (e.g. page.evaluate to set style.display='block') — or screenshot the page with a clip rect instead.
- Verify your selector matches a real visible element (log await locator.isVisible()).
Example fix
// before
await page.$('.hidden-panel').then(el => el.screenshot({ path: 'p.png' })); // display:none
// after
const panel = page.locator('.hidden-panel');
await page.evaluate("document.querySelector('.hidden-panel').style.display = 'block'");
await panel.waitFor({ state: 'visible' });
await panel.screenshot({ path: 'p.png' }); Defensive patterns
Strategy: validation
Validate before calling
if (!(await locator.isVisible())) throw new Error('element not visible; open it before screenshot'); Type guard
async function hasLayoutBox(loc) {
const bb = await loc.boundingBox();
return bb !== null && bb.width > 0 && bb.height > 0;
} Try / catch
try { await el.screenshot({ path: 'e.png' }); }
catch (e) { if (String(e).includes('not visible or not an HTMLElement')) { /* reveal element, retry */ } else throw e; } Prevention
- Check locator.isVisible() before screenshotting.
- Open modals/tabs that contain the target element first.
- Re-resolve handles in SPAs right before use.
When it happens
Trigger: elementHandle.screenshot() on an element that is display:none / visibility:hidden / position:fixed with no box, an element removed from the DOM between wait and boundingBox, or a handle to a text/comment node.
Common situations: Hidden-by-CSS widgets (tabs, modals not yet opened); anti-bot CSS hiding content; stale handles in SPAs after re-render; elements inside closed shadow roots resolved incorrectly.
Related errors
- scrolling element into view: %w
- parsing element handle screenshot options: %w
- getting original viewport size: %w
- node has 0 width
- node has 0 height
AI-assisted analysis of grafana/k6@93accf6570 (2026-08-15).
Data as JSON: /api/errors/15eb956e02cfd718.
Report an issue: GitHub.