{"record":{"id":"acb31ee521b19bff","repo":"microsoft/playwright","slug":"malformed-selector-part-name-part-body","errorCode":null,"errorMessage":"Malformed selector: ${part.name}=${part.body}","messagePattern":"Malformed selector: (.+?)=(.+?)","errorType":"exception","errorClass":"InvalidSelectorError","httpStatus":null,"severity":"error","filePath":"packages/isomorphic/selectorParser.ts","lineNumber":65,"sourceCode":"  for (const part of parsedStrings.parts) {\n    if (part.name === 'css' || part.name === 'css:light') {\n      if (part.name === 'css:light')\n        part.body = ':light(' + part.body + ')';\n      const parsedCSS = parseCSS(part.body, customCSSNames);\n      parts.push({\n        name: 'css',\n        body: parsedCSS.selector,\n        source: part.body\n      });\n      continue;\n    }\n    if (kNestedSelectorNames.has(part.name)) {\n      let innerSelector: string;\n      let distance: number | undefined;\n      try {\n        const unescaped = JSON.parse('[' + part.body + ']');\n        if (!Array.isArray(unescaped) || unescaped.length < 1 || unescaped.length > 2 || typeof unescaped[0] !== 'string')\n          throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);\n        innerSelector = unescaped[0];\n        if (unescaped.length === 2) {\n          if (typeof unescaped[1] !== 'number' || !kNestedSelectorNamesWithDistance.has(part.name))\n            throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);\n          distance = unescaped[1];\n        }\n      } catch (e) {\n        throw new InvalidSelectorError(`Malformed selector: ${part.name}=` + part.body);\n      }\n      const nested = { name: part.name, source: part.body, body: { parsed: parseSelector(innerSelector), distance } };\n      const lastFrame = [...nested.body.parsed.parts].reverse().find(part => part.name === 'internal:control' && part.body === 'enter-frame');\n      const lastFrameIndex = lastFrame ? nested.body.parsed.parts.indexOf(lastFrame) : -1;\n      // Allow nested selectors to start with the same frame selector.\n      if (lastFrameIndex !== -1 && selectorPartsEqual(nested.body.parsed.parts.slice(0, lastFrameIndex + 1), parts.slice(0, lastFrameIndex + 1)))\n        nested.body.parsed.parts.splice(0, lastFrameIndex + 1);\n      parts.push(nested);\n      continue;\n    }","sourceCodeStart":47,"sourceCodeEnd":83,"githubUrl":"https://github.com/microsoft/playwright/blob/c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6/packages/isomorphic/selectorParser.ts#L47-L83","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Use the high-level locator API: locator.has(child), locator.and(other), locator.or(other) — Playwright builds the body correctly.","If authoring raw, wrap the inner selector as a JSON array string: internal:has=[\"div >> span\"].","Validate the body is a JSON array with a string first element before constructing the selector."],"exampleFix":"// before\nawait page.locator('div >> internal:has=span').click();\n\n// after\nawait page.locator('div').locator(page.locator('span')).click();\n// or first().filter({ has: page.locator('span') })","handlingStrategy":"try-catch","validationCode":"function isValidNestedBody(body: string): boolean {\n  try {\n    const a = JSON.parse('[' + body + ']');\n    return Array.isArray(a) && a.length >= 1 && a.length <= 2 && typeof a[0] === 'string';\n  } catch { return false; }\n}","typeGuard":null,"tryCatchPattern":"try { await page.locator(sel).click(); }\ncatch (e) { if (isInvalidSelectorError(e) && /Malformed selector/.test(e.message)) handleBadNested(sel); else throw e; }","preventionTips":["Use locator.has()/filter()/and()/or() instead of hand-writing nested engine bodies.","When authoring raw, always JSON.stringify([innerSelector]).","Validate nested bodies parse as ['string'] before sending."],"tags":["selector","nested-selector","parsing","invalid-selector"],"backgroundTag":null,"analyzedSha":"c8fc3bf8d31542d59b4d4d9eaab1df93ff541dc6","analyzedAt":"2026-08-12T07:26:36.950Z","schemaVersion":2},"datasetVersion":"2026-08-12T13:17:24.610Z"}