mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
fix(selectors): emit valid XPath 1.0 string literals via concat()
Both the Go translator and the V8 runtime escaped " by prepending \, which XPath 1.0 doesn't accept (its string literals have no escape syntax). A `text:` value containing a quote produced malformed XPath that chromedp/document.evaluate rejected. Use the standard concat() composition: when the value contains both ' and ", split on " and join with `, '"', ` so each fragment is wrapped in single or double quotes individually.
This commit is contained in:
1 parent
a69bac7abd
commit
372fd23d87
3 files changed
+40
-5
No files matched your search
@@ -37,7 +37,7 @@ func TranslateStringSelector(selector string) (string, bool, error) {
|
||||
case "tag":
|
||||
return cssEscape(value), false, nil
|
||||
case "text":
|
||||
return `//*[normalize-space(text())="` + xpathEscape(value) + `"]`, true, nil
|
||||
return `//*[normalize-space(text())=` + xpathStringLiteral(value) + `]`, true, nil
|
||||
case "desc", "label", "content-desc", "accessibilityLabel", "accessibilityText", "ariaLabel", "aria-label":
|
||||
return `[aria-label="` + cssEscape(value) + `"]`, false, nil
|
||||
case "descPrefix":
|
||||
@@ -82,6 +82,28 @@ func cssEscape(value string) string {
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
func xpathEscape(value string) string {
|
||||
return strings.ReplaceAll(value, `"`, `\"`)
|
||||
// xpathStringLiteral wraps the value in a valid XPath 1.0 string literal.
|
||||
// XPath 1.0 has no escape syntax, so a value containing both ' and " must be
|
||||
// composed via concat(). The output already includes the surrounding quotes
|
||||
// (or concat() call), so callers don't quote it again.
|
||||
func xpathStringLiteral(value string) string {
|
||||
if !strings.ContainsRune(value, '"') {
|
||||
return `"` + value + `"`
|
||||
}
|
||||
if !strings.ContainsRune(value, '\'') {
|
||||
return `'` + value + `'`
|
||||
}
|
||||
parts := strings.Split(value, `"`)
|
||||
var builder strings.Builder
|
||||
builder.WriteString(`concat(`)
|
||||
for index, part := range parts {
|
||||
if index > 0 {
|
||||
builder.WriteString(`, '"', `)
|
||||
}
|
||||
builder.WriteByte('"')
|
||||
builder.WriteString(part)
|
||||
builder.WriteByte('"')
|
||||
}
|
||||
builder.WriteByte(')')
|
||||
return builder.String()
|
||||
}
|
||||
@@ -13,6 +13,9 @@ func TestTranslateStringSelector_KnownKeys(t *testing.T) {
|
||||
{"class:btn-primary", `[class~="btn-primary"]`, false},
|
||||
{"tag:button", `button`, false},
|
||||
{"text:Sign in", `//*[normalize-space(text())="Sign in"]`, true},
|
||||
{`text:Say "hi"`, `//*[normalize-space(text())='Say "hi"']`, true},
|
||||
{`text:it's`, `//*[normalize-space(text())="it's"]`, true},
|
||||
{`text:it's "fine"`, `//*[normalize-space(text())=concat("it's ", '"', "fine", '"', "")]`, true},
|
||||
{"desc:logout", `[aria-label="logout"]`, false},
|
||||
{"label:logout", `[aria-label="logout"]`, false},
|
||||
{"accessibilityLabel:logout", `[aria-label="logout"]`, false},
|
||||
|
||||
@@ -111,12 +111,22 @@ function selectorFromObject(selector: Record<string, string | boolean | undefine
|
||||
}
|
||||
if (textValue !== undefined && parts.length === 0) {
|
||||
return {
|
||||
xpath: `//*[normalize-space(text())="${textValue.replace(/"/g, '\\"')}"]`,
|
||||
xpath: `//*[normalize-space(text())=${xpathStringLiteral(textValue)}]`,
|
||||
};
|
||||
}
|
||||
return { css: parts.join("") };
|
||||
}
|
||||
|
||||
// xpathStringLiteral wraps the value in a valid XPath 1.0 string literal.
|
||||
// XPath 1.0 has no escape syntax, so a value containing both ' and " must be
|
||||
// composed via concat().
|
||||
function xpathStringLiteral(value: string): string {
|
||||
if (!value.includes('"')) return `"${value}"`;
|
||||
if (!value.includes("'")) return `'${value}'`;
|
||||
const parts = value.split('"');
|
||||
return `concat(${parts.map((p) => `"${p}"`).join(`, '"', `)})`;
|
||||
}
|
||||
|
||||
function selectorFromString(selector: string): { css?: string; xpath?: string } {
|
||||
const colon = selector.indexOf(":");
|
||||
if (colon <= 0) {
|
||||
@@ -125,7 +135,7 @@ function selectorFromString(selector: string): { css?: string; xpath?: string }
|
||||
const kind = selector.slice(0, colon);
|
||||
const value = selector.slice(colon + 1);
|
||||
if (kind === "text") {
|
||||
return { xpath: `//*[normalize-space(text())="${value.replace(/"/g, '\\"')}"]` };
|
||||
return { xpath: `//*[normalize-space(text())=${xpathStringLiteral(value)}]` };
|
||||
}
|
||||
if (kind === "descPrefix") {
|
||||
return { css: `[aria-label^="${cssEscape(value)}"]` };
|
||||
|
||||
Reference in new issue
Block a user