@micha.bigler/ui-core-micha 3.14.3 → 4.0.0

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.
Files changed (367) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +7 -0
  3. package/dist/auth/UserMenu.js +1 -1
  4. package/dist/auth/apiClient.js +7 -6
  5. package/dist/chat/ChatList.js +1 -1
  6. package/dist/chat/ChatThread.js +2 -1
  7. package/dist/chat/ChatWorkspace.js +1 -1
  8. package/dist/chat/ModelPicker.js +2 -5
  9. package/dist/chat/kitSxRegistry.js +9 -0
  10. package/dist/chat/useChatSocket.js +1 -1
  11. package/dist/components/LoginForm.js +1 -1
  12. package/dist/components/SecurityComponent.js +1 -1
  13. package/dist/components/UserListComponent.js +2 -8
  14. package/dist/components/charts/ChartFrame.js +1 -1
  15. package/dist/components/charts/ScatterChart.js +2 -1
  16. package/dist/components/charts/kitSxRegistry.js +6 -0
  17. package/dist/entries/charts.js +6 -0
  18. package/dist/entries/chat.js +8 -0
  19. package/dist/entries/markdown.js +1 -0
  20. package/dist/index.js +0 -14
  21. package/dist/layout/SectionNav.js +1 -1
  22. package/dist/messaging/AttachmentList.js +2 -2
  23. package/dist/messaging/Composer.js +2 -1
  24. package/dist/messaging/ConversationLaunchers.js +1 -4
  25. package/dist/messaging/ConversationList.js +1 -1
  26. package/dist/messaging/DirectMessageLauncher.js +1 -1
  27. package/dist/messaging/MessagingProvider.js +0 -0
  28. package/dist/messaging/PollCard.js +7 -5
  29. package/dist/messaging/ReadTicks.js +2 -2
  30. package/dist/messaging/Thread.js +5 -3
  31. package/dist/notifications/NotificationBell.js +4 -7
  32. package/dist/notifications/NotificationSettings.js +1 -1
  33. package/dist/onboarding/OnboardingProvider.js +3 -6
  34. package/dist/pages/AccountPage.js +1 -2
  35. package/dist/pages/LoginPage.js +2 -3
  36. package/dist/pages/PasswordChangePage.js +2 -3
  37. package/dist/pages/PasswordInvitePage.js +2 -3
  38. package/dist/pages/PasswordResetRequestPage.js +2 -3
  39. package/dist/pages/SignUpPage.js +2 -3
  40. package/dist/pages/SignupConfirmPage.js +2 -3
  41. package/dist/theme/createAppTheme.js +5 -4
  42. package/dist/theme/kitSxEntry.js +11 -0
  43. package/dist/theme/kitSxRegistry.js +2 -15
  44. package/dist/theme/themeCompleteness.js +3 -12
  45. package/dist/utils/auth-errors.js +4 -3
  46. package/package.json +41 -16
  47. package/.github/workflows/publish.yml +0 -126
  48. package/DESIGN.md +0 -60
  49. package/WORK_ORDERS.md +0 -148
  50. package/dev/entries.jsx +0 -721
  51. package/dev/fixtures.js +0 -26
  52. package/dev/main.jsx +0 -75
  53. package/dev/mockTransport.jsx +0 -128
  54. package/docs/CHART-LAYOUT.md +0 -157
  55. package/docs/DARK-TOKENS.md +0 -140
  56. package/docs/TYPE-SCALE.md +0 -111
  57. package/docs/messaging-deviations.md +0 -118
  58. package/index.html +0 -12
  59. package/renovate.json +0 -10
  60. package/scripts/chart-api-census.mjs +0 -415
  61. package/src/auth/AuthContext.jsx +0 -158
  62. package/src/auth/ReauthModal.jsx +0 -82
  63. package/src/auth/UserMenu.jsx +0 -121
  64. package/src/auth/apiClient.jsx +0 -232
  65. package/src/auth/authApi.jsx +0 -539
  66. package/src/auth/authConfig.jsx +0 -34
  67. package/src/auth/reauth.js +0 -47
  68. package/src/chat/ChatComposer.jsx +0 -36
  69. package/src/chat/ChatList.jsx +0 -87
  70. package/src/chat/ChatThread.jsx +0 -128
  71. package/src/chat/ChatWorkspace.jsx +0 -244
  72. package/src/chat/MarkdownReply.jsx +0 -22
  73. package/src/chat/ModelPicker.jsx +0 -85
  74. package/src/chat/api.js +0 -18
  75. package/src/chat/useChatSocket.js +0 -32
  76. package/src/chat/wire.js +0 -7
  77. package/src/components/AccessCodeManager.jsx +0 -288
  78. package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
  79. package/src/components/AllowedEmailDomainsManager.jsx +0 -92
  80. package/src/components/AuthFactorRequirementCard.jsx +0 -95
  81. package/src/components/BulkInviteCsvTab.jsx +0 -198
  82. package/src/components/EmptyState.jsx +0 -38
  83. package/src/components/LoginForm.jsx +0 -159
  84. package/src/components/MFAComponent.jsx +0 -350
  85. package/src/components/MfaLoginComponent.jsx +0 -252
  86. package/src/components/MobileBottomNav.jsx +0 -82
  87. package/src/components/PasskeysComponent.jsx +0 -252
  88. package/src/components/PasswordChangeForm.jsx +0 -87
  89. package/src/components/PasswordResetRequestForm.jsx +0 -43
  90. package/src/components/PasswordSetForm.jsx +0 -97
  91. package/src/components/ProfileComponent.jsx +0 -348
  92. package/src/components/QrSignupManager.jsx +0 -404
  93. package/src/components/QrSignupValidityManager.jsx +0 -106
  94. package/src/components/RegistrationMethodsManager.jsx +0 -150
  95. package/src/components/SecurityComponent.jsx +0 -141
  96. package/src/components/SocialLoginButtons.jsx +0 -83
  97. package/src/components/SoftChip.jsx +0 -93
  98. package/src/components/StatTile.jsx +0 -39
  99. package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
  100. package/src/components/TurnstileWidget.jsx +0 -99
  101. package/src/components/UserInviteComponent.jsx +0 -76
  102. package/src/components/UserListComponent.jsx +0 -618
  103. package/src/components/charts/BarChart.jsx +0 -102
  104. package/src/components/charts/ChartFrame.jsx +0 -214
  105. package/src/components/charts/LineChart.jsx +0 -126
  106. package/src/components/charts/ScatterChart.jsx +0 -299
  107. package/src/components/charts/TimeSeriesChart.jsx +0 -300
  108. package/src/components/charts/chartDefaults.js +0 -644
  109. package/src/components/charts/chartLabels.js +0 -36
  110. package/src/components/charts/exportChart.js +0 -258
  111. package/src/components/charts/formatters.js +0 -39
  112. package/src/components/charts/palette.js +0 -33
  113. package/src/components/charts/yearTickInterval.js +0 -7
  114. package/src/i18n/authTranslations.ts +0 -2651
  115. package/src/i18n/chartsTranslations.ts +0 -91
  116. package/src/i18n/chatTranslations.ts +0 -55
  117. package/src/i18n/messagingTranslations.ts +0 -120
  118. package/src/i18n/notificationsTranslations.ts +0 -32
  119. package/src/i18n/onboardingTranslations.ts +0 -29
  120. package/src/i18n/sectionNavTranslations.ts +0 -20
  121. package/src/i18n/uiCoreTranslations.js +0 -29
  122. package/src/i18n/userMenuTranslations.ts +0 -5
  123. package/src/index.js +0 -158
  124. package/src/layout/PageLayout.jsx +0 -36
  125. package/src/layout/SectionNav.jsx +0 -320
  126. package/src/messaging/AttachmentList.jsx +0 -145
  127. package/src/messaging/Composer.jsx +0 -197
  128. package/src/messaging/ConversationLaunchers.jsx +0 -51
  129. package/src/messaging/ConversationList.jsx +0 -109
  130. package/src/messaging/DirectMessageLauncher.jsx +0 -112
  131. package/src/messaging/MessageBubble.jsx +0 -183
  132. package/src/messaging/MessagingProvider.jsx +0 -0
  133. package/src/messaging/MessagingScopeConfig.jsx +0 -24
  134. package/src/messaging/PollCard.jsx +0 -163
  135. package/src/messaging/ReactionBar.jsx +0 -43
  136. package/src/messaging/ReadTicks.jsx +0 -82
  137. package/src/messaging/Thread.jsx +0 -195
  138. package/src/messaging/api.js +0 -48
  139. package/src/messaging/composerImageCompression.js +0 -35
  140. package/src/messaging/linkifyText.jsx +0 -37
  141. package/src/notifications/NotificationBell.jsx +0 -84
  142. package/src/notifications/NotificationSettings.jsx +0 -330
  143. package/src/notifications/NotificationsProvider.jsx +0 -224
  144. package/src/notifications/PopupSurface.jsx +0 -105
  145. package/src/notifications/api.js +0 -55
  146. package/src/notifications/feedApi.js +0 -18
  147. package/src/notifications/realtime.jsx +0 -138
  148. package/src/notifications/serviceWorker/sw.js +0 -50
  149. package/src/onboarding/OnboardingProvider.jsx +0 -326
  150. package/src/onboarding/OnboardingWizard.jsx +0 -61
  151. package/src/onboarding/WizardDialogShell.jsx +0 -51
  152. package/src/onboarding/api.js +0 -13
  153. package/src/onboarding/stepSelection.js +0 -16
  154. package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
  155. package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
  156. package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
  157. package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
  158. package/src/pages/AccountPage.jsx +0 -394
  159. package/src/pages/LoginPage.jsx +0 -343
  160. package/src/pages/PasswordChangePage.jsx +0 -62
  161. package/src/pages/PasswordInvitePage.jsx +0 -177
  162. package/src/pages/PasswordResetRequestPage.jsx +0 -68
  163. package/src/pages/SignUpPage.jsx +0 -268
  164. package/src/pages/SignupConfirmPage.jsx +0 -155
  165. package/src/testing/checkKitIntegration.js +0 -233
  166. package/src/theme/createAppTheme.js +0 -423
  167. package/src/theme/fonts.js +0 -3
  168. package/src/theme/index.js +0 -21
  169. package/src/theme/kitSxRegistry.js +0 -285
  170. package/src/theme/themeCompleteness.js +0 -1045
  171. package/src/theme/tokens.js +0 -478
  172. package/src/utils/auth-errors.js +0 -76
  173. package/src/utils/authService.js +0 -223
  174. package/src/utils/resolveErrorText.js +0 -28
  175. package/src/utils/webauthn.js +0 -51
  176. package/tests/AccountPage.profileStatements.test.jsx +0 -162
  177. package/tests/AccountPage.test.jsx +0 -200
  178. package/tests/AuthContext.test.jsx +0 -129
  179. package/tests/BarChart.test.jsx +0 -112
  180. package/tests/BrowserPushStep.test.jsx +0 -110
  181. package/tests/ChartFrame.test.jsx +0 -336
  182. package/tests/ConversationList.test.jsx +0 -144
  183. package/tests/CookieConsentStep.test.jsx +0 -69
  184. package/tests/EmptyState.test.jsx +0 -62
  185. package/tests/LineChart.test.jsx +0 -80
  186. package/tests/MessagingProvider.test.jsx +0 -50
  187. package/tests/MobileBottomNav.test.jsx +0 -319
  188. package/tests/NotificationBell.test.jsx +0 -60
  189. package/tests/NotificationSettings.test.jsx +0 -276
  190. package/tests/NotificationsProvider.test.jsx +0 -240
  191. package/tests/OnboardingProvider.test.jsx +0 -294
  192. package/tests/OnboardingWizard.test.jsx +0 -143
  193. package/tests/PasswordChangeForm.test.jsx +0 -70
  194. package/tests/PasswordInvitePage.test.jsx +0 -142
  195. package/tests/PasswordSetForm.test.jsx +0 -70
  196. package/tests/PopupSurface.test.jsx +0 -142
  197. package/tests/ProfileComponent.refresh.test.jsx +0 -194
  198. package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
  199. package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
  200. package/tests/ScatterChart.test.jsx +0 -270
  201. package/tests/SectionNav.test.jsx +0 -363
  202. package/tests/SignUpPage.test.jsx +0 -208
  203. package/tests/SoftChip.test.jsx +0 -105
  204. package/tests/StatTile.test.jsx +0 -57
  205. package/tests/Thread.test.jsx +0 -151
  206. package/tests/TimeSeriesChart.test.jsx +0 -367
  207. package/tests/UserListComponent.test.jsx +0 -192
  208. package/tests/UserMenu.test.jsx +0 -171
  209. package/tests/apiClientLanguage.test.js +0 -33
  210. package/tests/apiClientPublicPaths.test.js +0 -178
  211. package/tests/authApiLogin.test.js +0 -42
  212. package/tests/authApiRegistration.test.js +0 -31
  213. package/tests/authErrorCodeRendering.test.jsx +0 -77
  214. package/tests/authErrors.test.js +0 -74
  215. package/tests/authTranslations.test.js +0 -74
  216. package/tests/chart-api-census.test.js +0 -366
  217. package/tests/chartAxisTickBand.test.jsx +0 -95
  218. package/tests/chartDefaults.test.js +0 -902
  219. package/tests/chartLabels.test.js +0 -50
  220. package/tests/chartsPalette.test.js +0 -99
  221. package/tests/chatApi.test.js +0 -48
  222. package/tests/chatDriverNames.test.jsx +0 -71
  223. package/tests/chatExports.test.js +0 -3
  224. package/tests/chatList.test.jsx +0 -84
  225. package/tests/chatPicker.test.jsx +0 -82
  226. package/tests/chatSocket.test.jsx +0 -26
  227. package/tests/chatThread.test.jsx +0 -116
  228. package/tests/chatTranslations.test.js +0 -47
  229. package/tests/chatWorkspace.test.jsx +0 -234
  230. package/tests/checkKitIntegration.test.jsx +0 -219
  231. package/tests/createAppTheme.test.js +0 -132
  232. package/tests/createAppThemeDark.test.js +0 -127
  233. package/tests/createAppThemeLightGuard.test.js +0 -40
  234. package/tests/createAppThemePageContrast.test.js +0 -108
  235. package/tests/createAppThemeTypography.test.js +0 -53
  236. package/tests/errorCallSiteGuard.test.js +0 -32
  237. package/tests/exportChart.test.js +0 -313
  238. package/tests/feedApi.test.js +0 -46
  239. package/tests/fixtures/light-theme-baseline.json +0 -2234
  240. package/tests/i18nAggregate.test.js +0 -120
  241. package/tests/i18nGermanVariant.test.js +0 -65
  242. package/tests/loginPageErrorResolution.test.jsx +0 -102
  243. package/tests/messagingApi.test.js +0 -32
  244. package/tests/messagingChunk6.test.jsx +0 -116
  245. package/tests/messagingCompactBubble.test.jsx +0 -64
  246. package/tests/messagingComposer.test.jsx +0 -136
  247. package/tests/messagingComposerChunk5.test.jsx +0 -113
  248. package/tests/messagingContractConformance.test.js +0 -69
  249. package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
  250. package/tests/messagingExports.test.js +0 -21
  251. package/tests/messagingInteractions.test.jsx +0 -136
  252. package/tests/messagingLastMessagePreview.test.jsx +0 -93
  253. package/tests/messagingLinkify.test.jsx +0 -51
  254. package/tests/messagingMessageActions.test.jsx +0 -163
  255. package/tests/messagingMsg6.test.jsx +0 -105
  256. package/tests/messagingMsg6f.test.jsx +0 -155
  257. package/tests/messagingMsg6g.test.jsx +0 -166
  258. package/tests/messagingMsg6h.test.jsx +0 -136
  259. package/tests/messagingMsg6i.test.jsx +0 -129
  260. package/tests/messagingPollRendering.test.jsx +0 -142
  261. package/tests/messagingReactionMerge.test.jsx +0 -77
  262. package/tests/messagingReadTicksTooltip.test.jsx +0 -36
  263. package/tests/messagingThreadReceipts.test.jsx +0 -178
  264. package/tests/messagingThreadingQuoting.test.jsx +0 -87
  265. package/tests/messagingUnreadLifecycle.test.jsx +0 -78
  266. package/tests/notificationsApi.test.js +0 -41
  267. package/tests/notificationsExports.test.js +0 -34
  268. package/tests/onboardingDescriptors.test.js +0 -132
  269. package/tests/packageTreeShaking.test.js +0 -126
  270. package/tests/realtime.test.jsx +0 -219
  271. package/tests/resolveErrorText.test.js +0 -72
  272. package/tests/stepSelection.test.js +0 -53
  273. package/tests/themeCompleteness.test.js +0 -626
  274. package/tests/timeSeriesLegend.test.js +0 -99
  275. package/tests/userScreenTranslations.test.jsx +0 -218
  276. package/tsconfig.build.json +0 -18
  277. package/vite.config.mjs +0 -6
  278. package/work-orders/AUTH-1.md +0 -74
  279. package/work-orders/AUTH-2.md +0 -74
  280. package/work-orders/AUTH-4.md +0 -183
  281. package/work-orders/AUTH-5.md +0 -154
  282. package/work-orders/AUTH-7.md +0 -181
  283. package/work-orders/AUTH-8.md +0 -184
  284. package/work-orders/CHART-1.md +0 -285
  285. package/work-orders/CHART-2.md +0 -222
  286. package/work-orders/CHART-3.md +0 -163
  287. package/work-orders/CHART-4.md +0 -203
  288. package/work-orders/CHART-5.md +0 -226
  289. package/work-orders/CHART-6.md +0 -428
  290. package/work-orders/CHART-7.md +0 -175
  291. package/work-orders/DX-1.md +0 -169
  292. package/work-orders/DX-2.md +0 -188
  293. package/work-orders/DX-3.md +0 -195
  294. package/work-orders/I18N-1.md +0 -254
  295. package/work-orders/I18N-3.md +0 -207
  296. package/work-orders/MSG-3.md +0 -375
  297. package/work-orders/MSG-3b.md +0 -1002
  298. package/work-orders/MSG-3c.md +0 -378
  299. package/work-orders/MSG-3d.md +0 -268
  300. package/work-orders/MSG-3e.md +0 -164
  301. package/work-orders/MSG-3f.md +0 -391
  302. package/work-orders/MSG-6e.md +0 -308
  303. package/work-orders/MSG-6f.md +0 -214
  304. package/work-orders/MSG-6g.md +0 -244
  305. package/work-orders/MSG-6h.md +0 -238
  306. package/work-orders/MSG-6i.md +0 -271
  307. package/work-orders/MSG-6j.md +0 -117
  308. package/work-orders/NOTIF-12.md +0 -214
  309. package/work-orders/NOTIF-13.md +0 -142
  310. package/work-orders/NOTIF-14.md +0 -151
  311. package/work-orders/PRIM-1.md +0 -177
  312. package/work-orders/SHELL-1.md +0 -255
  313. package/work-orders/SHELL-2.md +0 -197
  314. package/work-orders/SHELL-3.md +0 -359
  315. package/work-orders/SHELL-4.md +0 -351
  316. package/work-orders/SHELL-5.md +0 -345
  317. package/work-orders/SHELL-6.md +0 -210
  318. package/work-orders/THEME-1.md +0 -587
  319. package/work-orders/THEME-10.md +0 -303
  320. package/work-orders/THEME-11.md +0 -208
  321. package/work-orders/THEME-2.md +0 -339
  322. package/work-orders/THEME-3.md +0 -210
  323. package/work-orders/THEME-4.md +0 -271
  324. package/work-orders/THEME-5.md +0 -341
  325. package/work-orders/THEME-6.md +0 -247
  326. package/work-orders/THEME-7.md +0 -82
  327. package/work-orders/THEME-8.md +0 -211
  328. package/work-orders/THEME-9.md +0 -194
  329. package/work-orders/UCM-AUTH-10.md +0 -490
  330. package/work-orders/UCM-AUTH-11.md +0 -185
  331. package/work-orders/UCM-AUTH-12.md +0 -98
  332. package/work-orders/UCM-AUTH-13.md +0 -47
  333. package/work-orders/UCM-AUTH-9.md +0 -207
  334. package/work-orders/UCM-CHART-10.md +0 -150
  335. package/work-orders/UCM-CHART-11.md +0 -132
  336. package/work-orders/UCM-CHART-12.md +0 -348
  337. package/work-orders/UCM-CHART-13.md +0 -122
  338. package/work-orders/UCM-CHART-14.md +0 -198
  339. package/work-orders/UCM-CHART-15.md +0 -187
  340. package/work-orders/UCM-CHART-16.md +0 -169
  341. package/work-orders/UCM-CHART-17.md +0 -195
  342. package/work-orders/UCM-CHART-18.md +0 -200
  343. package/work-orders/UCM-CHART-8.md +0 -147
  344. package/work-orders/UCM-CHART-9.md +0 -265
  345. package/work-orders/UCM-CHAT-1.md +0 -190
  346. package/work-orders/UCM-CHAT-2.md +0 -75
  347. package/work-orders/UCM-DEP-1.md +0 -63
  348. package/work-orders/UCM-I18N-4.md +0 -226
  349. package/work-orders/UCM-I18N-5.md +0 -211
  350. package/work-orders/UCM-MSG-20.md +0 -192
  351. package/work-orders/UCM-PRIM-2.md +0 -114
  352. package/work-orders/UCM-SHELL-7.md +0 -134
  353. package/work-orders/UCM-SHELL-8.md +0 -125
  354. package/work-orders/UCM-TEST-1.md +0 -304
  355. package/work-orders/UCM-THEME-13.md +0 -69
  356. package/work-orders/UCM-THEME-14.md +0 -312
  357. package/work-orders/UCM-THEME-15.md +0 -356
  358. package/work-orders/UCM-THEME-16.md +0 -80
  359. package/work-orders/UCM-THEME-17.md +0 -111
  360. package/work-orders/UCM-THEME-18.md +0 -184
  361. package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
  362. package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
  363. package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
  364. package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
  365. package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
  366. package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
  367. package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
@@ -1,902 +0,0 @@
1
- // @vitest-environment jsdom
2
- import React from 'react';
3
- import i18next from 'i18next';
4
- import { afterEach, describe, expect, it, vi } from 'vitest';
5
- import { cleanup, render } from '@testing-library/react';
6
- import { I18nextProvider } from 'react-i18next';
7
- import { ThemeProvider } from '@mui/material/styles';
8
-
9
- const barSpy = vi.hoisted(() => vi.fn(() => null));
10
- const lineSpy = vi.hoisted(() => vi.fn(() => null));
11
- vi.mock('@mui/x-charts/BarChart', () => ({ BarChart: barSpy }));
12
- vi.mock('@mui/x-charts/LineChart', () => ({ LineChart: lineSpy, MarkElement: 'path' }));
13
-
14
- import { BarChart } from '../src/components/charts/BarChart';
15
- import { LineChart } from '../src/components/charts/LineChart';
16
- import { TimeSeriesChart } from '../src/components/charts/TimeSeriesChart';
17
- import {
18
- CHART_SIZE_SPACING_UNITS,
19
- PACKAGE_DEFAULT_MARGIN,
20
- assertRemovedChartProp,
21
- defaultNumericTickFormatter,
22
- resolveChartLayout,
23
- resolveLegendPosition,
24
- sizeYAxisForContent,
25
- withAxisDefaults,
26
- withChartSlotDefaults,
27
- } from '../src/components/charts/chartDefaults';
28
- import { createAppTheme } from '../src/theme/createAppTheme';
29
-
30
- const theme = createAppTheme({ palette: { primary: { main: '#3D5A99' } } });
31
- const i18n = i18next.createInstance();
32
- i18n.init({
33
- lng: 'en',
34
- resources: {
35
- en: {
36
- translation: {
37
- 'TimeSeriesChart.RANGE_LABEL': 'Range',
38
- 'TimeSeriesChart.RANGE_1_DAY': '1 day',
39
- 'TimeSeriesChart.RANGE_1_WEEK': '1 week',
40
- 'TimeSeriesChart.RANGE_1_MONTH': '1 month',
41
- 'TimeSeriesChart.RANGE_1_YEAR': '1 year',
42
- 'TimeSeriesChart.SERIES_LABEL': 'Series',
43
- 'ChartFrame.EMPTY_DEFAULT': 'No data available.',
44
- 'ChartFrame.LOADING': 'Loading chart.',
45
- 'ChartFrame.ERROR_DEFAULT': 'The chart could not be loaded.',
46
- },
47
- },
48
- },
49
- interpolation: { escapeValue: false },
50
- });
51
-
52
- function withProviders(children) {
53
- return React.createElement(
54
- ThemeProvider,
55
- { theme },
56
- React.createElement(I18nextProvider, { i18n }, children),
57
- );
58
- }
59
-
60
- function renderBar(props = {}) {
61
- return render(withProviders(
62
- React.createElement(BarChart, {
63
- xAxisLabel: 'Month',
64
- yAxisLabel: 'Cases',
65
- series: [{ label: 'Cases', data: [1, 2] }],
66
- ...props,
67
- }),
68
- ));
69
- }
70
-
71
- function renderLine(props = {}) {
72
- return render(withProviders(
73
- React.createElement(LineChart, {
74
- xAxisLabel: 'Month',
75
- yAxisLabel: 'Cases',
76
- series: [{ label: 'Cases', data: [1, 2] }],
77
- ...props,
78
- }),
79
- ));
80
- }
81
-
82
- describe('chart chrome defaults', () => {
83
- afterEach(() => {
84
- cleanup();
85
- vi.clearAllMocks();
86
- });
87
-
88
- it('gives minimal BarChart, LineChart, and TimeSeriesChart calls the baseline ticks and horizontal grid, with line marks off', () => {
89
- renderBar();
90
- const barProps = barSpy.mock.calls.at(-1)[0];
91
- expect(barProps.xAxis[0].tickLabelStyle.fontSize).toBe(theme.typography.caption.fontSize);
92
- expect(barProps.yAxis[0].tickLabelStyle.fontSize).toBe(theme.typography.caption.fontSize);
93
- expect(barProps.grid).toEqual({ horizontal: true });
94
-
95
- renderLine();
96
- const lineProps = lineSpy.mock.calls.at(-1)[0];
97
- expect(lineProps.xAxis[0].tickLabelStyle.fontSize).toBe(theme.typography.caption.fontSize);
98
- expect(lineProps.yAxis[0].tickLabelStyle.fontSize).toBe(theme.typography.caption.fontSize);
99
- expect(lineProps.grid).toEqual({ horizontal: true });
100
- expect(lineProps.series[0].showMark).toBe(false);
101
-
102
- render(withProviders(React.createElement(TimeSeriesChart, {
103
- title: 'Activity',
104
- xAxisLabel: 'Day',
105
- yAxisLabel: 'Cases',
106
- data: {
107
- xLabels: ['Mon', 'Tue'],
108
- series: [{ key: 'cases', label: 'Cases', data: [1, 2] }],
109
- },
110
- skipAnimation: true,
111
- })));
112
- const timeSeriesProps = barSpy.mock.calls.at(-1)[0];
113
- expect(timeSeriesProps.xAxis[0].tickLabelStyle.fontSize).toBe(theme.typography.caption.fontSize);
114
- expect(timeSeriesProps.yAxis[0].tickLabelStyle.fontSize).toBe(theme.typography.caption.fontSize);
115
- expect(timeSeriesProps.grid).toEqual({ horizontal: true });
116
- expect(timeSeriesProps.skipAnimation).toBe(true);
117
- // UCM-CHART-12: TimeSeriesChart's inner BarChart now takes size="standard" instead of a
118
- // fixed height prop. UCM-CHART-15 shifted "standard" from 320px to 400px.
119
- expect(timeSeriesProps.height).toBe(400);
120
- });
121
-
122
- it('keeps caller tick font, grid, showMark, and legendPosition values instead of the defaults', () => {
123
- const legendPosition = { vertical: 'top', horizontal: 'end' };
124
- renderLine({
125
- xAxis: [{ data: ['Jan', 'Feb'], tickLabelStyle: { fontSize: 17 } }],
126
- grid: { horizontal: false, vertical: true },
127
- series: [{ label: 'Cases', data: [1, 2], showMark: true }],
128
- legendPosition,
129
- });
130
-
131
- const props = lineSpy.mock.calls.at(-1)[0];
132
- expect(props.xAxis[0].tickLabelStyle.fontSize).toBe(17);
133
- expect(props.grid).toEqual({ horizontal: false, vertical: true });
134
- expect(props.series[0].showMark).toBe(true);
135
- expect(props.slotProps.legend.position).toEqual(legendPosition);
136
- });
137
-
138
- it('adds a caller-requested vertical grid to the default horizontal grid', () => {
139
- renderLine({ grid: { vertical: true } });
140
-
141
- expect(lineSpy.mock.calls.at(-1)[0].grid).toEqual({
142
- horizontal: true,
143
- vertical: true,
144
- });
145
- });
146
-
147
- it('lets a caller switch off both grid directions explicitly', () => {
148
- renderBar({ grid: { horizontal: false, vertical: false } });
149
-
150
- expect(barSpy.mock.calls.at(-1)[0].grid).toEqual({
151
- horizontal: false,
152
- vertical: false,
153
- });
154
- });
155
-
156
- // Found live in the dev harness (not by this mocked test, which cannot
157
- // reproduce a real MUI scale computation): a caller-supplied xAxis with
158
- // `data` but no `scaleType` lost the wrapper's default scaleType entirely,
159
- // leaving MUI's point/band scale undefined and every rendered path NaN.
160
- // This asserts the prop-level contract; the harness's "Chart defaults" /
161
- // "Caller overrides" LineChart specimens are the real-render proof.
162
- it('keeps the default scaleType on a caller-supplied axis that does not set one', () => {
163
- renderLine({ xAxis: [{ data: ['Jan', 'Feb'] }] });
164
- expect(lineSpy.mock.calls.at(-1)[0].xAxis[0].scaleType).toBe('point');
165
-
166
- renderBar({ xAxis: [{ data: ['Jan', 'Feb'] }] });
167
- expect(barSpy.mock.calls.at(-1)[0].xAxis[0].scaleType).toBe('band');
168
- });
169
-
170
- // UCM-CHART-12: rotation is now an explicit xLabels="angled" request, not an implicit
171
- // consequence of a caller-supplied tickLabelStyle.angle -- replaces the old CHART-10 regression
172
- // test (grows only xAxis.height, never margin.bottom on top of it) against the new API.
173
- it('grows only xAxis.height for xLabels="angled", never margin.bottom on top of it', () => {
174
- renderBar({
175
- xAxis: [{ data: ['A label long enough to need real rotation room'] }],
176
- xLabels: 'angled',
177
- });
178
- const rotatedProps = barSpy.mock.calls.at(-1)[0];
179
- expect(rotatedProps.margin.bottom).toBe(PACKAGE_DEFAULT_MARGIN.bottom);
180
- expect(rotatedProps.xAxis[0].height).toBeGreaterThan(45);
181
-
182
- cleanup();
183
- renderBar({ xAxis: [{ data: ['Jan', 'Feb'] }], xLabels: 'horizontal' });
184
- expect(barSpy.mock.calls.at(-1)[0].margin.bottom).toBe(PACKAGE_DEFAULT_MARGIN.bottom);
185
- });
186
-
187
- // THEME-11 -- the package forms its own opinion on chart margins (was: MUI's own flat 20px on
188
- // every side). UCM-CHART-12: there is no caller-facing `margin` prop any more (the model owns
189
- // spacing completely), so these assert the rendered default directly rather than through
190
- // `withMarginDefaults`, which UCM-CHART-12 removed as dead code once its only caller (the
191
- // deleted `margin` prop) was gone.
192
- describe('package margin default (THEME-11 / UCM-CHART-12)', () => {
193
- it('applies the package default on every side when there is no legend to make room for', () => {
194
- renderBar({ hideLegend: true });
195
- expect(barSpy.mock.calls.at(-1)[0].margin).toEqual(PACKAGE_DEFAULT_MARGIN);
196
- });
197
-
198
- it('gives a rotated-tick chart the same flat package bottom margin as an unrotated one', () => {
199
- renderBar({
200
- xAxis: [{ data: ['January', 'February'] }],
201
- xLabels: 'angled',
202
- hideLegend: true,
203
- });
204
- const props = barSpy.mock.calls.at(-1)[0];
205
- expect(props.margin.bottom).toBe(PACKAGE_DEFAULT_MARGIN.bottom);
206
- expect(props.xAxis[0].height).toBeGreaterThan(45);
207
- });
208
-
209
- it('keeps a wider default right margin than left/bottom, for a linear x-axis\'s edge-sitting last tick', () => {
210
- expect(PACKAGE_DEFAULT_MARGIN.right).toBeGreaterThan(PACKAGE_DEFAULT_MARGIN.left);
211
- expect(PACKAGE_DEFAULT_MARGIN.right).toBeGreaterThan(PACKAGE_DEFAULT_MARGIN.bottom);
212
- });
213
-
214
- it('applies the SAME package default to LineChart as BarChart', () => {
215
- renderLine({ hideLegend: true });
216
- expect(lineSpy.mock.calls.at(-1)[0].margin).toEqual(PACKAGE_DEFAULT_MARGIN);
217
- });
218
-
219
- it('grows margin.bottom by the legend band when a legend renders at the bottom (the default)', () => {
220
- renderBar({ series: [{ data: [1] }, { data: [2] }] }); // 2 series -> legend shown
221
- const props = barSpy.mock.calls.at(-1)[0];
222
- expect(props.margin.bottom).toBeGreaterThan(PACKAGE_DEFAULT_MARGIN.bottom);
223
- expect(props.margin.top).toBe(PACKAGE_DEFAULT_MARGIN.top);
224
- });
225
-
226
- it('grows margin.top instead when legendPosition.vertical is "top"', () => {
227
- renderBar({
228
- series: [{ data: [1] }, { data: [2] }],
229
- legendPosition: { vertical: 'top' },
230
- });
231
- const props = barSpy.mock.calls.at(-1)[0];
232
- expect(props.margin.top).toBeGreaterThan(PACKAGE_DEFAULT_MARGIN.top);
233
- expect(props.margin.bottom).toBe(PACKAGE_DEFAULT_MARGIN.bottom);
234
- });
235
- });
236
-
237
- it('uses the series colour as the fill when a caller opts line markers back in', () => {
238
- renderLine({ series: [{ label: 'Cases', data: [1, 2], showMark: true }] });
239
- const props = lineSpy.mock.calls.at(-1)[0];
240
- const mark = props.slots.mark({
241
- color: '#3D5A99',
242
- id: 'cases',
243
- dataIndex: 0,
244
- shape: 'circle',
245
- x: 10,
246
- y: 10,
247
- });
248
-
249
- expect(mark.props.style.fill).toBe('#3D5A99');
250
- expect(mark.props.style.fill).not.toBe(theme.palette.background.paper);
251
- });
252
-
253
- it('defaults legendPosition to bottom-start and lets a caller choose another MUI position', () => {
254
- renderBar({ series: [{ data: [1] }, { data: [2] }] });
255
- expect(barSpy.mock.calls.at(-1)[0].slotProps.legend.position).toEqual({
256
- vertical: 'bottom',
257
- horizontal: 'start',
258
- });
259
-
260
- cleanup();
261
- const requested = { vertical: 'middle' };
262
- renderBar({ series: [{ data: [1] }, { data: [2] }], legendPosition: requested });
263
- expect(barSpy.mock.calls.at(-1)[0].slotProps.legend.position).toEqual({
264
- vertical: 'middle',
265
- horizontal: 'start',
266
- });
267
-
268
- cleanup();
269
- renderBar({
270
- series: [{ data: [1] }, { data: [2] }],
271
- slotProps: { legend: { position: { horizontal: 'end' } } },
272
- });
273
- expect(barSpy.mock.calls.at(-1)[0].slotProps.legend.position).toEqual({
274
- vertical: 'bottom',
275
- horizontal: 'end',
276
- });
277
- });
278
- });
279
-
280
- // UCM-CHART-12: removed props (`minHeight`, `aspect`, `margin`) throw a dev-mode error naming
281
- // their replacement instead of being silently ignored -- 38 existing call sites across the estate
282
- // hit this. Gated on NODE_ENV so a shared package cannot crash a consumer's production page.
283
- describe('removed chart props throw in dev, naming their replacement (UCM-CHART-12)', () => {
284
- afterEach(cleanup);
285
-
286
- it('assertRemovedChartProp is a no-op when the value is unset', () => {
287
- expect(() => assertRemovedChartProp('BarChart', 'minHeight', undefined, 'x')).not.toThrow();
288
- });
289
-
290
- it('assertRemovedChartProp throws in development, naming the component, prop, and replacement', () => {
291
- expect(() => assertRemovedChartProp('BarChart', 'minHeight', 320, 'Use size instead.'))
292
- .toThrow(/BarChart.*minHeight.*Use size instead\./s);
293
- });
294
-
295
- // UCM-CHART-13 (codex review finding): a shared helper hardcoding one WO/version string would
296
- // misattribute the removal for a different call site's own, different removal.
297
- it('defaults removedIn to UCM-CHART-12/v3.0.0 (the four presets\' own call sites), but a caller can override it', () => {
298
- expect(() => assertRemovedChartProp('BarChart', 'minHeight', 320, 'Use size instead.'))
299
- .toThrow(/UCM-CHART-12, v3\.0\.0/);
300
- expect(() => assertRemovedChartProp('ChartFrame', 'aspect', 1.8, 'Use size instead.', 'UCM-CHART-13, v3.1.0'))
301
- .toThrow(/UCM-CHART-13, v3\.1\.0/);
302
- });
303
-
304
- it('assertRemovedChartProp stays inert in production', () => {
305
- const original = process.env.NODE_ENV;
306
- process.env.NODE_ENV = 'production';
307
- try {
308
- expect(() => assertRemovedChartProp('BarChart', 'minHeight', 320, 'Use size instead.')).not.toThrow();
309
- } finally {
310
- process.env.NODE_ENV = original;
311
- }
312
- });
313
-
314
- it.each([
315
- ['BarChart', BarChart, 'minHeight', 320],
316
- ['BarChart', BarChart, 'aspect', 1.8],
317
- ['BarChart', BarChart, 'margin', { top: 1 }],
318
- ['LineChart', LineChart, 'minHeight', 320],
319
- ['LineChart', LineChart, 'aspect', 1.8],
320
- ['LineChart', LineChart, 'margin', { top: 1 }],
321
- ])('%s throws when a caller still passes %s', (_name, Component, prop, value) => {
322
- expect(() => render(withProviders(React.createElement(Component, {
323
- series: [{ label: 'Cases', data: [1, 2] }],
324
- [prop]: value,
325
- })))).toThrow();
326
- });
327
- });
328
-
329
- // UCM-CHART-12 -- the composition invariant, the actual deliverable of this WO. Two things are
330
- // asserted per case, and they are NOT the same guarantee (reviewer finding R1):
331
- // (1) the SUM `plotHeight + xAxisBand + xTitleBand + legendBand === chartHeight` -- true by
332
- // construction (`plotHeight` is defined as the residual of the other three), so on its own
333
- // this proves only that `resolveChartLayout`'s return shape is internally consistent, NOT the
334
- // absence of a double-counted term.
335
- // (2) the PER-TERM MAPPING -- `xAxis[0].height` carries EXACTLY `xAxisBand + xTitleBand` and
336
- // nothing else, `margin`'s growth over the package baseline carries EXACTLY `legendBand` and
337
- // nothing else. THIS is what would have caught a `UCM-CHART-10`-shaped bug (the same
338
- // allowance reserved twice, once in axis height and again in margin) -- assertion (1) alone
339
- // would not have, since a term double-counted in both places still sums correctly.
340
- describe('resolveChartLayout composition invariant (UCM-CHART-12)', () => {
341
- const bandAxis = (data) => [{ scaleType: 'band', data, label: undefined }];
342
-
343
- it.each([
344
- ['none', []],
345
- ['short', ['Jan', 'Feb']],
346
- ['long', ['A much longer ward name than the others']],
347
- ['long-and-many', Array.from({ length: 12 }, (_, i) => `Ward number ${i} with a longish name`)],
348
- ['empty-strings', ['', '', '']],
349
- ])('holds the height invariant for the "%s" label load, with no term double-counted', (_label, data) => {
350
- const layout = resolveChartLayout({
351
- size: 'standard',
352
- xAxis: bandAxis(data),
353
- yAxis: [{ scaleType: 'linear', min: 0, max: 100 }],
354
- spacing: theme.spacing,
355
- measureTextWidth: () => null, // force the deterministic estimate fallback
356
- });
357
- const { bands } = layout;
358
- expect(bands.plotHeight + bands.xAxisBand + bands.xTitleBand + bands.legendBand)
359
- .toBe(layout.chartHeight);
360
- // The per-term mapping -- the actual double-count guard (see the describe-level comment).
361
- expect(layout.xAxis[0].height).toBe(bands.xAxisBand + bands.xTitleBand);
362
- expect(layout.margin.top + layout.margin.bottom - PACKAGE_DEFAULT_MARGIN.top - PACKAGE_DEFAULT_MARGIN.bottom)
363
- .toBe(bands.legendBand);
364
- });
365
-
366
- it('collapses the x-axis band to zero when every tick label is blank (the Access-scatter case)', () => {
367
- const layout = resolveChartLayout({
368
- xAxis: [{ scaleType: 'band', data: ['', '', ''] }],
369
- yAxis: [{ scaleType: 'linear' }],
370
- spacing: theme.spacing,
371
- });
372
- expect(layout.bands.xAxisBand).toBe(0);
373
- expect(layout.xAxis[0].height).toBe(0);
374
- });
375
-
376
- it('collapses the x-axis band to zero when there is no tick data at all', () => {
377
- const layout = resolveChartLayout({
378
- xAxis: [{ scaleType: 'band', data: [] }],
379
- yAxis: [{ scaleType: 'linear' }],
380
- spacing: theme.spacing,
381
- });
382
- expect(layout.bands.xAxisBand).toBe(0);
383
- });
384
-
385
- it('assumes non-empty content when the axis has no data array at all (a numeric scale)', () => {
386
- const layout = resolveChartLayout({
387
- xAxis: [{ scaleType: 'linear' }],
388
- yAxis: [{ scaleType: 'linear' }],
389
- spacing: theme.spacing,
390
- });
391
- expect(layout.bands.xAxisBand).toBeGreaterThan(0);
392
- });
393
-
394
- it('reserves the x-title band only when the x-axis carries a label', () => {
395
- const withoutTitle = resolveChartLayout({
396
- xAxis: [{ scaleType: 'band', data: ['Jan'] }],
397
- yAxis: [{ scaleType: 'linear' }],
398
- spacing: theme.spacing,
399
- });
400
- expect(withoutTitle.bands.xTitleBand).toBe(0);
401
-
402
- const withTitle = resolveChartLayout({
403
- xAxis: [{ scaleType: 'band', data: ['Jan'], label: 'Month' }],
404
- yAxis: [{ scaleType: 'linear' }],
405
- spacing: theme.spacing,
406
- });
407
- expect(withTitle.bands.xTitleBand).toBeGreaterThan(0);
408
- });
409
-
410
- it('reserves the legend band only when the legend is not hidden', () => {
411
- const hidden = resolveChartLayout({
412
- xAxis: [{ scaleType: 'band', data: ['Jan'] }],
413
- yAxis: [{ scaleType: 'linear' }],
414
- hideLegend: true,
415
- spacing: theme.spacing,
416
- });
417
- expect(hidden.bands.legendBand).toBe(0);
418
-
419
- const shown = resolveChartLayout({
420
- xAxis: [{ scaleType: 'band', data: ['Jan'] }],
421
- yAxis: [{ scaleType: 'linear' }],
422
- hideLegend: false,
423
- spacing: theme.spacing,
424
- });
425
- expect(shown.bands.legendBand).toBeGreaterThan(0);
426
- });
427
-
428
- // Reviewer finding R2: a `vertical: 'middle'` legend renders on the SIDE (consuming width, not
429
- // height) -- it must not reserve a height band or grow margin.top/bottom, or the height
430
- // invariant would over-reserve height for space the legend never occupies there.
431
- it('reserves no height band for a side-placed ("middle") legend', () => {
432
- const layout = resolveChartLayout({
433
- xAxis: [{ scaleType: 'band', data: ['Jan'] }],
434
- yAxis: [{ scaleType: 'linear' }],
435
- hideLegend: false,
436
- legendPosition: { vertical: 'middle' },
437
- spacing: theme.spacing,
438
- });
439
- expect(layout.bands.legendBand).toBe(0);
440
- expect(layout.margin.top).toBe(PACKAGE_DEFAULT_MARGIN.top);
441
- expect(layout.margin.bottom).toBe(PACKAGE_DEFAULT_MARGIN.bottom);
442
- });
443
-
444
- it('holds the width invariant when a containerWidth is supplied (test-only -- presets never pass this)', () => {
445
- const layout = resolveChartLayout({
446
- xAxis: [{ scaleType: 'band', data: ['Jan', 'Feb'] }],
447
- yAxis: [{ scaleType: 'linear', min: 0, max: 250000 }],
448
- tickFontSize: 12,
449
- spacing: theme.spacing,
450
- containerWidth: 600,
451
- });
452
- const { bands } = layout;
453
- expect(bands.yAxisBand + bands.plotWidth + bands.rightPad).toBe(600);
454
- expect(bands.yAxisBand).toBeGreaterThan(0);
455
- });
456
-
457
- // Codex second-pass review finding: a dual-axis chart (TimeSeriesChart's axis: 'secondary'
458
- // feature) has a SECOND y-axis on the right, which the resolver previously ignored entirely --
459
- // `sizedYAxis[0]` was the only width ever accounted for. The width invariant must include it.
460
- it('accounts for a secondary (right-positioned) y-axis in both margin.right and the width invariant', () => {
461
- const single = resolveChartLayout({
462
- xAxis: [{ scaleType: 'band', data: ['Jan', 'Feb'] }],
463
- yAxis: [{ scaleType: 'linear', id: 'primary', min: 0, max: 100 }],
464
- tickFontSize: 12,
465
- spacing: theme.spacing,
466
- containerWidth: 600,
467
- });
468
- const dual = resolveChartLayout({
469
- xAxis: [{ scaleType: 'band', data: ['Jan', 'Feb'] }],
470
- yAxis: [
471
- { scaleType: 'linear', id: 'primary', position: 'left', min: 0, max: 100 },
472
- { scaleType: 'linear', id: 'secondary', position: 'right', min: 0, max: 500000 },
473
- ],
474
- tickFontSize: 12,
475
- spacing: theme.spacing,
476
- containerWidth: 600,
477
- });
478
-
479
- expect(dual.bands.secondaryYAxisBand).toBeGreaterThan(0);
480
- // The wider secondary axis (500000 vs 100) must reserve MORE than the primary alone.
481
- expect(dual.margin.right).toBeGreaterThan(single.margin.right);
482
- expect(dual.margin.right).toBe(PACKAGE_DEFAULT_MARGIN.right + dual.bands.secondaryYAxisBand);
483
- // Width invariant, now with the secondary band as a fourth term.
484
- expect(dual.bands.yAxisBand + dual.bands.plotWidth + dual.bands.secondaryYAxisBand + dual.bands.rightPad)
485
- .toBe(600);
486
- });
487
-
488
- it('an unmarked (no explicit position) second y-axis defaults to the LEFT, matching MUI, and reserves no secondary band', () => {
489
- const layout = resolveChartLayout({
490
- xAxis: [{ scaleType: 'band', data: ['Jan'] }],
491
- yAxis: [
492
- { scaleType: 'linear', id: 'a', min: 0, max: 1 },
493
- { scaleType: 'linear', id: 'b', min: 0, max: 1 },
494
- ],
495
- tickFontSize: 12,
496
- spacing: theme.spacing,
497
- });
498
- expect(layout.bands.secondaryYAxisBand).toBe(0);
499
- });
500
-
501
- // Codex second-pass review finding: a caller-supplied `valueFormatter` that blanks every tick
502
- // must collapse the y-axis band to zero too (Rule 2's general principle), not just the x-axis
503
- // case the WO's Definition of Done names explicitly.
504
- it('collapses the y-axis band to zero when every tick formats to a blank string', () => {
505
- const layout = resolveChartLayout({
506
- xAxis: [{ scaleType: 'band', data: ['Jan'] }],
507
- yAxis: [{ scaleType: 'linear', min: 0, max: 1, valueFormatter: () => '' }],
508
- tickFontSize: 12,
509
- spacing: theme.spacing,
510
- });
511
- expect(layout.bands.yAxisBand).toBe(0);
512
- expect(layout.yAxis[0].width).toBe(0);
513
- });
514
-
515
- // Codex second-pass review finding: `slotProps.legend.position` can override `legendPosition` at
516
- // render time (`withChartSlotDefaults`) -- the resolver must reserve space for wherever the
517
- // legend ACTUALLY renders, not just what `legendPosition` alone says.
518
- it('reserves space for the slotProps.legend.position override, not just legendPosition', () => {
519
- const layout = resolveChartLayout({
520
- xAxis: [{ scaleType: 'band', data: ['Jan'] }],
521
- yAxis: [{ scaleType: 'linear' }],
522
- legendPosition: { vertical: 'bottom' },
523
- slotProps: { legend: { position: { vertical: 'top' } } },
524
- spacing: theme.spacing,
525
- });
526
- expect(layout.margin.top).toBeGreaterThan(PACKAGE_DEFAULT_MARGIN.top);
527
- expect(layout.margin.bottom).toBe(PACKAGE_DEFAULT_MARGIN.bottom);
528
- });
529
-
530
- it('resolveLegendPosition matches withChartSlotDefaults\' own merge precedence exactly', () => {
531
- const legendPosition = { vertical: 'bottom', horizontal: 'start' };
532
- const slotProps = { legend: { position: { horizontal: 'end' } } };
533
- const resolved = resolveLegendPosition(legendPosition, slotProps);
534
- const fromSlotDefaults = withChartSlotDefaults(slotProps, legendPosition).legend.position;
535
- expect(resolved).toEqual(fromSlotDefaults);
536
- expect(resolved).toEqual({ vertical: 'bottom', horizontal: 'end' });
537
- });
538
-
539
- // Codex second-pass review finding: a caller-set `xAxis[0].height` bypasses the model's SIZE,
540
- // but rotation must still be governed entirely by `xLabels` -- a stale caller-set angle must not
541
- // survive under a mode that says "no rotation".
542
- it('still governs rotation via xLabels even when the caller sets an explicit axis height', () => {
543
- const layout = resolveChartLayout({
544
- xAxis: [{ scaleType: 'band', data: ['Jan'], height: 60, tickLabelStyle: { angle: -60 } }],
545
- yAxis: [{ scaleType: 'linear' }],
546
- xLabels: 'horizontal',
547
- spacing: theme.spacing,
548
- });
549
- expect(layout.xAxis[0].height).toBe(60); // the caller's own height escape still wins
550
- expect(layout.xAxis[0].tickLabelStyle.angle).toBeFalsy(); // but the stale rotation does not survive
551
- });
552
- });
553
-
554
- describe('resolveChartLayout size tokens and height escape (UCM-CHART-12, UCM-CHART-15)', () => {
555
- it.each(Object.entries(CHART_SIZE_SPACING_UNITS))('resolves size="%s" through the theme spacing scale', (size, units) => {
556
- const layout = resolveChartLayout({ size, spacing: theme.spacing });
557
- expect(layout.chartHeight).toBe(units * 8);
558
- });
559
-
560
- // UCM-CHART-15: hardcoded independently of CHART_SIZE_SPACING_UNITS -- the test above computes
561
- // its expectation from the same source under test and would pass unchanged against either the
562
- // old or the new scale. This one is written to fail against the pre-CHART-15 values (240/320/400).
563
- it('resolves the five size tokens to their documented pixel heights', () => {
564
- const px = Object.fromEntries(['compact', 'standard', 'tall', 'extra_tall', 'super_tall'].map(
565
- (size) => [size, resolveChartLayout({ size, spacing: theme.spacing }).chartHeight],
566
- ));
567
- expect(px).toEqual({
568
- compact: 320, standard: 400, tall: 480, extra_tall: 560, super_tall: 640,
569
- });
570
- });
571
-
572
- it('lets height override the size token entirely', () => {
573
- const layout = resolveChartLayout({ size: 'tall', height: 111, spacing: theme.spacing });
574
- expect(layout.chartHeight).toBe(111);
575
- });
576
-
577
- // UCM-CHART-15: the height escape still wins for a token added in this WO, not only a
578
- // pre-existing one.
579
- it('lets height override a token that has no call site yet', () => {
580
- const layout = resolveChartLayout({ size: 'super_tall', height: 111, spacing: theme.spacing });
581
- expect(layout.chartHeight).toBe(111);
582
- });
583
-
584
- it('throws on an unknown size token, naming all five valid tokens', () => {
585
- expect(() => resolveChartLayout({ size: 'huge', spacing: theme.spacing })).toThrow(/Unknown chart size/);
586
- expect(() => resolveChartLayout({ size: 'huge', spacing: theme.spacing })).toThrow(
587
- /"compact" \| "standard" \| "tall" \| "extra_tall" \| "super_tall"/,
588
- );
589
- });
590
-
591
- it('falls back to a numeric 8px unit when no spacing function is given', () => {
592
- const layout = resolveChartLayout({ size: 'standard' });
593
- expect(layout.chartHeight).toBe(400);
594
- });
595
- });
596
-
597
- describe('resolveChartLayout flat x-axis tick band leaves MUI room to render (UCM-CHART-16)', () => {
598
- // Third occurrence (WO Part A): no title, just a tick font 1px above the untitled default --
599
- // already enough to blank every label under the pre-fix, unconditional TICK_BAND_BASE_PX=25.
600
- // 26 is not invented: binary-searched against the real, unmocked `BarChart` rendered in this
601
- // repo's own dev harness (Chrome, this package's own DM Sans default) -- 25 still blanked every
602
- // tick, 26 was the smallest axis height that didn't. A test computing its own expectation from
603
- // `CHART_TICK_TEXT_HEIGHT_FACTOR`-style constants would pass whatever those constants say,
604
- // proving nothing; this number came from outside the code under test.
605
- it('reserves enough room for a tick font one pixel above the untitled default (measured threshold: 26px)', () => {
606
- const layout = resolveChartLayout({
607
- xAxis: [{
608
- scaleType: 'band',
609
- data: ['Urban', 'Peri-urban', 'Rural', 'Access', 'Village'],
610
- tickLabelStyle: { fontSize: 13 },
611
- }],
612
- yAxis: [{ scaleType: 'linear' }],
613
- xLabels: 'horizontal',
614
- spacing: theme.spacing,
615
- });
616
- expect(layout.xAxis[0].height).toBeGreaterThanOrEqual(26);
617
- });
618
-
619
- // Rule 2 (WO non-goal guardrail): an axis without a title still reserves only the tick band --
620
- // this WO's extra title-driven margin must not leak in when there is no title to share it with.
621
- // Reviewer finding: asserting `xTitleBand === 0` alone passes whether or not the +2px title
622
- // margin leaked into `xAxisBand` instead -- the band/height values themselves have to be
623
- // checked against the same tick font's no-title formula (`ceil(fontSize * 1.3) + 9`).
624
- it('does not add the title margin when the axis has no label', () => {
625
- const layout = resolveChartLayout({
626
- xAxis: [{ scaleType: 'band', data: ['A', 'B'], tickLabelStyle: { fontSize: 13 } }],
627
- yAxis: [{ scaleType: 'linear' }],
628
- xLabels: 'horizontal',
629
- spacing: theme.spacing,
630
- });
631
- expect(layout.bands.xTitleBand).toBe(0);
632
- expect(layout.bands.xAxisBand).toBe(26);
633
- expect(layout.xAxis[0].height).toBe(26);
634
- });
635
- });
636
-
637
- describe('resolveChartLayout xLabels rotation (UCM-CHART-12)', () => {
638
- it('"horizontal" never rotates, regardless of tick count or label length', () => {
639
- const layout = resolveChartLayout({
640
- xAxis: [{ scaleType: 'band', data: Array.from({ length: 20 }, (_, i) => `A rather long label ${i}`) }],
641
- yAxis: [{ scaleType: 'linear' }],
642
- xLabels: 'horizontal',
643
- spacing: theme.spacing,
644
- });
645
- expect(layout.xAxis[0].tickLabelStyle?.angle).toBeFalsy();
646
- });
647
-
648
- it('"angled" always rotates, even for a single short label', () => {
649
- const layout = resolveChartLayout({
650
- xAxis: [{ scaleType: 'band', data: ['Jan'] }],
651
- yAxis: [{ scaleType: 'linear' }],
652
- xLabels: 'angled',
653
- spacing: theme.spacing,
654
- });
655
- expect(layout.xAxis[0].tickLabelStyle.angle).toBe(-45);
656
- });
657
-
658
- it('"auto" stays horizontal for a few short labels, rotates for many long ones (measured width, not char count)', () => {
659
- // codex second-pass review finding: "auto" must decide from the label's MEASURED width, not a
660
- // raw character count -- these inject a deterministic measurer instead of relying on the
661
- // per-glyph estimate, and a `tickFontSize` fallback so the resolver can actually measure.
662
- const few = resolveChartLayout({
663
- xAxis: [{ scaleType: 'band', data: ['Jan', 'Feb', 'Mar'] }],
664
- yAxis: [{ scaleType: 'linear' }],
665
- xLabels: 'auto',
666
- tickFontSize: 12,
667
- spacing: theme.spacing,
668
- measureTextWidth: () => 20, // narrow -- well under the rotation threshold
669
- });
670
- expect(few.xAxis[0].tickLabelStyle?.angle).toBeFalsy();
671
-
672
- const many = resolveChartLayout({
673
- xAxis: [{ scaleType: 'band', data: Array.from({ length: 12 }, (_, i) => `Ward name ${i}`) }],
674
- yAxis: [{ scaleType: 'linear' }],
675
- xLabels: 'auto',
676
- tickFontSize: 12,
677
- spacing: theme.spacing,
678
- measureTextWidth: () => 90, // wide -- over the rotation threshold
679
- });
680
- expect(many.xAxis[0].tickLabelStyle.angle).toBe(-45);
681
- });
682
-
683
- it('"auto" does not rotate a few WIDE labels -- tick count and measured width must both cross the threshold', () => {
684
- const layout = resolveChartLayout({
685
- xAxis: [{ scaleType: 'band', data: ['A very wide single label'] }],
686
- yAxis: [{ scaleType: 'linear' }],
687
- xLabels: 'auto',
688
- tickFontSize: 12,
689
- spacing: theme.spacing,
690
- measureTextWidth: () => 200, // wide, but only one category
691
- });
692
- expect(layout.xAxis[0].tickLabelStyle?.angle).toBeFalsy();
693
- });
694
-
695
- it('"auto" does not rotate many labels that are individually narrow', () => {
696
- const layout = resolveChartLayout({
697
- xAxis: [{ scaleType: 'band', data: Array.from({ length: 12 }, (_, i) => `${i}`) }],
698
- yAxis: [{ scaleType: 'linear' }],
699
- xLabels: 'auto',
700
- tickFontSize: 12,
701
- spacing: theme.spacing,
702
- measureTextWidth: () => 8, // many categories, but each label is tiny
703
- });
704
- expect(layout.xAxis[0].tickLabelStyle?.angle).toBeFalsy();
705
- });
706
-
707
- it('drives the rotated tick band from a measured width, not a flat constant', () => {
708
- const short = resolveChartLayout({
709
- xAxis: [{ scaleType: 'band', data: ['A'], tickLabelStyle: { fontSize: 12 } }],
710
- yAxis: [{ scaleType: 'linear' }],
711
- xLabels: 'angled',
712
- spacing: theme.spacing,
713
- measureTextWidth: () => 10,
714
- });
715
- const long = resolveChartLayout({
716
- xAxis: [{ scaleType: 'band', data: ['A much longer label'], tickLabelStyle: { fontSize: 12 } }],
717
- yAxis: [{ scaleType: 'linear' }],
718
- xLabels: 'angled',
719
- spacing: theme.spacing,
720
- measureTextWidth: () => 140,
721
- });
722
- expect(long.bands.xAxisBand).toBeGreaterThan(short.bands.xAxisBand);
723
- });
724
-
725
- it('does not override an axis that already sets its own height', () => {
726
- const layout = resolveChartLayout({
727
- xAxis: [{ scaleType: 'band', data: ['January'], height: 200 }],
728
- yAxis: [{ scaleType: 'linear' }],
729
- xLabels: 'angled',
730
- spacing: theme.spacing,
731
- });
732
- expect(layout.xAxis[0].height).toBe(200);
733
- });
734
- });
735
-
736
- // THEME-9 #2 -- `withAxisDefaults` broadcast the caller's single xAxisLabel/
737
- // yAxisLabel string onto EVERY entry of a multi-axis array via `.map()`.
738
- // Measured live: hram's dual-axis chart carried the identical 43-character
739
- // label on both y-axes. This is the defect that actually shipped, pinned
740
- // directly against the exported function rather than only through a
741
- // rendered BarChart (which never happens to build a multi-axis array).
742
- describe('withAxisDefaults label broadcast (THEME-9)', () => {
743
- it('applies the single label argument only to the first axis of a multi-entry array', () => {
744
- const result = withAxisDefaults(
745
- [{ id: 'primary' }, { id: 'secondary' }],
746
- 'Shared label',
747
- { scaleType: 'linear' },
748
- '12px',
749
- );
750
- expect(result[0].label).toBe('Shared label');
751
- expect(result[1].label).toBeUndefined();
752
- // The exact defect: never let one string land on two axes.
753
- expect(result.filter((axis) => axis.label === 'Shared label')).toHaveLength(1);
754
- });
755
-
756
- it('leaves axes that set their own label untouched, whatever position they are in', () => {
757
- const result = withAxisDefaults(
758
- [{ label: 'Primary' }, { label: 'Secondary' }],
759
- 'Fallback',
760
- { scaleType: 'linear' },
761
- '12px',
762
- );
763
- expect(result[0].label).toBe('Primary');
764
- expect(result[1].label).toBe('Secondary');
765
- });
766
-
767
- it('still labels the common single-axis case unchanged', () => {
768
- const result = withAxisDefaults([{ id: 'only' }], 'Cases', { scaleType: 'linear' }, '12px');
769
- expect(result[0].label).toBe('Cases');
770
-
771
- const synthesized = withAxisDefaults(undefined, 'Cases', { scaleType: 'linear' }, '12px');
772
- expect(synthesized[0].label).toBe('Cases');
773
- });
774
- });
775
-
776
- // THEME-9 #4 -- BarChart set no default tick formatter at all (raw domain
777
- // numbers); a hand-rolled `Intl` compact-notation default was measured to be
778
- // actively wrong in two of the four locales this package's consumers use:
779
- // German does not compact thousands at all, and Swahili prepends a word
780
- // ("elfu") that is WIDER than the raw number. Plain grouped formatting (no
781
- // `notation: 'compact'`) is pinned here against a live `Intl.NumberFormat`
782
- // computation rather than hardcoded locale punctuation, so the assertion
783
- // survives ICU data updates.
784
- describe('defaultNumericTickFormatter (THEME-9)', () => {
785
- it.each(['de-CH', 'en', 'fr', 'sw'])('formats %s ticks the same way Intl grouping would, with no compact notation', (locale) => {
786
- const formatter = defaultNumericTickFormatter(locale);
787
- for (const value of [12500, 998000, 1000000000]) {
788
- const expected = new Intl.NumberFormat(locale, { maximumFractionDigits: 1 }).format(value);
789
- expect(formatter(value, { location: 'tick' })).toBe(expected);
790
- // The exact failure mode measured live: compact notation prepending a
791
- // wider word in Swahili, or a 'K'/'M'/'B' suffix elsewhere.
792
- expect(formatter(value, { location: 'tick' })).not.toMatch(/[KMB]|elfu/i);
793
- }
794
- });
795
-
796
- it('keeps full precision outside the tick context (tooltip/legend) rather than rounding away a meaningful difference', () => {
797
- const formatter = defaultNumericTickFormatter('en');
798
- expect(formatter(12500.456, { location: 'tooltip' })).toBe('12,500.456');
799
- expect(formatter(12500.456, { location: 'tick' })).toBe('12,500.5');
800
- });
801
- });
802
-
803
- // THEME-9 #1/#3 -- MUI's own y-axis width default is a flat 45px (ticks) +
804
- // 20px (label) regardless of what the ticks actually say (confirmed against
805
- // @mui/x-charts' own DEFAULT_AXIS_SIZE_WIDTH/AXIS_LABEL_DEFAULT_HEIGHT and
806
- // its useChartDimensions selector, where margin and axis size are additive).
807
- // That is both too generous for short ticks and too stingy for wide
808
- // unit-suffixed ones -- the measured Accessibility-panel overlap. Margins
809
- // and the exact rendered overlap are verified live, not by unit test (per
810
- // the WO); this pins the sizing FUNCTION's own contract.
811
- describe('sizeYAxisForContent (THEME-9)', () => {
812
- it('sizes a linear axis from its own caller-supplied min/max', () => {
813
- const [axis] = sizeYAxisForContent(
814
- [{ scaleType: 'linear', min: 0, max: 1, label: 'Coverage (%)' }],
815
- [],
816
- '12px',
817
- );
818
- expect(axis.width).toBeGreaterThan(0);
819
- });
820
-
821
- it('reserves more width for a longer formatted tick, all else equal', () => {
822
- const [shortTick] = sizeYAxisForContent(
823
- [{ scaleType: 'linear', min: 0, max: 1 }], [], '12px',
824
- );
825
- const [wideTick] = sizeYAxisForContent(
826
- [{ scaleType: 'linear', min: 0, max: 1, valueFormatter: (v) => `${v} minutes` }],
827
- [], '12px',
828
- );
829
- expect(wideTick.width).toBeGreaterThan(shortTick.width);
830
- });
831
-
832
- it('falls back to the plotted series when the caller sets neither min nor max', () => {
833
- const [axis] = sizeYAxisForContent(
834
- [{ scaleType: 'linear' }],
835
- [{ data: [1, 250000] }],
836
- '12px',
837
- );
838
- expect(axis.width).toBeGreaterThan(0);
839
- });
840
-
841
- it('leaves the axis untouched when there is nothing to measure against', () => {
842
- const [axis] = sizeYAxisForContent([{ scaleType: 'linear' }], [], '12px');
843
- expect(axis.width).toBeUndefined();
844
- });
845
-
846
- it('never overrides a caller-supplied width', () => {
847
- const [axis] = sizeYAxisForContent(
848
- [{ scaleType: 'linear', min: 0, max: 1, width: 42 }], [], '12px',
849
- );
850
- expect(axis.width).toBe(42);
851
- });
852
-
853
- // UCM-CHART-12 Rule 2 (codex second-pass review finding): candidates existed (min/max set), but
854
- // a caller `valueFormatter` reduced every tick to a blank string -- distinct from "nothing to
855
- // measure at all" (the untouched/undefined case below), this must collapse to a real 0, not the
856
- // font-size floor a blank string previously still received.
857
- it('collapses to width 0 when every formatted tick is blank and there is no axis title', () => {
858
- const [axis] = sizeYAxisForContent(
859
- [{ scaleType: 'linear', min: 0, max: 1, valueFormatter: () => '' }], [], '12px',
860
- );
861
- expect(axis.width).toBe(0);
862
- });
863
-
864
- it('still reserves width for the axis TITLE when ticks are blank but a label is set', () => {
865
- const [axis] = sizeYAxisForContent(
866
- [{ scaleType: 'linear', min: 0, max: 1, valueFormatter: () => '', label: 'Coverage' }], [], '12px',
867
- );
868
- expect(axis.width).toBeGreaterThan(0);
869
- });
870
-
871
- it('leaves a categorical (band/point) axis alone -- its ticks are names, not numbers', () => {
872
- const [axis] = sizeYAxisForContent(
873
- [{ scaleType: 'band', data: ['a', 'b'] }], [], '12px',
874
- );
875
- expect(axis.width).toBeUndefined();
876
- });
877
-
878
- // Reviewer-caught regression (THEME-9): a series with no explicit
879
- // `yAxisId` is NOT "assigned to no axis" -- MUI defaults it to the FIRST
880
- // axis in the array (`yAxisIds[0]`), whatever that axis' id is. Matching
881
- // only `undefined === undefined` left the primary axis of a genuine
882
- // dual-axis chart (id set, series unmarked) with zero matched series and
883
- // therefore MUI's untouched flat default -- a silent no-op on exactly the
884
- // dual-axis case this WO was measured against (TimeSeriesChart's own
885
- // secondary-axis feature, CHART-5).
886
- it('assigns an unmarked series to the FIRST axis in a dual-axis array, matching MUI\'s own default', () => {
887
- const dualAxis = [{ scaleType: 'linear', id: 'primary' }, { scaleType: 'linear', id: 'secondary' }];
888
- const series = [
889
- { data: [1, 2, 3] }, // no yAxisId -- MUI assigns this to 'primary'
890
- { data: [400000, 500000], yAxisId: 'secondary' },
891
- ];
892
-
893
- const [primary, secondary] = sizeYAxisForContent(dualAxis, series, '12px');
894
- expect(primary.width).toBeGreaterThan(0);
895
- expect(secondary.width).toBeGreaterThan(0);
896
- // The secondary axis' series format much wider numbers -- it must
897
- // reserve more width than the primary axis, not the same (which is what
898
- // "both axes silently untouched" or "both matched the same series" would
899
- // produce).
900
- expect(secondary.width).toBeGreaterThan(primary.width);
901
- });
902
- });