microsoft/playwright · error · InvalidSelectorError

Error while parsing css selector "${selector}". Did you mean

Error message

Error while parsing css selector "${selector}". Did you mean to CSS.escape it?

What it means

Thrown at the end of parseCSS after function arguments are consumed. Every argument of a pseudo-class function (like :has(), :is(), :not()) is expected to be a CSSComplexSelector — an object containing a 'simples' array. If any argument is instead a bare string or number (i.e. the function was called with a non-selector argument shape), the structural check fails and this error is raised.

Source

Thrown at packages/isomorphic/cssParser.ts:249

    let s = '';
    let balance = 1;  // First open paren is a part of a function token.
    while (!isEOF()) {
      if (isOpenParen() || isFunction())
        balance++;
      if (isCloseParen())
        balance--;
      if (!balance)
        break;
      s += tokens[pos++].toSource();
    }
    return s;
  }

  const result = consumeFunctionArguments();
  if (!isEOF())
    throw unexpected();
  if (result.some(arg => typeof arg !== 'object' || !('simples' in arg)))
    throw new InvalidSelectorError(`Error while parsing css selector "${selector}". Did you mean to CSS.escape it?`);
  return { selector: result as CSSComplexSelector[], names: Array.from(names) };
}

export function serializeSelector(args: CSSFunctionArgument[]) {
  return args.map(arg => {
    if (typeof arg === 'string')
      return `"${arg}"`;
    if (typeof arg === 'number')
      return String(arg);
    return arg.simples.map(({ selector, combinator }) => {
      let s = selector.css || '';
      s = s + selector.functions.map(func => `:${func.name}(${serializeSelector(func.args)})`).join('');
      if (combinator)
        s += ' ' + combinator;
      return s;
    }).join(' ');
  }).join(', ');
}

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Inspect the selector in the message: each function argument that is a selector must be a valid selector expression, not a literal.
  2. Replace literal arguments with proper sub-selectors, e.g. change :has(foo) so 'foo' is itself a valid selector.
  3. If you need text matching use the dedicated engines: :has-text("..."), :text("..."), :text-is("...").
  4. Run the selector through page.locator() in isolation to reproduce, then simplify until it parses.

Example fix

// before
await page.locator(':has(div)').click(); // missing base + arg shape

// after
await page.locator('div:has(span)').click(); // base 'div' + valid sub-selector 'span'
Defensive patterns

Strategy: try-catch

Validate before calling

function looksLikeValidFunctionArg(sel: string): boolean {
  // crude: pseudo-class function args that are selectors should not be bare literals
  return !/^\s*"|\d+\s*$/.test(sel);
}

Try / catch

try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e)) handleBadSelector(sel); else throw e; }

Prevention

When it happens

Trigger: Using a Playwright custom pseudo-class with a non-selector argument, e.g. :has-text() is fine (string) but :has(123) or :is("str") where a complex selector is required; malformed nested selectors where the closing of an inner complex selector is missing so the parser falls back to treating the arg as a primitive.

Common situations: Hand-writing selectors with custom engines (:not(...), :has(...), :is(...), :where(...)) and putting a literal where a sub-selector goes; refactoring a selector and dropping a nested combinator; using a string where the grammar requires brackets/selector syntax.

Related errors


AI-assisted analysis of microsoft/playwright@c8fc3bf8d3 (2026-08-12). Data as JSON: /api/errors/e456513d2fed950d. Report an issue: GitHub.