@webamoki/web-svelte 1.2.2 → 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.
|
|
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"
|