Skip to content

Commit 12efbe3

Browse files
committed
test: add comprehensive tests for hx-chunked-mode=swap
Add 6 new tests covering the swap mode functionality: - Default append mode accumulates all chunks - Swap mode only swaps new incremental content - Duplicate progress events are ignored (same length) - Swap mode prevents final htmx:beforeSwap event - Append mode allows final swap (no prevention) - Non-chunked responses always allow final swap Tests verify: - _chunkedMode is set correctly from hx-chunked-mode attribute - _chunkedLastLen tracks response length for incremental extraction - Only new content (response.slice(lastLen)) is swapped in swap mode - Full response is swapped in append mode (default) - beforeSwap event shouldSwap flag is modified correctly All 13 tests pass (7 core + 6 swap mode tests)
1 parent 4670d7e commit 12efbe3

File tree

1 file changed

+202
-0
lines changed

1 file changed

+202
-0
lines changed

index.test.ts

Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -232,4 +232,206 @@ describe("chunked-transfer extension", () => {
232232
expect(target.innerHTML).toBe("<div>transformed</div><p>content</p>");
233233
});
234234
});
235+
236+
describe("hx-chunked-mode=swap", () => {
237+
test("default mode (append) - accumulates all chunks", () => {
238+
const element = document.createElement("div");
239+
// No hx-chunked-mode attribute = defaults to append
240+
241+
const mockXhr = {
242+
getResponseHeader: (header: string) => {
243+
if (header === "Transfer-Encoding") return "chunked";
244+
return null;
245+
},
246+
response: "",
247+
onprogress: null as any,
248+
_chunkedMode: undefined,
249+
_chunkedLastLen: 0,
250+
};
251+
252+
const event = {
253+
target: element,
254+
detail: { xhr: mockXhr },
255+
};
256+
257+
registeredExtension.onEvent("htmx:beforeRequest", event);
258+
259+
// Verify mode was set to append
260+
expect(mockXhr._chunkedMode).toBe("append");
261+
262+
// Simulate progressive chunks - full response each time
263+
mockXhr.response = "<p>Loading...</p>";
264+
mockXhr.onprogress!();
265+
expect(target.innerHTML).toBe("<p>Loading...</p>");
266+
267+
mockXhr.response = "<p>Loading...</p><p>50%</p>";
268+
mockXhr.onprogress!();
269+
expect(target.innerHTML).toBe("<p>Loading...</p><p>50%</p>");
270+
271+
mockXhr.response = "<p>Loading...</p><p>50%</p><p>Done!</p>";
272+
mockXhr.onprogress!();
273+
expect(target.innerHTML).toBe("<p>Loading...</p><p>50%</p><p>Done!</p>");
274+
});
275+
276+
test("swap mode - only swaps new content (incremental)", () => {
277+
const element = document.createElement("div");
278+
element.setAttribute("hx-chunked-mode", "swap");
279+
280+
const mockXhr = {
281+
getResponseHeader: (header: string) => {
282+
if (header === "Transfer-Encoding") return "chunked";
283+
return null;
284+
},
285+
response: "",
286+
onprogress: null as any,
287+
_chunkedMode: undefined,
288+
_chunkedLastLen: 0,
289+
};
290+
291+
const event = {
292+
target: element,
293+
detail: { xhr: mockXhr },
294+
};
295+
296+
registeredExtension.onEvent("htmx:beforeRequest", event);
297+
298+
// Verify mode was set to swap
299+
expect(mockXhr._chunkedMode).toBe("swap");
300+
301+
// First chunk - everything is new
302+
mockXhr.response = "<p>Loading...</p>";
303+
mockXhr.onprogress!();
304+
expect(target.innerHTML).toBe("<p>Loading...</p>");
305+
expect(mockXhr._chunkedLastLen).toBe(mockXhr.response.length);
306+
307+
// Second chunk - only new content swapped
308+
mockXhr.response = "<p>Loading...</p><p>50%</p>";
309+
mockXhr.onprogress!();
310+
expect(target.innerHTML).toBe("<p>50%</p>"); // Only the NEW part
311+
expect(mockXhr._chunkedLastLen).toBe(mockXhr.response.length);
312+
313+
// Third chunk - only newest content swapped
314+
mockXhr.response = "<p>Loading...</p><p>50%</p><p>Done!</p>";
315+
mockXhr.onprogress!();
316+
expect(target.innerHTML).toBe("<p>Done!</p>"); // Only the NEWEST part
317+
expect(mockXhr._chunkedLastLen).toBe(mockXhr.response.length);
318+
});
319+
320+
test("swap mode - ignores duplicate progress events (same length)", () => {
321+
const element = document.createElement("div");
322+
element.setAttribute("hx-chunked-mode", "swap");
323+
324+
const mockXhr = {
325+
getResponseHeader: (header: string) => {
326+
if (header === "Transfer-Encoding") return "chunked";
327+
return null;
328+
},
329+
response: "<p>Content</p>",
330+
onprogress: null as any,
331+
_chunkedMode: undefined,
332+
_chunkedLastLen: 0,
333+
};
334+
335+
const event = {
336+
target: element,
337+
detail: { xhr: mockXhr },
338+
};
339+
340+
registeredExtension.onEvent("htmx:beforeRequest", event);
341+
342+
// First progress event
343+
mockXhr.onprogress!();
344+
expect(target.innerHTML).toBe("<p>Content</p>");
345+
const firstHtml = target.innerHTML;
346+
347+
// Duplicate progress event - same response length
348+
mockXhr.onprogress!();
349+
// Should not swap again (innerHTML unchanged)
350+
expect(target.innerHTML).toBe(firstHtml);
351+
});
352+
353+
test("swap mode - prevents final htmx:beforeSwap", () => {
354+
const element = document.createElement("div");
355+
element.setAttribute("hx-chunked-mode", "swap");
356+
357+
const mockXhr = {
358+
getResponseHeader: (header: string) => {
359+
if (header === "Transfer-Encoding") return "chunked";
360+
return null;
361+
},
362+
response: "<p>Final</p>",
363+
_chunkedMode: "swap",
364+
};
365+
366+
const beforeSwapEvent = {
367+
target: element,
368+
detail: {
369+
xhr: mockXhr,
370+
shouldSwap: true, // Initially true
371+
},
372+
};
373+
374+
// Trigger beforeSwap event
375+
registeredExtension.onEvent("htmx:beforeSwap", beforeSwapEvent);
376+
377+
// Verify shouldSwap was set to false
378+
expect(beforeSwapEvent.detail.shouldSwap).toBe(false);
379+
});
380+
381+
test("append mode - allows final htmx:beforeSwap", () => {
382+
const element = document.createElement("div");
383+
// Default append mode
384+
385+
const mockXhr = {
386+
getResponseHeader: (header: string) => {
387+
if (header === "Transfer-Encoding") return "chunked";
388+
return null;
389+
},
390+
response: "<p>Final</p>",
391+
_chunkedMode: "append",
392+
};
393+
394+
const beforeSwapEvent = {
395+
target: element,
396+
detail: {
397+
xhr: mockXhr,
398+
shouldSwap: true,
399+
},
400+
};
401+
402+
// Trigger beforeSwap event
403+
registeredExtension.onEvent("htmx:beforeSwap", beforeSwapEvent);
404+
405+
// Verify shouldSwap is still true (not prevented)
406+
expect(beforeSwapEvent.detail.shouldSwap).toBe(true);
407+
});
408+
409+
test("swap mode - non-chunked responses allow final swap", () => {
410+
const element = document.createElement("div");
411+
element.setAttribute("hx-chunked-mode", "swap");
412+
413+
const mockXhr = {
414+
getResponseHeader: (header: string) => {
415+
// NOT chunked
416+
return null;
417+
},
418+
response: "<p>Final</p>",
419+
_chunkedMode: "swap",
420+
};
421+
422+
const beforeSwapEvent = {
423+
target: element,
424+
detail: {
425+
xhr: mockXhr,
426+
shouldSwap: true,
427+
},
428+
};
429+
430+
// Trigger beforeSwap event
431+
registeredExtension.onEvent("htmx:beforeSwap", beforeSwapEvent);
432+
433+
// Non-chunked responses should NOT be prevented
434+
expect(beforeSwapEvent.detail.shouldSwap).toBe(true);
435+
});
436+
});
235437
});

0 commit comments

Comments
 (0)