microsoft/playwright · error · InvalidSelectorError

Malformed selector: ${part.name}=${part.body}

Error message

Malformed selector: ${part.name}=${part.body}

What it means

Thrown while parsing the body of a nested selector engine (internal:has, internal:has-not, internal:and, internal:or, internal:chain, left-of, right-of, above, below, near). The body must be a JSON array of the form ["innerSelector"] or ["innerSelector", distance]. This specific throw fires when JSON parsed but the value is not an array, has wrong length (<1 or >2), or its first element is not a string.

Source

Thrown at packages/isomorphic/selectorParser.ts:65

  for (const part of parsedStrings.parts) {
    if (part.name === 'css' || part.name === 'css:light') {
      if (part.name === 'css:light')
        part.body = ':light(' + part.body + ')';
      const parsedCSS = parseCSS(part.body, customCSSNames);
      parts.push({
        name: 'css',
        body: parsedCSS.selector,
        source: part.body
      });
      continue;
    }
    if (kNestedSelectorNames.has(part.name)) {
      let innerSelector: string;
      let distance: number | undefined;
      try {
        const unescaped = JSON.parse('[' + part.body + ']');
        if (!Array.isArray(unescaped) || unescaped.length < 1 || unescaped.length > 2 || typeof unescaped[0] !== 'string')
          throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
        innerSelector = unescaped[0];
        if (unescaped.length === 2) {
          if (typeof unescaped[1] !== 'number' || !kNestedSelectorNamesWithDistance.has(part.name))
            throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
          distance = unescaped[1];
        }
      } catch (e) {
        throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);
      }
      const nested = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };
      const lastFrame = [...nested.body.parsed.parts].reverse().find(part => part.name === 'internal:control' && part.body === 'enter-frame');
      const lastFrameIndex = lastFrame ? nested.body.parsed.parts.indexOf(lastFrame) : -1;
      // Allow nested selectors to start with the same frame selector.
      if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), parts.slice(0, lastFrameIndex + 1)))
        nested.body.parsed.parts.splice(0, lastFrameIndex + 1);
      parts.push(nested);
      continue;
    }

View on GitHub (pinned to c8fc3bf8d3)

Solutions

  1. Use the high-level locator API: locator.has(child), locator.and(other), locator.or(other) — Playwright builds the body correctly.
  2. If authoring raw, wrap the inner selector as a JSON array string: internal:has=["div >> span"].
  3. Validate the body is a JSON array with a string first element before constructing the selector.

Example fix

// before
await page.locator('div >> internal:has=span').click();

// after
await page.locator('div').locator(page.locator('span')).click();
// or first().filter({ has: page.locator('span') })
Defensive patterns

Strategy: try-catch

Validate before calling

function isValidNestedBody(body: string): boolean {
  try {
    const a = JSON.parse('[' + body + ']');
    return Array.isArray(a) && a.length >= 1 && a.length <= 2 && typeof a[0] === 'string';
  } catch { return false; }
}

Try / catch

try { await page.locator(sel).click(); }
catch (e) { if (isInvalidSelectorError(e) && /Malformed selector/.test(e.message)) handleBadNested(sel); else throw e; }

Prevention

When it happens

Trigger: Writing a nested engine selector with a malformed body, e.g. internal:has=div (not a JSON array), internal:has=[123] (first element not a string), internal:has=[] (empty), or internal:has=["a","b","c"] (too many elements).

Common situations: Hand-authoring or string-building nested selectors instead of using locator API (.has(), .and(), .or()); forgetting that the internal serialization wraps the inner selector as a JSON array; migrating from text-based selectors.

Understand the failure class

Related errors


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