Skip to content

Commit f36b300

Browse files
committed
test: add comprehensive tests for comment filtering
Add 7 new tests covering comment filtering functionality: - Ignores single-line comment-only chunks (heartbeats) - Ignores multi-line comment-only chunks - Ignores multiple comments with only whitespace - Processes chunks with comments and HTML content - Processes chunks with comments between HTML elements - Processes empty chunks (no filtering) - Processes whitespace-only chunks (passes through) Also enable CI on pull_request events. All 20 tests pass
1 parent fdc577d commit f36b300

2 files changed

Lines changed: 165 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ name: CI
22

33
on:
44
push:
5+
pull_request:
56

67
jobs:
78
build:

index.test.ts

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,170 @@ describe("chunked-transfer extension", () => {
233233
});
234234
});
235235

236+
describe("comment filtering (heartbeats)", () => {
237+
test("ignores comment-only chunks", () => {
238+
const element = document.createElement("div");
239+
const mockXhr = {
240+
getResponseHeader: (header: string) => {
241+
if (header === "Transfer-Encoding") return "chunked";
242+
return null;
243+
},
244+
response: "<!-- heartbeat -->",
245+
onprogress: null as any,
246+
};
247+
248+
const event = {
249+
target: element,
250+
detail: { xhr: mockXhr },
251+
};
252+
253+
registeredExtension.onEvent("htmx:beforeRequest", event);
254+
mockXhr.onprogress!();
255+
256+
// Target should remain empty (comment-only chunk ignored)
257+
expect(target.innerHTML).toBe("");
258+
});
259+
260+
test("ignores multi-line comment-only chunks", () => {
261+
const element = document.createElement("div");
262+
const mockXhr = {
263+
getResponseHeader: (header: string) => {
264+
if (header === "Transfer-Encoding") return "chunked";
265+
return null;
266+
},
267+
response: "<!-- \n heartbeat\n still processing\n -->",
268+
onprogress: null as any,
269+
};
270+
271+
const event = {
272+
target: element,
273+
detail: { xhr: mockXhr },
274+
};
275+
276+
registeredExtension.onEvent("htmx:beforeRequest", event);
277+
mockXhr.onprogress!();
278+
279+
// Multi-line comment should also be ignored
280+
expect(target.innerHTML).toBe("");
281+
});
282+
283+
test("ignores multiple comments without content", () => {
284+
const element = document.createElement("div");
285+
const mockXhr = {
286+
getResponseHeader: (header: string) => {
287+
if (header === "Transfer-Encoding") return "chunked";
288+
return null;
289+
},
290+
response: "<!-- comment 1 --><!-- comment 2 --> ",
291+
onprogress: null as any,
292+
};
293+
294+
const event = {
295+
target: element,
296+
detail: { xhr: mockXhr },
297+
};
298+
299+
registeredExtension.onEvent("htmx:beforeRequest", event);
300+
mockXhr.onprogress!();
301+
302+
// Multiple comments with only whitespace should be ignored
303+
expect(target.innerHTML).toBe("");
304+
});
305+
306+
test("processes chunks with comments and HTML content", () => {
307+
const element = document.createElement("div");
308+
const mockXhr = {
309+
getResponseHeader: (header: string) => {
310+
if (header === "Transfer-Encoding") return "chunked";
311+
return null;
312+
},
313+
response: "<!-- debug info --><p>Content</p>",
314+
onprogress: null as any,
315+
};
316+
317+
const event = {
318+
target: element,
319+
detail: { xhr: mockXhr },
320+
};
321+
322+
registeredExtension.onEvent("htmx:beforeRequest", event);
323+
mockXhr.onprogress!();
324+
325+
// Should process the chunk (has HTML beyond comments)
326+
expect(target.innerHTML).toBe("<!-- debug info --><p>Content</p>");
327+
});
328+
329+
test("processes chunks with comments between HTML", () => {
330+
const element = document.createElement("div");
331+
const mockXhr = {
332+
getResponseHeader: (header: string) => {
333+
if (header === "Transfer-Encoding") return "chunked";
334+
return null;
335+
},
336+
response: "<p>Start</p><!-- middle --><p>End</p>",
337+
onprogress: null as any,
338+
};
339+
340+
const event = {
341+
target: element,
342+
detail: { xhr: mockXhr },
343+
};
344+
345+
registeredExtension.onEvent("htmx:beforeRequest", event);
346+
mockXhr.onprogress!();
347+
348+
// Should process the entire chunk
349+
expect(target.innerHTML).toBe("<p>Start</p><!-- middle --><p>End</p>");
350+
});
351+
352+
test("processes empty chunks (no comments)", () => {
353+
const element = document.createElement("div");
354+
const mockXhr = {
355+
getResponseHeader: (header: string) => {
356+
if (header === "Transfer-Encoding") return "chunked";
357+
return null;
358+
},
359+
response: "",
360+
onprogress: null as any,
361+
};
362+
363+
const event = {
364+
target: element,
365+
detail: { xhr: mockXhr },
366+
};
367+
368+
registeredExtension.onEvent("htmx:beforeRequest", event);
369+
mockXhr.onprogress!();
370+
371+
// Empty chunks pass through (no content added, but swap is called)
372+
expect(target.innerHTML).toBe("");
373+
});
374+
375+
test("processes whitespace-only chunks (no comments)", () => {
376+
const element = document.createElement("div");
377+
const mockXhr = {
378+
getResponseHeader: (header: string) => {
379+
if (header === "Transfer-Encoding") return "chunked";
380+
return null;
381+
},
382+
response: " \n \t ",
383+
onprogress: null as any,
384+
};
385+
386+
const event = {
387+
target: element,
388+
detail: { xhr: mockXhr },
389+
};
390+
391+
registeredExtension.onEvent("htmx:beforeRequest", event);
392+
mockXhr.onprogress!();
393+
394+
// Whitespace-only chunks (no comments) pass through and get trimmed by DOM
395+
// The whitespace gets normalized to empty when set as innerHTML
396+
expect(target.innerHTML.trim()).toBe("");
397+
});
398+
});
399+
236400
describe("hx-chunked-mode=swap", () => {
237401
test("default mode (append) - accumulates all chunks", () => {
238402
const element = document.createElement("div");

0 commit comments

Comments
 (0)