format.test.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213
  1. /**
  2. * Tests for cfc-frontend utils/format.js
  3. *
  4. * Tests the price/rate/score formatting utilities.
  5. * These are pure functions - no uni mock needed.
  6. */
  7. describe('format.js', () => {
  8. let format
  9. beforeAll(() => {
  10. format = require('@/utils/format')
  11. })
  12. // --------------- formatPrice ---------------
  13. describe('formatPrice', () => {
  14. test('converts cents to yuan with 2 decimals', () => {
  15. expect(format.formatPrice(1250)).toBe('12.50')
  16. expect(format.formatPrice(0)).toBe('0.00')
  17. expect(format.formatPrice(999)).toBe('9.99')
  18. expect(format.formatPrice(10000)).toBe('100.00')
  19. })
  20. test('handles null and undefined', () => {
  21. expect(format.formatPrice(null)).toBeNull()
  22. expect(format.formatPrice(undefined)).toBeNull()
  23. })
  24. test('handles zero cents', () => {
  25. expect(format.formatPrice(0)).toBe('0.00')
  26. })
  27. })
  28. // --------------- formatPriceWithSymbol ---------------
  29. describe('formatPriceWithSymbol', () => {
  30. test('adds ¥ prefix and 2 decimals', () => {
  31. expect(format.formatPriceWithSymbol(1250)).toBe('¥12.50')
  32. expect(format.formatPriceWithSymbol(0)).toBe('¥0.00')
  33. expect(format.formatPriceWithSymbol(999)).toBe('¥9.99')
  34. })
  35. test('returns login message for null/undefined', () => {
  36. expect(format.formatPriceWithSymbol(null)).toBe('登录查看')
  37. expect(format.formatPriceWithSymbol(undefined)).toBe('登录查看')
  38. })
  39. })
  40. // --------------- formatRate ---------------
  41. describe('formatRate', () => {
  42. test('converts permillage to percentage string', () => {
  43. expect(format.formatRate(235)).toBe('23.5%')
  44. expect(format.formatRate(0)).toBe('0%')
  45. expect(format.formatRate(1000)).toBe('100%')
  46. expect(format.formatRate(5)).toBe('0.5%')
  47. })
  48. test('returns -- for null/undefined', () => {
  49. expect(format.formatRate(null)).toBe('--')
  50. expect(format.formatRate(undefined)).toBe('--')
  51. })
  52. })
  53. // --------------- formatScore ---------------
  54. describe('formatScore', () => {
  55. test('converts percent-scale to fixed decimal', () => {
  56. expect(format.formatScore(450)).toBe('4.5')
  57. expect(format.formatScore(0)).toBe('0.0')
  58. expect(format.formatScore(1000)).toBe('10.0')
  59. })
  60. test('uses default 1 decimal place', () => {
  61. expect(format.formatScore(450)).toBe('4.5')
  62. })
  63. test('supports custom decimal places', () => {
  64. expect(format.formatScore(450, 2)).toBe('4.50')
  65. expect(format.formatScore(450, 0)).toBe('5')
  66. })
  67. test('returns -- for null/undefined', () => {
  68. expect(format.formatScore(null)).toBe('--')
  69. expect(format.formatScore(undefined)).toBe('--')
  70. })
  71. })
  72. // --------------- parseDate ---------------
  73. describe('parseDate', () => {
  74. // 断言使用本地时间字段(getFullYear/getMonth/getDate/...),
  75. // 避免 Node(V8)对纯日期字符串按 UTC 解析带来的时区差异。
  76. const ymd = (d) => {
  77. return d.getFullYear() + '-' + (d.getMonth() + 1) + '-' + d.getDate()
  78. }
  79. test('parses MySQL DATETIME with space separator "yyyy-MM-dd HH:mm"', () => {
  80. // iOS 原生 new Date() 无法解析空格分隔格式,parseDate 必须归一化成功
  81. const d = format.parseDate('2026-08-22 14:00')
  82. expect(d).not.toBeNull()
  83. expect(ymd(d)).toBe('2026-8-22')
  84. expect(d.getHours()).toBe(14)
  85. expect(d.getMinutes()).toBe(0)
  86. })
  87. test('parses MySQL DATETIME with seconds', () => {
  88. const d = format.parseDate('2026-08-22 14:00:30')
  89. expect(d).not.toBeNull()
  90. expect(ymd(d)).toBe('2026-8-22')
  91. expect(d.getSeconds()).toBe(30)
  92. })
  93. test('parses Java "EEE MMM dd HH:mm:ss zzz yyyy" (CST) format', () => {
  94. // 后端 Java 常见格式,iOS 原生无法解析
  95. const d = format.parseDate('Sat Aug 22 14:00:00 CST 2026')
  96. expect(d).not.toBeNull()
  97. expect(ymd(d)).toBe('2026-8-22')
  98. expect(d.getHours()).toBe(14)
  99. })
  100. test('parses slash-separated "yyyy/MM/dd"', () => {
  101. const d = format.parseDate('2026/08/22')
  102. expect(d).not.toBeNull()
  103. expect(ymd(d)).toBe('2026-8-22')
  104. })
  105. test('parses ISO 8601 "yyyy-MM-ddTHH:mm:ss"', () => {
  106. const d = format.parseDate('2026-08-22T14:00:00')
  107. expect(d).not.toBeNull()
  108. expect(ymd(d)).toBe('2026-8-22')
  109. })
  110. test('parses plain date "yyyy-MM-dd"', () => {
  111. const d = format.parseDate('2026-08-22')
  112. expect(d).not.toBeNull()
  113. expect(ymd(d)).toBe('2026-8-22')
  114. })
  115. test('parses numeric timestamp', () => {
  116. const d = format.parseDate(1780000000000)
  117. expect(d).not.toBeNull()
  118. expect(d.getTime()).toBe(1780000000000)
  119. })
  120. test('returns same Date object when given a Date', () => {
  121. const date = new Date()
  122. expect(format.parseDate(date)).toBe(date)
  123. })
  124. test('returns null for null/undefined/empty string', () => {
  125. expect(format.parseDate(null)).toBeNull()
  126. expect(format.parseDate(undefined)).toBeNull()
  127. expect(format.parseDate('')).toBeNull()
  128. })
  129. test('returns null for unparseable string', () => {
  130. expect(format.parseDate('not-a-date')).toBeNull()
  131. expect(format.parseDate('2026-13-45')).toBeNull()
  132. })
  133. })
  134. // --------------- recordDateOf ---------------
  135. // 业务日期字段优先:判定"今日是否打卡"应看业务日期(recordDate/startTime/sleepTime/checkinDate),
  136. // 而非 createdAt。createdAt 是记录写入时间(补打时=今天,业务日期=补打日),且后端 Jackson 输出的
  137. // createdAt 形如 "2026-10-30T13:13:32.000+08:00"(带毫秒+时区偏移),iOS JavaScriptCore 无法解析
  138. // (parseDate 注释:只支持不带毫秒的 yyyy-MM-ddTHH:mm:ss+HH:mm)。recordDate/checkinDate 为纯日期,
  139. // 所有环境都能解析。
  140. describe('recordDateOf', () => {
  141. test('prefers recordDate over createdAt (补打:recordDate=昨天, createdAt=今天 → 昨天)', () => {
  142. // 补打场景:recordDate 是实际用餐日(昨天),createdAt 是记录创建时间(今天)。
  143. // 若 createdAt 优先会误判"今天已打卡";正确应取业务日期 recordDate。
  144. expect(format.recordDateOf({
  145. recordDate: '2026-10-29',
  146. createdAt: '2026-10-30T13:13:32.000+08:00'
  147. })).toBe('2026-10-29')
  148. })
  149. test('falls back to createdAt when business date field missing', () => {
  150. expect(format.recordDateOf({
  151. createdAt: '2026-10-30T13:13:32.000+08:00'
  152. })).toBe('2026-10-30')
  153. })
  154. test('prefers startTime/sleepTime/checkinDate (运动/睡眠/心情业务字段优先)', () => {
  155. expect(format.recordDateOf({
  156. startTime: '2026-10-30T06:00:00.000+08:00',
  157. createdAt: '2026-10-31T00:00:00.000+08:00'
  158. })).toBe('2026-10-30')
  159. expect(format.recordDateOf({
  160. sleepTime: '2026-10-30T22:00:00.000+08:00',
  161. createdAt: '2026-10-31T00:00:00.000+08:00'
  162. })).toBe('2026-10-30')
  163. expect(format.recordDateOf({
  164. checkinDate: '2026-10-30',
  165. createdAt: '2026-10-31T00:00:00.000+08:00'
  166. })).toBe('2026-10-30')
  167. })
  168. test('returns empty string for null/empty record', () => {
  169. expect(format.recordDateOf(null)).toBe('')
  170. expect(format.recordDateOf({})).toBe('')
  171. })
  172. test('parses ISO 8601 with milliseconds + timezone offset (后端 Jackson 默认输出)', () => {
  173. // 后端 Date 序列化真实格式:2026-10-30T13:13:32.000+08:00。
  174. // recordDateOf 对纯 createdAt 也应能正确解析出日期。
  175. expect(format.recordDateOf({
  176. createdAt: '2026-10-30T13:13:32.000+08:00'
  177. })).toBe('2026-10-30')
  178. })
  179. })
  180. })