@micha.bigler/ui-core-micha 3.14.4 → 4.0.1

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 (341) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/README.md +7 -0
  3. package/dist/chat/kitSxRegistry.js +9 -0
  4. package/dist/components/charts/kitSxRegistry.js +6 -0
  5. package/dist/entries/charts.js +6 -0
  6. package/dist/entries/chat.js +8 -0
  7. package/dist/entries/markdown.js +1 -0
  8. package/dist/index.js +0 -14
  9. package/dist/pages/AccountPage.js +1 -2
  10. package/dist/pages/LoginPage.js +1 -2
  11. package/dist/pages/PasswordChangePage.js +1 -2
  12. package/dist/pages/PasswordInvitePage.js +1 -2
  13. package/dist/pages/PasswordResetRequestPage.js +1 -2
  14. package/dist/pages/SignUpPage.js +1 -2
  15. package/dist/pages/SignupConfirmPage.js +1 -2
  16. package/dist/theme/kitSxEntry.js +11 -0
  17. package/dist/theme/kitSxRegistry.js +2 -15
  18. package/package.json +31 -7
  19. package/.github/workflows/ci.yml +0 -35
  20. package/.github/workflows/publish.yml +0 -126
  21. package/DESIGN.md +0 -60
  22. package/WORK_ORDERS.md +0 -150
  23. package/dev/entries.jsx +0 -721
  24. package/dev/fixtures.js +0 -26
  25. package/dev/main.jsx +0 -75
  26. package/dev/mockTransport.jsx +0 -128
  27. package/docs/CHART-LAYOUT.md +0 -157
  28. package/docs/DARK-TOKENS.md +0 -140
  29. package/docs/TYPE-SCALE.md +0 -111
  30. package/docs/messaging-deviations.md +0 -118
  31. package/index.html +0 -12
  32. package/renovate.json +0 -10
  33. package/scripts/chart-api-census.mjs +0 -415
  34. package/src/auth/AuthContext.jsx +0 -158
  35. package/src/auth/ReauthModal.jsx +0 -82
  36. package/src/auth/UserMenu.jsx +0 -121
  37. package/src/auth/apiClient.jsx +0 -232
  38. package/src/auth/authApi.jsx +0 -539
  39. package/src/auth/authConfig.jsx +0 -34
  40. package/src/auth/reauth.js +0 -47
  41. package/src/chat/ChatComposer.jsx +0 -36
  42. package/src/chat/ChatList.jsx +0 -87
  43. package/src/chat/ChatThread.jsx +0 -128
  44. package/src/chat/ChatWorkspace.jsx +0 -244
  45. package/src/chat/MarkdownReply.jsx +0 -22
  46. package/src/chat/ModelPicker.jsx +0 -85
  47. package/src/chat/api.js +0 -18
  48. package/src/chat/useChatSocket.js +0 -32
  49. package/src/chat/wire.js +0 -7
  50. package/src/components/AccessCodeManager.jsx +0 -288
  51. package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
  52. package/src/components/AllowedEmailDomainsManager.jsx +0 -92
  53. package/src/components/AuthFactorRequirementCard.jsx +0 -95
  54. package/src/components/BulkInviteCsvTab.jsx +0 -198
  55. package/src/components/EmptyState.jsx +0 -38
  56. package/src/components/LoginForm.jsx +0 -159
  57. package/src/components/MFAComponent.jsx +0 -350
  58. package/src/components/MfaLoginComponent.jsx +0 -252
  59. package/src/components/MobileBottomNav.jsx +0 -82
  60. package/src/components/PasskeysComponent.jsx +0 -252
  61. package/src/components/PasswordChangeForm.jsx +0 -87
  62. package/src/components/PasswordResetRequestForm.jsx +0 -43
  63. package/src/components/PasswordSetForm.jsx +0 -97
  64. package/src/components/ProfileComponent.jsx +0 -348
  65. package/src/components/QrSignupManager.jsx +0 -404
  66. package/src/components/QrSignupValidityManager.jsx +0 -106
  67. package/src/components/RegistrationMethodsManager.jsx +0 -150
  68. package/src/components/SecurityComponent.jsx +0 -141
  69. package/src/components/SocialLoginButtons.jsx +0 -83
  70. package/src/components/SoftChip.jsx +0 -93
  71. package/src/components/StatTile.jsx +0 -39
  72. package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
  73. package/src/components/TurnstileWidget.jsx +0 -99
  74. package/src/components/UserInviteComponent.jsx +0 -76
  75. package/src/components/UserListComponent.jsx +0 -618
  76. package/src/components/charts/BarChart.jsx +0 -102
  77. package/src/components/charts/ChartFrame.jsx +0 -214
  78. package/src/components/charts/LineChart.jsx +0 -126
  79. package/src/components/charts/ScatterChart.jsx +0 -299
  80. package/src/components/charts/TimeSeriesChart.jsx +0 -300
  81. package/src/components/charts/chartDefaults.js +0 -644
  82. package/src/components/charts/chartLabels.js +0 -36
  83. package/src/components/charts/exportChart.js +0 -258
  84. package/src/components/charts/formatters.js +0 -39
  85. package/src/components/charts/palette.js +0 -33
  86. package/src/components/charts/yearTickInterval.js +0 -7
  87. package/src/i18n/authTranslations.ts +0 -2651
  88. package/src/i18n/chartsTranslations.ts +0 -91
  89. package/src/i18n/chatTranslations.ts +0 -55
  90. package/src/i18n/messagingTranslations.ts +0 -120
  91. package/src/i18n/notificationsTranslations.ts +0 -32
  92. package/src/i18n/onboardingTranslations.ts +0 -29
  93. package/src/i18n/sectionNavTranslations.ts +0 -20
  94. package/src/i18n/uiCoreTranslations.js +0 -29
  95. package/src/i18n/userMenuTranslations.ts +0 -5
  96. package/src/index.js +0 -158
  97. package/src/layout/PageLayout.jsx +0 -36
  98. package/src/layout/SectionNav.jsx +0 -320
  99. package/src/messaging/AttachmentList.jsx +0 -145
  100. package/src/messaging/Composer.jsx +0 -197
  101. package/src/messaging/ConversationLaunchers.jsx +0 -51
  102. package/src/messaging/ConversationList.jsx +0 -109
  103. package/src/messaging/DirectMessageLauncher.jsx +0 -112
  104. package/src/messaging/MessageBubble.jsx +0 -183
  105. package/src/messaging/MessagingProvider.jsx +0 -0
  106. package/src/messaging/MessagingScopeConfig.jsx +0 -24
  107. package/src/messaging/PollCard.jsx +0 -163
  108. package/src/messaging/ReactionBar.jsx +0 -43
  109. package/src/messaging/ReadTicks.jsx +0 -82
  110. package/src/messaging/Thread.jsx +0 -195
  111. package/src/messaging/api.js +0 -48
  112. package/src/messaging/composerImageCompression.js +0 -35
  113. package/src/messaging/linkifyText.jsx +0 -37
  114. package/src/notifications/NotificationBell.jsx +0 -84
  115. package/src/notifications/NotificationSettings.jsx +0 -330
  116. package/src/notifications/NotificationsProvider.jsx +0 -224
  117. package/src/notifications/PopupSurface.jsx +0 -105
  118. package/src/notifications/api.js +0 -55
  119. package/src/notifications/feedApi.js +0 -18
  120. package/src/notifications/realtime.jsx +0 -138
  121. package/src/notifications/serviceWorker/sw.js +0 -50
  122. package/src/onboarding/OnboardingProvider.jsx +0 -326
  123. package/src/onboarding/OnboardingWizard.jsx +0 -61
  124. package/src/onboarding/WizardDialogShell.jsx +0 -51
  125. package/src/onboarding/api.js +0 -13
  126. package/src/onboarding/stepSelection.js +0 -16
  127. package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
  128. package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
  129. package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
  130. package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
  131. package/src/pages/AccountPage.jsx +0 -394
  132. package/src/pages/LoginPage.jsx +0 -343
  133. package/src/pages/PasswordChangePage.jsx +0 -62
  134. package/src/pages/PasswordInvitePage.jsx +0 -177
  135. package/src/pages/PasswordResetRequestPage.jsx +0 -68
  136. package/src/pages/SignUpPage.jsx +0 -268
  137. package/src/pages/SignupConfirmPage.jsx +0 -155
  138. package/src/testing/checkKitIntegration.js +0 -233
  139. package/src/theme/createAppTheme.js +0 -423
  140. package/src/theme/fonts.js +0 -3
  141. package/src/theme/index.js +0 -21
  142. package/src/theme/kitSxRegistry.js +0 -285
  143. package/src/theme/themeCompleteness.js +0 -1045
  144. package/src/theme/tokens.js +0 -478
  145. package/src/utils/auth-errors.js +0 -76
  146. package/src/utils/authService.js +0 -223
  147. package/src/utils/resolveErrorText.js +0 -28
  148. package/src/utils/webauthn.js +0 -51
  149. package/tests/AccountPage.profileStatements.test.jsx +0 -162
  150. package/tests/AccountPage.test.jsx +0 -200
  151. package/tests/AuthContext.test.jsx +0 -129
  152. package/tests/BarChart.test.jsx +0 -112
  153. package/tests/BrowserPushStep.test.jsx +0 -110
  154. package/tests/ChartFrame.test.jsx +0 -336
  155. package/tests/ConversationList.test.jsx +0 -144
  156. package/tests/CookieConsentStep.test.jsx +0 -69
  157. package/tests/EmptyState.test.jsx +0 -62
  158. package/tests/LineChart.test.jsx +0 -80
  159. package/tests/MessagingProvider.test.jsx +0 -50
  160. package/tests/MobileBottomNav.test.jsx +0 -319
  161. package/tests/NotificationBell.test.jsx +0 -60
  162. package/tests/NotificationSettings.test.jsx +0 -276
  163. package/tests/NotificationsProvider.test.jsx +0 -240
  164. package/tests/OnboardingProvider.test.jsx +0 -294
  165. package/tests/OnboardingWizard.test.jsx +0 -143
  166. package/tests/PasswordChangeForm.test.jsx +0 -70
  167. package/tests/PasswordInvitePage.test.jsx +0 -142
  168. package/tests/PasswordSetForm.test.jsx +0 -70
  169. package/tests/PopupSurface.test.jsx +0 -142
  170. package/tests/ProfileComponent.refresh.test.jsx +0 -194
  171. package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
  172. package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
  173. package/tests/ScatterChart.test.jsx +0 -270
  174. package/tests/SectionNav.test.jsx +0 -363
  175. package/tests/SignUpPage.test.jsx +0 -208
  176. package/tests/SoftChip.test.jsx +0 -105
  177. package/tests/StatTile.test.jsx +0 -57
  178. package/tests/Thread.test.jsx +0 -151
  179. package/tests/TimeSeriesChart.test.jsx +0 -367
  180. package/tests/UserListComponent.test.jsx +0 -192
  181. package/tests/UserMenu.test.jsx +0 -171
  182. package/tests/apiClientLanguage.test.js +0 -33
  183. package/tests/apiClientPublicPaths.test.js +0 -178
  184. package/tests/authApiLogin.test.js +0 -42
  185. package/tests/authApiRegistration.test.js +0 -31
  186. package/tests/authErrorCodeRendering.test.jsx +0 -77
  187. package/tests/authErrors.test.js +0 -74
  188. package/tests/authTranslations.test.js +0 -74
  189. package/tests/chart-api-census.test.js +0 -366
  190. package/tests/chartAxisTickBand.test.jsx +0 -95
  191. package/tests/chartDefaults.test.js +0 -902
  192. package/tests/chartLabels.test.js +0 -50
  193. package/tests/chartsPalette.test.js +0 -99
  194. package/tests/chatApi.test.js +0 -48
  195. package/tests/chatDriverNames.test.jsx +0 -71
  196. package/tests/chatExports.test.js +0 -3
  197. package/tests/chatList.test.jsx +0 -84
  198. package/tests/chatPicker.test.jsx +0 -82
  199. package/tests/chatSocket.test.jsx +0 -26
  200. package/tests/chatThread.test.jsx +0 -116
  201. package/tests/chatTranslations.test.js +0 -47
  202. package/tests/chatWorkspace.test.jsx +0 -234
  203. package/tests/checkKitIntegration.test.jsx +0 -219
  204. package/tests/createAppTheme.test.js +0 -132
  205. package/tests/createAppThemeDark.test.js +0 -127
  206. package/tests/createAppThemeLightGuard.test.js +0 -40
  207. package/tests/createAppThemePageContrast.test.js +0 -108
  208. package/tests/createAppThemeTypography.test.js +0 -53
  209. package/tests/errorCallSiteGuard.test.js +0 -32
  210. package/tests/exportChart.test.js +0 -313
  211. package/tests/feedApi.test.js +0 -46
  212. package/tests/fixtures/light-theme-baseline.json +0 -2234
  213. package/tests/i18nAggregate.test.js +0 -120
  214. package/tests/i18nGermanVariant.test.js +0 -65
  215. package/tests/loginPageErrorResolution.test.jsx +0 -102
  216. package/tests/messagingApi.test.js +0 -32
  217. package/tests/messagingChunk6.test.jsx +0 -116
  218. package/tests/messagingCompactBubble.test.jsx +0 -64
  219. package/tests/messagingComposer.test.jsx +0 -136
  220. package/tests/messagingComposerChunk5.test.jsx +0 -113
  221. package/tests/messagingContractConformance.test.js +0 -69
  222. package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
  223. package/tests/messagingExports.test.js +0 -21
  224. package/tests/messagingInteractions.test.jsx +0 -136
  225. package/tests/messagingLastMessagePreview.test.jsx +0 -93
  226. package/tests/messagingLinkify.test.jsx +0 -51
  227. package/tests/messagingMessageActions.test.jsx +0 -163
  228. package/tests/messagingMsg6.test.jsx +0 -105
  229. package/tests/messagingMsg6f.test.jsx +0 -155
  230. package/tests/messagingMsg6g.test.jsx +0 -166
  231. package/tests/messagingMsg6h.test.jsx +0 -136
  232. package/tests/messagingMsg6i.test.jsx +0 -129
  233. package/tests/messagingPollRendering.test.jsx +0 -142
  234. package/tests/messagingReactionMerge.test.jsx +0 -77
  235. package/tests/messagingReadTicksTooltip.test.jsx +0 -36
  236. package/tests/messagingThreadReceipts.test.jsx +0 -178
  237. package/tests/messagingThreadingQuoting.test.jsx +0 -87
  238. package/tests/messagingUnreadLifecycle.test.jsx +0 -78
  239. package/tests/notificationsApi.test.js +0 -41
  240. package/tests/notificationsExports.test.js +0 -34
  241. package/tests/onboardingDescriptors.test.js +0 -132
  242. package/tests/packageTreeShaking.test.js +0 -126
  243. package/tests/realtime.test.jsx +0 -219
  244. package/tests/resolveErrorText.test.js +0 -72
  245. package/tests/stepSelection.test.js +0 -53
  246. package/tests/themeCompleteness.test.js +0 -626
  247. package/tests/timeSeriesLegend.test.js +0 -99
  248. package/tests/userScreenTranslations.test.jsx +0 -218
  249. package/tsconfig.build.json +0 -18
  250. package/vite.config.mjs +0 -6
  251. package/work-orders/AUTH-1.md +0 -74
  252. package/work-orders/AUTH-2.md +0 -74
  253. package/work-orders/AUTH-4.md +0 -183
  254. package/work-orders/AUTH-5.md +0 -154
  255. package/work-orders/AUTH-7.md +0 -181
  256. package/work-orders/AUTH-8.md +0 -184
  257. package/work-orders/CHART-1.md +0 -285
  258. package/work-orders/CHART-2.md +0 -222
  259. package/work-orders/CHART-3.md +0 -163
  260. package/work-orders/CHART-4.md +0 -203
  261. package/work-orders/CHART-5.md +0 -226
  262. package/work-orders/CHART-6.md +0 -428
  263. package/work-orders/CHART-7.md +0 -175
  264. package/work-orders/DX-1.md +0 -169
  265. package/work-orders/DX-2.md +0 -188
  266. package/work-orders/DX-3.md +0 -195
  267. package/work-orders/I18N-1.md +0 -254
  268. package/work-orders/I18N-3.md +0 -207
  269. package/work-orders/MSG-3.md +0 -375
  270. package/work-orders/MSG-3b.md +0 -1002
  271. package/work-orders/MSG-3c.md +0 -378
  272. package/work-orders/MSG-3d.md +0 -268
  273. package/work-orders/MSG-3e.md +0 -164
  274. package/work-orders/MSG-3f.md +0 -391
  275. package/work-orders/MSG-6e.md +0 -308
  276. package/work-orders/MSG-6f.md +0 -214
  277. package/work-orders/MSG-6g.md +0 -244
  278. package/work-orders/MSG-6h.md +0 -238
  279. package/work-orders/MSG-6i.md +0 -271
  280. package/work-orders/MSG-6j.md +0 -117
  281. package/work-orders/NOTIF-12.md +0 -214
  282. package/work-orders/NOTIF-13.md +0 -142
  283. package/work-orders/NOTIF-14.md +0 -151
  284. package/work-orders/PRIM-1.md +0 -177
  285. package/work-orders/SHELL-1.md +0 -255
  286. package/work-orders/SHELL-2.md +0 -197
  287. package/work-orders/SHELL-3.md +0 -359
  288. package/work-orders/SHELL-4.md +0 -351
  289. package/work-orders/SHELL-5.md +0 -345
  290. package/work-orders/SHELL-6.md +0 -210
  291. package/work-orders/THEME-1.md +0 -587
  292. package/work-orders/THEME-10.md +0 -303
  293. package/work-orders/THEME-11.md +0 -208
  294. package/work-orders/THEME-2.md +0 -339
  295. package/work-orders/THEME-3.md +0 -210
  296. package/work-orders/THEME-4.md +0 -271
  297. package/work-orders/THEME-5.md +0 -341
  298. package/work-orders/THEME-6.md +0 -247
  299. package/work-orders/THEME-7.md +0 -82
  300. package/work-orders/THEME-8.md +0 -211
  301. package/work-orders/THEME-9.md +0 -194
  302. package/work-orders/UCM-AUTH-10.md +0 -490
  303. package/work-orders/UCM-AUTH-11.md +0 -185
  304. package/work-orders/UCM-AUTH-12.md +0 -98
  305. package/work-orders/UCM-AUTH-13.md +0 -47
  306. package/work-orders/UCM-AUTH-9.md +0 -207
  307. package/work-orders/UCM-CHART-10.md +0 -150
  308. package/work-orders/UCM-CHART-11.md +0 -132
  309. package/work-orders/UCM-CHART-12.md +0 -348
  310. package/work-orders/UCM-CHART-13.md +0 -122
  311. package/work-orders/UCM-CHART-14.md +0 -198
  312. package/work-orders/UCM-CHART-15.md +0 -187
  313. package/work-orders/UCM-CHART-16.md +0 -169
  314. package/work-orders/UCM-CHART-17.md +0 -195
  315. package/work-orders/UCM-CHART-18.md +0 -200
  316. package/work-orders/UCM-CHART-8.md +0 -147
  317. package/work-orders/UCM-CHART-9.md +0 -265
  318. package/work-orders/UCM-CHAT-1.md +0 -190
  319. package/work-orders/UCM-CHAT-2.md +0 -75
  320. package/work-orders/UCM-CI-1.md +0 -65
  321. package/work-orders/UCM-DEP-1.md +0 -63
  322. package/work-orders/UCM-I18N-4.md +0 -226
  323. package/work-orders/UCM-I18N-5.md +0 -211
  324. package/work-orders/UCM-MSG-20.md +0 -192
  325. package/work-orders/UCM-PRIM-2.md +0 -114
  326. package/work-orders/UCM-SHELL-7.md +0 -134
  327. package/work-orders/UCM-SHELL-8.md +0 -125
  328. package/work-orders/UCM-TEST-1.md +0 -304
  329. package/work-orders/UCM-THEME-13.md +0 -69
  330. package/work-orders/UCM-THEME-14.md +0 -312
  331. package/work-orders/UCM-THEME-15.md +0 -356
  332. package/work-orders/UCM-THEME-16.md +0 -80
  333. package/work-orders/UCM-THEME-17.md +0 -111
  334. package/work-orders/UCM-THEME-18.md +0 -184
  335. package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
  336. package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
  337. package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
  338. package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
  339. package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
  340. package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
  341. 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
- });