fix(chrome): harden cssEscape for control chars + use [class~=]

Previous cssEscape only handled " and \, leaving NUL/newlines/control
chars to break out of the CSS string literal. Port the CSSOM string
serialization rules: NUL becomes U+FFFD, control chars become \HEX,
quotes/backslashes get escaped.

Class selector switched from `.x` (which would need separate identifier
escaping) to `[class~="x"]`, which is also semantically correct for
multi-class elements.
This commit is contained in:
pj committed 2026-05-03 10:58:18 +07:00
1 parent c55925d550
commit d027bf304d
2 files changed
+47 -11

No files matched your search

+26 -1
View File
@@ -10,7 +10,7 @@ func TestTranslateStringSelector_KnownKeys(t *testing.T) {
}{
{"id:email", `[id="email"]`, false},
{"resource-id:account-name", `[id="account-name"]`, false},
{"class:btn-primary", `.btn-primary`, false},
{"class:btn-primary", `[class~="btn-primary"]`, false},
{"tag:button", `button`, false},
{"text:Sign in", `//*[normalize-space(text())="Sign in"]`, true},
{"desc:logout", `[aria-label="logout"]`, false},
@@ -63,3 +63,28 @@ func TestTranslateStringSelector_RejectsMissingPrefix(t *testing.T) {
t.Error("expected error for empty selector")
}
}
func TestCSSEscape_ControlCharactersAndNUL(t *testing.T) {
cases := []struct {
name string
input string
want string
}{
{"plain ascii", "hello", "hello"},
{"double quote", `say "hi"`, `say \"hi\"`},
{"backslash", `a\b`, `a\\b`},
{"newline", "a\nb", `a\A b`},
{"carriage return", "a\rb", `a\D b`},
{"form feed", "a\fb", `a\C b`},
{"NUL replaced", "a\x00b", "a�b"},
{"DEL", "a\x7Fb", `a\7F b`},
{"non-ascii passes through", "café", "café"},
}
for _, testCase := range cases {
got := cssEscape(testCase.input)
if got != testCase.want {
t.Errorf("%s: cssEscape(%q) = %q, want %q",
testCase.name, testCase.input, got, testCase.want)
}
}
}