Skip to content

Commit ebd0a1d

Browse files
committed
test: add unit tests for core utilities (Phase 4)
Add comprehensive unit tests for core utility functions: - utils.ts: 12 tests for cn() function (class name merging with Tailwind) - stageUtils.ts: 11 tests for sortStagesByOrder() sorting algorithm - markdown.ts: 21 tests for parseMarkdown() with GFM support - timeUtils.ts: 44 tests for time formatting and timezone conversion functions - formatTimeRange, formatDateTime, formatTimeOnly - toDatetimeLocal, toISOString - combineDateAndTime, convertLocalTimeToUTC All utilities tests include: - Edge cases and null/undefined handling - Format validation - Timezone conversions - Complex data scenarios Total: 292 tests passing (Phases 1-4 combined)
1 parent 1667281 commit ebd0a1d

File tree

4 files changed

+665
-0
lines changed

4 files changed

+665
-0
lines changed

src/lib/__tests__/markdown.test.ts

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2+
import { parseMarkdown } from "../markdown";
3+
4+
describe("parseMarkdown", () => {
5+
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
6+
7+
beforeEach(() => {
8+
consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {});
9+
});
10+
11+
afterEach(() => {
12+
consoleErrorSpy.mockRestore();
13+
});
14+
15+
it("returns empty string for empty input", () => {
16+
expect(parseMarkdown("")).toBe("");
17+
});
18+
19+
it("parses basic markdown text", () => {
20+
const result = parseMarkdown("Hello world");
21+
expect(result).toContain("Hello world");
22+
});
23+
24+
it("parses bold text", () => {
25+
const result = parseMarkdown("**bold text**");
26+
expect(result).toContain("<strong>");
27+
expect(result).toContain("bold text");
28+
expect(result).toContain("</strong>");
29+
});
30+
31+
it("parses italic text", () => {
32+
const result = parseMarkdown("*italic text*");
33+
expect(result).toContain("<em>");
34+
expect(result).toContain("italic text");
35+
expect(result).toContain("</em>");
36+
});
37+
38+
it("parses links", () => {
39+
const result = parseMarkdown("[link text](https://example.com)");
40+
expect(result).toContain('<a href="https://example.com">');
41+
expect(result).toContain("link text");
42+
expect(result).toContain("</a>");
43+
});
44+
45+
it("parses headings", () => {
46+
const result = parseMarkdown("# Heading 1");
47+
expect(result).toContain("<h1>");
48+
expect(result).toContain("Heading 1");
49+
expect(result).toContain("</h1>");
50+
});
51+
52+
it("parses multiple heading levels", () => {
53+
const result = parseMarkdown("## Heading 2\n### Heading 3");
54+
expect(result).toContain("<h2>");
55+
expect(result).toContain("<h3>");
56+
});
57+
58+
it("parses unordered lists", () => {
59+
const result = parseMarkdown("- Item 1\n- Item 2");
60+
expect(result).toContain("<ul>");
61+
expect(result).toContain("<li>");
62+
expect(result).toContain("Item 1");
63+
expect(result).toContain("Item 2");
64+
expect(result).toContain("</ul>");
65+
});
66+
67+
it("parses ordered lists", () => {
68+
const result = parseMarkdown("1. First\n2. Second");
69+
expect(result).toContain("<ol>");
70+
expect(result).toContain("<li>");
71+
expect(result).toContain("First");
72+
expect(result).toContain("Second");
73+
expect(result).toContain("</ol>");
74+
});
75+
76+
it("converts line breaks with breaks: true option", () => {
77+
const result = parseMarkdown("Line 1\nLine 2");
78+
expect(result).toContain("<br>");
79+
});
80+
81+
it("parses code blocks", () => {
82+
const result = parseMarkdown("`code here`");
83+
expect(result).toContain("<code>");
84+
expect(result).toContain("code here");
85+
expect(result).toContain("</code>");
86+
});
87+
88+
it("parses fenced code blocks", () => {
89+
const result = parseMarkdown("```\ncode block\n```");
90+
expect(result).toContain("<pre>");
91+
expect(result).toContain("<code>");
92+
expect(result).toContain("code block");
93+
});
94+
95+
it("parses blockquotes", () => {
96+
const result = parseMarkdown("> Quote here");
97+
expect(result).toContain("<blockquote>");
98+
expect(result).toContain("Quote here");
99+
expect(result).toContain("</blockquote>");
100+
});
101+
102+
it("handles mixed markdown elements", () => {
103+
const markdown = "# Title\n\n**Bold** and *italic* text\n\n- List item";
104+
const result = parseMarkdown(markdown);
105+
expect(result).toContain("<h1>");
106+
expect(result).toContain("<strong>");
107+
expect(result).toContain("<em>");
108+
expect(result).toContain("<ul>");
109+
});
110+
111+
it("supports GitHub Flavored Markdown (GFM)", () => {
112+
const result = parseMarkdown("~~strikethrough~~");
113+
expect(result).toContain("strikethrough");
114+
});
115+
116+
it("handles tables (GFM feature)", () => {
117+
const markdown = "| Header |\n| ------ |\n| Cell |";
118+
const result = parseMarkdown(markdown);
119+
expect(result).toContain("<table>");
120+
expect(result).toContain("Header");
121+
expect(result).toContain("Cell");
122+
});
123+
124+
it("returns plain text with line breaks on parse error", () => {
125+
expect(parseMarkdown("Normal text\nWith newline")).toBeTruthy();
126+
});
127+
128+
it("handles special characters", () => {
129+
const result = parseMarkdown("Text with & and < and >");
130+
expect(result).toContain("&amp;");
131+
expect(result).toContain("&lt;");
132+
expect(result).toContain("&gt;");
133+
});
134+
135+
it("handles URLs in text", () => {
136+
const result = parseMarkdown("Visit https://example.com");
137+
expect(result).toBeTruthy();
138+
});
139+
140+
it("parses nested markdown", () => {
141+
const result = parseMarkdown("**bold with *italic* inside**");
142+
expect(result).toContain("<strong>");
143+
expect(result).toContain("<em>");
144+
});
145+
146+
it("handles multiple paragraphs", () => {
147+
const result = parseMarkdown("Paragraph 1\n\nParagraph 2");
148+
expect(result).toContain("<p>");
149+
expect(result).toContain("Paragraph 1");
150+
expect(result).toContain("Paragraph 2");
151+
});
152+
});
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import { describe, expect, it } from "vitest";
2+
import { sortStagesByOrder } from "../stageUtils";
3+
4+
describe("sortStagesByOrder", () => {
5+
it("sorts stages with order > 0 by their order value", () => {
6+
const stages = [
7+
{ name: "Stage C", stage_order: 3 },
8+
{ name: "Stage A", stage_order: 1 },
9+
{ name: "Stage B", stage_order: 2 },
10+
];
11+
12+
const sorted = sortStagesByOrder(stages);
13+
14+
expect(sorted[0].name).toBe("Stage A");
15+
expect(sorted[1].name).toBe("Stage B");
16+
expect(sorted[2].name).toBe("Stage C");
17+
});
18+
19+
it("places stages with order 0 at the end, sorted alphabetically", () => {
20+
const stages = [
21+
{ name: "Stage B", stage_order: 0 },
22+
{ name: "Stage A", stage_order: 0 },
23+
{ name: "Stage C", stage_order: 0 },
24+
];
25+
26+
const sorted = sortStagesByOrder(stages);
27+
28+
expect(sorted[0].name).toBe("Stage A");
29+
expect(sorted[1].name).toBe("Stage B");
30+
expect(sorted[2].name).toBe("Stage C");
31+
});
32+
33+
it("places ordered stages before unordered stages", () => {
34+
const stages = [
35+
{ name: "Zebra Stage", stage_order: 0 },
36+
{ name: "Main Stage", stage_order: 1 },
37+
{ name: "Second Stage", stage_order: 2 },
38+
{ name: "Alpha Stage", stage_order: 0 },
39+
];
40+
41+
const sorted = sortStagesByOrder(stages);
42+
43+
expect(sorted[0].name).toBe("Main Stage");
44+
expect(sorted[1].name).toBe("Second Stage");
45+
expect(sorted[2].name).toBe("Alpha Stage");
46+
expect(sorted[3].name).toBe("Zebra Stage");
47+
});
48+
49+
it("handles null stage_order as 0", () => {
50+
const stages = [
51+
{ name: "Stage B", stage_order: null },
52+
{ name: "Stage A", stage_order: 1 },
53+
{ name: "Stage C", stage_order: null },
54+
];
55+
56+
const sorted = sortStagesByOrder(stages);
57+
58+
expect(sorted[0].name).toBe("Stage A");
59+
expect(sorted[1].name).toBe("Stage B");
60+
expect(sorted[2].name).toBe("Stage C");
61+
});
62+
63+
it("handles undefined stage_order as 0", () => {
64+
const stages = [
65+
{ name: "Stage B", stage_order: undefined },
66+
{ name: "Stage A", stage_order: 1 },
67+
{ name: "Stage C", stage_order: undefined },
68+
];
69+
70+
const sorted = sortStagesByOrder(stages);
71+
72+
expect(sorted[0].name).toBe("Stage A");
73+
expect(sorted[1].name).toBe("Stage B");
74+
expect(sorted[2].name).toBe("Stage C");
75+
});
76+
77+
it("handles mixed ordered and unordered stages", () => {
78+
const stages = [
79+
{ name: "Unordered Z", stage_order: 0 },
80+
{ name: "Ordered 3", stage_order: 3 },
81+
{ name: "Unordered A", stage_order: 0 },
82+
{ name: "Ordered 1", stage_order: 1 },
83+
{ name: "Ordered 2", stage_order: 2 },
84+
{ name: "Unordered M", stage_order: 0 },
85+
];
86+
87+
const sorted = sortStagesByOrder(stages);
88+
89+
expect(sorted[0].name).toBe("Ordered 1");
90+
expect(sorted[1].name).toBe("Ordered 2");
91+
expect(sorted[2].name).toBe("Ordered 3");
92+
expect(sorted[3].name).toBe("Unordered A");
93+
expect(sorted[4].name).toBe("Unordered M");
94+
expect(sorted[5].name).toBe("Unordered Z");
95+
});
96+
97+
it("handles empty array", () => {
98+
const stages: Array<{ name: string; stage_order: number | null }> = [];
99+
const sorted = sortStagesByOrder(stages);
100+
expect(sorted).toEqual([]);
101+
});
102+
103+
it("handles single stage", () => {
104+
const stages = [{ name: "Only Stage", stage_order: 1 }];
105+
const sorted = sortStagesByOrder(stages);
106+
expect(sorted).toHaveLength(1);
107+
expect(sorted[0].name).toBe("Only Stage");
108+
});
109+
110+
it("preserves other properties of stages", () => {
111+
const stages = [
112+
{ name: "Stage B", stage_order: 2, color: "#ff0000", id: "b" },
113+
{ name: "Stage A", stage_order: 1, color: "#00ff00", id: "a" },
114+
];
115+
116+
const sorted = sortStagesByOrder(stages);
117+
118+
expect(sorted[0]).toEqual({
119+
name: "Stage A",
120+
stage_order: 1,
121+
color: "#00ff00",
122+
id: "a",
123+
});
124+
expect(sorted[1]).toEqual({
125+
name: "Stage B",
126+
stage_order: 2,
127+
color: "#ff0000",
128+
id: "b",
129+
});
130+
});
131+
132+
it("is stable for stages with same order", () => {
133+
const stages = [
134+
{ name: "Stage C", stage_order: 1 },
135+
{ name: "Stage A", stage_order: 1 },
136+
{ name: "Stage B", stage_order: 1 },
137+
];
138+
139+
const sorted = sortStagesByOrder(stages);
140+
141+
expect(sorted).toHaveLength(3);
142+
expect(sorted.every((s) => s.stage_order === 1)).toBe(true);
143+
});
144+
145+
it("handles case-sensitive alphabetical sorting", () => {
146+
const stages = [
147+
{ name: "zebra", stage_order: 0 },
148+
{ name: "Apple", stage_order: 0 },
149+
{ name: "banana", stage_order: 0 },
150+
];
151+
152+
const sorted = sortStagesByOrder(stages);
153+
154+
expect(sorted[0].name).toBe("Apple");
155+
expect(sorted[1].name).toBe("banana");
156+
expect(sorted[2].name).toBe("zebra");
157+
});
158+
});

0 commit comments

Comments
 (0)