@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.
- package/CHANGELOG.md +29 -0
- package/README.md +7 -0
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +1 -2
- package/dist/pages/PasswordChangePage.js +1 -2
- package/dist/pages/PasswordInvitePage.js +1 -2
- package/dist/pages/PasswordResetRequestPage.js +1 -2
- package/dist/pages/SignUpPage.js +1 -2
- package/dist/pages/SignupConfirmPage.js +1 -2
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/package.json +31 -7
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -150
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-CI-1.md +0 -65
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- 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
|
-
});
|