@webamoki/web-svelte 1.2.1 → 1.2.3

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.
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,755 @@
1
+ import { CalendarDate, CalendarDateTime, Time, ZonedDateTime } from '@internationalized/date';
2
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3
+ import { ageFromDob, checkOverlap, dateDiffWeeks, datesWithin, formatAbsolute, formatDateFull, formatDateISO, formatDateNum, formatDateShort, formatDayLetter, formatDayShort, formatMonth, formatTimeEnd, formatTimeFull, formatTimeShort, getDayOfDate, getLastDateOfDay, getLastDatesOfDay, getLastMonths, getNextDateOfDay, isDateDay, isDateToday } from './index.js';
4
+ const SERVER_TIME_ZONE = 'Europe/London';
5
+ describe('getDayOfDate', () => {
6
+ it('returns the correct day of the week (0 = Monday)', () => {
7
+ // Test specific dates with known days of the week
8
+ expect(getDayOfDate(new CalendarDate(2023, 5, 1))).toBe('Monday'); // May 1, 2023 was a Monday
9
+ expect(getDayOfDate(new CalendarDate(2023, 5, 2))).toBe('Tuesday'); // May 2, 2023 was a Tuesday
10
+ expect(getDayOfDate(new CalendarDate(2023, 5, 3))).toBe('Wednesday'); // May 3, 2023 was a Wednesday
11
+ expect(getDayOfDate(new CalendarDate(2023, 5, 4))).toBe('Thursday'); // May 4, 2023 was a Thursday
12
+ expect(getDayOfDate(new CalendarDate(2023, 5, 5))).toBe('Friday'); // May 5, 2023 was a Friday
13
+ expect(getDayOfDate(new CalendarDate(2023, 5, 6))).toBe('Saturday'); // May 6, 2023 was a Saturday
14
+ expect(getDayOfDate(new CalendarDate(2023, 5, 7))).toBe('Sunday'); // May 7, 2023 was a Sunday
15
+ });
16
+ it('handles dates across different months and years', () => {
17
+ expect(getDayOfDate(new CalendarDate(2023, 12, 25))).toBe('Monday'); // December 25, 2023 was a Monday
18
+ expect(getDayOfDate(new CalendarDate(2024, 1, 1))).toBe('Monday'); // January 1, 2024 was a Monday
19
+ expect(getDayOfDate(new CalendarDate(2024, 2, 29))).toBe('Thursday'); // February 29, 2024 (leap year) was a Thursday
20
+ });
21
+ });
22
+ describe('isDateDay', () => {
23
+ it('correctly identifies when a date matches a specific day of week', () => {
24
+ // Using known dates for testing specific days
25
+ expect(isDateDay(new CalendarDate(2023, 5, 1), 'Monday')).toBe(true);
26
+ expect(isDateDay(new CalendarDate(2023, 5, 2), 'Tuesday')).toBe(true);
27
+ expect(isDateDay(new CalendarDate(2023, 5, 3), 'Wednesday')).toBe(true);
28
+ expect(isDateDay(new CalendarDate(2023, 5, 4), 'Thursday')).toBe(true);
29
+ expect(isDateDay(new CalendarDate(2023, 5, 5), 'Friday')).toBe(true);
30
+ expect(isDateDay(new CalendarDate(2023, 5, 6), 'Saturday')).toBe(true);
31
+ expect(isDateDay(new CalendarDate(2023, 5, 7), 'Sunday')).toBe(true);
32
+ });
33
+ it('correctly identifies when a date does not match a specific day of week', () => {
34
+ // May 1, 2023 was a Monday, check against all other days
35
+ const testDate = new CalendarDate(2023, 5, 1);
36
+ expect(isDateDay(testDate, 'Tuesday')).toBe(false);
37
+ expect(isDateDay(testDate, 'Wednesday')).toBe(false);
38
+ expect(isDateDay(testDate, 'Thursday')).toBe(false);
39
+ expect(isDateDay(testDate, 'Friday')).toBe(false);
40
+ expect(isDateDay(testDate, 'Saturday')).toBe(false);
41
+ expect(isDateDay(testDate, 'Sunday')).toBe(false);
42
+ });
43
+ it('handles dates across year boundaries', () => {
44
+ // December 31, 2023 was a Sunday
45
+ expect(isDateDay(new CalendarDate(2023, 12, 31), 'Sunday')).toBe(true);
46
+ // January 1, 2024 was a Monday
47
+ expect(isDateDay(new CalendarDate(2024, 1, 1), 'Monday')).toBe(true);
48
+ });
49
+ it('handles leap year dates correctly', () => {
50
+ // February 29, 2024 was a Thursday
51
+ expect(isDateDay(new CalendarDate(2024, 2, 29), 'Thursday')).toBe(true);
52
+ expect(isDateDay(new CalendarDate(2024, 2, 29), 'Friday')).toBe(false);
53
+ });
54
+ });
55
+ describe('isDateToday', () => {
56
+ beforeEach(() => {
57
+ // Mock current date to 2024-05-15
58
+ const currentDate = new CalendarDate(2024, 5, 15);
59
+ vi.useFakeTimers();
60
+ vi.setSystemTime(currentDate.toDate(SERVER_TIME_ZONE));
61
+ });
62
+ afterEach(() => {
63
+ vi.useRealTimers();
64
+ });
65
+ it('returns true when date is today', () => {
66
+ const today = new CalendarDate(2024, 5, 15);
67
+ expect(isDateToday(today, SERVER_TIME_ZONE)).toBe(true);
68
+ });
69
+ it('returns false when date is in the past', () => {
70
+ const yesterday = new CalendarDate(2024, 5, 14);
71
+ expect(isDateToday(yesterday, SERVER_TIME_ZONE)).toBe(false);
72
+ const lastMonth = new CalendarDate(2024, 4, 15);
73
+ expect(isDateToday(lastMonth, SERVER_TIME_ZONE)).toBe(false);
74
+ const lastYear = new CalendarDate(2023, 5, 15);
75
+ expect(isDateToday(lastYear, SERVER_TIME_ZONE)).toBe(false);
76
+ });
77
+ it('returns false when date is in the future', () => {
78
+ const tomorrow = new CalendarDate(2024, 5, 16);
79
+ expect(isDateToday(tomorrow, SERVER_TIME_ZONE)).toBe(false);
80
+ const nextMonth = new CalendarDate(2024, 6, 15);
81
+ expect(isDateToday(nextMonth, SERVER_TIME_ZONE)).toBe(false);
82
+ const nextYear = new CalendarDate(2025, 5, 15);
83
+ expect(isDateToday(nextYear, SERVER_TIME_ZONE)).toBe(false);
84
+ });
85
+ it('handles date comparison at day boundaries', () => {
86
+ // Still the same day regardless of time of day
87
+ vi.setSystemTime(new CalendarDateTime(2024, 5, 15, 0, 0, 1).toDate(SERVER_TIME_ZONE));
88
+ expect(isDateToday(new CalendarDate(2024, 5, 15), SERVER_TIME_ZONE)).toBe(true);
89
+ vi.setSystemTime(new CalendarDateTime(2024, 5, 15, 23, 59, 59).toDate(SERVER_TIME_ZONE));
90
+ expect(isDateToday(new CalendarDate(2024, 5, 15), SERVER_TIME_ZONE)).toBe(true);
91
+ });
92
+ });
93
+ describe('ageFromDob', () => {
94
+ beforeEach(() => {
95
+ // Mock current date to 2025-03-30
96
+ const mockDate = new CalendarDate(2025, 3, 30);
97
+ vi.useFakeTimers();
98
+ vi.setSystemTime(mockDate.toDate(SERVER_TIME_ZONE));
99
+ });
100
+ afterEach(() => {
101
+ vi.useRealTimers();
102
+ });
103
+ it('should calculate age correctly for a normal case', () => {
104
+ const dob = new CalendarDate(2000, 3, 30);
105
+ expect(ageFromDob(dob, SERVER_TIME_ZONE)).toBe(25);
106
+ });
107
+ it('should calculate age correctly when birthday is today', () => {
108
+ const dob = new CalendarDate(2000, 3, 30);
109
+ expect(ageFromDob(dob, SERVER_TIME_ZONE)).toBe(25);
110
+ });
111
+ it('should calculate age correctly for someone born yesterday', () => {
112
+ const dob = new CalendarDate(2000, 3, 29);
113
+ expect(ageFromDob(dob, SERVER_TIME_ZONE)).toBe(25);
114
+ });
115
+ it('should calculate age correctly for someone born tomorrow', () => {
116
+ const dob = new CalendarDate(2000, 3, 31);
117
+ expect(ageFromDob(dob, SERVER_TIME_ZONE)).toBe(24);
118
+ });
119
+ it('should handle leap year birthdays', () => {
120
+ const dob = new CalendarDate(2000, 2, 29);
121
+ expect(ageFromDob(dob, SERVER_TIME_ZONE)).toBe(25);
122
+ });
123
+ it('should return undefined for future dates', () => {
124
+ const futureDob = new CalendarDate(2026, 1, 1);
125
+ expect(() => ageFromDob(futureDob, SERVER_TIME_ZONE)).toThrow();
126
+ });
127
+ it('should handle month boundary cases', () => {
128
+ const dob = new CalendarDate(2000, 2, 28);
129
+ expect(ageFromDob(dob, SERVER_TIME_ZONE)).toBe(25);
130
+ });
131
+ it('should handle year boundary cases', () => {
132
+ const dob = new CalendarDate(2000, 12, 31);
133
+ expect(ageFromDob(dob, SERVER_TIME_ZONE)).toBe(24);
134
+ });
135
+ });
136
+ describe('getNextDateOfDay', () => {
137
+ it('returns the next date within the week', () => {
138
+ const date = new CalendarDate(2025, 8, 11); // Monday
139
+ const nextTuesday = getNextDateOfDay('Tuesday', date);
140
+ expect(formatDateISO(nextTuesday)).toBe('2025-08-12');
141
+ const nextSunday = getNextDateOfDay('Sunday', date);
142
+ expect(formatDateISO(nextSunday)).toBe('2025-08-17');
143
+ });
144
+ it('returns the next date crossing over', () => {
145
+ const date = new CalendarDate(2025, 8, 14); // Thursday
146
+ const nextMonday = getNextDateOfDay('Monday', date);
147
+ expect(formatDateISO(nextMonday)).toBe('2025-08-18');
148
+ const nextWednesday = getNextDateOfDay('Wednesday', date);
149
+ expect(formatDateISO(nextWednesday)).toBe('2025-08-20');
150
+ });
151
+ });
152
+ describe('getLastDateOfDay', () => {
153
+ const startDate = new CalendarDate(2024, 4, 10);
154
+ it('returns today when it matches the requested day', () => {
155
+ // Wednesday is day 2
156
+ expect(formatDateISO(getLastDateOfDay('Wednesday', startDate))).toBe('2024-04-10');
157
+ });
158
+ it('returns the most recent occurrence of the requested day', () => {
159
+ // Most recent Monday (day 0) was 2 days ago
160
+ expect(formatDateISO(getLastDateOfDay('Monday', startDate))).toBe('2024-04-08');
161
+ // Most recent Tuesday (day 1) was 1 day ago
162
+ expect(formatDateISO(getLastDateOfDay('Tuesday', startDate))).toBe('2024-04-09');
163
+ // Most recent Thursday (day 3) was 6 days ago
164
+ expect(formatDateISO(getLastDateOfDay('Thursday', startDate))).toBe('2024-04-04');
165
+ });
166
+ it('handles week boundaries correctly', () => {
167
+ // Most recent Sunday (day 6) was 3 days ago
168
+ expect(formatDateISO(getLastDateOfDay('Sunday', startDate))).toBe('2024-04-07');
169
+ });
170
+ });
171
+ describe('getLastDatesOfDay', () => {
172
+ const startDate = new CalendarDate(2024, 4, 10);
173
+ it('returns the most recent date for a given day of the week', () => {
174
+ // If today is Wednesday (day 2), the most recent Monday (day 0) was 2 days ago
175
+ const mondayDates = getLastDatesOfDay('Monday', 1, startDate);
176
+ expect(mondayDates).toHaveLength(1);
177
+ expect(formatDateISO(mondayDates[0])).toBe('2024-04-08');
178
+ // The most recent Tuesday (day 1) was 1 day ago
179
+ const tuesdayDates = getLastDatesOfDay('Tuesday', 1, startDate);
180
+ expect(tuesdayDates).toHaveLength(1);
181
+ expect(formatDateISO(tuesdayDates[0])).toBe('2024-04-09');
182
+ // The most recent Wednesday (day 2) is today
183
+ const wednesdayDates = getLastDatesOfDay('Wednesday', 1, startDate);
184
+ expect(wednesdayDates).toHaveLength(1);
185
+ expect(formatDateISO(wednesdayDates[0])).toBe('2024-04-10');
186
+ });
187
+ it('returns multiple dates when count > 1', () => {
188
+ // Get the last 3 Mondays (ordered from oldest to most recent)
189
+ const mondayDates = getLastDatesOfDay('Monday', 3, startDate);
190
+ expect(mondayDates).toHaveLength(3);
191
+ expect(formatDateISO(mondayDates[0])).toBe('2024-03-25'); // Oldest Monday
192
+ expect(formatDateISO(mondayDates[1])).toBe('2024-04-01'); // Middle Monday
193
+ expect(formatDateISO(mondayDates[2])).toBe('2024-04-08'); // Most recent Monday
194
+ });
195
+ it('returns empty array when count is 0 or negative', () => {
196
+ expect(getLastDatesOfDay('Monday', 0, startDate)).toEqual([]);
197
+ expect(getLastDatesOfDay('Monday', -1, startDate)).toEqual([]);
198
+ });
199
+ it('handles week boundaries correctly when looking back', () => {
200
+ // If today is Wednesday (day 2), the most recent Sunday (day 6) was 3 days ago
201
+ const sundayDates = getLastDatesOfDay('Sunday', 2, startDate);
202
+ expect(sundayDates).toHaveLength(2);
203
+ expect(formatDateISO(sundayDates[0])).toBe('2024-03-31'); // Previous Sunday (oldest)
204
+ expect(formatDateISO(sundayDates[1])).toBe('2024-04-07'); // Most recent Sunday
205
+ });
206
+ });
207
+ describe('getLastMonths', () => {
208
+ const startDate = new CalendarDate(2024, 5, 15);
209
+ it('should return the correct first day of the last 3 months', () => {
210
+ const months = getLastMonths(3, startDate);
211
+ expect(months).toHaveLength(3);
212
+ expect(formatDateISO(months[0])).toBe('2024-03-01');
213
+ expect(formatDateISO(months[1])).toBe('2024-04-01');
214
+ expect(formatDateISO(months[2])).toBe('2024-05-01');
215
+ });
216
+ it('should return the correct first day of the last month', () => {
217
+ const months = getLastMonths(1, startDate);
218
+ expect(months).toHaveLength(1);
219
+ expect(formatDateISO(months[0])).toBe('2024-05-01');
220
+ });
221
+ it('should handle year boundaries correctly', () => {
222
+ // Mock current date to 2024-02-15
223
+ const startDate = new CalendarDate(2024, 2, 15);
224
+ const months = getLastMonths(4, startDate);
225
+ expect(months).toHaveLength(4);
226
+ expect(formatDateISO(months[0])).toBe('2023-11-01');
227
+ expect(formatDateISO(months[1])).toBe('2023-12-01');
228
+ expect(formatDateISO(months[2])).toBe('2024-01-01');
229
+ expect(formatDateISO(months[3])).toBe('2024-02-01');
230
+ });
231
+ it('should return an empty array when count is 0', () => {
232
+ const months = getLastMonths(0, startDate);
233
+ expect(months).toEqual([]);
234
+ });
235
+ it('should return an empty array when count is negative', () => {
236
+ const months = getLastMonths(-2, startDate);
237
+ expect(months).toEqual([]);
238
+ });
239
+ it('should handle the start of a month correctly', () => {
240
+ const startDate = new CalendarDate(2023, 8, 1);
241
+ const months = getLastMonths(3, startDate);
242
+ expect(months).toHaveLength(3);
243
+ expect(formatDateISO(months[0])).toBe('2023-06-01');
244
+ expect(formatDateISO(months[1])).toBe('2023-07-01');
245
+ expect(formatDateISO(months[2])).toBe('2023-08-01');
246
+ });
247
+ it('should handle the end of a month correctly', () => {
248
+ // Mock current date to 2024-03-31
249
+ const startDate = new CalendarDate(2024, 3, 31);
250
+ const months = getLastMonths(2, startDate);
251
+ expect(months).toHaveLength(2);
252
+ expect(formatDateISO(months[0])).toBe('2024-02-01');
253
+ expect(formatDateISO(months[1])).toBe('2024-03-01');
254
+ });
255
+ });
256
+ describe('checkOverlap', () => {
257
+ // Helper Times
258
+ const t0900 = new Time(9, 0);
259
+ const t1000 = new Time(10, 0);
260
+ const t1100 = new Time(11, 0);
261
+ const t1200 = new Time(12, 0);
262
+ const t0000 = new Time(0, 0);
263
+ const t2359 = new Time(23, 59);
264
+ // 1. No Overlap
265
+ it('should return false when Range 1 is strictly before Range 2', () => {
266
+ // R1: 09:00 - 10:00, R2: 11:00 - 12:00
267
+ expect(checkOverlap(t0900, t1000, t1100, t1200)).toBe(false);
268
+ });
269
+ it('should return false when Range 1 is strictly after Range 2', () => {
270
+ // R1: 11:00 - 12:00, R2: 09:00 - 10:00
271
+ expect(checkOverlap(t1100, t1200, t0900, t1000)).toBe(false);
272
+ });
273
+ // 2. Partial Overlap
274
+ it('should return true when Range 1 starts before and ends within Range 2', () => {
275
+ // R1: 09:00 - 11:00, R2: 10:00 - 12:00
276
+ expect(checkOverlap(t0900, t1100, t1000, t1200)).toBe(true);
277
+ });
278
+ it('should return true when Range 2 starts before and ends within Range 1', () => {
279
+ // R1: 10:00 - 12:00, R2: 09:00 - 11:00
280
+ expect(checkOverlap(t1000, t1200, t0900, t1100)).toBe(true);
281
+ });
282
+ // 3. Complete Overlap
283
+ it('should return true when Range 2 is fully contained within Range 1 (subset)', () => {
284
+ // R1: 09:00 - 12:00, R2: 10:00 - 11:00
285
+ expect(checkOverlap(t0900, t1200, t1000, t1100)).toBe(true);
286
+ });
287
+ it('should return true when Range 1 is fully contained within Range 2 (superset)', () => {
288
+ // R1: 10:00 - 11:00, R2: 09:00 - 12:00
289
+ expect(checkOverlap(t1000, t1100, t0900, t1200)).toBe(true);
290
+ });
291
+ // 4. Boundary Conditions (Touching - not considered overlapping)
292
+ it('should return false when end time of Range 1 equals start time of Range 2', () => {
293
+ // R1: 09:00 - 10:00, R2: 10:00 - 11:00
294
+ expect(checkOverlap(t0900, t1000, t1000, t1100)).toBe(false);
295
+ });
296
+ it('should return false when start time of Range 1 equals end time of Range 2', () => {
297
+ // R1: 10:00 - 11:00, R2: 09:00 - 10:00
298
+ expect(checkOverlap(t1000, t1100, t0900, t1000)).toBe(false);
299
+ });
300
+ // 5. Identical Ranges
301
+ it('should return true when ranges are identical', () => {
302
+ // R1: 10:00 - 11:00, R2: 10:00 - 11:00
303
+ expect(checkOverlap(t1000, t1100, t1000, t1100)).toBe(true);
304
+ });
305
+ // 6. Edge Cases
306
+ it('should handle overlap involving the earliest time (00:00)', () => {
307
+ // R1: 00:00 - 09:00, R2: 08:00 - 10:00
308
+ expect(checkOverlap(t0000, t0900, new Time(8, 0), t1000)).toBe(true);
309
+ // R1: 00:00 - 01:00, R2: 01:00 - 02:00 (Boundary)
310
+ expect(checkOverlap(t0000, new Time(1, 0), new Time(1, 0), new Time(2, 0))).toBe(false);
311
+ // R1: 00:00 - 01:00, R2: 02:00 - 03:00 (No overlap)
312
+ expect(checkOverlap(t0000, new Time(1, 0), new Time(2, 0), new Time(3, 0))).toBe(false);
313
+ });
314
+ it('should handle overlap involving the latest time (23:59)', () => {
315
+ // R1: 22:00 - 23:59, R2: 23:00 - 23:59
316
+ expect(checkOverlap(new Time(22, 0), t2359, new Time(23, 0), t2359)).toBe(true);
317
+ // R1: 23:00 - 23:59, R2: 22:00 - 23:00 (Boundary)
318
+ expect(checkOverlap(new Time(23, 0), t2359, new Time(22, 0), new Time(23, 0))).toBe(false);
319
+ // R1: 23:00 - 23:59, R2: 21:00 - 22:00 (No overlap)
320
+ expect(checkOverlap(new Time(23, 0), t2359, new Time(21, 0), new Time(22, 0))).toBe(false);
321
+ });
322
+ it('should handle zero-duration ranges', () => {
323
+ // Zero-duration R1 touching start of R2
324
+ // R1: 10:00 - 10:00, R2: 10:00 - 11:00
325
+ expect(checkOverlap(t1000, t1000, t1000, t1100)).toBe(false);
326
+ // Zero-duration R1 touching end of R2
327
+ // R1: 11:00 - 11:00, R2: 10:00 - 11:00
328
+ expect(checkOverlap(t1100, t1100, t1000, t1100)).toBe(false);
329
+ // Zero-duration R1 inside R2
330
+ // R1: 10:30 - 10:30, R2: 10:00 - 11:00
331
+ const t1030 = new Time(10, 30);
332
+ expect(checkOverlap(t1030, t1030, t1000, t1100)).toBe(true);
333
+ // Zero-duration R1 outside R2
334
+ // R1: 09:00 - 09:00, R2: 10:00 - 11:00
335
+ expect(checkOverlap(t0900, t0900, t1000, t1100)).toBe(false);
336
+ // Two zero-duration ranges at the same time
337
+ // R1: 10:00 - 10:00, R2: 10:00 - 10:00
338
+ expect(checkOverlap(t1000, t1000, t1000, t1000)).toBe(false);
339
+ // Two zero-duration ranges at different times
340
+ // R1: 09:00 - 09:00, R2: 10:00 - 10:00
341
+ expect(checkOverlap(t0900, t0900, t1000, t1000)).toBe(false);
342
+ });
343
+ });
344
+ describe('datesWithin', () => {
345
+ it('should return true when dates are within the specified duration', () => {
346
+ const date1 = new CalendarDate(2024, 5, 1);
347
+ const date2 = new CalendarDate(2024, 5, 3);
348
+ // 3 days duration should include the end date
349
+ expect(datesWithin(date1, date2, { days: 3 })).toBe(true);
350
+ expect(datesWithin(date1, date2, { days: 2 })).toBe(true);
351
+ });
352
+ it('should return false when dates are outside the specified duration', () => {
353
+ const date1 = new CalendarDate(2024, 5, 1);
354
+ const date2 = new CalendarDate(2024, 5, 5);
355
+ // 3 days duration should not include date 4 days later
356
+ expect(datesWithin(date1, date2, { days: 3 })).toBe(false);
357
+ expect(datesWithin(date1, date2, { days: 1 })).toBe(false);
358
+ });
359
+ it('should handle same date as within duration', () => {
360
+ const date = new CalendarDate(2024, 5, 1);
361
+ expect(datesWithin(date, date, { days: 0 })).toBe(true);
362
+ expect(datesWithin(date, date, { days: 1 })).toBe(true);
363
+ });
364
+ it('should return false when date1 is after date2', () => {
365
+ const date1 = new CalendarDate(2024, 5, 5);
366
+ const date2 = new CalendarDate(2024, 5, 1);
367
+ expect(datesWithin(date1, date2, { days: 10 })).toBe(false);
368
+ });
369
+ it('should handle week-based durations', () => {
370
+ const date1 = new CalendarDate(2024, 5, 1);
371
+ const date2 = new CalendarDate(2024, 5, 8); // 7 days later
372
+ expect(datesWithin(date1, date2, { weeks: 1 })).toBe(true);
373
+ expect(datesWithin(date1, date2, { days: 7 })).toBe(true);
374
+ const date3 = new CalendarDate(2024, 5, 15); // 14 days later
375
+ expect(datesWithin(date1, date3, { weeks: 2 })).toBe(true);
376
+ expect(datesWithin(date1, date3, { weeks: 1 })).toBe(false);
377
+ });
378
+ it('should handle month-based durations', () => {
379
+ const date1 = new CalendarDate(2024, 5, 1);
380
+ const date2 = new CalendarDate(2024, 6, 1); // 1 month later
381
+ expect(datesWithin(date1, date2, { months: 1 })).toBe(true);
382
+ expect(datesWithin(date1, date2, { days: 30 })).toBe(false); // May has 31 days
383
+ const date3 = new CalendarDate(2024, 7, 1); // 2 months later
384
+ expect(datesWithin(date1, date3, { months: 2 })).toBe(true);
385
+ expect(datesWithin(date1, date3, { months: 1 })).toBe(false);
386
+ });
387
+ it('should handle year-based durations', () => {
388
+ const date1 = new CalendarDate(2024, 5, 1);
389
+ const date2 = new CalendarDate(2025, 5, 1); // 1 year later
390
+ expect(datesWithin(date1, date2, { years: 1 })).toBe(true);
391
+ expect(datesWithin(date1, date2, { months: 12 })).toBe(true);
392
+ const date3 = new CalendarDate(2026, 5, 1); // 2 years later
393
+ expect(datesWithin(date1, date3, { years: 2 })).toBe(true);
394
+ expect(datesWithin(date1, date3, { years: 1 })).toBe(false);
395
+ });
396
+ it('should handle boundary conditions', () => {
397
+ const date1 = new CalendarDate(2024, 5, 1);
398
+ const date2 = new CalendarDate(2024, 5, 2); // exactly 1 day later
399
+ expect(datesWithin(date1, date2, { days: 1 })).toBe(true);
400
+ const date3 = new CalendarDate(2024, 5, 1);
401
+ const date4 = new CalendarDate(2024, 5, 1); // same day
402
+ expect(datesWithin(date3, date4, { days: 0 })).toBe(true);
403
+ });
404
+ });
405
+ describe('dateDiffWeeks', () => {
406
+ it('should calculate the difference in weeks between two dates', () => {
407
+ const date1 = new CalendarDate(2024, 5, 1);
408
+ const date2 = new CalendarDate(2024, 5, 15);
409
+ expect(dateDiffWeeks(date1, date2)).toBe(2);
410
+ const date3 = new CalendarDate(2024, 5, 1);
411
+ const date4 = new CalendarDate(2024, 5, 8);
412
+ expect(dateDiffWeeks(date3, date4)).toBe(1);
413
+ const date5 = new CalendarDate(2024, 5, 1);
414
+ const date6 = new CalendarDate(2024, 5, 7);
415
+ expect(dateDiffWeeks(date5, date6)).toBe(0);
416
+ const date7 = new CalendarDate(2024, 5, 1);
417
+ const date8 = new CalendarDate(2024, 5, 6);
418
+ expect(dateDiffWeeks(date7, date8)).toBe(0);
419
+ const date9 = new CalendarDate(2024, 5, 1);
420
+ const date10 = new CalendarDate(2024, 5, 2);
421
+ expect(dateDiffWeeks(date9, date10)).toBe(0);
422
+ });
423
+ });
424
+ describe('formatDayShort', () => {
425
+ it('should format all days of the week correctly', () => {
426
+ expect(formatDayShort('Monday')).toBe('Mon');
427
+ expect(formatDayShort('Tuesday')).toBe('Tue');
428
+ expect(formatDayShort('Wednesday')).toBe('Wed');
429
+ expect(formatDayShort('Thursday')).toBe('Thu');
430
+ expect(formatDayShort('Friday')).toBe('Fri');
431
+ expect(formatDayShort('Saturday')).toBe('Sat');
432
+ expect(formatDayShort('Sunday')).toBe('Sun');
433
+ });
434
+ });
435
+ describe('formatDayLetter', () => {
436
+ it('should format all days of the week correctly', () => {
437
+ expect(formatDayLetter('Monday')).toBe('M');
438
+ expect(formatDayLetter('Tuesday')).toBe('T');
439
+ expect(formatDayLetter('Wednesday')).toBe('W');
440
+ expect(formatDayLetter('Thursday')).toBe('T');
441
+ expect(formatDayLetter('Friday')).toBe('F');
442
+ expect(formatDayLetter('Saturday')).toBe('S');
443
+ expect(formatDayLetter('Sunday')).toBe('S');
444
+ });
445
+ });
446
+ describe('formatDateShort', () => {
447
+ it('formats date in DD MMM format', () => {
448
+ // Different dates to test various month and day combinations
449
+ expect(formatDateShort(new CalendarDate(2023, 1, 5))).toBe('5 Jan');
450
+ expect(formatDateShort(new CalendarDate(2023, 5, 15))).toBe('15 May');
451
+ expect(formatDateShort(new CalendarDate(2023, 12, 25))).toBe('25 Dec');
452
+ });
453
+ it('handles single-digit days correctly', () => {
454
+ expect(formatDateShort(new CalendarDate(2023, 7, 1))).toBe('1 Jul');
455
+ expect(formatDateShort(new CalendarDate(2023, 9, 9))).toBe('9 Sept');
456
+ });
457
+ it('handles double-digit days correctly', () => {
458
+ expect(formatDateShort(new CalendarDate(2023, 3, 10))).toBe('10 Mar');
459
+ expect(formatDateShort(new CalendarDate(2023, 11, 30))).toBe('30 Nov');
460
+ });
461
+ it('formats February correctly', () => {
462
+ expect(formatDateShort(new CalendarDate(2023, 2, 14))).toBe('14 Feb');
463
+ expect(formatDateShort(new CalendarDate(2024, 2, 29))).toBe('29 Feb'); // Leap year
464
+ });
465
+ });
466
+ describe('formatDateFull', () => {
467
+ it('formats date in DD MMM YYYY format', () => {
468
+ // Test various dates to ensure consistent formatting
469
+ expect(formatDateFull(new CalendarDate(2023, 1, 5))).toBe('5 Jan 2023');
470
+ expect(formatDateFull(new CalendarDate(2023, 5, 15))).toBe('15 May 2023');
471
+ expect(formatDateFull(new CalendarDate(2023, 12, 25))).toBe('25 Dec 2023');
472
+ });
473
+ it('handles single-digit days correctly', () => {
474
+ expect(formatDateFull(new CalendarDate(2023, 7, 1))).toBe('1 Jul 2023');
475
+ expect(formatDateFull(new CalendarDate(2023, 9, 9))).toBe('9 Sept 2023');
476
+ });
477
+ it('handles double-digit days correctly', () => {
478
+ expect(formatDateFull(new CalendarDate(2023, 3, 10))).toBe('10 Mar 2023');
479
+ expect(formatDateFull(new CalendarDate(2023, 11, 30))).toBe('30 Nov 2023');
480
+ });
481
+ it('handles different years correctly', () => {
482
+ expect(formatDateFull(new CalendarDate(2020, 6, 15))).toBe('15 Jun 2020');
483
+ expect(formatDateFull(new CalendarDate(2024, 6, 15))).toBe('15 Jun 2024');
484
+ expect(formatDateFull(new CalendarDate(2025, 6, 15))).toBe('15 Jun 2025');
485
+ });
486
+ it('handles leap year dates correctly', () => {
487
+ expect(formatDateFull(new CalendarDate(2024, 2, 29))).toBe('29 Feb 2024');
488
+ expect(formatDateFull(new CalendarDate(2020, 2, 29))).toBe('29 Feb 2020');
489
+ });
490
+ it('handles year boundaries correctly', () => {
491
+ expect(formatDateFull(new CalendarDate(2023, 12, 31))).toBe('31 Dec 2023');
492
+ expect(formatDateFull(new CalendarDate(2024, 1, 1))).toBe('1 Jan 2024');
493
+ });
494
+ it('handles all months correctly', () => {
495
+ expect(formatDateFull(new CalendarDate(2023, 1, 15))).toBe('15 Jan 2023');
496
+ expect(formatDateFull(new CalendarDate(2023, 2, 15))).toBe('15 Feb 2023');
497
+ expect(formatDateFull(new CalendarDate(2023, 3, 15))).toBe('15 Mar 2023');
498
+ expect(formatDateFull(new CalendarDate(2023, 4, 15))).toBe('15 Apr 2023');
499
+ expect(formatDateFull(new CalendarDate(2023, 5, 15))).toBe('15 May 2023');
500
+ expect(formatDateFull(new CalendarDate(2023, 6, 15))).toBe('15 Jun 2023');
501
+ expect(formatDateFull(new CalendarDate(2023, 7, 15))).toBe('15 Jul 2023');
502
+ expect(formatDateFull(new CalendarDate(2023, 8, 15))).toBe('15 Aug 2023');
503
+ expect(formatDateFull(new CalendarDate(2023, 9, 15))).toBe('15 Sept 2023');
504
+ expect(formatDateFull(new CalendarDate(2023, 10, 15))).toBe('15 Oct 2023');
505
+ expect(formatDateFull(new CalendarDate(2023, 11, 15))).toBe('15 Nov 2023');
506
+ expect(formatDateFull(new CalendarDate(2023, 12, 15))).toBe('15 Dec 2023');
507
+ });
508
+ });
509
+ describe('formatDateISO', () => {
510
+ it('formats date in YYYY-MM-DD format', () => {
511
+ // Test various dates to ensure consistent ISO format
512
+ expect(formatDateISO(new CalendarDate(2023, 1, 5))).toBe('2023-01-05');
513
+ expect(formatDateISO(new CalendarDate(2023, 5, 15))).toBe('2023-05-15');
514
+ expect(formatDateISO(new CalendarDate(2023, 12, 25))).toBe('2023-12-25');
515
+ });
516
+ it('handles single-digit days with zero padding', () => {
517
+ expect(formatDateISO(new CalendarDate(2023, 7, 1))).toBe('2023-07-01');
518
+ expect(formatDateISO(new CalendarDate(2023, 9, 9))).toBe('2023-09-09');
519
+ });
520
+ it('handles single-digit months with zero padding', () => {
521
+ expect(formatDateISO(new CalendarDate(2023, 1, 15))).toBe('2023-01-15');
522
+ expect(formatDateISO(new CalendarDate(2023, 9, 15))).toBe('2023-09-15');
523
+ });
524
+ it('handles double-digit days and months correctly', () => {
525
+ expect(formatDateISO(new CalendarDate(2023, 10, 10))).toBe('2023-10-10');
526
+ expect(formatDateISO(new CalendarDate(2023, 11, 30))).toBe('2023-11-30');
527
+ });
528
+ it('handles different years correctly', () => {
529
+ expect(formatDateISO(new CalendarDate(2020, 6, 15))).toBe('2020-06-15');
530
+ expect(formatDateISO(new CalendarDate(2024, 6, 15))).toBe('2024-06-15');
531
+ expect(formatDateISO(new CalendarDate(2025, 6, 15))).toBe('2025-06-15');
532
+ });
533
+ it('handles leap year dates correctly', () => {
534
+ expect(formatDateISO(new CalendarDate(2024, 2, 29))).toBe('2024-02-29');
535
+ expect(formatDateISO(new CalendarDate(2020, 2, 29))).toBe('2020-02-29');
536
+ });
537
+ it('handles year boundaries correctly', () => {
538
+ expect(formatDateISO(new CalendarDate(2023, 12, 31))).toBe('2023-12-31');
539
+ expect(formatDateISO(new CalendarDate(2024, 1, 1))).toBe('2024-01-01');
540
+ });
541
+ it('handles all months correctly with proper zero padding', () => {
542
+ expect(formatDateISO(new CalendarDate(2023, 1, 15))).toBe('2023-01-15');
543
+ expect(formatDateISO(new CalendarDate(2023, 2, 15))).toBe('2023-02-15');
544
+ expect(formatDateISO(new CalendarDate(2023, 3, 15))).toBe('2023-03-15');
545
+ expect(formatDateISO(new CalendarDate(2023, 4, 15))).toBe('2023-04-15');
546
+ expect(formatDateISO(new CalendarDate(2023, 5, 15))).toBe('2023-05-15');
547
+ expect(formatDateISO(new CalendarDate(2023, 6, 15))).toBe('2023-06-15');
548
+ expect(formatDateISO(new CalendarDate(2023, 7, 15))).toBe('2023-07-15');
549
+ expect(formatDateISO(new CalendarDate(2023, 8, 15))).toBe('2023-08-15');
550
+ expect(formatDateISO(new CalendarDate(2023, 9, 15))).toBe('2023-09-15');
551
+ expect(formatDateISO(new CalendarDate(2023, 10, 15))).toBe('2023-10-15');
552
+ expect(formatDateISO(new CalendarDate(2023, 11, 15))).toBe('2023-11-15');
553
+ expect(formatDateISO(new CalendarDate(2023, 12, 15))).toBe('2023-12-15');
554
+ });
555
+ it('handles edge cases for date ranges', () => {
556
+ // First day of year
557
+ expect(formatDateISO(new CalendarDate(2023, 1, 1))).toBe('2023-01-01');
558
+ // Last day of year
559
+ expect(formatDateISO(new CalendarDate(2023, 12, 31))).toBe('2023-12-31');
560
+ // February non-leap year
561
+ expect(formatDateISO(new CalendarDate(2023, 2, 28))).toBe('2023-02-28');
562
+ // February leap year
563
+ expect(formatDateISO(new CalendarDate(2024, 2, 29))).toBe('2024-02-29');
564
+ });
565
+ });
566
+ describe('formatDateNum', () => {
567
+ it('formats dates in number format', () => {
568
+ expect(formatDateNum(new CalendarDate(2023, 1, 1))).toBe('01/01/2023');
569
+ expect(formatDateNum(new CalendarDate(2023, 2, 15))).toBe('15/02/2023');
570
+ expect(formatDateNum(new CalendarDate(2023, 3, 20))).toBe('20/03/2023');
571
+ expect(formatDateNum(new CalendarDate(2023, 4, 25))).toBe('25/04/2023');
572
+ expect(formatDateNum(new CalendarDate(2023, 5, 30))).toBe('30/05/2023');
573
+ expect(formatDateNum(new CalendarDate(2023, 6, 5))).toBe('05/06/2023');
574
+ expect(formatDateNum(new CalendarDate(2023, 7, 10))).toBe('10/07/2023');
575
+ expect(formatDateNum(new CalendarDate(2023, 8, 15))).toBe('15/08/2023');
576
+ expect(formatDateNum(new CalendarDate(2023, 9, 20))).toBe('20/09/2023');
577
+ expect(formatDateNum(new CalendarDate(2023, 10, 25))).toBe('25/10/2023');
578
+ expect(formatDateNum(new CalendarDate(2023, 11, 30))).toBe('30/11/2023');
579
+ expect(formatDateNum(new CalendarDate(2023, 12, 5))).toBe('05/12/2023');
580
+ });
581
+ });
582
+ describe('formatMonth', () => {
583
+ it('should format month correctly for all months', () => {
584
+ expect(formatMonth(new CalendarDate(2023, 1, 1))).toBe('Jan 23');
585
+ expect(formatMonth(new CalendarDate(2023, 2, 1))).toBe('Feb 23');
586
+ expect(formatMonth(new CalendarDate(2023, 3, 1))).toBe('Mar 23');
587
+ expect(formatMonth(new CalendarDate(2023, 4, 1))).toBe('Apr 23');
588
+ expect(formatMonth(new CalendarDate(2023, 5, 1))).toBe('May 23');
589
+ expect(formatMonth(new CalendarDate(2023, 6, 1))).toBe('Jun 23');
590
+ expect(formatMonth(new CalendarDate(2023, 7, 1))).toBe('Jul 23');
591
+ expect(formatMonth(new CalendarDate(2023, 8, 1))).toBe('Aug 23');
592
+ expect(formatMonth(new CalendarDate(2023, 9, 1))).toBe('Sept 23');
593
+ expect(formatMonth(new CalendarDate(2023, 10, 1))).toBe('Oct 23');
594
+ expect(formatMonth(new CalendarDate(2023, 11, 1))).toBe('Nov 23');
595
+ expect(formatMonth(new CalendarDate(2023, 12, 1))).toBe('Dec 23');
596
+ });
597
+ it('should return the same month regardless of the day or year', () => {
598
+ expect(formatMonth(new CalendarDate(2023, 1, 31))).toBe('Jan 23');
599
+ expect(formatMonth(new CalendarDate(2024, 1, 1))).toBe('Jan 24');
600
+ });
601
+ });
602
+ describe('formatTimeShort', () => {
603
+ it('should format time correctly with 2-digit hours and 2-digit minutes', () => {
604
+ const time = new Time(14, 30);
605
+ expect(formatTimeShort(time)).toBe('14:30');
606
+ });
607
+ it('should pad single digit hours with leading zero', () => {
608
+ const time = new Time(9, 45);
609
+ expect(formatTimeShort(time)).toBe('09:45');
610
+ });
611
+ it('should pad single digit minutes with leading zero', () => {
612
+ const time = new Time(12, 5);
613
+ expect(formatTimeShort(time)).toBe('12:05');
614
+ });
615
+ it('should pad both single digit hours and minutes with leading zeros', () => {
616
+ const time = new Time(1, 7);
617
+ expect(formatTimeShort(time)).toBe('01:07');
618
+ });
619
+ it('should format midnight correctly', () => {
620
+ const time = new Time(0, 0);
621
+ expect(formatTimeShort(time)).toBe('00:00');
622
+ });
623
+ it('should format end of day correctly', () => {
624
+ const time = new Time(23, 59);
625
+ expect(formatTimeShort(time)).toBe('23:59');
626
+ });
627
+ });
628
+ describe('formatTimeFull', () => {
629
+ it('should format time correctly with hours, minutes, and seconds', () => {
630
+ const time = new Time(14, 30, 45);
631
+ expect(formatTimeFull(time)).toBe('14:30:45');
632
+ });
633
+ it('should pad single digit hours with leading zero', () => {
634
+ const time = new Time(9, 45, 30);
635
+ expect(formatTimeFull(time)).toBe('09:45:30');
636
+ });
637
+ it('should pad single digit minutes with leading zero', () => {
638
+ const time = new Time(12, 5, 30);
639
+ expect(formatTimeFull(time)).toBe('12:05:30');
640
+ });
641
+ it('should pad single digit seconds with leading zero', () => {
642
+ const time = new Time(12, 30, 5);
643
+ expect(formatTimeFull(time)).toBe('12:30:05');
644
+ });
645
+ it('should pad all single digits with leading zeros', () => {
646
+ const time = new Time(1, 7, 9);
647
+ expect(formatTimeFull(time)).toBe('01:07:09');
648
+ });
649
+ it('should format midnight correctly', () => {
650
+ const time = new Time(0, 0, 0);
651
+ expect(formatTimeFull(time)).toBe('00:00:00');
652
+ });
653
+ it('should format end of day correctly', () => {
654
+ const time = new Time(23, 59, 59);
655
+ expect(formatTimeFull(time)).toBe('23:59:59');
656
+ });
657
+ it('should handle times without seconds specified (defaults to 0)', () => {
658
+ const time = new Time(10, 30);
659
+ expect(formatTimeFull(time)).toBe('10:30:00');
660
+ });
661
+ it('should handle times with milliseconds (ignores milliseconds)', () => {
662
+ const time = new Time(15, 45, 30, 500);
663
+ expect(formatTimeFull(time)).toBe('15:45:30');
664
+ });
665
+ });
666
+ describe('formatTimeEnd', () => {
667
+ it('should format the end time correctly for a short duration', () => {
668
+ expect(formatTimeEnd(new Time(10, 0), 30)).toBe('10:30');
669
+ });
670
+ it('should handle hour rollover correctly', () => {
671
+ expect(formatTimeEnd(new Time(9, 45), 30)).toBe('10:15');
672
+ });
673
+ it('should handle day rollover correctly', () => {
674
+ expect(formatTimeEnd(new Time(23, 30), 60)).toBe('00:30');
675
+ });
676
+ it('should handle multiple hour additions', () => {
677
+ expect(formatTimeEnd(new Time(10, 0), 120)).toBe('12:00');
678
+ });
679
+ it('should handle zero duration', () => {
680
+ expect(formatTimeEnd(new Time(15, 30), 0)).toBe('15:30');
681
+ });
682
+ it('should handle large duration across multiple days', () => {
683
+ expect(formatTimeEnd(new Time(12, 0), 1440)).toBe('12:00'); // 24 hours = 1440 minutes
684
+ });
685
+ it('should pad times correctly', () => {
686
+ expect(formatTimeEnd(new Time(9, 5), 5)).toBe('09:10');
687
+ });
688
+ it('should handle minute overflow correctly', () => {
689
+ expect(formatTimeEnd(new Time(14, 55), 10)).toBe('15:05');
690
+ expect(formatTimeEnd(new Time(23, 55), 10)).toBe('00:05');
691
+ });
692
+ it('should handle exact hour boundaries', () => {
693
+ expect(formatTimeEnd(new Time(8, 0), 60)).toBe('09:00');
694
+ expect(formatTimeEnd(new Time(23, 0), 60)).toBe('00:00');
695
+ });
696
+ it('should handle negative durations by treating them as zero', () => {
697
+ // Note: This behavior depends on the Time.add implementation
698
+ // If it doesn't handle negative values, this test might need adjustment
699
+ expect(formatTimeEnd(new Time(10, 30), 0)).toBe('10:30');
700
+ });
701
+ it('should handle very large durations', () => {
702
+ // 25 hours = 1500 minutes, should wrap around to next day + 1 hour
703
+ expect(formatTimeEnd(new Time(10, 0), 1500)).toBe('11:00');
704
+ });
705
+ });
706
+ describe('formatAbsolute', () => {
707
+ it('should format a ZonedDateTime correctly', () => {
708
+ const datetime = new ZonedDateTime(2024, 5, 15, 'Europe/London', 0, 14, 30);
709
+ expect(formatAbsolute(datetime)).toBe('15/05/2024 14:30:00');
710
+ });
711
+ it('should handle midnight correctly', () => {
712
+ const datetime = new ZonedDateTime(2024, 1, 1, 'Europe/London', 0, 0, 0);
713
+ expect(formatAbsolute(datetime)).toBe('01/01/2024 00:00:00');
714
+ });
715
+ it('should handle end of day correctly', () => {
716
+ const datetime = new ZonedDateTime(2024, 12, 31, 'Europe/London', 0, 23, 59);
717
+ expect(formatAbsolute(datetime)).toBe('31/12/2024 23:59:00');
718
+ });
719
+ it('should pad single digit days and months', () => {
720
+ const datetime = new ZonedDateTime(2024, 2, 5, 'Europe/London', 0, 9, 7);
721
+ expect(formatAbsolute(datetime)).toBe('05/02/2024 09:07:00');
722
+ });
723
+ it('should handle leap year dates', () => {
724
+ const datetime = new ZonedDateTime(2024, 2, 29, 'Europe/London', 0, 12, 0);
725
+ expect(formatAbsolute(datetime)).toBe('29/02/2024 12:00:00');
726
+ });
727
+ it('should handle different years', () => {
728
+ const datetime2020 = new ZonedDateTime(2020, 6, 15, 'Europe/London', 0, 10, 30);
729
+ expect(formatAbsolute(datetime2020)).toBe('15/06/2020 10:30:00');
730
+ const datetime2025 = new ZonedDateTime(2025, 6, 15, 'Europe/London', 0, 10, 30);
731
+ expect(formatAbsolute(datetime2025)).toBe('15/06/2025 10:30:00');
732
+ });
733
+ it('should handle all months correctly', () => {
734
+ const datetime1 = new ZonedDateTime(2024, 1, 15, 'Europe/London', 0, 12, 0);
735
+ expect(formatAbsolute(datetime1)).toBe('15/01/2024 12:00:00');
736
+ const datetime12 = new ZonedDateTime(2024, 12, 15, 'Europe/London', 0, 12, 0);
737
+ expect(formatAbsolute(datetime12)).toBe('15/12/2024 12:00:00');
738
+ });
739
+ it('should handle different time zones consistently', () => {
740
+ // Note: The function converts to CalendarDate and Time, so timezone shouldn't affect the output format
741
+ const datetimeUTC = new ZonedDateTime(2024, 5, 15, 'UTC', 0, 14, 30);
742
+ const datetimeNY = new ZonedDateTime(2024, 5, 15, 'America/New_York', 0, 14, 30);
743
+ // Both should format the same since we're using the same local date/time components
744
+ expect(formatAbsolute(datetimeUTC)).toBe('15/05/2024 14:30:00');
745
+ expect(formatAbsolute(datetimeNY)).toBe('15/05/2024 14:30:00');
746
+ });
747
+ it('should include seconds in the time portion', () => {
748
+ const datetime = new ZonedDateTime(2024, 5, 15, 'Europe/London', 0, 14, 30, 45);
749
+ expect(formatAbsolute(datetime)).toBe('15/05/2024 14:30:45');
750
+ });
751
+ it('should handle times with milliseconds (ignores milliseconds)', () => {
752
+ const datetime = new ZonedDateTime(2024, 5, 15, 'Europe/London', 0, 14, 30, 45, 500);
753
+ expect(formatAbsolute(datetime)).toBe('15/05/2024 14:30:45');
754
+ });
755
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,335 @@
1
+ import { beforeEach, describe, expect, it, vi } from 'vitest';
2
+ // Mock global fetch and DOMParser
3
+ const mockFetch = vi.fn();
4
+ global.fetch = mockFetch;
5
+ // Mock DOMParser for XML parsing
6
+ const mockDOMParser = vi.fn(() => ({
7
+ parseFromString: vi.fn((xmlString) => {
8
+ // Simple mock XML parser for testing
9
+ const messageIdMatch = xmlString.match(/<MessageId>(.*?)<\/MessageId>/);
10
+ const errorCodeMatch = xmlString.match(/<Code>(.*?)<\/Code>/);
11
+ const errorMessageMatch = xmlString.match(/<Message>(.*?)<\/Message>/);
12
+ return {
13
+ querySelector: (selector) => {
14
+ if (selector === 'MessageId' && messageIdMatch) {
15
+ return { textContent: messageIdMatch[1] };
16
+ }
17
+ if (selector === 'Error' && (errorCodeMatch || errorMessageMatch)) {
18
+ return {
19
+ querySelector: (innerSelector) => {
20
+ if (innerSelector === 'Code' && errorCodeMatch) {
21
+ return { textContent: errorCodeMatch[1] };
22
+ }
23
+ if (innerSelector === 'Message' && errorMessageMatch) {
24
+ return { textContent: errorMessageMatch[1] };
25
+ }
26
+ return null;
27
+ }
28
+ };
29
+ }
30
+ return null;
31
+ }
32
+ };
33
+ })
34
+ }));
35
+ global.DOMParser = mockDOMParser;
36
+ // Import after mocking
37
+ import { sendEmail } from './ses.js';
38
+ const testAwsCredentials = {
39
+ awsAccessKeyId: 'test-access-key',
40
+ awsRegion: 'us-east-1',
41
+ awsSecretAccessKey: 'test-secret-key'
42
+ };
43
+ describe('sendEmail', () => {
44
+ beforeEach(() => {
45
+ mockFetch.mockClear();
46
+ });
47
+ it('should throw error if options is not provided', async () => {
48
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
49
+ await expect(sendEmail(null)).rejects.toThrow('sendEmail: options is required');
50
+ });
51
+ it('should throw error if "to" field is missing', async () => {
52
+ await expect(sendEmail({
53
+ from: 'sender@example.com',
54
+ subject: 'Test',
55
+ text: 'Test message',
56
+ ...testAwsCredentials
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
58
+ })).rejects.toThrow('at least one valid recipient is required');
59
+ });
60
+ it('should throw error if "to" field is empty string', async () => {
61
+ await expect(sendEmail({
62
+ from: 'sender@example.com',
63
+ subject: 'Test',
64
+ text: 'Test message',
65
+ to: ' ',
66
+ ...testAwsCredentials
67
+ })).rejects.toThrow('at least one valid recipient is required');
68
+ });
69
+ it('should throw error if "to" field is empty array', async () => {
70
+ await expect(sendEmail({
71
+ from: 'sender@example.com',
72
+ subject: 'Test',
73
+ text: 'Test message',
74
+ to: [],
75
+ ...testAwsCredentials
76
+ })).rejects.toThrow('at least one valid recipient is required');
77
+ });
78
+ it('should throw error if "to" field contains only empty strings', async () => {
79
+ await expect(sendEmail({
80
+ from: 'sender@example.com',
81
+ subject: 'Test',
82
+ text: 'Test message',
83
+ to: ['', ' ', ''],
84
+ ...testAwsCredentials
85
+ })).rejects.toThrow('at least one valid recipient is required');
86
+ });
87
+ it('should filter out empty strings from "to" array and accept valid emails', async () => {
88
+ mockFetch.mockResolvedValue({
89
+ ok: true,
90
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
91
+ });
92
+ await sendEmail({
93
+ from: 'sender@example.com',
94
+ subject: 'Test',
95
+ text: 'Test message',
96
+ to: ['', 'valid@example.com', ' ', 'another@example.com'],
97
+ ...testAwsCredentials
98
+ });
99
+ expect(mockFetch).toHaveBeenCalledTimes(1);
100
+ const fetchCall = mockFetch.mock.calls[0];
101
+ const body = fetchCall[1].body;
102
+ expect(body).toContain('Destination.ToAddresses.member.1=valid%40example.com');
103
+ expect(body).toContain('Destination.ToAddresses.member.2=another%40example.com');
104
+ });
105
+ it('should filter out empty strings from cc and bcc arrays', async () => {
106
+ mockFetch.mockResolvedValue({
107
+ ok: true,
108
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
109
+ });
110
+ await sendEmail({
111
+ bcc: [' ', 'bcc@example.com'],
112
+ cc: ['', 'cc@example.com', ' '],
113
+ from: 'sender@example.com',
114
+ subject: 'Test',
115
+ text: 'Test message',
116
+ to: 'test@example.com',
117
+ ...testAwsCredentials
118
+ });
119
+ expect(mockFetch).toHaveBeenCalledTimes(1);
120
+ const fetchCall = mockFetch.mock.calls[0];
121
+ const body = fetchCall[1].body;
122
+ expect(body).toContain('Destination.CcAddresses.member.1=cc%40example.com');
123
+ expect(body).toContain('Destination.BccAddresses.member.1=bcc%40example.com');
124
+ });
125
+ it('should omit cc/bcc if all values are empty strings', async () => {
126
+ mockFetch.mockResolvedValue({
127
+ ok: true,
128
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
129
+ });
130
+ await sendEmail({
131
+ bcc: [' '],
132
+ cc: ['', ' '],
133
+ from: 'sender@example.com',
134
+ subject: 'Test',
135
+ text: 'Test message',
136
+ to: 'test@example.com',
137
+ ...testAwsCredentials
138
+ });
139
+ expect(mockFetch).toHaveBeenCalledTimes(1);
140
+ const fetchCall = mockFetch.mock.calls[0];
141
+ const body = fetchCall[1].body;
142
+ expect(body).not.toContain('Destination.CcAddresses');
143
+ expect(body).not.toContain('Destination.BccAddresses');
144
+ });
145
+ it('should throw error if subject is missing', async () => {
146
+ await expect(sendEmail({
147
+ text: 'Test message',
148
+ to: 'test@example.com',
149
+ ...testAwsCredentials
150
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
151
+ })).rejects.toThrow('subject is required');
152
+ });
153
+ it('should throw error if both text and html are missing', async () => {
154
+ await expect(sendEmail({
155
+ subject: 'Test',
156
+ to: 'test@example.com',
157
+ ...testAwsCredentials
158
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
159
+ })).rejects.toThrow('at least one of text or html body must be provided');
160
+ });
161
+ it('should throw error if from is not provided', async () => {
162
+ await expect(sendEmail({
163
+ subject: 'Test',
164
+ text: 'Test message',
165
+ to: 'test@example.com',
166
+ ...testAwsCredentials
167
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
168
+ })).rejects.toThrow('sender `from` is required');
169
+ });
170
+ it('should accept single recipient as string', async () => {
171
+ mockFetch.mockResolvedValue({
172
+ ok: true,
173
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
174
+ });
175
+ await sendEmail({
176
+ from: 'sender@example.com',
177
+ subject: 'Test',
178
+ text: 'Test message',
179
+ to: 'test@example.com',
180
+ ...testAwsCredentials
181
+ });
182
+ expect(mockFetch).toHaveBeenCalledTimes(1);
183
+ });
184
+ it('should accept multiple recipients as array', async () => {
185
+ mockFetch.mockResolvedValue({
186
+ ok: true,
187
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
188
+ });
189
+ await sendEmail({
190
+ from: 'sender@example.com',
191
+ subject: 'Test',
192
+ text: 'Test message',
193
+ to: ['test1@example.com', 'test2@example.com'],
194
+ ...testAwsCredentials
195
+ });
196
+ expect(mockFetch).toHaveBeenCalledTimes(1);
197
+ });
198
+ it('should format source with fromName when provided', async () => {
199
+ mockFetch.mockResolvedValue({
200
+ ok: true,
201
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
202
+ });
203
+ await sendEmail({
204
+ from: 'sender@example.com',
205
+ fromName: 'Test Sender',
206
+ subject: 'Test',
207
+ text: 'Test message',
208
+ to: 'test@example.com',
209
+ ...testAwsCredentials
210
+ });
211
+ expect(mockFetch).toHaveBeenCalledTimes(1);
212
+ const fetchCall = mockFetch.mock.calls[0];
213
+ const body = fetchCall[1].body;
214
+ expect(body).toContain('Source=Test+Sender+%3Csender%40example.com%3E');
215
+ });
216
+ it('should handle CC and BCC recipients', async () => {
217
+ mockFetch.mockResolvedValue({
218
+ ok: true,
219
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
220
+ });
221
+ await sendEmail({
222
+ bcc: ['bcc1@example.com', 'bcc2@example.com'],
223
+ cc: 'cc@example.com',
224
+ from: 'sender@example.com',
225
+ subject: 'Test',
226
+ text: 'Test message',
227
+ to: 'test@example.com',
228
+ ...testAwsCredentials
229
+ });
230
+ expect(mockFetch).toHaveBeenCalledTimes(1);
231
+ const fetchCall = mockFetch.mock.calls[0];
232
+ const body = fetchCall[1].body;
233
+ expect(body).toContain('Destination.CcAddresses.member.1=cc%40example.com');
234
+ expect(body).toContain('Destination.BccAddresses.member.1=bcc1%40example.com');
235
+ expect(body).toContain('Destination.BccAddresses.member.2=bcc2%40example.com');
236
+ });
237
+ it('should include both HTML and text body when provided', async () => {
238
+ mockFetch.mockResolvedValue({
239
+ ok: true,
240
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
241
+ });
242
+ await sendEmail({
243
+ from: 'sender@example.com',
244
+ html: '<p>HTML version</p>',
245
+ subject: 'Test',
246
+ text: 'Plain text version',
247
+ to: 'test@example.com',
248
+ ...testAwsCredentials
249
+ });
250
+ expect(mockFetch).toHaveBeenCalledTimes(1);
251
+ const fetchCall = mockFetch.mock.calls[0];
252
+ const body = fetchCall[1].body;
253
+ expect(body).toContain('Message.Body.Text.Data=Plain+text+version');
254
+ expect(body).toContain('Message.Body.Html.Data=%3Cp%3EHTML+version%3C%2Fp%3E');
255
+ });
256
+ it('should return MessageId on success', async () => {
257
+ mockFetch.mockResolvedValue({
258
+ ok: true,
259
+ text: async () => '<SendEmailResponse><MessageId>test-message-id-123</MessageId></SendEmailResponse>'
260
+ });
261
+ const result = await sendEmail({
262
+ from: 'sender@example.com',
263
+ subject: 'Test',
264
+ text: 'Test message',
265
+ to: 'test@example.com',
266
+ ...testAwsCredentials
267
+ });
268
+ expect(result).toBe('test-message-id-123');
269
+ });
270
+ it('should throw error when SES response is missing MessageId', async () => {
271
+ mockFetch.mockResolvedValue({
272
+ ok: true,
273
+ text: async () => '<SendEmailResponse></SendEmailResponse>'
274
+ });
275
+ await expect(sendEmail({
276
+ from: 'sender@example.com',
277
+ subject: 'Test',
278
+ text: 'Test message',
279
+ to: 'test@example.com',
280
+ ...testAwsCredentials
281
+ })).rejects.toThrow('SES response did not contain a MessageId');
282
+ });
283
+ it('should handle SES errors gracefully', async () => {
284
+ mockFetch.mockResolvedValue({
285
+ ok: false,
286
+ status: 400,
287
+ statusText: 'Bad Request',
288
+ text: async () => '<ErrorResponse><Error><Code>MessageRejected</Code><Message>Email address is not verified</Message></Error></ErrorResponse>'
289
+ });
290
+ await expect(sendEmail({
291
+ from: 'sender@example.com',
292
+ subject: 'Test',
293
+ text: 'Test message',
294
+ to: 'test@example.com',
295
+ ...testAwsCredentials
296
+ })).rejects.toThrow(/failed to send email/);
297
+ });
298
+ it('should handle reply-to addresses', async () => {
299
+ mockFetch.mockResolvedValue({
300
+ ok: true,
301
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
302
+ });
303
+ await sendEmail({
304
+ from: 'sender@example.com',
305
+ replyTo: 'reply@example.com',
306
+ subject: 'Test',
307
+ text: 'Test message',
308
+ to: 'test@example.com',
309
+ ...testAwsCredentials
310
+ });
311
+ expect(mockFetch).toHaveBeenCalledTimes(1);
312
+ const fetchCall = mockFetch.mock.calls[0];
313
+ const body = fetchCall[1].body;
314
+ expect(body).toContain('ReplyToAddresses.member.1=reply%40example.com');
315
+ });
316
+ it('should handle multiple reply-to addresses', async () => {
317
+ mockFetch.mockResolvedValue({
318
+ ok: true,
319
+ text: async () => '<SendEmailResponse><MessageId>test-message-id</MessageId></SendEmailResponse>'
320
+ });
321
+ await sendEmail({
322
+ from: 'sender@example.com',
323
+ replyTo: ['reply1@example.com', 'reply2@example.com'],
324
+ subject: 'Test',
325
+ text: 'Test message',
326
+ to: 'test@example.com',
327
+ ...testAwsCredentials
328
+ });
329
+ expect(mockFetch).toHaveBeenCalledTimes(1);
330
+ const fetchCall = mockFetch.mock.calls[0];
331
+ const body = fetchCall[1].body;
332
+ expect(body).toContain('ReplyToAddresses.member.1=reply1%40example.com');
333
+ expect(body).toContain('ReplyToAddresses.member.2=reply2%40example.com');
334
+ });
335
+ });
package/package.json CHANGED
@@ -4,16 +4,14 @@
4
4
  "access": "public",
5
5
  "provenance": true
6
6
  },
7
- "version": "1.2.1",
7
+ "version": "1.2.3",
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",
11
11
  "url": "git+https://github.com/Webamoki/Web-svelte.git"
12
12
  },
13
13
  "files": [
14
- "dist",
15
- "!dist/**/*.test.*",
16
- "!dist/**/*.spec.*"
14
+ "dist"
17
15
  ],
18
16
  "sideEffects": [
19
17
  "**/*.css"
@@ -136,7 +134,6 @@
136
134
  "pnpm": "^10.0.0"
137
135
  },
138
136
  "scripts": {
139
- "preinstall": "npx only-allow pnpm",
140
137
  "dev": "vite dev",
141
138
  "build:site": "vite build",
142
139
  "build:lib": "pnpm prepack",