mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 11:07:10 +00:00
test(web-runtime): table-test selector builder quoting and escaping
This commit is contained in:
1 parent
e4dddb4ad5
commit
d6afef7d07
1 file changed
+108
@@ -9,6 +9,44 @@ import { test } from "node:test";
|
|||||||
// A 64-bit seed that loses precision as a JS Number must survive as a BigInt.
|
// A 64-bit seed that loses precision as a JS Number must survive as a BigInt.
|
||||||
process.env.SANDERLING_SEED = "9007199254740993";
|
process.env.SANDERLING_SEED = "9007199254740993";
|
||||||
|
|
||||||
|
// cssEscape delegates to the browser's CSS.escape, absent in node. Install the
|
||||||
|
// WHATWG CSSOM escape algorithm so selector-builder tests exercise the real
|
||||||
|
// escaping production relies on, not a stub.
|
||||||
|
if (!(globalThis as { CSS?: unknown }).CSS) {
|
||||||
|
(globalThis as { CSS?: { escape(v: string): string } }).CSS = {
|
||||||
|
escape(value: string): string {
|
||||||
|
let out = "";
|
||||||
|
for (let i = 0; i < value.length; i++) {
|
||||||
|
const c = value.charCodeAt(i);
|
||||||
|
if (c === 0) {
|
||||||
|
out += "�";
|
||||||
|
} else if (
|
||||||
|
(c >= 0x1 && c <= 0x1f) ||
|
||||||
|
c === 0x7f ||
|
||||||
|
(i === 0 && c >= 0x30 && c <= 0x39) ||
|
||||||
|
(i === 1 && c >= 0x30 && c <= 0x39 && value.charCodeAt(0) === 0x2d)
|
||||||
|
) {
|
||||||
|
out += "\\" + c.toString(16) + " ";
|
||||||
|
} else if (i === 0 && c === 0x2d && value.length === 1) {
|
||||||
|
out += "\\" + value[i];
|
||||||
|
} else if (
|
||||||
|
c >= 0x80 ||
|
||||||
|
c === 0x2d ||
|
||||||
|
c === 0x5f ||
|
||||||
|
(c >= 0x30 && c <= 0x39) ||
|
||||||
|
(c >= 0x41 && c <= 0x5a) ||
|
||||||
|
(c >= 0x61 && c <= 0x7a)
|
||||||
|
) {
|
||||||
|
out += value[i];
|
||||||
|
} else {
|
||||||
|
out += "\\" + value[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const { __testing__ } = await import("../src/web-runtime.ts");
|
const { __testing__ } = await import("../src/web-runtime.ts");
|
||||||
const { host } = __testing__;
|
const { host } = __testing__;
|
||||||
|
|
||||||
@@ -231,3 +269,73 @@ test("sanitize preserves arrays and nested plain values", () => {
|
|||||||
const result = sanitizeViaExtract({ items: [1, "two", { ok: true }] });
|
const result = sanitizeViaExtract({ items: [1, "two", { ok: true }] });
|
||||||
assert.deepEqual(result, { items: [1, "two", { ok: true }] });
|
assert.deepEqual(result, { items: [1, "two", { ok: true }] });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// xpathStringLiteral builds XPath 1.0 string literals by hand (no escape
|
||||||
|
// syntax in XPath 1.0). A value carrying a quote that isn't wrapped or
|
||||||
|
// concat()-composed produces a malformed expression, so document.evaluate
|
||||||
|
// throws or, worse, matches the wrong node by truncating at the quote.
|
||||||
|
const { xpathStringLiteral } = __testing__;
|
||||||
|
|
||||||
|
test("xpathStringLiteral table: quote handling stays well-formed", () => {
|
||||||
|
const cases: Array<[string, string]> = [
|
||||||
|
["plain", '"plain"'],
|
||||||
|
['has"double', `'has"double'`],
|
||||||
|
["has'single", `"has'single"`],
|
||||||
|
[`both"and'`, `concat("both", '"', "and'")`],
|
||||||
|
[`"`, `'"'`],
|
||||||
|
];
|
||||||
|
for (const [input, want] of cases) {
|
||||||
|
assert.equal(xpathStringLiteral(input), want, input);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// selectorFromString routes a "kind:value" prefix; text becomes an XPath
|
||||||
|
// equality, everything else a CSS attribute selector. A value containing a
|
||||||
|
// colon must not be re-split, and a quote in a text value must reach the
|
||||||
|
// well-formed XPath literal rather than corrupting the predicate.
|
||||||
|
const { selectorFromString, selectorFromObject } = __testing__;
|
||||||
|
|
||||||
|
test("selectorFromString routes text to a normalize-space XPath", () => {
|
||||||
|
assert.deepEqual(selectorFromString("text:Hello"), {
|
||||||
|
xpath: `//*[normalize-space(text())="Hello"]`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectorFromString keeps colons in the value intact", () => {
|
||||||
|
// Only the first colon splits kind from value; the rest is the value.
|
||||||
|
assert.deepEqual(selectorFromString("text:a:b:c"), {
|
||||||
|
xpath: `//*[normalize-space(text())="a:b:c"]`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectorFromString text value with both quote kinds uses concat", () => {
|
||||||
|
assert.deepEqual(selectorFromString(`text:say "hi" o'clock`), {
|
||||||
|
xpath: `//*[normalize-space(text())=concat("say ", '"', "hi", '"', " o'clock")]`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectorFromObject escapes attribute values to prevent injection", () => {
|
||||||
|
// A quote in an id value, left unescaped, would close the attribute selector
|
||||||
|
// early and match a different element.
|
||||||
|
assert.deepEqual(selectorFromObject({ id: 'a"]' }), {
|
||||||
|
css: `[id="a\\"\\]"]`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectorFromObject maps known keys to their canonical attribute", () => {
|
||||||
|
assert.deepEqual(selectorFromObject({ testID: "submit" }), {
|
||||||
|
css: `[data-testid="submit"]`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectorFromObject falls back to a literal attribute for unknown keys", () => {
|
||||||
|
assert.deepEqual(selectorFromObject({ "data-foo": "bar" }), {
|
||||||
|
css: `[data-foo="bar"]`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("selectorFromObject text-only selector becomes an XPath", () => {
|
||||||
|
assert.deepEqual(selectorFromObject({ text: "Go" }), {
|
||||||
|
xpath: `//*[normalize-space(text())="Go"]`,
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user