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
- Fetch the attribute manually via evaluation to inspect what the page really returns: await page.evaluate(`document.querySelector(sel).getAttribute(name)`) and check typeof.
- If a polyfill/override is the cause, target the element's property directly (el.href) or use a different attribute API (page.evaluate).
- 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.
- 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
- On sites known to patch DOM prototypes, read attributes via page.evaluate from the start.
- Keep k6 and its bundled/launched Chromium aligned in version.
- Report reproducible unexpected-type results upstream with a minimal page.
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
- "level" attribute must be compared to a number
- "name" attribute must be a string or a regular expression
- Role must not be empty
- an error occurred communicating with the provisioning API
- capturing screenshot
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)