@micha.bigler/ui-core-micha 3.14.3 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +31 -0
- package/README.md +7 -0
- package/dist/auth/UserMenu.js +1 -1
- package/dist/auth/apiClient.js +7 -6
- package/dist/chat/ChatList.js +1 -1
- package/dist/chat/ChatThread.js +2 -1
- package/dist/chat/ChatWorkspace.js +1 -1
- package/dist/chat/ModelPicker.js +2 -5
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/chat/useChatSocket.js +1 -1
- package/dist/components/LoginForm.js +1 -1
- package/dist/components/SecurityComponent.js +1 -1
- package/dist/components/UserListComponent.js +2 -8
- package/dist/components/charts/ChartFrame.js +1 -1
- package/dist/components/charts/ScatterChart.js +2 -1
- 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/layout/SectionNav.js +1 -1
- package/dist/messaging/AttachmentList.js +2 -2
- package/dist/messaging/Composer.js +2 -1
- package/dist/messaging/ConversationLaunchers.js +1 -4
- package/dist/messaging/ConversationList.js +1 -1
- package/dist/messaging/DirectMessageLauncher.js +1 -1
- package/dist/messaging/MessagingProvider.js +0 -0
- package/dist/messaging/PollCard.js +7 -5
- package/dist/messaging/ReadTicks.js +2 -2
- package/dist/messaging/Thread.js +5 -3
- package/dist/notifications/NotificationBell.js +4 -7
- package/dist/notifications/NotificationSettings.js +1 -1
- package/dist/onboarding/OnboardingProvider.js +3 -6
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +2 -3
- package/dist/pages/PasswordChangePage.js +2 -3
- package/dist/pages/PasswordInvitePage.js +2 -3
- package/dist/pages/PasswordResetRequestPage.js +2 -3
- package/dist/pages/SignUpPage.js +2 -3
- package/dist/pages/SignupConfirmPage.js +2 -3
- package/dist/theme/createAppTheme.js +5 -4
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/dist/theme/themeCompleteness.js +3 -12
- package/dist/utils/auth-errors.js +4 -3
- package/package.json +41 -16
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -148
- 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-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
package/tests/UserMenu.test.jsx
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
-
import { createTheme, ThemeProvider } from '@mui/material/styles';
|
|
6
|
-
|
|
7
|
-
const translation = vi.hoisted(() => ({
|
|
8
|
-
t: vi.fn((key) => `translated:${key}`),
|
|
9
|
-
}));
|
|
10
|
-
|
|
11
|
-
vi.mock('react-i18next', () => ({ useTranslation: () => translation }));
|
|
12
|
-
|
|
13
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
14
|
-
import { UserMenu } from '../src/auth/UserMenu';
|
|
15
|
-
import { calculateContrastRatio, createAppTheme } from '../src/theme';
|
|
16
|
-
|
|
17
|
-
const defaultTheme = createAppTheme({ palette: { primary: { main: createTheme().palette.primary.main } } });
|
|
18
|
-
|
|
19
|
-
function renderUserMenu({ user, logout = vi.fn(), theme = defaultTheme, ...props } = {}) {
|
|
20
|
-
return render(
|
|
21
|
-
<ThemeProvider theme={theme}>
|
|
22
|
-
<AuthContext.Provider value={{ user, logout }}>
|
|
23
|
-
<UserMenu {...props} />
|
|
24
|
-
</AuthContext.Provider>
|
|
25
|
-
</ThemeProvider>,
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function getAvatarContrast() {
|
|
30
|
-
const avatar = screen.getByText(/^[A-Z]+$/).closest('.MuiAvatar-root');
|
|
31
|
-
const styles = window.getComputedStyle(avatar);
|
|
32
|
-
return calculateContrastRatio(styles.color, styles.backgroundColor);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
describe('UserMenu', () => {
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
cleanup();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
beforeEach(() => {
|
|
41
|
-
vi.clearAllMocks();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('with no items and no overrides, renders identity, Profil and Abmelden', () => {
|
|
45
|
-
const user = { first_name: 'Ada', last_name: 'Lovelace', email: 'ada@example.com', username: 'ada' };
|
|
46
|
-
renderUserMenu({ user });
|
|
47
|
-
|
|
48
|
-
fireEvent.click(screen.getByRole('button', { name: 'translated:UserMenu.TITLE' }));
|
|
49
|
-
|
|
50
|
-
expect(screen.getByText('Ada Lovelace')).toBeTruthy();
|
|
51
|
-
expect(screen.getByText('ada@example.com')).toBeTruthy();
|
|
52
|
-
expect(screen.getByText('translated:UserMenu.PROFILE')).toBeTruthy();
|
|
53
|
-
expect(screen.getByText('translated:UserMenu.LOGOUT')).toBeTruthy();
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('Abmelden calls AuthContext logout', () => {
|
|
57
|
-
const logout = vi.fn();
|
|
58
|
-
const user = { username: 'ada' };
|
|
59
|
-
renderUserMenu({ user, logout });
|
|
60
|
-
|
|
61
|
-
fireEvent.click(screen.getByRole('button', { name: 'translated:UserMenu.TITLE' }));
|
|
62
|
-
fireEvent.click(screen.getByText('translated:UserMenu.LOGOUT'));
|
|
63
|
-
|
|
64
|
-
expect(logout).toHaveBeenCalledTimes(1);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('Profil navigates via resolveLink to /account by default, and to an override when given', () => {
|
|
68
|
-
const resolveLink = vi.fn();
|
|
69
|
-
const user = { username: 'ada' };
|
|
70
|
-
renderUserMenu({ user, resolveLink });
|
|
71
|
-
|
|
72
|
-
fireEvent.click(screen.getByRole('button', { name: 'translated:UserMenu.TITLE' }));
|
|
73
|
-
fireEvent.click(screen.getByText('translated:UserMenu.PROFILE'));
|
|
74
|
-
|
|
75
|
-
expect(resolveLink).toHaveBeenCalledWith('/account');
|
|
76
|
-
cleanup();
|
|
77
|
-
|
|
78
|
-
const resolveLinkOverride = vi.fn();
|
|
79
|
-
renderUserMenu({ user, resolveLink: resolveLinkOverride, profileLink: '/settings/profile' });
|
|
80
|
-
fireEvent.click(screen.getByRole('button', { name: 'translated:UserMenu.TITLE' }));
|
|
81
|
-
fireEvent.click(screen.getByText('translated:UserMenu.PROFILE'));
|
|
82
|
-
|
|
83
|
-
expect(resolveLinkOverride).toHaveBeenCalledWith('/settings/profile');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('renders host items in their slot with onSelect invoked, and Abmelden stays last', () => {
|
|
87
|
-
const onSelectA = vi.fn();
|
|
88
|
-
const onSelectB = vi.fn();
|
|
89
|
-
const user = { username: 'ada' };
|
|
90
|
-
renderUserMenu({
|
|
91
|
-
user,
|
|
92
|
-
items: [
|
|
93
|
-
{ id: 'a', label: 'Item A', onSelect: onSelectA },
|
|
94
|
-
{ id: 'b', label: 'Item B', onSelect: onSelectB },
|
|
95
|
-
],
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
fireEvent.click(screen.getByRole('button', { name: 'translated:UserMenu.TITLE' }));
|
|
99
|
-
|
|
100
|
-
const menuItems = screen.getAllByRole('menuitem').map((el) => el.textContent);
|
|
101
|
-
expect(menuItems).toEqual([
|
|
102
|
-
'translated:UserMenu.PROFILE',
|
|
103
|
-
'Item A',
|
|
104
|
-
'Item B',
|
|
105
|
-
'translated:UserMenu.LOGOUT',
|
|
106
|
-
]);
|
|
107
|
-
|
|
108
|
-
fireEvent.click(screen.getByText('Item A'));
|
|
109
|
-
expect(onSelectA).toHaveBeenCalledTimes(1);
|
|
110
|
-
expect(onSelectB).not.toHaveBeenCalled();
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('Escape closes the menu and returns focus to the trigger', async () => {
|
|
114
|
-
const user = { username: 'ada' };
|
|
115
|
-
renderUserMenu({ user });
|
|
116
|
-
|
|
117
|
-
const trigger = screen.getByRole('button', { name: 'translated:UserMenu.TITLE' });
|
|
118
|
-
trigger.focus();
|
|
119
|
-
fireEvent.click(trigger);
|
|
120
|
-
expect(screen.getByText('translated:UserMenu.LOGOUT')).toBeTruthy();
|
|
121
|
-
|
|
122
|
-
fireEvent.keyDown(screen.getByRole('menu'), { key: 'Escape' });
|
|
123
|
-
|
|
124
|
-
await waitFor(() => {
|
|
125
|
-
expect(screen.queryByRole('menu')).toBeFalsy();
|
|
126
|
-
expect(document.activeElement).toBe(trigger);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('does not repeat the email when it is also the display name (no first/last name, username === email)', () => {
|
|
131
|
-
const user = { username: 'ada@example.com', email: 'ada@example.com' };
|
|
132
|
-
renderUserMenu({ user });
|
|
133
|
-
|
|
134
|
-
fireEvent.click(screen.getByRole('button', { name: 'translated:UserMenu.TITLE' }));
|
|
135
|
-
|
|
136
|
-
expect(screen.getAllByText('ada@example.com')).toHaveLength(1);
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('avatar renders correct initials, falling back to username when names are absent', () => {
|
|
140
|
-
renderUserMenu({ user: { first_name: 'Ada', last_name: 'Lovelace', username: 'ada' } });
|
|
141
|
-
expect(screen.getByText('AL')).toBeTruthy();
|
|
142
|
-
cleanup();
|
|
143
|
-
|
|
144
|
-
renderUserMenu({ user: { username: 'grace' } });
|
|
145
|
-
expect(screen.getByText('G')).toBeTruthy();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("uses a contrasting avatar pair with jg-ferien's primary colour", () => {
|
|
149
|
-
const theme = createAppTheme({ palette: { primary: { main: '#367964' } } });
|
|
150
|
-
renderUserMenu({ theme, user: { username: 'ab' } });
|
|
151
|
-
|
|
152
|
-
expect(getAvatarContrast()).toBeGreaterThanOrEqual(4.5);
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it('uses a contrasting avatar pair with the library baseline primary colour', () => {
|
|
156
|
-
renderUserMenu({ user: { username: 'ab' } });
|
|
157
|
-
|
|
158
|
-
expect(getAvatarContrast()).toBeGreaterThanOrEqual(4.5);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('falls back to the dark text pair for a medium-luminance primary colour', () => {
|
|
162
|
-
// A synthetic mid-grey (luminance ~0.216, no real brand meaning) picked to
|
|
163
|
-
// fail white-on-background at 4.5:1 (~3.95:1) so this test actually
|
|
164
|
-
// exercises the black-fallback branch, not just the white branch every
|
|
165
|
-
// other test here happens to land on.
|
|
166
|
-
const theme = createAppTheme({ palette: { primary: { main: '#808080' } } });
|
|
167
|
-
renderUserMenu({ theme, user: { username: 'ab' } });
|
|
168
|
-
|
|
169
|
-
expect(getAvatarContrast()).toBeGreaterThanOrEqual(4.5);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it } from 'vitest';
|
|
2
|
-
import i18next from 'i18next';
|
|
3
|
-
|
|
4
|
-
import apiClient from '../src/auth/apiClient';
|
|
5
|
-
|
|
6
|
-
function requestConfig() {
|
|
7
|
-
const interceptor = apiClient.interceptors.request.handlers.find(Boolean);
|
|
8
|
-
return interceptor.fulfilled({ headers: { Existing: 'value' } });
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('apiClient Accept-Language interceptor', () => {
|
|
12
|
-
let wasInitialized;
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
wasInitialized = i18next.isInitialized;
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('uses the active default i18next language and follows language changes', async () => {
|
|
19
|
-
await i18next.init({ lng: 'de', fallbackLng: 'en', resources: {} });
|
|
20
|
-
expect(requestConfig().headers['Accept-Language']).toBe('de');
|
|
21
|
-
|
|
22
|
-
await i18next.changeLanguage('en');
|
|
23
|
-
expect(requestConfig().headers['Accept-Language']).toBe('en');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('sends no header when the default i18next instance is not initialized', () => {
|
|
27
|
-
i18next.isInitialized = false;
|
|
28
|
-
const config = requestConfig();
|
|
29
|
-
expect(config.headers).not.toHaveProperty('Accept-Language');
|
|
30
|
-
expect(config.headers.Existing).toBe('value');
|
|
31
|
-
i18next.isInitialized = wasInitialized;
|
|
32
|
-
});
|
|
33
|
-
});
|
|
@@ -1,178 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
// AUTH-5: addPublicPath/matchesPublicPath/removePublicPath must accept a RegExp alongside a
|
|
3
|
-
// string, evaluated exclusively against pathname. Driven through the real response interceptor
|
|
4
|
-
// (not a reimplementation of matchesPublicPath) so these tests fail if the wiring, not just the
|
|
5
|
-
// matcher, breaks.
|
|
6
|
-
//
|
|
7
|
-
// The module keeps a `redirectingToLogin` latch that only resets on a real navigation, which
|
|
8
|
-
// never happens in jsdom — so each test re-imports a fresh module instance via
|
|
9
|
-
// vi.resetModules()/dynamic import, rather than sharing one import across the file.
|
|
10
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
11
|
-
|
|
12
|
-
function notAuthenticatedError(status = 401) {
|
|
13
|
-
return {
|
|
14
|
-
response: {
|
|
15
|
-
status,
|
|
16
|
-
data: { code: 'not_authenticated' },
|
|
17
|
-
},
|
|
18
|
-
config: {},
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function rejectThroughInterceptor(client, error) {
|
|
23
|
-
const handler = client.interceptors.response.handlers.find(Boolean);
|
|
24
|
-
try {
|
|
25
|
-
await handler.rejected(error);
|
|
26
|
-
} catch {
|
|
27
|
-
// The interceptor always re-rejects after handling — expected, not the assertion.
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// jsdom's real `window.location` ties `pathname` to `history.pushState` through internal
|
|
32
|
-
// wiring that a deleted/replaced `location` object doesn't preserve, and `assign` is
|
|
33
|
-
// non-configurable so `vi.spyOn` can't touch it directly either. Sidestep both by installing a
|
|
34
|
-
// fully synthetic `location` this file controls end to end — the interceptor only ever reads
|
|
35
|
-
// `.pathname` and calls `.assign()`, so nothing else needs to be real.
|
|
36
|
-
function installLocationMock(initialPathname = '/') {
|
|
37
|
-
const assign = vi.fn();
|
|
38
|
-
delete window.location;
|
|
39
|
-
window.location = { pathname: initialPathname, search: '', hash: '', assign };
|
|
40
|
-
return assign;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function setPathname(pathname) {
|
|
44
|
-
window.location.pathname = pathname;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe('apiClient public-path registry (AUTH-5)', () => {
|
|
48
|
-
let assignSpy;
|
|
49
|
-
let apiClient;
|
|
50
|
-
let addPublicPath;
|
|
51
|
-
let removePublicPath;
|
|
52
|
-
|
|
53
|
-
beforeEach(async () => {
|
|
54
|
-
vi.resetModules();
|
|
55
|
-
({ default: apiClient, addPublicPath, removePublicPath } = await import('../src/auth/apiClient'));
|
|
56
|
-
assignSpy = installLocationMock();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('string registration still matches by prefix (regression)', async () => {
|
|
60
|
-
setPathname('/custom-public');
|
|
61
|
-
addPublicPath('/custom-public');
|
|
62
|
-
|
|
63
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
64
|
-
|
|
65
|
-
expect(assignSpy).not.toHaveBeenCalled();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('a bare "/" registration does not match a deeper path via startsWith (regression)', async () => {
|
|
69
|
-
addPublicPath('/');
|
|
70
|
-
setPathname('/somewhere-else');
|
|
71
|
-
|
|
72
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
73
|
-
|
|
74
|
-
expect(assignSpy).toHaveBeenCalledWith('/login');
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('a registered RegExp matches the intended dynamic path', async () => {
|
|
78
|
-
addPublicPath(/^\/[^/]+\/team$/);
|
|
79
|
-
setPathname('/jg-bern/team');
|
|
80
|
-
|
|
81
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
82
|
-
|
|
83
|
-
expect(assignSpy).not.toHaveBeenCalled();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('a non-matching path still redirects even with an unrelated RegExp registered', async () => {
|
|
87
|
-
addPublicPath(/^\/[^/]+\/team$/);
|
|
88
|
-
setPathname('/account');
|
|
89
|
-
|
|
90
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
91
|
-
|
|
92
|
-
expect(assignSpy).toHaveBeenCalledWith('/login');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('the RegExp is tested against pathname, not the query string', async () => {
|
|
96
|
-
// A pattern that would only match if the query string leaked into the tested value.
|
|
97
|
-
setPathname('/blocked-page');
|
|
98
|
-
window.location.search = '?next=/public';
|
|
99
|
-
addPublicPath(/public/);
|
|
100
|
-
|
|
101
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
102
|
-
|
|
103
|
-
expect(assignSpy).toHaveBeenCalledWith('/login');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('the RegExp is tested against pathname, not the fragment', async () => {
|
|
107
|
-
setPathname('/blocked-page');
|
|
108
|
-
window.location.hash = '#panel';
|
|
109
|
-
addPublicPath(/panel/);
|
|
110
|
-
|
|
111
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
112
|
-
|
|
113
|
-
expect(assignSpy).toHaveBeenCalledWith('/login');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('a pathname-anchored RegExp still matches when the URL also carries a query string', async () => {
|
|
117
|
-
setPathname('/public');
|
|
118
|
-
window.location.search = '?next=/admin';
|
|
119
|
-
addPublicPath(/^\/public$/);
|
|
120
|
-
|
|
121
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
122
|
-
|
|
123
|
-
expect(assignSpy).not.toHaveBeenCalled();
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it('removePublicPath removes a RegExp entry by identity', async () => {
|
|
127
|
-
const pattern = /^\/temp$/;
|
|
128
|
-
addPublicPath(pattern);
|
|
129
|
-
removePublicPath(pattern);
|
|
130
|
-
setPathname('/temp');
|
|
131
|
-
|
|
132
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
133
|
-
|
|
134
|
-
expect(assignSpy).toHaveBeenCalledWith('/login');
|
|
135
|
-
});
|
|
136
|
-
|
|
137
|
-
it('removePublicPath does not remove a RegExp entry given only an equal-looking literal', async () => {
|
|
138
|
-
addPublicPath(/^\/temp$/);
|
|
139
|
-
removePublicPath(/^\/temp$/); // a different object, same source — must NOT remove the original
|
|
140
|
-
setPathname('/temp');
|
|
141
|
-
|
|
142
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
143
|
-
|
|
144
|
-
expect(assignSpy).not.toHaveBeenCalled();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('removePublicPath cannot remove a builtin path', async () => {
|
|
148
|
-
removePublicPath('/login');
|
|
149
|
-
setPathname('/login');
|
|
150
|
-
|
|
151
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
152
|
-
|
|
153
|
-
expect(assignSpy).not.toHaveBeenCalled();
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('a stateful (global-flag) RegExp matches consistently across repeated calls', async () => {
|
|
157
|
-
// Without resetting lastIndex, a `g`-flagged RegExp would alternate
|
|
158
|
-
// matched/unmatched across calls since it is reused from the module-level Set.
|
|
159
|
-
addPublicPath(/^\/public$/g);
|
|
160
|
-
setPathname('/public');
|
|
161
|
-
|
|
162
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
163
|
-
expect(assignSpy).not.toHaveBeenCalled();
|
|
164
|
-
|
|
165
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
166
|
-
expect(assignSpy).not.toHaveBeenCalled();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it('a RegExp entry in the registry does not break evaluation of string entries', async () => {
|
|
170
|
-
addPublicPath(/^\/gallery\/\d+$/);
|
|
171
|
-
addPublicPath('/custom-string');
|
|
172
|
-
setPathname('/custom-string');
|
|
173
|
-
|
|
174
|
-
await rejectThroughInterceptor(apiClient, notAuthenticatedError());
|
|
175
|
-
|
|
176
|
-
expect(assignSpy).not.toHaveBeenCalled();
|
|
177
|
-
});
|
|
178
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const client = vi.hoisted(() => ({
|
|
4
|
-
post: vi.fn(),
|
|
5
|
-
get: vi.fn(),
|
|
6
|
-
}));
|
|
7
|
-
|
|
8
|
-
vi.mock('../src/auth/apiClient', () => ({ default: client }));
|
|
9
|
-
|
|
10
|
-
import { loginWithPassword } from '../src/auth/authApi';
|
|
11
|
-
|
|
12
|
-
describe('loginWithPassword', () => {
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
vi.clearAllMocks();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('returns the current user when login reports an existing session', async () => {
|
|
18
|
-
const user = { id: 1, email: 'ada@example.com' };
|
|
19
|
-
client.post.mockRejectedValue({ response: { status: 409 } });
|
|
20
|
-
client.get.mockResolvedValue({ data: user });
|
|
21
|
-
|
|
22
|
-
await expect(loginWithPassword('ada@example.com', 'password')).resolves.toEqual({
|
|
23
|
-
user,
|
|
24
|
-
needsMfa: false,
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('normalizes a failure while retrying the current user after a 409', async () => {
|
|
29
|
-
const retryError = {
|
|
30
|
-
response: { status: 429, data: { detail: 'Too many requests' } },
|
|
31
|
-
};
|
|
32
|
-
client.post.mockRejectedValue({ response: { status: 409 } });
|
|
33
|
-
client.get.mockRejectedValue(retryError);
|
|
34
|
-
|
|
35
|
-
await expect(loginWithPassword('ada@example.com', 'password')).rejects.toSatisfy((error) => {
|
|
36
|
-
expect(error).toBeInstanceOf(Error);
|
|
37
|
-
expect(error.code).toBeDefined();
|
|
38
|
-
expect(error).not.toBe(retryError);
|
|
39
|
-
return true;
|
|
40
|
-
});
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
|
|
3
|
-
const client = vi.hoisted(() => ({
|
|
4
|
-
post: vi.fn(),
|
|
5
|
-
}));
|
|
6
|
-
|
|
7
|
-
vi.mock('../src/auth/apiClient', () => ({ default: client }));
|
|
8
|
-
|
|
9
|
-
import { submitRegistrationRequest } from '../src/auth/authApi';
|
|
10
|
-
|
|
11
|
-
describe('submitRegistrationRequest', () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
vi.clearAllMocks();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('includes a Turnstile token in the registration payload', async () => {
|
|
17
|
-
client.post.mockResolvedValue({ data: { ok: true } });
|
|
18
|
-
|
|
19
|
-
await submitRegistrationRequest({
|
|
20
|
-
email: 'ada@example.com',
|
|
21
|
-
mode: 'self_signup_open',
|
|
22
|
-
turnstileToken: 'turnstile-token',
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
expect(client.post).toHaveBeenCalledWith('/api/users/register-request/', {
|
|
26
|
-
email: 'ada@example.com',
|
|
27
|
-
mode: 'self_signup_open',
|
|
28
|
-
turnstile_token: 'turnstile-token',
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { I18nextProvider } from 'react-i18next';
|
|
6
|
-
import i18next from 'i18next';
|
|
7
|
-
|
|
8
|
-
const authApi = vi.hoisted(() => ({
|
|
9
|
-
changePassword: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock('../src/auth/authApi', () => authApi);
|
|
13
|
-
vi.mock('../src/utils/authService', () => ({ startSocialLogin: vi.fn() }));
|
|
14
|
-
|
|
15
|
-
import { authTranslations } from '../src/i18n/authTranslations';
|
|
16
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
17
|
-
import { SecurityComponent } from '../src/components/SecurityComponent';
|
|
18
|
-
|
|
19
|
-
const translations = Object.fromEntries(
|
|
20
|
-
Object.entries(authTranslations).map(([key, value]) => [key, value.en]),
|
|
21
|
-
);
|
|
22
|
-
|
|
23
|
-
const i18n = i18next.createInstance();
|
|
24
|
-
i18n.init({
|
|
25
|
-
lng: 'en',
|
|
26
|
-
fallbackLng: 'en',
|
|
27
|
-
resources: { en: { translation: translations } },
|
|
28
|
-
keySeparator: false,
|
|
29
|
-
nsSeparator: false,
|
|
30
|
-
interpolation: { escapeValue: false },
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
function renderSecurity() {
|
|
34
|
-
return render(
|
|
35
|
-
<I18nextProvider i18n={i18n}>
|
|
36
|
-
<AuthContext.Provider value={{ authMethods: { password_change: true } }}>
|
|
37
|
-
<SecurityComponent />
|
|
38
|
-
</AuthContext.Provider>
|
|
39
|
-
</I18nextProvider>,
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
function submitPasswordChange() {
|
|
44
|
-
fireEvent.change(screen.getByLabelText(/Current password/), { target: { value: 'currentpw' } });
|
|
45
|
-
fireEvent.change(screen.getByLabelText(/New password/), { target: { value: 'a-fine-new-password' } });
|
|
46
|
-
fireEvent.click(screen.getByRole('button', { name: 'Change password' }));
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
afterEach(() => {
|
|
50
|
-
cleanup();
|
|
51
|
-
authApi.changePassword.mockReset();
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('SecurityComponent — backend auth error code rendering', () => {
|
|
55
|
-
it('shows translated human text, not the raw code, for a mapped error', async () => {
|
|
56
|
-
authApi.changePassword.mockRejectedValue({ code: 'enter_current_password' });
|
|
57
|
-
renderSecurity();
|
|
58
|
-
|
|
59
|
-
submitPasswordChange();
|
|
60
|
-
|
|
61
|
-
await waitFor(() => {
|
|
62
|
-
expect(screen.getByText('The current password is incorrect.')).toBeTruthy();
|
|
63
|
-
});
|
|
64
|
-
expect(screen.queryByText('enter_current_password')).toBeNull();
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('falls back to the generic failure message when the backend sends no code at all', async () => {
|
|
68
|
-
authApi.changePassword.mockRejectedValue(new Error('network failure'));
|
|
69
|
-
renderSecurity();
|
|
70
|
-
|
|
71
|
-
submitPasswordChange();
|
|
72
|
-
|
|
73
|
-
await waitFor(() => {
|
|
74
|
-
expect(screen.getByText('The password could not be changed.')).toBeTruthy();
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
package/tests/authErrors.test.js
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest';
|
|
2
|
-
import { extractErrorInfo, normaliseApiError } from '../src/utils/auth-errors.js';
|
|
3
|
-
|
|
4
|
-
describe('auth error normalisation', () => {
|
|
5
|
-
it('uses the caller default for generic detail errors while preserving the detail message', () => {
|
|
6
|
-
const responseError = {
|
|
7
|
-
response: { status: 429, data: { detail: 'Die Anfrage wurde gedrosselt...' } },
|
|
8
|
-
};
|
|
9
|
-
const info = extractErrorInfo(responseError);
|
|
10
|
-
const error = normaliseApiError(responseError, 'Auth.LOGIN_FAILED');
|
|
11
|
-
|
|
12
|
-
expect(info.code).toBeNull();
|
|
13
|
-
expect(error.code).toBe('Auth.LOGIN_FAILED');
|
|
14
|
-
expect(error.message).toBe('Die Anfrage wurde gedrosselt...');
|
|
15
|
-
expect(error.status).toBe(429);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('uses its namespaced default when no caller default is provided', () => {
|
|
19
|
-
const error = normaliseApiError({
|
|
20
|
-
response: { status: 429, data: { detail: 'Die Anfrage wurde gedrosselt...' } },
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
expect(error.code).toBe('Auth.GENERIC_ERROR');
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('preserves a backend code from the errors array', () => {
|
|
27
|
-
const error = normaliseApiError(
|
|
28
|
-
{ response: { data: { errors: [{ code: 'enter_current_password', message: 'x' }] } } },
|
|
29
|
-
'Auth.PROFILE_UPDATE_FAILED',
|
|
30
|
-
);
|
|
31
|
-
|
|
32
|
-
expect(error.code).toBe('enter_current_password');
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('preserves a backend code from the response data', () => {
|
|
36
|
-
const error = normaliseApiError(
|
|
37
|
-
{ response: { data: { code: 'invalid_credentials' } } },
|
|
38
|
-
'Auth.LOGIN_FAILED',
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
expect(error.code).toBe('invalid_credentials');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('keeps field, explicit i18n key, and backend message separate', () => {
|
|
45
|
-
const error = normaliseApiError({
|
|
46
|
-
response: {
|
|
47
|
-
status: 400,
|
|
48
|
-
data: {
|
|
49
|
-
errors: [{
|
|
50
|
-
code: 'invalid',
|
|
51
|
-
param: 'email',
|
|
52
|
-
i18nKey: 'Auth.EMAIL_INVALID',
|
|
53
|
-
message: 'Backend message',
|
|
54
|
-
}],
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
expect(error.code).toBe('invalid');
|
|
60
|
-
expect(error.field).toBe('email');
|
|
61
|
-
expect(error.i18nKey).toBe('Auth.EMAIL_INVALID');
|
|
62
|
-
expect(error.backendMessage).toBe('Backend message');
|
|
63
|
-
expect(error.message).toBe('Backend message');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('does not turn a missing backend message into a code message', () => {
|
|
67
|
-
const error = normaliseApiError({
|
|
68
|
-
response: { data: { code: 'unknown_backend_code' } },
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
expect(error.backendMessage).toBeUndefined();
|
|
72
|
-
expect(error.message).toBe('unknown_backend_code');
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
import { authTranslations } from '../src/i18n/authTranslations';
|
|
4
|
-
|
|
5
|
-
const REQUIRED_LOCALES = ['de', 'fr', 'en'];
|
|
6
|
-
|
|
7
|
-
const AUTH1_KEYS = [
|
|
8
|
-
'Auth.PASSWORD_RULES_HINT',
|
|
9
|
-
'Auth.PASSWORD_TOO_SHORT_LOCAL',
|
|
10
|
-
'Auth.PASSWORD_NUMERIC_LOCAL',
|
|
11
|
-
];
|
|
12
|
-
|
|
13
|
-
// Raw backend error codes (allauth-headless + Django password validators), keyed
|
|
14
|
-
// verbatim so the existing `t(err.code)` call sites resolve as a drop-in. See AUTH-2.
|
|
15
|
-
const AUTH2_CODE_KEYS = [
|
|
16
|
-
'email_password_mismatch',
|
|
17
|
-
'username_password_mismatch',
|
|
18
|
-
'phone_password_mismatch',
|
|
19
|
-
'too_many_login_attempts',
|
|
20
|
-
'invalid_login',
|
|
21
|
-
'account_inactive',
|
|
22
|
-
'enter_current_password',
|
|
23
|
-
'password_too_short',
|
|
24
|
-
'password_too_common',
|
|
25
|
-
'password_entirely_numeric',
|
|
26
|
-
'password_too_similar',
|
|
27
|
-
'incorrect_code',
|
|
28
|
-
];
|
|
29
|
-
|
|
30
|
-
describe('authTranslations — AUTH-1 password-rules keys', () => {
|
|
31
|
-
it.each(AUTH1_KEYS)('%s has de/fr/en entries', (key) => {
|
|
32
|
-
expect(authTranslations[key]).toBeDefined();
|
|
33
|
-
for (const locale of REQUIRED_LOCALES) {
|
|
34
|
-
expect(authTranslations[key][locale]).toBeTruthy();
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
describe('authTranslations — AUTH-2 raw backend error codes', () => {
|
|
40
|
-
it.each(AUTH2_CODE_KEYS)('%s has de/fr/en entries', (code) => {
|
|
41
|
-
expect(authTranslations[code]).toBeDefined();
|
|
42
|
-
for (const locale of REQUIRED_LOCALES) {
|
|
43
|
-
expect(authTranslations[code][locale]).toBeTruthy();
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('does not shadow the existing generic Auth.*_FAILED fallbacks', () => {
|
|
48
|
-
expect(authTranslations['Auth.PASSWORD_CHANGE_FAILED']).toBeDefined();
|
|
49
|
-
expect(authTranslations['Auth.LOGIN_FAILED']).toBeDefined();
|
|
50
|
-
expect(authTranslations['Auth.MFA_AUTHENTICATE_FAILED']).toBeDefined();
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('authTranslations — UserList pagination labels', () => {
|
|
55
|
-
it.each(['UserList.DISPLAYED_ROWS', 'UserList.DISPLAYED_ROWS_MORE_THAN'])('%s has all supported locale entries', (key) => {
|
|
56
|
-
expect(authTranslations[key]).toBeDefined();
|
|
57
|
-
for (const locale of ['de', 'fr', 'en', 'sw']) {
|
|
58
|
-
expect(authTranslations[key][locale]).toEqual(expect.any(String));
|
|
59
|
-
expect(authTranslations[key][locale].trim()).not.toBe('');
|
|
60
|
-
}
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
// A nonempty string isn't enough -- a locale missing an interpolation placeholder the
|
|
64
|
-
// others carry would silently render the literal "{{count}}" to a real user (UCM-I18N-5 review).
|
|
65
|
-
it.each(['de', 'fr', 'en', 'sw'])('%s has {{from}} and {{to}} in both pagination keys, and {{count}} only in the exact-count one', (locale) => {
|
|
66
|
-
expect(authTranslations['UserList.DISPLAYED_ROWS'][locale]).toContain('{{from}}');
|
|
67
|
-
expect(authTranslations['UserList.DISPLAYED_ROWS'][locale]).toContain('{{to}}');
|
|
68
|
-
expect(authTranslations['UserList.DISPLAYED_ROWS'][locale]).toContain('{{count}}');
|
|
69
|
-
|
|
70
|
-
expect(authTranslations['UserList.DISPLAYED_ROWS_MORE_THAN'][locale]).toContain('{{from}}');
|
|
71
|
-
expect(authTranslations['UserList.DISPLAYED_ROWS_MORE_THAN'][locale]).toContain('{{to}}');
|
|
72
|
-
expect(authTranslations['UserList.DISPLAYED_ROWS_MORE_THAN'][locale]).not.toContain('{{count}}');
|
|
73
|
-
});
|
|
74
|
-
});
|