index.test.ts 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. import Arrangement, { Holiday } from '../../src/holidays/arrangement';
  2. import dayjs from "../../src/utils/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. // _validateDate 函数会对无效的日期输入抛出错误。
  15. // 注意:即使这些测试通过捕获抛出的错误来验证其执行,
  16. //覆盖率工具仍可能错误地报告 _validateDate 中的确切 'throw' 语句行未被覆盖。
  17. // 错误信息现在使用 `typeof dateInput` (原始传入类型)。
  18. expect(() => isHoliday('invalid-date')).toThrow(
  19. 'unsupported type string, expected type is Date or Dayjs' // typeof 'invalid-date' 为 'string'
  20. );
  21. // 测试其他使用 _validateDate 的函数处理各种无效输入的情况
  22. expect(() => isWorkday('invalid-date-for-isWorkday')).toThrow(
  23. 'unsupported type string, expected type is Date or Dayjs' // typeof 'invalid-date-for-isWorkday' 为 'string'
  24. );
  25. expect(() => getDayDetail('yet-another-invalid-date')).toThrow( // 使用一个已知的无效字符串
  26. 'unsupported type string, expected type is Date or Dayjs' // typeof 'yet-another-invalid-date' 为 'string'
  27. );
  28. // 对于像 12345 这样的数字输入,dayjs(12345) 是一个有效的日期 (Unix毫秒时间戳)。
  29. // 因此,_validateDate 不会抛出错误。isInLieu(12345) 会基于该日期进行计算。
  30. // 假设 1970-01-01T00:00:12.345Z 不是一个调休日。
  31. expect(isInLieu(12345)).toBe(false); // 此前的判断是正确的。
  32. // 通过调用 _validateDate 获取开始和结束日期的范围函数,间接测试 _validateDate
  33. // 使用一个已知的无效字符串作为一端,一个有效日期作为另一端,以确保正确处理
  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. // 通过提供一个导致 date.isValid() 返回 false 的无效字符串输入,
  41. // 特别针对 _validateDate 中的 throw 语句行。
  42. // 错误信息将使用 `typeof dateInput` (此处为 'string')。
  43. const testThrowLineDirectlyViaGetDayDetail = () => {
  44. getDayDetail('final-check-invalid-date');
  45. };
  46. expect(testThrowLineDirectlyViaGetDayDetail).toThrow('unsupported type string, expected type is Date or Dayjs');
  47. // 使用一个实际的无效 Date 对象进行测试。
  48. // dayjs(new Date('foo')) 会产生一个 isValid() 为 false 的 Dayjs 对象。
  49. // typeof dateInput (Date 对象本身) 是 '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: '劳动节' },
  55. { date: '2024-05-06', expected: false, desc: '普通星期一' },
  56. { date: '2024-01-01', expected: true, desc: '2024年元旦' },
  57. { date: '2023-12-31', expected: true, desc: '2023年元旦前夕的周日 (周末)' },
  58. { date: '2024-02-29', expected: false, desc: '2024年闰日 (星期四)' },
  59. { date: '2023-02-28', expected: false, desc: '非闰年2月28日 (星期二)' },
  60. { date: '2024-10-05', expected: true, desc: '国庆节假日期间 (星期六)' }, // 节假日期间的周末
  61. { date: '2024-10-07', expected: true, desc: '国庆节假日期间 (星期一)' }, // 节假日期间的工作日
  62. { date: '2024-04-04', expected: true, desc: '2024年清明节 (星期四)' },
  63. { date: '2024-04-06', expected: true, desc: '清明节调休 (星期六)' },
  64. { date: '2024-04-07', expected: false, desc: '清明节调休上班 (星期日)' }, // 这是工作日
  65. ]},
  66. { fn: isWorkday, fnName: 'isWorkday', cases: [
  67. { date: '2024-05-01', expected: false, desc: '劳动节' },
  68. { date: '2024-05-06', expected: true, desc: '普通星期一' },
  69. { date: '2024-01-01', expected: false, desc: '2024年元旦' },
  70. { date: '2023-12-31', expected: false, desc: '2023年元旦前夕的周日 (周末)' },
  71. { date: '2024-02-29', expected: true, desc: '2024年闰日 (星期四)' },
  72. { date: '2023-02-28', expected: true, desc: '非闰年2月28日 (星期二)' },
  73. { date: '2024-10-05', expected: false, desc: '国庆节假日期间 (星期六)' },
  74. { date: '2024-10-07', expected: false, desc: '国庆节假日期间 (星期一)' },
  75. { date: '2024-04-07', expected: true, desc: '清明节调休上班 (星期日)' }, // 这是工作日
  76. { date: '2024-05-11', expected: true, desc: '劳动节调休上班 (星期六)' }, // 这是工作日
  77. ]},
  78. { fn: isInLieu, fnName: 'isInLieu', cases: [
  79. { date: '2024-05-01', expected: false, desc: '劳动节 (节日本身,非调休)' }, // 劳动节本身不是“调休”产生的假日,但它所属的假期包含调休日
  80. { date: '2024-05-03', expected: true, desc: '劳动节假期 (调休)' },
  81. { date: '2024-05-06', expected: false, desc: '普通星期一 (非调休)' },
  82. { date: '2024-02-15', expected: true, desc: '春节 (调休)' }, // 2月15日是调休
  83. { date: '2024-02-16', expected: true, desc: '春节 (调休)' }, // 2月16日是调休
  84. { date: '2024-02-17', expected: false, desc: '春节假期 (星期六),但非特指调休性质的假日' }, // 2月17日是春节假期,但不是“inLieu”定义的调休产生的假日
  85. { date: '2024-02-18', expected: false, desc: '春节调休上班 (星期日), 非调休性质的假日'},
  86. ]},
  87. ])('$fnName', ({ fn, cases }) => {
  88. test.each(cases)('$fnName("$date") 应该为 $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'; // 普通星期一
  94. const detail = getDayDetail(date);
  95. expect(detail).toEqual({
  96. date: '2024-04-29',
  97. work: true,
  98. name: "Monday",
  99. });
  100. });
  101. describe('getDayDetail', () => {
  102. const testCases = [
  103. {
  104. date: '2025-01-26', // 周日,但是春节的调休工作日
  105. expected: { date: '2025-01-26', work: true, name: "Spring Festival,春节,4" },
  106. desc: '春节的调休工作日 (周日)'
  107. },
  108. {
  109. date: '2024-05-01', // 劳动节 (节假日)
  110. expected: { date: '2024-05-01', work: false, name: "Labour Day,劳动节,1" },
  111. desc: '实际节假日 (劳动节)'
  112. },
  113. {
  114. date: '2025-05-01', // 2025年劳动节 (节假日)
  115. expected: { date: '2025-05-01', work: false, name: "Labour Day,劳动节,2" },
  116. desc: '实际节假日 (2025年劳动节)'
  117. },
  118. {
  119. date: '2024-09-17', // 2024年中秋节 (节假日)
  120. expected: { date: '2024-09-17', work: false, name: "Mid-autumn Festival,中秋,1" },
  121. desc: '中秋节 (节假日)'
  122. },
  123. {
  124. date: '2024-09-14', // 周六,但是中秋节的调休工作日
  125. expected: { date: '2024-09-14', work: true, name: "Mid-autumn Festival,中秋,1" }, // 调休工作日通常关联主要节假日名称
  126. desc: '中秋节的调休工作日 (周六)'
  127. },
  128. {
  129. date: '2024-07-06', // 普通周六 (周末)
  130. expected: { date: '2024-07-06', work: false, name: "Saturday" },
  131. desc: '普通周末 (周六)'
  132. },
  133. {
  134. date: '2024-07-08', // 普通星期一
  135. expected: { date: '2024-07-08', work: true, name: "Monday" },
  136. desc: '普通工作日 (星期一)'
  137. }
  138. ];
  139. test.each(testCases)('对于日期 $date ($desc),应返回正确的详情', ({ date, expected }) => {
  140. const detail = getDayDetail(date);
  141. expect(detail).toEqual(expected);
  142. });
  143. });
  144. test('getHolidaysInRange should return correct holidays within a range', () => {
  145. const start = '2024-05-01';
  146. const end = '2024-05-31';
  147. const holidaysInRange = getHolidaysInRange(start, end, false); // 仅官方节假日
  148. // 2024年劳动节是5月1-5日。"false"表示不包含*常规*周末。
  149. // 但如果周末某天是官方假期的一部分,则应包含在内。
  150. expect(holidaysInRange).toEqual([
  151. "2024-05-01", "2024-05-02", "2024-05-03", "2024-05-04", "2024-05-05"
  152. ]);
  153. });
  154. test('getHolidaysInRange should return correct holidays including weekends within a range', () => {
  155. const start = '2024-05-01';
  156. const end = '2024-05-05'; // 包含劳动节和周末的短范围
  157. const holidaysInRange = getHolidaysInRange(start, end, true); // 包含周末
  158. expect(holidaysInRange).toEqual([
  159. "2024-05-01", // 节假日
  160. "2024-05-02", // 节假日
  161. "2024-05-03", // 节假日
  162. "2024-05-04", // 周六
  163. "2024-05-05", // 周日
  164. ]);
  165. });
  166. // getHolidaysInRange 边缘情况的更多测试
  167. describe('getHolidaysInRange - Edge Cases', () => {
  168. test('should handle year boundaries', () => {
  169. const holidays = getHolidaysInRange('2023-12-30', '2024-01-02', true);
  170. expect(holidays).toEqual(['2023-12-30', '2023-12-31', '2024-01-01']); // 12月30日(周六), 12月31日(周日), 1月1日(节假日)
  171. });
  172. test('should handle leap year February', () => {
  173. const holidays = getHolidaysInRange('2024-02-28', '2024-03-02', true);
  174. // 2月28日(周三, 工作日), 2月29日(周四, 工作日), 3月1日(周五, 工作日), 3月2日(周六, 周末)
  175. expect(holidays).toEqual(['2024-03-02']);
  176. });
  177. test('should return empty array for a range with no holidays', () => {
  178. const holidays = getHolidaysInRange('2024-07-08', '2024-07-09', false); // 周一, 周二 (没有官方节假日)
  179. expect(holidays).toEqual([]);
  180. });
  181. test('should return only weekends if no official holidays in range and includeWeekends is true', () => {
  182. const holidays = getHolidaysInRange('2024-07-08', '2024-07-14', true); // 范围包含一个周末
  183. expect(holidays).toEqual(['2024-07-13', '2024-07-14']);
  184. });
  185. });
  186. test('getWorkdaysInRange should return correct workdays within a range (excluding weekends unless makeup)', () => {
  187. const start = '2024-05-01'; // 周三 (节假日)
  188. const end = '2024-05-12'; // 周日 (周末, 但5月11日是调休工作日)
  189. const workdaysInRange = getWorkdaysInRange(start, end, false); // 排除正常周末
  190. // 根据当前代码逻辑: includeWeekends:false 表示仅返回周一至周五的工作日。
  191. // 因此, 2024-05-11 (周六, 调休工作日) 应被排除。
  192. expect(workdaysInRange).toEqual([
  193. // 2024-05-01 至 05-05 是劳动节假期
  194. '2024-05-06', // 周一
  195. '2024-05-07', // 周二
  196. '2024-05-08', // 周三
  197. '2024-05-09', // 周四
  198. '2024-05-10', // 周五
  199. // '2024-05-11', // 周六 (调休工作日) - 因 includeWeekends: false 而排除
  200. ]);
  201. });
  202. test('getWorkdaysInRange should return correct workdays including normal workdays on weekends', () => {
  203. const start = '2024-05-01';
  204. const end = '2024-05-12';
  205. const workdaysInRange = getWorkdaysInRange(start, end, true); // 包括所有工作日 (正常 + 调休)
  206. expect(workdaysInRange).toEqual([
  207. '2024-05-06',
  208. '2024-05-07',
  209. '2024-05-08',
  210. '2024-05-09',
  211. '2024-05-10',
  212. '2024-05-11', // 调休工作日
  213. ]);
  214. });
  215. // getWorkdaysInRange 边缘情况的更多测试
  216. describe('getWorkdaysInRange - Edge Cases', () => {
  217. test('should handle year boundaries', () => {
  218. const workdays = getWorkdaysInRange('2023-12-30', '2024-01-03', true);
  219. // 2023-12-30 (周六), 2023-12-31 (周日), 2024-01-01 (周一, 元旦节假日)
  220. // 2024-01-02 (周二), 2024-01-03 (周三)
  221. expect(workdays).toEqual(['2024-01-02', '2024-01-03']);
  222. });
  223. test('should handle leap year February', () => {
  224. const workdays = getWorkdaysInRange('2024-02-28', '2024-03-02', true);
  225. // 2月28日(周三), 2月29日(周四), 3月1日(周五), 3月2日(周六, 周末)
  226. expect(workdays).toEqual(['2024-02-28', '2024-02-29', '2024-03-01']);
  227. });
  228. test('should return empty array for a range with no workdays (e.g. full holiday period)', () => {
  229. const workdays = getWorkdaysInRange('2024-10-01', '2024-10-07', true); // 国庆节假期周
  230. expect(workdays).toEqual([]);
  231. });
  232. });
  233. test('findWorkday should return correct workday', () => {
  234. const date = '2024-05-01'; // 劳动节 (节假日)
  235. const nextWorkday = findWorkday(1, date); // 从5月1日开始的下一个工作日
  236. expect(nextWorkday).toBe('2024-05-06'); // 5月6日是劳动节假期后的第一个工作日
  237. });
  238. describe('findWorkday', () => {
  239. const testCases = [
  240. // 正向 delta
  241. { delta: 1, date: '2024-05-01', expected: '2024-05-06', desc: '节假日后的下一个工作日' },
  242. { delta: 1, date: '2024-05-06', expected: '2024-05-07', desc: '工作日的下一个工作日' },
  243. { delta: 1, date: '2024-05-10', expected: '2024-05-11', desc: '下一个工作日是调休的周六工作日' },
  244. { delta: 2, date: '2024-05-10', expected: '2024-05-13', desc: '隔一个工作日,跳过调休周六后的周末' },
  245. { delta: 1, date: '2023-12-29', expected: '2024-01-02', desc: '跨元旦假期的下一个工作日' }, // 周五 -> 周二 (周一元旦)
  246. // 负向 delta
  247. // 已修正预期:5月6日(周一)的前一个工作日是4月30日(周二),因为5月1-5日是劳动节假期,4月28日(周日)是之前的调休工作日。
  248. // findWorkday(-1, '2024-05-06') 结果是 '2024-04-30'
  249. { delta: -1, date: '2024-05-06', expected: '2024-04-30', desc: '从周一开始,跳过劳动节假期到4月30日的前一个工作日' },
  250. { delta: -1, date: '2024-05-13', expected: '2024-05-11', desc: '前一个工作日是调休的周六' },
  251. { delta: -1, date: '2024-01-02', expected: '2023-12-29', desc: '跨元旦假期的前一个工作日' }, // 周二 -> 周五 (周一元旦)
  252. // delta 为 0
  253. { delta: 0, date: '2024-05-11', expected: '2024-05-11', desc: '当天是调休的周六工作日' },
  254. { delta: 0, date: '2024-05-12', expected: '2024-05-13', desc: '当天是周日(节假日),查找下一个工作日' }, // 周日,下一个是周一
  255. { delta: 0, date: '2024-05-01', expected: '2024-05-06', desc: '当天是劳动节(节假日),查找下一个工作日'}, // 5月1日是节假日,下一个工作日是5月6日
  256. { delta: 0, date: '2024-07-08', expected: '2024-07-08', desc: '当天是普通工作日(星期一)'},
  257. // 针对 line 86 (while循环内的 if (isWorkday(date))) 的特定新测试
  258. {
  259. delta: 1,
  260. date: '2024-05-04', // 周六 (节假日)
  261. expected: '2024-05-06', // 下一个工作日是周一
  262. desc: 'Line 86: 循环遇到节假日(周日),然后是工作日(周一)'
  263. // 迭代1: date 变为 2024-05-05 (周日, 节假日)。isWorkday(date) 为 FALSE。daysToAdd = 1。
  264. // 迭代2: date 变为 2024-05-06 (周一, 工作日)。isWorkday(date) 为 TRUE。daysToAdd = 0。循环结束。
  265. },
  266. {
  267. delta: 2,
  268. date: '2024-05-04', // 周六 (节假日)
  269. expected: '2024-05-07', // 第二个工作日
  270. desc: 'Line 86: 循环遇到节假日, 工作日, 工作日'
  271. // 迭代1: date 变为 2024-05-05 (周日, 节假日)。isWorkday(date) 为 FALSE。daysToAdd = 2。
  272. // 迭代2: date 变为 2024-05-06 (周一, 工作日)。isWorkday(date) 为 TRUE。daysToAdd = 1。
  273. // 迭代3: date 变为 2024-05-07 (周二, 工作日)。isWorkday(date) 为 TRUE。daysToAdd = 0。循环结束。
  274. }
  275. ];
  276. test.each(testCases)('findWorkday($delta, "$date") 应该为 $expected ($desc)', ({ delta, date, expected }) => {
  277. expect(findWorkday(delta, date)).toBe(expected);
  278. });
  279. describe('findWorkday with default date (today)', () => {
  280. let originalDayjs: typeof dayjs;
  281. beforeEach(() => {
  282. originalDayjs = dayjs; // 保存原始的 dayjs
  283. });
  284. afterEach(() => {
  285. // @ts-ignore
  286. dayjs = originalDayjs; // 恢复原始的 dayjs
  287. });
  288. test('should return today if today is a workday and delta is 0', () => {
  289. const mockTodayWorkday = "2024-05-06"; // 周一, 已知的工作日
  290. // @ts-ignore
  291. dayjs = jest.fn((dateInput?: any) => {
  292. if (dateInput === undefined || dateInput === null || dateInput === '') {
  293. return originalDayjs(mockTodayWorkday);
  294. }
  295. return originalDayjs(dateInput);
  296. });
  297. Object.assign(dayjs, originalDayjs);
  298. expect(findWorkday()).toBe(mockTodayWorkday);
  299. });
  300. test('should return next workday if today is a holiday and delta is 0', () => {
  301. const mockTodayHoliday = "2024-05-05"; // 周日, 已知的节假日
  302. // @ts-ignore
  303. dayjs = jest.fn((dateInput?: any) => {
  304. if (dateInput === undefined || dateInput === null || dateInput === '') {
  305. return originalDayjs(mockTodayHoliday);
  306. }
  307. return originalDayjs(dateInput);
  308. });
  309. Object.assign(dayjs, originalDayjs);
  310. expect(findWorkday(0)).toBe("2024-05-06"); // 下一个工作日
  311. });
  312. test('should return next workday if today is a workday and delta is 1', () => {
  313. const mockTodayWorkday = "2024-05-06"; // 周一, 已知的工作日
  314. // @ts-ignore
  315. dayjs = jest.fn((dateInput?: any) => {
  316. if (dateInput === undefined || dateInput === null || dateInput === '') {
  317. return originalDayjs(mockTodayWorkday);
  318. }
  319. return originalDayjs(dateInput);
  320. });
  321. Object.assign(dayjs, originalDayjs);
  322. expect(findWorkday(1)).toBe("2024-05-07");
  323. });
  324. });
  325. });
  326. });
  327. describe('Arrangement Class', () => {
  328. let arrangement: Arrangement;
  329. beforeEach(() => {
  330. arrangement = new Arrangement();
  331. });
  332. test('should correctly handle 2023 holidays', () => {
  333. arrangement.y(2024)
  334. .ny().r(1, 1)
  335. .s().r(2, 10).to(2, 17).w(2, 4).w(2, 18).i(2, 15).to(2, 16)
  336. .t().r(4, 4).to(4, 6).w(4, 7).i(4, 5)
  337. .l().r(5, 1).to(5, 5).w(4, 28).w(5, 11).i(5, 2).to(5, 3)
  338. .d().r(6, 10)
  339. .m().r(9, 15).to(9, 17).w(9, 14).i(9, 16)
  340. .n().r(10, 1).to(10, 7).w(9, 29).w(10, 12).i(10, 4).i(10, 7)
  341. expect(arrangement.holidays).toHaveProperty('2024-05-01');
  342. expect(arrangement.holidays).toHaveProperty('2024-05-02');
  343. expect(arrangement.holidays).toHaveProperty('2024-05-04');
  344. expect(arrangement.holidays).toHaveProperty('2024-05-05');
  345. expect(arrangement.workdays).toHaveProperty('2024-04-28');
  346. expect(arrangement.workdays).toHaveProperty('2024-05-11');
  347. });
  348. });