import { describe, it, expect } from "bun:test";
import { gcTick } from "harness";
var setTimeoutAsync = (fn, delay) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
try {
resolve(fn());
} catch (e) {
reject(e);
}
}, delay);
});
};
describe("HTMLRewriter", () => {
it("HTMLRewriter: async replacement", async () => {
await gcTick();
const res = new HTMLRewriter()
.on("div", {
async element(element) {
await setTimeoutAsync(() => {
element.setInnerContent("replace", { html: true });
}, 5);
},
})
.transform(new Response("
example.com
"));
await gcTick();
expect(await res.text()).toBe("replace
");
await gcTick();
});
it("supports element handlers", async () => {
var rewriter = new HTMLRewriter();
rewriter.on("div", {
element(element) {
element.setInnerContent("", { html: true });
},
});
var input = new Response("hello
");
var output = rewriter.transform(input);
expect(await output.text()).toBe("");
});
it("(from file) supports element handlers", async () => {
var rewriter = new HTMLRewriter();
rewriter.on("div", {
element(element) {
element.setInnerContent("", { html: true });
},
});
await Bun.write("/tmp/html-rewriter.txt.js", "hello
");
var input = new Response(Bun.file("/tmp/html-rewriter.txt.js"));
var output = rewriter.transform(input);
expect(await output.text()).toBe("");
});
it("supports attribute iterator", async () => {
var rewriter = new HTMLRewriter();
var expected = [
["first", ""],
["second", "alrihgt"],
["third", "123"],
["fourth", "5"],
["fifth", "helloooo"],
];
rewriter.on("div", {
element(element2) {
for (let attr of element2.attributes) {
const stack = expected.shift();
expect(stack[0]).toBe(attr[0]);
expect(stack[1]).toBe(attr[1]);
}
},
});
var input = new Response('hello
');
var output = rewriter.transform(input);
expect(await output.text()).toBe('hello
');
expect(expected.length).toBe(0);
});
it("handles element specific mutations", async () => {
// prepend/append
let res = new HTMLRewriter()
.on("p", {
element(element) {
element.prepend("prepend");
element.prepend("prepend html", { html: true });
element.append("append");
element.append("append html", { html: true });
},
})
.transform(new Response("test
"));
expect(await res.text()).toBe(
[
"",
"prepend html",
"<span>prepend</span>",
"test",
"<span>append</span>",
"append html",
"
",
].join(""),
);
// setInnerContent
res = new HTMLRewriter()
.on("p", {
element(element) {
element.setInnerContent("replace");
},
})
.transform(new Response("test
"));
expect(await res.text()).toBe("<span>replace</span>
");
res = new HTMLRewriter()
.on("p", {
element(element) {
element.setInnerContent("replace", { html: true });
},
})
.transform(new Response("test
"));
expect(await res.text()).toBe("replace
");
// removeAndKeepContent
res = new HTMLRewriter()
.on("p", {
element(element) {
element.removeAndKeepContent();
},
})
.transform(new Response("test
"));
expect(await res.text()).toBe("test");
});
it("handles element class properties", async () => {
class Handler {
constructor(content) {
this.content = content;
}
// noinspection JSUnusedGlobalSymbols
element(element) {
element.setInnerContent(this.content);
}
}
const res = new HTMLRewriter().on("p", new Handler("new")).transform(new Response("test
"));
expect(await res.text()).toBe("new
");
});
const commentsMutationsInput = "";
const commentsMutationsExpected = {
beforeAfter: [
"",
"<span>before</span>",
"before html",
"",
"after html",
"<span>after</span>",
"
",
].join(""),
replace: "<span>replace</span>
",
replaceHtml: "replace
",
remove: "",
};
const commentPropertiesMacro = async func => {
const res = func(new HTMLRewriter(), comment => {
expect(comment.removed).toBe(false);
expect(comment.text).toBe("test");
comment.text = "new";
expect(comment.text).toBe("new");
}).transform(new Response(""));
expect(await res.text()).toBe("");
};
it("HTMLRewriter: handles comment properties", () =>
commentPropertiesMacro((rw, comments) => {
rw.on("p", { comments });
return rw;
}));
it("selector tests", async () => {
const checkSelector = async (selector, input, expected) => {
const res = new HTMLRewriter()
.on(selector, {
element(element) {
element.setInnerContent("new");
},
})
.transform(new Response(input));
expect(await res.text()).toBe(expected);
};
await checkSelector("*", "1
2
", "new
new
");
await checkSelector("p", "1
2
", "1
new
");
await checkSelector(
"p:nth-child(2)",
"",
"",
);
await checkSelector(
"p:first-child",
"",
"",
);
await checkSelector(
"p:nth-of-type(2)",
"",
"",
);
await checkSelector(
"p:first-of-type",
"",
"",
);
await checkSelector(
"p:not(:first-child)",
"",
"",
);
await checkSelector("p.red", '1
2
', 'new
2
');
await checkSelector("h1#header", '2
', '2
');
await checkSelector("p[data-test]", "1
2
", "new
2
");
await checkSelector(
'p[data-test="one"]',
'1
2
',
'new
2
',
);
await checkSelector(
'p[data-test="one" i]',
'1
2
3
',
'new
new
3
',
);
await checkSelector(
'p[data-test="one" s]',
'1
2
3
',
'new
2
3
',
);
await checkSelector(
'p[data-test~="two"]',
'1
2
3
',
'new
new
3
',
);
await checkSelector(
'p[data-test^="a"]',
'1
2
3
',
'new
new
3
',
);
await checkSelector(
'p[data-test$="1"]',
'1
2
3
',
'new
2
new
',
);
await checkSelector(
'p[data-test*="b"]',
'1
2
3
',
'new
new
3
',
);
await checkSelector(
'p[data-test|="a"]',
'1
2
3
',
'new
new
3
',
);
await checkSelector(
"div span",
"1
23",
"new
new3",
);
await checkSelector(
"div > span",
"1
23",
"1
new3",
);
});
it("supports deleting innerContent", async () => {
expect(
await new HTMLRewriter()
.on("div", {
element(elem) {
// https://github.com/oven-sh/bun/issues/2323
elem.setInnerContent("");
},
})
.transform(new Response("content
"))
.text(),
).toEqual("");
});
it("supports deleting innerHTML", async () => {
expect(
await new HTMLRewriter()
.on("div", {
element(elem) {
// https://github.com/oven-sh/bun/issues/2323
elem.setInnerContent("", { html: true });
},
})
.transform(new Response("content
"))
.text(),
).toEqual("");
});
it("it supports lastInTextNode", async () => {
let lastInTextNode;
await new HTMLRewriter()
.on("p", {
text(text) {
lastInTextNode ??= text.lastInTextNode;
},
})
.transform(new Response("Lorem ipsum!
"))
.text();
expect(lastInTextNode).toBeBoolean();
});
it("it supports selfClosing", async () => {
const selfClosing = {};
await new HTMLRewriter()
.on("*", {
element(el) {
selfClosing[el.tagName] = el.selfClosing;
},
})
.transform(new Response("Lorem ipsum!
"))
.text();
expect(selfClosing).toEqual({
p: false,
br: false,
div: true,
});
});
it("it supports canHaveContent", async () => {
const canHaveContent = {};
await new HTMLRewriter()
.on("*", {
element(el) {
canHaveContent[el.tagName] = el.canHaveContent;
},
})
.transform(new Response("Lorem ipsum!
"))
.text();
expect(canHaveContent).toEqual({
p: true,
br: false,
div: true,
svg: true,
circle: false,
});
});
});
// By not segfaulting, this test passes
it("#3334 regression", async () => {
for (let i = 0; i < 10; i++) {
const headers = new Headers({
"content-type": "text/html",
});
const response = new Response("content
", { headers });
const result = await new HTMLRewriter()
.on("div", {
element(elem) {
elem.setInnerContent("new");
},
})
.transform(response)
.text();
expect(result).toEqual("new
");
}
Bun.gc(true);
});
it("#3489", async () => {
var el;
await new HTMLRewriter()
.on("p", {
element(element) {
el = element.getAttribute("id");
},
})
.transform(new Response(''))
.text();
expect(el).toEqual("Šžõäöü");
});
it("get attribute - ascii", async () => {
for (let i = 0; i < 10; i++) {
var el;
await new HTMLRewriter()
.on("p", {
element(element) {
el = element.getAttribute("id");
},
})
.transform(new Response(``))
.text();
expect(el).toEqual("asciii");
}
});
it("#3520", async () => {
const pairs = [];
await new HTMLRewriter()
.on("p", {
element(element) {
for (const pair of element.attributes) {
pairs.push(pair);
}
},
})
.transform(new Response(''))
.text();
expect(pairs).toEqual([
["šž", "Õäöü"],
["ab", "Õäöü"],
["šž", "Õäöü"],
["šž", "dc"],
["šž", "🕵🏻"],
]);
});