diff --git a/plugins/template-function-xml/package.json b/plugins/template-function-xml/package.json
index c54a09279..f476cc0a2 100644
--- a/plugins/template-function-xml/package.json
+++ b/plugins/template-function-xml/package.json
@@ -8,7 +8,8 @@
"types": "src/index.ts",
"scripts": {
"build": "yaakcli build",
- "dev": "yaakcli dev"
+ "dev": "yaakcli dev",
+ "test": "vp test --run tests"
},
"dependencies": {
"@xmldom/xmldom": "^0.9.12",
diff --git a/plugins/template-function-xml/src/index.ts b/plugins/template-function-xml/src/index.ts
index 1d3ec5c99..6bb15b9da 100755
--- a/plugins/template-function-xml/src/index.ts
+++ b/plugins/template-function-xml/src/index.ts
@@ -77,11 +77,19 @@ export function filterXPath(
return String(items);
}
if (!Array.isArray(items) || result === "first") {
- return items[0] != null ? String(items[0].firstChild ?? "") : "";
+ return items[0] != null ? nodeText(items[0]) : "";
}
if (result === "join") {
- return items.map((item) => String(item.firstChild ?? "")).join(join ?? "");
+ return items.map(nodeText).join(join ?? "");
}
// Not sure what cases this happens in (?)
return String(items);
}
+
+function nodeText(node: Node): string {
+ // Attribute, text and CDATA nodes have no children, so their value is on the node itself
+ if (xpath.isAttribute(node) || xpath.isTextNode(node) || xpath.isCDATASection(node)) {
+ return node.nodeValue ?? "";
+ }
+ return String(node.firstChild ?? "");
+}
diff --git a/plugins/template-function-xml/tests/xpath.test.ts b/plugins/template-function-xml/tests/xpath.test.ts
new file mode 100644
index 000000000..8aba8d2a3
--- /dev/null
+++ b/plugins/template-function-xml/tests/xpath.test.ts
@@ -0,0 +1,36 @@
+import type { Context } from "@yaakapp/api";
+import { describe, expect, it } from "vite-plus/test";
+import { filterXPath, plugin } from "../src";
+
+const USERS =
+ 'AliceBob';
+
+describe("xml.xpath", () => {
+ const xpathFunction = plugin.templateFunctions?.find((f) => f.name === "xml.xpath");
+
+ const select = async (query: string, result = "first", join = ", ") =>
+ await xpathFunction?.onRender({} as Context, {
+ values: { input: USERS, query, result, join },
+ purpose: "send",
+ });
+
+ it("returns the text of a selected element", async () => {
+ expect(await select("//user/name")).toBe("Alice");
+ });
+
+ it("returns the value of a selected attribute", async () => {
+ expect(await select("//user/@id")).toBe("42");
+ });
+
+ it("joins the values of selected attributes", async () => {
+ expect(await select("//user/@id", "join")).toBe("42, 7");
+ });
+
+ it("returns the value of a selected text node", async () => {
+ expect(await select("//user/name/text()")).toBe("Alice");
+ });
+
+ it("returns the value of a selected CDATA section", () => {
+ expect(filterXPath("", "/a/text()", "first", null)).toBe("x