grafana/k6 · error · InvalidSelectorError

Error while parsing selector

Error message

Error while parsing selector `${selector}` - selector cannot be empty

What it means

After a successful getAttribute evaluation (frame.go:1025), k6 type-asserts the CDP-returned value to string; if the DevTools protocol returned anything other than a string (or nil, which is handled), the assertion fails with 'unexpected type %T (expecting string)'. Because Element.getAttribute() in the DOM spec always returns string or null, a non-string means the page environment returned an abnormal value: page JavaScript overriding Element.prototype.getAttribute, or a protocol conversion quirk. This is a defensive invariant check, so hitting it usually indicates page tampering or a k6/browser bug.

Solutions

  1. Fetch the attribute manually via evaluation to inspect what the page really returns: await page.evaluate(`document.querySelector(sel).getAttribute(name)`) and check typeof.
  2. If a polyfill/override is the cause, target the element's property directly (el.href) or use a different attribute API (page.evaluate).
  3. Report a k6 issue (grafana/k6) with the page snippet and k6 version if the value is a plain string in DevTools but the error persists.
  4. Pin the browser: launch a known-good Chromium via the browser module's launch options to rule out version drift.

Example fix

// before
const v = await page.getAttribute('#a', 'href'); // unexpected type ...

// after
const v = await page.evaluate(`
  document.querySelector('#a').getAttribute('href')
`); // returns whatever the page really produces
if (typeof v !== 'string' && v !== null) throw new Error('page overrides getAttribute');
Defensive patterns

Strategy: fallback

Validate before calling

const t = await page.evaluate(`
  (() => { const el = document.querySelector('#a'); return el ? typeof el.getAttribute('href') : 'no-el'; })()
`);
if (t !== 'string' && t !== 'object') throw new Error('getAttribute patched on this page');

Type guard

function isAttrResult(v) {
  return v === null || typeof v === 'string';
}

Try / catch

try {
  return await page.getAttribute(sel, name);
} catch (e) {
  if (String(e).includes('unexpected type')) {
    // fall back to raw evaluation
    return await page.evaluate(`document.querySelector('${sel}').getAttribute('${name}')`);
  }
  throw e;
}

Prevention

When it happens

Trigger: The page redefines Element.prototype.getAttribute to return an object/number/undefined; a browser extension or injected script mutates DOM prototypes; a CDP RemoteObject conversion edge case returns a non-string; version mismatch between k6's browser module and the launched Chromium.

Common situations: Testing sites with aggressive anti-bot monkey-patching; pages pulling in polyfills that replace native DOM getters; custom Chromium builds or unusual versions launched via executablePath.

Related errors


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

Appendix: source

Thrown at internal/js/modules/k6/browser/common/js/injected_script.js:1141

    eat1();
    if (operator !== "=" && typeof value !== "string")
      throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - cannot use ${operator} in attribute with non-string matching value - ${value}`);
    return { name: jsonPath.join("."), jsonPath, op: operator, value, caseSensitive };
  }
  const result = {
    name: "",
    attributes: []
  };
  result.name = readIdentifier();
  skipSpaces();
  while (next() === "[") {
    result.attributes.push(readAttribute());
    skipSpaces();
  }
  if (!EOL)
    syntaxError(void 0);
  if (!result.name && !result.attributes.length)
    throw new InvalidSelectorError(`Error while parsing selector \`${selector}\` - selector cannot be empty`);
  return result;
}

// packages/injected/src/roleSelectorEngine.ts
var kSupportedAttributes = ["selected", "checked", "pressed", "expanded", "level", "disabled", "name", "include-hidden"];
kSupportedAttributes.sort();
function validateSupportedRole(attr, roles, role) {
  if (!roles.includes(role))
    throw new Error(`"${attr}" attribute is only supported for roles: ${roles.slice().sort().map((role2) => `"${role2}"`).join(", ")}`);
}
function validateSupportedValues(attr, values) {
  if (attr.op !== "<truthy>" && !values.includes(attr.value))
    throw new Error(`"${attr.name}" must be one of ${values.map((v) => JSON.stringify(v)).join(", ")}`);
}
function validateSupportedOp(attr, ops) {
  if (!ops.includes(attr.op))
    throw new Error(`"${attr.name}" does not support "${attr.op}" matcher`);
}

View on GitHub (pinned to 3fcf5388d7)