grafana/k6 · error

node is either not visible or not an HTMLElement

Error message

node is either not visible or not an HTMLElement: %w

What it means

Element-screenshot step: after scrolling into view, boundingBox() failed, so the module reports the element is either not visible or not an HTMLElement. boundingBox needs the element's box model via CDP; failures mean the node has no box (not rendered), was detached, or is not an element node (e.g. a text node handle).

Solutions

  1. Ensure the element is actually visible and expanded (open the accordion/tab, remove display:none) before the screenshot.
  2. Re-query the element right before capturing so it cannot be detached.
  3. Wait for visible state and for animations/transitions to finish.
  4. Check the wrapped error for detaches — 'node is detached from the document'.

Example fix

// before
await page.click('#details-summary'); // panel animating open
await page.$('#details-body').screenshot({ path: 'd.png' });

// after
await page.click('#details-summary');
const el = page.$('#details-body');
await el.waitFor({ state: 'visible' });
await el.screenshot({ path: 'd.png' });
Defensive patterns

Strategy: type-guard

Validate before calling

await handle.waitFor({ state: 'visible' });
const box = await handle.boundingBox();
if (!box || box.width <= 0 || box.height <= 0) throw new Error('element not renderable');
await handle.screenshot({ path: 'e.png' });

Type guard

const isRenderableElement = async (h) => {
  const b = await h.boundingBox();
  return !!b && b.width > 0 && b.height > 0;
};

Try / catch

try {
  await handle.screenshot({ path: 'e.png' });
} catch (e) {
  if (/not visible or not an HTMLElement/i.test(String(e))) {
    // element hidden/detached: open container or re-query before retrying
    await page.locator(sel).waitFor({ state: 'visible' });
    await page.locator(sel).screenshot({ path: 'e.png' });
  } else throw e;
}

Prevention

When it happens

Trigger: Screenshotting a hidden element (display:none/visibility:hidden ancestor), an element detached between scroll and measurement, an SVG-in-shadow-DOM edge case, or a handle to a non-element node.

Common situations: SPA re-render detaching the node mid-call; elements inside collapsed accordions/tabs not opened; content in an iframe whose layout changed; screenshotting immediately after style changes with transitions pending.

Related errors


AI-assisted analysis of grafana/k6@3fcf5388d7 (2026-08-18). Data as JSON: /api/errors/15eb956e02cfd718. Report an issue: GitHub.

Appendix: source

Thrown at internal/js/modules/k6/browser/common/screenshotter.go:304

func (s *screenshotter) screenshotElement(
	h *ElementHandle, opts *ElementHandleScreenshotOptions,
) (buf []byte, err error) {
	format := opts.Format
	viewportSize, err := s.originalViewportSize(h.frame.page)
	if err != nil {
		return nil, fmt.Errorf("getting original viewport size: %w", err)
	}
	originalSize := h.frame.page.emulatedSize

	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")
	}

	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()
		defer func() {
			if restoreErr := s.restoreViewport(h.frame.page, originalSize); restoreErr != nil {
				err = errors.Join(err, fmt.Errorf("restoring viewport: %w", restoreErr))
			}

View on GitHub (pinned to 3fcf5388d7)