@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
|
@@ -1,294 +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
|
-
|
|
6
|
-
const onboardingApi = vi.hoisted(() => ({
|
|
7
|
-
getOnboardingStepConfig: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
const notificationsApi = vi.hoisted(() => ({
|
|
10
|
-
getNotificationPreferences: vi.fn(),
|
|
11
|
-
}));
|
|
12
|
-
|
|
13
|
-
vi.mock('../src/onboarding/api', () => onboardingApi);
|
|
14
|
-
vi.mock('../src/notifications/api', () => notificationsApi);
|
|
15
|
-
|
|
16
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
17
|
-
import { OnboardingProvider, useOnboarding } from '../src/onboarding/OnboardingProvider';
|
|
18
|
-
|
|
19
|
-
function Probe() {
|
|
20
|
-
const onboarding = useOnboarding();
|
|
21
|
-
return <span data-testid="email-opted-in">{String(onboarding?.ctx.emailOptedIn)}</span>;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function ActiveStepsProbe() {
|
|
25
|
-
const onboarding = useOnboarding();
|
|
26
|
-
return (
|
|
27
|
-
<>
|
|
28
|
-
<span data-testid="active-steps">{onboarding?.activeSteps.map((step) => step.id).join(',')}</span>
|
|
29
|
-
<span data-testid="push-subscribed">{String(onboarding?.ctx.pushState?.subscribed)}</span>
|
|
30
|
-
</>
|
|
31
|
-
);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function MarkStepSeenProbe() {
|
|
35
|
-
const onboarding = useOnboarding();
|
|
36
|
-
return (
|
|
37
|
-
<>
|
|
38
|
-
<ActiveStepsProbe />
|
|
39
|
-
<button type="button" onClick={() => onboarding.markStepSeen('browser_push')}>Mark browser push seen</button>
|
|
40
|
-
</>
|
|
41
|
-
);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function renderProvider({ browserPush, extraContext, extraSteps, pwaInstallEnabled, children = <Probe /> } = {}) {
|
|
45
|
-
return render(
|
|
46
|
-
<AuthContext.Provider value={{
|
|
47
|
-
user: {
|
|
48
|
-
id: 1,
|
|
49
|
-
accepted_privacy_statement: true,
|
|
50
|
-
first_name: 'Ada',
|
|
51
|
-
last_name: 'Lovelace',
|
|
52
|
-
},
|
|
53
|
-
}}>
|
|
54
|
-
<OnboardingProvider
|
|
55
|
-
extraContext={extraContext}
|
|
56
|
-
extraSteps={extraSteps}
|
|
57
|
-
pwaInstallEnabled={pwaInstallEnabled}
|
|
58
|
-
browserPush={browserPush}
|
|
59
|
-
>
|
|
60
|
-
{children}
|
|
61
|
-
</OnboardingProvider>
|
|
62
|
-
</AuthContext.Provider>,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
describe('OnboardingProvider notification preferences', () => {
|
|
67
|
-
beforeEach(() => {
|
|
68
|
-
cleanup();
|
|
69
|
-
vi.clearAllMocks();
|
|
70
|
-
onboardingApi.getOnboardingStepConfig.mockResolvedValue([]);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
it('populates emailOptedIn from notification preferences', async () => {
|
|
74
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({ email_opt_in: true });
|
|
75
|
-
|
|
76
|
-
renderProvider();
|
|
77
|
-
|
|
78
|
-
await waitFor(() => expect(screen.getByTestId('email-opted-in').textContent).toBe('true'));
|
|
79
|
-
expect(notificationsApi.getNotificationPreferences).toHaveBeenCalledOnce();
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('defaults emailOptedIn to false when notification preferences cannot be loaded', async () => {
|
|
83
|
-
notificationsApi.getNotificationPreferences.mockRejectedValue(new Error('unavailable'));
|
|
84
|
-
|
|
85
|
-
renderProvider();
|
|
86
|
-
|
|
87
|
-
await waitFor(() => expect(notificationsApi.getNotificationPreferences).toHaveBeenCalledOnce());
|
|
88
|
-
expect(screen.getByTestId('email-opted-in').textContent).toBe('false');
|
|
89
|
-
});
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
describe('OnboardingProvider extra context', () => {
|
|
93
|
-
const unreadMessagesCondition = vi.fn((ctx) => (ctx.unreadCount || 0) > 0);
|
|
94
|
-
const unreadMessagesStep = {
|
|
95
|
-
id: 'unread_messages',
|
|
96
|
-
condition: unreadMessagesCondition,
|
|
97
|
-
persistDismissed: false,
|
|
98
|
-
};
|
|
99
|
-
|
|
100
|
-
beforeEach(() => {
|
|
101
|
-
cleanup();
|
|
102
|
-
vi.clearAllMocks();
|
|
103
|
-
onboardingApi.getOnboardingStepConfig.mockResolvedValue([]);
|
|
104
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({ email_opt_in: true });
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('shows an app step when its extra context condition is met', async () => {
|
|
108
|
-
renderProvider({
|
|
109
|
-
extraContext: { unreadCount: 2 },
|
|
110
|
-
extraSteps: [unreadMessagesStep],
|
|
111
|
-
children: <ActiveStepsProbe />,
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
await waitFor(() => expect(screen.getByTestId('active-steps').textContent).toContain('unread_messages'));
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it.each([
|
|
118
|
-
['zero', { unreadCount: 0 }],
|
|
119
|
-
['omitted', undefined],
|
|
120
|
-
])('does not show an app step when unreadCount is %s', async (_label, extraContext) => {
|
|
121
|
-
renderProvider({
|
|
122
|
-
extraContext,
|
|
123
|
-
extraSteps: [unreadMessagesStep],
|
|
124
|
-
children: <ActiveStepsProbe />,
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
await waitFor(() => expect(unreadMessagesCondition).toHaveBeenCalled());
|
|
128
|
-
expect(screen.getByTestId('active-steps').textContent).not.toContain('unread_messages');
|
|
129
|
-
});
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
describe('OnboardingProvider pwa_install capture', () => {
|
|
133
|
-
beforeEach(() => {
|
|
134
|
-
cleanup();
|
|
135
|
-
vi.clearAllMocks();
|
|
136
|
-
onboardingApi.getOnboardingStepConfig.mockResolvedValue([]);
|
|
137
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({ email_opt_in: true });
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
function dispatchBeforeInstallPrompt() {
|
|
141
|
-
const event = new Event('beforeinstallprompt', { cancelable: true });
|
|
142
|
-
event.prompt = vi.fn();
|
|
143
|
-
event.userChoice = Promise.resolve({ outcome: 'accepted' });
|
|
144
|
-
window.dispatchEvent(event);
|
|
145
|
-
return event;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
it('does not show pwa_install without the app opt-in prop, and does not suppress the browser default install UI', async () => {
|
|
149
|
-
renderProvider({ pwaInstallEnabled: false, children: <ActiveStepsProbe /> });
|
|
150
|
-
await waitFor(() => expect(onboardingApi.getOnboardingStepConfig).toHaveBeenCalled());
|
|
151
|
-
|
|
152
|
-
const event = dispatchBeforeInstallPrompt();
|
|
153
|
-
|
|
154
|
-
await waitFor(() => expect(screen.getByTestId('active-steps').textContent).not.toContain('pwa_install'));
|
|
155
|
-
// Regression guard: a non-opted-in app must keep the browser's own
|
|
156
|
-
// native install UI, since it has no replacement to show instead.
|
|
157
|
-
expect(event.defaultPrevented).toBe(false);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('shows pwa_install once opted in and a beforeinstallprompt event is captured', async () => {
|
|
161
|
-
renderProvider({ pwaInstallEnabled: true, children: <ActiveStepsProbe /> });
|
|
162
|
-
await waitFor(() => expect(onboardingApi.getOnboardingStepConfig).toHaveBeenCalled());
|
|
163
|
-
|
|
164
|
-
const event = dispatchBeforeInstallPrompt();
|
|
165
|
-
|
|
166
|
-
await waitFor(() => expect(screen.getByTestId('active-steps').textContent).toContain('pwa_install'));
|
|
167
|
-
expect(event.defaultPrevented).toBe(true);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('does not show pwa_install when opted in but no prompt was ever captured (non-iOS)', async () => {
|
|
171
|
-
renderProvider({ pwaInstallEnabled: true, children: <ActiveStepsProbe /> });
|
|
172
|
-
await waitFor(() => expect(onboardingApi.getOnboardingStepConfig).toHaveBeenCalled());
|
|
173
|
-
|
|
174
|
-
expect(screen.getByTestId('active-steps').textContent).not.toContain('pwa_install');
|
|
175
|
-
});
|
|
176
|
-
});
|
|
177
|
-
|
|
178
|
-
describe('OnboardingProvider browser_push configuration', () => {
|
|
179
|
-
const serviceWorkerDescriptor = Object.getOwnPropertyDescriptor(navigator, 'serviceWorker');
|
|
180
|
-
const pushManagerDescriptor = Object.getOwnPropertyDescriptor(window, 'PushManager');
|
|
181
|
-
const localStorageDescriptor = Object.getOwnPropertyDescriptor(window, 'localStorage');
|
|
182
|
-
const localStorageItems = new Map();
|
|
183
|
-
|
|
184
|
-
function installLocalStorage() {
|
|
185
|
-
Object.defineProperty(window, 'localStorage', {
|
|
186
|
-
configurable: true,
|
|
187
|
-
value: {
|
|
188
|
-
getItem: (key) => localStorageItems.get(key) || null,
|
|
189
|
-
setItem: (key, value) => localStorageItems.set(key, String(value)),
|
|
190
|
-
clear: () => localStorageItems.clear(),
|
|
191
|
-
},
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function installPushState(subscribed) {
|
|
196
|
-
Object.defineProperty(navigator, 'serviceWorker', {
|
|
197
|
-
configurable: true,
|
|
198
|
-
value: {
|
|
199
|
-
ready: Promise.resolve({
|
|
200
|
-
pushManager: { getSubscription: () => Promise.resolve(subscribed ? {} : null) },
|
|
201
|
-
}),
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
Object.defineProperty(window, 'PushManager', {
|
|
205
|
-
configurable: true,
|
|
206
|
-
value: window.PushManager || function PushManager() {},
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
function restorePushSupport() {
|
|
211
|
-
if (serviceWorkerDescriptor) {
|
|
212
|
-
Object.defineProperty(navigator, 'serviceWorker', serviceWorkerDescriptor);
|
|
213
|
-
} else {
|
|
214
|
-
delete navigator.serviceWorker;
|
|
215
|
-
}
|
|
216
|
-
if (pushManagerDescriptor) {
|
|
217
|
-
Object.defineProperty(window, 'PushManager', pushManagerDescriptor);
|
|
218
|
-
} else {
|
|
219
|
-
delete window.PushManager;
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
function restoreLocalStorage() {
|
|
224
|
-
if (localStorageDescriptor) {
|
|
225
|
-
Object.defineProperty(window, 'localStorage', localStorageDescriptor);
|
|
226
|
-
} else {
|
|
227
|
-
delete window.localStorage;
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
beforeEach(() => {
|
|
232
|
-
cleanup();
|
|
233
|
-
vi.clearAllMocks();
|
|
234
|
-
installLocalStorage();
|
|
235
|
-
window.localStorage.clear();
|
|
236
|
-
onboardingApi.getOnboardingStepConfig.mockResolvedValue([]);
|
|
237
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({ email_opt_in: false });
|
|
238
|
-
installPushState(true);
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
afterEach(() => {
|
|
242
|
-
restorePushSupport();
|
|
243
|
-
restoreLocalStorage();
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
it('uses any-channel by default and all-channels when explicitly configured', async () => {
|
|
247
|
-
const defaultRender = renderProvider({ children: <ActiveStepsProbe /> });
|
|
248
|
-
|
|
249
|
-
await waitFor(() => expect(screen.getByTestId('push-subscribed').textContent).toBe('true'));
|
|
250
|
-
expect(screen.getByTestId('active-steps').textContent).not.toContain('browser_push');
|
|
251
|
-
|
|
252
|
-
defaultRender.unmount();
|
|
253
|
-
renderProvider({
|
|
254
|
-
browserPush: { nagUntil: 'all-channels' },
|
|
255
|
-
children: <ActiveStepsProbe />,
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
await waitFor(() => expect(screen.getByTestId('push-subscribed').textContent).toBe('true'));
|
|
259
|
-
expect(screen.getByTestId('active-steps').textContent).toContain('browser_push');
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('never shows browser_push when nagUntil is off', async () => {
|
|
263
|
-
renderProvider({ browserPush: { nagUntil: 'off' }, children: <ActiveStepsProbe /> });
|
|
264
|
-
|
|
265
|
-
await waitFor(() => expect(screen.getByTestId('push-subscribed').textContent).toBe('true'));
|
|
266
|
-
expect(screen.getByTestId('active-steps').textContent).not.toContain('browser_push');
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('persists showOnce for the next provider session without hiding the current session step', async () => {
|
|
270
|
-
installPushState(false);
|
|
271
|
-
const firstRender = renderProvider({
|
|
272
|
-
browserPush: { showOnce: true },
|
|
273
|
-
children: <MarkStepSeenProbe />,
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
await waitFor(() => expect(screen.getByTestId('push-subscribed').textContent).toBe('false'));
|
|
277
|
-
await waitFor(() => expect(screen.getByTestId('active-steps').textContent).toContain('browser_push'));
|
|
278
|
-
|
|
279
|
-
fireEvent.click(screen.getByRole('button', { name: 'Mark browser push seen' }));
|
|
280
|
-
|
|
281
|
-
expect(screen.getByTestId('active-steps').textContent).toContain('browser_push');
|
|
282
|
-
expect(JSON.parse(window.localStorage.getItem('onboarding_seen'))).toContain('browser_push');
|
|
283
|
-
|
|
284
|
-
firstRender.unmount();
|
|
285
|
-
renderProvider({
|
|
286
|
-
browserPush: { showOnce: true },
|
|
287
|
-
extraSteps: [{ id: 'provider_loaded', condition: () => true, persistDismissed: false }],
|
|
288
|
-
children: <ActiveStepsProbe />,
|
|
289
|
-
});
|
|
290
|
-
|
|
291
|
-
await waitFor(() => expect(screen.getByTestId('active-steps').textContent).toContain('provider_loaded'));
|
|
292
|
-
expect(screen.getByTestId('active-steps').textContent).not.toContain('browser_push');
|
|
293
|
-
});
|
|
294
|
-
});
|
|
@@ -1,143 +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 } from '@testing-library/react';
|
|
5
|
-
|
|
6
|
-
vi.mock('react-i18next', () => ({
|
|
7
|
-
useTranslation: () => ({
|
|
8
|
-
t: (key, params) => (params && Object.keys(params).length ? `${key}:${JSON.stringify(params)}` : key),
|
|
9
|
-
}),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
const onboardingModule = vi.hoisted(() => ({ useOnboarding: vi.fn() }));
|
|
13
|
-
vi.mock('../src/onboarding/OnboardingProvider', () => onboardingModule);
|
|
14
|
-
|
|
15
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
16
|
-
import { OnboardingWizard } from '../src/onboarding/OnboardingWizard';
|
|
17
|
-
|
|
18
|
-
function StepBody({ onComplete, onDismiss }) {
|
|
19
|
-
return (
|
|
20
|
-
<div>
|
|
21
|
-
<button onClick={onComplete}>step-complete</button>
|
|
22
|
-
<button onClick={onDismiss}>step-dismiss</button>
|
|
23
|
-
</div>
|
|
24
|
-
);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function makeStep(overrides = {}) {
|
|
28
|
-
return {
|
|
29
|
-
id: 'step-1',
|
|
30
|
-
blocking: false,
|
|
31
|
-
persistDismissed: false,
|
|
32
|
-
Component: StepBody,
|
|
33
|
-
...overrides,
|
|
34
|
-
};
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function renderWizard({ onboarding, refreshUser = vi.fn().mockResolvedValue() } = {}) {
|
|
38
|
-
onboardingModule.useOnboarding.mockReturnValue(onboarding);
|
|
39
|
-
return render(
|
|
40
|
-
<AuthContext.Provider value={{ refreshUser }}>
|
|
41
|
-
<OnboardingWizard />
|
|
42
|
-
</AuthContext.Provider>,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe('OnboardingWizard regression guard (NOTIF-12 shell extraction)', () => {
|
|
47
|
-
afterEach(() => {
|
|
48
|
-
cleanup();
|
|
49
|
-
vi.clearAllMocks();
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it('renders nothing when there is no active step', () => {
|
|
53
|
-
renderWizard({ onboarding: { activeSteps: [], markStepSeen: vi.fn(), dismissStep: vi.fn(), ctx: {} } });
|
|
54
|
-
|
|
55
|
-
expect(screen.queryByRole('dialog')).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('shows the single-step SETUP title with no progress bar for exactly one step', () => {
|
|
59
|
-
renderWizard({ onboarding: {
|
|
60
|
-
activeSteps: [makeStep()],
|
|
61
|
-
markStepSeen: vi.fn(),
|
|
62
|
-
dismissStep: vi.fn(),
|
|
63
|
-
ctx: {},
|
|
64
|
-
} });
|
|
65
|
-
|
|
66
|
-
expect(screen.getByText('Onboarding.SETUP')).toBeTruthy();
|
|
67
|
-
expect(screen.queryByRole('progressbar')).toBeNull();
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it('shows the step counter and progress bar across multiple steps, advancing on completion', () => {
|
|
71
|
-
renderWizard({ onboarding: {
|
|
72
|
-
activeSteps: [makeStep({ id: 'a' }), makeStep({ id: 'b' })],
|
|
73
|
-
markStepSeen: vi.fn(),
|
|
74
|
-
dismissStep: vi.fn(),
|
|
75
|
-
ctx: {},
|
|
76
|
-
} });
|
|
77
|
-
|
|
78
|
-
expect(screen.getByText('Onboarding.STEP_COUNTER:{"current":1,"total":2}')).toBeTruthy();
|
|
79
|
-
expect(screen.getByRole('progressbar')).toBeTruthy();
|
|
80
|
-
|
|
81
|
-
fireEvent.click(screen.getByRole('button', { name: 'step-complete' }));
|
|
82
|
-
|
|
83
|
-
expect(screen.getByText('Onboarding.STEP_COUNTER:{"current":2,"total":2}')).toBeTruthy();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('marks the current step seen and session-dismisses it once completed, revealing the next step', () => {
|
|
87
|
-
const markStepSeen = vi.fn();
|
|
88
|
-
renderWizard({ onboarding: {
|
|
89
|
-
activeSteps: [makeStep({ id: 'a' }), makeStep({ id: 'b' })],
|
|
90
|
-
markStepSeen,
|
|
91
|
-
dismissStep: vi.fn(),
|
|
92
|
-
ctx: {},
|
|
93
|
-
} });
|
|
94
|
-
|
|
95
|
-
expect(markStepSeen).toHaveBeenCalledWith('a');
|
|
96
|
-
|
|
97
|
-
fireEvent.click(screen.getByRole('button', { name: 'step-complete' }));
|
|
98
|
-
|
|
99
|
-
expect(markStepSeen).toHaveBeenCalledWith('b');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
it('disables escape-key close for a blocking step', () => {
|
|
103
|
-
renderWizard({ onboarding: {
|
|
104
|
-
activeSteps: [makeStep({ blocking: true })],
|
|
105
|
-
markStepSeen: vi.fn(),
|
|
106
|
-
dismissStep: vi.fn(),
|
|
107
|
-
ctx: {},
|
|
108
|
-
} });
|
|
109
|
-
|
|
110
|
-
const dialog = screen.getByRole('dialog');
|
|
111
|
-
fireEvent.keyDown(dialog, { key: 'Escape', code: 'Escape' });
|
|
112
|
-
|
|
113
|
-
expect(screen.getByRole('dialog')).toBeTruthy();
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('calls dismissStep only when the dismissed step is persistDismissed', () => {
|
|
117
|
-
const dismissStep = vi.fn();
|
|
118
|
-
renderWizard({ onboarding: {
|
|
119
|
-
activeSteps: [makeStep({ persistDismissed: true })],
|
|
120
|
-
markStepSeen: vi.fn(),
|
|
121
|
-
dismissStep,
|
|
122
|
-
ctx: {},
|
|
123
|
-
} });
|
|
124
|
-
|
|
125
|
-
fireEvent.click(screen.getByRole('button', { name: 'step-dismiss' }));
|
|
126
|
-
|
|
127
|
-
expect(dismissStep).toHaveBeenCalledWith('step-1');
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
it('does not call dismissStep for a non-persistDismissed step on dismiss', () => {
|
|
131
|
-
const dismissStep = vi.fn();
|
|
132
|
-
renderWizard({ onboarding: {
|
|
133
|
-
activeSteps: [makeStep({ persistDismissed: false })],
|
|
134
|
-
markStepSeen: vi.fn(),
|
|
135
|
-
dismissStep,
|
|
136
|
-
ctx: {},
|
|
137
|
-
} });
|
|
138
|
-
|
|
139
|
-
fireEvent.click(screen.getByRole('button', { name: 'step-dismiss' }));
|
|
140
|
-
|
|
141
|
-
expect(dismissStep).not.toHaveBeenCalled();
|
|
142
|
-
});
|
|
143
|
-
});
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { cleanup, fireEvent, render, screen } from '@testing-library/react';
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
|
|
6
|
-
vi.mock('react-i18next', () => ({
|
|
7
|
-
useTranslation: () => ({ t: (key, fallback) => (typeof fallback === 'string' ? fallback : key) }),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
import { PasswordChangeForm } from '../src/components/PasswordChangeForm';
|
|
11
|
-
|
|
12
|
-
afterEach(() => {
|
|
13
|
-
cleanup();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
function fillAndSubmit(currentPassword, newPassword) {
|
|
17
|
-
fireEvent.change(screen.getByLabelText(/Auth\.CURRENT_PASSWORD_LABEL/), { target: { value: currentPassword } });
|
|
18
|
-
fireEvent.change(screen.getByLabelText(/Auth\.NEW_PASSWORD_LABEL/), { target: { value: newPassword } });
|
|
19
|
-
fireEvent.click(screen.getByRole('button', { name: 'Auth.CHANGE_PASSWORD_BUTTON' }));
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
describe('PasswordChangeForm', () => {
|
|
23
|
-
it('renders the default password-rules helperText', () => {
|
|
24
|
-
render(<PasswordChangeForm onSubmit={vi.fn()} />);
|
|
25
|
-
expect(screen.getAllByText('Auth.PASSWORD_RULES_HINT').length).toBeGreaterThan(0);
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('lets a caller override the helperText via passwordRulesHint', () => {
|
|
29
|
-
render(<PasswordChangeForm onSubmit={vi.fn()} passwordRulesHint="Custom rule text" />);
|
|
30
|
-
expect(screen.getAllByText('Custom rule text').length).toBeGreaterThan(0);
|
|
31
|
-
expect(screen.queryByText('Auth.PASSWORD_RULES_HINT')).toBeNull();
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('blocks submit and shows the local error for a new password shorter than 8 characters', () => {
|
|
35
|
-
const onSubmit = vi.fn();
|
|
36
|
-
render(<PasswordChangeForm onSubmit={onSubmit} />);
|
|
37
|
-
fillAndSubmit('currentpw', 'short1');
|
|
38
|
-
expect(screen.getByText('Auth.PASSWORD_TOO_SHORT_LOCAL')).toBeTruthy();
|
|
39
|
-
expect(onSubmit).not.toHaveBeenCalled();
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
it('blocks submit and shows the local error for an entirely numeric new password', () => {
|
|
43
|
-
const onSubmit = vi.fn();
|
|
44
|
-
render(<PasswordChangeForm onSubmit={onSubmit} />);
|
|
45
|
-
fillAndSubmit('currentpw', '12345678');
|
|
46
|
-
expect(screen.getByText('Auth.PASSWORD_NUMERIC_LOCAL')).toBeTruthy();
|
|
47
|
-
expect(onSubmit).not.toHaveBeenCalled();
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('calls onSubmit for a valid, non-numeric new password of sufficient length', async () => {
|
|
51
|
-
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
52
|
-
render(<PasswordChangeForm onSubmit={onSubmit} />);
|
|
53
|
-
fillAndSubmit('currentpw', 'correct-horse-1');
|
|
54
|
-
expect(onSubmit).toHaveBeenCalledWith('currentpw', 'correct-horse-1');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('honours a custom minPasswordLength for an app with a looser backend policy', () => {
|
|
58
|
-
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
59
|
-
render(<PasswordChangeForm onSubmit={onSubmit} minPasswordLength={4} />);
|
|
60
|
-
fillAndSubmit('currentpw', 'ab12');
|
|
61
|
-
expect(onSubmit).toHaveBeenCalledWith('currentpw', 'ab12');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('allows an entirely numeric new password when allowNumericPassword is set', () => {
|
|
65
|
-
const onSubmit = vi.fn().mockResolvedValue(undefined);
|
|
66
|
-
render(<PasswordChangeForm onSubmit={onSubmit} allowNumericPassword />);
|
|
67
|
-
fillAndSubmit('currentpw', '12345678');
|
|
68
|
-
expect(onSubmit).toHaveBeenCalledWith('currentpw', '12345678');
|
|
69
|
-
});
|
|
70
|
-
});
|
|
@@ -1,142 +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 { MemoryRouter, Route, Routes, useLocation } from 'react-router-dom';
|
|
6
|
-
|
|
7
|
-
const authApi = vi.hoisted(() => ({
|
|
8
|
-
verifyResetToken: vi.fn(),
|
|
9
|
-
setNewPassword: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock('../src/auth/authApi', () => authApi);
|
|
13
|
-
vi.mock('react-helmet', () => ({ Helmet: ({ children }) => <>{children}</> }));
|
|
14
|
-
vi.mock('react-i18next', () => {
|
|
15
|
-
const KEYS = {
|
|
16
|
-
'Auth.PAGE_INVITE_TITLE': 'Set password',
|
|
17
|
-
'Auth.PAGE_INVITE_SUBTITLE': 'Choose a password.',
|
|
18
|
-
'Auth.PAGE_RESET_PASSWORD_TITLE': 'Reset password',
|
|
19
|
-
'Auth.PAGE_RESET_PASSWORD_SUBTITLE': 'Choose a new password.',
|
|
20
|
-
'Auth.PAGE_CHECKING_LINK_TITLE': 'Checking link',
|
|
21
|
-
'Auth.PAGE_CHECKING_LINK_TEXT': 'Checking…',
|
|
22
|
-
'Auth.RESET_LINK_INVALID': 'The link is invalid or expired.',
|
|
23
|
-
'Auth.RESET_PASSWORD_SUCCESS_INVITE': 'Password set successfully.',
|
|
24
|
-
'Auth.RESET_PASSWORD_SUCCESS_RESET': 'Password changed successfully.',
|
|
25
|
-
'Auth.SIGNUP_GO_TO_LOGIN': 'Go to login',
|
|
26
|
-
'Auth.NEW_PASSWORD_LABEL': 'New password',
|
|
27
|
-
'Auth.PASSWORD_CONFIRM_LABEL': 'Confirm password',
|
|
28
|
-
'Auth.PASSWORD_RULES_HINT': 'Password rules',
|
|
29
|
-
'Auth.PASSWORD_SET_BUTTON': 'Set password',
|
|
30
|
-
'Auth.PASSWORD_SET_BUTTON_LOADING': 'Setting password…',
|
|
31
|
-
};
|
|
32
|
-
const t = (key, fallback) => KEYS[key] ?? (typeof fallback === 'string' ? fallback : key);
|
|
33
|
-
return {
|
|
34
|
-
useTranslation: () => ({
|
|
35
|
-
t,
|
|
36
|
-
// resolveErrorText (AUTH-10) reads i18n.exists/i18n.t off the object
|
|
37
|
-
// useTranslation() returns — this mock must carry both, not just `t`.
|
|
38
|
-
i18n: { exists: (key) => Object.hasOwn(KEYS, key), t },
|
|
39
|
-
}),
|
|
40
|
-
};
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
function LocationProbe() {
|
|
44
|
-
const location = useLocation();
|
|
45
|
-
return <output data-testid="pathname">{location.pathname}</output>;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function renderPage(route = '/invite/uid/token') {
|
|
49
|
-
return render(
|
|
50
|
-
<MemoryRouter initialEntries={[route]}>
|
|
51
|
-
<LocationProbe />
|
|
52
|
-
<Routes>
|
|
53
|
-
<Route path="/invite/:uid/:token" element={<PasswordInvitePage />} />
|
|
54
|
-
<Route path="/reset/:uid/:token" element={<PasswordInvitePage />} />
|
|
55
|
-
</Routes>
|
|
56
|
-
</MemoryRouter>,
|
|
57
|
-
);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
import { PasswordInvitePage } from '../src/pages/PasswordInvitePage';
|
|
61
|
-
|
|
62
|
-
describe('PasswordInvitePage auth flow', () => {
|
|
63
|
-
beforeEach(() => {
|
|
64
|
-
vi.clearAllMocks();
|
|
65
|
-
authApi.verifyResetToken.mockResolvedValue({});
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
afterEach(() => cleanup());
|
|
69
|
-
|
|
70
|
-
it('keeps the form and shows the server password reason so the person can retry', async () => {
|
|
71
|
-
authApi.setNewPassword
|
|
72
|
-
.mockRejectedValueOnce({
|
|
73
|
-
response: {
|
|
74
|
-
data: {
|
|
75
|
-
code: 'Auth.RESET_PASSWORD_INVALID',
|
|
76
|
-
messages: ['This password is too common.'],
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
})
|
|
80
|
-
.mockResolvedValueOnce({});
|
|
81
|
-
|
|
82
|
-
renderPage();
|
|
83
|
-
await waitFor(() => expect(screen.getByLabelText('New password')).toBeTruthy());
|
|
84
|
-
|
|
85
|
-
fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'password123' } });
|
|
86
|
-
fireEvent.change(screen.getByLabelText('Confirm password'), { target: { value: 'password123' } });
|
|
87
|
-
fireEvent.click(screen.getByRole('button', { name: 'Set password' }));
|
|
88
|
-
|
|
89
|
-
await waitFor(() => expect(screen.getByText('This password is too common.')).toBeTruthy());
|
|
90
|
-
expect(screen.getByLabelText('New password')).toBeTruthy();
|
|
91
|
-
|
|
92
|
-
fireEvent.click(screen.getByRole('button', { name: 'Set password' }));
|
|
93
|
-
await waitFor(() => expect(screen.getByText('Password set successfully.')).toBeTruthy());
|
|
94
|
-
expect(authApi.setNewPassword).toHaveBeenCalledTimes(2);
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
it('shows the invalid-link state, not a retryable form, when the link expires during submit', async () => {
|
|
98
|
-
// The initial verifyResetToken check can pass and the token can still
|
|
99
|
-
// expire before setNewPassword is called (a real race, not hypothetical)
|
|
100
|
-
// -- told apart by the server's CODE, not by treating every submit
|
|
101
|
-
// error as a password rejection.
|
|
102
|
-
authApi.setNewPassword.mockRejectedValueOnce({ code: 'Auth.RESET_LINK_INVALID' });
|
|
103
|
-
|
|
104
|
-
renderPage();
|
|
105
|
-
await waitFor(() => expect(screen.getByLabelText('New password')).toBeTruthy());
|
|
106
|
-
|
|
107
|
-
fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'password123' } });
|
|
108
|
-
fireEvent.change(screen.getByLabelText('Confirm password'), { target: { value: 'password123' } });
|
|
109
|
-
fireEvent.click(screen.getByRole('button', { name: 'Set password' }));
|
|
110
|
-
|
|
111
|
-
await waitFor(() => expect(screen.getByText('The link is invalid or expired.')).toBeTruthy());
|
|
112
|
-
expect(screen.queryByLabelText('New password')).toBeNull();
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it('shows the invalid-link state without a password form when token verification fails', async () => {
|
|
116
|
-
authApi.verifyResetToken.mockRejectedValue({ code: 'Auth.RESET_LINK_INVALID' });
|
|
117
|
-
|
|
118
|
-
renderPage();
|
|
119
|
-
|
|
120
|
-
await waitFor(() => expect(screen.getByText('The link is invalid or expired.')).toBeTruthy());
|
|
121
|
-
expect(screen.queryByLabelText('New password')).toBeNull();
|
|
122
|
-
expect(screen.queryByLabelText('Confirm password')).toBeNull();
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
it('shows confirmation and waits for an explicit login action after success', async () => {
|
|
126
|
-
authApi.setNewPassword.mockResolvedValue({});
|
|
127
|
-
|
|
128
|
-
renderPage('/reset/uid/token?next=/account');
|
|
129
|
-
await waitFor(() => expect(screen.getByLabelText('New password')).toBeTruthy());
|
|
130
|
-
|
|
131
|
-
fireEvent.change(screen.getByLabelText('New password'), { target: { value: 'new-password' } });
|
|
132
|
-
fireEvent.change(screen.getByLabelText('Confirm password'), { target: { value: 'new-password' } });
|
|
133
|
-
fireEvent.click(screen.getByRole('button', { name: 'Set password' }));
|
|
134
|
-
|
|
135
|
-
await waitFor(() => expect(screen.getByText('Password changed successfully.')).toBeTruthy());
|
|
136
|
-
expect(screen.getByRole('button', { name: 'Go to login' })).toBeTruthy();
|
|
137
|
-
expect(screen.getByTestId('pathname').textContent).toBe('/reset/uid/token');
|
|
138
|
-
|
|
139
|
-
fireEvent.click(screen.getByRole('button', { name: 'Go to login' }));
|
|
140
|
-
await waitFor(() => expect(screen.getByTestId('pathname').textContent).toBe('/login'));
|
|
141
|
-
});
|
|
142
|
-
});
|