@hobenakicoffee/libraries 3.4.2 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +119 -246
  3. package/package.json +25 -22
  4. package/src/App.tsx +194 -19
  5. package/src/index.css +0 -1
  6. package/src/types/supabase.ts +940 -3
  7. package/src/components/turnstile-captcha.tsx +0 -47
  8. package/src/components/ui/button.tsx +0 -77
  9. package/src/components/ui/calendar.tsx +0 -235
  10. package/src/components/ui/spinner.tsx +0 -18
  11. package/src/constants/common.test.ts +0 -33
  12. package/src/constants/legal.test.ts +0 -72
  13. package/src/constants/payment.test.ts +0 -259
  14. package/src/constants/platforms.test.ts +0 -66
  15. package/src/constants/services.test.ts +0 -58
  16. package/src/lib/utils.ts +0 -6
  17. package/src/moderation/profanity-service.test.ts +0 -106
  18. package/src/providers/theme-provider.tsx +0 -73
  19. package/src/utils/check-moderation.test.ts +0 -321
  20. package/src/utils/format-amount.test.ts +0 -30
  21. package/src/utils/format-count.test.ts +0 -56
  22. package/src/utils/format-date.test.ts +0 -19
  23. package/src/utils/format-number.test.ts +0 -29
  24. package/src/utils/format-plain-text.test.ts +0 -36
  25. package/src/utils/get-newsletter-post-link.test.ts +0 -27
  26. package/src/utils/get-product-link.test.ts +0 -34
  27. package/src/utils/get-social-handle.test.ts +0 -32
  28. package/src/utils/get-social-link.test.ts +0 -63
  29. package/src/utils/get-user-name-initials.test.ts +0 -34
  30. package/src/utils/get-user-page-link.test.ts +0 -9
  31. package/src/utils/open-to-new-window.test.ts +0 -34
  32. package/src/utils/post-to-facebook.test.ts +0 -43
  33. package/src/utils/post-to-instagram.test.ts +0 -56
  34. package/src/utils/post-to-linkedin.test.ts +0 -43
  35. package/src/utils/post-to-x.test.ts +0 -45
  36. package/src/utils/qr-svg-utils.test.ts +0 -104
  37. package/src/utils/to-human-readable.test.ts +0 -25
  38. package/src/utils/validate-phone-number.test.ts +0 -28
@@ -1,321 +0,0 @@
1
- import { beforeEach, describe, expect, mock, test } from "bun:test";
2
- import type OpenAI from "openai";
3
- import { checkModeration } from "./check-moderation";
4
-
5
- describe("checkModeration", () => {
6
- let mockOpenAiClient: OpenAI;
7
- let mockModerations: any;
8
-
9
- beforeEach(() => {
10
- mockModerations = {
11
- create: mock(async () => ({
12
- results: [
13
- {
14
- flagged: false,
15
- categories: {},
16
- },
17
- ],
18
- })),
19
- };
20
-
21
- mockOpenAiClient = {
22
- moderations: mockModerations,
23
- } as unknown as OpenAI;
24
- });
25
-
26
- test("returns OpenAI result when no profanity detected", async () => {
27
- mockModerations.create = mock(async () => ({
28
- results: [
29
- {
30
- flagged: true,
31
- categories: {
32
- hate: true,
33
- sexual: false,
34
- },
35
- },
36
- ],
37
- }));
38
-
39
- const result = await checkModeration(mockOpenAiClient, "clean text");
40
-
41
- expect(result.flagged).toBe(true);
42
- expect(result.source).toBe("openai");
43
- expect(result.categories?.hate).toBe(true);
44
- expect(result.categories?.sexual).toBe(false);
45
- expect(result.error).toBeNull();
46
- });
47
-
48
- test("returns flagged false when OpenAI approves text", async () => {
49
- mockModerations.create = mock(async () => ({
50
- results: [
51
- {
52
- flagged: false,
53
- categories: {},
54
- },
55
- ],
56
- }));
57
-
58
- const result = await checkModeration(mockOpenAiClient, "safe text");
59
-
60
- expect(result.flagged).toBe(false);
61
- expect(result.source).toBe("openai");
62
- expect(result.error).toBeNull();
63
- });
64
-
65
- test("calls OpenAI moderations API with correct model", async () => {
66
- mockModerations.create = mock(async () => ({
67
- results: [
68
- {
69
- flagged: false,
70
- categories: {},
71
- },
72
- ],
73
- }));
74
-
75
- const testText = "test content";
76
- await checkModeration(mockOpenAiClient, testText);
77
-
78
- expect(mockModerations.create).toHaveBeenCalledWith({
79
- model: "omni-moderation-latest",
80
- input: testText,
81
- });
82
- });
83
-
84
- test("returns error object when OpenAI API throws", async () => {
85
- const testError = new Error("API Error");
86
- mockModerations.create = mock(() => {
87
- throw testError;
88
- });
89
-
90
- const result = await checkModeration(mockOpenAiClient, "test");
91
-
92
- expect(result.flagged).toBe(false);
93
- expect(result.categories).toBeNull();
94
- expect(result.error).toBe(testError);
95
- expect(result.source).toBeNull();
96
- });
97
-
98
- test("returns error with stack trace when API fails", async () => {
99
- const testError = new Error("Network error");
100
- mockModerations.create = mock(() => {
101
- throw testError;
102
- });
103
-
104
- const result = await checkModeration(mockOpenAiClient, "test");
105
-
106
- expect(typeof result.error).toBe("object");
107
- expect(result.source).toBeNull();
108
- });
109
-
110
- test("returns proper result structure for OpenAI case", async () => {
111
- mockModerations.create = mock(async () => ({
112
- results: [
113
- {
114
- flagged: false,
115
- categories: {},
116
- },
117
- ],
118
- }));
119
-
120
- const result = await checkModeration(mockOpenAiClient, "test");
121
-
122
- expect(result).toHaveProperty("flagged");
123
- expect(result).toHaveProperty("categories");
124
- expect(result).toHaveProperty("error");
125
- expect(result).toHaveProperty("source");
126
- });
127
-
128
- test("returns proper result structure for error case", async () => {
129
- mockModerations.create = mock(() => {
130
- throw new Error("Test error");
131
- });
132
-
133
- const result = await checkModeration(mockOpenAiClient, "test");
134
-
135
- expect(result).toHaveProperty("flagged");
136
- expect(result).toHaveProperty("categories");
137
- expect(result).toHaveProperty("error");
138
- expect(result).toHaveProperty("source");
139
- });
140
-
141
- test("handles OpenAI result with null categories", async () => {
142
- mockModerations.create = mock(async () => ({
143
- results: [
144
- {
145
- flagged: false,
146
- categories: null,
147
- },
148
- ],
149
- }));
150
-
151
- const result = await checkModeration(mockOpenAiClient, "test");
152
-
153
- expect(result.categories).toBeNull();
154
- expect(result.source).toBe("openai");
155
- });
156
-
157
- test("handles empty OpenAI results array gracefully", async () => {
158
- mockModerations.create = mock(async () => ({
159
- results: [],
160
- }));
161
-
162
- const result = await checkModeration(mockOpenAiClient, "test");
163
-
164
- expect(result.flagged).toBe(false);
165
- expect(result.categories).toBeNull();
166
- expect(result.source).toBe("openai");
167
- });
168
-
169
- test("handles empty text input", async () => {
170
- mockModerations.create = mock(async () => ({
171
- results: [
172
- {
173
- flagged: false,
174
- categories: {},
175
- },
176
- ],
177
- }));
178
-
179
- const result = await checkModeration(mockOpenAiClient, "");
180
-
181
- expect(result.source).toBe("openai");
182
- expect(result.error).toBeNull();
183
- });
184
-
185
- test("handles special characters in text", async () => {
186
- mockModerations.create = mock(async () => ({
187
- results: [
188
- {
189
- flagged: false,
190
- categories: {},
191
- },
192
- ],
193
- }));
194
-
195
- const result = await checkModeration(mockOpenAiClient, "test!@#$%^&*()");
196
-
197
- expect(result.source).toBe("openai");
198
- expect(result.error).toBeNull();
199
- });
200
-
201
- test("handles very long text", async () => {
202
- mockModerations.create = mock(async () => ({
203
- results: [
204
- {
205
- flagged: false,
206
- categories: {},
207
- },
208
- ],
209
- }));
210
-
211
- const longText = "test ".repeat(10_000);
212
- const result = await checkModeration(mockOpenAiClient, longText);
213
-
214
- expect(result.source).toBe("openai");
215
- expect(result.error).toBeNull();
216
- });
217
-
218
- test("returns source as null on error", async () => {
219
- mockModerations.create = mock(() => {
220
- throw new Error("API Error");
221
- });
222
-
223
- const result = await checkModeration(mockOpenAiClient, "test");
224
-
225
- expect(result.source).toBeNull();
226
- });
227
-
228
- test("returns flagged as false when error occurs", async () => {
229
- mockModerations.create = mock(() => {
230
- throw new Error("API Error");
231
- });
232
-
233
- const result = await checkModeration(mockOpenAiClient, "test");
234
-
235
- expect(result.flagged).toBe(false);
236
- });
237
-
238
- test("handles multiple category flags", async () => {
239
- mockModerations.create = mock(async () => ({
240
- results: [
241
- {
242
- flagged: true,
243
- categories: {
244
- hate: true,
245
- sexual: true,
246
- violence: true,
247
- harassment: false,
248
- },
249
- },
250
- ],
251
- }));
252
-
253
- const result = await checkModeration(mockOpenAiClient, "test");
254
-
255
- expect(result.flagged).toBe(true);
256
- expect(result.categories?.hate).toBe(true);
257
- expect(result.categories?.sexual).toBe(true);
258
- expect(result.categories?.violence).toBe(true);
259
- });
260
-
261
- test("passes input to OpenAI exactly as provided", async () => {
262
- mockModerations.create = mock(async () => ({
263
- results: [
264
- {
265
- flagged: false,
266
- categories: {},
267
- },
268
- ],
269
- }));
270
-
271
- const customText = "Custom text with special @#$ chars";
272
- await checkModeration(mockOpenAiClient, customText);
273
-
274
- const callArgs = mockModerations.create.mock.calls[0][0];
275
- expect(callArgs.input).toBe(customText);
276
- });
277
-
278
- test("preserves error details in result", async () => {
279
- const errorMessage = "Specific API Error";
280
- mockModerations.create = mock(() => {
281
- throw new Error(errorMessage);
282
- });
283
-
284
- const result = await checkModeration(mockOpenAiClient, "test");
285
-
286
- expect((result.error as Error).message).toBe(errorMessage);
287
- });
288
-
289
- test("handles undefined OpenAI result fields", async () => {
290
- mockModerations.create = mock(async () => ({
291
- results: [
292
- {
293
- flagged: undefined,
294
- categories: undefined,
295
- },
296
- ],
297
- }));
298
-
299
- const result = await checkModeration(mockOpenAiClient, "test");
300
-
301
- expect(result.flagged).toBe(false);
302
- expect(result.categories).toBeNull();
303
- });
304
-
305
- test("correctly identifies moderation source as openai", async () => {
306
- mockModerations.create = mock(async () => ({
307
- results: [
308
- {
309
- flagged: false,
310
- categories: {},
311
- },
312
- ],
313
- }));
314
-
315
- const result = await checkModeration(mockOpenAiClient, "clean content");
316
-
317
- expect(result.source).toBe("openai");
318
- expect(result.source).not.toBe("profanity");
319
- expect(result.source).not.toBeNull();
320
- });
321
- });
@@ -1,30 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { formatAmount, formatSignedAmount } from "./format-amount";
3
-
4
- describe("formatAmount", () => {
5
- test("formats absolute number with currency symbol", () => {
6
- const expected = new Intl.NumberFormat(undefined, {
7
- maximumFractionDigits: 0,
8
- }).format(1234);
9
-
10
- expect(formatAmount(-1234)).toBe(`৳${expected}`);
11
- });
12
- });
13
-
14
- describe("formatSignedAmount", () => {
15
- test("uses minus sign for debit", () => {
16
- const expected = new Intl.NumberFormat(undefined, {
17
- maximumFractionDigits: 0,
18
- }).format(2000);
19
-
20
- expect(formatSignedAmount(2000, "debit")).toBe(`- ৳${expected}`);
21
- });
22
-
23
- test("uses plus sign for credit and absolute value", () => {
24
- const expected = new Intl.NumberFormat(undefined, {
25
- maximumFractionDigits: 0,
26
- }).format(2000);
27
-
28
- expect(formatSignedAmount(-2000, "credit")).toBe(`+ ৳${expected}`);
29
- });
30
- });
@@ -1,56 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { formatCount } from "./format-count";
3
-
4
- describe("formatCount", () => {
5
- test("formats zero and small numbers without suffix", () => {
6
- expect(formatCount(0)).toBe("0");
7
- expect(formatCount(7)).toBe("7");
8
- expect(formatCount(999)).toBe("999");
9
- });
10
-
11
- test("handles negative numbers", () => {
12
- expect(formatCount(-1000)).toBe("-1k");
13
- expect(formatCount(-1500)).toBe("-1.5k");
14
- expect(formatCount(-1_000_000)).toBe("-1M");
15
- });
16
-
17
- test("formats thousands with k suffix", () => {
18
- expect(formatCount(1000)).toBe("1k");
19
- expect(formatCount(10_000)).toBe("10k");
20
- expect(formatCount(100_000)).toBe("100k");
21
- });
22
-
23
- test("shows decimal for non-round thousands", () => {
24
- expect(formatCount(1500)).toBe("1.5k");
25
- expect(formatCount(10_500)).toBe("10.5k");
26
- expect(formatCount(15_500)).toBe("15.5k");
27
- });
28
-
29
- test("formats millions with M suffix", () => {
30
- expect(formatCount(1_000_000)).toBe("1M");
31
- expect(formatCount(10_000_000)).toBe("10M");
32
- expect(formatCount(100_000_000)).toBe("100M");
33
- });
34
-
35
- test("shows decimal for non-round millions", () => {
36
- expect(formatCount(1_500_000)).toBe("1.5M");
37
- expect(formatCount(10_500_000)).toBe("10.5M");
38
- });
39
-
40
- test("formats billions with B suffix", () => {
41
- expect(formatCount(1_000_000_000)).toBe("1B");
42
- expect(formatCount(10_000_000_000)).toBe("10B");
43
- expect(formatCount(100_000_000_000)).toBe("100B");
44
- });
45
-
46
- test("shows decimal for non-round billions", () => {
47
- expect(formatCount(1_500_000_000)).toBe("1.5B");
48
- expect(formatCount(2_100_000_000)).toBe("2.1B");
49
- });
50
-
51
- test("uses whole numbers for large round values", () => {
52
- expect(formatCount(1000)).toBe("1k");
53
- expect(formatCount(1_000_000)).toBe("1M");
54
- expect(formatCount(1_000_000_000)).toBe("1B");
55
- });
56
- });
@@ -1,19 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { formatDate } from "./format-date";
3
-
4
- describe("formatDate", () => {
5
- test("returns dash for invalid date", () => {
6
- expect(formatDate("not-a-date")).toBe("-");
7
- });
8
-
9
- test("formats a valid date", () => {
10
- const input = "2026-02-13T00:00:00.000Z";
11
- const expected = new Date(input).toLocaleDateString(undefined, {
12
- month: "short",
13
- day: "numeric",
14
- year: "numeric",
15
- });
16
-
17
- expect(formatDate(input)).toBe(expected);
18
- });
19
- });
@@ -1,29 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { formatNumber } from "./format-number";
3
-
4
- function digitsFromFormatted(formatted: string) {
5
- const digits = formatted.replace(/\D/g, "");
6
- return Number(digits);
7
- }
8
-
9
- describe("formatNumber", () => {
10
- test("formats positive numbers and rounds correctly", () => {
11
- expect(digitsFromFormatted(formatNumber(1234.56))).toBe(1235);
12
- });
13
-
14
- test("formats negative numbers using absolute value", () => {
15
- expect(digitsFromFormatted(formatNumber(-9876.5))).toBe(9877);
16
- });
17
-
18
- test("formats zero", () => {
19
- expect(digitsFromFormatted(formatNumber(0))).toBe(0);
20
- });
21
-
22
- test("rounds small fractional numbers to 0", () => {
23
- expect(digitsFromFormatted(formatNumber(-0.4))).toBe(0);
24
- });
25
-
26
- test("formats large numbers", () => {
27
- expect(digitsFromFormatted(formatNumber(1_000_000))).toBe(1_000_000);
28
- });
29
- });
@@ -1,36 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { formatMetadataKey, formatToPlainText } from "./format-plain-text";
3
-
4
- describe("formatToPlainText", () => {
5
- test("returns empty string for null and undefined", () => {
6
- expect(formatToPlainText(null)).toBe("");
7
- expect(formatToPlainText(undefined)).toBe("");
8
- });
9
-
10
- test("formats booleans to Yes/No by default", () => {
11
- expect(formatToPlainText(true)).toBe("Yes");
12
- expect(formatToPlainText(false)).toBe("No");
13
- });
14
-
15
- test("can keep raw boolean text", () => {
16
- expect(formatToPlainText(true, { formatBooleans: false })).toBe("true");
17
- });
18
-
19
- test("truncates long strings with ellipsis", () => {
20
- expect(formatToPlainText("abcdef", { maxStringLength: 5 })).toBe("ab...");
21
- });
22
-
23
- test("stringifies objects and arrays", () => {
24
- expect(formatToPlainText({ a: 1 })).toBe(JSON.stringify({ a: 1 }, null, 2));
25
- expect(formatToPlainText(["x", "y"])).toBe(
26
- JSON.stringify(["x", "y"], null, 2)
27
- );
28
- });
29
- });
30
-
31
- describe("formatMetadataKey", () => {
32
- test("formats camelCase and snake_case keys", () => {
33
- expect(formatMetadataKey("supporterName")).toBe("Supporter Name");
34
- expect(formatMetadataKey("is_monthly")).toBe("Is monthly");
35
- });
36
- });
@@ -1,27 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { getNewsletterPostLink } from "./get-newsletter-post-link";
3
-
4
- describe("getNewsletterPostLink", () => {
5
- test("builds relative path without baseUrl", () => {
6
- const result = getNewsletterPostLink("johndoe", "my-post");
7
- expect(result).toBe("/@johndoe/posts/my-post");
8
- });
9
-
10
- test("builds absolute URL with baseUrl", () => {
11
- const result = getNewsletterPostLink(
12
- "johndoe",
13
- "my-post",
14
- "https://hobenaki.coffee"
15
- );
16
- expect(result).toBe("https://hobenaki.coffee/@johndoe/posts/my-post");
17
- });
18
-
19
- test("handles different usernames and slugs", () => {
20
- expect(getNewsletterPostLink("alice", "welcome-newsletter")).toBe(
21
- "/@alice/posts/welcome-newsletter"
22
- );
23
- expect(getNewsletterPostLink("bob", "updates-2024")).toBe(
24
- "/@bob/posts/updates-2024"
25
- );
26
- });
27
- });
@@ -1,34 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { getProductLink } from "./get-product-link";
3
-
4
- describe("getProductLink", () => {
5
- test("builds product link with default baseUrl", () => {
6
- const result = getProductLink("johndoe", "my-product");
7
- expect(result).toBe(
8
- "https://hobenakicoffee.com/@johndoe/shop/products/my-product"
9
- );
10
- });
11
-
12
- test("builds product link with custom baseUrl", () => {
13
- const result = getProductLink(
14
- "johndoe",
15
- "my-product",
16
- "https://custom.com"
17
- );
18
- expect(result).toBe("https://custom.com/@johndoe/shop/products/my-product");
19
- });
20
-
21
- test("sanitizes username with whitespace", () => {
22
- const result = getProductLink(" john doe ", "my-product");
23
- expect(result).toBe(
24
- "https://hobenakicoffee.com/@johndoe/shop/products/my-product"
25
- );
26
- });
27
-
28
- test("does not encode slug - passes through as-is", () => {
29
- const result = getProductLink("johndoe", "my product 123");
30
- expect(result).toBe(
31
- "https://hobenakicoffee.com/@johndoe/shop/products/my-product-123"
32
- );
33
- });
34
- });
@@ -1,32 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { getSocialUrl } from "./get-social-handle";
3
- import { getUserPageLink } from "./get-user-page-link";
4
-
5
- describe("getSocialUrl", () => {
6
- test("returns our platform URL when username is provided", () => {
7
- expect(getSocialUrl("alice", "x", "ignored")).toBe(
8
- getUserPageLink("alice")
9
- );
10
- });
11
-
12
- test("returns # when required fields are missing", () => {
13
- expect(getSocialUrl(undefined, "x", "")).toBe("#");
14
- expect(getSocialUrl(undefined, null, "name")).toBe("#");
15
- });
16
-
17
- test("builds social URL with sanitized handle", () => {
18
- expect(getSocialUrl(undefined, "instagram", " @john doe ")).toBe(
19
- "https://instagram.com/johndoe"
20
- );
21
- });
22
-
23
- test("uses platform-specific format for youtube", () => {
24
- expect(getSocialUrl(undefined, "youtube", "Jane")).toBe(
25
- "https://youtube.com/@Jane"
26
- );
27
- });
28
-
29
- test("returns # for unsupported platform", () => {
30
- expect(getSocialUrl(undefined, "myspace" as any, "Jane")).toBe("#");
31
- });
32
- });
@@ -1,63 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { SupporterPlatforms } from "../constants";
3
- import { getSocialLink } from "./get-social-link";
4
-
5
- describe("getSocialLink", () => {
6
- test("returns null when username or platform is missing", () => {
7
- expect(getSocialLink(undefined, SupporterPlatforms.FACEBOOK)).toBeNull();
8
- expect(getSocialLink("alice", undefined)).toBeNull();
9
- expect(getSocialLink()).toBeNull();
10
- });
11
-
12
- test("returns correct Facebook URL", () => {
13
- expect(getSocialLink("alice", SupporterPlatforms.FACEBOOK)).toBe(
14
- "https://facebook.com/alice"
15
- );
16
- });
17
-
18
- test("returns correct Instagram URL with spaces", () => {
19
- expect(getSocialLink(" john doe ", SupporterPlatforms.INSTAGRAM)).toBe(
20
- "https://instagram.com/johndoe"
21
- );
22
- });
23
-
24
- test("returns correct TikTok URL", () => {
25
- expect(getSocialLink("user123", SupporterPlatforms.TIKTOK)).toBe(
26
- "https://tiktok.com/@user123"
27
- );
28
- });
29
-
30
- test("returns correct YouTube URL", () => {
31
- expect(getSocialLink("Jane", SupporterPlatforms.YOUTUBE)).toBe(
32
- "https://youtube.com/Jane"
33
- );
34
- });
35
-
36
- test("returns correct X (Twitter) URL", () => {
37
- expect(getSocialLink("bob", SupporterPlatforms.X)).toBe(
38
- "https://x.com/bob"
39
- );
40
- });
41
-
42
- test("returns correct LinkedIn URL", () => {
43
- expect(getSocialLink("johnsmith", SupporterPlatforms.LINKEDIN)).toBe(
44
- "https://linkedin.com/in/johnsmith"
45
- );
46
- });
47
-
48
- test("returns correct GitHub URL", () => {
49
- expect(getSocialLink("octocat", SupporterPlatforms.GITHUB)).toBe(
50
- "https://github.com/octocat"
51
- );
52
- });
53
-
54
- test("returns null for unsupported platform", () => {
55
- expect(getSocialLink("alice", "MYSPACE" as any)).toBeNull();
56
- });
57
-
58
- test("sanitizes username with special characters", () => {
59
- expect(getSocialLink("user name!@#", SupporterPlatforms.FACEBOOK)).toBe(
60
- "https://facebook.com/username!%40%23"
61
- );
62
- });
63
- });
@@ -1,34 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { getInitials } from "./get-user-name-initials";
3
-
4
- describe("getInitials", () => {
5
- test("returns initials for a full name", () => {
6
- expect(getInitials("John Doe")).toBe("JD");
7
- expect(getInitials("Jane Ann Smith")).toBe("JS");
8
- expect(getInitials("Alice Bob Carol")).toBe("AC");
9
- });
10
-
11
- test("returns single initial for single name", () => {
12
- expect(getInitials("John")).toBe("J");
13
- expect(getInitials("A")).toBe("A");
14
- });
15
-
16
- test("handles extra spaces", () => {
17
- expect(getInitials(" John Doe ")).toBe("JD");
18
- expect(getInitials(" Alice ")).toBe("A");
19
- expect(getInitials(" Alice Bob Carol ")).toBe("AC");
20
- });
21
-
22
- test("returns ? for empty or undefined", () => {
23
- expect(getInitials("")).toBe("?");
24
- expect(getInitials(" ")).toBe("?");
25
- expect(getInitials(undefined)).toBe("?");
26
- expect(getInitials(null)).toBe("?");
27
- });
28
-
29
- test("handles non-ASCII and special characters", () => {
30
- expect(getInitials("Élodie Durand")).toBe("ÉD");
31
- expect(getInitials("李 小龙")).toBe("李小");
32
- expect(getInitials("O’Connor")).toBe("O");
33
- });
34
- });