From 7e417219df9dee082ac6e6d5cfc7751b66d5a64a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Fri, 29 May 2026 13:21:58 +0800 Subject: [PATCH] feat(desktop): add selector/nth-path builders for element picker Co-Authored-By: Claude Sonnet 4.6 --- desktop/src/preview-agent/selector.test.ts | 22 +++++++++++++++ desktop/src/preview-agent/selector.ts | 31 ++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 desktop/src/preview-agent/selector.test.ts create mode 100644 desktop/src/preview-agent/selector.ts diff --git a/desktop/src/preview-agent/selector.test.ts b/desktop/src/preview-agent/selector.test.ts new file mode 100644 index 00000000..ef26d54b --- /dev/null +++ b/desktop/src/preview-agent/selector.test.ts @@ -0,0 +1,22 @@ +import { describe, expect, it, beforeEach } from 'vitest' +import { buildSelector, buildNthPath } from './selector' + +beforeEach(() => { document.body.innerHTML = ` +

A

x

y

` }) + +describe('buildSelector', () => { + it('uses id when present', () => { + expect(buildSelector(document.getElementById('title')!)).toBe('#title') + }) + it('uses nth-of-type for ambiguous siblings, stopping at nearest id', () => { + const secondP = document.querySelectorAll('p')[1]! + expect(buildSelector(secondP)).toBe('main > section > p:nth-of-type(2)') + }) +}) + +describe('buildNthPath', () => { + it('builds a child-index path from root', () => { + const secondP = document.querySelectorAll('p')[1]! + expect(buildNthPath(secondP)).toMatch(/p:nth-child\(3\)$/) + }) +}) diff --git a/desktop/src/preview-agent/selector.ts b/desktop/src/preview-agent/selector.ts new file mode 100644 index 00000000..73107dce --- /dev/null +++ b/desktop/src/preview-agent/selector.ts @@ -0,0 +1,31 @@ +function cssTag(el: Element): string { + const tag = el.tagName.toLowerCase() + const parent = el.parentElement + if (!parent) return tag + const same = Array.from(parent.children).filter((c) => c.tagName === el.tagName) + if (same.length <= 1) return tag + return `${tag}:nth-of-type(${same.indexOf(el) + 1})` +} + +export function buildSelector(el: Element): string { + if (el.id) return `#${el.id}` + const parts: string[] = [] + let node: Element | null = el + while (node && node.nodeType === 1 && node.tagName.toLowerCase() !== 'html' && node.tagName.toLowerCase() !== 'body') { + if (node.id) { parts.unshift(`#${node.id}`); break } + parts.unshift(cssTag(node)) + node = node.parentElement + } + return parts.join(' > ') +} + +export function buildNthPath(el: Element): string { + const parts: string[] = [] + let node: Element | null = el + while (node && node.parentElement) { + const idx = Array.from(node.parentElement.children).indexOf(node) + 1 + parts.unshift(`${node.tagName.toLowerCase()}:nth-child(${idx})`) + node = node.parentElement + } + return parts.join(' > ') +}