Skip to content

Commit 067f5d6

Browse files
Feat/gh helpers (#1)
* feat: add new helpers for string manipulation and CTRF formatting * refactor: update module resolution and remove esm compatibility script * chore: update duration formatting logic
1 parent f3063eb commit 067f5d6

33 files changed

+2255
-121
lines changed

README.md

Lines changed: 379 additions & 0 deletions
Large diffs are not rendered by default.

__tests__/helpers/array.test.ts

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
mapHelper,
1818
pluckHelper,
1919
reverseHelper,
20+
sliceHelper,
2021
someHelper,
2122
sortByHelper,
2223
sortHelper,
@@ -187,6 +188,74 @@ describe("lengthEqualHelper", () => {
187188
});
188189
});
189190

191+
describe("sliceHelper", () => {
192+
it("returns slice of array as regular helper", () => {
193+
const array = ["a", "b", "c", "d", "e"];
194+
expect(sliceHelper.fn(array, 1, 4)).toEqual(["b", "c", "d"]);
195+
});
196+
197+
it("works as block helper with Handlebars context", () => {
198+
const array = ["item1", "item2", "item3", "item4"];
199+
const mockOptions = {
200+
fn: (item: string) => `<li>${item}</li>`,
201+
};
202+
const result = sliceHelper.fn(array, 1, 3, mockOptions);
203+
expect(result).toBe("<li>item2</li><li>item3</li>");
204+
});
205+
206+
it("handles string indices", () => {
207+
const array = [1, 2, 3, 4, 5];
208+
expect(sliceHelper.fn(array, "1", "4")).toEqual([2, 3, 4]);
209+
});
210+
211+
it("uses defaults for invalid indices", () => {
212+
const array = ["a", "b", "c"];
213+
expect(sliceHelper.fn(array, "invalid", "also_invalid")).toEqual(["a", "b", "c"]);
214+
expect(sliceHelper.fn(array, 0, "invalid")).toEqual(["a", "b", "c"]);
215+
});
216+
217+
it("returns empty string for non-array input", () => {
218+
expect(sliceHelper.fn(null, 0, 2)).toBe("");
219+
expect(sliceHelper.fn("not an array", 0, 2)).toBe("");
220+
});
221+
222+
it("handles edge cases with indices", () => {
223+
const array = ["a", "b", "c"];
224+
// Start index beyond array length
225+
expect(sliceHelper.fn(array, 5, 10)).toEqual([]);
226+
// Negative start index with positive end (should return empty like native slice)
227+
expect(sliceHelper.fn(array, -1, 2)).toEqual([]);
228+
// Negative start index without end (should return last item)
229+
expect(sliceHelper.fn(array, -1)).toEqual(["c"]);
230+
// Negative indices both (should work like native slice)
231+
expect(sliceHelper.fn(array, -2, -1)).toEqual(["b"]);
232+
// End index beyond array length
233+
expect(sliceHelper.fn(array, 0, 10)).toEqual(["a", "b", "c"]);
234+
});
235+
236+
it("works for pagination scenarios", () => {
237+
const testResults = [
238+
{ name: "test1" }, { name: "test2" }, { name: "test3" },
239+
{ name: "test4" }, { name: "test5" }, { name: "test6" }
240+
];
241+
242+
// Page 1: items 0-2
243+
expect(sliceHelper.fn(testResults, 0, 3)).toEqual([
244+
{ name: "test1" }, { name: "test2" }, { name: "test3" }
245+
]);
246+
247+
// Page 2: items 3-5
248+
expect(sliceHelper.fn(testResults, 3, 6)).toEqual([
249+
{ name: "test4" }, { name: "test5" }, { name: "test6" }
250+
]);
251+
});
252+
253+
it("should be categorized as Array helper", () => {
254+
expect(sliceHelper.category).toBe("Array");
255+
expect(sliceHelper.name).toBe("slice");
256+
});
257+
});
258+
190259
describe("mapHelper", () => {
191260
it("maps array with function", () => {
192261
const double = (x: number) => x * 2;

__tests__/helpers/ctrf.test.ts

Lines changed: 269 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import {
77
filterPassedTestsHelper,
88
formatDurationFromTimesHelper,
99
formatDurationHelper,
10+
formatTestMessageHelper,
11+
formatTestMessagePreCodeHelper,
12+
getCtrfEmojiHelper,
13+
limitFailedTestsHelper,
1014
sortTestsByFailRateHelper,
1115
sortTestsByFlakyRateHelper,
1216
} from "../../src/helpers/ctrf";
@@ -174,6 +178,128 @@ describe("CTRF Helpers", () => {
174178
});
175179
});
176180

181+
describe("limitFailedTestsHelper", () => {
182+
it("should filter failed tests and limit to specified number", () => {
183+
const result = limitFailedTestsHelper.fn(mockTests, 1);
184+
185+
if (Array.isArray(result)) {
186+
expect(result).toHaveLength(1);
187+
expect(result[0].status).toBe("failed");
188+
expect(result[0].name).toBe("Failing Test 1");
189+
}
190+
});
191+
192+
it("should return all failed tests when limit is greater than failed count", () => {
193+
const result = limitFailedTestsHelper.fn(mockTests, 10);
194+
195+
if (Array.isArray(result)) {
196+
expect(result).toHaveLength(2); // Only 2 failed tests in mockTests
197+
expect(result.every((test: Test) => test.status === "failed")).toBe(true);
198+
expect(result[0].name).toBe("Failing Test 1");
199+
expect(result[1].name).toBe("Failing Test 2");
200+
}
201+
});
202+
203+
it("should handle zero limit", () => {
204+
const result = limitFailedTestsHelper.fn(mockTests, 0);
205+
expect(result).toHaveLength(0);
206+
});
207+
208+
it("should handle negative limit by defaulting to 10", () => {
209+
const result = limitFailedTestsHelper.fn(mockTests, -5);
210+
211+
if (Array.isArray(result)) {
212+
expect(result).toHaveLength(2); // All 2 failed tests, since -5 defaults to 10
213+
expect(result.every((test: Test) => test.status === "failed")).toBe(true);
214+
}
215+
});
216+
217+
it("should handle string limit by parsing to number", () => {
218+
const result = limitFailedTestsHelper.fn(mockTests, "1");
219+
220+
if (Array.isArray(result)) {
221+
expect(result).toHaveLength(1);
222+
expect(result[0].status).toBe("failed");
223+
expect(result[0].name).toBe("Failing Test 1");
224+
}
225+
});
226+
227+
it("should handle invalid string limit by defaulting to 10", () => {
228+
const result = limitFailedTestsHelper.fn(mockTests, "invalid");
229+
230+
if (Array.isArray(result)) {
231+
expect(result).toHaveLength(2); // All 2 failed tests, since "invalid" defaults to 10
232+
expect(result.every((test: Test) => test.status === "failed")).toBe(true);
233+
}
234+
});
235+
236+
it("should handle undefined limit by defaulting to 10", () => {
237+
const result = limitFailedTestsHelper.fn(mockTests, undefined);
238+
239+
if (Array.isArray(result)) {
240+
expect(result).toHaveLength(2); // All 2 failed tests, since undefined defaults to 10
241+
expect(result.every((test: Test) => test.status === "failed")).toBe(true);
242+
}
243+
});
244+
245+
it("should return empty array when no failed tests", () => {
246+
const passedOnlyTests: Test[] = [
247+
{ name: "Passed Test", status: "passed", duration: 1000 },
248+
{ name: "Skipped Test", status: "skipped", duration: 0 },
249+
];
250+
251+
const result = limitFailedTestsHelper.fn(passedOnlyTests, 5);
252+
expect(result).toHaveLength(0);
253+
});
254+
255+
it("should return empty array for non-array input", () => {
256+
expect(limitFailedTestsHelper.fn(null, 5)).toEqual([]);
257+
expect(limitFailedTestsHelper.fn(undefined, 5)).toEqual([]);
258+
expect(limitFailedTestsHelper.fn("not-an-array", 5)).toEqual([]);
259+
expect(limitFailedTestsHelper.fn({}, 5)).toEqual([]);
260+
});
261+
262+
it("should handle edge case with many failed tests", () => {
263+
const manyFailedTests: Test[] = Array.from({ length: 20 }, (_, i) => ({
264+
name: `Failed Test ${i + 1}`,
265+
status: "failed" as const,
266+
duration: 1000,
267+
}));
268+
269+
const result = limitFailedTestsHelper.fn(manyFailedTests, 3);
270+
271+
if (Array.isArray(result)) {
272+
expect(result).toHaveLength(3);
273+
expect(result[0].name).toBe("Failed Test 1");
274+
expect(result[1].name).toBe("Failed Test 2");
275+
expect(result[2].name).toBe("Failed Test 3");
276+
expect(result.every((test: Test) => test.status === "failed")).toBe(true);
277+
}
278+
});
279+
280+
it("should preserve order of failed tests", () => {
281+
const orderedTests: Test[] = [
282+
{ name: "First Failed", status: "failed", duration: 100 },
283+
{ name: "Passed Test", status: "passed", duration: 200 },
284+
{ name: "Second Failed", status: "failed", duration: 300 },
285+
{ name: "Third Failed", status: "failed", duration: 400 },
286+
];
287+
288+
const result = limitFailedTestsHelper.fn(orderedTests, 2);
289+
290+
if (Array.isArray(result)) {
291+
expect(result).toHaveLength(2);
292+
expect(result[0].name).toBe("First Failed");
293+
expect(result[1].name).toBe("Second Failed");
294+
}
295+
});
296+
297+
it("should be categorized as CTRF helper", () => {
298+
expect(limitFailedTestsHelper.category).toBe("CTRF");
299+
expect(limitFailedTestsHelper.name).toBe("limitFailedTests");
300+
});
301+
});
302+
177303
describe("filterOtherTestsHelper", () => {
178304
it("should filter skipped, pending, and other status tests", () => {
179305
const result = filterOtherTestsHelper.fn(mockTests);
@@ -333,7 +459,7 @@ describe("CTRF Helpers", () => {
333459

334460
it("should handle negative duration (stop before start)", () => {
335461
const result = formatDurationFromTimesHelper.fn(2000, 1000);
336-
expect(result).toBe("not captured");
462+
expect(result).toBe("1ms");
337463
});
338464
});
339465

@@ -371,6 +497,140 @@ describe("CTRF Helpers", () => {
371497
});
372498
});
373499

500+
describe("formatTestMessageHelper (formatTestMessage)", () => {
501+
it("should convert ANSI codes to HTML and newlines to <br> for test messages", () => {
502+
expect(formatTestMessageHelper.fn("Line1\nLine2")).toBe("Line1<br>Line2");
503+
expect(formatTestMessageHelper.fn("Test \u001b[31mFAILED\u001b[0m\nNext line")).toBe("Test <span style=\"color:#A00\">FAILED</span><br>Next line");
504+
});
505+
506+
it("should handle multiple newlines in test output", () => {
507+
expect(formatTestMessageHelper.fn("Error\n\nStack trace")).toBe("Error<br><br>Stack trace");
508+
expect(formatTestMessageHelper.fn("Line1\n\n\nLine4")).toBe("Line1<br><br>Line4");
509+
});
510+
511+
it("should handle complex ANSI sequences in test failures", () => {
512+
const input = "\u001b[32mExpected\u001b[0m\n\u001b[31mActual\u001b[0m\n\u001b[1mDiff\u001b[0m";
513+
const expected = "<span style=\"color:#0A0\">Expected</span><br><span style=\"color:#A00\">Actual</span><br><b>Diff</b>";
514+
expect(formatTestMessageHelper.fn(input)).toBe(expected);
515+
});
516+
517+
it("should handle empty and null test messages", () => {
518+
expect(formatTestMessageHelper.fn("")).toBe("No message available");
519+
expect(formatTestMessageHelper.fn(null as unknown)).toBe("");
520+
expect(formatTestMessageHelper.fn(undefined as unknown)).toBe("");
521+
});
522+
523+
it("should handle non-string inputs gracefully", () => {
524+
expect(formatTestMessageHelper.fn(123 as unknown)).toBe("");
525+
expect(formatTestMessageHelper.fn({} as unknown)).toBe("");
526+
expect(formatTestMessageHelper.fn([] as unknown)).toBe("");
527+
});
528+
529+
it("should handle real-world test failure scenarios", () => {
530+
const assertionError = "AssertionError: expected 'false' to be 'true'\n\u001b[31m- false\u001b[0m\n\u001b[32m+ true\u001b[0m";
531+
const expected = "AssertionError: expected 'false' to be 'true'<br><span style=\"color:#A00\">- false</span><br><span style=\"color:#0A0\">+ true</span>";
532+
expect(formatTestMessageHelper.fn(assertionError)).toBe(expected);
533+
});
534+
535+
it("should handle Jest/Vitest style test output", () => {
536+
const jestOutput = "\u001b[1m\u001b[32m✓\u001b[0m Test passed\n\u001b[1m\u001b[31m✗\u001b[0m Test failed\nStack trace";
537+
const result = formatTestMessageHelper.fn(jestOutput);
538+
expect(result).toContain("<b><span style=\"color:#0A0\">✓</span></b>");
539+
expect(result).toContain("<b><span style=\"color:#A00\">✗</span></b>");
540+
expect(result).toContain("<br>");
541+
});
542+
543+
it("should be categorized as CTRF helper", () => {
544+
expect(formatTestMessageHelper.category).toBe("CTRF");
545+
expect(formatTestMessageHelper.name).toBe("formatTestMessage");
546+
});
547+
});
548+
549+
describe("formatTestMessagePreCodeHelper", () => {
550+
it("should convert ANSI codes to HTML but preserve newlines for code formatting", () => {
551+
expect(formatTestMessagePreCodeHelper.fn("Line1\nLine2")).toBe("Line1\nLine2");
552+
expect(formatTestMessagePreCodeHelper.fn("Code \u001b[31mError\u001b[0m\nNext line")).toBe("Code <span style=\"color:#A00\">Error</span>\nNext line");
553+
});
554+
555+
it("should minimize consecutive newlines but preserve single ones", () => {
556+
expect(formatTestMessagePreCodeHelper.fn("Line1\n\nLine3")).toBe("Line1\nLine3");
557+
expect(formatTestMessagePreCodeHelper.fn("Line1\n\n\nLine4")).toBe("Line1\nLine4");
558+
expect(formatTestMessagePreCodeHelper.fn("Line1\n\n\n\nLine5")).toBe("Line1\nLine5");
559+
});
560+
561+
it("should handle stack traces with ANSI codes", () => {
562+
const stackTrace = "Error: Test failed\n at test.js:10:5\n\u001b[31m at runner.js:45:12\u001b[0m\n at main.js:20:3";
563+
const expected = "Error: Test failed\n at test.js:10:5\n<span style=\"color:#A00\"> at runner.js:45:12</span>\n at main.js:20:3";
564+
expect(formatTestMessagePreCodeHelper.fn(stackTrace)).toBe(expected);
565+
});
566+
567+
it("should handle code blocks with syntax highlighting", () => {
568+
const codeBlock = "function test() {\n \u001b[32mconsole.log\u001b[0m('hello');\n \u001b[31mthrow new Error\u001b[0m('test');\n}";
569+
const expected = "function test() {\n <span style=\"color:#0A0\">console.log</span>('hello');\n <span style=\"color:#A00\">throw new Error</span>('test');\n}";
570+
expect(formatTestMessagePreCodeHelper.fn(codeBlock)).toBe(expected);
571+
});
572+
573+
it("should handle empty and null inputs", () => {
574+
expect(formatTestMessagePreCodeHelper.fn("")).toBe("No message available");
575+
expect(formatTestMessagePreCodeHelper.fn(null as unknown)).toBe("");
576+
expect(formatTestMessagePreCodeHelper.fn(undefined as unknown)).toBe("");
577+
});
578+
579+
it("should handle non-string inputs gracefully", () => {
580+
expect(formatTestMessagePreCodeHelper.fn(123 as unknown)).toBe("");
581+
expect(formatTestMessagePreCodeHelper.fn({} as unknown)).toBe("");
582+
expect(formatTestMessagePreCodeHelper.fn([] as unknown)).toBe("");
583+
});
584+
585+
it("should be ideal for pre-formatted content", () => {
586+
const preFormattedContent = "Code:\n function add(a, b) {\n \u001b[33mreturn a + b;\u001b[0m\n }\n\n\nOutput:\n \u001b[32m5\u001b[0m";
587+
const expected = "Code:\n function add(a, b) {\n <span style=\"color:#A50\">return a + b;</span>\n }\nOutput:\n <span style=\"color:#0A0\">5</span>";
588+
expect(formatTestMessagePreCodeHelper.fn(preFormattedContent)).toBe(expected);
589+
});
590+
591+
it("should be categorized as CTRF helper", () => {
592+
expect(formatTestMessagePreCodeHelper.category).toBe("CTRF");
593+
expect(formatTestMessagePreCodeHelper.name).toBe("formatTestMessagePreCode");
594+
});
595+
});
596+
597+
describe("getCtrfEmojiHelper", () => {
598+
it("should return correct emojis for test statuses", () => {
599+
expect(getCtrfEmojiHelper.fn("passed")).toBe("✅");
600+
expect(getCtrfEmojiHelper.fn("failed")).toBe("❌");
601+
expect(getCtrfEmojiHelper.fn("skipped")).toBe("⏭️");
602+
expect(getCtrfEmojiHelper.fn("pending")).toBe("⏳");
603+
expect(getCtrfEmojiHelper.fn("other")).toBe("❓");
604+
});
605+
606+
it("should return correct emojis for categories", () => {
607+
expect(getCtrfEmojiHelper.fn("build")).toBe("🏗️");
608+
expect(getCtrfEmojiHelper.fn("duration")).toBe("⏱️");
609+
expect(getCtrfEmojiHelper.fn("flaky")).toBe("🍂");
610+
expect(getCtrfEmojiHelper.fn("tests")).toBe("📝");
611+
expect(getCtrfEmojiHelper.fn("result")).toBe("🧪");
612+
expect(getCtrfEmojiHelper.fn("warning")).toBe("⚠️");
613+
});
614+
615+
it("should return default emoji for unknown status", () => {
616+
expect(getCtrfEmojiHelper.fn("unknown")).toBe("❓");
617+
expect(getCtrfEmojiHelper.fn("invalid")).toBe("❓");
618+
});
619+
620+
it("should return default emoji for non-string input", () => {
621+
expect(getCtrfEmojiHelper.fn(null)).toBe("❓");
622+
expect(getCtrfEmojiHelper.fn(undefined)).toBe("❓");
623+
expect(getCtrfEmojiHelper.fn(123)).toBe("❓");
624+
expect(getCtrfEmojiHelper.fn({})).toBe("❓");
625+
expect(getCtrfEmojiHelper.fn([])).toBe("❓");
626+
});
627+
628+
it("should be categorized as CTRF helper", () => {
629+
expect(getCtrfEmojiHelper.category).toBe("CTRF");
630+
expect(getCtrfEmojiHelper.name).toBe("getCtrfEmoji");
631+
});
632+
});
633+
374634
describe("Helper metadata", () => {
375635
it("should have correct helper names", () => {
376636
expect(sortTestsByFlakyRateHelper.name).toBe("sortTestsByFlakyRate");
@@ -384,6 +644,10 @@ describe("CTRF Helpers", () => {
384644
"formatDurationFromTimes",
385645
);
386646
expect(formatDurationHelper.name).toBe("formatDuration");
647+
expect(formatTestMessageHelper.name).toBe("formatTestMessage");
648+
expect(formatTestMessagePreCodeHelper.name).toBe("formatTestMessagePreCode");
649+
expect(getCtrfEmojiHelper.name).toBe("getCtrfEmoji");
650+
expect(limitFailedTestsHelper.name).toBe("limitFailedTests");
387651
});
388652

389653
it("should have correct helper categories", () => {
@@ -392,11 +656,15 @@ describe("CTRF Helpers", () => {
392656
filterPassedTestsHelper,
393657
filterFailedTestsHelper,
394658
filterOtherTestsHelper,
659+
limitFailedTestsHelper,
395660
countFlakyTestsHelper,
396661
anyFlakyTestsHelper,
397662
sortTestsByFailRateHelper,
398663
formatDurationFromTimesHelper,
399664
formatDurationHelper,
665+
formatTestMessageHelper,
666+
formatTestMessagePreCodeHelper,
667+
getCtrfEmojiHelper,
400668
];
401669

402670
helpers.forEach((helper) => {

0 commit comments

Comments
 (0)