index.test.ts 15 KB

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