index.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384
  1. import Arrangement, { Holiday } from '../../src/holidays/arrangement';
  2. import dayjs from "../../src/utils/dayjs"; // Import dayjs
  3. import {
  4. isHoliday,
  5. isWorkday,
  6. isInLieu,
  7. getDayDetail,
  8. getHolidaysInRange,
  9. getWorkdaysInRange,
  10. findWorkday,
  11. } from '../../src';
  12. describe('Holiday Functions', () => {
  13. test('should throw an error for invalid date', () => {
  14. // The _validateDate function throws an error for invalid date inputs.
  15. // Note: Coverage tools might misreport the exact 'throw' line within _validateDate
  16. // as uncovered, even though these tests validate its execution by catching the thrown error.
  17. // The error message now uses `typeof dateInput` (the original passed type).
  18. expect(() => isHoliday('invalid-date')).toThrow(
  19. 'unsupported type string, expected type is Date or Dayjs' // typeof 'invalid-date' is 'string'
  20. );
  21. // Test with other functions that use _validateDate with various invalid inputs
  22. expect(() => isWorkday('invalid-date-for-isWorkday')).toThrow(
  23. 'unsupported type string, expected type is Date or Dayjs' // typeof 'invalid-date-for-isWorkday' is 'string'
  24. );
  25. expect(() => getDayDetail('yet-another-invalid-date')).toThrow( // Use a known invalid string
  26. 'unsupported type string, expected type is Date or Dayjs' // typeof 'yet-another-invalid-date' is 'string'
  27. );
  28. // For numeric input like 12345, dayjs(12345) is a valid date (Unix ms timestamp).
  29. // So, _validateDate does NOT throw an error. isInLieu(12345) would then calculate based on that date.
  30. // Assuming 1970-01-01T00:00:12.345Z is not an inLieu day.
  31. expect(isInLieu(12345)).toBe(false); // This was correct.
  32. // Test _validateDate with multiple arguments (indirectly through the range functions that call _validateDate for start and end)
  33. // Use a known invalid string for one and a valid for other to ensure proper handling
  34. expect(() => getHolidaysInRange('invalid-start', '2024-01-01')).toThrow(
  35. 'unsupported type string, expected type is Date or Dayjs'
  36. );
  37. expect(() => getWorkdaysInRange('2024-01-01', 'invalid-end')).toThrow(
  38. 'unsupported type string, expected type is Date or Dayjs'
  39. );
  40. // Specifically target the throw line in _validateDate by providing an input
  41. // (an invalid string) that results in date.isValid() being false.
  42. // The error message will use `typeof dateInput` (which is 'string' here).
  43. const testThrowLineDirectlyViaGetDayDetail = () => {
  44. getDayDetail('final-check-invalid-date');
  45. };
  46. expect(testThrowLineDirectlyViaGetDayDetail).toThrow('unsupported type string, expected type is Date or Dayjs');
  47. // Test with an actual invalid Date object.
  48. // dayjs(new Date('foo')) results in a Dayjs object where isValid() is false.
  49. // typeof dateInput (the Date object itself) is 'object'.
  50. expect(() => isHoliday(new Date('foo'))).toThrow('unsupported type object, expected type is Date or Dayjs');
  51. });
  52. describe.each([
  53. { fn: isHoliday, fnName: 'isHoliday', cases: [
  54. { date: '2024-05-01', expected: true, desc: 'Labour Day' },
  55. { date: '2024-05-06', expected: false, desc: 'Regular Monday' },
  56. { date: '2024-01-01', expected: true, desc: "New Year's Day 2024" },
  57. { date: '2023-12-31', expected: true, desc: 'Sunday NYE 2023 (Weekend)' },
  58. { date: '2024-02-29', expected: false, desc: 'Leap day 2024 (Thursday)' },
  59. { date: '2023-02-28', expected: false, desc: 'Non-leap year Feb 28th (Tuesday)' },
  60. { date: '2024-10-05', expected: true, desc: 'National Day Holiday Period (Saturday)' }, // Weekend during holiday period
  61. { date: '2024-10-07', expected: true, desc: 'National Day Holiday Period (Monday)' }, // Weekday during holiday period
  62. { date: '2024-04-04', expected: true, desc: 'Tomb Sweeping Day 2024 (Thursday)' },
  63. { date: '2024-04-06', expected: true, desc: 'Tomb Sweeping Day makeup (Saturday)' },
  64. { date: '2024-04-07', expected: false, desc: 'Tomb Sweeping Day makeup work (Sunday)' }, // This is a workday
  65. ]},
  66. { fn: isWorkday, fnName: 'isWorkday', cases: [
  67. { date: '2024-05-01', expected: false, desc: 'Labour Day' },
  68. { date: '2024-05-06', expected: true, desc: 'Regular Monday' },
  69. { date: '2024-01-01', expected: false, desc: "New Year's Day 2024" },
  70. { date: '2023-12-31', expected: false, desc: 'Sunday NYE 2023 (Weekend)' },
  71. { date: '2024-02-29', expected: true, desc: 'Leap day 2024 (Thursday)' },
  72. { date: '2023-02-28', expected: true, desc: 'Non-leap year Feb 28th (Tuesday)' },
  73. { date: '2024-10-05', expected: false, desc: 'National Day Holiday Period (Saturday)' },
  74. { date: '2024-10-07', expected: false, desc: 'National Day Holiday Period (Monday)' },
  75. { date: '2024-04-07', expected: true, desc: 'Tomb Sweeping Day makeup work (Sunday)' }, // This is a workday
  76. { date: '2024-05-11', expected: true, desc: 'Labour Day makeup work (Saturday)' }, // This is a workday
  77. ]},
  78. { fn: isInLieu, fnName: 'isInLieu', cases: [
  79. { date: '2024-05-01', expected: false, desc: 'Labour Day (actual holiday, not in lieu)' }, // Labour day itself is not "inLieu" but part of a holiday period that has inLieu days
  80. { date: '2024-05-03', expected: true, desc: 'Labour Day holiday period (in lieu)' },
  81. { date: '2024-05-06', expected: false, desc: 'Regular Monday (not in lieu)' },
  82. { date: '2024-02-15', expected: true, desc: 'Spring Festival (in lieu)' }, // Feb 15 is an inLieu day
  83. { date: '2024-02-16', expected: true, desc: 'Spring Festival (in lieu)' }, // Feb 16 is an inLieu day
  84. { date: '2024-02-17', expected: false, desc: 'Spring Festival holiday (Saturday), but not specifically inLieu' }, // Feb 17 is a holiday, not inLieu
  85. { date: '2024-02-18', expected: false, desc: 'Spring Festival makeup work (Sunday), not inLieu holiday'},
  86. ]},
  87. ])('$fnName', ({ fn, cases }) => {
  88. test.each(cases)('$fnName("$date") should be $expected ($desc)', ({ date, expected }) => {
  89. expect(fn(date)).toBe(expected);
  90. });
  91. });
  92. test('getDayDetail should return correct details', () => {
  93. const date = '2024-04-29'; // Regular Monday
  94. const detail = getDayDetail(date);
  95. expect(detail).toEqual({
  96. date: '2024-04-29',
  97. work: true,
  98. name: "Monday",
  99. });
  100. });
  101. // Refactor getDayDetail tests to be data-driven
  102. describe('getDayDetail', () => {
  103. const testCases = [
  104. {
  105. date: '2025-01-26', // Sunday, but a makeup workday for Spring Festival
  106. expected: { date: '2025-01-26', work: true, name: "Spring Festival,春节,4" },
  107. desc: 'Makeup workday (Sunday) for Spring Festival'
  108. },
  109. {
  110. date: '2024-05-01', // Labour Day (Holiday)
  111. expected: { date: '2024-05-01', work: false, name: "Labour Day,劳动节,1" },
  112. desc: 'Actual holiday (Labour Day)'
  113. },
  114. {
  115. date: '2025-05-01', // Labour Day 2025 (Holiday)
  116. expected: { date: '2025-05-01', work: false, name: "Labour Day,劳动节,2" },
  117. desc: 'Actual holiday (Labour Day 2025)'
  118. },
  119. {
  120. date: '2024-09-17', // Mid-Autumn Festival 2024 (Holiday)
  121. expected: { date: '2024-09-17', work: false, name: "Mid-autumn Festival,中秋,1" },
  122. desc: 'Mid-Autumn Festival (Holiday)'
  123. },
  124. {
  125. date: '2024-09-14', // Saturday, but a makeup workday for Mid-Autumn Festival
  126. expected: { date: '2024-09-14', work: true, name: "Mid-autumn Festival,中秋,1" }, // Makeup days often refer to the primary holiday name
  127. desc: 'Makeup workday (Saturday) for Mid-Autumn Festival'
  128. },
  129. {
  130. date: '2024-07-06', // Regular Saturday (Weekend)
  131. expected: { date: '2024-07-06', work: false, name: "Saturday" },
  132. desc: 'Regular weekend (Saturday)'
  133. },
  134. {
  135. date: '2024-07-08', // Regular Monday
  136. expected: { date: '2024-07-08', work: true, name: "Monday" },
  137. desc: 'Regular weekday (Monday)'
  138. }
  139. ];
  140. test.each(testCases)('should return correct details for $date ($desc)', ({ date, expected }) => {
  141. const detail = getDayDetail(date);
  142. expect(detail).toEqual(expected);
  143. });
  144. });
  145. test('getHolidaysInRange should return correct holidays within a range', () => {
  146. const start = '2024-05-01';
  147. const end = '2024-05-31';
  148. const holidaysInRange = getHolidaysInRange(start, end, false); // Only official holidays
  149. // Labour Day 2024 is May 1-5. "false" means don't include *normal* weekends.
  150. // But if a weekend day IS an official holiday, it should be included.
  151. expect(holidaysInRange).toEqual([
  152. "2024-05-01", "2024-05-02", "2024-05-03", "2024-05-04", "2024-05-05"
  153. ]);
  154. });
  155. test('getHolidaysInRange should return correct holidays including weekends within a range', () => {
  156. const start = '2024-05-01';
  157. const end = '2024-05-05'; // Short range covering Labour day and a weekend
  158. const holidaysInRange = getHolidaysInRange(start, end, true); // Include weekends
  159. expect(holidaysInRange).toEqual([
  160. "2024-05-01", // Holiday
  161. "2024-05-02", // Holiday
  162. "2024-05-03", // Holiday
  163. "2024-05-04", // Saturday
  164. "2024-05-05", // Sunday
  165. ]);
  166. });
  167. // Adding more tests for getHolidaysInRange for edge cases
  168. describe('getHolidaysInRange - Edge Cases', () => {
  169. test('should handle year boundaries', () => {
  170. const holidays = getHolidaysInRange('2023-12-30', '2024-01-02', true);
  171. expect(holidays).toEqual(['2023-12-30', '2023-12-31', '2024-01-01']); // Dec 30 (Sat), Dec 31 (Sun), Jan 1 (Holiday)
  172. });
  173. test('should handle leap year February', () => {
  174. const holidays = getHolidaysInRange('2024-02-28', '2024-03-02', true);
  175. // Feb 28 (Wed, Workday), Feb 29 (Thu, Workday), Mar 1 (Fri, Workday), Mar 2 (Sat, Weekend)
  176. expect(holidays).toEqual(['2024-03-02']);
  177. });
  178. test('should return empty array for a range with no holidays', () => {
  179. const holidays = getHolidaysInRange('2024-07-08', '2024-07-09', false); // Mon, Tue (no official holidays)
  180. expect(holidays).toEqual([]);
  181. });
  182. test('should return only weekends if no official holidays in range and includeWeekends is true', () => {
  183. const holidays = getHolidaysInRange('2024-07-08', '2024-07-14', true); // Range includes a weekend
  184. expect(holidays).toEqual(['2024-07-13', '2024-07-14']);
  185. });
  186. });
  187. test('getWorkdaysInRange should return correct workdays within a range (excluding weekends unless makeup)', () => {
  188. const start = '2024-05-01'; // Wed (Holiday)
  189. const end = '2024-05-12'; // Sun (Weekend, but 5/11 is makeup workday)
  190. const workdaysInRange = getWorkdaysInRange(start, end, false); // Exclude normal weekends
  191. // Based on current code logic: includeWeekends:false means only Mon-Fri workdays.
  192. // So, 2024-05-11 (Saturday, makeup workday) should be excluded.
  193. expect(workdaysInRange).toEqual([
  194. // 2024-05-01 to 05-05 are Labour Day holidays
  195. '2024-05-06', // Mon
  196. '2024-05-07', // Tue
  197. '2024-05-08', // Wed
  198. '2024-05-09', // Thu
  199. '2024-05-10', // Fri
  200. // '2024-05-11', // Sat (Makeup workday) - EXCLUDED due to includeWeekends: false
  201. ]);
  202. });
  203. test('getWorkdaysInRange should return correct workdays including normal workdays on weekends', () => {
  204. const start = '2024-05-01';
  205. const end = '2024-05-12';
  206. const workdaysInRange = getWorkdaysInRange(start, end, true); // Include all workdays (normal + makeup)
  207. expect(workdaysInRange).toEqual([
  208. '2024-05-06',
  209. '2024-05-07',
  210. '2024-05-08',
  211. '2024-05-09',
  212. '2024-05-10',
  213. '2024-05-11', // Makeup workday
  214. ]);
  215. });
  216. // Adding more tests for getWorkdaysInRange for edge cases
  217. describe('getWorkdaysInRange - Edge Cases', () => {
  218. test('should handle year boundaries', () => {
  219. const workdays = getWorkdaysInRange('2023-12-30', '2024-01-03', true);
  220. // 2023-12-30 (Sat), 2023-12-31 (Sun), 2024-01-01 (Mon, Holiday NYD)
  221. // 2024-01-02 (Tue), 2024-01-03 (Wed)
  222. expect(workdays).toEqual(['2024-01-02', '2024-01-03']);
  223. });
  224. test('should handle leap year February', () => {
  225. const workdays = getWorkdaysInRange('2024-02-28', '2024-03-02', true);
  226. // Feb 28 (Wed), Feb 29 (Thu), Mar 1 (Fri), Mar 2 (Sat, Weekend)
  227. expect(workdays).toEqual(['2024-02-28', '2024-02-29', '2024-03-01']);
  228. });
  229. test('should return empty array for a range with no workdays (e.g. full holiday period)', () => {
  230. const workdays = getWorkdaysInRange('2024-10-01', '2024-10-07', true); // National Day holiday week
  231. expect(workdays).toEqual([]);
  232. });
  233. });
  234. test('findWorkday should return correct workday', () => {
  235. const date = '2024-05-01'; // Labour Day (Holiday)
  236. const nextWorkday = findWorkday(1, date); // Next workday from May 1st
  237. expect(nextWorkday).toBe('2024-05-06'); // May 6th is the first workday after Labour day holiday period
  238. });
  239. // Refactor findWorkday tests to be data-driven
  240. describe('findWorkday', () => {
  241. const testCases = [
  242. // Positive delta
  243. { delta: 1, date: '2024-05-01', expected: '2024-05-06', desc: 'Next workday after a holiday' },
  244. { delta: 1, date: '2024-05-06', expected: '2024-05-07', desc: 'Next workday from a workday' },
  245. { delta: 1, date: '2024-05-10', expected: '2024-05-11', desc: 'Next workday is a makeup Saturday workday' },
  246. { delta: 2, date: '2024-05-10', expected: '2024-05-13', desc: 'Second next workday, skipping weekend after makeup Saturday' },
  247. { delta: 1, date: '2023-12-29', expected: '2024-01-02', desc: 'Next workday across New Year holiday' }, // Fri -> Tue (Mon is NYD)
  248. // Negative delta
  249. // Corrected expectation: Previous workday for May 6 (Mon) is Apr 30 (Tue), as May 1-5 are holidays and Apr 28 (Sun) was a workday before that.
  250. // findWorkday(-1, '2024-05-06') result is '2024-04-30'
  251. { delta: -1, date: '2024-05-06', expected: '2024-04-30', desc: 'Previous workday from Mon, skipping Labour Day holiday period to Apr 30' },
  252. { delta: -1, date: '2024-05-13', expected: '2024-05-11', desc: 'Previous workday is a makeup Saturday' },
  253. { delta: -1, date: '2024-01-02', expected: '2023-12-29', desc: 'Previous workday across New Year holiday' }, // Tue -> Fri (Mon is NYD)
  254. // Zero delta
  255. { delta: 0, date: '2024-05-11', expected: '2024-05-11', desc: 'Current day is a makeup Saturday workday' },
  256. { delta: 0, date: '2024-05-12', expected: '2024-05-13', desc: 'Current day is Sunday (holiday), finds next workday' }, // Sunday, next is Monday
  257. { delta: 0, date: '2024-05-01', expected: '2024-05-06', desc: 'Current day is Labour Day (holiday), finds next workday'}, // May 1st is holiday, next workday is May 6th
  258. { delta: 0, date: '2024-07-08', expected: '2024-07-08', desc: 'Current day is a regular workday (Monday)'},
  259. // New specific tests for line 86 (if (isWorkday(date)) inside while loop)
  260. {
  261. delta: 1,
  262. date: '2024-05-04', // Saturday (Holiday)
  263. expected: '2024-05-06', // Next workday is Monday
  264. desc: 'Line 86: Loop hits Holiday (Sun), then Workday (Mon)'
  265. // Iteration 1: date becomes 2024-05-05 (Sun, Holiday). isWorkday(date) is FALSE. daysToAdd = 1.
  266. // Iteration 2: date becomes 2024-05-06 (Mon, Workday). isWorkday(date) is TRUE. daysToAdd = 0. Loop ends.
  267. },
  268. {
  269. delta: 2,
  270. date: '2024-05-04', // Saturday (Holiday)
  271. expected: '2024-05-07', // Second workday
  272. desc: 'Line 86: Loop hits Hol, Work, Work'
  273. // Iteration 1: date becomes 2024-05-05 (Sun, Holiday). isWorkday(date) is FALSE. daysToAdd = 2.
  274. // Iteration 2: date becomes 2024-05-06 (Mon, Workday). isWorkday(date) is TRUE. daysToAdd = 1.
  275. // Iteration 3: date becomes 2024-05-07 (Tue, Workday). isWorkday(date) is TRUE. daysToAdd = 0. Loop ends.
  276. }
  277. ];
  278. test.each(testCases)('findWorkday($delta, "$date") should be $expected ($desc)', ({ delta, date, expected }) => {
  279. expect(findWorkday(delta, date)).toBe(expected);
  280. });
  281. describe('findWorkday with default date (today)', () => {
  282. let originalDayjs: typeof dayjs;
  283. beforeEach(() => {
  284. originalDayjs = dayjs; // Store original dayjs
  285. });
  286. afterEach(() => {
  287. // @ts-ignore
  288. dayjs = originalDayjs; // Restore original dayjs
  289. });
  290. test('should return today if today is a workday and delta is 0', () => {
  291. const mockTodayWorkday = "2024-05-06"; // Monday, a known workday
  292. // @ts-ignore
  293. dayjs = jest.fn((dateInput?: any) => {
  294. if (dateInput === undefined || dateInput === null || dateInput === '') {
  295. return originalDayjs(mockTodayWorkday);
  296. }
  297. return originalDayjs(dateInput);
  298. });
  299. Object.assign(dayjs, originalDayjs);
  300. expect(findWorkday(0)).toBe(mockTodayWorkday);
  301. });
  302. test('should return next workday if today is a holiday and delta is 0', () => {
  303. const mockTodayHoliday = "2024-05-05"; // Sunday, a known holiday
  304. // @ts-ignore
  305. dayjs = jest.fn((dateInput?: any) => {
  306. if (dateInput === undefined || dateInput === null || dateInput === '') {
  307. return originalDayjs(mockTodayHoliday);
  308. }
  309. return originalDayjs(dateInput);
  310. });
  311. Object.assign(dayjs, originalDayjs);
  312. expect(findWorkday(0)).toBe("2024-05-06"); // Next workday
  313. });
  314. test('should return next workday if today is a workday and delta is 1', () => {
  315. const mockTodayWorkday = "2024-05-06"; // Monday, a known workday
  316. // @ts-ignore
  317. dayjs = jest.fn((dateInput?: any) => {
  318. if (dateInput === undefined || dateInput === null || dateInput === '') {
  319. return originalDayjs(mockTodayWorkday);
  320. }
  321. return originalDayjs(dateInput);
  322. });
  323. Object.assign(dayjs, originalDayjs);
  324. expect(findWorkday(1)).toBe("2024-05-07");
  325. });
  326. });
  327. });
  328. });
  329. describe('Arrangement Class', () => {
  330. let arrangement: Arrangement;
  331. beforeEach(() => {
  332. arrangement = new Arrangement();
  333. });
  334. test('should correctly handle 2023 holidays', () => {
  335. arrangement.y(2024)
  336. .ny().r(1, 1)
  337. .s().r(2, 10).to(2, 17).w(2, 4).w(2, 18).i(2, 15).to(2, 16)
  338. .t().r(4, 4).to(4, 6).w(4, 7).i(4, 5)
  339. .l().r(5, 1).to(5, 5).w(4, 28).w(5, 11).i(5, 2).to(5, 3)
  340. .d().r(6, 10)
  341. .m().r(9, 15).to(9, 17).w(9, 14).i(9, 16)
  342. .n().r(10, 1).to(10, 7).w(9, 29).w(10, 12).i(10, 4).i(10, 7)
  343. expect(arrangement.holidays).toHaveProperty('2024-05-01');
  344. expect(arrangement.holidays).toHaveProperty('2024-05-02');
  345. expect(arrangement.holidays).toHaveProperty('2024-05-04');
  346. expect(arrangement.holidays).toHaveProperty('2024-05-05');
  347. expect(arrangement.workdays).toHaveProperty('2024-04-28');
  348. expect(arrangement.workdays).toHaveProperty('2024-05-11');
  349. });
  350. });