Devtools' "Copy → Copy selector", crash-reporting tools, and test recorders all do the same thing: given an element, produce a CSS selector string that points back to it uniquely. The trick is walking from the element up to a root, emitting one segment per level — an #id when there is one (it anchors the whole path), otherwise tag:nth-of-type(n) to distinguish it from same-tag siblings — and joining them into a ancestor > … > element path.
Implement generateCssSelector(element, root). Build a selector such that root.querySelector(result) === element. Use #id (and stop) when a node has an id; otherwise tag:nth-of-type(n).
function generateCssSelector(element, root) {
// returns a selector string; root.querySelector(it) must be `element`
}
// <div><section id=main><p><b>x</b></p></section></div>
generateCssSelector(b, root); // '#main > p:nth-of-type(1) > b:nth-of-type(1)'
// <ul><li/><li/><li/></ul> (the 3rd li)
generateCssSelector(li3, root); // 'ul:nth-of-type(1) > li:nth-of-type(3)'
element up to (but not including) root, prepending each segment.#id anchors — an id uniquely identifies a node, so emit #id and stop climbing.:nth-of-type(n) — for id-less nodes, n is the 1-based position among same-tag siblings (count previousElementSiblings with the same tagName).> — the child combinator makes each step a direct-parent relationship, keeping the path exact. The result must round-trip via querySelector.